Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 10 commits into from
Jul 31, 2023
1 change: 0 additions & 1 deletion docs/Config.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@
* [Keybindings](./keybindings)
* [Undo/Redo](./Undoing.md)
* [Searching/Filtering](./Searching.md)
* [Stacked Branches](./Stacked_Branches.md)
* [Dev docs](./dev)
18 changes: 18 additions & 0 deletions docs/Stacked_Branches.md
Original file line number Diff line number Diff line change
@@ -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)).
15 changes: 15 additions & 0 deletions pkg/commands/git_commands/branch.go
Original file line number Diff line number Diff line change
Expand Up @@ -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").
Expand Down
2 changes: 1 addition & 1 deletion pkg/commands/git_commands/branch_loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
4 changes: 4 additions & 0 deletions pkg/commands/git_commands/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,7 @@ func (self *ConfigCommands) GetCoreCommentChar() byte {

return '#'
}

func (self *ConfigCommands) GetRebaseUpdateRefs() bool {
return self.gitConfig.GetBool("rebase.updateRefs")
}
12 changes: 12 additions & 0 deletions pkg/commands/git_commands/status.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package git_commands

import (
"os"
"path/filepath"
"strconv"
"strings"
Expand Down Expand Up @@ -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 ""
}
88 changes: 43 additions & 45 deletions pkg/config/user_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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{
Expand Down
4 changes: 4 additions & 0 deletions pkg/gui/context/branches_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,7 @@ func (self *BranchesContext) GetDiffTerminals() []string {
}
return nil
}

func (self *BranchesContext) ShowBranchHeadsInSubCommits() bool {
return true
}
4 changes: 4 additions & 0 deletions pkg/gui/context/local_commits_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions pkg/gui/context/reflog_commits_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,7 @@ func (self *ReflogCommitsContext) GetDiffTerminals() []string {

return []string{itemId}
}

func (self *ReflogCommitsContext) ShowBranchHeadsInSubCommits() bool {
return false
}
4 changes: 4 additions & 0 deletions pkg/gui/context/remote_branches_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,7 @@ func (self *RemoteBranchesContext) GetDiffTerminals() []string {

return []string{itemId}
}

func (self *RemoteBranchesContext) ShowBranchHeadsInSubCommits() bool {
return true
}
26 changes: 25 additions & 1 deletion pkg/gui/context/sub_commits_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,31 @@ 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()
if selectedCommit != nil {
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,
Expand Down Expand Up @@ -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) {
Expand All @@ -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 {
Expand Down
4 changes: 4 additions & 0 deletions pkg/gui/context/tags_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,7 @@ func (self *TagsContext) GetDiffTerminals() []string {

return []string{itemId}
}

func (self *TagsContext) ShowBranchHeadsInSubCommits() bool {
return true
}
33 changes: 33 additions & 0 deletions pkg/gui/controllers/helpers/refresh_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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)
}
Expand Down Expand Up @@ -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()
}

Expand Down
2 changes: 2 additions & 0 deletions pkg/gui/controllers/switch_to_sub_commits_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ var _ types.IController = &SwitchToSubCommitsController{}
type CanSwitchToSubCommits interface {
types.Context
GetSelectedRef() types.Ref
ShowBranchHeadsInSubCommits() bool
}

type SwitchToSubCommitsController struct {
Expand Down Expand Up @@ -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()

Expand Down
2 changes: 1 addition & 1 deletion pkg/gui/presentation/branches.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading