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,
}
unitExternalTracker, err := repo.GetUnit(ctx, unit.TypeExternalTracker)
if err == nil {
metas["format"] = unitExternalTracker.ExternalTrackerConfig().ExternalTrackerFormat
unitInternalTracker, _ := repo.GetUnit(ctx, unit.TypeIssues)
unitExternalTracker, _ := repo.GetUnit(ctx, unit.TypeExternalTracker)
if unitInternalTracker != nil {
metas["internalTrackerEnabled"] = "true"
}
if unitExternalTracker != nil {
metas["externalTrackerLinkFormat"] = unitExternalTracker.ExternalTrackerConfig().ExternalTrackerFormat
switch unitExternalTracker.ExternalTrackerConfig().ExternalTrackerStyle {
case 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, "testRepo", metas["repo"])
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)
+15 -9
View File
@@ -39,7 +39,7 @@ func link(href, class, contents string) string {
}
var numericMetas = map[string]string{
"format": "https://someurl.com/{user}/{repo}/{index}",
"externalTrackerLinkFormat": "https://someurl.com/{user}/{repo}/{index}",
"user": "someUser",
"repo": "someRepo",
"style": IssueNameStyleNumeric,
@@ -47,7 +47,7 @@ var numericMetas = map[string]string{
}
var alphanumericMetas = map[string]string{
"format": "https://someurl.com/{user}/{repo}/{index}",
"externalTrackerLinkFormat": "https://someurl.com/{user}/{repo}/{index}",
"user": "someUser",
"repo": "someRepo",
"style": IssueNameStyleAlphanumeric,
@@ -55,10 +55,10 @@ var alphanumericMetas = map[string]string{
}
var regexpMetas = map[string]string{
"format": "https://someurl.com/{user}/{repo}/{index}",
"user": "someUser",
"repo": "someRepo",
"style": IssueNameStyleRegexp,
"externalTrackerLinkFormat": "https://someurl.com/{user}/{repo}/{index}",
"user": "someUser",
"repo": "someRepo",
"style": IssueNameStyleRegexp,
}
// 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",
"ISSUE-(\\d+)",
`ISSUE-(\d+)`,
[]string{"123"},
[]string{"ISSUE-123"},
)
test("abc (ISSUE 123) def", "abc %s def",
"\\(ISSUE (\\d+)\\)",
`\(ISSUE (\d+)\)`,
[]string{"123"},
[]string{"(ISSUE 123)"},
)
test("abc ISSUE-123 def", "abc %s def",
"(ISSUE-(\\d+))",
`(ISSUE-(\d+))`,
[]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))
}
+14 -7
View File
@@ -110,16 +110,23 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
next := node.NextSibling
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
// 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
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"] {
case "", IssueNameStyleNumeric:
ref = refNumeric
// when internal tracker is enabled, Numeric (#123) style should only be use for internal tracker
useExtTrackerLink = !hasInternalTracker
case IssueNameStyleAlphanumeric:
ref = references.FindRenderizableReferenceAlphanumeric(node.Data)
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
// 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
// Allow a free-pass when non-numeric pattern wasn't found.
if ref == nil || refNumeric.RefLocation.Start < ref.RefLocation.Start {
@@ -146,10 +153,10 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
var link *html.Node
refText := node.Data[ref.RefLocation.Start:ref.RefLocation.End]
if hasExtTrackFormat && !ref.IsPull {
if useExtTrackerLink && !ref.IsPull {
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 {
// 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)
@@ -183,7 +190,7 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
// Decorate action keywords if actionable
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])
} else {
keyword = &html.Node{
+16 -2
View File
@@ -378,9 +378,23 @@ func FindRenderizableReferenceRegexp(content string, pattern *regexp.Regexp) *Re
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{
Issue: content[match[2]:match[3]],
Issue: content[issueStart:issueEnd],
RefLocation: &RefSpan{Start: match[0], End: match[1]},
Action: action,
ActionLocation: location,
+2 -2
View File
@@ -2140,15 +2140,15 @@
"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_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_external_issue_tracker": "Use External Issue Tracker",
"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_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_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_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.alphanumeric": "Alphanumeric",
"repo.settings.tracker_issue_style.regexp": "Regular Expression",
+22 -29
View File
@@ -25,6 +25,7 @@ import (
"gitea.dev/modules/git"
"gitea.dev/modules/label"
"gitea.dev/modules/log"
"gitea.dev/modules/markup"
"gitea.dev/modules/optional"
repo_module "gitea.dev/modules/repository"
"gitea.dev/modules/setting"
@@ -612,6 +613,7 @@ func Edit(ctx *context.APIContext) {
}
if err := updateRepoUnits(ctx, opts); err != nil {
ctx.APIErrorAuto(err)
return
}
@@ -750,24 +752,21 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err
// updateRepoUnits updates repo units: Issue settings, Wiki settings, PR settings
func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
owner := ctx.Repo.Owner
repo := ctx.Repo.Repository
var units []repo_model.RepoUnit
var deleteUnitTypes []unit_model.Type
if opts.HasIssues != nil {
if *opts.HasIssues && opts.ExternalTracker != nil && !unit_model.TypeExternalTracker.UnitGlobalDisabled() {
// Check that values are valid
if !validation.IsValidURL(opts.ExternalTracker.ExternalTrackerURL) {
err := errors.New("External tracker URL not valid")
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return err
if opts.HasIssues != nil && *opts.HasIssues {
if opts.ExternalTracker != nil && !unit_model.TypeExternalTracker.UnitGlobalDisabled() {
if (opts.InternalTracker == nil || opts.ExternalTracker.ExternalTrackerURL != "") && !validation.IsValidURL(opts.ExternalTracker.ExternalTrackerURL) {
return util.ErrorWrap(util.ErrUnprocessableContent, "external tracker URL not valid")
}
if len(opts.ExternalTracker.ExternalTrackerFormat) != 0 && !validation.IsValidExternalTrackerURLFormat(opts.ExternalTracker.ExternalTrackerFormat) {
err := errors.New("External tracker URL format not valid")
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return err
if opts.InternalTracker != nil && (opts.ExternalTracker.ExternalTrackerStyle == "" || opts.ExternalTracker.ExternalTrackerStyle == markup.IssueNameStyleNumeric) {
return util.ErrorWrap(util.ErrUnprocessableContent, "external tracker style Numeric is only used for internal tracker")
}
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{
@@ -780,8 +779,10 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
ExternalTrackerRegexpPattern: opts.ExternalTracker.ExternalTrackerRegexpPattern,
},
})
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeIssues)
} else if *opts.HasIssues && opts.ExternalTracker == nil && !unit_model.TypeIssues.UnitGlobalDisabled() {
} else {
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker)
}
if (opts.ExternalTracker == nil || opts.InternalTracker != nil) && !unit_model.TypeIssues.UnitGlobalDisabled() {
// Default to built-in tracker
var config *repo_model.IssuesConfig
@@ -807,24 +808,20 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
Type: unit_model.TypeIssues,
Config: config,
})
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker)
} else if !*opts.HasIssues {
if !unit_model.TypeExternalTracker.UnitGlobalDisabled() {
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker)
}
if !unit_model.TypeIssues.UnitGlobalDisabled() {
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeIssues)
}
} else {
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 && opts.ExternalWiki != nil && !unit_model.TypeExternalWiki.UnitGlobalDisabled() {
// Check that values are valid
if !validation.IsValidURL(opts.ExternalWiki.ExternalWikiURL) {
err := errors.New("External wiki URL not valid")
ctx.APIError(http.StatusUnprocessableEntity, "Invalid external wiki URL")
return err
return util.ErrorWrap(util.ErrUnprocessableContent, "external wiki URL not valid")
}
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.
if opts.AllowMergeUpdate != nil || opts.AllowRebaseUpdate != nil || opts.DefaultUpdateStyle != nil {
if err := config.ValidateUpdateSettings(); err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return err
}
}
@@ -977,12 +973,9 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
if len(units)+len(deleteUnitTypes) > 0 {
if err := repo_service.UpdateRepositoryUnits(ctx, repo, units, deleteUnitTypes); err != nil {
ctx.APIErrorInternal(err)
return err
}
}
log.Trace("Repository advanced settings updated: %s/%s", owner.Name, repo.Name)
return nil
}
+6 -3
View File
@@ -96,10 +96,13 @@ func MustEnableIssues(ctx *context.Context) {
return
}
unit, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypeExternalTracker)
unitExtTracker, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypeExternalTracker)
if err == nil {
ctx.Redirect(unit.ExternalTrackerConfig().ExternalTrackerURL)
return
extURL := unitExtTracker.ExternalTrackerConfig().ExternalTrackerURL
if extURL != "" {
ctx.Redirect(extURL)
return
}
}
}
+21 -29
View File
@@ -24,6 +24,7 @@ import (
"gitea.dev/modules/indexer/stats"
"gitea.dev/modules/lfs"
"gitea.dev/modules/log"
"gitea.dev/modules/markup"
"gitea.dev/modules/setting"
"gitea.dev/modules/structs"
"gitea.dev/modules/templates"
@@ -570,10 +571,6 @@ func handleSettingsPostAdvanced(ctx *context.Context) {
var units []repo_model.RepoUnit
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 {
repo.CloseIssuesViaCommitInAnyBranch = form.EnableCloseIssuesViaCommitInAnyBranch
repoChanged = true
@@ -587,8 +584,7 @@ func handleSettingsPostAdvanced(ctx *context.Context) {
if form.EnableWiki && form.EnableExternalWiki && !unit_model.TypeExternalWiki.UnitGlobalDisabled() {
if !validation.IsValidURL(form.ExternalWikiURL) {
ctx.Flash.Error(ctx.Tr("repo.settings.external_wiki_url_error"))
ctx.Redirect(repo.Link() + "/settings")
ctx.JSONError(ctx.Tr("repo.settings.external_wiki_url_error"))
return
}
@@ -611,19 +607,21 @@ func handleSettingsPostAdvanced(ctx *context.Context) {
if form.DefaultWikiBranch != "" {
if err := wiki_service.ChangeDefaultWikiBranch(ctx, repo, form.DefaultWikiBranch); err != nil {
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 !validation.IsValidURL(form.ExternalTrackerURL) {
ctx.Flash.Error(ctx.Tr("repo.settings.external_tracker_url_error"))
ctx.Redirect(repo.Link() + "/settings")
if form.EnableExternalTracker && !unit_model.TypeExternalTracker.UnitGlobalDisabled() {
if (!form.EnableInternalTracker || form.ExternalTrackerURL != "") && !validation.IsValidURL(form.ExternalTrackerURL) {
ctx.JSONError(ctx.Tr("repo.settings.external_tracker_url_error"))
return
}
if len(form.TrackerURLFormat) != 0 && !validation.IsValidExternalTrackerURLFormat(form.TrackerURLFormat) {
ctx.Flash.Error(ctx.Tr("repo.settings.tracker_url_format_error"))
ctx.Redirect(repo.Link() + "/settings")
if form.TrackerURLFormat != "" && !validation.IsValidExternalTrackerURLFormat(form.TrackerURLFormat) {
ctx.JSONError(ctx.Tr("repo.settings.tracker_url_format_error"))
return
}
if form.EnableInternalTracker && (form.TrackerIssueStyle == "" || form.TrackerIssueStyle == markup.IssueNameStyleNumeric) {
ctx.JSONError(ctx.Tr("repo.settings.tracker_issue_style_desc"))
return
}
units = append(units, newRepoUnit(repo, unit_model.TypeExternalTracker, &repo_model.ExternalTrackerConfig{
@@ -632,21 +630,18 @@ func handleSettingsPostAdvanced(ctx *context.Context) {
ExternalTrackerStyle: form.TrackerIssueStyle,
ExternalTrackerRegexpPattern: form.ExternalTrackerRegexpPattern,
}))
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeIssues)
} else if form.EnableIssues && !form.EnableExternalTracker && !unit_model.TypeIssues.UnitGlobalDisabled() {
} else {
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker)
}
if form.EnableInternalTracker && !unit_model.TypeIssues.UnitGlobalDisabled() {
units = append(units, newRepoUnit(repo, unit_model.TypeIssues, &repo_model.IssuesConfig{
EnableTimetracker: form.EnableTimetracker,
AllowOnlyContributorsToTrackTime: form.AllowOnlyContributorsToTrackTime,
EnableDependencies: form.EnableIssueDependencies,
}))
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker)
} else {
if !unit_model.TypeExternalTracker.UnitGlobalDisabled() {
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker)
}
if !unit_model.TypeIssues.UnitGlobalDisabled() {
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeIssues)
}
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeIssues)
}
if form.EnableProjects && !unit_model.TypeProjects.UnitGlobalDisabled() {
@@ -689,8 +684,7 @@ func handleSettingsPostAdvanced(ctx *context.Context) {
DefaultTargetBranch: strings.TrimSpace(form.DefaultTargetBranch),
}
if err := prConfig.ValidateUpdateSettings(); err != nil {
ctx.Flash.Error(err.Error())
ctx.Redirect(repo.Link() + "/settings")
ctx.JSONErrorAuto(err)
return
}
units = append(units, newRepoUnit(repo, unit_model.TypePullRequests, prConfig))
@@ -699,8 +693,7 @@ func handleSettingsPostAdvanced(ctx *context.Context) {
}
if len(units) == 0 {
ctx.Flash.Error(ctx.Tr("repo.settings.update_settings_no_unit"))
ctx.Redirect(ctx.Repo.RepoLink + "/settings")
ctx.JSONError(ctx.Tr("repo.settings.update_settings_no_unit"))
return
}
@@ -714,10 +707,9 @@ func handleSettingsPostAdvanced(ctx *context.Context) {
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.Redirect(ctx.Repo.RepoLink + "/settings")
ctx.JSONRedirect("")
}
func handleSettingsPostSigning(ctx *context.Context) {
+4 -4
View File
@@ -105,12 +105,12 @@ type RepoSettingForm struct {
DefaultWikiBranch string
ExternalWikiURL string
EnableIssues bool
EnableInternalTracker bool
EnableExternalTracker bool
ExternalTrackerURL string
TrackerURLFormat string
ExternalTrackerURL string `binding:"TrimSpace"`
TrackerURLFormat string `binding:"TrimSpace"`
TrackerIssueStyle string
ExternalTrackerRegexpPattern string
ExternalTrackerRegexpPattern string `binding:"TrimSpace"`
EnableCloseIssuesViaCommitInAnyBranch bool
EnableProjects bool
+19 -20
View File
@@ -303,7 +303,7 @@
{{ctx.Locale.Tr "repo.settings.advanced_settings"}}
</h4>
<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">
{{$isCodeEnabled := .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeCode}}
@@ -357,25 +357,19 @@
<div class="divider"></div>
{{$isIssuesEnabled := or (.Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeIssues) (.Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeExternalTracker)}}
{{$isIssuesGlobalDisabled := ctx.Consts.RepoUnitTypeIssues.UnitGlobalDisabled}}
{{$isExternalTrackerGlobalDisabled := ctx.Consts.RepoUnitTypeExternalTracker.UnitGlobalDisabled}}
{{$isIssuesAndExternalGlobalDisabled := and $isIssuesGlobalDisabled $isExternalTrackerGlobalDisabled}}
<div class="inline field">
<div class="field">
<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}}>
<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">
{{$isInternalTrackerEnabled := .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeIssues}}
<div class="field">
<div class="ui radio 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}}>
<div class="ui checkbox {{if $isIssuesGlobalDisabled}}disabled{{end}}"{{if $isIssuesGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{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>
</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}}
<div class="field">
<div class="ui checkbox">
@@ -401,13 +395,15 @@
<label>{{ctx.Locale.Tr "repo.settings.admin_enable_close_issues_via_commit_in_any_branch"}}</label>
</div>
</div>
{{$isExternalTrackerEnabled := .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeExternalTracker}}
<div class="field">
<div class="ui radio 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}}>
<div class="ui checkbox {{if $isExternalTrackerGlobalDisabled}}disabled{{end}}" {{if $isExternalTrackerGlobalDisabled}}data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{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>
</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">
<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}}">
@@ -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}">
<p class="help">{{ctx.Locale.Tr "repo.settings.tracker_url_format_desc"}}</p>
</div>
<div class="inline fields">
<label for="issue_style">{{ctx.Locale.Tr "repo.settings.tracker_issue_style"}}</label>
<div class="field">
<div class="field tw-m-0">
<label>{{ctx.Locale.Tr "repo.settings.tracker_issue_style"}}</label>
<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">
{{$externalTracker := (.Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeExternalTracker)}}
{{$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>
</div>
</div>
<div class="field">
<div>
<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}}>
<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 class="field">
<div>
<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}}>
<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"
user_model "gitea.dev/models/user"
"gitea.dev/modules/git"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/modules/test"
"gitea.dev/services/migrations"
mirror_service "gitea.dev/services/mirror"
"gitea.dev/tests"
@@ -42,7 +45,8 @@ func getRepoEditOptionFromRepo(repo *repo_model.Repository) *api.EditRepoOption
AllowOnlyContributorsToTrackTime: config.AllowOnlyContributorsToTrackTime,
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()
hasIssues = true
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"))
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{
MirrorPassword: &newPassword,
}).AddTokenAuth(token2)
@@ -521,7 +529,7 @@ func TestAPIRepoEditPullUpdateSettingsValidation(t *testing.T) {
AllowMergeUpdate: &allowMergeUpdate,
AllowRebaseUpdate: &allowRebaseUpdate,
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusUnprocessableEntity)
MakeRequest(t, req, http.StatusBadRequest)
allowRebaseUpdate = true
defaultUpdateStyle := string(repo_model.UpdateStyleMerge)
@@ -530,5 +538,5 @@ func TestAPIRepoEditPullUpdateSettingsValidation(t *testing.T) {
AllowRebaseUpdate: &allowRebaseUpdate,
DefaultUpdateStyle: &defaultUpdateStyle,
}).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);
// 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'});
}
}