diff --git a/integrations/api_issue_milestone_test.go b/integrations/api_issue_milestone_test.go
index f1f306b768..4cc574b9eb 100644
--- a/integrations/api_issue_milestone_test.go
+++ b/integrations/api_issue_milestone_test.go
@@ -55,6 +55,18 @@ func TestAPIIssuesMilestone(t *testing.T) {
 	assert.Equal(t, "wow", apiMilestone.Title)
 	assert.Equal(t, structs.StateClosed, apiMilestone.State)
 
+	var apiMilestones []structs.Milestone
+	req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/milestones?state=%s&token=%s", owner.Name, repo.Name, "all", token))
+	resp = session.MakeRequest(t, req, http.StatusOK)
+	DecodeJSON(t, resp, &apiMilestones)
+	assert.Len(t, apiMilestones, 4)
+
+	req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/milestones?state=%s&name=%s&token=%s", owner.Name, repo.Name, "all", "milestone2", token))
+	resp = session.MakeRequest(t, req, http.StatusOK)
+	DecodeJSON(t, resp, &apiMilestones)
+	assert.Len(t, apiMilestones, 1)
+	assert.Equal(t, int64(2), apiMilestones[0].ID)
+
 	req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/%s/milestones/%d?token=%s", owner.Name, repo.Name, apiMilestone.ID, token))
 	resp = session.MakeRequest(t, req, http.StatusNoContent)
 }
diff --git a/models/issue_milestone.go b/models/issue_milestone.go
index 4648274459..824b939a56 100644
--- a/models/issue_milestone.go
+++ b/models/issue_milestone.go
@@ -330,41 +330,38 @@ func (milestones MilestoneList) getMilestoneIDs() []int64 {
 	return ids
 }
 
