-
Notifications
You must be signed in to change notification settings - Fork 351
feat: add sparse checkout support #3903
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feat/git-checkout-features
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| package job | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "context" | ||
| "errors" | ||
| "fmt" | ||
|
|
@@ -343,6 +344,117 @@ func hasGitSubmodules(sh *shell.Shell) bool { | |
| return osutil.FileExists(filepath.Join(sh.Getwd(), ".gitmodules")) | ||
| } | ||
|
|
||
| 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) { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| 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) | ||
| } | ||
|
Comment on lines
+432
to
+453
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I suggest writing an end-to-end test to verify that the behavior of |
||
|
|
||
| return true, nil | ||
| } | ||
|
|
||
| func hasGitCommit(ctx context.Context, sh *shell.Shell, gitDir, commit string) bool { | ||
| // Resolve commit to an actual commit object | ||
| output, err := sh.Command("git", "--git-dir", gitDir, "rev-parse", commit+"^{commit}").RunAndCaptureStdout(ctx, shell.ShowStderr(false)) | ||
|
|
@@ -918,6 +1030,11 @@ func (e *Executor) defaultCheckoutPhase(ctx context.Context) error { | |
| return err | ||
| } | ||
|
|
||
| sparseCheckoutActive, err := e.setupSparseCheckout(ctx) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| gitCheckoutFlags := e.GitCheckoutFlags | ||
|
|
||
| if e.Commit == "HEAD" { | ||
|
|
@@ -932,10 +1049,13 @@ func (e *Executor) defaultCheckoutPhase(ctx context.Context) 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") | ||
| } | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change does not seem to get mentioned on the PR description?