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

@@ -98,28 +98,31 @@ func (err *ErrPushRejected) Unwrap() error {
// GenerateMessage generates the remote message from the stderr
func (err *ErrPushRejected) GenerateMessage() {
messageBuilder := &strings.Builder{}
i := strings.Index(err.StdErr, "remote: ")
if i < 0 {
err.Message = ""
// The stderr is like this:
//
// > remote: error: push is rejected .....
// > To /work/gitea/tests/integration/gitea-integration-sqlite/gitea-repositories/user2/repo1.git
// > ! [remote rejected] 44e67c77559211d21b630b902cdcc6ab9d4a4f51 -> develop (pre-receive hook declined)
// > error: failed to push some refs to '/work/gitea/tests/integration/gitea-integration-sqlite/gitea-repositories/user2/repo1.git'
//
// The local message contains sensitive information, so we only need the remote message
const prefixRemote = "remote: "
const prefixError = "error: "
pos := strings.Index(err.StdErr, prefixRemote)
if pos < 0 {
err.Message = "push is rejected"
return
}
for {
if len(err.StdErr) <= i+8 {
break
}
if err.StdErr[i:i+8] != "remote: " {
break
}
i += 8
nl := strings.IndexByte(err.StdErr[i:], '\n')
if nl >= 0 {
messageBuilder.WriteString(err.StdErr[i : i+nl+1])
i = i + nl + 1
} else {
messageBuilder.WriteString(err.StdErr[i:])
i = len(err.StdErr)
messageBuilder := &strings.Builder{}
lines := strings.SplitSeq(err.StdErr, "\n")
for line := range lines {
line, ok := strings.CutPrefix(line, prefixRemote)
if !ok {
continue
}
line = strings.TrimPrefix(line, prefixError)
messageBuilder.WriteString(strings.TrimSpace(line) + "\n")
}
err.Message = strings.TrimSpace(messageBuilder.String())
}

View File

@@ -8,12 +8,14 @@ import "time"
// FileOptions options for all file APIs
type FileOptions struct {
// message (optional) for the commit of this file. if not supplied, a default message will be used
// message (optional) is the commit message of the changes. If not supplied, a default message will be used
Message string `json:"message"`
// branch (optional) to base this file from. if not given, the default branch is used
// branch (optional) is the base branch for the changes. If not supplied, the default branch is used
BranchName string `json:"branch" binding:"GitRefName;MaxSize(100)"`
// new_branch (optional) will make a new branch from `branch` before creating the file
// new_branch (optional) will make a new branch from base branch for the changes. If not supplied, the changes will be committed to the base branch
NewBranchName string `json:"new_branch" binding:"GitRefName;MaxSize(100)"`
// force_push (optional) will do a force-push if the new branch already exists
ForcePush bool `json:"force_push"`
// `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)
Author Identity `json:"author"`
Committer Identity `json:"committer"`