Skip to content

Commit a58094b

Browse files
committed
feature: add support for sparse checkouts
1 parent d55ecac commit a58094b

File tree

9 files changed

+504
-0
lines changed

9 files changed

+504
-0
lines changed

agent/agent_configuration.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ type AgentConfiguration struct {
2323
GitCloneMirrorFlags string
2424
GitCleanFlags string
2525
GitFetchFlags string
26+
GitSparseCheckout bool
27+
GitSparseCheckoutPaths string
28+
GitCloneDepth string
29+
GitCloneFilter string
2630
GitSubmodules bool
2731
AllowedRepositories []*regexp.Regexp
2832
AllowedPlugins []*regexp.Regexp

agent/job_runner.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,10 @@ func (r *JobRunner) createEnvironment(ctx context.Context) ([]string, error) {
554554
env["BUILDKITE_GIT_FETCH_FLAGS"] = r.conf.AgentConfiguration.GitFetchFlags
555555
env["BUILDKITE_GIT_CLONE_MIRROR_FLAGS"] = r.conf.AgentConfiguration.GitCloneMirrorFlags
556556
env["BUILDKITE_GIT_CLEAN_FLAGS"] = r.conf.AgentConfiguration.GitCleanFlags
557+
env["BUILDKITE_GIT_SPARSE_CHECKOUT"] = fmt.Sprint(r.conf.AgentConfiguration.GitSparseCheckout)
558+
env["BUILDKITE_GIT_SPARSE_CHECKOUT_PATHS"] = r.conf.AgentConfiguration.GitSparseCheckoutPaths
559+
env["BUILDKITE_GIT_CLONE_DEPTH"] = r.conf.AgentConfiguration.GitCloneDepth
560+
env["BUILDKITE_GIT_CLONE_FILTER"] = r.conf.AgentConfiguration.GitCloneFilter
557561
env["BUILDKITE_GIT_MIRRORS_LOCK_TIMEOUT"] = strconv.Itoa(r.conf.AgentConfiguration.GitMirrorsLockTimeout)
558562
env["BUILDKITE_SHELL"] = r.conf.AgentConfiguration.Shell
559563
env["BUILDKITE_AGENT_EXPERIMENT"] = strings.Join(experiments.Enabled(ctx), ",")

clicommand/agent_start.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,10 @@ type AgentStartConfig struct {
149149
GitCloneMirrorFlags string `cli:"git-clone-mirror-flags"`
150150
GitCleanFlags string `cli:"git-clean-flags"`
151151
GitFetchFlags string `cli:"git-fetch-flags"`
152+
GitSparseCheckout bool `cli:"git-sparse-checkout"`
153+
GitSparseCheckoutPaths string `cli:"git-sparse-checkout-paths"`
154+
GitCloneDepth string `cli:"git-clone-depth"`
155+
GitCloneFilter string `cli:"git-clone-filter"`
152156
GitMirrorsPath string `cli:"git-mirrors-path" normalize:"filepath"`
153157
GitMirrorsLockTimeout int `cli:"git-mirrors-lock-timeout"`
154158
GitMirrorsSkipUpdate bool `cli:"git-mirrors-skip-update"`
@@ -509,6 +513,29 @@ var AgentStartCommand = cli.Command{
509513
Usage: "Flags to pass to \"git fetch\" command",
510514
EnvVar: "BUILDKITE_GIT_FETCH_FLAGS",
511515
},
516+
cli.BoolFlag{
517+
Name: "git-sparse-checkout",
518+
Usage: "Enable sparse checkout for partial clones",
519+
EnvVar: "BUILDKITE_GIT_SPARSE_CHECKOUT",
520+
},
521+
cli.StringFlag{
522+
Name: "git-sparse-checkout-paths",
523+
Value: "",
524+
Usage: "Paths to include in sparse checkout (comma-separated)",
525+
EnvVar: "BUILDKITE_GIT_SPARSE_CHECKOUT_PATHS",
526+
},
527+
cli.StringFlag{
528+
Name: "git-clone-depth",
529+
Value: "",
530+
Usage: "Clone depth for shallow clones (e.g., \"200\")",
531+
EnvVar: "BUILDKITE_GIT_CLONE_DEPTH",
532+
},
533+
cli.StringFlag{
534+
Name: "git-clone-filter",
535+
Value: "",
536+
Usage: "Filter specification for partial clones (e.g., \"tree:0\")",
537+
EnvVar: "BUILDKITE_GIT_CLONE_FILTER",
538+
},
512539
cli.StringFlag{
513540
Name: "git-clone-mirror-flags",
514541
Value: "-v",
@@ -1019,6 +1046,10 @@ var AgentStartCommand = cli.Command{
10191046
GitCloneMirrorFlags: cfg.GitCloneMirrorFlags,
10201047
GitCleanFlags: cfg.GitCleanFlags,
10211048
GitFetchFlags: cfg.GitFetchFlags,
1049+
GitSparseCheckout: cfg.GitSparseCheckout,
1050+
GitSparseCheckoutPaths: cfg.GitSparseCheckoutPaths,
1051+
GitCloneDepth: cfg.GitCloneDepth,
1052+
GitCloneFilter: cfg.GitCloneFilter,
10221053
GitSubmodules: !cfg.NoGitSubmodules,
10231054
SSHKeyscan: !cfg.NoSSHKeyscan,
10241055
CommandEval: !cfg.NoCommandEval,

clicommand/bootstrap.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ type BootstrapConfig struct {
6969
GitFetchFlags string `cli:"git-fetch-flags"`
7070
GitCloneMirrorFlags string `cli:"git-clone-mirror-flags"`
7171
GitCleanFlags string `cli:"git-clean-flags"`
72+
GitSparseCheckout bool `cli:"git-sparse-checkout"`
73+
GitSparseCheckoutPaths string `cli:"git-sparse-checkout-paths"`
74+
GitCloneDepth string `cli:"git-clone-depth"`
75+
GitCloneFilter string `cli:"git-clone-filter"`
7276
GitMirrorsPath string `cli:"git-mirrors-path" normalize:"filepath"`
7377
GitMirrorsLockTimeout int `cli:"git-mirrors-lock-timeout"`
7478
GitMirrorsSkipUpdate bool `cli:"git-mirrors-skip-update"`
@@ -244,6 +248,29 @@ var BootstrapCommand = cli.Command{
244248
Usage: "Flags to pass to \"git fetch\" command",
245249
EnvVar: "BUILDKITE_GIT_FETCH_FLAGS",
246250
},
251+
cli.BoolFlag{
252+
Name: "git-sparse-checkout",
253+
Usage: "Enable sparse checkout for partial clones",
254+
EnvVar: "BUILDKITE_GIT_SPARSE_CHECKOUT",
255+
},
256+
cli.StringFlag{
257+
Name: "git-sparse-checkout-paths",
258+
Value: "",
259+
Usage: "Paths to include in sparse checkout (comma-separated)",
260+
EnvVar: "BUILDKITE_GIT_SPARSE_CHECKOUT_PATHS",
261+
},
262+
cli.StringFlag{
263+
Name: "git-clone-depth",
264+
Value: "",
265+
Usage: "Clone depth for shallow clones (e.g., \"200\")",
266+
EnvVar: "BUILDKITE_GIT_CLONE_DEPTH",
267+
},
268+
cli.StringFlag{
269+
Name: "git-clone-filter",
270+
Value: "",
271+
Usage: "Filter specification for partial clones (e.g., \"tree:0\")",
272+
EnvVar: "BUILDKITE_GIT_CLONE_FILTER",
273+
},
247274
cli.StringSliceFlag{
248275
Name: "git-submodule-clone-config",
249276
Value: &cli.StringSlice{},
@@ -466,6 +493,10 @@ var BootstrapCommand = cli.Command{
466493
GitCloneFlags: cfg.GitCloneFlags,
467494
GitCloneMirrorFlags: cfg.GitCloneMirrorFlags,
468495
GitFetchFlags: cfg.GitFetchFlags,
496+
GitSparseCheckout: cfg.GitSparseCheckout,
497+
GitSparseCheckoutPaths: cfg.GitSparseCheckoutPaths,
498+
GitCloneDepth: cfg.GitCloneDepth,
499+
GitCloneFilter: cfg.GitCloneFilter,
469500
GitMirrorsLockTimeout: cfg.GitMirrorsLockTimeout,
470501
GitMirrorsPath: cfg.GitMirrorsPath,
471502
GitMirrorsSkipUpdate: cfg.GitMirrorsSkipUpdate,

docs/partial-clone.md

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# Partial Clone and Sparse Checkout
2+
3+
The Buildkite Agent supports partial clones and sparse checkouts, which can significantly reduce clone times and disk space usage for large repositories.
4+
5+
## Overview
6+
7+
Partial clones allow you to clone a repository without downloading all of its history or objects, while sparse checkout allows you to check out only specific directories or files from a repository.
8+
9+
## Configuration
10+
11+
### Environment Variables
12+
13+
The following environment variables control partial clone behavior:
14+
15+
- `BUILDKITE_GIT_SPARSE_CHECKOUT` - Enable sparse checkout (boolean: `true` or `false`)
16+
- `BUILDKITE_GIT_SPARSE_CHECKOUT_PATHS` - Comma-separated list of paths to include in sparse checkout
17+
- `BUILDKITE_GIT_CLONE_DEPTH` - Clone depth for shallow clones (e.g., `200`)
18+
- `BUILDKITE_GIT_CLONE_FILTER` - Filter specification for partial clones (e.g., `tree:0`)
19+
20+
### Command Line Flags
21+
22+
When starting an agent, you can also use command line flags:
23+
24+
```bash
25+
buildkite-agent start \
26+
--git-sparse-checkout \
27+
--git-sparse-checkout-paths "src/frontend,src/backend" \
28+
--git-clone-depth 200 \
29+
--git-clone-filter "tree:0"
30+
```
31+
32+
## Examples
33+
34+
### Example 1: Sparse Checkout with Multiple Directories
35+
36+
To check out only specific directories from a monorepo:
37+
38+
```yaml
39+
env:
40+
BUILDKITE_GIT_SPARSE_CHECKOUT: "true"
41+
BUILDKITE_GIT_SPARSE_CHECKOUT_PATHS: "services/api,services/web,shared/utils"
42+
```
43+
44+
### Example 2: Shallow Clone with Partial Objects
45+
46+
For a shallow clone with limited history and filtered objects:
47+
48+
```yaml
49+
env:
50+
BUILDKITE_GIT_CLONE_DEPTH: "100"
51+
BUILDKITE_GIT_CLONE_FILTER: "blob:none"
52+
```
53+
54+
### Example 3: Complete Partial Clone Setup
55+
56+
Combining all features for maximum optimization:
57+
58+
```yaml
59+
env:
60+
BUILDKITE_GIT_CLONE_DEPTH: "200"
61+
BUILDKITE_GIT_CLONE_FILTER: "tree:0"
62+
BUILDKITE_GIT_SPARSE_CHECKOUT: "true"
63+
BUILDKITE_GIT_SPARSE_CHECKOUT_PATHS: "my-service"
64+
```
65+
66+
This configuration will:
67+
1. Clone only the last 200 commits
68+
2. Exclude tree objects that aren't needed (`tree:0`)
69+
3. Only check out the `my-service` directory
70+
71+
## Git Clone Filters
72+
73+
The `BUILDKITE_GIT_CLONE_FILTER` supports various filter specifications:
74+
75+
- `blob:none` - Omit all blob objects (file contents)
76+
- `blob:limit=<size>` - Omit blobs larger than `<size>` bytes
77+
- `tree:0` - Omit all tree objects (directory listings)
78+
- `tree:<depth>` - Omit tree objects at specified depth
79+
80+
## Sparse Checkout Paths
81+
82+
The `BUILDKITE_GIT_SPARSE_CHECKOUT_PATHS` variable accepts:
83+
- Single directory: `"src/frontend"`
84+
- Multiple directories: `"src/frontend,src/backend,docs"`
85+
- Paths with wildcards are not supported in cone mode (which is the default)
86+
87+
## Performance Considerations
88+
89+
1. **Network Usage**: Partial clones significantly reduce network bandwidth usage
90+
2. **Disk Space**: Sparse checkouts reduce local disk space usage
91+
3. **Clone Time**: Both features can dramatically reduce initial clone times
92+
4. **Fetch Time**: Subsequent fetches will only download required objects
93+
94+
## Compatibility
95+
96+
- Requires Git 2.25.0 or later for partial clone support
97+
- Requires Git 2.25.0 or later for cone-mode sparse checkout
98+
- The repository must be hosted on a server that supports partial clones
99+
100+
## Troubleshooting
101+
102+
1. **Missing objects**: If you encounter "object not found" errors, you may need to adjust your filter settings or disable partial clones
103+
2. **Sparse checkout not working**: Ensure paths are comma-separated and don't contain spaces
104+
3. **Performance issues**: Some operations may trigger on-demand object downloads; monitor your builds to ensure partial clones provide the expected benefits

internal/job/checkout.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,18 @@ func (e *Executor) defaultCheckoutPhase(ctx context.Context) error {
567567
gitCloneFlags += fmt.Sprintf(" --reference %q", mirrorDir)
568568
}
569569

570+
// Add partial clone flags if specified
571+
if e.GitCloneDepth != "" {
572+
gitCloneFlags += fmt.Sprintf(" --depth=%s", e.GitCloneDepth)
573+
}
574+
if e.GitCloneFilter != "" {
575+
gitCloneFlags += fmt.Sprintf(" --filter=%s", e.GitCloneFilter)
576+
}
577+
// For sparse checkout, we need to add --no-checkout
578+
if e.GitSparseCheckout && e.GitSparseCheckoutPaths != "" {
579+
gitCloneFlags += " --no-checkout"
580+
}
581+
570582
// Does the git directory exist?
571583
existingGitDir := filepath.Join(e.shell.Getwd(), ".git")
572584
if osutil.FileExists(existingGitDir) {
@@ -575,10 +587,54 @@ func (e *Executor) defaultCheckoutPhase(ctx context.Context) error {
575587
if _, err := e.updateRemoteURL(ctx, "", e.Repository); err != nil {
576588
return fmt.Errorf("setting origin: %w", err)
577589
}
590+
591+
// Handle sparse checkout for existing repos
592+
if e.GitSparseCheckout && e.GitSparseCheckoutPaths != "" {
593+
e.shell.Commentf("Configuring sparse checkout for existing repository")
594+
595+
// Check if sparse checkout is already initialized
596+
sparseCheckoutFile := filepath.Join(existingGitDir, "info", "sparse-checkout")
597+
if !osutil.FileExists(sparseCheckoutFile) {
598+
// Initialize sparse checkout
599+
if err := gitSparseCheckoutInit(ctx, e.shell, true); err != nil {
600+
return fmt.Errorf("initializing sparse checkout: %w", err)
601+
}
602+
}
603+
604+
// Parse comma-separated paths
605+
paths := strings.Split(e.GitSparseCheckoutPaths, ",")
606+
for i := range paths {
607+
paths[i] = strings.TrimSpace(paths[i])
608+
}
609+
610+
e.shell.Commentf("Setting sparse checkout paths: %v", paths)
611+
if err := gitSparseCheckoutSet(ctx, e.shell, paths); err != nil {
612+
return fmt.Errorf("setting sparse checkout paths: %w", err)
613+
}
614+
}
578615
} else {
579616
if err := gitClone(ctx, e.shell, gitCloneFlags, e.Repository, "."); err != nil {
580617
return fmt.Errorf("cloning git repository: %w", err)
581618
}
619+
620+
// If sparse checkout is enabled, initialize it right after cloning
621+
if e.GitSparseCheckout && e.GitSparseCheckoutPaths != "" {
622+
e.shell.Commentf("Initializing sparse checkout")
623+
if err := gitSparseCheckoutInit(ctx, e.shell, true); err != nil {
624+
return fmt.Errorf("initializing sparse checkout: %w", err)
625+
}
626+
627+
// Parse comma-separated paths
628+
paths := strings.Split(e.GitSparseCheckoutPaths, ",")
629+
for i := range paths {
630+
paths[i] = strings.TrimSpace(paths[i])
631+
}
632+
633+
e.shell.Commentf("Setting sparse checkout paths: %v", paths)
634+
if err := gitSparseCheckoutSet(ctx, e.shell, paths); err != nil {
635+
return fmt.Errorf("setting sparse checkout paths: %w", err)
636+
}
637+
}
582638
}
583639

584640
// Git clean prior to checkout, we do this even if submodules have been
@@ -594,6 +650,11 @@ func (e *Executor) defaultCheckoutPhase(ctx context.Context) error {
594650
}
595651

596652
gitFetchFlags := e.GitFetchFlags
653+
654+
// Add filter flag for partial clones during fetch
655+
if e.GitCloneFilter != "" {
656+
gitFetchFlags += fmt.Sprintf(" --filter=%s", e.GitCloneFilter)
657+
}
597658

598659
switch {
599660
case e.RefSpec != "":

internal/job/config.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,18 @@ type ExecutorConfig struct {
9090
// Config key=value pairs to pass to "git" when submodule init commands are invoked
9191
GitSubmoduleCloneConfig []string `env:"BUILDKITE_GIT_SUBMODULE_CLONE_CONFIG" normalize:"list"`
9292

93+
// Enable sparse checkout for partial clones
94+
GitSparseCheckout bool `env:"BUILDKITE_GIT_SPARSE_CHECKOUT"`
95+
96+
// Paths to include in sparse checkout (comma-separated)
97+
GitSparseCheckoutPaths string `env:"BUILDKITE_GIT_SPARSE_CHECKOUT_PATHS"`
98+
99+
// Clone depth for shallow clones
100+
GitCloneDepth string `env:"BUILDKITE_GIT_CLONE_DEPTH"`
101+
102+
// Filter specification for partial clones (e.g., "tree:0")
103+
GitCloneFilter string `env:"BUILDKITE_GIT_CLONE_FILTER"`
104+
93105
// Whether or not to run the hooks/commands in a PTY
94106
RunInPty bool
95107

internal/job/git.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,3 +396,31 @@ var gitCheckRefFormatDenyRegexp = regexp.MustCompile(strings.Join([]string{
396396
func gitCheckRefFormat(ref string) bool {
397397
return !gitCheckRefFormatDenyRegexp.MatchString(ref)
398398
}
399+
400+
func gitSparseCheckoutInit(ctx context.Context, sh *shell.Shell, cone bool) error {
401+
args := []string{"sparse-checkout", "init"}
402+
if cone {
403+
args = append(args, "--cone")
404+
}
405+
406+
if err := sh.Command("git", args...).Run(ctx); err != nil {
407+
return &gitError{error: err, Type: gitErrorCheckout}
408+
}
409+
410+
return nil
411+
}
412+
413+
func gitSparseCheckoutSet(ctx context.Context, sh *shell.Shell, paths []string) error {
414+
if len(paths) == 0 {
415+
return fmt.Errorf("no paths provided for sparse checkout")
416+
}
417+
418+
args := []string{"sparse-checkout", "set"}
419+
args = append(args, paths...)
420+
421+
if err := sh.Command("git", args...).Run(ctx); err != nil {
422+
return &gitError{error: err, Type: gitErrorCheckout}
423+
}
424+
425+
return nil
426+
}

0 commit comments

Comments
 (0)