-
-
Notifications
You must be signed in to change notification settings - Fork 6.2k
Support updating branch via API #35951
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 5 commits
f467a9d
2d34bdb
2ad3451
45a4056
c8f5aa9
f424bab
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -380,6 +380,91 @@ func ListBranches(ctx *context.APIContext) { | |
| ctx.JSON(http.StatusOK, apiBranches) | ||
| } | ||
|
|
||
| // UpdateBranch moves a branch reference to a new commit. | ||
| func UpdateBranch(ctx *context.APIContext) { | ||
| // swagger:operation PUT /repos/{owner}/{repo}/branches/{branch} repository repoUpdateBranch | ||
| // --- | ||
| // summary: Update a branch reference to a new commit | ||
| // consumes: | ||
| // - application/json | ||
| // produces: | ||
| // - application/json | ||
| // parameters: | ||
| // - name: owner | ||
| // in: path | ||
| // description: owner of the repo | ||
| // type: string | ||
| // required: true | ||
| // - name: repo | ||
| // in: path | ||
| // description: name of the repo | ||
| // type: string | ||
| // required: true | ||
| // - name: branch | ||
| // in: path | ||
| // description: name of the branch | ||
| // type: string | ||
| // required: true | ||
| // - name: body | ||
| // in: body | ||
| // schema: | ||
| // "$ref": "#/definitions/UpdateBranchRepoOption" | ||
| // responses: | ||
| // "204": | ||
| // "$ref": "#/responses/empty" | ||
| // "403": | ||
| // "$ref": "#/responses/forbidden" | ||
| // "404": | ||
| // "$ref": "#/responses/notFound" | ||
| // "409": | ||
| // "$ref": "#/responses/conflict" | ||
| // "422": | ||
| // "$ref": "#/responses/validationError" | ||
|
|
||
| opt := web.GetForm(ctx).(*api.UpdateBranchRepoOption) | ||
|
|
||
| branchName := ctx.PathParam("*") | ||
| repo := ctx.Repo.Repository | ||
|
|
||
| if repo.IsEmpty { | ||
| ctx.APIError(http.StatusNotFound, "Git Repository is empty.") | ||
| return | ||
| } | ||
|
|
||
| if repo.IsMirror { | ||
| ctx.APIError(http.StatusForbidden, "Git Repository is a mirror.") | ||
| return | ||
| } | ||
|
|
||
| if ctx.Repo.GitRepo == nil { | ||
| ctx.APIErrorInternal(nil) | ||
| return | ||
| } | ||
|
|
||
| if err := repo_service.UpdateBranch(ctx, repo, ctx.Doer, branchName, opt.NewCommitID, opt.OldCommitID, opt.Force); err != nil { | ||
| switch { | ||
| case git_model.IsErrBranchNotExist(err): | ||
| ctx.APIError(http.StatusNotFound, "Branch doesn't exist.") | ||
| case repo_service.IsErrBranchCommitDoesNotMatch(err): | ||
| ctx.APIError(http.StatusConflict, err) | ||
| case git.IsErrPushOutOfDate(err): | ||
| ctx.APIError(http.StatusConflict, "The update is not a fast-forward.") | ||
| case git.IsErrPushRejected(err): | ||
| rej := err.(*git.ErrPushRejected) | ||
| ctx.APIError(http.StatusForbidden, rej.Message) | ||
| case repo_model.IsErrUserDoesNotHaveAccessToRepo(err): | ||
| ctx.APIError(http.StatusForbidden, err) | ||
| case git.IsErrNotExist(err): | ||
| ctx.APIError(http.StatusUnprocessableEntity, err) | ||
| default: | ||
| ctx.APIErrorInternal(err) | ||
| } | ||
|
Comment on lines
441
to
453
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Most errors should be able to unwrapped to |
||
| return | ||
| } | ||
|
|
||
| ctx.Status(http.StatusNoContent) | ||
| } | ||
|
|
||
| // RenameBranch renames a repository's branch. | ||
| func RenameBranch(ctx *context.APIContext) { | ||
| // swagger:operation PATCH /repos/{owner}/{repo}/branches/{branch} repository repoRenameBranch | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -32,6 +32,7 @@ import ( | |
| webhook_module "code.gitea.io/gitea/modules/webhook" | ||
| actions_service "code.gitea.io/gitea/services/actions" | ||
| notify_service "code.gitea.io/gitea/services/notify" | ||
| pull_service "code.gitea.io/gitea/services/pull" | ||
| release_service "code.gitea.io/gitea/services/release" | ||
|
|
||
| "xorm.io/builder" | ||
|
|
@@ -483,8 +484,150 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, doer *user_m | |
| return "", nil | ||
| } | ||
|
|
||
| // UpdateBranch moves a branch reference to the provided commit. | ||
| func UpdateBranch(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, branchName, newCommitID, expectedOldCommitID string, force bool) error { | ||
| if err := repo.MustNotBeArchived(); err != nil { | ||
| return err | ||
| } | ||
|
|
||
| perm, err := access_model.GetUserRepoPermission(ctx, repo, doer) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| if !perm.CanWrite(unit.TypeCode) { | ||
| return repo_model.ErrUserDoesNotHaveAccessToRepo{ | ||
| UserID: doer.ID, | ||
| RepoName: repo.LowerName, | ||
| } | ||
| } | ||
|
|
||
| gitRepo, err := gitrepo.OpenRepository(ctx, repo) | ||
| if err != nil { | ||
| return fmt.Errorf("OpenRepository: %w", err) | ||
| } | ||
| defer gitRepo.Close() | ||
|
|
||
| branchCommit, err := gitRepo.GetBranchCommit(branchName) | ||
| if err != nil { | ||
| if git.IsErrNotExist(err) { | ||
| return git_model.ErrBranchNotExist{RepoID: repo.ID, BranchName: branchName} | ||
| } | ||
lunny marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| return err | ||
| } | ||
| currentCommitID := branchCommit.ID.String() | ||
|
|
||
| if expectedOldCommitID != "" { | ||
| expectedID, err := gitRepo.ConvertToGitID(expectedOldCommitID) | ||
| if err != nil { | ||
| return fmt.Errorf("ConvertToGitID(old): %w", err) | ||
| } | ||
| if expectedID.String() != currentCommitID { | ||
| return ErrBranchCommitDoesNotMatch{Expected: currentCommitID, Given: expectedID.String()} | ||
| } | ||
| } | ||
|
|
||
| newID, err := gitRepo.ConvertToGitID(newCommitID) | ||
| if err != nil { | ||
| return fmt.Errorf("ConvertToGitID(new): %w", err) | ||
| } | ||
| newCommit, err := gitRepo.GetCommit(newID.String()) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| if newCommit.ID.String() == currentCommitID { | ||
| return nil | ||
| } | ||
|
|
||
| isForcePush, err := newCommit.IsForcePush(currentCommitID) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| if isForcePush && !force { | ||
| return &git.ErrPushOutOfDate{Err: errors.New("non fast-forward update requires force"), StdErr: "non-fast-forward", StdOut: ""} | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why manually construct a so complex error, and its fields are not really used? |
||
| } | ||
|
|
||
| pushEnv := repo_module.PushingEnvironment(doer, repo) | ||
|
|
||
| protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, repo.ID, branchName) | ||
| if err != nil { | ||
| return fmt.Errorf("GetFirstMatchProtectedBranchRule: %w", err) | ||
| } | ||
| if protectedBranch != nil { | ||
| protectedBranch.Repo = repo | ||
|
Comment on lines
+526
to
+533
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Where is the code copied from? I believe it's not the first time to duplicate such "protection" check. |
||
| globsProtected := protectedBranch.GetProtectedFilePatterns() | ||
| if len(globsProtected) > 0 { | ||
| changedProtectedFiles, protectErr := pull_service.CheckFileProtection(gitRepo, branchName, currentCommitID, newCommit.ID.String(), globsProtected, 1, pushEnv) | ||
| if protectErr != nil { | ||
| if !pull_service.IsErrFilePathProtected(protectErr) { | ||
| return fmt.Errorf("CheckFileProtection: %w", protectErr) | ||
| } | ||
| protectedPath := "" | ||
| if len(changedProtectedFiles) > 0 { | ||
| protectedPath = changedProtectedFiles[0] | ||
| } else if pathErr, ok := protectErr.(pull_service.ErrFilePathProtected); ok { | ||
| protectedPath = pathErr.Path | ||
| } | ||
| if protectedPath == "" { | ||
| protectedPath = branchName | ||
| } | ||
| return &git.ErrPushRejected{Message: fmt.Sprintf("branch %s is protected from changing file %s", branchName, protectedPath)} | ||
| } | ||
| } | ||
|
|
||
| if isForcePush { | ||
| if !protectedBranch.CanUserForcePush(ctx, doer) { | ||
| return &git.ErrPushRejected{Message: "Not allowed to force-push to protected branch " + branchName} | ||
| } | ||
| } else if !protectedBranch.CanUserPush(ctx, doer) { | ||
| globsUnprotected := protectedBranch.GetUnprotectedFilePatterns() | ||
| if len(globsUnprotected) > 0 { | ||
| unprotectedOnly, unprotectedErr := pull_service.CheckUnprotectedFiles(gitRepo, branchName, currentCommitID, newCommit.ID.String(), globsUnprotected, pushEnv) | ||
| if unprotectedErr != nil { | ||
| return fmt.Errorf("CheckUnprotectedFiles: %w", unprotectedErr) | ||
| } | ||
| if !unprotectedOnly { | ||
| return &git.ErrPushRejected{Message: "Not allowed to push to protected branch " + branchName} | ||
| } | ||
| } else { | ||
| return &git.ErrPushRejected{Message: "Not allowed to push to protected branch " + branchName} | ||
| } | ||
| } | ||
| } | ||
|
|
||
| pushOpts := git.PushOptions{ | ||
| Remote: repo.RepoPath(), | ||
| Branch: fmt.Sprintf("%s:%s%s", newCommit.ID.String(), git.BranchPrefix, branchName), | ||
| Env: pushEnv, | ||
| } | ||
|
|
||
| if expectedOldCommitID != "" { | ||
| pushOpts.ForceWithLease = fmt.Sprintf("%s:%s", git.BranchPrefix+branchName, currentCommitID) | ||
| } | ||
| if isForcePush || force { | ||
| pushOpts.Force = true | ||
| } | ||
| return gitrepo.Push(ctx, repo, pushOpts) | ||
| } | ||
|
|
||
| var ErrBranchIsDefault = util.ErrorWrap(util.ErrPermissionDenied, "branch is default") | ||
|
|
||
| // ErrBranchCommitDoesNotMatch indicates the provided old commit id does not match the branch tip. | ||
| type ErrBranchCommitDoesNotMatch struct { | ||
| Expected string | ||
| Given string | ||
| } | ||
|
|
||
| // IsErrBranchCommitDoesNotMatch checks if the error is ErrBranchCommitDoesNotMatch. | ||
| func IsErrBranchCommitDoesNotMatch(err error) bool { | ||
| _, ok := err.(ErrBranchCommitDoesNotMatch) | ||
| return ok | ||
| } | ||
|
|
||
| func (e ErrBranchCommitDoesNotMatch) Error() string { | ||
| return fmt.Sprintf("branch commit does not match [expected: %s, given: %s]", e.Expected, e.Given) | ||
| } | ||
|
|
||
| func CanDeleteBranch(ctx context.Context, repo *repo_model.Repository, branchName string, doer *user_model.User) error { | ||
| if branchName == repo.DefaultBranch { | ||
| return ErrBranchIsDefault | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.