1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-06-04 23:37:41 +02:00

Visualize local branch heads in commits panel, 2nd approach (#2775)

This commit is contained in:
Stefan Haller 2023-07-31 08:40:09 +02:00 committed by GitHub
commit a6af31a4cb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 444 additions and 144 deletions

View File

@ -62,7 +62,6 @@ gui:
showListFooter: true # for seeing the '5 of 20' message in list panels showListFooter: true # for seeing the '5 of 20' message in list panels
showRandomTip: true showRandomTip: true
showBranchCommitHash: false # show commit hashes alongside branch names 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) showBottomLine: true # for hiding the bottom information line (unless it has important information to tell you)
showCommandLog: true showCommandLog: true
showIcons: false # deprecated: use nerdFontsVersion instead showIcons: false # deprecated: use nerdFontsVersion instead

View File

@ -6,4 +6,5 @@
* [Keybindings](./keybindings) * [Keybindings](./keybindings)
* [Undo/Redo](./Undoing.md) * [Undo/Redo](./Undoing.md)
* [Searching/Filtering](./Searching.md) * [Searching/Filtering](./Searching.md)
* [Stacked Branches](./Stacked_Branches.md)
* [Dev docs](./dev) * [Dev docs](./dev)

18
docs/Stacked_Branches.md Normal file
View File

@ -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)).

View File

@ -70,6 +70,21 @@ func (self *BranchCommands) CurrentBranchInfo() (BranchInfo, error) {
}, nil }, 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 // Delete delete branch
func (self *BranchCommands) Delete(branch string, force bool) error { func (self *BranchCommands) Delete(branch string, force bool) error {
cmdArgs := NewGitCmd("branch"). cmdArgs := NewGitCmd("branch").

View File

@ -171,7 +171,7 @@ var branchFields = []string{
"upstream:short", "upstream:short",
"upstream:track", "upstream:track",
"subject", "subject",
fmt.Sprintf("objectname:short=%d", utils.COMMIT_HASH_SHORT_SIZE), "objectname",
} }
// Obtain branch information from parsed line output of getRawBranches() // Obtain branch information from parsed line output of getRawBranches()

View File

@ -107,3 +107,7 @@ func (self *ConfigCommands) GetCoreCommentChar() byte {
return '#' return '#'
} }
func (self *ConfigCommands) GetRebaseUpdateRefs() bool {
return self.gitConfig.GetBool("rebase.updateRefs")
}

View File

