// Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package repository import ( "context" "errors" "fmt" "strings" "code.gitea.io/gitea/models" git_model "code.gitea.io/gitea/models/git" 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/log" "code.gitea.io/gitea/modules/notification" repo_module "code.gitea.io/gitea/modules/repository" pull_service "code.gitea.io/gitea/services/pull" ) // CreateNewBranch creates a new repository branch func CreateNewBranch(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, oldBranchName, branchName string) (err error) { // Check if branch name can be used if err := checkBranchName(ctx, repo, branchName); err != nil { return err } if !git.IsBranchExist(ctx, repo.RepoPath(), oldBranchName) { return models.ErrBranchDoesNotExist{ BranchName: oldBranchName, } } if err := git.Push(ctx, repo.RepoPath(), git.PushOptions{ Remote: repo.RepoPath(), Branch: fmt.Sprintf("%s%s:%s%s", git.BranchPrefix, oldBranchName, git.BranchPrefix, branchName), Env: repo_module.PushingEnvironment(doer, repo), }); err != nil { if git.IsErrPushOutOfDate(err) || git.IsErrPushRejected(err) { return err } return fmt.Errorf("Push: %w", err) } return nil } // GetBranches returns branches from the repository, skipping skip initial branches and // returning at most limit branches, or all branches if limit is 0. func GetBranches(ctx context.Context, repo *repo_model.Repository, skip, limit int) ([]*git.Branch, int, error) { return git.GetBranchesByPath(ctx, repo.RepoPath(), skip, limit) } func GetBranchCommitID(ctx context.Context, repo *repo_model.Repository, branch string) (string, error) { return git.GetBranchCommitID(ctx, repo.RepoPath(), branch) } // checkBranchName validates branch name with existing repository branches func checkBranchName(ctx context.Context, repo *repo_model.Repository, name string) error { _, err := git.WalkReferences(ctx, repo.RepoPath(), func(_, refName string) error { branchRefName := strings.TrimPrefix(refName, git.BranchPrefix) switch { case branchRefName == name: return models.ErrBranchAlreadyExists{ BranchName: name, } // If branchRefName like a/b but we want to create a branch named a then we have a conflict case strings.HasPrefix(branchRefName, name+"/"): return models.ErrBranchNameConflict{ BranchName: branchRefName, } // Conversely if branchRefName like a but we want to create a branch named a/b then we also have a conflict case strings.HasPrefix(name, branchRefName+"/"): return models.ErrBranchNameConflict{ BranchName: branchRefName, } case refName == git.TagPrefix+name: return models.ErrTagAlreadyExists{ TagName: name, } } return nil }) return err } // CreateNewBranchFromCommit creates a new repository branch func CreateNewBranchFromCommit(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, commit, branchName string) (err error) { // Check if branch name can be used if err := checkBranchName(ctx, repo, branchName); err != nil { return err } if err := git.Push(ctx, repo.RepoPath(), git.PushOptions{ Remote: repo.RepoPath(), Branch: fmt.Sprintf("%s:%s%s", commit, git.BranchPrefix, branchName), Env: repo_module.PushingEnvironment(doer, repo), }); err != nil { if git.IsErrPushOutOfDate(err) || git.IsErrPushRejected(err) { return err } return fmt.Errorf("Push: %w", err) } return nil } // RenameBranch rename a branch func RenameBranch(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, gitRepo *git.Repository, from, to string) (string, error) { if from == to { return "target_exist", nil } if gitRepo.IsBranchExist(to) { return "target_exist", nil } if !gitRepo.IsBranchExist(from) { return "from_not_exist", nil } if err := git_model.RenameBranch(ctx, repo, from, to, func(isDefault bool) error { err2 := gitRepo.RenameBranch(from, to) if err2 != nil { return err2 } if isDefault { err2 = gitRepo.SetDefaultBranch(to) if err2 != nil { return err2 } } return nil }); err != nil { return "", err } refNameTo := git.RefNameFromBranch(to) refID, err := gitRepo.GetRefCommitID(refNameTo.String()) if err != nil { return "", err } notification.NotifyDeleteRef(ctx, doer, repo, git.RefNameFromBranch(from)) notification.NotifyCreateRef(ctx, doer, repo, refNameTo, refID) return "", nil } // enmuerates all branch related errors var ( ErrBranchIsDefault = errors.New("branch is default") ) // DeleteBranch delete branch func DeleteBranch(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, gitRepo *git.Repository, branchName string) error { if branchName == repo.DefaultBranch { return ErrBranchIsDefault } isProtected, err := git_model.IsBranchProtected(ctx, repo.ID, branchName) if err != nil { return err } if isProtected { return git_model.ErrBranchIsProtected } commit, err := gitRepo.GetBranchCommit(branchName) if err != nil { return err } if err := gitRepo.DeleteBranch(branchName, git.DeleteBranchOptions{ Force: true, }); err != nil { return err } if err := pull_service.CloseBranchPulls(doer, repo.ID, branchName); err != nil { return err } // Don't return error below this if err := PushUpdate( &repo_module.PushUpdateOptions{ RefFullName: git.RefNameFromBranch(branchName), OldCommitID: commit.ID.String(), NewCommitID: git.EmptySHA, PusherID: doer.ID, PusherName: doer.Name, RepoUserName: repo.OwnerName, RepoName: repo.Name, }); err != nil { log.Error("Update: %v", err) } if err := git_model.AddDeletedBranch(ctx, repo.ID, branchName, commit.ID.String(), doer.ID); err != nil { log.Warn("AddDeletedBranch: %v", err) } return nil }