Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
5582b85
Delete non-exist branch should return 404
lunny Feb 21, 2026
2456394
Use integration test because there is no push queue in unit tests
lunny Feb 22, 2026
cee9e25
Merge branch 'main' into lunny/delete_branch_not_exist
GiteaBot Feb 24, 2026
6db7812
Merge branch 'main' into lunny/delete_branch_not_exist
GiteaBot Feb 25, 2026
2bd8f8b
Merge branch 'main' into lunny/delete_branch_not_exist
GiteaBot Feb 25, 2026
2126c6d
adjustment
lunny Feb 25, 2026
8c5ffa6
Merge branch 'main' into lunny/delete_branch_not_exist
lunny Feb 25, 2026
233c054
Merge branch 'lunny/delete_branch_not_exist' of github.com:lunny/gite…
lunny Feb 25, 2026
ce6c229
Merge branch 'main' into lunny/delete_branch_not_exist
GiteaBot Feb 25, 2026
919dbe9
Merge branch 'main' into lunny/delete_branch_not_exist
GiteaBot Feb 25, 2026
1ea224e
Merge branch 'main' into lunny/delete_branch_not_exist
GiteaBot Feb 25, 2026
ccee442
Merge branch 'main' into lunny/delete_branch_not_exist
GiteaBot Feb 25, 2026
9dee849
Fix bug
lunny Feb 25, 2026
a78297c
Merge branch 'lunny/delete_branch_not_exist' of github.com:lunny/gite…
lunny Feb 25, 2026
0bc32aa
Merge branch 'main' into lunny/delete_branch_not_exist
lunny Feb 25, 2026
f92b8e7
refactor
wxiaoguang Mar 2, 2026
99e7c34
Merge branch 'main' into lunny/delete_branch_not_exist
wxiaoguang Mar 2, 2026
b71e87b
fix merge
wxiaoguang Mar 2, 2026
ca3da14
use api tests
wxiaoguang Mar 2, 2026
e9a9d85
Merge branch 'main' into lunny/delete_branch_not_exist
lunny Mar 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions models/git/branch.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ func DeleteBranches(ctx context.Context, repoID, doerID int64, branchIDs []int64
return err
}
for _, branch := range branches {
if err := AddDeletedBranch(ctx, repoID, branch.Name, doerID); err != nil {
if err := MarkBranchAsDeleted(ctx, repoID, branch.Name, doerID); err != nil {
return err
}
}
Expand All @@ -268,8 +268,8 @@ func UpdateBranch(ctx context.Context, repoID, pusherID int64, branchName string
})
}

// AddDeletedBranch adds a deleted branch to the database
func AddDeletedBranch(ctx context.Context, repoID int64, branchName string, deletedByID int64) error {
// MarkBranchAsDeleted marks branch as deleted
func MarkBranchAsDeleted(ctx context.Context, repoID int64, branchName string, deletedByID int64) error {
branch, err := GetBranch(ctx, repoID, branchName)
if err != nil {
return err
Expand Down
4 changes: 2 additions & 2 deletions models/git/branch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ func TestAddDeletedBranch(t *testing.T) {
firstBranch := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{ID: 1})

assert.True(t, firstBranch.IsDeleted)
assert.NoError(t, git_model.AddDeletedBranch(t.Context(), repo.ID, firstBranch.Name, firstBranch.DeletedByID))
assert.NoError(t, git_model.AddDeletedBranch(t.Context(), repo.ID, "branch2", int64(1)))
assert.NoError(t, git_model.MarkBranchAsDeleted(t.Context(), repo.ID, firstBranch.Name, firstBranch.DeletedByID))
assert.NoError(t, git_model.MarkBranchAsDeleted(t.Context(), repo.ID, "branch2", int64(1)))

secondBranch := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo.ID, Name: "branch2"})
assert.True(t, secondBranch.IsDeleted)
Expand Down
2 changes: 1 addition & 1 deletion routers/api/v1/repo/branch.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ func DeleteBranch(ctx *context.APIContext) {
}
}

