From 4f9f0fc4b86f1995d3d5ac88b98b04df94394672 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Tue, 24 Mar 2026 02:23:42 +0800 Subject: [PATCH 01/13] Fix various trivial problems (#36953) 1. remove `TEST_CONFLICTING_PATCHES_WITH_GIT_APPLY` * it defaults to false and is unlikely to be useful for most users (see #22130) * with new git versions (>= 2.40), "merge-tree" is used, "checkConflictsByTmpRepo" isn't called, the option does nothing. 2. fix fragile `db.Cell2Int64` (new: `CellToInt`) 3. allow more routes in maintenance mode (e.g.: captcha) 4. fix MockLocale html escaping to make it have the same behavior as production locale --- custom/conf/app.example.ini | 3 - models/auth/source.go | 6 +- models/auth/source_test.go | 40 +++---- models/db/convert.go | 22 ++-- models/repo/repo_unit.go | 6 +- modules/setting/repository.go | 2 - modules/translation/mock.go | 41 ++++--- routers/common/maintenancemode.go | 48 ++++---- services/pull/patch.go | 162 +-------------------------- tests/integration/pull_merge_test.go | 81 -------------- 10 files changed, 97 insertions(+), 314 deletions(-) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 803231ff128..5eb4a5e9956 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -1153,9 +1153,6 @@ LEVEL = Info ;; Add co-authored-by and co-committed-by trailers if committer does not match author ;ADD_CO_COMMITTER_TRAILERS = true ;; -;; In addition to testing patches using the three-way merge method, re-test conflicting patches with git apply -;TEST_CONFLICTING_PATCHES_WITH_GIT_APPLY = false -;; ;; Retarget child pull requests to the parent pull request branch target on merge of parent pull request. It only works on merged PRs where the head and base branch target the same repo. ;RETARGET_CHILDREN_ON_MERGE = true ;; diff --git a/models/auth/source.go b/models/auth/source.go index c0b262f870a..7a008f08a8b 100644 --- a/models/auth/source.go +++ b/models/auth/source.go @@ -12,6 +12,7 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" @@ -139,7 +140,10 @@ func init() { // BeforeSet is invoked from XORM before setting the value of a field of this object. func (source *Source) BeforeSet(colName string, val xorm.Cell) { if colName == "type" { - typ := Type(db.Cell2Int64(val)) + typ, _, err := db.CellToInt(val, NoType) + if err != nil { + setting.PanicInDevOrTesting("Unable to convert login source (id=%d) type: %v", source.ID, err) + } constructor, ok := registeredConfigs[typ] if !ok { return diff --git a/models/auth/source_test.go b/models/auth/source_test.go index ebc462c5811..fbd663a64fa 100644 --- a/models/auth/source_test.go +++ b/models/auth/source_test.go @@ -17,13 +17,9 @@ import ( ) type TestSource struct { - auth_model.ConfigBase + auth_model.ConfigBase `json:"-"` - Provider string - ClientID string - ClientSecret string - OpenIDConnectAutoDiscoveryURL string - IconURL string + TestField string } // FromDB fills up a LDAPConfig from serialized format. @@ -37,27 +33,23 @@ func (source *TestSource) ToDB() ([]byte, error) { } func TestDumpAuthSource(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) + require.NoError(t, unittest.PrepareTestDatabase()) authSourceSchema, err := unittest.GetXORMEngine().TableInfo(new(auth_model.Source)) - assert.NoError(t, err) + require.NoError(t, err) auth_model.RegisterTypeConfig(auth_model.OAuth2, new(TestSource)) + source := &auth_model.Source{ + Type: auth_model.OAuth2, + Name: "TestSource", + Cfg: &TestSource{TestField: "TestValue"}, + } + require.NoError(t, auth_model.CreateSource(t.Context(), source)) - auth_model.CreateSource(t.Context(), &auth_model.Source{ - Type: auth_model.OAuth2, - Name: "TestSource", - IsActive: false, - Cfg: &TestSource{ - Provider: "ConvertibleSourceName", - ClientID: "42", - }, - }) - - sb := new(strings.Builder) - - // TODO: this test is quite hacky, it should use a low-level "select" (without model processors) but not a database dump - engine := unittest.GetXORMEngine() - require.NoError(t, engine.DumpTables([]*schemas.Table{authSourceSchema}, sb)) - assert.Contains(t, sb.String(), `"Provider":"ConvertibleSourceName"`) + // intentionally test the "dump" to make sure the dumped JSON is correct: https://github.com/go-gitea/gitea/pull/16847 + sb := &strings.Builder{} + require.NoError(t, unittest.GetXORMEngine().DumpTables([]*schemas.Table{authSourceSchema}, sb)) + // the dumped SQL is something like: + // INSERT INTO `login_source` (`id`, `type`, `name`, `is_active`, `is_sync_enabled`, `two_factor_policy`, `cfg`, `created_unix`, `updated_unix`) VALUES (1,6,'TestSource',0,0,'','{"TestField":"TestValue"}',1774179784,1774179784); + assert.Contains(t, sb.String(), `'{"TestField":"TestValue"}'`) } diff --git a/models/db/convert.go b/models/db/convert.go index 80b0f7b04b4..374dbfd8a82 100644 --- a/models/db/convert.go +++ b/models/db/convert.go @@ -5,12 +5,11 @@ package db import ( "fmt" - "strconv" - "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "xorm.io/xorm" + "xorm.io/xorm/convert" "xorm.io/xorm/schemas" ) @@ -74,15 +73,14 @@ WHERE ST.name ='varchar'`) return err } -// Cell2Int64 converts a xorm.Cell type to int64, -// and handles possible irregular cases. -func Cell2Int64(val xorm.Cell) int64 { - switch (*val).(type) { - case []uint8: - log.Trace("Cell2Int64 ([]uint8): %v", *val) - - v, _ := strconv.ParseInt(string((*val).([]uint8)), 10, 64) - return v +// CellToInt converts a xorm.Cell field value to an int value +func CellToInt[T ~int | int64](cell xorm.Cell, def T) (ret T, has bool, err error) { + if *cell == nil { + return def, false, nil } - return (*val).(int64) + val, err := convert.AsInt64(*cell) + if err != nil { + return def, false, err + } + return T(val), true, err } diff --git a/models/repo/repo_unit.go b/models/repo/repo_unit.go index 18ebd6be541..c32adbbcd43 100644 --- a/models/repo/repo_unit.go +++ b/models/repo/repo_unit.go @@ -227,7 +227,11 @@ func (cfg *ProjectsConfig) IsProjectsAllowed(m ProjectsMode) bool { func (r *RepoUnit) BeforeSet(colName string, val xorm.Cell) { switch colName { case "type": - r.Type = unit.Type(db.Cell2Int64(val)) + var err error + r.Type, _, err = db.CellToInt(val, unit.TypeInvalid) + if err != nil { + setting.PanicInDevOrTesting("Unable to convert repo unit (id=%d) type: %v", r.ID, err) + } switch r.Type { case unit.TypeExternalWiki: r.Config = new(ExternalWikiConfig) diff --git a/modules/setting/repository.go b/modules/setting/repository.go index f4e45d4702f..9195b7ee503 100644 --- a/modules/setting/repository.go +++ b/modules/setting/repository.go @@ -86,7 +86,6 @@ var ( DefaultMergeMessageOfficialApproversOnly bool PopulateSquashCommentWithCommitMessages bool AddCoCommitterTrailers bool - TestConflictingPatchesWithGitApply bool RetargetChildrenOnMerge bool DelayCheckForInactiveDays int DefaultDeleteBranchAfterMerge bool @@ -211,7 +210,6 @@ var ( DefaultMergeMessageOfficialApproversOnly bool PopulateSquashCommentWithCommitMessages bool AddCoCommitterTrailers bool - TestConflictingPatchesWithGitApply bool RetargetChildrenOnMerge bool DelayCheckForInactiveDays int DefaultDeleteBranchAfterMerge bool diff --git a/modules/translation/mock.go b/modules/translation/mock.go index f457271ea54..02b19d9583d 100644 --- a/modules/translation/mock.go +++ b/modules/translation/mock.go @@ -5,8 +5,8 @@ package translation import ( "fmt" + "html" "html/template" - "strings" ) // MockLocale provides a mocked locale without any translations @@ -20,25 +20,40 @@ func (l MockLocale) Language() string { return "en" } -func (l MockLocale) TrString(s string, args ...any) string { - return sprintAny(s, args...) +func (l MockLocale) TrString(format string, args ...any) (ret string) { + ret = format + ":" + for _, arg := range args { + // usually there is no arg or at most 1-2 args, so a simple string concatenation is more efficient + switch v := arg.(type) { + case string: + ret += v + "," + default: + ret += fmt.Sprint(v) + "," + } + } + return ret[:len(ret)-1] } -func (l MockLocale) Tr(s string, args ...any) template.HTML { - return template.HTML(sprintAny(s, args...)) +func (l MockLocale) Tr(format string, args ...any) (ret template.HTML) { + ret = template.HTML(html.EscapeString(format)) + ":" + for _, arg := range args { + // usually there is no arg or at most 1-2 args, so a simple string concatenation is more efficient + switch v := arg.(type) { + case template.HTML: + ret += v + "," + case string: + ret += template.HTML(html.EscapeString(v)) + "," + default: + ret += template.HTML(html.EscapeString(fmt.Sprint(v))) + "," + } + } + return ret[:len(ret)-1] } func (l MockLocale) TrN(cnt any, key1, keyN string, args ...any) template.HTML { - return template.HTML(sprintAny(key1, args...)) + return l.Tr(key1, args...) } func (l MockLocale) PrettyNumber(v any) string { return fmt.Sprint(v) } - -func sprintAny(s string, args ...any) string { - if len(args) == 0 { - return s - } - return s + ":" + fmt.Sprintf(strings.Repeat(",%v", len(args))[1:], args...) -} diff --git a/routers/common/maintenancemode.go b/routers/common/maintenancemode.go index b5827ac94f4..adf099df573 100644 --- a/routers/common/maintenancemode.go +++ b/routers/common/maintenancemode.go @@ -7,29 +7,39 @@ import ( "net/http" "strings" + "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/setting" ) -func isMaintenanceModeAllowedRequest(req *http.Request) bool { - if strings.HasPrefix(req.URL.Path, "/-/") { - // URLs like "/-/admin", "/-/fetch-redirect" and "/-/markup" are still accessible in maintenance mode - return true - } - if strings.HasPrefix(req.URL.Path, "/api/internal/") { - // internal APIs should be allowed - return true - } - if strings.HasPrefix(req.URL.Path, "/user/") { - // URLs like "/user/signin" and "/user/signup" are still accessible in maintenance mode - return true - } - if strings.HasPrefix(req.URL.Path, "/assets/") { - return true - } - return false -} - func MaintenanceModeHandler() func(h http.Handler) http.Handler { + allowedPrefixes := []string{ + "/.well-known/", + "/assets/", + "/avatars/", + + // admin: "/-/admin" + // general-purpose URLs: "/-/fetch-redirect", "/-/markup", etc. + "/-/", + + // internal APIs + "/api/internal/", + + // user login (for admin to login): "/user/login", "/user/logout", "/catpcha/..." + "/user/", + "/captcha/", + } + allowedPaths := container.SetOf( + "/api/healthz", + ) + isMaintenanceModeAllowedRequest := func(req *http.Request) bool { + for _, prefix := range allowedPrefixes { + if strings.HasPrefix(req.URL.Path, prefix) { + return true + } + } + return allowedPaths.Contains(req.URL.Path) + } + return func(next http.Handler) http.Handler { return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { maintenanceMode := setting.Config().Instance.MaintenanceMode.Value(req.Context()) diff --git a/services/pull/patch.go b/services/pull/patch.go index 30f07f89312..114a437cbc3 100644 --- a/services/pull/patch.go +++ b/services/pull/patch.go @@ -5,7 +5,6 @@ package pull import ( - "bufio" "context" "fmt" "io" @@ -15,15 +14,12 @@ import ( git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" - "code.gitea.io/gitea/models/unit" - "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git/gitcmd" "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/glob" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/process" - "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" ) @@ -57,15 +53,6 @@ func DownloadDiffOrPatch(ctx context.Context, pr *issues_model.PullRequest, w io return nil } -var patchErrorSuffices = []string{ - ": already exists in index", - ": patch does not apply", - ": already exists in working directory", - "unrecognized input", - ": No such file or directory", - ": does not exist in index", -} - func checkPullRequestBranchMergeable(ctx context.Context, pr *issues_model.PullRequest) error { ctx, _, finished := process.GetManager().AddContext(ctx, fmt.Sprintf("checkPullRequestBranchMergeable: %s", pr)) defer finished() @@ -349,151 +336,10 @@ func checkConflictsByTmpRepo(ctx context.Context, pr *issues_model.PullRequest, } // 3. OK the three-way merge method has detected conflicts - // 3a. Are still testing with GitApply? If not set the conflict status and move on - if !setting.Repository.PullRequest.TestConflictingPatchesWithGitApply { - pr.Status = issues_model.PullRequestStatusConflict - pr.ConflictedFiles = conflictFiles - - log.Trace("Found %d files conflicted: %v", len(pr.ConflictedFiles), pr.ConflictedFiles) - return true, nil - } - - // 3b. Create a plain patch from head to base - tmpPatchFile, cleanup, err := setting.AppDataTempDir("git-repo-content").CreateTempFileRandom("patch") - if err != nil { - log.Error("Unable to create temporary patch file! Error: %v", err) - return false, fmt.Errorf("unable to create temporary patch file! Error: %w", err) - } - defer cleanup() - - if err := gitRepo.GetDiffBinary(pr.MergeBase+"...tracking", tmpPatchFile); err != nil { - log.Error("Unable to get patch file from %s to %s in %s Error: %v", pr.MergeBase, pr.HeadBranch, pr.BaseRepo.FullName(), err) - return false, fmt.Errorf("unable to get patch file from %s to %s in %s Error: %w", pr.MergeBase, pr.HeadBranch, pr.BaseRepo.FullName(), err) - } - stat, err := tmpPatchFile.Stat() - if err != nil { - return false, fmt.Errorf("unable to stat patch file: %w", err) - } - patchPath := tmpPatchFile.Name() - tmpPatchFile.Close() - - // 3c. if the size of that patch is 0 - there can be no conflicts! - if stat.Size() == 0 { - log.Debug("PullRequest[%d]: Patch is empty - ignoring", pr.ID) - pr.Status = issues_model.PullRequestStatusEmpty - return false, nil - } - - log.Trace("PullRequest[%d].checkPullRequestMergeableByTmpRepo (patchPath): %s", pr.ID, patchPath) - - // 4. Read the base branch in to the index of the temporary repository - _, _, err = gitcmd.NewCommand("read-tree", tmpRepoBaseBranch).WithDir(tmpBasePath).RunStdString(ctx) - if err != nil { - return false, fmt.Errorf("git read-tree %s: %w", pr.BaseBranch, err) - } - - // 5. Now get the pull request configuration to check if we need to ignore whitespace - prUnit, err := pr.BaseRepo.GetUnit(ctx, unit.TypePullRequests) - if err != nil { - return false, err - } - prConfig := prUnit.PullRequestsConfig() - - // 6. Prepare the arguments to apply the patch against the index - cmdApply := gitcmd.NewCommand("apply", "--check", "--cached") - if prConfig.IgnoreWhitespaceConflicts { - cmdApply.AddArguments("--ignore-whitespace") - } - is3way := false - if git.DefaultFeatures().CheckVersionAtLeast("2.32.0") { - cmdApply.AddArguments("--3way") - is3way = true - } - cmdApply.AddDynamicArguments(patchPath) - - // 7. Prep the pipe: - // - Here we could do the equivalent of: - // `git apply --check --cached patch_file > conflicts` - // Then iterate through the conflicts. However, that means storing all the conflicts - // in memory - which is very wasteful. - // - alternatively we can do the equivalent of: - // `git apply --check ... | grep ...` - // meaning we don't store all the conflicts unnecessarily. - stderrReader, stderrReaderClose := cmdApply.MakeStderrPipe() - defer stderrReaderClose() - - // 8. Run the check command - conflict = false - err = cmdApply. - WithDir(tmpBasePath). - WithPipelineFunc(func(ctx gitcmd.Context) error { - const prefix = "error: patch failed:" - const errorPrefix = "error: " - const threewayFailed = "Failed to perform three-way merge..." - const appliedPatchPrefix = "Applied patch to '" - const withConflicts = "' with conflicts." - - conflicts := make(container.Set[string]) - - // Now scan the output from the command - scanner := bufio.NewScanner(stderrReader) - for scanner.Scan() { - line := scanner.Text() - log.Trace("PullRequest[%d].checkPullRequestMergeableByTmpRepo: stderr: %s", pr.ID, line) - if strings.HasPrefix(line, prefix) { - conflict = true - filepath := strings.TrimSpace(strings.Split(line[len(prefix):], ":")[0]) - conflicts.Add(filepath) - } else if is3way && line == threewayFailed { - conflict = true - } else if strings.HasPrefix(line, errorPrefix) { - conflict = true - for _, suffix := range patchErrorSuffices { - if strings.HasSuffix(line, suffix) { - filepath := strings.TrimSpace(strings.TrimSuffix(line[len(errorPrefix):], suffix)) - if filepath != "" { - conflicts.Add(filepath) - } - break - } - } - } else if is3way && strings.HasPrefix(line, appliedPatchPrefix) && strings.HasSuffix(line, withConflicts) { - conflict = true - filepath := strings.TrimPrefix(strings.TrimSuffix(line, withConflicts), appliedPatchPrefix) - if filepath != "" { - conflicts.Add(filepath) - } - } - // only list part of conflicted files - if len(conflicts) >= gitrepo.MaxConflictedDetectFiles { - break - } - } - - if len(conflicts) > 0 { - pr.ConflictedFiles = make([]string, 0, len(conflicts)) - for key := range conflicts { - pr.ConflictedFiles = append(pr.ConflictedFiles, key) - } - } - - return nil - }). - Run(gitRepo.Ctx) - - // 9. Check if the found conflicted files is non-zero, "err" could be non-nil, so we should ignore it if we found conflicts. - // Note: `"err" could be non-nil` is due that if enable 3-way merge, it doesn't return any error on found conflicts. - if len(pr.ConflictedFiles) > 0 { - if conflict { - pr.Status = issues_model.PullRequestStatusConflict - log.Trace("Found %d files conflicted: %v", len(pr.ConflictedFiles), pr.ConflictedFiles) - - return true, nil - } - } else if err != nil { - return false, fmt.Errorf("git apply --check: %w", err) - } - return false, nil + pr.Status = issues_model.PullRequestStatusConflict + pr.ConflictedFiles = conflictFiles + log.Trace("Found %d files conflicted: %v", len(pr.ConflictedFiles), pr.ConflictedFiles) + return true, nil } // ErrFilePathProtected represents a "FilePathProtected" kind of error. diff --git a/tests/integration/pull_merge_test.go b/tests/integration/pull_merge_test.go index c2859e2e162..53709e6ff4b 100644 --- a/tests/integration/pull_merge_test.go +++ b/tests/integration/pull_merge_test.go @@ -39,7 +39,6 @@ import ( pull_service "code.gitea.io/gitea/services/pull" repo_service "code.gitea.io/gitea/services/repository" commitstatus_service "code.gitea.io/gitea/services/repository/commitstatus" - files_service "code.gitea.io/gitea/services/repository/files" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -549,86 +548,6 @@ func TestCantFastForwardOnlyMergeDiverging(t *testing.T) { }) } -func TestConflictChecking(t *testing.T) { - onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { - user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - - // Create new clean repo to test conflict checking. - baseRepo, err := repo_service.CreateRepository(t.Context(), user, user, repo_service.CreateRepoOptions{ - Name: "conflict-checking", - Description: "Tempo repo", - AutoInit: true, - Readme: "Default", - DefaultBranch: "main", - }) - assert.NoError(t, err) - assert.NotEmpty(t, baseRepo) - - // create a commit on new branch. - _, err = files_service.ChangeRepoFiles(t.Context(), baseRepo, user, &files_service.ChangeRepoFilesOptions{ - Files: []*files_service.ChangeRepoFile{ - { - Operation: "create", - TreePath: "important_file", - ContentReader: strings.NewReader("Just a non-important file"), - }, - }, - Message: "Add a important file", - OldBranch: "main", - NewBranch: "important-secrets", - }) - assert.NoError(t, err) - - // create a commit on main branch. - _, err = files_service.ChangeRepoFiles(t.Context(), baseRepo, user, &files_service.ChangeRepoFilesOptions{ - Files: []*files_service.ChangeRepoFile{ - { - Operation: "create", - TreePath: "important_file", - ContentReader: strings.NewReader("Not the same content :P"), - }, - }, - Message: "Add a important file", - OldBranch: "main", - NewBranch: "main", - }) - assert.NoError(t, err) - - // create Pull to merge the important-secrets branch into main branch. - pullIssue := &issues_model.Issue{ - RepoID: baseRepo.ID, - Title: "PR with conflict!", - PosterID: user.ID, - Poster: user, - IsPull: true, - } - - pullRequest := &issues_model.PullRequest{ - HeadRepoID: baseRepo.ID, - BaseRepoID: baseRepo.ID, - HeadBranch: "important-secrets", - BaseBranch: "main", - HeadRepo: baseRepo, - BaseRepo: baseRepo, - Type: issues_model.PullRequestGitea, - } - prOpts := &pull_service.NewPullRequestOptions{Repo: baseRepo, Issue: pullIssue, PullRequest: pullRequest} - err = pull_service.NewPullRequest(t.Context(), prOpts) - assert.NoError(t, err) - - issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: "PR with conflict!"}) - assert.NoError(t, issue.LoadPullRequest(t.Context())) - conflictingPR := issue.PullRequest - - // Ensure conflictedFiles is populated. - assert.Len(t, conflictingPR.ConflictedFiles, 1) - // Check if status is correct. - assert.Equal(t, issues_model.PullRequestStatusConflict, conflictingPR.Status) - // Ensure that mergeable returns false - assert.False(t, conflictingPR.Mergeable(t.Context())) - }) -} - func TestPullRetargetChildOnBranchDelete(t *testing.T) { onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { session := loginUser(t, "user1") // FIXME: don't use admin user for testing From cf1e4d7c42ac530162eeea7b8decf94d235f0d97 Mon Sep 17 00:00:00 2001 From: silverwind Date: Mon, 23 Mar 2026 22:42:36 +0100 Subject: [PATCH 02/13] Update GitHub Actions to latest major versions (#36964) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update all Actions to their latest major versions: - `actions/checkout`: v5 → v6 - `dorny/paths-filter`: v3 → v4 - `pnpm/action-setup`: v4 → v5 - `docker/setup-qemu-action`: v3 → v4 - `docker/setup-buildx-action`: v3 → v4 - `docker/build-push-action`: v6 → v7 - `docker/metadata-action`: v5 → v6 - `docker/login-action`: v3 → v4 - `crazy-max/ghaction-import-gpg`: v6 → v7 - `aws-actions/configure-aws-credentials`: v5 → v6 All updates are Node 24 runtime bumps with no workflow-breaking changes for our usage. Co-authored-by: Claude (Opus 4.6) --- .github/workflows/cron-flake-updater.yml | 2 +- .github/workflows/files-changed.yml | 2 +- .github/workflows/pull-compliance.yml | 10 +++++----- .github/workflows/pull-docker-dryrun.yml | 8 ++++---- .github/workflows/pull-e2e-tests.yml | 2 +- .github/workflows/release-nightly.yml | 22 +++++++++++----------- .github/workflows/release-tag-rc.yml | 22 +++++++++++----------- .github/workflows/release-tag-version.yml | 22 +++++++++++----------- 8 files changed, 45 insertions(+), 45 deletions(-) diff --git a/.github/workflows/cron-flake-updater.yml b/.github/workflows/cron-flake-updater.yml index 105802e5587..c9a1f22a2ae 100644 --- a/.github/workflows/cron-flake-updater.yml +++ b/.github/workflows/cron-flake-updater.yml @@ -13,7 +13,7 @@ jobs: pull-requests: write runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: DeterminateSystems/determinate-nix-action@v3 - uses: DeterminateSystems/update-flake-lock@main with: diff --git a/.github/workflows/files-changed.yml b/.github/workflows/files-changed.yml index 332e9e0d6ff..55d206bb0f6 100644 --- a/.github/workflows/files-changed.yml +++ b/.github/workflows/files-changed.yml @@ -40,7 +40,7 @@ jobs: json: ${{ steps.changes.outputs.json }} steps: - uses: actions/checkout@v6 - - uses: dorny/paths-filter@v3 + - uses: dorny/paths-filter@v4 id: changes with: filters: | diff --git a/.github/workflows/pull-compliance.yml b/.github/workflows/pull-compliance.yml index fb81622bd69..e44a7875872 100644 --- a/.github/workflows/pull-compliance.yml +++ b/.github/workflows/pull-compliance.yml @@ -40,7 +40,7 @@ jobs: - uses: actions/checkout@v6 - uses: astral-sh/setup-uv@v7 - run: uv python install 3.14 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5 - uses: actions/setup-node@v6 with: node-version: 24 @@ -71,7 +71,7 @@ jobs: contents: read steps: - uses: actions/checkout@v6 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5 - uses: actions/setup-node@v5 with: node-version: 24 @@ -86,7 +86,7 @@ jobs: contents: read steps: - uses: actions/checkout@v6 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5 - uses: actions/setup-node@v6 with: node-version: 24 @@ -168,7 +168,7 @@ jobs: contents: read steps: - uses: actions/checkout@v6 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5 - uses: actions/setup-node@v6 with: node-version: 24 @@ -222,7 +222,7 @@ jobs: contents: read steps: - uses: actions/checkout@v6 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5 - uses: actions/setup-node@v6 with: node-version: 24 diff --git a/.github/workflows/pull-docker-dryrun.yml b/.github/workflows/pull-docker-dryrun.yml index bcc19e3eba8..201825ccbaa 100644 --- a/.github/workflows/pull-docker-dryrun.yml +++ b/.github/workflows/pull-docker-dryrun.yml @@ -21,17 +21,17 @@ jobs: contents: read steps: - uses: actions/checkout@v6 - - uses: docker/setup-qemu-action@v3 - - uses: docker/setup-buildx-action@v3 + - uses: docker/setup-qemu-action@v4 + - uses: docker/setup-buildx-action@v4 - name: Build regular container image - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: . platforms: linux/amd64,linux/arm64,linux/riscv64 push: false cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful - name: Build rootless container image - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: . push: false diff --git a/.github/workflows/pull-e2e-tests.yml b/.github/workflows/pull-e2e-tests.yml index c77f7af3f08..3472d517c15 100644 --- a/.github/workflows/pull-e2e-tests.yml +++ b/.github/workflows/pull-e2e-tests.yml @@ -25,7 +25,7 @@ jobs: with: go-version-file: go.mod check-latest: true - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5 - uses: actions/setup-node@v6 with: node-version: 24 diff --git a/.github/workflows/release-nightly.yml b/.github/workflows/release-nightly.yml index a7b2fda0426..eaebccd7fbe 100644 --- a/.github/workflows/release-nightly.yml +++ b/.github/workflows/release-nightly.yml @@ -22,7 +22,7 @@ jobs: with: go-version-file: go.mod check-latest: true - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5 - uses: actions/setup-node@v6 with: node-version: 24 @@ -35,7 +35,7 @@ jobs: TAGS: bindata sqlite sqlite_unlock_notify - name: import gpg key id: import_gpg - uses: crazy-max/ghaction-import-gpg@v6 + uses: crazy-max/ghaction-import-gpg@v7 with: gpg_private_key: ${{ secrets.GPGSIGN_KEY }} passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }} @@ -52,7 +52,7 @@ jobs: echo "Cleaned name is ${REF_NAME}" echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT" - name: configure aws - uses: aws-actions/configure-aws-credentials@v5 + uses: aws-actions/configure-aws-credentials@v6 with: aws-region: ${{ secrets.AWS_REGION }} aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} @@ -71,14 +71,14 @@ jobs: # fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 - run: git fetch --unshallow --quiet --tags --force - - uses: docker/setup-qemu-action@v3 - - uses: docker/setup-buildx-action@v3 + - uses: docker/setup-qemu-action@v4 + - uses: docker/setup-buildx-action@v4 - name: Get cleaned branch name id: clean_name run: | REF_NAME=$(echo "${{ github.ref }}" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\///' -e 's/release\/v//') echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT" - - uses: docker/metadata-action@v5 + - uses: docker/metadata-action@v6 id: meta with: images: |- @@ -88,7 +88,7 @@ jobs: type=raw,value=${{ steps.clean_name.outputs.branch }} annotations: | org.opencontainers.image.authors="maintainers@gitea.io" - - uses: docker/metadata-action@v5 + - uses: docker/metadata-action@v6 id: meta_rootless with: images: |- @@ -102,18 +102,18 @@ jobs: annotations: | org.opencontainers.image.authors="maintainers@gitea.io" - name: Login to Docker Hub - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GHCR using PAT - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: build regular docker image - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: . platforms: linux/amd64,linux/arm64,linux/riscv64 @@ -123,7 +123,7 @@ jobs: cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful cache-to: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful,mode=max - name: build rootless docker image - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: . platforms: linux/amd64,linux/arm64,linux/riscv64 diff --git a/.github/workflows/release-tag-rc.yml b/.github/workflows/release-tag-rc.yml index fab468c9b4b..248fa532eeb 100644 --- a/.github/workflows/release-tag-rc.yml +++ b/.github/workflows/release-tag-rc.yml @@ -23,7 +23,7 @@ jobs: with: go-version-file: go.mod check-latest: true - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5 - uses: actions/setup-node@v6 with: node-version: 24 @@ -36,7 +36,7 @@ jobs: TAGS: bindata sqlite sqlite_unlock_notify - name: import gpg key id: import_gpg - uses: crazy-max/ghaction-import-gpg@v6 + uses: crazy-max/ghaction-import-gpg@v7 with: gpg_private_key: ${{ secrets.GPGSIGN_KEY }} passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }} @@ -53,7 +53,7 @@ jobs: echo "Cleaned name is ${REF_NAME}" echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT" - name: configure aws - uses: aws-actions/configure-aws-credentials@v5 + uses: aws-actions/configure-aws-credentials@v6 with: aws-region: ${{ secrets.AWS_REGION }} aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} @@ -81,9 +81,9 @@ jobs: # fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 - run: git fetch --unshallow --quiet --tags --force - - uses: docker/setup-qemu-action@v3 - - uses: docker/setup-buildx-action@v3 - - uses: docker/metadata-action@v5 + - uses: docker/setup-qemu-action@v4 + - uses: docker/setup-buildx-action@v4 + - uses: docker/metadata-action@v6 id: meta with: images: |- @@ -96,7 +96,7 @@ jobs: type=semver,pattern={{version}} annotations: | org.opencontainers.image.authors="maintainers@gitea.io" - - uses: docker/metadata-action@v5 + - uses: docker/metadata-action@v6 id: meta_rootless with: images: |- @@ -112,18 +112,18 @@ jobs: annotations: | org.opencontainers.image.authors="maintainers@gitea.io" - name: Login to Docker Hub - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GHCR using PAT - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: build regular container image - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: . platforms: linux/amd64,linux/arm64,linux/riscv64 @@ -131,7 +131,7 @@ jobs: tags: ${{ steps.meta.outputs.tags }} annotations: ${{ steps.meta.outputs.annotations }} - name: build rootless container image - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: . platforms: linux/amd64,linux/arm64,linux/riscv64 diff --git a/.github/workflows/release-tag-version.yml b/.github/workflows/release-tag-version.yml index 113a33c3c7f..1e84ae1739f 100644 --- a/.github/workflows/release-tag-version.yml +++ b/.github/workflows/release-tag-version.yml @@ -26,7 +26,7 @@ jobs: with: go-version-file: go.mod check-latest: true - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5 - uses: actions/setup-node@v6 with: node-version: 24 @@ -39,7 +39,7 @@ jobs: TAGS: bindata sqlite sqlite_unlock_notify - name: import gpg key id: import_gpg - uses: crazy-max/ghaction-import-gpg@v6 + uses: crazy-max/ghaction-import-gpg@v7 with: gpg_private_key: ${{ secrets.GPGSIGN_KEY }} passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }} @@ -56,7 +56,7 @@ jobs: echo "Cleaned name is ${REF_NAME}" echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT" - name: configure aws - uses: aws-actions/configure-aws-credentials@v5 + uses: aws-actions/configure-aws-credentials@v6 with: aws-region: ${{ secrets.AWS_REGION }} aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} @@ -84,9 +84,9 @@ jobs: # fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 - run: git fetch --unshallow --quiet --tags --force - - uses: docker/setup-qemu-action@v3 - - uses: docker/setup-buildx-action@v3 - - uses: docker/metadata-action@v5 + - uses: docker/setup-qemu-action@v4 + - uses: docker/setup-buildx-action@v4 + - uses: docker/metadata-action@v6 id: meta with: images: |- @@ -103,7 +103,7 @@ jobs: type=semver,pattern={{major}}.{{minor}} annotations: | org.opencontainers.image.authors="maintainers@gitea.io" - - uses: docker/metadata-action@v5 + - uses: docker/metadata-action@v6 id: meta_rootless with: images: |- @@ -124,18 +124,18 @@ jobs: annotations: | org.opencontainers.image.authors="maintainers@gitea.io" - name: Login to Docker Hub - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GHCR using PAT - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: build regular container image - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: . platforms: linux/amd64,linux/arm64,linux/riscv64 @@ -143,7 +143,7 @@ jobs: tags: ${{ steps.meta.outputs.tags }} annotations: ${{ steps.meta.outputs.annotations }} - name: build rootless container image - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: . platforms: linux/amd64,linux/arm64,linux/riscv64 From 86401fd5fd35fc8f337dfc052cb63e53d9c2017a Mon Sep 17 00:00:00 2001 From: Nicolas Date: Mon, 23 Mar 2026 23:30:48 +0100 Subject: [PATCH 03/13] Fix user settings sidebar showing disabled features on some pages (#36958) Move UserDisabledFeatures context data into a shared SettingsCtxData middleware for the /user/settings route group, so it is set consistently on all pages (including Notifications, Actions, etc.) instead of only on the handlers that remembered to set it individually. Fixes #36954 --- routers/web/repo/setting/secrets.go | 2 -- routers/web/user/setting/account.go | 1 - routers/web/user/setting/applications.go | 3 --- routers/web/user/setting/block.go | 2 -- routers/web/user/setting/keys.go | 3 --- routers/web/user/setting/packages.go | 6 ------ routers/web/user/setting/profile.go | 6 ------ routers/web/user/setting/security/security.go | 1 - routers/web/user/setting/settings.go | 8 ++++++++ routers/web/user/setting/webhooks.go | 2 -- routers/web/web.go | 2 +- 11 files changed, 9 insertions(+), 27 deletions(-) diff --git a/routers/web/repo/setting/secrets.go b/routers/web/repo/setting/secrets.go index cd32a7dbb74..419aa268671 100644 --- a/routers/web/repo/setting/secrets.go +++ b/routers/web/repo/setting/secrets.go @@ -7,7 +7,6 @@ import ( "errors" "net/http" - user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" shared "code.gitea.io/gitea/routers/web/shared/secrets" @@ -74,7 +73,6 @@ func Secrets(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("actions.actions") ctx.Data["PageType"] = "secrets" ctx.Data["PageIsSharedSettingsSecrets"] = true - ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) sCtx, err := getSecretsCtx(ctx) if err != nil { diff --git a/routers/web/user/setting/account.go b/routers/web/user/setting/account.go index b333f364627..23bdf33d5fa 100644 --- a/routers/web/user/setting/account.go +++ b/routers/web/user/setting/account.go @@ -321,7 +321,6 @@ func loadAccountData(ctx *context.Context) { ctx.Data["Emails"] = emails ctx.Data["ActivationsPending"] = pendingActivation ctx.Data["CanAddEmails"] = !pendingActivation || !setting.Service.RegisterEmailConfirm - ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) if setting.Service.UserDeleteWithCommentsMaxTime != 0 { ctx.Data["UserDeleteWithCommentsMaxTime"] = setting.Service.UserDeleteWithCommentsMaxTime.String() diff --git a/routers/web/user/setting/applications.go b/routers/web/user/setting/applications.go index 2498c43b84a..9e33b487ea4 100644 --- a/routers/web/user/setting/applications.go +++ b/routers/web/user/setting/applications.go @@ -10,7 +10,6 @@ import ( auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" - user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/util" @@ -27,7 +26,6 @@ const ( func Applications(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("settings.applications") ctx.Data["PageIsSettingsApplications"] = true - ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) loadApplicationsData(ctx) @@ -39,7 +37,6 @@ func ApplicationsPost(ctx *context.Context) { form := web.GetForm(ctx).(*forms.NewAccessTokenForm) ctx.Data["Title"] = ctx.Tr("settings_title") ctx.Data["PageIsSettingsApplications"] = true - ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) _ = ctx.Req.ParseForm() var scopeNames []string diff --git a/routers/web/user/setting/block.go b/routers/web/user/setting/block.go index 3756495fd24..3a1625ccf9b 100644 --- a/routers/web/user/setting/block.go +++ b/routers/web/user/setting/block.go @@ -6,7 +6,6 @@ package setting import ( "net/http" - user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" shared_user "code.gitea.io/gitea/routers/web/shared/user" @@ -20,7 +19,6 @@ const ( func BlockedUsers(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("user.block.list") ctx.Data["PageIsSettingsBlockedUsers"] = true - ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) shared_user.BlockedUsers(ctx, ctx.Doer) if ctx.Written() { diff --git a/routers/web/user/setting/keys.go b/routers/web/user/setting/keys.go index b78a0ec4347..b82fa24a5df 100644 --- a/routers/web/user/setting/keys.go +++ b/routers/web/user/setting/keys.go @@ -35,7 +35,6 @@ func Keys(ctx *context.Context) { ctx.Data["DisableSSH"] = setting.SSH.Disabled ctx.Data["BuiltinSSH"] = setting.SSH.StartBuiltinServer ctx.Data["AllowPrincipals"] = setting.SSH.AuthorizedPrincipalsEnabled - ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) loadKeysData(ctx) @@ -50,7 +49,6 @@ func KeysPost(ctx *context.Context) { ctx.Data["DisableSSH"] = setting.SSH.Disabled ctx.Data["BuiltinSSH"] = setting.SSH.StartBuiltinServer ctx.Data["AllowPrincipals"] = setting.SSH.AuthorizedPrincipalsEnabled - ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) if ctx.HasError() { loadKeysData(ctx) @@ -341,5 +339,4 @@ func loadKeysData(ctx *context.Context) { ctx.Data["VerifyingID"] = ctx.FormString("verify_gpg") ctx.Data["VerifyingFingerprint"] = ctx.FormString("verify_ssh") - ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) } diff --git a/routers/web/user/setting/packages.go b/routers/web/user/setting/packages.go index 51f8c469084..62b0240642d 100644 --- a/routers/web/user/setting/packages.go +++ b/routers/web/user/setting/packages.go @@ -25,7 +25,6 @@ const ( func Packages(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("packages.title") ctx.Data["PageIsSettingsPackages"] = true - ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) shared.SetPackagesContext(ctx, ctx.Doer) @@ -35,7 +34,6 @@ func Packages(ctx *context.Context) { func PackagesRuleAdd(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("packages.title") ctx.Data["PageIsSettingsPackages"] = true - ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) shared.SetRuleAddContext(ctx) @@ -45,7 +43,6 @@ func PackagesRuleAdd(ctx *context.Context) { func PackagesRuleEdit(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("packages.title") ctx.Data["PageIsSettingsPackages"] = true - ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) shared.SetRuleEditContext(ctx, ctx.Doer) @@ -55,7 +52,6 @@ func PackagesRuleEdit(ctx *context.Context) { func PackagesRuleAddPost(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("settings_title") ctx.Data["PageIsSettingsPackages"] = true - ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) shared.PerformRuleAddPost( ctx, @@ -68,7 +64,6 @@ func PackagesRuleAddPost(ctx *context.Context) { func PackagesRuleEditPost(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("packages.title") ctx.Data["PageIsSettingsPackages"] = true - ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) shared.PerformRuleEditPost( ctx, @@ -81,7 +76,6 @@ func PackagesRuleEditPost(ctx *context.Context) { func PackagesRulePreview(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("packages.title") ctx.Data["PageIsSettingsPackages"] = true - ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) shared.SetRulePreviewContext(ctx, ctx.Doer) diff --git a/routers/web/user/setting/profile.go b/routers/web/user/setting/profile.go index 81a4a558e96..88d8e75d0cd 100644 --- a/routers/web/user/setting/profile.go +++ b/routers/web/user/setting/profile.go @@ -49,8 +49,6 @@ func Profile(ctx *context.Context) { ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice() ctx.Data["DisableGravatar"] = setting.Config().Picture.DisableGravatar.Value(ctx) - ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) - ctx.HTML(http.StatusOK, tplSettingsProfile) } @@ -60,7 +58,6 @@ func ProfilePost(ctx *context.Context) { ctx.Data["PageIsSettingsProfile"] = true ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice() ctx.Data["DisableGravatar"] = setting.Config().Picture.DisableGravatar.Value(ctx) - ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) if ctx.HasError() { ctx.HTML(http.StatusOK, tplSettingsProfile) @@ -200,7 +197,6 @@ func DeleteAvatar(ctx *context.Context) { func Organization(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("settings.organization") ctx.Data["PageIsSettingsOrganization"] = true - ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) opts := organization.FindOrgOptions{ ListOptions: db.ListOptions{ @@ -232,7 +228,6 @@ func Organization(ctx *context.Context) { func Repos(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("settings.repos") ctx.Data["PageIsSettingsRepos"] = true - ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) ctx.Data["allowAdopt"] = ctx.IsUserSiteAdmin() || setting.Repository.AllowAdoptionOfUnadoptedRepositories ctx.Data["allowDelete"] = ctx.IsUserSiteAdmin() || setting.Repository.AllowDeleteOfUnadoptedRepositories @@ -340,7 +335,6 @@ func Appearance(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("settings.appearance") ctx.Data["PageIsSettingsAppearance"] = true ctx.Data["AllThemes"] = webtheme.GetAvailableThemes() - ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) var hiddenCommentTypes *big.Int val, err := user_model.GetUserSetting(ctx, ctx.Doer.ID, user_model.SettingsKeyHiddenCommentTypes) diff --git a/routers/web/user/setting/security/security.go b/routers/web/user/setting/security/security.go index cc4c44993af..1b7efa27d5f 100644 --- a/routers/web/user/setting/security/security.go +++ b/routers/web/user/setting/security/security.go @@ -156,5 +156,4 @@ func loadSecurityData(ctx *context.Context) { return } ctx.Data["OpenIDs"] = openid - ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) } diff --git a/routers/web/user/setting/settings.go b/routers/web/user/setting/settings.go index 111931633da..e02ddc39af8 100644 --- a/routers/web/user/setting/settings.go +++ b/routers/web/user/setting/settings.go @@ -9,9 +9,17 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/services/context" ) +func SettingsCtxData(ctx *context.Context) { + ctx.Data["PageIsUserSettings"] = true + ctx.Data["EnablePackages"] = setting.Packages.Enabled + ctx.Data["EnableNotifyMail"] = setting.Service.EnableNotifyMail + ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) +} + func UpdatePreferences(ctx *context.Context) { type preferencesForm struct { CodeViewShowFileTree bool `json:"codeViewShowFileTree"` diff --git a/routers/web/user/setting/webhooks.go b/routers/web/user/setting/webhooks.go index 72a95a92e5c..3c5d54cbc69 100644 --- a/routers/web/user/setting/webhooks.go +++ b/routers/web/user/setting/webhooks.go @@ -7,7 +7,6 @@ import ( "net/http" "code.gitea.io/gitea/models/db" - user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/models/webhook" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" @@ -25,7 +24,6 @@ func Webhooks(ctx *context.Context) { ctx.Data["BaseLink"] = setting.AppSubURL + "/user/settings/hooks" ctx.Data["BaseLinkNew"] = setting.AppSubURL + "/user/settings/hooks" ctx.Data["Description"] = ctx.Tr("settings.hooks.desc") - ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) ws, err := db.Find[webhook.Webhook](ctx, webhook.ListWebhookOptions{OwnerID: ctx.Doer.ID}) if err != nil { diff --git a/routers/web/web.go b/routers/web/web.go index a76a68ed800..75cc437b43e 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -721,7 +721,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) { m.Get("", user_setting.BlockedUsers) m.Post("", web.Bind(forms.BlockUserForm{}), user_setting.BlockedUsersPost) }) - }, reqSignIn, ctxDataSet("PageIsUserSettings", true, "EnablePackages", setting.Packages.Enabled, "EnableNotifyMail", setting.Service.EnableNotifyMail)) + }, reqSignIn, user_setting.SettingsCtxData) m.Group("/user", func() { m.Get("/activate", auth.Activate) From 63c2b692597a384168f8383a58aa590430b04c92 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Tue, 24 Mar 2026 07:19:08 +0800 Subject: [PATCH 04/13] Make PUBLIC_URL_DETECTION default to "auto" (#36955) Related issues including: #36939 , #35619, #34950 , #34253 , #32554 For users who use reverse-proxy, we have documented the requirements clearly since long time ago : https://docs.gitea.com/administration/reverse-proxies --- custom/conf/app.example.ini | 9 ++++----- modules/setting/server.go | 2 +- routers/web/admin/admin_test.go | 23 +++++++++++++++++------ 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 5eb4a5e9956..b752a81ca93 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -69,13 +69,12 @@ RUN_USER = ; git ;; Most users should set it to the real website URL of their Gitea instance when there is a reverse proxy. ;ROOT_URL = ;; -;; Controls how to detect the public URL. -;; Although it defaults to "legacy" (to avoid breaking existing users), most instances should use the "auto" behavior, +;; Controls how to detect the public URL. Most instances should use the "auto" behavior, ;; especially when the Gitea instance needs to be accessed in a container network. -;; * legacy: detect the public URL from "Host" header if "X-Forwarded-Proto" header exists, otherwise use "ROOT_URL". -;; * auto: always use "Host" header, and also use "X-Forwarded-Proto" header if it exists. If no "Host" header, use "ROOT_URL". +;; * legacy: (default <= 1.25) detect the public URL from "Host" header if "X-Forwarded-Proto" header exists, otherwise use "ROOT_URL". +;; * auto: (default >= 1.26) always use "Host" header, and also use "X-Forwarded-Proto" header if it exists. If no "Host" header, use "ROOT_URL". ;; * never: always use "ROOT_URL", never detect from request headers. -;PUBLIC_URL_DETECTION = legacy +;PUBLIC_URL_DETECTION = auto ;; ;; For development purpose only. It makes Gitea handle sub-path ("/sub-path/owner/repo/...") directly when debugging without a reverse proxy. ;; DO NOT USE IT IN PRODUCTION!!! diff --git a/modules/setting/server.go b/modules/setting/server.go index 7e7611b802d..f0fbbce970a 100644 --- a/modules/setting/server.go +++ b/modules/setting/server.go @@ -286,7 +286,7 @@ func loadServerFrom(rootCfg ConfigProvider) { defaultAppURL := string(Protocol) + "://" + Domain + ":" + HTTPPort AppURL = sec.Key("ROOT_URL").MustString(defaultAppURL) - PublicURLDetection = sec.Key("PUBLIC_URL_DETECTION").MustString(PublicURLLegacy) + PublicURLDetection = sec.Key("PUBLIC_URL_DETECTION").MustString(PublicURLAuto) if PublicURLDetection != PublicURLAuto && PublicURLDetection != PublicURLLegacy && PublicURLDetection != PublicURLNever { log.Fatal("Invalid PUBLIC_URL_DETECTION value: %s", PublicURLDetection) } diff --git a/routers/web/admin/admin_test.go b/routers/web/admin/admin_test.go index a568c7c5c81..ecdd462f9e7 100644 --- a/routers/web/admin/admin_test.go +++ b/routers/web/admin/admin_test.go @@ -13,6 +13,7 @@ import ( "code.gitea.io/gitea/services/contexttest" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestShadowPassword(t *testing.T) { @@ -74,19 +75,29 @@ func TestShadowPassword(t *testing.T) { } func TestSelfCheckPost(t *testing.T) { + defer test.MockVariableValue(&setting.PublicURLDetection)() defer test.MockVariableValue(&setting.AppURL, "http://config/sub/")() defer test.MockVariableValue(&setting.AppSubURL, "/sub")() - ctx, resp := contexttest.MockContext(t, "GET http://host/sub/admin/self_check?location_origin=http://frontend") - SelfCheckPost(ctx) - assert.Equal(t, http.StatusOK, resp.Code) - data := struct { Problems []string `json:"problems"` }{} - err := json.Unmarshal(resp.Body.Bytes(), &data) - assert.NoError(t, err) + + setting.PublicURLDetection = setting.PublicURLLegacy + ctx, resp := contexttest.MockContext(t, "GET http://host/sub/admin/self_check?location_origin=http://frontend") + SelfCheckPost(ctx) + assert.Equal(t, http.StatusOK, resp.Code) + require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &data)) assert.Equal(t, []string{ ctx.Locale.TrString("admin.self_check.location_origin_mismatch", "http://frontend/sub/", "http://config/sub/"), }, data.Problems) + + setting.PublicURLDetection = setting.PublicURLAuto + ctx, resp = contexttest.MockContext(t, "GET http://host/sub/admin/self_check?location_origin=http://frontend") + SelfCheckPost(ctx) + assert.Equal(t, http.StatusOK, resp.Code) + require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &data)) + assert.Equal(t, []string{ + ctx.Locale.TrString("admin.self_check.location_origin_mismatch", "http://frontend/sub/", "http://host/sub/"), + }, data.Problems) } From c5e196dedb8a5f145203dd956907726450411d6f Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Tue, 24 Mar 2026 00:45:32 +0000 Subject: [PATCH 05/13] [skip ci] Updated translations via Crowdin --- options/locale/locale_ga-IE.json | 53 +++++++++++++++++++- options/locale/locale_ko-KR.json | 83 ++++++++++++++++++++++++++------ 2 files changed, 119 insertions(+), 17 deletions(-) diff --git a/options/locale/locale_ga-IE.json b/options/locale/locale_ga-IE.json index f43c9ad3556..663de757726 100644 --- a/options/locale/locale_ga-IE.json +++ b/options/locale/locale_ga-IE.json @@ -81,6 +81,7 @@ "retry": "Atriail", "rerun": "Ath-rith", "rerun_all": "Ath-rith na poist go léir", + "rerun_failed": "Athrith poist theipithe", "save": "Sábháil", "add": "Cuir", "add_all": "Cuir Gach", @@ -168,6 +169,7 @@ "search.exact_tooltip": "Ní chuir san áireamh ach torthaí a mheaitseálann leis an téarma", "search.repo_kind": "Cuardaigh stórais…", "search.user_kind": "Cuardaigh úsáideoirí…", + "search.badge_kind": "Cuardaigh suaitheantais…", "search.org_kind": "Cuardaigh eagraíochtaí…", "search.team_kind": "Cuardaigh foirne…", "search.code_kind": "Cuardaigh cód…", @@ -542,6 +544,7 @@ "form.glob_pattern_error": " tá patrún glob neamhbhailí: %s.", "form.regex_pattern_error": "tá patrún regex neamhbhailí: %s.", "form.username_error": "Ní féidir ach carachtair alfa-uimhriúla ('0-9','a-z','A-Z'), fleasc ('-'), fo-líne ('_') agus ponc ('.') a bheith i `. Ní féidir é a thosú ná a chríochnú le carachtair neamh-alfa-uimhriúla, agus tá cosc ar charachtair neamh-alfa-uimhriúla as a chéile ach an oiread.`", + "form.invalid_slug_error": " neamhbhailí.", "form.invalid_group_team_map_error": " tá mapáil neamhbhailí: %s", "form.unknown_error": "Earráid anaithnid:", "form.captcha_incorrect": "Tá an cód CAPTCHA mícheart.", @@ -645,6 +648,7 @@ "user.block.note.edit": "Cuir nóta in eagar", "user.block.list": "Úsáideoirí blocáilte", "user.block.list.none": "Níor chuir tú bac ar aon úsáideoirí.", + "settings.general": "Ginearálta", "settings.profile": "Próifíl", "settings.account": "Cuntas", "settings.appearance": "Dealramh", @@ -2856,6 +2860,30 @@ "admin.hooks": "Crúcaí Gréasán", "admin.integrations": "Comhtháthaithe", "admin.authentication": "Foinsí Fíordheimhnithe", + "admin.badges": "Suaitheantais", + "admin.badges.badges_manage_panel": "Bainistíocht Suaitheantais", + "admin.badges.details": "Sonraí Suaitheantais", + "admin.badges.new_badge": "Cruthaigh Suaitheantas Nua", + "admin.badges.slug": "Sluga", + "admin.badges.slug_been_taken": "Tá an sluga tógtha cheana féin.", + "admin.badges.description": "Cur síos", + "admin.badges.image_url": "URL na híomhá", + "admin.badges.new_success": "Tá an suaitheantas \"%s\" cruthaithe.", + "admin.badges.update_success": "Tá an suaitheantas nuashonraithe.", + "admin.badges.deletion_success": "Tá an suaitheantas scriosta.", + "admin.badges.edit_badge": "Cuir Suaitheantas in Eagar", + "admin.badges.update_badge": "Nuashonraigh Suaitheantas", + "admin.badges.delete_badge": "Scrios Suaitheantas", + "admin.badges.delete_badge_desc": "An bhfuil tú cinnte gur mian leat an suaitheantas seo a scriosadh go buan?", + "admin.badges.users_with_badge": "Úsáideoirí a bhfuil suaitheantas acu: %s", + "admin.badges.not_found": "Níor aimsíodh an suaitheantas.", + "admin.badges.user_already_has": "Tá an suaitheantas seo ag an úsáideoir cheana féin.", + "admin.badges.user_add_success": "Sannadh suaitheantas don úsáideoir go rathúil.", + "admin.badges.user_remove_success": "Baineadh an suaitheantas den úsáideoir go rathúil.", + "admin.badges.manage_users": "Bainistigh Úsáideoirí", + "admin.badges.add_user": "Cuir Úsáideoir leis", + "admin.badges.remove_user": "Bain Úsáideoir", + "admin.badges.delete_user_desc": "An bhfuil tú cinnte gur mian leat an t-úsáideoir seo a bhaint den suaitheantas?", "admin.emails": "Seoltaí Ríomhphoist Úsáideoirí", "admin.config": "Cumraíocht", "admin.config_summary": "Achoimre", @@ -3707,6 +3735,10 @@ "actions.runs.not_done": "Níl an rith sreabha oibre seo críochnaithe.", "actions.runs.view_workflow_file": "Féach ar chomhad sreabha oibre", "actions.runs.workflow_graph": "Graf Sreabhadh Oibre", + "actions.runs.summary": "Achoimre", + "actions.runs.all_jobs": "Gach post", + "actions.runs.triggered_via": "Spreagtha trí %s", + "actions.runs.total_duration": "Fad iomlán:", "actions.workflow.disable": "Díchumasaigh sreabhadh oibre", "actions.workflow.disable_success": "D'éirigh le sreabhadh oibre '%s' a dhíchumasú.", "actions.workflow.enable": "Cumasaigh sreabhadh oibre", @@ -3756,5 +3788,24 @@ "git.filemode.normal_file": "Rialta", "git.filemode.executable_file": "Inrite", "git.filemode.symbolic_link": "Nasc siombalach", - "git.filemode.submodule": "Fo-mhodúl" + "git.filemode.submodule": "Fo-mhodúl", + "org.repos.none": "Gan aon stórtha.", + "actions.general.permissions": "Ceadanna Comhartha Gníomhartha", + "actions.general.token_permissions.mode": "Ceadanna Réamhshocraithe Comharthaí", + "actions.general.token_permissions.mode.desc": "Úsáidfidh post Gníomhartha na ceadanna réamhshocraithe mura ndearbhaíonn sé a cheadanna sa chomhad sreabha oibre.", + "actions.general.token_permissions.mode.permissive": "Ceadaitheach", + "actions.general.token_permissions.mode.permissive.desc": "Ceadanna léigh agus scríbhneoireachta do stórlann an phoist.", + "actions.general.token_permissions.mode.restricted": "Srianta", + "actions.general.token_permissions.mode.restricted.desc": "Ceadanna léite amháin d'aonaid ábhair (cód, eisiúintí) stórlann an phoist.", + "actions.general.token_permissions.override_owner": "Sáraigh cumraíocht leibhéal an úinéara", + "actions.general.token_permissions.override_owner_desc": "Má tá sé cumasaithe, úsáidfidh an stór seo a chumraíocht Gníomhartha féin in ionad an chumraíocht ar leibhéal an úinéara (úsáideoir nó eagraíocht) a leanúint.", + "actions.general.token_permissions.maximum": "Uasmhéid Ceadanna Comharthaí", + "actions.general.token_permissions.maximum.description": "Beidh ceadanna éifeachtacha an phoist gníomhartha teoranta ag na ceadanna uasta.", + "actions.general.token_permissions.fork_pr_note": "Mura gcuirtear tús le post trí iarratas tarraingthe ó fhorc, ní rachaidh a cheadanna éifeachtacha thar na ceadanna léite amháin.", + "actions.general.token_permissions.customize_max_permissions": "Saincheap na ceadanna uasta", + "actions.general.cross_repo": "Rochtain Tras-Stórtha", + "actions.general.cross_repo_desc": "Ceadaigh rochtain (léamh amháin) a bheith ag na stórtha uile san úinéir seo ar na stórtha roghnaithe le GITEA_TOKEN agus poist Gníomhartha á reáchtáil.", + "actions.general.cross_repo_selected": "Stórtha roghnaithe", + "actions.general.cross_repo_target_repos": "Stórtha Spriocdhírithe", + "actions.general.cross_repo_add": "Cuir Stór Sprioc leis" } diff --git a/options/locale/locale_ko-KR.json b/options/locale/locale_ko-KR.json index 8f572e9f9b2..e4ea00f31c7 100644 --- a/options/locale/locale_ko-KR.json +++ b/options/locale/locale_ko-KR.json @@ -81,6 +81,7 @@ "retry": "재시도", "rerun": "다시 실행", "rerun_all": "모든 작업 다시 실행", + "rerun_failed": "실패한 작업 다시 실행", "save": "저장", "add": "추가", "add_all": "모두 추가", @@ -168,6 +169,7 @@ "search.exact_tooltip": "검색어와 정확하게 일치하는 결과만 포함합니다", "search.repo_kind": "리포지토리 검색…", "search.user_kind": "사용자 검색…", + "search.badge_kind": "배지 검색…", "search.org_kind": "조직 검색…", "search.team_kind": "팀 검색…", "search.code_kind": "코드 검색…", @@ -542,6 +544,7 @@ "form.glob_pattern_error": " 와일드카드 패턴이 잘못됨: %s.", "form.regex_pattern_error": " 정규 표현식 패턴이 잘못됨: %s.", "form.username_error": " `영숫자 ('0-9','a-z','A-Z'), 대시('-'), 밑줄('_'), 마침점 ('.') 만 포함할 수 있습니다. 시작과 끝문자는 반드시 영숫자이어야 하고, 영숫자가 아닌 문자를 연속해서 사용할 수 없습니다.`.", + "form.invalid_slug_error": " 유효하지 않음.", "form.invalid_group_team_map_error": " 매핑이 잘못되었습니다: %s", "form.unknown_error": "알 수 없는 오류:", "form.captcha_incorrect": "CAPTCHA 코드가 올바르지 않습니다.", @@ -645,6 +648,7 @@ "user.block.note.edit": "노트 편집", "user.block.list": "차단된 사용자", "user.block.list.none": "차단한 사용자가 없습니다.", + "settings.general": "일반", "settings.profile": "프로필", "settings.account": "계정", "settings.appearance": "외관", @@ -1154,7 +1158,7 @@ "repo.unstar": "별점취소", "repo.star": "별점", "repo.fork": "포크", - "repo.action.blocked_user": "리포지토리 소유자에게 차단되어 작업을 수행할 수 없습니다.", + "repo.action.blocked_user": "리포지토리 소유자에게 차단되어 액션을 수행할 수 없습니다.", "repo.download_archive": "리포지토리 다운로드", "repo.more_operations": "추가 작업", "repo.quick_guide": "퀵 가이드", @@ -1178,7 +1182,7 @@ "repo.pulls": "풀 리퀘스트", "repo.projects": "프로젝트", "repo.packages": "패키지", - "repo.actions": "동작", + "repo.actions": "액션", "repo.labels": "레이블", "repo.org_labels_desc": "이 조직의 모든 리포지토리에서 사용할 수 있는 조직 수준 레이블", "repo.org_labels_desc_manage": "관리", @@ -2140,7 +2144,7 @@ "repo.settings.projects_mode_repo": "리포지토리 프로젝트만", "repo.settings.projects_mode_owner": "사용자 또는 조직 프로젝트만", "repo.settings.projects_mode_all": "모든 프로젝트", - "repo.settings.actions_desc": "리포지토리 동작 활성화", + "repo.settings.actions_desc": "리포지토리 액션 활성화", "repo.settings.admin_settings": "운영자 설정", "repo.settings.admin_enable_health_check": "리포지토리 헬스 체크 활성화 (git fsck)", "repo.settings.admin_code_indexer": "코드 인덱서", @@ -2174,7 +2178,7 @@ "repo.settings.transfer_in_progress": "현재 진행 중인 이전이 있습니다. 이 저장소를 다른 사용자에게 이전하려면 먼저 취소하십시오.", "repo.settings.transfer_notices_1": "- 개별 사용자에게 리포지토리를 이전하면 리포지토리에 대한 액세스 권한을 잃게 됩니다.", "repo.settings.transfer_notices_2": "- 당신이 (공동)소유하고 있는 조직으로 리포지토리를 이전하면 리포지토리에 대한 액세스 권한을 유지합니다.", - "repo.settings.transfer_notices_3": "- 리포지토리가 비공개이고 개별 사용자에게 이전되는 경우, 이 작업은 해당 사용자가 최소한 읽기 권한을 가지도록 보장합니다 (필요하다면 권한을 변경함).", + "repo.settings.transfer_notices_3": "- 저장소가 비공개이고 개별 사용자에게 이전되는 경우, 이 액션은 해당 사용자가 최소한 읽기 권한을 가지도록 보장합니다(필요하다면 권한을 변경함).", "repo.settings.transfer_notices_4": "- 리포지토리가 조직에 속하고 다른 조직 또는 개인에게 이전하는 경우, 저장소의 이슈와 조직의 프로젝트 보드 간의 연결이 끊어집니다.", "repo.settings.transfer_owner": "새 소유자", "repo.settings.transfer_perform": "이전 수행", @@ -2789,7 +2793,7 @@ "org.teams.can_create_org_repo": "리포지토리 생성", "org.teams.can_create_org_repo_helper": "멤버는 조직에서 새 리포지토리를 만들 수 있습니다. 생성자는 새 리포지토리에 대한 운영자 액세스 권한을 얻습니다.", "org.teams.none_access": "액세스 없음", - "org.teams.none_access_helper": "멤버는 이 단위에서 어떤 작업도 보거나 수행할 수 없습니다. 공개 리포지토리에는 영향을 미치지 않습니다.", + "org.teams.none_access_helper": "멤버는 이 단위에서 어떤 액션도 보거나 수행할 수 없습니다. 공개 리포지토리에는 영향을 미치지 않습니다.", "org.teams.general_access": "일반 액세스", "org.teams.general_access_helper": "멤버 권한은 아래 권한 테이블에 따라 결정됩니다.", "org.teams.read_access": "읽음", @@ -2856,6 +2860,30 @@ "admin.hooks": "Webhook", "admin.integrations": "통합", "admin.authentication": "인증 소스", + "admin.badges": "배지", + "admin.badges.badges_manage_panel": "배지 관리", + "admin.badges.details": "배지 상세정보", + "admin.badges.new_badge": "새 배지 만들기", + "admin.badges.slug": "슬러그", + "admin.badges.slug_been_taken": "이미 사용 중인 슬러그입니다.", + "admin.badges.description": "설명", + "admin.badges.image_url": "이미지 URL", + "admin.badges.new_success": "배지 \"%s\"가 생성되었습니다.", + "admin.badges.update_success": "배지가 업데이트 되었습니다.", + "admin.badges.deletion_success": "배지가 삭제되었습니다.", + "admin.badges.edit_badge": "배지 수정", + "admin.badges.update_badge": "배지 업데이트", + "admin.badges.delete_badge": "배지 삭제", + "admin.badges.delete_badge_desc": "이 배지를 영구적으로 삭제하겠습니까?", + "admin.badges.users_with_badge": "배지를 가진 사용자: %s", + "admin.badges.not_found": "배지를 찾을 수 없음.", + "admin.badges.user_already_has": "사용자는 이 배지를 이미 갖고 있습니다.", + "admin.badges.user_add_success": "사용자에게 배지가 성공적으로 지정되었습니다.", + "admin.badges.user_remove_success": "사용자에게 배지를 제거하는데 성공하였습니다.", + "admin.badges.manage_users": "사용자 관리", + "admin.badges.add_user": "사용자 추가", + "admin.badges.remove_user": "사용자 삭제", + "admin.badges.delete_user_desc": "이 사용자를 배지에서 제거하는 것이 확실한가요?", "admin.emails": "사용자 이메일 주소", "admin.config": "구성", "admin.config_summary": "요약", @@ -2909,7 +2937,7 @@ "admin.dashboard.sync_external_users": "외부 사용자 데이터 동기화", "admin.dashboard.cleanup_hook_task_table": "hook_task 테이블 정리", "admin.dashboard.cleanup_packages": "만료된 패키지 정리", - "admin.dashboard.cleanup_actions": "만료된 작업 리소스 정리", + "admin.dashboard.cleanup_actions": "만료된 액션 리소스 정리", "admin.dashboard.server_uptime": "서버를 켠 시간", "admin.dashboard.current_goroutine": "현재 Go루틴", "admin.dashboard.current_memory_usage": "현재 메모리 사용율", @@ -2944,10 +2972,10 @@ "admin.dashboard.update_checker": "업데이트 확인", "admin.dashboard.delete_old_system_notices": "데이터베이스에서 모든 오래된 시스템 알림 삭제", "admin.dashboard.gc_lfs": "LFS 메타 객체 가비지 컬렉션", - "admin.dashboard.stop_zombie_tasks": "좀비 작업 동작 중지", + "admin.dashboard.stop_zombie_tasks": "좀비 작업 액션 중지", "admin.dashboard.stop_endless_tasks": "끝나지 않는 작업 중지", - "admin.dashboard.cancel_abandoned_jobs": "포기한 작업 동작 취소", - "admin.dashboard.start_schedule_tasks": "예약된 작업 동작 시작", + "admin.dashboard.cancel_abandoned_jobs": "포기한 작업 액션 취소", + "admin.dashboard.start_schedule_tasks": "예약된 작업 액션 시작", "admin.dashboard.sync_branch.started": "브랜치 동기화 시작됨", "admin.dashboard.sync_tag.started": "태그 동기화 시작됨", "admin.dashboard.rebuild_issue_indexer": "이슈 인덱서 재구축", @@ -3611,7 +3639,7 @@ "packages.owner.settings.chef.keypair": "키 쌍 생성", "packages.owner.settings.chef.keypair.description": "Chef 레지스트리에 인증하려면 키 쌍이 필요합니다. 이전에 키 쌍을 생성한 경우, 새 키 쌍를 생성하면 이전 키 쌍은 폐기됩니다.", "secrets.secrets": "비밀", - "secrets.description": "비밀 키는 특정 동작에 전달되며 다른 방법으로는 읽을 수 없습니다.", + "secrets.description": "비밀 키는 특정 액션에 전달되며 다른 방법으로는 읽을 수 없습니다.", "secrets.none": "아직 비밀이 없습니다.", "secrets.creation.description": "설명", "secrets.creation.name_placeholder": "대소문자를 구분하지 않으며, 영숫자 또는 밑줄 문자만, GITEA_ 또는 GITHUB_로 시작할 수 없음", @@ -3626,8 +3654,8 @@ "secrets.deletion.success": "비밀이 삭제되었습니다.", "secrets.deletion.failed": "비밀 제거에 실패했습니다.", "secrets.management": "비밀 관리", - "actions.actions": "동작", - "actions.unit.desc": "동작 관리", + "actions.actions": "액션", + "actions.unit.desc": "액션 관리", "actions.status.unknown": "알수없음", "actions.status.waiting": "대기 중", "actions.status.running": "실행중", @@ -3703,10 +3731,14 @@ "actions.runs.expire_log_message": "로그가 너무 오래되어 제거되었습니다.", "actions.runs.delete": "워크플로우 실행 삭제", "actions.runs.cancel": "워크플로우 실행 취소", - "actions.runs.delete.description": "이 워크플로우 실행을 영구적으로 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.", + "actions.runs.delete.description": "이 워크플로우 실행을 영구적으로 삭제하시겠습니까? 이 동작은 되돌릴 수 없습니다.", "actions.runs.not_done": "이 워크플로우 실행은 완료되지 않았습니다.", "actions.runs.view_workflow_file": "워크플로우 파일 표시", "actions.runs.workflow_graph": "워크플로우 그래프", + "actions.runs.summary": "요약", + "actions.runs.all_jobs": "모든 작업", + "actions.runs.triggered_via": "%s를 통해 트리거됨", + "actions.runs.total_duration": "총기간:", "actions.workflow.disable": "워크플로 비활성화", "actions.workflow.disable_success": "워크플로 '%s'가 성공적으로 비활성화되었습니다.", "actions.workflow.enable": "워크플로 활성화", @@ -3726,7 +3758,7 @@ "actions.variables.none": "아직 변수가 없습니다.", "actions.variables.deletion": "변수 제거", "actions.variables.deletion.description": "변수 제거는 영구적이며 되돌릴 수 없습니다. 계속하시겠습니까?", - "actions.variables.description": "변수는 특정 동작에 전달되며 다른 방법으로는 읽을 수 없습니다.", + "actions.variables.description": "변수는 특정 액션에 전달되며 다른 방법으로는 읽을 수 없습니다.", "actions.variables.id_not_exist": "ID %d인 변수가 존재하지 않습니다.", "actions.variables.edit": "변수 수정", "actions.variables.deletion.failed": "변수 제거에 실패했습니다.", @@ -3738,7 +3770,7 @@ "actions.logs.always_auto_scroll": "로그 자동 스크롤 항상 켜기", "actions.logs.always_expand_running": "실행 중인 로그 항상 펼침", "actions.general": "일반", - "actions.general.enable_actions": "동작 활성화", + "actions.general.enable_actions": "액션 활성화", "actions.general.collaborative_owners_management": "보조 소유자 관리", "actions.general.collaborative_owners_management_help": "보조 소유자는 이 리포지토리의 액션 및 워크플로에 접근할 수 있는 개인 리포지토리를 가진 사용자 또는 조직입니다.", "actions.general.add_collaborative_owner": "보조 소유자 추가", @@ -3756,5 +3788,24 @@ "git.filemode.normal_file": "일반", "git.filemode.executable_file": "실행파일", "git.filemode.symbolic_link": "Symlink", - "git.filemode.submodule": "서브모듈" + "git.filemode.submodule": "서브모듈", + "org.repos.none": "리포지토리 없음.", + "actions.general.permissions": "액션 토큰 권한", + "actions.general.token_permissions.mode": "기본 토큰 권한", + "actions.general.token_permissions.mode.desc": "워크플로 파일에서 권한을 선언하지 않으면 액션 작업은 기본 권한을 사용합니다.", + "actions.general.token_permissions.mode.permissive": "허용적", + "actions.general.token_permissions.mode.permissive.desc": "작업 리포지토리에 대한 읽기 쓰기 권한.", + "actions.general.token_permissions.mode.restricted": "제한됨", + "actions.general.token_permissions.mode.restricted.desc": "작업 리포지토리의 콘텐츠 단위(코드, 릴리스)에 대한 읽기 전용 권한.", + "actions.general.token_permissions.override_owner": "소유자 수준의 구성 오버라이드", + "actions.general.token_permissions.override_owner_desc": "활성화하면, 이 리포지토리는 다음 소유자-수준(사용자 혹은 조직) 구성 대신에 자신의 액션 구성을 사용합니다.", + "actions.general.token_permissions.maximum": "최대 토큰 권한", + "actions.general.token_permissions.maximum.description": "액션 작업의 효과 권한은 최대 권한으로 제한됩니다.", + "actions.general.token_permissions.fork_pr_note": "포크로부터 생성된 풀 리퀘스트로 작업이 시작된 경우, 해당 작업의 유효 권한은 읽기 전용 권한을 초과하지 않습니다.", + "actions.general.token_permissions.customize_max_permissions": "최대 권한을 커스터마이징", + "actions.general.cross_repo": "크로스 리포지토리 억세스", + "actions.general.cross_repo_desc": "선택된 저장소가 이 소유자의 모든 저장소에서 액션 작업을 실행할 때 GITEA_TOKEN을 사용하여 (읽기 전용으로) 액세스할 수 있도록 허용합니다.", + "actions.general.cross_repo_selected": "선택된 리포지토리", + "actions.general.cross_repo_target_repos": "대상 리포지토리", + "actions.general.cross_repo_add": "대상 리포지토리 추가" } From c453d09c36fad094405314dba2f370434b200711 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Mon, 23 Mar 2026 21:08:48 -0700 Subject: [PATCH 06/13] Catch scanner error when possible to avoid bypass (#36963) --- cmd/hook.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/cmd/hook.go b/cmd/hook.go index a0280e283f7..992e52c2790 100644 --- a/cmd/hook.go +++ b/cmd/hook.go @@ -276,6 +276,9 @@ Gitea or set your environment appropriately.`, "") lastline = 0 } } + if err := scanner.Err(); err != nil { + return fail(ctx, "Hook failed: stdin read error", "scanner error: %v", err) + } if count > 0 { hookOptions.OldCommitIDs = oldCommitIDs[:count] @@ -415,6 +418,11 @@ Gitea or set your environment appropriately.`, "") count = 0 } } + if err := scanner.Err(); err != nil { + _ = dWriter.Close() + hookPrintResults(results) + return fail(ctx, "Hook failed: stdin read error", "scanner error: %v", err) + } if count == 0 { if wasEmpty && masterPushed { From 66b8178e59cf7b44528fecd93637d299582c991e Mon Sep 17 00:00:00 2001 From: silverwind Date: Tue, 24 Mar 2026 17:49:29 +0100 Subject: [PATCH 07/13] Improve AGENTS.md (#36974) 1. Remove header line, useless context bloat 2. Reword all "before commiting" lines because some people may not be using the agent to commit, only to write changes. --- AGENTS.md | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 402a9d6945e..3db66637b71 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,10 +1,8 @@ -# Instructions for agents - - Use `make help` to find available development targets -- Before committing `.go` changes, run `make fmt` to format, and run `make lint-go` to lint -- Before committing `.ts` changes, run `make lint-js` to lint -- Before committing `go.mod` changes, run `make tidy` -- Before committing new `.go` files, add the current year into the copyright header -- Before committing any files, remove all trailing whitespace from source code lines +- Run `make fmt` to format `.go` files, and run `make lint-go` to lint them +- Run `make lint-js` to lint `.ts` files +- Run `make tidy` after any `go.mod` changes +- Add the current year into the copyright header of new `.go` files +- Ensure no trailing whitespace in edited files - Never force-push to pull request branches - Always start issue and pull request comments with an authorship attribution From c96cc701445cfe31fce65dc4ff667ab49f218857 Mon Sep 17 00:00:00 2001 From: Tyrone Yeh Date: Wed, 25 Mar 2026 01:23:13 +0800 Subject: [PATCH 08/13] Add class "list-header-filters" to the div for projects (#36889) closes #36886 --- templates/projects/view.tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl index e1b7364f41c..3e1afab79f0 100644 --- a/templates/projects/view.tmpl +++ b/templates/projects/view.tmpl @@ -4,7 +4,7 @@

{{.Project.Title}}

- @@ -917,21 +904,8 @@
-
- -
-
- - -
- -
- - -
+ {{template "repo/settings/repo_name_confirm_fields" (dict "RepoName" .Repository.Name)}} + {{template "base/modal_actions_confirm" (dict "ModalButtonDangerText" (ctx.Locale.Tr "repo.settings.convert_fork_confirm"))}}
@@ -949,25 +923,13 @@
-
- -
-
- - -
+ {{template "repo/settings/repo_name_confirm_fields" (dict "RepoName" .Repository.Name)}}
-
- - -
+ {{template "base/modal_actions_confirm" (dict "ModalButtonDangerText" (ctx.Locale.Tr "repo.settings.transfer_perform"))}}
@@ -986,49 +948,57 @@
-
- -
-
- - -
- -
- - -
+ {{template "repo/settings/repo_name_confirm_fields" (dict "RepoName" .Repository.Name)}} + {{template "base/modal_actions_confirm" (dict "ModalButtonDangerText" (ctx.Locale.Tr "repo.settings.confirm_delete"))}}
{{if not .Repository.IsFork}} -
-
- -
-
- - -
- -
- - -
+ {{template "repo/settings/repo_name_confirm_fields" (dict "RepoName" .Repository.Name)}} + {{template "base/modal_actions_confirm" (dict "ModalButtonDangerText" (ctx.Locale.Tr "repo.settings.confirm_wiki_delete"))}}
diff --git a/templates/repo/settings/repo_name_confirm_fields.tmpl b/templates/repo/settings/repo_name_confirm_fields.tmpl new file mode 100644 index 00000000000..a8f9800e715 --- /dev/null +++ b/templates/repo/settings/repo_name_confirm_fields.tmpl @@ -0,0 +1,10 @@ +
+ +
+
+ + +
diff --git a/tests/integration/repo_visibility_test.go b/tests/integration/repo_visibility_test.go new file mode 100644 index 00000000000..e7ad8ddd876 --- /dev/null +++ b/tests/integration/repo_visibility_test.go @@ -0,0 +1,59 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "net/http" + "testing" + + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/test" + "code.gitea.io/gitea/tests" + + "github.com/stretchr/testify/assert" +) + +func TestRepositoryVisibilityChange(t *testing.T) { + defer tests.PrepareTestEnv(t)() + session := loginUser(t, "user2") + + t.Run("MakePrivateRequiresCorrectName", func(t *testing.T) { + // Wrong name should be rejected with a JSON error + req := NewRequestWithValues(t, "POST", "/user2/repo1/settings", map[string]string{ + "action": "visibility", + "private": "true", + "confirm_repo_name": "wrong-name", + }) + resp := session.MakeRequest(t, req, http.StatusBadRequest) + assert.NotEmpty(t, test.ParseJSONError(resp.Body.Bytes()).ErrorMessage) + + repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + assert.False(t, repo1.IsPrivate) + + // Correct full name (owner/repo) should succeed with a JSON redirect + req = NewRequestWithValues(t, "POST", "/user2/repo1/settings", map[string]string{ + "action": "visibility", + "private": "true", + "confirm_repo_name": "user2/repo1", + }) + resp = session.MakeRequest(t, req, http.StatusOK) + assert.NotEmpty(t, test.ParseJSONRedirect(resp.Body.Bytes()).Redirect) + + repo1 = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + assert.True(t, repo1.IsPrivate) + }) + + t.Run("MakePublicDoesNotRequireName", func(t *testing.T) { + req := NewRequestWithValues(t, "POST", "/user2/repo2/settings", map[string]string{ + "action": "visibility", + "private": "false", + }) + resp := session.MakeRequest(t, req, http.StatusOK) + assert.NotEmpty(t, test.ParseJSONRedirect(resp.Body.Bytes()).Redirect) + + repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) + assert.False(t, repo2.IsPrivate) + }) +} From e24c3f7a40dade43ae7d9e3354491184ad64c141 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Wed, 25 Mar 2026 08:23:11 +0100 Subject: [PATCH 11/13] Fix org contact email not clearable once set (#36975) When the email field was submitted as empty in org settings (web and API), the previous guard `if form.Email != ""` silently skipped the update, making it impossible to remove a contact email after it was set. --------- Co-authored-by: wxiaoguang --- modules/structs/org.go | 16 +++--- routers/api/v1/org/org.go | 20 ++++--- routers/web/org/setting.go | 20 +++---- services/forms/org.go | 14 ++--- services/org/org.go | 17 ++++++ services/org/org_test.go | 54 ++++++++++++++----- services/user/email.go | 62 +++++++++++----------- services/user/email_test.go | 75 +++++++++++---------------- templates/swagger/v1_json.tmpl | 2 +- tests/integration/api_org_test.go | 55 ++++++++++++-------- tests/integration/integration_test.go | 3 +- tests/integration/org_test.go | 58 ++++++++++++++------- 12 files changed, 232 insertions(+), 164 deletions(-) diff --git a/modules/structs/org.go b/modules/structs/org.go index c3d70ebf000..d79b1d1d1c0 100644 --- a/modules/structs/org.go +++ b/modules/structs/org.go @@ -66,23 +66,21 @@ type CreateOrgOption struct { RepoAdminChangeTeamAccess bool `json:"repo_admin_change_team_access"` } -// TODO: make EditOrgOption fields optional after https://gitea.com/go-chi/binding/pulls/5 got merged - // EditOrgOption options for editing an organization type EditOrgOption struct { // The full display name of the organization - FullName string `json:"full_name" binding:"MaxSize(100)"` - // The email address of the organization - Email string `json:"email" binding:"MaxSize(255)"` + FullName *string `json:"full_name" binding:"MaxSize(100)"` + // The email address of the organization; use empty string to clear + Email *string `json:"email" binding:"MaxSize(255)"` // The description of the organization - Description string `json:"description" binding:"MaxSize(255)"` + Description *string `json:"description" binding:"MaxSize(255)"` // The website URL of the organization - Website string `json:"website" binding:"ValidUrl;MaxSize(255)"` + Website *string `json:"website" binding:"ValidUrl;MaxSize(255)"` // The location of the organization - Location string `json:"location" binding:"MaxSize(50)"` + Location *string `json:"location" binding:"MaxSize(50)"` // possible values are `public`, `limited` or `private` // enum: public,limited,private - Visibility string `json:"visibility" binding:"In(,public,limited,private)"` + Visibility *string `json:"visibility" binding:"In(,public,limited,private)"` // Whether repository administrators can change team access RepoAdminChangeTeamAccess *bool `json:"repo_admin_change_team_access"` } diff --git a/routers/api/v1/org/org.go b/routers/api/v1/org/org.go index d42241f0540..ce2a2e5580c 100644 --- a/routers/api/v1/org/org.go +++ b/routers/api/v1/org/org.go @@ -5,6 +5,7 @@ package org import ( + "errors" "net/http" activities_model "code.gitea.io/gitea/models/activities" @@ -14,6 +15,7 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/optional" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/api/v1/user" "code.gitea.io/gitea/routers/api/v1/utils" @@ -379,19 +381,21 @@ func Edit(ctx *context.APIContext) { form := web.GetForm(ctx).(*api.EditOrgOption) - if form.Email != "" { - if err := user_service.ReplacePrimaryEmailAddress(ctx, ctx.Org.Organization.AsUser(), form.Email); err != nil { - ctx.APIErrorInternal(err) + if err := org.UpdateOrgEmailAddress(ctx, ctx.Org.Organization, form.Email); err != nil { + if errors.Is(err, util.ErrInvalidArgument) { + ctx.APIError(http.StatusUnprocessableEntity, err) return } + ctx.APIErrorInternal(err) + return } opts := &user_service.UpdateOptions{ - FullName: optional.Some(form.FullName), - Description: optional.Some(form.Description), - Website: optional.Some(form.Website), - Location: optional.Some(form.Location), - Visibility: optional.FromMapLookup(api.VisibilityModes, form.Visibility), + FullName: optional.FromPtr(form.FullName), + Description: optional.FromPtr(form.Description), + Website: optional.FromPtr(form.Website), + Location: optional.FromPtr(form.Location), + Visibility: optional.FromMapLookup(api.VisibilityModes, optional.FromPtr(form.Visibility).Value()), RepoAdminChangeTeamAccess: optional.FromPtr(form.RepoAdminChangeTeamAccess), } if err := user_service.UpdateUser(ctx, ctx.Org.Organization.AsUser(), opts); err != nil { diff --git a/routers/web/org/setting.go b/routers/web/org/setting.go index 04baa58b734..ca1da6617e5 100644 --- a/routers/web/org/setting.go +++ b/routers/web/org/setting.go @@ -5,6 +5,7 @@ package org import ( + "errors" "net/http" "net/url" @@ -69,24 +70,25 @@ func SettingsPost(ctx *context.Context) { } org := ctx.Org.Organization - - if form.Email != "" { - if err := user_service.ReplacePrimaryEmailAddress(ctx, org.AsUser(), form.Email); err != nil { + if err := org_service.UpdateOrgEmailAddress(ctx, org, form.Email); err != nil { + if errors.Is(err, util.ErrInvalidArgument) { ctx.Data["Err_Email"] = true ctx.RenderWithErrDeprecated(ctx.Tr("form.email_invalid"), tplSettingsOptions, &form) return } + ctx.ServerError("UpdateOrgEmailAddress", err) + return } opts := &user_service.UpdateOptions{ - FullName: optional.Some(form.FullName), - Description: optional.Some(form.Description), - Website: optional.Some(form.Website), - Location: optional.Some(form.Location), - RepoAdminChangeTeamAccess: optional.Some(form.RepoAdminChangeTeamAccess), + FullName: optional.FromPtr(form.FullName), + Description: optional.FromPtr(form.Description), + Website: optional.FromPtr(form.Website), + Location: optional.FromPtr(form.Location), + RepoAdminChangeTeamAccess: optional.FromPtr(form.RepoAdminChangeTeamAccess), } if ctx.Doer.IsAdmin { - opts.MaxRepoCreation = optional.Some(form.MaxRepoCreation) + opts.MaxRepoCreation = optional.FromPtr(form.MaxRepoCreation) } if err := user_service.UpdateUser(ctx, org.AsUser(), opts); err != nil { diff --git a/services/forms/org.go b/services/forms/org.go index 3997e1da845..8a8106e8cf1 100644 --- a/services/forms/org.go +++ b/services/forms/org.go @@ -36,13 +36,13 @@ func (f *CreateOrgForm) Validate(req *http.Request, errs binding.Errors) binding // UpdateOrgSettingForm form for updating organization settings type UpdateOrgSettingForm struct { - FullName string `binding:"MaxSize(100)"` - Email string `binding:"MaxSize(255)"` - Description string `binding:"MaxSize(255)"` - Website string `binding:"ValidUrl;MaxSize(255)"` - Location string `binding:"MaxSize(50)"` - MaxRepoCreation int - RepoAdminChangeTeamAccess bool + FullName *string `binding:"MaxSize(100)"` + Email *string `binding:"MaxSize(255)"` + Description *string `binding:"MaxSize(255)"` + Website *string `binding:"ValidUrl;MaxSize(255)"` + Location *string `binding:"MaxSize(50)"` + MaxRepoCreation *int + RepoAdminChangeTeamAccess *bool } // Validate validates the fields diff --git a/services/org/org.go b/services/org/org.go index 8da77c691c1..32c46d7cb9c 100644 --- a/services/org/org.go +++ b/services/org/org.go @@ -168,3 +168,20 @@ func ChangeOrganizationVisibility(ctx context.Context, org *org_model.Organizati return nil }) } + +// UpdateOrgEmailAddress validates and updates the organization's contact email. +// A nil email means no change. +func UpdateOrgEmailAddress(ctx context.Context, org *org_model.Organization, email *string) error { + if email == nil { + return nil + } + + if *email != "" { + if err := user_model.ValidateEmail(*email); err != nil { + return err + } + } + + org.Email = *email + return user_model.UpdateUserCols(ctx, org.AsUser(), "email") +} diff --git a/services/org/org_test.go b/services/org/org_test.go index 5fdc1a6fd5e..5253c739020 100644 --- a/services/org/org_test.go +++ b/services/org/org_test.go @@ -10,28 +10,54 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/util" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestMain(m *testing.M) { unittest.MainTest(m) } -func TestDeleteOrganization(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 6}) - assert.NoError(t, DeleteOrganization(t.Context(), org, false)) - unittest.AssertNotExistsBean(t, &organization.Organization{ID: 6}) - unittest.AssertNotExistsBean(t, &organization.OrgUser{OrgID: 6}) - unittest.AssertNotExistsBean(t, &organization.Team{OrgID: 6}) +func TestOrg(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) - org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) - err := DeleteOrganization(t.Context(), org, false) - assert.Error(t, err) - assert.True(t, repo_model.IsErrUserOwnRepos(err)) + t.Run("UpdateOrgEmailAddress", func(t *testing.T) { + org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) + originalEmail := org.Email - user := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 5}) - assert.Error(t, DeleteOrganization(t.Context(), user, false)) - unittest.CheckConsistencyFor(t, &user_model.User{}, &organization.Team{}) + require.NoError(t, UpdateOrgEmailAddress(t.Context(), org, nil)) + unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3, Email: originalEmail}) + + newEmail := "contact@org3.example.com" + require.NoError(t, UpdateOrgEmailAddress(t.Context(), org, &newEmail)) + unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3, Email: newEmail}) + + invalidEmail := "invalid email" + err := UpdateOrgEmailAddress(t.Context(), org, &invalidEmail) + require.ErrorIs(t, err, util.ErrInvalidArgument) + unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3, Email: newEmail}) + + require.NoError(t, UpdateOrgEmailAddress(t.Context(), org, new(""))) + org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3, Email: ""}) + assert.Empty(t, org.Email) + }) + + t.Run("DeleteOrganization", func(t *testing.T) { + org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 6}) + assert.NoError(t, DeleteOrganization(t.Context(), org, false)) + unittest.AssertNotExistsBean(t, &organization.Organization{ID: 6}) + unittest.AssertNotExistsBean(t, &organization.OrgUser{OrgID: 6}) + unittest.AssertNotExistsBean(t, &organization.Team{OrgID: 6}) + + org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) + err := DeleteOrganization(t.Context(), org, false) + assert.Error(t, err) + assert.True(t, repo_model.IsErrUserOwnRepos(err)) + + user := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 5}) + assert.Error(t, DeleteOrganization(t.Context(), user, false)) + unittest.CheckConsistencyFor(t, &user_model.User{}, &organization.Team{}) + }) } diff --git a/services/user/email.go b/services/user/email.go index c45b3b3ec9f..927e4b72341 100644 --- a/services/user/email.go +++ b/services/user/email.go @@ -14,7 +14,14 @@ import ( "code.gitea.io/gitea/modules/util" ) +// ReplacePrimaryEmailAddress replaces the user's primary email address with the given email address. +// It also updates the user's email field to match the new primary email address. func ReplacePrimaryEmailAddress(ctx context.Context, u *user_model.User, emailStr string) error { + // FIXME: this check is from old logic, but it is not right, there are far more user types, not only "organization" + if u.IsOrganization() { + return util.NewInvalidArgumentErrorf("user %s is an organization", u.Name) + } + if strings.EqualFold(u.Email, emailStr) { return nil } @@ -24,41 +31,38 @@ func ReplacePrimaryEmailAddress(ctx context.Context, u *user_model.User, emailSt } return db.WithTx(ctx, func(ctx context.Context) error { - if !u.IsOrganization() { - // Check if address exists already - email, err := user_model.GetEmailAddressByEmail(ctx, emailStr) - if err != nil && !errors.Is(err, util.ErrNotExist) { - return err - } - if email != nil { - if email.IsPrimary && email.UID == u.ID { - return nil - } - return user_model.ErrEmailAlreadyUsed{Email: emailStr} + // Check if address exists already + email, err := user_model.GetEmailAddressByEmail(ctx, emailStr) + if err != nil && !errors.Is(err, util.ErrNotExist) { + return err + } + if email != nil { + if email.IsPrimary && email.UID == u.ID { + return nil } + return user_model.ErrEmailAlreadyUsed{Email: emailStr} + } - // Remove old primary address - primary, err := user_model.GetPrimaryEmailAddressOfUser(ctx, u.ID) - if err != nil { - return err - } - if _, err := db.DeleteByID[user_model.EmailAddress](ctx, primary.ID); err != nil { - return err - } + // Remove old primary address + primary, err := user_model.GetPrimaryEmailAddressOfUser(ctx, u.ID) + if err != nil { + return err + } + if _, err := db.DeleteByID[user_model.EmailAddress](ctx, primary.ID); err != nil { + return err + } - // Insert new primary address - if _, err := user_model.InsertEmailAddress(ctx, &user_model.EmailAddress{ - UID: u.ID, - Email: emailStr, - IsActivated: true, - IsPrimary: true, - }); err != nil { - return err - } + // Insert new primary address + if _, err := user_model.InsertEmailAddress(ctx, &user_model.EmailAddress{ + UID: u.ID, + Email: emailStr, + IsActivated: true, + IsPrimary: true, + }); err != nil { + return err } u.Email = emailStr - return user_model.UpdateUserCols(ctx, u, "email") }) } diff --git a/services/user/email_test.go b/services/user/email_test.go index a031b12cadc..9fb0560b05e 100644 --- a/services/user/email_test.go +++ b/services/user/email_test.go @@ -6,17 +6,16 @@ package user import ( "testing" - organization_model "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" "github.com/stretchr/testify/assert" ) -func TestReplacePrimaryEmailAddress(t *testing.T) { +func TestUserEmail(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - t.Run("User", func(t *testing.T) { + t.Run("PrimaryEmailAddress", func(t *testing.T) { user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 13}) emails, err := user_model.GetEmailAddresses(t.Context(), user.ID) @@ -42,50 +41,36 @@ func TestReplacePrimaryEmailAddress(t *testing.T) { assert.NoError(t, ReplacePrimaryEmailAddress(t.Context(), user, "primary-13@example.com")) }) - t.Run("Organization", func(t *testing.T) { - org := unittest.AssertExistsAndLoadBean(t, &organization_model.Organization{ID: 3}) + t.Run("AddEmailAddresses", func(t *testing.T) { + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - assert.Equal(t, "org3@example.com", org.Email) + assert.Error(t, AddEmailAddresses(t.Context(), user, []string{" invalid email "})) - assert.NoError(t, ReplacePrimaryEmailAddress(t.Context(), org.AsUser(), "primary-org@example.com")) + emails := []string{"user1234@example.com", "user5678@example.com"} - assert.Equal(t, "primary-org@example.com", org.Email) + assert.NoError(t, AddEmailAddresses(t.Context(), user, emails)) + + err := AddEmailAddresses(t.Context(), user, emails) + assert.Error(t, err) + assert.True(t, user_model.IsErrEmailAlreadyUsed(err)) + }) + + t.Run("DeleteEmailAddresses", func(t *testing.T) { + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + + emails := []string{"user2-2@example.com"} + + err := DeleteEmailAddresses(t.Context(), user, emails) + assert.NoError(t, err) + + err = DeleteEmailAddresses(t.Context(), user, emails) + assert.Error(t, err) + assert.True(t, user_model.IsErrEmailAddressNotExist(err)) + + emails = []string{"user2@example.com"} + + err = DeleteEmailAddresses(t.Context(), user, emails) + assert.Error(t, err) + assert.True(t, user_model.IsErrPrimaryEmailCannotDelete(err)) }) } - -func TestAddEmailAddresses(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - - assert.Error(t, AddEmailAddresses(t.Context(), user, []string{" invalid email "})) - - emails := []string{"user1234@example.com", "user5678@example.com"} - - assert.NoError(t, AddEmailAddresses(t.Context(), user, emails)) - - err := AddEmailAddresses(t.Context(), user, emails) - assert.Error(t, err) - assert.True(t, user_model.IsErrEmailAlreadyUsed(err)) -} - -func TestDeleteEmailAddresses(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - - emails := []string{"user2-2@example.com"} - - err := DeleteEmailAddresses(t.Context(), user, emails) - assert.NoError(t, err) - - err = DeleteEmailAddresses(t.Context(), user, emails) - assert.Error(t, err) - assert.True(t, user_model.IsErrEmailAddressNotExist(err)) - - emails = []string{"user2@example.com"} - - err = DeleteEmailAddresses(t.Context(), user, emails) - assert.Error(t, err) - assert.True(t, user_model.IsErrPrimaryEmailCannotDelete(err)) -} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 20db48b91a0..adc6c181755 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -24855,7 +24855,7 @@ "x-go-name": "Description" }, "email": { - "description": "The email address of the organization", + "description": "The email address of the organization; use empty string to clear", "type": "string", "x-go-name": "Email" }, diff --git a/tests/integration/api_org_test.go b/tests/integration/api_org_test.go index 6b7826fbb8b..42f9e4cbf6f 100644 --- a/tests/integration/api_org_test.go +++ b/tests/integration/api_org_test.go @@ -137,34 +137,45 @@ func TestAPIOrgGeneral(t *testing.T) { }) t.Run("OrgEdit", func(t *testing.T) { - org := api.EditOrgOption{ - FullName: "Org3 organization new full name", - Description: "A new description", - Website: "https://try.gitea.io/new", - Location: "Beijing", - Visibility: "private", - } - req := NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &org).AddTokenAuth(user1Token) - resp := MakeRequest(t, req, http.StatusOK) + org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "org3"}) + assert.NotEqual(t, api.VisibleTypeLimited, org3.Visibility) - var apiOrg api.Organization - DecodeJSON(t, resp, &apiOrg) + org3Edit := api.EditOrgOption{ + FullName: new("new full name"), + Description: new("new description"), + Website: new("https://org3-new-website.example.com"), + Location: new("new location"), + Visibility: new("limited"), + Email: new("org3-new-email@example.com"), + } + req := NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &org3Edit).AddTokenAuth(user1Token) + resp := MakeRequest(t, req, http.StatusOK) + apiOrg := DecodeJSON(t, resp, &api.Organization{}) assert.Equal(t, "org3", apiOrg.Name) - assert.Equal(t, org.FullName, apiOrg.FullName) - assert.Equal(t, org.Description, apiOrg.Description) - assert.Equal(t, org.Website, apiOrg.Website) - assert.Equal(t, org.Location, apiOrg.Location) - assert.Equal(t, org.Visibility, apiOrg.Visibility) + assert.Equal(t, *org3Edit.FullName, apiOrg.FullName) + assert.Equal(t, *org3Edit.Description, apiOrg.Description) + assert.Equal(t, *org3Edit.Website, apiOrg.Website) + assert.Equal(t, *org3Edit.Location, apiOrg.Location) + assert.Equal(t, *org3Edit.Visibility, apiOrg.Visibility) + assert.Equal(t, *org3Edit.Email, apiOrg.Email) + org3 = unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "org3"}) + assert.Equal(t, api.VisibleTypeLimited, org3.Visibility) + + // empty email can clear the email, nil fields won't change the settings + req = NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &api.EditOrgOption{ + Email: new(""), + }).AddTokenAuth(user1Token) + resp = MakeRequest(t, req, http.StatusOK) + apiOrg = DecodeJSON(t, resp, &api.Organization{}) + assert.Equal(t, *org3Edit.FullName, apiOrg.FullName) + assert.Equal(t, *org3Edit.Visibility, apiOrg.Visibility) + assert.Empty(t, apiOrg.Email) }) - t.Run("OrgEditBadVisibility", func(t *testing.T) { + t.Run("OrgEditInvalidVisibility", func(t *testing.T) { org := api.EditOrgOption{ - FullName: "Org3 organization new full name", - Description: "A new description", - Website: "https://try.gitea.io/new", - Location: "Beijing", - Visibility: "badvisibility", + Visibility: new("invalid-visibility"), } req := NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &org).AddTokenAuth(user1Token) MakeRequest(t, req, http.StatusUnprocessableEntity) diff --git a/tests/integration/integration_test.go b/tests/integration/integration_test.go index a4807814dfb..e8b0cbd6414 100644 --- a/tests/integration/integration_test.go +++ b/tests/integration/integration_test.go @@ -410,12 +410,13 @@ func logUnexpectedResponse(t testing.TB, recorder *httptest.ResponseRecorder) { } } -func DecodeJSON(t testing.TB, resp *httptest.ResponseRecorder, v any) { +func DecodeJSON[T any](t testing.TB, resp *httptest.ResponseRecorder, v T) (ret T) { t.Helper() // FIXME: JSON-KEY-CASE: for testing purpose only, because many structs don't provide `json` tags, they just use capitalized field names decoder := json.NewDecoderCaseInsensitive(resp.Body) require.NoError(t, decoder.Decode(v)) + return v } func VerifyJSONSchema(t testing.TB, resp *httptest.ResponseRecorder, schemaFile string) { diff --git a/tests/integration/org_test.go b/tests/integration/org_test.go index 3ed7baa5ba1..cedc0406ca8 100644 --- a/tests/integration/org_test.go +++ b/tests/integration/org_test.go @@ -23,9 +23,18 @@ import ( "github.com/stretchr/testify/require" ) -func TestOrgRepos(t *testing.T) { +func TestOrg(t *testing.T) { defer tests.PrepareTestEnv(t)() + t.Run("OrgRepos", testOrgRepos) + t.Run("PrivateOrg", testPrivateOrg) + t.Run("LimitedOrg", testLimitedOrg) + t.Run("OrgMembers", testOrgMembers) + t.Run("OrgRestrictedUser", testOrgRestrictedUser) + t.Run("TeamSearch", testTeamSearch) + t.Run("OrgSettings", testOrgSettings) +} +func testOrgRepos(t *testing.T) { var ( users = []string{"user1", "user2"} cases = map[string][]string{ @@ -53,10 +62,8 @@ func TestOrgRepos(t *testing.T) { } } -func TestLimitedOrg(t *testing.T) { - defer tests.PrepareTestEnv(t)() - - // not logged in user +func testLimitedOrg(t *testing.T) { + // not logged-in user req := NewRequest(t, "GET", "/limited_org") MakeRequest(t, req, http.StatusNotFound) req = NewRequest(t, "GET", "/limited_org/public_repo_on_limited_org") @@ -83,10 +90,8 @@ func TestLimitedOrg(t *testing.T) { session.MakeRequest(t, req, http.StatusOK) } -func TestPrivateOrg(t *testing.T) { - defer tests.PrepareTestEnv(t)() - - // not logged in user +func testPrivateOrg(t *testing.T) { + // not logged-in user req := NewRequest(t, "GET", "/privated_org") MakeRequest(t, req, http.StatusNotFound) req = NewRequest(t, "GET", "/privated_org/public_repo_on_private_org") @@ -122,10 +127,8 @@ func TestPrivateOrg(t *testing.T) { session.MakeRequest(t, req, http.StatusOK) } -func TestOrgMembers(t *testing.T) { - defer tests.PrepareTestEnv(t)() - - // not logged in user +func testOrgMembers(t *testing.T) { + // not logged-in user req := NewRequest(t, "GET", "/org/org25/members") MakeRequest(t, req, http.StatusOK) @@ -140,9 +143,7 @@ func TestOrgMembers(t *testing.T) { session.MakeRequest(t, req, http.StatusOK) } -func TestOrgRestrictedUser(t *testing.T) { - defer tests.PrepareTestEnv(t)() - +func testOrgRestrictedUser(t *testing.T) { // privated_org is a private org who has id 23 orgName := "privated_org" @@ -200,9 +201,7 @@ func TestOrgRestrictedUser(t *testing.T) { restrictedSession.MakeRequest(t, req, http.StatusOK) } -func TestTeamSearch(t *testing.T) { - defer tests.PrepareTestEnv(t)() - +func testTeamSearch(t *testing.T) { user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 15}) org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 17}) @@ -251,3 +250,24 @@ func TestTeamSearch(t *testing.T) { assert.Len(t, teams, 1) // team permission is "write", so can write "code" }) } + +func testOrgSettings(t *testing.T) { + session := loginUser(t, "user2") + + req := NewRequestWithValues(t, "POST", "/org/org3/settings", map[string]string{ + "full_name": "org3 new full name", + "email": "org3-new-email@example.com", + }) + session.MakeRequest(t, req, http.StatusSeeOther) + org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) + assert.Equal(t, "org3 new full name", org.FullName) + assert.Equal(t, "org3-new-email@example.com", org.Email) + + req = NewRequestWithValues(t, "POST", "/org/org3/settings", map[string]string{ + "email": "", // empty email means "clear email" + }) + session.MakeRequest(t, req, http.StatusSeeOther) + org = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) + assert.Equal(t, "org3 new full name", org.FullName) + assert.Empty(t, org.Email) +} From bb1e22bba4cbdf85e86b96cd39a82f2a1a4a8b8d Mon Sep 17 00:00:00 2001 From: silverwind Date: Wed, 25 Mar 2026 08:40:46 +0100 Subject: [PATCH 12/13] Allow text selection on checkbox labels (#36970) Remove `user-select: none` from checkbox labels to allow text selection which is sometimes useful. --------- Signed-off-by: silverwind Co-authored-by: Claude (Opus 4.6) --- web_src/css/modules/checkbox.css | 1 - 1 file changed, 1 deletion(-) diff --git a/web_src/css/modules/checkbox.css b/web_src/css/modules/checkbox.css index 558486e63a8..220abfc17d2 100644 --- a/web_src/css/modules/checkbox.css +++ b/web_src/css/modules/checkbox.css @@ -101,7 +101,6 @@ input[type="checkbox"]:indeterminate::before { cursor: auto; position: relative; display: block; - user-select: none; } .ui.checkbox label, From 435123fe65f7f0c94d5ee39a4b5d638803d4bd95 Mon Sep 17 00:00:00 2001 From: techknowlogick Date: Wed, 25 Mar 2026 10:53:13 -0400 Subject: [PATCH 13/13] Switch `cmd/` to use constructor functions. (#36962) This is a step towards potentially splitting command groups into their own folders to clean up `cmd/` as one folder for all cli commands. Returning fresh command instances will also aid in adding tests as you don't need to concern yourself with the whole command tree being one mutable variable. --------- Co-authored-by: wxiaoguang --- cmd/actions.go | 13 +- cmd/admin.go | 41 +++-- cmd/admin_auth.go | 11 +- cmd/admin_regenerate.go | 10 +- cmd/admin_user.go | 24 +-- cmd/admin_user_generate_access_token.go | 52 +++--- cmd/admin_user_list.go | 20 +-- cmd/docs.go | 33 ++-- cmd/doctor.go | 118 +++++++------- cmd/doctor_convert.go | 13 +- cmd/doctor_test.go | 2 +- cmd/dump.go | 143 ++++++++--------- cmd/dump_repo.go | 107 ++++++------- cmd/embedded.go | 29 ++-- cmd/generate.go | 31 ++-- cmd/hook.go | 36 +++-- cmd/keys.go | 68 ++++---- cmd/main.go | 35 +++-- cmd/manager.go | 44 ++++-- cmd/manager_logging.go | 14 +- cmd/migrate.go | 13 +- cmd/migrate_storage.go | 201 ++++++++++++------------ cmd/restore_repo.go | 65 ++++---- cmd/serv.go | 31 ++-- cmd/web.go | 69 ++++---- tests/integration/cmd_keys_test.go | 6 +- 26 files changed, 651 insertions(+), 578 deletions(-) diff --git a/cmd/actions.go b/cmd/actions.go index 2c51c6a1bcc..44b6b7b54cf 100644 --- a/cmd/actions.go +++ b/cmd/actions.go @@ -13,17 +13,18 @@ import ( "github.com/urfave/cli/v3" ) -var ( - // CmdActions represents the available actions sub-commands. - CmdActions = &cli.Command{ +func newActionsCommand() *cli.Command { + return &cli.Command{ Name: "actions", Usage: "Manage Gitea Actions", Commands: []*cli.Command{ - subcmdActionsGenRunnerToken, + newActionsGenerateRunnerTokenCommand(), }, } +} - subcmdActionsGenRunnerToken = &cli.Command{ +func newActionsGenerateRunnerTokenCommand() *cli.Command { + return &cli.Command{ Name: "generate-runner-token", Usage: "Generate a new token for a runner to use to register with the server", Action: runGenerateActionsRunnerToken, @@ -37,7 +38,7 @@ var ( }, }, } -) +} func runGenerateActionsRunnerToken(ctx context.Context, c *cli.Command) error { setting.MustInstalled() diff --git a/cmd/admin.go b/cmd/admin.go index dbd48e57279..c0e21731cbe 100644 --- a/cmd/admin.go +++ b/cmd/admin.go @@ -18,36 +18,41 @@ import ( "github.com/urfave/cli/v3" ) -var ( - // CmdAdmin represents the available admin sub-command. - CmdAdmin = &cli.Command{ +func newAdminCommand() *cli.Command { + return &cli.Command{ Name: "admin", Usage: "Perform common administrative operations", Commands: []*cli.Command{ - subcmdUser, - subcmdRepoSyncReleases, - subcmdRegenerate, - subcmdAuth, - subcmdSendMail, + newUserCommand(), + newRepoSyncReleasesCommand(), + newRegenerateCommand(), + newAuthCommand(), + newSendMailCommand(), }, } +} - subcmdRepoSyncReleases = &cli.Command{ +func newRepoSyncReleasesCommand() *cli.Command { + return &cli.Command{ Name: "repo-sync-releases", Usage: "Synchronize repository releases with tags", Action: runRepoSyncReleases, } +} - subcmdRegenerate = &cli.Command{ +func newRegenerateCommand() *cli.Command { + return &cli.Command{ Name: "regenerate", Usage: "Regenerate specific files", Commands: []*cli.Command{ - microcmdRegenHooks, - microcmdRegenKeys, + newRegenerateHooksCommand(), + newRegenerateKeysCommand(), }, } +} - subcmdAuth = &cli.Command{ +func newAuthCommand() *cli.Command { + return &cli.Command{ Name: "auth", Usage: "Modify external auth providers", Commands: []*cli.Command{ @@ -59,12 +64,14 @@ var ( microcmdAuthUpdateLdapSimpleAuth(), microcmdAuthAddSMTP(), microcmdAuthUpdateSMTP(), - microcmdAuthList, - microcmdAuthDelete, + newAuthListCommand(), + newAuthDeleteCommand(), }, } +} - subcmdSendMail = &cli.Command{ +func newSendMailCommand() *cli.Command { + return &cli.Command{ Name: "sendmail", Usage: "Send a message to all users", Action: runSendMail, @@ -86,7 +93,7 @@ var ( }, }, } -) +} func idFlag() *cli.Int64Flag { return &cli.Int64Flag{ diff --git a/cmd/admin_auth.go b/cmd/admin_auth.go index 1a093667229..b55bb1481cf 100644 --- a/cmd/admin_auth.go +++ b/cmd/admin_auth.go @@ -17,14 +17,17 @@ import ( "github.com/urfave/cli/v3" ) -var ( - microcmdAuthDelete = &cli.Command{ +func newAuthDeleteCommand() *cli.Command { + return &cli.Command{ Name: "delete", Usage: "Delete specific auth source", Flags: []cli.Flag{idFlag()}, Action: runDeleteAuth, } - microcmdAuthList = &cli.Command{ +} + +func newAuthListCommand() *cli.Command { + return &cli.Command{ Name: "list", Usage: "List auth sources", Action: runListAuth, @@ -55,7 +58,7 @@ var ( }, }, } -) +} func runListAuth(ctx context.Context, c *cli.Command) error { if err := initDB(ctx); err != nil { diff --git a/cmd/admin_regenerate.go b/cmd/admin_regenerate.go index a5f1bd51059..aa235441bac 100644 --- a/cmd/admin_regenerate.go +++ b/cmd/admin_regenerate.go @@ -13,19 +13,21 @@ import ( "github.com/urfave/cli/v3" ) -var ( - microcmdRegenHooks = &cli.Command{ +func newRegenerateHooksCommand() *cli.Command { + return &cli.Command{ Name: "hooks", Usage: "Regenerate git-hooks", Action: runRegenerateHooks, } +} - microcmdRegenKeys = &cli.Command{ +func newRegenerateKeysCommand() *cli.Command { + return &cli.Command{ Name: "keys", Usage: "Regenerate authorized_keys file", Action: runRegenerateKeys, } -) +} func runRegenerateHooks(ctx context.Context, _ *cli.Command) error { if err := initDB(ctx); err != nil { diff --git a/cmd/admin_user.go b/cmd/admin_user.go index 3a24c3e56f1..8dd8bb4eca1 100644 --- a/cmd/admin_user.go +++ b/cmd/admin_user.go @@ -7,15 +7,17 @@ import ( "github.com/urfave/cli/v3" ) -var subcmdUser = &cli.Command{ - Name: "user", - Usage: "Modify users", - Commands: []*cli.Command{ - microcmdUserCreate(), - microcmdUserList, - microcmdUserChangePassword(), - microcmdUserDelete(), - microcmdUserGenerateAccessToken, - microcmdUserMustChangePassword(), - }, +func newUserCommand() *cli.Command { + return &cli.Command{ + Name: "user", + Usage: "Modify users", + Commands: []*cli.Command{ + microcmdUserCreate(), + newUserListCommand(), + microcmdUserChangePassword(), + microcmdUserDelete(), + newUserGenerateAccessTokenCommand(), + microcmdUserMustChangePassword(), + }, + } } diff --git a/cmd/admin_user_generate_access_token.go b/cmd/admin_user_generate_access_token.go index 61064fdef4b..7f8330ff7e4 100644 --- a/cmd/admin_user_generate_access_token.go +++ b/cmd/admin_user_generate_access_token.go @@ -14,32 +14,34 @@ import ( "github.com/urfave/cli/v3" ) -var microcmdUserGenerateAccessToken = &cli.Command{ - Name: "generate-access-token", - Usage: "Generate an access token for a specific user", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "username", - Aliases: []string{"u"}, - Usage: "Username", +func newUserGenerateAccessTokenCommand() *cli.Command { + return &cli.Command{ + Name: "generate-access-token", + Usage: "Generate an access token for a specific user", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "username", + Aliases: []string{"u"}, + Usage: "Username", + }, + &cli.StringFlag{ + Name: "token-name", + Aliases: []string{"t"}, + Usage: "Token name", + Value: "gitea-admin", + }, + &cli.BoolFlag{ + Name: "raw", + Usage: "Display only the token value", + }, + &cli.StringFlag{ + Name: "scopes", + Value: "all", + Usage: `Comma separated list of scopes to apply to access token, examples: "all", "public-only,read:issue", "write:repository,write:user"`, + }, }, - &cli.StringFlag{ - Name: "token-name", - Aliases: []string{"t"}, - Usage: "Token name", - Value: "gitea-admin", - }, - &cli.BoolFlag{ - Name: "raw", - Usage: "Display only the token value", - }, - &cli.StringFlag{ - Name: "scopes", - Value: "all", - Usage: `Comma separated list of scopes to apply to access token, examples: "all", "public-only,read:issue", "write:repository,write:user"`, - }, - }, - Action: runGenerateAccessToken, + Action: runGenerateAccessToken, + } } func runGenerateAccessToken(ctx context.Context, c *cli.Command) error { diff --git a/cmd/admin_user_list.go b/cmd/admin_user_list.go index e3d345e2f24..2958fe0cc8b 100644 --- a/cmd/admin_user_list.go +++ b/cmd/admin_user_list.go @@ -14,16 +14,18 @@ import ( "github.com/urfave/cli/v3" ) -var microcmdUserList = &cli.Command{ - Name: "list", - Usage: "List users", - Action: runListUsers, - Flags: []cli.Flag{ - &cli.BoolFlag{ - Name: "admin", - Usage: "List only admin users", +func newUserListCommand() *cli.Command { + return &cli.Command{ + Name: "list", + Usage: "List users", + Action: runListUsers, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "admin", + Usage: "List only admin users", + }, }, - }, + } } func runListUsers(ctx context.Context, c *cli.Command) error { diff --git a/cmd/docs.go b/cmd/docs.go index 098c0e9a8a1..8e0f9428df3 100644 --- a/cmd/docs.go +++ b/cmd/docs.go @@ -13,23 +13,24 @@ import ( "github.com/urfave/cli/v3" ) -// CmdDocs represents the available docs sub-command. -var CmdDocs = &cli.Command{ - Name: "docs", - Usage: "Output CLI documentation", - Description: "A command to output Gitea's CLI documentation, optionally to a file.", - Action: runDocs, - Flags: []cli.Flag{ - &cli.BoolFlag{ - Name: "man", - Usage: "Output man pages instead", +func newDocsCommand() *cli.Command { + return &cli.Command{ + Name: "docs", + Usage: "Output CLI documentation", + Description: "A command to output Gitea's CLI documentation, optionally to a file.", + Action: runDocs, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "man", + Usage: "Output man pages instead", + }, + &cli.StringFlag{ + Name: "output", + Aliases: []string{"o"}, + Usage: "Path to output to instead of stdout (will overwrite if exists)", + }, }, - &cli.StringFlag{ - Name: "output", - Aliases: []string{"o"}, - Usage: "Path to output to instead of stdout (will overwrite if exists)", - }, - }, + } } func runDocs(_ context.Context, cmd *cli.Command) error { diff --git a/cmd/doctor.go b/cmd/doctor.go index 596dd611786..188740dbcea 100644 --- a/cmd/doctor.go +++ b/cmd/doctor.go @@ -24,73 +24,77 @@ import ( "xorm.io/xorm" ) -// CmdDoctor represents the available doctor sub-command. -var CmdDoctor = &cli.Command{ - Name: "doctor", - Usage: "Diagnose and optionally fix problems, convert or re-create database tables", - Description: "A command to diagnose problems with the current Gitea instance according to the given configuration. Some problems can optionally be fixed by modifying the database or data storage.", - - Commands: []*cli.Command{ - cmdDoctorCheck, - cmdRecreateTable, - cmdDoctorConvert, - }, +func newDoctorCommand() *cli.Command { + return &cli.Command{ + Name: "doctor", + Usage: "Diagnose and optionally fix problems, convert or re-create database tables", + Description: "A command to diagnose problems with the current Gitea instance according to the given configuration. Some problems can optionally be fixed by modifying the database or data storage.", + Commands: []*cli.Command{ + newDoctorCheckCommand(), + newRecreateTableCommand(), + newDoctorConvertCommand(), + }, + } } -var cmdDoctorCheck = &cli.Command{ - Name: "check", - Usage: "Diagnose and optionally fix problems", - Description: "A command to diagnose problems with the current Gitea instance according to the given configuration. Some problems can optionally be fixed by modifying the database or data storage.", - Action: runDoctorCheck, - Flags: []cli.Flag{ - &cli.BoolFlag{ - Name: "list", - Usage: "List the available checks", +func newDoctorCheckCommand() *cli.Command { + return &cli.Command{ + Name: "check", + Usage: "Diagnose and optionally fix problems", + Description: "A command to diagnose problems with the current Gitea instance according to the given configuration. Some problems can optionally be fixed by modifying the database or data storage.", + Action: runDoctorCheck, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "list", + Usage: "List the available checks", + }, + &cli.BoolFlag{ + Name: "default", + Usage: "Run the default checks (if neither --run or --all is set, this is the default behaviour)", + }, + &cli.StringSliceFlag{ + Name: "run", + Usage: "Run the provided checks - (if --default is set, the default checks will also run)", + }, + &cli.BoolFlag{ + Name: "all", + Usage: "Run all the available checks", + }, + &cli.BoolFlag{ + Name: "fix", + Usage: "Automatically fix what we can", + }, + &cli.StringFlag{ + Name: "log-file", + Usage: `Name of the log file (no verbose log output by default). Set to "-" to output to stdout`, + }, + &cli.BoolFlag{ + Name: "color", + Aliases: []string{"H"}, + Usage: "Use color for outputted information", + }, }, - &cli.BoolFlag{ - Name: "default", - Usage: "Run the default checks (if neither --run or --all is set, this is the default behaviour)", - }, - &cli.StringSliceFlag{ - Name: "run", - Usage: "Run the provided checks - (if --default is set, the default checks will also run)", - }, - &cli.BoolFlag{ - Name: "all", - Usage: "Run all the available checks", - }, - &cli.BoolFlag{ - Name: "fix", - Usage: "Automatically fix what we can", - }, - &cli.StringFlag{ - Name: "log-file", - Usage: `Name of the log file (no verbose log output by default). Set to "-" to output to stdout`, - }, - &cli.BoolFlag{ - Name: "color", - Aliases: []string{"H"}, - Usage: "Use color for outputted information", - }, - }, + } } -var cmdRecreateTable = &cli.Command{ - Name: "recreate-table", - Usage: "Recreate tables from XORM definitions and copy the data.", - ArgsUsage: "[TABLE]... : (TABLEs to recreate - leave blank for all)", - Flags: []cli.Flag{ - &cli.BoolFlag{ - Name: "debug", - Usage: "Print SQL commands sent", +func newRecreateTableCommand() *cli.Command { + return &cli.Command{ + Name: "recreate-table", + Usage: "Recreate tables from XORM definitions and copy the data.", + ArgsUsage: "[TABLE]... : (TABLEs to recreate - leave blank for all)", + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "debug", + Usage: "Print SQL commands sent", + }, }, - }, - Description: `The database definitions Gitea uses change across versions, sometimes changing default values and leaving old unused columns. + Description: `The database definitions Gitea uses change across versions, sometimes changing default values and leaving old unused columns. This command will cause Xorm to recreate tables, copying over the data and deleting the old table. You should back-up your database before doing this and ensure that your database is up-to-date first.`, - Action: runRecreateTable, + Action: runRecreateTable, + } } func runRecreateTable(ctx context.Context, cmd *cli.Command) error { diff --git a/cmd/doctor_convert.go b/cmd/doctor_convert.go index 8cb718d3839..f4867912ab3 100644 --- a/cmd/doctor_convert.go +++ b/cmd/doctor_convert.go @@ -14,12 +14,13 @@ import ( "github.com/urfave/cli/v3" ) -// cmdDoctorConvert represents the available convert sub-command. -var cmdDoctorConvert = &cli.Command{ - Name: "convert", - Usage: "Convert the database", - Description: "A command to convert an existing MySQL database from utf8 to utf8mb4 or MSSQL database from varchar to nvarchar", - Action: runDoctorConvert, +func newDoctorConvertCommand() *cli.Command { + return &cli.Command{ + Name: "convert", + Usage: "Convert the database", + Description: "A command to convert an existing MySQL database from utf8 to utf8mb4 or MSSQL database from varchar to nvarchar", + Action: runDoctorConvert, + } } func runDoctorConvert(ctx context.Context, cmd *cli.Command) error { diff --git a/cmd/doctor_test.go b/cmd/doctor_test.go index da942b38b60..7d2f3589475 100644 --- a/cmd/doctor_test.go +++ b/cmd/doctor_test.go @@ -23,7 +23,7 @@ func TestDoctorRun(t *testing.T) { SkipDatabaseInitialization: true, }) app := &cli.Command{ - Commands: []*cli.Command{cmdDoctorCheck}, + Commands: []*cli.Command{newDoctorCheckCommand()}, } err := app.Run(t.Context(), []string{"./gitea", "check", "--run", "test-check"}) assert.NoError(t, err) diff --git a/cmd/dump.go b/cmd/dump.go index 7f0b23ed984..49f4d9e8946 100644 --- a/cmd/dump.go +++ b/cmd/dump.go @@ -23,78 +23,79 @@ import ( "github.com/urfave/cli/v3" ) -// CmdDump represents the available dump sub-command. -var CmdDump = &cli.Command{ - Name: "dump", - Usage: "Dump Gitea files and database", - Description: `Dump compresses all related files and database into zip file. It can be used for backup and capture Gitea server image to send to maintainer`, - Action: runDump, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "file", - Aliases: []string{"f"}, - Usage: `Name of the dump file which will be created, default to "gitea-dump-{time}.zip". Supply '-' for stdout. See type for available types.`, +func newDumpCommand() *cli.Command { + return &cli.Command{ + Name: "dump", + Usage: "Dump Gitea files and database", + Description: `Dump compresses all related files and database into zip file. It can be used for backup and capture Gitea server image to send to maintainer`, + Action: runDump, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "file", + Aliases: []string{"f"}, + Usage: `Name of the dump file which will be created, default to "gitea-dump-{time}.zip". Supply '-' for stdout. See type for available types.`, + }, + &cli.BoolFlag{ + Name: "verbose", + Aliases: []string{"V"}, + Usage: "Show process details", + }, + &cli.BoolFlag{ + Name: "quiet", + Aliases: []string{"q"}, + Usage: "Only display warnings and errors", + }, + &cli.StringFlag{ + Name: "tempdir", + Aliases: []string{"t"}, + Value: os.TempDir(), + Usage: "Temporary dir path", + }, + &cli.StringFlag{ + Name: "database", + Aliases: []string{"d"}, + Usage: "Specify the database SQL syntax: sqlite3, mysql, mssql, postgres", + }, + &cli.BoolFlag{ + Name: "skip-repository", + Aliases: []string{"R"}, + Usage: "Skip the repository dumping", + }, + &cli.BoolFlag{ + Name: "skip-log", + Aliases: []string{"L"}, + Usage: "Skip the log dumping", + }, + &cli.BoolFlag{ + Name: "skip-custom-dir", + Usage: "Skip custom directory", + }, + &cli.BoolFlag{ + Name: "skip-lfs-data", + Usage: "Skip LFS data", + }, + &cli.BoolFlag{ + Name: "skip-attachment-data", + Usage: "Skip attachment data", + }, + &cli.BoolFlag{ + Name: "skip-package-data", + Usage: "Skip package data", + }, + &cli.BoolFlag{ + Name: "skip-index", + Usage: "Skip bleve index data", + }, + &cli.BoolFlag{ + Name: "skip-db", + Usage: "Skip database", + }, + &cli.StringFlag{ + Name: "type", + Usage: `Dump output format, default to "zip", supported types: ` + strings.Join(dump.SupportedOutputTypes, ", "), + }, }, - &cli.BoolFlag{ - Name: "verbose", - Aliases: []string{"V"}, - Usage: "Show process details", - }, - &cli.BoolFlag{ - Name: "quiet", - Aliases: []string{"q"}, - Usage: "Only display warnings and errors", - }, - &cli.StringFlag{ - Name: "tempdir", - Aliases: []string{"t"}, - Value: os.TempDir(), - Usage: "Temporary dir path", - }, - &cli.StringFlag{ - Name: "database", - Aliases: []string{"d"}, - Usage: "Specify the database SQL syntax: sqlite3, mysql, mssql, postgres", - }, - &cli.BoolFlag{ - Name: "skip-repository", - Aliases: []string{"R"}, - Usage: "Skip the repository dumping", - }, - &cli.BoolFlag{ - Name: "skip-log", - Aliases: []string{"L"}, - Usage: "Skip the log dumping", - }, - &cli.BoolFlag{ - Name: "skip-custom-dir", - Usage: "Skip custom directory", - }, - &cli.BoolFlag{ - Name: "skip-lfs-data", - Usage: "Skip LFS data", - }, - &cli.BoolFlag{ - Name: "skip-attachment-data", - Usage: "Skip attachment data", - }, - &cli.BoolFlag{ - Name: "skip-package-data", - Usage: "Skip package data", - }, - &cli.BoolFlag{ - Name: "skip-index", - Usage: "Skip bleve index data", - }, - &cli.BoolFlag{ - Name: "skip-db", - Usage: "Skip database", - }, - &cli.StringFlag{ - Name: "type", - Usage: `Dump output format, default to "zip", supported types: ` + strings.Join(dump.SupportedOutputTypes, ", "), - }, - }, + } } func fatal(format string, args ...any) { diff --git a/cmd/dump_repo.go b/cmd/dump_repo.go index beda305c85f..367454366ec 100644 --- a/cmd/dump_repo.go +++ b/cmd/dump_repo.go @@ -22,61 +22,62 @@ import ( "github.com/urfave/cli/v3" ) -// CmdDumpRepository represents the available dump repository sub-command. -var CmdDumpRepository = &cli.Command{ - Name: "dump-repo", - Usage: "Dump the repository from git/github/gitea/gitlab", - Description: "This is a command for dumping the repository data.", - Action: runDumpRepository, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "git_service", - Value: "", - Usage: "Git service, git, github, gitea, gitlab. If clone_addr could be recognized, this could be ignored.", - }, - &cli.StringFlag{ - Name: "repo_dir", - Aliases: []string{"r"}, - Value: "./data", - Usage: "Repository dir path to store the data", - }, - &cli.StringFlag{ - Name: "clone_addr", - Value: "", - Usage: "The URL will be clone, currently could be a git/github/gitea/gitlab http/https URL", - }, - &cli.StringFlag{ - Name: "auth_username", - Value: "", - Usage: "The username to visit the clone_addr", - }, - &cli.StringFlag{ - Name: "auth_password", - Value: "", - Usage: "The password to visit the clone_addr", - }, - &cli.StringFlag{ - Name: "auth_token", - Value: "", - Usage: "The personal token to visit the clone_addr", - }, - &cli.StringFlag{ - Name: "owner_name", - Value: "", - Usage: "The data will be stored on a directory with owner name if not empty", - }, - &cli.StringFlag{ - Name: "repo_name", - Value: "", - Usage: "The data will be stored on a directory with repository name if not empty", - }, - &cli.StringFlag{ - Name: "units", - Value: "", - Usage: `Which items will be migrated, one or more units should be separated as comma. +func newDumpRepositoryCommand() *cli.Command { + return &cli.Command{ + Name: "dump-repo", + Usage: "Dump the repository from git/github/gitea/gitlab", + Description: "This is a command for dumping the repository data.", + Action: runDumpRepository, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "git_service", + Value: "", + Usage: "Git service, git, github, gitea, gitlab. If clone_addr could be recognized, this could be ignored.", + }, + &cli.StringFlag{ + Name: "repo_dir", + Aliases: []string{"r"}, + Value: "./data", + Usage: "Repository dir path to store the data", + }, + &cli.StringFlag{ + Name: "clone_addr", + Value: "", + Usage: "The URL will be clone, currently could be a git/github/gitea/gitlab http/https URL", + }, + &cli.StringFlag{ + Name: "auth_username", + Value: "", + Usage: "The username to visit the clone_addr", + }, + &cli.StringFlag{ + Name: "auth_password", + Value: "", + Usage: "The password to visit the clone_addr", + }, + &cli.StringFlag{ + Name: "auth_token", + Value: "", + Usage: "The personal token to visit the clone_addr", + }, + &cli.StringFlag{ + Name: "owner_name", + Value: "", + Usage: "The data will be stored on a directory with owner name if not empty", + }, + &cli.StringFlag{ + Name: "repo_name", + Value: "", + Usage: "The data will be stored on a directory with repository name if not empty", + }, + &cli.StringFlag{ + Name: "units", + Value: "", + Usage: `Which items will be migrated, one or more units should be separated as comma. wiki, issues, labels, releases, release_assets, milestones, pull_requests, comments are allowed. Empty means all units.`, + }, }, - }, + } } func runDumpRepository(ctx context.Context, cmd *cli.Command) error { diff --git a/cmd/embedded.go b/cmd/embedded.go index 9180407fd18..e2110756b8f 100644 --- a/cmd/embedded.go +++ b/cmd/embedded.go @@ -23,20 +23,23 @@ import ( "github.com/urfave/cli/v3" ) -// CmdEmbedded represents the available extract sub-command. -var ( - CmdEmbedded = &cli.Command{ +var matchedAssetFiles []assetFile + +func newEmbeddedCommand() *cli.Command { + return &cli.Command{ Name: "embedded", Usage: "Extract embedded resources", Description: "A command for extracting embedded resources, like templates and images", Commands: []*cli.Command{ - subcmdList, - subcmdView, - subcmdExtract, + newEmbeddedListCommand(), + newEmbeddedViewCommand(), + newEmbeddedExtractCommand(), }, } +} - subcmdList = &cli.Command{ +func newEmbeddedListCommand() *cli.Command { + return &cli.Command{ Name: "list", Usage: "List files matching the given pattern", Action: runList, @@ -48,8 +51,10 @@ var ( }, }, } +} - subcmdView = &cli.Command{ +func newEmbeddedViewCommand() *cli.Command { + return &cli.Command{ Name: "view", Usage: "View a file matching the given pattern", Action: runView, @@ -61,8 +66,10 @@ var ( }, }, } +} - subcmdExtract = &cli.Command{ +func newEmbeddedExtractCommand() *cli.Command { + return &cli.Command{ Name: "extract", Usage: "Extract resources", Action: runExtract, @@ -91,9 +98,7 @@ var ( }, }, } - - matchedAssetFiles []assetFile -) +} type assetFile struct { fs *assetfs.LayeredFS diff --git a/cmd/generate.go b/cmd/generate.go index 9cb4cf39171..b94ff79aaec 100644 --- a/cmd/generate.go +++ b/cmd/generate.go @@ -15,45 +15,52 @@ import ( "github.com/urfave/cli/v3" ) -var ( - // CmdGenerate represents the available generate sub-command. - CmdGenerate = &cli.Command{ +func newGenerateCommand() *cli.Command { + return &cli.Command{ Name: "generate", Usage: "Generate Gitea's secrets/keys/tokens", Commands: []*cli.Command{ - subcmdSecret, + newGenerateSecretCommand(), }, } +} - subcmdSecret = &cli.Command{ +func newGenerateSecretCommand() *cli.Command { + return &cli.Command{ Name: "secret", Usage: "Generate a secret token", Commands: []*cli.Command{ - microcmdGenerateInternalToken, - microcmdGenerateLfsJwtSecret, - microcmdGenerateSecretKey, + newGenerateInternalTokenCommand(), + newGenerateLfsJWTSecretCommand(), + newGenerateSecretKeyCommand(), }, } +} - microcmdGenerateInternalToken = &cli.Command{ +func newGenerateInternalTokenCommand() *cli.Command { + return &cli.Command{ Name: "INTERNAL_TOKEN", Usage: "Generate a new INTERNAL_TOKEN", Action: runGenerateInternalToken, } +} - microcmdGenerateLfsJwtSecret = &cli.Command{ +func newGenerateLfsJWTSecretCommand() *cli.Command { + return &cli.Command{ Name: "JWT_SECRET", Aliases: []string{"LFS_JWT_SECRET"}, Usage: "Generate a new JWT_SECRET", Action: runGenerateLfsJwtSecret, } +} - microcmdGenerateSecretKey = &cli.Command{ +func newGenerateSecretKeyCommand() *cli.Command { + return &cli.Command{ Name: "SECRET_KEY", Usage: "Generate a new SECRET_KEY", Action: runGenerateSecretKey, } -) +} func runGenerateInternalToken(_ context.Context, c *cli.Command) error { internalToken, err := generate.NewInternalToken() diff --git a/cmd/hook.go b/cmd/hook.go index 992e52c2790..4a6c7c2905a 100644 --- a/cmd/hook.go +++ b/cmd/hook.go @@ -28,23 +28,24 @@ const ( hookBatchSize = 500 ) -var ( - // CmdHook represents the available hooks sub-command. - CmdHook = &cli.Command{ +func newHookCommand() *cli.Command { + return &cli.Command{ Name: "hook", Usage: "(internal) Should only be called by Git", Hidden: true, // internal commands shouldn't be visible Description: "Delegate commands to corresponding Git hooks", Before: PrepareConsoleLoggerLevel(log.FATAL), Commands: []*cli.Command{ - subcmdHookPreReceive, - subcmdHookUpdate, - subcmdHookPostReceive, - subcmdHookProcReceive, + newHookPreReceiveCommand(), + newHookUpdateCommand(), + newHookPostReceiveCommand(), + newHookProcReceiveCommand(), }, } +} - subcmdHookPreReceive = &cli.Command{ +func newHookPreReceiveCommand() *cli.Command { + return &cli.Command{ Name: "pre-receive", Usage: "Delegate pre-receive Git hook", Description: "This command should only be called by Git", @@ -55,7 +56,10 @@ var ( }, }, } - subcmdHookUpdate = &cli.Command{ +} + +func newHookUpdateCommand() *cli.Command { + return &cli.Command{ Name: "update", Usage: "Delegate update Git hook", Description: "This command should only be called by Git", @@ -66,7 +70,10 @@ var ( }, }, } - subcmdHookPostReceive = &cli.Command{ +} + +func newHookPostReceiveCommand() *cli.Command { + return &cli.Command{ Name: "post-receive", Usage: "Delegate post-receive Git hook", Description: "This command should only be called by Git", @@ -77,8 +84,11 @@ var ( }, }, } - // Note: new hook since git 2.29 - subcmdHookProcReceive = &cli.Command{ +} + +// Note: new hook since git 2.29 +func newHookProcReceiveCommand() *cli.Command { + return &cli.Command{ Name: "proc-receive", Usage: "Delegate proc-receive Git hook", Description: "This command should only be called by Git", @@ -89,7 +99,7 @@ var ( }, }, } -) +} type delayWriter struct { internal io.Writer diff --git a/cmd/keys.go b/cmd/keys.go index 035d39bfb86..912cf250915 100644 --- a/cmd/keys.go +++ b/cmd/keys.go @@ -15,40 +15,42 @@ import ( "github.com/urfave/cli/v3" ) -// CmdKeys represents the available keys sub-command -var CmdKeys = &cli.Command{ - Name: "keys", - Usage: "(internal) Should only be called by SSH server", - Hidden: true, // internal commands shouldn't be visible - Description: "Queries the Gitea database to get the authorized command for a given ssh key fingerprint", - Before: PrepareConsoleLoggerLevel(log.FATAL), - Action: runKeys, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "expected", - Aliases: []string{"e"}, - Value: "git", - Usage: "Expected user for whom provide key commands", +// NewKeysCommand returns the internal SSH key lookup sub-command. +func NewKeysCommand() *cli.Command { + return &cli.Command{ + Name: "keys", + Usage: "(internal) Should only be called by SSH server", + Hidden: true, // internal commands shouldn't be visible + Description: "Queries the Gitea database to get the authorized command for a given ssh key fingerprint", + Before: PrepareConsoleLoggerLevel(log.FATAL), + Action: runKeys, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "expected", + Aliases: []string{"e"}, + Value: "git", + Usage: "Expected user for whom provide key commands", + }, + &cli.StringFlag{ + Name: "username", + Aliases: []string{"u"}, + Value: "", + Usage: "Username trying to log in by SSH", + }, + &cli.StringFlag{ + Name: "type", + Aliases: []string{"t"}, + Value: "", + Usage: "Type of the SSH key provided to the SSH Server (requires content to be provided too)", + }, + &cli.StringFlag{ + Name: "content", + Aliases: []string{"k"}, + Value: "", + Usage: "Base64 encoded content of the SSH key provided to the SSH Server (requires type to be provided too)", + }, }, - &cli.StringFlag{ - Name: "username", - Aliases: []string{"u"}, - Value: "", - Usage: "Username trying to log in by SSH", - }, - &cli.StringFlag{ - Name: "type", - Aliases: []string{"t"}, - Value: "", - Usage: "Type of the SSH key provided to the SSH Server (requires content to be provided too)", - }, - &cli.StringFlag{ - Name: "content", - Aliases: []string{"k"}, - Value: "", - Usage: "Base64 encoded content of the SSH key provided to the SSH Server (requires type to be provided too)", - }, - }, + } } func runKeys(ctx context.Context, c *cli.Command) error { diff --git a/cmd/main.go b/cmd/main.go index 2ee00382d75..a6b89a6fada 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -112,35 +112,36 @@ func NewMainApp(appVer AppVersion) *cli.Command { Usage: "Set custom path (defaults to '{WorkPath}/custom')", }, } + webCmd := newWebCommand() // these sub-commands need to use a config file subCmdWithConfig := []*cli.Command{ - CmdWeb, - CmdServ, - CmdHook, - CmdKeys, - CmdDump, - CmdAdmin, - CmdMigrate, - CmdDoctor, - CmdManager, - CmdEmbedded, - CmdMigrateStorage, - CmdDumpRepository, - CmdRestoreRepository, - CmdActions, + webCmd, + newServCommand(), + newHookCommand(), + NewKeysCommand(), + newDumpCommand(), + newAdminCommand(), + newMigrateCommand(), + newDoctorCommand(), + newManagerCommand(), + newEmbeddedCommand(), + newMigrateStorageCommand(), + newDumpRepositoryCommand(), + newRestoreRepositoryCommand(), + newActionsCommand(), } // these sub-commands do not need the config file, and they do not depend on any path or environment variable. subCmdStandalone := []*cli.Command{ cmdConfig(), cmdCert(), - CmdGenerate, - CmdDocs, + newGenerateCommand(), + newDocsCommand(), } // TODO: we should eventually drop the default command, // but not sure whether it would break Windows users who used to double-click the EXE to run. - app.DefaultCommand = CmdWeb.Name + app.DefaultCommand = webCmd.Name app.Before = PrepareConsoleLoggerLevel(log.INFO) for i := range subCmdWithConfig { diff --git a/cmd/manager.go b/cmd/manager.go index f0935ea0657..586c65990bc 100644 --- a/cmd/manager.go +++ b/cmd/manager.go @@ -13,22 +13,24 @@ import ( "github.com/urfave/cli/v3" ) -var ( - // CmdManager represents the manager command - CmdManager = &cli.Command{ +func newManagerCommand() *cli.Command { + return &cli.Command{ Name: "manager", Usage: "Manage the running gitea process", Description: "This is a command for managing the running gitea process", Commands: []*cli.Command{ - subcmdShutdown, - subcmdRestart, - subcmdReloadTemplates, - subcmdFlushQueues, - subcmdLogging, - subCmdProcesses, + newShutdownCommand(), + newRestartCommand(), + newReloadTemplatesCommand(), + newFlushQueuesCommand(), + newLoggingCommand(), + newProcessesCommand(), }, } - subcmdShutdown = &cli.Command{ +} + +func newShutdownCommand() *cli.Command { + return &cli.Command{ Name: "shutdown", Usage: "Gracefully shutdown the running process", Flags: []cli.Flag{ @@ -38,7 +40,10 @@ var ( }, Action: runShutdown, } - subcmdRestart = &cli.Command{ +} + +func newRestartCommand() *cli.Command { + return &cli.Command{ Name: "restart", Usage: "Gracefully restart the running process - (not implemented for windows servers)", Flags: []cli.Flag{ @@ -48,7 +53,10 @@ var ( }, Action: runRestart, } - subcmdReloadTemplates = &cli.Command{ +} + +func newReloadTemplatesCommand() *cli.Command { + return &cli.Command{ Name: "reload-templates", Usage: "Reload template files in the running process", Flags: []cli.Flag{ @@ -58,7 +66,10 @@ var ( }, Action: runReloadTemplates, } - subcmdFlushQueues = &cli.Command{ +} + +func newFlushQueuesCommand() *cli.Command { + return &cli.Command{ Name: "flush-queues", Usage: "Flush queues in the running process", Action: runFlushQueues, @@ -77,7 +88,10 @@ var ( }, }, } - subCmdProcesses = &cli.Command{ +} + +func newProcessesCommand() *cli.Command { + return &cli.Command{ Name: "processes", Usage: "Display running processes within the current process", Action: runProcesses, @@ -107,7 +121,7 @@ var ( }, }, } -) +} func runShutdown(ctx context.Context, c *cli.Command) error { setup(ctx, c.Bool("debug")) diff --git a/cmd/manager_logging.go b/cmd/manager_logging.go index ac29e7d3e50..5812e707e20 100644 --- a/cmd/manager_logging.go +++ b/cmd/manager_logging.go @@ -15,8 +15,8 @@ import ( "github.com/urfave/cli/v3" ) -var ( - defaultLoggingFlags = []cli.Flag{ +func defaultLoggingFlags() []cli.Flag { + return []cli.Flag{ &cli.StringFlag{ Name: "logger", Usage: `Logger name - will default to "default"`, @@ -57,8 +57,10 @@ var ( Name: "debug", }, } +} - subcmdLogging = &cli.Command{ +func newLoggingCommand() *cli.Command { + return &cli.Command{ Name: "logging", Usage: "Adjust logging commands", Commands: []*cli.Command{ @@ -109,7 +111,7 @@ var ( { Name: "file", Usage: "Add a file logger", - Flags: append(defaultLoggingFlags, []cli.Flag{ + Flags: append(defaultLoggingFlags(), []cli.Flag{ &cli.StringFlag{ Name: "filename", Aliases: []string{"f"}, @@ -150,7 +152,7 @@ var ( }, { Name: "conn", Usage: "Add a net conn logger", - Flags: append(defaultLoggingFlags, []cli.Flag{ + Flags: append(defaultLoggingFlags(), []cli.Flag{ &cli.BoolFlag{ Name: "reconnect-on-message", Aliases: []string{"R"}, @@ -191,7 +193,7 @@ var ( }, }, } -) +} func runRemoveLogger(ctx context.Context, c *cli.Command) error { setup(ctx, c.Bool("debug")) diff --git a/cmd/migrate.go b/cmd/migrate.go index e24dc9e5720..016f8a0db56 100644 --- a/cmd/migrate.go +++ b/cmd/migrate.go @@ -14,12 +14,13 @@ import ( "github.com/urfave/cli/v3" ) -// CmdMigrate represents the available migrate sub-command. -var CmdMigrate = &cli.Command{ - Name: "migrate", - Usage: "Migrate the database", - Description: `This is a command for migrating the database, so that you can run "gitea admin create user" before starting the server.`, - Action: runMigrate, +func newMigrateCommand() *cli.Command { + return &cli.Command{ + Name: "migrate", + Usage: "Migrate the database", + Description: `This is a command for migrating the database, so that you can run "gitea admin create user" before starting the server.`, + Action: runMigrate, + } } func runMigrate(ctx context.Context, c *cli.Command) error { diff --git a/cmd/migrate_storage.go b/cmd/migrate_storage.go index a6bf9fa4b54..c9b82055c13 100644 --- a/cmd/migrate_storage.go +++ b/cmd/migrate_storage.go @@ -25,107 +25,108 @@ import ( "github.com/urfave/cli/v3" ) -// CmdMigrateStorage represents the available migrate storage sub-command. -var CmdMigrateStorage = &cli.Command{ - Name: "migrate-storage", - Usage: "Migrate the storage", - Description: "Copies stored files from storage configured in app.ini to parameter-configured storage", - Action: runMigrateStorage, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "type", - Aliases: []string{"t"}, - Value: "", - Usage: "Type of stored files to copy. Allowed types: 'attachments', 'lfs', 'avatars', 'repo-avatars', 'repo-archivers', 'packages', 'actions-log', 'actions-artifacts'", +func newMigrateStorageCommand() *cli.Command { + return &cli.Command{ + Name: "migrate-storage", + Usage: "Migrate the storage", + Description: "Copies stored files from storage configured in app.ini to parameter-configured storage", + Action: runMigrateStorage, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "type", + Aliases: []string{"t"}, + Value: "", + Usage: "Type of stored files to copy. Allowed types: 'attachments', 'lfs', 'avatars', 'repo-avatars', 'repo-archivers', 'packages', 'actions-log', 'actions-artifacts'", + }, + &cli.StringFlag{ + Name: "storage", + Aliases: []string{"s"}, + Value: "", + Usage: "New storage type: local (default), minio or azureblob", + }, + &cli.StringFlag{ + Name: "path", + Aliases: []string{"p"}, + Value: "", + Usage: "New storage placement if store is local (leave blank for default)", + }, + // Minio Storage special configurations + &cli.StringFlag{ + Name: "minio-endpoint", + Value: "", + Usage: "Minio storage endpoint", + }, + &cli.StringFlag{ + Name: "minio-access-key-id", + Value: "", + Usage: "Minio storage accessKeyID", + }, + &cli.StringFlag{ + Name: "minio-secret-access-key", + Value: "", + Usage: "Minio storage secretAccessKey", + }, + &cli.StringFlag{ + Name: "minio-bucket", + Value: "", + Usage: "Minio storage bucket", + }, + &cli.StringFlag{ + Name: "minio-location", + Value: "", + Usage: "Minio storage location to create bucket", + }, + &cli.StringFlag{ + Name: "minio-base-path", + Value: "", + Usage: "Minio storage base path on the bucket", + }, + &cli.BoolFlag{ + Name: "minio-use-ssl", + Usage: "Enable SSL for minio", + }, + &cli.BoolFlag{ + Name: "minio-insecure-skip-verify", + Usage: "Skip SSL verification", + }, + &cli.StringFlag{ + Name: "minio-checksum-algorithm", + Value: "", + Usage: "Minio checksum algorithm (default/md5)", + }, + &cli.StringFlag{ + Name: "minio-bucket-lookup-type", + Value: "", + Usage: "Minio bucket lookup type", + }, + // Azure Blob Storage special configurations + &cli.StringFlag{ + Name: "azureblob-endpoint", + Value: "", + Usage: "Azure Blob storage endpoint", + }, + &cli.StringFlag{ + Name: "azureblob-account-name", + Value: "", + Usage: "Azure Blob storage account name", + }, + &cli.StringFlag{ + Name: "azureblob-account-key", + Value: "", + Usage: "Azure Blob storage account key", + }, + &cli.StringFlag{ + Name: "azureblob-container", + Value: "", + Usage: "Azure Blob storage container", + }, + &cli.StringFlag{ + Name: "azureblob-base-path", + Value: "", + Usage: "Azure Blob storage base path", + }, }, - &cli.StringFlag{ - Name: "storage", - Aliases: []string{"s"}, - Value: "", - Usage: "New storage type: local (default), minio or azureblob", - }, - &cli.StringFlag{ - Name: "path", - Aliases: []string{"p"}, - Value: "", - Usage: "New storage placement if store is local (leave blank for default)", - }, - // Minio Storage special configurations - &cli.StringFlag{ - Name: "minio-endpoint", - Value: "", - Usage: "Minio storage endpoint", - }, - &cli.StringFlag{ - Name: "minio-access-key-id", - Value: "", - Usage: "Minio storage accessKeyID", - }, - &cli.StringFlag{ - Name: "minio-secret-access-key", - Value: "", - Usage: "Minio storage secretAccessKey", - }, - &cli.StringFlag{ - Name: "minio-bucket", - Value: "", - Usage: "Minio storage bucket", - }, - &cli.StringFlag{ - Name: "minio-location", - Value: "", - Usage: "Minio storage location to create bucket", - }, - &cli.StringFlag{ - Name: "minio-base-path", - Value: "", - Usage: "Minio storage base path on the bucket", - }, - &cli.BoolFlag{ - Name: "minio-use-ssl", - Usage: "Enable SSL for minio", - }, - &cli.BoolFlag{ - Name: "minio-insecure-skip-verify", - Usage: "Skip SSL verification", - }, - &cli.StringFlag{ - Name: "minio-checksum-algorithm", - Value: "", - Usage: "Minio checksum algorithm (default/md5)", - }, - &cli.StringFlag{ - Name: "minio-bucket-lookup-type", - Value: "", - Usage: "Minio bucket lookup type", - }, - // Azure Blob Storage special configurations - &cli.StringFlag{ - Name: "azureblob-endpoint", - Value: "", - Usage: "Azure Blob storage endpoint", - }, - &cli.StringFlag{ - Name: "azureblob-account-name", - Value: "", - Usage: "Azure Blob storage account name", - }, - &cli.StringFlag{ - Name: "azureblob-account-key", - Value: "", - Usage: "Azure Blob storage account key", - }, - &cli.StringFlag{ - Name: "azureblob-container", - Value: "", - Usage: "Azure Blob storage container", - }, - &cli.StringFlag{ - Name: "azureblob-base-path", - Value: "", - Usage: "Azure Blob storage base path", - }, - }, + } } func migrateAttachments(ctx context.Context, dstStorage storage.ObjectStorage) error { diff --git a/cmd/restore_repo.go b/cmd/restore_repo.go index c61f5a582ef..26b4682f136 100644 --- a/cmd/restore_repo.go +++ b/cmd/restore_repo.go @@ -13,40 +13,41 @@ import ( "github.com/urfave/cli/v3" ) -// CmdRestoreRepository represents the available restore a repository sub-command. -var CmdRestoreRepository = &cli.Command{ - Name: "restore-repo", - Usage: "Restore the repository from disk", - Description: "This is a command for restoring the repository data.", - Action: runRestoreRepository, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "repo_dir", - Aliases: []string{"r"}, - Value: "./data", - Usage: "Repository dir path to restore from", - }, - &cli.StringFlag{ - Name: "owner_name", - Value: "", - Usage: "Restore destination owner name", - }, - &cli.StringFlag{ - Name: "repo_name", - Value: "", - Usage: "Restore destination repository name", - }, - &cli.StringFlag{ - Name: "units", - Value: "", - Usage: `Which items will be restored, one or more units should be separated as comma. +func newRestoreRepositoryCommand() *cli.Command { + return &cli.Command{ + Name: "restore-repo", + Usage: "Restore the repository from disk", + Description: "This is a command for restoring the repository data.", + Action: runRestoreRepository, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "repo_dir", + Aliases: []string{"r"}, + Value: "./data", + Usage: "Repository dir path to restore from", + }, + &cli.StringFlag{ + Name: "owner_name", + Value: "", + Usage: "Restore destination owner name", + }, + &cli.StringFlag{ + Name: "repo_name", + Value: "", + Usage: "Restore destination repository name", + }, + &cli.StringFlag{ + Name: "units", + Value: "", + Usage: `Which items will be restored, one or more units should be separated as comma. wiki, issues, labels, releases, release_assets, milestones, pull_requests, comments are allowed. Empty means all units.`, + }, + &cli.BoolFlag{ + Name: "validation", + Usage: "Sanity check the content of the files before trying to load them", + }, }, - &cli.BoolFlag{ - Name: "validation", - Usage: "Sanity check the content of the files before trying to load them", - }, - }, + } } func runRestoreRepository(ctx context.Context, c *cli.Command) error { diff --git a/cmd/serv.go b/cmd/serv.go index 4110fda0d50..a35d476c860 100644 --- a/cmd/serv.go +++ b/cmd/serv.go @@ -35,22 +35,23 @@ import ( "github.com/urfave/cli/v3" ) -// CmdServ represents the available serv sub-command. -var CmdServ = &cli.Command{ - Name: "serv", - Usage: "(internal) Should only be called by SSH shell", - Description: "Serv provides access auth for repositories", - Hidden: true, // Internal commands shouldn't be visible in help - Before: PrepareConsoleLoggerLevel(log.FATAL), - Action: runServ, - Flags: []cli.Flag{ - &cli.BoolFlag{ - Name: "enable-pprof", +func newServCommand() *cli.Command { + return &cli.Command{ + Name: "serv", + Usage: "(internal) Should only be called by SSH shell", + Description: "Serv provides access auth for repositories", + Hidden: true, // Internal commands shouldn't be visible in help + Before: PrepareConsoleLoggerLevel(log.FATAL), + Action: runServ, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "enable-pprof", + }, + &cli.BoolFlag{ + Name: "debug", + }, }, - &cli.BoolFlag{ - Name: "debug", - }, - }, + } } func setup(ctx context.Context, debug bool) { diff --git a/cmd/web.go b/cmd/web.go index 5000e780c56..994c481fc08 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -34,42 +34,43 @@ import ( // PIDFile could be set from build tag var PIDFile = "/run/gitea.pid" -// CmdWeb represents the available web sub-command. -var CmdWeb = &cli.Command{ - Name: "web", - Usage: "Start Gitea web server", - Description: `Gitea web server is the only thing you need to run, +func newWebCommand() *cli.Command { + return &cli.Command{ + Name: "web", + Usage: "Start Gitea web server", + Description: `Gitea web server is the only thing you need to run, and it takes care of all the other things for you`, - Before: PrepareConsoleLoggerLevel(log.INFO), - Action: runWeb, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "port", - Aliases: []string{"p"}, - Value: "3000", - Usage: "Temporary port number to prevent conflict", + Before: PrepareConsoleLoggerLevel(log.INFO), + Action: runWeb, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "port", + Aliases: []string{"p"}, + Value: "3000", + Usage: "Temporary port number to prevent conflict", + }, + &cli.StringFlag{ + Name: "install-port", + Value: "3000", + Usage: "Temporary port number to run the install page on to prevent conflict", + }, + &cli.StringFlag{ + Name: "pid", + Aliases: []string{"P"}, + Value: PIDFile, + Usage: "Custom pid file path", + }, + &cli.BoolFlag{ + Name: "quiet", + Aliases: []string{"q"}, + Usage: "Only display Fatal logging errors until logging is set-up", + }, + &cli.BoolFlag{ + Name: "verbose", + Usage: "Set initial logging to TRACE level until logging is properly set-up", + }, }, - &cli.StringFlag{ - Name: "install-port", - Value: "3000", - Usage: "Temporary port number to run the install page on to prevent conflict", - }, - &cli.StringFlag{ - Name: "pid", - Aliases: []string{"P"}, - Value: PIDFile, - Usage: "Custom pid file path", - }, - &cli.BoolFlag{ - Name: "quiet", - Aliases: []string{"q"}, - Usage: "Only display Fatal logging errors until logging is set-up", - }, - &cli.BoolFlag{ - Name: "verbose", - Usage: "Set initial logging to TRACE level until logging is properly set-up", - }, - }, + } } func runHTTPRedirector() { diff --git a/tests/integration/cmd_keys_test.go b/tests/integration/cmd_keys_test.go index d911bdf17d2..b71d023f72f 100644 --- a/tests/integration/cmd_keys_test.go +++ b/tests/integration/cmd_keys_test.go @@ -10,7 +10,6 @@ import ( "code.gitea.io/gitea/cmd" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/util" "github.com/stretchr/testify/assert" @@ -38,13 +37,14 @@ func Test_CmdKeys(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // FIXME: this test is not quite right. Each "command run" always re-initializes settings - defer test.MockVariableValue(&cmd.CmdKeys.Before, nil)() // don't re-initialize logger during the test + keysCmd := cmd.NewKeysCommand() + keysCmd.Before = nil // don't re-initialize logger during the test var stdout, stderr bytes.Buffer app := &cli.Command{ Writer: &stdout, ErrWriter: &stderr, - Commands: []*cli.Command{cmd.CmdKeys}, + Commands: []*cli.Command{keysCmd}, } err := app.Run(t.Context(), append([]string{"prog"}, tt.args...)) if tt.wantErr {