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: