feat: adds option to force update new branch in contents routes (#35592)

Allows users to specify a "force" option in API /contents routes when
modifying files in a new branch. When "force" is true, and the branch
already exists, a force push will occur provided the branch does not
have a branch protection rule that disables force pushing.

This is useful as a way to manage a branch remotely through only the
API. For example in an automated release tool you can pull commits,
analyze, and update a release PR branch all remotely without needing to
clone or perform any local git operations.

Resolve #35538

---------

Co-authored-by: Rob Gonnella <rob.gonnella@papayapay.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
Rob Gonnella
2025-10-07 00:23:14 -04:00
committed by GitHub
parent ad2ff67343
commit c9e7fde8b3
11 changed files with 160 additions and 47 deletions

View File

@@ -306,7 +306,7 @@ func alterRepositoryContent(ctx context.Context, doer *user_model.User, repo *re
return err
}
return t.Push(ctx, doer, commitHash, repo.DefaultBranch)
return t.Push(ctx, doer, commitHash, repo.DefaultBranch, false)
}
func writeObjectToIndex(ctx context.Context, t *files_service.TemporaryUploadRepository, path string, r io.Reader) error {

View File

@@ -131,7 +131,7 @@ func CherryPick(ctx context.Context, repo *repo_model.Repository, doer *user_mod
}
// Then push this tree to NewBranch
if err := t.Push(ctx, doer, commitHash, opts.NewBranch); err != nil {
if err := t.Push(ctx, doer, commitHash, opts.NewBranch, false); err != nil {
return nil, err
}

View File

@@ -201,7 +201,7 @@ func ApplyDiffPatch(ctx context.Context, repo *repo_model.Repository, doer *user
}
// Then push this tree to NewBranch
if err := t.Push(ctx, doer, commitHash, opts.NewBranch); err != nil {
if err := t.Push(ctx, doer, commitHash, opts.NewBranch, false); err != nil {
return nil, err
}

View File

@@ -354,20 +354,18 @@ func (t *TemporaryUploadRepository) CommitTree(ctx context.Context, opts *Commit
}
// Push the provided commitHash to the repository branch by the provided user
func (t *TemporaryUploadRepository) Push(ctx context.Context, doer *user_model.User, commitHash, branch string) error {
func (t *TemporaryUploadRepository) Push(ctx context.Context, doer *user_model.User, commitHash, branch string, force bool) error {
// Because calls hooks we need to pass in the environment
env := repo_module.PushingEnvironment(doer, t.repo)
if err := git.Push(ctx, t.basePath, git.PushOptions{
Remote: t.repo.RepoPath(),
Branch: strings.TrimSpace(commitHash) + ":" + git.BranchPrefix + strings.TrimSpace(branch),
Env: env,
Force: force,
}); err != nil {
if git.IsErrPushOutOfDate(err) {
return err
} else if git.IsErrPushRejected(err) {
rejectErr := err.(*git.ErrPushRejected)
log.Info("Unable to push back to repo from temporary repo due to rejection: %s (%s)\nStdout: %s\nStderr: %s\nError: %v",
t.repo.FullName(), t.basePath, rejectErr.StdOut, rejectErr.StdErr, rejectErr.Err)
return err
}
log.Error("Unable to push back to repo from temporary repo: %s (%s)\nError: %v",

View File

@@ -60,6 +60,7 @@ type ChangeRepoFilesOptions struct {
Committer *IdentityOptions
Dates *CommitDateOptions
Signoff bool
ForcePush bool
}
type RepoFileOptions struct {
@@ -176,8 +177,11 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
return nil, err
}
if exist {
return nil, git_model.ErrBranchAlreadyExists{
BranchName: opts.NewBranch,
if !opts.ForcePush {
// branch exists but force option not set
return nil, git_model.ErrBranchAlreadyExists{
BranchName: opts.NewBranch,
}
}
}
} else if err := VerifyBranchProtection(ctx, repo, doer, opts.OldBranch, treePaths); err != nil {
@@ -303,8 +307,7 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
}
// Then push this tree to NewBranch
if err := t.Push(ctx, doer, commitHash, opts.NewBranch); err != nil {
log.Error("%T %v", err, err)
if err := t.Push(ctx, doer, commitHash, opts.NewBranch, opts.ForcePush); err != nil {
return nil, err
}