mirror of
https://github.com/go-gitea/gitea.git
synced 2025-10-27 00:23:41 +09:00
Honor delete branch on merge repo setting when using merge API (#35488)
Fix #35463. --------- Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
@@ -5,7 +5,6 @@ package git
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -25,7 +24,7 @@ import (
|
|||||||
"xorm.io/builder"
|
"xorm.io/builder"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrBranchIsProtected = errors.New("branch is protected")
|
var ErrBranchIsProtected = util.ErrorWrap(util.ErrPermissionDenied, "branch is protected")
|
||||||
|
|
||||||
// ProtectedBranch struct
|
// ProtectedBranch struct
|
||||||
type ProtectedBranch struct {
|
type ProtectedBranch struct {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ package util
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"html/template"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Common Errors forming the base of our error system
|
// Common Errors forming the base of our error system
|
||||||
@@ -40,22 +41,6 @@ func (w errorWrapper) Unwrap() error {
|
|||||||
return w.Err
|
return w.Err
|
||||||
}
|
}
|
||||||
|
|
||||||
type LocaleWrapper struct {
|
|
||||||
err error
|
|
||||||
TrKey string
|
|
||||||
TrArgs []any
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error returns the message
|
|
||||||
func (w LocaleWrapper) Error() string {
|
|
||||||
return w.err.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unwrap returns the underlying error
|
|
||||||
func (w LocaleWrapper) Unwrap() error {
|
|
||||||
return w.err
|
|
||||||
}
|
|
||||||
|
|
||||||
// ErrorWrap returns an error that formats as the given text but unwraps as the provided error
|
// ErrorWrap returns an error that formats as the given text but unwraps as the provided error
|
||||||
func ErrorWrap(unwrap error, message string, args ...any) error {
|
func ErrorWrap(unwrap error, message string, args ...any) error {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
@@ -84,15 +69,39 @@ func NewNotExistErrorf(message string, args ...any) error {
|
|||||||
return ErrorWrap(ErrNotExist, message, args...)
|
return ErrorWrap(ErrNotExist, message, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrorWrapLocale wraps an err with a translation key and arguments
|
// ErrorTranslatable wraps an error with translation information
|
||||||
func ErrorWrapLocale(err error, trKey string, trArgs ...any) error {
|
type ErrorTranslatable interface {
|
||||||
return LocaleWrapper{err: err, TrKey: trKey, TrArgs: trArgs}
|
error
|
||||||
|
Unwrap() error
|
||||||
|
Translate(ErrorLocaleTranslator) template.HTML
|
||||||
}
|
}
|
||||||
|
|
||||||
func ErrorAsLocale(err error) *LocaleWrapper {
|
type errorTranslatableWrapper struct {
|
||||||
var e LocaleWrapper
|
err error
|
||||||
|
trKey string
|
||||||
|
trArgs []any
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrorLocaleTranslator interface {
|
||||||
|
Tr(key string, args ...any) template.HTML
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *errorTranslatableWrapper) Error() string { return w.err.Error() }
|
||||||
|
|
||||||
|
func (w *errorTranslatableWrapper) Unwrap() error { return w.err }
|
||||||
|
|
||||||
|
func (w *errorTranslatableWrapper) Translate(t ErrorLocaleTranslator) template.HTML {
|
||||||
|
return t.Tr(w.trKey, w.trArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ErrorWrapTranslatable(err error, trKey string, trArgs ...any) ErrorTranslatable {
|
||||||
|
return &errorTranslatableWrapper{err: err, trKey: trKey, trArgs: trArgs}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ErrorAsTranslatable(err error) ErrorTranslatable {
|
||||||
|
var e *errorTranslatableWrapper
|
||||||
if errors.As(err, &e) {
|
if errors.As(err, &e) {
|
||||||
return &e
|
return e
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
29
modules/util/error_test.go
Normal file
29
modules/util/error_test.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestErrorTranslatable(t *testing.T) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
err = ErrorWrapTranslatable(io.EOF, "key", 1)
|
||||||
|
assert.ErrorIs(t, err, io.EOF)
|
||||||
|
assert.Equal(t, "EOF", err.Error())
|
||||||
|
assert.Equal(t, "key", err.(*errorTranslatableWrapper).trKey)
|
||||||
|
assert.Equal(t, []any{1}, err.(*errorTranslatableWrapper).trArgs)
|
||||||
|
|
||||||
|
err = ErrorWrap(err, "new msg %d", 100)
|
||||||
|
assert.ErrorIs(t, err, io.EOF)
|
||||||
|
assert.Equal(t, "new msg 100", err.Error())
|
||||||
|
|
||||||
|
errTr := ErrorAsTranslatable(err)
|
||||||
|
assert.Equal(t, "EOF", errTr.Error())
|
||||||
|
assert.Equal(t, "key", errTr.(*errorTranslatableWrapper).trKey)
|
||||||
|
}
|
||||||
@@ -13,7 +13,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
activities_model "code.gitea.io/gitea/models/activities"
|
activities_model "code.gitea.io/gitea/models/activities"
|
||||||
git_model "code.gitea.io/gitea/models/git"
|
|
||||||
issues_model "code.gitea.io/gitea/models/issues"
|
issues_model "code.gitea.io/gitea/models/issues"
|
||||||
access_model "code.gitea.io/gitea/models/perm/access"
|
access_model "code.gitea.io/gitea/models/perm/access"
|
||||||
pull_model "code.gitea.io/gitea/models/pull"
|
pull_model "code.gitea.io/gitea/models/pull"
|
||||||
@@ -938,7 +937,7 @@ func MergePullRequest(ctx *context.APIContext) {
|
|||||||
} else if errors.Is(err, pull_service.ErrNoPermissionToMerge) {
|
} else if errors.Is(err, pull_service.ErrNoPermissionToMerge) {
|
||||||
ctx.APIError(http.StatusMethodNotAllowed, "User not allowed to merge PR")
|
ctx.APIError(http.StatusMethodNotAllowed, "User not allowed to merge PR")
|
||||||
} else if errors.Is(err, pull_service.ErrHasMerged) {
|
} else if errors.Is(err, pull_service.ErrHasMerged) {
|
||||||
ctx.APIError(http.StatusMethodNotAllowed, "")
|
ctx.APIError(http.StatusMethodNotAllowed, "The PR is already merged")
|
||||||
} else if errors.Is(err, pull_service.ErrIsWorkInProgress) {
|
} else if errors.Is(err, pull_service.ErrIsWorkInProgress) {
|
||||||
ctx.APIError(http.StatusMethodNotAllowed, "Work in progress PRs cannot be merged")
|
ctx.APIError(http.StatusMethodNotAllowed, "Work in progress PRs cannot be merged")
|
||||||
} else if errors.Is(err, pull_service.ErrNotMergeableState) {
|
} else if errors.Is(err, pull_service.ErrNotMergeableState) {
|
||||||
@@ -989,8 +988,14 @@ func MergePullRequest(ctx *context.APIContext) {
|
|||||||
message += "\n\n" + form.MergeMessageField
|
message += "\n\n" + form.MergeMessageField
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deleteBranchAfterMerge, err := pull_service.ShouldDeleteBranchAfterMerge(ctx, form.DeleteBranchAfterMerge, ctx.Repo.Repository, pr)
|
||||||
|
if err != nil {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if form.MergeWhenChecksSucceed {
|
if form.MergeWhenChecksSucceed {
|
||||||
scheduled, err := automerge.ScheduleAutoMerge(ctx, ctx.Doer, pr, repo_model.MergeStyle(form.Do), message, form.DeleteBranchAfterMerge)
|
scheduled, err := automerge.ScheduleAutoMerge(ctx, ctx.Doer, pr, repo_model.MergeStyle(form.Do), message, deleteBranchAfterMerge)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if pull_model.IsErrAlreadyScheduledToAutoMerge(err) {
|
if pull_model.IsErrAlreadyScheduledToAutoMerge(err) {
|
||||||
ctx.APIError(http.StatusConflict, err)
|
ctx.APIError(http.StatusConflict, err)
|
||||||
@@ -1035,47 +1040,10 @@ func MergePullRequest(ctx *context.APIContext) {
|
|||||||
}
|
}
|
||||||
log.Trace("Pull request merged: %d", pr.ID)
|
log.Trace("Pull request merged: %d", pr.ID)
|
||||||
|
|
||||||
// for agit flow, we should not delete the agit reference after merge
|
if deleteBranchAfterMerge {
|
||||||
if form.DeleteBranchAfterMerge && pr.Flow == issues_model.PullRequestFlowGithub {
|
if err = repo_service.DeleteBranchAfterMerge(ctx, ctx.Doer, pr.ID, nil); err != nil {
|
||||||
// check permission even it has been checked in repo_service.DeleteBranch so that we don't need to
|
// no way to tell users that what error happens, and the PR has been merged, so ignore the error
|
||||||
// do RetargetChildrenOnMerge
|
log.Debug("DeleteBranchAfterMerge: pr %d, err: %v", pr.ID, err)
|
||||||
if err := repo_service.CanDeleteBranch(ctx, pr.HeadRepo, pr.HeadBranch, ctx.Doer); err == nil {
|
|
||||||
// Don't cleanup when there are other PR's that use this branch as head branch.
|
|
||||||
exist, err := issues_model.HasUnmergedPullRequestsByHeadInfo(ctx, pr.HeadRepoID, pr.HeadBranch)
|
|
||||||
if err != nil {
|
|
||||||
ctx.APIErrorInternal(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if exist {
|
|
||||||
ctx.Status(http.StatusOK)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var headRepo *git.Repository
|
|
||||||
if ctx.Repo != nil && ctx.Repo.Repository != nil && ctx.Repo.Repository.ID == pr.HeadRepoID && ctx.Repo.GitRepo != nil {
|
|
||||||
headRepo = ctx.Repo.GitRepo
|
|
||||||
} else {
|
|
||||||
headRepo, err = gitrepo.OpenRepository(ctx, pr.HeadRepo)
|
|
||||||
if err != nil {
|
|
||||||
ctx.APIErrorInternal(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer headRepo.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := repo_service.DeleteBranch(ctx, ctx.Doer, pr.HeadRepo, headRepo, pr.HeadBranch, pr); err != nil {
|
|
||||||
switch {
|
|
||||||
case git.IsErrBranchNotExist(err):
|
|
||||||
ctx.APIErrorNotFound(err)
|
|
||||||
case errors.Is(err, repo_service.ErrBranchIsDefault):
|
|
||||||
ctx.APIError(http.StatusForbidden, errors.New("can not delete default branch"))
|
|
||||||
case errors.Is(err, git_model.ErrBranchIsProtected):
|
|
||||||
ctx.APIError(http.StatusForbidden, errors.New("branch protected"))
|
|
||||||
default:
|
|
||||||
ctx.APIErrorInternal(err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -943,8 +943,8 @@ func Run(ctx *context_module.Context) {
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errLocale := util.ErrorAsLocale(err); errLocale != nil {
|
if errTr := util.ErrorAsTranslatable(err); errTr != nil {
|
||||||
ctx.Flash.Error(ctx.Tr(errLocale.TrKey, errLocale.TrArgs...))
|
ctx.Flash.Error(errTr.Translate(ctx.Locale))
|
||||||
ctx.Redirect(redirectURL)
|
ctx.Redirect(redirectURL)
|
||||||
} else {
|
} else {
|
||||||
ctx.ServerError("DispatchActionWorkflow", err)
|
ctx.ServerError("DispatchActionWorkflow", err)
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ func NewDiffPatchPost(ctx *context.Context) {
|
|||||||
Committer: parsed.GitCommitter,
|
Committer: parsed.GitCommitter,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = util.ErrorWrapLocale(err, "repo.editor.fail_to_apply_patch")
|
err = util.ErrorWrapTranslatable(err, "repo.editor.fail_to_apply_patch")
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
editorHandleFileOperationError(ctx, parsed.NewBranchName, err)
|
editorHandleFileOperationError(ctx, parsed.NewBranchName, err)
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ func CherryPickPost(ctx *context.Context) {
|
|||||||
opts.Content = buf.String()
|
opts.Content = buf.String()
|
||||||
_, err = files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, opts)
|
_, err = files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = util.ErrorWrapLocale(err, "repo.editor.fail_to_apply_patch")
|
err = util.ErrorWrapTranslatable(err, "repo.editor.fail_to_apply_patch")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -38,8 +38,8 @@ func editorHandleFileOperationErrorRender(ctx *context_service.Context, message,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func editorHandleFileOperationError(ctx *context_service.Context, targetBranchName string, err error) {
|
func editorHandleFileOperationError(ctx *context_service.Context, targetBranchName string, err error) {
|
||||||
if errAs := util.ErrorAsLocale(err); errAs != nil {
|
if errAs := util.ErrorAsTranslatable(err); errAs != nil {
|
||||||
ctx.JSONError(ctx.Tr(errAs.TrKey, errAs.TrArgs...))
|
ctx.JSONError(errAs.Translate(ctx.Locale))
|
||||||
} else if errAs, ok := errorAs[git.ErrNotExist](err); ok {
|
} else if errAs, ok := errorAs[git.ErrNotExist](err); ok {
|
||||||
ctx.JSONError(ctx.Tr("repo.editor.file_modifying_no_longer_exists", errAs.RelPath))
|
ctx.JSONError(ctx.Tr("repo.editor.file_modifying_no_longer_exists", errAs.RelPath))
|
||||||
} else if errAs, ok := errorAs[git_model.ErrLFSFileLocked](err); ok {
|
} else if errAs, ok := errorAs[git_model.ErrLFSFileLocked](err); ok {
|
||||||
|
|||||||
@@ -1130,11 +1130,17 @@ func MergePullRequest(ctx *context.Context) {
|
|||||||
message += "\n\n" + form.MergeMessageField
|
message += "\n\n" + form.MergeMessageField
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deleteBranchAfterMerge, err := pull_service.ShouldDeleteBranchAfterMerge(ctx, form.DeleteBranchAfterMerge, ctx.Repo.Repository, pr)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("ShouldDeleteBranchAfterMerge", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if form.MergeWhenChecksSucceed {
|
if form.MergeWhenChecksSucceed {
|
||||||
// delete all scheduled auto merges
|
// delete all scheduled auto merges
|
||||||
_ = pull_model.DeleteScheduledAutoMerge(ctx, pr.ID)
|
_ = pull_model.DeleteScheduledAutoMerge(ctx, pr.ID)
|
||||||
// schedule auto merge
|
// schedule auto merge
|
||||||
scheduled, err := automerge.ScheduleAutoMerge(ctx, ctx.Doer, pr, repo_model.MergeStyle(form.Do), message, form.DeleteBranchAfterMerge)
|
scheduled, err := automerge.ScheduleAutoMerge(ctx, ctx.Doer, pr, repo_model.MergeStyle(form.Do), message, deleteBranchAfterMerge)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("ScheduleAutoMerge", err)
|
ctx.ServerError("ScheduleAutoMerge", err)
|
||||||
return
|
return
|
||||||
@@ -1220,35 +1226,27 @@ func MergePullRequest(ctx *context.Context) {
|
|||||||
|
|
||||||
log.Trace("Pull request merged: %d", pr.ID)
|
log.Trace("Pull request merged: %d", pr.ID)
|
||||||
|
|
||||||
if !form.DeleteBranchAfterMerge {
|
if deleteBranchAfterMerge {
|
||||||
ctx.JSONRedirect(issue.Link())
|
deleteBranchAfterMergeAndFlashMessage(ctx, pr.ID)
|
||||||
|
if ctx.Written() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
ctx.JSONRedirect(issue.Link())
|
||||||
|
}
|
||||||
|
|
||||||
// Don't cleanup when other pr use this branch as head branch
|
func deleteBranchAfterMergeAndFlashMessage(ctx *context.Context, prID int64) {
|
||||||
exist, err := issues_model.HasUnmergedPullRequestsByHeadInfo(ctx, pr.HeadRepoID, pr.HeadBranch)
|
var fullBranchName string
|
||||||
if err != nil {
|
err := repo_service.DeleteBranchAfterMerge(ctx, ctx.Doer, prID, &fullBranchName)
|
||||||
ctx.ServerError("HasUnmergedPullRequestsByHeadInfo", err)
|
if errTr := util.ErrorAsTranslatable(err); errTr != nil {
|
||||||
|
ctx.Flash.Error(errTr.Translate(ctx.Locale))
|
||||||
|
return
|
||||||
|
} else if err == nil {
|
||||||
|
ctx.Flash.Success(ctx.Tr("repo.branch.deletion_success", fullBranchName))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if exist {
|
// catch unknown errors
|
||||||
ctx.JSONRedirect(issue.Link())
|
ctx.ServerError("DeleteBranchAfterMerge", err)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var headRepo *git.Repository
|
|
||||||
if ctx.Repo != nil && ctx.Repo.Repository != nil && pr.HeadRepoID == ctx.Repo.Repository.ID && ctx.Repo.GitRepo != nil {
|
|
||||||
headRepo = ctx.Repo.GitRepo
|
|
||||||
} else {
|
|
||||||
headRepo, err = gitrepo.OpenRepository(ctx, pr.HeadRepo)
|
|
||||||
if err != nil {
|
|
||||||
ctx.ServerError(fmt.Sprintf("OpenRepository[%s]", pr.HeadRepo.FullName()), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer headRepo.Close()
|
|
||||||
}
|
|
||||||
deleteBranch(ctx, pr, headRepo)
|
|
||||||
ctx.JSONRedirect(issue.Link())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CancelAutoMergePullRequest cancels a scheduled pr
|
// CancelAutoMergePullRequest cancels a scheduled pr
|
||||||
@@ -1437,131 +1435,17 @@ func CompareAndPullRequestPost(ctx *context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CleanUpPullRequest responses for delete merged branch when PR has been merged
|
// CleanUpPullRequest responses for delete merged branch when PR has been merged
|
||||||
|
// Used by "DeleteBranchLink" for "delete branch" button
|
||||||
func CleanUpPullRequest(ctx *context.Context) {
|
func CleanUpPullRequest(ctx *context.Context) {
|
||||||
issue, ok := getPullInfo(ctx)
|
issue, ok := getPullInfo(ctx)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
deleteBranchAfterMergeAndFlashMessage(ctx, issue.PullRequest.ID)
|
||||||
pr := issue.PullRequest
|
if ctx.Written() {
|
||||||
|
|
||||||
// Don't cleanup unmerged and unclosed PRs and agit PRs
|
|
||||||
if !pr.HasMerged && !issue.IsClosed && pr.Flow != issues_model.PullRequestFlowGithub {
|
|
||||||
ctx.NotFound(nil)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't cleanup when there are other PR's that use this branch as head branch.
|
|
||||||
exist, err := issues_model.HasUnmergedPullRequestsByHeadInfo(ctx, pr.HeadRepoID, pr.HeadBranch)
|
|
||||||
if err != nil {
|
|
||||||
ctx.ServerError("HasUnmergedPullRequestsByHeadInfo", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if exist {
|
|
||||||
ctx.NotFound(nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := pr.LoadHeadRepo(ctx); err != nil {
|
|
||||||
ctx.ServerError("LoadHeadRepo", err)
|
|
||||||
return
|
|
||||||
} else if pr.HeadRepo == nil {
|
|
||||||
// Forked repository has already been deleted
|
|
||||||
ctx.NotFound(nil)
|
|
||||||
return
|
|
||||||
} else if err = pr.LoadBaseRepo(ctx); err != nil {
|
|
||||||
ctx.ServerError("LoadBaseRepo", err)
|
|
||||||
return
|
|
||||||
} else if err = pr.HeadRepo.LoadOwner(ctx); err != nil {
|
|
||||||
ctx.ServerError("HeadRepo.LoadOwner", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := repo_service.CanDeleteBranch(ctx, pr.HeadRepo, pr.HeadBranch, ctx.Doer); err != nil {
|
|
||||||
if errors.Is(err, util.ErrPermissionDenied) {
|
|
||||||
ctx.NotFound(nil)
|
|
||||||
} else {
|
|
||||||
ctx.ServerError("CanDeleteBranch", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fullBranchName := pr.HeadRepo.Owner.Name + "/" + pr.HeadBranch
|
|
||||||
|
|
||||||
var gitBaseRepo *git.Repository
|
|
||||||
|
|
||||||
// Assume that the base repo is the current context (almost certainly)
|
|
||||||
if ctx.Repo != nil && ctx.Repo.Repository != nil && ctx.Repo.Repository.ID == pr.BaseRepoID && ctx.Repo.GitRepo != nil {
|
|
||||||
gitBaseRepo = ctx.Repo.GitRepo
|
|
||||||
} else {
|
|
||||||
// If not just open it
|
|
||||||
gitBaseRepo, err = gitrepo.OpenRepository(ctx, pr.BaseRepo)
|
|
||||||
if err != nil {
|
|
||||||
ctx.ServerError(fmt.Sprintf("OpenRepository[%s]", pr.BaseRepo.FullName()), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer gitBaseRepo.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now assume that the head repo is the same as the base repo (reasonable chance)
|
|
||||||
gitRepo := gitBaseRepo
|
|
||||||
// But if not: is it the same as the context?
|
|
||||||
if pr.BaseRepoID != pr.HeadRepoID && ctx.Repo != nil && ctx.Repo.Repository != nil && ctx.Repo.Repository.ID == pr.HeadRepoID && ctx.Repo.GitRepo != nil {
|
|
||||||
gitRepo = ctx.Repo.GitRepo
|
|
||||||
} else if pr.BaseRepoID != pr.HeadRepoID {
|
|
||||||
// Otherwise just load it up
|
|
||||||
gitRepo, err = gitrepo.OpenRepository(ctx, pr.HeadRepo)
|
|
||||||
if err != nil {
|
|
||||||
ctx.ServerError(fmt.Sprintf("OpenRepository[%s]", pr.HeadRepo.FullName()), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer gitRepo.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
ctx.JSONRedirect(issue.Link())
|
ctx.JSONRedirect(issue.Link())
|
||||||
}()
|
|
||||||
|
|
||||||
// Check if branch has no new commits
|
|
||||||
headCommitID, err := gitBaseRepo.GetRefCommitID(pr.GetGitHeadRefName())
|
|
||||||
if err != nil {
|
|
||||||
log.Error("GetRefCommitID: %v", err)
|
|
||||||
ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
branchCommitID, err := gitRepo.GetBranchCommitID(pr.HeadBranch)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("GetBranchCommitID: %v", err)
|
|
||||||
ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if headCommitID != branchCommitID {
|
|
||||||
ctx.Flash.Error(ctx.Tr("repo.branch.delete_branch_has_new_commits", fullBranchName))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteBranch(ctx, pr, gitRepo)
|
|
||||||
}
|
|
||||||
|
|
||||||
func deleteBranch(ctx *context.Context, pr *issues_model.PullRequest, gitRepo *git.Repository) {
|
|
||||||
fullBranchName := pr.HeadRepo.FullName() + ":" + pr.HeadBranch
|
|
||||||
|
|
||||||
if err := repo_service.DeleteBranch(ctx, ctx.Doer, pr.HeadRepo, gitRepo, pr.HeadBranch, pr); err != nil {
|
|
||||||
switch {
|
|
||||||
case git.IsErrBranchNotExist(err):
|
|
||||||
ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName))
|
|
||||||
case errors.Is(err, repo_service.ErrBranchIsDefault):
|
|
||||||
ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName))
|
|
||||||
case errors.Is(err, git_model.ErrBranchIsProtected):
|
|
||||||
ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName))
|
|
||||||
default:
|
|
||||||
log.Error("DeleteBranch: %v", err)
|
|
||||||
ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName))
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Flash.Success(ctx.Tr("repo.branch.deletion_success", fullBranchName))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DownloadPullDiff render a pull's raw diff
|
// DownloadPullDiff render a pull's raw diff
|
||||||
|
|||||||
@@ -46,14 +46,14 @@ func EnableOrDisableWorkflow(ctx *context.APIContext, workflowID string, isEnabl
|
|||||||
|
|
||||||
func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, repo *repo_model.Repository, gitRepo *git.Repository, workflowID, ref string, processInputs func(model *model.WorkflowDispatch, inputs map[string]any) error) error {
|
func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, repo *repo_model.Repository, gitRepo *git.Repository, workflowID, ref string, processInputs func(model *model.WorkflowDispatch, inputs map[string]any) error) error {
|
||||||
if workflowID == "" {
|
if workflowID == "" {
|
||||||
return util.ErrorWrapLocale(
|
return util.ErrorWrapTranslatable(
|
||||||
util.NewNotExistErrorf("workflowID is empty"),
|
util.NewNotExistErrorf("workflowID is empty"),
|
||||||
"actions.workflow.not_found", workflowID,
|
"actions.workflow.not_found", workflowID,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if ref == "" {
|
if ref == "" {
|
||||||
return util.ErrorWrapLocale(
|
return util.ErrorWrapTranslatable(
|
||||||
util.NewNotExistErrorf("ref is empty"),
|
util.NewNotExistErrorf("ref is empty"),
|
||||||
"form.target_ref_not_exist", ref,
|
"form.target_ref_not_exist", ref,
|
||||||
)
|
)
|
||||||
@@ -63,7 +63,7 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re
|
|||||||
cfgUnit := repo.MustGetUnit(ctx, unit.TypeActions)
|
cfgUnit := repo.MustGetUnit(ctx, unit.TypeActions)
|
||||||
cfg := cfgUnit.ActionsConfig()
|
cfg := cfgUnit.ActionsConfig()
|
||||||
if cfg.IsWorkflowDisabled(workflowID) {
|
if cfg.IsWorkflowDisabled(workflowID) {
|
||||||
return util.ErrorWrapLocale(
|
return util.ErrorWrapTranslatable(
|
||||||
util.NewPermissionDeniedErrorf("workflow is disabled"),
|
util.NewPermissionDeniedErrorf("workflow is disabled"),
|
||||||
"actions.workflow.disabled",
|
"actions.workflow.disabled",
|
||||||
)
|
)
|
||||||
@@ -82,7 +82,7 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re
|
|||||||
runTargetCommit, err = gitRepo.GetBranchCommit(ref)
|
runTargetCommit, err = gitRepo.GetBranchCommit(ref)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.ErrorWrapLocale(
|
return util.ErrorWrapTranslatable(
|
||||||
util.NewNotExistErrorf("ref %q doesn't exist", ref),
|
util.NewNotExistErrorf("ref %q doesn't exist", ref),
|
||||||
"form.target_ref_not_exist", ref,
|
"form.target_ref_not_exist", ref,
|
||||||
)
|
)
|
||||||
@@ -122,7 +122,7 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re
|
|||||||
}
|
}
|
||||||
|
|
||||||
if entry == nil {
|
if entry == nil {
|
||||||
return util.ErrorWrapLocale(
|
return util.ErrorWrapTranslatable(
|
||||||
util.NewNotExistErrorf("workflow %q doesn't exist", workflowID),
|
util.NewNotExistErrorf("workflow %q doesn't exist", workflowID),
|
||||||
"actions.workflow.not_found", workflowID,
|
"actions.workflow.not_found", workflowID,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -205,18 +205,6 @@ func handlePullRequestAutoMerge(pullID int64, sha string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var headGitRepo *git.Repository
|
|
||||||
if pr.BaseRepoID == pr.HeadRepoID {
|
|
||||||
headGitRepo = baseGitRepo
|
|
||||||
} else {
|
|
||||||
headGitRepo, err = gitrepo.OpenRepository(ctx, pr.HeadRepo)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("OpenRepository %-v: %v", pr.HeadRepo, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer headGitRepo.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
switch pr.Flow {
|
switch pr.Flow {
|
||||||
case issues_model.PullRequestFlowGithub:
|
case issues_model.PullRequestFlowGithub:
|
||||||
headBranchExist := pr.HeadRepo != nil && gitrepo.IsBranchExist(ctx, pr.HeadRepo, pr.HeadBranch)
|
headBranchExist := pr.HeadRepo != nil && gitrepo.IsBranchExist(ctx, pr.HeadRepo, pr.HeadBranch)
|
||||||
@@ -276,9 +264,12 @@ func handlePullRequestAutoMerge(pullID int64, sha string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if pr.Flow == issues_model.PullRequestFlowGithub && scheduledPRM.DeleteBranchAfterMerge {
|
deleteBranchAfterMerge, err := pull_service.ShouldDeleteBranchAfterMerge(ctx, &scheduledPRM.DeleteBranchAfterMerge, pr.BaseRepo, pr)
|
||||||
if err := repo_service.DeleteBranch(ctx, doer, pr.HeadRepo, headGitRepo, pr.HeadBranch, pr); err != nil {
|
if err != nil {
|
||||||
log.Error("DeletePullRequestHeadBranch: %v", err)
|
log.Error("ShouldDeleteBranchAfterMerge: %v", err)
|
||||||
|
} else if deleteBranchAfterMerge {
|
||||||
|
if err = repo_service.DeleteBranchAfterMerge(ctx, doer, pr.ID, nil); err != nil {
|
||||||
|
log.Error("DeleteBranchAfterMerge: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -540,7 +540,7 @@ type MergePullRequestForm struct {
|
|||||||
HeadCommitID string `json:"head_commit_id,omitempty"`
|
HeadCommitID string `json:"head_commit_id,omitempty"`
|
||||||
ForceMerge bool `json:"force_merge,omitempty"`
|
ForceMerge bool `json:"force_merge,omitempty"`
|
||||||
MergeWhenChecksSucceed bool `json:"merge_when_checks_succeed,omitempty"`
|
MergeWhenChecksSucceed bool `json:"merge_when_checks_succeed,omitempty"`
|
||||||
DeleteBranchAfterMerge bool `json:"delete_branch_after_merge,omitempty"`
|
DeleteBranchAfterMerge *bool `json:"delete_branch_after_merge,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate validates the fields
|
// Validate validates the fields
|
||||||
|
|||||||
@@ -730,3 +730,24 @@ func SetMerged(ctx context.Context, pr *issues_model.PullRequest, mergedCommitID
|
|||||||
return true, nil
|
return true, nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ShouldDeleteBranchAfterMerge(ctx context.Context, userOption *bool, repo *repo_model.Repository, pr *issues_model.PullRequest) (bool, error) {
|
||||||
|
if pr.Flow != issues_model.PullRequestFlowGithub {
|
||||||
|
// only support GitHub workflow (branch-based)
|
||||||
|
// for agit workflow, there is no branch, so nothing to delete
|
||||||
|
// TODO: maybe in the future, it should delete the "agit reference (refs/for/xxxx)"?
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// if user has set an option, respect it
|
||||||
|
if userOption != nil {
|
||||||
|
return *userOption, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise, use repository default
|
||||||
|
prUnit, err := repo.GetUnit(ctx, unit.TypePullRequests)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return prUnit.PullRequestsConfig().DefaultDeleteBranchAfterMerge, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -484,10 +484,7 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, doer *user_m
|
|||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// enmuerates all branch related errors
|
var ErrBranchIsDefault = util.ErrorWrap(util.ErrPermissionDenied, "branch is default")
|
||||||
var (
|
|
||||||
ErrBranchIsDefault = errors.New("branch is default")
|
|
||||||
)
|
|
||||||
|
|
||||||
func CanDeleteBranch(ctx context.Context, repo *repo_model.Repository, branchName string, doer *user_model.User) error {
|
func CanDeleteBranch(ctx context.Context, repo *repo_model.Repository, branchName string, doer *user_model.User) error {
|
||||||
if branchName == repo.DefaultBranch {
|
if branchName == repo.DefaultBranch {
|
||||||
@@ -745,3 +742,89 @@ func GetBranchDivergingInfo(ctx reqctx.RequestContext, baseRepo *repo_model.Repo
|
|||||||
info.BaseHasNewCommits = info.HeadCommitsBehind > 0
|
info.BaseHasNewCommits = info.HeadCommitsBehind > 0
|
||||||
return info, nil
|
return info, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DeleteBranchAfterMerge(ctx context.Context, doer *user_model.User, prID int64, outFullBranchName *string) error {
|
||||||
|
pr, err := issues_model.GetPullRequestByID(ctx, prID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = pr.LoadIssue(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = pr.LoadBaseRepo(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := pr.LoadHeadRepo(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if pr.HeadRepo == nil {
|
||||||
|
// Forked repository has already been deleted
|
||||||
|
return util.ErrorWrapTranslatable(util.ErrNotExist, "repo.branch.deletion_failed", "(deleted-repo):"+pr.HeadBranch)
|
||||||
|
}
|
||||||
|
if err = pr.HeadRepo.LoadOwner(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fullBranchName := pr.HeadRepo.FullName() + ":" + pr.HeadBranch
|
||||||
|
if outFullBranchName != nil {
|
||||||
|
*outFullBranchName = fullBranchName
|
||||||
|
}
|
||||||
|
|
||||||
|
errFailedToDelete := func(err error) error {
|
||||||
|
return util.ErrorWrapTranslatable(err, "repo.branch.deletion_failed", fullBranchName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't clean up unmerged and unclosed PRs and agit PRs
|
||||||
|
if !pr.HasMerged && !pr.Issue.IsClosed && pr.Flow != issues_model.PullRequestFlowGithub {
|
||||||
|
return errFailedToDelete(util.ErrUnprocessableContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't clean up when there are other PR's that use this branch as head branch.
|
||||||
|
exist, err := issues_model.HasUnmergedPullRequestsByHeadInfo(ctx, pr.HeadRepoID, pr.HeadBranch)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if exist {
|
||||||
|
return errFailedToDelete(util.ErrUnprocessableContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := CanDeleteBranch(ctx, pr.HeadRepo, pr.HeadBranch, doer); err != nil {
|
||||||
|
if errors.Is(err, util.ErrPermissionDenied) {
|
||||||
|
return errFailedToDelete(err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
gitBaseRepo, gitBaseCloser, err := gitrepo.RepositoryFromContextOrOpen(ctx, pr.BaseRepo)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer gitBaseCloser.Close()
|
||||||
|
|
||||||
|
gitHeadRepo, gitHeadCloser, err := gitrepo.RepositoryFromContextOrOpen(ctx, pr.HeadRepo)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer gitHeadCloser.Close()
|
||||||
|
|
||||||
|
// Check if branch has no new commits
|
||||||
|
headCommitID, err := gitBaseRepo.GetRefCommitID(pr.GetGitHeadRefName())
|
||||||
|
if err != nil {
|
||||||
|
log.Error("GetRefCommitID: %v", err)
|
||||||
|
return errFailedToDelete(err)
|
||||||
|
}
|
||||||
|
branchCommitID, err := gitHeadRepo.GetBranchCommitID(pr.HeadBranch)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("GetBranchCommitID: %v", err)
|
||||||
|
return errFailedToDelete(err)
|
||||||
|
}
|
||||||
|
if headCommitID != branchCommitID {
|
||||||
|
return util.ErrorWrapTranslatable(util.ErrUnprocessableContent, "repo.branch.delete_branch_has_new_commits", fullBranchName)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = DeleteBranch(ctx, doer, pr.HeadRepo, gitHeadRepo, pr.HeadBranch, pr)
|
||||||
|
if errors.Is(err, util.ErrPermissionDenied) || errors.Is(err, util.ErrNotExist) {
|
||||||
|
return errFailedToDelete(err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="item-section-right">
|
<div class="item-section-right">
|
||||||
<button class="delete-button ui button" data-url="{{.DeleteBranchLink}}">{{ctx.Locale.Tr "repo.branch.delete_html"}}</button>
|
<button class="ui button link-action delete-branch-after-merge" data-url="{{.DeleteBranchLink}}">{{ctx.Locale.Tr "repo.branch.delete_html"}}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{{if and .IsPullBranchDeletable (not .IsPullRequestBroken)}}
|
{{if and .IsPullBranchDeletable (not .IsPullRequestBroken)}}
|
||||||
<div class="item-section-right">
|
<div class="item-section-right">
|
||||||
<button class="delete-button ui button" data-url="{{.DeleteBranchLink}}">{{ctx.Locale.Tr "repo.branch.delete_html"}}</button>
|
<button class="ui button link-action delete-branch-after-merge" data-url="{{.DeleteBranchLink}}">{{ctx.Locale.Tr "repo.branch.delete_html"}}</button>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ func TestAPIRepoIssueConfigPaths(t *testing.T) {
|
|||||||
configData, err := yaml.Marshal(configMap)
|
configData, err := yaml.Marshal(configMap)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
_, err = createFileInBranch(owner, repo, fullPath, repo.DefaultBranch, string(configData))
|
_, err = createFile(owner, repo, fullPath, string(configData))
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
issueConfig := getIssueConfig(t, owner.Name, repo.Name)
|
issueConfig := getIssueConfig(t, owner.Name, repo.Name)
|
||||||
|
|||||||
@@ -17,19 +17,23 @@ import (
|
|||||||
issues_model "code.gitea.io/gitea/models/issues"
|
issues_model "code.gitea.io/gitea/models/issues"
|
||||||
"code.gitea.io/gitea/models/perm"
|
"code.gitea.io/gitea/models/perm"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
|
unit_model "code.gitea.io/gitea/models/unit"
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
"code.gitea.io/gitea/services/convert"
|
"code.gitea.io/gitea/services/convert"
|
||||||
"code.gitea.io/gitea/services/forms"
|
"code.gitea.io/gitea/services/forms"
|
||||||
"code.gitea.io/gitea/services/gitdiff"
|
"code.gitea.io/gitea/services/gitdiff"
|
||||||
issue_service "code.gitea.io/gitea/services/issue"
|
issue_service "code.gitea.io/gitea/services/issue"
|
||||||
pull_service "code.gitea.io/gitea/services/pull"
|
pull_service "code.gitea.io/gitea/services/pull"
|
||||||
|
repo_service "code.gitea.io/gitea/services/repository"
|
||||||
files_service "code.gitea.io/gitea/services/repository/files"
|
files_service "code.gitea.io/gitea/services/repository/files"
|
||||||
"code.gitea.io/gitea/tests"
|
"code.gitea.io/gitea/tests"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAPIViewPulls(t *testing.T) {
|
func TestAPIViewPulls(t *testing.T) {
|
||||||
@@ -186,6 +190,76 @@ func TestAPIMergePullWIP(t *testing.T) {
|
|||||||
MakeRequest(t, req, http.StatusMethodNotAllowed)
|
MakeRequest(t, req, http.StatusMethodNotAllowed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAPIMergePull(t *testing.T) {
|
||||||
|
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
|
||||||
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||||
|
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||||
|
apiCtx := NewAPITestContext(t, repo.OwnerName, repo.Name, auth_model.AccessTokenScopeWriteRepository)
|
||||||
|
|
||||||
|
checkBranchExists := func(t *testing.T, branchName string, status int) {
|
||||||
|
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/branches/%s", owner.Name, repo.Name, branchName)).AddTokenAuth(apiCtx.Token)
|
||||||
|
MakeRequest(t, req, status)
|
||||||
|
}
|
||||||
|
|
||||||
|
createTestBranchPR := func(t *testing.T, branchName string) *api.PullRequest {
|
||||||
|
testCreateFileInBranch(t, owner, repo, createFileInBranchOptions{NewBranch: branchName}, map[string]string{"a-new-file-" + branchName + ".txt": "dummy content"})
|
||||||
|
prDTO, err := doAPICreatePullRequest(apiCtx, repo.OwnerName, repo.Name, repo.DefaultBranch, branchName)(t)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return &prDTO
|
||||||
|
}
|
||||||
|
|
||||||
|
performMerge := func(t *testing.T, prIndex int64, params map[string]any, optExpectedStatus ...int) {
|
||||||
|
req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/merge", owner.Name, repo.Name, prIndex), params).AddTokenAuth(apiCtx.Token)
|
||||||
|
expectedStatus := util.OptionalArg(optExpectedStatus, http.StatusOK)
|
||||||
|
MakeRequest(t, req, expectedStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("Normal", func(t *testing.T) {
|
||||||
|
newBranch := "test-pull-1"
|
||||||
|
prDTO := createTestBranchPR(t, newBranch)
|
||||||
|
performMerge(t, prDTO.Index, map[string]any{"do": "merge"})
|
||||||
|
checkBranchExists(t, newBranch, http.StatusOK)
|
||||||
|
// try to merge again, make sure we cannot perform a merge on the same PR
|
||||||
|
performMerge(t, prDTO.Index, map[string]any{"do": "merge"}, http.StatusMethodNotAllowed)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("DeleteBranchAfterMergePassedByFormField", func(t *testing.T) {
|
||||||
|
newBranch := "test-pull-2"
|
||||||
|
prDTO := createTestBranchPR(t, newBranch)
|
||||||
|
performMerge(t, prDTO.Index, map[string]any{"do": "merge", "delete_branch_after_merge": true})
|
||||||
|
checkBranchExists(t, newBranch, http.StatusNotFound)
|
||||||
|
})
|
||||||
|
|
||||||
|
updateRepoUnitDefaultDeleteBranchAfterMerge := func(t *testing.T, repo *repo_model.Repository, value bool) {
|
||||||
|
prUnit, err := repo.GetUnit(t.Context(), unit_model.TypePullRequests)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
prUnit.PullRequestsConfig().DefaultDeleteBranchAfterMerge = value
|
||||||
|
require.NoError(t, repo_service.UpdateRepositoryUnits(t.Context(), repo, []repo_model.RepoUnit{{
|
||||||
|
RepoID: repo.ID,
|
||||||
|
Type: unit_model.TypePullRequests,
|
||||||
|
Config: prUnit.PullRequestsConfig(),
|
||||||
|
}}, nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("DeleteBranchAfterMergePassedByRepoSettings", func(t *testing.T) {
|
||||||
|
newBranch := "test-pull-3"
|
||||||
|
prDTO := createTestBranchPR(t, newBranch)
|
||||||
|
updateRepoUnitDefaultDeleteBranchAfterMerge(t, repo, true)
|
||||||
|
performMerge(t, prDTO.Index, map[string]any{"do": "merge"})
|
||||||
|
checkBranchExists(t, newBranch, http.StatusNotFound)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("DeleteBranchAfterMergeFormFieldIsSetButNotRepoSettings", func(t *testing.T) {
|
||||||
|
newBranch := "test-pull-4"
|
||||||
|
prDTO := createTestBranchPR(t, newBranch)
|
||||||
|
updateRepoUnitDefaultDeleteBranchAfterMerge(t, repo, false)
|
||||||
|
performMerge(t, prDTO.Index, map[string]any{"do": "merge", "delete_branch_after_merge": true})
|
||||||
|
checkBranchExists(t, newBranch, http.StatusNotFound)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestAPICreatePullSuccess(t *testing.T) {
|
func TestAPICreatePullSuccess(t *testing.T) {
|
||||||
defer tests.PrepareTestEnv(t)()
|
defer tests.PrepareTestEnv(t)()
|
||||||
repo10 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10})
|
repo10 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10})
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ func BenchmarkAPICreateFileSmall(b *testing.B) {
|
|||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
for n := 0; b.Loop(); n++ {
|
for n := 0; b.Loop(); n++ {
|
||||||
treePath := fmt.Sprintf("update/file%d.txt", n)
|
treePath := fmt.Sprintf("update/file%d.txt", n)
|
||||||
_, _ = createFileInBranch(user2, repo1, treePath, repo1.DefaultBranch, treePath)
|
_, _ = createFile(user2, repo1, treePath)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -149,7 +149,7 @@ func BenchmarkAPICreateFileMedium(b *testing.B) {
|
|||||||
for n := 0; b.Loop(); n++ {
|
for n := 0; b.Loop(); n++ {
|
||||||
treePath := fmt.Sprintf("update/file%d.txt", n)
|
treePath := fmt.Sprintf("update/file%d.txt", n)
|
||||||
copy(data, treePath)
|
copy(data, treePath)
|
||||||
_, _ = createFileInBranch(user2, repo1, treePath, repo1.DefaultBranch, treePath)
|
_, _ = createFile(user2, repo1, treePath)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,26 +6,36 @@ package integration
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"strings"
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
files_service "code.gitea.io/gitea/services/repository/files"
|
files_service "code.gitea.io/gitea/services/repository/files"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func createFileInBranch(user *user_model.User, repo *repo_model.Repository, treePath, branchName, content string) (*api.FilesResponse, error) {
|
type createFileInBranchOptions struct {
|
||||||
|
OldBranch, NewBranch string
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCreateFileInBranch(t *testing.T, user *user_model.User, repo *repo_model.Repository, createOpts createFileInBranchOptions, files map[string]string) *api.FilesResponse {
|
||||||
|
resp, err := createFileInBranch(user, repo, createOpts, files)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
func createFileInBranch(user *user_model.User, repo *repo_model.Repository, createOpts createFileInBranchOptions, files map[string]string) (*api.FilesResponse, error) {
|
||||||
ctx := context.TODO()
|
ctx := context.TODO()
|
||||||
opts := &files_service.ChangeRepoFilesOptions{
|
opts := &files_service.ChangeRepoFilesOptions{OldBranch: createOpts.OldBranch, NewBranch: createOpts.NewBranch}
|
||||||
Files: []*files_service.ChangeRepoFile{
|
for path, content := range files {
|
||||||
{
|
opts.Files = append(opts.Files, &files_service.ChangeRepoFile{
|
||||||
Operation: "create",
|
Operation: "create",
|
||||||
TreePath: treePath,
|
TreePath: path,
|
||||||
ContentReader: strings.NewReader(content),
|
ContentReader: strings.NewReader(content),
|
||||||
},
|
})
|
||||||
},
|
|
||||||
OldBranch: branchName,
|
|
||||||
Author: nil,
|
|
||||||
Committer: nil,
|
|
||||||
}
|
}
|
||||||
return files_service.ChangeRepoFiles(ctx, repo, user, opts)
|
return files_service.ChangeRepoFiles(ctx, repo, user, opts)
|
||||||
}
|
}
|
||||||
@@ -53,10 +63,12 @@ func createOrReplaceFileInBranch(user *user_model.User, repo *repo_model.Reposit
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = createFileInBranch(user, repo, treePath, branchName, content)
|
_, err = createFileInBranch(user, repo, createFileInBranchOptions{OldBranch: branchName}, map[string]string{treePath: content})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func createFile(user *user_model.User, repo *repo_model.Repository, treePath string) (*api.FilesResponse, error) {
|
// TODO: replace all usages of this function with testCreateFileInBranch or testCreateFile
|
||||||
return createFileInBranch(user, repo, treePath, repo.DefaultBranch, "This is a NEW file")
|
func createFile(user *user_model.User, repo *repo_model.Repository, treePath string, optContent ...string) (*api.FilesResponse, error) {
|
||||||
|
content := util.OptionalArg(optContent, "This is a NEW file") // some tests need this default content because its SHA is hardcoded
|
||||||
|
return createFileInBranch(user, repo, createFileInBranchOptions{}, map[string]string{treePath: content})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ func testPullCleanUp(t *testing.T, session *TestSession, user, repo, pullnum str
|
|||||||
|
|
||||||
// Click the little button to create a pull
|
// Click the little button to create a pull
|
||||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||||
link, exists := htmlDoc.doc.Find(".timeline-item .delete-button").Attr("data-url")
|
link, exists := htmlDoc.doc.Find(".timeline-item .delete-branch-after-merge").Attr("data-url")
|
||||||
assert.True(t, exists, "The template has changed, can not find delete button url")
|
assert.True(t, exists, "The template has changed, can not find delete button url")
|
||||||
req = NewRequestWithValues(t, "POST", link, map[string]string{
|
req = NewRequestWithValues(t, "POST", link, map[string]string{
|
||||||
"_csrf": htmlDoc.GetCSRF(),
|
"_csrf": htmlDoc.GetCSRF(),
|
||||||
|
|||||||
Reference in New Issue
Block a user