| @@ -8,6 +8,7 @@ import ( | |||
| "encoding/json" | |||
| "errors" | |||
| "fmt" | |||
| "regexp" | |||
| "strings" | |||
| "time" | |||
| @@ -32,6 +33,20 @@ const ( | |||
| OP_COMMENT_ISSUE | |||
| ) | |||
| var ( | |||
| ErrNotImplemented = errors.New("Not implemented yet") | |||
| ) | |||
| var ( | |||
| // Same as Github. See https://help.github.com/articles/closing-issues-via-commit-messages | |||
| IssueKeywords = []string{"close", "closes", "closed", "fix", "fixes", "fixed", "resolve", "resolves", "resolved"} | |||
| IssueKeywordsPat *regexp.Regexp | |||
| ) | |||
| func init() { | |||
| IssueKeywordsPat = regexp.MustCompile(fmt.Sprintf(`(?i)(?:%s) \S+`, strings.Join(IssueKeywords, "|"))) | |||
| } | |||
| // Action represents user operation type and other information to repository., | |||
| // it implemented interface base.Actioner so that can be used in template render. | |||
| type Action struct { | |||
| @@ -78,6 +93,52 @@ func (a Action) GetContent() string { | |||
| return a.Content | |||
| } | |||
| func updateIssuesCommit(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:] | |||
| if len(ref) == 0 { | |||
| continue | |||
| } | |||
| // Add repo name if missing | |||
| if ref[0] == '#' { | |||
| ref = fmt.Sprintf("%s/%s%s", repoUserName, repoName, ref) | |||
| } else if strings.Contains(ref, "/") == false { | |||
| // We don't support User#ID syntax yet | |||
| // return ErrNotImplemented | |||
| continue | |||
| } | |||
| issue, err := GetIssueByRef(ref) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| if issue.IsClosed { | |||
| continue | |||
| } | |||
| issue.IsClosed = true | |||
| if err = UpdateIssue(issue); err != nil { | |||
| return err | |||
| } | |||
| if err = ChangeMilestoneIssueStats(issue); err != nil { | |||
| return err | |||
| } | |||
| } | |||
| } | |||
| return nil | |||
| } | |||
| // CommitRepoAction adds new action for committing repository. | |||
| func CommitRepoAction(userId, repoUserId int64, userName, actEmail string, | |||
| repoId int64, repoUserName, repoName string, refFullName string, commit *base.PushCommits) error { | |||
| @@ -107,6 +168,12 @@ func CommitRepoAction(userId, repoUserId int64, userName, actEmail string, | |||
| return errors.New("action.CommitRepoAction(UpdateRepository): " + err.Error()) | |||
| } | |||
| err = updateIssuesCommit(repoUserName, repoName, commit.Commits) | |||
| if err != nil { | |||
| log.Debug("action.CommitRepoAction(updateIssuesCommit): ", err) | |||
| } | |||
| if err = NotifyWatchers(&Action{ActUserId: userId, ActUserName: userName, ActEmail: actEmail, | |||
| OpType: opType, Content: string(bs), RepoId: repoId, RepoUserName: repoUserName, | |||
| RepoName: repoName, RefName: refName, | |||
| @@ -7,6 +7,7 @@ package models | |||
| import ( | |||
| "bytes" | |||
| "errors" | |||
| "strconv" | |||
| "strings" | |||
| "time" | |||
| @@ -16,10 +17,11 @@ import ( | |||
| ) | |||
| var ( | |||
| ErrIssueNotExist = errors.New("Issue does not exist") | |||
| ErrLabelNotExist = errors.New("Label does not exist") | |||
| ErrMilestoneNotExist = errors.New("Milestone does not exist") | |||
| ErrWrongIssueCounter = errors.New("Invalid number of issues for this milestone") | |||
| ErrIssueNotExist = errors.New("Issue does not exist") | |||
| ErrLabelNotExist = errors.New("Label does not exist") | |||
| ErrMilestoneNotExist = errors.New("Milestone does not exist") | |||
| ErrWrongIssueCounter = errors.New("Invalid number of issues for this milestone") | |||
| ErrMissingIssueNumber = errors.New("No issue number specified") | |||
| ) | |||
| // Issue represents an issue or pull request of repository. | |||
| @@ -122,6 +124,29 @@ func NewIssue(issue *Issue) (err error) { | |||
| return | |||
| } | |||
| // GetIssueByRef returns an Issue specified by a GFM reference. | |||
| // See https://help.github.com/articles/writing-on-github#references for more information on the syntax. | |||
| func GetIssueByRef(ref string) (issue *Issue, err error) { | |||
| var issueNumber int64 | |||
| var repo *Repository | |||
| n := strings.IndexByte(ref, byte('#')) | |||
| if n == -1 { | |||
| return nil, ErrMissingIssueNumber | |||
| } | |||
| if issueNumber, err = strconv.ParseInt(ref[n+1:], 10, 64); err != nil { | |||
| return | |||
| } | |||
| if repo, err = GetRepositoryByRef(ref[:n]); err != nil { | |||
| return | |||
| } | |||
| return GetIssueByIndex(repo.Id, issueNumber) | |||
| } | |||
| // GetIssueByIndex returns issue by given index in repository. | |||
| func GetIssueByIndex(rid, index int64) (*Issue, error) { | |||
| issue := &Issue{RepoId: rid, Index: index} | |||
| @@ -400,6 +425,11 @@ func GetUserIssueStats(uid int64, filterMode int) *IssueStats { | |||
| // UpdateIssue updates information of issue. | |||
| func UpdateIssue(issue *Issue) error { | |||
| _, err := x.Id(issue.Id).AllCols().Update(issue) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| return err | |||
| } | |||
| @@ -670,6 +700,32 @@ func ChangeMilestoneStatus(m *Milestone, isClosed bool) (err error) { | |||
| return sess.Commit() | |||
| } | |||
| // ChangeMilestoneIssueStats updates the open/closed issues counter and progress for the | |||
| // milestone associated witht the given issue. | |||
| func ChangeMilestoneIssueStats(issue *Issue) error { | |||
| if issue.MilestoneId == 0 { | |||
| return nil | |||
| } | |||
| m, err := GetMilestoneById(issue.MilestoneId) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| if issue.IsClosed { | |||
| m.NumOpenIssues-- | |||
| m.NumClosedIssues++ | |||
| } else { | |||
| m.NumOpenIssues++ | |||
| m.NumClosedIssues-- | |||
| } | |||
| m.Completeness = m.NumClosedIssues * 100 / m.NumIssues | |||
| return UpdateMilestone(m) | |||
| } | |||
| // ChangeMilestoneAssign changes assignment of milestone for issue. | |||
| func ChangeMilestoneAssign(oldMid, mid int64, issue *Issue) (err error) { | |||
| sess := x.NewSession() | |||
| @@ -693,6 +749,7 @@ func ChangeMilestoneAssign(oldMid, mid int64, issue *Issue) (err error) { | |||
| } else { | |||
| m.Completeness = 0 | |||
| } | |||
| if _, err = sess.Id(m.Id).Update(m); err != nil { | |||
| sess.Rollback() | |||
| return err | |||
| @@ -710,6 +767,7 @@ func ChangeMilestoneAssign(oldMid, mid int64, issue *Issue) (err error) { | |||
| if err != nil { | |||
| return err | |||
| } | |||
| m.NumIssues++ | |||
| if issue.IsClosed { | |||
| m.NumClosedIssues++ | |||
| @@ -731,6 +789,7 @@ func ChangeMilestoneAssign(oldMid, mid int64, issue *Issue) (err error) { | |||
| return err | |||
| } | |||
| } | |||
| return sess.Commit() | |||
| } | |||
| @@ -7,9 +7,9 @@ package models | |||
| import ( | |||
| "errors" | |||
| "fmt" | |||
| "io/ioutil" | |||
| "html" | |||
| "html/template" | |||
| "io/ioutil" | |||
| "os" | |||
| "path" | |||
| "path/filepath" | |||
| @@ -43,6 +43,7 @@ var ( | |||
| ErrRepoNameIllegal = errors.New("Repository name contains illegal characters") | |||
| ErrRepoFileNotLoaded = errors.New("Repository file not loaded") | |||
| ErrMirrorNotExist = errors.New("Mirror does not exist") | |||
| ErrInvalidReference = errors.New("Invalid reference specified") | |||
| ) | |||
| var ( | |||
| @@ -837,6 +838,26 @@ func DeleteRepository(userId, repoId int64, userName string) error { | |||
| return sess.Commit() | |||
| } | |||
| // GetRepositoryByRef returns a Repository specified by a GFM reference. | |||
| // See https://help.github.com/articles/writing-on-github#references for more information on the syntax. | |||
| func GetRepositoryByRef(ref string) (*Repository, error) { | |||
| n := strings.IndexByte(ref, byte('/')) | |||
| if n < 2 { | |||
| return nil, ErrInvalidReference | |||
| } | |||
| userName, repoName := ref[:n], ref[n+1:] | |||
| user, err := GetUserByName(userName) | |||
| if err != nil { | |||
| return nil, err | |||
| } | |||
| return GetRepositoryByName(user.Id, repoName) | |||
| } | |||
| // GetRepositoryByName returns the repository by given name under user if exists. | |||
| func GetRepositoryByName(userId int64, repoName string) (*Repository, error) { | |||
| repo := &Repository{ | |||
| @@ -1017,4 +1038,4 @@ func IsWatching(uid, rid int64) bool { | |||
| func ForkRepository(repoName string, uid int64) { | |||
| } | |||
| } | |||
| @@ -644,24 +644,8 @@ func Comment(ctx *middleware.Context, params martini.Params) { | |||
| // Change open/closed issue counter for the associated milestone | |||
| if issue.MilestoneId > 0 { | |||
| l, err := models.GetMilestoneById(issue.MilestoneId) | |||
| if err != nil { | |||
| ctx.Handle(500, "issue.Comment(GetLabelById)", err) | |||
| return | |||
| } | |||
| if issue.IsClosed { | |||
| l.NumOpenIssues = l.NumOpenIssues - 1 | |||
| l.NumClosedIssues = l.NumClosedIssues + 1 | |||
| } else { | |||
| l.NumOpenIssues = l.NumOpenIssues + 1 | |||
| l.NumClosedIssues = l.NumClosedIssues - 1 | |||
| } | |||
| if err = models.UpdateMilestone(l); err != nil { | |||
| ctx.Handle(500, "issue.Comment(UpdateLabel)", err) | |||
| return | |||
| if err = models.ChangeMilestoneIssueStats(issue); err != nil { | |||
| ctx.Handle(500, "issue.Comment(ChangeMilestoneIssueStats)", err) | |||
| } | |||
| } | |||