diff --git a/.eslintrc.yaml b/.eslintrc.yaml
index 04eb023634..0dd9a6687d 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/models/db/context_committer_test.go b/models/db/context_committer_test.go
index 38e91f22ed..849c5dea41 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 010b56948e..0ed268e346 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 2db630e2ea..aab4c5d77d 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 8eaa17cb04..850ec65502 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 88a555c86f..f14c7d656b 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/structs/repo.go b/modules/structs/repo.go
index 832ffa8bcc..fb784bd8b3 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 f0abce0d4b..776d2bdc2b 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 e3f7a0116f..53bd5bc4f1 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 d30aedc54f..3a81e64822 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 f28ee980e1..96365e7c14 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 53f3b4648a..946203e97e 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/pull.go b/routers/api/v1/repo/pull.go
index 0893d53713..57eb8ff999 100644
--- a/routers/api/v1/repo/pull.go
+++ b/routers/api/v1/repo/pull.go
@@ -391,8 +391,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
}
diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go
index 39c98c666e..125605d98f 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 833f59981b..5397411b59 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/services/context/repo.go b/services/context/repo.go
index cf328ca97b..9b54439110 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 c06c0ad154..82a301da2f 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 8e49516aa7..24a041de17 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 f3188eb49f..9565e4d209 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 4338e19617..8b6605eac8 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 0bd3af29e4..d28e0916f7 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 b1f49cbe92..89982747ea 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 40932f6667..90860720e4 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();
});