Skip to content

Commit 90333f2

Browse files
committed
fix: address roborev-ci review findings
- validateRefineContext: upstream resolution now happens only when --since is empty. --since provides the merge base explicitly, so an unfetched or configured-but-missing upstream must not block a valid --since invocation. - tryBranchReview: treat UpstreamMissingError as "skip branch review" (return "", false) instead of silently falling back to the default branch. Hooks must never block commits, but falling back after a missing-but-configured upstream would enqueue a review against the wrong commit range in fork workflows.
1 parent 5ba30f6 commit 90333f2

4 files changed

Lines changed: 107 additions & 28 deletions

File tree

cmd/roborev/refine.go

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -237,30 +237,6 @@ func validateRefineContext(
237237
)
238238
}
239239

240-
// Prefer the current branch's upstream tracking ref (matches
241-
// `git status` "ahead of upstream" semantics); fall back to the
242-
// repository default branch when no upstream is configured.
243-
upstream, uerr := git.GetUpstream(repoPath, "HEAD")
244-
var missing *git.UpstreamMissingError
245-
if errors.As(uerr, &missing) {
246-
return "", "", "", "",
247-
fmt.Errorf(
248-
"%w (run 'git fetch' or pass --since)", missing,
249-
)
250-
}
251-
if uerr == nil && upstream != "" {
252-
base = upstream
253-
}
254-
if base == "" {
255-
base, err = git.GetDefaultBranch(repoPath)
256-
if err != nil {
257-
return "", "", "", "",
258-
fmt.Errorf(
259-
"cannot determine default branch: %w", err,
260-
)
261-
}
262-
}
263-
264240
currentBranch = git.GetCurrentBranch(repoPath)
265241

266242
// --branch: validate the user is on the expected branch
@@ -273,6 +249,9 @@ func validateRefineContext(
273249
}
274250

