1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-11-22 04:42:37 +03:00
Files
lazygit/pkg/commands/git_commands/branch.go
Stefan Haller 4e0194d8f7 Replace MergeOpts struct with MergeVariant enum
- Squash and FastForwardOnly are mutually exclusive, and instead of asserting
  this at runtime, model the API so that they can't be passed together.
- FastForwardOnly is unused, so remove it; however, we are going to need --ff
  and --no-ff in the next commit, so add those instead.
- Instead of putting the enum into the MergeOpts struct, replace the struct by
  the enum. We can reintroduce the struct when we add more arguments, but for
  now it's an unnecessary indirection.
2025-10-19 21:24:28 +02:00

347 lines
9.5 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
}
// 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.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()
}
// 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) 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()
}