Skip to content

Commit 52c2e82

Browse files
strkAlexanderBeyntechknowlogicklafriks6543
authored
Custom regexp external issues (#17624)
* Implement custom regular expression for external issue tracking. Signed-off-by: Alexander Beyn <[email protected]> * Fix syntax/style * Update repo.go * Set metas['regexp'] * gofmt * fix some tests * fix more tests * refactor frontend * use LRU cache for regexp * Update modules/markup/html_internal_test.go Co-authored-by: Alexander Beyn <[email protected]> Co-authored-by: techknowlogick <[email protected]> Co-authored-by: Lauris BH <[email protected]> Co-authored-by: 6543 <[email protected]> Co-authored-by: wxiaoguang <[email protected]>
1 parent 5f61824 commit 52c2e82

File tree

13 files changed

+206
-26
lines changed

13 files changed

+206
-26
lines changed

models/repo/repo.go

+3
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,9 @@ func (repo *Repository) ComposeMetas() map[string]string {
414414
switch unit.ExternalTrackerConfig().ExternalTrackerStyle {
415415
case markup.IssueNameStyleAlphanumeric:
416416
metas["style"] = markup.IssueNameStyleAlphanumeric
417+
case markup.IssueNameStyleRegexp:
418+
metas["style"] = markup.IssueNameStyleRegexp
419+
metas["regexp"] = unit.ExternalTrackerConfig().ExternalTrackerRegexpPattern
417420
default:
418421
metas["style"] = markup.IssueNameStyleNumeric
419422
}

models/repo/repo_unit.go

+4-3
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,10 @@ func (cfg *ExternalWikiConfig) ToDB() ([]byte, error) {
7676

7777
// ExternalTrackerConfig describes external tracker config
7878
type ExternalTrackerConfig struct {
79-
ExternalTrackerURL string
80-
ExternalTrackerFormat string
81-
ExternalTrackerStyle string
79+
ExternalTrackerURL string
80+
ExternalTrackerFormat string
81+
ExternalTrackerStyle string
82+
ExternalTrackerRegexpPattern string
8283
}
8384

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

models/repo_test.go

+3
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ func TestMetas(t *testing.T) {
7474
externalTracker.ExternalTrackerConfig().ExternalTrackerStyle = markup.IssueNameStyleNumeric
7575
testSuccess(markup.IssueNameStyleNumeric)
7676

77+
externalTracker.ExternalTrackerConfig().ExternalTrackerStyle = markup.IssueNameStyleRegexp
78+
testSuccess(markup.IssueNameStyleRegexp)
79+
7780
repo, err := repo_model.GetRepositoryByID(3)
7881
assert.NoError(t, err)
7982

modules/markup/html.go

+29-11
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"code.gitea.io/gitea/modules/log"
2121
"code.gitea.io/gitea/modules/markup/common"
2222
"code.gitea.io/gitea/modules/references"
23+
"code.gitea.io/gitea/modules/regexplru"
2324
"code.gitea.io/gitea/modules/setting"
2425
"code.gitea.io/gitea/modules/templates/vars"
2526
"code.gitea.io/gitea/modules/util"
@@ -33,6 +34,7 @@ import (
3334
const (
3435
IssueNameStyleNumeric = "numeric"
3536
IssueNameStyleAlphanumeric = "alphanumeric"
37+
IssueNameStyleRegexp = "regexp"
3638
)
3739

3840
var (
@@ -815,19 +817,35 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
815817
)
816818

817819
next := node.NextSibling
820+
818821
for node != nil && node != next {
819-
_, exttrack := ctx.Metas["format"]
820-
alphanum := ctx.Metas["style"] == IssueNameStyleAlphanumeric
822+
_, hasExtTrackFormat := ctx.Metas["format"]
821823

822824
// Repos with external issue trackers might still need to reference local PRs
823825
// We need to concern with the first one that shows up in the text, whichever it is
824-
found, ref = references.FindRenderizableReferenceNumeric(node.Data, exttrack && alphanum)
825-
if exttrack && alphanum {
826-
if found2, ref2 := references.FindRenderizableReferenceAlphanumeric(node.Data); found2 {
827-
if !found || ref2.RefLocation.Start < ref.RefLocation.Start {
828-
found = true
829-
ref = ref2
830-
}
826+
isNumericStyle := ctx.Metas["style"] == "" || ctx.Metas["style"] == IssueNameStyleNumeric
827+
foundNumeric, refNumeric := references.FindRenderizableReferenceNumeric(node.Data, hasExtTrackFormat && !isNumericStyle)
828+
829+
switch ctx.Metas["style"] {
830+
case "", IssueNameStyleNumeric:
831+
found, ref = foundNumeric, refNumeric
832+
case IssueNameStyleAlphanumeric:
833+
found, ref = references.FindRenderizableReferenceAlphanumeric(node.Data)
834+
case IssueNameStyleRegexp:
835+
pattern, err := regexplru.GetCompiled(ctx.Metas["regexp"])
836+
if err != nil {
837+
return
838+
}
839+
found, ref = references.FindRenderizableReferenceRegexp(node.Data, pattern)
840+
}
841+
842+
// Repos with external issue trackers might still need to reference local PRs
843+
// We need to concern with the first one that shows up in the text, whichever it is
844+
if hasExtTrackFormat && !isNumericStyle {
845+
// If numeric (PR) was found, and it was BEFORE the non-numeric pattern, use that
846+
if foundNumeric && refNumeric.RefLocation.Start < ref.RefLocation.Start {
847+
found = foundNumeric
848+
ref = refNumeric
831849
}
832850
}
833851
if !found {
@@ -836,7 +854,7 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
836854

837855
var link *html.Node
838856
reftext := node.Data[ref.RefLocation.Start:ref.RefLocation.End]
839-
if exttrack && !ref.IsPull {
857+
if hasExtTrackFormat && !ref.IsPull {
840858
ctx.Metas["index"] = ref.Issue
841859

842860
res, err := vars.Expand(ctx.Metas["format"], ctx.Metas)
@@ -869,7 +887,7 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
869887

870888
// Decorate action keywords if actionable
871889
var keyword *html.Node
872-
if references.IsXrefActionable(ref, exttrack, alphanum) {
890+
if references.IsXrefActionable(ref, hasExtTrackFormat) {
873891
keyword = createKeyword(node.Data[ref.ActionLocation.Start:ref.ActionLocation.End])
874892
} else {
875893
keyword = &html.Node{

modules/markup/html_internal_test.go

+48-4
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ const (
2121
TestRepoURL = TestAppURL + TestOrgRepo + "/"
2222
)
2323

24-
// alphanumLink an HTML link to an alphanumeric-style issue
25-
func alphanumIssueLink(baseURL, class, name string) string {
24+
// externalIssueLink an HTML link to an alphanumeric-style issue
25+
func externalIssueLink(baseURL, class, name string) string {
2626
return link(util.URLJoin(baseURL, name), class, name)
2727
}
2828

@@ -54,6 +54,13 @@ var alphanumericMetas = map[string]string{
5454
"style": IssueNameStyleAlphanumeric,
5555
}
5656

57+
var regexpMetas = map[string]string{
58+
"format": "https://someurl.com/{user}/{repo}/{index}",
59+
"user": "someUser",
60+
"repo": "someRepo",
61+
"style": IssueNameStyleRegexp,
62+
}
63+
5764
// these values should match the TestOrgRepo const above
5865
var localMetas = map[string]string{
5966
"user": "gogits",
@@ -184,7 +191,7 @@ func TestRender_IssueIndexPattern4(t *testing.T) {
184191
test := func(s, expectedFmt string, names ...string) {
185192
links := make([]interface{}, len(names))
186193
for i, name := range names {
187-
links[i] = alphanumIssueLink("https://someurl.com/someUser/someRepo/", "ref-issue ref-external-issue", name)
194+
links[i] = externalIssueLink("https://someurl.com/someUser/someRepo/", "ref-issue ref-external-issue", name)
188195
}
189196
expected := fmt.Sprintf(expectedFmt, links...)
190197
testRenderIssueIndexPattern(t, s, expected, &RenderContext{Metas: alphanumericMetas})
@@ -194,6 +201,43 @@ func TestRender_IssueIndexPattern4(t *testing.T) {
194201
test("test issue ABCDEFGHIJ-1234567890", "test issue %s", "ABCDEFGHIJ-1234567890")
195202
}
196203

204+
func TestRender_IssueIndexPattern5(t *testing.T) {
205+
setting.AppURL = TestAppURL
206+
207+
// regexp: render inputs without valid mentions
208+
test := func(s, expectedFmt, pattern string, ids, names []string) {
209+
metas := regexpMetas
210+
metas["regexp"] = pattern
211+
links := make([]interface{}, len(ids))
212+
for i, id := range ids {
213+
links[i] = link(util.URLJoin("https://someurl.com/someUser/someRepo/", id), "ref-issue ref-external-issue", names[i])
214+
}
215+
216+
expected := fmt.Sprintf(expectedFmt, links...)
217+
testRenderIssueIndexPattern(t, s, expected, &RenderContext{Metas: metas})
218+
}
219+
220+
test("abc ISSUE-123 def", "abc %s def",
221+
"ISSUE-(\\d+)",
222+
[]string{"123"},
223+
[]string{"ISSUE-123"},
224+
)
225+
226+
test("abc (ISSUE 123) def", "abc %s def",
227+
"\\(ISSUE (\\d+)\\)",
228+
[]string{"123"},
229+
[]string{"(ISSUE 123)"},
230+
)
231+
232+
test("abc ISSUE-123 def", "abc %s def",
233+
"(ISSUE-(\\d+))",
234+
[]string{"ISSUE-123"},
235+
[]string{"ISSUE-123"},
236+
)
237+
238+
testRenderIssueIndexPattern(t, "will not match", "will not match", &RenderContext{Metas: regexpMetas})
239+
}
240+
197241
func testRenderIssueIndexPattern(t *testing.T, input, expected string, ctx *RenderContext) {
198242
if ctx.URLPrefix == "" {
199243
ctx.URLPrefix = TestAppURL
@@ -202,7 +246,7 @@ func testRenderIssueIndexPattern(t *testing.T, input, expected string, ctx *Rend
202246
var buf strings.Builder
203247
err := postProcess(ctx, []processor{issueIndexPatternProcessor}, strings.NewReader(input), &buf)
204248
assert.NoError(t, err)
205-
assert.Equal(t, expected, buf.String())
249+
assert.Equal(t, expected, buf.String(), "input=%q", input)
206250
}
207251

208252
func TestRender_AutoLink(t *testing.T) {

modules/references/references.go

+19-1
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,24 @@ func FindRenderizableReferenceNumeric(content string, prOnly bool) (bool, *Rende
351351
}
352352
}
353353

354+
// FindRenderizableReferenceRegexp returns the first regexp unvalidated references found in a string.
355+
func FindRenderizableReferenceRegexp(content string, pattern *regexp.Regexp) (bool, *RenderizableReference) {
356+
match := pattern.FindStringSubmatchIndex(content)
357+
if len(match) < 4 {
358+
return false, nil
359+
}
360+
361+
action, location := findActionKeywords([]byte(content), match[2])
362+
363+
return true, &RenderizableReference{
364+
Issue: content[match[2]:match[3]],
365+
RefLocation: &RefSpan{Start: match[0], End: match[1]},
366+
Action: action,
367+
ActionLocation: location,
368+
IsPull: false,
369+
}
370+
}
371+
354372
// FindRenderizableReferenceAlphanumeric returns the first alphanumeric unvalidated references found in a string.
355373
func FindRenderizableReferenceAlphanumeric(content string) (bool, *RenderizableReference) {
356374
match := issueAlphanumericPattern.FindStringSubmatchIndex(content)
@@ -547,7 +565,7 @@ func findActionKeywords(content []byte, start int) (XRefAction, *RefSpan) {
547565
}
548566

549567
// IsXrefActionable returns true if the xref action is actionable (i.e. produces a result when resolved)
550-
func IsXrefActionable(ref *RenderizableReference, extTracker, alphaNum bool) bool {
568+
func IsXrefActionable(ref *RenderizableReference, extTracker bool) bool {
551569
if extTracker {
552570
// External issues cannot be automatically closed
553571
return false

modules/regexplru/regexplru.go

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright 2022 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package regexplru
6+
7+
import (
8+
"regexp"
9+
10+
"code.gitea.io/gitea/modules/log"
11+
12+
lru "github.com/hashicorp/golang-lru"
13+
)
14+
15+
var lruCache *lru.Cache
16+
17+
func init() {
18+
var err error
19+
lruCache, err = lru.New(1000)
20+
if err != nil {
21+
log.Fatal("failed to new LRU cache, err: %v", err)
22+
}
23+
}
24+
25+
// GetCompiled works like regexp.Compile, the compiled expr or error is stored in LRU cache
26+
func GetCompiled(expr string) (r *regexp.Regexp, err error) {
27+
v, ok := lruCache.Get(expr)
28+
if !ok {
29+
r, err = regexp.Compile(expr)
30+
if err != nil {
31+
lruCache.Add(expr, err)
32+
return nil, err
33+
}
34+
lruCache.Add(expr, r)
35+
} else {
36+
r, ok = v.(*regexp.Regexp)
37+
if !ok {
38+
if err, ok = v.(error); ok {
39+
return nil, err
40+
}
41+
panic("impossible")
42+
}
43+
}
44+
return r, nil
45+
}

modules/regexplru/regexplru_test.go

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright 2022 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package regexplru
6+
7+
import (
8+
"testing"
9+
10+
"github.com/stretchr/testify/assert"
11+
)
12+
13+
func TestRegexpLru(t *testing.T) {
14+
r, err := GetCompiled("a")
15+
assert.NoError(t, err)
16+
assert.True(t, r.MatchString("a"))
17+
18+
r, err = GetCompiled("a")
19+
assert.NoError(t, err)
20+
assert.True(t, r.MatchString("a"))
21+
22+
assert.EqualValues(t, 1, lruCache.Len())
23+
24+
_, err = GetCompiled("(")
25+
assert.Error(t, err)
26+
assert.EqualValues(t, 2, lruCache.Len())
27+
}

options/locale/locale_en-US.ini

+3
Original file line numberDiff line numberDiff line change
@@ -1811,6 +1811,9 @@ settings.tracker_url_format_error = The external issue tracker URL format is not
18111811
settings.tracker_issue_style = External Issue Tracker Number Format
18121812
settings.tracker_issue_style.numeric = Numeric
18131813
settings.tracker_issue_style.alphanumeric = Alphanumeric
1814+
settings.tracker_issue_style.regexp = Regular Expression
1815+
settings.tracker_issue_style.regexp_pattern = Regular Expression Pattern
1816+
settings.tracker_issue_style.regexp_pattern_desc = The first captured group will be used in place of <code>{index}</code>.
18141817
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.
18151818
settings.enable_timetracker = Enable Time Tracking
18161819
settings.allow_only_contributors_to_track_time = Let Only Contributors Track Time

routers/web/repo/setting.go

+4-3
Original file line numberDiff line numberDiff line change
@@ -434,9 +434,10 @@ func SettingsPost(ctx *context.Context) {
434434
RepoID: repo.ID,
435435
Type: unit_model.TypeExternalTracker,
436436
Config: &repo_model.ExternalTrackerConfig{
437-
ExternalTrackerURL: form.ExternalTrackerURL,
438-
ExternalTrackerFormat: form.TrackerURLFormat,
439-
ExternalTrackerStyle: form.TrackerIssueStyle,
437+
ExternalTrackerURL: form.ExternalTrackerURL,
438+
ExternalTrackerFormat: form.TrackerURLFormat,
439+
ExternalTrackerStyle: form.TrackerIssueStyle,
440+
ExternalTrackerRegexpPattern: form.ExternalTrackerRegexpPattern,
440441
},
441442
})
442443
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeIssues)

services/forms/repo_form.go

+1
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ type RepoSettingForm struct {
141141
ExternalTrackerURL string
142142
TrackerURLFormat string
143143
TrackerIssueStyle string
144+
ExternalTrackerRegexpPattern string
144145
EnableCloseIssuesViaCommitInAnyBranch bool
145146
EnableProjects bool
146147
EnablePackages bool

templates/repo/settings/options.tmpl

+15-4
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}}/>
365-
<label>{{.i18n.Tr "repo.settings.tracker_issue_style.numeric"}} <span class="ui light grey text">(#1234)</span></label>
364+
<input class="js-tracker-issue-style" name="tracker_issue_style" type="radio" value="numeric" {{if eq $externalTrackerStyle "numeric"}}checked{{end}}>
365+
<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}} />
371-
<label>{{.i18n.Tr "repo.settings.tracker_issue_style.alphanumeric"}} <span class="ui light grey text">(ABC-123, DEFG-234)</span></label>
370+
<input class="js-tracker-issue-style" name="tracker_issue_style" type="radio" value="alphanumeric" {{if eq $externalTrackerStyle "alphanumeric"}}checked{{end}}>
371+
<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="js-tracker-issue-style" name="tracker_issue_style" type="radio" value="regexp" {{if eq $externalTrackerStyle "regexp"}}checked{{end}}>
377+
<label>{{.i18n.Tr "repo.settings.tracker_issue_style.regexp"}} <span class="ui light grey text">(ISSUE-\d+) , ISSUE-(\d+)</span></label>
378+
</div>
379+
</div>
380+
</div>
381+
<div class="field {{if ne $externalTrackerStyle "regexp"}}disabled{{end}}" id="tracker-issue-style-regex-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

+5
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,11 @@ export function initRepository() {
462462
if (typeof $(this).data('context') !== 'undefined') $($(this).data('context')).addClass('disabled');
463463
}
464464
});
465+
const $trackerIssueStyleRadios = $('.js-tracker-issue-style');
466+
$trackerIssueStyleRadios.on('change input', () => {
467+
const checkedVal = $trackerIssueStyleRadios.filter(':checked').val();
468+
$('#tracker-issue-style-regex-box').toggleClass('disabled', checkedVal !== 'regexp');
469+
});
465470
}
466471

467472
// Labels

0 commit comments

Comments
 (0)