| @@ -11,6 +11,7 @@ import ( | |||
| "regexp" | |||
| "strings" | |||
| "time" | |||
| "unicode" | |||
| "github.com/gogits/git" | |||
| @@ -93,12 +94,15 @@ func (a Action) GetContent() string { | |||
| return a.Content | |||
| } | |||
| func updateIssuesCommit(repoUserName, repoName string, commits []*base.PushCommit) error { | |||
| func updateIssuesCommit(userId, repoId int64, repoUserName, repoName string, commits []*base.PushCommit) error { | |||
| for _, c := range commits { | |||
| refs := IssueKeywordsPat.FindAllString(c.Message, -1) | |||
| for _, ref := range refs { | |||
| ref := ref[strings.IndexByte(ref, byte(' '))+1:] | |||
| ref = strings.TrimRightFunc(ref, func(c rune) bool { | |||
| return !unicode.IsDigit(c) | |||
| }) | |||
| if len(ref) == 0 { | |||
| continue | |||
| @@ -120,24 +124,44 @@ func updateIssuesCommit(repoUserName, repoName string, commits []*base.PushCommi | |||
| return err | |||
| } | |||
| if issue.IsClosed { | |||
| continue | |||
| } | |||
| issue.IsClosed = true | |||
| url := fmt.Sprintf("/%s/%s/commit/%s", repoUserName, repoName, c.Sha1) | |||
| message := fmt.Sprintf(`<a href="%s">%s</a>`, url, c.Message) | |||
| if err = UpdateIssue(issue); err != nil { | |||
| if err = CreateComment(userId, issue.RepoId, issue.Id, 0, 0, COMMIT, message); err != nil { | |||
| return err | |||
| } | |||
| issue.Repo.NumClosedIssues++ | |||
| if issue.RepoId == repoId { | |||
| if issue.IsClosed { | |||
| continue | |||
| } | |||
| if err = UpdateRepository(issue.Repo); err != nil { | |||
| return err | |||
| } | |||
| issue.IsClosed = true | |||
| if err = ChangeMilestoneIssueStats(issue); err != nil { | |||
| return err | |||
| if err = UpdateIssue(issue); err != nil { | |||
| return err | |||
| } | |||
| issue.Repo, err = GetRepositoryById(issue.RepoId) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| issue.Repo.NumClosedIssues++ | |||
| if err = UpdateRepository(issue.Repo); err != nil { | |||
| return err | |||
| } | |||
| if err = ChangeMilestoneIssueStats(issue); err != nil { | |||
| return err | |||
| } | |||
| // If commit happened in the referenced repository, it means the issue can be closed. | |||
| if err = CreateComment(userId, repoId, issue.Id, 0, 0, CLOSE, ""); err != nil { | |||
| return err | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -174,7 +198,7 @@ func CommitRepoAction(userId, repoUserId int64, userName, actEmail string, | |||
| return errors.New("action.CommitRepoAction(UpdateRepository): " + err.Error()) | |||
| } | |||
| err = updateIssuesCommit(repoUserName, repoName, commit.Commits) | |||
| err = updateIssuesCommit(userId, repoId, repoUserName, repoName, commit.Commits) | |||
| if err != nil { | |||
| log.Debug("action.CommitRepoAction(updateIssuesCommit): ", err) | |||
| @@ -7,6 +7,7 @@ package models | |||
| import ( | |||
| "bytes" | |||
| "errors" | |||
| "html/template" | |||
| "strconv" | |||
| "strings" | |||
| "time" | |||
| @@ -833,17 +834,33 @@ func DeleteMilestone(m *Milestone) (err error) { | |||
| // \______ /\____/|__|_| /__|_| /\___ >___| /__| | |||
| // \/ \/ \/ \/ \/ | |||
| // Issue types. | |||
| // CommentType defines whether a comment is just a simple comment, an action (like close) or a reference. | |||
| type CommentType int | |||
| const ( | |||
| IT_PLAIN = iota // Pure comment. | |||
| IT_REOPEN // Issue reopen status change prompt. | |||
| IT_CLOSE // Issue close status change prompt. | |||
| // Plain comment, can be associated with a commit (CommitId > 0) and a line (Line > 0) | |||
| COMMENT CommentType = iota | |||
| // Reopen action | |||
| REOPEN | |||
| // Close action | |||
| CLOSE | |||
| // Reference from another issue | |||
| ISSUE | |||
| // Reference from some commit (not part of a pull request) | |||
| COMMIT | |||
| // Reference from some pull request | |||
| PULL | |||
| ) | |||
| // Comment represents a comment in commit and issue page. | |||
| type Comment struct { | |||
| Id int64 | |||
| Type int | |||
| Type CommentType | |||
| PosterId int64 | |||
| Poster *User `xorm:"-"` | |||
| IssueId int64 | |||
| @@ -854,7 +871,7 @@ type Comment struct { | |||
| } | |||
| // CreateComment creates comment of issue or commit. | |||
| func CreateComment(userId, repoId, issueId, commitId, line int64, cmtType int, content string) error { | |||
| func CreateComment(userId, repoId, issueId, commitId, line int64, cmtType CommentType, content string) error { | |||
| sess := x.NewSession() | |||
| defer sess.Close() | |||
| if err := sess.Begin(); err != nil { | |||
| @@ -869,19 +886,19 @@ func CreateComment(userId, repoId, issueId, commitId, line int64, cmtType int, c | |||
| // Check comment type. | |||
| switch cmtType { | |||
| case IT_PLAIN: | |||
| case COMMENT: | |||
| rawSql := "UPDATE `issue` SET num_comments = num_comments + 1 WHERE id = ?" | |||
| if _, err := sess.Exec(rawSql, issueId); err != nil { | |||
| sess.Rollback() | |||
| return err | |||
| } | |||
| case IT_REOPEN: | |||
| case REOPEN: | |||
| rawSql := "UPDATE `repository` SET num_closed_issues = num_closed_issues - 1 WHERE id = ?" | |||
| if _, err := sess.Exec(rawSql, repoId); err != nil { | |||
| sess.Rollback() | |||
| return err | |||
| } | |||
| case IT_CLOSE: | |||
| case CLOSE: | |||
| rawSql := "UPDATE `repository` SET num_closed_issues = num_closed_issues + 1 WHERE id = ?" | |||
| if _, err := sess.Exec(rawSql, repoId); err != nil { | |||
| sess.Rollback() | |||
| @@ -891,6 +908,10 @@ func CreateComment(userId, repoId, issueId, commitId, line int64, cmtType int, c | |||
| return sess.Commit() | |||
| } | |||
| func (c *Comment) ContentHtml() template.HTML { | |||
| return template.HTML(c.Content) | |||
| } | |||
| // GetIssueComments returns list of comment by given issue id. | |||
| func GetIssueComments(issueId int64) ([]Comment, error) { | |||
| comments := make([]Comment, 0, 10) | |||
| @@ -1258,9 +1258,16 @@ body { | |||
| } | |||
| #issue .issue-child .panel-heading .user, | |||
| #issue .issue-closed a.user, | |||
| #issue .issue-opened a.user { | |||
| #issue .issue-opened a.user, | |||
| #issue .issue-reference a.user { | |||
| font-weight: bold; | |||
| } | |||
| #issue .issue-child .issue-content .user .avatar { | |||
| height: 21px; | |||
| width: 21px; | |||
| } | |||
| #issue .issue-line { | |||
| border-color: #CCC; | |||
| } | |||
| @@ -1280,18 +1287,26 @@ body { | |||
| width: 60%; | |||
| } | |||
| #issue .issue-closed .issue-content, | |||
| #issue .issue-opened .issue-content { | |||
| #issue .issue-opened .issue-content, | |||
| #issue .issue-reference .issue-content { | |||
| line-height: 42px; | |||
| } | |||
| #issue .issue-closed, | |||
| #issue .issue-opened { | |||
| #issue .issue-opened, | |||
| #issue .issue-reference { | |||
| border-bottom: 2px solid #CCC; | |||
| margin-bottom: 24px; | |||
| padding-bottom: 24px; | |||
| } | |||
| #issue .issue-reference { | |||
| padding-bottom: 6px; | |||
| } | |||
| #issue .issue-closed .label-danger, | |||
| #issue .issue-opened .label-success { | |||
| margin: 0 .8em; | |||
| #issue .issue-opened .label-success, | |||
| #issue .issue-reference .label-primary { | |||
| margin: 0.8em; | |||
| } | |||
| #issue .milestone-item .actions { | |||
| margin-top: 10px; | |||
| @@ -393,7 +393,10 @@ func ViewIssue(ctx *middleware.Context, params martini.Params) { | |||
| return | |||
| } | |||
| comments[i].Poster = u | |||
| comments[i].Content = string(base.RenderMarkdown([]byte(comments[i].Content), ctx.Repo.RepoLink)) | |||
| if comments[i].Type == models.COMMENT { | |||
| comments[i].Content = string(base.RenderMarkdown([]byte(comments[i].Content), ctx.Repo.RepoLink)) | |||
| } | |||
| } | |||
| ctx.Data["Title"] = issue.Name | |||
| @@ -649,9 +652,9 @@ func Comment(ctx *middleware.Context, params martini.Params) { | |||
| } | |||
| } | |||
| cmtType := models.IT_CLOSE | |||
| cmtType := models.CLOSE | |||
| if !issue.IsClosed { | |||
| cmtType = models.IT_REOPEN | |||
| cmtType = models.REOPEN | |||
| } | |||
| if err = models.CreateComment(ctx.User.Id, ctx.Repo.Repository.Id, issue.Id, 0, 0, cmtType, ""); err != nil { | |||
| @@ -667,7 +670,7 @@ func Comment(ctx *middleware.Context, params martini.Params) { | |||
| if len(content) > 0 { | |||
| switch params["action"] { | |||
| case "new": | |||
| if err = models.CreateComment(ctx.User.Id, ctx.Repo.Repository.Id, issue.Id, 0, 0, models.IT_PLAIN, content); err != nil { | |||
| if err = models.CreateComment(ctx.User.Id, ctx.Repo.Repository.Id, issue.Id, 0, 0, models.COMMENT, content); err != nil { | |||
| ctx.Handle(500, "issue.Comment(create comment)", err) | |||
| return | |||
| } | |||
| @@ -49,6 +49,7 @@ | |||
| </div> | |||
| </div> | |||
| {{range .Comments}} | |||
| {{/* 0 = COMMENT, 1 = REOPEN, 2 = CLOSE, 3 = ISSUE, 4 = COMMIT, 5 = PULL */}} | |||
| {{if eq .Type 0}} | |||
| <div class="issue-child" id="issue-comment-{{.Id}}"> | |||
| <a class="user pull-left" href="/user/{{.Poster.Name}}"><img class="avatar" src="{{.Poster.AvatarLink}}" alt=""/></a> | |||
| @@ -78,6 +79,17 @@ | |||
| <a class="user pull-left" href="/user/{{.Poster.Name}}">{{.Poster.Name}}</a> <span class="label label-danger">Closed</span> this issue <span class="time">{{TimeSince .Created}}</span> | |||
| </div> | |||
| </div> | |||
| {{else if eq .Type 4}} | |||
| <div class="issue-child issue-reference issue-reference-commit"> | |||
| <a class="user pull-left" href="/user/{{.Poster.Name}}"><img class="avatar" src="{{.Poster.AvatarLink}}" alt=""/></a> | |||
| <div class="issue-content"> | |||
| <a class="user pull-left" href="/user/{{.Poster.Name}}">{{.Poster.Name}}</a> <span class="label label-primary">Referenced</span> this issue <span class="time">{{TimeSince .Created}}</span> | |||
| <p> | |||
| <a class="user pull-left" href="/user/{{.Poster.Name}}"><img class="avatar" src="{{.Poster.AvatarLink}}" alt=""/></a> | |||
| {{.ContentHtml}} | |||
| </p> | |||
| </div> | |||
| </div> | |||
| {{end}} | |||
| {{end}} | |||
| <hr class="issue-line"/> | |||