Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 46 additions & 10 deletions pkg/commands/git_commands/commit_loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"os"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
"sync"
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand All @@ -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{}

Expand Down Expand Up @@ -222,6 +248,7 @@ func (self *CommitLoader) extractCommitFromLine(line string) *models.Commit {
AuthorName: authorName,
AuthorEmail: authorEmail,
Parents: parents,
Divergence: divergence,
}
}

Expand Down Expand Up @@ -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
})
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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").
Expand All @@ -632,11 +667,12 @@ 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()

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`
17 changes: 9 additions & 8 deletions pkg/commands/git_commands/commit_loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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")).
Expand Down Expand Up @@ -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")).
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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)
})
}
Expand Down
18 changes: 18 additions & 0 deletions pkg/commands/models/branch.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package models

import "fmt"

// Branch : A git branch
// duplicating this for now
type Branch struct {
Expand Down Expand Up @@ -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()
}
Expand Down
12 changes: 12 additions & 0 deletions pkg/commands/models/commit.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,17 @@ const (
ActionConflict = todo.Comment + 1
)

type Divergence int
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd add a comment explaining that this is only set when comparing refs

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added one in 8d163ca.


// 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
Expand All @@ -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
Expand Down
49 changes: 42 additions & 7 deletions pkg/gui/context/sub_commits_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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() == "",
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really too bad; I'd like to have the graph in the left/right view, too. But I tried, and it's too hard to do; not only would we have to render the graph in two sections, but we'd actually have to calculate two separate sets of pipe sets; and then there's this global cache for these. I'll leave this as a future improvement.

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,
Expand All @@ -96,6 +125,7 @@ func NewSubCommitsContext(
ListRenderer: ListRenderer{
list: viewModel,
getDisplayStrings: getDisplayStrings,
getNonModelItems: getNonModelItems,
},
c: c,
refreshViewportOnChange: true,
Expand All @@ -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
Expand All @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down
Loading