Skip to content

Commit c9e7fde

Browse files
robgonnellaRob Gonnellawxiaoguang
authored
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 <[email protected]> Co-authored-by: wxiaoguang <[email protected]>
1 parent ad2ff67 commit c9e7fde

File tree

11 files changed

+159
-46
lines changed

11 files changed

+159
-46
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ __debug_bin*
2525
# Visual Studio
2626
/.vs/
2727

28+
# mise version managment tool
29+
mise.toml
30+
2831
*.cgo1.go
2932
*.cgo2.c
3033
_cgo_defun.c
@@ -121,4 +124,3 @@ prime/
121124
/AGENT.md
122125
/CLAUDE.md
123126
/llms.txt
124-

modules/git/error.go

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -98,28 +98,31 @@ func (err *ErrPushRejected) Unwrap() error {
9898

9999
// GenerateMessage generates the remote message from the stderr
100100
func (err *ErrPushRejected) GenerateMessage() {
101-
messageBuilder := &strings.Builder{}
102-
i := strings.Index(err.StdErr, "remote: ")
103-
if i < 0 {
104-
err.Message = ""
101+
// The stderr is like this:
102+
//
103+
// > remote: error: push is rejected .....
104+
// > To /work/gitea/tests/integration/gitea-integration-sqlite/gitea-repositories/user2/repo1.git
105+
// > ! [remote rejected] 44e67c77559211d21b630b902cdcc6ab9d4a4f51 -> develop (pre-receive hook declined)
106+
// > error: failed to push some refs to '/work/gitea/tests/integration/gitea-integration-sqlite/gitea-repositories/user2/repo1.git'
107+
//
108+
// The local message contains sensitive information, so we only need the remote message
109+
const prefixRemote = "remote: "
110+
const prefixError = "error: "
111+
pos := strings.Index(err.StdErr, prefixRemote)
112+
if pos < 0 {
113+
err.Message = "push is rejected"
105114
return
106115
}
107-
for {
108-
if len(err.StdErr) <= i+8 {
109-
break
110-
}
111-
if err.StdErr[i:i+8] != "remote: " {
112-
break
113-
}
114-
i += 8
115-
nl := strings.IndexByte(err.StdErr[i:], '\n')
116-
if nl >= 0 {
117-
messageBuilder.WriteString(err.StdErr[i : i+nl+1])
118-
i = i + nl + 1
119-
} else {
120-
messageBuilder.WriteString(err.StdErr[i:])
121-
i = len(err.StdErr)
116+
117+
messageBuilder := &strings.Builder{}
118+
lines := strings.SplitSeq(err.StdErr, "\n")
119+
for line := range lines {
120+
line, ok := strings.CutPrefix(line, prefixRemote)
121+
if !ok {
122+
continue
122123
}
124+
line = strings.TrimPrefix(line, prefixError)
125+
messageBuilder.WriteString(strings.TrimSpace(line) + "\n")
123126
}
124127
err.Message = strings.TrimSpace(messageBuilder.String())
125128
}

modules/structs/repo_file.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@ import "time"
88

99
// FileOptions options for all file APIs
1010
type FileOptions struct {
11-
// message (optional) for the commit of this file. if not supplied, a default message will be used
11+
// message (optional) is the commit message of the changes. If not supplied, a default message will be used
1212
Message string `json:"message"`
13-
// branch (optional) to base this file from. if not given, the default branch is used
13+
// branch (optional) is the base branch for the changes. If not supplied, the default branch is used
1414
BranchName string `json:"branch" binding:"GitRefName;MaxSize(100)"`
15-
// new_branch (optional) will make a new branch from `branch` before creating the file
15+
// 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
1616
NewBranchName string `json:"new_branch" binding:"GitRefName;MaxSize(100)"`
17+
// force_push (optional) will do a force-push if the new branch already exists
18+
ForcePush bool `json:"force_push"`
1719
// `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)
1820
Author Identity `json:"author"`
1921
Committer Identity `json:"committer"`

routers/api/v1/repo/file.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,7 @@ func ReqChangeRepoFileOptionsAndCheck(ctx *context.APIContext) {
355355
Message: commonOpts.Message,
356356
OldBranch: commonOpts.BranchName,
357357
NewBranch: commonOpts.NewBranchName,
358+
ForcePush: commonOpts.ForcePush,
358359
Committer: &files_service.IdentityOptions{
359360
GitUserName: commonOpts.Committer.Name,
360361
GitUserEmail: commonOpts.Committer.Email,
@@ -591,6 +592,11 @@ func UpdateFile(ctx *context.APIContext) {
591592
}
592593

593594
func handleChangeRepoFilesError(ctx *context.APIContext, err error) {
595+
if git.IsErrPushRejected(err) {
596+
err := err.(*git.ErrPushRejected)
597+
ctx.APIError(http.StatusForbidden, err.Message)
598+
return
599+
}
594600
if files_service.IsErrUserCannotCommit(err) || pull_service.IsErrFilePathProtected(err) {
595601
ctx.APIError(http.StatusForbidden, err)
596602
return

services/packages/cargo/index.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ func alterRepositoryContent(ctx context.Context, doer *user_model.User, repo *re
306306
return err
307307
}
308308

309-
return t.Push(ctx, doer, commitHash, repo.DefaultBranch)
309+
return t.Push(ctx, doer, commitHash, repo.DefaultBranch, false)
310310
}
311311

312312
func writeObjectToIndex(ctx context.Context, t *files_service.TemporaryUploadRepository, path string, r io.Reader) error {

services/repository/files/cherry_pick.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ func CherryPick(ctx context.Context, repo *repo_model.Repository, doer *user_mod
131131
}
132132

133133
// Then push this tree to NewBranch
134-
if err := t.Push(ctx, doer, commitHash, opts.NewBranch); err != nil {
134+
if err := t.Push(ctx, doer, commitHash, opts.NewBranch, false); err != nil {
135135
return nil, err
136136
}
137137

services/repository/files/patch.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ func ApplyDiffPatch(ctx context.Context, repo *repo_model.Repository, doer *user
201201
}
202202

203203
// Then push this tree to NewBranch
204-
if err := t.Push(ctx, doer, commitHash, opts.NewBranch); err != nil {
204+
if err := t.Push(ctx, doer, commitHash, opts.NewBranch, false); err != nil {
205205
return nil, err
206206
}
207207

services/repository/files/temp_repo.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -354,20 +354,18 @@ func (t *TemporaryUploadRepository) CommitTree(ctx context.Context, opts *Commit
354354
}
355355

356356
// Push the provided commitHash to the repository branch by the provided user
357-
func (t *TemporaryUploadRepository) Push(ctx context.Context, doer *user_model.User, commitHash, branch string) error {
357+
func (t *TemporaryUploadRepository) Push(ctx context.Context, doer *user_model.User, commitHash, branch string, force bool) error {
358358
// Because calls hooks we need to pass in the environment
359359
env := repo_module.PushingEnvironment(doer, t.repo)
360360
if err := git.Push(ctx, t.basePath, git.PushOptions{
361361
Remote: t.repo.RepoPath(),
362362
Branch: strings.TrimSpace(commitHash) + ":" + git.BranchPrefix + strings.TrimSpace(branch),
363363
Env: env,
364+
Force: force,
364365
}); err != nil {
365366
if git.IsErrPushOutOfDate(err) {
366367
return err
367368
} else if git.IsErrPushRejected(err) {
368-
rejectErr := err.(*git.ErrPushRejected)
369-
log.Info("Unable to push back to repo from temporary repo due to rejection: %s (%s)\nStdout: %s\nStderr: %s\nError: %v",
370-
t.repo.FullName(), t.basePath, rejectErr.StdOut, rejectErr.StdErr, rejectErr.Err)
371369
return err
372370
}
373371
log.Error("Unable to push back to repo from temporary repo: %s (%s)\nError: %v",

services/repository/files/update.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ type ChangeRepoFilesOptions struct {
6060
Committer *IdentityOptions
6161
Dates *CommitDateOptions
6262
Signoff bool
63+
ForcePush bool
6364
}
6465

6566
type RepoFileOptions struct {
@@ -176,8 +177,11 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
176177
return nil, err
177178
}
178179
if exist {
179-
return nil, git_model.ErrBranchAlreadyExists{
180-
BranchName: opts.NewBranch,
180+
if !opts.ForcePush {
181+
// branch exists but force option not set
182+
return nil, git_model.ErrBranchAlreadyExists{
183+
BranchName: opts.NewBranch,
184+
}
181185
}
182186
}
183187
} 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
303307
}
304308

305309
// Then push this tree to NewBranch
306-
if err := t.Push(ctx, doer, commitHash, opts.NewBranch); err != nil {
307-
log.Error("%T %v", err, err)
310+
if err := t.Push(ctx, doer, commitHash, opts.NewBranch, opts.ForcePush); err != nil {
308311
return nil, err
309312
}
310313

templates/swagger/v1_json.tmpl

Lines changed: 32 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)