Declare all pushed checkpoints as haves to avoid re-fetching ancestry · Entire

Declare all pushed checkpoints as haves to avoid re-fetching ancestry

67849a6→main·

Soph·2mo ago·2 files·+133 added/-9 removed

With topological ordering, individual first-parent steps can land on a merge whose other parent's ancestry was already pushed in earlier iterations as its own side-branch checkpoint. Previously each fetch only declared the most recent temp-ref position as a have, so the source had to re-send those side-branch objects with every merge.

Track every successfully-pushed checkpoint in a per-batch slice and thread the full set into the fetch's haves on subsequent iterations. On resume, seed the slice from the chain prefix at-or-before current: those commits must have been pushed in prior runs to advance the temp ref, but in topo order they aren't necessarily ancestors of current (they can sit on side branches yet to be merged), so declaring just current misses them.

Sessions

46264228955fView transcript

[?
can you rebase soph/progress-indicators onto soph/smart-subdivisionClaude Code·1 step](/content/gh/entireio/git-sync/session/3ee1ca7a-a436-44c1-906a-a912c6d33f96#timeline-46264228955f/index.html)

Changes

2

333 unmodified lines

334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
25 unmodified lines

384
385
386
368
387
388
389
390
173 unmodified lines

564
565
566
567
568
569
570
594 unmodified lines

1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1153
1200
1201
1202
1203
1204
1158
1159
1160
1161
1162
1163
1164
1205
1206
1207
1208

333 unmodified lines

startIdx = 0
    }

// Track every commit known to be in the target so the source
    // can use them all as fetch haves. With topo ordering a merge's
    // "delta" otherwise drags in the full ancestry of the non-first
    // parent, even though we pushed those commits as their own
    // checkpoints earlier in the chain (or in earlier runs).
    //
    // On resume, seed from the chain prefix at-or-before `current`:
    // every prior topo iteration must have pushed those commits to
    // advance the temp ref to its current position. Declaring just
    // `current` is insufficient because in topo order the earlier
    // chain entries aren't necessarily its ancestors — they can sit
    // on side branches that only get merged later.
    pushedCheckpoints := make([]plumbing.Hash, 0, len(batch.chain))
    if !current.IsZero() && len(batch.chain) > 0 {
        if idx := chainPosition(batch.chain, current); idx >= 0 {
            pushedCheckpoints = append(pushedCheckpoints, batch.chain[:idx+1]...)
        }
    }

// Manual index loop: subdivide may insert checkpoints at the current
    // index, so we must not auto-increment after a retry.
    idx := startIdx
25 unmodified lines

})

packReader, err := packReaderForCheckpoint(ctx, p, batch, checkpoint, current, completedRefs, fetchLimit)
        packReader, err := packReaderForCheckpoint(ctx, p, batch, checkpoint, pushedCheckpoints, completedRefs, fetchLimit)
        if err != nil {
            return result, fmt.Errorf("fetch source batch pack for %s: %w", batch.Plan.TargetRef, err)
        }
173 unmodified lines

"batch", idx+1,
        "batch_total", len(batch.Checkpoints))
        current = checkpoint
        pushedCheckpoints = append(pushedCheckpoints, checkpoint)
        result.BatchCount++
        idx++
    }

return checkpoints
}

// buildCheckpointHaves merges every checkpoint already pushed in this
// batch with the already-completed branch tips into a single haves
// map for the next checkpoint fetch. Under topo ordering each
// checkpoint covers a different region of the graph: the latest one
// is on the chain's frontier, but earlier checkpoints capture side
// branches that aren't ancestors of the latest. Declaring all of
// them keeps merge commits' deltas to genuinely-new content instead
// of re-sending side-branch ancestry we already pushed.
//
// Each pushed checkpoint gets a synthetic ref name so the map
// deduplicates by position; the wire only carries the hash values.
func buildCheckpointHaves(
    tempRef plumbing.ReferenceName,
    pushedCheckpoints []plumbing.Hash,
    completedRefs map[plumbing.ReferenceName]plumbing.Hash,
) map[plumbing.ReferenceName]plumbing.Hash {
    haves := planner.CopyRefHashMap(completedRefs)
    for i, h := range pushedCheckpoints {
        if h.IsZero() {
            continue
        }
        name := plumbing.ReferenceName(fmt.Sprintf("%s-have-%d", tempRef, i))
         haves[name] = h
    }
    return haves
}

func packReaderForCheckpoint(
    ctx context.Context,
    p Params,
    batch plannedBatch,
    checkpoint plumbing.Hash,
    current plumbing.Hash,
    pushedCheckpoints []plumbing.Hash,
    completedRefs map[plumbing.ReferenceName]plumbing.Hash,
    batchLimit int64,
) (io.ReadCloser, error) {
    desired := singleGP(batch.Plan.SourceRef, batch.TempRef, checkpoint)
    // Merge the current checkpoint (within this branch) with any
    // already-completed branch tips so the source can delta-compress
    // against objects the target already has from previous branches.
    haves := planner.CopyRefHashMap(completedRefs)
    if !current.IsZero() {
        haves[batch.TempRef] = current
    }
    haves := buildCheckpointHaves(batch.TempRef, pushedCheckpoints, completedRefs)
    packReader, err := p.SourceService.FetchPack(ctx, p.SourceConn, desired, haves)
    if err != nil {
        return nil, fmt.Errorf("fetch checkpoint pack: %w", err)
    }