Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions agent/agent_configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type AgentConfiguration struct {
GitCloneMirrorFlags string
GitCleanFlags string
GitFetchFlags string
GitSparseCheckoutPaths []string
GitSubmodules bool
GitSubmoduleCloneConfig []string
SkipCheckout bool
Expand Down
1 change: 1 addition & 0 deletions agent/job_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,7 @@ BUILDKITE_AGENT_JWKS_KEY_ID`
setEnv("BUILDKITE_GIT_CHECKOUT_FLAGS", r.conf.AgentConfiguration.GitCheckoutFlags)
setEnv("BUILDKITE_GIT_CLONE_FLAGS", r.conf.AgentConfiguration.GitCloneFlags)
setEnv("BUILDKITE_GIT_FETCH_FLAGS", r.conf.AgentConfiguration.GitFetchFlags)
setEnv("BUILDKITE_GIT_SPARSE_CHECKOUT_PATHS", strings.Join(r.conf.AgentConfiguration.GitSparseCheckoutPaths, ","))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this setEnv be conditional, (ie. if len(r.conf.AgentConfiguration.GitSparseCheckoutPaths) > 0 { setEnv("BUILDKITE_GIT_SPARSE_CHECKOUT_PATHS", strings.Join(r.conf.AgentConfiguration.GitSparseCheckoutPaths, ",")) })?

setEnv("BUILDKITE_GIT_CLONE_MIRROR_FLAGS", r.conf.AgentConfiguration.GitCloneMirrorFlags)
Comment thread
linear-code[bot] marked this conversation as resolved.
setEnv("BUILDKITE_GIT_MIRROR_CHECKOUT_MODE", r.conf.AgentConfiguration.GitMirrorCheckoutMode)
setEnv("BUILDKITE_GIT_CLEAN_FLAGS", r.conf.AgentConfiguration.GitCleanFlags)
Expand Down
3 changes: 3 additions & 0 deletions clicommand/agent_start.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ type AgentStartConfig struct {
GitCloneMirrorFlags string `cli:"git-clone-mirror-flags"`
GitCleanFlags string `cli:"git-clean-flags"`
GitFetchFlags string `cli:"git-fetch-flags"`
GitSparseCheckoutPaths []string `cli:"git-sparse-checkout-paths" normalize:"list"`
GitMirrorsPath string `cli:"git-mirrors-path" normalize:"filepath"`
GitMirrorCheckoutMode string `cli:"git-mirror-checkout-mode"`
GitMirrorsLockTimeout int `cli:"git-mirrors-lock-timeout"`
Expand Down Expand Up @@ -534,6 +535,7 @@ var AgentStartCommand = cli.Command{
GitCloneFlagsFlag,
GitCleanFlagsFlag,
GitFetchFlagsFlag,
GitSparseCheckoutPathsFlag,
GitCloneMirrorFlagsFlag,
GitMirrorsPathFlag,
GitMirrorCheckoutModeFlag,
Expand Down Expand Up @@ -1069,6 +1071,7 @@ var AgentStartCommand = cli.Command{
GitCloneMirrorFlags: cfg.GitCloneMirrorFlags,
GitCleanFlags: cfg.GitCleanFlags,
GitFetchFlags: cfg.GitFetchFlags,
GitSparseCheckoutPaths: cfg.GitSparseCheckoutPaths,
GitSubmodules: !cfg.NoGitSubmodules,
GitSubmoduleCloneConfig: cfg.GitSubmoduleCloneConfig,
SkipCheckout: cfg.SkipCheckout,
Expand Down
3 changes: 3 additions & 0 deletions clicommand/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ type BootstrapConfig struct {
GitCheckoutFlags string `cli:"git-checkout-flags"`
GitCloneFlags string `cli:"git-clone-flags"`
GitFetchFlags string `cli:"git-fetch-flags"`
GitSparseCheckoutPaths []string `cli:"git-sparse-checkout-paths" normalize:"list"`
GitCloneMirrorFlags string `cli:"git-clone-mirror-flags"`
GitCleanFlags string `cli:"git-clean-flags"`
GitMirrorsPath string `cli:"git-mirrors-path" normalize:"filepath"`
Expand Down Expand Up @@ -243,6 +244,7 @@ var BootstrapCommand = cli.Command{
GitCloneMirrorFlagsFlag,
GitCleanFlagsFlag,
GitFetchFlagsFlag,
GitSparseCheckoutPathsFlag,
GitMirrorsPathFlag,
GitMirrorCheckoutModeFlag,
GitMirrorsLockTimeoutFlag,
Expand Down Expand Up @@ -450,6 +452,7 @@ var BootstrapCommand = cli.Command{
GitCloneFlags: cfg.GitCloneFlags,
GitCloneMirrorFlags: cfg.GitCloneMirrorFlags,
GitFetchFlags: cfg.GitFetchFlags,
GitSparseCheckoutPaths: cfg.GitSparseCheckoutPaths,
GitMirrorsLockTimeout: cfg.GitMirrorsLockTimeout,
GitMirrorsPath: cfg.GitMirrorsPath,
GitMirrorCheckoutMode: cfg.GitMirrorCheckoutMode,
Expand Down
7 changes: 7 additions & 0 deletions clicommand/global.go
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,13 @@ var (
EnvVar: "BUILDKITE_GIT_FETCH_FLAGS",
}

GitSparseCheckoutPathsFlag = cli.StringSliceFlag{
Name: "git-sparse-checkout-paths",
Value: &cli.StringSlice{},
Usage: "Comma-separated list of paths for git sparse checkout (cone mode). When set, only the listed paths are materialized in the working tree.",
EnvVar: "BUILDKITE_GIT_SPARSE_CHECKOUT_PATHS",
}

GitMirrorsPathFlag = cli.StringFlag{
Name: "git-mirrors-path",
Value: "",
Expand Down
12 changes: 10 additions & 2 deletions internal/job/checkout.go
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the size of checkout.go and the fact that this code change is quite isolated, I recommend opening a internal/job/checkout_sparse.go file.

Original file line number Diff line number Diff line change
Expand Up @@ -955,6 +955,11 @@ func (e *Executor) defaultCheckoutPhase(ctx context.Context) (retErr error) {
return err
}

sparseCheckoutActive, err := e.setupSparseCheckout(ctx)
if err != nil {
return err
}

gitCheckoutFlags := e.GitCheckoutFlags

if e.Commit == "HEAD" {
Expand All @@ -969,10 +974,13 @@ func (e *Executor) defaultCheckoutPhase(ctx context.Context) (retErr error) {

gitSubmodules := false
if hasGitSubmodules(e.shell) {
if e.GitSubmodules {
switch {
case sparseCheckoutActive:
e.shell.Commentf("Submodule initialization skipped during sparse checkout")
case e.GitSubmodules:
e.shell.Commentf("Git submodules detected")
gitSubmodules = true
} else {
default:
e.shell.OptionalWarningf("submodules-disabled", "This repository has submodules, but submodules are disabled")
}
}
Expand Down
124 changes: 124 additions & 0 deletions internal/job/checkout_sparse.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package job

import (
"bytes"
"context"
"fmt"
"os"
"path/filepath"
"strings"

"github.com/buildkite/agent/v3/internal/osutil"
"github.com/buildkite/agent/v3/internal/shell"
)

func cleanGitSparseCheckoutPaths(paths []string) []string {
cleaned := make([]string, 0, len(paths))
for _, path := range paths {
path = strings.TrimSpace(path)
if path != "" {
cleaned = append(cleaned, path)
}
}
return cleaned
}

func parseGitVersion(output string) (major, minor int, ok bool) {
if _, err := fmt.Sscanf(output, "git version %d.%d", &major, &minor); err != nil {
return 0, 0, false
}
return major, minor, true
}

func gitVersionAtLeast(ctx context.Context, sh *shell.Shell, major, minor int) (bool, error) {
output, err := sh.Command("git", "--version").RunAndCaptureStdout(ctx)
if err != nil {
return false, err
}

gitMajor, gitMinor, ok := parseGitVersion(strings.TrimSpace(output))
if !ok {
return false, fmt.Errorf("parsing git version from %q", strings.TrimSpace(output))
}

if gitMajor != major {
return gitMajor > major, nil
}
return gitMinor >= minor, nil
}

// sparseCheckoutMayBeConfigured does a cheap filesystem check for marker files
// that indicate sparse checkout (or the worktree-config extension that
// `sparse-checkout` enables) might already be in effect, so we can avoid
// shelling out to `git config` on every checkout. It resolves the .git dir
// directly to handle the worktree/submodule case where .git is a file
// containing `gitdir: <path>`.
func sparseCheckoutMayBeConfigured(sh *shell.Shell) bool {
gitDir := filepath.Join(sh.Getwd(), ".git")
if data, err := os.ReadFile(gitDir); err == nil && bytes.HasPrefix(data, []byte("gitdir:")) {
gitDirValue := strings.TrimSpace(string(bytes.TrimPrefix(data, []byte("gitdir:"))))
if !filepath.IsAbs(gitDirValue) {
gitDirValue = filepath.Join(sh.Getwd(), gitDirValue)
}
gitDir = gitDirValue
}

return osutil.FileExists(filepath.Join(gitDir, "info", "sparse-checkout")) ||
osutil.FileExists(filepath.Join(gitDir, "config.worktree"))
}

func (e *Executor) disableSparseCheckoutIfConfigured(ctx context.Context) {
if !sparseCheckoutMayBeConfigured(e.shell) {
return
}

sparseOutput, err := e.shell.Command("git", "config", "--get", "core.sparseCheckout").RunAndCaptureStdout(ctx, shell.ShowStderr(false))
if err != nil || strings.TrimSpace(sparseOutput) != "true" {
return
}

e.shell.Commentf("Disabling sparse checkout from previous build")
if err := e.shell.Command("git", "sparse-checkout", "disable").Run(ctx); err != nil {
e.shell.Warningf("Failed to disable sparse checkout: %v", err)
}

// `sparse-checkout disable` leaves extensions.worktreeConfig set, which
// can cause problems for subsequent git operations. Only unset it if no
// other worktree-scoped config remains, to avoid clobbering user config.
worktreeConfig, err := e.shell.Command("git", "config", "--worktree", "--list").RunAndCaptureStdout(ctx, shell.ShowStderr(false))
if err == nil && strings.TrimSpace(worktreeConfig) == "" {
_ = e.shell.Command("git", "config", "--unset", "extensions.worktreeConfig").Run(ctx)
}
}

// setupSparseCheckout configures (or disables) git sparse checkout for the
// current working tree. It returns true if sparse checkout was successfully
// applied for this build, so callers can adjust later behaviour (e.g. skip
// submodule init, which requires the full tree).
func (e *Executor) setupSparseCheckout(ctx context.Context) (bool, error) {
paths := cleanGitSparseCheckoutPaths(e.GitSparseCheckoutPaths)
if len(paths) == 0 {
e.disableSparseCheckoutIfConfigured(ctx)
return false, nil
}

ok, err := gitVersionAtLeast(ctx, e.shell, 2, 26)
if err != nil {
e.shell.Warningf("Sparse checkout requires git >= 2.26; falling back to full checkout (%v)", err)
e.disableSparseCheckoutIfConfigured(ctx)
return false, nil
}
if !ok {
e.shell.Warningf("Sparse checkout requires git >= 2.26; falling back to full checkout")
e.disableSparseCheckoutIfConfigured(ctx)
return false, nil
}

e.shell.Commentf("Setting up sparse checkout for paths: %s", strings.Join(paths, ","))
args := append([]string{"sparse-checkout", "set", "--cone"}, paths...)
if err := e.shell.Command("git", args...).Run(ctx); err != nil {
return false, fmt.Errorf("setting sparse checkout paths: %w", err)
}

return true, nil
}
Loading
Loading