diff --git a/docs/Config.md b/docs/Config.md index bc4b5b92b69..447ccae5f35 100644 --- a/docs/Config.md +++ b/docs/Config.md @@ -62,7 +62,6 @@ gui: 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 showIcons: false # deprecated: use nerdFontsVersion instead diff --git a/docs/README.md b/docs/README.md index acce8338c7d..604c8a07a9d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -6,4 +6,5 @@ * [Keybindings](./keybindings) * [Undo/Redo](./Undoing.md) * [Searching/Filtering](./Searching.md) +* [Stacked Branches](./Stacked_Branches.md) * [Dev docs](./dev) diff --git a/docs/Stacked_Branches.md b/docs/Stacked_Branches.md new file mode 100644 index 00000000000..b73e4aca59b --- /dev/null +++ b/docs/Stacked_Branches.md @@ -0,0 +1,18 @@ +# Working with stacked branches + +When working on a large branch it can often be useful to break it down into +smaller pieces, and it can help to create separate branches for each independent +chunk of changes. For example, you could have one branch for preparatory +refactorings, one for backend changes, and one for frontend changes. Those +branches would then all be stacked onto each other. + +Git has support for rebasing such a stack as a whole; you can enable it by +setting the git config `rebase.updateRfs` to true. If you then rebase the +topmost branch of the stack, the other ones in the stack will follow. This +includes interactive rebases, so for example amending a commit in the first +branch of the stack will "just work" in the sense that it keeps the other +branches properly stacked onto it. + +Lazygit visualizes the invidual branch heads in the stack by marking them with a +cyan asterisk (or a cyan branch symbol if you are using [nerd +fonts](Config.md#display-nerd-fonts-icons)). diff --git a/pkg/commands/git_commands/branch.go b/pkg/commands/git_commands/branch.go index caa876f3f8e..24244080bda 100644 --- a/pkg/commands/git_commands/branch.go +++ b/pkg/commands/git_commands/branch.go @@ -70,6 +70,21 @@ func (self *BranchCommands) CurrentBranchInfo() (BranchInfo, error) { }, 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 +} + // Delete delete branch func (self *BranchCommands) Delete(branch string, force bool) error { cmdArgs := NewGitCmd("branch"). diff --git a/pkg/commands/git_commands/branch_loader.go b/pkg/commands/git_commands/branch_loader.go index 094354cadbf..b7e55a9108e 100644 --- a/pkg/commands/git_commands/branch_loader.go +++ b/pkg/commands/git_commands/branch_loader.go @@ -171,7 +171,7 @@ var branchFields = []string{ "upstream:short", "upstream:track", "subject", - fmt.Sprintf("objectname:short=%d", utils.COMMIT_HASH_SHORT_SIZE), + "objectname", } // Obtain branch information from parsed line output of getRawBranches() diff --git a/pkg/commands/git_commands/config.go b/pkg/commands/git_commands/config.go index 46a00f9dbbb..de9707fe9ba 100644 --- a/pkg/commands/git_commands/config.go +++ b/pkg/commands/git_commands/config.go @@ -107,3 +107,7 @@ func (self *ConfigCommands) GetCoreCommentChar() byte { return '#' } + +func (self *ConfigCommands) GetRebaseUpdateRefs() bool { + return self.gitConfig.GetBool("rebase.updateRefs") +} diff --git a/pkg/commands/git_commands/status.go b/pkg/commands/git_commands/status.go index 13ff02cc015..784ddb424db 100644 --- a/pkg/commands/git_commands/status.go +++ b/pkg/commands/git_commands/status.go @@ -1,6 +1,7 @@ package git_commands import ( + "os" "path/filepath" "strconv" "strings" @@ -71,3 +72,14 @@ func IsBareRepo(osCommand *oscommands.OSCommand) (bool, error) { func (self *StatusCommands) IsInMergeState() (bool, error) { return self.os.FileExists(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "MERGE_HEAD")) } + +// Full ref (e.g. "refs/heads/mybranch") of the branch that is currently +// being rebased, or empty string when we're not in a rebase +func (self *StatusCommands) BranchBeingRebased() string { + for _, dir := range []string{"rebase-merge", "rebase-apply"} { + if bytesContent, err := os.ReadFile(filepath.Join(self.repoPaths.WorktreeGitDirPath(), dir, "head-name")); err == nil { + return strings.TrimSpace(string(bytesContent)) + } + } + return "" +} diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go index 6ac64b2b57c..a5121f0c5ed 100644 --- a/pkg/config/user_config.go +++ b/pkg/config/user_config.go @@ -27,36 +27,35 @@ type RefresherConfig struct { } type GuiConfig struct { - AuthorColors map[string]string `yaml:"authorColors"` - BranchColors map[string]string `yaml:"branchColors"` - ScrollHeight int `yaml:"scrollHeight"` - ScrollPastBottom bool `yaml:"scrollPastBottom"` - MouseEvents bool `yaml:"mouseEvents"` - SkipDiscardChangeWarning bool `yaml:"skipDiscardChangeWarning"` - SkipStashWarning bool `yaml:"skipStashWarning"` - SidePanelWidth float64 `yaml:"sidePanelWidth"` - ExpandFocusedSidePanel bool `yaml:"expandFocusedSidePanel"` - MainPanelSplitMode string `yaml:"mainPanelSplitMode"` - Language string `yaml:"language"` - TimeFormat string `yaml:"timeFormat"` - ShortTimeFormat string `yaml:"shortTimeFormat"` - Theme ThemeConfig `yaml:"theme"` - CommitLength CommitLengthConfig `yaml:"commitLength"` - SkipNoStagedFilesWarning bool `yaml:"skipNoStagedFilesWarning"` - ShowListFooter bool `yaml:"showListFooter"` - ShowFileTree bool `yaml:"showFileTree"` - ShowRandomTip bool `yaml:"showRandomTip"` - ShowCommandLog bool `yaml:"showCommandLog"` - ShowBottomLine bool `yaml:"showBottomLine"` - ShowIcons bool `yaml:"showIcons"` - NerdFontsVersion string `yaml:"nerdFontsVersion"` - ShowBranchCommitHash bool `yaml:"showBranchCommitHash"` - ExperimentalShowBranchHeads bool `yaml:"experimentalShowBranchHeads"` - CommandLogSize int `yaml:"commandLogSize"` - SplitDiff string `yaml:"splitDiff"` - SkipRewordInEditorWarning bool `yaml:"skipRewordInEditorWarning"` - WindowSize string `yaml:"windowSize"` - Border string `yaml:"border"` + AuthorColors map[string]string `yaml:"authorColors"` + BranchColors map[string]string `yaml:"branchColors"` + ScrollHeight int `yaml:"scrollHeight"` + ScrollPastBottom bool `yaml:"scrollPastBottom"` + MouseEvents bool `yaml:"mouseEvents"` + SkipDiscardChangeWarning bool `yaml:"skipDiscardChangeWarning"` + SkipStashWarning bool `yaml:"skipStashWarning"` + SidePanelWidth float64 `yaml:"sidePanelWidth"` + ExpandFocusedSidePanel bool `yaml:"expandFocusedSidePanel"` + MainPanelSplitMode string `yaml:"mainPanelSplitMode"` + Language string `yaml:"language"` + TimeFormat string `yaml:"timeFormat"` + ShortTimeFormat string `yaml:"shortTimeFormat"` + Theme ThemeConfig `yaml:"theme"` + CommitLength CommitLengthConfig `yaml:"commitLength"` + SkipNoStagedFilesWarning bool `yaml:"skipNoStagedFilesWarning"` + ShowListFooter bool `yaml:"showListFooter"` + ShowFileTree bool `yaml:"showFileTree"` + ShowRandomTip bool `yaml:"showRandomTip"` + ShowCommandLog bool `yaml:"showCommandLog"` + ShowBottomLine bool `yaml:"showBottomLine"` + ShowIcons bool `yaml:"showIcons"` + NerdFontsVersion string `yaml:"nerdFontsVersion"` + ShowBranchCommitHash bool `yaml:"showBranchCommitHash"` + CommandLogSize int `yaml:"commandLogSize"` + SplitDiff string `yaml:"splitDiff"` + SkipRewordInEditorWarning bool `yaml:"skipRewordInEditorWarning"` + WindowSize string `yaml:"windowSize"` + Border string `yaml:"border"` } type ThemeConfig struct { @@ -436,21 +435,20 @@ func GetDefaultConfig() *UserConfig { UnstagedChangesColor: []string{"red"}, DefaultFgColor: []string{"default"}, }, - CommitLength: CommitLengthConfig{Show: true}, - SkipNoStagedFilesWarning: false, - ShowListFooter: true, - ShowCommandLog: true, - ShowBottomLine: true, - ShowFileTree: true, - ShowRandomTip: true, - ShowIcons: false, - NerdFontsVersion: "", - ExperimentalShowBranchHeads: false, - ShowBranchCommitHash: false, - CommandLogSize: 8, - SplitDiff: "auto", - SkipRewordInEditorWarning: false, - Border: "single", + CommitLength: CommitLengthConfig{Show: true}, + SkipNoStagedFilesWarning: false, + ShowListFooter: true, + ShowCommandLog: true, + ShowBottomLine: true, + ShowFileTree: true, + ShowRandomTip: true, + ShowIcons: false, + NerdFontsVersion: "", + ShowBranchCommitHash: false, + CommandLogSize: 8, + SplitDiff: "auto", + SkipRewordInEditorWarning: false, + Border: "single", }, Git: GitConfig{ Paging: PagingConfig{ diff --git a/pkg/gui/context/branches_context.go b/pkg/gui/context/branches_context.go index 85c45c64a80..e4806165f3b 100644 --- a/pkg/gui/context/branches_context.go +++ b/pkg/gui/context/branches_context.go @@ -83,3 +83,7 @@ func (self *BranchesContext) GetDiffTerminals() []string { } return nil } + +func (self *BranchesContext) ShowBranchHeadsInSubCommits() bool { + return true +} diff --git a/pkg/gui/context/local_commits_context.go b/pkg/gui/context/local_commits_context.go index 84204591cba..417a4f30a65 100644 --- a/pkg/gui/context/local_commits_context.go +++ b/pkg/gui/context/local_commits_context.go @@ -38,10 +38,14 @@ func NewLocalCommitsContext(c *ContextCommon) *LocalCommitsContext { } showYouAreHereLabel := c.Model().WorkingTreeStateAtLastCommitRefresh == enums.REBASE_MODE_REBASING + showBranchMarkerForHeadCommit := c.Git().Config.GetRebaseUpdateRefs() return presentation.GetCommitListDisplayStrings( c.Common, c.Model().Commits, + c.Model().Branches, + c.Model().CheckedOutBranch, + showBranchMarkerForHeadCommit, c.State().GetRepoState().GetScreenMode() != types.SCREEN_NORMAL, c.Modes().CherryPicking.SelectedShaSet(), c.Modes().Diffing.Ref, diff --git a/pkg/gui/context/reflog_commits_context.go b/pkg/gui/context/reflog_commits_context.go index 421a7c8d5a3..5038b18706e 100644 --- a/pkg/gui/context/reflog_commits_context.go +++ b/pkg/gui/context/reflog_commits_context.go @@ -86,3 +86,7 @@ func (self *ReflogCommitsContext) GetDiffTerminals() []string { return []string{itemId} } + +func (self *ReflogCommitsContext) ShowBranchHeadsInSubCommits() bool { + return false +} diff --git a/pkg/gui/context/remote_branches_context.go b/pkg/gui/context/remote_branches_context.go index 602a19a65e8..fbc91f3526f 100644 --- a/pkg/gui/context/remote_branches_context.go +++ b/pkg/gui/context/remote_branches_context.go @@ -72,3 +72,7 @@ func (self *RemoteBranchesContext) GetDiffTerminals() []string { return []string{itemId} } + +func (self *RemoteBranchesContext) ShowBranchHeadsInSubCommits() bool { + return true +} diff --git a/pkg/gui/context/sub_commits_context.go b/pkg/gui/context/sub_commits_context.go index 0cf8845898c..ba2f5e3f60a 100644 --- a/pkg/gui/context/sub_commits_context.go +++ b/pkg/gui/context/sub_commits_context.go @@ -37,6 +37,13 @@ func NewSubCommitsContext( } getDisplayStrings := func(startIdx int, length int) [][]string { + // This can happen if a sub-commits view is asked to be rerendered while + // it is invisble; for example when switching screen modes, which + // rerenders all views. + if viewModel.GetRef() == nil { + return [][]string{} + } + selectedCommitSha := "" if c.CurrentContext().GetKey() == SUB_COMMITS_CONTEXT_KEY { selectedCommit := viewModel.GetSelected() @@ -44,9 +51,17 @@ func NewSubCommitsContext( selectedCommitSha = selectedCommit.Sha } } + branches := []*models.Branch{} + if viewModel.GetShowBranchHeads() { + branches = c.Model().Branches + } + showBranchMarkerForHeadCommit := c.Git().Config.GetRebaseUpdateRefs() return presentation.GetCommitListDisplayStrings( c.Common, c.Model().SubCommits, + branches, + viewModel.GetRef().RefName(), + showBranchMarkerForHeadCommit, c.State().GetRepoState().GetScreenMode() != types.SCREEN_NORMAL, c.Modes().CherryPicking.SelectedShaSet(), c.Modes().Diffing.Ref, @@ -97,7 +112,8 @@ type SubCommitsViewModel struct { ref types.Ref *ListViewModel[*models.Commit] - limitCommits bool + limitCommits bool + showBranchHeads bool } func (self *SubCommitsViewModel) SetRef(ref types.Ref) { @@ -108,6 +124,14 @@ func (self *SubCommitsViewModel) GetRef() types.Ref { return self.ref } +func (self *SubCommitsViewModel) SetShowBranchHeads(value bool) { + self.showBranchHeads = value +} + +func (self *SubCommitsViewModel) GetShowBranchHeads() bool { + return self.showBranchHeads +} + func (self *SubCommitsContext) GetSelectedItemId() string { item := self.GetSelected() if item == nil { diff --git a/pkg/gui/context/tags_context.go b/pkg/gui/context/tags_context.go index 71ea369814e..95b845a28e9 100644 --- a/pkg/gui/context/tags_context.go +++ b/pkg/gui/context/tags_context.go @@ -69,3 +69,7 @@ func (self *TagsContext) GetDiffTerminals() []string { return []string{itemId} } + +func (self *TagsContext) ShowBranchHeadsInSubCommits() bool { + return true +} diff --git a/pkg/gui/controllers/helpers/refresh_helper.go b/pkg/gui/controllers/helpers/refresh_helper.go index 182e20d85a6..fd586c4d596 100644 --- a/pkg/gui/controllers/helpers/refresh_helper.go +++ b/pkg/gui/controllers/helpers/refresh_helper.go @@ -272,6 +272,32 @@ func (self *RefreshHelper) refreshCommitsAndCommitFiles() { } } +func (self *RefreshHelper) determineCheckedOutBranchName() string { + if rebasedBranch := self.c.Git().Status.BranchBeingRebased(); rebasedBranch != "" { + // During a rebase we're on a detached head, so cannot determine the + // branch name in the usual way. We need to read it from the + // ".git/rebase-merge/head-name" file instead. + return strings.TrimPrefix(rebasedBranch, "refs/heads/") + } + + if bisectInfo := self.c.Git().Bisect.GetInfo(); bisectInfo.Bisecting() && bisectInfo.GetStartSha() != "" { + // Likewise, when we're bisecting we're on a detached head as well. In + // this case we read the branch name from the ".git/BISECT_START" file. + return bisectInfo.GetStartSha() + } + + // In all other cases, get the branch name by asking git what branch is + // checked out. Note that if we're on a detached head (for reasons other + // than rebasing or bisecting, i.e. it was explicitly checked out), then + // this will return its sha. + if branchName, err := self.c.Git().Branch.CurrentBranchName(); err == nil { + return branchName + } + + // Should never get here unless the working copy is corrupt + return "" +} + func (self *RefreshHelper) refreshCommitsWithLimit() error { self.c.Mutexes().LocalCommitsMutex.Lock() defer self.c.Mutexes().LocalCommitsMutex.Unlock() @@ -291,6 +317,7 @@ func (self *RefreshHelper) refreshCommitsWithLimit() error { self.c.Model().Commits = commits self.RefreshAuthors(commits) self.c.Model().WorkingTreeStateAtLastCommitRefresh = self.c.Git().Status.WorkingTreeState() + self.c.Model().CheckedOutBranch = self.determineCheckedOutBranchName() return self.c.PostRefreshUpdate(self.c.Contexts().LocalCommits) } @@ -412,6 +439,12 @@ func (self *RefreshHelper) refreshBranches() { self.c.Log.Error(err) } + // Need to re-render the commits view because the visualization of local + // branch heads might have changed + if err := self.c.Contexts().LocalCommits.HandleRender(); err != nil { + self.c.Log.Error(err) + } + self.refreshStatus() } diff --git a/pkg/gui/controllers/switch_to_sub_commits_controller.go b/pkg/gui/controllers/switch_to_sub_commits_controller.go index 8aa92f14bd6..68fc7d0a08f 100644 --- a/pkg/gui/controllers/switch_to_sub_commits_controller.go +++ b/pkg/gui/controllers/switch_to_sub_commits_controller.go @@ -11,6 +11,7 @@ var _ types.IController = &SwitchToSubCommitsController{} type CanSwitchToSubCommits interface { types.Context GetSelectedRef() types.Ref + ShowBranchHeadsInSubCommits() bool } type SwitchToSubCommitsController struct { @@ -79,6 +80,7 @@ func (self *SwitchToSubCommitsController) viewCommits() error { subCommitsContext.SetTitleRef(ref.Description()) subCommitsContext.SetRef(ref) subCommitsContext.SetLimitCommits(true) + subCommitsContext.SetShowBranchHeads(self.context.ShowBranchHeadsInSubCommits()) subCommitsContext.ClearSearchString() subCommitsContext.GetView().ClearSearch() diff --git a/pkg/gui/presentation/branches.go b/pkg/gui/presentation/branches.go index 849a56ebabd..6d4620238a8 100644 --- a/pkg/gui/presentation/branches.go +++ b/pkg/gui/presentation/branches.go @@ -71,7 +71,7 @@ func getBranchDisplayStrings( } if fullDescription || userConfig.Gui.ShowBranchCommitHash { - res = append(res, b.CommitHash) + res = append(res, utils.ShortSha(b.CommitHash)) } res = append(res, coloredName) diff --git a/pkg/gui/presentation/commits.go b/pkg/gui/presentation/commits.go index 4da03f02fbd..c00564e9df0 100644 --- a/pkg/gui/presentation/commits.go +++ b/pkg/gui/presentation/commits.go @@ -39,6 +39,9 @@ type bisectBounds struct { func GetCommitListDisplayStrings( common *common.Common, commits []*models.Commit, + branches []*models.Branch, + currentBranchName string, + showBranchMarkerForHeadCommit bool, fullDescription bool, cherryPickedCommitShaSet *set.Set[string], diffName string, @@ -99,6 +102,30 @@ func GetCommitListDisplayStrings( getGraphLine = func(idx int) string { return "" } } + // Determine the hashes of the local branches for which we want to show a + // branch marker in the commits list. We only want to do this for branches + // that are not the current branch, and not any of the main branches. The + // goal is to visualize stacks of local branches, so anything that doesn't + // contribute to a branch stack shouldn't show a marker. + // + // If there are other branches pointing to the current head commit, we only + // want to show the marker if the rebase.updateRefs config is on. + branchHeadsToVisualize := set.NewFromSlice(lo.FilterMap(branches, + func(b *models.Branch, index int) (string, bool) { + return b.CommitHash, + // Don't consider branches that don't have a commit hash. As far + // as I can see, this happens for a detached head, so filter + // these out + b.CommitHash != "" && + // Don't show a marker for the current branch + b.Name != currentBranchName && + // Don't show a marker for main branches + !lo.Contains(common.UserConfig.Git.MainBranches, b.Name) && + // Don't show a marker for the head commit unless the + // rebase.updateRefs config is on + (showBranchMarkerForHeadCommit || b.CommitHash != commits[0].Sha) + })) + lines := make([][]string, 0, len(filteredCommits)) var bisectStatus BisectStatus for i, commit := range filteredCommits { @@ -112,6 +139,7 @@ func GetCommitListDisplayStrings( lines = append(lines, displayCommit( common, commit, + branchHeadsToVisualize, cherryPickedCommitShaSet, diffName, timeFormat, @@ -260,6 +288,7 @@ func getBisectStatusText(bisectStatus BisectStatus, bisectInfo *git_commands.Bis func displayCommit( common *common.Common, commit *models.Commit, + branchHeadsToVisualize *set.Set[string], cherryPickedCommitShaSet *set.Set[string], diffName string, timeFormat string, @@ -289,8 +318,11 @@ func displayCommit( } else { if len(commit.Tags) > 0 { tagString = theme.DiffTerminalColor.SetBold().Sprint(strings.Join(commit.Tags, " ")) + " " - } else if common.UserConfig.Gui.ExperimentalShowBranchHeads && commit.ExtraInfo != "" { - tagString = style.FgMagenta.SetBold().Sprint("(*)") + " " + } + + if branchHeadsToVisualize.Includes(commit.Sha) && commit.Status != models.StatusMerged { + tagString = style.FgCyan.SetBold().Sprint( + lo.Ternary(icons.IsIconEnabled(), icons.BRANCH_ICON, "*") + " " + tagString) } } diff --git a/pkg/gui/presentation/commits_test.go b/pkg/gui/presentation/commits_test.go index 31c6c813dae..4bf4c09af03 100644 --- a/pkg/gui/presentation/commits_test.go +++ b/pkg/gui/presentation/commits_test.go @@ -28,6 +28,9 @@ func TestGetCommitListDisplayStrings(t *testing.T) { scenarios := []struct { testName string commits []*models.Commit + branches []*models.Branch + currentBranchName string + hasUpdateRefConfig bool fullDescription bool cherryPickedCommitShaSet *set.Set[string] diffName string @@ -72,6 +75,120 @@ func TestGetCommitListDisplayStrings(t *testing.T) { sha2 commit2 `), }, + { + testName: "commit with tags", + commits: []*models.Commit{ + {Name: "commit1", Sha: "sha1", Tags: []string{"tag1", "tag2"}}, + {Name: "commit2", Sha: "sha2"}, + }, + startIdx: 0, + length: 2, + showGraph: false, + bisectInfo: git_commands.NewNullBisectInfo(), + cherryPickedCommitShaSet: set.New[string](), + now: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), + expected: formatExpected(` + sha1 tag1 tag2 commit1 + sha2 commit2 + `), + }, + { + testName: "show local branch head, except the current branch, main branches, or merged branches", + commits: []*models.Commit{ + {Name: "commit1", Sha: "sha1"}, + {Name: "commit2", Sha: "sha2"}, + {Name: "commit3", Sha: "sha3"}, + {Name: "commit4", Sha: "sha4", Status: models.StatusMerged}, + }, + branches: []*models.Branch{ + {Name: "current-branch", CommitHash: "sha1", Head: true}, + {Name: "other-branch", CommitHash: "sha2", Head: false}, + {Name: "master", CommitHash: "sha3", Head: false}, + {Name: "old-branch", CommitHash: "sha4", Head: false}, + }, + currentBranchName: "current-branch", + hasUpdateRefConfig: true, + startIdx: 0, + length: 4, + showGraph: false, + bisectInfo: git_commands.NewNullBisectInfo(), + cherryPickedCommitShaSet: set.New[string](), + now: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), + expected: formatExpected(` + sha1 commit1 + sha2 * commit2 + sha3 commit3 + sha4 commit4 + `), + }, + { + testName: "show local branch head for head commit if updateRefs is on", + commits: []*models.Commit{ + {Name: "commit1", Sha: "sha1"}, + {Name: "commit2", Sha: "sha2"}, + }, + branches: []*models.Branch{ + {Name: "current-branch", CommitHash: "sha1", Head: true}, + {Name: "other-branch", CommitHash: "sha1", Head: false}, + }, + currentBranchName: "current-branch", + hasUpdateRefConfig: true, + startIdx: 0, + length: 2, + showGraph: false, + bisectInfo: git_commands.NewNullBisectInfo(), + cherryPickedCommitShaSet: set.New[string](), + now: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), + expected: formatExpected(` + sha1 * commit1 + sha2 commit2 + `), + }, + { + testName: "don't show local branch head for head commit if updateRefs is off", + commits: []*models.Commit{ + {Name: "commit1", Sha: "sha1"}, + {Name: "commit2", Sha: "sha2"}, + }, + branches: []*models.Branch{ + {Name: "current-branch", CommitHash: "sha1", Head: true}, + {Name: "other-branch", CommitHash: "sha1", Head: false}, + }, + currentBranchName: "current-branch", + hasUpdateRefConfig: false, + startIdx: 0, + length: 2, + showGraph: false, + bisectInfo: git_commands.NewNullBisectInfo(), + cherryPickedCommitShaSet: set.New[string](), + now: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), + expected: formatExpected(` + sha1 commit1 + sha2 commit2 + `), + }, + { + testName: "show local branch head and tag if both exist", + commits: []*models.Commit{ + {Name: "commit1", Sha: "sha1"}, + {Name: "commit2", Sha: "sha2", Tags: []string{"some-tag"}}, + {Name: "commit3", Sha: "sha3"}, + }, + branches: []*models.Branch{ + {Name: "some-branch", CommitHash: "sha2"}, + }, + startIdx: 0, + length: 3, + showGraph: false, + bisectInfo: git_commands.NewNullBisectInfo(), + cherryPickedCommitShaSet: set.New[string](), + now: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), + expected: formatExpected(` + sha1 commit1 + sha2 * some-tag commit2 + sha3 commit3 + `), + }, { testName: "showing graph", commits: []*models.Commit{ @@ -285,6 +402,9 @@ func TestGetCommitListDisplayStrings(t *testing.T) { result := GetCommitListDisplayStrings( common, s.commits, + s.branches, + s.currentBranchName, + s.hasUpdateRefConfig, s.fullDescription, s.cherryPickedCommitShaSet, s.diffName, diff --git a/pkg/gui/types/common.go b/pkg/gui/types/common.go index 8be1c869793..6480f1fcc20 100644 --- a/pkg/gui/types/common.go +++ b/pkg/gui/types/common.go @@ -217,6 +217,10 @@ type Model struct { RemoteBranches []*models.RemoteBranch Tags []*models.Tag + // Name of the currently checked out branch. This will be set even when + // we're on a detached head because we're rebasing or bisecting. + CheckedOutBranch string + // for displaying suggestions while typing in a file name FilesTrie *patricia.Trie diff --git a/pkg/integration/tests/bisect/basic.go b/pkg/integration/tests/bisect/basic.go index e17f0d50f1a..1dfe6368b01 100644 --- a/pkg/integration/tests/bisect/basic.go +++ b/pkg/integration/tests/bisect/basic.go @@ -11,6 +11,7 @@ var Basic = NewIntegrationTest(NewIntegrationTestArgs{ Skip: false, SetupRepo: func(shell *Shell) { shell. + NewBranch("mybranch"). CreateNCommits(10) }, SetupConfig: func(cfg *config.AppConfig) {}, @@ -31,20 +32,21 @@ var Basic = NewIntegrationTest(NewIntegrationTestArgs{ t.Views().Commits(). Focus(). - SelectedLine(Contains("commit 10")). - NavigateToLine(Contains("commit 09")). + SelectedLine(Contains("CI commit 10")). + NavigateToLine(Contains("CI commit 09")). Tap(func() { markCommitAsBad() t.Views().Information().Content(Contains("Bisecting")) }). SelectedLine(Contains("<-- bad")). - NavigateToLine(Contains("commit 02")). + NavigateToLine(Contains("CI commit 02")). Tap(markCommitAsGood). + TopLines(Contains("CI commit 10")). // lazygit will land us in the commit between our good and bad commits. - SelectedLine(Contains("commit 05").Contains("<-- current")). + SelectedLine(Contains("CI commit 05").Contains("<-- current")). Tap(markCommitAsBad). - SelectedLine(Contains("commit 04").Contains("<-- current")). + SelectedLine(Contains("CI commit 04").Contains("<-- current")). Tap(func() { markCommitAsGood() @@ -52,7 +54,7 @@ var Basic = NewIntegrationTest(NewIntegrationTestArgs{ t.ExpectPopup().Alert().Title(Equals("Bisect complete")).Content(MatchesRegexp("(?s)commit 05.*Do you want to reset")).Confirm() }). IsFocused(). - Content(Contains("commit 04")) + Content(Contains("CI commit 04")) t.Views().Information().Content(DoesNotContain("Bisecting")) }, diff --git a/pkg/integration/tests/interactive_rebase/drop_todo_commit_with_update_ref.go b/pkg/integration/tests/interactive_rebase/drop_todo_commit_with_update_ref.go index 4bd738d6a35..3fa221d7281 100644 --- a/pkg/integration/tests/interactive_rebase/drop_todo_commit_with_update_ref.go +++ b/pkg/integration/tests/interactive_rebase/drop_todo_commit_with_update_ref.go @@ -10,12 +10,16 @@ var DropTodoCommitWithUpdateRef = NewIntegrationTest(NewIntegrationTestArgs{ ExtraCmdArgs: []string{}, Skip: false, GitVersion: AtLeast("2.38.0"), - SetupConfig: func(config *config.AppConfig) {}, + SetupConfig: func(config *config.AppConfig) { + config.GetUserConfig().Git.MainBranches = []string{"master"} + }, SetupRepo: func(shell *Shell) { shell. - CreateNCommits(3). - NewBranch("mybranch"). - CreateNCommitsStartingAt(3, 4) + CreateNCommits(1). + NewBranch("branch1"). + CreateNCommitsStartingAt(3, 2). + NewBranch("branch2"). + CreateNCommitsStartingAt(3, 5) shell.SetConfig("rebase.updateRefs", "true") }, @@ -23,26 +27,28 @@ var DropTodoCommitWithUpdateRef = NewIntegrationTest(NewIntegrationTestArgs{ t.Views().Commits(). Focus(). Lines( - Contains("commit 06").IsSelected(), - Contains("commit 05"), - Contains("commit 04"), - Contains("commit 03"), - Contains("commit 02"), - Contains("commit 01"), + Contains("CI commit 07").IsSelected(), + Contains("CI commit 06"), + Contains("CI commit 05"), + Contains("CI * commit 04"), + Contains("CI commit 03"), + Contains("CI commit 02"), + Contains("CI commit 01"), ). - NavigateToLine(Contains("commit 01")). + NavigateToLine(Contains("commit 02")). Press(keys.Universal.Edit). Focus(). Lines( - Contains("pick").Contains("commit 06"), - Contains("pick").Contains("commit 05"), - Contains("pick").Contains("commit 04"), - Contains("update-ref").Contains("master"), - Contains("pick").Contains("commit 03"), - Contains("pick").Contains("commit 02"), - Contains("<-- YOU ARE HERE --- commit 01"), + Contains("pick").Contains("CI commit 07"), + Contains("pick").Contains("CI commit 06"), + Contains("pick").Contains("CI commit 05"), + Contains("update-ref").Contains("branch1").DoesNotContain("*"), + Contains("pick").Contains("CI * commit 04"), + Contains("pick").Contains("CI commit 03"), + Contains("<-- YOU ARE HERE --- commit 02"), + Contains("CI commit 01"), ). - NavigateToLine(Contains("commit 05")). + NavigateToLine(Contains("commit 06")). Press(keys.Universal.Remove) t.Common().ContinueRebase() @@ -50,11 +56,12 @@ var DropTodoCommitWithUpdateRef = NewIntegrationTest(NewIntegrationTestArgs{ t.Views().Commits(). IsFocused(). Lines( - Contains("commit 06"), - Contains("commit 04"), - Contains("commit 03"), - Contains("commit 02"), - Contains("commit 01"), + Contains("CI commit 07"), + Contains("CI commit 05"), + Contains("CI * commit 04"), + Contains("CI commit 03"), + Contains("CI commit 02"), + Contains("CI commit 01"), ) }, }) diff --git a/pkg/integration/tests/interactive_rebase/drop_todo_commit_with_update_ref_show_branch_heads.go b/pkg/integration/tests/interactive_rebase/drop_todo_commit_with_update_ref_show_branch_heads.go deleted file mode 100644 index b8cd4105514..00000000000 --- a/pkg/integration/tests/interactive_rebase/drop_todo_commit_with_update_ref_show_branch_heads.go +++ /dev/null @@ -1,62 +0,0 @@ -package interactive_rebase - -import ( - "github.com/jesseduffield/lazygit/pkg/config" - . "github.com/jesseduffield/lazygit/pkg/integration/components" -) - -var DropTodoCommitWithUpdateRefShowBranchHeads = NewIntegrationTest(NewIntegrationTestArgs{ - Description: "Drops a commit during interactive rebase when there is an update-ref in the git-rebase-todo file (with experimentalShowBranchHeads on)", - ExtraCmdArgs: []string{}, - Skip: false, - GitVersion: AtLeast("2.38.0"), - SetupConfig: func(config *config.AppConfig) { - config.UserConfig.Gui.ExperimentalShowBranchHeads = true - }, - SetupRepo: func(shell *Shell) { - shell. - CreateNCommits(3). - NewBranch("mybranch"). - CreateNCommitsStartingAt(3, 4) - - shell.SetConfig("rebase.updateRefs", "true") - }, - Run: func(t *TestDriver, keys config.KeybindingConfig) { - t.Views().Commits(). - Focus(). - Lines( - Contains("(*) commit 06").IsSelected(), - Contains("commit 05"), - Contains("commit 04"), - Contains("(*) commit 03"), - Contains("commit 02"), - Contains("commit 01"), - ). - NavigateToLine(Contains("commit 01")). - Press(keys.Universal.Edit). - Focus(). - Lines( - Contains("pick").Contains("(*) commit 06"), - Contains("pick").Contains("commit 05"), - Contains("pick").Contains("commit 04"), - Contains("update-ref").Contains("master"), - Contains("pick").Contains("(*) commit 03"), - Contains("pick").Contains("commit 02"), - Contains("<-- YOU ARE HERE --- commit 01"), - ). - NavigateToLine(Contains("commit 05")). - Press(keys.Universal.Remove) - - t.Common().ContinueRebase() - - t.Views().Commits(). - IsFocused(). - Lines( - Contains("(*) commit 06"), - Contains("commit 04"), - Contains("(*) commit 03"), - Contains("commit 02"), - Contains("commit 01"), - ) - }, -}) diff --git a/pkg/integration/tests/reflog/do_not_show_branch_markers_in_reflog_subcommits.go b/pkg/integration/tests/reflog/do_not_show_branch_markers_in_reflog_subcommits.go new file mode 100644 index 00000000000..8e888c95aa2 --- /dev/null +++ b/pkg/integration/tests/reflog/do_not_show_branch_markers_in_reflog_subcommits.go @@ -0,0 +1,71 @@ +package reflog + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var DoNotShowBranchMarkersInReflogSubcommits = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Verify that no branch heads are shown in the subcommits view of a reflog entry", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + shell.NewBranch("branch1") + shell.EmptyCommit("one") + shell.EmptyCommit("two") + shell.NewBranch("branch2") + shell.EmptyCommit("three") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + // Check that the local commits view does show a branch marker for branch1 + t.Views().Commits(). + Lines( + Contains("CI three"), + Contains("CI * two"), + Contains("CI one"), + ) + + t.Views().Branches(). + Focus(). + // Check out branch1 + NavigateToLine(Contains("branch1")). + PressPrimaryAction(). + // Look at the subcommits of branch2 + NavigateToLine(Contains("branch2")). + PressEnter(). + // Check that we see a marker for branch1 here (but not for + // branch2), even though branch1 is checked out + Tap(func() { + t.Views().SubCommits(). + IsFocused(). + Lines( + Contains("CI three"), + Contains("CI * two"), + Contains("CI one"), + ). + PressEscape() + }). + // Check out branch2 again + NavigateToLine(Contains("branch2")). + PressPrimaryAction() + + t.Views().ReflogCommits(). + Focus(). + TopLines( + Contains("checkout: moving from branch1 to branch2").IsSelected(), + ). + PressEnter(). + // Check that the subcommits view for a reflog entry doesn't show + // any branch markers + Tap(func() { + t.Views().SubCommits(). + IsFocused(). + Lines( + Contains("CI three"), + Contains("CI two"), + Contains("CI one"), + ) + }) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index bdd2a2bfd0f..9dcb57deeb4 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -117,7 +117,6 @@ var tests = []*components.IntegrationTest{ interactive_rebase.AmendMerge, interactive_rebase.AmendNonHeadCommitDuringRebase, interactive_rebase.DropTodoCommitWithUpdateRef, - interactive_rebase.DropTodoCommitWithUpdateRefShowBranchHeads, interactive_rebase.DropWithCustomCommentChar, interactive_rebase.EditFirstCommit, interactive_rebase.EditNonTodoCommitDuringRebase, @@ -164,6 +163,7 @@ var tests = []*components.IntegrationTest{ patch_building.StartNewPatch, reflog.Checkout, reflog.CherryPick, + reflog.DoNotShowBranchMarkersInReflogSubcommits, reflog.Patch, reflog.Reset, staging.DiffContextChange,