44 "errors"
55 "fmt"
66 "strconv"
7+ "sync"
78
89 "github.com/roborev-dev/roborev/internal/git"
910 "github.com/spf13/cobra"
@@ -17,7 +18,7 @@ func waitCmd() *cobra.Command {
1718 )
1819
1920 cmd := & cobra.Command {
20- Use : "wait [job_id|sha]" ,
21+ Use : "wait [job_id|sha ... ]" ,
2122 Short : "Wait for an existing review job to complete" ,
2223 Long : `Wait for an already-running review job to complete, without enqueuing a new one.
2324
@@ -29,17 +30,21 @@ calls wait to block until the result is ready.
2930The argument can be a job ID (numeric) or a git ref (commit SHA, branch, HEAD).
3031If no argument is given, defaults to HEAD.
3132
33+ Multiple arguments can be given to wait for several jobs concurrently.
34+ With --job, all arguments are treated as job IDs.
35+
3236Exit codes:
33- 0 Review completed with verdict PASS
37+ 0 All reviews completed with verdict PASS
3438 1 Any failure (FAIL verdict, no job found, job error)
3539
3640Examples:
3741 roborev wait # Wait for most recent job for HEAD
3842 roborev wait abc123 # Wait for most recent job for commit
3943 roborev wait 42 # Job ID (if "42" is not a valid git ref)
4044 roborev wait --job 42 # Force as job ID
45+ roborev wait --job 10 20 30 # Wait for multiple job IDs
4146 roborev wait --sha HEAD~1 # Wait for job matching HEAD~1` ,
42- Args : cobra .MaximumNArgs ( 1 ) ,
47+ Args : cobra .ArbitraryArgs ,
4348 RunE : func (cmd * cobra.Command , args []string ) error {
4449 // In quiet mode, suppress cobra's error output
4550 if quiet {
@@ -55,6 +60,11 @@ Examples:
5560 return fmt .Errorf ("--job requires a job ID argument" )
5661 }
5762
63+ // Multiple args: wait for all concurrently
64+ if len (args ) > 1 {
65+ return waitMultiple (cmd , args , forceJobID , quiet )
66+ }
67+
5868 // Resolve the target to a job ID (local validation first,
5969 // daemon contact deferred until actually needed)
6070 var jobID int64
@@ -151,8 +161,145 @@ Examples:
151161 }
152162
153163 cmd .Flags ().StringVar (& shaFlag , "sha" , "" , "git ref to find the most recent job for" )
154- cmd .Flags ().BoolVar (& forceJobID , "job" , false , "force argument to be treated as job ID " )
164+ cmd .Flags ().BoolVar (& forceJobID , "job" , false , "force arguments to be treated as job IDs " )
155165 cmd .Flags ().BoolVarP (& quiet , "quiet" , "q" , false , "suppress output (for use in hooks)" )
156166
157167 return cmd
158168}
169+
170+ // resolvedArg holds locally-validated arg data before daemon contact.
171+ type resolvedArg struct {
172+ jobID int64 // set if arg is a numeric job ID
173+ sha string // set if arg is a git ref (resolved SHA)
174+ ref string // original ref string (for error messages)
175+ }
176+
177+ // waitMultiple resolves multiple args to job IDs and waits for all
178+ // concurrently. Returns exit code 1 if any job fails or has a FAIL
179+ // verdict.
180+ func waitMultiple (
181+ cmd * cobra.Command ,
182+ args []string ,
183+ forceJobID , quiet bool ,
184+ ) error {
185+ // Phase 1: Local validation (no daemon contact).
186+ repoRoot , _ := git .GetRepoRoot ("." )
187+ resolved := make ([]resolvedArg , 0 , len (args ))
188+ for _ , arg := range args {
189+ if forceJobID {
190+ id , err := strconv .ParseInt (arg , 10 , 64 )
191+ if err != nil || id <= 0 {
192+ return fmt .Errorf ("invalid job ID: %s" , arg )
193+ }
194+ resolved = append (resolved , resolvedArg {jobID : id })
195+ } else {
196+ // Try git ref first
197+ var sha string
198+ if repoRoot != "" {
199+ if s , err := git .ResolveSHA (repoRoot , arg ); err == nil {
200+ sha = s
201+ }
202+ }
203+ if sha != "" {
204+ resolved = append (resolved , resolvedArg {sha : sha , ref : arg })
205+ } else {
206+ id , err := strconv .ParseInt (arg , 10 , 64 )
207+ if err != nil || id <= 0 {
208+ return fmt .Errorf (
209+ "argument %q is not a valid git ref or job ID" ,
210+ arg ,
211+ )
212+ }
213+ resolved = append (resolved , resolvedArg {jobID : id })
214+ }
215+ }
216+ }
217+
218+ // Phase 2: Ensure daemon is running.
219+ if err := ensureDaemon (); err != nil {
220+ return fmt .Errorf ("daemon not running: %w" , err )
221+ }
222+
223+ // Phase 3: Resolve git refs to job IDs via daemon.
224+ jobIDs := make ([]int64 , 0 , len (resolved ))
225+ for _ , r := range resolved {
226+ if r .jobID != 0 {
227+ jobIDs = append (jobIDs , r .jobID )
228+ continue
229+ }
230+ mainRoot , _ := git .GetMainRepoRoot ("." )
231+ if mainRoot == "" {
232+ mainRoot = repoRoot
233+ }
234+ job , err := findJobForCommit (mainRoot , r .sha )
235+ if err != nil {
236+ return err
237+ }
238+ if job == nil {
239+ if ! quiet {
240+ cmd .Printf ("No job found for %s\n " , r .ref )
241+ }
242+ cmd .SilenceErrors = true
243+ cmd .SilenceUsage = true
244+ return & exitError {code : 1 }
245+ }
246+ jobIDs = append (jobIDs , job .ID )
247+ }
248+
249+ addr := getDaemonAddr ()
250+
251+ // Wait for all jobs concurrently.
252+ // Always poll in quiet mode to avoid interleaved output from
253+ // goroutines; we display results serially after all complete.
254+ type result struct {
255+ jobID int64
256+ err error
257+ }
258+ results := make ([]result , len (jobIDs ))
259+ var wg sync.WaitGroup
260+ for i , id := range jobIDs {
261+ wg .Add (1 )
262+ go func (idx int , jobID int64 ) {
263+ defer wg .Done ()
264+ err := waitForJob (cmd , addr , jobID , true )
265+ results [idx ] = result {jobID : jobID , err : err }
266+ }(i , id )
267+ }
268+ wg .Wait ()
269+
270+ // Report results serially.
271+ var hasErr bool
272+ for _ , r := range results {
273+ if r .err == nil {
274+ continue
275+ }
276+ hasErr = true
277+ if ! quiet {
278+ switch {
279+ case errors .Is (r .err , ErrJobNotFound ):
280+ cmd .Printf ("Job %d: no job found\n " , r .jobID )
281+ case isExitCode (r .err , 1 ):
282+ cmd .Printf ("Job %d: review has issues\n " , r .jobID )
283+ default :
284+ cmd .Printf ("Job %d: %v\n " , r .jobID , r .err )
285+ }
286+ }
287+ }
288+
289+ if hasErr {
290+ cmd .SilenceErrors = true
291+ cmd .SilenceUsage = true
292+ return & exitError {code : 1 }
293+ }
294+
295+ return nil
296+ }
297+
298+ // isExitCode reports whether err is an *exitError with the given code.
299+ func isExitCode (err error , code int ) bool {
300+ var e * exitError
301+ if errors .As (err , & e ) {
302+ return e .code == code
303+ }
304+ return false
305+ }
0 commit comments