From b54447e2eb25e55880119a76658a9453830d220c Mon Sep 17 00:00:00 2001 From: Alisha Kawaguchi Date: Fri, 17 Jul 2026 12:24:37 -0700 Subject: [PATCH] fix(dispatch): surface merged work on feature branches with no own commits (ENT-1201) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `dispatch --local` returned an empty dispatch when run from a feature-branch worktree that has no commits of its own — the standard `marvin wt` / git-worktree setup where the branch sits on the up-to-date default branch. The local engine scopes a non-default branch to ..HEAD to isolate the branch's own work. When the branch has no unique commits that range is empty, so the reachable-checkpoint-trailer set is empty and every merged checkpoint is dropped — "no activity" reported while HEAD is sitting on a full window of merged work. ENT-1188 already recognized this empty-range problem but patched it only for the literal default branch, matched by name. The real predicate is "is there any branch-specific history to isolate?", so detect the empty range directly and fall back to summarizing everything reachable from HEAD — the same reasoning ENT-1188 applied to the default branch, generalized. Feature branches with their own commits keep the scoped ..HEAD view unchanged. A git failure while measuring the range is treated as non-empty, so the exclusion is preserved on error (never widens scope). Note: the default/cloud `entire dispatch` emptiness for entireio/cli is a separate backend `dispatches/generate` bug (the backend holds the checkpoints — its activity view reports them — but the dispatch generator returns 0); the CLI sends a correct request. That half is out of scope for this repo. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01XL2teZnsLbEw2xPSy2f9Rs Entire-Checkpoint: 01KXRRKGDK6VHPYCVE28QW1R8R --- cmd/entire/cli/dispatch/mode_local.go | 30 +++++++++-- cmd/entire/cli/dispatch/mode_local_test.go | 62 ++++++++++++++++++++++ 2 files changed, 89 insertions(+), 3 deletions(-) diff --git a/cmd/entire/cli/dispatch/mode_local.go b/cmd/entire/cli/dispatch/mode_local.go index 9659e0a8f5..e0797fb9f7 100644 --- a/cmd/entire/cli/dispatch/mode_local.go +++ b/cmd/entire/cli/dispatch/mode_local.go @@ -376,15 +376,39 @@ func reachableCheckpointIDsInRange(ctx context.Context, repoRoot, revRange strin // and leave the dispatch effectively empty. In that case we summarize // everything reachable from HEAD in the window, matching the server-side // dispatch. The base..HEAD exclusion only applies to feature branches. +// +// The same "nothing to exclude" reasoning holds for any branch whose base..HEAD +// range is empty — a feature-branch worktree freshly created off the up-to-date +// default branch (the standard git-worktree workflow), or a branch whose work +// has already merged into the base. There the exclusion would again drop all +// reachable in-window activity and report "no activity" despite HEAD clearly +// carrying merged work (ENT-1201). Detecting the empty range directly, rather +// than matching the default branch by name, covers both cases. func branchLocalRevRange(ctx context.Context, repoRoot, currentBranch string) string { + const headRev = "HEAD" base := defaultBranchRef(ctx, repoRoot) if base == "" { - return "HEAD" + return headRev } if currentBranch != "" && strings.TrimPrefix(base, "origin/") == currentBranch { - return "HEAD" + return headRev + } + if revRangeIsEmpty(ctx, repoRoot, base+".."+headRev) { + return headRev + } + return base + ".." + headRev +} + +// revRangeIsEmpty reports whether revRange contains no commits (i.e. HEAD has no +// commits the base lacks). A git failure is treated as non-empty so the caller +// keeps the base..HEAD exclusion — the conservative choice that never widens +// scope on error. +func revRangeIsEmpty(ctx context.Context, repoRoot, revRange string) bool { + out, ok := runGitOutput(ctx, repoRoot, "rev-list", "--count", revRange) + if !ok { + return false } - return base + "..HEAD" + return strings.TrimSpace(out) == "0" } // defaultBranchRef resolves the repository's default branch, preferring diff --git a/cmd/entire/cli/dispatch/mode_local_test.go b/cmd/entire/cli/dispatch/mode_local_test.go index 24ff20edee..2d51d08f08 100644 --- a/cmd/entire/cli/dispatch/mode_local_test.go +++ b/cmd/entire/cli/dispatch/mode_local_test.go @@ -600,6 +600,68 @@ func TestLocalMode_ImplicitCurrentBranchOnDefaultBranchIncludesMergedWork(t *tes } } +// TestLocalMode_ImplicitCurrentBranchFeatureBranchNoOwnCommitsIncludesMergedWork +// is the regression test for ENT-1201: dispatching from a feature-branch +// worktree that has no commits of its own (a fresh worktree branched off the +// up-to-date default branch, or a branch whose work has already merged into it) +// must still surface the default branch's in-window merged work reachable from +// HEAD. The bug: branchLocalRevRange returned ..HEAD for any +// non-default branch, which is EMPTY here, so the reachable-trailer set was +// empty and every merged checkpoint was dropped — the dispatch came back empty +// even though HEAD clearly carried the work. ENT-1188 fixed this only for the +// literal default branch (matched by name); this covers the general +// empty-range case, which the common worktree workflow hits constantly. +func TestLocalMode_ImplicitCurrentBranchFeatureBranchNoOwnCommitsIncludesMergedWork(t *testing.T) { + dir := t.TempDir() + stubGeneratedLocalDispatch(t) + testutil.InitRepo(t, dir) + testutil.WriteFile(t, dir, "seed.txt", "seed") + testutil.GitAdd(t, dir, "seed.txt") + testutil.GitCommit(t, dir, "initial") + addOriginRemote(t, dir) + + // Merged feature-branch work now sitting ON the default branch, carrying a + // checkpoint trailer and reachable from HEAD. The checkpoint's own branch is + // the (merged) feature branch, not the default branch. + testutil.WriteFile(t, dir, "feature.md", "ship it") + testutil.GitAdd(t, dir, "feature.md") + commitWithMessage(t, dir, trailers.FormatCheckpoint("feature work", mustCheckpointID(t, testCheckpointID))) + + createdAt := time.Now().UTC() + seedCommittedCheckpoint(t, dir, seededCheckpoint{ + id: testCheckpointID, + branch: "already-merged-feature", + createdAt: createdAt, + filesTouched: []string{"feature.md"}, + outcome: testLocalFallbackText, + }) + + // A fresh worktree branch off the default branch's tip: it shares HEAD with + // the default branch and has no commits of its own, so ..HEAD is + // empty. This is the standard `marvin wt new` / git-worktree setup. + testutil.GitCheckoutNewBranch(t, dir, "my-worktree-branch") + + oldNow := nowUTC + nowUTC = func() time.Time { return createdAt.Add(time.Hour) } + t.Cleanup(func() { nowUTC = oldNow }) + + t.Chdir(dir) + + got, err := Run(context.Background(), Options{ + Mode: ModeLocal, + Since: "7d", + Branches: []string{"my-worktree-branch"}, + ImplicitCurrentBranch: true, + }) + if err != nil { + t.Fatal(err) + } + if len(got.Repos) != 1 || len(got.Repos[0].Sections) == 0 || len(got.Repos[0].Sections[0].Bullets) == 0 || + got.Repos[0].Sections[0].Bullets[0].Text != testLocalFallbackText { + t.Fatalf("merged work missing from feature-branch-with-no-own-commits dispatch: %+v", got) + } +} + // TestLocalMode_IncludesReachableCheckpointMissingFromLocalStore is the other // half of the ENT-1188 fix: dispatch --local must summarize work reachable from // HEAD by commit trailer even when the checkpoint itself is absent from the