diff --git a/pkg/commands/git_commands/commit_loader.go b/pkg/commands/git_commands/commit_loader.go index 372bc7a4d..4b0a23692 100644 --- a/pkg/commands/git_commands/commit_loader.go +++ b/pkg/commands/git_commands/commit_loader.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" "regexp" + "sort" "strconv" "strings" "sync" @@ -68,6 +69,8 @@ type GetCommitsOptions struct { RefForPushedStatus string // the ref to use for determining pushed/unpushed status // determines if we show the whole git graph i.e. pass the '--all' flag All bool + // If non-empty, show divergence from this ref (left-right log) + RefToShowDivergenceFrom string } // GetCommits obtains the commits of the current branch @@ -93,17 +96,21 @@ func (self *CommitLoader) GetCommits(opts GetCommitsOptions) ([]*models.Commit, defer wg.Done() logErr = self.getLogCmd(opts).RunAndProcessLines(func(line string) (bool, error) { - commit := self.extractCommitFromLine(line) + commit := self.extractCommitFromLine(line, opts.RefToShowDivergenceFrom != "") commits = append(commits, commit) return false, nil }) }) var ancestor string + var remoteAncestor string go utils.Safe(func() { defer wg.Done() ancestor = self.getMergeBase(opts.RefName) + if opts.RefToShowDivergenceFrom != "" { + remoteAncestor = self.getMergeBase(opts.RefToShowDivergenceFrom) + } }) passedFirstPushedCommit := false @@ -137,8 +144,23 @@ func (self *CommitLoader) GetCommits(opts GetCommitsOptions) ([]*models.Commit, return commits, nil } - if ancestor != "" { - commits = setCommitMergedStatuses(ancestor, commits) + if opts.RefToShowDivergenceFrom != "" { + sort.SliceStable(commits, func(i, j int) bool { + // In the divergence view we want incoming commits to come first + return commits[i].Divergence > commits[j].Divergence + }) + + _, localSectionStart, found := lo.FindIndexOf(commits, func(commit *models.Commit) bool { + return commit.Divergence == models.DivergenceLeft + }) + if !found { + localSectionStart = len(commits) + } + + setCommitMergedStatuses(remoteAncestor, commits[:localSectionStart]) + setCommitMergedStatuses(ancestor, commits[localSectionStart:]) + } else { + setCommitMergedStatuses(ancestor, commits) } return commits, nil @@ -179,8 +201,8 @@ func (self *CommitLoader) MergeRebasingCommits(commits []*models.Commit) ([]*mod // then puts them into a commit object // example input: // 8ad01fe32fcc20f07bc6693f87aa4977c327f1e1|10 hours ago|Jesse Duffield| (HEAD -> master, tag: v0.15.2)|refresh commits when adding a tag -func (self *CommitLoader) extractCommitFromLine(line string) *models.Commit { - split := strings.SplitN(line, "\x00", 7) +func (self *CommitLoader) extractCommitFromLine(line string, showDivergence bool) *models.Commit { + split := strings.SplitN(line, "\x00", 8) sha := split[0] unixTimestamp := split[1] @@ -189,6 +211,10 @@ func (self *CommitLoader) extractCommitFromLine(line string) *models.Commit { extraInfo := strings.TrimSpace(split[4]) parentHashes := split[5] message := split[6] + divergence := models.DivergenceNone + if showDivergence { + divergence = lo.Ternary(split[7] == "<", models.DivergenceLeft, models.DivergenceRight) + } tags := []string{} @@ -222,6 +248,7 @@ func (self *CommitLoader) extractCommitFromLine(line string) *models.Commit { AuthorName: authorName, AuthorEmail: authorEmail, Parents: parents, + Divergence: divergence, } } @@ -251,7 +278,7 @@ func (self *CommitLoader) getHydratedRebasingCommits(rebaseMode enums.RebaseMode fullCommits := map[string]*models.Commit{} err = cmdObj.RunAndProcessLines(func(line string) (bool, error) { - commit := self.extractCommitFromLine(line) + commit := self.extractCommitFromLine(line, false) fullCommits[commit.Sha] = commit return false, nil }) @@ -495,7 +522,11 @@ func (self *CommitLoader) commitFromPatch(content string) *models.Commit { } } -func setCommitMergedStatuses(ancestor string, commits []*models.Commit) []*models.Commit { +func setCommitMergedStatuses(ancestor string, commits []*models.Commit) { + if ancestor == "" { + return + } + passedAncestor := false for i, commit := range commits { // some commits aren't really commits and don't have sha's, such as the update-ref todo @@ -509,7 +540,6 @@ func setCommitMergedStatuses(ancestor string, commits []*models.Commit) []*model commits[i].Status = models.StatusMerged } } - return commits } func (self *CommitLoader) getMergeBase(refName string) string { @@ -622,8 +652,13 @@ func (self *CommitLoader) getFirstPushedCommit(refName string) (string, error) { func (self *CommitLoader) getLogCmd(opts GetCommitsOptions) oscommands.ICmdObj { config := self.UserConfig.Git.Log + refSpec := opts.RefName + if opts.RefToShowDivergenceFrom != "" { + refSpec += "..." + opts.RefToShowDivergenceFrom + } + cmdArgs := NewGitCmd("log"). - Arg(opts.RefName). + Arg(refSpec). ArgIf(config.Order != "default", "--"+config.Order). ArgIf(opts.All, "--all"). Arg("--oneline"). @@ -632,6 +667,7 @@ func (self *CommitLoader) getLogCmd(opts GetCommitsOptions) oscommands.ICmdObj { ArgIf(opts.Limit, "-300"). ArgIf(opts.FilterPath != "", "--follow"). Arg("--no-show-signature"). + ArgIf(opts.RefToShowDivergenceFrom != "", "--left-right"). Arg("--"). ArgIf(opts.FilterPath != "", opts.FilterPath). ToArgv() @@ -639,4 +675,4 @@ func (self *CommitLoader) getLogCmd(opts GetCommitsOptions) oscommands.ICmdObj { return self.cmd.New(cmdArgs).DontLog() } -const prettyFormat = `--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%s` +const prettyFormat = `--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%s%x00%m` diff --git a/pkg/commands/git_commands/commit_loader_test.go b/pkg/commands/git_commands/commit_loader_test.go index 8cf9b95a2..3f8fbd58e 100644 --- a/pkg/commands/git_commands/commit_loader_test.go +++ b/pkg/commands/git_commands/commit_loader_test.go @@ -45,7 +45,7 @@ func TestGetCommits(t *testing.T) { opts: GetCommitsOptions{RefName: "HEAD", RefForPushedStatus: "mybranch", IncludeRebaseCommits: false}, runner: oscommands.NewFakeRunner(t). ExpectGitArgs([]string{"merge-base", "mybranch", "mybranch@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil). - ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%s", "--abbrev=40", "--no-show-signature", "--"}, "", nil), + ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%s%x00%m", "--abbrev=40", "--no-show-signature", "--"}, "", nil), expectedCommits: []*models.Commit{}, expectedError: nil, @@ -57,7 +57,7 @@ func TestGetCommits(t *testing.T) { opts: GetCommitsOptions{RefName: "refs/heads/mybranch", RefForPushedStatus: "refs/heads/mybranch", IncludeRebaseCommits: false}, runner: oscommands.NewFakeRunner(t). ExpectGitArgs([]string{"merge-base", "refs/heads/mybranch", "mybranch@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil). - ExpectGitArgs([]string{"log", "refs/heads/mybranch", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%s", "--abbrev=40", "--no-show-signature", "--"}, "", nil), + ExpectGitArgs([]string{"log", "refs/heads/mybranch", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%s%x00%m", "--abbrev=40", "--no-show-signature", "--"}, "", nil), expectedCommits: []*models.Commit{}, expectedError: nil, @@ -72,7 +72,7 @@ func TestGetCommits(t *testing.T) { // here it's seeing which commits are yet to be pushed ExpectGitArgs([]string{"merge-base", "mybranch", "mybranch@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil). // here it's actually getting all the commits in a formatted form, one per line - ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%s", "--abbrev=40", "--no-show-signature", "--"}, commitsOutput, nil). + ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%s%x00%m", "--abbrev=40", "--no-show-signature", "--"}, commitsOutput, nil). // here it's testing which of the configured main branches have an upstream ExpectGitArgs([]string{"rev-parse", "--symbolic-full-name", "master@{u}"}, "refs/remotes/origin/master", nil). // this one does ExpectGitArgs([]string{"rev-parse", "--symbolic-full-name", "main@{u}"}, "", errors.New("error")). // this one doesn't, so it checks origin instead @@ -209,7 +209,7 @@ func TestGetCommits(t *testing.T) { // here it's seeing which commits are yet to be pushed ExpectGitArgs([]string{"merge-base", "mybranch", "mybranch@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil). // here it's actually getting all the commits in a formatted form, one per line - ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%s", "--abbrev=40", "--no-show-signature", "--"}, singleCommitOutput, nil). + ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%s%x00%m", "--abbrev=40", "--no-show-signature", "--"}, singleCommitOutput, nil). // here it's testing which of the configured main branches exist; neither does ExpectGitArgs([]string{"rev-parse", "--symbolic-full-name", "master@{u}"}, "", errors.New("error")). ExpectGitArgs([]string{"rev-parse", "--verify", "--quiet", "refs/remotes/origin/master"}, "", errors.New("error")). @@ -246,7 +246,7 @@ func TestGetCommits(t *testing.T) { // here it's seeing which commits are yet to be pushed ExpectGitArgs([]string{"merge-base", "mybranch", "mybranch@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil). // here it's actually getting all the commits in a formatted form, one per line - ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%s", "--abbrev=40", "--no-show-signature", "--"}, singleCommitOutput, nil). + ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%s%x00%m", "--abbrev=40", "--no-show-signature", "--"}, singleCommitOutput, nil). // here it's testing which of the configured main branches exist ExpectGitArgs([]string{"rev-parse", "--symbolic-full-name", "master@{u}"}, "refs/remotes/origin/master", nil). ExpectGitArgs([]string{"rev-parse", "--symbolic-full-name", "main@{u}"}, "", errors.New("error")). @@ -282,7 +282,7 @@ func TestGetCommits(t *testing.T) { opts: GetCommitsOptions{RefName: "HEAD", RefForPushedStatus: "mybranch", IncludeRebaseCommits: false}, runner: oscommands.NewFakeRunner(t). ExpectGitArgs([]string{"merge-base", "mybranch", "mybranch@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil). - ExpectGitArgs([]string{"log", "HEAD", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%s", "--abbrev=40", "--no-show-signature", "--"}, "", nil), + ExpectGitArgs([]string{"log", "HEAD", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%s%x00%m", "--abbrev=40", "--no-show-signature", "--"}, "", nil), expectedCommits: []*models.Commit{}, expectedError: nil, @@ -294,7 +294,7 @@ func TestGetCommits(t *testing.T) { opts: GetCommitsOptions{RefName: "HEAD", RefForPushedStatus: "mybranch", FilterPath: "src"}, runner: oscommands.NewFakeRunner(t). ExpectGitArgs([]string{"merge-base", "mybranch", "mybranch@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil). - ExpectGitArgs([]string{"log", "HEAD", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%s", "--abbrev=40", "--follow", "--no-show-signature", "--", "src"}, "", nil), + ExpectGitArgs([]string{"log", "HEAD", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%s%x00%m", "--abbrev=40", "--follow", "--no-show-signature", "--", "src"}, "", nil), expectedCommits: []*models.Commit{}, expectedError: nil, @@ -548,7 +548,8 @@ func TestCommitLoader_setCommitMergedStatuses(t *testing.T) { for _, scenario := range scenarios { t.Run(scenario.testName, func(t *testing.T) { - expectedCommits := setCommitMergedStatuses(scenario.ancestor, scenario.commits) + expectedCommits := scenario.commits + setCommitMergedStatuses(scenario.ancestor, expectedCommits) assert.Equal(t, scenario.expectedCommits, expectedCommits) }) } diff --git a/pkg/commands/models/branch.go b/pkg/commands/models/branch.go index b4dcc0a79..5fc887f87 100644 --- a/pkg/commands/models/branch.go +++ b/pkg/commands/models/branch.go @@ -1,5 +1,7 @@ package models +import "fmt" + // Branch : A git branch // duplicating this for now type Branch struct { @@ -43,6 +45,22 @@ func (b *Branch) ParentRefName() string { return b.RefName() + "^" } +func (b *Branch) FullUpstreamRefName() string { + if b.UpstreamRemote == "" || b.UpstreamBranch == "" { + return "" + } + + return fmt.Sprintf("refs/remotes/%s/%s", b.UpstreamRemote, b.UpstreamBranch) +} + +func (b *Branch) ShortUpstreamRefName() string { + if b.UpstreamRemote == "" || b.UpstreamBranch == "" { + return "" + } + + return fmt.Sprintf("%s/%s", b.UpstreamRemote, b.UpstreamBranch) +} + func (b *Branch) ID() string { return b.RefName() } diff --git a/pkg/commands/models/commit.go b/pkg/commands/models/commit.go index 73ed523d2..c2cdc4815 100644 --- a/pkg/commands/models/commit.go +++ b/pkg/commands/models/commit.go @@ -30,6 +30,17 @@ const ( ActionConflict = todo.Comment + 1 ) +type Divergence int + +// For a divergence log (left/right comparison of two refs) this is set to +// either DivergenceLeft or DivergenceRight for each commit; for normal +// commit views it is always DivergenceNone. +const ( + DivergenceNone Divergence = iota + DivergenceLeft + DivergenceRight +) + // Commit : A git commit type Commit struct { Sha string @@ -41,6 +52,7 @@ type Commit struct { AuthorName string // something like 'Jesse Duffield' AuthorEmail string // something like 'jessedduffield@gmail.com' UnixTimestamp int64 + Divergence Divergence // set to DivergenceNone unless we are showing the divergence view // SHAs of parent commits (will be multiple if it's a merge commit) Parents []string diff --git a/pkg/gui/context/sub_commits_context.go b/pkg/gui/context/sub_commits_context.go index 43cc513c0..7bf34d190 100644 --- a/pkg/gui/context/sub_commits_context.go +++ b/pkg/gui/context/sub_commits_context.go @@ -8,7 +8,7 @@ import ( "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/gui/presentation" "github.com/jesseduffield/lazygit/pkg/gui/types" - "github.com/jesseduffield/lazygit/pkg/utils" + "github.com/samber/lo" ) type SubCommitsContext struct { @@ -73,12 +73,41 @@ func NewSubCommitsContext( selectedCommitSha, startIdx, endIdx, - shouldShowGraph(c), + // Don't show the graph in the left/right view; we'd like to, but + // it's too complicated: + shouldShowGraph(c) && viewModel.GetRefToShowDivergenceFrom() == "", git_commands.NewNullBisectInfo(), false, ) } + getNonModelItems := func() []*NonModelItem { + result := []*NonModelItem{} + if viewModel.GetRefToShowDivergenceFrom() != "" { + _, upstreamIdx, found := lo.FindIndexOf( + c.Model().SubCommits, func(c *models.Commit) bool { return c.Divergence == models.DivergenceRight }) + if !found { + upstreamIdx = 0 + } + result = append(result, &NonModelItem{ + Index: upstreamIdx, + Content: fmt.Sprintf("--- %s ---", c.Tr.DivergenceSectionHeaderRemote), + }) + + _, localIdx, found := lo.FindIndexOf( + c.Model().SubCommits, func(c *models.Commit) bool { return c.Divergence == models.DivergenceLeft }) + if !found { + localIdx = len(c.Model().SubCommits) + } + result = append(result, &NonModelItem{ + Index: localIdx, + Content: fmt.Sprintf("--- %s ---", c.Tr.DivergenceSectionHeaderLocal), + }) + } + + return result + } + ctx := &SubCommitsContext{ c: c, SubCommitsViewModel: viewModel, @@ -96,6 +125,7 @@ func NewSubCommitsContext( ListRenderer: ListRenderer{ list: viewModel, getDisplayStrings: getDisplayStrings, + getNonModelItems: getNonModelItems, }, c: c, refreshViewportOnChange: true, @@ -112,7 +142,8 @@ func NewSubCommitsContext( type SubCommitsViewModel struct { // name of the ref that the sub-commits are shown for - ref types.Ref + ref types.Ref + refToShowDivergenceFrom string *ListViewModel[*models.Commit] limitCommits bool @@ -127,6 +158,14 @@ func (self *SubCommitsViewModel) GetRef() types.Ref { return self.ref } +func (self *SubCommitsViewModel) SetRefToShowDivergenceFrom(ref string) { + self.refToShowDivergenceFrom = ref +} + +func (self *SubCommitsViewModel) GetRefToShowDivergenceFrom() string { + return self.refToShowDivergenceFrom +} + func (self *SubCommitsViewModel) SetShowBranchHeads(value bool) { self.showBranchHeads = value } @@ -160,10 +199,6 @@ func (self *SubCommitsContext) GetCommits() []*models.Commit { return self.getModel() } -func (self *SubCommitsContext) Title() string { - return fmt.Sprintf(self.c.Tr.SubCommitsDynamicTitle, utils.TruncateWithEllipsis(self.ref.RefName(), 50)) -} - func (self *SubCommitsContext) SetLimitCommits(value bool) { self.limitCommits = value } diff --git a/pkg/gui/controllers.go b/pkg/gui/controllers.go index 1430ad239..40a482f30 100644 --- a/pkg/gui/controllers.go +++ b/pkg/gui/controllers.go @@ -76,6 +76,13 @@ func (gui *Gui) resetHelpersAndControllers() { helperCommon, func() *status.StatusManager { return gui.statusManager }, ) + + setSubCommits := func(commits []*models.Commit) { + gui.Mutexes.SubCommitsMutex.Lock() + defer gui.Mutexes.SubCommitsMutex.Unlock() + + gui.State.Model.SubCommits = commits + } gui.helpers = &helpers.Helpers{ Refs: refsHelper, Host: helpers.NewHostHelper(helperCommon), @@ -111,8 +118,9 @@ func (gui *Gui) resetHelpersAndControllers() { modeHelper, appStatusHelper, ), - Search: helpers.NewSearchHelper(helperCommon), - Worktree: worktreeHelper, + Search: helpers.NewSearchHelper(helperCommon), + Worktree: worktreeHelper, + SubCommits: helpers.NewSubCommitsHelper(helperCommon, refreshHelper, setSubCommits), } gui.CustomCommandsClient = custom_commands.NewClient( @@ -206,13 +214,6 @@ func (gui *Gui) resetHelpersAndControllers() { controllers.AttachControllers(context, sideWindowControllerFactory.Create(context)) } - setSubCommits := func(commits []*models.Commit) { - gui.Mutexes.SubCommitsMutex.Lock() - defer gui.Mutexes.SubCommitsMutex.Unlock() - - gui.State.Model.SubCommits = commits - } - for _, context := range []controllers.CanSwitchToSubCommits{ gui.State.Contexts.Branches, gui.State.Contexts.RemoteBranches, @@ -220,7 +221,7 @@ func (gui *Gui) resetHelpersAndControllers() { gui.State.Contexts.ReflogCommits, } { controllers.AttachControllers(context, controllers.NewSwitchToSubCommitsController( - common, setSubCommits, context, + common, context, )) } diff --git a/pkg/gui/controllers/branches_controller.go b/pkg/gui/controllers/branches_controller.go index 84d9e838e..67174439c 100644 --- a/pkg/gui/controllers/branches_controller.go +++ b/pkg/gui/controllers/branches_controller.go @@ -9,6 +9,7 @@ import ( "github.com/jesseduffield/lazygit/pkg/commands/git_commands" "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/gui/context" + "github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/utils" ) @@ -141,6 +142,27 @@ func (self *BranchesController) setUpstream(selectedBranch *models.Branch) error return self.c.Menu(types.CreateMenuOptions{ Title: self.c.Tr.Actions.SetUnsetUpstream, Items: []*types.MenuItem{ + { + LabelColumns: []string{self.c.Tr.ViewDivergenceFromUpstream}, + OnPress: func() error { + branch := self.context().GetSelected() + if branch == nil { + return nil + } + + if !branch.RemoteBranchStoredLocally() { + return self.c.ErrorMsg(self.c.Tr.DivergenceNoUpstream) + } + return self.c.Helpers().SubCommits.ViewSubCommits(helpers.ViewSubCommitsOpts{ + Ref: branch, + TitleRef: fmt.Sprintf("%s <-> %s", branch.RefName(), branch.ShortUpstreamRefName()), + RefToShowDivergenceFrom: branch.FullUpstreamRefName(), + Context: self.context(), + ShowBranchHeads: false, + }) + }, + Key: 'v', + }, { LabelColumns: []string{self.c.Tr.UnsetUpstream}, OnPress: func() error { diff --git a/pkg/gui/controllers/helpers/helpers.go b/pkg/gui/controllers/helpers/helpers.go index 22a7ea91b..06c5cc973 100644 --- a/pkg/gui/controllers/helpers/helpers.go +++ b/pkg/gui/controllers/helpers/helpers.go @@ -49,6 +49,7 @@ type Helpers struct { WindowArrangement *WindowArrangementHelper Search *SearchHelper Worktree *WorktreeHelper + SubCommits *SubCommitsHelper } func NewStubHelpers() *Helpers { @@ -83,5 +84,6 @@ func NewStubHelpers() *Helpers { WindowArrangement: &WindowArrangementHelper{}, Search: &SearchHelper{}, Worktree: &WorktreeHelper{}, + SubCommits: &SubCommitsHelper{}, } } diff --git a/pkg/gui/controllers/helpers/refresh_helper.go b/pkg/gui/controllers/helpers/refresh_helper.go index 0fe8884c1..578089af1 100644 --- a/pkg/gui/controllers/helpers/refresh_helper.go +++ b/pkg/gui/controllers/helpers/refresh_helper.go @@ -334,11 +334,12 @@ func (self *RefreshHelper) refreshSubCommitsWithLimit() error { commits, err := self.c.Git().Loaders.CommitLoader.GetCommits( git_commands.GetCommitsOptions{ - Limit: self.c.Contexts().SubCommits.GetLimitCommits(), - FilterPath: self.c.Modes().Filtering.GetPath(), - IncludeRebaseCommits: false, - RefName: self.c.Contexts().SubCommits.GetRef().FullRefName(), - RefForPushedStatus: self.c.Contexts().SubCommits.GetRef().FullRefName(), + Limit: self.c.Contexts().SubCommits.GetLimitCommits(), + FilterPath: self.c.Modes().Filtering.GetPath(), + IncludeRebaseCommits: false, + RefName: self.c.Contexts().SubCommits.GetRef().FullRefName(), + RefToShowDivergenceFrom: self.c.Contexts().SubCommits.GetRefToShowDivergenceFrom(), + RefForPushedStatus: self.c.Contexts().SubCommits.GetRef().FullRefName(), }, ) if err != nil { diff --git a/pkg/gui/controllers/helpers/sub_commits_helper.go b/pkg/gui/controllers/helpers/sub_commits_helper.go new file mode 100644 index 000000000..74c50fd14 --- /dev/null +++ b/pkg/gui/controllers/helpers/sub_commits_helper.go @@ -0,0 +1,73 @@ +package helpers + +import ( + "github.com/jesseduffield/lazygit/pkg/commands/git_commands" + "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/jesseduffield/lazygit/pkg/utils" +) + +type SubCommitsHelper struct { + c *HelperCommon + + refreshHelper *RefreshHelper + setSubCommits func([]*models.Commit) +} + +func NewSubCommitsHelper( + c *HelperCommon, + refreshHelper *RefreshHelper, + setSubCommits func([]*models.Commit), +) *SubCommitsHelper { + return &SubCommitsHelper{ + c: c, + refreshHelper: refreshHelper, + setSubCommits: setSubCommits, + } +} + +type ViewSubCommitsOpts struct { + Ref types.Ref + RefToShowDivergenceFrom string + TitleRef string + Context types.Context + ShowBranchHeads bool +} + +func (self *SubCommitsHelper) ViewSubCommits(opts ViewSubCommitsOpts) error { + commits, err := self.c.Git().Loaders.CommitLoader.GetCommits( + git_commands.GetCommitsOptions{ + Limit: true, + FilterPath: self.c.Modes().Filtering.GetPath(), + IncludeRebaseCommits: false, + RefName: opts.Ref.FullRefName(), + RefForPushedStatus: opts.Ref.FullRefName(), + RefToShowDivergenceFrom: opts.RefToShowDivergenceFrom, + }, + ) + if err != nil { + return err + } + + self.setSubCommits(commits) + self.refreshHelper.RefreshAuthors(commits) + + subCommitsContext := self.c.Contexts().SubCommits + subCommitsContext.SetSelectedLineIdx(0) + subCommitsContext.SetParentContext(opts.Context) + subCommitsContext.SetWindowName(opts.Context.GetWindowName()) + subCommitsContext.SetTitleRef(utils.TruncateWithEllipsis(opts.TitleRef, 50)) + subCommitsContext.SetRef(opts.Ref) + subCommitsContext.SetRefToShowDivergenceFrom(opts.RefToShowDivergenceFrom) + subCommitsContext.SetLimitCommits(true) + subCommitsContext.SetShowBranchHeads(opts.ShowBranchHeads) + subCommitsContext.ClearSearchString() + subCommitsContext.GetView().ClearSearch() + + err = self.c.PostRefreshUpdate(self.c.Contexts().SubCommits) + if err != nil { + return err + } + + return self.c.PushContext(self.c.Contexts().SubCommits) +} diff --git a/pkg/gui/controllers/switch_to_sub_commits_controller.go b/pkg/gui/controllers/switch_to_sub_commits_controller.go index 5b86854ca..3109f559e 100644 --- a/pkg/gui/controllers/switch_to_sub_commits_controller.go +++ b/pkg/gui/controllers/switch_to_sub_commits_controller.go @@ -1,8 +1,7 @@ package controllers import ( - "github.com/jesseduffield/lazygit/pkg/commands/git_commands" - "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers" "github.com/jesseduffield/lazygit/pkg/gui/types" ) @@ -18,20 +17,16 @@ type SwitchToSubCommitsController struct { baseController c *ControllerCommon context CanSwitchToSubCommits - - setSubCommits func([]*models.Commit) } func NewSwitchToSubCommitsController( controllerCommon *ControllerCommon, - setSubCommits func([]*models.Commit), context CanSwitchToSubCommits, ) *SwitchToSubCommitsController { return &SwitchToSubCommitsController{ baseController: baseController{}, c: controllerCommon, context: context, - setSubCommits: setSubCommits, } } @@ -57,40 +52,12 @@ func (self *SwitchToSubCommitsController) viewCommits() error { return nil } - // need to populate my sub commits - commits, err := self.c.Git().Loaders.CommitLoader.GetCommits( - git_commands.GetCommitsOptions{ - Limit: true, - FilterPath: self.c.Modes().Filtering.GetPath(), - IncludeRebaseCommits: false, - RefName: ref.FullRefName(), - RefForPushedStatus: ref.FullRefName(), - }, - ) - if err != nil { - return err - } - - self.setSubCommits(commits) - self.c.Helpers().Refresh.RefreshAuthors(commits) - - subCommitsContext := self.c.Contexts().SubCommits - subCommitsContext.SetSelectedLineIdx(0) - subCommitsContext.SetParentContext(self.context) - subCommitsContext.SetWindowName(self.context.GetWindowName()) - subCommitsContext.SetTitleRef(ref.Description()) - subCommitsContext.SetRef(ref) - subCommitsContext.SetLimitCommits(true) - subCommitsContext.SetShowBranchHeads(self.context.ShowBranchHeadsInSubCommits()) - subCommitsContext.ClearSearchString() - subCommitsContext.GetView().ClearSearch() - - err = self.c.PostRefreshUpdate(self.c.Contexts().SubCommits) - if err != nil { - return err - } - - return self.c.PushContext(self.c.Contexts().SubCommits) + return self.c.Helpers().SubCommits.ViewSubCommits(helpers.ViewSubCommitsOpts{ + Ref: ref, + TitleRef: ref.RefName(), + Context: self.context, + ShowBranchHeads: self.context.ShowBranchHeadsInSubCommits(), + }) } func (self *SwitchToSubCommitsController) Context() types.Context { diff --git a/pkg/gui/presentation/commits.go b/pkg/gui/presentation/commits.go index 6ae04f95a..b4297f6ed 100644 --- a/pkg/gui/presentation/commits.go +++ b/pkg/gui/presentation/commits.go @@ -359,7 +359,9 @@ func displayCommit( } cols := make([]string, 0, 7) - if icons.IsIconEnabled() { + if commit.Divergence != models.DivergenceNone { + cols = append(cols, shaColor.Sprint(lo.Ternary(commit.Divergence == models.DivergenceLeft, "↑", "↓"))) + } else if icons.IsIconEnabled() { cols = append(cols, shaColor.Sprint(icons.IconForCommit(commit))) } cols = append(cols, shaColor.Sprint(commit.ShortSha())) @@ -430,6 +432,8 @@ func getShaColor( shaColor = theme.DiffTerminalColor } else if cherryPickedCommitShaSet.Includes(commit.Sha) { shaColor = theme.CherryPickedCommitTextStyle + } else if commit.Divergence == models.DivergenceRight && commit.Status != models.StatusMerged { + shaColor = style.FgBlue } return shaColor diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 93909cbdc..3dbe405ff 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -348,6 +348,10 @@ type TranslationSet struct { SetAsUpstream string SetUpstream string UnsetUpstream string + ViewDivergenceFromUpstream string + DivergenceNoUpstream string + DivergenceSectionHeaderLocal string + DivergenceSectionHeaderRemote string SetUpstreamTitle string SetUpstreamMessage string EditRemote string @@ -1128,6 +1132,10 @@ func EnglishTranslationSet() TranslationSet { SetAsUpstream: "Set as upstream of checked-out branch", SetUpstream: "Set upstream of selected branch", UnsetUpstream: "Unset upstream of selected branch", + ViewDivergenceFromUpstream: "View divergence from upstream", + DivergenceNoUpstream: "Cannot show divergence of a branch that has no (locally tracked) upstream", + DivergenceSectionHeaderLocal: "Local", + DivergenceSectionHeaderRemote: "Remote", SetUpstreamTitle: "Set upstream branch", SetUpstreamMessage: "Are you sure you want to set the upstream branch of '{{.checkedOut}}' to '{{.selected}}'", EditRemote: "Edit remote", diff --git a/pkg/integration/components/text_matcher.go b/pkg/integration/components/text_matcher.go index 2d2c0e577..a5b987646 100644 --- a/pkg/integration/components/text_matcher.go +++ b/pkg/integration/components/text_matcher.go @@ -39,6 +39,18 @@ func (self *TextMatcher) DoesNotContain(target string) *TextMatcher { return self } +func (self *TextMatcher) DoesNotContainAnyOf(targets []string) *TextMatcher { + self.appendRule(matcherRule[string]{ + name: fmt.Sprintf("does not contain any of '%s'", targets), + testFn: func(value string) (bool, string) { + return lo.NoneBy(targets, func(target string) bool { return strings.Contains(value, target) }), + fmt.Sprintf("Expected none of '%s' to be found in '%s'", targets, value) + }, + }) + + return self +} + func (self *TextMatcher) MatchesRegexp(target string) *TextMatcher { self.appendRule(matcherRule[string]{ name: fmt.Sprintf("matches regular expression '%s'", target), @@ -107,6 +119,10 @@ func DoesNotContain(target string) *TextMatcher { return AnyString().DoesNotContain(target) } +func DoesNotContainAnyOf(targets ...string) *TextMatcher { + return AnyString().DoesNotContainAnyOf(targets) +} + func MatchesRegexp(target string) *TextMatcher { return AnyString().MatchesRegexp(target) } diff --git a/pkg/integration/tests/branch/show_divergence_from_upstream.go b/pkg/integration/tests/branch/show_divergence_from_upstream.go new file mode 100644 index 000000000..10fc29986 --- /dev/null +++ b/pkg/integration/tests/branch/show_divergence_from_upstream.go @@ -0,0 +1,54 @@ +package branch + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var ShowDivergenceFromUpstream = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Show divergence from upstream", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + shell.CreateFileAndAdd("file", "content1") + shell.Commit("one") + shell.UpdateFileAndAdd("file", "content2") + shell.Commit("two") + shell.CreateFileAndAdd("file3", "content3") + shell.Commit("three") + + shell.CloneIntoRemote("origin") + + shell.SetBranchUpstream("master", "origin/master") + + shell.HardReset("HEAD^^") + shell.CreateFileAndAdd("file4", "content4") + shell.Commit("four") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Commits(). + Lines( + Contains("four"), + Contains("one"), + ) + + t.Views().Branches(). + Focus(). + Lines(Contains("master")). + Press(keys.Branches.SetUpstream) + + t.ExpectPopup().Menu().Title(Contains("upstream")).Select(Contains("View divergence from upstream")).Confirm() + + t.Views().SubCommits(). + IsFocused(). + Title(Contains("Commits (master <-> origin/master)")). + Lines( + DoesNotContainAnyOf("↓", "↑").Contains("--- Remote ---"), + Contains("↓").Contains("three"), + Contains("↓").Contains("two"), + DoesNotContainAnyOf("↓", "↑").Contains("--- Local ---"), + Contains("↑").Contains("four"), + ) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index 779286fbc..0a50b7441 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -49,6 +49,7 @@ var tests = []*components.IntegrationTest{ branch.Reset, branch.ResetUpstream, branch.SetUpstream, + branch.ShowDivergenceFromUpstream, branch.Suggestions, cherry_pick.CherryPick, cherry_pick.CherryPickConflicts,