@ -1,6 +1,7 @@
package git_commands package git_commands
import ( import (
"os"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
@ -71,3 +72,14 @@ func IsBareRepo(osCommand *oscommands.OSCommand) (bool, error) {
func (self *StatusCommands) IsInMergeState() (bool, error) { func (self *StatusCommands) IsInMergeState() (bool, error) {
return self.os.FileExists(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "MERGE_HEAD")) 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 ""
}

View File

@ -27,36 +27,35 @@ type RefresherConfig struct {
} }
type GuiConfig struct { type GuiConfig struct {
AuthorColors map[string]string `yaml:"authorColors"` AuthorColors map[string]string `yaml:"authorColors"`
BranchColors map[string]string `yaml:"branchColors"` BranchColors map[string]string `yaml:"branchColors"`
ScrollHeight int `yaml:"scrollHeight"` ScrollHeight int `yaml:"scrollHeight"`
ScrollPastBottom bool `yaml:"scrollPastBottom"` ScrollPastBottom bool `yaml:"scrollPastBottom"`
MouseEvents bool `yaml:"mouseEvents"` MouseEvents bool `yaml:"mouseEvents"`
SkipDiscardChangeWarning bool `yaml:"skipDiscardChangeWarning"` SkipDiscardChangeWarning bool `yaml:"skipDiscardChangeWarning"`
SkipStashWarning bool `yaml:"skipStashWarning"` SkipStashWarning bool `yaml:"skipStashWarning"`
SidePanelWidth float64 `yaml:"sidePanelWidth"` SidePanelWidth float64 `yaml:"sidePanelWidth"`
ExpandFocusedSidePanel bool `yaml:"expandFocusedSidePanel"` ExpandFocusedSidePanel bool `yaml:"expandFocusedSidePanel"`
MainPanelSplitMode string `yaml:"mainPanelSplitMode"` MainPanelSplitMode string `yaml:"mainPanelSplitMode"`
Language string `yaml:"language"` Language string `yaml:"language"`
TimeFormat string `yaml:"timeFormat"` TimeFormat string `yaml:"timeFormat"`
ShortTimeFormat string `yaml:"shortTimeFormat"` ShortTimeFormat string `yaml:"shortTimeFormat"`
Theme ThemeConfig `yaml:"theme"` Theme ThemeConfig `yaml:"theme"`
CommitLength CommitLengthConfig `yaml:"commitLength"` CommitLength CommitLengthConfig `yaml:"commitLength"`
SkipNoStagedFilesWarning bool `yaml:"skipNoStagedFilesWarning"` SkipNoStagedFilesWarning bool `yaml:"skipNoStagedFilesWarning"`
ShowListFooter bool `yaml:"showListFooter"` ShowListFooter bool `yaml:"showListFooter"`
ShowFileTree bool `yaml:"showFileTree"` ShowFileTree bool `yaml:"showFileTree"`
ShowRandomTip bool `yaml:"showRandomTip"` ShowRandomTip bool `yaml:"showRandomTip"`
ShowCommandLog bool `yaml:"showCommandLog"` ShowCommandLog bool `yaml:"showCommandLog"`
ShowBottomLine bool `yaml:"showBottomLine"` ShowBottomLine bool `yaml:"showBottomLine"`
ShowIcons bool `yaml:"showIcons"` ShowIcons bool `yaml:"showIcons"`
NerdFontsVersion string `yaml:"nerdFontsVersion"` NerdFontsVersion string `yaml:"nerdFontsVersion"`
ShowBranchCommitHash bool `yaml:"showBranchCommitHash"` ShowBranchCommitHash bool `yaml:"showBranchCommitHash"`
ExperimentalShowBranchHeads bool `yaml:"experimentalShowBranchHeads"` CommandLogSize int `yaml:"commandLogSize"`
CommandLogSize int `yaml:"commandLogSize"` SplitDiff string `yaml:"splitDiff"`
SplitDiff string `yaml:"splitDiff"` SkipRewordInEditorWarning bool `yaml:"skipRewordInEditorWarning"`
SkipRewordInEditorWarning bool `yaml:"skipRewordInEditorWarning"` WindowSize string `yaml:"windowSize"`
WindowSize string `yaml:"windowSize"` Border string `yaml:"border"`
Border string `yaml:"border"`
} }
type ThemeConfig struct { type ThemeConfig struct {
@ -436,21 +435,20 @@ func GetDefaultConfig() *UserConfig {
UnstagedChangesColor: []string{"red"}, UnstagedChangesColor: []string{"red"},
DefaultFgColor: []string{"default"}, DefaultFgColor: []string{"default"},
}, },
CommitLength: CommitLengthConfig{Show: true}, CommitLength: CommitLengthConfig{Show: true},
SkipNoStagedFilesWarning: false, SkipNoStagedFilesWarning: false,
ShowListFooter: true, ShowListFooter: true,
ShowCommandLog: true, ShowCommandLog: true,
ShowBottomLine: true, ShowBottomLine: true,
ShowFileTree: true, ShowFileTree: true,
ShowRandomTip: true, ShowRandomTip: true,
ShowIcons: false, ShowIcons: false,
NerdFontsVersion: "", NerdFontsVersion: "",
ExperimentalShowBranchHeads: false, ShowBranchCommitHash: false,
ShowBranchCommitHash: false, CommandLogSize: 8,
CommandLogSize: 8, SplitDiff: "auto",
SplitDiff: "auto", SkipRewordInEditorWarning: false,
SkipRewordInEditorWarning: false, Border: "single",
Border: "single",
}, },
Git: GitConfig{ Git: GitConfig{
Paging: PagingConfig{ Paging: PagingConfig{

View File

@ -83,3 +83,7 @@ func (self *BranchesContext) GetDiffTerminals() []string {
} }
return nil return nil
} }
func (self *BranchesContext) ShowBranchHeadsInSubCommits() bool {
return true
}

View File

@ -38,10 +38,14 @@ func NewLocalCommitsContext(c *ContextCommon) *LocalCommitsContext {
} }
showYouAreHereLabel := c.Model().WorkingTreeStateAtLastCommitRefresh == enums.REBASE_MODE_REBASING showYouAreHereLabel := c.Model().WorkingTreeStateAtLastCommitRefresh == enums.REBASE_MODE_REBASING
showBranchMarkerForHeadCommit := c.Git().Config.GetRebaseUpdateRefs()
return presentation.GetCommitListDisplayStrings( return presentation.GetCommitListDisplayStrings(
c.Common, c.Common,
c.Model().Commits, c.Model().Commits,
c.Model().Branches,
c.Model().CheckedOutBranch,
showBranchMarkerForHeadCommit,
c.State().GetRepoState().GetScreenMode() != types.SCREEN_NORMAL, c.State().GetRepoState().GetScreenMode() != types.SCREEN_NORMAL,
c.Modes().CherryPicking.SelectedShaSet(), c.Modes().CherryPicking.SelectedShaSet(),
c.Modes().Diffing.Ref, c.Modes().Diffing.Ref,

View File

@ -86,3 +86,7 @@ func (self *ReflogCommitsContext) GetDiffTerminals() []string {
return []string{itemId} return []string{itemId}
} }
func (self *ReflogCommitsContext) ShowBranchHeadsInSubCommits() bool {
return false
}

View File

@ -72,3 +72,7 @@ func (self *RemoteBranchesContext) GetDiffTerminals() []string {
return []string{itemId} return []string{itemId}
} }
func (self *RemoteBranchesContext) ShowBranchHeadsInSubCommits() bool {
return true
}

View File

@ -37,6 +37,13 @@ func NewSubCommitsContext(
} }
getDisplayStrings := func(startIdx int, length int) [][]string { 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 := "" selectedCommitSha := ""
if c.CurrentContext().GetKey() == SUB_COMMITS_CONTEXT_KEY { if c.CurrentContext().GetKey() == SUB_COMMITS_CONTEXT_KEY {
selectedCommit := viewModel.GetSelected() selectedCommit := viewModel.GetSelected()
@ -44,9 +51,17 @@ func NewSubCommitsContext(
selectedCommitSha = selectedCommit.Sha selectedCommitSha = selectedCommit.Sha
} }
} }
branches := []*models.Branch{}
if viewModel.GetShowBranchHeads() {
branches = c.Model().Branches
}
showBranchMarkerForHeadCommit := c.Git().Config.GetRebaseUpdateRefs()
return presentation.GetCommitListDisplayStrings( return presentation.GetCommitListDisplayStrings(
c.Common, c.Common,
c.Model().SubCommits, c.Model().SubCommits,
branches,
viewModel.GetRef().RefName(),
showBranchMarkerForHeadCommit,
c.State().GetRepoState().GetScreenMode() != types.SCREEN_NORMAL, c.State().GetRepoState().GetScreenMode() != types.SCREEN_NORMAL,
c.Modes().CherryPicking.SelectedShaSet(), c.Modes().CherryPicking.SelectedShaSet(),
c.Modes().Diffing.Ref, c.Modes().Diffing.Ref,
@ -97,7 +112,8 @@ type SubCommitsViewModel struct {
ref types.Ref ref types.Ref
*ListViewModel[*models.Commit] *ListViewModel[*models.Commit]
limitCommits bool limitCommits bool
showBranchHeads bool
} }
func (self *SubCommitsViewModel) SetRef(ref types.Ref) { func (self *SubCommitsViewModel) SetRef(ref types.Ref) {
@ -108,6 +124,14 @@ func (self *SubCommitsViewModel) GetRef() types.Ref {
return self.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 { func (self *SubCommitsContext) GetSelectedItemId() string {
item := self.GetSelected() item := self.GetSelected()
if item == nil { if item == nil {

View File

@ -69,3 +69,7 @@ func (self *TagsContext) GetDiffTerminals() []string {
return []string{itemId} return []string{itemId}
} }
func (self *TagsContext) ShowBranchHeadsInSubCommits() bool {
return true
}

View File

@ -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 { func (self *RefreshHelper) refreshCommitsWithLimit() error {
self.c.Mutexes().LocalCommitsMutex.Lock() self.c.Mutexes().LocalCommitsMutex.Lock()
defer self.c.Mutexes().LocalCommitsMutex.Unlock() defer self.c.Mutexes().LocalCommitsMutex.Unlock()
@ -291,6 +317,7 @@ func (self *RefreshHelper) refreshCommitsWithLimit() error {
self.c.Model().Commits = commits self.c.Model().Commits = commits
self.RefreshAuthors(commits) self.RefreshAuthors(commits)
self.c.Model().WorkingTreeStateAtLastCommitRefresh = self.c.Git().Status.WorkingTreeState() self.c.Model().WorkingTreeStateAtLastCommitRefresh = self.c.Git().Status.WorkingTreeState()
self.c.Model().CheckedOutBranch = self.determineCheckedOutBranchName()
return self.c.PostRefreshUpdate(self.c.Contexts().LocalCommits) return self.c.PostRefreshUpdate(self.c.Contexts().LocalCommits)
} }
@ -412,6 +439,12 @@ func (self *RefreshHelper) refreshBranches() {
self.c.Log.Error(err) 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() self.refreshStatus()
} }

View File

@ -11,6 +11,7 @@ var _ types.IController = &SwitchToSubCommitsController{}
type CanSwitchToSubCommits interface { type CanSwitchToSubCommits interface {
types.Context types.Context
GetSelectedRef() types.Ref GetSelectedRef() types.Ref
ShowBranchHeadsInSubCommits() bool
} }
type SwitchToSubCommitsController struct { type SwitchToSubCommitsController struct {
@ -79,6 +80,7 @@ func (self *SwitchToSubCommitsController) viewCommits() error {
subCommitsContext.SetTitleRef(ref.Description()) subCommitsContext.SetTitleRef(ref.Description())
subCommitsContext.SetRef(ref) subCommitsContext.SetRef(ref)
subCommitsContext.SetLimitCommits(true) subCommitsContext.SetLimitCommits(true)
subCommitsContext.SetShowBranchHeads(self.context.ShowBranchHeadsInSubCommits())
subCommitsContext.ClearSearchString() subCommitsContext.ClearSearchString()
subCommitsContext.GetView().ClearSearch() subCommitsContext.GetView().ClearSearch()

View File

@ -71,7 +71,7 @@ func getBranchDisplayStrings(
} }
if fullDescription || userConfig.Gui.ShowBranchCommitHash { if fullDescription || userConfig.Gui.ShowBranchCommitHash {
res = append(res, b.CommitHash) res = append(res, utils.ShortSha(b.CommitHash))
} }
res = append(res, coloredName) res = append(res, coloredName)

View File

@ -39,6 +39,9 @@ type bisectBounds struct {
func GetCommitListDisplayStrings( func GetCommitListDisplayStrings(
common *common.Common, common *common.Common,
commits []*models.Commit, commits []*models.Commit,
branches []*models.Branch,
currentBranchName string,
showBranchMarkerForHeadCommit bool,
fullDescription bool, fullDescription bool,
cherryPickedCommitShaSet *set.Set[string], cherryPickedCommitShaSet *set.Set[string],
diffName string, diffName string,
@ -99,6 +102,30 @@ func GetCommitListDisplayStrings(
getGraphLine = func(idx int) string { return "" } 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)) lines := make([][]string, 0, len(filteredCommits))
var bisectStatus BisectStatus var bisectStatus BisectStatus
for i, commit := range filteredCommits { for i, commit := range filteredCommits {
@ -112,6 +139,7 @@ func GetCommitListDisplayStrings(
lines = append(lines, displayCommit( lines = append(lines, displayCommit(
common, common,
commit, commit,
branchHeadsToVisualize,
cherryPickedCommitShaSet, cherryPickedCommitShaSet,
diffName, diffName,
timeFormat, timeFormat,
@ -260,6 +288,7 @@ func getBisectStatusText(bisectStatus BisectStatus, bisectInfo *git_commands.Bis
func displayCommit( func displayCommit(
common *common.Common, common *common.Common,
commit *models.Commit, commit *models.Commit,
branchHeadsToVisualize *set.Set[string],
cherryPickedCommitShaSet *set.Set[string], cherryPickedCommitShaSet *set.Set[string],
diffName string, diffName string,
timeFormat string, timeFormat string,
@ -289,8 +318,11 @@ func displayCommit(
} else { } else {
if len(commit.Tags) > 0 { if len(commit.Tags) > 0 {
tagString = theme.DiffTerminalColor.SetBold().Sprint(strings.Join(commit.Tags, " ")) + " " 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)
} }
} }

View File

@ -28,6 +28,9 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
scenarios := []struct { scenarios := []struct {
testName string testName string
commits []*models.Commit commits []*models.Commit
branches []*models.Branch
currentBranchName string
hasUpdateRefConfig bool
fullDescription bool fullDescription bool
cherryPickedCommitShaSet *set.Set[string] cherryPickedCommitShaSet *set.Set[string]
diffName string diffName string
@ -72,6 +75,120 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
sha2 commit2 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", testName: "showing graph",
commits: []*models.Commit{ commits: []*models.Commit{
@ -285,6 +402,9 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
result := GetCommitListDisplayStrings( result := GetCommitListDisplayStrings(
common, common,
s.commits, s.commits,
s.branches,
s.currentBranchName,
s.hasUpdateRefConfig,
s.fullDescription, s.fullDescription,
s.cherryPickedCommitShaSet, s.cherryPickedCommitShaSet,
s.diffName, s.diffName,

View File

@ -217,6 +217,10 @@ type Model struct {
RemoteBranches []*models.RemoteBranch RemoteBranches []*models.RemoteBranch
Tags []*models.Tag 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 // for displaying suggestions while typing in a file name
FilesTrie *patricia.Trie FilesTrie *patricia.Trie

View File

@ -11,6 +11,7 @@ var Basic = NewIntegrationTest(NewIntegrationTestArgs{
Skip: false, Skip: false,
SetupRepo: func(shell *Shell) { SetupRepo: func(shell *Shell) {
shell. shell.
NewBranch("mybranch").
CreateNCommits(10) CreateNCommits(10)
}, },
SetupConfig: func(cfg *config.AppConfig) {}, SetupConfig: func(cfg *config.AppConfig) {},
@ -31,20 +32,21 @@ var Basic = NewIntegrationTest(NewIntegrationTestArgs{
t.Views().Commits(). t.Views().Commits().
Focus(). Focus().
SelectedLine(Contains("commit 10")). SelectedLine(Contains("CI commit 10")).
NavigateToLine(Contains("commit 09")). NavigateToLine(Contains("CI commit 09")).
Tap(func() { Tap(func() {
markCommitAsBad() markCommitAsBad()
t.Views().Information().Content(Contains("Bisecting")) t.Views().Information().Content(Contains("Bisecting"))
}). }).
SelectedLine(Contains("<-- bad")). SelectedLine(Contains("<-- bad")).
NavigateToLine(Contains("commit 02")). NavigateToLine(Contains("CI commit 02")).
Tap(markCommitAsGood). Tap(markCommitAsGood).
TopLines(Contains("CI commit 10")).
// lazygit will land us in the commit between our good and bad commits. // 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). Tap(markCommitAsBad).
SelectedLine(Contains("commit 04").Contains("<-- current")). SelectedLine(Contains("CI commit 04").Contains("<-- current")).
Tap(func() { Tap(func() {
markCommitAsGood() 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() t.ExpectPopup().Alert().Title(Equals("Bisect complete")).Content(MatchesRegexp("(?s)commit 05.*Do you want to reset")).Confirm()
}). }).
IsFocused(). IsFocused().
Content(Contains("commit 04")) Content(Contains("CI commit 04"))
t.Views().Information().Content(DoesNotContain("Bisecting")) t.Views().Information().Content(DoesNotContain("Bisecting"))
}, },

View File

@ -10,12 +10,16 @@ var DropTodoCommitWithUpdateRef = NewIntegrationTest(NewIntegrationTestArgs{
ExtraCmdArgs: []string{}, ExtraCmdArgs: []string{},
Skip: false, Skip: false,
GitVersion: AtLeast("2.38.0"), 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) { SetupRepo: func(shell *Shell) {
shell. shell.
CreateNCommits(3). CreateNCommits(1).
NewBranch("mybranch"). NewBranch("branch1").
CreateNCommitsStartingAt(3, 4) CreateNCommitsStartingAt(3, 2).
NewBranch("branch2").
CreateNCommitsStartingAt(3, 5)
shell.SetConfig("rebase.updateRefs", "true") shell.SetConfig("rebase.updateRefs", "true")
}, },
@ -23,26 +27,28 @@ var DropTodoCommitWithUpdateRef = NewIntegrationTest(NewIntegrationTestArgs{
t.Views().Commits(). t.Views().Commits().
Focus(). Focus().
Lines( Lines(
Contains("commit 06").IsSelected(), Contains("CI commit 07").IsSelected(),
Contains("commit 05"), Contains("CI commit 06"),
Contains("commit 04"), Contains("CI commit 05"),
Contains("commit 03"), Contains("CI * commit 04"),
Contains("commit 02"), Contains("CI commit 03"),
Contains("commit 01"), Contains("CI commit 02"),
Contains("CI commit 01"),
). ).
NavigateToLine(Contains("commit 01")). NavigateToLine(Contains("commit 02")).
Press(keys.Universal.Edit). Press(keys.Universal.Edit).
Focus(). Focus().
Lines( Lines(
Contains("pick").Contains("commit 06"), Contains("pick").Contains("CI commit 07"),
Contains("pick").Contains("commit 05"), Contains("pick").Contains("CI commit 06"),
Contains("pick").Contains("commit 04"), Contains("pick").Contains("CI commit 05"),
Contains("update-ref").Contains("master"), Contains("update-ref").Contains("branch1").DoesNotContain("*"),
Contains("pick").Contains("commit 03"), Contains("pick").Contains("CI * commit 04"),
Contains("pick").Contains("commit 02"), Contains("pick").Contains("CI commit 03"),
Contains("<-- YOU ARE HERE --- commit 01"), Contains("<-- YOU ARE HERE --- commit 02"),
Contains("CI commit 01"),
). ).
NavigateToLine(Contains("commit 05")). NavigateToLine(Contains("commit 06")).
Press(keys.Universal.Remove) Press(keys.Universal.Remove)
t.Common().ContinueRebase() t.Common().ContinueRebase()
@ -50,11 +56,12 @@ var DropTodoCommitWithUpdateRef = NewIntegrationTest(NewIntegrationTestArgs{
t.Views().Commits(). t.Views().Commits().
IsFocused(). IsFocused().
Lines( Lines(
Contains("commit 06"), Contains("CI commit 07"),
Contains("commit 04"), Contains("CI commit 05"),
Contains("commit 03"), Contains("CI * commit 04"),
Contains("commit 02"), Contains("CI commit 03"),
Contains("commit 01"), Contains("CI commit 02"),
Contains("CI commit 01"),
) )
}, },
}) })

View File

@ -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"),
)
},
})

View File

@ -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"),
)
})
},
})

View File

@ -117,7 +117,6 @@ var tests = []*components.IntegrationTest{
interactive_rebase.AmendMerge, interactive_rebase.AmendMerge,
interactive_rebase.AmendNonHeadCommitDuringRebase, interactive_rebase.AmendNonHeadCommitDuringRebase,
interactive_rebase.DropTodoCommitWithUpdateRef, interactive_rebase.DropTodoCommitWithUpdateRef,
interactive_rebase.DropTodoCommitWithUpdateRefShowBranchHeads,
interactive_rebase.DropWithCustomCommentChar, interactive_rebase.DropWithCustomCommentChar,
interactive_rebase.EditFirstCommit, interactive_rebase.EditFirstCommit,
interactive_rebase.EditNonTodoCommitDuringRebase, interactive_rebase.EditNonTodoCommitDuringRebase,
@ -164,6 +163,7 @@ var tests = []*components.IntegrationTest{
patch_building.StartNewPatch, patch_building.StartNewPatch,
reflog.Checkout, reflog.Checkout,
reflog.CherryPick, reflog.CherryPick,
reflog.DoNotShowBranchMarkersInReflogSubcommits,
reflog.Patch, reflog.Patch,
reflog.Reset, reflog.Reset,
staging.DiffContextChange, staging.DiffContextChange,