From f94869d2d19afd7110a0b996a7e6da339fb4e161 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Mon, 30 Jan 2017 20:46:45 +0800
Subject: [PATCH] Track labels changed on issue view & resolved #542 (#788)

* track labels changed on issue view & resolved #542

* add missing head comment & sort & fix refresh
---
 models/issue.go                        | 127 ++++++++++++++----
 models/issue_comment.go                |  44 +++++++
 models/issue_label.go                  |  36 ++++--
 options/locale/locale_en-US.ini        |   2 +
 options/locale/locale_zh-CN.ini        |   2 +
 public/js/index.js                     |   4 +
 routers/api/v1/repo/issue_label.go     |   4 +-
 routers/repo/issue.go                  | 161 +----------------------
 routers/repo/issue_label.go            | 170 +++++++++++++++++++++++++
 templates/repo/issue/view_content.tmpl |  11 +-
 10 files changed, 366 insertions(+), 195 deletions(-)
 create mode 100644 routers/repo/issue_label.go

diff --git a/models/issue.go b/models/issue.go
index 6d557ad4e..0102656f0 100644
--- a/models/issue.go
+++ b/models/issue.go
@@ -7,6 +7,7 @@ package models
 import (
 	"errors"
 	"fmt"
+	"sort"
 	"strings"
 	"time"
 
@@ -103,11 +104,17 @@ func (issue *Issue) GetPullRequest() (pr *PullRequest, err error) {
 	return
 }
 
-func (issue *Issue) loadAttributes(e Engine) (err error) {
-	if err := issue.loadRepo(e); err != nil {
-		return err
+func (issue *Issue) loadLabels(e Engine) (err error) {
+	if issue.Labels == nil {
+		issue.Labels, err = getLabelsByIssueID(e, issue.ID)
+		if err != nil {
+			return fmt.Errorf("getLabelsByIssueID [%d]: %v", issue.ID, err)
+		}
 	}
+	return nil
+}
 
+func (issue *Issue) loadPoster(e Engine) (err error) {
 	if issue.Poster == nil {
 		issue.Poster, err = getUserByID(e, issue.PosterID)
 		if err != nil {
@@ -120,12 +127,20 @@ func (issue *Issue) loadAttributes(e Engine) (err error) {
 			return
 		}
 	}
+	return
+}
 
-	if issue.Labels == nil {
-		issue.Labels, err = getLabelsByIssueID(e, issue.ID)
-		if err != nil {
-			return fmt.Errorf("getLabelsByIssueID [%d]: %v", issue.ID, err)
-		}
+func (issue *Issue) loadAttributes(e Engine) (err error) {
+	if err = issue.loadRepo(e); err != nil {
+		return
+	}
+
+	if err = issue.loadPoster(e); err != nil {
+		return
+	}
+
+	if err = issue.loadLabels(e); err != nil {
+		return
 	}
 
 	if issue.Milestone == nil && issue.MilestoneID > 0 {
@@ -289,13 +304,13 @@ func (issue *Issue) sendLabelUpdatedWebhook(doer *User) {
 	}
 }
 
-func (issue *Issue) addLabel(e *xorm.Session, label *Label) error {
-	return newIssueLabel(e, issue, label)
+func (issue *Issue) addLabel(e *xorm.Session, label *Label, doer *User) error {
+	return newIssueLabel(e, issue, label, doer)
 }
 
 // AddLabel adds a new label to the issue.
 func (issue *Issue) AddLabel(doer *User, label *Label) error {
-	if err := NewIssueLabel(issue, label); err != nil {
+	if err := NewIssueLabel(issue, label, doer); err != nil {
 		return err
 	}
 
@@ -303,13 +318,13 @@ func (issue *Issue) AddLabel(doer *User, label *Label) error {
 	return nil
 }
 
-func (issue *Issue) addLabels(e *xorm.Session, labels []*Label) error {
-	return newIssueLabels(e, issue, labels)
+func (issue *Issue) addLabels(e *xorm.Session, labels []*Label, doer *User) error {
+	return newIssueLabels(e, issue, labels, doer)
 }
 
 // AddLabels adds a list of new labels to the issue.
 func (issue *Issue) AddLabels(doer *User, labels []*Label) error {
-	if err := NewIssueLabels(issue, labels); err != nil {
+	if err := NewIssueLabels(issue, labels, doer); err != nil {
 		return err
 	}
 
@@ -329,8 +344,8 @@ func (issue *Issue) getLabels(e Engine) (err error) {
 	return nil
 }
 
-func (issue *Issue) removeLabel(e *xorm.Session, label *Label) error {
-	return deleteIssueLabel(e, issue, label)
+func (issue *Issue) removeLabel(e *xorm.Session, doer *User, label *Label) error {
+	return deleteIssueLabel(e, doer, issue, label)
 }
 
 // RemoveLabel removes a label from issue by given ID.
@@ -345,7 +360,7 @@ func (issue *Issue) RemoveLabel(doer *User, label *Label) error {
 		return ErrLabelNotExist{}
 	}
 
-	if err := DeleteIssueLabel(issue, label); err != nil {
+	if err := DeleteIssueLabel(issue, doer, label); err != nil {
 		return err
 	}
 
@@ -353,13 +368,13 @@ func (issue *Issue) RemoveLabel(doer *User, label *Label) error {
 	return nil
 }
 
-func (issue *Issue) clearLabels(e *xorm.Session) (err error) {
+func (issue *Issue) clearLabels(e *xorm.Session, doer *User) (err error) {
 	if err = issue.getLabels(e); err != nil {
 		return fmt.Errorf("getLabels: %v", err)
 	}
 
 	for i := range issue.Labels {
-		if err = issue.removeLabel(e, issue.Labels[i]); err != nil {
+		if err = issue.removeLabel(e, doer, issue.Labels[i]); err != nil {
 			return fmt.Errorf("removeLabel: %v", err)
 		}
 	}
@@ -386,7 +401,7 @@ func (issue *Issue) ClearLabels(doer *User) (err error) {
 		return ErrLabelNotExist{}
 	}
 
-	if err = issue.clearLabels(sess); err != nil {
+	if err = issue.clearLabels(sess, doer); err != nil {
 		return err
 	}
 
@@ -417,19 +432,75 @@ func (issue *Issue) ClearLabels(doer *User) (err error) {
 	return nil
 }
 
+type labelSorter []*Label
+
+func (ts labelSorter) Len() int {
+	return len([]*Label(ts))
+}
+
+func (ts labelSorter) Less(i, j int) bool {
+	return []*Label(ts)[i].ID < []*Label(ts)[j].ID
+}
+
+func (ts labelSorter) Swap(i, j int) {
+	[]*Label(ts)[i], []*Label(ts)[j] = []*Label(ts)[j], []*Label(ts)[i]
+}
+
 // ReplaceLabels removes all current labels and add new labels to the issue.
 // Triggers appropriate WebHooks, if any.
-func (issue *Issue) ReplaceLabels(labels []*Label) (err error) {
+func (issue *Issue) ReplaceLabels(labels []*Label, doer *User) (err error) {
 	sess := x.NewSession()
 	defer sessionRelease(sess)
 	if err = sess.Begin(); err != nil {
 		return err
 	}
 
-	if err = issue.clearLabels(sess); err != nil {
-		return fmt.Errorf("clearLabels: %v", err)
-	} else if err = issue.addLabels(sess, labels); err != nil {
-		return fmt.Errorf("addLabels: %v", err)
+	if err = issue.loadLabels(sess); err != nil {
+		return err
+	}
+
+	sort.Sort(labelSorter(labels))
+	sort.Sort(labelSorter(issue.Labels))
+
+	var toAdd, toRemove []*Label
+	for _, l := range labels {
+		var exist bool
+		for _, oriLabel := range issue.Labels {
+			if oriLabel.ID == l.ID {
+				exist = true
+				break
+			}
+		}
+		if !exist {
+			toAdd = append(toAdd, l)
+		}
+	}
+
+	for _, oriLabel := range issue.Labels {
+		var exist bool
+		for _, l := range labels {
+			if oriLabel.ID == l.ID {
+				exist = true
+				break
+			}
+		}
+		if !exist {
+			toRemove = append(toRemove, oriLabel)
+		}
+	}
+
+	if len(toAdd) > 0 {
+		if err = issue.addLabels(sess, toAdd, doer); err != nil {
+			return fmt.Errorf("addLabels: %v", err)
+		}
+	}
+
+	if len(toRemove) > 0 {
+		for _, l := range toRemove {
+			if err = issue.removeLabel(sess, doer, l); err != nil {
+				return fmt.Errorf("removeLabel: %v", err)
+			}
+		}
 	}
 
 	return sess.Commit()
@@ -731,13 +802,17 @@ func newIssue(e *xorm.Session, opts NewIssueOptions) (err error) {
 			return fmt.Errorf("find all labels [label_ids: %v]: %v", opts.LableIDs, err)
 		}
 
+		if err = opts.Issue.loadPoster(e); err != nil {
+			return err
+		}
+
 		for _, label := range labels {
 			// Silently drop invalid labels.
 			if label.RepoID != opts.Repo.ID {
 				continue
 			}
 
-			if err = opts.Issue.addLabel(e, label); err != nil {
+			if err = opts.Issue.addLabel(e, label, opts.Issue.Poster); err != nil {
 				return fmt.Errorf("addLabel [id: %d]: %v", label.ID, err)
 			}
 		}
diff --git a/models/issue_comment.go b/models/issue_comment.go
index bab002fca..be7044a8e 100644
--- a/models/issue_comment.go
+++ b/models/issue_comment.go
@@ -36,6 +36,8 @@ const (
 	CommentTypeCommentRef
 	// Reference from a pull request
 	CommentTypePullRef
+	// Labels changed
+	CommentTypeLabel
 )
 
 // CommentTag defines comment tag type
@@ -57,6 +59,8 @@ type Comment struct {
 	Poster          *User `xorm:"-"`
 	IssueID         int64 `xorm:"INDEX"`
 	CommitID        int64
+	LabelID         int64
+	Label           *Label `xorm:"-"`
 	Line            int64
 	Content         string `xorm:"TEXT"`
 	RenderedContent string `xorm:"-"`
@@ -185,6 +189,21 @@ func (c *Comment) EventTag() string {
 	return "event-" + com.ToStr(c.ID)
 }
 
+// LoadLabel if comment.Type is CommentTypeLabel, then load Label
+func (c *Comment) LoadLabel() error {
+	var label Label
+	has, err := x.ID(c.LabelID).Get(&label)
+	if err != nil {
+		return err
+	} else if !has {
+		return ErrLabelNotExist{
+			LabelID: c.LabelID,
+		}
+	}
+	c.Label = &label
+	return nil
+}
+
 // MailParticipants sends new comment emails to repository watchers
 // and mentioned people.
 func (c *Comment) MailParticipants(e Engine, opType ActionType, issue *Issue) (err error) {
@@ -209,11 +228,16 @@ func (c *Comment) MailParticipants(e Engine, opType ActionType, issue *Issue) (e
 }
 
 func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err error) {
+	var LabelID int64
+	if opts.Label != nil {
+		LabelID = opts.Label.ID
+	}
 	comment := &Comment{
 		Type:      opts.Type,
 		PosterID:  opts.Doer.ID,
 		Poster:    opts.Doer,
 		IssueID:   opts.Issue.ID,
+		LabelID:   LabelID,
 		CommitID:  opts.CommitID,
 		CommitSHA: opts.CommitSHA,
 		Line:      opts.LineNum,
@@ -223,6 +247,10 @@ func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err
 		return nil, err
 	}
 
+	if err = opts.Repo.getOwner(e); err != nil {
+		return nil, err
+	}
+
 	// Compose comment action, could be plain comment, close or reopen issue/pull request.
 	// This object will be used to notify watchers in the end of function.
 	act := &Action{
@@ -324,12 +352,28 @@ func createStatusComment(e *xorm.Session, doer *User, repo *Repository, issue *I
 	})
 }
 
+func createLabelComment(e *xorm.Session, doer *User, repo *Repository, issue *Issue, label *Label, add bool) (*Comment, error) {
+	var content string
+	if add {
+		content = "1"
+	}
+	return createComment(e, &CreateCommentOptions{
+		Type:    CommentTypeLabel,
+		Doer:    doer,
+		Repo:    repo,
+		Issue:   issue,
+		Label:   label,
+		Content: content,
+	})
+}
+
 // CreateCommentOptions defines options for creating comment
 type CreateCommentOptions struct {
 	Type  CommentType
 	Doer  *User
 	Repo  *Repository
 	Issue *Issue
+	Label *Label
 
 	CommitID    int64
 	CommitSHA   string
diff --git a/models/issue_label.go b/models/issue_label.go
index 0e1c6d6c4..02397f146 100644
--- a/models/issue_label.go
+++ b/models/issue_label.go
@@ -276,7 +276,7 @@ func HasIssueLabel(issueID, labelID int64) bool {
 	return hasIssueLabel(x, issueID, labelID)
 }
 
-func newIssueLabel(e *xorm.Session, issue *Issue, label *Label) (err error) {
+func newIssueLabel(e *xorm.Session, issue *Issue, label *Label, doer *User) (err error) {
 	if _, err = e.Insert(&IssueLabel{
 		IssueID: issue.ID,
 		LabelID: label.ID,
@@ -284,6 +284,14 @@ func newIssueLabel(e *xorm.Session, issue *Issue, label *Label) (err error) {
 		return err
 	}
 
+	if err = issue.loadRepo(e); err != nil {
+		return
+	}
+
+	if _, err = createLabelComment(e, doer, issue.Repo, issue, label, true); err != nil {
+		return err
+	}
+
 	label.NumIssues++
 	if issue.IsClosed {
 		label.NumClosedIssues++
@@ -292,7 +300,7 @@ func newIssueLabel(e *xorm.Session, issue *Issue, label *Label) (err error) {
 }
 
 // NewIssueLabel creates a new issue-label relation.
-func NewIssueLabel(issue *Issue, label *Label) (err error) {
+func NewIssueLabel(issue *Issue, label *Label, doer *User) (err error) {
 	if HasIssueLabel(issue.ID, label.ID) {
 		return nil
 	}
@@ -303,20 +311,20 @@ func NewIssueLabel(issue *Issue, label *Label) (err error) {
 		return err
 	}
 
-	if err = newIssueLabel(sess, issue, label); err != nil {
+	if err = newIssueLabel(sess, issue, label, doer); err != nil {
 		return err
 	}
 
 	return sess.Commit()
 }
 
-func newIssueLabels(e *xorm.Session, issue *Issue, labels []*Label) (err error) {
+func newIssueLabels(e *xorm.Session, issue *Issue, labels []*Label, doer *User) (err error) {
 	for i := range labels {
 		if hasIssueLabel(e, issue.ID, labels[i].ID) {
 			continue
 		}
 
-		if err = newIssueLabel(e, issue, labels[i]); err != nil {
+		if err = newIssueLabel(e, issue, labels[i], doer); err != nil {
 			return fmt.Errorf("newIssueLabel: %v", err)
 		}
 	}
@@ -325,14 +333,14 @@ func newIssueLabels(e *xorm.Session, issue *Issue, labels []*Label) (err error)
 }
 
 // NewIssueLabels creates a list of issue-label relations.
-func NewIssueLabels(issue *Issue, labels []*Label) (err error) {
+func NewIssueLabels(issue *Issue, labels []*Label, doer *User) (err error) {
 	sess := x.NewSession()
 	defer sessionRelease(sess)
 	if err = sess.Begin(); err != nil {
 		return err
 	}
 
-	if err = newIssueLabels(sess, issue, labels); err != nil {
+	if err = newIssueLabels(sess, issue, labels, doer); err != nil {
 		return err
 	}
 
@@ -352,7 +360,7 @@ func GetIssueLabels(issueID int64) ([]*IssueLabel, error) {
 	return getIssueLabels(x, issueID)
 }
 
-func deleteIssueLabel(e *xorm.Session, issue *Issue, label *Label) (err error) {
+func deleteIssueLabel(e *xorm.Session, doer *User, issue *Issue, label *Label) (err error) {
 	if _, err = e.Delete(&IssueLabel{
 		IssueID: issue.ID,
 		LabelID: label.ID,
@@ -360,6 +368,14 @@ func deleteIssueLabel(e *xorm.Session, issue *Issue, label *Label) (err error) {
 		return err
 	}
 
+	if err = issue.loadRepo(e); err != nil {
+		return
+	}
+
+	if _, err = createLabelComment(e, doer, issue.Repo, issue, label, false); err != nil {
+		return err
+	}
+
 	label.NumIssues--
 	if issue.IsClosed {
 		label.NumClosedIssues--
@@ -368,14 +384,14 @@ func deleteIssueLabel(e *xorm.Session, issue *Issue, label *Label) (err error) {
 }
 
 // DeleteIssueLabel deletes issue-label relation.
-func DeleteIssueLabel(issue *Issue, label *Label) (err error) {
+func DeleteIssueLabel(issue *Issue, doer *User, label *Label) (err error) {
 	sess := x.NewSession()
 	defer sessionRelease(sess)
 	if err = sess.Begin(); err != nil {
 		return err
 	}
 
-	if err = deleteIssueLabel(sess, issue, label); err != nil {
+	if err = deleteIssueLabel(sess, doer, issue, label); err != nil {
 		return err
 	}
 
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 71130403e..546b9489b 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -541,6 +541,8 @@ issues.label_templates.info = There aren't any labels yet. You can click on the
 issues.label_templates.helper = Select a label set
 issues.label_templates.use = Use this label set
 issues.label_templates.fail_to_load_file = Failed to load label template file '%s': %v
+issues.add_label_at = `added the <div class="ui label" style="color: %s; background-color: %s">%s</div> label %s`
+issues.remove_label_at = `removed the <div class="ui label" style="color: %s; background-color: %s">%s</div> label %s`
 issues.open_tab = %d Open
 issues.close_tab = %d Closed
 issues.filter_label = Label
diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini
index e70f84a9b..840ff36b5 100644
--- a/options/locale/locale_zh-CN.ini
+++ b/options/locale/locale_zh-CN.ini
@@ -501,6 +501,8 @@ issues.label_templates.info=此仓库还未创建任何标签,您可以通过
 issues.label_templates.helper=选择标签模板
 issues.label_templates.use=加载标签模板
 issues.label_templates.fail_to_load_file=加载标签模板文件 '%s' 时发生错误:%v
+issues.add_label_at = ` %[4]s 添加了标签 <div class="ui label" style="color: %[1]s; background-color: %[2]s">%[3]s</div>`
+issues.remove_label_at = ` %[4]s 删除了标签 <div class="ui label" style="color: %[1]s; background-color: %[2]s">%[3]s</div>`
 issues.open_tab=%d 个开启中
 issues.close_tab=%d 个已关闭
 issues.filter_label=标签筛选
diff --git a/public/js/index.js b/public/js/index.js
index 8ccbfc175..47bdbe892 100644
--- a/public/js/index.js
+++ b/public/js/index.js
@@ -108,6 +108,10 @@ function initCommentForm() {
         });
     }
 
+    $('.select-label').dropdown('setting', 'onHide', function(){
+        location.reload();
+    });
+
     $labelMenu.find('.item:not(.no-select)').click(function () {
         if ($(this).hasClass('checked')) {
             $(this).removeClass('checked');
diff --git a/routers/api/v1/repo/issue_label.go b/routers/api/v1/repo/issue_label.go
index e91b8e9da..20de41b75 100644
--- a/routers/api/v1/repo/issue_label.go
+++ b/routers/api/v1/repo/issue_label.go
@@ -98,7 +98,7 @@ func DeleteIssueLabel(ctx *context.APIContext) {
 		return
 	}
 
-	if err := models.DeleteIssueLabel(issue, label); err != nil {
+	if err := models.DeleteIssueLabel(issue, ctx.User, label); err != nil {
 		ctx.Error(500, "DeleteIssueLabel", err)
 		return
 	}
@@ -129,7 +129,7 @@ func ReplaceIssueLabels(ctx *context.APIContext, form api.IssueLabelsOption) {
 		return
 	}
 
-	if err := issue.ReplaceLabels(labels); err != nil {
+	if err := issue.ReplaceLabels(labels, ctx.User); err != nil {
 		ctx.Error(500, "ReplaceLabels", err)
 		return
 	}
diff --git a/routers/repo/issue.go b/routers/repo/issue.go
index 7ae614038..b2490e242 100644
--- a/routers/repo/issue.go
+++ b/routers/repo/issue.go
@@ -34,8 +34,6 @@ const (
 	tplIssueNew  base.TplName = "repo/issue/new"
 	tplIssueView base.TplName = "repo/issue/view"
 
-	tplLabels base.TplName = "repo/issue/labels"
-
 	tplMilestone     base.TplName = "repo/issue/milestones"
 	tplMilestoneNew  base.TplName = "repo/issue/milestone_new"
 	tplMilestoneEdit base.TplName = "repo/issue/milestone_edit"
@@ -86,21 +84,6 @@ func MustAllowPulls(ctx *context.Context) {
 	}
 }
 
-// RetrieveLabels find all the labels of a repository
-func RetrieveLabels(ctx *context.Context) {
-	labels, err := models.GetLabelsByRepoID(ctx.Repo.Repository.ID, ctx.Query("sort"))
-	if err != nil {
-		ctx.Handle(500, "RetrieveLabels.GetLabels", err)
-		return
-	}
-	for _, l := range labels {
-		l.CalOpenIssues()
-	}
-	ctx.Data["Labels"] = labels
-	ctx.Data["NumLabels"] = len(labels)
-	ctx.Data["SortType"] = ctx.Query("sort")
-}
-
 // Issues render issues page
 func Issues(ctx *context.Context) {
 	isPullList := ctx.Params(":type") == "pulls"
@@ -629,6 +612,11 @@ func ViewIssue(ctx *context.Context) {
 			if !isAdded && !issue.IsPoster(comment.Poster.ID) {
 				participants = append(participants, comment.Poster)
 			}
+		} else if comment.Type == models.CommentTypeLabel {
+			if err = comment.LoadLabel(); err != nil {
+				ctx.Handle(500, "LoadLabel", err)
+				return
+			}
 		}
 	}
 
@@ -723,48 +711,6 @@ func UpdateIssueContent(ctx *context.Context) {
 	})
 }
 
-// UpdateIssueLabel change issue's labels
-func UpdateIssueLabel(ctx *context.Context) {
-	issue := getActionIssue(ctx)
-	if ctx.Written() {
-		return
-	}
-
-	if ctx.Query("action") == "clear" {
-		if err := issue.ClearLabels(ctx.User); err != nil {
-			ctx.Handle(500, "ClearLabels", err)
-			return
-		}
-	} else {
-		isAttach := ctx.Query("action") == "attach"
-		label, err := models.GetLabelByID(ctx.QueryInt64("id"))
-		if err != nil {
-			if models.IsErrLabelNotExist(err) {
-				ctx.Error(404, "GetLabelByID")
-			} else {
-				ctx.Handle(500, "GetLabelByID", err)
-			}
-			return
-		}
-
-		if isAttach && !issue.HasLabel(label.ID) {
-			if err = issue.AddLabel(ctx.User, label); err != nil {
-				ctx.Handle(500, "AddLabel", err)
-				return
-			}
-		} else if !isAttach && issue.HasLabel(label.ID) {
-			if err = issue.RemoveLabel(ctx.User, label); err != nil {
-				ctx.Handle(500, "RemoveLabel", err)
-				return
-			}
-		}
-	}
-
-	ctx.JSON(200, map[string]interface{}{
-		"ok": true,
-	})
-}
-
 // UpdateIssueMilestone change issue's milestone
 func UpdateIssueMilestone(ctx *context.Context) {
 	issue := getActionIssue(ctx)
@@ -966,103 +912,6 @@ func DeleteComment(ctx *context.Context) {
 	ctx.Status(200)
 }
 
-// Labels render issue's labels page
-func Labels(ctx *context.Context) {
-	ctx.Data["Title"] = ctx.Tr("repo.labels")
-	ctx.Data["PageIsIssueList"] = true
-	ctx.Data["PageIsLabels"] = true
-	ctx.Data["RequireMinicolors"] = true
-	ctx.Data["LabelTemplates"] = models.LabelTemplates
-	ctx.HTML(200, tplLabels)
-}
-
-// InitializeLabels init labels for a repository
-func InitializeLabels(ctx *context.Context, form auth.InitializeLabelsForm) {
-	if ctx.HasError() {
-		ctx.Redirect(ctx.Repo.RepoLink + "/labels")
-		return
-	}
-	list, err := models.GetLabelTemplateFile(form.TemplateName)
-	if err != nil {
-		ctx.Flash.Error(ctx.Tr("repo.issues.label_templates.fail_to_load_file", form.TemplateName, err))
-		ctx.Redirect(ctx.Repo.RepoLink + "/labels")
-		return
-	}
-
-	labels := make([]*models.Label, len(list))
-	for i := 0; i < len(list); i++ {
-		labels[i] = &models.Label{
-			RepoID: ctx.Repo.Repository.ID,
-			Name:   list[i][0],
-			Color:  list[i][1],
-		}
-	}
-	if err := models.NewLabels(labels...); err != nil {
-		ctx.Handle(500, "NewLabels", err)
-		return
-	}
-	ctx.Redirect(ctx.Repo.RepoLink + "/labels")
-}
-
-// NewLabel create new label for repository
-func NewLabel(ctx *context.Context, form auth.CreateLabelForm) {
-	ctx.Data["Title"] = ctx.Tr("repo.labels")
-	ctx.Data["PageIsLabels"] = true
-
-	if ctx.HasError() {
-		ctx.Flash.Error(ctx.Data["ErrorMsg"].(string))
-		ctx.Redirect(ctx.Repo.RepoLink + "/labels")
-		return
-	}
-
-	l := &models.Label{
-		RepoID: ctx.Repo.Repository.ID,
-		Name:   form.Title,
-		Color:  form.Color,
-	}
-	if err := models.NewLabels(l); err != nil {
-		ctx.Handle(500, "NewLabel", err)
-		return
-	}
-	ctx.Redirect(ctx.Repo.RepoLink + "/labels")
-}
-
-// UpdateLabel update a label's name and color
-func UpdateLabel(ctx *context.Context, form auth.CreateLabelForm) {
-	l, err := models.GetLabelByID(form.ID)
-	if err != nil {
-		switch {
-		case models.IsErrLabelNotExist(err):
-			ctx.Error(404)
-		default:
-			ctx.Handle(500, "UpdateLabel", err)
-		}
-		return
-	}
-
-	l.Name = form.Title
-	l.Color = form.Color
-	if err := models.UpdateLabel(l); err != nil {
-		ctx.Handle(500, "UpdateLabel", err)
-		return
-	}
-	ctx.Redirect(ctx.Repo.RepoLink + "/labels")
-}
-
-// DeleteLabel delete a label
-func DeleteLabel(ctx *context.Context) {
-	if err := models.DeleteLabel(ctx.Repo.Repository.ID, ctx.QueryInt64("id")); err != nil {
-		ctx.Flash.Error("DeleteLabel: " + err.Error())
-	} else {
-		ctx.Flash.Success(ctx.Tr("repo.issues.label_deletion_success"))
-	}
-
-	ctx.JSON(200, map[string]interface{}{
-		"redirect": ctx.Repo.RepoLink + "/labels",
-	})
-	return
-}
-
 // Milestones render milestones page
 func Milestones(ctx *context.Context) {
 	ctx.Data["Title"] = ctx.Tr("repo.milestones")
diff --git a/routers/repo/issue_label.go b/routers/repo/issue_label.go
new file mode 100644
index 000000000..679294766
--- /dev/null
+++ b/routers/repo/issue_label.go
@@ -0,0 +1,170 @@
+// 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 repo
+
+import (
+	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/modules/auth"
+	"code.gitea.io/gitea/modules/base"
+	"code.gitea.io/gitea/modules/context"
+)
+
+const (
+	tplLabels base.TplName = "repo/issue/labels"
+)
+
+// Labels render issue's labels page
+func Labels(ctx *context.Context) {
+	ctx.Data["Title"] = ctx.Tr("repo.labels")
+	ctx.Data["PageIsIssueList"] = true
+	ctx.Data["PageIsLabels"] = true
+	ctx.Data["RequireMinicolors"] = true
+	ctx.Data["LabelTemplates"] = models.LabelTemplates
+	ctx.HTML(200, tplLabels)
+}
+
+// InitializeLabels init labels for a repository
+func InitializeLabels(ctx *context.Context, form auth.InitializeLabelsForm) {
+	if ctx.HasError() {
+		ctx.Redirect(ctx.Repo.RepoLink + "/labels")
+		return
+	}
+	list, err := models.GetLabelTemplateFile(form.TemplateName)
+	if err != nil {
+		ctx.Flash.Error(ctx.Tr("repo.issues.label_templates.fail_to_load_file", form.TemplateName, err))
+		ctx.Redirect(ctx.Repo.RepoLink + "/labels")
+		return
+	}
+
+	labels := make([]*models.Label, len(list))
+	for i := 0; i < len(list); i++ {
+		labels[i] = &models.Label{
+			RepoID: ctx.Repo.Repository.ID,
+			Name:   list[i][0],
+			Color:  list[i][1],
+		}
+	}
+	if err := models.NewLabels(labels...); err != nil {
+		ctx.Handle(500, "NewLabels", err)
+		return
+	}
+	ctx.Redirect(ctx.Repo.RepoLink + "/labels")
+}
+
+// RetrieveLabels find all the labels of a repository
+func RetrieveLabels(ctx *context.Context) {
+	labels, err := models.GetLabelsByRepoID(ctx.Repo.Repository.ID, ctx.Query("sort"))
+	if err != nil {
+		ctx.Handle(500, "RetrieveLabels.GetLabels", err)
+		return
+	}
+	for _, l := range labels {
+		l.CalOpenIssues()
+	}
+	ctx.Data["Labels"] = labels
+	ctx.Data["NumLabels"] = len(labels)
+	ctx.Data["SortType"] = ctx.Query("sort")
+}
+
+// NewLabel create new label for repository
+func NewLabel(ctx *context.Context, form auth.CreateLabelForm) {
+	ctx.Data["Title"] = ctx.Tr("repo.labels")
+	ctx.Data["PageIsLabels"] = true
+
+	if ctx.HasError() {
+		ctx.Flash.Error(ctx.Data["ErrorMsg"].(string))
+		ctx.Redirect(ctx.Repo.RepoLink + "/labels")
+		return
+	}
+
+	l := &models.Label{
+		RepoID: ctx.Repo.Repository.ID,
+		Name:   form.Title,
+		Color:  form.Color,
+	}
+	if err := models.NewLabels(l); err != nil {
+		ctx.Handle(500, "NewLabel", err)
+		return
+	}
+	ctx.Redirect(ctx.Repo.RepoLink + "/labels")
+}
+
+// UpdateLabel update a label's name and color
+func UpdateLabel(ctx *context.Context, form auth.CreateLabelForm) {
+	l, err := models.GetLabelByID(form.ID)
+	if err != nil {
+		switch {
+		case models.IsErrLabelNotExist(err):
+			ctx.Error(404)
+		default:
+			ctx.Handle(500, "UpdateLabel", err)
+		}
+		return
+	}
+
+	l.Name = form.Title
+	l.Color = form.Color
+	if err := models.UpdateLabel(l); err != nil {
+		ctx.Handle(500, "UpdateLabel", err)
+		return
+	}
+	ctx.Redirect(ctx.Repo.RepoLink + "/labels")
+}
+
+// DeleteLabel delete a label
+func DeleteLabel(ctx *context.Context) {
+	if err := models.DeleteLabel(ctx.Repo.Repository.ID, ctx.QueryInt64("id")); err != nil {
+		ctx.Flash.Error("DeleteLabel: " + err.Error())
+	} else {
+		ctx.Flash.Success(ctx.Tr("repo.issues.label_deletion_success"))
+	}
+
+	ctx.JSON(200, map[string]interface{}{
+		"redirect": ctx.Repo.RepoLink + "/labels",
+	})
+	return
+}
+
+// UpdateIssueLabel change issue's labels
+func UpdateIssueLabel(ctx *context.Context) {
+	issue := getActionIssue(ctx)
+	if ctx.Written() {
+		return
+	}
+
+	if ctx.Query("action") == "clear" {
+		if err := issue.ClearLabels(ctx.User); err != nil {
+			ctx.Handle(500, "ClearLabels", err)
+			return
+		}
+	} else {
+		isAttach := ctx.Query("action") == "attach"
+		label, err := models.GetLabelByID(ctx.QueryInt64("id"))
+		if err != nil {
+			if models.IsErrLabelNotExist(err) {
+				ctx.Error(404, "GetLabelByID")
+			} else {
+				ctx.Handle(500, "GetLabelByID", err)
+			}
+			return
+		}
+
+		if isAttach && !issue.HasLabel(label.ID) {
+			if err = issue.AddLabel(ctx.User, label); err != nil {
+				ctx.Handle(500, "AddLabel", err)
+				return
+			}
+		} else if !isAttach && issue.HasLabel(label.ID) {
+			if err = issue.RemoveLabel(ctx.User, label); err != nil {
+				ctx.Handle(500, "RemoveLabel", err)
+				return
+			}
+		}
+	}
+
+	ctx.JSON(200, map[string]interface{}{
+		"ok": true,
+	})
+}
diff --git a/templates/repo/issue/view_content.tmpl b/templates/repo/issue/view_content.tmpl
index 8c1f266f1..ef32d0ba1 100644
--- a/templates/repo/issue/view_content.tmpl
+++ b/templates/repo/issue/view_content.tmpl
@@ -58,7 +58,7 @@
 			{{range .Issue.Comments}}
 				{{ $createdStr:= TimeSince .Created $.Lang }}
 
-				<!-- 0 = COMMENT, 1 = REOPEN, 2 = CLOSE, 3 = ISSUE_REF, 4 = COMMIT_REF, 5 = COMMENT_REF, 6 = PULL_REF -->
+				<!-- 0 = COMMENT, 1 = REOPEN, 2 = CLOSE, 3 = ISSUE_REF, 4 = COMMIT_REF, 5 = COMMENT_REF, 6 = PULL_REF, 7 = COMMENT_LABEL -->
 				{{if eq .Type 0}}
 					<div class="comment" id="{{.HashTag}}">
 						<a class="avatar" {{if gt .Poster.ID 0}}href="{{.Poster.HomeLink}}"{{end}}>
@@ -144,6 +144,15 @@
 							<span class="text grey">{{.Content | Str2html}}</span>
 						</div>
 					</div>
+				{{else if eq .Type 7}}
+					<div class="event">
+						<span class="octicon octicon-primitive-dot"></span>
+						<a class="ui avatar image" href="{{.Poster.HomeLink}}">
+							<img src="{{.Poster.RelAvatarLink}}">
+						</a>
+						<span class="text grey"><a href="{{.Poster.HomeLink}}">{{.Poster.Name}}</a>
+						{{if .Content}}{{$.i18n.Tr "repo.issues.add_label_at" .Label.ForegroundColor .Label.Color  .Label.Name $createdStr | Safe}}{{else}}{{$.i18n.Tr "repo.issues.remove_label_at" .Label.ForegroundColor .Label.Color .Label.Name $createdStr | Safe}}{{end}}</span>
+					</div>
 				{{end}}
 
 			{{end}}