fix(sync): keep the live progress counters correct and race-free

Two defects in the in-place progress line introduced with the reporting
work:

- started() only ran for repositories that actually synced, but
  finished() decrements for every repository that records a result. Any
  repository skipped by the eligibility or pre-sync checks therefore
  decremented a counter it had never incremented, so the running "active"
  count went negative and was displayed as such. Count a repository as
  active before the skip checks can exit.

- clear() mutates the line width but ran under outputMu, while draw()
  mutates the same field under the progress mutex. With parallel jobs one
  worker could erase the line while another repainted it. Route clear()
  through the same lock, keeping outputMu around the surrounding writes.

Verified by reverting the first fix and watching the new test report
"active = -3".

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
This commit is contained in:
2026-09-28 20:47:44 +01:00
parent f4286edff2
commit a221c63d94
2 changed files with 51 additions and 3 deletions
+6 -3
View File
@@ -484,6 +484,10 @@ func syncSelectedWithProgress(root string, repos, workspaceRepos []repo, jobs in
defer wg.Done()
for next := range work {
r := next.repo
// Counted active before any skip path can record: finished()
// decrements again, so an early exit that skipped started()
// would drive the active count negative.
live.update(func() { live.started(); live.draw() })
if !r.Eligible {
record(next.index, syncResult{Path: r.RelativePath, Skipped: true, Message: r.BlockReason})
continue
@@ -503,18 +507,17 @@ func syncSelectedWithProgress(root string, repos, workspaceRepos []repo, jobs in
}
if !quiet {
outputMu.Lock()
live.clear()
live.update(func() { live.clear() })
fmt.Fprintf(stdout, "\nSTART %s (%s)\n", r.RelativePath, r.Branch)
outputMu.Unlock()
}
live.update(func() { live.started(); live.draw() })
started := time.Now()
report, err := syncRepositoryWith(r.Path, timeout, len(fresh.Dirty) > 0, fresh.NestedRepoEntries, syncOptions{CreateMissing: createMissing})
message := report.Message
duration := time.Since(started).Round(time.Millisecond).String()
if !quiet {
outputMu.Lock()
live.clear()
live.update(func() { live.clear() })
state := "DONE"
if err != nil {
state = "FAIL"