275251
if since != "" {
252+
// --since provides an explicit merge base, so upstream/default-branch
253+
// resolution is unnecessary. Skip it so a misconfigured or unfetched
254+
// upstream doesn't block an otherwise-valid --since invocation.
276255
mergeBase, err = git.ResolveSHA(repoPath, since)
277256
if err != nil {
278257
return "", "", "", "",
@@ -299,6 +278,30 @@ func validateRefineContext(
299278
)
300279
}
301280
} else {
281+
// Prefer the current branch's upstream tracking ref (matches
282+
// `git status` "ahead of upstream" semantics); fall back to the
283+
// repository default branch when no upstream is configured.
284+
upstream, uerr := git.GetUpstream(repoPath, "HEAD")
285+
var missing *git.UpstreamMissingError
286+
if errors.As(uerr, &missing) {
287+
return "", "", "", "",
288+
fmt.Errorf(
289+
"%w (run 'git fetch' or pass --since)", missing,
290+
)
291+
}
292+
if uerr == nil && upstream != "" {
293+
base = upstream
294+
}
295+
if base == "" {
296+
base, err = git.GetDefaultBranch(repoPath)
297+
if err != nil {
298+
return "", "", "", "",
299+
fmt.Errorf(
300+
"cannot determine default branch: %w", err,
301+
)
302+
}
303+
}
304+
302305
if git.IsOnBaseBranch(repoPath, currentBranch, base) {
303306
return "", "", "", "", fmt.Errorf(
304307
"refusing to refine on %s branch "+

cmd/roborev/refine_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/roborev-dev/roborev/internal/agent"
1212
"github.com/roborev-dev/roborev/internal/config"
1313
"github.com/roborev-dev/roborev/internal/daemon"
14+
"github.com/roborev-dev/roborev/internal/git"
1415
"github.com/roborev-dev/roborev/internal/storage"
1516
"github.com/roborev-dev/roborev/internal/testutil"
1617
"github.com/stretchr/testify/assert"
@@ -563,6 +564,54 @@ func TestValidateRefineContext_AllowsMainBranchWithSince(t *testing.T) {
563564
assert.Equal(t, mergeBase, baseSHA)
564565
}
565566

567+
func TestValidateRefineContext_SinceIgnoresUpstreamMissing(t *testing.T) {
568+
if _, err := exec.LookPath("git"); err != nil {
569+
t.Skip("git not available")
570+
}
571+
572+
// --since provides the merge base explicitly, so an unfetched or
573+
// missing @{upstream} must not block a valid --since invocation.
574+
repo := testutil.InitTestRepo(t)
575+
baseSHA := repo.RevParse("HEAD")
576+
repo.CommitFile("second.txt", "second", "second commit")
577+
578+
// Configure tracking against an upstream that never resolves locally.
579+
repo.Run("config", "branch.main.remote", "upstream")
580+
repo.Run("config", "branch.main.merge", "refs/heads/main")
581+
582+
chdirForTest(t, repo.Root)
583+
584+
repoPath, currentBranch, _, mergeBase, err := validateRefineContext("", baseSHA, "")
585+
require.NoError(t, err, "--since should bypass upstream resolution: %v", err)
586+
587+
assert.NotEmpty(t, repoPath)
588+
assert.Equal(t, "main", currentBranch)
589+
assert.Equal(t, baseSHA, mergeBase)
590+
}
591+
592+
func TestValidateRefineContext_UpstreamMissingErrorsWithoutSince(t *testing.T) {
593+
if _, err := exec.LookPath("git"); err != nil {
594+
t.Skip("git not available")
595+
}
596+
597+
// Without --since, a configured-but-unfetched upstream must surface
598+
// as UpstreamMissingError rather than silently falling back to the
599+
// repository default branch (which could yield the wrong range).
600+
repo := testutil.InitTestRepo(t)
601+
repo.RunGit("checkout", "-b", "feature")
602+
repo.CommitFile("feature.txt", "feature", "feature commit")
603+
repo.Run("config", "branch.feature.remote", "upstream")
604+
repo.Run("config", "branch.feature.merge", "refs/heads/main")
605+
606+
chdirForTest(t, repo.Root)
607+
608+
_, _, _, _, err := validateRefineContext("", "", "")
609+
require.Error(t, err, "expected missing-upstream error without --since")
610+
611+
var missing *git.UpstreamMissingError
612+
assert.ErrorAs(t, err, &missing, "expected UpstreamMissingError, got: %v", err)
613+
}
614+
566615
func TestValidateRefineContext_SinceWorksOnFeatureBranch(t *testing.T) {
567616
if _, err := exec.LookPath("git"); err != nil {
568617
t.Skip("git not available")

cmd/roborev/review.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -494,10 +494,16 @@ func tryBranchReview(root, baseBranchOverride string) (string, bool) {
494494
base := baseBranchOverride
495495
if base == "" {
496496
// Prefer the branch's upstream tracking ref — matches `git status`
497-
// semantics. Hooks must never block commits, so any error here —
498-
// including an unresolvable @{upstream} — falls through silently to
499-
// the default-branch lookup.
500-
if upstream, err := git.GetUpstream(root, "HEAD"); err == nil && upstream != "" {
497+
// semantics. Hooks must never block commits, but a configured-yet-
498+
// unresolvable upstream means we cannot confidently pick a base;
499+
// falling back to the default branch here would enqueue a review
500+
// against the wrong commit range in fork workflows. Skip instead.
501+
upstream, err := git.GetUpstream(root, "HEAD")
502+
var missing *git.UpstreamMissingError
503+
if errors.As(err, &missing) {
504+
return "", false
505+
}
506+
if err == nil && upstream != "" {
501507
base = upstream
502508
}
503509
}

cmd/roborev/review_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -611,6 +611,27 @@ func TestTryBranchReview(t *testing.T) {
611611
assert.False(t, ok)
612612
})
613613

614+
t.Run("skips when upstream is configured but unresolvable", func(t *testing.T) {
615+
// Regression: when @{upstream} is configured but the remote-tracking
616+
// ref has not been fetched, tryBranchReview must skip rather than
617+
// silently fall back to origin/main or local main — that fallback
618+
// would enqueue a review against the wrong commit range in fork
619+
// workflows.
620+
repo := newTestGitRepo(t)
621+
repo.Run("symbolic-ref", "HEAD", "refs/heads/main")
622+
repo.CommitFile("file.txt", "content", "initial")
623+
repo.Run("checkout", "-b", "feature")
624+
repo.CommitFile("feature.txt", "feature", "feature commit")
625+
// Configure tracking against an upstream that never resolves locally.
626+
repo.Run("config", "branch.feature.remote", "upstream")
627+
repo.Run("config", "branch.feature.merge", "refs/heads/main")
628+
writeRoborevConfig(t, repo, `post_commit_review = "branch"`)
629+
630+
ref, ok := tryBranchReview(repo.Dir, "")
631+
assert.False(t, ok, "must skip when upstream is configured but missing")
632+
assert.Empty(t, ref)
633+
})
634+
614635
t.Run("blocks local main tracking non-origin upstream", func(t *testing.T) {
615636
// Regression: LocalBranchName only stripped "origin/", so
616637
// current="main" vs base="upstream/main" missed the guardrail

0 commit comments

Comments
 (0)