enhance: improve issue-pattern capture groups and support both internal&external trackers enabled (#39354)

* Fix #39351
* Fix #17621
* Fix #34881

By the way, fix error handling bugs in `updateRepoUnits`

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
breken
2026-09-18 08:53:45 -07:00
committed by GitHub
co-authored by wxiaoguang
parent 85eaf5c71c
commit b27e7d0289
13 changed files with 145 additions and 112 deletions
+7 -3
View File
@@ -473,9 +473,13 @@ func (repo *Repository) composeCommonMetas(ctx context.Context) map[string]strin
"repo": repo.Name, "repo": repo.Name,
} }
unitExternalTracker, err := repo.GetUnit(ctx, unit.TypeExternalTracker) unitInternalTracker, _ := repo.GetUnit(ctx, unit.TypeIssues)
if err == nil { unitExternalTracker, _ := repo.GetUnit(ctx, unit.TypeExternalTracker)
metas["format"] = unitExternalTracker.ExternalTrackerConfig().ExternalTrackerFormat if unitInternalTracker != nil {
metas["internalTrackerEnabled"] = "true"
}
if unitExternalTracker != nil {
metas["externalTrackerLinkFormat"] = unitExternalTracker.ExternalTrackerConfig().ExternalTrackerFormat
switch unitExternalTracker.ExternalTrackerConfig().ExternalTrackerStyle { switch unitExternalTracker.ExternalTrackerConfig().ExternalTrackerStyle {
case markup.IssueNameStyleAlphanumeric: case markup.IssueNameStyleAlphanumeric:
metas["style"] = markup.IssueNameStyleAlphanumeric metas["style"] = markup.IssueNameStyleAlphanumeric
+1 -1
View File
@@ -103,7 +103,7 @@ func TestMetas(t *testing.T) {
assert.Equal(t, expectedStyle, metas["style"]) assert.Equal(t, expectedStyle, metas["style"])
assert.Equal(t, "testRepo", metas["repo"]) assert.Equal(t, "testRepo", metas["repo"])
assert.Equal(t, "testOwner", metas["user"]) assert.Equal(t, "testOwner", metas["user"])
assert.Equal(t, "https://someurl.com/{user}/{repo}/{issue}", metas["format"]) assert.Equal(t, "https://someurl.com/{user}/{repo}/{issue}", metas["externalTrackerLinkFormat"])
} }
testSuccess(markup.IssueNameStyleNumeric) testSuccess(markup.IssueNameStyleNumeric)
+15 -9
View File
@@ -39,7 +39,7 @@ func link(href, class, contents string) string {
} }
var numericMetas = map[string]string{ var numericMetas = map[string]string{
"format": "https://someurl.com/{user}/{repo}/{index}", "externalTrackerLinkFormat": "https://someurl.com/{user}/{repo}/{index}",
"user": "someUser", "user": "someUser",
"repo": "someRepo", "repo": "someRepo",
"style": IssueNameStyleNumeric, "style": IssueNameStyleNumeric,
@@ -47,7 +47,7 @@ var numericMetas = map[string]string{
} }
var alphanumericMetas = map[string]string{ var alphanumericMetas = map[string]string{
"format": "https://someurl.com/{user}/{repo}/{index}", "externalTrackerLinkFormat": "https://someurl.com/{user}/{repo}/{index}",
"user": "someUser", "user": "someUser",
"repo": "someRepo", "repo": "someRepo",
"style": IssueNameStyleAlphanumeric, "style": IssueNameStyleAlphanumeric,
@@ -55,10 +55,10 @@ var alphanumericMetas = map[string]string{
} }
var regexpMetas = map[string]string{ var regexpMetas = map[string]string{
"format": "https://someurl.com/{user}/{repo}/{index}", "externalTrackerLinkFormat": "https://someurl.com/{user}/{repo}/{index}",
"user": "someUser", "user": "someUser",
"repo": "someRepo", "repo": "someRepo",
"style": IssueNameStyleRegexp, "style": IssueNameStyleRegexp,
} }
// these values should match the TestOrgRepo const above // these values should match the TestOrgRepo const above
@@ -219,23 +219,29 @@ func TestRender_IssueIndexPattern5(t *testing.T) {
} }
test("abc ISSUE-123 def", "abc %s def", test("abc ISSUE-123 def", "abc %s def",
"ISSUE-(\\d+)", `ISSUE-(\d+)`,
[]string{"123"}, []string{"123"},
[]string{"ISSUE-123"}, []string{"ISSUE-123"},
) )
test("abc (ISSUE 123) def", "abc %s def", test("abc (ISSUE 123) def", "abc %s def",
"\\(ISSUE (\\d+)\\)", `\(ISSUE (\d+)\)`,
[]string{"123"}, []string{"123"},
[]string{"(ISSUE 123)"}, []string{"(ISSUE 123)"},
) )
test("abc ISSUE-123 def", "abc %s def", test("abc ISSUE-123 def", "abc %s def",
"(ISSUE-(\\d+))", `(ISSUE-(\d+))`,
[]string{"ISSUE-123"}, []string{"ISSUE-123"},
[]string{"ISSUE-123"}, []string{"ISSUE-123"},
) )
test("123456: TEST-123456", "%s %s",
`(\d+):|TEST-(\d+)`,
[]string{"123456", "123456"},
[]string{"123456:", "TEST-123456"},
)
testRenderIssueIndexPattern(t, "will not match", "will not match", NewTestRenderContext(regexpMetas)) testRenderIssueIndexPattern(t, "will not match", "will not match", NewTestRenderContext(regexpMetas))
} }
+14 -7
View File
@@ -110,16 +110,23 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
next := node.NextSibling next := node.NextSibling
for node != nil && node != next { for node != nil && node != next {
_, hasExtTrackFormat := ctx.RenderOptions.Metas["format"] _, hasExternalTracker := ctx.RenderOptions.Metas["externalTrackerLinkFormat"]
hasInternalTracker := ctx.RenderOptions.Metas["internalTrackerEnabled"] == "true"
if !hasExternalTracker && !hasInternalTracker {
hasInternalTracker = true // legacy logic: if no tracker is enabled, fallback to internal
}
// Repos with external issue trackers might still need to reference local PRs // Repos with external issue trackers might still need to reference local PRs
// We need to concern with the first one that shows up in the text, whichever it is // We need to concern with the first one that shows up in the text, whichever it is
isNumericStyle := ctx.RenderOptions.Metas["style"] == "" || ctx.RenderOptions.Metas["style"] == IssueNameStyleNumeric isNumericStyle := ctx.RenderOptions.Metas["style"] == "" || ctx.RenderOptions.Metas["style"] == IssueNameStyleNumeric
refNumeric := references.FindRenderizableReferenceNumeric(node.Data, hasExtTrackFormat && !isNumericStyle, crossLinkOnly) prOnly := hasExternalTracker && !isNumericStyle
refNumeric := references.FindRenderizableReferenceNumeric(node.Data, prOnly, crossLinkOnly)
useExtTrackerLink := true
switch ctx.RenderOptions.Metas["style"] { switch ctx.RenderOptions.Metas["style"] {
case "", IssueNameStyleNumeric: case "", IssueNameStyleNumeric:
ref = refNumeric ref = refNumeric
// when internal tracker is enabled, Numeric (#123) style should only be use for internal tracker
useExtTrackerLink = !hasInternalTracker
case IssueNameStyleAlphanumeric: case IssueNameStyleAlphanumeric:
ref = references.FindRenderizableReferenceAlphanumeric(node.Data) ref = references.FindRenderizableReferenceAlphanumeric(node.Data)
case IssueNameStyleRegexp: case IssueNameStyleRegexp:
@@ -132,7 +139,7 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
// Repos with external issue trackers might still need to reference local PRs // Repos with external issue trackers might still need to reference local PRs
// We need to concern with the first one that shows up in the text, whichever it is // We need to concern with the first one that shows up in the text, whichever it is
if hasExtTrackFormat && !isNumericStyle && refNumeric != nil { if useExtTrackerLink && !isNumericStyle && refNumeric != nil {
// If numeric (PR) was found, and it was BEFORE the non-numeric pattern, use that // If numeric (PR) was found, and it was BEFORE the non-numeric pattern, use that
// Allow a free-pass when non-numeric pattern wasn't found. // Allow a free-pass when non-numeric pattern wasn't found.
if ref == nil || refNumeric.RefLocation.Start < ref.RefLocation.Start { if ref == nil || refNumeric.RefLocation.Start < ref.RefLocation.Start {
@@ -146,10 +153,10 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
var link *html.Node var link *html.Node
refText := node.Data[ref.RefLocation.Start:ref.RefLocation.End] refText := node.Data[ref.RefLocation.Start:ref.RefLocation.End]
if hasExtTrackFormat && !ref.IsPull { if useExtTrackerLink && !ref.IsPull {
ctx.RenderOptions.Metas["index"] = ref.Issue ctx.RenderOptions.Metas["index"] = ref.Issue
res, err := vars.ExpandCurlyBrace(ctx.RenderOptions.Metas["format"], ctx.RenderOptions.Metas) res, err := vars.ExpandCurlyBrace(ctx.RenderOptions.Metas["externalTrackerLinkFormat"], ctx.RenderOptions.Metas)
if err != nil { if err != nil {
// here we could just log the error and continue the rendering // here we could just log the error and continue the rendering
log.Error("unable to expand template vars for ref %s, err: %v", ref.Issue, err) log.Error("unable to expand template vars for ref %s, err: %v", ref.Issue, err)
@@ -183,7 +190,7 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
// Decorate action keywords if actionable // Decorate action keywords if actionable
var keyword *html.Node var keyword *html.Node
if references.IsXrefActionable(ref, hasExtTrackFormat) { if references.IsXrefActionable(ref, useExtTrackerLink) {
keyword = createKeyword(ctx, node.Data[ref.ActionLocation.Start:ref.ActionLocation.End]) keyword = createKeyword(ctx, node.Data[ref.ActionLocation.Start:ref.ActionLocation.End])
} else { } else {
keyword = &html.Node{ keyword = &html.Node{
+16 -2
View File
@@ -378,9 +378,23 @@ func FindRenderizableReferenceRegexp(content string, pattern *regexp.Regexp) *Re
return nil return nil
} }
action, location := findActionKeywords([]byte(content), match[2]) // The external tracker pattern can use alternatives with separate capture
// groups. Pick the first group that participated in this match instead of
// assuming the first group always did.
issueStart, issueEnd := -1, -1
for i := 2; i+1 < len(match); i += 2 {
if match[i] >= 0 {
issueStart, issueEnd = match[i], match[i+1]
break
}
}
if issueStart < 0 {
return nil
}
action, location := findActionKeywords([]byte(content), issueStart)
return &RenderizableReference{ return &RenderizableReference{
Issue: content[match[2]:match[3]], Issue: content[issueStart:issueEnd],
RefLocation: &RefSpan{Start: match[0], End: match[1]}, RefLocation: &RefSpan{Start: match[0], End: match[1]},
Action: action, Action: action,
ActionLocation: location, ActionLocation: location,
+2 -2
View File
@@ -2140,15 +2140,15 @@
"repo.settings.external_wiki_url": "External Wiki URL", "repo.settings.external_wiki_url": "External Wiki URL",
"repo.settings.external_wiki_url_error": "The external wiki URL is not a valid URL.", "repo.settings.external_wiki_url_error": "The external wiki URL is not a valid URL.",
"repo.settings.external_wiki_url_desc": "Visitors are redirected to the external wiki URL when clicking the wiki tab.", "repo.settings.external_wiki_url_desc": "Visitors are redirected to the external wiki URL when clicking the wiki tab.",
"repo.settings.issues_desc": "Enable Repository Issue Tracker",
"repo.settings.use_internal_issue_tracker": "Use Built-In Issue Tracker", "repo.settings.use_internal_issue_tracker": "Use Built-In Issue Tracker",
"repo.settings.use_external_issue_tracker": "Use External Issue Tracker", "repo.settings.use_external_issue_tracker": "Use External Issue Tracker",
"repo.settings.external_tracker_url": "External Issue Tracker URL", "repo.settings.external_tracker_url": "External Issue Tracker URL",
"repo.settings.external_tracker_url_error": "The external issue tracker URL is not a valid URL.", "repo.settings.external_tracker_url_error": "The external issue tracker URL is not a valid URL.",
"repo.settings.external_tracker_url_desc": "Visitors are redirected to the external issue tracker URL when clicking on the issues tab.", "repo.settings.external_tracker_url_desc": "When built-in issue tracker is disabled, visitors are redirected to the external issue tracker URL when clicking on the issues tab.",
"repo.settings.tracker_url_format": "External Issue Tracker URL Format", "repo.settings.tracker_url_format": "External Issue Tracker URL Format",
"repo.settings.tracker_url_format_error": "The external issue tracker URL format is not a valid URL.", "repo.settings.tracker_url_format_error": "The external issue tracker URL format is not a valid URL.",
"repo.settings.tracker_issue_style": "External Issue Tracker Number Format", "repo.settings.tracker_issue_style": "External Issue Tracker Number Format",
"repo.settings.tracker_issue_style_desc": "When the internal issue tracker is enabled, the Numeric style can only be used for the internal issue tracker.",
"repo.settings.tracker_issue_style.numeric": "Numeric", "repo.settings.tracker_issue_style.numeric": "Numeric",
"repo.settings.tracker_issue_style.alphanumeric": "Alphanumeric", "repo.settings.tracker_issue_style.alphanumeric": "Alphanumeric",
"repo.settings.tracker_issue_style.regexp": "Regular Expression", "repo.settings.tracker_issue_style.regexp": "Regular Expression",
+22 -29
View File
@@ -25,6 +25,7 @@ import (
"gitea.dev/modules/git" "gitea.dev/modules/git"
"gitea.dev/modules/label" "gitea.dev/modules/label"
"gitea.dev/modules/log" "gitea.dev/modules/log"
"gitea.dev/modules/markup"
"gitea.dev/modules/optional" "gitea.dev/modules/optional"
repo_module "gitea.dev/modules/repository" repo_module "gitea.dev/modules/repository"
"gitea.dev/modules/setting" "gitea.dev/modules/setting"
@@ -612,6 +613,7 @@ func Edit(ctx *context.APIContext) {
} }
if err := updateRepoUnits(ctx, opts); err != nil { if err := updateRepoUnits(ctx, opts); err != nil {
ctx.APIErrorAuto(err)
return return
} }
@@ -750,24 +752,21 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err
// updateRepoUnits updates repo units: Issue settings, Wiki settings, PR settings // updateRepoUnits updates repo units: Issue settings, Wiki settings, PR settings
func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error { func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
owner := ctx.Repo.Owner
repo := ctx.Repo.Repository repo := ctx.Repo.Repository
var units []repo_model.RepoUnit var units []repo_model.RepoUnit
var deleteUnitTypes []unit_model.Type var deleteUnitTypes []unit_model.Type
if opts.HasIssues != nil { if opts.HasIssues != nil && *opts.HasIssues {
if *opts.HasIssues && opts.ExternalTracker != nil && !unit_model.TypeExternalTracker.UnitGlobalDisabled() { if opts.ExternalTracker != nil && !unit_model.TypeExternalTracker.UnitGlobalDisabled() {
// Check that values are valid if (opts.InternalTracker == nil || opts.ExternalTracker.ExternalTrackerURL != "") && !validation.IsValidURL(opts.ExternalTracker.ExternalTrackerURL) {
if !validation.IsValidURL(opts.ExternalTracker.ExternalTrackerURL) { return util.ErrorWrap(util.ErrUnprocessableContent, "external tracker URL not valid")
err := errors.New("External tracker URL not valid")
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return err
} }
if len(opts.ExternalTracker.ExternalTrackerFormat) != 0 && !validation.IsValidExternalTrackerURLFormat(opts.ExternalTracker.ExternalTrackerFormat) { if opts.InternalTracker != nil && (opts.ExternalTracker.ExternalTrackerStyle == "" || opts.ExternalTracker.ExternalTrackerStyle == markup.IssueNameStyleNumeric) {
err := errors.New("External tracker URL format not valid") return util.ErrorWrap(util.ErrUnprocessableContent, "external tracker style Numeric is only used for internal tracker")
ctx.APIError(http.StatusUnprocessableEntity, err.Error()) }
return err if opts.ExternalTracker.ExternalTrackerFormat != "" && !validation.IsValidExternalTrackerURLFormat(opts.ExternalTracker.ExternalTrackerFormat) {
return util.ErrorWrap(util.ErrUnprocessableContent, "External tracker URL format not valid")
} }
units = append(units, repo_model.RepoUnit{ units = append(units, repo_model.RepoUnit{
@@ -780,8 +779,10 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
ExternalTrackerRegexpPattern: opts.ExternalTracker.ExternalTrackerRegexpPattern, ExternalTrackerRegexpPattern: opts.ExternalTracker.ExternalTrackerRegexpPattern,
}, },
}) })
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeIssues) } else {
} else if *opts.HasIssues && opts.ExternalTracker == nil && !unit_model.TypeIssues.UnitGlobalDisabled() { deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker)
}
if (opts.ExternalTracker == nil || opts.InternalTracker != nil) && !unit_model.TypeIssues.UnitGlobalDisabled() {
// Default to built-in tracker // Default to built-in tracker
var config *repo_model.IssuesConfig var config *repo_model.IssuesConfig
@@ -807,24 +808,20 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
Type: unit_model.TypeIssues, Type: unit_model.TypeIssues,
Config: config, Config: config,
}) })
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker) } else {
} else if !*opts.HasIssues { deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeIssues)
if !unit_model.TypeExternalTracker.UnitGlobalDisabled() {
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker)
}
if !unit_model.TypeIssues.UnitGlobalDisabled() {
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeIssues)
}
} }
} }
if opts.HasIssues != nil && !*opts.HasIssues {
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker)
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeIssues)
}
if opts.HasWiki != nil { if opts.HasWiki != nil {
if *opts.HasWiki && opts.ExternalWiki != nil && !unit_model.TypeExternalWiki.UnitGlobalDisabled() { if *opts.HasWiki && opts.ExternalWiki != nil && !unit_model.TypeExternalWiki.UnitGlobalDisabled() {
// Check that values are valid // Check that values are valid
if !validation.IsValidURL(opts.ExternalWiki.ExternalWikiURL) { if !validation.IsValidURL(opts.ExternalWiki.ExternalWikiURL) {
err := errors.New("External wiki URL not valid") return util.ErrorWrap(util.ErrUnprocessableContent, "external wiki URL not valid")
ctx.APIError(http.StatusUnprocessableEntity, "Invalid external wiki URL")
return err
} }
units = append(units, repo_model.RepoUnit{ units = append(units, repo_model.RepoUnit{
@@ -902,7 +899,6 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
// so unrelated PATCH calls don't reject historical configs. // so unrelated PATCH calls don't reject historical configs.
if opts.AllowMergeUpdate != nil || opts.AllowRebaseUpdate != nil || opts.DefaultUpdateStyle != nil { if opts.AllowMergeUpdate != nil || opts.AllowRebaseUpdate != nil || opts.DefaultUpdateStyle != nil {
if err := config.ValidateUpdateSettings(); err != nil { if err := config.ValidateUpdateSettings(); err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return err return err
} }
} }
@@ -977,12 +973,9 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
if len(units)+len(deleteUnitTypes) > 0 { if len(units)+len(deleteUnitTypes) > 0 {
if err := repo_service.UpdateRepositoryUnits(ctx, repo, units, deleteUnitTypes); err != nil { if err := repo_service.UpdateRepositoryUnits(ctx, repo, units, deleteUnitTypes); err != nil {
ctx.APIErrorInternal(err)
return err return err
} }
} }
log.Trace("Repository advanced settings updated: %s/%s", owner.Name, repo.Name)
return nil return nil
} }
+6 -3
View File
@@ -96,10 +96,13 @@ func MustEnableIssues(ctx *context.Context) {
return return
} }
unit, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypeExternalTracker) unitExtTracker, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypeExternalTracker)
if err == nil { if err == nil {
ctx.Redirect(unit.ExternalTrackerConfig().ExternalTrackerURL) extURL := unitExtTracker.ExternalTrackerConfig().ExternalTrackerURL
return if extURL != "" {
ctx.Redirect(extURL)
return
}
} }
} }
+21 -29
View File
@@ -24,6 +24,7 @@ import (
"gitea.dev/modules/indexer/stats" "gitea.dev/modules/indexer/stats"
"gitea.dev/modules/lfs" "gitea.dev/modules/lfs"
"gitea.dev/modules/log" "gitea.dev/modules/log"
"gitea.dev/modules/markup"
"gitea.dev/modules/setting" "gitea.dev/modules/setting"
"gitea.dev/modules/structs" "gitea.dev/modules/structs"
"gitea.dev/modules/templates" "gitea.dev/modules/templates"
@@ -570,10 +571,6 @@ func handleSettingsPostAdvanced(ctx *context.Context) {
var units []repo_model.RepoUnit var units []repo_model.RepoUnit
var deleteUnitTypes []unit_model.Type var deleteUnitTypes []unit_model.Type
// This section doesn't require repo_name/RepoName to be set in the form, don't show it
// as an error on the UI for this action
ctx.Data["Err_RepoName"] = nil
if repo.CloseIssuesViaCommitInAnyBranch != form.EnableCloseIssuesViaCommitInAnyBranch { if repo.CloseIssuesViaCommitInAnyBranch != form.EnableCloseIssuesViaCommitInAnyBranch {
repo.CloseIssuesViaCommitInAnyBranch = form.EnableCloseIssuesViaCommitInAnyBranch repo.CloseIssuesViaCommitInAnyBranch = form.EnableCloseIssuesViaCommitInAnyBranch
repoChanged = true repoChanged = true
@@ -587,8 +584,7 @@ func handleSettingsPostAdvanced(ctx *context.Context) {
if form.EnableWiki && form.EnableExternalWiki && !unit_model.TypeExternalWiki.UnitGlobalDisabled() { if form.EnableWiki && form.EnableExternalWiki && !unit_model.TypeExternalWiki.UnitGlobalDisabled() {
if !validation.IsValidURL(form.ExternalWikiURL) { if !validation.IsValidURL(form.ExternalWikiURL) {
ctx.Flash.Error(ctx.Tr("repo.settings.external_wiki_url_error")) ctx.JSONError(ctx.Tr("repo.settings.external_wiki_url_error"))
ctx.Redirect(repo.Link() + "/settings")
return return
} }
@@ -611,19 +607,21 @@ func handleSettingsPostAdvanced(ctx *context.Context) {
if form.DefaultWikiBranch != "" { if form.DefaultWikiBranch != "" {
if err := wiki_service.ChangeDefaultWikiBranch(ctx, repo, form.DefaultWikiBranch); err != nil { if err := wiki_service.ChangeDefaultWikiBranch(ctx, repo, form.DefaultWikiBranch); err != nil {
log.Error("ChangeDefaultWikiBranch failed, err: %v", err) log.Error("ChangeDefaultWikiBranch failed, err: %v", err)
ctx.Flash.Warning(ctx.Tr("repo.settings.failed_to_change_default_wiki_branch")) ctx.Flash.Warning(ctx.Tr("repo.settings.failed_to_change_default_wiki_branch")) // skip the error, continue, and reload page
} }
} }
if form.EnableIssues && form.EnableExternalTracker && !unit_model.TypeExternalTracker.UnitGlobalDisabled() { if form.EnableExternalTracker && !unit_model.TypeExternalTracker.UnitGlobalDisabled() {
if !validation.IsValidURL(form.ExternalTrackerURL) { if (!form.EnableInternalTracker || form.ExternalTrackerURL != "") && !validation.IsValidURL(form.ExternalTrackerURL) {
ctx.Flash.Error(ctx.Tr("repo.settings.external_tracker_url_error")) ctx.JSONError(ctx.Tr("repo.settings.external_tracker_url_error"))
ctx.Redirect(repo.Link() + "/settings")
return return
} }
if len(form.TrackerURLFormat) != 0 && !validation.IsValidExternalTrackerURLFormat(form.TrackerURLFormat) { if form.TrackerURLFormat != "" && !validation.IsValidExternalTrackerURLFormat(form.TrackerURLFormat) {
ctx.Flash.Error(ctx.Tr("repo.settings.tracker_url_format_error")) ctx.JSONError(ctx.Tr("repo.settings.tracker_url_format_error"))
ctx.Redirect(repo.Link() + "/settings") return
}
if form.EnableInternalTracker && (form.TrackerIssueStyle == "" || form.TrackerIssueStyle == markup.IssueNameStyleNumeric) {
ctx.JSONError(ctx.Tr("repo.settings.tracker_issue_style_desc"))
return return
} }
units = append(units, newRepoUnit(repo, unit_model.TypeExternalTracker, &repo_model.ExternalTrackerConfig{ units = append(units, newRepoUnit(repo, unit_model.TypeExternalTracker, &repo_model.ExternalTrackerConfig{
@@ -632,21 +630,18 @@ func handleSettingsPostAdvanced(ctx *context.Context) {
ExternalTrackerStyle: form.TrackerIssueStyle, ExternalTrackerStyle: form.TrackerIssueStyle,
ExternalTrackerRegexpPattern: form.ExternalTrackerRegexpPattern, ExternalTrackerRegexpPattern: form.ExternalTrackerRegexpPattern,
})) }))
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeIssues) } else {
} else if form.EnableIssues && !form.EnableExternalTracker && !unit_model.TypeIssues.UnitGlobalDisabled() { deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker)
}
if form.EnableInternalTracker && !unit_model.TypeIssues.UnitGlobalDisabled() {
units = append(units, newRepoUnit(repo, unit_model.TypeIssues, &repo_model.IssuesConfig{ units = append(units, newRepoUnit(repo, unit_model.TypeIssues, &repo_model.IssuesConfig{
EnableTimetracker: form.EnableTimetracker, EnableTimetracker: form.EnableTimetracker,
AllowOnlyContributorsToTrackTime: form.AllowOnlyContributorsToTrackTime, AllowOnlyContributorsToTrackTime: form.AllowOnlyContributorsToTrackTime,
EnableDependencies: form.EnableIssueDependencies, EnableDependencies: form.EnableIssueDependencies,
})) }))
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker)
} else { } else {
if !unit_model.TypeExternalTracker.UnitGlobalDisabled() { deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeIssues)
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker)
}
if !unit_model.TypeIssues.UnitGlobalDisabled() {
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeIssues)
}
} }
if form.EnableProjects && !unit_model.TypeProjects.UnitGlobalDisabled() { if form.EnableProjects && !unit_model.TypeProjects.UnitGlobalDisabled() {
@@ -689,8 +684,7 @@ func handleSettingsPostAdvanced(ctx *context.Context) {
DefaultTargetBranch: strings.TrimSpace(form.DefaultTargetBranch), DefaultTargetBranch: strings.TrimSpace(form.DefaultTargetBranch),
} }
if err := prConfig.ValidateUpdateSettings(); err != nil { if err := prConfig.ValidateUpdateSettings(); err != nil {
ctx.Flash.Error(err.Error()) ctx.JSONErrorAuto(err)
ctx.Redirect(repo.Link() + "/settings")
return return
} }
units = append(units, newRepoUnit(repo, unit_model.TypePullRequests, prConfig)) units = append(units, newRepoUnit(repo, unit_model.TypePullRequests, prConfig))
@@ -699,8 +693,7 @@ func handleSettingsPostAdvanced(ctx *context.Context) {
} }
if len(units) == 0 { if len(units) == 0 {
ctx.Flash.Error(ctx.Tr("repo.settings.update_settings_no_unit")) ctx.JSONError(ctx.Tr("repo.settings.update_settings_no_unit"))
ctx.Redirect(ctx.Repo.RepoLink + "/settings")
return return
} }
@@ -714,10 +707,9 @@ func handleSettingsPostAdvanced(ctx *context.Context) {
return return
} }
} }
log.Trace("Repository advanced settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success")) ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
ctx.Redirect(ctx.Repo.RepoLink + "/settings") ctx.JSONRedirect("")
} }
func handleSettingsPostSigning(ctx *context.Context) { func handleSettingsPostSigning(ctx *context.Context) {
+4 -4
View File
@@ -105,12 +105,12 @@ type RepoSettingForm struct {
DefaultWikiBranch string DefaultWikiBranch string
ExternalWikiURL string ExternalWikiURL string
EnableIssues bool EnableInternalTracker bool
EnableExternalTracker bool EnableExternalTracker bool
ExternalTrackerURL string ExternalTrackerURL string `binding:"TrimSpace"`
TrackerURLFormat string TrackerURLFormat string `binding:"TrimSpace"`
TrackerIssueStyle string TrackerIssueStyle string
ExternalTrackerRegexpPattern string ExternalTrackerRegexpPattern string `binding:"TrimSpace"`
EnableCloseIssuesViaCommitInAnyBranch bool EnableCloseIssuesViaCommitInAnyBranch bool
EnableProjects bool EnableProjects bool
+19 -20
View File
@@ -303,7 +303,7 @@
{{ctx.Locale.Tr "repo.settings.advanced_settings"}} {{ctx.Locale.Tr "repo.settings.advanced_settings"}}
</h4> </h4>
<div class="ui attached segment"> <div class="ui attached segment">
<form class="ui form" method="post"> <form class="ui form form-fetch-action" method="post">
<input type="hidden" name="action" value="advanced"> <input type="hidden" name="action" value="advanced">
{{$isCodeEnabled := .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeCode}} {{$isCodeEnabled := .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeCode}}
@@ -357,25 +357,19 @@
<div class="divider"></div> <div class="divider"></div>
{{$isIssuesEnabled := or (.Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeIssues) (.Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeExternalTracker)}}
{{$isIssuesGlobalDisabled := ctx.Consts.RepoUnitTypeIssues.UnitGlobalDisabled}} {{$isIssuesGlobalDisabled := ctx.Consts.RepoUnitTypeIssues.UnitGlobalDisabled}}
{{$isExternalTrackerGlobalDisabled := ctx.Consts.RepoUnitTypeExternalTracker.UnitGlobalDisabled}} {{$isExternalTrackerGlobalDisabled := ctx.Consts.RepoUnitTypeExternalTracker.UnitGlobalDisabled}}
{{$isIssuesAndExternalGlobalDisabled := and $isIssuesGlobalDisabled $isExternalTrackerGlobalDisabled}} {{$isIssuesAndExternalGlobalDisabled := and $isIssuesGlobalDisabled $isExternalTrackerGlobalDisabled}}
<div class="inline field"> <div class="field">
<label>{{ctx.Locale.Tr "repo.issues"}}</label> <label>{{ctx.Locale.Tr "repo.issues"}}</label>
<div class="ui checkbox{{if $isIssuesAndExternalGlobalDisabled}} disabled{{end}}"{{if $isIssuesAndExternalGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}> {{$isInternalTrackerEnabled := .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeIssues}}
<input class="enable-system" name="enable_issues" type="checkbox" data-target="#issue_box" {{if $isIssuesEnabled}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.issues_desc"}}</label>
</div>
</div>
<div class="field {{if not $isIssuesEnabled}}disabled{{end}}" id="issue_box">
<div class="field"> <div class="field">
<div class="ui radio checkbox{{if $isIssuesGlobalDisabled}} disabled{{end}}"{{if $isIssuesGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}> <div class="ui checkbox {{if $isIssuesGlobalDisabled}}disabled{{end}}"{{if $isIssuesGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
<input class="enable-system-radio" name="enable_external_tracker" type="radio" value="false" data-context="#internal_issue_box" data-target="#external_issue_box" {{if not (.Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeExternalTracker)}}checked{{end}}> <input class="enable-system" name="enable_internal_tracker" type="checkbox" data-target="#internal_issue_box" {{if $isInternalTrackerEnabled}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.use_internal_issue_tracker"}}</label> <label>{{ctx.Locale.Tr "repo.settings.use_internal_issue_tracker"}}</label>
</div> </div>
</div> </div>
<div class="field tw-pl-4 {{if (.Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeExternalTracker)}}disabled{{end}}" id="internal_issue_box"> <div class="field tw-pl-4 {{if not $isInternalTrackerEnabled}}disabled{{end}}" id="internal_issue_box">
{{if .Repository.CanEnableTimetracker}} {{if .Repository.CanEnableTimetracker}}
<div class="field"> <div class="field">
<div class="ui checkbox"> <div class="ui checkbox">
@@ -401,13 +395,15 @@
<label>{{ctx.Locale.Tr "repo.settings.admin_enable_close_issues_via_commit_in_any_branch"}}</label> <label>{{ctx.Locale.Tr "repo.settings.admin_enable_close_issues_via_commit_in_any_branch"}}</label>
</div> </div>
</div> </div>
{{$isExternalTrackerEnabled := .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeExternalTracker}}
<div class="field"> <div class="field">
<div class="ui radio checkbox{{if $isExternalTrackerGlobalDisabled}} disabled{{end}}"{{if $isExternalTrackerGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}> <div class="ui checkbox {{if $isExternalTrackerGlobalDisabled}}disabled{{end}}" {{if $isExternalTrackerGlobalDisabled}}data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
<input class="enable-system-radio" name="enable_external_tracker" type="radio" value="true" data-context="#internal_issue_box" data-target="#external_issue_box" {{if .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeExternalTracker}}checked{{end}}> <input class="enable-system" name="enable_external_tracker" type="checkbox" data-target="#external_issue_box" {{if $isExternalTrackerEnabled}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.use_external_issue_tracker"}}</label> <label>{{ctx.Locale.Tr "repo.settings.use_external_issue_tracker"}}</label>
</div> </div>
</div> </div>
<div class="field tw-pl-4 {{if not (.Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeExternalTracker)}}disabled{{end}}" id="external_issue_box"> <div class="field tw-pl-4 {{if not $isExternalTrackerEnabled}}disabled{{end}}" id="external_issue_box">
<div class="field"> <div class="field">
<label for="external_tracker_url">{{ctx.Locale.Tr "repo.settings.external_tracker_url"}}</label> <label for="external_tracker_url">{{ctx.Locale.Tr "repo.settings.external_tracker_url"}}</label>
<input id="external_tracker_url" name="external_tracker_url" type="url" value="{{(.Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeExternalTracker).ExternalTrackerConfig.ExternalTrackerURL}}"> <input id="external_tracker_url" name="external_tracker_url" type="url" value="{{(.Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeExternalTracker).ExternalTrackerConfig.ExternalTrackerURL}}">
@@ -418,9 +414,12 @@
<input id="tracker_url_format" name="tracker_url_format" type="url" value="{{(.Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeExternalTracker).ExternalTrackerConfig.ExternalTrackerFormat}}" placeholder="https://github.com/{user}/{repo}/issues/{index}"> <input id="tracker_url_format" name="tracker_url_format" type="url" value="{{(.Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeExternalTracker).ExternalTrackerConfig.ExternalTrackerFormat}}" placeholder="https://github.com/{user}/{repo}/issues/{index}">
<p class="help">{{ctx.Locale.Tr "repo.settings.tracker_url_format_desc"}}</p> <p class="help">{{ctx.Locale.Tr "repo.settings.tracker_url_format_desc"}}</p>
</div> </div>
<div class="inline fields"> <div class="field tw-m-0">
<label for="issue_style">{{ctx.Locale.Tr "repo.settings.tracker_issue_style"}}</label> <label>{{ctx.Locale.Tr "repo.settings.tracker_issue_style"}}</label>
<div class="field"> <p class="help">{{ctx.Locale.Tr "repo.settings.tracker_issue_style_desc"}}</p>
</div>
<div class="flex-text-block flex-wrap tw-gap-4 tw-mb-2">
<div>
<div class="ui radio checkbox"> <div class="ui radio checkbox">
{{$externalTracker := (.Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeExternalTracker)}} {{$externalTracker := (.Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeExternalTracker)}}
{{$externalTrackerStyle := $externalTracker.ExternalTrackerConfig.ExternalTrackerStyle}} {{$externalTrackerStyle := $externalTracker.ExternalTrackerConfig.ExternalTrackerStyle}}
@@ -428,13 +427,13 @@
<label>{{ctx.Locale.Tr "repo.settings.tracker_issue_style.numeric"}} <span class="ui light grey text">#1234</span></label> <label>{{ctx.Locale.Tr "repo.settings.tracker_issue_style.numeric"}} <span class="ui light grey text">#1234</span></label>
</div> </div>
</div> </div>
<div class="field"> <div>
<div class="ui radio checkbox"> <div class="ui radio checkbox">
<input class="js-tracker-issue-style" name="tracker_issue_style" type="radio" value="alphanumeric" {{if eq $externalTrackerStyle "alphanumeric"}}checked{{end}}> <input class="js-tracker-issue-style" name="tracker_issue_style" type="radio" value="alphanumeric" {{if eq $externalTrackerStyle "alphanumeric"}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.tracker_issue_style.alphanumeric"}} <span class="ui light grey text">ABC-123 , DEFG-234</span></label> <label>{{ctx.Locale.Tr "repo.settings.tracker_issue_style.alphanumeric"}} <span class="ui light grey text">ABC-123 , DEFG-234</span></label>
</div> </div>
</div> </div>
<div class="field"> <div>
<div class="ui radio checkbox"> <div class="ui radio checkbox">
<input class="js-tracker-issue-style" name="tracker_issue_style" type="radio" value="regexp" {{if eq $externalTrackerStyle "regexp"}}checked{{end}}> <input class="js-tracker-issue-style" name="tracker_issue_style" type="radio" value="regexp" {{if eq $externalTrackerStyle "regexp"}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.tracker_issue_style.regexp"}} <span class="ui light grey text">(ISSUE-\d+) , ISSUE-(\d+)</span></label> <label>{{ctx.Locale.Tr "repo.settings.tracker_issue_style.regexp"}} <span class="ui light grey text">(ISSUE-\d+) , ISSUE-(\d+)</span></label>
+11 -3
View File
@@ -16,7 +16,10 @@ import (
"gitea.dev/models/unittest" "gitea.dev/models/unittest"
user_model "gitea.dev/models/user" user_model "gitea.dev/models/user"
"gitea.dev/modules/git" "gitea.dev/modules/git"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs" api "gitea.dev/modules/structs"
"gitea.dev/modules/test"
"gitea.dev/services/migrations"
mirror_service "gitea.dev/services/mirror" mirror_service "gitea.dev/services/mirror"
"gitea.dev/tests" "gitea.dev/tests"
@@ -42,7 +45,8 @@ func getRepoEditOptionFromRepo(repo *repo_model.Repository) *api.EditRepoOption
AllowOnlyContributorsToTrackTime: config.AllowOnlyContributorsToTrackTime, AllowOnlyContributorsToTrackTime: config.AllowOnlyContributorsToTrackTime,
EnableIssueDependencies: config.EnableDependencies, EnableIssueDependencies: config.EnableDependencies,
} }
} else if unit, err := repo.GetUnit(ctx, unit_model.TypeExternalTracker); err == nil { }
if unit, err := repo.GetUnit(ctx, unit_model.TypeExternalTracker); err == nil {
config := unit.ExternalTrackerConfig() config := unit.ExternalTrackerConfig()
hasIssues = true hasIssues = true
externalTracker = &api.ExternalTracker{ externalTracker = &api.ExternalTracker{
@@ -460,6 +464,10 @@ func TestAPIRepoEdit(t *testing.T) {
require.NoError(t, mirror_service.UpdateAddress(ctx, mirror, "https://existing-user:existing-password@example.com/user2/repo1.git")) require.NoError(t, mirror_service.UpdateAddress(ctx, mirror, "https://existing-user:existing-password@example.com/user2/repo1.git"))
defer migrations.Init()
defer test.MockVariableValue(&setting.Migrations.AllowedDomains, "*")()
_ = migrations.Init()
req = NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/repos/%s/%s", mirrorRepo.OwnerName, mirrorRepo.Name), &api.EditRepoOption{ req = NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/repos/%s/%s", mirrorRepo.OwnerName, mirrorRepo.Name), &api.EditRepoOption{
MirrorPassword: &newPassword, MirrorPassword: &newPassword,
}).AddTokenAuth(token2) }).AddTokenAuth(token2)
@@ -521,7 +529,7 @@ func TestAPIRepoEditPullUpdateSettingsValidation(t *testing.T) {
AllowMergeUpdate: &allowMergeUpdate, AllowMergeUpdate: &allowMergeUpdate,
AllowRebaseUpdate: &allowRebaseUpdate, AllowRebaseUpdate: &allowRebaseUpdate,
}).AddTokenAuth(token) }).AddTokenAuth(token)
MakeRequest(t, req, http.StatusUnprocessableEntity) MakeRequest(t, req, http.StatusBadRequest)
allowRebaseUpdate = true allowRebaseUpdate = true
defaultUpdateStyle := string(repo_model.UpdateStyleMerge) defaultUpdateStyle := string(repo_model.UpdateStyleMerge)
@@ -530,5 +538,5 @@ func TestAPIRepoEditPullUpdateSettingsValidation(t *testing.T) {
AllowRebaseUpdate: &allowRebaseUpdate, AllowRebaseUpdate: &allowRebaseUpdate,
DefaultUpdateStyle: &defaultUpdateStyle, DefaultUpdateStyle: &defaultUpdateStyle,
}).AddTokenAuth(token) }).AddTokenAuth(token)
MakeRequest(t, req, http.StatusUnprocessableEntity) MakeRequest(t, req, http.StatusBadRequest)
} }
+7
View File
@@ -443,4 +443,11 @@ export function initGlobalFetchAction() {
}); });
registerGlobalSelectorFunc('[data-fetch-url]', initFetchActionTrigger); registerGlobalSelectorFunc('[data-fetch-url]', initFetchActionTrigger);
// when the page is reloaded after a fetch action, scroll to the flash message if any
const elFlashMessage = document.querySelector('.ui.message.flash-message');
if (elFlashMessage) {
window.history.scrollRestoration = 'manual';
elFlashMessage?.scrollIntoView({block: 'center'});
}
} }