Skip to content

Implement custom regular expression for external issue tracking. #4265

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

Closed
4 changes: 4 additions & 0 deletions models/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -519,9 +519,13 @@ func (repo *Repository) ComposeMetas() map[string]string {
switch unit.ExternalTrackerConfig().ExternalTrackerStyle {
case markup.IssueNameStyleAlphanumeric:
repo.ExternalMetas["style"] = markup.IssueNameStyleAlphanumeric
case markup.IssueNameStyleRegexp:
repo.ExternalMetas["style"] = markup.IssueNameStyleRegexp
default:
repo.ExternalMetas["style"] = markup.IssueNameStyleNumeric
}
repo.ExternalMetas["format"] = unit.ExternalTrackerConfig().ExternalTrackerFormat
repo.ExternalMetas["regexp"] = unit.ExternalTrackerConfig().ExternalTrackerRegexpPattern

}
return repo.ExternalMetas
Expand Down
3 changes: 3 additions & 0 deletions models/repo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ func TestRepo(t *testing.T) {

externalTracker.ExternalTrackerConfig().ExternalTrackerStyle = markup.IssueNameStyleNumeric
testSuccess(markup.IssueNameStyleNumeric)

externalTracker.ExternalTrackerConfig().ExternalTrackerStyle = markup.IssueNameStyleRegexp
testSuccess(markup.IssueNameStyleRegexp)
}

