Skip to content

Commit e710a34

Browse files
authored
Add spent time to referenced issue in commit message (#12220)
1 parent 4c557ef commit e710a34

File tree

4 files changed

+184
-40
lines changed

4 files changed

+184
-40
lines changed

docs/content/doc/usage/linked-references.en-us.md

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ Example:
4242
This is also valid for teams and organizations:
4343

4444
> [@Documenters](#), we need to plan for this.
45-
4645
> [@CoolCompanyInc](#), this issue concerns us all!
4746
4847
Teams will receive mail notifications when appropriate, but whole organizations won't.
@@ -123,6 +122,33 @@ The default _keywords_ are:
123122
* **Closing**: close, closes, closed, fix, fixes, fixed, resolve, resolves, resolved
124123
* **Reopening**: reopen, reopens, reopened
125124

125+
## Time tracking in Pull Requests and Commit Messages
126+
127+
When commit or merging of pull request results in automatic closing of issue
128+
it is possible to also add spent time resolving this issue through commit message.
129+
130+
To specify spent time on resolving issue you need to specify time in format
131+
`@<number><time-unit>` after issue number. In one commit message you can specify
132+
multiple fixed issues and spent time for each of them.
133+
134+
Supported time units (`<time-unit>`):
135+
136+
* `m` - minutes
137+
* `h` - hours
138+
* `d` - days (equals to 8 hours)
139+
* `w` - weeks (equals to 5 days)
140+
* `mo` - months (equals to 4 weeks)
141+
142+
Numbers to specify time (`<number>`) can be also decimal numbers, ex. `@1.5h` would
143+
result in one and half hours. Multiple time units can be combined, ex. `@1h10m` would
144+
mean 1 hour and 10 minutes.
145+
146+
Example of commit message:
147+
148+
> Fixed #123 spent @1h, refs #102, fixes #124 @1.5h
149+
150+
This would result in 1 hour added to issue #123 and 1 and half hours added to issue #124.
151+
126152
## External Trackers
127153

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

134160
> This is issue [#1234](#), and links to the external tracker.
135-
136161
> This is pull request [!1234](#), and links to a pull request in Gitea.
137162
138163
The `!` and `#` can be used interchangeably for issues and pull request _except_

modules/references/references.go

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ var (
3737
crossReferenceIssueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([0-9a-zA-Z-_\.]+/[0-9a-zA-Z-_\.]+[#!][0-9]+)(?:\s|$|\)|\]|[:;,.?!]\s|[:;,.?!]$)`)
3838
// spaceTrimmedPattern let's us find the trailing space
3939
spaceTrimmedPattern = regexp.MustCompile(`(?:.*[0-9a-zA-Z-_])\s`)
40+
// timeLogPattern matches string for time tracking
41+
timeLogPattern = regexp.MustCompile(`(?:\s|^|\(|\[)(@([0-9]+([\.,][0-9]+)?(w|d|m|h))+)(?:\s|$|\)|\]|[:;,.?!]\s|[:;,.?!]$)`)
4042

4143
issueCloseKeywordsPat, issueReopenKeywordsPat *regexp.Regexp
4244
issueKeywordsOnce sync.Once
@@ -62,10 +64,11 @@ const (
6264

6365
// IssueReference contains an unverified cross-reference to a local issue or pull request
6466
type IssueReference struct {
65-
Index int64
66-
Owner string
67-
Name string
68-
Action XRefAction
67+
Index int64
68+
Owner string
69+
Name string
70+
Action XRefAction
71+
TimeLog string
6972
}
7073

7174
// RenderizableReference contains an unverified cross-reference to with rendering information
@@ -91,16 +94,18 @@ type rawReference struct {
9194
issue string
9295
refLocation *RefSpan
9396
actionLocation *RefSpan
97+
timeLog string
9498
}
9599

96100
func rawToIssueReferenceList(reflist []*rawReference) []IssueReference {
97101
refarr := make([]IssueReference, len(reflist))
98102
for i, r := range reflist {
99103
refarr[i] = IssueReference{
100-
Index: r.index,
101-
Owner: r.owner,
102-
Name: r.name,
103-
Action: r.action,
104+
Index: r.index,
105+
Owner: r.owner,
106+
Name: r.name,
107+
Action: r.action,
108+
TimeLog: r.timeLog,
104109
}
105110
}
106111
return refarr
@@ -386,6 +391,38 @@ func findAllIssueReferencesBytes(content []byte, links []string) []*rawReference
386391
}
387392
}
388393

394+
if len(ret) == 0 {
395+
return ret
396+
}
397+
398+
pos = 0
399+
400+
for {
401+
match := timeLogPattern.FindSubmatchIndex(content[pos:])
402+
if match == nil {
403+
break
404+
}
405+
406+
timeLogEntry := string(content[match[2]+pos+1 : match[3]+pos])
407+
408+
var f *rawReference
409+
for _, ref := range ret {
410+
if ref.refLocation != nil && ref.refLocation.End < match[2]+pos && (f == nil || f.refLocation.End < ref.refLocation.End) {
411+
f = ref
412+
}
413+
}
414+
415+
pos = match[1] + pos
416+
417+
if f == nil {
418+
f = ret[0]
419+
}
420+
421+
if len(f.timeLog) == 0 {
422+
f.timeLog = timeLogEntry
423+
}
424+
}
425+
389426
return ret
390427
}
391428

modules/references/references_test.go

Lines changed: 40 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ type testResult struct {
2626
Action XRefAction
2727
RefLocation *RefSpan
2828
ActionLocation *RefSpan
29+
TimeLog string
2930
}
3031

3132
func TestFindAllIssueReferences(t *testing.T) {
@@ -34,19 +35,19 @@ func TestFindAllIssueReferences(t *testing.T) {
3435
{
3536
"Simply closes: #29 yes",
3637
[]testResult{
37-
{29, "", "", "29", false, XRefActionCloses, &RefSpan{Start: 15, End: 18}, &RefSpan{Start: 7, End: 13}},
38+
{29, "", "", "29", false, XRefActionCloses, &RefSpan{Start: 15, End: 18}, &RefSpan{Start: 7, End: 13}, ""},
3839
},
3940
},
4041
{
4142
"Simply closes: !29 yes",
4243
[]testResult{
43-
{29, "", "", "29", true, XRefActionCloses, &RefSpan{Start: 15, End: 18}, &RefSpan{Start: 7, End: 13}},
44+
{29, "", "", "29", true, XRefActionCloses, &RefSpan{Start: 15, End: 18}, &RefSpan{Start: 7, End: 13}, ""},
4445
},
4546
},
4647
{
4748
" #124 yes, this is a reference.",
4849
[]testResult{
49-
{124, "", "", "124", false, XRefActionNone, &RefSpan{Start: 0, End: 4}, nil},
50+
{124, "", "", "124", false, XRefActionNone, &RefSpan{Start: 0, End: 4}, nil, ""},
5051
},
5152
},
5253
{
@@ -60,13 +61,13 @@ func TestFindAllIssueReferences(t *testing.T) {
6061
{
6162
"This user3/repo4#200 yes.",
6263
[]testResult{
63-
{200, "user3", "repo4", "200", false, XRefActionNone, &RefSpan{Start: 5, End: 20}, nil},
64+
{200, "user3", "repo4", "200", false, XRefActionNone, &RefSpan{Start: 5, End: 20}, nil, ""},
6465
},
6566
},
6667
{
6768
"This user3/repo4!200 yes.",
6869
[]testResult{
69-
{200, "user3", "repo4", "200", true, XRefActionNone, &RefSpan{Start: 5, End: 20}, nil},
70+
{200, "user3", "repo4", "200", true, XRefActionNone, &RefSpan{Start: 5, End: 20}, nil, ""},
7071
},
7172
},
7273
{
@@ -76,19 +77,19 @@ func TestFindAllIssueReferences(t *testing.T) {
7677
{
7778
"This [two](/user2/repo1/issues/921) yes.",
7879
[]testResult{
79-
{921, "user2", "repo1", "921", false, XRefActionNone, nil, nil},
80+
{921, "user2", "repo1", "921", false, XRefActionNone, nil, nil, ""},
8081
},
8182
},
8283
{
8384
"This [three](/user2/repo1/pulls/922) yes.",
8485
[]testResult{
85-
{922, "user2", "repo1", "922", true, XRefActionNone, nil, nil},
86+
{922, "user2", "repo1", "922", true, XRefActionNone, nil, nil, ""},
8687
},
8788
},
8889
{
8990
"This [four](http://gitea.com:3000/user3/repo4/issues/203) yes.",
9091
[]testResult{
91-
{203, "user3", "repo4", "203", false, XRefActionNone, nil, nil},
92+
{203, "user3", "repo4", "203", false, XRefActionNone, nil, nil, ""},
9293
},
9394
},
9495
{
@@ -102,49 +103,49 @@ func TestFindAllIssueReferences(t *testing.T) {
102103
{
103104
"This http://gitea.com:3000/user4/repo5/pulls/202 yes.",
104105
[]testResult{
105-
{202, "user4", "repo5", "202", true, XRefActionNone, nil, nil},
106+
{202, "user4", "repo5", "202", true, XRefActionNone, nil, nil, ""},
106107
},
107108
},
108109
{
109110
"This http://GiTeA.COM:3000/user4/repo6/pulls/205 yes.",
110111
[]testResult{
111-
{205, "user4", "repo6", "205", true, XRefActionNone, nil, nil},
112+
{205, "user4", "repo6", "205", true, XRefActionNone, nil, nil, ""},
112113
},
113114
},
114115
{
115116
"Reopens #15 yes",
116117
[]testResult{
117-
{15, "", "", "15", false, XRefActionReopens, &RefSpan{Start: 8, End: 11}, &RefSpan{Start: 0, End: 7}},
118+
{15, "", "", "15", false, XRefActionReopens, &RefSpan{Start: 8, End: 11}, &RefSpan{Start: 0, End: 7}, ""},
118119
},
119120
},
120121
{
121122
"This closes #20 for you yes",
122123
[]testResult{
123-
{20, "", "", "20", false, XRefActionCloses, &RefSpan{Start: 12, End: 15}, &RefSpan{Start: 5, End: 11}},
124+
{20, "", "", "20", false, XRefActionCloses, &RefSpan{Start: 12, End: 15}, &RefSpan{Start: 5, End: 11}, ""},
124125
},
125126
},
126127
{
127128
"Do you fix user6/repo6#300 ? yes",
128129
[]testResult{
129-
{300, "user6", "repo6", "300", false, XRefActionCloses, &RefSpan{Start: 11, End: 26}, &RefSpan{Start: 7, End: 10}},
130+
{300, "user6", "repo6", "300", false, XRefActionCloses, &RefSpan{Start: 11, End: 26}, &RefSpan{Start: 7, End: 10}, ""},
130131
},
131132
},
132133
{
133134
"For 999 #1235 no keyword, but yes",
134135
[]testResult{
135-
{1235, "", "", "1235", false, XRefActionNone, &RefSpan{Start: 8, End: 13}, nil},
136+
{1235, "", "", "1235", false, XRefActionNone, &RefSpan{Start: 8, End: 13}, nil, ""},
136137
},
137138
},
138139
{
139140
"For [!123] yes",
140141
[]testResult{
141-
{123, "", "", "123", true, XRefActionNone, &RefSpan{Start: 5, End: 9}, nil},
142+
{123, "", "", "123", true, XRefActionNone, &RefSpan{Start: 5, End: 9}, nil, ""},
142143
},
143144
},
144145
{
145146
"For (#345) yes",
146147
[]testResult{
147-
{345, "", "", "345", false, XRefActionNone, &RefSpan{Start: 5, End: 9}, nil},
148+
{345, "", "", "345", false, XRefActionNone, &RefSpan{Start: 5, End: 9}, nil, ""},
148149
},
149150
},
150151
{
@@ -154,31 +155,39 @@ func TestFindAllIssueReferences(t *testing.T) {
154155
{
155156
"For #24, and #25. yes; also #26; #27? #28! and #29: should",
156157
[]testResult{
157-
{24, "", "", "24", false, XRefActionNone, &RefSpan{Start: 4, End: 7}, nil},
158-
{25, "", "", "25", false, XRefActionNone, &RefSpan{Start: 13, End: 16}, nil},
159-
{26, "", "", "26", false, XRefActionNone, &RefSpan{Start: 28, End: 31}, nil},
160-
{27, "", "", "27", false, XRefActionNone, &RefSpan{Start: 33, End: 36}, nil},
161-
{28, "", "", "28", false, XRefActionNone, &RefSpan{Start: 38, End: 41}, nil},
162-
{29, "", "", "29", false, XRefActionNone, &RefSpan{Start: 47, End: 50}, nil},
158+
{24, "", "", "24", false, XRefActionNone, &RefSpan{Start: 4, End: 7}, nil, ""},
159+
{25, "", "", "25", false, XRefActionNone, &RefSpan{Start: 13, End: 16}, nil, ""},
160+
{26, "", "", "26", false, XRefActionNone, &RefSpan{Start: 28, End: 31}, nil, ""},
161+
{27, "", "", "27", false, XRefActionNone, &RefSpan{Start: 33, End: 36}, nil, ""},
162+
{28, "", "", "28", false, XRefActionNone, &RefSpan{Start: 38, End: 41}, nil, ""},
163+
{29, "", "", "29", false, XRefActionNone, &RefSpan{Start: 47, End: 50}, nil, ""},
163164
},
164165
},
165166
{
166167
"This user3/repo4#200, yes.",
167168
[]testResult{
168-
{200, "user3", "repo4", "200", false, XRefActionNone, &RefSpan{Start: 5, End: 20}, nil},
169+
{200, "user3", "repo4", "200", false, XRefActionNone, &RefSpan{Start: 5, End: 20}, nil, ""},
169170
},
170171
},
171172
{
172173
"Which abc. #9434 same as above",
173174
[]testResult{
174-
{9434, "", "", "9434", false, XRefActionNone, &RefSpan{Start: 11, End: 16}, nil},
175+
{9434, "", "", "9434", false, XRefActionNone, &RefSpan{Start: 11, End: 16}, nil, ""},
175176
},
176177
},
177178
{
178179
"This closes #600 and reopens #599",
179180
[]testResult{
180-
{600, "", "", "600", false, XRefActionCloses, &RefSpan{Start: 12, End: 16}, &RefSpan{Start: 5, End: 11}},
181-
{599, "", "", "599", false, XRefActionReopens, &RefSpan{Start: 29, End: 33}, &RefSpan{Start: 21, End: 28}},
181+
{600, "", "", "600", false, XRefActionCloses, &RefSpan{Start: 12, End: 16}, &RefSpan{Start: 5, End: 11}, ""},
182+
{599, "", "", "599", false, XRefActionReopens, &RefSpan{Start: 29, End: 33}, &RefSpan{Start: 21, End: 28}, ""},
183+
},
184+
},
185+
{
186+
"This fixes #100 spent @40m and reopens #101, also fixes #102 spent @4h15m",
187+
[]testResult{
188+
{100, "", "", "100", false, XRefActionCloses, &RefSpan{Start: 11, End: 15}, &RefSpan{Start: 5, End: 10}, "40m"},
189+
{101, "", "", "101", false, XRefActionReopens, &RefSpan{Start: 39, End: 43}, &RefSpan{Start: 31, End: 38}, ""},
190+
{102, "", "", "102", false, XRefActionCloses, &RefSpan{Start: 56, End: 60}, &RefSpan{Start: 50, End: 55}, "4h15m"},
182191
},
183192
},
184193
}
@@ -237,6 +246,7 @@ func testFixtures(t *testing.T, fixtures []testFixture, context string) {
237246
issue: e.Issue,
238247
refLocation: e.RefLocation,
239248
actionLocation: e.ActionLocation,
249+
timeLog: e.TimeLog,
240250
}
241251
}
242252
expref := rawToIssueReferenceList(expraw)
@@ -382,25 +392,25 @@ func TestCustomizeCloseKeywords(t *testing.T) {
382392
{
383393
"Simplemente cierra: #29 yes",
384394
[]testResult{
385-
{29, "", "", "29", false, XRefActionCloses, &RefSpan{Start: 20, End: 23}, &RefSpan{Start: 12, End: 18}},
395+
{29, "", "", "29", false, XRefActionCloses, &RefSpan{Start: 20, End: 23}, &RefSpan{Start: 12, End: 18}, ""},
386396
},
387397
},
388398
{
389399
"Closes: #123 no, this English.",
390400
[]testResult{
391-
{123, "", "", "123", false, XRefActionNone, &RefSpan{Start: 8, End: 12}, nil},
401+
{123, "", "", "123", false, XRefActionNone, &RefSpan{Start: 8, End: 12}, nil, ""},
392402
},
393403
},
394404
{
395405
"Cerró user6/repo6#300 yes",
396406
[]testResult{
397-
{300, "user6", "repo6", "300", false, XRefActionCloses, &RefSpan{Start: 7, End: 22}, &RefSpan{Start: 0, End: 6}},
407+
{300, "user6", "repo6", "300", false, XRefActionCloses, &RefSpan{Start: 7, End: 22}, &RefSpan{Start: 0, End: 6}, ""},
398408
},
399409
},
400410
{
401411
"Reabre user3/repo4#200 yes",
402412
[]testResult{
403-
{200, "user3", "repo4", "200", false, XRefActionReopens, &RefSpan{Start: 7, End: 22}, &RefSpan{Start: 0, End: 6}},
413+
{200, "user3", "repo4", "200", false, XRefActionReopens, &RefSpan{Start: 7, End: 22}, &RefSpan{Start: 0, End: 6}, ""},
404414
},
405415
},
406416
}

0 commit comments

Comments
 (0)