From 15f8ad31e0b4cfa0aeb947c4360fe2bf73a3eb6b Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sun, 27 Jul 2025 18:38:53 +0200 Subject: [PATCH 1/3] Cleanup: use nil for empty slice --- pkg/commands/git_commands/commit_loader.go | 2 +- .../git_commands/commit_loader_test.go | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/pkg/commands/git_commands/commit_loader.go b/pkg/commands/git_commands/commit_loader.go index e8480ec1b..783975f09 100644 --- a/pkg/commands/git_commands/commit_loader.go +++ b/pkg/commands/git_commands/commit_loader.go @@ -214,7 +214,7 @@ func (self *CommitLoader) extractCommitFromLine(hashPool *utils.StringPool, line } message := split[7] - tags := []string{} + var tags []string if extraInfo != "" { extraInfoFields := strings.Split(extraInfo, ",") diff --git a/pkg/commands/git_commands/commit_loader_test.go b/pkg/commands/git_commands/commit_loader_test.go index ba0cd9d84..2f7547b85 100644 --- a/pkg/commands/git_commands/commit_loader_test.go +++ b/pkg/commands/git_commands/commit_loader_test.go @@ -86,7 +86,7 @@ func TestGetCommits(t *testing.T) { Name: "better typing for rebase mode", Status: models.StatusUnpushed, Action: models.ActionNone, - Tags: []string{}, + Tags: nil, ExtraInfo: "(HEAD -> better-tests)", AuthorName: "Jesse Duffield", AuthorEmail: "jessedduffield@gmail.com", @@ -100,7 +100,7 @@ func TestGetCommits(t *testing.T) { Name: "fix logging", Status: models.StatusPushed, Action: models.ActionNone, - Tags: []string{}, + Tags: nil, ExtraInfo: "(origin/better-tests)", AuthorName: "Jesse Duffield", AuthorEmail: "jessedduffield@gmail.com", @@ -128,7 +128,7 @@ func TestGetCommits(t *testing.T) { Name: "WIP", Status: models.StatusPushed, Action: models.ActionNone, - Tags: []string{}, + Tags: nil, ExtraInfo: "", AuthorName: "Jesse Duffield", AuthorEmail: "jessedduffield@gmail.com", @@ -142,7 +142,7 @@ func TestGetCommits(t *testing.T) { Name: "WIP", Status: models.StatusPushed, Action: models.ActionNone, - Tags: []string{}, + Tags: nil, ExtraInfo: "", AuthorName: "Jesse Duffield", AuthorEmail: "jessedduffield@gmail.com", @@ -156,7 +156,7 @@ func TestGetCommits(t *testing.T) { Name: "WIP", Status: models.StatusMerged, Action: models.ActionNone, - Tags: []string{}, + Tags: nil, ExtraInfo: "", AuthorName: "Jesse Duffield", AuthorEmail: "jessedduffield@gmail.com", @@ -170,7 +170,7 @@ func TestGetCommits(t *testing.T) { Name: "WIP", Status: models.StatusMerged, Action: models.ActionNone, - Tags: []string{}, + Tags: nil, ExtraInfo: "", AuthorName: "Jesse Duffield", AuthorEmail: "jessedduffield@gmail.com", @@ -184,7 +184,7 @@ func TestGetCommits(t *testing.T) { Name: "refactoring the config struct", Status: models.StatusMerged, Action: models.ActionNone, - Tags: []string{}, + Tags: nil, ExtraInfo: "", AuthorName: "Jesse Duffield", AuthorEmail: "jessedduffield@gmail.com", @@ -220,7 +220,7 @@ func TestGetCommits(t *testing.T) { Name: "better typing for rebase mode", Status: models.StatusUnpushed, Action: models.ActionNone, - Tags: []string{}, + Tags: nil, ExtraInfo: "(HEAD -> better-tests)", AuthorName: "Jesse Duffield", AuthorEmail: "jessedduffield@gmail.com", @@ -258,7 +258,7 @@ func TestGetCommits(t *testing.T) { Name: "better typing for rebase mode", Status: models.StatusUnpushed, Action: models.ActionNone, - Tags: []string{}, + Tags: nil, ExtraInfo: "(HEAD -> better-tests)", AuthorName: "Jesse Duffield", AuthorEmail: "jessedduffield@gmail.com", From c54232e86388d7ad9d2c3bc44f2da4317d99673e Mon Sep 17 00:00:00 2001 From: neo Date: Sun, 27 Jul 2025 18:13:32 +0200 Subject: [PATCH 2/3] Move parentHashes and divergence fields before extraInfo in git log format Like message, extraField can get very long (when there are thousands of tags on a single commit), so move it to the end; this allows us to truncate overly long lines in the output and still get all the essential fields. Co-authored-by: Stefan Haller --- pkg/commands/git_commands/commit_loader.go | 8 ++--- .../git_commands/commit_loader_test.go | 32 +++++++++---------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/pkg/commands/git_commands/commit_loader.go b/pkg/commands/git_commands/commit_loader.go index 783975f09..a0127d7da 100644 --- a/pkg/commands/git_commands/commit_loader.go +++ b/pkg/commands/git_commands/commit_loader.go @@ -206,12 +206,12 @@ func (self *CommitLoader) extractCommitFromLine(hashPool *utils.StringPool, line unixTimestamp := split[1] authorName := split[2] authorEmail := split[3] - extraInfo := strings.TrimSpace(split[4]) - parentHashes := split[5] + parentHashes := split[4] divergence := models.DivergenceNone if showDivergence { - divergence = lo.Ternary(split[6] == "<", models.DivergenceLeft, models.DivergenceRight) + divergence = lo.Ternary(split[5] == "<", models.DivergenceLeft, models.DivergenceRight) } + extraInfo := strings.TrimSpace(split[6]) message := split[7] var tags []string @@ -614,4 +614,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%D%x00%P%x00%m%x00%s` +const prettyFormat = `--pretty=format:+%H%x00%at%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 2f7547b85..1eb60cca3 100644 --- a/pkg/commands/git_commands/commit_loader_test.go +++ b/pkg/commands/git_commands/commit_loader_test.go @@ -15,16 +15,16 @@ import ( "github.com/stretchr/testify/assert" ) -var commitsOutput = strings.ReplaceAll(`+0eea75e8c631fba6b58135697835d58ba4c18dbc|1640826609|Jesse Duffield|jessedduffield@gmail.com|HEAD -> better-tests|b21997d6b4cbdf84b149|>|better typing for rebase mode -+b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164|1640824515|Jesse Duffield|jessedduffield@gmail.com|origin/better-tests|e94e8fc5b6fab4cb755f|>|fix logging -+e94e8fc5b6fab4cb755f29f1bdb3ee5e001df35c|1640823749|Jesse Duffield|jessedduffield@gmail.com|tag: 123, tag: 456|d8084cd558925eb7c9c3|>|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|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 singleCommitOutput = strings.ReplaceAll(`+0eea75e8c631fba6b58135697835d58ba4c18dbc|1640826609|Jesse Duffield|jessedduffield@gmail.com|HEAD -> better-tests|b21997d6b4cbdf84b149|>|better typing for rebase mode`, "|", "\x00") +var singleCommitOutput = strings.ReplaceAll(`+0eea75e8c631fba6b58135697835d58ba4c18dbc|1640826609|Jesse Duffield|jessedduffield@gmail.com|b21997d6b4cbdf84b149|>|HEAD -> better-tests|better typing for rebase mode`, "|", "\x00") func TestGetCommits(t *testing.T) { type scenario struct { @@ -44,7 +44,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%m%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%P%x00%m%x00%D%x00%s", "--abbrev=40", "--no-show-signature", "--"}, "", nil), expectedCommitOpts: []models.NewCommitOpts{}, expectedError: nil, @@ -55,7 +55,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%m%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%P%x00%m%x00%D%x00%s", "--abbrev=40", "--no-show-signature", "--"}, "", nil), expectedCommitOpts: []models.NewCommitOpts{}, expectedError: nil, @@ -69,7 +69,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%m%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%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 @@ -205,7 +205,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%m%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%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")). @@ -241,7 +241,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%m%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%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")). @@ -276,7 +276,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%m%x00%s", "--abbrev=40", "--no-show-signature", "--"}, "", 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), expectedCommitOpts: []models.NewCommitOpts{}, expectedError: nil, @@ -287,7 +287,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%m%x00%s", "--abbrev=40", "--follow", "--name-status", "--no-show-signature", "--", "src"}, "", 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), expectedCommitOpts: []models.NewCommitOpts{}, expectedError: nil, From 1c67093ed90ce3508c569bdad04d70dbaae37c51 Mon Sep 17 00:00:00 2001 From: neo Date: Sun, 27 Jul 2025 17:44:51 +0900 Subject: [PATCH 3/3] Fix index out of bounds panic when repository has massive tag lists Co-authored-by: Stefan Haller --- pkg/commands/git_commands/commit_loader.go | 13 +- .../git_commands/commit_loader_test.go | 202 ++++++++++++++++++ 2 files changed, 214 insertions(+), 1 deletion(-) diff --git a/pkg/commands/git_commands/commit_loader.go b/pkg/commands/git_commands/commit_loader.go index a0127d7da..e2d546c9a 100644 --- a/pkg/commands/git_commands/commit_loader.go +++ b/pkg/commands/git_commands/commit_loader.go @@ -202,6 +202,12 @@ func (self *CommitLoader) MergeRebasingCommits(hashPool *utils.StringPool, commi func (self *CommitLoader) extractCommitFromLine(hashPool *utils.StringPool, line string, showDivergence bool) *models.Commit { split := strings.SplitN(line, "\x00", 8) + // 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) + return nil + } + hash := split[0] unixTimestamp := split[1] authorName := split[2] @@ -212,7 +218,12 @@ func (self *CommitLoader) extractCommitFromLine(hashPool *utils.StringPool, line divergence = lo.Ternary(split[5] == "<", models.DivergenceLeft, models.DivergenceRight) } extraInfo := strings.TrimSpace(split[6]) - message := 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] + } var tags []string diff --git a/pkg/commands/git_commands/commit_loader_test.go b/pkg/commands/git_commands/commit_loader_test.go index 1eb60cca3..087ac1c7e 100644 --- a/pkg/commands/git_commands/commit_loader_test.go +++ b/pkg/commands/git_commands/commit_loader_test.go @@ -588,3 +588,205 @@ func TestCommitLoader_setCommitMergedStatuses(t *testing.T) { }) } } + +func TestCommitLoader_extractCommitFromLine(t *testing.T) { + common := common.NewDummyCommon() + hashPool := &utils.StringPool{} + + loader := &CommitLoader{ + Common: common, + } + + scenarios := []struct { + testName string + line string + showDivergence bool + expectedCommit *models.Commit + }{ + { + 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", + showDivergence: false, + expectedCommit: models.NewCommit(hashPool, models.NewCommitOpts{ + Hash: "0eea75e8c631fba6b58135697835d58ba4c18dbc", + Name: "better typing for rebase mode", + Tags: nil, + ExtraInfo: "(HEAD -> better-tests)", + UnixTimestamp: 1640826609, + AuthorName: "Jesse Duffield", + AuthorEmail: "jessedduffield@gmail.com", + Parents: []string{"b21997d6b4cbdf84b149"}, + Divergence: models.DivergenceNone, + }), + }, + { + testName: "normal commit line with left divergence", + line: "hash123\x001234567890\x00John Doe\x00john@example.com\x00parent1 parent2\x00<\x00origin/main\x00commit message", + showDivergence: true, + expectedCommit: models.NewCommit(hashPool, models.NewCommitOpts{ + Hash: "hash123", + Name: "commit message", + Tags: nil, + ExtraInfo: "(origin/main)", + UnixTimestamp: 1234567890, + AuthorName: "John Doe", + AuthorEmail: "john@example.com", + Parents: []string{"parent1", "parent2"}, + Divergence: models.DivergenceLeft, + }), + }, + { + testName: "commit line with tags in extraInfo", + line: "abc123\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", + Name: "tagged release", + Tags: []string{"v1.0", "release"}, + ExtraInfo: "(tag: v1.0, tag: release)", + UnixTimestamp: 1640000000, + AuthorName: "Jane Smith", + AuthorEmail: "jane@example.com", + Parents: []string{"parenthash"}, + Divergence: models.DivergenceRight, + }), + }, + { + testName: "commit line with empty extraInfo", + line: "def456\x001640000000\x00Bob Wilson\x00bob@example.com\x00parenthash\x00>\x00\x00simple commit", + showDivergence: true, + expectedCommit: models.NewCommit(hashPool, models.NewCommitOpts{ + Hash: "def456", + Name: "simple commit", + Tags: nil, + ExtraInfo: "", + UnixTimestamp: 1640000000, + AuthorName: "Bob Wilson", + AuthorEmail: "bob@example.com", + Parents: []string{"parenthash"}, + Divergence: models.DivergenceRight, + }), + }, + { + testName: "commit line with no parents (root commit)", + line: "root123\x001640000000\x00Alice Cooper\x00alice@example.com\x00\x00>\x00\x00initial commit", + showDivergence: true, + expectedCommit: models.NewCommit(hashPool, models.NewCommitOpts{ + Hash: "root123", + Name: "initial commit", + Tags: nil, + ExtraInfo: "", + UnixTimestamp: 1640000000, + AuthorName: "Alice Cooper", + AuthorEmail: "alice@example.com", + Parents: nil, + Divergence: models.DivergenceRight, + }), + }, + { + testName: "malformed line with only 3 fields", + line: "hash\x00timestamp\x00author", + showDivergence: false, + expectedCommit: nil, + }, + { + testName: "malformed line with only 4 fields", + line: "hash\x00timestamp\x00author\x00email", + showDivergence: false, + expectedCommit: nil, + }, + { + testName: "malformed line with only 5 fields", + line: "hash\x00timestamp\x00author\x00email\x00parents", + showDivergence: false, + expectedCommit: nil, + }, + { + testName: "malformed line with only 6 fields", + line: "hash\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", + showDivergence: true, + expectedCommit: models.NewCommit(hashPool, models.NewCommitOpts{ + Hash: "hash", + Name: "", + Tags: nil, + ExtraInfo: "(extraInfo)", + UnixTimestamp: 0, + AuthorName: "author", + AuthorEmail: "email", + Parents: []string{"parents"}, + Divergence: models.DivergenceRight, + }), + }, + { + testName: "minimal valid line with 7 fields (empty extraInfo)", + line: "hash\x00timestamp\x00author\x00email\x00parents\x00>\x00", + showDivergence: true, + expectedCommit: models.NewCommit(hashPool, models.NewCommitOpts{ + Hash: "hash", + Name: "", + Tags: nil, + ExtraInfo: "", + UnixTimestamp: 0, + AuthorName: "author", + AuthorEmail: "email", + Parents: []string{"parents"}, + Divergence: models.DivergenceRight, + }), + }, + { + testName: "valid line with 8 fields (complete)", + line: "hash\x00timestamp\x00author\x00email\x00parents\x00<\x00extraInfo\x00message", + showDivergence: true, + expectedCommit: models.NewCommit(hashPool, models.NewCommitOpts{ + Hash: "hash", + Name: "message", + Tags: nil, + ExtraInfo: "(extraInfo)", + UnixTimestamp: 0, + AuthorName: "author", + AuthorEmail: "email", + Parents: []string{"parents"}, + Divergence: models.DivergenceLeft, + }), + }, + { + testName: "empty line", + line: "", + showDivergence: false, + expectedCommit: nil, + }, + { + 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'", + showDivergence: true, + expectedCommit: models.NewCommit(hashPool, models.NewCommitOpts{ + Hash: "special123", + Name: "fix: handle \x00 null bytes and 'quotes'", + Tags: nil, + ExtraInfo: "", + UnixTimestamp: 1640000000, + AuthorName: "Dev User", + AuthorEmail: "dev@example.com", + Parents: []string{"parenthash"}, + Divergence: models.DivergenceRight, + }), + }, + } + + for _, scenario := range scenarios { + t.Run(scenario.testName, func(t *testing.T) { + result := loader.extractCommitFromLine(hashPool, scenario.line, scenario.showDivergence) + if scenario.expectedCommit == nil { + assert.Nil(t, result) + } else { + assert.Equal(t, scenario.expectedCommit, result) + } + }) + } +}