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 uint8 // 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() } // 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 func (self *BranchCommands) CurrentBranchName() (string, error) { cmdArgs := NewGitCmd("rev-parse"). Arg("--abbrev-ref"). Arg("--verify"). Arg("HEAD"). ToArgv() output, err := self.cmd.New(cmdArgs).DontLog().RunWithOutput() if err == nil { return strings.TrimSpace(output), nil } return "", err } // LocalDelete delete branch locally 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.ICmdObj { 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 MergeOpts struct { FastForwardOnly bool Squash bool } func (self *BranchCommands) Merge(branchName string, opts MergeOpts) error { if opts.Squash && opts.FastForwardOnly { panic("Squash and FastForwardOnly can't both be true") } cmdArgs := NewGitCmd("merge"). Arg("--no-edit"). Arg(strings.Fields(self.UserConfig().Git.Merging.Args)...). ArgIf(opts.FastForwardOnly, "--ff-only"). ArgIf(opts.Squash, "--squash", "--ff"). Arg(branchName). ToArgv() return self.cmd.New(cmdArgs).Run() } func (self *BranchCommands) AllBranchesLogCmdObj() oscommands.ICmdObj { // Only choose between non-empty, non-identical commands candidates := lo.Uniq(lo.WithoutEmpty(append([]string{ self.UserConfig().Git.AllBranchesLogCmd, }, self.UserConfig().Git.AllBranchesLogCmds..., ))) n := len(candidates) i := self.allBranchesLogCmdIndex self.allBranchesLogCmdIndex = uint8((int(i) + 1) % n) return self.cmd.New(str.ToArgv(candidates[i])).DontLog() } 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).RunWithOutputs() if err != nil { return false, err } return stdout == "", nil }