Skip to content

Commit a5eea51

Browse files
wesmclaude
andauthored
Include branchless jobs in fix, set branch on analyze (#175)
## Summary - `roborev fix` was not finding analyze jobs because they had no branch set and the branch filter excluded them - `roborev analyze` now sets the branch field when enqueuing jobs - New `WithBranchOrEmpty` storage option includes jobs with empty/NULL branch, used only by the fix query path via `branch_include_empty=true` API param - `WithBranch` remains strict exact-match for all other callers (TUI, status, etc.) ## Files changed | File | What | |------|------| | `cmd/roborev/analyze.go` | Set `branch` from `git.GetCurrentBranch()` in `enqueueAnalysisJob` | | `cmd/roborev/fix.go` | Pass `branch_include_empty=true` in fix query | | `internal/storage/jobs.go` | Add `WithBranchOrEmpty` option (matches branch OR empty/NULL) | | `internal/daemon/server.go` | Support `branch_include_empty=true` query param | | `internal/storage/db_test.go` | `TestWithBranchOrEmpty` — strict excludes branchless, inclusive includes them | ## Test plan - [x] `go build ./...` - [x] `go test ./...` — all pass - [x] `TestWithBranchOrEmpty` — verifies both strict and inclusive behavior - [x] Verified: `fix` with branch=main now returns 19 jobs (was 1) for a repo with branchless analyze jobs 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent e1612df commit a5eea51

File tree

5 files changed

+72
-7
lines changed

5 files changed

+72
-7
lines changed

cmd/roborev/analyze.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,9 +437,11 @@ func buildOutputPrefix(analysisType string, filePaths []string) string {
437437

438438
// enqueueAnalysisJob sends a job to the daemon
439439
func enqueueAnalysisJob(repoRoot, prompt, outputPrefix, label string, opts analyzeOptions) (*storage.ReviewJob, error) {
440+
branch := git.GetCurrentBranch(repoRoot)
440441
reqBody, _ := json.Marshal(map[string]interface{}{
441442
"repo_path": repoRoot,
442443
"git_ref": label, // Use analysis type name as the TUI label
444+
"branch": branch,
443445
"agent": opts.agentName,
444446
"model": opts.model,
445447
"reasoning": opts.reasoning,

cmd/roborev/fix.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,7 @@ func queryUnaddressedJobs(repoRoot, branch string) ([]int64, error) {
373373
queryURL := fmt.Sprintf("%s/api/jobs?status=done&repo=%s&addressed=false&limit=0",
374374
serverAddr, url.QueryEscape(repoRoot))
375375
if branch != "" {
376-
queryURL += "&branch=" + url.QueryEscape(branch)
376+
queryURL += "&branch=" + url.QueryEscape(branch) + "&branch_include_empty=true"
377377
}
378378

379379
resp, err := http.Get(queryURL)

internal/daemon/server.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -596,7 +596,11 @@ func (s *Server) handleListJobs(w http.ResponseWriter, r *http.Request) {
596596
listOpts = append(listOpts, storage.WithGitRef(gitRef))
597597
}
598598
if branch := r.URL.Query().Get("branch"); branch != "" {
599-
listOpts = append(listOpts, storage.WithBranch(branch))
599+
if r.URL.Query().Get("branch_include_empty") == "true" {
600+
listOpts = append(listOpts, storage.WithBranchOrEmpty(branch))
601+
} else {
602+
listOpts = append(listOpts, storage.WithBranch(branch))
603+
}
600604
}
601605
if addrStr := r.URL.Query().Get("addressed"); addrStr == "true" || addrStr == "false" {
602606
listOpts = append(listOpts, storage.WithAddressed(addrStr == "true"))

internal/storage/db_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1759,6 +1759,51 @@ func TestListJobsWithBranchAndAddressedFilters(t *testing.T) {
17591759
})
17601760
}
17611761

1762+
func TestWithBranchOrEmpty(t *testing.T) {
1763+
db := openTestDB(t)
1764+
defer db.Close()
1765+
1766+
repo, err := db.GetOrCreateRepo("/tmp/repo-branch-empty")
1767+
if err != nil {
1768+
t.Fatalf("GetOrCreateRepo failed: %v", err)
1769+
}
1770+
1771+
// Create jobs: one on "main", one on "feature", one branchless
1772+
for i, br := range []string{"main", "feature", ""} {
1773+
sha := fmt.Sprintf("sha-be-%d", i)
1774+
commit, err := db.GetOrCreateCommit(repo.ID, sha, "Author", "Subject", time.Now())
1775+
if err != nil {
1776+
t.Fatalf("GetOrCreateCommit failed: %v", err)
1777+
}
1778+
job, err := db.EnqueueJob(repo.ID, commit.ID, sha, br, "codex", "", "")
1779+
if err != nil {
1780+
t.Fatalf("EnqueueJob failed: %v", err)
1781+
}
1782+
db.ClaimJob("w")
1783+
db.CompleteJob(job.ID, "codex", "", fmt.Sprintf("output %d", i))
1784+
}
1785+
1786+
t.Run("WithBranch strict excludes branchless", func(t *testing.T) {
1787+
jobs, err := db.ListJobs("", "", 50, 0, WithBranch("main"))
1788+
if err != nil {
1789+
t.Fatalf("ListJobs failed: %v", err)
1790+
}
1791+
if len(jobs) != 1 {
1792+
t.Errorf("Expected 1 job, got %d", len(jobs))
1793+
}
1794+
})
1795+
1796+
t.Run("WithBranchOrEmpty includes branchless", func(t *testing.T) {
1797+
jobs, err := db.ListJobs("", "", 50, 0, WithBranchOrEmpty("main"))
1798+
if err != nil {
1799+
t.Fatalf("ListJobs failed: %v", err)
1800+
}
1801+
if len(jobs) != 2 {
1802+
t.Errorf("Expected 2 jobs (main + branchless), got %d", len(jobs))
1803+
}
1804+
})
1805+
}
1806+
17621807
func TestReenqueueJob(t *testing.T) {
17631808
db := openTestDB(t)
17641809
defer db.Close()

internal/storage/jobs.go

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1098,21 +1098,31 @@ func (db *DB) GetJobRetryCount(jobID int64) (int, error) {
10981098
type ListJobsOption func(*listJobsOptions)
10991099

11001100
type listJobsOptions struct {
1101-
gitRef string
1102-
branch string
1103-
addressed *bool
1101+
gitRef string
1102+
branch string
1103+
branchIncludeEmpty bool
1104+
addressed *bool
11041105
}
11051106

11061107
// WithGitRef filters jobs by git ref.
11071108
func WithGitRef(ref string) ListJobsOption {
11081109
return func(o *listJobsOptions) { o.gitRef = ref }
11091110
}
11101111

1111-
// WithBranch filters jobs by branch name.
1112+
// WithBranch filters jobs by exact branch name.
11121113
func WithBranch(branch string) ListJobsOption {
11131114
return func(o *listJobsOptions) { o.branch = branch }
11141115
}
11151116

1117+
// WithBranchOrEmpty filters jobs by branch name, also including jobs
1118+
// with no branch set (empty string or NULL).
1119+
func WithBranchOrEmpty(branch string) ListJobsOption {
1120+
return func(o *listJobsOptions) {
1121+
o.branch = branch
1122+
o.branchIncludeEmpty = true
1123+
}
1124+
}
1125+
11161126
// WithAddressed filters jobs by addressed state (true/false).
11171127
func WithAddressed(addressed bool) ListJobsOption {
11181128
return func(o *listJobsOptions) { o.addressed = &addressed }
@@ -1151,7 +1161,11 @@ func (db *DB) ListJobs(statusFilter string, repoFilter string, limit, offset int
11511161
args = append(args, o.gitRef)
11521162
}
11531163
if o.branch != "" {
1154-
conditions = append(conditions, "j.branch = ?")
1164+
if o.branchIncludeEmpty {
1165+
conditions = append(conditions, "(j.branch = ? OR j.branch = '' OR j.branch IS NULL)")
1166+
} else {
1167+
conditions = append(conditions, "j.branch = ?")
1168+
}
11551169
args = append(args, o.branch)
11561170
}
11571171
if o.addressed != nil {

0 commit comments

Comments
 (0)