func TestGetRepositoryCount(t *testing.T) {
Expand Down
7 changes: 4 additions & 3 deletions models/repo_unit.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,10 @@ func (cfg *ExternalWikiConfig) ToDB() ([]byte, error) {

// ExternalTrackerConfig describes external tracker config
type ExternalTrackerConfig struct {
ExternalTrackerURL string
ExternalTrackerFormat string
ExternalTrackerStyle string
ExternalTrackerURL string
ExternalTrackerFormat string
ExternalTrackerStyle string
ExternalTrackerRegexpPattern string
}

// FromDB fills up a ExternalTrackerConfig from serialized format.
Expand Down
1 change: 1 addition & 0 deletions modules/auth/repo_form.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ type RepoSettingForm struct {
ExternalTrackerURL string
TrackerURLFormat string
TrackerIssueStyle string
ExternalTrackerRegexpPattern string
EnablePulls bool
PullsIgnoreWhitespace bool
PullsAllowMerge bool
Expand Down
46 changes: 34 additions & 12 deletions modules/markup/html.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
const (
IssueNameStyleNumeric = "numeric"
IssueNameStyleAlphanumeric = "alphanumeric"
IssueNameStyleRegexp = "regexp"
)

var (
Expand Down Expand Up @@ -552,29 +553,50 @@ func issueIndexPatternProcessor(ctx *postProcessCtx, node *html.Node) {
}
// default to numeric pattern, unless alphanumeric is requested.
pattern := issueNumericPattern
if ctx.metas["style"] == IssueNameStyleAlphanumeric {
switch ctx.metas["style"] {
case IssueNameStyleAlphanumeric:
pattern = issueAlphanumericPattern
case IssueNameStyleRegexp:
var err error
pattern, err = regexp.Compile(ctx.metas["regexp"])
if err != nil {
return
}
}

match := pattern.FindStringSubmatchIndex(node.Data)
if match == nil {
if match == nil || len(match) < 4 {
return
}

id := node.Data[match[2]:match[3]]
var index, content string
var start, end int
switch ctx.metas["style"] {
case IssueNameStyleAlphanumeric:
content = node.Data[match[2]:match[3]]
index = content
start = match[2]
end = match[3]
case IssueNameStyleRegexp:
index = node.Data[match[2]:match[3]]
content = node.Data[match[0]:match[1]]
start = match[0]
end = match[1]
default:
content = node.Data[match[2]:match[3]]
index = content[1:]
start = match[2]
end = match[3]
}

var link *html.Node
if _, ok := ctx.metas["format"]; ok {
// Support for external issue tracker
if ctx.metas["style"] == IssueNameStyleAlphanumeric {
ctx.metas["index"] = id
} else {
ctx.metas["index"] = id[1:]
}
link = createLink(com.Expand(ctx.metas["format"], ctx.metas), id)
ctx.metas["index"] = index
link = createLink(com.Expand(ctx.metas["format"], ctx.metas), content)
} else {
link = createLink(util.URLJoin(setting.AppURL, ctx.metas["user"], ctx.metas["repo"], "issues", id[1:]), id)
link = createLink(util.URLJoin(setting.AppURL, ctx.metas["user"], ctx.metas["repo"], "issues", index), content)
}
replaceContent(node, match[2], match[3], link)
replaceContent(node, start, end, link)
}

func crossReferenceIssueIndexPatternProcessor(ctx *postProcessCtx, node *html.Node) {
Expand Down
41 changes: 41 additions & 0 deletions modules/markup/html_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ var alphanumericMetas = map[string]string{
"style": IssueNameStyleAlphanumeric,
}

var regexpMetas = map[string]string{
"format": "https://someurl.com/{user}/{repo}/{index}",
"user": "someUser",
"repo": "someRepo",
"style": IssueNameStyleRegexp,
}

// these values should match the Repo const above
var localMetas = map[string]string{
"user": "gogits",
Expand Down Expand Up @@ -164,6 +171,40 @@ func TestRender_IssueIndexPattern4(t *testing.T) {
test("test issue ABCDEFGHIJ-1234567890", "test issue %s", "ABCDEFGHIJ-1234567890")
}

func TestRender_IssueIndexPattern5(t *testing.T) {
test := func(s, expectedFmt string, pattern string, ids []string, names []string) {
metas := regexpMetas
metas["regexp"] = pattern
links := make([]interface{}, len(ids))
for i, id := range ids {
links[i] = link(util.URLJoin("https://someurl.com/someUser/someRepo/", id), names[i])
}

expected := fmt.Sprintf(expectedFmt, links...)
testRenderIssueIndexPattern(t, s, expected, &postProcessCtx{metas: metas})
}

test("abc ISSUE-123 def", "abc %s def", "ISSUE-(\\d+)",
[]string{"123"},
[]string{"ISSUE-123"},
)

test("abc (ISSUE 123) def", "abc %s def",
"\\(ISSUE (\\d+)\\)",
[]string{"123"},
[]string{"(ISSUE 123)"},
)

test("abc (ISSUE 123) def (TASK 456) ghi", "abc %s def %s ghi", "\\((?:ISSUE|TASK) (\\d+)\\)",
[]string{"123", "456"},
[]string{"(ISSUE 123)", "(TASK 456)"},
)

metas := regexpMetas
metas["regexp"] = "no matches"
testRenderIssueIndexPattern(t, "will not match", "will not match", &postProcessCtx{metas: metas})
}

func testRenderIssueIndexPattern(t *testing.T, input, expected string, ctx *postProcessCtx) {
if ctx == nil {
ctx = new(postProcessCtx)
Expand Down
3 changes: 3 additions & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -1137,6 +1137,9 @@ settings.tracker_url_format_error = The external issue tracker URL format is not
settings.tracker_issue_style = External Issue Tracker Number Format
settings.tracker_issue_style.numeric = Numeric
settings.tracker_issue_style.alphanumeric = Alphanumeric
settings.tracker_issue_style.regexp = Regular Expression
settings.tracker_issue_style.regexp_pattern = Regular Expression Pattern
settings.tracker_issue_style.regexp_pattern_desc = The first captured group will be used in place of <code>{index}</code>.
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.
settings.enable_timetracker = Enable Time Tracking
settings.allow_only_contributors_to_track_time = Let Only Contributors Track Time
Expand Down
9 changes: 9 additions & 0 deletions public/js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,15 @@ function initRepository() {
if (typeof $(this).data('context') !== 'undefined') $($(this).data('context')).addClass('disabled');
}
});
$('.enable-system-pick').change(function () {
if ($(this).data('context') && $(this).data('target')) {
if ($(this).data('context') === this.value) {
$($(this).data('target')).removeClass('disabled')
} else {
$($(this).data('target')).addClass('disabled')
}
}
})
}

// Labels
Expand Down
7 changes: 4 additions & 3 deletions routers/repo/setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -258,9 +258,10 @@ func SettingsPost(ctx *context.Context, form auth.RepoSettingForm) {
RepoID: repo.ID,
Type: models.UnitTypeExternalTracker,
Config: &models.ExternalTrackerConfig{
ExternalTrackerURL: form.ExternalTrackerURL,
ExternalTrackerFormat: form.TrackerURLFormat,
ExternalTrackerStyle: form.TrackerIssueStyle,
ExternalTrackerURL: form.ExternalTrackerURL,
ExternalTrackerFormat: form.TrackerURLFormat,
ExternalTrackerStyle: form.TrackerIssueStyle,
ExternalTrackerRegexpPattern: form.ExternalTrackerRegexpPattern,
},
})
} else {
Expand Down
15 changes: 13 additions & 2 deletions templates/repo/settings/options.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -203,16 +203,27 @@
<div class="ui radio checkbox">
{{$externalTracker := (.Repository.MustGetUnit $.UnitTypeExternalTracker)}}
{{$externalTrackerStyle := $externalTracker.ExternalTrackerConfig.ExternalTrackerStyle}}
<input class="hidden" tabindex="0" name="tracker_issue_style" type="radio" value="numeric" {{if $externalTrackerStyle}}{{if eq $externalTrackerStyle "numeric"}}checked=""{{end}}{{end}}/>
<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}}/>
<label>{{.i18n.Tr "repo.settings.tracker_issue_style.numeric"}} <span class="ui light grey text">(#1234)</span></label>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input class="hidden" tabindex="0" name="tracker_issue_style" type="radio" value="alphanumeric" {{if $externalTrackerStyle}}{{if eq $externalTracker.ExternalTrackerConfig.ExternalTrackerStyle "alphanumeric"}}checked=""{{end}}{{end}} />
<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}} />
<label>{{.i18n.Tr "repo.settings.tracker_issue_style.alphanumeric"}} <span class="ui light grey text">(ABC-123, DEFG-234)</span></label>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<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}} />
<label>{{.i18n.Tr "repo.settings.tracker_issue_style.regexp"}} <span class="ui light grey text">((?:TASK|ISSUE) (\d+))</span></label>
</div>
</div>
</div>
<div class="field {{if ne $externalTracker.ExternalTrackerConfig.ExternalTrackerStyle "regexp"}}disabled{{end}}" id="tracker_regexp_pattern_box">
<label for="external_tracker_regexp_pattern">{{.i18n.Tr "repo.settings.tracker_issue_style.regexp_pattern"}}</label>
<input id="external_tracker_regexp_pattern" name="external_tracker_regexp_pattern" value="{{(.Repository.MustGetUnit $.UnitTypeExternalTracker).ExternalTrackerConfig.ExternalTrackerRegexpPattern}}">
<p class="help">{{.i18n.Tr "repo.settings.tracker_issue_style.regexp_pattern_desc" | Str2html}}</p>
</div>
</div>
</div>
Expand Down