diff --git a/private/pkg/git/cloner.go b/private/pkg/git/cloner.go index eb230ea2e8..b42fc8e3cf 100644 --- a/private/pkg/git/cloner.go +++ b/private/pkg/git/cloner.go @@ -20,6 +20,7 @@ import ( "errors" "fmt" "log/slog" + "slices" "strconv" "strings" @@ -37,6 +38,21 @@ type cloner struct { options ClonerOptions } +// gitConfigNoAutoMaintenanceArgs prevents background Git tasks from racing with +// storage.Copy over files in the cloned .git directory. +var gitConfigNoAutoMaintenanceArgs = []string{ + "-c", + "maintenance.auto=false", + "-c", + "maintenance.autoDetach=false", + "-c", + "gc.auto=0", + "-c", + "gc.autoDetach=false", + "-c", + "fetch.writeCommitGraph=false", +} + func newCloner( logger *slog.Logger, storageosProvider storageos.Provider, @@ -118,15 +134,15 @@ func (c *cloner) CloneToBucket( return newGitCommandError(err, buffer) } - var gitConfigAuthArgs []string + gitConfigArgs := slices.Clone(gitConfigNoAutoMaintenanceArgs) if strings.HasPrefix(url, "https://") { - // These extraArgs MUST be first, as the -c flag potentially produced - // is only a flag on the parent git command, not on git fetch. + // These extraArgs MUST be before the sub-command, as the -c flag potentially + // produced is only a flag on the parent git command, not on git fetch. extraArgs, err := c.getArgsForHTTPSCommand(envContainer) if err != nil { return err } - gitConfigAuthArgs = append(gitConfigAuthArgs, extraArgs...) + gitConfigArgs = append(gitConfigArgs, extraArgs...) } if strings.HasPrefix(url, "ssh://") { @@ -138,7 +154,7 @@ func (c *cloner) CloneToBucket( // Build the args for the fetch command. fetchArgs := []string{} - fetchArgs = append(fetchArgs, gitConfigAuthArgs...) + fetchArgs = append(fetchArgs, gitConfigArgs...) fetchArgs = append( fetchArgs, "fetch", @@ -191,7 +207,7 @@ func (c *cloner) CloneToBucket( if err := xexec.Run( ctx, "git", - xexec.WithArgs(append(gitConfigAuthArgs, "sparse-checkout", "set", options.SubDir)...), + xexec.WithArgs(append(gitConfigArgs, "sparse-checkout", "set", options.SubDir)...), xexec.WithEnv(app.Environ(envContainer)), xexec.WithStderr(buffer), xexec.WithDir(baseDir.Path()), @@ -206,7 +222,7 @@ func (c *cloner) CloneToBucket( if err := xexec.Run( ctx, "git", - xexec.WithArgs(append(gitConfigAuthArgs, "checkout", "--force", "FETCH_HEAD")...), + xexec.WithArgs(append(gitConfigArgs, "checkout", "--force", "FETCH_HEAD")...), xexec.WithEnv(app.Environ(envContainer)), xexec.WithStderr(buffer), xexec.WithDir(baseDir.Path()), @@ -220,7 +236,7 @@ func (c *cloner) CloneToBucket( if err := xexec.Run( ctx, "git", - xexec.WithArgs(append(gitConfigAuthArgs, "checkout", "--force", checkoutRef)...), + xexec.WithArgs(append(gitConfigArgs, "checkout", "--force", checkoutRef)...), xexec.WithEnv(app.Environ(envContainer)), xexec.WithStderr(buffer), xexec.WithDir(baseDir.Path()), @@ -235,7 +251,7 @@ func (c *cloner) CloneToBucket( ctx, "git", xexec.WithArgs(append( - gitConfigAuthArgs, + gitConfigArgs, "submodule", "update", "--init", diff --git a/private/pkg/git/git_test.go b/private/pkg/git/git_test.go index 8ad5013038..0a2f8dde59 100644 --- a/private/pkg/git/git_test.go +++ b/private/pkg/git/git_test.go @@ -16,6 +16,7 @@ package git import ( "context" + "encoding/json" "errors" "fmt" "io/fs" @@ -24,6 +25,7 @@ import ( "os" "os/exec" "path/filepath" + "slices" "strings" "testing" @@ -70,6 +72,51 @@ func TestGitCloner(t *testing.T) { assert.Equal(t, "// submodule", string(content)) }) + t.Run("auto_maintenance_disabled", func(t *testing.T) { + t.Parallel() + + tracePath := filepath.Join(t.TempDir(), "git-trace.json") + readBucketForName(ctx, t, workDir, readBucketForNameOptions{ + recurseSubmodules: true, + envOverrides: map[string]string{ + "GIT_TRACE2_EVENT": tracePath, + }, + }) + + traceData, err := os.ReadFile(tracePath) + require.NoError(t, err) + + var gitArgvs [][]string + for line := range strings.SplitSeq(string(traceData), "\n") { + if line == "" { + continue + } + var event struct { + Event string `json:"event"` + Argv []string `json:"argv"` + } + require.NoError(t, json.Unmarshal([]byte(line), &event)) + if event.Event == "start" && len(event.Argv) > 0 { + gitArgvs = append(gitArgvs, event.Argv) + } + } + + for _, subcommand := range []string{"fetch", "checkout", "submodule"} { + var found bool + for _, argv := range gitArgvs { + subcommandIndex := slices.Index(argv, subcommand) + if subcommandIndex < 0 { + continue + } + require.GreaterOrEqual(t, subcommandIndex, len(gitConfigNoAutoMaintenanceArgs)) + assert.Equal(t, gitConfigNoAutoMaintenanceArgs, argv[subcommandIndex-len(gitConfigNoAutoMaintenanceArgs):subcommandIndex]) + found = true + break + } + require.Truef(t, found, "missing git %q invocation", subcommand) + } + }) + t.Run("main", func(t *testing.T) { t.Parallel() readBucket := readBucketForName(ctx, t, workDir, readBucketForNameOptions{name: NewBranchName("main")})