mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-10 07:36:27 -04:00
403 lines
11 KiB
Go
403 lines
11 KiB
Go
package git_commands
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
|
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
|
"github.com/mgutz/str"
|
|
"github.com/samber/lo"
|
|
)
|
|
|
|
type BranchCommands struct {
|
|
*GitCommon
|
|
allBranchesLogCmdIndex int // keeps track of current all branches log command
|
|
}
|
|
|
|
func NewBranchCommands(gitCommon *GitCommon) *BranchCommands {
|
|
return &BranchCommands{
|
|
GitCommon: gitCommon,
|
|
}
|
|
}
|
|
|
|
// New creates a new branch
|
|
func (self *BranchCommands) New(name string, base string) error {
|
|
cmdArgs := NewGitCmd("checkout").
|
|
Arg("-b", name, base).
|
|
ToArgv()
|
|
|
|
return self.cmd.New(cmdArgs).Run()
|
|
}
|
|
|
|
func (self *BranchCommands) NewWithoutTracking(name string, base string) error {
|
|
cmdArgs := NewGitCmd("checkout").
|
|
Arg("-b", name, base).
|
|
Arg("--no-track").
|
|
ToArgv()
|
|
|
|
return self.cmd.New(cmdArgs).Run()
|
|
}
|
|
|
|
// NewWithoutCheckout creates a new branch without checking it out
|
|
func (self *BranchCommands) NewWithoutCheckout(name string, base string) error {
|
|
cmdArgs := NewGitCmd("branch").
|
|
Arg(name, base).
|
|
ToArgv()
|
|
|
|
return self.cmd.New(cmdArgs).Run()
|
|
}
|
|
|
|
// CreateWithUpstream creates a new branch with a given upstream, but without
|
|
// checking it out
|
|
func (self *BranchCommands) CreateWithUpstream(name string, upstream string) error {
|
|
cmdArgs := NewGitCmd("branch").
|
|
Arg("--track").
|
|
Arg(name, upstream).
|
|
ToArgv()
|
|
|
|
return self.cmd.New(cmdArgs).Run()
|
|
}
|
|
|
|
// CurrentBranchInfo get the current branch information.
|
|
func (self *BranchCommands) CurrentBranchInfo() (BranchInfo, error) {
|
|
branchName, err := self.cmd.New(
|
|
NewGitCmd("symbolic-ref").
|
|
Arg("--short", "HEAD").
|
|
ToArgv(),
|
|
).DontLog().RunWithOutput()
|
|
if err == nil && branchName != "HEAD\n" {
|
|
trimmedBranchName := strings.TrimSpace(branchName)
|
|
return BranchInfo{
|
|
RefName: trimmedBranchName,
|
|
DisplayName: trimmedBranchName,
|
|
DetachedHead: false,
|
|
}, nil
|
|
}
|
|
output, err := self.cmd.New(
|
|
NewGitCmd("branch").
|
|
Arg("--points-at=HEAD", "--format=%(HEAD)%00%(objectname)%00%(refname)").
|
|
ToArgv(),
|
|
).DontLog().RunWithOutput()
|
|
if err != nil {
|
|
return BranchInfo{}, err
|
|
}
|
|
for _, line := range utils.SplitLines(output) {
|
|
split := strings.Split(strings.TrimRight(line, "\r\n"), "\x00")
|
|
if len(split) == 3 && split[0] == "*" {
|
|
hash := split[1]
|
|
displayName := split[2]
|
|
return BranchInfo{
|
|
RefName: hash,
|
|
DisplayName: displayName,
|
|
DetachedHead: true,
|
|
}, nil
|
|
}
|
|
}
|
|
return BranchInfo{
|
|
RefName: "HEAD",
|
|
DisplayName: "HEAD",
|
|
DetachedHead: true,
|
|
}, nil
|
|
}
|
|
|
|
// CurrentBranchName get name of current branch. Returns empty string if HEAD is detached.
|
|
func (self *BranchCommands) CurrentBranchName() (string, error) {
|
|
cmdArgs := NewGitCmd("branch").
|
|
Arg("--show-current").
|
|
ToArgv()
|
|
|
|
output, err := self.cmd.New(cmdArgs).DontLog().RunWithOutput()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return strings.TrimSpace(output), nil
|
|
}
|
|
|
|
// Gets the full ref name of the previously checked out branch. Can return an empty string (but no
|
|
// error) e.g. when the previously checked out thing was a detached head.
|
|
func (self *BranchCommands) PreviousRef() (string, error) {
|
|
cmdArgs := NewGitCmd("rev-parse").
|
|
Arg("--symbolic-full-name").
|
|
Arg("@{-1}").
|
|
ToArgv()
|
|
|
|
output, err := self.cmd.New(cmdArgs).DontLog().RunWithOutput()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return strings.TrimSpace(output), nil
|
|
}
|
|
|
|
// RestoreBranch recreates a deleted local branch at the given commit hash and,
|
|
// if exactly one remote-tracking branch with the same name still exists,
|
|
// re-attaches it as the upstream. Returns the upstream ref name that was
|
|
// re-attached (or "" if none was).
|
|
func (self *BranchCommands) RestoreBranch(name string, commitHash string) (string, error) {
|
|
cmdArgs := NewGitCmd("branch").
|
|
Arg(name, commitHash).
|
|
ToArgv()
|
|
|
|
if err := self.cmd.New(cmdArgs).Run(); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
upstream := ""
|
|
remoteRefs, err := self.cmd.New(
|
|
NewGitCmd("for-each-ref").
|
|
Arg("--format=%(refname:short)").
|
|
Arg("refs/remotes").
|
|
ToArgv(),
|
|
).DontLog().RunWithOutput()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
matchingRefs := lo.Filter(strings.Split(strings.TrimSpace(remoteRefs), "\n"), func(ref string, _ int) bool {
|
|
return "refs/remotes/"+ref == "refs/remotes/"+name || strings.HasSuffix(ref, "/"+name)
|
|
})
|
|
if len(matchingRefs) == 1 {
|
|
matchingRef := strings.TrimSpace(matchingRefs[0])
|
|
parts := strings.SplitN(matchingRef, "/", 2)
|
|
if len(parts) == 2 {
|
|
if err := self.SetUpstream(parts[0], parts[1], name); err == nil {
|
|
upstream = matchingRef
|
|
}
|
|
}
|
|
}
|
|
|
|
return upstream, nil
|
|
}
|
|
|
|
// LocalDelete delete local branch
|
|
func (self *BranchCommands) LocalDelete(branches []string, force bool) error {
|
|
cmdArgs := NewGitCmd("branch").
|
|
ArgIfElse(force, "-D", "-d").
|
|
Arg(branches...).
|
|
ToArgv()
|
|
|
|
return self.cmd.New(cmdArgs).Run()
|
|
}
|
|
|
|
// Checkout checks out a branch (or commit), with --force if you set the force arg to true
|
|
type CheckoutOptions struct {
|
|
Force bool
|
|
EnvVars []string
|
|
}
|
|
|
|
func (self *BranchCommands) Checkout(branch string, options CheckoutOptions) error {
|
|
cmdArgs := NewGitCmd("checkout").
|
|
ArgIf(options.Force, "--force").
|
|
Arg(branch).
|
|
ToArgv()
|
|
|
|
return self.cmd.New(cmdArgs).
|
|
// prevents git from prompting us for input which would freeze the program
|
|
// TODO: see if this is actually needed here
|
|
AddEnvVars("GIT_TERMINAL_PROMPT=0").
|
|
AddEnvVars(options.EnvVars...).
|
|
Run()
|
|
}
|
|
|
|
// GetGraph gets the color-formatted graph of the log for the given branch
|
|
// Currently it limits the result to 100 commits, but when we get async stuff
|
|
// working we can do lazy loading
|
|
func (self *BranchCommands) GetGraph(branchName string) (string, error) {
|
|
return self.GetGraphCmdObj(branchName).DontLog().RunWithOutput()
|
|
}
|
|
|
|
func (self *BranchCommands) GetGraphCmdObj(branchName string) *oscommands.CmdObj {
|
|
branchLogCmdTemplate := self.UserConfig().Git.BranchLogCmd
|
|
templateValues := map[string]string{
|
|
"branchName": self.cmd.Quote(branchName),
|
|
}
|
|
|
|
resolvedTemplate := utils.ResolvePlaceholderString(branchLogCmdTemplate, templateValues)
|
|
|
|
return self.cmd.New(str.ToArgv(resolvedTemplate)).DontLog()
|
|
}
|
|
|
|
func (self *BranchCommands) SetCurrentBranchUpstream(remoteName string, remoteBranchName string) error {
|
|
cmdArgs := NewGitCmd("branch").
|
|
Arg(fmt.Sprintf("--set-upstream-to=%s/%s", remoteName, remoteBranchName)).
|
|
ToArgv()
|
|
|
|
return self.cmd.New(cmdArgs).Run()
|
|
}
|
|
|
|
func (self *BranchCommands) SetUpstream(remoteName string, remoteBranchName string, branchName string) error {
|
|
cmdArgs := NewGitCmd("branch").
|
|
Arg(fmt.Sprintf("--set-upstream-to=%s/%s", remoteName, remoteBranchName)).
|
|
Arg(branchName).
|
|
ToArgv()
|
|
|
|
return self.cmd.New(cmdArgs).Run()
|
|
}
|
|
|
|
func (self *BranchCommands) UnsetUpstream(branchName string) error {
|
|
cmdArgs := NewGitCmd("branch").Arg("--unset-upstream", branchName).
|
|
ToArgv()
|
|
|
|
return self.cmd.New(cmdArgs).Run()
|
|
}
|
|
|
|
func (self *BranchCommands) GetCurrentBranchUpstreamDifferenceCount() (string, string) {
|
|
return self.GetCommitDifferences("HEAD", "HEAD@{u}")
|
|
}
|
|
|
|
func (self *BranchCommands) GetUpstreamDifferenceCount(branchName string) (string, string) {
|
|
return self.GetCommitDifferences(branchName, branchName+"@{u}")
|
|
}
|
|
|
|
// GetCommitDifferences checks how many pushables/pullables there are for the
|
|
// current branch
|
|
func (self *BranchCommands) GetCommitDifferences(from, to string) (string, string) {
|
|
pushableCount, err := self.countDifferences(to, from)
|
|
if err != nil {
|
|
return "?", "?"
|
|
}
|
|
pullableCount, err := self.countDifferences(from, to)
|
|
if err != nil {
|
|
return "?", "?"
|
|
}
|
|
return strings.TrimSpace(pushableCount), strings.TrimSpace(pullableCount)
|
|
}
|
|
|
|
func (self *BranchCommands) countDifferences(from, to string) (string, error) {
|
|
cmdArgs := NewGitCmd("rev-list").
|
|
Arg(fmt.Sprintf("%s..%s", from, to)).
|
|
Arg("--count").
|
|
ToArgv()
|
|
|
|
return self.cmd.New(cmdArgs).DontLog().RunWithOutput()
|
|
}
|
|
|
|
func (self *BranchCommands) IsHeadDetached() bool {
|
|
cmdArgs := NewGitCmd("symbolic-ref").Arg("-q", "HEAD").ToArgv()
|
|
|
|
err := self.cmd.New(cmdArgs).DontLog().Run()
|
|
return err != nil
|
|
}
|
|
|
|
func (self *BranchCommands) Rename(oldName string, newName string) error {
|
|
cmdArgs := NewGitCmd("branch").
|
|
Arg("--move", oldName, newName).
|
|
ToArgv()
|
|
|
|
return self.cmd.New(cmdArgs).Run()
|
|
}
|
|
|
|
type MergeVariant int
|
|
|
|
const (
|
|
MERGE_VARIANT_REGULAR MergeVariant = iota
|
|
MERGE_VARIANT_FAST_FORWARD
|
|
MERGE_VARIANT_NON_FAST_FORWARD
|
|
MERGE_VARIANT_SQUASH
|
|
)
|
|
|
|
func (self *BranchCommands) Merge(branchName string, variant MergeVariant) error {
|
|
extraArgs := func() []string {
|
|
switch variant {
|
|
case MERGE_VARIANT_REGULAR:
|
|
return []string{}
|
|
case MERGE_VARIANT_FAST_FORWARD:
|
|
return []string{"--ff"}
|
|
case MERGE_VARIANT_NON_FAST_FORWARD:
|
|
return []string{"--no-ff"}
|
|
case MERGE_VARIANT_SQUASH:
|
|
return []string{"--squash", "--ff"}
|
|
}
|
|
|
|
panic("shouldn't get here")
|
|
}()
|
|
|
|
cmdArgs := NewGitCmd("merge").
|
|
Arg("--no-edit").
|
|
Arg(strings.Fields(self.UserConfig().Git.Merging.Args)...).
|
|
Arg(extraArgs...).
|
|
Arg(branchName).
|
|
ToArgv()
|
|
|
|
return self.cmd.New(cmdArgs).Run()
|
|
}
|
|
|
|
// Returns whether refName can be fast-forward merged into the current branch
|
|
func (self *BranchCommands) CanDoFastForwardMerge(refName string) bool {
|
|
cmdArgs := NewGitCmd("merge-base").
|
|
Arg("--is-ancestor").
|
|
Arg("HEAD", refName).
|
|
ToArgv()
|
|
err := self.cmd.New(cmdArgs).DontLog().Run()
|
|
return err == nil
|
|
}
|
|
|
|
// Only choose between non-empty, non-identical commands
|
|
func (self *BranchCommands) allBranchesLogCandidates() []string {
|
|
return lo.Uniq(lo.WithoutEmpty(self.UserConfig().Git.AllBranchesLogCmds))
|
|
}
|
|
|
|
func (self *BranchCommands) AllBranchesLogCmdObj() *oscommands.CmdObj {
|
|
candidates := self.allBranchesLogCandidates()
|
|
|
|
if self.allBranchesLogCmdIndex >= len(candidates) {
|
|
self.allBranchesLogCmdIndex = 0
|
|
}
|
|
|
|
i := self.allBranchesLogCmdIndex
|
|
return self.cmd.New(str.ToArgv(candidates[i])).DontLog()
|
|
}
|
|
|
|
func (self *BranchCommands) RotateAllBranchesLogIdx() {
|
|
n := len(self.allBranchesLogCandidates())
|
|
i := self.allBranchesLogCmdIndex
|
|
self.allBranchesLogCmdIndex = (i + 1) % n
|
|
}
|
|
|
|
func (self *BranchCommands) RotateAllBranchesLogIdxBackward() {
|
|
n := len(self.allBranchesLogCandidates())
|
|
i := self.allBranchesLogCmdIndex
|
|
self.allBranchesLogCmdIndex = (i - 1 + n) % n
|
|
}
|
|
|
|
func (self *BranchCommands) GetAllBranchesLogIdxAndCount() (int, int) {
|
|
n := len(self.allBranchesLogCandidates())
|
|
i := self.allBranchesLogCmdIndex
|
|
return i, n
|
|
}
|
|
|
|
func (self *BranchCommands) IsBranchMerged(branch *models.Branch, mainBranches *MainBranches) (bool, error) {
|
|
branchesToCheckAgainst := []string{"HEAD"}
|
|
if branch.RemoteBranchStoredLocally() {
|
|
branchesToCheckAgainst = append(branchesToCheckAgainst, fmt.Sprintf("%s@{upstream}", branch.Name))
|
|
}
|
|
branchesToCheckAgainst = append(branchesToCheckAgainst, mainBranches.Get()...)
|
|
|
|
cmdArgs := NewGitCmd("rev-list").
|
|
Arg("--max-count=1").
|
|
Arg(branch.Name).
|
|
Arg(lo.Map(branchesToCheckAgainst, func(branch string, _ int) string {
|
|
return fmt.Sprintf("^%s", branch)
|
|
})...).
|
|
Arg("--").
|
|
ToArgv()
|
|
|
|
stdout, _, err := self.cmd.New(cmdArgs).DontLog().RunWithOutputs()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
return stdout == "", nil
|
|
}
|
|
|
|
func (self *BranchCommands) UpdateBranchRefs(updateCommands string) error {
|
|
cmdArgs := NewGitCmd("update-ref").
|
|
Arg("--stdin").
|
|
ToArgv()
|
|
|
|
return self.cmd.New(cmdArgs).SetStdin(updateCommands).Run()
|
|
}
|