From b525b0f08fc4da3449b7ea82740c7629bb252758 Mon Sep 17 00:00:00 2001 From: Dai Ha Date: Sat, 15 Aug 2026 08:47:12 +0200 Subject: [PATCH] CB-576: hasUncommitted tolerates an already-gone worktree --- .../dev/ltms/bridged/session/GitWorktrees.java | 8 ++++++++ .../java/dev/ltms/bridged/session/Worktrees.java | 4 ++++ .../ltms/bridged/session/GitWorktreesTest.java | 15 +++++++++++++++ 3 files changed, 27 insertions(+) diff --git a/bridged/src/main/java/dev/ltms/bridged/session/GitWorktrees.java b/bridged/src/main/java/dev/ltms/bridged/session/GitWorktrees.java index 9dbcbd1..052b0c5 100644 --- a/bridged/src/main/java/dev/ltms/bridged/session/GitWorktrees.java +++ b/bridged/src/main/java/dev/ltms/bridged/session/GitWorktrees.java @@ -169,6 +169,14 @@ public final class GitWorktrees implements Worktrees { @Override public boolean hasUncommitted(String worktreePath) { + // A worktree that is already gone holds no work to lose, and it must not break teardown: + // git -C status exits non-zero and would throw where release() is mid-way + // through stopping a pane. Mirror remove()'s already-gone tolerance by treating it as clean. + Path p = Path.of(worktreePath); + if (!Files.exists(p)) { + log.debug("worktree {} already gone — nothing can be uncommitted", worktreePath); + return false; + } // No --untracked-files=no: the exact shape of the work lost in CB-576 was a new file // that was never added, so an untracked-only worktree is still dirty. String out = exec("git", "-C", worktreePath, "status", "--porcelain"); diff --git a/bridged/src/main/java/dev/ltms/bridged/session/Worktrees.java b/bridged/src/main/java/dev/ltms/bridged/session/Worktrees.java index f858bf0..dd532ce 100644 --- a/bridged/src/main/java/dev/ltms/bridged/session/Worktrees.java +++ b/bridged/src/main/java/dev/ltms/bridged/session/Worktrees.java @@ -15,6 +15,10 @@ public interface Worktrees { * modifications, staged files, or untracked files. {@code git status --porcelain} is the * test; an empty result means clean. Callers use this to decide whether removing the * worktree would silently destroy a worker's only copy of its work. + * + *

An already-gone worktree is reported as clean (no throw), matching {@link #remove}'s + * idempotent contract: a path that does not exist holds no work to lose, and must not break + * a teardown that is mid-way through stopping the pane. */ boolean hasUncommitted(String worktreePath); diff --git a/bridged/src/test/java/dev/ltms/bridged/session/GitWorktreesTest.java b/bridged/src/test/java/dev/ltms/bridged/session/GitWorktreesTest.java index b5c6a83..8dd65aa 100644 --- a/bridged/src/test/java/dev/ltms/bridged/session/GitWorktreesTest.java +++ b/bridged/src/test/java/dev/ltms/bridged/session/GitWorktreesTest.java @@ -200,6 +200,21 @@ class GitWorktreesTest { "a tracked modification must also count as dirty"); } + /** + * CB-576 review. {@code hasUncommitted} must tolerate a missing worktree exactly like + * {@code remove}: an already-gone directory holds no work to lose, and throwing here would + * break teardown — SessionManager.release() calls it before stopping the pane, so an + * exception would orphan a live pane and skip the release notification (CB-516). + */ + @Test + void hasUncommittedOnAMissingWorktreeReturnsFalseWithoutThrowing(@TempDir Path tmp) { + GitWorktrees gitWorktrees = new GitWorktrees(tmp.resolve("wts").toString()); + String gone = tmp.resolve("wts").resolve("does-not-exist").toString(); + + assertFalse(gitWorktrees.hasUncommitted(gone), + "a missing worktree is reported clean, not an error"); + } + /** All three protected configs are covered: each one present in a worktree is neutralized and hidden. */ @Test void allThreeConfigsAreNeutralizedWhenPresent(@TempDir Path tmp) throws Exception {