fix(explain): find checkpoints merged into non-default target branches#1727
Open
entire[bot] wants to merge 7 commits into
Open
fix(explain): find checkpoints merged into non-default target branches#1727entire[bot] wants to merge 7 commits into
entire[bot] wants to merge 7 commits into
Conversation
Merging a feature branch into a target branch dropped every session reference (checkpoint links and agent labels) from the target when the merge produced a merge commit and the target was not the repository's default branch. getBranchCheckpoints and getAssociatedCommits scanned the branch with a first-parent-only walk on every non-default branch. Checkpoints brought in by a merge live on the merge commit's second parent, which a first-parent walk never visits, so `entire checkpoint list` and `entire checkpoint explain` showed nothing on the target branch. The earlier default-branch fix restored a full DAG walk only for the repo default branch, leaving every other merge target broken -- a release branch, or even main when git resolves the default branch elsewhere. Replace the first-parent walk on non-default branches with a DAG walk (walkBranchOwnCommits) that follows every parent not reachable from the default branch. This discovers checkpoints merged in via second parents while still pruning the default branch's own history at the merge base -- the property that motivated first-parent traversal in the first place, so "main merged into a feature branch" still excludes main's checkpoints. True fast-forward merges were already correct (the moved commits and their trailers become the target's linear history); this covers the merge-commit case the fast-forward path could not. Fixes #931
suhaanthayyil
marked this pull request as ready for review
July 12, 2026 01:20
Contributor
There was a problem hiding this comment.
Pull request overview
Fixes checkpoint discovery on non-default target branches when work is merged via a merge commit (so checkpoint trailers live on non-first-parent ancestry). This restores entire checkpoint list / entire checkpoint explain visibility for merged work on branches like release.
Changes:
- Replace non-default-branch first-parent traversal with a branch-scoped DAG walk (
walkBranchOwnCommits) that follows non-default history including merge second parents while pruning the default branch’s spine. - Update
getAssociatedCommitsandgetBranchCheckpointsto use the new walk on non-default branches. - Add/adjust tests covering non-default merge-target behavior and updated associated-commit expectations.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| cmd/entire/cli/explain.go | Introduces walkBranchOwnCommits and uses it to find checkpoint trailers on non-default branches through merge second parents. |
| cmd/entire/cli/explain_test.go | Updates expectations for associated commits and adds a regression test for non-default merge targets. |
Comments suppressed due to low confidence (1)
cmd/entire/cli/explain.go:1404
- getAssociatedCommits now uses walkBranchOwnCommits, which does not guarantee commit order (it’s a BFS over parents). The returned slice is used to pick the author from commits[0] and is also displayed to users, so non-deterministic / non-recency ordering can produce incorrect author attribution and confusing output. Sort the collected commits by committer time (descending) before returning so both code paths are stable.
err = walkBranchOwnCommits(ctx, repo, head.Hash(), reachableFromMain, commitScanLimit, func(c *object.Commit) error {
cpID, found := trailers.ParseCheckpoint(c.Message)
if found && cpID.String() == targetID {
collectCommit(c)
}
return nil
})
Strengthen the issue #931 regression tests so the branch-scoped walk (walkBranchOwnCommits) is verified in both directions — merged-in discovery and default-branch exclusion — and add adversarial merge topologies. - Anchor the "release" target at master's tip in TestGetBranchCheckpoints_NonDefaultTargetFindsMergedCheckpoints so the master-only checkpoint is a genuine first-parent ancestor. It previously branched from the base commit, leaving the "must not leak" assertion vacuous (that checkpoint was unreachable regardless of pruning); the assertion now fails if the reachable-from-default prune is removed. - Add TestGetBranchCheckpoints_MainMergedIntoFeatureExcludesMainCheckpoint: merging the default branch INTO a feature branch must not surface the default branch's checkpoints, which arrive on the merge's second parent. - Add TestGetBranchCheckpoints_OctopusMergeIntoNonDefaultTarget: an octopus merge (three feature parents) surfaces every merged checkpoint. - Add TestGetBranchCheckpoints_NestedMergeIntoNonDefaultTarget: a transitively merged sub-feature checkpoint is discovered. Each exclusion assertion fails when the prune is removed and each discovery assertion fails under a first-parent-only walk, so the traversal scoping is locked down in both directions.
…rge-aware Two read-path gaps in the issue #931 area, both mutation-verified. computeReachableFromMain walked only the last MaxCommitTraversalDepth (1000) commits of main's first-parent chain. A branch that merged main in long ago -- with main since advanced past that bound -- left the merged-in former-main-tip outside the reachable set, so walkBranchOwnCommits descended into main's older history and surfaced main's checkpoints on the branch. Walk main's entire first-parent chain (no depth bound); this runs only on the read path (checkpoint list/explain), so the linear scan is acceptable. isShadowBranchReachable (the ephemeral/shadow-branch path) still used a first-parent-only walk. A shadow branch whose base commit entered history via a merge's second parent -- an in-progress session on a feature branch merged into a non-default target -- was judged unreachable and its temporary checkpoints dropped, the ephemeral counterpart of #931. Replace it with a full-DAG reachability walk (isCommitReachable), consistent with the committed-checkpoint path's merge awareness. Tests: deep-history leak (main advanced past the old bound); shadow base on a merge second parent (unit + end-to-end via the ephemeral store). Each fails under its pre-fix behavior (depth-limited set; first-parent-only reachability).
…il-safe computeReachableFromMain walked main's entire first-parent history -- a prior change removed the depth bound to stop a deep-history leak. That turned a bounded read-path step into an O(main) traversal (~0.6s at 10k commits, ~5s at 30k) run synchronously on every checkpoint list/explain. Restore a bounded scan (mainSpineScanLimit) and, when it stops before main's root, fall back to a commit-date frontier: any commit at or before the oldest scanned main commit is treated as main's history. This is the same commit-date boundary git's rev-list uses for main..HEAD; it keeps the deep-history leak closed (a former main tip merged in beyond the scanned window is older than the frontier, so it is still pruned) without the unbounded walk. First-parent-only semantics are unchanged, so merged-in feature checkpoints are still shown. The scan is ~60ms regardless of main length and runs once per command. The fail-safe errs toward exclusion: past the bound it may hide a branch-unique commit older than the frontier rather than risk leaking main's history. Tests: deep-history no-leak (main advanced past the bound; the frontier fail-safe prunes the old main checkpoint), scan-is-bounded (at most mainSpineScanLimit scanned regardless of main length), and once-per-call. The first fails without the fail-safe; the second fails with an unbounded scan.
The ephemeral (shadow-branch) reachability check used a full-DAG walk with no default-branch pruning, unlike the committed-checkpoint path. Once a branch merged the default branch in, a shadow branch based on the default branch's own history became reachable, so in-progress checkpoints from a session created while working on the default branch leaked onto an unrelated branch's checkpoint list -- the false-positive counterpart of the earlier ephemeral false-negative fix. Make isShadowBranchReachable reuse walkBranchOwnCommits (the committed path's walk), so the ephemeral and committed paths agree: a base merged in via a merge commit's second parent is still found (the issue #931 ephemeral fix is preserved), while a base on the default branch's own history is pruned (sharedWithMain) and does not leak. The shared-with-main test is computed once in getBranchCheckpoints and reused by both walks. Tradeoff (documented in isShadowBranchReachable): the shadow base is only the session's starting anchor, so a session started at a commit shared with the default branch -- e.g. a brand-new feature branch before its first own commit -- is treated as shared and its in-progress checkpoints are not shown until the branch has a commit of its own. This is the safe direction (never leak one branch's in-progress session data onto another) and cannot be avoided at read time: the shadow branch records only base commit + worktree, not the branch it was created on, so a default-branch session and a feature session that start at the same shared commit are indistinguishable. Tests: a default-branch session's shadow does not leak onto feature/x after a merge while the feature session's shadow still shows; the unit reachability test now asserts a shared base is excluded. Both fail under a non-pruning full-DAG walk.
Issue #931 is a committed-checkpoint read-path bug. Earlier commits on this branch also changed the ephemeral (shadow-branch) reachability path (isShadowBranchReachable) -- first to a full-DAG walk, then to reuse walkBranchOwnCommits. Each swapped one shadow-path harm for another: the full-DAG walk leaks a default-branch session's in-progress checkpoints onto a branch that merged the default branch in, while pruning shared-with-main hides the active session on a brand-new branch (the common "checkout -b then start coding" workflow, before the first commit). The two harms are irreducible without session-origin metadata that isn't persisted today, which is a feature-sized change to the write path and out of scope for #931. Revert isShadowBranchReachable and getReachableTemporaryCheckpoints to the first-parent-only behavior that shipped before this branch, so the PR introduces no shadow-path change (neither the leak nor the fresh-branch hiding). Tracked as follow-up #1730. The committed-checkpoint #931 fix is unchanged: walkBranchOwnCommits for committed checkpoints, the bounded main first-parent scan with a commit-date frontier, and their tests. This commit also removes the shadow-specific tests that asserted the reverted behavior.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Trail: https://entire.io/gh/entireio/cli/trails/834
Fixes #931.
Summary
Merging a feature branch into a target branch dropped every session reference — checkpoint links and agent labels — from the target branch whenever the merge produced a merge commit and the target was not the repository's default branch.
entire checkpoint listandentire checkpoint explainshowed nothing for the merged work on that branch.Root cause
getBranchCheckpointsandgetAssociatedCommitsscan history forEntire-Checkpointtrailers. On every branch that is not the repository default, they used a first-parent-only walk. Checkpoints brought in by a merge live on the merge commit's second parent, which a first-parent walk never visits — so the references vanished from the target branch.A prior fix restored a full DAG walk, but only for the repository default branch (
isOnDefault). Every other merge target stayed broken:releasebranch, ormainitself, when git resolves the default branch elsewhere (e.g.origin/HEADstill points atmaster).True fast-forward merges were already fine — the moved commits and their trailers become the target's linear (first-parent) history — which is why the symptom tracked "merge commit into a non-default target" rather than literally fast-forward.
Fix
Replace the first-parent walk on non-default branches with
walkBranchOwnCommits: a DAG walk that follows every parent not reachable from the default branch, pruning (never descending into) commits shared with the default branch.commitScanLimit; zero new cost on write/commit/push paths.One adjacent read-path gap closed alongside it
computeReachableFromMainmarked only the most recentMaxCommitTraversalDepth(1000) commits of main's first-parent chain. A branch that merged main in long ago — with main since advanced past that bound — left the merged-in former-main-tip outside the set, so the walk descended into main's older history and surfaced main's checkpoints on the branch (the leak invariant, in reverse). The scan stays bounded (mainSpineScanLimit, ~60ms regardless of main length — an unbounded scan was multiple seconds on a repo with tens of thousands of commits and runs on every read); when it stops before main's root it falls back to a commit-date frontier — any commit at or before the oldest scanned main commit is treated as main's history. This is the same commit-date boundary git's ownrev-listuses formain..HEAD: it keeps the deep-history leak closed (a former main tip merged in beyond the window is older than the frontier, so still pruned) while erring toward exclusion (past the bound it may hide a branch-unique commit older than the frontier rather than risk a leak). First-parent-only semantics are preserved, so merged-in feature commits are still shown, and the scan runs once per command.Out of scope: the ephemeral (shadow-branch) counterpart
The same "first-parent misses a merge's second parent" shape exists in the temporary / in-progress (shadow-branch) read path (
isShadowBranchReachable). Making it merge-aware trades one harm for another — a full-DAG walk leaks a default-branch session's in-progress checkpoints onto a branch that merged the default branch in, while pruning shared-with-main hides the active session on a brand-new branch (checkout -bthen start coding, before the first commit). The two are the same topological case and irreducible without persisting the session's origin branch, a feature-sized write-path change. This PR therefore leaves the shadow path at its pre-existing first-parent-only behavior (isShadowBranchReachableis byte-identical tomain) and tracks the fix as follow-up #1730. #931 itself is a committed-checkpoint read-path bug.Testing
go test ./cmd/entire/cli/... ./cmd/entire/cli/strategy/...green;gofmt -sandgolangci-lint(0 issues) clean; affected tests also pass under-race.Traversal scoping is locked down in both directions — merged-in discovery and default-branch exclusion — and every case below is mutation-verified: removing the reachable-from-default prune fails every exclusion assertion; reverting to a first-parent-only walk fails every discovery assertion; restoring the depth limit fails the deep-history leak test.
TestGetBranchCheckpoints_NonDefaultTargetFindsMergedCheckpoints— the merged checkpoint is found on a non-default target, and a default-only checkpoint does not leak. The target is anchored at the default branch's tip so that default-only checkpoint is a genuine first-parent ancestor (previously it branched from the base commit, which made the "must not leak" assertion vacuous — it passed even with the prune removed).TestGetBranchCheckpoints_MainMergedIntoFeatureExcludesMainCheckpoint— merging the default branch INTO a feature branch does not surface the default branch's checkpoints (they arrive on the merge's second parent).TestGetBranchCheckpoints_OctopusMergeIntoNonDefaultTarget— an octopus merge (three feature parents) surfaces every merged checkpoint; default-branch checkpoints stay excluded.TestGetBranchCheckpoints_NestedMergeIntoNonDefaultTarget— a transitively merged sub-feature checkpoint (sub-feature → feature A → release) is discovered.TestGetBranchCheckpoints_DeepHistoryDoesNotLeakMainCheckpoint— with main advanced past the scan bound, an old main checkpoint merged in via a former-main-tip does not leak onto the branch (the commit-date frontier fail-safe prunes it). Fails without the fail-safe.TestComputeReachableFromMain_ScanIsBounded— the main first-parent scan visits at mostmainSpineScanLimitcommits regardless of main length (no O(main) read-path blowup). Fails with an unbounded scan.TestGetBranchCheckpoints_ComputesReachabilityOncePerCall— the scan runs once pergetBranchCheckpointscall, not once per checkpoint.Live before/after — same repo, non-default target
release, feature merged via--no-ffVerified end-to-end via the deterministic
vogonhook-replay agent (real session hooks → shadow branch → condensed checkpoint onentire/checkpoints/v1), running a pre-fix binary and the fixed binary against the same repo.Committed checkpoint (baseline pre-fix → fixed):
Invariant re-checked live: on a feature branch with the default branch merged in (
--no-ff), both the branch-point and the merged-in default-branch checkpoints stay excluded — the fixed and pre-fix binaries agree, confirming no leakage was introduced. The shadow (in-progress) path is unchanged frommain, so the fresh-branch "checkout -bthen start coding" session still shows exactly as before.Edge matrix (live, isolated scratch repos)
--no-ffmerge commit (the bug)--ff-onlymain),--no-ffand divergent merge