Skip to content

Commit 735228c

Browse files
committed
fix: Use more robust default branch detection in Git-based expressions
1 parent a47eae9 commit 735228c

File tree

2 files changed

+150
-9
lines changed

2 files changed

+150
-9
lines changed

cli/flags/shared/shared.go

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -251,15 +251,7 @@ func NewFilterFlags(l log.Logger, opts *options.TerragruntOptions) cli.Flags {
251251
l.Warnf("Warning: You have uncommitted changes. The --filter-affected flag may not include all your local modifications.")
252252
}
253253

254-
defaultBranch := "main"
255-
256-
if b, err := gitRunner.Config(ctx.Context, "init.defaultBranch"); err == nil && b != "" {
257-
l.Debugf("Using default branch discovered from git config: %s", b)
258-
259-
defaultBranch = b
260-
} else {
261-
l.Warnf("Failed to get default branch from `git config init.defaultBranch`, using main.")
262-
}
254+
defaultBranch := gitRunner.GetDefaultBranch(ctx.Context, l)
263255

264256
opts.FilterQueries = append(opts.FilterQueries, fmt.Sprintf("[%s...HEAD]", defaultBranch))
265257

internal/git/git.go

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

3334
const (
@@ -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+
454603
func (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

Comments
 (0)