Surface current pack number during batched bootstrap · Entire
Surface current pack number during batched bootstrap
81df289→main·
Bootstrap's checkpoint loop already knew it was on "batch 3 of 8" and logged it via slog. Pipe that through to the live --progress ticker so the user can see which packfile is in flight on a long batched bootstrap, not just the cumulative byte counter.
The path: statsCollector grows a setPhase / getPhase pair backed by atomic.Pointer[string]; bstrap.Params grows an OnPhase func hook that the syncer wires to setPhase; the renderer reads the phase and appends "(pack 3/8)" to live frames (suppressed on the final frame, where it would read as still-running). The same hook also covers the one-shot push ("pushing pack") and the post-batch tag push ("pushing tags"), so the user sees something meaningful during whichever bootstrap shape runs.
Sessions
35446bef46faView transcript
?\can you rebase soph/progress-indicators onto soph/smart-subdivisionClaude Code·Opus 4.7[1m]·1 step
Changes
5
internal
strategy/bootstrap
Mbootstrap.go+15
syncer
Mprogress.go+10
Mprogress_test.go+36
Mstats.go+19
Msyncer.go+1
59 unmodified lines
60
61
62
63
64
65
66
67
68
69
70
71
65 unmodified lines
137
138
139
140
141
142
143
144
145
162 unmodified lines
308
309
310
311
312
313
314
315
316
125 unmodified lines
442
443
444
445
446
447
448
449
450
59 unmodified lines
TargetMaxPack int64
Verbose bool
Logger *slog.Logger
// OnPhase, when non-nil, is called with a short human-readable label
// describing the current bootstrap activity (e.g. "pack 3/8") so a
// live progress renderer can surface what is currently in flight.
// Called from the goroutine driving Execute; implementations must not
// block.
OnPhase func(string)
}
// Result holds the outcome of the bootstrap strategy.
65 unmodified lines
packReader = closeOnce(packReader)
p.log("bootstrap pushing refs to target", "ref_count", len(plans))
if p.OnPhase != nil {
p.OnPhase("pushing pack")
}
cmds := convert.PlansToPushCommands(plans)
pushErr := p.TargetPusher.PushPack(ctx, cmds, packReader)
_ = packReader.Close()
162 unmodified lines
idx := startIdx
for idx < len(batch.Checkpoints) {
checkpoint := batch.Checkpoints[idx]
if p.OnPhase != nil {
p.OnPhase(fmt.Sprintf("pack %d/%d", idx+1, len(batch.Checkpoints)))
}
p.log("bootstrap batch push checkpoint",
"branch", batch.Plan.TargetRef.String(),
"batch", idx+1,
125 unmodified lines
// Tag phase (issue #1)
if len(tagPlans) > 0 {
p.log("bootstrap batch pushing tags after branch batches", "tag_count", len(tagPlans))
if p.OnPhase != nil {
p.OnPhase("pushing tags")
}
tagTargetRefs := planner.CopyRefHashMap(p.TargetRefs)
for _, batch := range batches {
tagTargetRefs[batch.Plan.TargetRef] = batch.Plan.SourceHash
``
Minternal/strategy/bootstrap/bootstrap.go+15
87 unmodified lines
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103
87 unmodified lines
} b.WriteString(formatSide(side, elapsed, final)) } // Surface the current activity label (e.g. "pack 3/8") on live // frames only. The final frame is implicitly "done" — appending // the last in-progress phase there would read as still-running. if !final { if phase := p.stats.getPhase(); phase != "" { b.WriteString(" (") b.WriteString(phase) b.WriteString(")") } } line := b.String()
p.mu.Lock()
Minternal/syncer/progress.go+10
168 unmodified lines
169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210
168 unmodified lines
}
func TestProgressReporterRendersPhase(t *testing.T) { t.Parallel() stats := newStats(true) stats.setSideDisplay("source", "github.com") stats.setSideDisplay("target", "example.test") stats.side("source").bytes.Store(1024) stats.side("target").bytes.Store(512) stats.setPhase("pack 3/8")
var buf bytes.Buffer p := newProgressReporter(&buf, stats, 0) p.render(false)
if !strings.Contains(buf.String(), "(pack 3/8)") { t.Errorf("live frame should include phase suffix: %q", buf.String()) } }
func TestProgressReporterFinalFrameOmitsPhase(t *testing.T) { t.Parallel() stats := newStats(true) stats.setSideDisplay("source", "github.com") stats.setSideDisplay("target", "example.test") stats.side("source").bytes.Store(1024) stats.side("target").bytes.Store(512) stats.setPhase("pack 3/8")
var buf bytes.Buffer p := newProgressReporter(&buf, stats, 0) p.render(true)
if strings.Contains(buf.String(), "pack 3/8") { t.Errorf("final frame should drop the in-flight phase: %q", buf.String()) } }
func TestFormatSideForceDoneAlwaysMarks(t *testing.T) { t.Parallel() side := SideBytes{
Minternal/syncer/progress_test.go+36
80 unmodified lines
81 82 83 84 85 86 87 88 89 90 91 47 unmodified lines
139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158
80 unmodified lines
items map[string]*ServiceStats sidesMu sync.RWMutex sides map[string]*sideCounter // phase carries an optional one-line activity label // (e.g. "pack 3/8") that the live progress reporter renders next // to the per-side counters. Updated atomically by strategies and // read by the reporter goroutine without contention. phase atomic.Pointer[string] }
func newStats(enabled bool) *statsCollector { 47 unmodified lines
return sc }
// setPhase records a short activity label that the live progress // reporter will surface alongside per-side counters. Pass "" to clear. func (s *statsCollector) setPhase(p string) { s.phase.Store(&p) }
// getPhase returns the most recent phase label, or "" if none was set. func (s *statsCollector) getPhase() string { if p := s.phase.Load(); p != nil { return *p } return "" }
// setSideDisplay attaches a human-readable name (typically the URL // hostname) to a side. Called once during session setup so the live // renderer can print "github.com → ..." instead of "source: ...".