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