diff --git a/models/fixtures/issue.yml b/models/fixtures/issue.yml
index 85bdac397f..c2d21089ce 100644
--- a/models/fixtures/issue.yml
+++ b/models/fixtures/issue.yml
@@ -19,6 +19,7 @@
   poster_id: 1
   name: issue2
   content: content2
+  milestone_id: 1
   is_closed: false
   is_pull: true
   created_unix: 946684810
diff --git a/models/fixtures/milestone.yml b/models/fixtures/milestone.yml
new file mode 100644
index 0000000000..8192c4fbe1
--- /dev/null
+++ b/models/fixtures/milestone.yml
@@ -0,0 +1,15 @@
+-
+  id: 1
+  repo_id: 1
+  name: milestone1
+  content: content1
+  is_closed: false
+  num_issues: 1
+
+-
+  id: 2
+  repo_id: 1
+  name: milestone2
+  content: content2
+  is_closed: false
+  num_issues: 0
diff --git a/models/fixtures/repository.yml b/models/fixtures/repository.yml
index 5e45d58b28..93463c0855 100644
--- a/models/fixtures/repository.yml
+++ b/models/fixtures/repository.yml
@@ -8,6 +8,7 @@
   num_closed_issues: 1
   num_pulls: 2
   num_closed_pulls: 0
+  num_milestones: 2
   num_watches: 2
 
 -
