diff --git a/.eslintrc.yaml b/.eslintrc.yaml index 04eb02363452d..0dd9a6687d8bb 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -16,10 +16,10 @@ parserOptions: parser: "@typescript-eslint/parser" # for vue plugin - https://eslint.vuejs.org/user-guide/#how-to-use-a-custom-parser settings: - import/extensions: [".js", ".ts"] - import/parsers: + import-x/extensions: [".js", ".ts"] + import-x/parsers: "@typescript-eslint/parser": [".js", ".ts"] - import/resolver: + import-x/resolver: typescript: true plugins: @@ -28,7 +28,7 @@ plugins: - "@typescript-eslint/eslint-plugin" - eslint-plugin-array-func - eslint-plugin-github - - eslint-plugin-i + - eslint-plugin-import-x - eslint-plugin-no-jquery - eslint-plugin-no-use-extend-native - eslint-plugin-regexp @@ -58,15 +58,15 @@ overrides: no-restricted-globals: [2, addEventListener, blur, close, closed, confirm, defaultStatus, defaultstatus, error, event, external, find, focus, frameElement, frames, history, innerHeight, innerWidth, isFinite, isNaN, length, locationbar, menubar, moveBy, moveTo, name, onblur, onerror, onfocus, onload, onresize, onunload, open, opener, opera, outerHeight, outerWidth, pageXOffset, pageYOffset, parent, print, removeEventListener, resizeBy, resizeTo, screen, screenLeft, screenTop, screenX, screenY, scroll, scrollbars, scrollBy, scrollTo, scrollX, scrollY, status, statusbar, stop, toolbar, top] - files: ["*.config.*"] rules: - i/no-unused-modules: [0] + import-x/no-unused-modules: [0] - files: ["**/*.d.ts"] rules: - i/no-unused-modules: [0] + import-x/no-unused-modules: [0] "@typescript-eslint/consistent-type-definitions": [0] "@typescript-eslint/consistent-type-imports": [0] - files: ["web_src/js/types.ts"] rules: - i/no-unused-modules: [0] + import-x/no-unused-modules: [0] - files: ["**/*.test.*", "web_src/js/test/setup.ts"] env: vitest-globals/env: true @@ -394,49 +394,49 @@ rules: id-blacklist: [0] id-length: [0] id-match: [0] - i/consistent-type-specifier-style: [0] - i/default: [0] - i/dynamic-import-chunkname: [0] - i/export: [2] - i/exports-last: [0] - i/extensions: [2, always, {ignorePackages: true}] - i/first: [2] - i/group-exports: [0] - i/max-dependencies: [0] - i/named: [2] - i/namespace: [0] - i/newline-after-import: [0] - i/no-absolute-path: [0] - i/no-amd: [2] - i/no-anonymous-default-export: [0] - i/no-commonjs: [2] - i/no-cycle: [2, {ignoreExternal: true, maxDepth: 1}] - i/no-default-export: [0] - i/no-deprecated: [0] - i/no-dynamic-require: [0] - i/no-empty-named-blocks: [2] - i/no-extraneous-dependencies: [2] - i/no-import-module-exports: [0] - i/no-internal-modules: [0] - i/no-mutable-exports: [0] - i/no-named-as-default-member: [0] - i/no-named-as-default: [0] - i/no-named-default: [0] - i/no-named-export: [0] - i/no-namespace: [0] - i/no-nodejs-modules: [0] - i/no-relative-packages: [0] - i/no-relative-parent-imports: [0] - i/no-restricted-paths: [0] - i/no-self-import: [2] - i/no-unassigned-import: [0] - i/no-unresolved: [2, {commonjs: true, ignore: ["\\?.+$"]}] - i/no-unused-modules: [2, {unusedExports: true}] - i/no-useless-path-segments: [2, {commonjs: true}] - i/no-webpack-loader-syntax: [2] - i/order: [0] - i/prefer-default-export: [0] - i/unambiguous: [0] + import-x/consistent-type-specifier-style: [0] + import-x/default: [0] + import-x/dynamic-import-chunkname: [0] + import-x/export: [2] + import-x/exports-last: [0] + import-x/extensions: [2, always, {ignorePackages: true}] + import-x/first: [2] + import-x/group-exports: [0] + import-x/max-dependencies: [0] + import-x/named: [2] + import-x/namespace: [0] + import-x/newline-after-import: [0] + import-x/no-absolute-path: [0] + import-x/no-amd: [2] + import-x/no-anonymous-default-export: [0] + import-x/no-commonjs: [2] + import-x/no-cycle: [2, {ignoreExternal: true, maxDepth: 1}] + import-x/no-default-export: [0] + import-x/no-deprecated: [0] + import-x/no-dynamic-require: [0] + import-x/no-empty-named-blocks: [2] + import-x/no-extraneous-dependencies: [2] + import-x/no-import-module-exports: [0] + import-x/no-internal-modules: [0] + import-x/no-mutable-exports: [0] + import-x/no-named-as-default-member: [0] + import-x/no-named-as-default: [0] + import-x/no-named-default: [0] + import-x/no-named-export: [0] + import-x/no-namespace: [0] + import-x/no-nodejs-modules: [0] + import-x/no-relative-packages: [0] + import-x/no-relative-parent-imports: [0] + import-x/no-restricted-paths: [0] + import-x/no-self-import: [2] + import-x/no-unassigned-import: [0] + import-x/no-unresolved: [2, {commonjs: true, ignore: ["\\?.+$"]}] + import-x/no-unused-modules: [2, {unusedExports: true}] + import-x/no-useless-path-segments: [2, {commonjs: true}] + import-x/no-webpack-loader-syntax: [2] + import-x/order: [0] + import-x/prefer-default-export: [0] + import-x/unambiguous: [0] init-declarations: [0] line-comment-position: [0] logical-assignment-operators: [0] diff --git a/.github/workflows/release-nightly.yml b/.github/workflows/release-nightly.yml index 6e1b6e0758417..2264c9e822d1c 100644 --- a/.github/workflows/release-nightly.yml +++ b/.github/workflows/release-nightly.yml @@ -10,7 +10,7 @@ concurrency: jobs: nightly-binary: - runs-on: nscloud + runs-on: namespace-profile-gitea-release-binary steps: - uses: actions/checkout@v4 # fetch all commits instead of only the last as some branches are long lived and could have many between versions @@ -58,7 +58,7 @@ jobs: run: | aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress nightly-docker-rootful: - runs-on: ubuntu-latest + runs-on: namespace-profile-gitea-release-docker steps: - uses: actions/checkout@v4 # fetch all commits instead of only the last as some branches are long lived and could have many between versions @@ -95,7 +95,7 @@ jobs: push: true tags: gitea/gitea:${{ steps.clean_name.outputs.branch }} nightly-docker-rootless: - runs-on: ubuntu-latest + runs-on: namespace-profile-gitea-release-docker steps: - uses: actions/checkout@v4 # fetch all commits instead of only the last as some branches are long lived and could have many between versions diff --git a/.github/workflows/release-tag-rc.yml b/.github/workflows/release-tag-rc.yml index 41037df29cbc0..a406602dc0a74 100644 --- a/.github/workflows/release-tag-rc.yml +++ b/.github/workflows/release-tag-rc.yml @@ -11,7 +11,7 @@ concurrency: jobs: binary: - runs-on: nscloud + runs-on: namespace-profile-gitea-release-binary steps: - uses: actions/checkout@v4 # fetch all commits instead of only the last as some branches are long lived and could have many between versions @@ -68,7 +68,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} docker-rootful: - runs-on: ubuntu-latest + runs-on: namespace-profile-gitea-release-docker steps: - uses: actions/checkout@v4 # fetch all commits instead of only the last as some branches are long lived and could have many between versions @@ -99,7 +99,7 @@ jobs: tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} docker-rootless: - runs-on: ubuntu-latest + runs-on: namespace-profile-gitea-release-docker steps: - uses: actions/checkout@v4 # fetch all commits instead of only the last as some branches are long lived and could have many between versions diff --git a/.github/workflows/release-tag-version.yml b/.github/workflows/release-tag-version.yml index a23e6982000ae..f67b76f40873b 100644 --- a/.github/workflows/release-tag-version.yml +++ b/.github/workflows/release-tag-version.yml @@ -13,7 +13,7 @@ concurrency: jobs: binary: - runs-on: nscloud + runs-on: namespace-profile-gitea-release-binary steps: - uses: actions/checkout@v4 # fetch all commits instead of only the last as some branches are long lived and could have many between versions @@ -70,7 +70,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} docker-rootful: - runs-on: ubuntu-latest + runs-on: namespace-profile-gitea-release-docker steps: - uses: actions/checkout@v4 # fetch all commits instead of only the last as some branches are long lived and could have many between versions @@ -105,7 +105,7 @@ jobs: tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} docker-rootless: - runs-on: ubuntu-latest + runs-on: namespace-profile-gitea-release-docker steps: - uses: actions/checkout@v4 # fetch all commits instead of only the last as some branches are long lived and could have many between versions diff --git a/models/db/context_committer_test.go b/models/db/context_committer_test.go index 38e91f22edb41..849c5dea411d1 100644 --- a/models/db/context_committer_test.go +++ b/models/db/context_committer_test.go @@ -4,7 +4,7 @@ package db // it's not db_test, because this file is for testing the private type halfCommitter import ( - "fmt" + "errors" "testing" "github.com/stretchr/testify/assert" @@ -80,7 +80,7 @@ func Test_halfCommitter(t *testing.T) { testWithCommitter(mockCommitter, func(committer Committer) error { defer committer.Close() if true { - return fmt.Errorf("error") + return errors.New("error") } return committer.Commit() }) @@ -94,7 +94,7 @@ func Test_halfCommitter(t *testing.T) { testWithCommitter(mockCommitter, func(committer Committer) error { committer.Close() committer.Commit() - return fmt.Errorf("error") + return errors.New("error") }) mockCommitter.Assert(t) diff --git a/modules/git/commit.go b/modules/git/commit.go index 010b56948ef60..0ed268e3469cc 100644 --- a/modules/git/commit.go +++ b/modules/git/commit.go @@ -474,3 +474,17 @@ func (c *Commit) GetRepositoryDefaultPublicGPGKey(forceUpdate bool) (*GPGSetting } return c.repo.GetDefaultPublicGPGKey(forceUpdate) } + +func IsStringLikelyCommitID(objFmt ObjectFormat, s string, minLength ...int) bool { + minLen := util.OptionalArg(minLength, objFmt.FullLength()) + if len(s) < minLen || len(s) > objFmt.FullLength() { + return false + } + for _, c := range s { + isHex := (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') + if !isHex { + return false + } + } + return true +} diff --git a/modules/git/ref.go b/modules/git/ref.go index 2db630e2ea9ad..aab4c5d77d75c 100644 --- a/modules/git/ref.go +++ b/modules/git/ref.go @@ -142,7 +142,6 @@ func (ref RefName) RemoteName() string { // ShortName returns the short name of the reference name func (ref RefName) ShortName() string { - refName := string(ref) if ref.IsBranch() { return ref.BranchName() } @@ -158,8 +157,7 @@ func (ref RefName) ShortName() string { if ref.IsFor() { return ref.ForBranchName() } - - return refName + return string(ref) // usually it is a commit ID } // RefGroup returns the group type of the reference diff --git a/modules/git/repo_ref.go b/modules/git/repo_ref.go index 8eaa17cb04182..850ec65502940 100644 --- a/modules/git/repo_ref.go +++ b/modules/git/repo_ref.go @@ -61,3 +61,31 @@ func parseTags(refs []string) []string { } return results } + +// UnstableGuessRefByShortName does the best guess to see whether a "short name" provided by user is a branch, tag or commit. +// It could guess wrongly if the input is already ambiguous. For example: +// * "refs/heads/the-name" vs "refs/heads/refs/heads/the-name" +// * "refs/tags/1234567890" vs commit "1234567890" +// In most cases, it SHOULD AVOID using this function, unless there is an irresistible reason (eg: make API friendly to end users) +// If the function is used, the caller SHOULD CHECK the ref type carefully. +func (repo *Repository) UnstableGuessRefByShortName(shortName string) RefName { + if repo.IsBranchExist(shortName) { + return RefNameFromBranch(shortName) + } + if repo.IsTagExist(shortName) { + return RefNameFromTag(shortName) + } + if strings.HasPrefix(shortName, "refs/") { + if repo.IsReferenceExist(shortName) { + return RefName(shortName) + } + } + commit, err := repo.GetCommit(shortName) + if err == nil { + commitIDString := commit.ID.String() + if strings.HasPrefix(commitIDString, shortName) { + return RefName(commitIDString) + } + } + return "" +} diff --git a/modules/globallock/globallock_test.go b/modules/globallock/globallock_test.go index 88a555c86f3bd..f14c7d656b64b 100644 --- a/modules/globallock/globallock_test.go +++ b/modules/globallock/globallock_test.go @@ -64,7 +64,7 @@ func TestLockAndDo(t *testing.T) { } func testLockAndDo(t *testing.T) { - const concurrency = 1000 + const concurrency = 50 ctx := context.Background() count := 0 diff --git a/modules/indexer/issues/bleve/bleve.go b/modules/indexer/issues/bleve/bleve.go index 7ef370e89c5d2..bf51bd6c14857 100644 --- a/modules/indexer/issues/bleve/bleve.go +++ b/modules/indexer/issues/bleve/bleve.go @@ -23,7 +23,7 @@ import ( const ( issueIndexerAnalyzer = "issueIndexer" issueIndexerDocType = "issueIndexerDocType" - issueIndexerLatestVersion = 4 + issueIndexerLatestVersion = 5 ) const unicodeNormalizeName = "unicodeNormalize" @@ -75,6 +75,7 @@ func generateIssueIndexMapping() (mapping.IndexMapping, error) { docMapping.AddFieldMappingsAt("is_pull", boolFieldMapping) docMapping.AddFieldMappingsAt("is_closed", boolFieldMapping) + docMapping.AddFieldMappingsAt("is_archived", boolFieldMapping) docMapping.AddFieldMappingsAt("label_ids", numberFieldMapping) docMapping.AddFieldMappingsAt("no_label", boolFieldMapping) docMapping.AddFieldMappingsAt("milestone_id", numberFieldMapping) @@ -185,6 +186,9 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( if options.IsClosed.Has() { queries = append(queries, inner_bleve.BoolFieldQuery(options.IsClosed.Value(), "is_closed")) } + if options.IsArchived.Has() { + queries = append(queries, inner_bleve.BoolFieldQuery(options.IsArchived.Value(), "is_archived")) + } if options.NoLabelOnly { queries = append(queries, inner_bleve.BoolFieldQuery(true, "no_label")) diff --git a/modules/indexer/issues/db/options.go b/modules/indexer/issues/db/options.go index 98b097f8713d3..42834f6e8863b 100644 --- a/modules/indexer/issues/db/options.go +++ b/modules/indexer/issues/db/options.go @@ -72,7 +72,7 @@ func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_m UpdatedAfterUnix: options.UpdatedAfterUnix.Value(), UpdatedBeforeUnix: options.UpdatedBeforeUnix.Value(), PriorityRepoID: 0, - IsArchived: optional.None[bool](), + IsArchived: options.IsArchived, Org: nil, Team: nil, User: nil, diff --git a/modules/indexer/issues/dboptions.go b/modules/indexer/issues/dboptions.go index 1a0f241e61590..4f6ad96d222d7 100644 --- a/modules/indexer/issues/dboptions.go +++ b/modules/indexer/issues/dboptions.go @@ -11,11 +11,12 @@ import ( func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOptions { searchOpt := &SearchOptions{ - Keyword: keyword, - RepoIDs: opts.RepoIDs, - AllPublic: opts.AllPublic, - IsPull: opts.IsPull, - IsClosed: opts.IsClosed, + Keyword: keyword, + RepoIDs: opts.RepoIDs, + AllPublic: opts.AllPublic, + IsPull: opts.IsPull, + IsClosed: opts.IsClosed, + IsArchived: opts.IsArchived, } if len(opts.LabelIDs) == 1 && opts.LabelIDs[0] == 0 { diff --git a/modules/indexer/issues/elasticsearch/elasticsearch.go b/modules/indexer/issues/elasticsearch/elasticsearch.go index 6f705150097f6..4c293f3f2a9c8 100644 --- a/modules/indexer/issues/elasticsearch/elasticsearch.go +++ b/modules/indexer/issues/elasticsearch/elasticsearch.go @@ -18,7 +18,7 @@ import ( ) const ( - issueIndexerLatestVersion = 1 + issueIndexerLatestVersion = 2 // multi-match-types, currently only 2 types are used // Reference: https://www.elastic.co/guide/en/elasticsearch/reference/7.0/query-dsl-multi-match-query.html#multi-match-types esMultiMatchTypeBestFields = "best_fields" @@ -58,6 +58,7 @@ const ( "is_pull": { "type": "boolean", "index": true }, "is_closed": { "type": "boolean", "index": true }, + "is_archived": { "type": "boolean", "index": true }, "label_ids": { "type": "integer", "index": true }, "no_label": { "type": "boolean", "index": true }, "milestone_id": { "type": "integer", "index": true }, @@ -168,6 +169,9 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( if options.IsClosed.Has() { query.Must(elastic.NewTermQuery("is_closed", options.IsClosed.Value())) } + if options.IsArchived.Has() { + query.Must(elastic.NewTermQuery("is_archived", options.IsArchived.Value())) + } if options.NoLabelOnly { query.Must(elastic.NewTermQuery("no_label", true)) diff --git a/modules/indexer/issues/indexer_test.go b/modules/indexer/issues/indexer_test.go index 7c3ba75bb072f..06a6a46c234fc 100644 --- a/modules/indexer/issues/indexer_test.go +++ b/modules/indexer/issues/indexer_test.go @@ -37,6 +37,7 @@ func TestDBSearchIssues(t *testing.T) { t.Run("search issues by ID", searchIssueByID) t.Run("search issues is pr", searchIssueIsPull) t.Run("search issues is closed", searchIssueIsClosed) + t.Run("search issues is archived", searchIssueIsArchived) t.Run("search issues by milestone", searchIssueByMilestoneID) t.Run("search issues by label", searchIssueByLabelID) t.Run("search issues by time", searchIssueByTime) @@ -298,6 +299,33 @@ func searchIssueIsClosed(t *testing.T) { } } +func searchIssueIsArchived(t *testing.T) { + tests := []struct { + opts SearchOptions + expectedIDs []int64 + }{ + { + SearchOptions{ + IsArchived: optional.Some(false), + }, + []int64{22, 21, 17, 16, 15, 13, 12, 11, 20, 6, 5, 19, 18, 10, 7, 4, 9, 8, 3, 2, 1}, + }, + { + SearchOptions{ + IsArchived: optional.Some(true), + }, + []int64{14}, + }, + } + for _, test := range tests { + issueIDs, _, err := SearchIssues(context.TODO(), &test.opts) + if !assert.NoError(t, err) { + return + } + assert.Equal(t, test.expectedIDs, issueIDs) + } +} + func searchIssueByMilestoneID(t *testing.T) { tests := []struct { opts SearchOptions diff --git a/modules/indexer/issues/internal/model.go b/modules/indexer/issues/internal/model.go index a43c6be005975..09dcbf4804c88 100644 --- a/modules/indexer/issues/internal/model.go +++ b/modules/indexer/issues/internal/model.go @@ -25,6 +25,7 @@ type IndexerData struct { // Fields used for filtering IsPull bool `json:"is_pull"` IsClosed bool `json:"is_closed"` + IsArchived bool `json:"is_archived"` LabelIDs []int64 `json:"label_ids"` NoLabel bool `json:"no_label"` // True if LabelIDs is empty MilestoneID int64 `json:"milestone_id"` @@ -81,8 +82,9 @@ type SearchOptions struct { RepoIDs []int64 // repository IDs which the issues belong to AllPublic bool // if include all public repositories - IsPull optional.Option[bool] // if the issues is a pull request - IsClosed optional.Option[bool] // if the issues is closed + IsPull optional.Option[bool] // if the issues is a pull request + IsClosed optional.Option[bool] // if the issues is closed + IsArchived optional.Option[bool] // if the repo is archived IncludedLabelIDs []int64 // labels the issues have ExcludedLabelIDs []int64 // labels the issues don't have diff --git a/modules/indexer/issues/meilisearch/meilisearch.go b/modules/indexer/issues/meilisearch/meilisearch.go index 9332319339215..1066e96272575 100644 --- a/modules/indexer/issues/meilisearch/meilisearch.go +++ b/modules/indexer/issues/meilisearch/meilisearch.go @@ -18,7 +18,7 @@ import ( ) const ( - issueIndexerLatestVersion = 3 + issueIndexerLatestVersion = 4 // TODO: make this configurable if necessary maxTotalHits = 10000 @@ -61,6 +61,7 @@ func NewIndexer(url, apiKey, indexerName string) *Indexer { "is_public", "is_pull", "is_closed", + "is_archived", "label_ids", "no_label", "milestone_id", @@ -145,6 +146,9 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( if options.IsClosed.Has() { query.And(inner_meilisearch.NewFilterEq("is_closed", options.IsClosed.Value())) } + if options.IsArchived.Has() { + query.And(inner_meilisearch.NewFilterEq("is_archived", options.IsArchived.Value())) + } if options.NoLabelOnly { query.And(inner_meilisearch.NewFilterEq("no_label", true)) diff --git a/modules/indexer/issues/util.go b/modules/indexer/issues/util.go index e752ae6f2436c..deb19adc49dd9 100644 --- a/modules/indexer/issues/util.go +++ b/modules/indexer/issues/util.go @@ -101,6 +101,7 @@ func getIssueIndexerData(ctx context.Context, issueID int64) (*internal.IndexerD Comments: comments, IsPull: issue.IsPull, IsClosed: issue.IsClosed, + IsArchived: issue.Repo.IsArchived, LabelIDs: labels, NoLabel: len(labels) == 0, MilestoneID: issue.MilestoneID, diff --git a/modules/structs/repo.go b/modules/structs/repo.go index 832ffa8bcc958..fb784bd8b37f8 100644 --- a/modules/structs/repo.go +++ b/modules/structs/repo.go @@ -278,6 +278,16 @@ type CreateBranchRepoOption struct { OldRefName string `json:"old_ref_name" binding:"GitRefName;MaxSize(100)"` } +// UpdateBranchRepoOption options when updating a branch in a repository +// swagger:model +type UpdateBranchRepoOption struct { + // New branch name + // + // required: true + // unique: true + Name string `json:"name" binding:"Required;GitRefName;MaxSize(100)"` +} + // TransferRepoOption options when transfer a repository's ownership // swagger:model type TransferRepoOption struct { diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini index f0abce0d4bfa8..776d2bdc2bf2c 100644 --- a/options/locale/locale_pt-PT.ini +++ b/options/locale/locale_pt-PT.ini @@ -47,7 +47,7 @@ webauthn_error_unknown=Ocorreu um erro desconhecido. Tente novamente, por favor. webauthn_error_insecure=`WebAuthn apenas suporta conexões seguras. Para testar sobre HTTP, pode usar a origem "localhost" ou "127.0.0.1"` webauthn_error_unable_to_process=O servidor não conseguiu processar o seu pedido. webauthn_error_duplicated=A chave de segurança não é permitida neste pedido. Certifique-se de que a chave não está já registada. -webauthn_error_empty=Você tem que definir um nome para esta chave. +webauthn_error_empty=Tem de definir um nome para esta chave. webauthn_error_timeout=O tempo limite foi atingido antes que a sua chave pudesse ser lida. Recarregue esta página e tente novamente. webauthn_reload=Recarregar @@ -1109,6 +1109,7 @@ delete_preexisting_success=Eliminados os ficheiros não adoptados em %s blame_prior=Ver a responsabilização anterior a esta modificação blame.ignore_revs=Ignorando as revisões em .git-blame-ignore-revs. Clique aqui para contornar e ver a vista normal de responsabilização. blame.ignore_revs.failed=Falhou ao ignorar as revisões em .git-blame-ignore-revs. +user_search_tooltip=Mostra um máximo de 30 utilizadores tree_path_not_found_commit=A localização %[1]s não existe no cometimento %[2]s tree_path_not_found_branch=A localização %[1]s não existe no ramo %[2]s @@ -1527,6 +1528,8 @@ issues.filter_assignee=Encarregado issues.filter_assginee_no_select=Todos os encarregados issues.filter_assginee_no_assignee=Sem encarregado issues.filter_poster=Autor(a) +issues.filter_user_placeholder=Procurar utilizadores +issues.filter_user_no_select=Todos os utilizadores issues.filter_type=Tipo issues.filter_type.all_issues=Todas as questões issues.filter_type.assigned_to_you=Atribuídas a si diff --git a/package-lock.json b/package-lock.json index e3f7a0116f763..53bd5bc4f1f40 100644 --- a/package-lock.json +++ b/package-lock.json @@ -87,7 +87,7 @@ "eslint-import-resolver-typescript": "3.7.0", "eslint-plugin-array-func": "4.0.0", "eslint-plugin-github": "5.1.3", - "eslint-plugin-i": "2.29.1", + "eslint-plugin-import-x": "4.5.0", "eslint-plugin-no-jquery": "3.1.0", "eslint-plugin-no-use-extend-native": "0.5.0", "eslint-plugin-playwright": "2.1.0", @@ -8385,56 +8385,6 @@ "node": "*" } }, - "node_modules/eslint-plugin-i": { - "version": "2.29.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-i/-/eslint-plugin-i-2.29.1.tgz", - "integrity": "sha512-ORizX37MelIWLbMyqI7hi8VJMf7A0CskMmYkB+lkCX3aF4pkGV7kwx5bSEb4qx7Yce2rAf9s34HqDRPjGRZPNQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.4", - "doctrine": "^3.0.0", - "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.8.0", - "get-tsconfig": "^4.7.2", - "is-glob": "^4.0.3", - "minimatch": "^3.1.2", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://opencollective.com/unts" - }, - "peerDependencies": { - "eslint": "^7.2.0 || ^8" - } - }, - "node_modules/eslint-plugin-i/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint-plugin-i/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/eslint-plugin-i18n-text": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/eslint-plugin-i18n-text/-/eslint-plugin-i18n-text-1.0.1.tgz", @@ -8479,6 +8429,48 @@ "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, + "node_modules/eslint-plugin-import-x": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import-x/-/eslint-plugin-import-x-4.5.0.tgz", + "integrity": "sha512-l0OTfnPF8RwmSXfjT75N8d6ZYLVrVYWpaGlgvVkVqFERCI5SyBfDP7QEMr3kt0zWi2sOa9EQ47clbdFsHkF83Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "^8.1.0", + "@typescript-eslint/utils": "^8.1.0", + "debug": "^4.3.4", + "doctrine": "^3.0.0", + "eslint-import-resolver-node": "^0.3.9", + "get-tsconfig": "^4.7.3", + "is-glob": "^4.0.3", + "minimatch": "^9.0.3", + "semver": "^7.6.3", + "stable-hash": "^0.0.4", + "tslib": "^2.6.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-import-x/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/eslint-plugin-import/node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", diff --git a/package.json b/package.json index d30aedc54f59e..3a81e64822833 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "eslint-import-resolver-typescript": "3.7.0", "eslint-plugin-array-func": "4.0.0", "eslint-plugin-github": "5.1.3", - "eslint-plugin-i": "2.29.1", + "eslint-plugin-import-x": "4.5.0", "eslint-plugin-no-jquery": "3.1.0", "eslint-plugin-no-use-extend-native": "0.5.0", "eslint-plugin-playwright": "2.1.0", diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index f28ee980e1043..96365e7c14dfc 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1195,6 +1195,7 @@ func Routes() *web.Router { m.Get("/*", repo.GetBranch) m.Delete("/*", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, repo.DeleteBranch) m.Post("", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, bind(api.CreateBranchRepoOption{}), repo.CreateBranch) + m.Patch("/*", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, bind(api.UpdateBranchRepoOption{}), repo.UpdateBranch) }, context.ReferencesGitRepo(), reqRepoReader(unit.TypeCode)) m.Group("/branch_protections", func() { m.Get("", repo.ListBranchProtections) diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go index 53f3b4648a592..946203e97ec0a 100644 --- a/routers/api/v1/repo/branch.go +++ b/routers/api/v1/repo/branch.go @@ -386,6 +386,77 @@ func ListBranches(ctx *context.APIContext) { ctx.JSON(http.StatusOK, apiBranches) } +// UpdateBranch updates a repository's branch. +func UpdateBranch(ctx *context.APIContext) { + // swagger:operation PATCH /repos/{owner}/{repo}/branches/{branch} repository repoUpdateBranch + // --- + // summary: Update a branch + // consumes: + // - application/json + // 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 + // - name: branch + // in: path + // description: name of the branch + // type: string + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/UpdateBranchRepoOption" + // responses: + // "204": + // "$ref": "#/responses/empty" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + // "422": + // "$ref": "#/responses/validationError" + + opt := web.GetForm(ctx).(*api.UpdateBranchRepoOption) + + oldName := ctx.PathParam("*") + repo := ctx.Repo.Repository + + if repo.IsEmpty { + ctx.Error(http.StatusNotFound, "", "Git Repository is empty.") + return + } + + if repo.IsMirror { + ctx.Error(http.StatusForbidden, "", "Git Repository is a mirror.") + return + } + + msg, err := repo_service.RenameBranch(ctx, repo, ctx.Doer, ctx.Repo.GitRepo, oldName, opt.Name) + if err != nil { + ctx.Error(http.StatusInternalServerError, "RenameBranch", err) + return + } + if msg == "target_exist" { + ctx.Error(http.StatusUnprocessableEntity, "", "Cannot rename a branch using the same name or rename to a branch that already exists.") + return + } + if msg == "from_not_exist" { + ctx.Error(http.StatusNotFound, "", "Branch doesn't exist.") + return + } + + ctx.Status(http.StatusNoContent) +} + // GetBranchProtection gets a branch protection func GetBranchProtection(ctx *context.APIContext) { // swagger:operation GET /repos/{owner}/{repo}/branch_protections/{name} repository repoGetBranchProtection diff --git a/routers/api/v1/repo/compare.go b/routers/api/v1/repo/compare.go index 38e5330b3acb8..1678bc033c639 100644 --- a/routers/api/v1/repo/compare.go +++ b/routers/api/v1/repo/compare.go @@ -64,22 +64,19 @@ func CompareDiff(ctx *context.APIContext) { } } - _, headGitRepo, ci, _, _ := parseCompareInfo(ctx, api.CreatePullRequestOption{ - Base: infos[0], - Head: infos[1], - }) + compareResult, closer := parseCompareInfo(ctx, api.CreatePullRequestOption{Base: infos[0], Head: infos[1]}) if ctx.Written() { return } - defer headGitRepo.Close() + defer closer() verification := ctx.FormString("verification") == "" || ctx.FormBool("verification") files := ctx.FormString("files") == "" || ctx.FormBool("files") - apiCommits := make([]*api.Commit, 0, len(ci.Commits)) + apiCommits := make([]*api.Commit, 0, len(compareResult.compareInfo.Commits)) userCache := make(map[string]*user_model.User) - for i := 0; i < len(ci.Commits); i++ { - apiCommit, err := convert.ToCommit(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, ci.Commits[i], userCache, + for i := 0; i < len(compareResult.compareInfo.Commits); i++ { + apiCommit, err := convert.ToCommit(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, compareResult.compareInfo.Commits[i], userCache, convert.ToCommitOptions{ Stat: true, Verification: verification, @@ -93,7 +90,7 @@ func CompareDiff(ctx *context.APIContext) { } ctx.JSON(http.StatusOK, &api.Compare{ - TotalCommits: len(ci.Commits), + TotalCommits: len(compareResult.compareInfo.Commits), Commits: apiCommits, }) } diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index 86b204f51e250..6f4f3efaa10fa 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -389,8 +389,7 @@ func CreatePullRequest(ctx *context.APIContext) { form := *web.GetForm(ctx).(*api.CreatePullRequestOption) if form.Head == form.Base { - ctx.Error(http.StatusUnprocessableEntity, "BaseHeadSame", - "Invalid PullRequest: There are no changes between the head and the base") + ctx.Error(http.StatusUnprocessableEntity, "BaseHeadSame", "Invalid PullRequest: There are no changes between the head and the base") return } @@ -401,14 +400,22 @@ func CreatePullRequest(ctx *context.APIContext) { ) // Get repo/branch information - headRepo, headGitRepo, compareInfo, baseBranch, headBranch := parseCompareInfo(ctx, form) + compareResult, closer := parseCompareInfo(ctx, form) if ctx.Written() { return } - defer headGitRepo.Close() + defer closer() + + if !compareResult.baseRef.IsBranch() || !compareResult.headRef.IsBranch() { + ctx.Error(http.StatusUnprocessableEntity, "BaseHeadInvalidRefType", "Invalid PullRequest: base and head must be branches") + return + } // Check if another PR exists with the same targets - existingPr, err := issues_model.GetUnmergedPullRequest(ctx, headRepo.ID, ctx.Repo.Repository.ID, headBranch, baseBranch, issues_model.PullRequestFlowGithub) + existingPr, err := issues_model.GetUnmergedPullRequest(ctx, compareResult.headRepo.ID, ctx.Repo.Repository.ID, + compareResult.headRef.ShortName(), compareResult.baseRef.ShortName(), + issues_model.PullRequestFlowGithub, + ) if err != nil { if !issues_model.IsErrPullRequestNotExist(err) { ctx.Error(http.StatusInternalServerError, "GetUnmergedPullRequest", err) @@ -484,13 +491,13 @@ func CreatePullRequest(ctx *context.APIContext) { DeadlineUnix: deadlineUnix, } pr := &issues_model.PullRequest{ - HeadRepoID: headRepo.ID, + HeadRepoID: compareResult.headRepo.ID, BaseRepoID: repo.ID, - HeadBranch: headBranch, - BaseBranch: baseBranch, - HeadRepo: headRepo, + HeadBranch: compareResult.headRef.ShortName(), + BaseBranch: compareResult.baseRef.ShortName(), + HeadRepo: compareResult.headRepo, BaseRepo: repo, - MergeBase: compareInfo.MergeBase, + MergeBase: compareResult.compareInfo.MergeBase, Type: issues_model.PullRequestGitea, } @@ -1080,32 +1087,32 @@ func MergePullRequest(ctx *context.APIContext) { ctx.Status(http.StatusOK) } -func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption) (*repo_model.Repository, *git.Repository, *git.CompareInfo, string, string) { - baseRepo := ctx.Repo.Repository +type parseCompareInfoResult struct { + headRepo *repo_model.Repository + headGitRepo *git.Repository + compareInfo *git.CompareInfo + baseRef git.RefName + headRef git.RefName +} +// parseCompareInfo returns non-nil if it succeeds, it always writes to the context and returns nil if it fails +func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption) (result *parseCompareInfoResult, closer func()) { + var err error // Get compared branches information // format: ...[:] // base<-head: master...head:feature // same repo: master...feature + baseRepo := ctx.Repo.Repository + baseRefToGuess := form.Base - // TODO: Validate form first? - - baseBranch := form.Base - - var ( - headUser *user_model.User - headBranch string - isSameRepo bool - err error - ) - - // If there is no head repository, it means pull request between same repository. - headInfos := strings.Split(form.Head, ":") - if len(headInfos) == 1 { - isSameRepo = true - headUser = ctx.Repo.Owner - headBranch = headInfos[0] + headUser := ctx.Repo.Owner + headRefToGuess := form.Head + if headInfos := strings.Split(form.Head, ":"); len(headInfos) == 1 { + // If there is no head repository, it means pull request between same repository. + // Do nothing here because the head variables have been assigned above. } else if len(headInfos) == 2 { + // There is a head repository (the head repository could also be the same base repo) + headRefToGuess = headInfos[1] headUser, err = user_model.GetUserByName(ctx, headInfos[0]) if err != nil { if user_model.IsErrUserNotExist(err) { @@ -1113,38 +1120,29 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption) } else { ctx.Error(http.StatusInternalServerError, "GetUserByName", err) } - return nil, nil, nil, "", "" + return nil, nil } - 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, "", "" + return nil, nil } - ctx.Repo.PullRequest.SameRepo = isSameRepo - 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.Repo.GitRepo.IsTagExist(baseBranch) { - ctx.NotFound("BaseNotExist") - return nil, nil, nil, "", "" - } + isSameRepo := ctx.Repo.Owner.ID == headUser.ID // Check if current user has fork of repository or in the same repository. headRepo := repo_model.GetForkedRepo(ctx, headUser.ID, baseRepo.ID) if headRepo == nil && !isSameRepo { - err := baseRepo.GetBaseRepo(ctx) + err = baseRepo.GetBaseRepo(ctx) if err != nil { ctx.Error(http.StatusInternalServerError, "GetBaseRepo", err) - return nil, nil, nil, "", "" + return nil, nil } // Check if baseRepo's base repository is the same as headUser's repository. if baseRepo.BaseRepo == nil || baseRepo.BaseRepo.OwnerID != headUser.ID { log.Trace("parseCompareInfo[%d]: does not have fork or in same repository", baseRepo.ID) ctx.NotFound("GetBaseRepo") - return nil, nil, nil, "", "" + return nil, nil } // Assign headRepo so it can be used below. headRepo = baseRepo.BaseRepo @@ -1154,67 +1152,68 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption) if isSameRepo { headRepo = ctx.Repo.Repository headGitRepo = ctx.Repo.GitRepo + closer = func() {} // no need to close the head repo because it shares the base repo } else { headGitRepo, err = gitrepo.OpenRepository(ctx, headRepo) if err != nil { ctx.Error(http.StatusInternalServerError, "OpenRepository", err) - return nil, nil, nil, "", "" + return nil, nil } + closer = func() { _ = headGitRepo.Close() } } + defer func() { + if result == nil && !isSameRepo { + _ = headGitRepo.Close() + } + }() // user should have permission to read baseRepo's codes and pulls, NOT headRepo's permBase, err := access_model.GetUserRepoPermission(ctx, baseRepo, ctx.Doer) if err != nil { - headGitRepo.Close() ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err) - return nil, nil, nil, "", "" + return nil, nil } + if !permBase.CanReadIssuesOrPulls(true) || !permBase.CanRead(unit.TypeCode) { - if log.IsTrace() { - log.Trace("Permission Denied: User %-v cannot create/read pull requests or cannot read code in Repo %-v\nUser in baseRepo has Permissions: %-+v", - ctx.Doer, - baseRepo, - permBase) - } - headGitRepo.Close() + log.Trace("Permission Denied: User %-v cannot create/read pull requests or cannot read code in Repo %-v\nUser in baseRepo has Permissions: %-+v", ctx.Doer, baseRepo, permBase) ctx.NotFound("Can't read pulls or can't read UnitTypeCode") - return nil, nil, nil, "", "" + return nil, nil } - // user should have permission to read headrepo's codes + // user should have permission to read headRepo's codes + // TODO: could the logic be simplified if the headRepo is the same as the baseRepo? Need to think more about it. permHead, err := access_model.GetUserRepoPermission(ctx, headRepo, ctx.Doer) if err != nil { - headGitRepo.Close() ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err) - return nil, nil, nil, "", "" + return nil, nil } if !permHead.CanRead(unit.TypeCode) { - if log.IsTrace() { - log.Trace("Permission Denied: User: %-v cannot read code in Repo: %-v\nUser in headRepo has Permissions: %-+v", - ctx.Doer, - headRepo, - permHead) - } - headGitRepo.Close() + log.Trace("Permission Denied: User: %-v cannot read code in Repo: %-v\nUser in headRepo has Permissions: %-+v", ctx.Doer, headRepo, permHead) ctx.NotFound("Can't read headRepo UnitTypeCode") - return nil, nil, nil, "", "" + return nil, nil } - // Check if head branch is valid. - if !headGitRepo.IsBranchExist(headBranch) && !headGitRepo.IsTagExist(headBranch) { - headGitRepo.Close() + baseRef := ctx.Repo.GitRepo.UnstableGuessRefByShortName(baseRefToGuess) + headRef := headGitRepo.UnstableGuessRefByShortName(headRefToGuess) + + log.Trace("Repo path: %q, base ref: %q->%q, head ref: %q->%q", ctx.Repo.GitRepo.Path, baseRefToGuess, baseRef, headRefToGuess, headRef) + + baseRefValid := baseRef.IsBranch() || baseRef.IsTag() || git.IsStringLikelyCommitID(git.ObjectFormatFromName(ctx.Repo.Repository.ObjectFormatName), baseRef.ShortName()) + headRefValid := headRef.IsBranch() || headRef.IsTag() || git.IsStringLikelyCommitID(git.ObjectFormatFromName(headRepo.ObjectFormatName), headRef.ShortName()) + // Check if base&head ref are valid. + if !baseRefValid || !headRefValid { ctx.NotFound() - return nil, nil, nil, "", "" + return nil, nil } - compareInfo, err := headGitRepo.GetCompareInfo(repo_model.RepoPath(baseRepo.Owner.Name, baseRepo.Name), baseBranch, headBranch, false, false) + compareInfo, err := headGitRepo.GetCompareInfo(repo_model.RepoPath(baseRepo.Owner.Name, baseRepo.Name), baseRef.ShortName(), headRef.ShortName(), false, false) if err != nil { - headGitRepo.Close() ctx.Error(http.StatusInternalServerError, "GetCompareInfo", err) - return nil, nil, nil, "", "" + return nil, nil } - return headRepo, headGitRepo, compareInfo, baseBranch, headBranch + result = &parseCompareInfoResult{headRepo: headRepo, headGitRepo: headGitRepo, compareInfo: compareInfo, baseRef: baseRef, headRef: headRef} + return result, closer } // UpdatePullRequest merge PR's baseBranch into headBranch diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go index 39c98c666e545..125605d98f5aa 100644 --- a/routers/api/v1/swagger/options.go +++ b/routers/api/v1/swagger/options.go @@ -90,6 +90,8 @@ type swaggerParameterBodies struct { // in:body EditRepoOption api.EditRepoOption // in:body + UpdateBranchRepoOption api.UpdateBranchRepoOption + // in:body TransferRepoOption api.TransferRepoOption // in:body CreateForkOption api.CreateForkOption diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index 833f59981b46b..5397411b59cc2 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -9,7 +9,6 @@ import ( "fmt" "html/template" "net/http" - "net/url" "strconv" "strings" @@ -114,7 +113,6 @@ func MustAllowPulls(ctx *context.Context) { // User can send pull request if owns a forked repository. if ctx.IsSigned && repo_model.HasForkedRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID) { ctx.Repo.PullRequest.Allowed = true - ctx.Repo.PullRequest.HeadInfoSubURL = url.PathEscape(ctx.Doer.Name) + ":" + util.PathEscapeSegments(ctx.Repo.BranchName) } } diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go index e30129bb44ccd..717d7cbce1dc3 100644 --- a/routers/web/repo/setting/setting.go +++ b/routers/web/repo/setting/setting.go @@ -22,6 +22,7 @@ import ( "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/indexer/code" + issue_indexer "code.gitea.io/gitea/modules/indexer/issues" "code.gitea.io/gitea/modules/indexer/stats" "code.gitea.io/gitea/modules/lfs" "code.gitea.io/gitea/modules/log" @@ -905,6 +906,9 @@ func SettingsPost(ctx *context.Context) { log.Error("CleanRepoScheduleTasks for archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err) } + // update issue indexer + issue_indexer.UpdateRepoIndexer(ctx, repo.ID) + ctx.Flash.Success(ctx.Tr("repo.settings.archive.success")) log.Trace("Repository was archived: %s/%s", ctx.Repo.Owner.Name, repo.Name) @@ -929,6 +933,9 @@ func SettingsPost(ctx *context.Context) { } } + // update issue indexer + issue_indexer.UpdateRepoIndexer(ctx, repo.ID) + ctx.Flash.Success(ctx.Tr("repo.settings.unarchive.success")) log.Trace("Repository was un-archived: %s/%s", ctx.Repo.Owner.Name, repo.Name) diff --git a/services/context/repo.go b/services/context/repo.go index cf328ca97b77c..9b5443911030a 100644 --- a/services/context/repo.go +++ b/services/context/repo.go @@ -39,10 +39,9 @@ import ( // PullRequest contains information to make a pull request type PullRequest struct { - BaseRepo *repo_model.Repository - Allowed bool - SameRepo bool - HeadInfoSubURL string // [:] url segment + BaseRepo *repo_model.Repository + Allowed bool // it only used by the web tmpl: "PullRequestCtx.Allowed" + SameRepo bool // it only used by the web tmpl: "PullRequestCtx.SameRepo" } // Repository contains information to operate a repository @@ -401,6 +400,7 @@ func repoAssignment(ctx *Context, repo *repo_model.Repository) { // RepoAssignment returns a middleware to handle repository assignment func RepoAssignment(ctx *Context) context.CancelFunc { if _, repoAssignmentOnce := ctx.Data["repoAssignmentExecuted"]; repoAssignmentOnce { + // FIXME: it should panic in dev/test modes to have a clear behavior log.Trace("RepoAssignment was exec already, skipping second call ...") return nil } @@ -697,7 +697,6 @@ func RepoAssignment(ctx *Context) context.CancelFunc { ctx.Data["BaseRepo"] = repo.BaseRepo ctx.Repo.PullRequest.BaseRepo = repo.BaseRepo ctx.Repo.PullRequest.Allowed = canPush - ctx.Repo.PullRequest.HeadInfoSubURL = url.PathEscape(ctx.Repo.Owner.Name) + ":" + util.PathEscapeSegments(ctx.Repo.BranchName) } else if repo.AllowsPulls(ctx) { // Or, this is repository accepts pull requests between branches. canCompare = true @@ -705,7 +704,6 @@ func RepoAssignment(ctx *Context) context.CancelFunc { ctx.Repo.PullRequest.BaseRepo = repo ctx.Repo.PullRequest.Allowed = canPush ctx.Repo.PullRequest.SameRepo = true - ctx.Repo.PullRequest.HeadInfoSubURL = util.PathEscapeSegments(ctx.Repo.BranchName) } ctx.Data["CanCompareOrPull"] = canCompare ctx.Data["PullRequestCtx"] = ctx.Repo.PullRequest @@ -771,20 +769,6 @@ func getRefNameFromPath(repo *Repository, path string, isExist func(string) bool return "" } -func isStringLikelyCommitID(objFmt git.ObjectFormat, s string, minLength ...int) bool { - minLen := util.OptionalArg(minLength, objFmt.FullLength()) - if len(s) < minLen || len(s) > objFmt.FullLength() { - return false - } - for _, c := range s { - isHex := (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') - if !isHex { - return false - } - } - return true -} - func getRefNameLegacy(ctx *Base, repo *Repository, optionalExtraRef ...string) (string, RepoRefType) { extraRef := util.OptionalArg(optionalExtraRef) reqPath := ctx.PathParam("*") @@ -799,7 +783,7 @@ func getRefNameLegacy(ctx *Base, repo *Repository, optionalExtraRef ...string) ( // For legacy support only full commit sha parts := strings.Split(reqPath, "/") - if isStringLikelyCommitID(git.ObjectFormatFromName(repo.Repository.ObjectFormatName), parts[0]) { + if git.IsStringLikelyCommitID(git.ObjectFormatFromName(repo.Repository.ObjectFormatName), parts[0]) { // FIXME: this logic is different from other types. Ideally, it should also try to GetCommit to check if it exists repo.TreePath = strings.Join(parts[1:], "/") return parts[0], RepoRefCommit @@ -849,7 +833,7 @@ func getRefName(ctx *Base, repo *Repository, pathType RepoRefType) string { return getRefNameFromPath(repo, path, repo.GitRepo.IsTagExist) case RepoRefCommit: parts := strings.Split(path, "/") - if isStringLikelyCommitID(repo.GetObjectFormat(), parts[0], 7) { + if git.IsStringLikelyCommitID(repo.GetObjectFormat(), parts[0], 7) { // FIXME: this logic is different from other types. Ideally, it should also try to GetCommit to check if it exists repo.TreePath = strings.Join(parts[1:], "/") return parts[0] @@ -985,7 +969,7 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func return cancel } ctx.Repo.CommitID = ctx.Repo.Commit.ID.String() - } else if isStringLikelyCommitID(ctx.Repo.GetObjectFormat(), refName, 7) { + } else if git.IsStringLikelyCommitID(ctx.Repo.GetObjectFormat(), refName, 7) { ctx.Repo.IsViewCommit = true ctx.Repo.CommitID = refName diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index c06c0ad1541c2..82a301da2fe99 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -5045,6 +5045,63 @@ "$ref": "#/responses/repoArchivedError" } } + }, + "patch": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Update a branch", + "operationId": "repoUpdateBranch", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the branch", + "name": "branch", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/UpdateBranchRepoOption" + } + } + ], + "responses": { + "204": { + "$ref": "#/responses/empty" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "422": { + "$ref": "#/responses/validationError" + } + } } }, "/repos/{owner}/{repo}/collaborators": { @@ -24968,6 +25025,22 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "UpdateBranchRepoOption": { + "description": "UpdateBranchRepoOption options when updating a branch in a repository", + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "description": "New branch name", + "type": "string", + "uniqueItems": true, + "x-go-name": "Name" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "UpdateFileOptions": { "description": "UpdateFileOptions options for updating files\nNote: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)", "type": "object", diff --git a/tests/integration/api_branch_test.go b/tests/integration/api_branch_test.go index 8e49516aa7220..24a041de17e1b 100644 --- a/tests/integration/api_branch_test.go +++ b/tests/integration/api_branch_test.go @@ -5,6 +5,7 @@ package integration import ( "net/http" + "net/http/httptest" "net/url" "testing" @@ -186,6 +187,37 @@ func testAPICreateBranch(t testing.TB, session *TestSession, user, repo, oldBran return resp.Result().StatusCode == status } +func TestAPIUpdateBranch(t *testing.T) { + onGiteaRun(t, func(t *testing.T, _ *url.URL) { + t.Run("UpdateBranchWithEmptyRepo", func(t *testing.T) { + testAPIUpdateBranch(t, "user10", "repo6", "master", "test", http.StatusNotFound) + }) + t.Run("UpdateBranchWithSameBranchNames", func(t *testing.T) { + resp := testAPIUpdateBranch(t, "user2", "repo1", "master", "master", http.StatusUnprocessableEntity) + assert.Contains(t, resp.Body.String(), "Cannot rename a branch using the same name or rename to a branch that already exists.") + }) + t.Run("UpdateBranchThatAlreadyExists", func(t *testing.T) { + resp := testAPIUpdateBranch(t, "user2", "repo1", "master", "branch2", http.StatusUnprocessableEntity) + assert.Contains(t, resp.Body.String(), "Cannot rename a branch using the same name or rename to a branch that already exists.") + }) + t.Run("UpdateBranchWithNonExistentBranch", func(t *testing.T) { + resp := testAPIUpdateBranch(t, "user2", "repo1", "i-dont-exist", "new-branch-name", http.StatusNotFound) + assert.Contains(t, resp.Body.String(), "Branch doesn't exist.") + }) + t.Run("RenameBranchNormalScenario", func(t *testing.T) { + testAPIUpdateBranch(t, "user2", "repo1", "branch2", "new-branch-name", http.StatusNoContent) + }) + }) +} + +func testAPIUpdateBranch(t *testing.T, ownerName, repoName, from, to string, expectedHTTPStatus int) *httptest.ResponseRecorder { + token := getUserToken(t, ownerName, auth_model.AccessTokenScopeWriteRepository) + req := NewRequestWithJSON(t, "PATCH", "api/v1/repos/"+ownerName+"/"+repoName+"/branches/"+from, &api.UpdateBranchRepoOption{ + Name: to, + }).AddTokenAuth(token) + return MakeRequest(t, req, expectedHTTPStatus) +} + func TestAPIBranchProtection(t *testing.T) { defer tests.PrepareTestEnv(t)() diff --git a/tests/integration/api_repo_compare_test.go b/tests/integration/api_repo_compare_test.go index f3188eb49f621..9565e4d2090bd 100644 --- a/tests/integration/api_repo_compare_test.go +++ b/tests/integration/api_repo_compare_test.go @@ -24,15 +24,27 @@ func TestAPICompareBranches(t *testing.T) { session := loginUser(t, user.Name) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) - repoName := "repo20" + t.Run("CompareBranches", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo20/compare/add-csv...remove-files-b").AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) - req := NewRequestf(t, "GET", "/api/v1/repos/user2/%s/compare/add-csv...remove-files-b", repoName). - AddTokenAuth(token) - resp := MakeRequest(t, req, http.StatusOK) + var apiResp *api.Compare + DecodeJSON(t, resp, &apiResp) - var apiResp *api.Compare - DecodeJSON(t, resp, &apiResp) + assert.Equal(t, 2, apiResp.TotalCommits) + assert.Len(t, apiResp.Commits, 2) + }) - assert.Equal(t, 2, apiResp.TotalCommits) - assert.Len(t, apiResp.Commits, 2) + t.Run("CompareCommits", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo20/compare/808038d2f71b0ab02099...c8e31bc7688741a5287f").AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + + var apiResp *api.Compare + DecodeJSON(t, resp, &apiResp) + + assert.Equal(t, 1, apiResp.TotalCommits) + assert.Len(t, apiResp.Commits, 1) + }) } diff --git a/tests/integration/integration_test.go b/tests/integration/integration_test.go index 4338e19617468..8b6605eac8f3e 100644 --- a/tests/integration/integration_test.go +++ b/tests/integration/integration_test.go @@ -440,7 +440,7 @@ func DecodeJSON(t testing.TB, resp *httptest.ResponseRecorder, v any) { t.Helper() decoder := json.NewDecoder(resp.Body) - assert.NoError(t, decoder.Decode(v)) + require.NoError(t, decoder.Decode(v)) } func VerifyJSONSchema(t testing.TB, resp *httptest.ResponseRecorder, schemaFile string) { diff --git a/tools/generate-images.js b/tools/generate-images.js index 0bd3af29e4c3d..d28e0916f7351 100755 --- a/tools/generate-images.js +++ b/tools/generate-images.js @@ -1,6 +1,6 @@ #!/usr/bin/env node -import imageminZopfli from 'imagemin-zopfli'; // eslint-disable-line i/no-unresolved -import {loadSVGFromString, Canvas, Rect, util} from 'fabric/node'; // eslint-disable-line i/no-unresolved +import imageminZopfli from 'imagemin-zopfli'; // eslint-disable-line import-x/no-unresolved +import {loadSVGFromString, Canvas, Rect, util} from 'fabric/node'; // eslint-disable-line import-x/no-unresolved import {optimize} from 'svgo'; import {readFile, writeFile} from 'node:fs/promises'; import {argv, exit} from 'node:process'; diff --git a/web_src/js/features/comp/EditorUpload.ts b/web_src/js/features/comp/EditorUpload.ts index b1f49cbe92667..89982747ea5de 100644 --- a/web_src/js/features/comp/EditorUpload.ts +++ b/web_src/js/features/comp/EditorUpload.ts @@ -178,6 +178,7 @@ export function initTextareaEvents(textarea, dropzoneEl) { }); textarea.addEventListener('drop', (e) => { if (!e.dataTransfer.files.length) return; + if (!dropzoneEl) return; handleUploadFiles(new TextareaEditor(textarea), dropzoneEl, e.dataTransfer.files, e); }); dropzoneEl?.dropzone.on(DropzoneCustomEventRemovedFile, ({fileUuid}) => { diff --git a/web_src/js/features/repo-common.ts b/web_src/js/features/repo-common.ts index 40932f66678a8..90860720e4fc2 100644 --- a/web_src/js/features/repo-common.ts +++ b/web_src/js/features/repo-common.ts @@ -75,12 +75,12 @@ function initCloneSchemeUrlSelection(parent: Element) { }; updateClonePanelUi(); - - tabSsh.addEventListener('click', () => { + // tabSsh or tabHttps might not both exist, eg: guest view, or one is disabled by the server + tabSsh?.addEventListener('click', () => { localStorage.setItem('repo-clone-protocol', 'ssh'); updateClonePanelUi(); }); - tabHttps.addEventListener('click', () => { + tabHttps?.addEventListener('click', () => { localStorage.setItem('repo-clone-protocol', 'https'); updateClonePanelUi(); });