mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-10 15:46:26 -04:00
This commit implements the ability to cycle backward through different all branches log visualization modes, complementing the existing forward cycling functionality. Users can now press 'A' (Shift+a) to cycle in reverse through the available log graph views, improving navigation efficiency when they overshoot their desired view. Changes: - Added RotateAllBranchesLogIdxBackward() method to BranchCommands for backward rotation through log command candidates - Introduced AllBranchesLogGraphReverse keybinding configuration with default key 'A' (uppercase) - Implemented switchToOrRotateAllBranchesLogsBackward() handler in StatusController that mirrors forward cycling logic - Added English translation for "Show/cycle all branch logs (reverse)" The implementation uses modulo arithmetic with proper handling of negative indices to ensure seamless backward cycling through the available log visualization options.
363 lines
10 KiB
Go
363 lines
10 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()
|
|
}
|
|
|
|
// 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()
|
|
}
|