diff --git a/.eslintrc.yaml b/.eslintrc.yaml index f28d2ac24..91019cde8 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -4,7 +4,6 @@ reportUnusedDisableDirectives: true ignorePatterns: - /web_src/js/vendor - /web_src/fomantic - - /public/assets/js parserOptions: sourceType: module @@ -311,7 +310,7 @@ rules: jquery/no-merge: [2] jquery/no-param: [2] jquery/no-parent: [0] - jquery/no-parents: [0] + jquery/no-parents: [2] jquery/no-parse-html: [2] jquery/no-prop: [2] jquery/no-proxy: [2] @@ -320,8 +319,8 @@ rules: jquery/no-show: [2] jquery/no-size: [2] jquery/no-sizzle: [0] - jquery/no-slide: [0] - jquery/no-submit: [0] + jquery/no-slide: [2] + jquery/no-submit: [2] jquery/no-text: [0] jquery/no-toggle: [2] jquery/no-trigger: [0] @@ -459,7 +458,7 @@ rules: no-jquery/no-other-utils: [2] no-jquery/no-param: [2] no-jquery/no-parent: [0] - no-jquery/no-parents: [0] + no-jquery/no-parents: [2] no-jquery/no-parse-html-literal: [0] no-jquery/no-parse-html: [2] no-jquery/no-parse-json: [2] diff --git a/.golangci.yml b/.golangci.yml index 6d52f9940..6d835909f 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,13 +1,14 @@ linters: + enable-all: false + disable-all: true + fast: false enable: - bidichk - # - deadcode # deprecated - https://github.com/golangci/golangci-lint/issues/1841 - depguard - dupl - errcheck - forbidigo - gocritic - # - gocyclo # The cyclomatic complexety of a lot of functions is too high, we should refactor those another time. - gofmt - gofumpt - gosimple @@ -17,16 +18,11 @@ linters: - nolintlint - revive - staticcheck - # - structcheck # deprecated - https://github.com/golangci/golangci-lint/issues/1841 - stylecheck - typecheck - unconvert - unused - # - varcheck # deprecated - https://github.com/golangci/golangci-lint/issues/1841 - wastedassign - enable-all: false - disable-all: true - fast: false run: timeout: 10m @@ -35,6 +31,9 @@ run: - public - web_src +output: + sort-results: true + linters-settings: stylecheck: checks: ["all", "-ST1005", "-ST1003"] @@ -51,27 +50,37 @@ linters-settings: errorCode: 1 warningCode: 1 rules: + - name: atomic + - name: bare-return - name: blank-imports + - name: constant-logical-expr - name: context-as-argument - name: context-keys-type - name: dot-imports + - name: duplicated-imports + - name: empty-lines + - name: error-naming - name: error-return - name: error-strings - - name: error-naming + - name: errorf - name: exported + - name: identical-branches - name: if-return - name: increment-decrement - - name: var-naming - - name: var-declaration + - name: indent-error-flow + - name: modifies-value-receiver - name: package-comments - name: range - name: receiver-naming + - name: redefines-builtin-id + - name: string-of-int + - name: superfluous-else - name: time-naming + - name: unconditional-recursion - name: unexported-return - - name: indent-error-flow - - name: errorf - - name: duplicated-imports - - name: modifies-value-receiver + - name: unreachable-code + - name: var-declaration + - name: var-naming gofumpt: extra-rules: true depguard: @@ -96,8 +105,12 @@ linters-settings: issues: max-issues-per-linter: 0 max-same-issues: 0 + exclude-dirs: [node_modules, public, web_src] + exclude-case-sensitive: true exclude-rules: - # Exclude some linters from running on tests files. + - path: models/db/sql_postgres_with_schema.go + linters: + - nolintlint - path: _test\.go linters: - gocyclo @@ -115,19 +128,19 @@ issues: - path: cmd linters: - forbidigo - - linters: + - text: "webhook" + linters: - dupl - text: "webhook" - - linters: + - text: "`ID' should not be capitalized" + linters: - gocritic - text: "`ID' should not be capitalized" - - linters: + - text: "swagger" + linters: - unused - deadcode - text: "swagger" - - linters: + - text: "argument x is overwritten before first use" + linters: - staticcheck - text: "argument x is overwritten before first use" - text: "commentFormatting: put a space between `//` and comment text" linters: - gocritic diff --git a/Makefile b/Makefile index 6e46283ac..6d4e3e070 100644 --- a/Makefile +++ b/Makefile @@ -913,8 +913,9 @@ webpack: $(WEBPACK_DEST) $(WEBPACK_DEST): $(WEBPACK_SOURCES) $(WEBPACK_CONFIGS) package-lock.json @$(MAKE) -s node-check node_modules - rm -rf $(WEBPACK_DEST_ENTRIES) - npx webpack + @rm -rf $(WEBPACK_DEST_ENTRIES) + @echo "Running webpack..." + @BROWSERSLIST_IGNORE_OLD_DATA=true npx webpack @touch $(WEBPACK_DEST) .PHONY: svg diff --git a/cmd/admin_auth.go b/cmd/admin_auth.go index ec92e342d..4777a9290 100644 --- a/cmd/admin_auth.go +++ b/cmd/admin_auth.go @@ -4,6 +4,7 @@ package cmd import ( + "errors" "fmt" "os" "text/tabwriter" @@ -91,7 +92,7 @@ func runListAuth(c *cli.Context) error { func runDeleteAuth(c *cli.Context) error { if !c.IsSet("id") { - return fmt.Errorf("--id flag is missing") + return errors.New("--id flag is missing") } ctx, cancel := installSignals() diff --git a/cmd/admin_auth_oauth.go b/cmd/admin_auth_oauth.go index c151c0af2..8e6239ac3 100644 --- a/cmd/admin_auth_oauth.go +++ b/cmd/admin_auth_oauth.go @@ -4,6 +4,7 @@ package cmd import ( + "errors" "fmt" "net/url" @@ -193,7 +194,7 @@ func runAddOauth(c *cli.Context) error { func runUpdateOauth(c *cli.Context) error { if !c.IsSet("id") { - return fmt.Errorf("--id flag is missing") + return errors.New("--id flag is missing") } ctx, cancel := installSignals() diff --git a/cmd/admin_auth_stmp.go b/cmd/admin_auth_stmp.go index 58a6e2ac2..d72474690 100644 --- a/cmd/admin_auth_stmp.go +++ b/cmd/admin_auth_stmp.go @@ -5,7 +5,6 @@ package cmd import ( "errors" - "fmt" "strings" auth_model "code.gitea.io/gitea/models/auth" @@ -166,7 +165,7 @@ func runAddSMTP(c *cli.Context) error { func runUpdateSMTP(c *cli.Context) error { if !c.IsSet("id") { - return fmt.Errorf("--id flag is missing") + return errors.New("--id flag is missing") } ctx, cancel := installSignals() diff --git a/cmd/admin_user_delete.go b/cmd/admin_user_delete.go index 1cbc6f752..520557554 100644 --- a/cmd/admin_user_delete.go +++ b/cmd/admin_user_delete.go @@ -4,6 +4,7 @@ package cmd import ( + "errors" "fmt" "strings" @@ -42,7 +43,7 @@ var microcmdUserDelete = &cli.Command{ func runDeleteUser(c *cli.Context) error { if !c.IsSet("id") && !c.IsSet("username") && !c.IsSet("email") { - return fmt.Errorf("You must provide the id, username or email of a user to delete") + return errors.New("You must provide the id, username or email of a user to delete") } ctx, cancel := installSignals() diff --git a/cmd/admin_user_generate_access_token.go b/cmd/admin_user_generate_access_token.go index 6e7893968..6c2c10494 100644 --- a/cmd/admin_user_generate_access_token.go +++ b/cmd/admin_user_generate_access_token.go @@ -4,6 +4,7 @@ package cmd import ( + "errors" "fmt" auth_model "code.gitea.io/gitea/models/auth" @@ -42,7 +43,7 @@ var microcmdUserGenerateAccessToken = &cli.Command{ func runGenerateAccessToken(c *cli.Context) error { if !c.IsSet("username") { - return fmt.Errorf("You must provide a username to generate a token for") + return errors.New("You must provide a username to generate a token for") } ctx, cancel := installSignals() @@ -68,7 +69,7 @@ func runGenerateAccessToken(c *cli.Context) error { return err } if exist { - return fmt.Errorf("access token name has been used already") + return errors.New("access token name has been used already") } // make sure the scopes are valid diff --git a/cmd/embedded.go b/cmd/embedded.go index 71d483d11..9f03f7be7 100644 --- a/cmd/embedded.go +++ b/cmd/embedded.go @@ -157,9 +157,9 @@ func runViewDo(c *cli.Context) error { } if len(matchedAssetFiles) == 0 { - return fmt.Errorf("no files matched the given pattern") + return errors.New("no files matched the given pattern") } else if len(matchedAssetFiles) > 1 { - return fmt.Errorf("too many files matched the given pattern, try to be more specific") + return errors.New("too many files matched the given pattern, try to be more specific") } data, err := matchedAssetFiles[0].fs.ReadFile(matchedAssetFiles[0].name) @@ -180,7 +180,7 @@ func runExtractDo(c *cli.Context) error { } if c.NArg() == 0 { - return fmt.Errorf("a list of pattern of files to extract is mandatory (e.g. '**' for all)") + return errors.New("a list of pattern of files to extract is mandatory (e.g. '**' for all)") } destdir := "." diff --git a/cmd/hook.go b/cmd/hook.go index 81955e753..ff3059f9d 100644 --- a/cmd/hook.go +++ b/cmd/hook.go @@ -482,7 +482,7 @@ func hookPrintResults(results []private.HookPostReceiveBranchResult) { fmt.Fprintf(os.Stderr, " %s\n", res.URL) } fmt.Fprintln(os.Stderr, "") - os.Stderr.Sync() + _ = os.Stderr.Sync() } } diff --git a/cmd/manager_logging.go b/cmd/manager_logging.go index 2c701f267..6049b00d5 100644 --- a/cmd/manager_logging.go +++ b/cmd/manager_logging.go @@ -4,6 +4,7 @@ package cmd import ( + "errors" "fmt" "os" @@ -249,7 +250,7 @@ func runAddFileLogger(c *cli.Context) error { if c.IsSet("filename") { vals["filename"] = c.String("filename") } else { - return fmt.Errorf("filename must be set when creating a file logger") + return errors.New("filename must be set when creating a file logger") } if c.IsSet("rotate") { vals["rotate"] = c.Bool("rotate") diff --git a/go.mod b/go.mod index a7ca6c202..4151d60e8 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4 github.com/42wim/sshsig v0.0.0-20211121163825-841cf5bbc121 github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 + github.com/ProtonMail/go-crypto v1.0.0 github.com/PuerkitoBio/goquery v1.8.1 github.com/alecthomas/chroma/v2 v2.13.0 github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb @@ -129,7 +130,6 @@ require ( github.com/Masterminds/semver/v3 v3.2.1 // indirect github.com/Masterminds/sprig/v3 v3.2.3 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect - github.com/ProtonMail/go-crypto v1.0.0 // indirect github.com/RoaringBitmap/roaring v1.7.0 // indirect github.com/andybalholm/brotli v1.1.0 // indirect github.com/andybalholm/cascadia v1.3.2 // indirect diff --git a/models/actions/run.go b/models/actions/run.go index 493bd0173..397455e41 100644 --- a/models/actions/run.go +++ b/models/actions/run.go @@ -98,13 +98,10 @@ func (run *ActionRun) LoadAttributes(ctx context.Context) error { return nil } - if run.Repo == nil { - repo, err := repo_model.GetRepositoryByID(ctx, run.RepoID) - if err != nil { - return err - } - run.Repo = repo + if err := run.LoadRepo(ctx); err != nil { + return err } + if err := run.Repo.LoadAttributes(ctx); err != nil { return err } @@ -120,6 +117,19 @@ func (run *ActionRun) LoadAttributes(ctx context.Context) error { return nil } +func (run *ActionRun) LoadRepo(ctx context.Context) error { + if run == nil || run.Repo != nil { + return nil + } + + repo, err := repo_model.GetRepositoryByID(ctx, run.RepoID) + if err != nil { + return err + } + run.Repo = repo + return nil +} + func (run *ActionRun) Duration() time.Duration { return calculateDuration(run.Started, run.Stopped, run.Status) + run.PreviousDuration } diff --git a/models/actions/runner.go b/models/actions/runner.go index 67f003387..9192925d5 100644 --- a/models/actions/runner.go +++ b/models/actions/runner.go @@ -270,7 +270,7 @@ func CountRunnersWithoutBelongingOwner(ctx context.Context) (int64, error) { // Only affect action runners were a owner ID is set, as actions runners // could also be created on a repository. return db.GetEngine(ctx).Table("action_runner"). - Join("LEFT", "user", "`action_runner`.owner_id = `user`.id"). + Join("LEFT", "`user`", "`action_runner`.owner_id = `user`.id"). Where("`action_runner`.owner_id != ?", 0). And(builder.IsNull{"`user`.id"}). Count(new(ActionRunner)) @@ -279,7 +279,7 @@ func CountRunnersWithoutBelongingOwner(ctx context.Context) (int64, error) { func FixRunnersWithoutBelongingOwner(ctx context.Context) (int64, error) { subQuery := builder.Select("`action_runner`.id"). From("`action_runner`"). - Join("LEFT", "user", "`action_runner`.owner_id = `user`.id"). + Join("LEFT", "`user`", "`action_runner`.owner_id = `user`.id"). Where(builder.Neq{"`action_runner`.owner_id": 0}). And(builder.IsNull{"`user`.id"}) b := builder.Delete(builder.In("id", subQuery)).From("`action_runner`") @@ -289,3 +289,25 @@ func FixRunnersWithoutBelongingOwner(ctx context.Context) (int64, error) { } return res.RowsAffected() } + +func CountRunnersWithoutBelongingRepo(ctx context.Context) (int64, error) { + return db.GetEngine(ctx).Table("action_runner"). + Join("LEFT", "`repository`", "`action_runner`.repo_id = `repository`.id"). + Where("`action_runner`.repo_id != ?", 0). + And(builder.IsNull{"`repository`.id"}). + Count(new(ActionRunner)) +} + +func FixRunnersWithoutBelongingRepo(ctx context.Context) (int64, error) { + subQuery := builder.Select("`action_runner`.id"). + From("`action_runner`"). + Join("LEFT", "`repository`", "`action_runner`.repo_id = `repository`.id"). + Where(builder.Neq{"`action_runner`.repo_id": 0}). + And(builder.IsNull{"`repository`.id"}) + b := builder.Delete(builder.In("id", subQuery)).From("`action_runner`") + res, err := db.GetEngine(ctx).Exec(b) + if err != nil { + return 0, err + } + return res.RowsAffected() +} diff --git a/models/actions/variable.go b/models/actions/variable.go index b0a455e67..8aff84465 100644 --- a/models/actions/variable.go +++ b/models/actions/variable.go @@ -92,6 +92,11 @@ func DeleteVariable(ctx context.Context, id int64) error { func GetVariablesOfRun(ctx context.Context, run *ActionRun) (map[string]string, error) { variables := map[string]string{} + if err := run.LoadRepo(ctx); err != nil { + log.Error("LoadRepo: %v", err) + return nil, err + } + // Global globalVariables, err := db.Find[ActionVariable](ctx, FindVariablesOpts{}) if err != nil { diff --git a/models/asymkey/gpg_key_object_verification.go b/models/asymkey/gpg_key_object_verification.go index 67764ffc5..e5c31a74a 100644 --- a/models/asymkey/gpg_key_object_verification.go +++ b/models/asymkey/gpg_key_object_verification.go @@ -94,7 +94,6 @@ func ParseObjectWithSignature(ctx context.Context, c *GitObject) *ObjectVerifica Reason: "gpg.error.no_committer_account", } } - } } diff --git a/models/auth/oauth2.go b/models/auth/oauth2.go index 5eabc7d9b..125d64b36 100644 --- a/models/auth/oauth2.go +++ b/models/auth/oauth2.go @@ -8,6 +8,7 @@ import ( "crypto/sha256" "encoding/base32" "encoding/base64" + "errors" "fmt" "net" "net/url" @@ -301,7 +302,7 @@ func UpdateOAuth2Application(ctx context.Context, opts UpdateOAuth2ApplicationOp return nil, err } if app.UID != opts.UserID { - return nil, fmt.Errorf("UID mismatch") + return nil, errors.New("UID mismatch") } builtinApps := BuiltinApplications() if _, builtin := builtinApps[app.ClientID]; builtin { diff --git a/models/auth/oauth2_test.go b/models/auth/oauth2_test.go index 5361f61db..6602f850c 100644 --- a/models/auth/oauth2_test.go +++ b/models/auth/oauth2_test.go @@ -16,8 +16,6 @@ import ( "github.com/stretchr/testify/assert" ) -//////////////////// Application - func TestOAuth2Application_GenerateClientSecret(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) app := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{ID: 1}) diff --git a/models/db/engine.go b/models/db/engine.go index 1f0cd1a5d..b7146e87f 100755 --- a/models/db/engine.go +++ b/models/db/engine.go @@ -236,7 +236,6 @@ func NamesToBean(names ...string) ([]any, error) { // Need to map provided names to beans... beanMap := make(map[string]any) for _, bean := range tables { - beanMap[strings.ToLower(reflect.Indirect(reflect.ValueOf(bean)).Type().Name())] = bean beanMap[strings.ToLower(x.TableName(bean))] = bean beanMap[strings.ToLower(x.TableName(bean, true))] = bean diff --git a/models/git/lfs_lock.go b/models/git/lfs_lock.go index 261c73032..2f65833fe 100644 --- a/models/git/lfs_lock.go +++ b/models/git/lfs_lock.go @@ -5,7 +5,7 @@ package git import ( "context" - "fmt" + "errors" "strings" "time" @@ -148,7 +148,7 @@ func DeleteLFSLockByID(ctx context.Context, id int64, repo *repo_model.Repositor } if !force && u.ID != lock.OwnerID { - return nil, fmt.Errorf("user doesn't own lock and force flag is not set") + return nil, errors.New("user doesn't own lock and force flag is not set") } if _, err := db.GetEngine(dbCtx).ID(id).Delete(new(LFSLock)); err != nil { diff --git a/models/issues/review.go b/models/issues/review.go index 92764db4d..3c6934b06 100644 --- a/models/issues/review.go +++ b/models/issues/review.go @@ -345,11 +345,9 @@ func CreateReview(ctx context.Context, opts CreateReviewOptions) (*Review, error return nil, err } } - } else if opts.ReviewerTeam != nil { review.Type = ReviewTypeRequest review.ReviewerTeamID = opts.ReviewerTeam.ID - } else { return nil, fmt.Errorf("provide either reviewer or reviewer team") } diff --git a/models/migrations/base/db.go b/models/migrations/base/db.go index a76ce29de..e58479338 100644 --- a/models/migrations/base/db.go +++ b/models/migrations/base/db.go @@ -214,7 +214,6 @@ func RecreateTable(sess *xorm.Session, bean any) error { return err } sequenceMap[sequence] = sequenceData - } // CASCADE causes postgres to drop all the constraints on the old table @@ -279,7 +278,6 @@ func RecreateTable(sess *xorm.Session, bean any) error { return err } } - } default: diff --git a/models/migrations/fixtures/Test_AddConfidentialClientColumnToOAuth2ApplicationTable/o_auth2_application.yml b/models/migrations/fixtures/Test_AddConfidentialClientColumnToOAuth2ApplicationTable/oauth2_application.yml similarity index 100% rename from models/migrations/fixtures/Test_AddConfidentialClientColumnToOAuth2ApplicationTable/o_auth2_application.yml rename to models/migrations/fixtures/Test_AddConfidentialClientColumnToOAuth2ApplicationTable/oauth2_application.yml diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 7146c4967..5d80f9fd1 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -22,7 +22,6 @@ import ( "code.gitea.io/gitea/models/migrations/v1_20" "code.gitea.io/gitea/models/migrations/v1_21" "code.gitea.io/gitea/models/migrations/v1_22" - "code.gitea.io/gitea/models/migrations/v1_23" "code.gitea.io/gitea/models/migrations/v1_6" "code.gitea.io/gitea/models/migrations/v1_7" "code.gitea.io/gitea/models/migrations/v1_8" @@ -576,14 +575,20 @@ var migrations = []Migration{ // v293 -> v294 NewMigration("Ensure every project has exactly one default column", v1_22.CheckProjectColumnsConsistency), - // Gitea 1.22.0 ends at 294 + // Gitea 1.22.0-rc0 ends at 294 // v294 -> v295 - NewMigration("Add unique index for project issue table", v1_23.AddUniqueIndexForProjectIssue), + NewMigration("Add unique index for project issue table", v1_22.AddUniqueIndexForProjectIssue), // v295 -> v296 - NewMigration("Add commit status summary table", v1_23.AddCommitStatusSummary), + NewMigration("Add commit status summary table", v1_22.AddCommitStatusSummary), // v296 -> v297 - NewMigration("Add missing field of commit status summary table", v1_23.AddCommitStatusSummary2), + NewMigration("Add missing field of commit status summary table", v1_22.AddCommitStatusSummary2), + // v297 -> v298 + NewMigration("Add everyone_access_mode for repo_unit", noopMigration), + // v298 -> v299 + NewMigration("Drop wrongly created table o_auth2_application", v1_22.DropWronglyCreatedTable), + + // Gitea 1.22.0-rc1 ends at 299 } // GetCurrentDBVersion returns the current db version diff --git a/models/migrations/v1_11/v111.go b/models/migrations/v1_11/v111.go index c18d7adae..cc3dc0d54 100644 --- a/models/migrations/v1_11/v111.go +++ b/models/migrations/v1_11/v111.go @@ -263,7 +263,6 @@ func AddBranchProtectionCanPushAndEnableWhitelist(x *xorm.Engine) error { for _, u := range units { var found bool for _, team := range teams { - var teamU []*TeamUnit var unitEnabled bool err = sess.Where("team_id = ?", team.ID).Find(&teamU) @@ -332,7 +331,6 @@ func AddBranchProtectionCanPushAndEnableWhitelist(x *xorm.Engine) error { } if !protectedBranch.EnableApprovalsWhitelist { - perm, err := getUserRepoPermission(sess, baseRepo, reviewer) if err != nil { return false, err diff --git a/models/migrations/v1_16/v210.go b/models/migrations/v1_16/v210.go index f9d1eb5c8..db45b11ae 100644 --- a/models/migrations/v1_16/v210.go +++ b/models/migrations/v1_16/v210.go @@ -68,11 +68,6 @@ func RemigrateU2FCredentials(x *xorm.Engine) error { if err != nil { return err } - case schemas.ORACLE: - _, err := x.Exec("ALTER TABLE webauthn_credential MODIFY credential_id VARCHAR(410)") - if err != nil { - return err - } case schemas.POSTGRES: _, err := x.Exec("ALTER TABLE webauthn_credential ALTER COLUMN credential_id TYPE VARCHAR(410)") if err != nil { diff --git a/models/migrations/v1_18/v230.go b/models/migrations/v1_18/v230.go index cf94926be..ea5b4d02e 100644 --- a/models/migrations/v1_18/v230.go +++ b/models/migrations/v1_18/v230.go @@ -9,9 +9,9 @@ import ( // AddConfidentialColumnToOAuth2ApplicationTable: add ConfidentialClient column, setting existing rows to true func AddConfidentialClientColumnToOAuth2ApplicationTable(x *xorm.Engine) error { - type OAuth2Application struct { + type oauth2Application struct { + ID int64 ConfidentialClient bool `xorm:"NOT NULL DEFAULT TRUE"` } - - return x.Sync(new(OAuth2Application)) + return x.Sync(new(oauth2Application)) } diff --git a/models/migrations/v1_18/v230_test.go b/models/migrations/v1_18/v230_test.go index 308f3a502..40db4c2ff 100644 --- a/models/migrations/v1_18/v230_test.go +++ b/models/migrations/v1_18/v230_test.go @@ -13,12 +13,12 @@ import ( func Test_AddConfidentialClientColumnToOAuth2ApplicationTable(t *testing.T) { // premigration - type OAuth2Application struct { + type oauth2Application struct { ID int64 } // Prepare and load the testing database - x, deferable := base.PrepareTestEnv(t, 0, new(OAuth2Application)) + x, deferable := base.PrepareTestEnv(t, 0, new(oauth2Application)) defer deferable() if x == nil || t.Failed() { return @@ -36,7 +36,7 @@ func Test_AddConfidentialClientColumnToOAuth2ApplicationTable(t *testing.T) { } got := []ExpectedOAuth2Application{} - if err := x.Table("o_auth2_application").Select("id, confidential_client").Find(&got); !assert.NoError(t, err) { + if err := x.Table("oauth2_application").Select("id, confidential_client").Find(&got); !assert.NoError(t, err) { return } diff --git a/models/migrations/v1_20/v250.go b/models/migrations/v1_20/v250.go index a09957b29..86388ef0b 100644 --- a/models/migrations/v1_20/v250.go +++ b/models/migrations/v1_20/v250.go @@ -104,7 +104,7 @@ func ChangeContainerMetadataMultiArch(x *xorm.Engine) error { // Convert to new metadata format - new := &MetadataNew{ + newMetadata := &MetadataNew{ Type: old.Type, IsTagged: old.IsTagged, Platform: old.Platform, @@ -119,7 +119,7 @@ func ChangeContainerMetadataMultiArch(x *xorm.Engine) error { Manifests: manifests, } - metadataJSON, err := json.Marshal(new) + metadataJSON, err := json.Marshal(newMetadata) if err != nil { return err } diff --git a/models/migrations/v1_23/v294.go b/models/migrations/v1_22/v294.go similarity index 98% rename from models/migrations/v1_23/v294.go rename to models/migrations/v1_22/v294.go index e3e18f68f..314b4519f 100644 --- a/models/migrations/v1_23/v294.go +++ b/models/migrations/v1_22/v294.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_23 //nolint +package v1_22 //nolint import ( "xorm.io/xorm" diff --git a/models/migrations/v1_23/v294_test.go b/models/migrations/v1_22/v294_test.go similarity index 98% rename from models/migrations/v1_23/v294_test.go rename to models/migrations/v1_22/v294_test.go index d9a44ad86..82a3bcd60 100644 --- a/models/migrations/v1_23/v294_test.go +++ b/models/migrations/v1_22/v294_test.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_23 //nolint +package v1_22 //nolint import ( "slices" diff --git a/models/migrations/v1_23/v295.go b/models/migrations/v1_22/v295.go similarity index 96% rename from models/migrations/v1_23/v295.go rename to models/migrations/v1_22/v295.go index 9a2003cfc..17bdadb4a 100644 --- a/models/migrations/v1_23/v295.go +++ b/models/migrations/v1_22/v295.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_23 //nolint +package v1_22 //nolint import "xorm.io/xorm" diff --git a/models/migrations/v1_23/v296.go b/models/migrations/v1_22/v296.go similarity index 95% rename from models/migrations/v1_23/v296.go rename to models/migrations/v1_22/v296.go index 495ae2ab2..1ecacab95 100644 --- a/models/migrations/v1_23/v296.go +++ b/models/migrations/v1_22/v296.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_23 //nolint +package v1_22 //nolint import "xorm.io/xorm" diff --git a/models/migrations/v1_22/v298.go b/models/migrations/v1_22/v298.go new file mode 100644 index 000000000..b9f3b95ad --- /dev/null +++ b/models/migrations/v1_22/v298.go @@ -0,0 +1,10 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_22 //nolint + +import "xorm.io/xorm" + +func DropWronglyCreatedTable(x *xorm.Engine) error { + return x.DropTables("o_auth2_application") +} diff --git a/models/migrations/v1_6/v71.go b/models/migrations/v1_6/v71.go index 4e50ca921..586187228 100644 --- a/models/migrations/v1_6/v71.go +++ b/models/migrations/v1_6/v71.go @@ -61,7 +61,6 @@ func AddScratchHash(x *xorm.Engine) error { if _, err := sess.ID(tfa.ID).Cols("scratch_salt, scratch_hash").Update(tfa); err != nil { return fmt.Errorf("couldn't add in scratch_hash and scratch_salt: %w", err) } - } } diff --git a/models/migrations/v1_9/v85.go b/models/migrations/v1_9/v85.go index 9419ee1aa..a23d7c5d6 100644 --- a/models/migrations/v1_9/v85.go +++ b/models/migrations/v1_9/v85.go @@ -81,7 +81,6 @@ func HashAppToken(x *xorm.Engine) error { if _, err := sess.ID(token.ID).Cols("token_hash, token_salt, token_last_eight, sha1").Update(token); err != nil { return fmt.Errorf("couldn't add in sha1, token_hash, token_salt and token_last_eight: %w", err) } - } } diff --git a/models/organization/team.go b/models/organization/team.go index 17db11c42..1b737c2d3 100644 --- a/models/organization/team.go +++ b/models/organization/team.go @@ -222,9 +222,8 @@ func GetTeamIDsByNames(ctx context.Context, orgID int64, names []string, ignoreN if err != nil { if ignoreNonExistent { continue - } else { - return nil, err } + return nil, err } ids = append(ids, u.ID) } diff --git a/models/project/board.go b/models/project/board.go index 5f142a356..7faabc52c 100644 --- a/models/project/board.go +++ b/models/project/board.go @@ -110,13 +110,11 @@ func createBoardsForProjectsType(ctx context.Context, project *Project) error { var items []string switch project.BoardType { - case BoardTypeBugTriage: items = setting.Project.ProjectBoardBugTriageType case BoardTypeBasicKanban: items = setting.Project.ProjectBoardBasicKanbanType - case BoardTypeNone: fallthrough default: diff --git a/models/repo/user_repo.go b/models/repo/user_repo.go index 219f4ff25..d3fbd961b 100644 --- a/models/repo/user_repo.go +++ b/models/repo/user_repo.go @@ -135,7 +135,6 @@ func GetReviewers(ctx context.Context, repo *Repository, doerID, posterID int64) // the owner of a private repo needs to be explicitly added. cond = cond.Or(builder.Eq{"`user`.id": repo.Owner.ID}) } - } else { // This is a "public" repository: // Any user that has read access, is a watcher or organization member can be requested to review diff --git a/models/repo_transfer.go b/models/repo_transfer.go index f20c5bcdc..0c23d759f 100644 --- a/models/repo_transfer.go +++ b/models/repo_transfer.go @@ -5,6 +5,7 @@ package models import ( "context" + "errors" "fmt" "code.gitea.io/gitea/models/db" @@ -120,7 +121,7 @@ func DeleteRepositoryTransfer(ctx context.Context, repoID int64) error { func TestRepositoryReadyForTransfer(status repo_model.RepositoryStatus) error { switch status { case repo_model.RepositoryBeingMigrated: - return fmt.Errorf("repo is not ready, currently migrating") + return errors.New("repo is not ready, currently migrating") case repo_model.RepositoryPendingTransfer: return ErrRepoTransferInProgress{} } diff --git a/models/user/user.go b/models/user/user.go index df129a5ef..10c4915f5 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -1007,9 +1007,8 @@ func GetUserIDsByNames(ctx context.Context, names []string, ignoreNonExistent bo if err != nil { if ignoreNonExistent { continue - } else { - return nil, err } + return nil, err } ids = append(ids, u.ID) } diff --git a/models/user/user_test.go b/models/user/user_test.go index 571b8b2a9..4bf8c7136 100644 --- a/models/user/user_test.go +++ b/models/user/user_test.go @@ -5,8 +5,8 @@ package user_test import ( "context" + "crypto/rand" "fmt" - "math/rand" "strings" "testing" "time" diff --git a/modules/auth/password/password.go b/modules/auth/password/password.go index 27074358a..85f978070 100644 --- a/modules/auth/password/password.go +++ b/modules/auth/password/password.go @@ -63,16 +63,16 @@ func NewComplexity() { func setupComplexity(values []string) { if len(values) != 1 || values[0] != "off" { for _, val := range values { - if complex, ok := charComplexities[val]; ok { - validChars += complex.ValidChars - requiredList = append(requiredList, complex) + if complexity, ok := charComplexities[val]; ok { + validChars += complexity.ValidChars + requiredList = append(requiredList, complexity) } } if len(requiredList) == 0 { // No valid character classes found; use all classes as default - for _, complex := range charComplexities { - validChars += complex.ValidChars - requiredList = append(requiredList, complex) + for _, complexity := range charComplexities { + validChars += complexity.ValidChars + requiredList = append(requiredList, complexity) } } } diff --git a/modules/auth/password/pwn/pwn_test.go b/modules/auth/password/pwn/pwn_test.go index f9deadc8d..a2a6b3a17 100644 --- a/modules/auth/password/pwn/pwn_test.go +++ b/modules/auth/password/pwn/pwn_test.go @@ -4,9 +4,8 @@ package pwn import ( - "math/rand" + "math/rand/v2" "net/http" - "os" "strings" "testing" "time" @@ -18,11 +17,6 @@ var client = New(WithHTTP(&http.Client{ Timeout: time.Second * 2, })) -func TestMain(m *testing.M) { - rand.Seed(time.Now().Unix()) - os.Exit(m.Run()) -} - func TestPassword(t *testing.T) { // Check input error _, err := client.CheckPassword("", false) @@ -81,24 +75,24 @@ func testPassword() string { // Set special character for i := 0; i < 5; i++ { - random := rand.Intn(len(specialCharSet)) + random := rand.IntN(len(specialCharSet)) password.WriteString(string(specialCharSet[random])) } // Set numeric for i := 0; i < 5; i++ { - random := rand.Intn(len(numberSet)) + random := rand.IntN(len(numberSet)) password.WriteString(string(numberSet[random])) } // Set uppercase for i := 0; i < 5; i++ { - random := rand.Intn(len(upperCharSet)) + random := rand.IntN(len(upperCharSet)) password.WriteString(string(upperCharSet[random])) } for i := 0; i < 5; i++ { - random := rand.Intn(len(allCharSet)) + random := rand.IntN(len(allCharSet)) password.WriteString(string(allCharSet[random])) } inRune := []rune(password.String()) diff --git a/modules/git/batch_reader.go b/modules/git/batch_reader.go index 043dbb44b..c988d6ab8 100644 --- a/modules/git/batch_reader.go +++ b/modules/git/batch_reader.go @@ -307,10 +307,10 @@ func ParseTreeLine(objectFormat ObjectFormat, rd *bufio.Reader, modeBuf, fnameBu // Deal with the binary hash idx = 0 - len := objectFormat.FullLength() / 2 - for idx < len { + length := objectFormat.FullLength() / 2 + for idx < length { var read int - read, err = rd.Read(shaBuf[idx:len]) + read, err = rd.Read(shaBuf[idx:length]) n += read if err != nil { return mode, fname, sha, n, err diff --git a/modules/git/commit_reader.go b/modules/git/commit_reader.go index 49159cc41..8e2523d7f 100644 --- a/modules/git/commit_reader.go +++ b/modules/git/commit_reader.go @@ -49,9 +49,8 @@ readLoop: if len(line) > 0 && line[0] == ' ' { _, _ = signatureSB.Write(line[1:]) continue - } else { - pgpsig = false } + pgpsig = false } if !message { diff --git a/modules/git/pipeline/lfs_nogogit.go b/modules/git/pipeline/lfs_nogogit.go index a3ee88396..349cfbd9c 100644 --- a/modules/git/pipeline/lfs_nogogit.go +++ b/modules/git/pipeline/lfs_nogogit.go @@ -213,7 +213,6 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err errChan <- err break } - } }() diff --git a/modules/git/repo_commit.go b/modules/git/repo_commit.go index 44273d225..f9168bef7 100644 --- a/modules/git/repo_commit.go +++ b/modules/git/repo_commit.go @@ -251,18 +251,18 @@ func (repo *Repository) CommitsByFileAndRange(opts CommitsByFileAndRangeOptions) return nil, err } - len := objectFormat.FullLength() + length := objectFormat.FullLength() commits := []*Commit{} - shaline := make([]byte, len+1) + shaline := make([]byte, length+1) for { n, err := io.ReadFull(stdoutReader, shaline) - if err != nil || n < len { + if err != nil || n < length { if err == io.EOF { err = nil } return commits, err } - objectID, err := NewIDFromString(string(shaline[0:len])) + objectID, err := NewIDFromString(string(shaline[0:length])) if err != nil { return nil, err } diff --git a/modules/git/submodule.go b/modules/git/submodule.go index 37813ea4c..b99c81582 100644 --- a/modules/git/submodule.go +++ b/modules/git/submodule.go @@ -64,7 +64,6 @@ func getRefURL(refURL, urlPrefix, repoFullName, sshDomain string) string { // ex: git@try.gitea.io:go-gitea/gitea match := scpSyntax.FindAllStringSubmatch(refURI, -1) if len(match) > 0 { - m := match[0] refHostname := m[2] pth := m[3] diff --git a/modules/indexer/code/bleve/bleve.go b/modules/indexer/code/bleve/bleve.go index ff0e37ca2..66724a344 100644 --- a/modules/indexer/code/bleve/bleve.go +++ b/modules/indexer/code/bleve/bleve.go @@ -193,7 +193,6 @@ func (b *Indexer) addDelete(filename string, repo *repo_model.Repository, batch func (b *Indexer) Index(ctx context.Context, repo *repo_model.Repository, sha string, changes *internal.RepoChanges) error { batch := inner_bleve.NewFlushingBatch(b.inner.Indexer, maxBatchSize) if len(changes.Updates) > 0 { - // Now because of some insanity with git cat-file not immediately failing if not run in a valid git directory we need to run git rev-parse first! if err := git.EnsureValidGitRepository(ctx, repo.RepoPath()); err != nil { log.Error("Unable to open git repo: %s for %-v: %v", repo.RepoPath(), repo, err) @@ -337,7 +336,6 @@ func (b *Indexer) Search(ctx context.Context, opts *internal.SearchOptions) (int if result, err = b.inner.Indexer.Search(facetRequest); err != nil { return 0, nil, nil, err } - } languagesFacet := result.Facets["languages"] for _, term := range languagesFacet.Terms.Terms() { diff --git a/modules/indexer/issues/dboptions.go b/modules/indexer/issues/dboptions.go index 4a98b4588..8f9408874 100644 --- a/modules/indexer/issues/dboptions.go +++ b/modules/indexer/issues/dboptions.go @@ -68,7 +68,7 @@ func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOp searchOpt.Paginator = opts.Paginator switch opts.SortType { - case "": + case "", "latest": searchOpt.SortBy = SortByCreatedDesc case "oldest": searchOpt.SortBy = SortByCreatedAsc @@ -86,7 +86,7 @@ func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOp searchOpt.SortBy = SortByDeadlineDesc case "priority", "priorityrepo", "project-column-sorting": // Unsupported sort type for search - searchOpt.SortBy = SortByUpdatedDesc + fallthrough default: searchOpt.SortBy = SortByUpdatedDesc } diff --git a/modules/indexer/issues/elasticsearch/elasticsearch.go b/modules/indexer/issues/elasticsearch/elasticsearch.go index 53b383c8d..c7cb59f2c 100644 --- a/modules/indexer/issues/elasticsearch/elasticsearch.go +++ b/modules/indexer/issues/elasticsearch/elasticsearch.go @@ -145,7 +145,6 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( query := elastic.NewBoolQuery() if options.Keyword != "" { - searchType := esMultiMatchTypePhrasePrefix if options.IsFuzzyKeyword { searchType = esMultiMatchTypeBestFields diff --git a/modules/log/event_format.go b/modules/log/event_format.go index 524ca3dd8..d9dbebf83 100644 --- a/modules/log/event_format.go +++ b/modules/log/event_format.go @@ -125,7 +125,6 @@ func EventFormatTextMessage(mode *WriterMode, event *Event, msgFormat string, ms if mode.Colorize { buf = append(buf, resetBytes...) } - } if flags&(Lshortfile|Llongfile) != 0 { if mode.Colorize { diff --git a/modules/markup/markdown/markdown_test.go b/modules/markup/markdown/markdown_test.go index 3e1d75b29..278494d95 100644 --- a/modules/markup/markdown/markdown_test.go +++ b/modules/markup/markdown/markdown_test.go @@ -460,7 +460,6 @@ func TestColorPreview(t *testing.T) { res, err := markdown.RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, test.testcase) assert.NoError(t, err, "Unexpected error in testcase: %q", test.testcase) assert.Equal(t, template.HTML(test.expected), res, "Unexpected result in testcase %q", test.testcase) - } negativeTests := []string{ @@ -555,7 +554,6 @@ func TestMathBlock(t *testing.T) { res, err := markdown.RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, test.testcase) assert.NoError(t, err, "Unexpected error in testcase: %q", test.testcase) assert.Equal(t, template.HTML(test.expected), res, "Unexpected result in testcase %q", test.testcase) - } } diff --git a/modules/markup/orgmode/orgmode.go b/modules/markup/orgmode/orgmode.go index 8efe4e395..391ee6c12 100644 --- a/modules/markup/orgmode/orgmode.go +++ b/modules/markup/orgmode/orgmode.go @@ -147,7 +147,6 @@ func (r *Writer) resolveLink(node org.Node) string { } if len(link) > 0 && !markup.IsLinkStr(link) && link[0] != '#' && !strings.HasPrefix(link, mailto) { - var base string if r.Ctx.IsWiki { base = r.Ctx.Links.WikiLink() diff --git a/modules/packages/rubygems/marshal.go b/modules/packages/rubygems/marshal.go index 8878dcf97..4e6a5fc5f 100644 --- a/modules/packages/rubygems/marshal.go +++ b/modules/packages/rubygems/marshal.go @@ -147,35 +147,35 @@ func (e *MarshalEncoder) marshalIntInternal(i int64) error { return e.w.WriteByte(byte(i - 5)) } - var len int + var length int if 122 < i && i <= 0xff { - len = 1 + length = 1 } else if 0xff < i && i <= 0xffff { - len = 2 + length = 2 } else if 0xffff < i && i <= 0xffffff { - len = 3 + length = 3 } else if 0xffffff < i && i <= 0x3fffffff { - len = 4 + length = 4 } else if -0x100 <= i && i < -123 { - len = -1 + length = -1 } else if -0x10000 <= i && i < -0x100 { - len = -2 + length = -2 } else if -0x1000000 <= i && i < -0x100000 { - len = -3 + length = -3 } else if -0x40000000 <= i && i < -0x1000000 { - len = -4 + length = -4 } else { return ErrInvalidIntRange } - if err := e.w.WriteByte(byte(len)); err != nil { + if err := e.w.WriteByte(byte(length)); err != nil { return err } - if len < 0 { - len = -len + if length < 0 { + length = -length } - for c := 0; c < len; c++ { + for c := 0; c < length; c++ { if err := e.w.WriteByte(byte(i >> uint(8*c) & 0xff)); err != nil { return err } @@ -244,13 +244,13 @@ func (e *MarshalEncoder) marshalArray(arr reflect.Value) error { return err } - len := arr.Len() + length := arr.Len() - if err := e.marshalIntInternal(int64(len)); err != nil { + if err := e.marshalIntInternal(int64(length)); err != nil { return err } - for i := 0; i < len; i++ { + for i := 0; i < length; i++ { if err := e.marshal(arr.Index(i).Interface()); err != nil { return err } diff --git a/modules/process/manager_stacktraces.go b/modules/process/manager_stacktraces.go index 49bd5071f..e26089311 100644 --- a/modules/process/manager_stacktraces.go +++ b/modules/process/manager_stacktraces.go @@ -339,7 +339,6 @@ func (pm *Manager) ProcessStacktraces(flat, noSystem bool) ([]*Process, int, int } sort.Slice(processes, after(processes)) if !flat { - var sortChildren func(process *Process) sortChildren = func(process *Process) { diff --git a/modules/repository/temp.go b/modules/repository/temp.go index 53646718e..04faa9db3 100644 --- a/modules/repository/temp.go +++ b/modules/repository/temp.go @@ -32,7 +32,6 @@ func CreateTemporaryPath(prefix string) (string, error) { if err != nil { log.Error("Unable to create temporary directory: %s-*.git (%v)", prefix, err) return "", fmt.Errorf("Failed to create dir %s-*.git: %w", prefix, err) - } return basePath, nil } diff --git a/modules/setting/time.go b/modules/setting/time.go index 6d2aa80f5..39acba12e 100644 --- a/modules/setting/time.go +++ b/modules/setting/time.go @@ -19,9 +19,8 @@ func loadTimeFrom(rootCfg ConfigProvider) { DefaultUILocation, err = time.LoadLocation(zone) if err != nil { log.Fatal("Load time zone failed: %v", err) - } else { - log.Info("Default UI Location is %v", zone) } + log.Info("Default UI Location is %v", zone) } if DefaultUILocation == nil { DefaultUILocation = time.Local diff --git a/modules/templates/htmlrenderer.go b/modules/templates/htmlrenderer.go index 8661653ad..55a55dd7f 100644 --- a/modules/templates/htmlrenderer.go +++ b/modules/templates/htmlrenderer.go @@ -138,10 +138,9 @@ func wrapTmplErrMsg(msg string) { if setting.IsProd { // in prod mode, Forgejo must have correct templates to run log.Fatal("Forgejo can't run with template errors: %s", msg) - } else { - // in dev mode, do not need to really exit, because the template errors could be fixed by developer soon and the templates get reloaded - log.Error("There are template errors but Forgejo continues to run in dev mode: %s", msg) } + // in dev mode, do not need to really exit, because the template errors could be fixed by developer soon and the templates get reloaded + log.Error("There are template errors but Forgejo continues to run in dev mode: %s", msg) } type templateErrorPrettier struct { diff --git a/modules/templates/mailer.go b/modules/templates/mailer.go index f1832cba0..7c97e1ea8 100644 --- a/modules/templates/mailer.go +++ b/modules/templates/mailer.go @@ -84,9 +84,8 @@ func Mailer(ctx context.Context) (*texttmpl.Template, *template.Template) { if err = buildSubjectBodyTemplate(subjectTemplates, bodyTemplates, tmplName, content); err != nil { if firstRun { log.Fatal("Failed to parse mail template, err: %v", err) - } else { - log.Error("Failed to parse mail template, err: %v", err) } + log.Error("Failed to parse mail template, err: %v", err) } } } diff --git a/modules/util/util_test.go b/modules/util/util_test.go index 5c5b13d04..de8f065ca 100644 --- a/modules/util/util_test.go +++ b/modules/util/util_test.go @@ -121,9 +121,9 @@ func Test_NormalizeEOL(t *testing.T) { } func Test_RandomInt(t *testing.T) { - int, err := CryptoRandomInt(255) - assert.True(t, int >= 0) - assert.True(t, int <= 255) + randInt, err := CryptoRandomInt(255) + assert.True(t, randInt >= 0) + assert.True(t, randInt <= 255) assert.NoError(t, err) } diff --git a/options/license/HPND-UC-export-US b/options/license/HPND-UC-export-US new file mode 100644 index 000000000..015556c5f --- /dev/null +++ b/options/license/HPND-UC-export-US @@ -0,0 +1,10 @@ +Copyright (C) 1985, 1990 Regents of the University of California. + +Permission to use, copy, modify, and distribute this +software and its documentation for any purpose and without +fee is hereby granted, provided that the above copyright +notice appear in all copies. The University of California +makes no representations about the suitability of this +software for any purpose. It is provided "as is" without +express or implied warranty. Export of this software outside +of the United States of America may require an export license. diff --git a/options/license/NCL b/options/license/NCL new file mode 100644 index 000000000..3bfb658c2 --- /dev/null +++ b/options/license/NCL @@ -0,0 +1,32 @@ +Copyright (c) 2004 the University Corporation for Atmospheric +Research ("UCAR"). All rights reserved. Developed by NCAR's +Computational and Information Systems Laboratory, UCAR, +www.cisl.ucar.edu. + +Redistribution and use of the Software in source and binary forms, +with or without modification, is permitted provided that the +following conditions are met: + +- Neither the names of NCAR's Computational and Information Systems +Laboratory, the University Corporation for Atmospheric Research, +nor the names of its sponsors or contributors may be used to +endorse or promote products derived from this Software without +specific prior written permission. + +- Redistributions of source code must retain the above copyright +notices, this list of conditions, and the disclaimer below. + +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions, and the disclaimer below in the +documentation and/or other materials provided with the +distribution. + +THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE CONTRIBUTORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE +SOFTWARE. diff --git a/package-lock.json b/package-lock.json index be6eadaee..3f50d2358 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,7 @@ "esbuild-loader": "4.1.0", "escape-goat": "4.0.0", "fast-glob": "3.3.2", - "htmx.org": "1.9.11", + "htmx.org": "1.9.12", "idiomorph": "0.3.0", "jquery": "3.7.1", "katex": "0.16.10", @@ -6924,9 +6924,9 @@ } }, "node_modules/htmx.org": { - "version": "1.9.11", - "resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-1.9.11.tgz", - "integrity": "sha512-WlVuICn8dfNOOgYmdYzYG8zSnP3++AdHkMHooQAzGZObWpVXYathpz/I37ycF4zikR6YduzfCvEcxk20JkIUsw==" + "version": "1.9.12", + "resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-1.9.12.tgz", + "integrity": "sha512-VZAohXyF7xPGS52IM8d1T1283y+X4D+Owf3qY1NZ9RuBypyu9l8cGsxUMAG5fEAb/DhT7rDoJ9Hpu5/HxFD3cw==" }, "node_modules/human-signals": { "version": "5.0.0", diff --git a/package.json b/package.json index 5a95503a5..ce6e673f1 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "esbuild-loader": "4.1.0", "escape-goat": "4.0.0", "fast-glob": "3.3.2", - "htmx.org": "1.9.11", + "htmx.org": "1.9.12", "idiomorph": "0.3.0", "jquery": "3.7.1", "katex": "0.16.10", diff --git a/routers/api/actions/artifacts.go b/routers/api/actions/artifacts.go index 2f8b3d021..416d7e518 100644 --- a/routers/api/actions/artifacts.go +++ b/routers/api/actions/artifacts.go @@ -144,7 +144,6 @@ func ArtifactContexter() func(next http.Handler) http.Handler { var task *actions.ActionTask if err == nil { - task, err = actions.GetTaskByID(req.Context(), tID) if err != nil { log.Error("Error runner api getting task by ID: %v", err) @@ -467,14 +466,15 @@ func (ar artifactRoutes) downloadArtifact(ctx *ArtifactContext) { log.Error("Error getting artifact: %v", err) ctx.Error(http.StatusInternalServerError, err.Error()) return - } else if !exist { + } + if !exist { log.Error("artifact with ID %d does not exist", artifactID) ctx.Error(http.StatusNotFound, fmt.Sprintf("artifact with ID %d does not exist", artifactID)) return } if artifact.RunID != runID { - log.Error("Error dismatch runID and artifactID, task: %v, artifact: %v", runID, artifactID) - ctx.Error(http.StatusBadRequest, err.Error()) + log.Error("Error mismatch runID and artifactID, task: %v, artifact: %v", runID, artifactID) + ctx.Error(http.StatusBadRequest) return } diff --git a/routers/api/packages/alpine/alpine.go b/routers/api/packages/alpine/alpine.go index cf0fe6c07..481cf70d3 100644 --- a/routers/api/packages/alpine/alpine.go +++ b/routers/api/packages/alpine/alpine.go @@ -144,12 +144,12 @@ func UploadPackageFile(ctx *context.Context) { return } - upload, close, err := ctx.UploadStream() + upload, needToClose, err := ctx.UploadStream() if err != nil { apiError(ctx, http.StatusInternalServerError, err) return } - if close { + if needToClose { defer upload.Close() } diff --git a/routers/api/packages/conan/conan.go b/routers/api/packages/conan/conan.go index c45e085a4..07ea3eda3 100644 --- a/routers/api/packages/conan/conan.go +++ b/routers/api/packages/conan/conan.go @@ -310,12 +310,12 @@ func uploadFile(ctx *context.Context, fileFilter container.Set[string], fileKey return } - upload, close, err := ctx.UploadStream() + upload, needToClose, err := ctx.UploadStream() if err != nil { apiError(ctx, http.StatusBadRequest, err) return } - if close { + if needToClose { defer upload.Close() } diff --git a/routers/api/packages/conda/conda.go b/routers/api/packages/conda/conda.go index 30c80fc15..c7e4544d5 100644 --- a/routers/api/packages/conda/conda.go +++ b/routers/api/packages/conda/conda.go @@ -174,12 +174,12 @@ func EnumeratePackages(ctx *context.Context) { } func UploadPackageFile(ctx *context.Context) { - upload, close, err := ctx.UploadStream() + upload, needToClose, err := ctx.UploadStream() if err != nil { apiError(ctx, http.StatusInternalServerError, err) return } - if close { + if needToClose { defer upload.Close() } diff --git a/routers/api/packages/container/container.go b/routers/api/packages/container/container.go index e51976614..2cb16daeb 100644 --- a/routers/api/packages/container/container.go +++ b/routers/api/packages/container/container.go @@ -385,9 +385,9 @@ func EndUploadBlob(ctx *context.Context) { } return } - close := true + doClose := true defer func() { - if close { + if doClose { uploader.Close() } }() @@ -427,7 +427,7 @@ func EndUploadBlob(ctx *context.Context) { apiError(ctx, http.StatusInternalServerError, err) return } - close = false + doClose = false if err := container_service.RemoveBlobUploadByID(ctx, uploader.ID); err != nil { apiError(ctx, http.StatusInternalServerError, err) diff --git a/routers/api/packages/cran/cran.go b/routers/api/packages/cran/cran.go index 2cec75294..f1d616724 100644 --- a/routers/api/packages/cran/cran.go +++ b/routers/api/packages/cran/cran.go @@ -151,12 +151,12 @@ func UploadBinaryPackageFile(ctx *context.Context) { } func uploadPackageFile(ctx *context.Context, compositeKey string, properties map[string]string) { - upload, close, err := ctx.UploadStream() + upload, needToClose, err := ctx.UploadStream() if err != nil { apiError(ctx, http.StatusBadRequest, err) return } - if close { + if needToClose { defer upload.Close() } diff --git a/routers/api/packages/debian/debian.go b/routers/api/packages/debian/debian.go index 241de3ac5..8c05476cb 100644 --- a/routers/api/packages/debian/debian.go +++ b/routers/api/packages/debian/debian.go @@ -127,12 +127,12 @@ func UploadPackageFile(ctx *context.Context) { return } - upload, close, err := ctx.UploadStream() + upload, needToClose, err := ctx.UploadStream() if err != nil { apiError(ctx, http.StatusInternalServerError, err) return } - if close { + if needToClose { defer upload.Close() } diff --git a/routers/api/packages/generic/generic.go b/routers/api/packages/generic/generic.go index 823293113..e66f3ee67 100644 --- a/routers/api/packages/generic/generic.go +++ b/routers/api/packages/generic/generic.go @@ -90,12 +90,12 @@ func UploadPackage(ctx *context.Context) { return } - upload, close, err := ctx.UploadStream() + upload, needToClose, err := ctx.UploadStream() if err != nil { apiError(ctx, http.StatusInternalServerError, err) return } - if close { + if needToClose { defer upload.Close() } diff --git a/routers/api/packages/goproxy/goproxy.go b/routers/api/packages/goproxy/goproxy.go index d658066bb..56a07dbd4 100644 --- a/routers/api/packages/goproxy/goproxy.go +++ b/routers/api/packages/goproxy/goproxy.go @@ -154,12 +154,12 @@ func resolvePackage(ctx *context.Context, ownerID int64, name, version string) ( } func UploadPackage(ctx *context.Context) { - upload, close, err := ctx.UploadStream() + upload, needToClose, err := ctx.UploadStream() if err != nil { apiError(ctx, http.StatusInternalServerError, err) return } - if close { + if needToClose { defer upload.Close() } diff --git a/routers/api/packages/nuget/nuget.go b/routers/api/packages/nuget/nuget.go index 09156ece6..26b0ae226 100644 --- a/routers/api/packages/nuget/nuget.go +++ b/routers/api/packages/nuget/nuget.go @@ -594,13 +594,13 @@ func UploadSymbolPackage(ctx *context.Context) { func processUploadedFile(ctx *context.Context, expectedType nuget_module.PackageType) (*nuget_module.Package, *packages_module.HashedBuffer, []io.Closer) { closables := make([]io.Closer, 0, 2) - upload, close, err := ctx.UploadStream() + upload, needToClose, err := ctx.UploadStream() if err != nil { apiError(ctx, http.StatusBadRequest, err) return nil, nil, closables } - if close { + if needToClose { closables = append(closables, upload) } diff --git a/routers/api/packages/rpm/rpm.go b/routers/api/packages/rpm/rpm.go index 4de361c21..c59366992 100644 --- a/routers/api/packages/rpm/rpm.go +++ b/routers/api/packages/rpm/rpm.go @@ -117,12 +117,12 @@ func GetRepositoryFile(ctx *context.Context) { } func UploadPackageFile(ctx *context.Context) { - upload, close, err := ctx.UploadStream() + upload, needToClose, err := ctx.UploadStream() if err != nil { apiError(ctx, http.StatusInternalServerError, err) return } - if close { + if needToClose { defer upload.Close() } diff --git a/routers/api/packages/rubygems/rubygems.go b/routers/api/packages/rubygems/rubygems.go index d2fbcd01f..ba5f4de08 100644 --- a/routers/api/packages/rubygems/rubygems.go +++ b/routers/api/packages/rubygems/rubygems.go @@ -197,12 +197,12 @@ func DownloadPackageFile(ctx *context.Context) { // UploadPackageFile adds a file to the package. If the package does not exist, it gets created. func UploadPackageFile(ctx *context.Context) { - upload, close, err := ctx.UploadStream() + upload, needToClose, err := ctx.UploadStream() if err != nil { apiError(ctx, http.StatusBadRequest, err) return } - if close { + if needToClose { defer upload.Close() } diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index a15210ac5..e4c848cd2 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -93,6 +93,7 @@ import ( "code.gitea.io/gitea/routers/api/v1/repo" "code.gitea.io/gitea/routers/api/v1/settings" "code.gitea.io/gitea/routers/api/v1/user" + "code.gitea.io/gitea/services/actions" "code.gitea.io/gitea/services/auth" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/forms" @@ -753,6 +754,34 @@ func Routes() *web.Route { m.Use(shared.Middlewares()...) + addActionsRoutes := func( + m *web.Route, + reqChecker func(ctx *context.APIContext), + act actions.API, + ) { + m.Group("/actions", func() { + m.Group("/secrets", func() { + m.Get("", reqToken(), reqChecker, act.ListActionsSecrets) + m.Combo("/{secretname}"). + Put(reqToken(), reqChecker, bind(api.CreateOrUpdateSecretOption{}), act.CreateOrUpdateSecret). + Delete(reqToken(), reqChecker, act.DeleteSecret) + }) + + m.Group("/variables", func() { + m.Get("", reqToken(), reqChecker, act.ListVariables) + m.Combo("/{variablename}"). + Get(reqToken(), reqChecker, act.GetVariable). + Delete(reqToken(), reqChecker, act.DeleteVariable). + Post(reqToken(), reqChecker, bind(api.CreateVariableOption{}), act.CreateVariable). + Put(reqToken(), reqChecker, bind(api.UpdateVariableOption{}), act.UpdateVariable) + }) + + m.Group("/runners", func() { + m.Get("/registration-token", reqToken(), reqChecker, act.GetRegistrationToken) + }) + }) + } + m.Group("", func() { // Miscellaneous (no scope required) if setting.API.EnableSwagger { @@ -994,26 +1023,11 @@ func Routes() *web.Route { m.Post("/accept", repo.AcceptTransfer) m.Post("/reject", repo.RejectTransfer) }, reqToken()) - m.Group("/actions", func() { - m.Group("/secrets", func() { - m.Combo("/{secretname}"). - Put(reqToken(), reqOwner(), bind(api.CreateOrUpdateSecretOption{}), repo.CreateOrUpdateSecret). - Delete(reqToken(), reqOwner(), repo.DeleteSecret) - }) - - m.Group("/variables", func() { - m.Get("", reqToken(), reqOwner(), repo.ListVariables) - m.Combo("/{variablename}"). - Get(reqToken(), reqOwner(), repo.GetVariable). - Delete(reqToken(), reqOwner(), repo.DeleteVariable). - Post(reqToken(), reqOwner(), bind(api.CreateVariableOption{}), repo.CreateVariable). - Put(reqToken(), reqOwner(), bind(api.UpdateVariableOption{}), repo.UpdateVariable) - }) - - m.Group("/runners", func() { - m.Get("/registration-token", reqToken(), reqOwner(), repo.GetRegistrationToken) - }) - }) + addActionsRoutes( + m, + reqOwner(), + repo.NewAction(), + ) m.Group("/hooks/git", func() { m.Combo("").Get(repo.ListGitHooks) m.Group("/{id}", func() { @@ -1405,27 +1419,11 @@ func Routes() *web.Route { m.Combo("/{username}").Get(reqToken(), org.IsMember). Delete(reqToken(), reqOrgOwnership(), org.DeleteMember) }) - m.Group("/actions", func() { - m.Group("/secrets", func() { - m.Get("", reqToken(), reqOrgOwnership(), org.ListActionsSecrets) - m.Combo("/{secretname}"). - Put(reqToken(), reqOrgOwnership(), bind(api.CreateOrUpdateSecretOption{}), org.CreateOrUpdateSecret). - Delete(reqToken(), reqOrgOwnership(), org.DeleteSecret) - }) - - m.Group("/variables", func() { - m.Get("", reqToken(), reqOrgOwnership(), org.ListVariables) - m.Combo("/{variablename}"). - Get(reqToken(), reqOrgOwnership(), org.GetVariable). - Delete(reqToken(), reqOrgOwnership(), org.DeleteVariable). - Post(reqToken(), reqOrgOwnership(), bind(api.CreateVariableOption{}), org.CreateVariable). - Put(reqToken(), reqOrgOwnership(), bind(api.UpdateVariableOption{}), org.UpdateVariable) - }) - - m.Group("/runners", func() { - m.Get("/registration-token", reqToken(), reqOrgOwnership(), org.GetRegistrationToken) - }) - }) + addActionsRoutes( + m, + reqOrgOwnership(), + org.NewAction(), + ) m.Group("/public_members", func() { m.Get("", org.ListPublicMembers) m.Combo("/{username}").Get(org.IsPublicMember). diff --git a/routers/api/v1/org/variables.go b/routers/api/v1/org/action.go similarity index 58% rename from routers/api/v1/org/variables.go rename to routers/api/v1/org/action.go index eaf7bdc45..03a1fa8cc 100644 --- a/routers/api/v1/org/variables.go +++ b/routers/api/v1/org/action.go @@ -9,16 +9,188 @@ import ( actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/db" + secret_model "code.gitea.io/gitea/models/secret" 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/shared" "code.gitea.io/gitea/routers/api/v1/utils" actions_service "code.gitea.io/gitea/services/actions" "code.gitea.io/gitea/services/context" + secret_service "code.gitea.io/gitea/services/secrets" ) +// ListActionsSecrets list an organization's actions secrets +func (Action) ListActionsSecrets(ctx *context.APIContext) { + // swagger:operation GET /orgs/{org}/actions/secrets organization orgListActionsSecrets + // --- + // summary: List an organization's actions secrets + // produces: + // - application/json + // parameters: + // - name: org + // in: path + // description: name of the organization + // type: string + // required: true + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/SecretList" + // "404": + // "$ref": "#/responses/notFound" + + opts := &secret_model.FindSecretsOptions{ + OwnerID: ctx.Org.Organization.ID, + ListOptions: utils.GetListOptions(ctx), + } + + secrets, count, err := db.FindAndCount[secret_model.Secret](ctx, opts) + if err != nil { + ctx.InternalServerError(err) + return + } + + apiSecrets := make([]*api.Secret, len(secrets)) + for k, v := range secrets { + apiSecrets[k] = &api.Secret{ + Name: v.Name, + Created: v.CreatedUnix.AsTime(), + } + } + + ctx.SetTotalCountHeader(count) + ctx.JSON(http.StatusOK, apiSecrets) +} + +// create or update one secret of the organization +func (Action) CreateOrUpdateSecret(ctx *context.APIContext) { + // swagger:operation PUT /orgs/{org}/actions/secrets/{secretname} organization updateOrgSecret + // --- + // summary: Create or Update a secret value in an organization + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: org + // in: path + // description: name of organization + // type: string + // required: true + // - name: secretname + // in: path + // description: name of the secret + // type: string + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/CreateOrUpdateSecretOption" + // responses: + // "201": + // description: response when creating a secret + // "204": + // description: response when updating a secret + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + opt := web.GetForm(ctx).(*api.CreateOrUpdateSecretOption) + + _, created, err := secret_service.CreateOrUpdateSecret(ctx, ctx.Org.Organization.ID, 0, ctx.Params("secretname"), opt.Data) + if err != nil { + if errors.Is(err, util.ErrInvalidArgument) { + ctx.Error(http.StatusBadRequest, "CreateOrUpdateSecret", err) + } else if errors.Is(err, util.ErrNotExist) { + ctx.Error(http.StatusNotFound, "CreateOrUpdateSecret", err) + } else { + ctx.Error(http.StatusInternalServerError, "CreateOrUpdateSecret", err) + } + return + } + + if created { + ctx.Status(http.StatusCreated) + } else { + ctx.Status(http.StatusNoContent) + } +} + +// DeleteSecret delete one secret of the organization +func (Action) DeleteSecret(ctx *context.APIContext) { + // swagger:operation DELETE /orgs/{org}/actions/secrets/{secretname} organization deleteOrgSecret + // --- + // summary: Delete a secret in an organization + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: org + // in: path + // description: name of organization + // type: string + // required: true + // - name: secretname + // in: path + // description: name of the secret + // type: string + // required: true + // responses: + // "204": + // description: delete one secret of the organization + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + err := secret_service.DeleteSecretByName(ctx, ctx.Org.Organization.ID, 0, ctx.Params("secretname")) + if err != nil { + if errors.Is(err, util.ErrInvalidArgument) { + ctx.Error(http.StatusBadRequest, "DeleteSecret", err) + } else if errors.Is(err, util.ErrNotExist) { + ctx.Error(http.StatusNotFound, "DeleteSecret", err) + } else { + ctx.Error(http.StatusInternalServerError, "DeleteSecret", err) + } + return + } + + ctx.Status(http.StatusNoContent) +} + +// https://docs.github.com/en/rest/actions/self-hosted-runners?apiVersion=2022-11-28#create-a-registration-token-for-an-organization +// GetRegistrationToken returns the token to register org runners +func (Action) GetRegistrationToken(ctx *context.APIContext) { + // swagger:operation GET /orgs/{org}/actions/runners/registration-token organization orgGetRunnerRegistrationToken + // --- + // summary: Get an organization's actions runner registration token + // produces: + // - application/json + // parameters: + // - name: org + // in: path + // description: name of the organization + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/RegistrationToken" + + shared.GetRegistrationToken(ctx, ctx.Org.Organization.ID, 0) +} + // ListVariables list org-level variables -func ListVariables(ctx *context.APIContext) { +func (Action) ListVariables(ctx *context.APIContext) { // swagger:operation GET /orgs/{org}/actions/variables organization getOrgVariablesList // --- // summary: Get an org-level variables list @@ -70,7 +242,7 @@ func ListVariables(ctx *context.APIContext) { } // GetVariable get an org-level variable -func GetVariable(ctx *context.APIContext) { +func (Action) GetVariable(ctx *context.APIContext) { // swagger:operation GET /orgs/{org}/actions/variables/{variablename} organization getOrgVariable // --- // summary: Get an org-level variable @@ -119,7 +291,7 @@ func GetVariable(ctx *context.APIContext) { } // DeleteVariable delete an org-level variable -func DeleteVariable(ctx *context.APIContext) { +func (Action) DeleteVariable(ctx *context.APIContext) { // swagger:operation DELETE /orgs/{org}/actions/variables/{variablename} organization deleteOrgVariable // --- // summary: Delete an org-level variable @@ -163,7 +335,7 @@ func DeleteVariable(ctx *context.APIContext) { } // CreateVariable create an org-level variable -func CreateVariable(ctx *context.APIContext) { +func (Action) CreateVariable(ctx *context.APIContext) { // swagger:operation POST /orgs/{org}/actions/variables/{variablename} organization createOrgVariable // --- // summary: Create an org-level variable @@ -227,7 +399,7 @@ func CreateVariable(ctx *context.APIContext) { } // UpdateVariable update an org-level variable -func UpdateVariable(ctx *context.APIContext) { +func (Action) UpdateVariable(ctx *context.APIContext) { // swagger:operation PUT /orgs/{org}/actions/variables/{variablename} organization updateOrgVariable // --- // summary: Update an org-level variable @@ -289,3 +461,13 @@ func UpdateVariable(ctx *context.APIContext) { ctx.Status(http.StatusNoContent) } + +var _ actions_service.API = new(Action) + +// Action implements actions_service.API +type Action struct{} + +// NewAction creates a new Action service +func NewAction() actions_service.API { + return Action{} +} diff --git a/routers/api/v1/org/runners.go b/routers/api/v1/org/runners.go deleted file mode 100644 index 2a52bd877..000000000 --- a/routers/api/v1/org/runners.go +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2023 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package org - -import ( - "code.gitea.io/gitea/routers/api/v1/shared" - "code.gitea.io/gitea/services/context" -) - -// https://docs.github.com/en/rest/actions/self-hosted-runners?apiVersion=2022-11-28#create-a-registration-token-for-an-organization - -// GetRegistrationToken returns the token to register org runners -func GetRegistrationToken(ctx *context.APIContext) { - // swagger:operation GET /orgs/{org}/actions/runners/registration-token organization orgGetRunnerRegistrationToken - // --- - // summary: Get an organization's actions runner registration token - // produces: - // - application/json - // parameters: - // - name: org - // in: path - // description: name of the organization - // type: string - // required: true - // responses: - // "200": - // "$ref": "#/responses/RegistrationToken" - - shared.GetRegistrationToken(ctx, ctx.Org.Organization.ID, 0) -} diff --git a/routers/api/v1/org/secrets.go b/routers/api/v1/org/secrets.go deleted file mode 100644 index abb6bb26c..000000000 --- a/routers/api/v1/org/secrets.go +++ /dev/null @@ -1,166 +0,0 @@ -// Copyright 2023 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package org - -import ( - "errors" - "net/http" - - "code.gitea.io/gitea/models/db" - secret_model "code.gitea.io/gitea/models/secret" - 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/utils" - "code.gitea.io/gitea/services/context" - secret_service "code.gitea.io/gitea/services/secrets" -) - -// ListActionsSecrets list an organization's actions secrets -func ListActionsSecrets(ctx *context.APIContext) { - // swagger:operation GET /orgs/{org}/actions/secrets organization orgListActionsSecrets - // --- - // summary: List an organization's actions secrets - // produces: - // - application/json - // parameters: - // - name: org - // in: path - // description: name of the organization - // type: string - // required: true - // - name: page - // in: query - // description: page number of results to return (1-based) - // type: integer - // - name: limit - // in: query - // description: page size of results - // type: integer - // responses: - // "200": - // "$ref": "#/responses/SecretList" - // "404": - // "$ref": "#/responses/notFound" - - opts := &secret_model.FindSecretsOptions{ - OwnerID: ctx.Org.Organization.ID, - ListOptions: utils.GetListOptions(ctx), - } - - secrets, count, err := db.FindAndCount[secret_model.Secret](ctx, opts) - if err != nil { - ctx.InternalServerError(err) - return - } - - apiSecrets := make([]*api.Secret, len(secrets)) - for k, v := range secrets { - apiSecrets[k] = &api.Secret{ - Name: v.Name, - Created: v.CreatedUnix.AsTime(), - } - } - - ctx.SetTotalCountHeader(count) - ctx.JSON(http.StatusOK, apiSecrets) -} - -// create or update one secret of the organization -func CreateOrUpdateSecret(ctx *context.APIContext) { - // swagger:operation PUT /orgs/{org}/actions/secrets/{secretname} organization updateOrgSecret - // --- - // summary: Create or Update a secret value in an organization - // consumes: - // - application/json - // produces: - // - application/json - // parameters: - // - name: org - // in: path - // description: name of organization - // type: string - // required: true - // - name: secretname - // in: path - // description: name of the secret - // type: string - // required: true - // - name: body - // in: body - // schema: - // "$ref": "#/definitions/CreateOrUpdateSecretOption" - // responses: - // "201": - // description: response when creating a secret - // "204": - // description: response when updating a secret - // "400": - // "$ref": "#/responses/error" - // "404": - // "$ref": "#/responses/notFound" - - opt := web.GetForm(ctx).(*api.CreateOrUpdateSecretOption) - - _, created, err := secret_service.CreateOrUpdateSecret(ctx, ctx.Org.Organization.ID, 0, ctx.Params("secretname"), opt.Data) - if err != nil { - if errors.Is(err, util.ErrInvalidArgument) { - ctx.Error(http.StatusBadRequest, "CreateOrUpdateSecret", err) - } else if errors.Is(err, util.ErrNotExist) { - ctx.Error(http.StatusNotFound, "CreateOrUpdateSecret", err) - } else { - ctx.Error(http.StatusInternalServerError, "CreateOrUpdateSecret", err) - } - return - } - - if created { - ctx.Status(http.StatusCreated) - } else { - ctx.Status(http.StatusNoContent) - } -} - -// DeleteSecret delete one secret of the organization -func DeleteSecret(ctx *context.APIContext) { - // swagger:operation DELETE /orgs/{org}/actions/secrets/{secretname} organization deleteOrgSecret - // --- - // summary: Delete a secret in an organization - // consumes: - // - application/json - // produces: - // - application/json - // parameters: - // - name: org - // in: path - // description: name of organization - // type: string - // required: true - // - name: secretname - // in: path - // description: name of the secret - // type: string - // required: true - // responses: - // "204": - // description: delete one secret of the organization - // "400": - // "$ref": "#/responses/error" - // "404": - // "$ref": "#/responses/notFound" - - err := secret_service.DeleteSecretByName(ctx, ctx.Org.Organization.ID, 0, ctx.Params("secretname")) - if err != nil { - if errors.Is(err, util.ErrInvalidArgument) { - ctx.Error(http.StatusBadRequest, "DeleteSecret", err) - } else if errors.Is(err, util.ErrNotExist) { - ctx.Error(http.StatusNotFound, "DeleteSecret", err) - } else { - ctx.Error(http.StatusInternalServerError, "DeleteSecret", err) - } - return - } - - ctx.Status(http.StatusNoContent) -} diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go index 03321d956..311cfca6e 100644 --- a/routers/api/v1/repo/action.go +++ b/routers/api/v1/repo/action.go @@ -9,17 +9,76 @@ import ( actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/db" + secret_model "code.gitea.io/gitea/models/secret" 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/shared" "code.gitea.io/gitea/routers/api/v1/utils" actions_service "code.gitea.io/gitea/services/actions" "code.gitea.io/gitea/services/context" secret_service "code.gitea.io/gitea/services/secrets" ) +// ListActionsSecrets list an repo's actions secrets +func (Action) ListActionsSecrets(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/actions/secrets repository repoListActionsSecrets + // --- + // summary: List an repo's actions secrets + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repository + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repository + // type: string + // required: true + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/SecretList" + // "404": + // "$ref": "#/responses/notFound" + + repo := ctx.Repo.Repository + + opts := &secret_model.FindSecretsOptions{ + RepoID: repo.ID, + ListOptions: utils.GetListOptions(ctx), + } + + secrets, count, err := db.FindAndCount[secret_model.Secret](ctx, opts) + if err != nil { + ctx.InternalServerError(err) + return + } + + apiSecrets := make([]*api.Secret, len(secrets)) + for k, v := range secrets { + apiSecrets[k] = &api.Secret{ + Name: v.Name, + Created: v.CreatedUnix.AsTime(), + } + } + + ctx.SetTotalCountHeader(count) + ctx.JSON(http.StatusOK, apiSecrets) +} + // create or update one secret of the repository -func CreateOrUpdateSecret(ctx *context.APIContext) { +func (Action) CreateOrUpdateSecret(ctx *context.APIContext) { // swagger:operation PUT /repos/{owner}/{repo}/actions/secrets/{secretname} repository updateRepoSecret // --- // summary: Create or Update a secret value in a repository @@ -82,7 +141,7 @@ func CreateOrUpdateSecret(ctx *context.APIContext) { } // DeleteSecret delete one secret of the repository -func DeleteSecret(ctx *context.APIContext) { +func (Action) DeleteSecret(ctx *context.APIContext) { // swagger:operation DELETE /repos/{owner}/{repo}/actions/secrets/{secretname} repository deleteRepoSecret // --- // summary: Delete a secret in a repository @@ -133,7 +192,7 @@ func DeleteSecret(ctx *context.APIContext) { } // GetVariable get a repo-level variable -func GetVariable(ctx *context.APIContext) { +func (Action) GetVariable(ctx *context.APIContext) { // swagger:operation GET /repos/{owner}/{repo}/actions/variables/{variablename} repository getRepoVariable // --- // summary: Get a repo-level variable @@ -186,7 +245,7 @@ func GetVariable(ctx *context.APIContext) { } // DeleteVariable delete a repo-level variable -func DeleteVariable(ctx *context.APIContext) { +func (Action) DeleteVariable(ctx *context.APIContext) { // swagger:operation DELETE /repos/{owner}/{repo}/actions/variables/{variablename} repository deleteRepoVariable // --- // summary: Delete a repo-level variable @@ -235,7 +294,7 @@ func DeleteVariable(ctx *context.APIContext) { } // CreateVariable create a repo-level variable -func CreateVariable(ctx *context.APIContext) { +func (Action) CreateVariable(ctx *context.APIContext) { // swagger:operation POST /repos/{owner}/{repo}/actions/variables/{variablename} repository createRepoVariable // --- // summary: Create a repo-level variable @@ -302,7 +361,7 @@ func CreateVariable(ctx *context.APIContext) { } // UpdateVariable update a repo-level variable -func UpdateVariable(ctx *context.APIContext) { +func (Action) UpdateVariable(ctx *context.APIContext) { // swagger:operation PUT /repos/{owner}/{repo}/actions/variables/{variablename} repository updateRepoVariable // --- // summary: Update a repo-level variable @@ -369,7 +428,7 @@ func UpdateVariable(ctx *context.APIContext) { } // ListVariables list repo-level variables -func ListVariables(ctx *context.APIContext) { +func (Action) ListVariables(ctx *context.APIContext) { // swagger:operation GET /repos/{owner}/{repo}/actions/variables repository getRepoVariablesList // --- // summary: Get repo-level variables list @@ -423,3 +482,38 @@ func ListVariables(ctx *context.APIContext) { ctx.SetTotalCountHeader(count) ctx.JSON(http.StatusOK, variables) } + +// GetRegistrationToken returns the token to register repo runners +func (Action) GetRegistrationToken(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/runners/registration-token repository repoGetRunnerRegistrationToken + // --- + // summary: Get a repository's actions runner registration token + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/RegistrationToken" + + shared.GetRegistrationToken(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.ID) +} + +var _ actions_service.API = new(Action) + +// Action implements actions_service.API +type Action struct{} + +// NewAction creates a new Action service +func NewAction() actions_service.API { + return Action{} +} diff --git a/routers/api/v1/repo/compare.go b/routers/api/v1/repo/compare.go index 549b9b7fa..cfd61d768 100644 --- a/routers/api/v1/repo/compare.go +++ b/routers/api/v1/repo/compare.go @@ -16,7 +16,7 @@ import ( // CompareDiff compare two branches or commits func CompareDiff(ctx *context.APIContext) { - // swagger:operation GET /repos/{owner}/{repo}/compare/{basehead} Get commit comparison information + // swagger:operation GET /repos/{owner}/{repo}/compare/{basehead} repository repoCompareDiff // --- // summary: Get commit comparison information // produces: diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go index 0d304dd66..35c1fdcc0 100644 --- a/routers/api/v1/repo/issue.go +++ b/routers/api/v1/repo/issue.go @@ -217,7 +217,6 @@ func SearchIssues(ctx *context.APIContext) { var includedAnyLabels []int64 { - labels := ctx.FormTrim("labels") var includedLabelNames []string if len(labels) > 0 { diff --git a/routers/api/v1/repo/mirror.go b/routers/api/v1/repo/mirror.go index 864644e1e..2a896de4f 100644 --- a/routers/api/v1/repo/mirror.go +++ b/routers/api/v1/repo/mirror.go @@ -180,7 +180,6 @@ func ListPushMirrors(ctx *context.APIContext) { if err == nil { responsePushMirrors = append(responsePushMirrors, m) } - } ctx.SetLinkHeader(len(responsePushMirrors), utils.GetListOptions(ctx).PageSize) ctx.SetTotalCountHeader(count) diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index eec3c49bc..852ec78ad 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -1058,7 +1058,6 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption) isSameRepo = true headUser = ctx.Repo.Owner headBranch = headInfos[0] - } else if len(headInfos) == 2 { headUser, err = user_model.GetUserByName(ctx, headInfos[0]) if err != nil { @@ -1072,18 +1071,16 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption) headBranch = headInfos[1] // The head repository can also point to the same repo isSameRepo = ctx.Repo.Owner.ID == headUser.ID - } else { ctx.NotFound() return nil, nil, nil, nil, "", "" } ctx.Repo.PullRequest.SameRepo = isSameRepo - log.Info("Base branch: %s", baseBranch) - log.Info("Repo path: %s", ctx.Repo.GitRepo.Path) + log.Trace("Repo path: %q, base branch: %q, head branch: %q", ctx.Repo.GitRepo.Path, baseBranch, headBranch) // Check if base branch is valid. - if !ctx.Repo.GitRepo.IsBranchExist(baseBranch) { - ctx.NotFound("IsBranchExist") + if !ctx.Repo.GitRepo.IsBranchExist(baseBranch) && !ctx.Repo.GitRepo.IsTagExist(baseBranch) { + ctx.NotFound("BaseNotExist") return nil, nil, nil, nil, "", "" } @@ -1146,7 +1143,7 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption) } // Check if head branch is valid. - if !headGitRepo.IsBranchExist(headBranch) { + if !headGitRepo.IsBranchExist(headBranch) && !headGitRepo.IsTagExist(headBranch) { headGitRepo.Close() ctx.NotFound() return nil, nil, nil, nil, "", "" diff --git a/routers/api/v1/repo/pull_review.go b/routers/api/v1/repo/pull_review.go index 77c0d25e2..6799e43c7 100644 --- a/routers/api/v1/repo/pull_review.go +++ b/routers/api/v1/repo/pull_review.go @@ -4,6 +4,7 @@ package repo import ( + "errors" "fmt" "net/http" "strings" @@ -519,7 +520,11 @@ func CreatePullReview(ctx *context.APIContext) { // create review and associate all pending review comments review, _, err := pull_service.SubmitReview(ctx, ctx.Doer, ctx.Repo.GitRepo, pr.Issue, reviewType, opts.Body, opts.CommitID, nil) if err != nil { - ctx.Error(http.StatusInternalServerError, "SubmitReview", err) + if errors.Is(err, pull_service.ErrSubmitReviewOnClosedPR) { + ctx.Error(http.StatusUnprocessableEntity, "", err) + } else { + ctx.Error(http.StatusInternalServerError, "SubmitReview", err) + } return } @@ -607,7 +612,11 @@ func SubmitPullReview(ctx *context.APIContext) { // create review and associate all pending review comments review, _, err = pull_service.SubmitReview(ctx, ctx.Doer, ctx.Repo.GitRepo, pr.Issue, reviewType, opts.Body, headCommitID, nil) if err != nil { - ctx.Error(http.StatusInternalServerError, "SubmitReview", err) + if errors.Is(err, pull_service.ErrSubmitReviewOnClosedPR) { + ctx.Error(http.StatusUnprocessableEntity, "", err) + } else { + ctx.Error(http.StatusInternalServerError, "SubmitReview", err) + } return } @@ -875,7 +884,6 @@ func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions } if ctx.Repo.Repository.Owner.IsOrganization() && len(opts.TeamReviewers) > 0 { - teamReviewers := make([]*organization.Team, 0, len(opts.TeamReviewers)) for _, t := range opts.TeamReviewers { var teamReviewer *organization.Team diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index 6692633fa..66eb227c1 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -1094,7 +1094,6 @@ func updateMirror(ctx *context.APIContext, opts api.EditRepoOption) error { // update MirrorInterval if opts.MirrorInterval != nil { - // MirrorInterval should be a duration interval, err := time.ParseDuration(*opts.MirrorInterval) if err != nil { diff --git a/routers/api/v1/repo/runners.go b/routers/api/v1/repo/runners.go deleted file mode 100644 index fe133b311..000000000 --- a/routers/api/v1/repo/runners.go +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2023 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package repo - -import ( - "code.gitea.io/gitea/routers/api/v1/shared" - "code.gitea.io/gitea/services/context" -) - -// GetRegistrationToken returns the token to register repo runners -func GetRegistrationToken(ctx *context.APIContext) { - // swagger:operation GET /repos/{owner}/{repo}/runners/registration-token repository repoGetRunnerRegistrationToken - // --- - // summary: Get a repository's actions runner registration token - // produces: - // - application/json - // parameters: - // - name: owner - // in: path - // description: owner of the repo - // type: string - // required: true - // - name: repo - // in: path - // description: name of the repo - // type: string - // required: true - // responses: - // "200": - // "$ref": "#/responses/RegistrationToken" - - shared.GetRegistrationToken(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.ID) -} diff --git a/routers/api/v1/repo/wiki.go b/routers/api/v1/repo/wiki.go index b6f51cdad..1b92c7bce 100644 --- a/routers/api/v1/repo/wiki.go +++ b/routers/api/v1/repo/wiki.go @@ -478,7 +478,6 @@ func findEntryForFile(commit *git.Commit, target string) (*git.TreeEntry, error) func findWikiRepoCommit(ctx *context.APIContext) (*git.Repository, *git.Commit) { wikiRepo, err := gitrepo.OpenWikiRepository(ctx, ctx.Repo.Repository) if err != nil { - if git.IsErrNotExist(err) || err.Error() == "no such file or directory" { ctx.NotFound(err) } else { diff --git a/routers/private/hook_pre_receive.go b/routers/private/hook_pre_receive.go index cb356a184..da9fa2008 100644 --- a/routers/private/hook_pre_receive.go +++ b/routers/private/hook_pre_receive.go @@ -259,7 +259,6 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r UserMsg: fmt.Sprintf("branch %s is protected from force push", branchName), }) return - } } diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index fa687bbb9..dc21f1a4e 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -552,7 +552,7 @@ func getRunJobs(ctx *context_module.Context, runIndex, jobIndex int64) (*actions return nil, nil } if len(jobs) == 0 { - ctx.Error(http.StatusNotFound, err.Error()) + ctx.Error(http.StatusNotFound) return nil, nil } @@ -689,7 +689,6 @@ func ArtifactsDownloadView(ctx *context_module.Context) { writer := zip.NewWriter(ctx.Resp) defer writer.Close() for _, art := range artifacts { - f, err := storage.ActionsArtifacts.Open(art.StoragePath) if err != nil { ctx.Error(http.StatusInternalServerError, err.Error()) diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go index 718454e06..33491ec69 100644 --- a/routers/web/repo/commit.go +++ b/routers/web/repo/commit.go @@ -212,8 +212,6 @@ func SearchCommits(ctx *context.Context) { // FileHistory show a file's reversions func FileHistory(ctx *context.Context) { - ctx.Data["IsRepoToolbarCommits"] = true - fileName := ctx.Repo.TreePath if len(fileName) == 0 { Commits(ctx) diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go index b07209c77..a61e23add 100644 --- a/routers/web/repo/compare.go +++ b/routers/web/repo/compare.go @@ -800,7 +800,6 @@ func CompareDiff(ctx *context.Context) { } ctx.Data["Title"] = "Comparing " + base.ShortSha(beforeCommitID) + separator + base.ShortSha(afterCommitID) - ctx.Data["IsRepoToolbarCommits"] = true ctx.Data["IsDiffCompare"] = true _, templateErrs := setTemplateIfExists(ctx, pullRequestTemplateKey, pullRequestTemplateCandidates) diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index b0b4b6533..97f219511 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -927,7 +927,6 @@ func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleFiles } } } - } if template.Ref != "" && !strings.HasPrefix(template.Ref, "refs/") { // Assume that the ref intended is always a branch - for tags users should use refs/tags/ @@ -1680,7 +1679,6 @@ func ViewIssue(ctx *context.Context) { if comment.ProjectID > 0 && comment.Project == nil { comment.Project = ghostProject } - } else if comment.Type == issues_model.CommentTypeAssignees || comment.Type == issues_model.CommentTypeReviewRequest { if err = comment.LoadAssigneeUserAndTeam(ctx); err != nil { ctx.ServerError("LoadAssigneeUserAndTeam", err) @@ -2605,7 +2603,6 @@ func SearchIssues(ctx *context.Context) { var includedAnyLabels []int64 { - labels := ctx.FormTrim("labels") var includedLabelNames []string if len(labels) > 0 { @@ -2993,7 +2990,6 @@ func NewComment(ctx *context.Context) { if (ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) || (ctx.IsSigned && issue.IsPoster(ctx.Doer.ID))) && (form.Status == "reopen" || form.Status == "close") && !(issue.IsPull && issue.PullRequest.HasMerged) { - // Duplication and conflict check should apply to reopen pull request. var pr *issues_model.PullRequest diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index c6c614253..a0dd36927 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -670,7 +670,6 @@ func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C } if pb != nil && pb.EnableStatusCheck { - var missingRequiredChecks []string for _, requiredContext := range pb.StatusCheckContexts { contextFound := false @@ -873,7 +872,6 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi // Validate the given commit sha to show (if any passed) if willShowSpecifiedCommit || willShowSpecifiedCommitRange { - foundStartCommit := len(specifiedStartCommit) == 0 foundEndCommit := len(specifiedEndCommit) == 0 @@ -1185,7 +1183,6 @@ func UpdatePullRequest(ctx *context.Context) { ctx.Flash.Error(flashError) ctx.Redirect(issue.Link()) return - } ctx.Flash.Error(err.Error()) ctx.Redirect(issue.Link()) @@ -1439,7 +1436,6 @@ func CompareAndPullRequestPost(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.pulls.compare_changes") ctx.Data["PageIsComparePull"] = true ctx.Data["IsDiffCompare"] = true - ctx.Data["IsRepoToolbarCommits"] = true ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled upload.AddUploadContext(ctx, "comment") diff --git a/routers/web/repo/pull_review.go b/routers/web/repo/pull_review.go index 81af2cbb5..24763668d 100644 --- a/routers/web/repo/pull_review.go +++ b/routers/web/repo/pull_review.go @@ -248,6 +248,8 @@ func SubmitReview(ctx *context.Context) { if issues_model.IsContentEmptyErr(err) { ctx.Flash.Error(ctx.Tr("repo.issues.review.content.empty")) ctx.JSONRedirect(fmt.Sprintf("%s/pulls/%d/files", ctx.Repo.RepoLink, issue.Index)) + } else if errors.Is(err, pull_service.ErrSubmitReviewOnClosedPR) { + ctx.Status(http.StatusUnprocessableEntity) } else { ctx.ServerError("SubmitReview", err) } @@ -302,7 +304,6 @@ func UpdateViewedFiles(ctx *context.Context) { updatedFiles := make(map[string]pull_model.ViewedState, len(data.Files)) for file, viewed := range data.Files { - // Only unviewed and viewed are possible, has-changed can not be set from the outside state := pull_model.Unviewed if viewed { diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index e98cd46c4..2644b2722 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -352,7 +352,6 @@ func loadLatestCommitData(ctx *context.Context, latestCommit *git.Commit) bool { // or of directory if not in root directory. ctx.Data["LatestCommit"] = latestCommit if latestCommit != nil { - verification := asymkey_model.ParseCommitWithSignature(ctx, latestCommit) if err := asymkey_model.CalculateTrustStatus(verification, ctx.Repo.Repository.GetTrustModel(), func(user *user_model.User) (bool, error) { diff --git a/routers/web/webfinger.go b/routers/web/webfinger.go index 099f6236a..1f3de70db 100644 --- a/routers/web/webfinger.go +++ b/routers/web/webfinger.go @@ -102,23 +102,11 @@ func WebfingerQuery(ctx *context.Context) { default: ctx.Error(http.StatusNotFound) return - - } - case 4: - //nolint:gocritic - if parts[3] == "teams" { - ctx.Error(http.StatusNotFound) - return - - } else { - ctx.Error(http.StatusNotFound) - return } default: ctx.Error(http.StatusNotFound) return - } default: diff --git a/services/actions/interface.go b/services/actions/interface.go new file mode 100644 index 000000000..d4fa782fe --- /dev/null +++ b/services/actions/interface.go @@ -0,0 +1,28 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import "code.gitea.io/gitea/services/context" + +// API for actions of a repository or organization +type API interface { + // ListActionsSecrets list secrets + ListActionsSecrets(*context.APIContext) + // CreateOrUpdateSecret create or update a secret + CreateOrUpdateSecret(*context.APIContext) + // DeleteSecret delete a secret + DeleteSecret(*context.APIContext) + // ListVariables list variables + ListVariables(*context.APIContext) + // GetVariable get a variable + GetVariable(*context.APIContext) + // DeleteVariable delete a variable + DeleteVariable(*context.APIContext) + // CreateVariable create a variable + CreateVariable(*context.APIContext) + // UpdateVariable update a variable + UpdateVariable(*context.APIContext) + // GetRegistrationToken get registration token + GetRegistrationToken(*context.APIContext) +} diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go index 365212d9c..78f413c21 100644 --- a/services/actions/notifier_helper.go +++ b/services/actions/notifier_helper.go @@ -329,13 +329,15 @@ func handleWorkflows( TriggerEvent: dwf.TriggerEvent.Name, Status: actions_model.StatusWaiting, } - if need, err := ifNeedApproval(ctx, run, input.Repo, input.Doer); err != nil { + + need, err := ifNeedApproval(ctx, run, input.Repo, input.Doer) + if err != nil { log.Error("check if need approval for repo %d with user %d: %v", input.Repo.ID, input.Doer.ID, err) continue - } else { - run.NeedApproval = need } + run.NeedApproval = need + if err := run.LoadAttributes(ctx); err != nil { log.Error("LoadAttributes: %v", err) continue diff --git a/services/actions/schedule_tasks.go b/services/actions/schedule_tasks.go index 33013e9a9..18f3324fd 100644 --- a/services/actions/schedule_tasks.go +++ b/services/actions/schedule_tasks.go @@ -132,13 +132,10 @@ func CreateScheduleTask(ctx context.Context, cron *actions_model.ActionSchedule) Status: actions_model.StatusWaiting, } - if err := run.LoadAttributes(ctx); err != nil { - log.Error("LoadAttributes: %v", err) - } - vars, err := actions_model.GetVariablesOfRun(ctx, run) if err != nil { - log.Error("GetVariablesOfSchedule: %v", err) + log.Error("GetVariablesOfRun: %v", err) + return err } // Parse the workflow specification from the cron schedule diff --git a/services/auth/source/ldap/source_sync.go b/services/auth/source/ldap/source_sync.go index 7c5d3da59..1f70edaa8 100644 --- a/services/auth/source/ldap/source_sync.go +++ b/services/auth/source/ldap/source_sync.go @@ -159,7 +159,6 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error { !strings.EqualFold(usr.Email, su.Mail) || usr.FullName != fullName || !usr.IsActive { - log.Trace("SyncExternalUsers[%s]: Updating user %s", source.authSource.Name, usr.Name) opts := &user_service.UpdateOptions{ diff --git a/services/context/repo.go b/services/context/repo.go index 43eeab809..3e30f2ba9 100644 --- a/services/context/repo.go +++ b/services/context/repo.go @@ -848,7 +848,6 @@ func getRefName(ctx *Base, repo *Repository, pathType RepoRefType) string { case RepoRefBranch: ref := getRefNameFromPath(ctx, repo, path, repo.GitRepo.IsBranchExist) if len(ref) == 0 { - // check if ref is HEAD parts := strings.Split(path, "/") if parts[0] == headRefName { @@ -991,7 +990,6 @@ func RepoRefByType(refType RepoRefType, ignoreNotExistErr ...bool) func(*Context return cancel } ctx.Repo.CommitID = ctx.Repo.Commit.ID.String() - } else if refType.RefTypeIncludesTags() && ctx.Repo.GitRepo.IsTagExist(refName) { ctx.Repo.IsViewTag = true ctx.Repo.TagName = refName diff --git a/services/doctor/dbconsistency.go b/services/doctor/dbconsistency.go index d036f75b2..6b931b703 100644 --- a/services/doctor/dbconsistency.go +++ b/services/doctor/dbconsistency.go @@ -159,6 +159,12 @@ func checkDBConsistency(ctx context.Context, logger log.Logger, autofix bool) er Fixer: actions_model.FixRunnersWithoutBelongingOwner, FixedMessage: "Removed", }, + { + Name: "Action Runners without existing repository", + Counter: actions_model.CountRunnersWithoutBelongingRepo, + Fixer: actions_model.FixRunnersWithoutBelongingRepo, + FixedMessage: "Removed", + }, { Name: "Topics with empty repository count", Counter: repo_model.CountOrphanedTopics, diff --git a/services/doctor/packages_nuget.go b/services/doctor/packages_nuget.go index 8c0a2d856..47fdb3ac1 100644 --- a/services/doctor/packages_nuget.go +++ b/services/doctor/packages_nuget.go @@ -51,7 +51,6 @@ func PackagesNugetNuspecCheck(ctx context.Context, logger log.Logger, autofix bo logger.Info("Found %d versions for package %s", len(pvs), pkg.Name) for _, pv := range pvs { - pfs, err := packages.GetFilesByVersionID(ctx, pv.ID) if err != nil { logger.Error("Failed to get files for package version %s %s: %v", pkg.Name, pv.Version, err) diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go index 0c89839ed..d9dbeedee 100644 --- a/services/gitdiff/gitdiff.go +++ b/services/gitdiff/gitdiff.go @@ -1040,8 +1040,8 @@ func createDiffFile(diff *Diff, line string) *DiffFile { // diff --git a/b b/b b/b b/b b/b b/b // midpoint := (len(line) + len(cmdDiffHead) - 1) / 2 - new, old := line[len(cmdDiffHead):midpoint], line[midpoint+1:] - if len(new) > 2 && len(old) > 2 && new[2:] == old[2:] { + newl, old := line[len(cmdDiffHead):midpoint], line[midpoint+1:] + if len(newl) > 2 && len(old) > 2 && newl[2:] == old[2:] { curFile.OldName = old[2:] curFile.Name = old[2:] } diff --git a/services/issue/commit.go b/services/issue/commit.go index e493a0321..8b927d52b 100644 --- a/services/issue/commit.go +++ b/services/issue/commit.go @@ -117,7 +117,6 @@ func UpdateIssuesCommit(ctx context.Context, doer *user_model.User, repo *repo_m var refIssue *issues_model.Issue var err error for _, ref := range references.FindAllIssueReferences(c.Message) { - // issue is from another repo if len(ref.Owner) > 0 && len(ref.Name) > 0 { refRepo, err = repo_model.GetRepositoryByOwnerAndName(ctx, ref.Owner, ref.Name) @@ -185,15 +184,15 @@ func UpdateIssuesCommit(ctx context.Context, doer *user_model.User, repo *repo_m continue } } - close := ref.Action == references.XRefActionCloses - if close && len(ref.TimeLog) > 0 { + isClosed := ref.Action == references.XRefActionCloses + if isClosed && len(ref.TimeLog) > 0 { if err := issueAddTime(ctx, refIssue, doer, c.Timestamp, ref.TimeLog); err != nil { return err } } - if close != refIssue.IsClosed { + if isClosed != refIssue.IsClosed { refIssue.Repo = refRepo - if err := ChangeStatus(ctx, refIssue, doer, c.Sha1, close); err != nil { + if err := ChangeStatus(ctx, refIssue, doer, c.Sha1, isClosed); err != nil { return err } } diff --git a/services/migrations/gitea_downloader.go b/services/migrations/gitea_downloader.go index d402a238f..272bf02e1 100644 --- a/services/migrations/gitea_downloader.go +++ b/services/migrations/gitea_downloader.go @@ -410,7 +410,6 @@ func (g *GiteaDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, err return nil, false, fmt.Errorf("error while listing issues: %w", err) } for _, issue := range issues { - labels := make([]*base.Label, 0, len(issue.Labels)) for i := range issue.Labels { labels = append(labels, g.convertGiteaLabel(issue.Labels[i])) diff --git a/services/migrations/gitlab.go b/services/migrations/gitlab.go index bbc44e958..065b687fa 100644 --- a/services/migrations/gitlab.go +++ b/services/migrations/gitlab.go @@ -421,7 +421,6 @@ func (g *GitlabDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, er return nil, false, fmt.Errorf("error while listing issues: %w", err) } for _, issue := range issues { - labels := make([]*base.Label, 0, len(issue.Labels)) for _, l := range issue.Labels { labels = append(labels, &base.Label{ diff --git a/services/mirror/mirror.go b/services/mirror/mirror.go index 72e545581..0270f8703 100644 --- a/services/mirror/mirror.go +++ b/services/mirror/mirror.go @@ -40,7 +40,7 @@ func Update(ctx context.Context, pullLimit, pushLimit int) error { } log.Trace("Doing: Update") - handler := func(idx int, bean any) error { + handler := func(bean any) error { var repo *repo_model.Repository var mirrorType SyncType var referenceID int64 @@ -91,7 +91,7 @@ func Update(ctx context.Context, pullLimit, pushLimit int) error { pullMirrorsRequested := 0 if pullLimit != 0 { if err := repo_model.MirrorsIterate(ctx, pullLimit, func(idx int, bean any) error { - if err := handler(idx, bean); err != nil { + if err := handler(bean); err != nil { return err } pullMirrorsRequested++ @@ -105,7 +105,7 @@ func Update(ctx context.Context, pullLimit, pushLimit int) error { pushMirrorsRequested := 0 if pushLimit != 0 { if err := repo_model.PushMirrorsIterate(ctx, pushLimit, func(idx int, bean any) error { - if err := handler(idx, bean); err != nil { + if err := handler(bean); err != nil { return err } pushMirrorsRequested++ diff --git a/services/mirror/mirror_pull.go b/services/mirror/mirror_pull.go index fa23986c5..9f7ffb29c 100644 --- a/services/mirror/mirror_pull.go +++ b/services/mirror/mirror_pull.go @@ -466,7 +466,7 @@ func SyncPullMirror(ctx context.Context, repoID int64) bool { log.Trace("SyncMirrors [repo: %-v]: %d branches updated", m.Repo, len(results)) if len(results) > 0 { - if ok := checkAndUpdateEmptyRepository(ctx, m, gitRepo, results); !ok { + if ok := checkAndUpdateEmptyRepository(ctx, m, results); !ok { log.Error("SyncMirrors [repo: %-v]: checkAndUpdateEmptyRepository: %v", m.Repo, err) return false } @@ -523,13 +523,13 @@ func SyncPullMirror(ctx context.Context, repoID int64) bool { theCommits.Commits = theCommits.Commits[:setting.UI.FeedMaxCommitNum] } - if newCommit, err := gitRepo.GetCommit(newCommitID); err != nil { + newCommit, err := gitRepo.GetCommit(newCommitID) + if err != nil { log.Error("SyncMirrors [repo: %-v]: unable to get commit %s: %v", m.Repo, newCommitID, err) continue - } else { - theCommits.HeadCommit = repo_module.CommitToPushCommit(newCommit) } + theCommits.HeadCommit = repo_module.CommitToPushCommit(newCommit) theCommits.CompareURL = m.Repo.ComposeCompareURL(oldCommitID, newCommitID) notify_service.SyncPushCommits(ctx, m.Repo.MustOwner(ctx), m.Repo, &repo_module.PushUpdateOptions{ @@ -557,7 +557,6 @@ func SyncPullMirror(ctx context.Context, repoID int64) bool { log.Error("SyncMirrors [repo: %-v]: unable to update repository 'updated_unix': %v", m.Repo, err) return false } - } log.Trace("SyncMirrors [repo: %-v]: Successfully updated", m.Repo) @@ -565,7 +564,7 @@ func SyncPullMirror(ctx context.Context, repoID int64) bool { return true } -func checkAndUpdateEmptyRepository(ctx context.Context, m *repo_model.Mirror, gitRepo *git.Repository, results []*mirrorSyncResult) bool { +func checkAndUpdateEmptyRepository(ctx context.Context, m *repo_model.Mirror, results []*mirrorSyncResult) bool { if !m.Repo.IsEmpty { return true } diff --git a/services/pull/merge.go b/services/pull/merge.go index 2989d77c6..1d6431ab6 100644 --- a/services/pull/merge.go +++ b/services/pull/merge.go @@ -237,9 +237,9 @@ func Merge(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.U if err = ref.Issue.LoadRepo(ctx); err != nil { return err } - close := ref.RefAction == references.XRefActionCloses - if close != ref.Issue.IsClosed { - if err = issue_service.ChangeStatus(ctx, ref.Issue, doer, pr.MergedCommitID, close); err != nil { + isClosed := ref.RefAction == references.XRefActionCloses + if isClosed != ref.Issue.IsClosed { + if err = issue_service.ChangeStatus(ctx, ref.Issue, doer, pr.MergedCommitID, isClosed); err != nil { // Allow ErrDependenciesLeft if !issues_model.IsErrDependenciesLeft(err) { return err diff --git a/services/pull/pull.go b/services/pull/pull.go index 720efdb0c..5fa426483 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -803,7 +803,6 @@ func GetSquashMergeCommitMessages(ctx context.Context, pr *issues_model.PullRequ if err != nil { log.Error("Unable to get commits between: %s %s Error: %v", pr.HeadBranch, pr.MergeBase, err) return "" - } if len(commits) == 0 { break diff --git a/services/pull/review.go b/services/pull/review.go index 6ad931b67..cff6f346a 100644 --- a/services/pull/review.go +++ b/services/pull/review.go @@ -6,6 +6,7 @@ package pull import ( "context" + "errors" "fmt" "io" "regexp" @@ -43,6 +44,9 @@ func (err ErrDismissRequestOnClosedPR) Unwrap() error { return util.ErrPermissionDenied } +// ErrSubmitReviewOnClosedPR represents an error when an user tries to submit an approve or reject review associated to a closed or merged PR. +var ErrSubmitReviewOnClosedPR = errors.New("can't submit review for a closed or merged PR") + // checkInvalidation checks if the line of code comment got changed by another commit. // If the line got changed the comment is going to be invalidated. func checkInvalidation(ctx context.Context, c *issues_model.Comment, doer *user_model.User, repo *git.Repository, branch string) error { @@ -293,6 +297,10 @@ func SubmitReview(ctx context.Context, doer *user_model.User, gitRepo *git.Repos if reviewType != issues_model.ReviewTypeApprove && reviewType != issues_model.ReviewTypeReject { stale = false } else { + if issue.IsClosed { + return nil, nil, ErrSubmitReviewOnClosedPR + } + headCommitID, err := gitRepo.GetRefCommitID(pr.GetGitRefName()) if err != nil { return nil, nil, err diff --git a/services/repository/adopt.go b/services/repository/adopt.go index b337eac38..31e3e581b 100644 --- a/services/repository/adopt.go +++ b/services/repository/adopt.go @@ -357,7 +357,6 @@ func ListUnadoptedRepositories(ctx context.Context, query string, opts *db.ListO return err } repoNamesToCheck = repoNamesToCheck[:0] - } return filepath.SkipDir }); err != nil { diff --git a/services/repository/contributors_graph.go b/services/repository/contributors_graph.go index 516e7f147..7241d3d65 100644 --- a/services/repository/contributors_graph.go +++ b/services/repository/contributors_graph.go @@ -194,7 +194,6 @@ func getExtendedCommitStats(repo *git.Repository, revision string /*, limit int Stats: &commitStats, } extendedCommitStats = append(extendedCommitStats, res) - } _ = stdoutReader.Close() return nil diff --git a/services/repository/files/update.go b/services/repository/files/update.go index e677949e8..81a61da5e 100644 --- a/services/repository/files/update.go +++ b/services/repository/files/update.go @@ -208,7 +208,6 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use return nil, fmt.Errorf("ConvertToSHA1: Invalid last commit ID: %w", err) } opts.LastCommitID = lastCommitID.String() - } for _, file := range opts.Files { @@ -360,7 +359,6 @@ func handleCheckErrors(file *ChangeRepoFile, commit *git.Commit, opts *ChangeRep Path: file.Options.treePath, } } - } } diff --git a/services/user/delete.go b/services/user/delete.go index e89099099..74dbc09b8 100644 --- a/services/user/delete.go +++ b/services/user/delete.go @@ -106,7 +106,6 @@ func deleteUser(ctx context.Context, u *user_model.User, purge bool) (err error) if purge || (setting.Service.UserDeleteWithCommentsMaxTime != 0 && u.CreatedUnix.AsTime().Add(setting.Service.UserDeleteWithCommentsMaxTime).After(time.Now())) { - // Delete Comments const batchSize = 50 for { diff --git a/services/user/update_test.go b/services/user/update_test.go index 7ed764b53..c2ff26a14 100644 --- a/services/user/update_test.go +++ b/services/user/update_test.go @@ -94,7 +94,7 @@ func TestUpdateAuth(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 28}) - copy := *user + userCopy := *user assert.NoError(t, UpdateAuth(db.DefaultContext, user, &UpdateAuthOptions{ LoginName: optional.Some("new-login"), @@ -106,8 +106,8 @@ func TestUpdateAuth(t *testing.T) { MustChangePassword: optional.Some(true), })) assert.True(t, user.MustChangePassword) - assert.NotEqual(t, copy.Passwd, user.Passwd) - assert.NotEqual(t, copy.Salt, user.Salt) + assert.NotEqual(t, userCopy.Passwd, user.Passwd) + assert.NotEqual(t, userCopy.Salt, user.Salt) assert.NoError(t, UpdateAuth(db.DefaultContext, user, &UpdateAuthOptions{ ProhibitLogin: optional.Some(true), diff --git a/services/webhook/discord.go b/services/webhook/discord.go index cb756688c..80f6cfb79 100644 --- a/services/webhook/discord.go +++ b/services/webhook/discord.go @@ -304,14 +304,12 @@ func (discordHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, func parseHookPullRequestEventType(event webhook_module.HookEventType) (string, error) { switch event { - case webhook_module.HookEventPullRequestReviewApproved: return "approved", nil case webhook_module.HookEventPullRequestReviewRejected: return "rejected", nil case webhook_module.HookEventPullRequestReviewComment: return "comment", nil - default: return "", errors.New("unknown event type") } diff --git a/services/webhook/matrix.go b/services/webhook/matrix.go index 24d6cc6d9..06176e8dd 100644 --- a/services/webhook/matrix.go +++ b/services/webhook/matrix.go @@ -217,7 +217,6 @@ func (m matrixConvertor) Push(p *api.PushPayload) (MatrixPayload, error) { if i < len(p.Commits)-1 { text += "
" } - } return m.newPayload(text, p.Commits...) diff --git a/templates/admin/dashboard.tmpl b/templates/admin/dashboard.tmpl index bfd2ee667..9b89b8335 100644 --- a/templates/admin/dashboard.tmpl +++ b/templates/admin/dashboard.tmpl @@ -76,7 +76,8 @@ {{ctx.Locale.Tr "admin.dashboard.system_status"}} {{/* TODO: make these stats work in multi-server deployments, likely needs per-server stats in DB */}} -
+
+
{{template "admin/system_status" .}}
diff --git a/templates/admin/layout_head.tmpl b/templates/admin/layout_head.tmpl index 8ba47f2f1..7cc6624d5 100644 --- a/templates/admin/layout_head.tmpl +++ b/templates/admin/layout_head.tmpl @@ -1,6 +1,6 @@ {{template "base/head" .ctxData}}
-
+
{{template "admin/navbar" .ctxData}}
{{template "base/alert" .ctxData}} diff --git a/templates/org/team/members.tmpl b/templates/org/team/members.tmpl index 5719328a2..7e9a59a6b 100644 --- a/templates/org/team/members.tmpl +++ b/templates/org/team/members.tmpl @@ -46,7 +46,7 @@
{{else}}
- {{ctx.Locale.Tr "org.teams.members.none"}} + {{ctx.Locale.Tr "org.teams.members.none"}}
{{end}}
diff --git a/templates/org/team/repositories.tmpl b/templates/org/team/repositories.tmpl index 98b4854eb..f5d68ce41 100644 --- a/templates/org/team/repositories.tmpl +++ b/templates/org/team/repositories.tmpl @@ -48,7 +48,7 @@
{{else}}
- {{ctx.Locale.Tr "org.teams.repos.none"}} + {{ctx.Locale.Tr "org.teams.repos.none"}}
{{end}}
diff --git a/templates/org/team/sidebar.tmpl b/templates/org/team/sidebar.tmpl index 9311a46e3..c9f80259e 100644 --- a/templates/org/team/sidebar.tmpl +++ b/templates/org/team/sidebar.tmpl @@ -22,7 +22,7 @@ {{if .Team.Description}} {{.Team.Description}} {{else}} - {{ctx.Locale.Tr "org.teams.no_desc"}} + {{ctx.Locale.Tr "org.teams.no_desc"}} {{end}} {{if eq .Team.LowerName "owners"}} diff --git a/templates/projects/list.tmpl b/templates/projects/list.tmpl index 0908f5648..b892cff99 100644 --- a/templates/projects/list.tmpl +++ b/templates/projects/list.tmpl @@ -41,9 +41,9 @@
{{range .Projects}}
  • -

    +

    {{svg .IconName 16}} - {{.Title}} + {{.Title}}

    diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl index f9b85360e..3e000660e 100644 --- a/templates/projects/view.tmpl +++ b/templates/projects/view.tmpl @@ -1,8 +1,8 @@ {{$canWriteProject := and .CanWriteProjects (or (not .Repository) (not .Repository.IsArchived))}} -
    -
    -

    {{.Project.Title}}

    +
    +
    +

    {{.Project.Title}}

    {{if $canWriteProject}}
    -