From 911aa7774b6e0210a0955d7a493d2da82ada54ad Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Mon, 21 Aug 2023 16:31:20 +0200 Subject: [PATCH 1/7] Don't return commits from setCommitMergedStatuses Since the slice stores pointers to objects, and we're only modifying the objects but not the slice itself, there's no need to return it and assign it back. This will allow us to call the function for subslices of commits. Also, move the condition that checks for an empty string inside the function; we're going to call it from more than one place, so this makes it easier. --- pkg/commands/git_commands/commit_loader.go | 11 ++++++----- pkg/commands/git_commands/commit_loader_test.go | 3 ++- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/pkg/commands/git_commands/commit_loader.go b/pkg/commands/git_commands/commit_loader.go index 372bc7a4d..b399cff18 100644 --- a/pkg/commands/git_commands/commit_loader.go +++ b/pkg/commands/git_commands/commit_loader.go @@ -137,9 +137,7 @@ func (self *CommitLoader) GetCommits(opts GetCommitsOptions) ([]*models.Commit, return commits, nil } - if ancestor != "" { - commits = setCommitMergedStatuses(ancestor, commits) - } + setCommitMergedStatuses(ancestor, commits) return commits, nil } @@ -495,7 +493,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 +511,6 @@ func setCommitMergedStatuses(ancestor string, commits []*models.Commit) []*model commits[i].Status = models.StatusMerged } } - return commits } func (self *CommitLoader) getMergeBase(refName string) string { diff --git a/pkg/commands/git_commands/commit_loader_test.go b/pkg/commands/git_commands/commit_loader_test.go index 8cf9b95a2..4ae7af94a 100644 --- a/pkg/commands/git_commands/commit_loader_test.go +++ b/pkg/commands/git_commands/commit_loader_test.go @@ -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) }) } From 12f24aa8b44aa9e5470099ae0bbc8f2231c912b7 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sat, 5 Aug 2023 11:50:11 +0200 Subject: [PATCH 2/7] Add option RefToShowDivergenceFrom to GetCommitsOptions Not used yet. --- pkg/commands/git_commands/commit_loader.go | 49 ++++++++++++++++--- .../git_commands/commit_loader_test.go | 14 +++--- pkg/commands/models/commit.go | 12 +++++ 3 files changed, 61 insertions(+), 14 deletions(-) diff --git a/pkg/commands/git_commands/commit_loader.go b/pkg/commands/git_commands/commit_loader.go index b399cff18..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,7 +144,24 @@ func (self *CommitLoader) GetCommits(opts GetCommitsOptions) ([]*models.Commit, return commits, nil } - 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 } @@ -177,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] @@ -187,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{} @@ -220,6 +248,7 @@ func (self *CommitLoader) extractCommitFromLine(line string) *models.Commit { AuthorName: authorName, AuthorEmail: authorEmail, Parents: parents, + Divergence: divergence, } } @@ -249,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 }) @@ -623,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"). @@ -633,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() @@ -640,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 4ae7af94a..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, 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 From 4de3fadb00e3829bcda3ae2d4379251c6a81c0a8 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Wed, 16 Aug 2023 08:55:34 +0200 Subject: [PATCH 3/7] Remove sub_commits_context's Title method It implemented this because it wants to do custom truncation of the ref name; however, we can achieve the same thing by passing the truncated ref name to our DynamicTitleBuilder, which was previously unused. --- pkg/gui/context/sub_commits_context.go | 6 ------ pkg/gui/controllers/switch_to_sub_commits_controller.go | 3 ++- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/pkg/gui/context/sub_commits_context.go b/pkg/gui/context/sub_commits_context.go index 43cc513c0..b9b3a91b0 100644 --- a/pkg/gui/context/sub_commits_context.go +++ b/pkg/gui/context/sub_commits_context.go @@ -1,14 +1,12 @@ package context import ( - "fmt" "time" "github.com/jesseduffield/lazygit/pkg/commands/git_commands" "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" ) type SubCommitsContext struct { @@ -160,10 +158,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/switch_to_sub_commits_controller.go b/pkg/gui/controllers/switch_to_sub_commits_controller.go index 5b86854ca..a2b25efbb 100644 --- a/pkg/gui/controllers/switch_to_sub_commits_controller.go +++ b/pkg/gui/controllers/switch_to_sub_commits_controller.go @@ -4,6 +4,7 @@ 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" ) var _ types.IController = &SwitchToSubCommitsController{} @@ -78,7 +79,7 @@ func (self *SwitchToSubCommitsController) viewCommits() error { subCommitsContext.SetSelectedLineIdx(0) subCommitsContext.SetParentContext(self.context) subCommitsContext.SetWindowName(self.context.GetWindowName()) - subCommitsContext.SetTitleRef(ref.Description()) + subCommitsContext.SetTitleRef(utils.TruncateWithEllipsis(ref.RefName(), 50)) subCommitsContext.SetRef(ref) subCommitsContext.SetLimitCommits(true) subCommitsContext.SetShowBranchHeads(self.context.ShowBranchHeadsInSubCommits()) From e8fac6ca7360faf5db1c82af2b2e9c87aa5a5edc Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sat, 5 Aug 2023 11:19:16 +0200 Subject: [PATCH 4/7] Extract a SubCommitsHelper from SwitchToSubCommitsController We want to use it from BranchesController too. --- pkg/gui/controllers.go | 21 +++--- pkg/gui/controllers/helpers/helpers.go | 2 + .../controllers/helpers/sub_commits_helper.go | 69 +++++++++++++++++++ .../switch_to_sub_commits_controller.go | 47 ++----------- 4 files changed, 88 insertions(+), 51 deletions(-) create mode 100644 pkg/gui/controllers/helpers/sub_commits_helper.go 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/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/sub_commits_helper.go b/pkg/gui/controllers/helpers/sub_commits_helper.go new file mode 100644 index 000000000..412c23f0d --- /dev/null +++ b/pkg/gui/controllers/helpers/sub_commits_helper.go @@ -0,0 +1,69 @@ +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 + 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(), + }, + ) + 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.Ref.RefName(), 50)) + subCommitsContext.SetRef(opts.Ref) + 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 a2b25efbb..71ed5747c 100644 --- a/pkg/gui/controllers/switch_to_sub_commits_controller.go +++ b/pkg/gui/controllers/switch_to_sub_commits_controller.go @@ -1,10 +1,8 @@ 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" - "github.com/jesseduffield/lazygit/pkg/utils" ) var _ types.IController = &SwitchToSubCommitsController{} @@ -19,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, } } @@ -58,40 +52,11 @@ 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(utils.TruncateWithEllipsis(ref.RefName(), 50)) - 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, + Context: self.context, + ShowBranchHeads: self.context.ShowBranchHeadsInSubCommits(), + }) } func (self *SwitchToSubCommitsController) Context() types.Context { From 572d1b5920104b06b3297e3861454f14738bf71b Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sat, 5 Aug 2023 12:06:37 +0200 Subject: [PATCH 5/7] Add "Show divergence from upstream" entry to Upstream menu in branches panel --- pkg/commands/models/branch.go | 18 ++++++++ pkg/gui/context/sub_commits_context.go | 45 ++++++++++++++++++- pkg/gui/controllers/branches_controller.go | 22 +++++++++ pkg/gui/controllers/helpers/refresh_helper.go | 11 ++--- .../controllers/helpers/sub_commits_helper.go | 22 +++++---- .../switch_to_sub_commits_controller.go | 1 + pkg/gui/presentation/commits.go | 6 ++- pkg/i18n/english.go | 8 ++++ 8 files changed, 116 insertions(+), 17 deletions(-) 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/gui/context/sub_commits_context.go b/pkg/gui/context/sub_commits_context.go index b9b3a91b0..7bf34d190 100644 --- a/pkg/gui/context/sub_commits_context.go +++ b/pkg/gui/context/sub_commits_context.go @@ -1,12 +1,14 @@ package context import ( + "fmt" "time" "github.com/jesseduffield/lazygit/pkg/commands/git_commands" "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/gui/presentation" "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/samber/lo" ) type SubCommitsContext struct { @@ -71,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, @@ -94,6 +125,7 @@ func NewSubCommitsContext( ListRenderer: ListRenderer{ list: viewModel, getDisplayStrings: getDisplayStrings, + getNonModelItems: getNonModelItems, }, c: c, refreshViewportOnChange: true, @@ -110,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 @@ -125,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 } 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/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 index 412c23f0d..74c50fd14 100644 --- a/pkg/gui/controllers/helpers/sub_commits_helper.go +++ b/pkg/gui/controllers/helpers/sub_commits_helper.go @@ -27,19 +27,22 @@ func NewSubCommitsHelper( } type ViewSubCommitsOpts struct { - Ref types.Ref - Context types.Context - ShowBranchHeads bool + 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(), + Limit: true, + FilterPath: self.c.Modes().Filtering.GetPath(), + IncludeRebaseCommits: false, + RefName: opts.Ref.FullRefName(), + RefForPushedStatus: opts.Ref.FullRefName(), + RefToShowDivergenceFrom: opts.RefToShowDivergenceFrom, }, ) if err != nil { @@ -53,8 +56,9 @@ func (self *SubCommitsHelper) ViewSubCommits(opts ViewSubCommitsOpts) error { subCommitsContext.SetSelectedLineIdx(0) subCommitsContext.SetParentContext(opts.Context) subCommitsContext.SetWindowName(opts.Context.GetWindowName()) - subCommitsContext.SetTitleRef(utils.TruncateWithEllipsis(opts.Ref.RefName(), 50)) + subCommitsContext.SetTitleRef(utils.TruncateWithEllipsis(opts.TitleRef, 50)) subCommitsContext.SetRef(opts.Ref) + subCommitsContext.SetRefToShowDivergenceFrom(opts.RefToShowDivergenceFrom) subCommitsContext.SetLimitCommits(true) subCommitsContext.SetShowBranchHeads(opts.ShowBranchHeads) subCommitsContext.ClearSearchString() diff --git a/pkg/gui/controllers/switch_to_sub_commits_controller.go b/pkg/gui/controllers/switch_to_sub_commits_controller.go index 71ed5747c..3109f559e 100644 --- a/pkg/gui/controllers/switch_to_sub_commits_controller.go +++ b/pkg/gui/controllers/switch_to_sub_commits_controller.go @@ -54,6 +54,7 @@ func (self *SwitchToSubCommitsController) viewCommits() error { return self.c.Helpers().SubCommits.ViewSubCommits(helpers.ViewSubCommitsOpts{ Ref: ref, + TitleRef: ref.RefName(), Context: self.context, ShowBranchHeads: self.context.ShowBranchHeadsInSubCommits(), }) 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", From ea532b47296d566e696ae34967d0ad8830325773 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Tue, 15 Aug 2023 16:31:50 +0200 Subject: [PATCH 6/7] Add DoesNotContainAnyOf matcher --- pkg/integration/components/text_matcher.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) 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) } From df38e954f48352a91ebf615a5be1118ea6ab227c Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sat, 5 Aug 2023 12:31:21 +0200 Subject: [PATCH 7/7] Add integration test for the new divergence log --- .../branch/show_divergence_from_upstream.go | 54 +++++++++++++++++++ pkg/integration/tests/test_list.go | 1 + 2 files changed, 55 insertions(+) create mode 100644 pkg/integration/tests/branch/show_divergence_from_upstream.go 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,