fix: Existing Folder workflow reuses local repo instead of bare clone#527
fix: Existing Folder workflow reuses local repo instead of bare clone#527
Conversation
PureWeen
left a comment
There was a problem hiding this comment.
PR #527 Code Review
Focus: regressions, bugs, data loss, race conditions. No style comments.
🔴 Critical — RemoveRepositoryAsync can delete the user's real project
File: RepoManager.cs (the deleteFromDisk path in RemoveRepositoryAsync)
if (deleteFromDisk && Directory.Exists(repo.BareClonePath))
try { Directory.Delete(repo.BareClonePath, recursive: true); } catch { }When a repo was added via "Existing Folder", BareClonePath now points to the user's actual working directory (e.g. ~/projects/my-app). This will recursively delete the user's entire project. The guard in RemoveWorktreeAsync that checks against managed paths does NOT protect this separate deletion site.
Fix: Before deleting, check whether BareClonePath is under the managed ReposDir:
bool isManagedBareClone = repo.BareClonePath.StartsWith(ReposDir, StringComparison.OrdinalIgnoreCase);
if (deleteFromDisk && isManagedBareClone && Directory.Exists(repo.BareClonePath))
try { Directory.Delete(repo.BareClonePath, recursive: true); } catch { }🔴 High — Existing bare clone silently orphaned when same repo added via folder
File: RepoManager.cs, AddRepositoryFromLocalAsync
existing.BareClonePath = localPath; // overwrites e.g. ~/.polypilot/repos/owner-repo.gitIf the user previously added the same repo via URL (creating a managed bare clone), then adds it again via "Existing Folder", the bare clone at ~/.polypilot/repos/ is orphaned and never cleaned up. Worse, all existing worktrees created from that bare clone become disconnected — their .git files still reference the old bare clone's worktrees/ directory, but future git fetch, git worktree list, and git branch -D calls now run against the local path instead.
Fix: When existing.BareClonePath is non-null and points to a managed path (under ReposDir), either keep using it or explicitly clean up the old bare clone before overwriting.
🟠 High — BackfillWorktreeClonePaths propagates non-bare path to pre-existing worktrees
File: RepoManager.cs, called from AddRepositoryFromLocalAsync after setting BareClonePath = localPath
BackfillWorktreeClonePaths overwrites BareClonePath on every worktree that has a blank value. Worktrees created before this re-add get their BareClonePath silently redirected to the user's non-bare repo. When RemoveWorktreeAsync subsequently runs git worktree remove using this path, it operates on the user's local repo — potentially affecting worktrees the user manages outside of PolyPilot.
🟠 High — git worktree add on non-bare repo fails when branch is already checked out
File: RepoManager.cs, CreateWorktreeAsync
git worktree add errors with fatal: '<branch>' is already checked out at '<path>' if the user's repo has the same branch checked out. This is most likely to happen for the default branch (main). The worktree reuse code mitigates this for the registered local path, but any new branch creation that matches the parent's current branch will fail.
🟡 Medium — testhost in production FindActiveLockPid risks false positives
File: ExternalSessionScanner.cs
testhost is the .NET test runner. If a user runs dotnet test and PID recycling causes a stale lock file to contain the testhost PID, FindActiveLockPid returns a false positive — claiming a session is active when it's not, blocking session cleanup.
This is a test-only concern that shouldn't be in production code. Fix: Make the process name filter injectable, then add testhost only in the test. Or simply assert matchesFilter || myName.Contains("testhost") in the test without touching production code.
🟡 Medium — Worktree reuse returns stale/corrupted worktrees without validation
File: RepoManager.cs, CreateWorktreeAsync (new reuse check)
&& Directory.Exists(w.Path)Directory.Exists returns true even if the worktree is corrupted (missing/broken .git file), locked, or has uncommitted changes from a previous session that will contaminate the new session's workspace.
Fix: Run git -C <path> rev-parse --git-dir before reusing, and optionally warn if the working tree is dirty.
🟡 Medium — GetDefaultBranch returns wrong branch for non-bare repos not on default branch
File: RepoManager.cs, GetDefaultBranch
git symbolic-ref HEAD on a non-bare repo returns the currently checked-out branch, not the repo's canonical default. If the user's repo is on feature/my-thing, the new worktree branches from feature/my-thing instead of main.
Fix: Use git rev-parse --abbrev-ref origin/HEAD to get the canonical default branch.
🔵 Low — RunGhAsync GIT_DIR guard stale after non-bare BareClonePath
File: RepoManager.cs, RunGhAsync
The guard if (workDir.EndsWith(".git")) setting GIT_DIR was written assuming BareClonePath always ends in .git. It's now a plain directory path. gh still discovers the remote, so it's not broken today — but the assumption is now wrong and the comment is misleading for future callers.
Summary
| Severity | Issue |
|---|---|
| 🔴 Critical | RemoveRepositoryAsync recursively deletes user's real project on deleteFromDisk |
| 🔴 High | Existing bare clone silently orphaned when same repo re-added via folder |
| 🟠 High | BackfillWorktreeClonePaths corrupts clone paths of pre-existing worktrees |
| 🟠 High | git worktree add fails when non-bare repo has same branch checked out |
| 🟡 Medium | testhost in production process filter — false positive risk |
| 🟡 Medium | Worktree reuse skips corruption/dirty-state validation |
| 🟡 Medium | GetDefaultBranch returns wrong branch for non-default-branch checkouts |
| 🔵 Low | RunGhAsync GIT_DIR guard stale assumption |
The Critical issue is a data-loss bug that should block merge. The three High issues reflect structural concerns with the "BareClonePath → non-bare repo" approach that need explicit guards.
PR #527 Multi-Model Code Review (Re-Review v2)CI status: Previous Findings — Status
Current Findings🔴 CRITICAL: Orphaned bare clone deletion breaks active worktrees (3/3 reviewers)File: When a repo previously added by URL is re-added via "Existing Folder":
But existing git worktrees have Compare with Fix: Before deleting
🟡 MODERATE: Safety tests are structural-only — zero production code coverage (3/3 reviewers)Files: The three safety "regression" tests never call any production code:
Removing the guards from production code would leave all these tests green. The critical data-loss guard (RemoveRepositoryAsync managed-path check) has zero behavioral test coverage. Fix: Add behavioral tests that:
🟡 MODERATE: Worktree reuse returns broken worktrees without validation (2/3 reviewers)File:
Fix: Add a minimal health check before reuse: verify Informational (single-reviewer, discarded after adversarial round)
Test Coverage Assessment
Recommendation
After fixing #1 (add worktree drain/skip before bare clone deletion) and adding at least one behavioral test for #2, this PR is ready to merge. |
- AddRepositoryFromLocalAsync now points BareClonePath at the user's existing repo instead of creating a redundant bare clone - EnsureRepoCloneInCurrentRootAsync skips clone management when BareClonePath points at a non-bare repo (.git dir/file exists) - CreateWorktreeAsync reuses an existing registered worktree when the requested branch matches (avoids duplicating huge repos like MAUI) - Removed nested worktree strategy — all worktrees now go to the centralized ~/.polypilot/worktrees/ directory - Removed localPath parameter from CreateWorktreeAsync, CreateSessionWithWorktreeAsync, and all UI callers - Fixed Path.Combine producing backslashes for Unix-style path in BuildContinuationTranscript - Fixed FindActiveLockPid process name filter to include testhost Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Guard RemoveRepositoryAsync to only delete BareClonePath under managed ReposDir, preventing recursive deletion of user's real project - Restrict worktree reuse to centralized WorktreesDir only — external user checkouts are never returned to avoid multi-session conflicts - Clean up orphaned managed bare clone when same repo is re-added via Existing Folder (prevents disk waste) - Fix GetDefaultBranch to prefer origin/HEAD over symbolic-ref HEAD so non-bare repos don't branch from the wrong base - Revert testhost from production FindActiveLockPid (test-only concern) - Update RunGhAsync comment to reflect non-bare repo support - Add regression tests for delete guard and worktree reuse scoping Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Replace AddRepositoryFromLocal_ClonesLocallyAndSetsRemoteUrl with AddRepositoryFromLocal_PointsBareClonePathAtLocalRepo (verifies BareClonePath points at user's local repo, no bare clone created) - Replace localCloneSource reflection test with source-code assertion that AddRepositoryFromLocalAsync never calls AddRepositoryAsync - Remove unused localCloneSource overload from AddRepositoryAsync - Add GetRepoRoot/ExtractMethodBody test helpers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
a231f7a to
9e21b09
Compare
Problem
When a user adds an existing repo via Existing Folder (e.g. their
dotnet/mauiclone), PolyPilot:~/.polypilot/repos/(redundant — user already has it).polypilot/worktrees/{branch}/For huge repos like MAUI (~5GB), this wasted significant disk space and time.
Changes
Skip bare clone for Existing Folder repos
AddRepositoryFromLocalAsyncnow pointsBareClonePathdirectly at the user's existing repo instead of creating a redundant bare cloneEnsureRepoCloneInCurrentRootAsyncdetects non-bare repos (.gitdir/file) and skips clone managementReuse existing checkout for same-branch sessions
CreateWorktreeAsyncchecks for an existing registered worktree on the requested branch before creating a new oneRemove nested worktree strategy
~/.polypilot/worktrees/directorylocalPathparameter fromCreateWorktreeAsync,CreateSessionWithWorktreeAsync, and all UI callersBug fixes
Path.Combineproducing backslashes for Unix-style path inBuildContinuationTranscriptFindActiveLockPidprocess name filter to includetesthostTesting
All 3269 tests pass.