diff --git a/internal/git/command_factory.go b/internal/git/command_factory.go index 24b284673d47d7f9bf82e21fe3a2b1247f412ed2..c677cdd751527e4929834e62792b7f4238305b6a 100644 --- a/internal/git/command_factory.go +++ b/internal/git/command_factory.go @@ -19,6 +19,7 @@ import ( "gitlab.com/gitlab-org/gitaly/v16/internal/git/trace2hooks" "gitlab.com/gitlab-org/gitaly/v16/internal/gitaly/config" "gitlab.com/gitlab-org/gitaly/v16/internal/gitaly/storage" + "gitlab.com/gitlab-org/gitaly/v16/internal/helper/perm" "gitlab.com/gitlab-org/gitaly/v16/internal/log" "gitlab.com/gitlab-org/gitaly/v16/internal/tracing" "gitlab.com/gitlab-org/labkit/correlation" @@ -433,6 +434,17 @@ func (cf *ExecCommandFactory) GitVersion(ctx context.Context) (Version, error) { return gitVersion, nil } +// scratchDir returns a location within Gitaly's runtime directory to spawn Git processes that do +// not operate in the context of a repository. The directory is created once if it does not exist. +func (cf *ExecCommandFactory) scratchDir() (string, error) { + dir := cf.cfg.GitScratchDir() + if err := os.Mkdir(dir, perm.PrivateDir); err != nil && !errors.Is(err, os.ErrExist) { + return "", fmt.Errorf("create scratch dir: %w", err) + } + + return dir, nil +} + // newCommand creates a new command.Command for the given git command. If a repo is given, then the // command will be run in the context of that repository. Note that this sets up arguments and // environment variables for git, but doesn't run in the directory itself. If a directory @@ -459,6 +471,18 @@ func (cf *ExecCommandFactory) newCommand(ctx context.Context, repo storage.Repos } env = append(alternates.Env(repoPath, repo.GetGitObjectDirectory(), repo.GetGitAlternateObjectDirectories()), env...) + } else { + // If the Git command is running outside the context of a repo, we restrict the working directory of the + // process to a fixed scratch directory. + scratchDir, err := cf.scratchDir() + if err != nil { + return nil, err + } + + // Ensure Git doesn't traverse up the directory tree. + env = append([]string{fmt.Sprintf("GIT_CEILING_DIRECTORIES=%s", scratchDir)}, env...) + + args = append([]string{"-C", scratchDir}, args...) } if config.worktreePath != "" { diff --git a/internal/git/command_factory_test.go b/internal/git/command_factory_test.go index 71a7cf40093d95581802f2c5e7712b6970299874..5fc82af15c6bdb3e77b4ea37c58948e387217ac8 100644 --- a/internal/git/command_factory_test.go +++ b/internal/git/command_factory_test.go @@ -632,6 +632,63 @@ func TestExecCommandFactory_config(t *testing.T) { require.Equal(t, expectedEnv, strings.Split(strings.TrimSpace(stdout.String()), "\n")) } +func TestExecCommandFactory_WorkingDirectoryNew(t *testing.T) { + ctx := testhelper.Context(t) + cfg := testcfg.Build(t) + gitCmdFactory := gittest.NewCommandFactory(t, cfg) + + t.Run("without a repository", func(t *testing.T) { + expectedWorkingDirectory := cfg.GitScratchDir() + + cmd, err := gitCmdFactory.NewWithoutRepo(ctx, git.Command{Name: "version"}) + require.NoError(t, err) + require.Contains(t, cmd.Env(), "GIT_CEILING_DIRECTORIES="+expectedWorkingDirectory) + require.Contains(t, cmd.Args(), "-C") + require.Contains(t, cmd.Args(), expectedWorkingDirectory) + }) + + t.Run("with a repository", func(t *testing.T) { + repo, repoDir := gittest.CreateRepository(t, ctx, cfg, gittest.CreateRepositoryConfig{ + SkipCreationViaService: true, + }) + + expectedWorkingDirectory := repoDir + + cmd, err := gitCmdFactory.New(ctx, repo, git.Command{ + Name: "rev-list", + Flags: []git.Option{ + git.Flag{Name: "--all"}, + }, + }) + require.NoError(t, err) + require.Contains(t, cmd.Args(), "--git-dir") + require.Contains(t, cmd.Args(), expectedWorkingDirectory) + + require.NotContains(t, cmd.Args(), "-C") + }) + + t.Run("with a repository and worktree", func(t *testing.T) { + repo, repoDir := gittest.CreateRepository(t, ctx, cfg, gittest.CreateRepositoryConfig{ + SkipCreationViaService: true, + }) + + expectedWorkingDirectory := filepath.Join(repoDir, "my-worktree") + + cmd, err := gitCmdFactory.New(ctx, repo, git.Command{ + Name: "rev-list", + Flags: []git.Option{ + git.Flag{Name: "--all"}, + }, + }, git.WithWorktree(expectedWorkingDirectory)) + + require.NoError(t, err) + require.Contains(t, cmd.Args(), "-C") + require.Contains(t, cmd.Args(), expectedWorkingDirectory) + + require.NotContains(t, cmd.Args(), "--git-dir") + }) +} + // TestFsckConfiguration tests the hardcoded configuration of the // git fsck subcommand generated through the command factory. func TestFsckConfiguration(t *testing.T) { diff --git a/internal/gitaly/config/config.go b/internal/gitaly/config/config.go index c9e228a61a4c326dc30161d8de29dfa4eec86ee5..14613a2fd2e46c9d5efd83a5ea6847f3ae73ba7a 100644 --- a/internal/gitaly/config/config.go +++ b/internal/gitaly/config/config.go @@ -944,6 +944,12 @@ func (cfg *Cfg) InternalSocketPath() string { return filepath.Join(cfg.InternalSocketDir(), "intern") } +// GitScratchDir is a fixed directory for running Git commands that don't operate +// against a repository. +func (cfg *Cfg) GitScratchDir() string { + return filepath.Join(cfg.RuntimeDir, "git-scratch") +} + func (cfg *Cfg) validateBinDir() error { if len(cfg.BinDir) == 0 { return fmt.Errorf("bin_dir: is not set")