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
+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) {