Surface current pack number during batched bootstrap · Entire

Surface current pack number during batched bootstrap

81df289main·

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

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: ...".