Skip to content

Commit 4c69e85

Browse files
committed
hooks: resolve enlistment root for worktrees outside enlistment tree
All native hooks (virtual-filesystem, read-object, post-index-changed) and managed hooks (GVFS.Hooks) resolved the primary enlistment root by walking up from CWD looking for a .gvfs/ directory. This meant worktrees placed outside the enlistment directory tree (e.g. in a temp directory or as a sibling) could not use git commands — the hooks would fail with 'must be run from inside a GVFS enlistment'. Fix by adding a worktree fallback path: when .gvfs walk-up fails, walk up looking for a .git file (indicating a linked worktree), then resolve the primary enlistment root through the gitdir chain — first from the gvfs-enlistment-root marker file, then by deriving from commondir. The resolved root is validated by checking that .gvfs/ exists there. Native hooks (common.windows.cpp): - Extract ReadFirstLine, Utf8ToWide, TryParseGitFile helpers - Refactor GetWorktreePipeSuffix to use shared helpers - Add TryResolveFromWorktree that returns both enlistment root and pipe suffix - GetGVFSPipeName tries .gvfs walk-up first, then worktree fallback Managed hooks (Program.cs): - After TryGetGVFSEnlistmentRoot fails, try TryGetWorktreeInfo -> GetEnlistmentRoot() before exiting - Validate .gvfs exists at the resolved root Tests: - Unit tests for GetEnlistmentRoot with marker file, SharedGitDir fallback, and marker-preferred scenarios - Functional test creating worktree in temp directory, verifying git status from root and subdirectory, file projection, commits Assisted-by: Claude Opus 4.6 Signed-off-by: Ty Larrabee <tyrielv@gmail.com>
1 parent 3c18fe5 commit 4c69e85

4 files changed

Lines changed: 402 additions & 58 deletions

File tree

GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/WorktreeTests.cs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,83 @@ public void ConcurrentWorktreeAddCommitRemove()
191191
}
192192
}
193193

194+
[TestCase]
195+
public void WorktreeOutsideEnlistmentTree()
196+
{
197+
string suffix = Guid.NewGuid().ToString("N").Substring(0, 8);
198+
string tempDir = Path.Combine(Path.GetTempPath(), $"gvfs-remote-wt-{suffix}");
199+
string worktreePath = Path.Combine(tempDir, "wt");
200+
string branchName = $"remote-wt-test-{suffix}";
201+
202+
try
203+
{
204+
Directory.CreateDirectory(tempDir);
205+
206+
// 1. Create worktree outside the enlistment tree
207+
ProcessResult addResult = GitHelpers.InvokeGitAgainstGVFSRepo(
208+
this.Enlistment.RepoRoot,
209+
$"worktree add -b {branchName} \"{worktreePath}\"");
210+
addResult.ExitCode.ShouldEqual(0,
211+
$"worktree add failed: {addResult.Errors}");
212+
213+
// 2. Verify GVFS mount is running
214+
this.AssertWorktreeMounted(worktreePath, "remote worktree");
215+
216+
// 3. Verify git status works from the worktree root
217+
ProcessResult statusResult = GitHelpers.InvokeGitAgainstGVFSRepo(
218+
worktreePath, "status --porcelain");
219+
statusResult.ExitCode.ShouldEqual(0,
220+
$"git status from worktree root failed: {statusResult.Errors}");
221+
statusResult.Output.Trim().ShouldBeEmpty(
222+
"Remote worktree should have clean status");
223+
224+
// 4. Verify projected files are visible
225+
File.Exists(Path.Combine(worktreePath, "Readme.md")).ShouldBeTrue(
226+
"Readme.md should be projected in remote worktree");
227+
228+
// 5. Verify git status works from a subdirectory
229+
string subDir = Path.Combine(worktreePath, "GVFS");
230+
Directory.Exists(subDir).ShouldBeTrue(
231+
"Subdirectory GVFS should be projected");
232+
ProcessResult subDirStatus = GitHelpers.InvokeGitAgainstGVFSRepo(
233+
subDir, "status --porcelain");
234+
subDirStatus.ExitCode.ShouldEqual(0,
235+
$"git status from subdirectory failed: {subDirStatus.Errors}");
236+
237+
// 6. Verify commits work
238+
File.WriteAllText(
239+
Path.Combine(worktreePath, "remote-test.txt"),
240+
"created in remote worktree");
241+
GitHelpers.InvokeGitAgainstGVFSRepo(worktreePath, "add remote-test.txt")
242+
.ExitCode.ShouldEqual(0);
243+
GitHelpers.InvokeGitAgainstGVFSRepo(
244+
worktreePath, "commit -m \"commit from remote worktree\"")
245+
.ExitCode.ShouldEqual(0);
246+
247+
// 7. Verify commit is visible from primary repo
248+
GitHelpers.InvokeGitAgainstGVFSRepo(
249+
this.Enlistment.RepoRoot, $"log -1 --format=%s {branchName}")
250+
.Output.ShouldContain(expectedSubstrings: new[] { "commit from remote worktree" });
251+
252+
// 8. Remove worktree
253+
ProcessResult removeResult = GitHelpers.InvokeGitAgainstGVFSRepo(
254+
this.Enlistment.RepoRoot,
255+
$"worktree remove --force \"{worktreePath}\"");
256+
removeResult.ExitCode.ShouldEqual(0,
257+
$"worktree remove failed: {removeResult.Errors}");
258+
Directory.Exists(worktreePath).ShouldBeFalse(
259+
"Remote worktree directory should be deleted");
260+
}
261+
finally
262+
{
263+
this.ForceCleanupWorktree(worktreePath, branchName);
264+
if (Directory.Exists(tempDir))
265+
{
266+
try { Directory.Delete(tempDir, recursive: true); } catch { }
267+
}
268+
}
269+
}
270+
194271
private void InitWorktreeArrays(int count, out string[] paths, out string[] branches)
195272
{
196273
paths = new string[count];

GVFS/GVFS.Hooks/Program.cs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using GVFS.Common.NamedPipes;
44
using GVFS.Common.Tracing;
55
using GVFS.Hooks.HooksPlatform;
6+
using GVFS.Platform.Windows;
67
using System;
78
using System.Collections.Generic;
89
using System.IO;
@@ -47,9 +48,21 @@ public static void Main(string[] args)
4748

4849
if (!GVFSHooksPlatform.TryGetGVFSEnlistmentRoot(Environment.CurrentDirectory, out enlistmentRoot, out errorMessage))
4950
{
50-
// Nothing to hook when being run outside of a GVFS repo.
51-
// This is also the path when run with --git-dir outside of a GVFS directory, see Story #949665
52-
Environment.Exit(0);
51+
// .gvfs walk-up failed — this may be a worktree placed
52+
// outside the primary enlistment tree. Try resolving
53+
// the enlistment root through the worktree chain.
54+
GVFSEnlistment.WorktreeInfo wtInfo = GVFSEnlistment.TryGetWorktreeInfo(normalizedCurrentDirectory);
55+
if (wtInfo != null)
56+
{
57+
enlistmentRoot = wtInfo.GetEnlistmentRoot();
58+
}
59+
60+
if (enlistmentRoot == null ||
61+
!Directory.Exists(Path.Combine(enlistmentRoot, WindowsPlatform.DotGVFSRoot)))
62+
{
63+
// Not in a GVFS repo or worktree. Nothing to hook.
64+
Environment.Exit(0);
65+
}
5366
}
5467

5568
enlistmentPipename = GVFSHooksPlatform.GetNamedPipeName(enlistmentRoot);

0 commit comments

Comments
 (0)