11package job
22
33import (
4+ "bytes"
45 "context"
56 "errors"
67 "fmt"
@@ -343,6 +344,118 @@ func hasGitSubmodules(sh *shell.Shell) bool {
343344 return osutil .FileExists (filepath .Join (sh .Getwd (), ".gitmodules" ))
344345}
345346
347+ func parseSparseCheckoutPaths (paths string ) []string {
348+ parts := strings .Split (paths , "," )
349+ parsed := make ([]string , 0 , len (parts ))
350+ for _ , part := range parts {
351+ part = strings .TrimSpace (part )
352+ if part != "" {
353+ parsed = append (parsed , part )
354+ }
355+ }
356+ return parsed
357+ }
358+
359+ func parseGitVersion (output string ) (major , minor int , ok bool ) {
360+ if _ , err := fmt .Sscanf (output , "git version %d.%d" , & major , & minor ); err != nil {
361+ return 0 , 0 , false
362+ }
363+ return major , minor , true
364+ }
365+
366+ func gitVersionAtLeast (ctx context.Context , sh * shell.Shell , major , minor int ) (bool , error ) {
367+ output , err := sh .Command ("git" , "--version" ).RunAndCaptureStdout (ctx )
368+ if err != nil {
369+ return false , err
370+ }
371+
372+ gitMajor , gitMinor , ok := parseGitVersion (strings .TrimSpace (output ))
373+ if ! ok {
374+ return false , fmt .Errorf ("parsing git version from %q" , strings .TrimSpace (output ))
375+ }
376+
377+ if gitMajor != major {
378+ return gitMajor > major , nil
379+ }
380+ return gitMinor >= minor , nil
381+ }
382+
383+ // sparseCheckoutMayBeConfigured does a cheap filesystem check for marker files
384+ // that indicate sparse checkout (or the worktree-config extension that
385+ // `sparse-checkout` enables) might already be in effect, so we can avoid
386+ // shelling out to `git config` on every checkout. It resolves the .git dir
387+ // directly to handle the worktree/submodule case where .git is a file
388+ // containing `gitdir: <path>`.
389+ func sparseCheckoutMayBeConfigured (sh * shell.Shell ) bool {
390+ gitDir := filepath .Join (sh .Getwd (), ".git" )
391+ if data , err := os .ReadFile (gitDir ); err == nil && bytes .HasPrefix (data , []byte ("gitdir:" )) {
392+ gitDirValue := strings .TrimSpace (string (bytes .TrimPrefix (data , []byte ("gitdir:" ))))
393+ if ! filepath .IsAbs (gitDirValue ) {
394+ gitDirValue = filepath .Join (sh .Getwd (), gitDirValue )
395+ }
396+ gitDir = gitDirValue
397+ }
398+
399+ return osutil .FileExists (filepath .Join (gitDir , "info" , "sparse-checkout" )) ||
400+ osutil .FileExists (filepath .Join (gitDir , "config.worktree" ))
401+ }
402+
403+ func (e * Executor ) disableSparseCheckoutIfConfigured (ctx context.Context ) {
404+ if ! sparseCheckoutMayBeConfigured (e .shell ) {
405+ return
406+ }
407+
408+ sparseOutput , err := e .shell .Command ("git" , "config" , "--get" , "core.sparseCheckout" ).RunAndCaptureStdout (ctx , shell .ShowStderr (false ))
409+ if err != nil || strings .TrimSpace (sparseOutput ) != "true" {
410+ return
411+ }
412+
413+ e .shell .Commentf ("Disabling sparse checkout from previous build" )
414+ if err := e .shell .Command ("git" , "sparse-checkout" , "disable" ).Run (ctx ); err != nil {
415+ e .shell .Warningf ("Failed to disable sparse checkout: %v" , err )
416+ }
417+
418+ // `sparse-checkout disable` leaves extensions.worktreeConfig set, which
419+ // can cause problems for subsequent git operations. Only unset it if no
420+ // other worktree-scoped config remains, to avoid clobbering user config.
421+ worktreeConfig , err := e .shell .Command ("git" , "config" , "--worktree" , "--list" ).RunAndCaptureStdout (ctx , shell .ShowStderr (false ))
422+ if err == nil && strings .TrimSpace (worktreeConfig ) == "" {
423+ _ = e .shell .Command ("git" , "config" , "--unset" , "extensions.worktreeConfig" ).Run (ctx )
424+ }
425+ }
426+
427+ // setupSparseCheckout configures (or disables) git sparse checkout for the
428+ // current working tree. It returns true if sparse checkout was successfully
429+ // applied for this build, so callers can adjust later behaviour (e.g. skip
430+ // submodule init, which requires the full tree).
431+ func (e * Executor ) setupSparseCheckout (ctx context.Context ) (bool , error ) {
432+ paths := parseSparseCheckoutPaths (e .SparseCheckoutPaths )
433+ if len (paths ) == 0 {
434+ e .disableSparseCheckoutIfConfigured (ctx )
435+ return false , nil
436+ }
437+
438+ ok , err := gitVersionAtLeast (ctx , e .shell , 2 , 26 )
439+ if err != nil {
440+ e .shell .Warningf ("Sparse checkout requires git >= 2.26; falling back to full checkout (%v)" , err )
441+ e .disableSparseCheckoutIfConfigured (ctx )
442+ return false , nil
443+ }
444+ if ! ok {
445+ e .shell .Warningf ("Sparse checkout requires git >= 2.26; falling back to full checkout" )
446+ e .disableSparseCheckoutIfConfigured (ctx )
447+ return false , nil
448+ }
449+
450+ e .shell .Commentf ("Setting up sparse checkout for paths: %s" , e .SparseCheckoutPaths )
451+ args := append ([]string {"sparse-checkout" , "set" , "--cone" }, paths ... )
452+ if err := e .shell .Command ("git" , args ... ).Run (ctx ); err != nil {
453+ return false , fmt .Errorf ("setting sparse checkout paths: %w" , err )
454+ }
455+
456+ return true , nil
457+ }
458+
346459func hasGitCommit (ctx context.Context , sh * shell.Shell , gitDir , commit string ) bool {
347460 // Resolve commit to an actual commit object
348461 output , err := sh .Command ("git" , "--git-dir" , gitDir , "rev-parse" , commit + "^{commit}" ).RunAndCaptureStdout (ctx , shell .ShowStderr (false ))
@@ -918,6 +1031,11 @@ func (e *Executor) defaultCheckoutPhase(ctx context.Context) error {
9181031 return err
9191032 }
9201033
1034+ sparseCheckoutActive , err := e .setupSparseCheckout (ctx )
1035+ if err != nil {
1036+ return err
1037+ }
1038+
9211039 gitCheckoutFlags := e .GitCheckoutFlags
9221040
9231041 if e .Commit == "HEAD" {
@@ -932,10 +1050,13 @@ func (e *Executor) defaultCheckoutPhase(ctx context.Context) error {
9321050
9331051 gitSubmodules := false
9341052 if hasGitSubmodules (e .shell ) {
935- if e .GitSubmodules {
1053+ switch {
1054+ case sparseCheckoutActive :
1055+ e .shell .Commentf ("Submodule initialization skipped during sparse checkout" )
1056+ case e .GitSubmodules :
9361057 e .shell .Commentf ("Git submodules detected" )
9371058 gitSubmodules = true
938- } else {
1059+ default :
9391060 e .shell .OptionalWarningf ("submodules-disabled" , "This repository has submodules, but submodules are disabled" )
9401061 }
9411062 }
0 commit comments