diff --git a/pkg/commands/git_commands/git_command_builder.go b/pkg/commands/git_commands/git_command_builder.go index 5e9c3b258..30496f453 100644 --- a/pkg/commands/git_commands/git_command_builder.go +++ b/pkg/commands/git_commands/git_command_builder.go @@ -2,6 +2,8 @@ package git_commands import ( "strings" + + "github.com/jesseduffield/lazygit/pkg/commands/oscommands" ) // convenience struct for building git commands. Especially useful when @@ -106,3 +108,30 @@ func (self *GitCommandBuilder) ToArgv() []string { func (self *GitCommandBuilder) ToString() string { return strings.Join(self.ToArgv(), " ") } + +// runGitCmdOnPaths runs `git -- `, splitting into +// multiple calls if needed to stay under the OS command-line length limit. +// Windows CreateProcess has a ~32 KB limit; we use 30 KB as a safe threshold. +func runGitCmdOnPaths(subcommand string, paths []string, cmd oscommands.ICmdObjBuilder) error { + const maxArgBytes = 30_000 + + start := 0 + for start < len(paths) { + end := start + total := 0 + for end < len(paths) { + total += len(paths[end]) + 1 // +1 for the separating space + if total > maxArgBytes && end > start { + break + } + end++ + } + if err := cmd.New(NewGitCmd(subcommand).Arg("--"). + Arg(paths[start:end]...). + ToArgv()).Run(); err != nil { + return err + } + start = end + } + return nil +} diff --git a/pkg/commands/git_commands/git_command_builder_test.go b/pkg/commands/git_commands/git_command_builder_test.go index 69d41854c..a839afb9a 100644 --- a/pkg/commands/git_commands/git_command_builder_test.go +++ b/pkg/commands/git_commands/git_command_builder_test.go @@ -1,8 +1,10 @@ package git_commands import ( + "strings" "testing" + "github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/stretchr/testify/assert" ) @@ -54,3 +56,44 @@ func TestGitCommandBuilder(t *testing.T) { assert.Equal(t, s.input, s.expected) } } + +func TestRunGitCmdOnPaths(t *testing.T) { + // Each path is 9000 bytes. Three fit within the 30 KB limit (27001 bytes + // including spaces), four do not (36002 bytes), so a four-path slice must + // be split into two calls of three and one. + longPath := func(ch string) string { return strings.Repeat(ch, 9_000) } + p1, p2, p3, p4 := longPath("a"), longPath("b"), longPath("c"), longPath("d") + + scenarios := []struct { + name string + paths []string + runner *oscommands.FakeCmdObjRunner + }{ + { + name: "empty list makes no calls", + paths: []string{}, + runner: oscommands.NewFakeRunner(t), + }, + { + name: "paths that fit in one batch make a single call", + paths: []string{p1, p2, p3}, + runner: oscommands.NewFakeRunner(t). + ExpectGitArgs(append([]string{"checkout", "--"}, p1, p2, p3), "", nil), + }, + { + name: "paths that exceed the limit are split across multiple calls", + paths: []string{p1, p2, p3, p4}, + runner: oscommands.NewFakeRunner(t). + ExpectGitArgs(append([]string{"checkout", "--"}, p1, p2, p3), "", nil). + ExpectGitArgs(append([]string{"checkout", "--"}, p4), "", nil), + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + cmd := oscommands.NewDummyCmdObjBuilder(s.runner) + assert.NoError(t, runGitCmdOnPaths("checkout", s.paths, cmd)) + s.runner.CheckForMissingCalls() + }) + } +}