Skip to content

Commit 83ea56e

Browse files
PureWeenCopilot
andcommitted
fix: 'Existing Folder' no longer overwrites URL-based repo's bare clone
When a repo was first added via 'Add from URL' (creating a managed bare clone) and then the same repo was added via 'Existing Folder', AddRepositoryFromLocalAsync was overwriting the existing repo's BareClonePath to point at the local folder and deleting the managed bare clone. This made the URL-based repo disappear from the sidebar. Fix: when an existing repo with the same remote URL is found, keep it as-is and only register the local folder as an external worktree. The UI caller (AddLocalFolderAsync) separately creates a 📁 local folder group, so both entries coexist in the sidebar. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent fa1e3b2 commit 83ea56e

2 files changed

Lines changed: 96 additions & 20 deletions

File tree

PolyPilot.Tests/AddExistingRepoTests.cs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,92 @@ public void AddRepositoryFromLocal_NoBareCloneCreatedInReposDir()
251251
Assert.Contains("BareClonePath = localPath", methodBody);
252252
}
253253

254+
[Fact]
255+
public async Task AddRepositoryFromLocal_DoesNotOverwriteExistingUrlBasedRepo()
256+
{
257+
// Regression: adding a local folder for a repo that was already added via URL
258+
// must NOT overwrite the existing repo's BareClonePath. The URL-based repo
259+
// (with its managed bare clone) should be preserved; the local folder is only
260+
// registered as an external worktree.
261+
var tempDir = Path.Combine(Path.GetTempPath(), $"local-overwrite-test-{Guid.NewGuid():N}");
262+
var testBaseDir = Path.Combine(Path.GetTempPath(), $"rmtest-{Guid.NewGuid():N}");
263+
Directory.CreateDirectory(tempDir);
264+
Directory.CreateDirectory(testBaseDir);
265+
try
266+
{
267+
var remoteUrl = "https://github.com/test-owner/overwrite-test.git";
268+
269+
// Create a local git repo with an origin remote
270+
await RunProcess("git", "init", tempDir);
271+
await RunProcess("git", "-C", tempDir, "config", "user.email", "test@test.com");
272+
await RunProcess("git", "-C", tempDir, "config", "user.name", "Test");
273+
await RunProcess("git", "-C", tempDir, "commit", "--allow-empty", "-m", "init");
274+
await RunProcess("git", "-C", tempDir, "remote", "add", "origin", remoteUrl);
275+
276+
var rm = new RepoManager();
277+
RepoManager.SetBaseDirForTesting(testBaseDir);
278+
try
279+
{
280+
// Simulate a repo already added via "Add from URL" with a managed bare clone.
281+
var id = RepoManager.RepoIdFromUrl(remoteUrl);
282+
var barePath = Path.Combine(testBaseDir, "repos", $"{id}.git");
283+
Directory.CreateDirectory(barePath);
284+
var urlRepo = new RepositoryInfo
285+
{
286+
Id = id,
287+
Name = "overwrite-test",
288+
Url = remoteUrl,
289+
BareClonePath = barePath,
290+
AddedAt = DateTime.UtcNow
291+
};
292+
// Inject the URL-based repo into state
293+
var state = new RepositoryState();
294+
state.Repositories.Add(urlRepo);
295+
var stateFile = Path.Combine(testBaseDir, "repos.json");
296+
File.WriteAllText(stateFile, System.Text.Json.JsonSerializer.Serialize(state));
297+
rm.Load();
298+
299+
// Now add the same repo from a local folder
300+
var repo = await rm.AddRepositoryFromLocalAsync(tempDir);
301+
302+
// The returned repo should be the SAME repo (same ID)
303+
Assert.Equal(id, repo.Id);
304+
305+
// CRITICAL: BareClonePath must still point at the managed bare clone,
306+
// NOT at the local folder. The local folder should only be registered
307+
// as an external worktree.
308+
Assert.Equal(Path.GetFullPath(barePath), Path.GetFullPath(repo.BareClonePath));
309+
310+
// The managed bare clone directory must still exist (not deleted)
311+
Assert.True(Directory.Exists(barePath));
312+
313+
// There should still be exactly ONE repo (not duplicated)
314+
Assert.Single(rm.Repositories.Where(r => r.Id == id));
315+
316+
// The local folder should be registered as an external worktree
317+
Assert.Contains(rm.Worktrees, w =>
318+
w.RepoId == id && PathsEqual(w.Path, tempDir));
319+
}
320+
finally
321+
{
322+
RepoManager.SetBaseDirForTesting(TestSetup.TestBaseDir);
323+
}
324+
}
325+
finally
326+
{
327+
ForceDeleteDirectory(tempDir);
328+
ForceDeleteDirectory(testBaseDir);
329+
}
330+
}
331+
332+
private static bool PathsEqual(string? left, string? right)
333+
{
334+
if (left == null || right == null) return false;
335+
var a = Path.GetFullPath(left).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
336+
var b = Path.GetFullPath(right).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
337+
return string.Equals(a, b, StringComparison.OrdinalIgnoreCase);
338+
}
339+
254340
private static string ExtractMethodBody(string source, string methodName)
255341
{
256342
var idx = source.IndexOf(methodName, StringComparison.Ordinal);

PolyPilot/Services/RepoManager.cs

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -607,9 +607,13 @@ await RunGitAsync(barePath, ct, "config", "remote.origin.fetch",
607607

608608
/// <summary>
609609
/// Add a repository from an existing local path (non-bare). Validates the folder is a
610-
/// git repository with an 'origin' remote, then creates a <see cref="RepositoryInfo"/>
610+
/// git repository with an 'origin' remote, then either reuses an existing
611+
/// <see cref="RepositoryInfo"/> (if one was already added via URL) or creates a new one
611612
/// whose <see cref="RepositoryInfo.BareClonePath"/> points directly at the user's local
612613
/// repo — no bare clone is created.
614+
/// If a repo with the same remote was already added via "Add from URL", the existing
615+
/// repo (and its managed bare clone) is preserved; the local folder is only registered
616+
/// as an external worktree.
613617
/// The local folder is also registered as an external worktree so it appears in the
614618
/// "📂 Existing" list when creating sessions.
615619
/// </summary>
@@ -660,23 +664,16 @@ public async Task<RepositoryInfo> AddRepositoryFromLocalAsync(
660664
var id = RepoIdFromUrl(url);
661665

662666
RepositoryInfo repo;
663-
string? oldBareClonePath = null;
664667
lock (_stateLock)
665668
{
666669
var existing = _state.Repositories.FirstOrDefault(r => r.Id == id);
667670
if (existing != null)
668671
{
669-
// If the old BareClonePath was a managed bare clone, remember it for cleanup.
670-
if (!string.IsNullOrWhiteSpace(existing.BareClonePath)
671-
&& !PathsEqual(existing.BareClonePath, localPath))
672-
{
673-
var fullOld = Path.GetFullPath(existing.BareClonePath);
674-
var managedPrefix = Path.GetFullPath(ReposDir) + Path.DirectorySeparatorChar;
675-
if (fullOld.StartsWith(managedPrefix, StringComparison.OrdinalIgnoreCase))
676-
oldBareClonePath = existing.BareClonePath;
677-
}
678-
existing.BareClonePath = localPath;
679-
BackfillWorktreeClonePaths(existing);
672+
// A repo with this remote already exists (e.g., added via "Add from URL").
673+
// Keep it as-is — don't overwrite its BareClonePath, which would destroy
674+
// the managed bare clone and break any worktrees that depend on it.
675+
// The local folder will be registered as an external worktree below,
676+
// and the UI caller (AddLocalFolderAsync) will create a 📁 local folder group.
680677
repo = existing;
681678
}
682679
else
@@ -695,13 +692,6 @@ public async Task<RepositoryInfo> AddRepositoryFromLocalAsync(
695692
Save();
696693
OnStateChanged?.Invoke();
697694

698-
// Clean up orphaned managed bare clone (if any) after state is saved.
699-
if (oldBareClonePath != null && Directory.Exists(oldBareClonePath))
700-
{
701-
try { Directory.Delete(oldBareClonePath, recursive: true); }
702-
catch (Exception ex) { Console.WriteLine($"[RepoManager] Failed to clean up old bare clone at '{oldBareClonePath}': {ex.Message}"); }
703-
}
704-
705695
// Register the local folder as an external worktree so it also appears in the
706696
// "📂 Existing" picker when creating repo-based sessions.
707697
await RegisterExternalWorktreeAsync(repo, localPath, ct);

0 commit comments

Comments
 (0)