if err := repo_service.DeleteBranch(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, branchName, nil); err != nil {
if err := repo_service.DeleteBranch(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, branchName); err != nil {
switch {
case git.IsErrBranchNotExist(err):
ctx.APIErrorNotFound(err)
Expand Down
6 changes: 3 additions & 3 deletions routers/private/hook_post_receive.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,10 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) {
}

if update.IsDelRef() {
if err := git_model.AddDeletedBranch(ctx, repo.ID, update.RefFullName.BranchName(), update.PusherID); err != nil {
log.Error("Failed to add deleted branch: %s/%s Error: %v", ownerName, repoName, err)
if err := git_model.MarkBranchAsDeleted(ctx, repo.ID, update.RefFullName.BranchName(), update.PusherID); err != nil {
log.Error("Failed to mark branch as deleted: %s/%s Error: %v", ownerName, repoName, err)
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
Err: fmt.Sprintf("Failed to add deleted branch: %s/%s Error: %v", ownerName, repoName, err),
Err: fmt.Sprintf("Failed to mark branch as deleted: %s/%s Error: %v", ownerName, repoName, err),
})
return
}
Expand Down
2 changes: 1 addition & 1 deletion routers/web/repo/branch.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ func DeleteBranchPost(ctx *context.Context) {
defer jsonRedirectBranches(ctx)
branchName := ctx.FormString("name")

if err := repo_service.DeleteBranch(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, branchName, nil); err != nil {
if err := repo_service.DeleteBranch(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, branchName); err != nil {
switch {
case git.IsErrBranchNotExist(err):
log.Debug("DeleteBranch: Can't delete non existing branch '%s'", branchName)
Expand Down
89 changes: 55 additions & 34 deletions services/repository/branch.go
Original file line number Diff line number Diff line change
Expand Up @@ -573,8 +573,38 @@ func CanDeleteBranch(ctx context.Context, repo *repo_model.Repository, branchNam
return nil
}

func deleteBranchInternal(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, branchName string, branchCommit *git.Commit) (branchExisted bool, err error) {
activeInDB, err := git_model.IsBranchExist(ctx, repo.ID, branchName)
if err != nil {
return false, fmt.Errorf("IsBranchExist: %w", err)
}

// process the branch in db
if activeInDB {
if err := git_model.MarkBranchAsDeleted(ctx, repo.ID, branchName, doer.ID); err != nil {
return false, err
}
}

// process the branch in git
if branchCommit != nil {
err := gitrepo.DeleteBranch(ctx, repo, branchName, true)
if err != nil {
return false, fmt.Errorf("DeleteBranch: %w", err)
}
// since the branch existed in git, return branchExisted=true
branchExisted = true
} else {
// the branch didn't exist in git, return activeInDB to indicate whether the branch was active in DB,
// for consistency with that the user had seen on the web ui or in the branch list API response.
branchExisted = activeInDB
}

return branchExisted, nil
}

// DeleteBranch delete branch
func DeleteBranch(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, gitRepo *git.Repository, branchName string, pr *issues_model.PullRequest) error {
func DeleteBranch(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, gitRepo *git.Repository, branchName string) error {
err := repo.MustNotBeArchived()
if err != nil {
return err
Expand All @@ -584,46 +614,31 @@ func DeleteBranch(ctx context.Context, doer *user_model.User, repo *repo_model.R
return err
}

rawBranch, err := git_model.GetBranch(ctx, repo.ID, branchName)
if err != nil && !git_model.IsErrBranchNotExist(err) {
return fmt.Errorf("GetBranch: %vc", err)
}

// database branch record not exist or it's a deleted branch
notExist := git_model.IsErrBranchNotExist(err) || rawBranch.IsDeleted

branchCommit, err := gitRepo.GetBranchCommit(branchName)
// branchCommit can be nil if the branch doesn't exist in git
if err != nil && !errors.Is(err, util.ErrNotExist) {
return err
}

if err := db.WithTx(ctx, func(ctx context.Context) error {
if !notExist {
if err := git_model.AddDeletedBranch(ctx, repo.ID, branchName, doer.ID); err != nil {
return err
}
}

if pr != nil {
if err := issues_model.AddDeletePRBranchComment(ctx, doer, pr.BaseRepo, pr.Issue.ID, pr.HeadBranch); err != nil {
return fmt.Errorf("DeleteBranch: %v", err)
}
}
if branchCommit == nil {
return nil
}

return gitrepo.DeleteBranch(ctx, repo, branchName, true)
}); err != nil {
branchExisted, err := db.WithTx2(ctx, func(ctx context.Context) (bool, error) {
return deleteBranchInternal(ctx, doer, repo, branchName, branchCommit)
})
if err != nil {
return err
}

if branchCommit == nil {
return nil
if !branchExisted {
return git.ErrBranchNotExist{Name: branchName}
}

// Don't return error below this
// Don't return error below this since the deletion has succeeded
if branchCommit != nil {
deleteBranchSuccessPostProcess(doer, repo, branchName, branchCommit)
}
return nil
}

func deleteBranchSuccessPostProcess(doer *user_model.User, repo *repo_model.Repository, branchName string, branchCommit *git.Commit) {
objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName)
if err := PushUpdate(
&repo_module.PushUpdateOptions{
Expand All @@ -637,8 +652,6 @@ func DeleteBranch(ctx context.Context, doer *user_model.User, repo *repo_model.R
}); err != nil {
log.Error("PushUpdateOptions: %v", err)
}

return nil
}

type BranchSyncOptions struct {
Expand Down Expand Up @@ -886,9 +899,17 @@ func DeleteBranchAfterMerge(ctx context.Context, doer *user_model.User, prID int
return util.ErrorWrapTranslatable(util.ErrUnprocessableContent, "repo.branch.delete_branch_has_new_commits", fullBranchName)
}

err = DeleteBranch(ctx, doer, pr.HeadRepo, gitHeadRepo, pr.HeadBranch, pr)
err = DeleteBranch(ctx, doer, pr.HeadRepo, gitHeadRepo, pr.HeadBranch)
if errors.Is(err, util.ErrPermissionDenied) || errors.Is(err, util.ErrNotExist) {
return errFailedToDelete(err)
}
return err
if err != nil {
return err
}

// intentionally ignore the following error, since the branch has already been deleted successfully
if err := issues_model.AddDeletePRBranchComment(ctx, doer, pr.BaseRepo, pr.Issue.ID, pr.HeadBranch); err != nil {
log.Error("AddDeletePRBranchComment: %v", err)
}
return nil
}
2 changes: 1 addition & 1 deletion tests/integration/actions_trigger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,7 @@ jobs:
assert.NotNil(t, run)

// delete the branch
err = repo_service.DeleteBranch(t.Context(), user2, repo, gitRepo, "test-create-branch", nil)
err = repo_service.DeleteBranch(t.Context(), user2, repo, gitRepo, "test-create-branch")
assert.NoError(t, err)
run = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{
Title: "add workflow",
Expand Down
2 changes: 2 additions & 0 deletions tests/integration/api_branch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,8 @@ func TestAPIBranchProtection(t *testing.T) {
// Test branch deletion
testAPIDeleteBranch(t, "master", http.StatusForbidden)
testAPIDeleteBranch(t, "branch2", http.StatusNoContent)
testAPIDeleteBranch(t, "branch2", http.StatusNotFound) // deleted branch, there is a record in DB with IsDelete=true
testAPIDeleteBranch(t, "no-such-branch", http.StatusNotFound) // non-existing branch, not exist in git or DB
}

func TestAPICreateBranchWithSyncBranches(t *testing.T) {
Expand Down