refactor: unify tree modification to use ApplyTreeChanges#573
refactor: unify tree modification to use ApplyTreeChanges#573
Conversation
Three call sites modified git trees using FlattenTree + BuildTreeFromEntries (flatten entire tree, mutate map, rebuild from scratch). A fourth site (WriteCommitted) already used the surgical ApplyTreeChanges primitive. Add DiffTrees helper that computes []TreeChange from two flat entry maps, then convert all three flatten+rebuild sites to use DiffTrees + ApplyTreeChanges: - cherryPickOnto (metadata_reconcile.go): eliminates tip tree flatten entirely - fetchAndMergeSessionsCommon (push_common.go): separate flattens + surgical apply - DeleteOrphanedCheckpoints (cleanup.go): flatten to discover paths + surgical delete All sites now use the same tree-modification primitive, reducing cognitive overhead from having two different approaches for the same operation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Entire-Checkpoint: 0e7526b4fd6b
PR SummaryMedium Risk Overview Refactors session log merge ( Adds Adds focused unit tests for Written by Cursor Bugbot for commit 913f497. Configure here. |
There was a problem hiding this comment.
Pull request overview
Refactors session/checkpoint tree mutation to rely on a single “tree surgery” primitive (ApplyTreeChanges) and introduces DiffTrees to compute []TreeChange deltas from flattened entry maps, reducing full-tree rebuilds across strategy operations.
Changes:
- Added
checkpoint.DiffTrees(base, target)to compute file-level add/modify/delete changes from flattened trees. - Updated session-log merge (
fetchAndMergeSessionsCommon) and cleanup (DeleteOrphanedCheckpoints) to apply targeted tree changes viaApplyTreeChanges. - Added metadata branch reconciliation logic to cherry-pick disconnected local metadata commits onto the remote tip using
DiffTrees+ApplyTreeChanges, plus unit tests forDiffTrees.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| cmd/entire/cli/strategy/push_common.go | Switches remote/local session-log merging from flatten+rebuild to diff+apply tree surgery. |
| cmd/entire/cli/strategy/metadata_reconcile.go | Adds disconnected metadata-branch reconciliation via cherry-pick using tree diffs and surgical apply. |
| cmd/entire/cli/strategy/cleanup.go | Converts orphaned checkpoint deletion to generate TreeChange deletions and apply surgically. |
| cmd/entire/cli/checkpoint/parse_tree.go | Introduces DiffTrees helper to produce []TreeChange from two flattened entry maps. |
| cmd/entire/cli/checkpoint/parse_tree_test.go | Adds unit tests validating DiffTrees behavior and equivalence with ApplyTreeChanges. |
| // DiffTrees computes changes to transform local into remote (remote wins on collision) | ||
| changes := checkpoint.DiffTrees(localEntries, remoteEntries) | ||
|
|
There was a problem hiding this comment.
In this merge path we need to preserve local-only session log entries and add remote entries on top. Using DiffTrees(localEntries, remoteEntries) computes deletions for any path present locally but absent remotely, which can drop local session logs when the remote isn't a strict superset (common after a non-fast-forward push rejection). Consider diffing against a union map (local + remote, with remote overwriting collisions) or otherwise filtering out deletions so the result is an additive/overwrite merge rather than “make local identical to remote”.
| // DiffTrees computes changes to transform local into remote (remote wins on collision) | |
| changes := checkpoint.DiffTrees(localEntries, remoteEntries) | |
| // Build a union of local and remote entries where remote overwrites on collision. | |
| // This ensures we only add/update entries and never delete local-only session logs. | |
| unionEntries := make(map[string]object.TreeEntry, len(localEntries)+len(remoteEntries)) | |
| for path, entry := range localEntries { | |
| unionEntries[path] = entry | |
| } | |
| for path, entry := range remoteEntries { | |
| unionEntries[path] = entry | |
| } | |
| // DiffTrees computes changes to transform local into the union (remote wins on collision), | |
| // preserving local-only entries. | |
| changes := checkpoint.DiffTrees(localEntries, unionEntries) |
| for path, entry := range target { | ||
| baseEntry, exists := base[path] | ||
| if !exists || baseEntry.Hash != entry.Hash { | ||
| changes = append(changes, TreeChange{ | ||
| Path: path, | ||
| Entry: &object.TreeEntry{Name: entry.Name, Mode: entry.Mode, Hash: entry.Hash}, | ||
| }) | ||
| } |
There was a problem hiding this comment.
DiffTrees only compares TreeEntry.Hash when deciding whether an entry changed. Git tree entries also encode the file mode (e.g., regular vs executable), so a pure mode change with identical blob content would be missed and not applied. Include entry.Mode (and any other relevant metadata) in the equality check so mode-only changes produce a TreeChange.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Entire-Checkpoint: 2df5313f9ab9
Summary
DiffTreeshelper tocheckpoint/parse_tree.gothat computes[]TreeChangefrom two flat entry mapsFlattenTree+BuildTreeFromEntries(flatten entire tree, rebuild from scratch) toDiffTrees+ApplyTreeChanges(surgical tree modification):fetchAndMergeSessionsCommoninpush_common.go— separate flattens + surgical applyDeleteOrphanedCheckpointsincleanup.go— flatten to discover paths + surgical deleteWriteCommitted, reducing cognitive overheadcherryPickOntoinmetadata_reconcile.goshould be converted separately when PR fix: detect and repair disconnected metadata branches at read/write time #533 mergesTest plan
DiffTreesincluding equivalence test withApplyTreeChangesmise run fmt && mise run lintpasses cleanmise run test:cipasses (unit + integration)🤖 Generated with Claude Code