Skip to content

Commit 6808b57

Browse files
AlexanderBeynstrk
authored andcommitted
Implement custom regular expression for external issue tracking.
Signed-off-by: Alexander Beyn <[email protected]>
1 parent 90eb9fb commit 6808b57

File tree

11 files changed

+134
-19
lines changed

11 files changed

+134
-19
lines changed

models/repo.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,9 +492,13 @@ func (repo *Repository) ComposeMetas() map[string]string {
492492
switch unit.ExternalTrackerConfig().ExternalTrackerStyle {
493493
case markup.IssueNameStyleAlphanumeric:
494494
metas["style"] = markup.IssueNameStyleAlphanumeric
495+
case markup.IssueNameStyleRegexp:
496+
metas["style"] = markup.IssueNameStyleRegexp
495497
default:
496498
metas["style"] = markup.IssueNameStyleNumeric
497499
}
500+
metas["format"] = unit.ExternalTrackerConfig().ExternalTrackerFormat
501+
metas["regexp"] = unit.ExternalTrackerConfig().ExternalTrackerRegexpPattern
498502
}
499503

500504
repo.MustOwner()

models/repo_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ func TestMetas(t *testing.T) {
5757
externalTracker.ExternalTrackerConfig().ExternalTrackerStyle = markup.IssueNameStyleNumeric
5858
testSuccess(markup.IssueNameStyleNumeric)
5959

60+
externalTracker.ExternalTrackerConfig().ExternalTrackerStyle = markup.IssueNameStyleRegexp
61+
testSuccess(markup.IssueNameStyleRegexp)
62+
6063
repo, err := GetRepositoryByID(3)
6164
assert.NoError(t, err)
6265

@@ -65,6 +68,7 @@ func TestMetas(t *testing.T) {
6568
assert.Contains(t, metas, "teams")
6669
assert.Equal(t, "user3", metas["org"])
6770
assert.Equal(t, ",owners,team1,", metas["teams"])
71+
6872
}
6973

7074
func TestGetRepositoryCount(t *testing.T) {

models/repo_unit.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,10 @@ func (cfg *ExternalUncycloConfig) ToDB() ([]byte, error) {
6060

6161
// ExternalTrackerConfig describes external tracker config
6262
type ExternalTrackerConfig struct {
63-
ExternalTrackerURL string
64-
ExternalTrackerFormat string
65-
ExternalTrackerStyle string
63+
ExternalTrackerURL string
64+
ExternalTrackerFormat string
65+
ExternalTrackerStyle string
66+
ExternalTrackerRegexpPattern string
6667
}
6768

6869
// FromDB fills up a ExternalTrackerConfig from serialized format.

modules/markup/html.go

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import (
3333
const (
3434
IssueNameStyleNumeric = "numeric"
3535
IssueNameStyleAlphanumeric = "alphanumeric"
36+
IssueNameStyleRegexp = "regexp"
3637
)
3738

3839
var (
@@ -806,19 +807,35 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
806807
)
807808

808809
next := node.NextSibling
810+
811+
_, exttrack := ctx.Metas["format"]
812+
notNumericStyle := ctx.Metas["style"] != IssueNameStyleNumeric
813+
foundNumeric, refNumeric := references.FindRenderizableReferenceNumeric(node.Data, exttrack && notNumericStyle)
814+
809815
for node != nil && node != next {
810-
_, exttrack := ctx.Metas["format"]
811-
alphanum := ctx.Metas["style"] == IssueNameStyleAlphanumeric
816+
switch ctx.Metas["style"] {
817+
case IssueNameStyleNumeric:
818+
found = foundNumeric
819+
ref = refNumeric
820+
case IssueNameStyleAlphanumeric:
821+
found, ref := references.FindRenderizableReferenceAlphanumeric(node.Data)
822+
case IssueNameStyleRegexp:
823+
// TODO: Compile only once, at regexp definition time
824+
pattern, err := regexp.Compile(ctx.metas["regexp"])
825+
if err == nil {
826+
return
827+
}
828+
found, ref := references.FindRenderizableReferenceRegexp(node.Data, pattern)
829+
}
812830

813831
// Repos with external issue trackers might still need to reference local PRs
814832
// We need to concern with the first one that shows up in the text, whichever it is
815-
found, ref = references.FindRenderizableReferenceNumeric(node.Data, exttrack && alphanum)
816-
if exttrack && alphanum {
817-
if found2, ref2 := references.FindRenderizableReferenceAlphanumeric(node.Data); found2 {
818-
if !found || ref2.RefLocation.Start < ref.RefLocation.Start {
819-
found = true
820-
ref = ref2
821-
}
833+
if exttrack && notNumericStyle {
834+
// If numeric (PR) was found and it was BEFORE the notNumeric
835+
// pattern, use that
836+
if foundNumeric && refNumeric.RefLocation.Start < ref.RefLocation.Start {
837+
found = foundNumeric
838+
ref = refNumeric
822839
}
823840
}
824841
if !found {
@@ -853,7 +870,7 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
853870

854871
// Decorate action keywords if actionable
855872
var keyword *html.Node
856-
if references.IsXrefActionable(ref, exttrack, alphanum) {
873+
if references.IsXrefActionable(ref, exttrack) {
857874
keyword = createKeyword(node.Data[ref.ActionLocation.Start:ref.ActionLocation.End])
858875
} else {
859876
keyword = &html.Node{

modules/markup/html_internal_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,13 @@ var alphanumericMetas = map[string]string{
5252
"style": IssueNameStyleAlphanumeric,
5353
}
5454

55+
var regexpMetas = map[string]string{
56+
"format": "https://someurl.com/{user}/{repo}/{index}",
57+
"user": "someUser",
58+
"repo": "someRepo",
59+
"style": IssueNameStyleRegexp,
60+
}
61+
5562
// these values should match the Repo const above
5663
var localMetas = map[string]string{
5764
"user": "gogits",
@@ -195,6 +202,45 @@ func TestRender_IssueIndexPattern4(t *testing.T) {
195202
test("test issue ABCDEFGHIJ-1234567890", "test issue %s", "ABCDEFGHIJ-1234567890")
196203
}
197204

205+
func TestRender_IssueIndexPattern5(t *testing.T) {
206+
setting.AppURL = AppURL
207+
setting.AppSubURL = AppSubURL
208+
209+
// regexp: render inputs without valid mentions
210+
test := func(s, expectedFmt string, pattern string, ids []string, names []string) {
211+
metas := regexpMetas
212+
metas["regexp"] = pattern
213+
links := make([]interface{}, len(ids))
214+
for i, id := range ids {
215+
links[i] = link(util.URLJoin("https://someurl.com/someUser/someRepo/", id), names[i])
216+
}
217+
218+
expected := fmt.Sprintf(expectedFmt, links...)
219+
testRenderIssueIndexPattern(t, s, expected, &postProcessCtx{metas: metas})
220+
}
221+
222+
test("abc ISSUE-123 def", "abc %s def", "ISSUE-(\\d+)",
223+
[]string{"123"},
224+
[]string{"ISSUE-123"},
225+
)
226+
227+
test("abc (ISSUE 123) def", "abc %s def",
228+
"\\(ISSUE (\\d+)\\)",
229+
[]string{"123"},
230+
[]string{"(ISSUE 123)"},
231+
)
232+
233+
test("abc (ISSUE 123) def (TASK 456) ghi", "abc %s def %s ghi", "\\((?:ISSUE|TASK) (\\d+)\\)",
234+
[]string{"123", "456"},
235+
[]string{"(ISSUE 123)", "(TASK 456)"},
236+
)
237+
238+
metas := regexpMetas
239+
metas["regexp"] = "no matches"
240+
testRenderIssueIndexPattern(t, "will not match", "will not match", &postProcessCtx{metas: metas})
241+
}
242+
243+
198244
func testRenderIssueIndexPattern(t *testing.T, input, expected string, ctx *RenderContext) {
199245
if ctx.URLPrefix == "" {
200246
ctx.URLPrefix = AppSubURL

modules/references/references.go

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,24 @@ func FindRenderizableReferenceNumeric(content string, prOnly bool) (bool, *Rende
340340
}
341341
}
342342

343+
// FindRenderizableReferenceRegexp returns the first regexp unvalidated references found in a string.
344+
func FindRenderizableReferenceRegexp(content string, pattern *Regexp) (bool, *RenderizableReference) {
345+
match := pattern.FindStringSubmatchIndex(content)
346+
if match == nil {
347+
return false, nil
348+
}
349+
350+
action, location := findActionKeywords([]byte(content), match[2])
351+
352+
return true, &RenderizableReference{
353+
Issue: string(content[match[2]:match[3]]),
354+
RefLocation: &RefSpan{Start: match[0], End: match[1]},
355+
Action: action,
356+
ActionLocation: location,
357+
IsPull: false,
358+
}
359+
}
360+
343361
// FindRenderizableReferenceAlphanumeric returns the first alphanumeric unvalidated references found in a string.
344362
func FindRenderizableReferenceAlphanumeric(content string) (bool, *RenderizableReference) {
345363
match := issueAlphanumericPattern.FindStringSubmatchIndex(content)
@@ -537,7 +555,7 @@ func findActionKeywords(content []byte, start int) (XRefAction, *RefSpan) {
537555
}
538556

539557
// IsXrefActionable returns true if the xref action is actionable (i.e. produces a result when resolved)
540-
func IsXrefActionable(ref *RenderizableReference, extTracker bool, alphaNum bool) bool {
558+
func IsXrefActionable(ref *RenderizableReference, extTracker bool) bool {
541559
if extTracker {
542560
// External issues cannot be automatically closed
543561
return false

options/locale/locale_en-US.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1690,6 +1690,9 @@ settings.tracker_url_format_error = The external issue tracker URL format is not
16901690
settings.tracker_issue_style = External Issue Tracker Number Format
16911691
settings.tracker_issue_style.numeric = Numeric
16921692
settings.tracker_issue_style.alphanumeric = Alphanumeric
1693+
settings.tracker_issue_style.regexp = Regular Expression
1694+
settings.tracker_issue_style.regexp_pattern = Regular Expression Pattern
1695+
settings.tracker_issue_style.regexp_pattern_desc = The first captured group will be used in place of <code>{index}</code>.
16931696
settings.tracker_url_format_desc = Use the placeholders <code>{user}</code>, <code>{repo}</code> and <code>{index}</code> for the username, repository name and issue index.
16941697
settings.enable_timetracker = Enable Time Tracking
16951698
settings.allow_only_contributors_to_track_time = Let Only Contributors Track Time

routers/web/repo/setting.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -395,9 +395,10 @@ func SettingsPost(ctx *context.Context) {
395395
RepoID: repo.ID,
396396
Type: unit_model.TypeExternalTracker,
397397
Config: &models.ExternalTrackerConfig{
398-
ExternalTrackerURL: form.ExternalTrackerURL,
399-
ExternalTrackerFormat: form.TrackerURLFormat,
400-
ExternalTrackerStyle: form.TrackerIssueStyle,
398+
ExternalTrackerURL: form.ExternalTrackerURL,
399+
ExternalTrackerFormat: form.TrackerURLFormat,
400+
ExternalTrackerStyle: form.TrackerIssueStyle,
401+
ExternalTrackerRegexpPattern: form.ExternalTrackerRegexpPattern,
401402
},
402403
})
403404
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeIssues)

services/forms/repo_form.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ type RepoSettingForm struct {
140140
ExternalTrackerURL string
141141
TrackerURLFormat string
142142
TrackerIssueStyle string
143+
ExternalTrackerRegexpPattern string
143144
EnableCloseIssuesViaCommitInAnyBranch bool
144145
EnableProjects bool
145146
EnablePulls bool

templates/repo/settings/options.tmpl

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -361,16 +361,27 @@
361361
<div class="ui radio checkbox">
362362
{{$externalTracker := (.Repository.MustGetUnit $.UnitTypeExternalTracker)}}
363363
{{$externalTrackerStyle := $externalTracker.ExternalTrackerConfig.ExternalTrackerStyle}}
364-
<input class="hidden" tabindex="0" name="tracker_issue_style" type="radio" value="numeric" {{if $externalTrackerStyle}}{{if eq $externalTrackerStyle "numeric"}}checked=""{{end}}{{end}}/>
364+
<input class="hidden enable-system-pick" tabindex="0" name="tracker_issue_style" type="radio" value="numeric" data-context="regexp" data-target="#tracker_regexp_pattern_box" {{if $externalTrackerStyle}}{{if eq $externalTrackerStyle "numeric"}}checked=""{{end}}{{end}}/>
365365
<label>{{.i18n.Tr "repo.settings.tracker_issue_style.numeric"}} <span class="ui light grey text">(#1234)</span></label>
366366
</div>
367367
</div>
368368
<div class="field">
369369
<div class="ui radio checkbox">
370-
<input class="hidden" tabindex="0" name="tracker_issue_style" type="radio" value="alphanumeric" {{if $externalTrackerStyle}}{{if eq $externalTracker.ExternalTrackerConfig.ExternalTrackerStyle "alphanumeric"}}checked=""{{end}}{{end}} />
370+
<input class="hidden enable-system-pick" tabindex="0" name="tracker_issue_style" type="radio" value="alphanumeric" data-context="regexp" data-target="#tracker_regexp_pattern_box" {{if $externalTrackerStyle}}{{if eq $externalTracker.ExternalTrackerConfig.ExternalTrackerStyle "alphanumeric"}}checked=""{{end}}{{end}} />
371371
<label>{{.i18n.Tr "repo.settings.tracker_issue_style.alphanumeric"}} <span class="ui light grey text">(ABC-123, DEFG-234)</span></label>
372372
</div>
373373
</div>
374+
<div class="field">
375+
<div class="ui radio checkbox">
376+
<input class="hidden enable-system-pick" tabindex="0" name="tracker_issue_style" type="radio" value="regexp" data-context="regexp" data-target="#tracker_regexp_pattern_box" {{if $externalTrackerStyle}}{{if eq $externalTracker.ExternalTrackerConfig.ExternalTrackerStyle "regexp"}}checked=""{{end}}{{end}} />
377+
<label>{{.i18n.Tr "repo.settings.tracker_issue_style.regexp"}} <span class="ui light grey text">((?:TASK|ISSUE) (\d+))</span></label>
378+
</div>
379+
</div>
380+
</div>
381+
<div class="field {{if ne $externalTracker.ExternalTrackerConfig.ExternalTrackerStyle "regexp"}}disabled{{end}}" id="tracker_regexp_pattern_box">
382+
<label for="external_tracker_regexp_pattern">{{.i18n.Tr "repo.settings.tracker_issue_style.regexp_pattern"}}</label>
383+
<input id="external_tracker_regexp_pattern" name="external_tracker_regexp_pattern" value="{{(.Repository.MustGetUnit $.UnitTypeExternalTracker).ExternalTrackerConfig.ExternalTrackerRegexpPattern}}">
384+
<p class="help">{{.i18n.Tr "repo.settings.tracker_issue_style.regexp_pattern_desc" | Str2html}}</p>
374385
</div>
375386
</div>
376387
</div>

web_src/js/features/repo-legacy.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,15 @@ export function initRepository() {
309309
if (typeof $(this).data('context') !== 'undefined') $($(this).data('context')).addClass('disabled');
310310
}
311311
});
312+
$('.enable-system-pick').change(function () {
313+
if ($(this).data('context') && $(this).data('target')) {
314+
if ($(this).data('context') === this.value) {
315+
$($(this).data('target')).removeClass('disabled')
316+
} else {
317+
$($(this).data('target')).addClass('disabled')
318+
}
319+
}
320+
})
312321
}
313322

314323
// Labels

0 commit comments

Comments
 (0)