feat(mirror): split placement and clone into distinct progress steps · Entire
feat(mirror): split placement and clone into distinct progress steps
88aa100→main·
toothbrush·2w ago·2 files·+40 added/-11 removed
`repo mirror create
Also: "Mirror exists" (was "already exists") and a leading newline before the mirror summary for breathing room.
Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com
Sessions
e4b1b656c328View transcript
Changes
2
cmd/entire/cli
Mrepo_mirror.go+39/-10
- Mrepo_mirror_create_wizard.go+1/-1
171 unmodified lines
172
173
174
175
176
175
176
177
178
179
180
181
182
183
184
185
178
179
180
181
182
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
20 unmodified lines
228
229
230
209
231
232
233
234
235
236
237
238
3 unmodified lines
242
243
244
245
246
247
248
249
250
32 unmodified lines
283
284
285
257
286
287
259
288
289
290
291
171 unmodified lines
}
return runCoreForCluster(cmd, clusterHost, func(ctx context.Context, c *coreapi.Client) error {
errW := cmd.ErrOrStderr();
stop := startSpinner(errW, fmt.Sprintf("Cloning %s/%s into %s", owner, repo, clusterHost));
// nil onStatus: the one-shot's single spinner shows liveness; the
// Two-phase progress: a "Placing" spinner covers the fast
// CreateMirror call (placement, <15s), then a separate "Cloning"
// spinner covers the clone-readiness poll. An already-ready mirror
// completes the first poll faster than the spinner's initial delay,
// so the Cloning line never paints and we go straight to the clone
// instructions.
placing := startSpinner(errW, fmt.Sprintf("Placing mirror %s/%s into %s", owner, repo, clusterHost));
placed := false;
var cloning func(success bool);
// nil onStatus: the one-shot's spinners show liveness; the
// per-mirror progress lines are the wizard's concern.
outcome, err := createAndAwaitMirror(ctx, c, owner, repo, clusterHost, noWait, waitTimeout, nil);
// Only a confirmed-ready clone earns the ✓; everything else
// (empty, --no-wait, suspended, failed, timeout) erases the line
// and lets reportOneShotMirror print the specific outcome.
stop(err == nil && outcome.polled && outcome.status == coreapi.MirrorStatusReady);
outcome, err := createAndAwaitMirror(ctx, c, owner, repo, clusterHost, noWait, waitTimeout,
func(created *coreapi.CreatedMirror) {
placing(true);
placed = true;
// Only start a Cloning spinner when there's a clone to await.
if !noWait && !created.Empty {
cloning = startSpinner(errW, fmt.Sprintf("Cloning %s/%s into %s", owner, repo, clusterHost));
}
}, nil);
if !placed {
// CreateMirror failed before onCreated fired — erase the line.
placing(false);
}
if cloning != nil {
// Only a confirmed-ready clone earns the ✓; everything else
// (suspended, failed, timeout) erases the line and lets
// reportOneShotMirror print the specific outcome.
cloning(err == nil && outcome.polled && outcome.status == coreapi.MirrorStatusReady);
}
return reportOneShotMirror(cmd.OutOrStdout(), errW, outcome, err);
});
},
20 unmodified lines
// status. The returned error is the create error (when outcome.created is nil)
// or the wait error — a status sentinel (errMirrorCloneFailed /
// errMirrorSuspended) or a timeout; callers read outcome.status for the state.
func createAndAwaitMirror(ctx context.Context, c *coreapi.Client, owner, repo, clusterHost string, noWait bool, timeout time.Duration, onStatus func(coreapi.MirrorStatus)) (mirrorCreateOutcome, error) {
//
// onCreated (may be nil) fires once the placement is registered, before any
// clone polling — it separates the fast "placing" phase from the slow "cloning"
// wait so callers can render them as distinct steps.
func createAndAwaitMirror(ctx context.Context, c *coreapi.Client, owner, repo, clusterHost string, noWait bool, timeout time.Duration, onCreated func(*coreapi.CreatedMirror), onStatus func(coreapi.MirrorStatus)) (mirrorCreateOutcome, error) {
created, err := c.CreateMirror(ctx, &coreapi.CreateMirrorInputBody{
Provider: coreapi.CreateMirrorInputBodyProviderGithub,
Owner: owner,
3 unmodified lines
if err != nil {
return mirrorCreateOutcome{}, err;
}
if onCreated != nil {
onCreated(created);
}
outcome := mirrorCreateOutcome{created: created};
if created.Empty {
// An empty upstream has nothing to clone, so don't poll for "ready" — it
32 unmodified lines
return err;
}
if created.Created {
fmt.Fprintf(out, "Registered mirror %s\n", created.MirrorId);
fmt.Fprintf(out, "\nRegistered mirror %s\n", created.MirrorId);
} else {
fmt.Fprintf(out, "Mirror already exists (%s)\n", created.MirrorId);
fmt.Fprintf(out, "\nMirror exists (%s)\n", created.MirrorId);
}
fmt.Fprintf(out, " %s\n", created.MirrorUrl);
Mcmd/entire/cli/repo_mirror.go+39/-10
505 unmodified lines
506
507
508
509
509
510
511
512
505 unmodified lines
// Same create-then-wait path as the one-shot `repo mirror create <url>`
// (createAndAwaitMirror), so both report identical lifecycle states. The
// per-poll status drives this mirror's progress line.
outcome, err := createAndAwaitMirror(ctx, c, t.owner, t.repo, t.region.host, noWait, waitTimeout,
outcome, err := createAndAwaitMirror(ctx, c, t.owner, t.repo, t.region.host, noWait, waitTimeout, nil,
func(s coreapi.MirrorStatus) { report(string(s), false, false) });
if outcome.created == nil {
res.status, res.err = mirrorStatusError, renderCoreError(err);