diff --git a/docs/Config.md b/docs/Config.md index 052d65957..938aaba29 100644 --- a/docs/Config.md +++ b/docs/Config.md @@ -58,6 +58,7 @@ gui: showFileTree: true # for rendering changes files in a tree format showListFooter: true # for seeing the '5 of 20' message in list panels showRandomTip: true + showBranchCommitHash: false # show commit hashes alongside branch names experimentalShowBranchHeads: false # visualize branch heads with (*) in commits list showBottomLine: true # for hiding the bottom information line (unless it has important information to tell you) showCommandLog: true diff --git a/pkg/commands/git.go b/pkg/commands/git.go index 2ae8694e7..d09ff88b0 100644 --- a/pkg/commands/git.go +++ b/pkg/commands/git.go @@ -128,7 +128,7 @@ func NewGitCommandAux( patchCommands := git_commands.NewPatchCommands(gitCommon, rebaseCommands, commitCommands, statusCommands, stashCommands, patchBuilder) bisectCommands := git_commands.NewBisectCommands(gitCommon) - branchLoader := git_commands.NewBranchLoader(cmn, branchCommands.GetRawBranches, branchCommands.CurrentBranchInfo, configCommands) + branchLoader := git_commands.NewBranchLoader(cmn, cmd, branchCommands.CurrentBranchInfo, configCommands) commitFileLoader := git_commands.NewCommitFileLoader(cmn, cmd) commitLoader := git_commands.NewCommitLoader(cmn, cmd, dotGitDir, statusCommands.RebaseMode) reflogCommitLoader := git_commands.NewReflogCommitLoader(cmn, cmd) diff --git a/pkg/commands/git_commands/branch.go b/pkg/commands/git_commands/branch.go index 3c4d97c82..caa876f3f 100644 --- a/pkg/commands/git_commands/branch.go +++ b/pkg/commands/git_commands/branch.go @@ -188,16 +188,6 @@ func (self *BranchCommands) Rename(oldName string, newName string) error { return self.cmd.New(cmdArgs).Run() } -func (self *BranchCommands) GetRawBranches() (string, error) { - cmdArgs := NewGitCmd("for-each-ref"). - Arg("--sort=-committerdate"). - Arg(`--format=%(HEAD)%00%(refname:short)%00%(upstream:short)%00%(upstream:track)`). - Arg("refs/heads"). - ToArgv() - - return self.cmd.New(cmdArgs).DontLog().RunWithOutput() -} - type MergeOpts struct { FastForwardOnly bool } diff --git a/pkg/commands/git_commands/branch_loader.go b/pkg/commands/git_commands/branch_loader.go index 58e7bb940..cb284f67f 100644 --- a/pkg/commands/git_commands/branch_loader.go +++ b/pkg/commands/git_commands/branch_loader.go @@ -1,6 +1,7 @@ package git_commands import ( + "fmt" "regexp" "strings" @@ -8,8 +9,10 @@ import ( "github.com/jesseduffield/generics/slices" "github.com/jesseduffield/go-git/v5/config" "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/common" "github.com/jesseduffield/lazygit/pkg/utils" + "github.com/samber/lo" ) // context: @@ -36,20 +39,20 @@ type BranchInfo struct { // BranchLoader returns a list of Branch objects for the current repo type BranchLoader struct { *common.Common - getRawBranches func() (string, error) + cmd oscommands.ICmdObjBuilder getCurrentBranchInfo func() (BranchInfo, error) config BranchLoaderConfigCommands } func NewBranchLoader( cmn *common.Common, - getRawBranches func() (string, error), + cmd oscommands.ICmdObjBuilder, getCurrentBranchInfo func() (BranchInfo, error), config BranchLoaderConfigCommands, ) *BranchLoader { return &BranchLoader{ Common: cmn, - getRawBranches: getRawBranches, + cmd: cmd, getCurrentBranchInfo: getCurrentBranchInfo, config: config, } @@ -128,8 +131,8 @@ func (self *BranchLoader) obtainBranches() []*models.Branch { } split := strings.Split(line, "\x00") - if len(split) != 4 { - // Ignore line if it isn't separated into 4 parts + if len(split) != len(branchFields) { + // Ignore line if it isn't separated into the expected number of parts // This is probably a warning message, for more info see: // https://github.com/jesseduffield/lazygit/issues/1385#issuecomment-885580439 return nil, false @@ -139,47 +142,81 @@ func (self *BranchLoader) obtainBranches() []*models.Branch { }) } -// Obtain branch information from parsed line output of getRawBranches() -// split contains the '|' separated tokens in the line of output -func obtainBranch(split []string) *models.Branch { - name := strings.TrimPrefix(split[1], "heads/") - branch := &models.Branch{ - Name: name, - Pullables: "?", - Pushables: "?", - Head: split[0] == "*", - } +func (self *BranchLoader) getRawBranches() (string, error) { + format := strings.Join( + lo.Map(branchFields, func(thing string, _ int) string { + return "%(" + thing + ")" + }), + "%00", + ) + cmdArgs := NewGitCmd("for-each-ref"). + Arg("--sort=-committerdate"). + Arg(fmt.Sprintf("--format=%s", format)). + Arg("refs/heads"). + ToArgv() + + return self.cmd.New(cmdArgs).DontLog().RunWithOutput() +} + +var branchFields = []string{ + "HEAD", + "refname:short", + "upstream:short", + "upstream:track", + "subject", + fmt.Sprintf("objectname:short=%d", utils.COMMIT_HASH_SHORT_SIZE), +} + +// Obtain branch information from parsed line output of getRawBranches() +func obtainBranch(split []string) *models.Branch { + headMarker := split[0] + fullName := split[1] upstreamName := split[2] + track := split[3] + subject := split[4] + commitHash := split[5] + + name := strings.TrimPrefix(fullName, "heads/") + pushables, pullables, gone := parseUpstreamInfo(upstreamName, track) + + return &models.Branch{ + Name: name, + Pushables: pushables, + Pullables: pullables, + UpstreamGone: gone, + Head: headMarker == "*", + Subject: subject, + CommitHash: commitHash, + } +} + +func parseUpstreamInfo(upstreamName string, track string) (string, string, bool) { if upstreamName == "" { // if we're here then it means we do not have a local version of the remote. // The branch might still be tracking a remote though, we just don't know // how many commits ahead/behind it is - return branch + return "?", "?", false } - track := split[3] if track == "[gone]" { - branch.UpstreamGone = true - } else { - re := regexp.MustCompile(`ahead (\d+)`) - match := re.FindStringSubmatch(track) - if len(match) > 1 { - branch.Pushables = match[1] - } else { - branch.Pushables = "0" - } - - re = regexp.MustCompile(`behind (\d+)`) - match = re.FindStringSubmatch(track) - if len(match) > 1 { - branch.Pullables = match[1] - } else { - branch.Pullables = "0" - } + return "?", "?", true } - return branch + pushables := parseDifference(track, `ahead (\d+)`) + pullables := parseDifference(track, `behind (\d+)`) + + return pushables, pullables, false +} + +func parseDifference(track string, regexStr string) string { + re := regexp.MustCompile(regexStr) + match := re.FindStringSubmatch(track) + if len(match) > 1 { + return match[1] + } else { + return "0" + } } // TODO: only look at the new reflog commits, and otherwise store the recencies in diff --git a/pkg/commands/git_commands/branch_loader_test.go b/pkg/commands/git_commands/branch_loader_test.go index c147c1484..f16dcf5f4 100644 --- a/pkg/commands/git_commands/branch_loader_test.go +++ b/pkg/commands/git_commands/branch_loader_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestObtainBanch(t *testing.T) { +func TestObtainBranch(t *testing.T) { type scenario struct { testName string input []string @@ -17,29 +17,65 @@ func TestObtainBanch(t *testing.T) { scenarios := []scenario{ { - testName: "TrimHeads", - input: []string{"", "heads/a_branch", "", ""}, - expectedBranch: &models.Branch{Name: "a_branch", Pushables: "?", Pullables: "?", Head: false}, + testName: "TrimHeads", + input: []string{"", "heads/a_branch", "", "", "subject", "123"}, + expectedBranch: &models.Branch{ + Name: "a_branch", + Pushables: "?", + Pullables: "?", + Head: false, + Subject: "subject", + CommitHash: "123", + }, }, { - testName: "NoUpstream", - input: []string{"", "a_branch", "", ""}, - expectedBranch: &models.Branch{Name: "a_branch", Pushables: "?", Pullables: "?", Head: false}, + testName: "NoUpstream", + input: []string{"", "a_branch", "", "", "subject", "123"}, + expectedBranch: &models.Branch{ + Name: "a_branch", + Pushables: "?", + Pullables: "?", + Head: false, + Subject: "subject", + CommitHash: "123", + }, }, { - testName: "IsHead", - input: []string{"*", "a_branch", "", ""}, - expectedBranch: &models.Branch{Name: "a_branch", Pushables: "?", Pullables: "?", Head: true}, + testName: "IsHead", + input: []string{"*", "a_branch", "", "", "subject", "123"}, + expectedBranch: &models.Branch{ + Name: "a_branch", + Pushables: "?", + Pullables: "?", + Head: true, + Subject: "subject", + CommitHash: "123", + }, }, { - testName: "IsBehindAndAhead", - input: []string{"", "a_branch", "a_remote/a_branch", "[behind 2, ahead 3]"}, - expectedBranch: &models.Branch{Name: "a_branch", Pushables: "3", Pullables: "2", Head: false}, + testName: "IsBehindAndAhead", + input: []string{"", "a_branch", "a_remote/a_branch", "[behind 2, ahead 3]", "subject", "123"}, + expectedBranch: &models.Branch{ + Name: "a_branch", + Pushables: "3", + Pullables: "2", + Head: false, + Subject: "subject", + CommitHash: "123", + }, }, { - testName: "RemoteBranchIsGone", - input: []string{"", "a_branch", "a_remote/a_branch", "[gone]"}, - expectedBranch: &models.Branch{Name: "a_branch", UpstreamGone: true, Pushables: "?", Pullables: "?", Head: false}, + testName: "RemoteBranchIsGone", + input: []string{"", "a_branch", "a_remote/a_branch", "[gone]", "subject", "123"}, + expectedBranch: &models.Branch{ + Name: "a_branch", + UpstreamGone: true, + Pushables: "?", + Pullables: "?", + Head: false, + Subject: "subject", + CommitHash: "123", + }, }, } diff --git a/pkg/commands/models/branch.go b/pkg/commands/models/branch.go index 895b0442a..b4dcc0a79 100644 --- a/pkg/commands/models/branch.go +++ b/pkg/commands/models/branch.go @@ -5,11 +5,16 @@ package models type Branch struct { Name string // the displayname is something like '(HEAD detached at 123asdf)', whereas in that case the name would be '123asdf' - DisplayName string - Recency string - Pushables string - Pullables string + DisplayName string + // indicator of when the branch was last checked out e.g. '2d', '3m' + Recency string + // how many commits ahead we are from the remote branch (how many commits we can push) + Pushables string + // how many commits behind we are from the remote branch (how many commits we can pull) + Pullables string + // whether the remote branch is 'gone' i.e. we're tracking a remote branch that has been deleted UpstreamGone bool + // whether this is the current branch. Exactly one branch should have this be true Head bool DetachedHead bool // if we have a named remote locally this will be the name of that remote e.g. @@ -17,6 +22,10 @@ type Branch struct { // 'git@github.com:tiwood/lazygit.git' UpstreamRemote string UpstreamBranch string + // subject line in commit message + Subject string + // commit hash + CommitHash string } func (b *Branch) FullRefName() string { diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go index 295c8178d..597253e3d 100644 --- a/pkg/config/user_config.go +++ b/pkg/config/user_config.go @@ -49,6 +49,7 @@ type GuiConfig struct { ShowCommandLog bool `yaml:"showCommandLog"` ShowBottomLine bool `yaml:"showBottomLine"` ShowIcons bool `yaml:"showIcons"` + ShowBranchCommitHash bool `yaml:"showBranchCommitHash"` ExperimentalShowBranchHeads bool `yaml:"experimentalShowBranchHeads"` CommandLogSize int `yaml:"commandLogSize"` SplitDiff string `yaml:"splitDiff"` @@ -426,6 +427,7 @@ func GetDefaultConfig() *UserConfig { ShowRandomTip: true, ShowIcons: false, ExperimentalShowBranchHeads: false, + ShowBranchCommitHash: false, CommandLogSize: 8, SplitDiff: "auto", SkipRewordInEditorWarning: false, diff --git a/pkg/gui/context/branches_context.go b/pkg/gui/context/branches_context.go index 1d6453c13..c2463ad20 100644 --- a/pkg/gui/context/branches_context.go +++ b/pkg/gui/context/branches_context.go @@ -25,6 +25,7 @@ func NewBranchesContext(c *ContextCommon) *BranchesContext { c.State().GetRepoState().GetScreenMode() != types.SCREEN_NORMAL, c.Modes().Diffing.Ref, c.Tr, + c.UserConfig, ) } diff --git a/pkg/gui/presentation/branches.go b/pkg/gui/presentation/branches.go index bb4570654..cf0802d6f 100644 --- a/pkg/gui/presentation/branches.go +++ b/pkg/gui/presentation/branches.go @@ -6,6 +6,7 @@ import ( "github.com/jesseduffield/generics/slices" "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/gui/presentation/icons" "github.com/jesseduffield/lazygit/pkg/gui/style" "github.com/jesseduffield/lazygit/pkg/i18n" @@ -15,15 +16,27 @@ import ( var branchPrefixColorCache = make(map[string]style.TextStyle) -func GetBranchListDisplayStrings(branches []*models.Branch, fullDescription bool, diffName string, tr *i18n.TranslationSet) [][]string { +func GetBranchListDisplayStrings( + branches []*models.Branch, + fullDescription bool, + diffName string, + tr *i18n.TranslationSet, + userConfig *config.UserConfig, +) [][]string { return slices.Map(branches, func(branch *models.Branch) []string { diffed := branch.Name == diffName - return getBranchDisplayStrings(branch, fullDescription, diffed, tr) + return getBranchDisplayStrings(branch, fullDescription, diffed, tr, userConfig) }) } // getBranchDisplayStrings returns the display string of branch -func getBranchDisplayStrings(b *models.Branch, fullDescription bool, diffed bool, tr *i18n.TranslationSet) []string { +func getBranchDisplayStrings( + b *models.Branch, + fullDescription bool, + diffed bool, + tr *i18n.TranslationSet, + userConfig *config.UserConfig, +) []string { displayName := b.Name if b.DisplayName != "" { displayName = b.DisplayName @@ -43,12 +56,18 @@ func getBranchDisplayStrings(b *models.Branch, fullDescription bool, diffed bool recencyColor = style.FgGreen } - res := make([]string, 0, 4) + res := make([]string, 0, 6) res = append(res, recencyColor.Sprint(b.Recency)) if icons.IsIconEnabled() { res = append(res, nameTextStyle.Sprint(icons.IconForBranch(b))) } + + if fullDescription || userConfig.Gui.ShowBranchCommitHash { + res = append(res, b.CommitHash) + } + res = append(res, coloredName) + if fullDescription { res = append( res, @@ -56,6 +75,7 @@ func getBranchDisplayStrings(b *models.Branch, fullDescription bool, diffed bool style.FgYellow.Sprint(b.UpstreamRemote), style.FgYellow.Sprint(b.UpstreamBranch), ), + utils.TruncateWithEllipsis(b.Subject, 60), ) } return res diff --git a/pkg/utils/formatting.go b/pkg/utils/formatting.go index 2a900d207..5d04a16a8 100644 --- a/pkg/utils/formatting.go +++ b/pkg/utils/formatting.go @@ -149,9 +149,11 @@ func SafeTruncate(str string, limit int) string { } } +const COMMIT_HASH_SHORT_SIZE = 8 + func ShortSha(sha string) string { - if len(sha) < 8 { + if len(sha) < COMMIT_HASH_SHORT_SIZE { return sha } - return sha[:8] + return sha[:COMMIT_HASH_SHORT_SIZE] }