Skip to content

Commit 0f7f1a5

Browse files
committed
Fix showing diffs for renamed file when filtering by path
When filtering for a file path we use the --follow option for "git log", so it will follow renames of the file, which is great. However, if you then selected one of the commits before a rename, you didn't see a diff, because we would pass the original filter path to the "git show" call. To fix this, call git log with the --name-status option when filtering by path, so that each commit reports which file paths are touched in this commit; remember these in the commit object, so that we can pass them to the "git show" call later. Be careful not to store too many such paths unnecessarily. When filtering by folder rather than file, all these paths will necessarily be inside the original filter path, so detect this and don't store them in this case. There is some unfortunate code duplication between loading commits and loading reflog commits, which I am too lazy to clean up right now.
1 parent 88dae1d commit 0f7f1a5

File tree

11 files changed

+119
-67
lines changed

11 files changed

+119
-67
lines changed

pkg/commands/git_commands/commit.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ func (self *CommitCommands) AmendHeadCmdObj() *oscommands.CmdObj {
253253
return self.cmd.New(cmdArgs)
254254
}
255255

256-
func (self *CommitCommands) ShowCmdObj(hash string, filterPath string) *oscommands.CmdObj {
256+
func (self *CommitCommands) ShowCmdObj(hash string, filterPaths []string) *oscommands.CmdObj {
257257
contextSize := self.UserConfig().Git.DiffContextSize
258258

259259
extDiffCmd := self.UserConfig().Git.Paging.ExternalDiffCommand
@@ -271,7 +271,7 @@ func (self *CommitCommands) ShowCmdObj(hash string, filterPath string) *oscomman
271271
ArgIf(self.UserConfig().Git.IgnoreWhitespaceInDiffView, "--ignore-all-space").
272272
Arg(fmt.Sprintf("--find-renames=%d%%", self.UserConfig().Git.RenameSimilarityThreshold)).
273273
Arg("--").
274-
ArgIf(filterPath != "", filterPath).
274+
Arg(filterPaths...).
275275
Dir(self.repoPaths.worktreePath).
276276
ToArgv()
277277

pkg/commands/git_commands/commit_loader.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ func (self *CommitLoader) GetCommits(opts GetCommitsOptions) ([]*models.Commit,
9090
defer wg.Done()
9191

9292
var realCommits []*models.Commit
93-
realCommits, logErr = loadCommits(self.getLogCmd(opts), func(line string) (*models.Commit, bool) {
93+
realCommits, logErr = loadCommits(self.getLogCmd(opts), opts.FilterPath, func(line string) (*models.Commit, bool) {
9494
return self.extractCommitFromLine(opts.HashPool, line, opts.RefToShowDivergenceFrom != ""), false
9595
})
9696
if logErr == nil {
@@ -294,7 +294,10 @@ func (self *CommitLoader) getHydratedTodoCommits(hashPool *utils.StringPool, tod
294294

295295
fullCommits := map[string]*models.Commit{}
296296
err := cmdObj.RunAndProcessLines(func(line string) (bool, error) {
297-
commit := self.extractCommitFromLine(hashPool, line, false)
297+
if line == "" || line[0] != '+' {
298+
return false, nil
299+
}
300+
commit := self.extractCommitFromLine(hashPool, line[1:], false)
298301
fullCommits[commit.Hash()] = commit
299302
return false, nil
300303
})
@@ -601,7 +604,7 @@ func (self *CommitLoader) getLogCmd(opts GetCommitsOptions) *oscommands.CmdObj {
601604
Arg("--abbrev=40").
602605
ArgIf(opts.FilterAuthor != "", "--author="+opts.FilterAuthor).
603606
ArgIf(opts.Limit, "-300").
604-
ArgIf(opts.FilterPath != "", "--follow").
607+
ArgIf(opts.FilterPath != "", "--follow", "--name-status").
605608
Arg("--no-show-signature").
606609
ArgIf(opts.RefToShowDivergenceFrom != "", "--left-right").
607610
Arg("--").
@@ -611,4 +614,4 @@ func (self *CommitLoader) getLogCmd(opts GetCommitsOptions) *oscommands.CmdObj {
611614
return self.cmd.New(cmdArgs).DontLog()
612615
}
613616

614-
const prettyFormat = `--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%P%x00%m%x00%s`
617+
const prettyFormat = `--pretty=format:+%H%x00%at%x00%aN%x00%ae%x00%D%x00%P%x00%m%x00%s`

pkg/commands/git_commands/commit_loader_test.go

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,16 @@ import (
1515
"github.com/stretchr/testify/assert"
1616
)
1717

18-
var commitsOutput = strings.ReplaceAll(`0eea75e8c631fba6b58135697835d58ba4c18dbc|1640826609|Jesse Duffield|[email protected]|HEAD -> better-tests|b21997d6b4cbdf84b149|>|better typing for rebase mode
19-
b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164|1640824515|Jesse Duffield|[email protected]|origin/better-tests|e94e8fc5b6fab4cb755f|>|fix logging
20-
e94e8fc5b6fab4cb755f29f1bdb3ee5e001df35c|1640823749|Jesse Duffield|[email protected]|tag: 123, tag: 456|d8084cd558925eb7c9c3|>|refactor
21-
d8084cd558925eb7c9c38afeed5725c21653ab90|1640821426|Jesse Duffield|[email protected]||65f910ebd85283b5cce9|>|WIP
22-
65f910ebd85283b5cce9bf67d03d3f1a9ea3813a|1640821275|Jesse Duffield|[email protected]||26c07b1ab33860a1a759|>|WIP
23-
26c07b1ab33860a1a7591a0638f9925ccf497ffa|1640750752|Jesse Duffield|[email protected]||3d4470a6c072208722e5|>|WIP
24-
3d4470a6c072208722e5ae9a54bcb9634959a1c5|1640748818|Jesse Duffield|[email protected]||053a66a7be3da43aacdc|>|WIP
25-
053a66a7be3da43aacdc7aa78e1fe757b82c4dd2|1640739815|Jesse Duffield|[email protected]||985fe482e806b172aea4|>|refactoring the config struct`, "|", "\x00")
18+
var commitsOutput = strings.ReplaceAll(`+0eea75e8c631fba6b58135697835d58ba4c18dbc|1640826609|Jesse Duffield|[email protected]|HEAD -> better-tests|b21997d6b4cbdf84b149|>|better typing for rebase mode
19+
+b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164|1640824515|Jesse Duffield|[email protected]|origin/better-tests|e94e8fc5b6fab4cb755f|>|fix logging
20+
+e94e8fc5b6fab4cb755f29f1bdb3ee5e001df35c|1640823749|Jesse Duffield|[email protected]|tag: 123, tag: 456|d8084cd558925eb7c9c3|>|refactor
21+
+d8084cd558925eb7c9c38afeed5725c21653ab90|1640821426|Jesse Duffield|[email protected]||65f910ebd85283b5cce9|>|WIP
22+
+65f910ebd85283b5cce9bf67d03d3f1a9ea3813a|1640821275|Jesse Duffield|[email protected]||26c07b1ab33860a1a759|>|WIP
23+
+26c07b1ab33860a1a7591a0638f9925ccf497ffa|1640750752|Jesse Duffield|[email protected]||3d4470a6c072208722e5|>|WIP
24+
+3d4470a6c072208722e5ae9a54bcb9634959a1c5|1640748818|Jesse Duffield|[email protected]||053a66a7be3da43aacdc|>|WIP
25+
+053a66a7be3da43aacdc7aa78e1fe757b82c4dd2|1640739815|Jesse Duffield|[email protected]||985fe482e806b172aea4|>|refactoring the config struct`, "|", "\x00")
2626

27-
var singleCommitOutput = strings.ReplaceAll(`0eea75e8c631fba6b58135697835d58ba4c18dbc|1640826609|Jesse Duffield|[email protected]|HEAD -> better-tests|b21997d6b4cbdf84b149|>|better typing for rebase mode`, "|", "\x00")
27+
var singleCommitOutput = strings.ReplaceAll(`+0eea75e8c631fba6b58135697835d58ba4c18dbc|1640826609|Jesse Duffield|[email protected]|HEAD -> better-tests|b21997d6b4cbdf84b149|>|better typing for rebase mode`, "|", "\x00")
2828

2929
func TestGetCommits(t *testing.T) {
3030
type scenario struct {
@@ -44,7 +44,7 @@ func TestGetCommits(t *testing.T) {
4444
opts: GetCommitsOptions{RefName: "HEAD", RefForPushedStatus: "mybranch", IncludeRebaseCommits: false},
4545
runner: oscommands.NewFakeRunner(t).
4646
ExpectGitArgs([]string{"merge-base", "mybranch", "mybranch@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
47-
ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%P%x00%m%x00%s", "--abbrev=40", "--no-show-signature", "--"}, "", nil),
47+
ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:+%H%x00%at%x00%aN%x00%ae%x00%D%x00%P%x00%m%x00%s", "--abbrev=40", "--no-show-signature", "--"}, "", nil),
4848

4949
expectedCommitOpts: []models.NewCommitOpts{},
5050
expectedError: nil,
@@ -55,7 +55,7 @@ func TestGetCommits(t *testing.T) {
5555
opts: GetCommitsOptions{RefName: "refs/heads/mybranch", RefForPushedStatus: "refs/heads/mybranch", IncludeRebaseCommits: false},
5656
runner: oscommands.NewFakeRunner(t).
5757
ExpectGitArgs([]string{"merge-base", "refs/heads/mybranch", "mybranch@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
58-
ExpectGitArgs([]string{"log", "refs/heads/mybranch", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%P%x00%m%x00%s", "--abbrev=40", "--no-show-signature", "--"}, "", nil),
58+
ExpectGitArgs([]string{"log", "refs/heads/mybranch", "--topo-order", "--oneline", "--pretty=format:+%H%x00%at%x00%aN%x00%ae%x00%D%x00%P%x00%m%x00%s", "--abbrev=40", "--no-show-signature", "--"}, "", nil),
5959

6060
expectedCommitOpts: []models.NewCommitOpts{},
6161
expectedError: nil,
@@ -69,7 +69,7 @@ func TestGetCommits(t *testing.T) {
6969
// here it's seeing which commits are yet to be pushed
7070
ExpectGitArgs([]string{"merge-base", "mybranch", "mybranch@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
7171
// here it's actually getting all the commits in a formatted form, one per line
72-
ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%P%x00%m%x00%s", "--abbrev=40", "--no-show-signature", "--"}, commitsOutput, nil).
72+
ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:+%H%x00%at%x00%aN%x00%ae%x00%D%x00%P%x00%m%x00%s", "--abbrev=40", "--no-show-signature", "--"}, commitsOutput, nil).
7373
// here it's testing which of the configured main branches have an upstream
7474
ExpectGitArgs([]string{"rev-parse", "--symbolic-full-name", "master@{u}"}, "refs/remotes/origin/master", nil). // this one does
7575
ExpectGitArgs([]string{"rev-parse", "--symbolic-full-name", "main@{u}"}, "", errors.New("error")). // this one doesn't, so it checks origin instead
@@ -205,7 +205,7 @@ func TestGetCommits(t *testing.T) {
205205
// here it's seeing which commits are yet to be pushed
206206
ExpectGitArgs([]string{"merge-base", "mybranch", "mybranch@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
207207
// here it's actually getting all the commits in a formatted form, one per line
208-
ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%P%x00%m%x00%s", "--abbrev=40", "--no-show-signature", "--"}, singleCommitOutput, nil).
208+
ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:+%H%x00%at%x00%aN%x00%ae%x00%D%x00%P%x00%m%x00%s", "--abbrev=40", "--no-show-signature", "--"}, singleCommitOutput, nil).
209209
// here it's testing which of the configured main branches exist; neither does
210210
ExpectGitArgs([]string{"rev-parse", "--symbolic-full-name", "master@{u}"}, "", errors.New("error")).
211211
ExpectGitArgs([]string{"rev-parse", "--verify", "--quiet", "refs/remotes/origin/master"}, "", errors.New("error")).
@@ -241,7 +241,7 @@ func TestGetCommits(t *testing.T) {
241241
// here it's seeing which commits are yet to be pushed
242242
ExpectGitArgs([]string{"merge-base", "mybranch", "mybranch@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
243243
// here it's actually getting all the commits in a formatted form, one per line
244-
ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%P%x00%m%x00%s", "--abbrev=40", "--no-show-signature", "--"}, singleCommitOutput, nil).
244+
ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:+%H%x00%at%x00%aN%x00%ae%x00%D%x00%P%x00%m%x00%s", "--abbrev=40", "--no-show-signature", "--"}, singleCommitOutput, nil).
245245
// here it's testing which of the configured main branches exist
246246
ExpectGitArgs([]string{"rev-parse", "--symbolic-full-name", "master@{u}"}, "refs/remotes/origin/master", nil).
247247
ExpectGitArgs([]string{"rev-parse", "--symbolic-full-name", "main@{u}"}, "", errors.New("error")).
@@ -276,7 +276,7 @@ func TestGetCommits(t *testing.T) {
276276
opts: GetCommitsOptions{RefName: "HEAD", RefForPushedStatus: "mybranch", IncludeRebaseCommits: false},
277277
runner: oscommands.NewFakeRunner(t).
278278
ExpectGitArgs([]string{"merge-base", "mybranch", "mybranch@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
279-
ExpectGitArgs([]string{"log", "HEAD", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%P%x00%m%x00%s", "--abbrev=40", "--no-show-signature", "--"}, "", nil),
279+
ExpectGitArgs([]string{"log", "HEAD", "--oneline", "--pretty=format:+%H%x00%at%x00%aN%x00%ae%x00%D%x00%P%x00%m%x00%s", "--abbrev=40", "--no-show-signature", "--"}, "", nil),
280280

281281
expectedCommitOpts: []models.NewCommitOpts{},
282282
expectedError: nil,
@@ -287,7 +287,7 @@ func TestGetCommits(t *testing.T) {
287287
opts: GetCommitsOptions{RefName: "HEAD", RefForPushedStatus: "mybranch", FilterPath: "src"},
288288
runner: oscommands.NewFakeRunner(t).
289289
ExpectGitArgs([]string{"merge-base", "mybranch", "mybranch@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
290-
ExpectGitArgs([]string{"log", "HEAD", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%P%x00%m%x00%s", "--abbrev=40", "--follow", "--no-show-signature", "--", "src"}, "", nil),
290+
ExpectGitArgs([]string{"log", "HEAD", "--oneline", "--pretty=format:+%H%x00%at%x00%aN%x00%ae%x00%D%x00%P%x00%m%x00%s", "--abbrev=40", "--follow", "--name-status", "--no-show-signature", "--", "src"}, "", nil),
291291

292292
expectedCommitOpts: []models.NewCommitOpts{},
293293
expectedError: nil,
Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,74 @@
11
package git_commands
22

33
import (
4+
"strings"
5+
46
"github.com/jesseduffield/lazygit/pkg/commands/models"
57
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
8+
"github.com/samber/lo"
69
)
710

811
func loadCommits(
912
cmd *oscommands.CmdObj,
13+
filterPath string,
1014
parseLogLine func(string) (*models.Commit, bool),
1115
) ([]*models.Commit, error) {
1216
commits := []*models.Commit{}
1317

18+
var commit *models.Commit
19+
var filterPaths []string
20+
// A string pool that stores interned strings to reduce memory usage
21+
pool := make(map[string]string)
22+
23+
finishLastCommit := func() {
24+
if commit != nil {
25+
// Only set the filter paths if we have one that is not contained in the original
26+
// filter path. When filtering on a directory, all file paths will start with that
27+
// directory, so we needn't bother storing the individual paths. Likewise, if we
28+
// filter on a file and the file path hasn't changed, we needn't store it either.
29+
// Only if a file has been moved or renamed do we need to store the paths, but then
30+
// we need them all so that we can properly render a diff for the rename.
31+
if lo.SomeBy(filterPaths, func(path string) bool {
32+
return !strings.HasPrefix(path, filterPath)
33+
}) {
34+
commit.FilterPaths = lo.Map(filterPaths, func(path string, _ int) string {
35+
if v, ok := pool[path]; ok {
36+
return v
37+
}
38+
pool[path] = path
39+
return path
40+
})
41+
}
42+
commits = append(commits, commit)
43+
commit = nil
44+
filterPaths = nil
45+
}
46+
}
1447
err := cmd.RunAndProcessLines(func(line string) (bool, error) {
15-
commit, stop := parseLogLine(line)
16-
if stop {
17-
return true, nil
48+
if line == "" {
49+
return false, nil
50+
}
51+
52+
if line[0] == '+' {
53+
finishLastCommit()
54+
var stop bool
55+
commit, stop = parseLogLine(line[1:])
56+
if stop {
57+
commit = nil
58+
return true, nil
59+
}
60+
} else if commit != nil && filterPath != "" {
61+
// We are filtering by path, and this line is the output of the --name-status flag
62+
fields := strings.Split(line, "\t")
63+
// We don't bother looking at the first field (it will be 'A', 'M', 'R072' or a bunch of others).
64+
// All we care about is the path(s), and there will be one for 'M' and 'A', and two for 'R' or 'C',
65+
// in which case we want them both so that we can show the diff between the two.
66+
if len(fields) > 1 {
67+
filterPaths = append(filterPaths, fields[1:]...)
68+
}
1869
}
19-
commits = append(commits, commit)
2070
return false, nil
2171
})
72+
finishLastCommit()
2273
return commits, err
2374
}

pkg/commands/git_commands/commit_test.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ func TestCommitCreateAmendCommit(t *testing.T) {
251251
func TestCommitShowCmdObj(t *testing.T) {
252252
type scenario struct {
253253
testName string
254-
filterPath string
254+
filterPaths []string
255255
contextSize uint64
256256
similarityThreshold int
257257
ignoreWhitespace bool
@@ -262,7 +262,7 @@ func TestCommitShowCmdObj(t *testing.T) {
262262
scenarios := []scenario{
263263
{
264264
testName: "Default case without filter path",
265-
filterPath: "",
265+
filterPaths: []string{},
266266
contextSize: 3,
267267
similarityThreshold: 50,
268268
ignoreWhitespace: false,
@@ -271,7 +271,7 @@ func TestCommitShowCmdObj(t *testing.T) {
271271
},
272272
{
273273
testName: "Default case with filter path",
274-
filterPath: "file.txt",
274+
filterPaths: []string{"file.txt"},
275275
contextSize: 3,
276276
similarityThreshold: 50,
277277
ignoreWhitespace: false,
@@ -280,7 +280,7 @@ func TestCommitShowCmdObj(t *testing.T) {
280280
},
281281
{
282282
testName: "Show diff with custom context size",
283-
filterPath: "",
283+
filterPaths: []string{},
284284
contextSize: 77,
285285
similarityThreshold: 50,
286286
ignoreWhitespace: false,
@@ -289,7 +289,7 @@ func TestCommitShowCmdObj(t *testing.T) {
289289
},
290290
{
291291
testName: "Show diff with custom similarity threshold",
292-
filterPath: "",
292+
filterPaths: []string{},
293293
contextSize: 3,
294294
similarityThreshold: 33,
295295
ignoreWhitespace: false,
@@ -298,7 +298,7 @@ func TestCommitShowCmdObj(t *testing.T) {
298298
},
299299
{
300300
testName: "Show diff, ignoring whitespace",
301-
filterPath: "",
301+
filterPaths: []string{},
302302
contextSize: 77,
303303
similarityThreshold: 50,
304304
ignoreWhitespace: true,
@@ -307,7 +307,7 @@ func TestCommitShowCmdObj(t *testing.T) {
307307
},
308308
{
309309
testName: "Show diff with external diff command",
310-
filterPath: "",
310+
filterPaths: []string{},
311311
contextSize: 3,
312312
similarityThreshold: 50,
313313
ignoreWhitespace: false,
@@ -330,7 +330,7 @@ func TestCommitShowCmdObj(t *testing.T) {
330330
}
331331
instance := buildCommitCommands(commonDeps{userConfig: userConfig, appState: &config.AppState{}, runner: runner, repoPaths: &repoPaths})
332332

333-
assert.NoError(t, instance.ShowCmdObj("1234567890", s.filterPath).Run())
333+
assert.NoError(t, instance.ShowCmdObj("1234567890", s.filterPaths).Run())
334334
runner.CheckForMissingCalls()
335335
})
336336
}

pkg/commands/git_commands/reflog_commit_loader.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,16 @@ func (self *ReflogCommitLoader) GetReflogCommits(hashPool *utils.StringPool, las
2828
cmdArgs := NewGitCmd("log").
2929
Config("log.showSignature=false").
3030
Arg("-g").
31-
Arg("--format=%H%x00%ct%x00%gs%x00%P").
31+
Arg("--format=+%H%x00%ct%x00%gs%x00%P").
3232
ArgIf(filterAuthor != "", "--author="+filterAuthor).
33-
ArgIf(filterPath != "", "--follow", "--", filterPath).
33+
ArgIf(filterPath != "", "--follow", "--name-status", "--", filterPath).
3434
ToArgv()
3535

3636
cmdObj := self.cmd.New(cmdArgs).DontLog()
3737

3838
onlyObtainedNewReflogCommits := false
3939

40-
commits, err := loadCommits(cmdObj, func(line string) (*models.Commit, bool) {
40+
commits, err := loadCommits(cmdObj, filterPath, func(line string) (*models.Commit, bool) {
4141
commit, ok := self.parseLine(hashPool, line)
4242
if !ok {
4343
return nil, false

0 commit comments

Comments
 (0)