From 5d43d760c9adf4e8cfd63da72399c3d8eaf8f5e7 Mon Sep 17 00:00:00 2001 From: Dawid Pietrykowski Date: Fri, 12 Dec 2025 13:24:17 +0100 Subject: [PATCH 1/3] Add commiter date to commit data --- pkg/commands/git_commands/commit_loader.go | 27 ++++--- .../git_commands/commit_loader_test.go | 77 ++++++++++++------- .../git_commands/reflog_commit_loader.go | 14 ++-- .../git_commands/reflog_commit_loader_test.go | 33 +++++--- pkg/commands/models/commit.go | 3 + pkg/gui/services/custom_commands/models.go | 1 + .../custom_commands/session_state_loader.go | 1 + 7 files changed, 98 insertions(+), 58 deletions(-) diff --git a/pkg/commands/git_commands/commit_loader.go b/pkg/commands/git_commands/commit_loader.go index da4bcb7ff5e..352e59279db 100644 --- a/pkg/commands/git_commands/commit_loader.go +++ b/pkg/commands/git_commands/commit_loader.go @@ -190,29 +190,30 @@ func (self *CommitLoader) MergeRebasingCommits(hashPool *utils.StringPool, commi // example input: // 8ad01fe32fcc20f07bc6693f87aa4977c327f1e1|10 hours ago|Jesse Duffield| (HEAD -> master, tag: v0.15.2)|refresh commits when adding a tag func (self *CommitLoader) extractCommitFromLine(hashPool *utils.StringPool, line string, showDivergence bool) *models.Commit { - split := strings.SplitN(line, "\x00", 8) + split := strings.SplitN(line, "\x00", 9) - // Ensure we have the minimum required fields (at least 7 for basic functionality) - if len(split) < 7 { - self.Log.Warnf("Malformed git log line: expected at least 7 fields, got %d. Line: %s", len(split), line) + // Ensure we have the minimum required fields (at least 8 for basic functionality) + if len(split) < 8 { + self.Log.Warnf("Malformed git log line: expected at least 8 fields, got %d. Line: %s", len(split), line) return nil } hash := split[0] unixTimestamp := split[1] - authorName := split[2] - authorEmail := split[3] - parentHashes := split[4] + committerTimestamp := split[2] + authorName := split[3] + authorEmail := split[4] + parentHashes := split[5] divergence := models.DivergenceNone if showDivergence { - divergence = lo.Ternary(split[5] == "<", models.DivergenceLeft, models.DivergenceRight) + divergence = lo.Ternary(split[6] == "<", models.DivergenceLeft, models.DivergenceRight) } - extraInfo := strings.TrimSpace(split[6]) + extraInfo := strings.TrimSpace(split[7]) // message (and the \x00 before it) might not be present if extraInfo is extremely long message := "" - if len(split) > 7 { - message = split[7] + if len(split) > 8 { + message = split[8] } var tags []string @@ -232,6 +233,7 @@ func (self *CommitLoader) extractCommitFromLine(hashPool *utils.StringPool, line } unitTimestampInt, _ := strconv.Atoi(unixTimestamp) + committerTimestampInt, _ := strconv.Atoi(committerTimestamp) parents := []string{} if len(parentHashes) > 0 { @@ -244,6 +246,7 @@ func (self *CommitLoader) extractCommitFromLine(hashPool *utils.StringPool, line Tags: tags, ExtraInfo: extraInfo, UnixTimestamp: int64(unitTimestampInt), + CommitterDate: int64(committerTimestampInt), AuthorName: authorName, AuthorEmail: authorEmail, Parents: parents, @@ -603,4 +606,4 @@ func (self *CommitLoader) getLogCmd(opts GetCommitsOptions) *oscommands.CmdObj { return self.cmd.New(cmdArgs).DontLog() } -const prettyFormat = `--pretty=format:+%H%x00%at%x00%aN%x00%ae%x00%P%x00%m%x00%D%x00%s` +const prettyFormat = `--pretty=format:+%H%x00%at%x00%ct%x00%aN%x00%ae%x00%P%x00%m%x00%D%x00%s` diff --git a/pkg/commands/git_commands/commit_loader_test.go b/pkg/commands/git_commands/commit_loader_test.go index 7f9873b0b0b..ddc45c1fffb 100644 --- a/pkg/commands/git_commands/commit_loader_test.go +++ b/pkg/commands/git_commands/commit_loader_test.go @@ -16,16 +16,16 @@ import ( "github.com/stretchr/testify/assert" ) -var commitsOutput = strings.ReplaceAll(`+0eea75e8c631fba6b58135697835d58ba4c18dbc|1640826609|Jesse Duffield|jessedduffield@gmail.com|b21997d6b4cbdf84b149|>|HEAD -> better-tests|better typing for rebase mode -+b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164|1640824515|Jesse Duffield|jessedduffield@gmail.com|e94e8fc5b6fab4cb755f|>|origin/better-tests|fix logging -+e94e8fc5b6fab4cb755f29f1bdb3ee5e001df35c|1640823749|Jesse Duffield|jessedduffield@gmail.com|d8084cd558925eb7c9c3|>|tag: 123, tag: 456|refactor -+d8084cd558925eb7c9c38afeed5725c21653ab90|1640821426|Jesse Duffield|jessedduffield@gmail.com|65f910ebd85283b5cce9|>||WIP -+65f910ebd85283b5cce9bf67d03d3f1a9ea3813a|1640821275|Jesse Duffield|jessedduffield@gmail.com|26c07b1ab33860a1a759|>||WIP -+26c07b1ab33860a1a7591a0638f9925ccf497ffa|1640750752|Jesse Duffield|jessedduffield@gmail.com|3d4470a6c072208722e5|>||WIP -+3d4470a6c072208722e5ae9a54bcb9634959a1c5|1640748818|Jesse Duffield|jessedduffield@gmail.com|053a66a7be3da43aacdc|>||WIP -+053a66a7be3da43aacdc7aa78e1fe757b82c4dd2|1640739815|Jesse Duffield|jessedduffield@gmail.com|985fe482e806b172aea4|>||refactoring the config struct`, "|", "\x00") +var commitsOutput = strings.ReplaceAll(`+0eea75e8c631fba6b58135697835d58ba4c18dbc|1640826609|1640826609|Jesse Duffield|jessedduffield@gmail.com|b21997d6b4cbdf84b149|>|HEAD -> better-tests|better typing for rebase mode ++b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164|1640824515|1640824515|Jesse Duffield|jessedduffield@gmail.com|e94e8fc5b6fab4cb755f|>|origin/better-tests|fix logging ++e94e8fc5b6fab4cb755f29f1bdb3ee5e001df35c|1640823749|1640823749|Jesse Duffield|jessedduffield@gmail.com|d8084cd558925eb7c9c3|>|tag: 123, tag: 456|refactor ++d8084cd558925eb7c9c38afeed5725c21653ab90|1640821426|1640821426|Jesse Duffield|jessedduffield@gmail.com|65f910ebd85283b5cce9|>||WIP ++65f910ebd85283b5cce9bf67d03d3f1a9ea3813a|1640821275|1640821275|Jesse Duffield|jessedduffield@gmail.com|26c07b1ab33860a1a759|>||WIP ++26c07b1ab33860a1a7591a0638f9925ccf497ffa|1640750752|1640750752|Jesse Duffield|jessedduffield@gmail.com|3d4470a6c072208722e5|>||WIP ++3d4470a6c072208722e5ae9a54bcb9634959a1c5|1640748818|1640748818|Jesse Duffield|jessedduffield@gmail.com|053a66a7be3da43aacdc|>||WIP ++053a66a7be3da43aacdc7aa78e1fe757b82c4dd2|1640739815|1640739815|Jesse Duffield|jessedduffield@gmail.com|985fe482e806b172aea4|>||refactoring the config struct`, "|", "\x00") -var singleCommitOutput = strings.ReplaceAll(`+0eea75e8c631fba6b58135697835d58ba4c18dbc|1640826609|Jesse Duffield|jessedduffield@gmail.com|b21997d6b4cbdf84b149|>|HEAD -> better-tests|better typing for rebase mode`, "|", "\x00") +var singleCommitOutput = strings.ReplaceAll(`+0eea75e8c631fba6b58135697835d58ba4c18dbc|1640826609|1640826609|Jesse Duffield|jessedduffield@gmail.com|b21997d6b4cbdf84b149|>|HEAD -> better-tests|better typing for rebase mode`, "|", "\x00") func TestGetCommits(t *testing.T) { type scenario struct { @@ -45,7 +45,7 @@ func TestGetCommits(t *testing.T) { opts: GetCommitsOptions{RefName: "HEAD", RefForPushedStatus: &models.Branch{Name: "mybranch"}, IncludeRebaseCommits: false}, runner: oscommands.NewFakeRunner(t). ExpectGitArgs([]string{"rev-list", "refs/heads/mybranch", "^mybranch@{u}"}, "", nil). - ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:+%H%x00%at%x00%aN%x00%ae%x00%P%x00%m%x00%D%x00%s", "--abbrev=40", "--no-show-signature", "--"}, "", nil), + ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:+%H%x00%at%x00%ct%x00%aN%x00%ae%x00%P%x00%m%x00%D%x00%s", "--abbrev=40", "--no-show-signature", "--"}, "", nil), expectedCommitOpts: []models.NewCommitOpts{}, expectedError: nil, @@ -56,7 +56,7 @@ func TestGetCommits(t *testing.T) { opts: GetCommitsOptions{RefName: "refs/heads/mybranch", RefForPushedStatus: &models.Branch{Name: "mybranch"}, IncludeRebaseCommits: false}, runner: oscommands.NewFakeRunner(t). ExpectGitArgs([]string{"rev-list", "refs/heads/mybranch", "^mybranch@{u}"}, "", nil). - ExpectGitArgs([]string{"log", "refs/heads/mybranch", "--topo-order", "--oneline", "--pretty=format:+%H%x00%at%x00%aN%x00%ae%x00%P%x00%m%x00%D%x00%s", "--abbrev=40", "--no-show-signature", "--"}, "", nil), + ExpectGitArgs([]string{"log", "refs/heads/mybranch", "--topo-order", "--oneline", "--pretty=format:+%H%x00%at%x00%ct%x00%aN%x00%ae%x00%P%x00%m%x00%D%x00%s", "--abbrev=40", "--no-show-signature", "--"}, "", nil), expectedCommitOpts: []models.NewCommitOpts{}, expectedError: nil, @@ -70,7 +70,7 @@ func TestGetCommits(t *testing.T) { // here it's seeing which commits are yet to be pushed ExpectGitArgs([]string{"rev-list", "refs/heads/mybranch", "^mybranch@{u}", "^refs/remotes/origin/master", "^refs/remotes/origin/main"}, "0eea75e8c631fba6b58135697835d58ba4c18dbc\n", 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%P%x00%m%x00%D%x00%s", "--abbrev=40", "--no-show-signature", "--"}, commitsOutput, nil). + ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:+%H%x00%at%x00%ct%x00%aN%x00%ae%x00%P%x00%m%x00%D%x00%s", "--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 @@ -93,6 +93,7 @@ func TestGetCommits(t *testing.T) { AuthorName: "Jesse Duffield", AuthorEmail: "jessedduffield@gmail.com", UnixTimestamp: 1640826609, + CommitterDate: 1640826609, Parents: []string{ "b21997d6b4cbdf84b149", }, @@ -107,6 +108,7 @@ func TestGetCommits(t *testing.T) { AuthorName: "Jesse Duffield", AuthorEmail: "jessedduffield@gmail.com", UnixTimestamp: 1640824515, + CommitterDate: 1640824515, Parents: []string{ "e94e8fc5b6fab4cb755f", }, @@ -121,6 +123,7 @@ func TestGetCommits(t *testing.T) { AuthorName: "Jesse Duffield", AuthorEmail: "jessedduffield@gmail.com", UnixTimestamp: 1640823749, + CommitterDate: 1640823749, Parents: []string{ "d8084cd558925eb7c9c3", }, @@ -135,6 +138,7 @@ func TestGetCommits(t *testing.T) { AuthorName: "Jesse Duffield", AuthorEmail: "jessedduffield@gmail.com", UnixTimestamp: 1640821426, + CommitterDate: 1640821426, Parents: []string{ "65f910ebd85283b5cce9", }, @@ -149,6 +153,7 @@ func TestGetCommits(t *testing.T) { AuthorName: "Jesse Duffield", AuthorEmail: "jessedduffield@gmail.com", UnixTimestamp: 1640821275, + CommitterDate: 1640821275, Parents: []string{ "26c07b1ab33860a1a759", }, @@ -163,6 +168,7 @@ func TestGetCommits(t *testing.T) { AuthorName: "Jesse Duffield", AuthorEmail: "jessedduffield@gmail.com", UnixTimestamp: 1640750752, + CommitterDate: 1640750752, Parents: []string{ "3d4470a6c072208722e5", }, @@ -177,6 +183,7 @@ func TestGetCommits(t *testing.T) { AuthorName: "Jesse Duffield", AuthorEmail: "jessedduffield@gmail.com", UnixTimestamp: 1640748818, + CommitterDate: 1640748818, Parents: []string{ "053a66a7be3da43aacdc", }, @@ -191,6 +198,7 @@ func TestGetCommits(t *testing.T) { AuthorName: "Jesse Duffield", AuthorEmail: "jessedduffield@gmail.com", UnixTimestamp: 1640739815, + CommitterDate: 1640739815, Parents: []string{ "985fe482e806b172aea4", }, @@ -207,7 +215,7 @@ func TestGetCommits(t *testing.T) { // here it's seeing which commits are yet to be pushed ExpectGitArgs([]string{"rev-list", "refs/heads/mybranch", "^mybranch@{u}"}, "0eea75e8c631fba6b58135697835d58ba4c18dbc\n", 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%P%x00%m%x00%D%x00%s", "--abbrev=40", "--no-show-signature", "--"}, singleCommitOutput, nil). + ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:+%H%x00%at%x00%ct%x00%aN%x00%ae%x00%P%x00%m%x00%D%x00%s", "--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")). @@ -227,6 +235,7 @@ func TestGetCommits(t *testing.T) { AuthorName: "Jesse Duffield", AuthorEmail: "jessedduffield@gmail.com", UnixTimestamp: 1640826609, + CommitterDate: 1640826609, Parents: []string{ "b21997d6b4cbdf84b149", }, @@ -243,7 +252,7 @@ func TestGetCommits(t *testing.T) { // here it's seeing which commits are yet to be pushed ExpectGitArgs([]string{"rev-list", "refs/heads/mybranch", "^mybranch@{u}", "^refs/remotes/origin/master", "^refs/remotes/origin/develop", "^refs/remotes/origin/1.0-hotfixes"}, "0eea75e8c631fba6b58135697835d58ba4c18dbc\n", 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%P%x00%m%x00%D%x00%s", "--abbrev=40", "--no-show-signature", "--"}, singleCommitOutput, nil). + ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:+%H%x00%at%x00%ct%x00%aN%x00%ae%x00%P%x00%m%x00%D%x00%s", "--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")). @@ -265,6 +274,7 @@ func TestGetCommits(t *testing.T) { AuthorName: "Jesse Duffield", AuthorEmail: "jessedduffield@gmail.com", UnixTimestamp: 1640826609, + CommitterDate: 1640826609, Parents: []string{ "b21997d6b4cbdf84b149", }, @@ -278,7 +288,7 @@ func TestGetCommits(t *testing.T) { opts: GetCommitsOptions{RefName: "HEAD", RefForPushedStatus: &models.Branch{Name: "mybranch"}, IncludeRebaseCommits: false}, runner: oscommands.NewFakeRunner(t). ExpectGitArgs([]string{"rev-list", "refs/heads/mybranch", "^mybranch@{u}"}, "", nil). - ExpectGitArgs([]string{"log", "HEAD", "--oneline", "--pretty=format:+%H%x00%at%x00%aN%x00%ae%x00%P%x00%m%x00%D%x00%s", "--abbrev=40", "--no-show-signature", "--"}, "", nil), + ExpectGitArgs([]string{"log", "HEAD", "--oneline", "--pretty=format:+%H%x00%at%x00%ct%x00%aN%x00%ae%x00%P%x00%m%x00%D%x00%s", "--abbrev=40", "--no-show-signature", "--"}, "", nil), expectedCommitOpts: []models.NewCommitOpts{}, expectedError: nil, @@ -289,7 +299,7 @@ func TestGetCommits(t *testing.T) { opts: GetCommitsOptions{RefName: "HEAD", RefForPushedStatus: &models.Branch{Name: "mybranch"}, FilterPath: "src"}, runner: oscommands.NewFakeRunner(t). ExpectGitArgs([]string{"rev-list", "refs/heads/mybranch", "^mybranch@{u}"}, "", nil). - ExpectGitArgs([]string{"log", "HEAD", "--oneline", "--pretty=format:+%H%x00%at%x00%aN%x00%ae%x00%P%x00%m%x00%D%x00%s", "--abbrev=40", "--follow", "--name-status", "--no-show-signature", "--", "src"}, "", nil), + ExpectGitArgs([]string{"log", "HEAD", "--oneline", "--pretty=format:+%H%x00%at%x00%ct%x00%aN%x00%ae%x00%P%x00%m%x00%D%x00%s", "--abbrev=40", "--follow", "--name-status", "--no-show-signature", "--", "src"}, "", nil), expectedCommitOpts: []models.NewCommitOpts{}, expectedError: nil, @@ -609,7 +619,7 @@ func TestCommitLoader_extractCommitFromLine(t *testing.T) { }{ { testName: "normal commit line with all fields", - line: "0eea75e8c631fba6b58135697835d58ba4c18dbc\x001640826609\x00Jesse Duffield\x00jessedduffield@gmail.com\x00b21997d6b4cbdf84b149\x00>\x00HEAD -> better-tests\x00better typing for rebase mode", + line: "0eea75e8c631fba6b58135697835d58ba4c18dbc\x001640826609\x001640826609\x00Jesse Duffield\x00jessedduffield@gmail.com\x00b21997d6b4cbdf84b149\x00>\x00HEAD -> better-tests\x00better typing for rebase mode", showDivergence: false, expectedCommit: models.NewCommit(hashPool, models.NewCommitOpts{ Hash: "0eea75e8c631fba6b58135697835d58ba4c18dbc", @@ -617,6 +627,7 @@ func TestCommitLoader_extractCommitFromLine(t *testing.T) { Tags: nil, ExtraInfo: "(HEAD -> better-tests)", UnixTimestamp: 1640826609, + CommitterDate: 1640826609, AuthorName: "Jesse Duffield", AuthorEmail: "jessedduffield@gmail.com", Parents: []string{"b21997d6b4cbdf84b149"}, @@ -625,7 +636,7 @@ func TestCommitLoader_extractCommitFromLine(t *testing.T) { }, { testName: "normal commit line with left divergence", - line: "hash123\x001234567890\x00John Doe\x00john@example.com\x00parent1 parent2\x00<\x00origin/main\x00commit message", + line: "hash123\x001234567890\x001234567890\x00John Doe\x00john@example.com\x00parent1 parent2\x00<\x00origin/main\x00commit message", showDivergence: true, expectedCommit: models.NewCommit(hashPool, models.NewCommitOpts{ Hash: "hash123", @@ -633,6 +644,7 @@ func TestCommitLoader_extractCommitFromLine(t *testing.T) { Tags: nil, ExtraInfo: "(origin/main)", UnixTimestamp: 1234567890, + CommitterDate: 1234567890, AuthorName: "John Doe", AuthorEmail: "john@example.com", Parents: []string{"parent1", "parent2"}, @@ -641,7 +653,7 @@ func TestCommitLoader_extractCommitFromLine(t *testing.T) { }, { testName: "commit line with tags in extraInfo", - line: "abc123\x001640000000\x00Jane Smith\x00jane@example.com\x00parenthash\x00>\x00tag: v1.0, tag: release\x00tagged release", + line: "abc123\x001640000000\x001640000000\x00Jane Smith\x00jane@example.com\x00parenthash\x00>\x00tag: v1.0, tag: release\x00tagged release", showDivergence: true, expectedCommit: models.NewCommit(hashPool, models.NewCommitOpts{ Hash: "abc123", @@ -649,6 +661,7 @@ func TestCommitLoader_extractCommitFromLine(t *testing.T) { Tags: []string{"v1.0", "release"}, ExtraInfo: "(tag: v1.0, tag: release)", UnixTimestamp: 1640000000, + CommitterDate: 1640000000, AuthorName: "Jane Smith", AuthorEmail: "jane@example.com", Parents: []string{"parenthash"}, @@ -657,7 +670,7 @@ func TestCommitLoader_extractCommitFromLine(t *testing.T) { }, { testName: "commit line with empty extraInfo", - line: "def456\x001640000000\x00Bob Wilson\x00bob@example.com\x00parenthash\x00>\x00\x00simple commit", + line: "def456\x001640000000\x001640000000\x00Bob Wilson\x00bob@example.com\x00parenthash\x00>\x00\x00simple commit", showDivergence: true, expectedCommit: models.NewCommit(hashPool, models.NewCommitOpts{ Hash: "def456", @@ -665,6 +678,7 @@ func TestCommitLoader_extractCommitFromLine(t *testing.T) { Tags: nil, ExtraInfo: "", UnixTimestamp: 1640000000, + CommitterDate: 1640000000, AuthorName: "Bob Wilson", AuthorEmail: "bob@example.com", Parents: []string{"parenthash"}, @@ -673,7 +687,7 @@ func TestCommitLoader_extractCommitFromLine(t *testing.T) { }, { testName: "commit line with no parents (root commit)", - line: "root123\x001640000000\x00Alice Cooper\x00alice@example.com\x00\x00>\x00\x00initial commit", + line: "root123\x001640000000\x001640000000\x00Alice Cooper\x00alice@example.com\x00\x00>\x00\x00initial commit", showDivergence: true, expectedCommit: models.NewCommit(hashPool, models.NewCommitOpts{ Hash: "root123", @@ -681,6 +695,7 @@ func TestCommitLoader_extractCommitFromLine(t *testing.T) { Tags: nil, ExtraInfo: "", UnixTimestamp: 1640000000, + CommitterDate: 1640000000, AuthorName: "Alice Cooper", AuthorEmail: "alice@example.com", Parents: nil, @@ -689,31 +704,31 @@ func TestCommitLoader_extractCommitFromLine(t *testing.T) { }, { testName: "malformed line with only 3 fields", - line: "hash\x00timestamp\x00author", + line: "hash\x00timestamp\x00timestamp\x00author", showDivergence: false, expectedCommit: nil, }, { testName: "malformed line with only 4 fields", - line: "hash\x00timestamp\x00author\x00email", + line: "hash\x00timestamp\x00timestamp\x00author\x00email", showDivergence: false, expectedCommit: nil, }, { testName: "malformed line with only 5 fields", - line: "hash\x00timestamp\x00author\x00email\x00parents", + line: "hash\x00timestamp\x00timestamp\x00author\x00email\x00parents", showDivergence: false, expectedCommit: nil, }, { testName: "malformed line with only 6 fields", - line: "hash\x00timestamp\x00author\x00email\x00parents\x00<", + line: "hash\x00timestamp\x00timestamp\x00author\x00email\x00parents\x00<", showDivergence: true, expectedCommit: nil, }, { testName: "minimal valid line with 7 fields (no message)", - line: "hash\x00timestamp\x00author\x00email\x00parents\x00>\x00extraInfo", + line: "hash\x00timestamp\x00timestamp\x00author\x00email\x00parents\x00>\x00extraInfo", showDivergence: true, expectedCommit: models.NewCommit(hashPool, models.NewCommitOpts{ Hash: "hash", @@ -721,6 +736,7 @@ func TestCommitLoader_extractCommitFromLine(t *testing.T) { Tags: nil, ExtraInfo: "(extraInfo)", UnixTimestamp: 0, + CommitterDate: 0, AuthorName: "author", AuthorEmail: "email", Parents: []string{"parents"}, @@ -729,7 +745,7 @@ func TestCommitLoader_extractCommitFromLine(t *testing.T) { }, { testName: "minimal valid line with 7 fields (empty extraInfo)", - line: "hash\x00timestamp\x00author\x00email\x00parents\x00>\x00", + line: "hash\x00timestamp\x00timestamp\x00author\x00email\x00parents\x00>\x00", showDivergence: true, expectedCommit: models.NewCommit(hashPool, models.NewCommitOpts{ Hash: "hash", @@ -737,6 +753,7 @@ func TestCommitLoader_extractCommitFromLine(t *testing.T) { Tags: nil, ExtraInfo: "", UnixTimestamp: 0, + CommitterDate: 0, AuthorName: "author", AuthorEmail: "email", Parents: []string{"parents"}, @@ -745,7 +762,7 @@ func TestCommitLoader_extractCommitFromLine(t *testing.T) { }, { testName: "valid line with 8 fields (complete)", - line: "hash\x00timestamp\x00author\x00email\x00parents\x00<\x00extraInfo\x00message", + line: "hash\x00timestamp\x00timestamp\x00author\x00email\x00parents\x00<\x00extraInfo\x00message", showDivergence: true, expectedCommit: models.NewCommit(hashPool, models.NewCommitOpts{ Hash: "hash", @@ -753,6 +770,7 @@ func TestCommitLoader_extractCommitFromLine(t *testing.T) { Tags: nil, ExtraInfo: "(extraInfo)", UnixTimestamp: 0, + CommitterDate: 0, AuthorName: "author", AuthorEmail: "email", Parents: []string{"parents"}, @@ -767,7 +785,7 @@ func TestCommitLoader_extractCommitFromLine(t *testing.T) { }, { testName: "line with special characters in commit message", - line: "special123\x001640000000\x00Dev User\x00dev@example.com\x00parenthash\x00>\x00\x00fix: handle \x00 null bytes and 'quotes'", + line: "special123\x001640000000\x001640000000\x00Dev User\x00dev@example.com\x00parenthash\x00>\x00\x00fix: handle \x00 null bytes and 'quotes'", showDivergence: true, expectedCommit: models.NewCommit(hashPool, models.NewCommitOpts{ Hash: "special123", @@ -775,6 +793,7 @@ func TestCommitLoader_extractCommitFromLine(t *testing.T) { Tags: nil, ExtraInfo: "", UnixTimestamp: 1640000000, + CommitterDate: 1640000000, AuthorName: "Dev User", AuthorEmail: "dev@example.com", Parents: []string{"parenthash"}, diff --git a/pkg/commands/git_commands/reflog_commit_loader.go b/pkg/commands/git_commands/reflog_commit_loader.go index 053fc45984b..076cd6a94c6 100644 --- a/pkg/commands/git_commands/reflog_commit_loader.go +++ b/pkg/commands/git_commands/reflog_commit_loader.go @@ -28,7 +28,7 @@ func (self *ReflogCommitLoader) GetReflogCommits(hashPool *utils.StringPool, las cmdArgs := NewGitCmd("log"). Config("log.showSignature=false"). Arg("-g"). - Arg("--format=+%H%x00%ct%x00%gs%x00%P"). + Arg("--format=+%H%x00%at%x00%ct%x00%gs%x00%P"). ArgIf(filterAuthor != "", "--author="+filterAuthor). ArgIf(filterPath != "", "--follow", "--name-status", "--", filterPath). ToArgv() @@ -63,18 +63,19 @@ func (self *ReflogCommitLoader) GetReflogCommits(hashPool *utils.StringPool, las } func (self *ReflogCommitLoader) sameReflogCommit(a *models.Commit, b *models.Commit) bool { - return a.Hash() == b.Hash() && a.UnixTimestamp == b.UnixTimestamp && a.Name == b.Name + return a.Hash() == b.Hash() && a.UnixTimestamp == b.UnixTimestamp && a.CommitterDate == b.CommitterDate && a.Name == b.Name } func (self *ReflogCommitLoader) parseLine(hashPool *utils.StringPool, line string) (*models.Commit, bool) { - fields := strings.SplitN(line, "\x00", 4) - if len(fields) <= 3 { + fields := strings.SplitN(line, "\x00", 5) + if len(fields) <= 4 { return nil, false } unixTimestamp, _ := strconv.Atoi(fields[1]) + committerDate, _ := strconv.Atoi(fields[2]) - parentHashes := fields[3] + parentHashes := fields[4] parents := []string{} if len(parentHashes) > 0 { parents = strings.Split(parentHashes, " ") @@ -82,8 +83,9 @@ func (self *ReflogCommitLoader) parseLine(hashPool *utils.StringPool, line strin return models.NewCommit(hashPool, models.NewCommitOpts{ Hash: fields[0], - Name: fields[2], + Name: fields[3], UnixTimestamp: int64(unixTimestamp), + CommitterDate: int64(committerDate), Status: models.StatusReflog, Parents: parents, }), true diff --git a/pkg/commands/git_commands/reflog_commit_loader_test.go b/pkg/commands/git_commands/reflog_commit_loader_test.go index 839533f3f0d..a62b05ae922 100644 --- a/pkg/commands/git_commands/reflog_commit_loader_test.go +++ b/pkg/commands/git_commands/reflog_commit_loader_test.go @@ -14,11 +14,11 @@ import ( "github.com/stretchr/testify/assert" ) -var reflogOutput = strings.ReplaceAll(`+c3c4b66b64c97ffeecde|1643150483|checkout: moving from A to B|51baa8c1 -+c3c4b66b64c97ffeecde|1643150483|checkout: moving from B to A|51baa8c1 -+c3c4b66b64c97ffeecde|1643150483|checkout: moving from A to B|51baa8c1 -+c3c4b66b64c97ffeecde|1643150483|checkout: moving from master to A|51baa8c1 -+f4ddf2f0d4be4ccc7efa|1643149435|checkout: moving from A to master|51baa8c1 +var reflogOutput = strings.ReplaceAll(`+c3c4b66b64c97ffeecde|1643150483|1643150483|checkout: moving from A to B|51baa8c1 ++c3c4b66b64c97ffeecde|1643150483|1643150483|checkout: moving from B to A|51baa8c1 ++c3c4b66b64c97ffeecde|1643150483|1643150483|checkout: moving from A to B|51baa8c1 ++c3c4b66b64c97ffeecde|1643150483|1643150483|checkout: moving from master to A|51baa8c1 ++f4ddf2f0d4be4ccc7efa|1643149435|1643149435|checkout: moving from A to master|51baa8c1 `, "|", "\x00") func TestGetReflogCommits(t *testing.T) { @@ -39,7 +39,7 @@ func TestGetReflogCommits(t *testing.T) { { testName: "no reflog entries", runner: oscommands.NewFakeRunner(t). - ExpectGitArgs([]string{"-c", "log.showSignature=false", "log", "-g", "--format=+%H%x00%ct%x00%gs%x00%P"}, "", nil), + ExpectGitArgs([]string{"-c", "log.showSignature=false", "log", "-g", "--format=+%H%x00%at%x00%ct%x00%gs%x00%P"}, "", nil), lastReflogCommit: nil, expectedCommitOpts: []models.NewCommitOpts{}, @@ -49,7 +49,7 @@ func TestGetReflogCommits(t *testing.T) { { testName: "some reflog entries", runner: oscommands.NewFakeRunner(t). - ExpectGitArgs([]string{"-c", "log.showSignature=false", "log", "-g", "--format=+%H%x00%ct%x00%gs%x00%P"}, reflogOutput, nil), + ExpectGitArgs([]string{"-c", "log.showSignature=false", "log", "-g", "--format=+%H%x00%at%x00%ct%x00%gs%x00%P"}, reflogOutput, nil), lastReflogCommit: nil, expectedCommitOpts: []models.NewCommitOpts{ @@ -58,6 +58,7 @@ func TestGetReflogCommits(t *testing.T) { Name: "checkout: moving from A to B", Status: models.StatusReflog, UnixTimestamp: 1643150483, + CommitterDate: 1643150483, Parents: []string{"51baa8c1"}, }, { @@ -65,6 +66,7 @@ func TestGetReflogCommits(t *testing.T) { Name: "checkout: moving from B to A", Status: models.StatusReflog, UnixTimestamp: 1643150483, + CommitterDate: 1643150483, Parents: []string{"51baa8c1"}, }, { @@ -72,6 +74,7 @@ func TestGetReflogCommits(t *testing.T) { Name: "checkout: moving from A to B", Status: models.StatusReflog, UnixTimestamp: 1643150483, + CommitterDate: 1643150483, Parents: []string{"51baa8c1"}, }, { @@ -79,6 +82,7 @@ func TestGetReflogCommits(t *testing.T) { Name: "checkout: moving from master to A", Status: models.StatusReflog, UnixTimestamp: 1643150483, + CommitterDate: 1643150483, Parents: []string{"51baa8c1"}, }, { @@ -86,6 +90,7 @@ func TestGetReflogCommits(t *testing.T) { Name: "checkout: moving from A to master", Status: models.StatusReflog, UnixTimestamp: 1643149435, + CommitterDate: 1643149435, Parents: []string{"51baa8c1"}, }, }, @@ -95,13 +100,14 @@ func TestGetReflogCommits(t *testing.T) { { testName: "some reflog entries where last commit is given", runner: oscommands.NewFakeRunner(t). - ExpectGitArgs([]string{"-c", "log.showSignature=false", "log", "-g", "--format=+%H%x00%ct%x00%gs%x00%P"}, reflogOutput, nil), + ExpectGitArgs([]string{"-c", "log.showSignature=false", "log", "-g", "--format=+%H%x00%at%x00%ct%x00%gs%x00%P"}, reflogOutput, nil), lastReflogCommit: models.NewCommit(hashPool, models.NewCommitOpts{ Hash: "c3c4b66b64c97ffeecde", Name: "checkout: moving from B to A", Status: models.StatusReflog, UnixTimestamp: 1643150483, + CommitterDate: 1643150483, Parents: []string{"51baa8c1"}, }), expectedCommitOpts: []models.NewCommitOpts{ @@ -110,6 +116,7 @@ func TestGetReflogCommits(t *testing.T) { Name: "checkout: moving from A to B", Status: models.StatusReflog, UnixTimestamp: 1643150483, + CommitterDate: 1643150483, Parents: []string{"51baa8c1"}, }, }, @@ -119,13 +126,14 @@ func TestGetReflogCommits(t *testing.T) { { testName: "when passing filterPath", runner: oscommands.NewFakeRunner(t). - ExpectGitArgs([]string{"-c", "log.showSignature=false", "log", "-g", "--format=+%H%x00%ct%x00%gs%x00%P", "--follow", "--name-status", "--", "path"}, reflogOutput, nil), + ExpectGitArgs([]string{"-c", "log.showSignature=false", "log", "-g", "--format=+%H%x00%at%x00%ct%x00%gs%x00%P", "--follow", "--name-status", "--", "path"}, reflogOutput, nil), lastReflogCommit: models.NewCommit(hashPool, models.NewCommitOpts{ Hash: "c3c4b66b64c97ffeecde", Name: "checkout: moving from B to A", Status: models.StatusReflog, UnixTimestamp: 1643150483, + CommitterDate: 1643150483, Parents: []string{"51baa8c1"}, }), filterPath: "path", @@ -135,6 +143,7 @@ func TestGetReflogCommits(t *testing.T) { Name: "checkout: moving from A to B", Status: models.StatusReflog, UnixTimestamp: 1643150483, + CommitterDate: 1643150483, Parents: []string{"51baa8c1"}, }, }, @@ -144,13 +153,14 @@ func TestGetReflogCommits(t *testing.T) { { testName: "when passing filterAuthor", runner: oscommands.NewFakeRunner(t). - ExpectGitArgs([]string{"-c", "log.showSignature=false", "log", "-g", "--format=+%H%x00%ct%x00%gs%x00%P", "--author=John Doe "}, reflogOutput, nil), + ExpectGitArgs([]string{"-c", "log.showSignature=false", "log", "-g", "--format=+%H%x00%at%x00%ct%x00%gs%x00%P", "--author=John Doe "}, reflogOutput, nil), lastReflogCommit: models.NewCommit(hashPool, models.NewCommitOpts{ Hash: "c3c4b66b64c97ffeecde", Name: "checkout: moving from B to A", Status: models.StatusReflog, UnixTimestamp: 1643150483, + CommitterDate: 1643150483, Parents: []string{"51baa8c1"}, }), filterAuthor: "John Doe ", @@ -160,6 +170,7 @@ func TestGetReflogCommits(t *testing.T) { Name: "checkout: moving from A to B", Status: models.StatusReflog, UnixTimestamp: 1643150483, + CommitterDate: 1643150483, Parents: []string{"51baa8c1"}, }, }, @@ -169,7 +180,7 @@ func TestGetReflogCommits(t *testing.T) { { testName: "when command returns error", runner: oscommands.NewFakeRunner(t). - ExpectGitArgs([]string{"-c", "log.showSignature=false", "log", "-g", "--format=+%H%x00%ct%x00%gs%x00%P"}, "", errors.New("haha")), + ExpectGitArgs([]string{"-c", "log.showSignature=false", "log", "-g", "--format=+%H%x00%at%x00%ct%x00%gs%x00%P"}, "", errors.New("haha")), lastReflogCommit: nil, filterPath: "", diff --git a/pkg/commands/models/commit.go b/pkg/commands/models/commit.go index 8dfec4d4b75..0e5fd066597 100644 --- a/pkg/commands/models/commit.go +++ b/pkg/commands/models/commit.go @@ -50,6 +50,7 @@ type Commit struct { AuthorName string // something like 'Jesse Duffield' AuthorEmail string // something like 'jessedduffield@gmail.com' UnixTimestamp int64 + CommitterDate int64 // Hashes of parent commits (will be multiple if it's a merge commit) parents []*string @@ -73,6 +74,7 @@ type NewCommitOpts struct { AuthorName string AuthorEmail string UnixTimestamp int64 + CommitterDate int64 Divergence Divergence Parents []string } @@ -88,6 +90,7 @@ func NewCommit(hashPool *utils.StringPool, opts NewCommitOpts) *Commit { AuthorName: opts.AuthorName, AuthorEmail: opts.AuthorEmail, UnixTimestamp: opts.UnixTimestamp, + CommitterDate: opts.CommitterDate, Divergence: opts.Divergence, parents: lo.Map(opts.Parents, func(s string, _ int) *string { return hashPool.Add(s) }), } diff --git a/pkg/gui/services/custom_commands/models.go b/pkg/gui/services/custom_commands/models.go index e79963458f9..24dd0420535 100644 --- a/pkg/gui/services/custom_commands/models.go +++ b/pkg/gui/services/custom_commands/models.go @@ -24,6 +24,7 @@ type Commit struct { AuthorName string AuthorEmail string UnixTimestamp int64 + CommitterDate int64 Divergence models.Divergence Parents []string } diff --git a/pkg/gui/services/custom_commands/session_state_loader.go b/pkg/gui/services/custom_commands/session_state_loader.go index b2a2e39c9fb..d4698a6b9f5 100644 --- a/pkg/gui/services/custom_commands/session_state_loader.go +++ b/pkg/gui/services/custom_commands/session_state_loader.go @@ -36,6 +36,7 @@ func commitShimFromModelCommit(commit *models.Commit) *Commit { AuthorName: commit.AuthorName, AuthorEmail: commit.AuthorEmail, UnixTimestamp: commit.UnixTimestamp, + CommitterDate: commit.CommitterDate, Divergence: commit.Divergence, Parents: commit.Parents(), } From 8e3203094f3c16f7d29d058c24eb81f25760104e Mon Sep 17 00:00:00 2001 From: Dawid Pietrykowski Date: Fri, 12 Dec 2025 13:24:47 +0100 Subject: [PATCH 2/3] Use commiter date in full commit view --- pkg/gui/presentation/commits.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/gui/presentation/commits.go b/pkg/gui/presentation/commits.go index a5799bcb333..aee0320788f 100644 --- a/pkg/gui/presentation/commits.go +++ b/pkg/gui/presentation/commits.go @@ -379,7 +379,7 @@ func displayCommit( descriptionString := "" if fullDescription { descriptionString = style.FgBlue.Sprint( - utils.UnixToDateSmart(now, commit.UnixTimestamp, timeFormat, shortTimeFormat), + utils.UnixToDateSmart(now, commit.CommitterDate, timeFormat, shortTimeFormat), ) } From 61e138c365bdd5695250f3cb014f404ce020656e Mon Sep 17 00:00:00 2001 From: Dawid Pietrykowski Date: Fri, 12 Dec 2025 13:47:05 +0100 Subject: [PATCH 3/3] Add `commitDateSource` option --- docs-master/Config.md | 4 ++++ pkg/config/app_config_test.go | 4 ++++ pkg/config/user_config.go | 4 ++++ pkg/config/user_config_validation.go | 4 ++++ pkg/gui/presentation/commits.go | 10 +++++++++- schema-master/config.json | 9 +++++++++ 6 files changed, 34 insertions(+), 1 deletion(-) diff --git a/docs-master/Config.md b/docs-master/Config.md index 21709da755a..06550995dde 100644 --- a/docs-master/Config.md +++ b/docs-master/Config.md @@ -321,6 +321,10 @@ gui: # is already active, go to next tab instead switchTabsWithPanelJumpKeys: false + # Commit list date source. + # One of 'author' (default) | 'committer' + commitDateSource: author + # Config relating to git git: # Array of pagers. Each entry has the following format: diff --git a/pkg/config/app_config_test.go b/pkg/config/app_config_test.go index 1109256a9d3..0c7f7ec6fa7 100644 --- a/pkg/config/app_config_test.go +++ b/pkg/config/app_config_test.go @@ -599,6 +599,10 @@ gui: # If true, when using the panel jump keys (default 1 through 5) and target panel is already active, go to next tab instead switchTabsWithPanelJumpKeys: false + # Commit list date source. + # One of 'author' (default) | 'committer' + commitDateSource: author + # Config relating to git git: # See https://github.com/jesseduffield/lazygit/blob/master/docs/Custom_Pagers.md diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go index bcb5c2f6f85..c7e89c99cb6 100644 --- a/pkg/config/user_config.go +++ b/pkg/config/user_config.go @@ -196,6 +196,9 @@ type GuiConfig struct { SwitchToFilesAfterStashApply bool `yaml:"switchToFilesAfterStashApply"` // If true, when using the panel jump keys (default 1 through 5) and target panel is already active, go to next tab instead SwitchTabsWithPanelJumpKeys bool `yaml:"switchTabsWithPanelJumpKeys"` + // Commit list date source. + // One of 'author' (default) | 'committer' + CommitDateSource string `yaml:"commitDateSource" jsonschema:"enum=author,enum=committer"` } func (c *GuiConfig) UseFuzzySearch() bool { @@ -820,6 +823,7 @@ func GetDefaultConfig() *UserConfig { SwitchToFilesAfterStashPop: true, SwitchToFilesAfterStashApply: true, SwitchTabsWithPanelJumpKeys: false, + CommitDateSource: "author", }, Git: GitConfig{ Commit: CommitConfig{ diff --git a/pkg/config/user_config_validation.go b/pkg/config/user_config_validation.go index 16be84521b5..3976a6a2a89 100644 --- a/pkg/config/user_config_validation.go +++ b/pkg/config/user_config_validation.go @@ -11,6 +11,10 @@ import ( ) func (config *UserConfig) Validate() error { + if err := validateEnum("gui.commitDateSource", config.Gui.CommitDateSource, + []string{"author", "committer"}); err != nil { + return err + } if err := validateEnum("gui.statusPanelView", config.Gui.StatusPanelView, []string{"dashboard", "allBranchesLog"}); err != nil { return err diff --git a/pkg/gui/presentation/commits.go b/pkg/gui/presentation/commits.go index aee0320788f..febc30f35fb 100644 --- a/pkg/gui/presentation/commits.go +++ b/pkg/gui/presentation/commits.go @@ -378,8 +378,16 @@ func displayCommit( descriptionString := "" if fullDescription { + timestamp := commit.UnixTimestamp + switch common.UserConfig().Gui.CommitDateSource { + case "committer": + timestamp = commit.CommitterDate + case "author": + timestamp = commit.UnixTimestamp + default: + } descriptionString = style.FgBlue.Sprint( - utils.UnixToDateSmart(now, commit.CommitterDate, timeFormat, shortTimeFormat), + utils.UnixToDateSmart(now, timestamp, timeFormat, shortTimeFormat), ) } diff --git a/schema-master/config.json b/schema-master/config.json index c371f14e9f7..c5388ebe199 100644 --- a/schema-master/config.json +++ b/schema-master/config.json @@ -790,6 +790,15 @@ "type": "boolean", "description": "If true, when using the panel jump keys (default 1 through 5) and target panel is already active, go to next tab instead", "default": false + }, + "commitDateSource": { + "type": "string", + "enum": [ + "author", + "committer" + ], + "description": "Commit list date source.\nOne of 'author' (default) | 'committer'", + "default": "author" } }, "additionalProperties": false,