Compare commits

...

3 Commits

Author SHA1 Message Date
GiteaBot
01351cc6c7 [skip ci] Updated translations via Crowdin 2025-12-11 00:39:32 +00:00
Lunny Xiao
a440116a16 Support updating branch via API (#35951)
Resolve #35368
2025-12-10 19:23:26 +00:00
Lunny Xiao
24b81ac8b9 Use gitrepo's clone and push when possible (#36093)
1 Move `IsRepositoryModelOrDirExist` and `CheckCreateRepository` to
service layer
2 Use `gitrepo.Pushxxx` instead of `git.Push` when possible
3 use `gitrepo.Clonexxx` instead of `gitrepo.Clone` when possible
2025-12-10 09:41:01 -08:00
27 changed files with 404 additions and 83 deletions

View File

@@ -869,16 +869,6 @@ func GetRepositoriesMapByIDs(ctx context.Context, ids []int64) (map[int64]*Repos
return repos, db.GetEngine(ctx).In("id", ids).Find(&repos)
}
// IsRepositoryModelOrDirExist returns true if the repository with given name under user has already existed.
func IsRepositoryModelOrDirExist(ctx context.Context, u *user_model.User, repoName string) (bool, error) {
has, err := IsRepositoryModelExist(ctx, u, repoName)
if err != nil {
return false, err
}
isDir, err := util.IsDir(RepoPath(u.Name, repoName))
return has || isDir, err
}
func IsRepositoryModelExist(ctx context.Context, u *user_model.User, repoName string) (bool, error) {
return db.GetEngine(ctx).Get(&Repository{
OwnerID: u.ID,

View File

@@ -9,8 +9,6 @@ import (
"time"
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util"
)
@@ -106,35 +104,6 @@ func (err ErrRepoFilesAlreadyExist) Unwrap() error {
return util.ErrAlreadyExist
}
// CheckCreateRepository check if doer could create a repository in new owner
func CheckCreateRepository(ctx context.Context, doer, owner *user_model.User, name string, overwriteOrAdopt bool) error {
if !doer.CanCreateRepoIn(owner) {
return ErrReachLimitOfRepo{owner.MaxRepoCreation}
}
if err := IsUsableRepoName(name); err != nil {
return err
}
has, err := IsRepositoryModelOrDirExist(ctx, owner, name)
if err != nil {
return fmt.Errorf("IsRepositoryExist: %w", err)
} else if has {
return ErrRepoAlreadyExist{owner.Name, name}
}
repoPath := RepoPath(owner.Name, name)
isExist, err := util.IsExist(repoPath)
if err != nil {
log.Error("Unable to check if %s exists. Error: %v", repoPath, err)
return err
}
if !overwriteOrAdopt && isExist {
return ErrRepoFilesAlreadyExist{owner.Name, name}
}
return nil
}
// UpdateRepoSize updates the repository size, calculating it using getDirectorySize
func UpdateRepoSize(ctx context.Context, repoID, gitSize, lfsSize int64) error {
_, err := db.GetEngine(ctx).ID(repoID).Cols("size", "git_size", "lfs_size").NoAutoTime().Update(&Repository{

View File

@@ -186,18 +186,21 @@ func Clone(ctx context.Context, from, to string, opts CloneRepoOptions) error {
// PushOptions options when push to remote
type PushOptions struct {
Remote string
Branch string
Force bool
Mirror bool
Env []string
Timeout time.Duration
Remote string
Branch string
Force bool
ForceWithLease string
Mirror bool
Env []string
Timeout time.Duration
}
// Push pushs local commits to given remote branch.
func Push(ctx context.Context, repoPath string, opts PushOptions) error {
cmd := gitcmd.NewCommand("push")
if opts.Force {
if opts.ForceWithLease != "" {
cmd.AddOptionFormat("--force-with-lease=%s", opts.ForceWithLease)
} else if opts.Force {
cmd.AddArguments("-f")
}
if opts.Mirror {

View File

@@ -9,6 +9,19 @@ import (
"code.gitea.io/gitea/modules/git"
)
func Push(ctx context.Context, repo Repository, opts git.PushOptions) error {
// PushToExternal pushes a managed repository to an external remote.
func PushToExternal(ctx context.Context, repo Repository, opts git.PushOptions) error {
return git.Push(ctx, repoPath(repo), opts)
}
// Push pushes from one managed repository to another managed repository.
func Push(ctx context.Context, fromRepo, toRepo Repository, opts git.PushOptions) error {
opts.Remote = repoPath(toRepo)
return git.Push(ctx, repoPath(fromRepo), opts)
}
// PushFromLocal pushes from a local path to a managed repository.
func PushFromLocal(ctx context.Context, fromLocalPath string, toRepo Repository, opts git.PushOptions) error {
opts.Remote = repoPath(toRepo)
return git.Push(ctx, fromLocalPath, opts)
}

View File

@@ -292,6 +292,21 @@ type RenameBranchRepoOption struct {
Name string `json:"name" binding:"Required;GitRefName;MaxSize(100)"`
}
// UpdateBranchRepoOption options when updating a branch reference in a repository
// swagger:model
type UpdateBranchRepoOption struct {
// New commit SHA (or any ref) the branch should point to
//
// required: true
NewCommitID string `json:"new_commit_id" binding:"Required"`
// Expected old commit SHA of the branch; if provided it must match the current tip
OldCommitID string `json:"old_commit_id"`
// Force update even if the change is not a fast-forward
Force bool `json:"force"`
}
// TransferRepoOption options when transfer a repository's ownership
// swagger:model
type TransferRepoOption struct {

View File

@@ -215,6 +215,7 @@ more=Plus
buttons.heading.tooltip=Ajouter un en-tête
buttons.bold.tooltip=Ajouter du texte en gras
buttons.italic.tooltip=Ajouter du texte en italique
buttons.strikethrough.tooltip=Ajouté un texte barré
buttons.quote.tooltip=Citer le texte
buttons.code.tooltip=Ajouter du code
buttons.link.tooltip=Ajouter un lien
@@ -1354,8 +1355,11 @@ editor.this_file_locked=Le fichier est verrouillé
editor.must_be_on_a_branch=Vous devez être sur une branche pour appliquer ou proposer des modifications à ce fichier.
editor.fork_before_edit=Vous devez faire bifurquer ce dépôt pour appliquer ou proposer des modifications à ce fichier.
editor.delete_this_file=Supprimer le fichier
editor.delete_this_directory=Supprimer le répertoire
editor.must_have_write_access=Vous devez avoir un accès en écriture pour appliquer ou proposer des modifications à ce fichier.
editor.file_delete_success=Le fichier "%s" a été supprimé.
editor.directory_delete_success=Le répertoire « %s » a été supprimé.
editor.delete_directory=Supprimer le répertoire « %s »
editor.name_your_file=Nommez votre fichier…
editor.filename_help=Ajoutez un dossier en entrant son nom suivi d'une barre oblique ('/'). Supprimez un dossier avec un retour arrière au début du champ.
editor.or=ou
@@ -1482,6 +1486,7 @@ projects.column.new_submit=Créer une colonne
projects.column.new=Nouvelle colonne
projects.column.set_default=Définir par défaut
projects.column.set_default_desc=Les tickets et demandes dajout non-catégorisés seront placés dans cette colonne.
projects.column.default_column_hint=Les nouveaux tickets ajoutés à ce projet seront ajoutés dans cette colonne
projects.column.delete=Supprimer la colonne
projects.column.deletion_desc=La suppression dune colonne déplace tous ses tickets dans la colonne par défaut. Continuer ?
projects.column.color=Couleur

View File

@@ -1242,6 +1242,7 @@ func Routes() *web.Router {
m.Get("/*", repo.GetBranch)
m.Delete("/*", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, repo.DeleteBranch)
m.Post("", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, bind(api.CreateBranchRepoOption{}), repo.CreateBranch)
m.Put("/*", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, bind(api.UpdateBranchRepoOption{}), repo.UpdateBranch)
m.Patch("/*", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, bind(api.RenameBranchRepoOption{}), repo.RenameBranch)
}, context.ReferencesGitRepo(), reqRepoReader(unit.TypeCode))
m.Group("/branch_protections", func() {

View File

@@ -380,6 +380,81 @@ 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
}
// permission check has been done in api.go
if err := repo_service.UpdateBranch(ctx, repo, ctx.Repo.GitRepo, ctx.Doer, branchName, opt.NewCommitID, opt.OldCommitID, opt.Force); err != nil {
switch {
case git_model.IsErrBranchNotExist(err):
ctx.APIErrorNotFound(err)
case errors.Is(err, util.ErrInvalidArgument):
ctx.APIError(http.StatusUnprocessableEntity, err)
case git.IsErrPushRejected(err):
rej := err.(*git.ErrPushRejected)
ctx.APIError(http.StatusForbidden, rej.Message)
default:
ctx.APIErrorInternal(err)
}
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

View File

@@ -147,6 +147,8 @@ type swaggerParameterBodies struct {
// in:body
CreateBranchRepoOption api.CreateBranchRepoOption
// in:body
UpdateBranchRepoOption api.UpdateBranchRepoOption
// in:body
CreateBranchProtectionOption api.CreateBranchProtectionOption

View File

@@ -15,6 +15,7 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
repo_module "code.gitea.io/gitea/modules/repository"
@@ -133,8 +134,7 @@ func RestoreBranchPost(ctx *context.Context) {
return
}
if err := git.Push(ctx, ctx.Repo.Repository.RepoPath(), git.PushOptions{
Remote: ctx.Repo.Repository.RepoPath(),
if err := gitrepo.Push(ctx, ctx.Repo.Repository, ctx.Repo.Repository, git.PushOptions{
Branch: fmt.Sprintf("%s:%s%s", deletedBranch.CommitID, git.BranchPrefix, deletedBranch.Name),
Env: repo_module.PushingEnvironment(ctx.Doer, ctx.Repo.Repository),
}); err != nil {

View File

@@ -13,6 +13,7 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
repo_module "code.gitea.io/gitea/modules/repository"
@@ -102,8 +103,7 @@ func getUniqueRepositoryName(ctx context.Context, ownerID int64, name string) st
}
func editorPushBranchToForkedRepository(ctx context.Context, doer *user_model.User, baseRepo *repo_model.Repository, baseBranchName string, targetRepo *repo_model.Repository, targetBranchName string) error {
return git.Push(ctx, baseRepo.RepoPath(), git.PushOptions{
Remote: targetRepo.RepoPath(),
return gitrepo.Push(ctx, baseRepo, targetRepo, git.PushOptions{
Branch: baseBranchName + ":" + targetBranchName,
Env: repo_module.PushingEnvironment(doer, targetRepo),
})

View File

@@ -16,6 +16,7 @@ import (
"code.gitea.io/gitea/models/renderhelper"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/htmlutil"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup/markdown"
@@ -141,8 +142,7 @@ func NewComment(ctx *context.Context) {
if prHeadCommitID != headBranchCommitID {
// force push to base repo
err := git.Push(ctx, pull.HeadRepo.RepoPath(), git.PushOptions{
Remote: pull.BaseRepo.RepoPath(),
err := gitrepo.Push(ctx, pull.HeadRepo, pull.BaseRepo, git.PushOptions{
Branch: pull.HeadBranch + ":" + prHeadRef,
Force: true,
Env: repo_module.InternalPushingEnvironment(pull.Issue.Poster, pull.BaseRepo),

View File

@@ -25,6 +25,7 @@ import (
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
"code.gitea.io/gitea/services/migrations"
repo_service "code.gitea.io/gitea/services/repository"
"code.gitea.io/gitea/services/task"
)
@@ -237,7 +238,7 @@ func MigratePost(ctx *context.Context) {
opts.AWSSecretAccessKey = form.AWSSecretAccessKey
}
err = repo_model.CheckCreateRepository(ctx, ctx.Doer, ctxUser, opts.RepoName, false)
err = repo_service.CheckCreateRepository(ctx, ctx.Doer, ctxUser, opts.RepoName, false)
if err != nil {
handleMigrateError(ctx, ctxUser, err, "MigratePost", tpl, form)
return

View File

@@ -20,6 +20,7 @@ import (
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/git/attribute"
"code.gitea.io/gitea/modules/git/pipeline"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/log"
repo_module "code.gitea.io/gitea/modules/repository"
@@ -112,7 +113,7 @@ func LFSLocks(ctx *context.Context) {
}
defer cleanup()
if err := git.Clone(ctx, ctx.Repo.Repository.RepoPath(), tmpBasePath, git.CloneRepoOptions{
if err := gitrepo.CloneRepoToLocal(ctx, ctx.Repo.Repository, tmpBasePath, git.CloneRepoOptions{
Bare: true,
Shared: true,
}); err != nil {

View File

@@ -215,7 +215,7 @@ func checkCommitGraph(ctx context.Context, logger log.Logger, autofix bool) erro
if !isExist {
numNeedUpdate++
if autofix {
if err := git.WriteCommitGraph(ctx, repo.RepoPath()); err != nil {
if err := gitrepo.WriteCommitGraph(ctx, repo); err != nil {
logger.Error("Unable to write commit-graph in %s. Error: %v", repo.FullName(), err)
return err
}

View File

@@ -153,7 +153,7 @@ func runPushSync(ctx context.Context, m *repo_model.PushMirror) error {
log.Trace("Pushing %s mirror[%d] remote %s", storageRepo.RelativePath(), m.ID, m.RemoteName)
envs := proxy.EnvWithProxy(remoteURL.URL)
if err := gitrepo.Push(ctx, storageRepo, git.PushOptions{
if err := gitrepo.PushToExternal(ctx, storageRepo, git.PushOptions{
Remote: m.RemoteName,
Force: true,
Mirror: true,

View File

@@ -570,13 +570,11 @@ func pushToBaseRepoHelper(ctx context.Context, pr *issues_model.PullRequest, pre
log.Error("Unable to load head repository for PR[%d] Error: %v", pr.ID, err)
return err
}
headRepoPath := pr.HeadRepo.RepoPath()
if err := pr.LoadBaseRepo(ctx); err != nil {
log.Error("Unable to load base repository for PR[%d] Error: %v", pr.ID, err)
return err
}
baseRepoPath := pr.BaseRepo.RepoPath()
if err = pr.LoadIssue(ctx); err != nil {
return fmt.Errorf("unable to load issue %d for pr %d: %w", pr.IssueID, pr.ID, err)
@@ -587,8 +585,7 @@ func pushToBaseRepoHelper(ctx context.Context, pr *issues_model.PullRequest, pre
gitRefName := pr.GetGitHeadRefName()
if err := git.Push(ctx, headRepoPath, git.PushOptions{
Remote: baseRepoPath,
if err := gitrepo.Push(ctx, pr.HeadRepo, pr.BaseRepo, git.PushOptions{
Branch: prefixHeadBranch + pr.HeadBranch + ":" + gitRefName,
Force: true,
// Use InternalPushingEnvironment here because we know that pre-receive and post-receive do not run on a refs/pulls/...

View File

@@ -385,8 +385,7 @@ func CreateNewBranchFromCommit(ctx context.Context, doer *user_model.User, repo
return err
}
if err := git.Push(ctx, repo.RepoPath(), git.PushOptions{
Remote: repo.RepoPath(),
if err := gitrepo.Push(ctx, repo, repo, git.PushOptions{
Branch: fmt.Sprintf("%s:%s%s", commitID, git.BranchPrefix, branchName),
Env: repo_module.PushingEnvironment(doer, repo),
}); err != nil {
@@ -483,6 +482,64 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, doer *user_m
return "", nil
}
// UpdateBranch moves a branch reference to the provided commit. permission check should be done before calling this function.
func UpdateBranch(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, doer *user_model.User, branchName, newCommitID, expectedOldCommitID string, force bool) error {
branch, err := git_model.GetBranch(ctx, repo.ID, branchName)
if err != nil {
return err
}
if branch.IsDeleted {
return git_model.ErrBranchNotExist{
BranchName: branchName,
}
}
if expectedOldCommitID != "" {
expectedID, err := gitRepo.ConvertToGitID(expectedOldCommitID)
if err != nil {
return fmt.Errorf("ConvertToGitID(old): %w", err)
}
if expectedID.String() != branch.CommitID {
return util.NewInvalidArgumentErrorf("branch commit does not match [expected: %s, given: %s]", expectedID.String(), branch.CommitID)
}
}
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() == branch.CommitID {
return nil
}
isForcePush, err := newCommit.IsForcePush(branch.CommitID)
if err != nil {
return err
}
if isForcePush && !force {
return util.NewInvalidArgumentErrorf("Force push %s need a confirm force parameter", branchName)
}
pushOpts := git.PushOptions{
Remote: repo.RepoPath(),
Branch: fmt.Sprintf("%s:%s%s", newCommit.ID.String(), git.BranchPrefix, branchName),
Env: repo_module.PushingEnvironment(doer, repo),
Force: isForcePush || force,
}
if expectedOldCommitID != "" {
pushOpts.ForceWithLease = fmt.Sprintf("%s:%s", git.BranchPrefix+branchName, branch.CommitID)
}
// branch protection will be checked in the pre received hook, so that we don't need any check here
return gitrepo.Push(ctx, repo, repo, pushOpts)
}
var ErrBranchIsDefault = util.ErrorWrap(util.ErrPermissionDenied, "branch is default")
func CanDeleteBranch(ctx context.Context, repo *repo_model.Repository, branchName string, doer *user_model.User) error {

View File

@@ -18,6 +18,7 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
@@ -362,8 +363,7 @@ func (t *TemporaryUploadRepository) CommitTree(ctx context.Context, opts *Commit
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(),
if err := gitrepo.PushFromLocal(ctx, t.basePath, t.repo, git.PushOptions{
Branch: strings.TrimSpace(commitHash) + ":" + git.BranchPrefix + strings.TrimSpace(branch),
Env: env,
Force: force,

View File

@@ -230,8 +230,7 @@ func generateRepoCommit(ctx context.Context, repo, templateRepo, generateRepo *r
)
// Clone to temporary path and do the init commit.
templateRepoPath := templateRepo.RepoPath()
if err := git.Clone(ctx, templateRepoPath, tmpDir, git.CloneRepoOptions{
if err := gitrepo.CloneRepoToLocal(ctx, templateRepo, tmpDir, git.CloneRepoOptions{
Depth: 1,
Branch: templateRepo.DefaultBranch,
}); err != nil {

View File

@@ -11,6 +11,7 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/reqctx"
"code.gitea.io/gitea/modules/util"
@@ -33,8 +34,7 @@ func MergeUpstream(ctx reqctx.RequestContext, doer *user_model.User, repo *repo_
return "up-to-date", nil
}
err = git.Push(ctx, repo.BaseRepo.RepoPath(), git.PushOptions{
Remote: repo.RepoPath(),
err = gitrepo.Push(ctx, repo.BaseRepo, repo, git.PushOptions{
Branch: fmt.Sprintf("%s:%s", divergingInfo.BaseBranchName, branch),
Env: repo_module.PushingEnvironment(doer, repo),
})

View File

@@ -74,8 +74,6 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User,
repo *repo_model.Repository, opts migration.MigrateOptions,
httpTransport *http.Transport,
) (*repo_model.Repository, error) {
repoPath := repo.RepoPath()
if u.IsOrganization() {
t, err := organization.OrgFromUser(u).GetOwnerTeam(ctx)
if err != nil {
@@ -92,7 +90,7 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User,
return repo, fmt.Errorf("failed to remove existing repo dir %q, err: %w", repo.FullName(), err)
}
if err := git.Clone(ctx, opts.CloneAddr, repoPath, git.CloneRepoOptions{
if err := gitrepo.CloneExternalRepo(ctx, opts.CloneAddr, repo, git.CloneRepoOptions{
Mirror: true,
Quiet: true,
Timeout: migrateTimeout,
@@ -104,7 +102,7 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User,
return repo, fmt.Errorf("clone error: %w", err)
}
if err := git.WriteCommitGraph(ctx, repoPath); err != nil {
if err := gitrepo.WriteCommitGraph(ctx, repo); err != nil {
return repo, err
}

View File

@@ -345,3 +345,31 @@ func HasWiki(ctx context.Context, repo *repo_model.Repository) bool {
}
return hasWiki && err == nil
}
// CheckCreateRepository check if doer could create a repository in new owner
func CheckCreateRepository(ctx context.Context, doer, owner *user_model.User, name string, overwriteOrAdopt bool) error {
if !doer.CanCreateRepoIn(owner) {
return repo_model.ErrReachLimitOfRepo{Limit: owner.MaxRepoCreation}
}
if err := repo_model.IsUsableRepoName(name); err != nil {
return err
}
has, err := repo_model.IsRepositoryModelExist(ctx, owner, name)
if err != nil {
return err
} else if has {
return repo_model.ErrRepoAlreadyExist{Uname: owner.Name, Name: name}
}
repo := repo_model.StorageRepo(repo_model.RelativePath(owner.Name, name))
isExist, err := gitrepo.IsRepositoryExist(ctx, repo)
if err != nil {
log.Error("Unable to check if %s exists. Error: %v", repo.RelativePath(), err)
return err
}
if !overwriteOrAdopt && isExist {
return repo_model.ErrRepoFilesAlreadyExist{Uname: owner.Name, Name: name}
}
return nil
}

View File

@@ -90,6 +90,17 @@ func AcceptTransferOwnership(ctx context.Context, repo *repo_model.Repository, d
return nil
}
// isRepositoryModelOrDirExist returns true if the repository with given name under user has already existed.
func isRepositoryModelOrDirExist(ctx context.Context, u *user_model.User, repoName string) (bool, error) {
has, err := repo_model.IsRepositoryModelExist(ctx, u, repoName)
if err != nil {
return false, err
}
repo := repo_model.StorageRepo(repo_model.RelativePath(u.Name, repoName))
isExist, err := gitrepo.IsRepositoryExist(ctx, repo)
return has || isExist, err
}
// transferOwnership transfers all corresponding repository items from old user to new one.
func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName string, repo *repo_model.Repository, teams []*organization.Team) (err error) {
repoRenamed := false
@@ -143,7 +154,7 @@ func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName
newOwnerName = newOwner.Name // ensure capitalisation matches
// Check if new owner has repository with same name.
if has, err := repo_model.IsRepositoryModelOrDirExist(ctx, newOwner, repo.Name); err != nil {
if has, err := isRepositoryModelOrDirExist(ctx, newOwner, repo.Name); err != nil {
return fmt.Errorf("IsRepositoryExist: %w", err)
} else if has {
return repo_model.ErrRepoAlreadyExist{
@@ -345,7 +356,7 @@ func changeRepositoryName(ctx context.Context, repo *repo_model.Repository, newR
return err
}
has, err := repo_model.IsRepositoryModelOrDirExist(ctx, repo.Owner, newRepoName)
has, err := isRepositoryModelOrDirExist(ctx, repo.Owner, newRepoName)
if err != nil {
return fmt.Errorf("IsRepositoryExist: %w", err)
} else if has {

View File

@@ -25,8 +25,6 @@ import (
repo_service "code.gitea.io/gitea/services/repository"
)
const DefaultRemote = "origin"
func getWikiWorkingLockKey(repoID int64) string {
return fmt.Sprintf("wiki_working_%d", repoID)
}
@@ -214,8 +212,7 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
return err
}
if err := git.Push(gitRepo.Ctx, basePath, git.PushOptions{
Remote: DefaultRemote,
if err := gitrepo.PushFromLocal(gitRepo.Ctx, basePath, repo.WikiStorageRepo(), git.PushOptions{
Branch: fmt.Sprintf("%s:%s%s", commitHash.String(), git.BranchPrefix, repo.DefaultWikiBranch),
Env: repo_module.FullPushingEnvironment(
doer,
@@ -333,8 +330,7 @@ func DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
return err
}
if err := git.Push(gitRepo.Ctx, basePath, git.PushOptions{
Remote: DefaultRemote,
if err := gitrepo.PushFromLocal(gitRepo.Ctx, basePath, repo.WikiStorageRepo(), git.PushOptions{
Branch: fmt.Sprintf("%s:%s%s", commitHash.String(), git.BranchPrefix, repo.DefaultWikiBranch),
Env: repo_module.FullPushingEnvironment(
doer,

View File

@@ -6750,6 +6750,66 @@
}
}
},
"put": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"repository"
],
"summary": "Update a branch reference to a new commit",
"operationId": "repoUpdateBranch",
"parameters": [
{
"type": "string",
"description": "owner of the repo",
"name": "owner",
"in": "path",
"required": true
},
{
"type": "string",
"description": "name of the repo",
"name": "repo",
"in": "path",
"required": true
},
{
"type": "string",
"description": "name of the branch",
"name": "branch",
"in": "path",
"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"
}
}
},
"delete": {
"produces": [
"application/json"
@@ -28702,6 +28762,31 @@
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"UpdateBranchRepoOption": {
"description": "UpdateBranchRepoOption options when updating a branch reference in a repository",
"type": "object",
"required": [
"new_commit_id"
],
"properties": {
"force": {
"description": "Force update even if the change is not a fast-forward",
"type": "boolean",
"x-go-name": "Force"
},
"new_commit_id": {
"description": "New commit SHA (or any ref) the branch should point to",
"type": "string",
"x-go-name": "NewCommitID"
},
"old_commit_id": {
"description": "Expected old commit SHA of the branch; if provided it must match the current tip",
"type": "string",
"x-go-name": "OldCommitID"
}
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"UpdateFileOptions": {
"description": "UpdateFileOptions options for updating or creating a file\nNote: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)",
"type": "object",

View File

@@ -4,6 +4,8 @@
package integration
import (
"encoding/base64"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
@@ -243,6 +245,79 @@ func TestAPIRenameBranch(t *testing.T) {
})
}
func TestAPIUpdateBranchReference(t *testing.T) {
defer tests.PrepareTestEnv(t)()
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
ctx := NewAPITestContext(t, "user2", "update-branch", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
giteaURL.Path = ctx.GitPath()
var defaultBranch string
t.Run("CreateRepo", doAPICreateRepository(ctx, false, func(t *testing.T, repo api.Repository) {
defaultBranch = repo.DefaultBranch
}))
createBranchReq := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/branches", ctx.Username, ctx.Reponame), &api.CreateBranchRepoOption{
BranchName: "feature",
OldRefName: defaultBranch,
}).AddTokenAuth(ctx.Token)
ctx.Session.MakeRequest(t, createBranchReq, http.StatusCreated)
var featureInitialCommit string
t.Run("LoadFeatureBranch", doAPIGetBranch(ctx, "feature", func(t *testing.T, branch api.Branch) {
featureInitialCommit = branch.Commit.ID
assert.NotEmpty(t, featureInitialCommit)
}))
content := base64.StdEncoding.EncodeToString([]byte("branch update test"))
var newCommit string
doAPICreateFile(ctx, "docs/update.txt", &api.CreateFileOptions{
FileOptions: api.FileOptions{
BranchName: defaultBranch,
NewBranchName: defaultBranch,
Message: "add docs/update.txt",
},
ContentBase64: content,
}, func(t *testing.T, resp api.FileResponse) {
newCommit = resp.Commit.SHA
assert.NotEmpty(t, newCommit)
})(t)
updateReq := NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/branches/%s", ctx.Username, ctx.Reponame, "feature"), &api.UpdateBranchRepoOption{
NewCommitID: newCommit,
OldCommitID: featureInitialCommit,
}).AddTokenAuth(ctx.Token)
ctx.Session.MakeRequest(t, updateReq, http.StatusNoContent)
t.Run("FastForwardApplied", doAPIGetBranch(ctx, "feature", func(t *testing.T, branch api.Branch) {
assert.Equal(t, newCommit, branch.Commit.ID)
}))
staleReq := NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/branches/%s", ctx.Username, ctx.Reponame, "feature"), &api.UpdateBranchRepoOption{
NewCommitID: newCommit,
OldCommitID: featureInitialCommit,
}).AddTokenAuth(ctx.Token)
ctx.Session.MakeRequest(t, staleReq, http.StatusUnprocessableEntity)
nonFFReq := NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/branches/%s", ctx.Username, ctx.Reponame, "feature"), &api.UpdateBranchRepoOption{
NewCommitID: featureInitialCommit,
OldCommitID: newCommit,
}).AddTokenAuth(ctx.Token)
ctx.Session.MakeRequest(t, nonFFReq, http.StatusUnprocessableEntity)
forceReq := NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/branches/%s", ctx.Username, ctx.Reponame, "feature"), &api.UpdateBranchRepoOption{
NewCommitID: featureInitialCommit,
OldCommitID: newCommit,
Force: true,
}).AddTokenAuth(ctx.Token)
ctx.Session.MakeRequest(t, forceReq, http.StatusNoContent)
t.Run("ForceApplied", doAPIGetBranch(ctx, "feature", func(t *testing.T, branch api.Branch) {
assert.Equal(t, featureInitialCommit, branch.Commit.ID)
}))
})
}
func testAPIRenameBranch(t *testing.T, doerName, ownerName, repoName, from, to string, expectedHTTPStatus int) *httptest.ResponseRecorder {
token := getUserToken(t, doerName, auth_model.AccessTokenScopeWriteRepository)
req := NewRequestWithJSON(t, "PATCH", "api/v1/repos/"+ownerName+"/"+repoName+"/branches/"+from, &api.RenameBranchRepoOption{