@@ -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) | |||
} | |||
} | |||