-// GetMilestonesByRepoID returns all opened milestones of a repository.
-func GetMilestonesByRepoID(repoID int64, state api.StateType, listOptions ListOptions) (MilestoneList, error) {
-	sess := x.Where("repo_id = ?", repoID)
+// GetMilestonesOption contain options to get milestones
+type GetMilestonesOption struct {
+	ListOptions
+	RepoID   int64
+	State    api.StateType
+	Name     string
+	SortType string
+}
 
-	switch state {
+// GetMilestones returns milestones filtered by GetMilestonesOption's
+func GetMilestones(opts GetMilestonesOption) (MilestoneList, error) {
+	sess := x.Where("repo_id = ?", opts.RepoID)
+
+	switch opts.State {
 	case api.StateClosed:
 		sess = sess.And("is_closed = ?", true)
-
 	case api.StateAll:
 		break
-
-	case api.StateOpen:
-		fallthrough
-
+	// api.StateOpen:
 	default:
 		sess = sess.And("is_closed = ?", false)
 	}
 
-	if listOptions.Page != 0 {
-		sess = listOptions.setSessionPagination(sess)
+	if len(opts.Name) != 0 {
+		sess = sess.And(builder.Like{"name", opts.Name})
 	}
 
-	miles := make([]*Milestone, 0, listOptions.PageSize)
-	return miles, sess.Asc("deadline_unix").Asc("id").Find(&miles)
-}
-
-// GetMilestones returns a list of milestones of given repository and status.
-func GetMilestones(repoID int64, page int, isClosed bool, sortType string) (MilestoneList, 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)
+	if opts.Page != 0 {
+		sess = opts.setSessionPagination(sess)
 	}
 
-	switch sortType {
+	switch opts.SortType {
 	case "furthestduedate":
 		sess.Desc("deadline_unix")
 	case "leastcomplete":
@@ -375,9 +372,13 @@ func GetMilestones(repoID int64, page int, isClosed bool, sortType string) (Mile
 		sess.Asc("num_issues")
 	case "mostissues":
 		sess.Desc("num_issues")
+	case "id":
+		sess.Asc("id")
 	default:
-		sess.Asc("deadline_unix")
+		sess.Asc("deadline_unix").Asc("id")
 	}
+
+	miles := make([]*Milestone, 0, opts.PageSize)
 	return miles, sess.Find(&miles)
 }
 
diff --git a/models/issue_milestone_test.go b/models/issue_milestone_test.go
index 07dd8d57c5..af264aa274 100644
--- a/models/issue_milestone_test.go
+++ b/models/issue_milestone_test.go
@@ -8,6 +8,7 @@ import (
 	"sort"
 	"testing"
 
+	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/timeutil"
 
@@ -49,7 +50,10 @@ func TestGetMilestonesByRepoID(t *testing.T) {
 	assert.NoError(t, PrepareTestDatabase())
 	test := func(repoID int64, state api.StateType) {
 		repo := AssertExistsAndLoadBean(t, &Repository{ID: repoID}).(*Repository)
-		milestones, err := GetMilestonesByRepoID(repo.ID, state, ListOptions{})
+		milestones, err := GetMilestones(GetMilestonesOption{
+			RepoID: repo.ID,
+			State:  state,
+		})
 		assert.NoError(t, err)
 
 		var n int
@@ -83,7 +87,10 @@ func TestGetMilestonesByRepoID(t *testing.T) {
 	test(3, api.StateClosed)
 	test(3, api.StateAll)
 
-	milestones, err := GetMilestonesByRepoID(NonexistentID, api.StateOpen, ListOptions{})
+	milestones, err := GetMilestones(GetMilestonesOption{
+		RepoID: NonexistentID,
+		State:  api.StateOpen,
+	})
 	assert.NoError(t, err)
 	assert.Len(t, milestones, 0)
 }
@@ -93,7 +100,15 @@ func TestGetMilestones(t *testing.T) {
 	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)
+			milestones, err := GetMilestones(GetMilestonesOption{
+				ListOptions: ListOptions{
+					Page:     page,
+					PageSize: setting.UI.IssuePagingNum,
+				},
+				RepoID:   repo.ID,
+				State:    api.StateOpen,
+				SortType: sortType,
+			})
 			assert.NoError(t, err)
 			assert.Len(t, milestones, repo.NumMilestones-repo.NumClosedMilestones)
 			values := make([]int, len(milestones))
@@ -102,7 +117,16 @@ func TestGetMilestones(t *testing.T) {
 			}
 			assert.True(t, sort.IntsAreSorted(values))
 
-			milestones, err = GetMilestones(repo.ID, page, true, sortType)
+			milestones, err = GetMilestones(GetMilestonesOption{
+				ListOptions: ListOptions{
+					Page:     page,
+					PageSize: setting.UI.IssuePagingNum,
+				},
+				RepoID:   repo.ID,
+				State:    api.StateClosed,
+				Name:     "",
+				SortType: sortType,
+			})
 			assert.NoError(t, err)
 			assert.Len(t, milestones, repo.NumClosedMilestones)
 			values = make([]int, len(milestones))
diff --git a/modules/migrations/gitea_test.go b/modules/migrations/gitea_test.go
index 3f03cbab31..c0d2dcd180 100644
--- a/modules/migrations/gitea_test.go
+++ b/modules/migrations/gitea_test.go
@@ -51,11 +51,17 @@ func TestGiteaUploadRepo(t *testing.T) {
 	repo := models.AssertExistsAndLoadBean(t, &models.Repository{OwnerID: user.ID, Name: repoName}).(*models.Repository)
 	assert.True(t, repo.HasWiki())
 
-	milestones, err := models.GetMilestones(repo.ID, 0, false, "")
+	milestones, err := models.GetMilestones(models.GetMilestonesOption{
+		RepoID: repo.ID,
+		State:  structs.StateOpen,
+	})
 	assert.NoError(t, err)
 	assert.EqualValues(t, 1, len(milestones))
 
-	milestones, err = models.GetMilestones(repo.ID, 0, true, "")
+	milestones, err = models.GetMilestones(models.GetMilestonesOption{
+		RepoID: repo.ID,
+		State:  structs.StateClosed,
+	})
 	assert.NoError(t, err)
 	assert.EqualValues(t, 0, len(milestones))
 
diff --git a/routers/api/v1/repo/milestone.go b/routers/api/v1/repo/milestone.go
index adb9deaee4..f6f6b29465 100644
--- a/routers/api/v1/repo/milestone.go
+++ b/routers/api/v1/repo/milestone.go
@@ -39,6 +39,10 @@ func ListMilestones(ctx *context.APIContext) {
 	//   in: query
 	//   description: Milestone state, Recognised values are open, closed and all. Defaults to "open"
 	//   type: string
+	// - name: name
+	//   in: query
+	//   description: filter by milestone name
+	//   type: string
 	// - name: page
 	//   in: query
 	//   description: page number of results to return (1-based)
@@ -51,9 +55,14 @@ func ListMilestones(ctx *context.APIContext) {
 	//   "200":
 	//     "$ref": "#/responses/MilestoneList"
 
-	milestones, err := models.GetMilestonesByRepoID(ctx.Repo.Repository.ID, api.StateType(ctx.Query("state")), utils.GetListOptions(ctx))
+	milestones, err := models.GetMilestones(models.GetMilestonesOption{
+		ListOptions: utils.GetListOptions(ctx),
+		RepoID:      ctx.Repo.Repository.ID,
+		State:       api.StateType(ctx.Query("state")),
+		Name:        ctx.Query("name"),
+	})
 	if err != nil {
-		ctx.Error(http.StatusInternalServerError, "GetMilestonesByRepoID", err)
+		ctx.Error(http.StatusInternalServerError, "GetMilestones", err)
 		return
 	}
 
diff --git a/routers/repo/issue.go b/routers/repo/issue.go
index e3ba5692c4..b3af6705ca 100644
--- a/routers/repo/issue.go
+++ b/routers/repo/issue.go
@@ -360,8 +360,11 @@ func Issues(ctx *context.Context) {
 	issues(ctx, ctx.QueryInt64("milestone"), util.OptionalBoolOf(isPullList))
 
 	var err error
-	// Get milestones.
-	ctx.Data["Milestones"], err = models.GetMilestonesByRepoID(ctx.Repo.Repository.ID, api.StateType(ctx.Query("state")), models.ListOptions{})
+	// Get milestones
+	ctx.Data["Milestones"], err = models.GetMilestones(models.GetMilestonesOption{
+		RepoID: ctx.Repo.Repository.ID,
+		State:  api.StateType(ctx.Query("state")),
+	})
 	if err != nil {
 		ctx.ServerError("GetAllRepoMilestones", err)
 		return
@@ -375,12 +378,18 @@ func Issues(ctx *context.Context) {
 // RetrieveRepoMilestonesAndAssignees find all the milestones and assignees of a repository
 func RetrieveRepoMilestonesAndAssignees(ctx *context.Context, repo *models.Repository) {
 	var err error
-	ctx.Data["OpenMilestones"], err = models.GetMilestones(repo.ID, -1, false, "")
+	ctx.Data["OpenMilestones"], err = models.GetMilestones(models.GetMilestonesOption{
+		RepoID: repo.ID,
+		State:  api.StateOpen,
+	})
 	if err != nil {
 		ctx.ServerError("GetMilestones", err)
 		return
 	}
-	ctx.Data["ClosedMilestones"], err = models.GetMilestones(repo.ID, -1, true, "")
+	ctx.Data["ClosedMilestones"], err = models.GetMilestones(models.GetMilestonesOption{
+		RepoID: repo.ID,
+		State:  api.StateClosed,
+	})
 	if err != nil {
 		ctx.ServerError("GetMilestones", err)
 		return
diff --git a/routers/repo/milestone.go b/routers/repo/milestone.go
index 9d8f34268d..0bd7344878 100644
--- a/routers/repo/milestone.go
+++ b/routers/repo/milestone.go
@@ -13,6 +13,7 @@ import (
 	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/markup/markdown"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/util"
 
@@ -47,13 +48,24 @@ func Milestones(ctx *context.Context) {
 	}
 
 	var total int
+	var state structs.StateType
 	if !isShowClosed {
 		total = int(stats.OpenCount)
+		state = structs.StateOpen
 	} else {
 		total = int(stats.ClosedCount)
+		state = structs.StateClosed
 	}
 
-	miles, err := models.GetMilestones(ctx.Repo.Repository.ID, page, isShowClosed, sortType)
+	miles, err := models.GetMilestones(models.GetMilestonesOption{
+		ListOptions: models.ListOptions{
+			Page:     page,
+			PageSize: setting.UI.IssuePagingNum,
+		},
+		RepoID:   ctx.Repo.Repository.ID,
+		State:    state,
+		SortType: sortType,
+	})
 	if err != nil {
 		ctx.ServerError("GetMilestones", err)
 		return
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index 795e179cb9..28a33fb3d3 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -6164,6 +6164,12 @@
             "name": "state",
             "in": "query"
           },
+          {
+            "type": "string",
+            "description": "filter by milestone name",
+            "name": "name",
+            "in": "query"
+          },
           {
             "type": "integer",
             "description": "page number of results to return (1-based)",