diff --git a/models/issue.go b/models/issue.go
index 97c633c234..2d57c48269 100644
--- a/models/issue.go
+++ b/models/issue.go
@@ -1350,369 +1350,3 @@ func updateIssue(e Engine, issue *Issue) error {
 func UpdateIssue(issue *Issue) error {
 	return updateIssue(x, issue)
 }
-
-//    _____  .__.__                   __
-//   /     \ |__|  |   ____   _______/  |_  ____   ____   ____
-//  /  \ /  \|  |  | _/ __ \ /  ___/\   __\/  _ \ /    \_/ __ \
-// /    Y    \  |  |_\  ___/ \___ \  |  | (  <_> )   |  \  ___/
-// \____|__  /__|____/\___  >____  > |__|  \____/|___|  /\___  >
-//         \/             \/     \/                   \/     \/
-
-// Milestone represents a milestone of repository.
-type Milestone struct {
-	ID              int64 `xorm:"pk autoincr"`
-	RepoID          int64 `xorm:"INDEX"`
-	Name            string
-	Content         string `xorm:"TEXT"`
-	RenderedContent string `xorm:"-"`
-	IsClosed        bool
-	NumIssues       int
-	NumClosedIssues int
-	NumOpenIssues   int  `xorm:"-"`
-	Completeness    int  // Percentage(1-100).
-	IsOverDue       bool `xorm:"-"`
-
-	DeadlineString string    `xorm:"-"`
-	Deadline       time.Time `xorm:"-"`
-	DeadlineUnix   int64
-	ClosedDate     time.Time `xorm:"-"`
-	ClosedDateUnix int64
-}
-
-// BeforeInsert is invoked from XORM before inserting an object of this type.
-func (m *Milestone) BeforeInsert() {
-	m.DeadlineUnix = m.Deadline.Unix()
-}
-
-// BeforeUpdate is invoked from XORM before updating this object.
-func (m *Milestone) BeforeUpdate() {
-	if m.NumIssues > 0 {
-		m.Completeness = m.NumClosedIssues * 100 / m.NumIssues
-	} else {
-		m.Completeness = 0
-	}
-
-	m.DeadlineUnix = m.Deadline.Unix()
-	m.ClosedDateUnix = m.ClosedDate.Unix()
-}
-
-// AfterSet is invoked from XORM after setting the value of a field of
-// this object.
-func (m *Milestone) AfterSet(colName string, _ xorm.Cell) {
-	switch colName {
-	case "num_closed_issues":
-		m.NumOpenIssues = m.NumIssues - m.NumClosedIssues
-
-	case "deadline_unix":
-		m.Deadline = time.Unix(m.DeadlineUnix, 0).Local()
-		if m.Deadline.Year() == 9999 {
-			return
-		}
-
-		m.DeadlineString = m.Deadline.Format("2006-01-02")
-		if time.Now().Local().After(m.Deadline) {
-			m.IsOverDue = true
-		}
-
-	case "closed_date_unix":
-		m.ClosedDate = time.Unix(m.ClosedDateUnix, 0).Local()
-	}
-}
-
-// State returns string representation of milestone status.
-func (m *Milestone) State() api.StateType {
-	if m.IsClosed {
-		return api.StateClosed
-	}
-	return api.StateOpen
-}
-
-// APIFormat returns this Milestone in API format.
-func (m *Milestone) APIFormat() *api.Milestone {
-	apiMilestone := &api.Milestone{
-		ID:           m.ID,
-		State:        m.State(),
-		Title:        m.Name,
-		Description:  m.Content,
-		OpenIssues:   m.NumOpenIssues,
-		ClosedIssues: m.NumClosedIssues,
-	}
-	if m.IsClosed {
-		apiMilestone.Closed = &m.ClosedDate
-	}
-	if m.Deadline.Year() < 9999 {
-		apiMilestone.Deadline = &m.Deadline
-	}
-	return apiMilestone
-}
-
-// NewMilestone creates new milestone of repository.
-func NewMilestone(m *Milestone) (err error) {
-	sess := x.NewSession()
-	defer sessionRelease(sess)
-	if err = sess.Begin(); err != nil {
-		return err
-	}
-
-	if _, err = sess.Insert(m); err != nil {
-		return err
-	}
-
-	if _, err = sess.Exec("UPDATE `repository` SET num_milestones = num_milestones + 1 WHERE id = ?", m.RepoID); err != nil {
-		return err
-	}
-	return sess.Commit()
-}
-
-func getMilestoneByRepoID(e Engine, repoID, id int64) (*Milestone, error) {
-	m := &Milestone{
-		ID:     id,
-		RepoID: repoID,
-	}
-	has, err := e.Get(m)
-	if err != nil {
-		return nil, err
-	} else if !has {
-		return nil, ErrMilestoneNotExist{id, repoID}
-	}
-	return m, nil
-}
-
-// GetMilestoneByRepoID returns the milestone in a repository.
-func GetMilestoneByRepoID(repoID, id int64) (*Milestone, error) {
-	return getMilestoneByRepoID(x, repoID, id)
-}
-
-// GetMilestonesByRepoID returns all milestones of a repository.
-func GetMilestonesByRepoID(repoID int64) ([]*Milestone, error) {
-	miles := make([]*Milestone, 0, 10)
-	return miles, x.Where("repo_id = ?", repoID).Find(&miles)
-}
-
-// GetMilestones returns a list of milestones of given repository and status.
-func GetMilestones(repoID int64, page int, isClosed bool, sortType string) ([]*Milestone, error) {
-	miles := make([]*Milestone, 0, setting.UI.IssuePagingNum)
-	sess := x.Where("repo_id = ? AND is_closed = ?", repoID, isClosed)
-	if page > 0 {
-		sess = sess.Limit(setting.UI.IssuePagingNum, (page-1)*setting.UI.IssuePagingNum)
-	}
-
-	switch sortType {
-	case "furthestduedate":
-		sess.Desc("deadline_unix")
-	case "leastcomplete":
-		sess.Asc("completeness")
-	case "mostcomplete":
-		sess.Desc("completeness")
-	case "leastissues":
-		sess.Asc("num_issues")
-	case "mostissues":
-		sess.Desc("num_issues")
-	default:
-		sess.Asc("deadline_unix")
-	}
-
-	return miles, sess.Find(&miles)
-}
-
-func updateMilestone(e Engine, m *Milestone) error {
-	_, err := e.Id(m.ID).AllCols().Update(m)
-	return err
-}
-
-// UpdateMilestone updates information of given milestone.
-func UpdateMilestone(m *Milestone) error {
-	return updateMilestone(x, m)
-}
-
-func countRepoMilestones(e Engine, repoID int64) int64 {
-	count, _ := e.
-		Where("repo_id=?", repoID).
-		Count(new(Milestone))
-	return count
-}
-
-// CountRepoMilestones returns number of milestones in given repository.
-func CountRepoMilestones(repoID int64) int64 {
-	return countRepoMilestones(x, repoID)
-}
-
-func countRepoClosedMilestones(e Engine, repoID int64) int64 {
-	closed, _ := e.
-		Where("repo_id=? AND is_closed=?", repoID, true).
-		Count(new(Milestone))
-	return closed
-}
-
-// CountRepoClosedMilestones returns number of closed milestones in given repository.
-func CountRepoClosedMilestones(repoID int64) int64 {
-	return countRepoClosedMilestones(x, repoID)
-}
-
-// MilestoneStats returns number of open and closed milestones of given repository.
-func MilestoneStats(repoID int64) (open int64, closed int64) {
-	open, _ = x.
-		Where("repo_id=? AND is_closed=?", repoID, false).
-		Count(new(Milestone))
-	return open, CountRepoClosedMilestones(repoID)
-}
-
-// ChangeMilestoneStatus changes the milestone open/closed status.
-func ChangeMilestoneStatus(m *Milestone, isClosed bool) (err error) {
-	repo, err := GetRepositoryByID(m.RepoID)
-	if err != nil {
-		return err
-	}
-
-	sess := x.NewSession()
-	defer sessionRelease(sess)
-	if err = sess.Begin(); err != nil {
-		return err
-	}
-
-	m.IsClosed = isClosed
-	if err = updateMilestone(sess, m); err != nil {
-		return err
-	}
-
-	repo.NumMilestones = int(countRepoMilestones(sess, repo.ID))
-	repo.NumClosedMilestones = int(countRepoClosedMilestones(sess, repo.ID))
-	if _, err = sess.Id(repo.ID).AllCols().Update(repo); err != nil {
-		return err
-	}
-	return sess.Commit()
-}
-
-func changeMilestoneIssueStats(e *xorm.Session, issue *Issue) error {
-	if issue.MilestoneID == 0 {
-		return nil
-	}
-
-	m, err := getMilestoneByRepoID(e, issue.RepoID, issue.MilestoneID)
-	if err != nil {
-		return err
-	}
-
-	if issue.IsClosed {
-		m.NumOpenIssues--
-		m.NumClosedIssues++
-	} else {
-		m.NumOpenIssues++
-		m.NumClosedIssues--
-	}
-
-	return updateMilestone(e, m)
-}
-
-// ChangeMilestoneIssueStats updates the open/closed issues counter and progress
-// for the milestone associated with the given issue.
-func ChangeMilestoneIssueStats(issue *Issue) (err error) {
-	sess := x.NewSession()
-	defer sessionRelease(sess)
-	if err = sess.Begin(); err != nil {
-		return err
-	}
-
-	if err = changeMilestoneIssueStats(sess, issue); err != nil {
-		return err
-	}
-
-	return sess.Commit()
-}
-
-func changeMilestoneAssign(e *xorm.Session, doer *User, issue *Issue, oldMilestoneID int64) error {
-	if oldMilestoneID > 0 {
-		m, err := getMilestoneByRepoID(e, issue.RepoID, oldMilestoneID)
-		if err != nil {
-			return err
-		}
-
-		m.NumIssues--
-		if issue.IsClosed {
-			m.NumClosedIssues--
-		}
-
-		if err = updateMilestone(e, m); err != nil {
-			return err
-		}
-	}
-
-	if issue.MilestoneID > 0 {
-		m, err := getMilestoneByRepoID(e, issue.RepoID, issue.MilestoneID)
-		if err != nil {
-			return err
-		}
-
-		m.NumIssues++
-		if issue.IsClosed {
-			m.NumClosedIssues++
-		}
-
-		if err = updateMilestone(e, m); err != nil {
-			return err
-		}
-	}
-
-	if err := issue.loadRepo(e); err != nil {
-		return err
-	}
-
-	if oldMilestoneID > 0 || issue.MilestoneID > 0 {
-		if _, err := createMilestoneComment(e, doer, issue.Repo, issue, oldMilestoneID, issue.MilestoneID); err != nil {
-			return err
-		}
-	}
-
-	return updateIssue(e, issue)
-}
-
-// ChangeMilestoneAssign changes assignment of milestone for issue.
-func ChangeMilestoneAssign(issue *Issue, doer *User, oldMilestoneID int64) (err error) {
-	sess := x.NewSession()
-	defer sess.Close()
-	if err = sess.Begin(); err != nil {
-		return err
-	}
-
-	if err = changeMilestoneAssign(sess, doer, issue, oldMilestoneID); err != nil {
-		return err
-	}
-	return sess.Commit()
-}
-
-// DeleteMilestoneByRepoID deletes a milestone from a repository.
-func DeleteMilestoneByRepoID(repoID, id int64) error {
-	m, err := GetMilestoneByRepoID(repoID, id)
-	if err != nil {
-		if IsErrMilestoneNotExist(err) {
-			return nil
-		}
-		return err
-	}
-
-	repo, err := GetRepositoryByID(m.RepoID)
-	if err != nil {
-		return err
-	}
-
-	sess := x.NewSession()
-	defer sessionRelease(sess)
-	if err = sess.Begin(); err != nil {
-		return err
-	}
-
-	if _, err = sess.Id(m.ID).Delete(new(Milestone)); err != nil {
-		return err
-	}
-
-	repo.NumMilestones = int(countRepoMilestones(sess, repo.ID))
-	repo.NumClosedMilestones = int(countRepoClosedMilestones(sess, repo.ID))
-	if _, err = sess.Id(repo.ID).AllCols().Update(repo); err != nil {
-		return err
-	}
-
-	if _, err = sess.Exec("UPDATE `issue` SET milestone_id = 0 WHERE milestone_id = ?", m.ID); err != nil {
-		return err
-	}
-	return sess.Commit()
-}
diff --git a/models/issue_milestone.go b/models/issue_milestone.go
new file mode 100644
index 0000000000..cfd2ce1707
--- /dev/null
+++ b/models/issue_milestone.go
@@ -0,0 +1,352 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package models
+
+import (
+	"time"
+
+	"code.gitea.io/gitea/modules/setting"
+	api "code.gitea.io/sdk/gitea"
+
+	"github.com/go-xorm/xorm"
+)
+
+// Milestone represents a milestone of repository.
+type Milestone struct {
+	ID              int64 `xorm:"pk autoincr"`
+	RepoID          int64 `xorm:"INDEX"`
+	Name            string
+	Content         string `xorm:"TEXT"`
+	RenderedContent string `xorm:"-"`
+	IsClosed        bool
+	NumIssues       int
+	NumClosedIssues int
+	NumOpenIssues   int  `xorm:"-"`
+	Completeness    int  // Percentage(1-100).
+	IsOverDue       bool `xorm:"-"`
+
+	DeadlineString string    `xorm:"-"`
+	Deadline       time.Time `xorm:"-"`
+	DeadlineUnix   int64
+	ClosedDate     time.Time `xorm:"-"`
+	ClosedDateUnix int64
+}
+
+// BeforeInsert is invoked from XORM before inserting an object of this type.
+func (m *Milestone) BeforeInsert() {
+	m.DeadlineUnix = m.Deadline.Unix()
+}
+
+// BeforeUpdate is invoked from XORM before updating this object.
+func (m *Milestone) BeforeUpdate() {
+	if m.NumIssues > 0 {
+		m.Completeness = m.NumClosedIssues * 100 / m.NumIssues
+	} else {
+		m.Completeness = 0
+	}
+
+	m.DeadlineUnix = m.Deadline.Unix()
+	m.ClosedDateUnix = m.ClosedDate.Unix()
+}
+
+// AfterSet is invoked from XORM after setting the value of a field of
+// this object.
+func (m *Milestone) AfterSet(colName string, _ xorm.Cell) {
+	switch colName {
+	case "num_closed_issues":
+		m.NumOpenIssues = m.NumIssues - m.NumClosedIssues
+
+	case "deadline_unix":
+		m.Deadline = time.Unix(m.DeadlineUnix, 0).Local()
+		if m.Deadline.Year() == 9999 {
+			return
+		}
+
+		m.DeadlineString = m.Deadline.Format("2006-01-02")
+		if time.Now().Local().After(m.Deadline) {
+			m.IsOverDue = true
+		}
+
+	case "closed_date_unix":
+		m.ClosedDate = time.Unix(m.ClosedDateUnix, 0).Local()
+	}
+}
+
+// State returns string representation of milestone status.
+func (m *Milestone) State() api.StateType {
+	if m.IsClosed {
+		return api.StateClosed
+	}
+	return api.StateOpen
+}
+
+// APIFormat returns this Milestone in API format.
+func (m *Milestone) APIFormat() *api.Milestone {
+	apiMilestone := &api.Milestone{
+		ID:           m.ID,
+		State:        m.State(),
+		Title:        m.Name,
+		Description:  m.Content,
+		OpenIssues:   m.NumOpenIssues,
+		ClosedIssues: m.NumClosedIssues,
+	}
+	if m.IsClosed {
+		apiMilestone.Closed = &m.ClosedDate
+	}
+	if m.Deadline.Year() < 9999 {
+		apiMilestone.Deadline = &m.Deadline
+	}
+	return apiMilestone
+}
+
+// NewMilestone creates new milestone of repository.
+func NewMilestone(m *Milestone) (err error) {
+	sess := x.NewSession()
+	defer sessionRelease(sess)
+	if err = sess.Begin(); err != nil {
+		return err
+	}
+
+	if _, err = sess.Insert(m); err != nil {
+		return err
+	}
+
+	if _, err = sess.Exec("UPDATE `repository` SET num_milestones = num_milestones + 1 WHERE id = ?", m.RepoID); err != nil {
+		return err
+	}
+	return sess.Commit()
+}
+
+func getMilestoneByRepoID(e Engine, repoID, id int64) (*Milestone, error) {
+	m := &Milestone{
+		ID:     id,
+		RepoID: repoID,
+	}
+	has, err := e.Get(m)
+	if err != nil {
+		return nil, err
+	} else if !has {
+		return nil, ErrMilestoneNotExist{id, repoID}
+	}
+	return m, nil
+}
+
+// GetMilestoneByRepoID returns the milestone in a repository.
+func GetMilestoneByRepoID(repoID, id int64) (*Milestone, error) {
+	return getMilestoneByRepoID(x, repoID, id)
+}
+
+// GetMilestonesByRepoID returns all milestones of a repository.
+func GetMilestonesByRepoID(repoID int64) ([]*Milestone, error) {
+	miles := make([]*Milestone, 0, 10)
+	return miles, x.Where("repo_id = ?", repoID).Find(&miles)
+}
+
+// GetMilestones returns a list of milestones of given repository and status.
+func GetMilestones(repoID int64, page int, isClosed bool, sortType string) ([]*Milestone, error) {
+	miles := make([]*Milestone, 0, setting.UI.IssuePagingNum)
+	sess := x.Where("repo_id = ? AND is_closed = ?", repoID, isClosed)
+	if page > 0 {
+		sess = sess.Limit(setting.UI.IssuePagingNum, (page-1)*setting.UI.IssuePagingNum)
+	}
+
+	switch sortType {
+	case "furthestduedate":
+		sess.Desc("deadline_unix")
+	case "leastcomplete":
+		sess.Asc("completeness")
+	case "mostcomplete":
+		sess.Desc("completeness")
+	case "leastissues":
+		sess.Asc("num_issues")
+	case "mostissues":
+		sess.Desc("num_issues")
+	default:
+		sess.Asc("deadline_unix")
+	}
+
+	return miles, sess.Find(&miles)
+}
+
+func updateMilestone(e Engine, m *Milestone) error {
+	_, err := e.Id(m.ID).AllCols().Update(m)
+	return err
+}
+
+// UpdateMilestone updates information of given milestone.
+func UpdateMilestone(m *Milestone) error {
+	return updateMilestone(x, m)
+}
+
+func countRepoMilestones(e Engine, repoID int64) int64 {
+	count, _ := e.
+		Where("repo_id=?", repoID).
+		Count(new(Milestone))
+	return count
+}
+
+func countRepoClosedMilestones(e Engine, repoID int64) int64 {
+	closed, _ := e.
+		Where("repo_id=? AND is_closed=?", repoID, true).
+		Count(new(Milestone))
+	return closed
+}
+
+// CountRepoClosedMilestones returns number of closed milestones in given repository.
+func CountRepoClosedMilestones(repoID int64) int64 {
+	return countRepoClosedMilestones(x, repoID)
+}
+
+// MilestoneStats returns number of open and closed milestones of given repository.
+func MilestoneStats(repoID int64) (open int64, closed int64) {
+	open, _ = x.
+		Where("repo_id=? AND is_closed=?", repoID, false).
+		Count(new(Milestone))
+	return open, CountRepoClosedMilestones(repoID)
+}
+
+// ChangeMilestoneStatus changes the milestone open/closed status.
+func ChangeMilestoneStatus(m *Milestone, isClosed bool) (err error) {
+	repo, err := GetRepositoryByID(m.RepoID)
+	if err != nil {
+		return err
+	}
+
+	sess := x.NewSession()
+	defer sessionRelease(sess)
+	if err = sess.Begin(); err != nil {
+		return err
+	}
+
+	m.IsClosed = isClosed
+	if err = updateMilestone(sess, m); err != nil {
+		return err
+	}
+
+	repo.NumMilestones = int(countRepoMilestones(sess, repo.ID))
+	repo.NumClosedMilestones = int(countRepoClosedMilestones(sess, repo.ID))
+	if _, err = sess.Id(repo.ID).AllCols().Update(repo); err != nil {
+		return err
+	}
+	return sess.Commit()
+}
+
+func changeMilestoneIssueStats(e *xorm.Session, issue *Issue) error {
+	if issue.MilestoneID == 0 {
+		return nil
+	}
+
+	m, err := getMilestoneByRepoID(e, issue.RepoID, issue.MilestoneID)
+	if err != nil {
+		return err
+	}
+
+	if issue.IsClosed {
+		m.NumOpenIssues--
+		m.NumClosedIssues++
+	} else {
+		m.NumOpenIssues++
+		m.NumClosedIssues--
+	}
+
+	return updateMilestone(e, m)
+}
+
+func changeMilestoneAssign(e *xorm.Session, doer *User, issue *Issue, oldMilestoneID int64) error {
+	if oldMilestoneID > 0 {
+		m, err := getMilestoneByRepoID(e, issue.RepoID, oldMilestoneID)
+		if err != nil {
+			return err
+		}
+
+		m.NumIssues--
+		if issue.IsClosed {
+			m.NumClosedIssues--
+		}
+
+		if err = updateMilestone(e, m); err != nil {
+			return err
+		}
+	}
+
+	if issue.MilestoneID > 0 {
+		m, err := getMilestoneByRepoID(e, issue.RepoID, issue.MilestoneID)
+		if err != nil {
+			return err
+		}
+
+		m.NumIssues++
+		if issue.IsClosed {
+			m.NumClosedIssues++
+		}
+
+		if err = updateMilestone(e, m); err != nil {
+			return err
+		}
+	}
+
+	if err := issue.loadRepo(e); err != nil {
+		return err
+	}
+
+	if oldMilestoneID > 0 || issue.MilestoneID > 0 {
+		if _, err := createMilestoneComment(e, doer, issue.Repo, issue, oldMilestoneID, issue.MilestoneID); err != nil {
+			return err
+		}
+	}
+
+	return updateIssue(e, issue)
+}
+
+// ChangeMilestoneAssign changes assignment of milestone for issue.
+func ChangeMilestoneAssign(issue *Issue, doer *User, oldMilestoneID int64) (err error) {
+	sess := x.NewSession()
+	defer sess.Close()
+	if err = sess.Begin(); err != nil {
+		return err
+	}
+
+	if err = changeMilestoneAssign(sess, doer, issue, oldMilestoneID); err != nil {
+		return err
+	}
+	return sess.Commit()
+}
+
+// DeleteMilestoneByRepoID deletes a milestone from a repository.
+func DeleteMilestoneByRepoID(repoID, id int64) error {
+	m, err := GetMilestoneByRepoID(repoID, id)
+	if err != nil {
+		if IsErrMilestoneNotExist(err) {
+			return nil
+		}
+		return err
+	}
+
+	repo, err := GetRepositoryByID(m.RepoID)
+	if err != nil {
+		return err
+	}
+
+	sess := x.NewSession()
+	defer sessionRelease(sess)
+	if err = sess.Begin(); err != nil {
+		return err
+	}
+
+	if _, err = sess.Id(m.ID).Delete(new(Milestone)); err != nil {
+		return err
+	}
+
+	repo.NumMilestones = int(countRepoMilestones(sess, repo.ID))
+	repo.NumClosedMilestones = int(countRepoClosedMilestones(sess, repo.ID))
+	if _, err = sess.Id(repo.ID).AllCols().Update(repo); err != nil {
+		return err
+	}
+
+	if _, err = sess.Exec("UPDATE `issue` SET milestone_id = 0 WHERE milestone_id = ?", m.ID); err != nil {
+		return err
+	}
+	return sess.Commit()
+}
diff --git a/models/issue_milestone_test.go b/models/issue_milestone_test.go
new file mode 100644
index 0000000000..aa4038dc6e
--- /dev/null
+++ b/models/issue_milestone_test.go
@@ -0,0 +1,240 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package models
+
+import (
+	"testing"
+
+	api "code.gitea.io/sdk/gitea"
+
+	"github.com/stretchr/testify/assert"
+	"sort"
+	"time"
+)
+
+func TestMilestone_State(t *testing.T) {
+	assert.Equal(t, api.StateOpen, (&Milestone{IsClosed: false}).State())
+	assert.Equal(t, api.StateClosed, (&Milestone{IsClosed: true}).State())
+}
+
+func TestMilestone_APIFormat(t *testing.T) {
+	milestone := &Milestone{
+		ID:              3,
+		RepoID:          4,
+		Name:            "milestoneName",
+		Content:         "milestoneContent",
+		IsClosed:        false,
+		NumOpenIssues:   5,
+		NumClosedIssues: 6,
+		Deadline:        time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC),
+	}
+	assert.Equal(t, api.Milestone{
+		ID:           milestone.ID,
+		State:        api.StateOpen,
+		Title:        milestone.Name,
+		Description:  milestone.Content,
+		OpenIssues:   milestone.NumOpenIssues,
+		ClosedIssues: milestone.NumClosedIssues,
+		Deadline:     &milestone.Deadline,
+	}, *milestone.APIFormat())
+}
+
+func TestNewMilestone(t *testing.T) {
+	assert.NoError(t, PrepareTestDatabase())
+	milestone := &Milestone{
+		RepoID:  1,
+		Name:    "milestoneName",
+		Content: "milestoneContent",
+	}
+
+	assert.NoError(t, NewMilestone(milestone))
+	AssertExistsAndLoadBean(t, milestone)
+	CheckConsistencyFor(t, &Repository{ID: milestone.RepoID}, &Milestone{})
+}
+
+func TestGetMilestoneByRepoID(t *testing.T) {
+	assert.NoError(t, PrepareTestDatabase())
+
+	milestone, err := GetMilestoneByRepoID(1, 1)
+	assert.NoError(t, err)
+	assert.EqualValues(t, 1, milestone.ID)
+	assert.EqualValues(t, 1, milestone.RepoID)
+
+	_, err = GetMilestoneByRepoID(NonexistentID, NonexistentID)
+	assert.True(t, IsErrMilestoneNotExist(err))
+}
+
+func TestGetMilestonesByRepoID(t *testing.T) {
+	assert.NoError(t, PrepareTestDatabase())
+	test := func(repoID int64) {
+		repo := AssertExistsAndLoadBean(t, &Repository{ID: repoID}).(*Repository)
+		milestones, err := GetMilestonesByRepoID(repo.ID)
+		assert.NoError(t, err)
+		assert.Len(t, milestones, repo.NumMilestones)
+		for _, milestone := range milestones {
+			assert.EqualValues(t, repoID, milestone.RepoID)
+		}
+	}
+	test(1)
+	test(2)
+	test(3)
+
+	milestones, err := GetMilestonesByRepoID(NonexistentID)
+	assert.NoError(t, err)
+	assert.Len(t, milestones, 0)
+}
+
+func TestGetMilestones(t *testing.T) {
+	assert.NoError(t, PrepareTestDatabase())
+	repo := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository)
+	test := func(sortType string, sortCond func(*Milestone) int) {
+		for _, page := range []int{0, 1} {
+			milestones, err := GetMilestones(repo.ID, page, false, sortType)
+			assert.NoError(t, err)
+			assert.Len(t, milestones, repo.NumMilestones-repo.NumClosedMilestones)
+			values := make([]int, len(milestones))
+			for i, milestone := range milestones {
+				values[i] = sortCond(milestone)
+			}
+			assert.True(t, sort.IntsAreSorted(values))
+
+			milestones, err = GetMilestones(repo.ID, page, true, sortType)
+			assert.NoError(t, err)
+			assert.Len(t, milestones, repo.NumClosedMilestones)
+			values = make([]int, len(milestones))
+			for i, milestone := range milestones {
+				values[i] = sortCond(milestone)
+			}
+			assert.True(t, sort.IntsAreSorted(values))
+		}
+	}
+	test("furthestduedate", func(milestone *Milestone) int {
+		return -int(milestone.DeadlineUnix)
+	})
+	test("leastcomplete", func(milestone *Milestone) int {
+		return milestone.Completeness
+	})
+	test("mostcomplete", func(milestone *Milestone) int {
+		return -milestone.Completeness
+	})
+	test("leastissues", func(milestone *Milestone) int {
+		return milestone.NumIssues
+	})
+	test("mostissues", func(milestone *Milestone) int {
+		return -milestone.NumIssues
+	})
+	test("soonestduedate", func(milestone *Milestone) int {
+		return int(milestone.DeadlineUnix)
+	})
+}
+
+func TestUpdateMilestone(t *testing.T) {
+	assert.NoError(t, PrepareTestDatabase())
+
+	milestone := AssertExistsAndLoadBean(t, &Milestone{ID: 1}).(*Milestone)
+	milestone.Name = "newMilestoneName"
+	milestone.Content = "newMilestoneContent"
+	assert.NoError(t, UpdateMilestone(milestone))
+	AssertExistsAndLoadBean(t, milestone)
+	CheckConsistencyFor(t, &Milestone{})
+}
+
+func TestCountRepoMilestones(t *testing.T) {
+	assert.NoError(t, PrepareTestDatabase())
+	test := func(repoID int64) {
+		repo := AssertExistsAndLoadBean(t, &Repository{ID: repoID}).(*Repository)
+		assert.EqualValues(t, repo.NumMilestones, countRepoMilestones(x, repoID))
+	}
+	test(1)
+	test(2)
+	test(3)
+	assert.EqualValues(t, 0, countRepoMilestones(x, NonexistentID))
+}
+
+func TestCountRepoClosedMilestones(t *testing.T) {
+	assert.NoError(t, PrepareTestDatabase())
+	test := func(repoID int64) {
+		repo := AssertExistsAndLoadBean(t, &Repository{ID: repoID}).(*Repository)
+		assert.EqualValues(t, repo.NumClosedMilestones, CountRepoClosedMilestones(repoID))
+	}
+	test(1)
+	test(2)
+	test(3)
+	assert.EqualValues(t, 0, countRepoMilestones(x, NonexistentID))
+}
+
+func TestMilestoneStats(t *testing.T) {
+	assert.NoError(t, PrepareTestDatabase())
+	test := func(repoID int64) {
+		repo := AssertExistsAndLoadBean(t, &Repository{ID: repoID}).(*Repository)
+		open, closed := MilestoneStats(repoID)
+		assert.EqualValues(t, repo.NumMilestones-repo.NumClosedMilestones, open)
+		assert.EqualValues(t, repo.NumClosedMilestones, closed)
+	}
+	test(1)
+	test(2)
+	test(3)
+
+	open, closed := MilestoneStats(NonexistentID)
+	assert.EqualValues(t, 0, open)
+	assert.EqualValues(t, 0, closed)
+}
+
+func TestChangeMilestoneStatus(t *testing.T) {
+	assert.NoError(t, PrepareTestDatabase())
+	milestone := AssertExistsAndLoadBean(t, &Milestone{ID: 1}).(*Milestone)
+
+	assert.NoError(t, ChangeMilestoneStatus(milestone, true))
+	AssertExistsAndLoadBean(t, &Milestone{ID: 1}, "is_closed=1")
+	CheckConsistencyFor(t, &Repository{ID: milestone.RepoID}, &Milestone{})
+
+	assert.NoError(t, ChangeMilestoneStatus(milestone, false))
+	AssertExistsAndLoadBean(t, &Milestone{ID: 1}, "is_closed=0")
+	CheckConsistencyFor(t, &Repository{ID: milestone.RepoID}, &Milestone{})
+}
+
+func TestChangeMilestoneIssueStats(t *testing.T) {
+	assert.NoError(t, PrepareTestDatabase())
+	issue := AssertExistsAndLoadBean(t, &Issue{MilestoneID: 1},
+		"is_closed=0").(*Issue)
+
+	issue.IsClosed = true
+	_, err := x.Cols("is_closed").Update(issue)
+	assert.NoError(t, err)
+	assert.NoError(t, changeMilestoneIssueStats(x.NewSession(), issue))
+	CheckConsistencyFor(t, &Milestone{})
+
+	issue.IsClosed = false
+	_, err = x.Cols("is_closed").Update(issue)
+	assert.NoError(t, err)
+	assert.NoError(t, changeMilestoneIssueStats(x.NewSession(), issue))
+	CheckConsistencyFor(t, &Milestone{})
+}
+
+func TestChangeMilestoneAssign(t *testing.T) {
+	assert.NoError(t, PrepareTestDatabase())
+	issue := AssertExistsAndLoadBean(t, &Issue{RepoID: 1}).(*Issue)
+	doer := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
+
+	oldMilestoneID := issue.MilestoneID
+	issue.MilestoneID = 2
+	assert.NoError(t, ChangeMilestoneAssign(issue, doer, oldMilestoneID))
+	AssertExistsAndLoadBean(t, &Comment{
+		IssueID:        issue.ID,
+		Type:           CommentTypeMilestone,
+		MilestoneID:    issue.MilestoneID,
+		OldMilestoneID: oldMilestoneID,
+	})
+	CheckConsistencyFor(t, &Milestone{}, &Issue{})
+}
+
+func TestDeleteMilestoneByRepoID(t *testing.T) {
+	assert.NoError(t, PrepareTestDatabase())
+	assert.NoError(t, DeleteMilestoneByRepoID(1, 1))
+	AssertNotExistsBean(t, &Milestone{ID: 1})
+	CheckConsistencyFor(t, &Repository{ID: 1})
+
+	assert.NoError(t, DeleteMilestoneByRepoID(NonexistentID, NonexistentID))
+}