diff --git a/.github/workflows/release-nightly.yml b/.github/workflows/release-nightly.yml
index 6e1b6e07584..2264c9e822d 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 41037df29cb..a406602dc0a 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 a23e6982000..f67b76f4087 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/actions/run.go b/models/actions/run.go
index 732fb48bb9a..f40bc1eb3db 100644
--- a/models/actions/run.go
+++ b/models/actions/run.go
@@ -37,6 +37,7 @@ type ActionRun struct {
TriggerUser *user_model.User `xorm:"-"`
ScheduleID int64
Ref string `xorm:"index"` // the commit/tag/… that caused the run
+ IsRefDeleted bool `xorm:"-"`
CommitSHA string
IsForkPullRequest bool // If this is triggered by a PR from a forked repository or an untrusted user, we need to check if it is approved and limit permissions when running the workflow.
NeedApproval bool // may need approval if it's a fork pull request
diff --git a/models/git/branch.go b/models/git/branch.go
index ba1ada5517d..e683ce47e65 100644
--- a/models/git/branch.go
+++ b/models/git/branch.go
@@ -12,6 +12,7 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
@@ -169,9 +170,22 @@ func GetBranch(ctx context.Context, repoID int64, branchName string) (*Branch, e
return &branch, nil
}
-func GetBranches(ctx context.Context, repoID int64, branchNames []string) ([]*Branch, error) {
+func GetBranches(ctx context.Context, repoID int64, branchNames []string, includeDeleted bool) ([]*Branch, error) {
branches := make([]*Branch, 0, len(branchNames))
- return branches, db.GetEngine(ctx).Where("repo_id=?", repoID).In("name", branchNames).Find(&branches)
+
+ sess := db.GetEngine(ctx).Where("repo_id=?", repoID).In("name", branchNames)
+ if !includeDeleted {
+ sess.And("is_deleted=?", false)
+ }
+ return branches, sess.Find(&branches)
+}
+
+func BranchesToNamesSet(branches []*Branch) container.Set[string] {
+ names := make(container.Set[string], len(branches))
+ for _, branch := range branches {
+ names.Add(branch.Name)
+ }
+ return names
}
func AddBranches(ctx context.Context, branches []*Branch) error {
diff --git a/modules/git/commit.go b/modules/git/commit.go
index 010b56948ef..0ed268e3469 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 2db630e2ea9..aab4c5d77d7 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 8eaa17cb041..850ec655029 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 88a555c86f3..f14c7d656b6 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 832ffa8bcc9..fb784bd8b37 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/routers/api/v1/api.go b/routers/api/v1/api.go
index f28ee980e10..96365e7c14d 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 53f3b4648a5..946203e97ec 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 38e5330b3ac..1678bc033c6 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 bad078414e6..fc690bee022 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,23 +1120,14 @@ 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, err := repo_model.GetForkedRepo(ctx, headUser.ID, baseRepo.ID)
@@ -1138,17 +1136,17 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
return nil, nil, nil, "", ""
}
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
@@ -1158,67 +1156,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, "", ""
- }
- 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()
- 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
+ if !permBase.CanReadIssuesOrPulls(true) || !permBase.CanRead(unit.TypeCode) {
+ 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
+ }
+
+ // 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 39c98c666e5..125605d98f5 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/actions/actions.go b/routers/web/repo/actions/actions.go
index ad16b9fb4e4..7ed37ea26b2 100644
--- a/routers/web/repo/actions/actions.go
+++ b/routers/web/repo/actions/actions.go
@@ -245,6 +245,10 @@ func List(ctx *context.Context) {
return
}
+ if err := loadIsRefDeleted(ctx, runs); err != nil {
+ log.Error("LoadIsRefDeleted", err)
+ }
+
ctx.Data["Runs"] = runs
actors, err := actions_model.GetActors(ctx, ctx.Repo.Repository.ID)
@@ -267,6 +271,34 @@ func List(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplListActions)
}
+// loadIsRefDeleted loads the IsRefDeleted field for each run in the list.
+// TODO: move this function to models/actions/run_list.go but now it will result in a circular import.
+func loadIsRefDeleted(ctx *context.Context, runs actions_model.RunList) error {
+ branches := make(container.Set[string], len(runs))
+ for _, run := range runs {
+ refName := git.RefName(run.Ref)
+ if refName.IsBranch() {
+ branches.Add(refName.ShortName())
+ }
+ }
+ if len(branches) == 0 {
+ return nil
+ }
+
+ branchInfos, err := git_model.GetBranches(ctx, ctx.Repo.Repository.ID, branches.Values(), false)
+ if err != nil {
+ return err
+ }
+ branchSet := git_model.BranchesToNamesSet(branchInfos)
+ for _, run := range runs {
+ refName := git.RefName(run.Ref)
+ if refName.IsBranch() && !branchSet.Contains(run.Ref) {
+ run.IsRefDeleted = true
+ }
+ }
+ return nil
+}
+
type WorkflowDispatchInput struct {
Name string `yaml:"name"`
Description string `yaml:"description"`
diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go
index 73c6e54fbf5..b711038da06 100644
--- a/routers/web/repo/actions/view.go
+++ b/routers/web/repo/actions/view.go
@@ -19,6 +19,7 @@ import (
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
+ git_model "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
@@ -136,8 +137,9 @@ type ViewUser struct {
}
type ViewBranch struct {
- Name string `json:"name"`
- Link string `json:"link"`
+ Name string `json:"name"`
+ Link string `json:"link"`
+ IsDeleted bool `json:"isDeleted"`
}
type ViewJobStep struct {
@@ -236,6 +238,16 @@ func ViewPost(ctx *context_module.Context) {
Name: run.PrettyRef(),
Link: run.RefLink(),
}
+ refName := git.RefName(run.Ref)
+ if refName.IsBranch() {
+ b, err := git_model.GetBranch(ctx, ctx.Repo.Repository.ID, refName.ShortName())
+ if err != nil && !git_model.IsErrBranchNotExist(err) {
+ log.Error("GetBranch: %v", err)
+ } else if git_model.IsErrBranchNotExist(err) || (b != nil && b.IsDeleted) {
+ branch.IsDeleted = true
+ }
+ }
+
resp.State.Run.Commit = ViewCommit{
ShortSha: base.ShortSha(run.CommitSHA),
Link: fmt.Sprintf("%s/commit/%s", run.Repo.Link(), run.CommitSHA),
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index 833f59981b4..5397411b59c 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 cf328ca97b7..9b544391103 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/services/repository/branch.go b/services/repository/branch.go
index 508817c83e6..3a95aab2649 100644
--- a/services/repository/branch.go
+++ b/services/repository/branch.go
@@ -305,7 +305,7 @@ func SyncBranchesToDB(ctx context.Context, repoID, pusherID int64, branchNames,
}
return db.WithTx(ctx, func(ctx context.Context) error {
- branches, err := git_model.GetBranches(ctx, repoID, branchNames)
+ branches, err := git_model.GetBranches(ctx, repoID, branchNames, true)
if err != nil {
return fmt.Errorf("git_model.GetBranches: %v", err)
}
diff --git a/templates/repo/actions/runs_list.tmpl b/templates/repo/actions/runs_list.tmpl
index 5537e9e6172..fa1adb3e3ba 100644
--- a/templates/repo/actions/runs_list.tmpl
+++ b/templates/repo/actions/runs_list.tmpl
@@ -27,10 +27,10 @@
- {{if .RefLink}}
-
{{.PrettyRef}}
+ {{if .IsRefDeleted}}
+
{{.PrettyRef}}
{{else}}
-
{{.PrettyRef}}
+
{{.PrettyRef}}
{{end}}
{{svg "octicon-calendar" 16}}{{DateUtils.TimeSince .Updated}}
diff --git a/templates/repo/contributors.tmpl b/templates/repo/contributors.tmpl
index 6b8a63fe996..882d8b205cf 100644
--- a/templates/repo/contributors.tmpl
+++ b/templates/repo/contributors.tmpl
@@ -1,6 +1,7 @@
{{if .Permission.CanRead ctx.Consts.RepoUnitTypeCode}}
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index c06c0ad1541..82a301da2fe 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 8e49516aa72..24a041de17e 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 f3188eb49f6..9565e4d2090 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 4338e196174..8b6605eac8f 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/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue
index 7f647b668a6..eece2efaf86 100644
--- a/web_src/js/components/RepoActionView.vue
+++ b/web_src/js/components/RepoActionView.vue
@@ -429,7 +429,8 @@ export function initRepositoryActionView() {
{{ run.commit.pusher.displayName }}
- {{ run.commit.branch.name }}
+ {{ run.commit.branch.name }}
+ {{ run.commit.branch.name }}
diff --git a/web_src/js/components/RepoContributors.vue b/web_src/js/components/RepoContributors.vue
index 97678b9a13b..e79fc80d8e3 100644
--- a/web_src/js/components/RepoContributors.vue
+++ b/web_src/js/components/RepoContributors.vue
@@ -1,5 +1,6 @@