From 3b4f49bbe56297f556000f4f5d1720411fbe08fc Mon Sep 17 00:00:00 2001 From: k3nny Date: Fri, 26 Jun 2026 23:18:51 +0200 Subject: [PATCH] feat(cli): auto-detect git branch as default context When no --branch, --tag, --source, or --var flags are given, glint now runs "git rev-parse --abbrev-ref HEAD" in the pipeline file's directory to determine the current branch. Falls back to "main" when the directory is not inside a git repository or the repo is in detached-HEAD state. This makes implicit context simulation accurate without requiring users to pass --branch on every invocation. Co-Authored-By: Claude Sonnet 4.6 --- cmd/glint/main.go | 69 ++++++++++++++++++++++++++++++++---------- cmd/glint/main_test.go | 28 +++++++++++++++++ 2 files changed, 81 insertions(+), 16 deletions(-) diff --git a/cmd/glint/main.go b/cmd/glint/main.go index 449610b..0af4b1f 100644 --- a/cmd/glint/main.go +++ b/cmd/glint/main.go @@ -44,6 +44,29 @@ var execCommandOutput = func(name string, args ...string) ([]byte, error) { return exec.Command(name, args...).Output() } +// gitBranchInDir is a variable so tests can mock branch detection. +var gitBranchInDir = func(dir string) ([]byte, error) { + cmd := exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD") + cmd.Dir = dir + return cmd.Output() +} + +// detectGitBranch returns the current git branch name by running +// "git rev-parse --abbrev-ref HEAD" in dir. Returns "" when dir is not +// inside a git repository, when the repo is in detached-HEAD state, or +// when git is not available. +func detectGitBranch(dir string) string { + out, err := gitBranchInDir(dir) + if err != nil { + return "" + } + b := strings.TrimSpace(string(out)) + if b == "" || b == "HEAD" { + return "" + } + return b +} + // gitDiffFiles runs "git diff --name-only " and returns the list of changed // file paths. Returns nil + error when the command fails (e.g. not in a git repo // or the ref doesn't exist). @@ -232,8 +255,10 @@ Options: Print help Note: when none of --branch, --tag, --source, or --var are given, glint -defaults to --branch main --source push so that rules:if: expressions are -always evaluated. +detects the current git branch automatically and uses it as the default +(falling back to 'main' when not inside a git repository or in detached-HEAD +state). --source defaults to 'push' so that rules:if: expressions are always +evaluated. Examples: glint check .gitlab-ci.yml @@ -260,12 +285,6 @@ Examples: } _ = fs.Parse(args) - // Apply implicit defaults only in single-context mode when no flags are given. - if len(contexts) == 0 && *branch == "" && *tag == "" && *source == "" && len(vars) == 0 { - *branch = "main" - *source = "push" - } - validFormats := map[string]bool{ "text": true, "json": true, "sarif": true, "junit": true, "github": true, } @@ -283,6 +302,17 @@ Examples: path := fs.Arg(0) rootDir := filepath.Dir(filepath.Clean(path)) + // Apply implicit defaults only in single-context mode when no flags are given. + // Prefer the actual git branch of the repository; fall back to "main". + if len(contexts) == 0 && *branch == "" && *tag == "" && *source == "" && len(vars) == 0 { + if detected := detectGitBranch(rootDir); detected != "" { + *branch = detected + } else { + *branch = "main" + } + *source = "push" + } + // Load project config (.glint.yml), searching from the pipeline directory // up to the git root. glintCfg, cfgErr := config.Load(rootDir) @@ -586,8 +616,10 @@ Options: Print help Note: when none of --branch, --tag, --source, or --var are given, glint -defaults to --branch main --source push so that rules:if: expressions are -always evaluated. +detects the current git branch automatically and uses it as the default +(falling back to 'main' when not inside a git repository or in detached-HEAD +state). --source defaults to 'push' so that rules:if: expressions are always +evaluated. Examples: glint graph .gitlab-ci.yml @@ -618,12 +650,6 @@ Examples: changesFrom := fs.String("changes-from", "", "git ref to diff against for rules:changes: evaluation (e.g. HEAD~1, origin/main)") _ = fs.Parse(args) - // Apply implicit defaults when no context flag is given at all. - if *branch == "" && *tag == "" && *source == "" && len(vars) == 0 { - *branch = "main" - *source = "push" - } - if fs.NArg() != 1 { fs.Usage() exit(2) @@ -632,6 +658,17 @@ Examples: path := fs.Arg(0) rootDir := filepath.Dir(filepath.Clean(path)) + // Apply implicit defaults when no context flag is given at all. + // Prefer the actual git branch of the repository; fall back to "main". + if *branch == "" && *tag == "" && *source == "" && len(vars) == 0 { + if detected := detectGitBranch(rootDir); detected != "" { + *branch = detected + } else { + *branch = "main" + } + *source = "push" + } + glintCfg, cfgErr := config.Load(rootDir) if cfgErr != nil { fmt.Fprintf(os.Stderr, "%s: [warning] %s: %v\n", path, config.Filename, cfgErr) diff --git a/cmd/glint/main_test.go b/cmd/glint/main_test.go index 337155d..91c92db 100644 --- a/cmd/glint/main_test.go +++ b/cmd/glint/main_test.go @@ -1250,3 +1250,31 @@ func TestIsSuppressed(t *testing.T) { if !isSuppressed("all-job", "GL042", suppressions) { t.Error("wildcard should suppress") } if isSuppressed("unknown-job", "GL001", suppressions) { t.Error("unknown job: not suppressed") } } + +func TestDetectGitBranch(t *testing.T) { + orig := gitBranchInDir + t.Cleanup(func() { gitBranchInDir = orig }) + + tests := []struct { + name string + output string + err error + want string + }{ + {"normal branch", "main\n", nil, "main"}, + {"branch with trailing newline", "feature/my-branch\n", nil, "feature/my-branch"}, + {"detached HEAD", "HEAD\n", nil, ""}, + {"git error (not a repo)", "", errors.New("exit 128"), ""}, + {"empty output", "\n", nil, ""}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gitBranchInDir = func(_ string) ([]byte, error) { + return []byte(tt.output), tt.err + } + if got := detectGitBranch("."); got != tt.want { + t.Errorf("detectGitBranch() = %q, want %q", got, tt.want) + } + }) + } +}