Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add spent time to referenced issue in commit message #12220

Merged
merged 3 commits into from
Sep 4, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 27 additions & 2 deletions docs/content/doc/usage/linked-references.en-us.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ Example:
This is also valid for teams and organizations:

> [@Documenters](#), we need to plan for this.

> [@CoolCompanyInc](#), this issue concerns us all!

Teams will receive mail notifications when appropriate, but whole organizations won't.
Expand Down Expand Up @@ -123,6 +122,33 @@ The default _keywords_ are:
* **Closing**: close, closes, closed, fix, fixes, fixed, resolve, resolves, resolved
* **Reopening**: reopen, reopens, reopened

## Time tracking in Pull Requests and Commit Messages

When commit or merging of pull request results in automatic closing of issue
it is possible to also add spent time resolving this issue through commit message.

To specify spent time on resolving issue you need to specify time in format
`@<number><time-unit>` after issue number. In one commit message you can specify
multiple fixed issues and spent time for each of them.

Supported time units (`<time-unit>`):

* `m` - minutes
* `h` - hours
* `d` - days (equals to 8 hours)
* `w` - weeks (equals to 5 days)
* `mo` - months (equals to 4 weeks)

Numbers to specify time (`<number>`) can be also decimal numbers, ex. `@1.5h` would
result in one and half hours. Multiple time units can be combined, ex. `@1h10m` would
mean 1 hour and 10 minutes.

Example of commit message:

> Fixed #123 spent @1h, refs #102, fixes #124 @1.5h

This would result in 1 hour added to issue #123 and 1 and half hours added to issue #124.

## External Trackers

Gitea supports the use of external issue trackers, and references to issues
Expand All @@ -132,7 +158,6 @@ the pull requests hosted in Gitea. To address this, Gitea allows the use of
the `!` marker to identify pull requests. For example:

> This is issue [#1234](#), and links to the external tracker.

> This is pull request [!1234](#), and links to a pull request in Gitea.

The `!` and `#` can be used interchangeably for issues and pull request _except_
Expand Down
53 changes: 45 additions & 8 deletions modules/references/references.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ var (
crossReferenceIssueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([0-9a-zA-Z-_\.]+/[0-9a-zA-Z-_\.]+[#!][0-9]+)(?:\s|$|\)|\]|[:;,.?!]\s|[:;,.?!]$)`)
// spaceTrimmedPattern let's us find the trailing space
spaceTrimmedPattern = regexp.MustCompile(`(?:.*[0-9a-zA-Z-_])\s`)
// timeLogPattern matches string for time tracking
timeLogPattern = regexp.MustCompile(`(?:\s|^|\(|\[)(@([0-9]+([\.,][0-9]+)?(w|d|m|h))+)(?:\s|$|\)|\]|[:;,.?!]\s|[:;,.?!]$)`)

issueCloseKeywordsPat, issueReopenKeywordsPat *regexp.Regexp
issueKeywordsOnce sync.Once
Expand All @@ -62,10 +64,11 @@ const (

// IssueReference contains an unverified cross-reference to a local issue or pull request
type IssueReference struct {
Index int64
Owner string
Name string
Action XRefAction
Index int64
Owner string
Name string
Action XRefAction
TimeLog string
}

// RenderizableReference contains an unverified cross-reference to with rendering information
Expand All @@ -91,16 +94,18 @@ type rawReference struct {
issue string
refLocation *RefSpan
actionLocation *RefSpan
timeLog string
}

func rawToIssueReferenceList(reflist []*rawReference) []IssueReference {
refarr := make([]IssueReference, len(reflist))
for i, r := range reflist {
refarr[i] = IssueReference{
Index: r.index,
Owner: r.owner,
Name: r.name,
Action: r.action,
Index: r.index,
Owner: r.owner,
Name: r.name,
Action: r.action,
TimeLog: r.timeLog,
}
}
return refarr
Expand Down Expand Up @@ -386,6 +391,38 @@ func findAllIssueReferencesBytes(content []byte, links []string) []*rawReference
}
}

if len(ret) == 0 {
return ret
}

pos = 0

for {
match := timeLogPattern.FindSubmatchIndex(content[pos:])
if match == nil {
break
}

timeLogEntry := string(content[match[2]+pos+1 : match[3]+pos])

var f *rawReference
for _, ref := range ret {
if ref.refLocation != nil && ref.refLocation.End < match[2]+pos && (f == nil || f.refLocation.End < ref.refLocation.End) {
f = ref
}
}

pos = match[1] + pos

if f == nil {
f = ret[0]
}

if len(f.timeLog) == 0 {
f.timeLog = timeLogEntry
}
}

return ret
}

Expand Down
70 changes: 40 additions & 30 deletions modules/references/references_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type testResult struct {
Action XRefAction
RefLocation *RefSpan
ActionLocation *RefSpan
TimeLog string
}

func TestFindAllIssueReferences(t *testing.T) {
Expand All @@ -34,19 +35,19 @@ func TestFindAllIssueReferences(t *testing.T) {
{
"Simply closes: #29 yes",
[]testResult{
{29, "", "", "29", false, XRefActionCloses, &RefSpan{Start: 15, End: 18}, &RefSpan{Start: 7, End: 13}},
{29, "", "", "29", false, XRefActionCloses, &RefSpan{Start: 15, End: 18}, &RefSpan{Start: 7, End: 13}, ""},
},
},
{
"Simply closes: !29 yes",
[]testResult{
{29, "", "", "29", true, XRefActionCloses, &RefSpan{Start: 15, End: 18}, &RefSpan{Start: 7, End: 13}},
{29, "", "", "29", true, XRefActionCloses, &RefSpan{Start: 15, End: 18}, &RefSpan{Start: 7, End: 13}, ""},
},
},
{
" #124 yes, this is a reference.",
[]testResult{
{124, "", "", "124", false, XRefActionNone, &RefSpan{Start: 0, End: 4}, nil},
{124, "", "", "124", false, XRefActionNone, &RefSpan{Start: 0, End: 4}, nil, ""},
},
},
{
Expand All @@ -60,13 +61,13 @@ func TestFindAllIssueReferences(t *testing.T) {
{
"This user3/repo4#200 yes.",
[]testResult{
{200, "user3", "repo4", "200", false, XRefActionNone, &RefSpan{Start: 5, End: 20}, nil},
{200, "user3", "repo4", "200", false, XRefActionNone, &RefSpan{Start: 5, End: 20}, nil, ""},
},
},
{
"This user3/repo4!200 yes.",
[]testResult{
{200, "user3", "repo4", "200", true, XRefActionNone, &RefSpan{Start: 5, End: 20}, nil},
{200, "user3", "repo4", "200", true, XRefActionNone, &RefSpan{Start: 5, End: 20}, nil, ""},
},
},
{
Expand All @@ -76,19 +77,19 @@ func TestFindAllIssueReferences(t *testing.T) {
{
"This [two](/user2/repo1/issues/921) yes.",
[]testResult{
{921, "user2", "repo1", "921", false, XRefActionNone, nil, nil},
{921, "user2", "repo1", "921", false, XRefActionNone, nil, nil, ""},
},
},
{
"This [three](/user2/repo1/pulls/922) yes.",
[]testResult{
{922, "user2", "repo1", "922", true, XRefActionNone, nil, nil},
{922, "user2", "repo1", "922", true, XRefActionNone, nil, nil, ""},
},
},
{
"This [four](http://gitea.com:3000/user3/repo4/issues/203) yes.",
[]testResult{
{203, "user3", "repo4", "203", false, XRefActionNone, nil, nil},
{203, "user3", "repo4", "203", false, XRefActionNone, nil, nil, ""},
},
},
{
Expand All @@ -102,49 +103,49 @@ func TestFindAllIssueReferences(t *testing.T) {
{
"This http://gitea.com:3000/user4/repo5/pulls/202 yes.",
[]testResult{
{202, "user4", "repo5", "202", true, XRefActionNone, nil, nil},
{202, "user4", "repo5", "202", true, XRefActionNone, nil, nil, ""},
},
},
{
"This http://GiTeA.COM:3000/user4/repo6/pulls/205 yes.",
[]testResult{
{205, "user4", "repo6", "205", true, XRefActionNone, nil, nil},
{205, "user4", "repo6", "205", true, XRefActionNone, nil, nil, ""},
},
},
{
"Reopens #15 yes",
[]testResult{
{15, "", "", "15", false, XRefActionReopens, &RefSpan{Start: 8, End: 11}, &RefSpan{Start: 0, End: 7}},
{15, "", "", "15", false, XRefActionReopens, &RefSpan{Start: 8, End: 11}, &RefSpan{Start: 0, End: 7}, ""},
},
},
{
"This closes #20 for you yes",
[]testResult{
{20, "", "", "20", false, XRefActionCloses, &RefSpan{Start: 12, End: 15}, &RefSpan{Start: 5, End: 11}},
{20, "", "", "20", false, XRefActionCloses, &RefSpan{Start: 12, End: 15}, &RefSpan{Start: 5, End: 11}, ""},
},
},
{
"Do you fix user6/repo6#300 ? yes",
[]testResult{
{300, "user6", "repo6", "300", false, XRefActionCloses, &RefSpan{Start: 11, End: 26}, &RefSpan{Start: 7, End: 10}},
{300, "user6", "repo6", "300", false, XRefActionCloses, &RefSpan{Start: 11, End: 26}, &RefSpan{Start: 7, End: 10}, ""},
},
},
{
"For 999 #1235 no keyword, but yes",
[]testResult{
{1235, "", "", "1235", false, XRefActionNone, &RefSpan{Start: 8, End: 13}, nil},
{1235, "", "", "1235", false, XRefActionNone, &RefSpan{Start: 8, End: 13}, nil, ""},
},
},
{
"For [!123] yes",
[]testResult{
{123, "", "", "123", true, XRefActionNone, &RefSpan{Start: 5, End: 9}, nil},
{123, "", "", "123", true, XRefActionNone, &RefSpan{Start: 5, End: 9}, nil, ""},
},
},
{
"For (#345) yes",
[]testResult{
{345, "", "", "345", false, XRefActionNone, &RefSpan{Start: 5, End: 9}, nil},
{345, "", "", "345", false, XRefActionNone, &RefSpan{Start: 5, End: 9}, nil, ""},
},
},
{
Expand All @@ -154,31 +155,39 @@ func TestFindAllIssueReferences(t *testing.T) {
{
"For #24, and #25. yes; also #26; #27? #28! and #29: should",
[]testResult{
{24, "", "", "24", false, XRefActionNone, &RefSpan{Start: 4, End: 7}, nil},
{25, "", "", "25", false, XRefActionNone, &RefSpan{Start: 13, End: 16}, nil},
{26, "", "", "26", false, XRefActionNone, &RefSpan{Start: 28, End: 31}, nil},
{27, "", "", "27", false, XRefActionNone, &RefSpan{Start: 33, End: 36}, nil},
{28, "", "", "28", false, XRefActionNone, &RefSpan{Start: 38, End: 41}, nil},
{29, "", "", "29", false, XRefActionNone, &RefSpan{Start: 47, End: 50}, nil},
{24, "", "", "24", false, XRefActionNone, &RefSpan{Start: 4, End: 7}, nil, ""},
{25, "", "", "25", false, XRefActionNone, &RefSpan{Start: 13, End: 16}, nil, ""},
{26, "", "", "26", false, XRefActionNone, &RefSpan{Start: 28, End: 31}, nil, ""},
{27, "", "", "27", false, XRefActionNone, &RefSpan{Start: 33, End: 36}, nil, ""},
{28, "", "", "28", false, XRefActionNone, &RefSpan{Start: 38, End: 41}, nil, ""},
{29, "", "", "29", false, XRefActionNone, &RefSpan{Start: 47, End: 50}, nil, ""},
},
},
{
"This user3/repo4#200, yes.",
[]testResult{
{200, "user3", "repo4", "200", false, XRefActionNone, &RefSpan{Start: 5, End: 20}, nil},
{200, "user3", "repo4", "200", false, XRefActionNone, &RefSpan{Start: 5, End: 20}, nil, ""},
},
},
{
"Which abc. #9434 same as above",
[]testResult{
{9434, "", "", "9434", false, XRefActionNone, &RefSpan{Start: 11, End: 16}, nil},
{9434, "", "", "9434", false, XRefActionNone, &RefSpan{Start: 11, End: 16}, nil, ""},
},
},
{
"This closes #600 and reopens #599",
[]testResult{
{600, "", "", "600", false, XRefActionCloses, &RefSpan{Start: 12, End: 16}, &RefSpan{Start: 5, End: 11}},
{599, "", "", "599", false, XRefActionReopens, &RefSpan{Start: 29, End: 33}, &RefSpan{Start: 21, End: 28}},
{600, "", "", "600", false, XRefActionCloses, &RefSpan{Start: 12, End: 16}, &RefSpan{Start: 5, End: 11}, ""},
{599, "", "", "599", false, XRefActionReopens, &RefSpan{Start: 29, End: 33}, &RefSpan{Start: 21, End: 28}, ""},
},
},
{
"This fixes #100 spent @40m and reopens #101, also fixes #102 spent @4h15m",
[]testResult{
{100, "", "", "100", false, XRefActionCloses, &RefSpan{Start: 11, End: 15}, &RefSpan{Start: 5, End: 10}, "40m"},
{101, "", "", "101", false, XRefActionReopens, &RefSpan{Start: 39, End: 43}, &RefSpan{Start: 31, End: 38}, ""},
{102, "", "", "102", false, XRefActionCloses, &RefSpan{Start: 56, End: 60}, &RefSpan{Start: 50, End: 55}, "4h15m"},
},
},
}
Expand Down Expand Up @@ -237,6 +246,7 @@ func testFixtures(t *testing.T, fixtures []testFixture, context string) {
issue: e.Issue,
refLocation: e.RefLocation,
actionLocation: e.ActionLocation,
timeLog: e.TimeLog,
}
}
expref := rawToIssueReferenceList(expraw)
Expand Down Expand Up @@ -382,25 +392,25 @@ func TestCustomizeCloseKeywords(t *testing.T) {
{
"Simplemente cierra: #29 yes",
[]testResult{
{29, "", "", "29", false, XRefActionCloses, &RefSpan{Start: 20, End: 23}, &RefSpan{Start: 12, End: 18}},
{29, "", "", "29", false, XRefActionCloses, &RefSpan{Start: 20, End: 23}, &RefSpan{Start: 12, End: 18}, ""},
},
},
{
"Closes: #123 no, this English.",
[]testResult{
{123, "", "", "123", false, XRefActionNone, &RefSpan{Start: 8, End: 12}, nil},
{123, "", "", "123", false, XRefActionNone, &RefSpan{Start: 8, End: 12}, nil, ""},
},
},
{
"Cerró user6/repo6#300 yes",
[]testResult{
{300, "user6", "repo6", "300", false, XRefActionCloses, &RefSpan{Start: 7, End: 22}, &RefSpan{Start: 0, End: 6}},
{300, "user6", "repo6", "300", false, XRefActionCloses, &RefSpan{Start: 7, End: 22}, &RefSpan{Start: 0, End: 6}, ""},
},
},
{
"Reabre user3/repo4#200 yes",
[]testResult{
{200, "user3", "repo4", "200", false, XRefActionReopens, &RefSpan{Start: 7, End: 22}, &RefSpan{Start: 0, End: 6}},
{200, "user3", "repo4", "200", false, XRefActionReopens, &RefSpan{Start: 7, End: 22}, &RefSpan{Start: 0, End: 6}, ""},
},
},
}
Expand Down
Loading