diff --git a/routers/api/v1/repo/issue_label.go b/routers/api/v1/repo/issue_label.go
index 492da244f..a9fb42a18 100644
--- a/routers/api/v1/repo/issue_label.go
+++ b/routers/api/v1/repo/issue_label.go
@@ -102,24 +102,8 @@ func AddIssueLabels(ctx *context.APIContext, form api.IssueLabelsOption) {
 	//   "403":
 	//     "$ref": "#/responses/forbidden"
 
-	issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+	issue, labels, err := prepareForReplaceOrAdd(ctx, form)
 	if err != nil {
-		if models.IsErrIssueNotExist(err) {
-			ctx.NotFound()
-		} else {
-			ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
-		}
-		return
-	}
-
-	if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
-		ctx.Status(http.StatusForbidden)
-		return
-	}
-
-	labels, err := models.GetLabelsInRepoByIDs(ctx.Repo.Repository.ID, form.Labels)
-	if err != nil {
-		ctx.Error(http.StatusInternalServerError, "GetLabelsInRepoByIDs", err)
 		return
 	}
 
@@ -204,7 +188,7 @@ func DeleteIssueLabel(ctx *context.APIContext) {
 		return
 	}
 
-	if err := models.DeleteIssueLabel(issue, label, ctx.User); err != nil {
+	if err := issue_service.RemoveLabel(issue, ctx.User, label); err != nil {
 		ctx.Error(http.StatusInternalServerError, "DeleteIssueLabel", err)
 		return
 	}
@@ -248,28 +232,12 @@ func ReplaceIssueLabels(ctx *context.APIContext, form api.IssueLabelsOption) {
 	//   "403":
 	//     "$ref": "#/responses/forbidden"
 
-	issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+	issue, labels, err := prepareForReplaceOrAdd(ctx, form)
 	if err != nil {
-		if models.IsErrIssueNotExist(err) {
-			ctx.NotFound()
-		} else {
-			ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
-		}
 		return
 	}
 
-	if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
-		ctx.Status(http.StatusForbidden)
-		return
-	}
-
-	labels, err := models.GetLabelsInRepoByIDs(ctx.Repo.Repository.ID, form.Labels)
-	if err != nil {
-		ctx.Error(http.StatusInternalServerError, "GetLabelsInRepoByIDs", err)
-		return
-	}
-
-	if err := issue.ReplaceLabels(labels, ctx.User); err != nil {
+	if err := issue_service.ReplaceLabels(issue, ctx.User, labels); err != nil {
 		ctx.Error(http.StatusInternalServerError, "ReplaceLabels", err)
 		return
 	}
@@ -339,3 +307,28 @@ func ClearIssueLabels(ctx *context.APIContext) {
 
 	ctx.Status(http.StatusNoContent)
 }
+
+func prepareForReplaceOrAdd(ctx *context.APIContext, form api.IssueLabelsOption) (issue *models.Issue, labels []*models.Label, err error) {
+	issue, err = models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+	if err != nil {
+		if models.IsErrIssueNotExist(err) {
+			ctx.NotFound()
+		} else {
+			ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
+		}
+		return
+	}
+
+	labels, err = models.GetLabelsInRepoByIDs(ctx.Repo.Repository.ID, form.Labels)
+	if err != nil {
+		ctx.Error(http.StatusInternalServerError, "GetLabelsInRepoByIDs", err)
+		return
+	}
+
+	if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
+		ctx.Status(http.StatusForbidden)
+		return
+	}
+
+	return
+}
diff --git a/services/issue/label.go b/services/issue/label.go
index 5e154df95..d2c1cd6ec 100644
--- a/services/issue/label.go
+++ b/services/issue/label.go
@@ -61,3 +61,18 @@ func RemoveLabel(issue *models.Issue, doer *models.User, label *models.Label) er
 	notification.NotifyIssueChangeLabels(doer, issue, nil, []*models.Label{label})
 	return nil
 }
+
+// ReplaceLabels removes all current labels and add new labels to the issue.
+func ReplaceLabels(issue *models.Issue, doer *models.User, labels []*models.Label) error {
+	old, err := models.GetLabelsByIssueID(issue.ID)
+	if err != nil {
+		return err
+	}
+
+	if err := issue.ReplaceLabels(labels, doer); err != nil {
+		return err
+	}
+
+	notification.NotifyIssueChangeLabels(doer, issue, labels, old)
+	return nil
+}