// Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package git import ( "fmt" "strings" "github.com/go-git/go-git/v5" git_config "github.com/go-git/go-git/v5/config" git_plumbing "github.com/go-git/go-git/v5/plumbing" git_transport "github.com/go-git/go-git/v5/plumbing/transport" ) // TeaCreateBranch creates a new branch in the repo, tracking from another branch. func (r TeaRepo) TeaCreateBranch(localBranchName, remoteBranchName, remoteName string) error { // save in .git/config to assign remote for future pulls localBranchRefName := git_plumbing.NewBranchReferenceName(localBranchName) err := r.CreateBranch(&git_config.Branch{ Name: localBranchName, Merge: git_plumbing.NewBranchReferenceName(remoteBranchName), Remote: remoteName, }) if err != nil { return err } // serialize the branch to .git/refs/heads remoteBranchRefName := git_plumbing.NewRemoteReferenceName(remoteName, remoteBranchName) remoteBranchRef, err := r.Storer.Reference(remoteBranchRefName) if err != nil { return err } localHashRef := git_plumbing.NewHashReference(localBranchRefName, remoteBranchRef.Hash()) return r.Storer.SetReference(localHashRef) } // TeaCheckout checks out the given branch in the worktree. func (r TeaRepo) TeaCheckout(ref git_plumbing.ReferenceName) error { tree, err := r.Worktree() if err != nil { return err } return tree.Checkout(&git.CheckoutOptions{Branch: ref}) } // TeaDeleteLocalBranch removes the given branch locally func (r TeaRepo) TeaDeleteLocalBranch(branch *git_config.Branch) error { err := r.DeleteBranch(branch.Name) // if the branch is not found that's ok, as .git/config may have no entry if // no remote tracking branch is configured for it (eg push without -u flag) if err != nil && err.Error() != "branch not found" { return err } return r.Storer.RemoveReference(git_plumbing.NewBranchReferenceName(branch.Name)) } // TeaDeleteRemoteBranch removes the given branch on the given remote via git protocol func (r TeaRepo) TeaDeleteRemoteBranch(remoteName, remoteBranch string, auth git_transport.AuthMethod) error { // delete remote branch via git protocol: // an empty source in the refspec means remote deletion to git 🙃 refspec := fmt.Sprintf(":%s", git_plumbing.NewBranchReferenceName(remoteBranch)) return r.Push(&git.PushOptions{ RemoteName: remoteName, RefSpecs: []git_config.RefSpec{git_config.RefSpec(refspec)}, Prune: true, Auth: auth, }) } // TeaFindBranchBySha returns a branch that is at the the given SHA and syncs to the // given remote repo. func (r TeaRepo) TeaFindBranchBySha(sha, repoURL string) (b *git_config.Branch, err error) { // find remote matching our repoURL remote, err := r.GetRemote(repoURL) if err != nil { return nil, err } if remote == nil { return nil, fmt.Errorf("No remote found for '%s'", repoURL) } remoteName := remote.Config().Name // check if the given remote has our branch (.git/refs/remotes//*) iter, err := r.References() if err != nil { return nil, err } defer iter.Close() var remoteRefName git_plumbing.ReferenceName var localRefName git_plumbing.ReferenceName err = iter.ForEach(func(ref *git_plumbing.Reference) error { if ref.Name().IsRemote() { name := ref.Name().Short() if ref.Hash().String() == sha && strings.HasPrefix(name, remoteName) { remoteRefName = ref.Name() } } if ref.Name().IsBranch() && ref.Hash().String() == sha { localRefName = ref.Name() } return nil }) if err != nil { return nil, err } if remoteRefName == "" || localRefName == "" { // no remote tracking branch found, so a potential local branch // can't be a match either return nil, nil } b = &git_config.Branch{ Remote: remoteName, Name: localRefName.Short(), Merge: localRefName, } return b, b.Validate() } // TeaFindBranchByName returns a branch that is at the the given local and // remote names and syncs to the given remote repo. This method is less precise // than TeaFindBranchBySha(), but may be desirable if local and remote branch // have diverged. func (r TeaRepo) TeaFindBranchByName(branchName, repoURL string) (b *git_config.Branch, err error) { // find remote matching our repoURL remote, err := r.GetRemote(repoURL) if err != nil { return nil, err } if remote == nil { return nil, fmt.Errorf("No remote found for '%s'", repoURL) } remoteName := remote.Config().Name // check if the given remote has our branch (.git/refs/remotes//*) iter, err := r.References() if err != nil { return nil, err } defer iter.Close() var remoteRefName git_plumbing.ReferenceName var localRefName git_plumbing.ReferenceName var remoteSearchingName = fmt.Sprintf("%s/%s", remoteName, branchName) err = iter.ForEach(func(ref *git_plumbing.Reference) error { if ref.Name().IsRemote() && ref.Name().Short() == remoteSearchingName { remoteRefName = ref.Name() } n := ref.Name() if n.IsBranch() && n.Short() == branchName { localRefName = n } return nil }) if err != nil { return nil, err } if remoteRefName == "" || localRefName == "" { return nil, nil } b = &git_config.Branch{ Remote: remoteName, Name: localRefName.Short(), Merge: localRefName, } return b, b.Validate() } // TeaFindBranchRemote gives the first remote that has a branch with the same name or sha, // depending on what is passed in. // This function is needed, as git does not always define branches in .git/config with remote entries. // Priority order is: first match of sha and branch -> first match of branch -> first match of sha func (r TeaRepo) TeaFindBranchRemote(branchName, hash string) (*git.Remote, error) { remotes, err := r.Remotes() if err != nil { return nil, err } switch { case len(remotes) == 0: return nil, nil case len(remotes) == 1: return remotes[0], nil } // check if the given remote has our branch (.git/refs/remotes//*) iter, err := r.References() if err != nil { return nil, err } defer iter.Close() var shaMatch *git.Remote var branchMatch *git.Remote var fullMatch *git.Remote if err := iter.ForEach(func(ref *git_plumbing.Reference) error { if ref.Name().IsRemote() { names := strings.SplitN(ref.Name().Short(), "/", 2) remote := names[0] branch := names[1] if branchMatch == nil && branchName != "" && branchName == branch { if branchMatch, err = r.Remote(remote); err != nil { return err } } if shaMatch == nil && hash != "" && hash == ref.Hash().String() { if shaMatch, err = r.Remote(remote); err != nil { return err } } if fullMatch == nil && branchName != "" && branchName == branch && hash != "" && hash == ref.Hash().String() { if fullMatch, err = r.Remote(remote); err != nil { return err } // stop asap you have a full match return nil } } return nil }); err != nil { return nil, err } if fullMatch != nil { return fullMatch, nil } else if branchMatch != nil { return branchMatch, nil } else if shaMatch != nil { return shaMatch, nil } return nil, nil } // TeaGetCurrentBranchNameAndSHA return the name and sha of the branch witch is currently active func (r TeaRepo) TeaGetCurrentBranchNameAndSHA() (string, string, error) { localHead, err := r.Head() if err != nil { return "", "", err } if !localHead.Name().IsBranch() { return "", "", fmt.Errorf("active ref is no branch") } return localHead.Name().Short(), localHead.Hash().String(), nil }