@@ -28,6 +28,7 @@ import (
2828
2929 "github.com/go-git/go-git/v6"
3030 "github.com/go-git/go-git/v6/storage/filesystem"
31+ "github.com/gruntwork-io/terragrunt/pkg/log"
3132)
3233
3334const (
@@ -451,6 +452,154 @@ func (g *GitRunner) Config(ctx context.Context, name string) (string, error) {
451452 return strings .TrimSpace (stdout .String ()), nil
452453}
453454
455+ // GetDefaultBranch implements the hybrid approach to detect the default branch:
456+ // 1. Tries to determine the default branch of the remote repository using the fast local method first
457+ // 2. Falls back to the network method if the local method fails
458+ // 3. Attempts to update local cache for future use
459+ // Returns the branch name (e.g., "main") or an error if both methods fail.
460+ func (g * GitRunner ) GetDefaultBranch (ctx context.Context , l log.Logger ) string {
461+ branch , err := g .GetDefaultBranchLocal (ctx )
462+ if err == nil && branch != "" {
463+ return branch
464+ }
465+
466+ branch , err = g .GetDefaultBranchRemote (ctx )
467+ if err == nil && branch != "" {
468+ err = g .SetRemoteHeadAuto (ctx )
469+ if err != nil {
470+ l .Warnf ("Failed to update local cache for default branch: %v" , err )
471+ }
472+
473+ return branch
474+ }
475+
476+ l .Debugf ("Failed to determine default branch of remote repository, attempting to get default branch of local repository" )
477+
478+ if b , err := g .Config (ctx , "init.defaultBranch" ); err == nil && b != "" {
479+ return b
480+ }
481+
482+ l .Debugf ("Failed to determine default branch of local repository, using 'main' as fallback" )
483+
484+ return "main"
485+ }
486+
487+ // GetDefaultBranchLocal attempts to get the default branch using the local cached remote HEAD.
488+ // Returns the branch name (e.g., "main") if successful, or an error if the local ref is not set.
489+ // This is fast and works offline, but requires that `git remote set-head origin --auto` has been run.
490+ func (g * GitRunner ) GetDefaultBranchLocal (ctx context.Context ) (string , error ) {
491+ if err := g .RequiresWorkDir (); err != nil {
492+ return "" , err
493+ }
494+
495+ cmd := g .prepareCommand (ctx , "rev-parse" , "--abbrev-ref" , "origin/HEAD" )
496+
497+ var stdout , stderr bytes.Buffer
498+
499+ cmd .Stdout = & stdout
500+ cmd .Stderr = & stderr
501+
502+ if err := cmd .Run (); err != nil {
503+ return "" , & WrappedError {
504+ Op : "git_rev_parse_origin_head" ,
505+ Context : stderr .String (),
506+ Err : ErrCommandSpawn ,
507+ }
508+ }
509+
510+ result := strings .TrimSpace (stdout .String ())
511+
512+ // If the result is just "origin/HEAD", the local ref is not properly set
513+ if result == "origin/HEAD" {
514+ return "" , & WrappedError {
515+ Op : "git_rev_parse_origin_head" ,
516+ Context : "local origin/HEAD ref not set" ,
517+ Err : ErrNoMatchingReference ,
518+ }
519+ }
520+
521+ if after , ok := strings .CutPrefix (result , "origin/" ); ok {
522+ return after , nil
523+ }
524+
525+ return result , nil
526+ }
527+
528+ // GetDefaultBranchRemote queries the remote repository to determine the default branch.
529+ // This is the most accurate method but requires network access.
530+ // Returns the branch name (e.g., "main") if successful.
531+ func (g * GitRunner ) GetDefaultBranchRemote (ctx context.Context ) (string , error ) {
532+ if err := g .RequiresWorkDir (); err != nil {
533+ return "" , err
534+ }
535+
536+ cmd := g .prepareCommand (ctx , "ls-remote" , "--symref" , "origin" , "HEAD" )
537+
538+ var stdout , stderr bytes.Buffer
539+
540+ cmd .Stdout = & stdout
541+ cmd .Stderr = & stderr
542+
543+ if err := cmd .Run (); err != nil {
544+ return "" , & WrappedError {
545+ Op : "git_ls_remote_symref" ,
546+ Context : stderr .String (),
547+ Err : ErrCommandSpawn ,
548+ }
549+ }
550+
551+ // Parse output: "ref: refs/heads/main HEAD"
552+ output := stdout .String ()
553+ lines := strings .SplitSeq (strings .TrimSpace (output ), "\n " )
554+
555+ for line := range lines {
556+ if line == "" {
557+ continue
558+ }
559+
560+ if strings .HasPrefix (line , "ref:" ) {
561+ parts := strings .Fields (line )
562+ if len (parts ) >= 2 { //nolint:mnd
563+ ref := parts [1 ]
564+
565+ if after , ok := strings .CutPrefix (ref , "refs/heads/" ); ok {
566+ return after , nil
567+ }
568+ }
569+ }
570+ }
571+
572+ return "" , & WrappedError {
573+ Op : "git_ls_remote_symref" ,
574+ Context : "could not parse default branch from ls-remote output" ,
575+ Err : ErrNoMatchingReference ,
576+ }
577+ }
578+
579+ // SetRemoteHeadAuto runs `git remote set-head origin --auto` to update the local cached remote HEAD.
580+ // This makes future calls to GetDefaultBranchLocal faster.
581+ func (g * GitRunner ) SetRemoteHeadAuto (ctx context.Context ) error {
582+ if err := g .RequiresWorkDir (); err != nil {
583+ return err
584+ }
585+
586+ cmd := g .prepareCommand (ctx , "remote" , "set-head" , "origin" , "--auto" )
587+
588+ var stderr bytes.Buffer
589+
590+ cmd .Stderr = & stderr
591+
592+ if err := cmd .Run (); err != nil {
593+ return & WrappedError {
594+ Op : "git_remote_set_head" ,
595+ Context : stderr .String (),
596+ Err : ErrCommandSpawn ,
597+ }
598+ }
599+
600+ return nil
601+ }
602+
454603func (g * GitRunner ) prepareCommand (ctx context.Context , name string , args ... string ) * exec.Cmd {
455604 cmd := exec .CommandContext (ctx , g .GitPath , append ([]string {name }, args ... )... )
456605
0 commit comments