Skip to content

Commit 9d83d18

Browse files
authored
Merge pull request #154 from kbwo/fix/wworktree
Fix worktree detection for worktrees inside .git directory
2 parents 70b2116 + d93a18d commit 9d83d18

File tree

2 files changed

+96
-10
lines changed

2 files changed

+96
-10
lines changed

denops/gin/git/finder.ts

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { resolve, SEPARATOR } from "jsr:@std/path@^1.0.0";
1+
import { resolve } from "jsr:@std/path@^1.0.0";
22
import { Cache } from "jsr:@lambdalisue/ttl-cache@^1.0.0";
33
import { execute } from "./process.ts";
44
import { decodeUtf8 } from "../util/text.ts";
@@ -7,6 +7,17 @@ const ttl = 30000; // seconds
77
const cacheWorktree = new Cache<string, string | Error>(ttl);
88
const cacheGitdir = new Cache<string, string | Error>(ttl);
99

10+
/**
11+
* Check if we've reached the filesystem root by comparing a path with its parent.
12+
* This works cross-platform (Windows, Linux, Mac).
13+
*
14+
* @param currentPath - The path to check
15+
* @returns true if the path is the filesystem root, false otherwise
16+
*/
17+
function isFilesystemRoot(currentPath: string): boolean {
18+
return currentPath === resolve(currentPath, "..");
19+
}
20+
1021
/**
1122
* Find a root path of a git working directory.
1223
*
@@ -18,12 +29,24 @@ export async function findWorktree(cwd: string): Promise<string> {
1829
const result = cacheWorktree.get(path) ?? await (async () => {
1930
let result: string | Error;
2031
try {
32+
// Search upward from current directory to find worktree root
33+
let currentPath = path;
34+
while (!isFilesystemRoot(currentPath)) {
35+
if (await isWorktreeRoot(currentPath)) {
36+
result = currentPath;
37+
cacheWorktree.set(path, result);
38+
return result;
39+
}
40+
currentPath = resolve(currentPath, "..");
41+
}
42+
43+
// If no worktree found, use normal detection for regular repositories
2144
result = await revParse(path, [
2245
"--show-toplevel",
2346
"--show-superproject-working-tree",
2447
]);
2548
} catch (e) {
26-
result = e;
49+
result = e as Error;
2750
}
2851
cacheWorktree.set(path, result);
2952
return result;
@@ -47,7 +70,7 @@ export async function findGitdir(cwd: string): Promise<string> {
4770
try {
4871
result = await revParse(path, ["--git-dir"]);
4972
} catch (e) {
50-
result = e;
73+
result = e as Error;
5174
}
5275
cacheGitdir.set(path, result);
5376
return result;
@@ -58,14 +81,22 @@ export async function findGitdir(cwd: string): Promise<string> {
5881
return result;
5982
}
6083

61-
async function revParse(cwd: string, args: string[]): Promise<string> {
62-
const terms = cwd.split(SEPARATOR);
63-
if (terms.includes(".git")) {
64-
// `git rev-parse` does not work in a ".git" directory
65-
// so use a parent directory of it instead.
66-
const index = terms.indexOf(".git");
67-
cwd = terms.slice(0, index).join(SEPARATOR);
84+
async function isWorktreeRoot(currentPath: string): Promise<boolean> {
85+
try {
86+
const gitPath = resolve(currentPath, ".git");
87+
const stat = await Deno.stat(gitPath);
88+
if (stat.isFile) {
89+
// Found a .git file, verify it's a valid git worktree
90+
await revParse(currentPath, ["--git-dir"]);
91+
return true;
92+
}
93+
} catch {
94+
// Either .git doesn't exist or it's not a valid worktree
6895
}
96+
return false;
97+
}
98+
99+
async function revParse(cwd: string, args: string[]): Promise<string> {
69100
const stdout = await execute(["rev-parse", ...args], { cwd });
70101
const output = decodeUtf8(stdout);
71102
return resolve(output.split(/\n/, 2)[0]);

denops/gin/git/finder_test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,61 @@ Deno.test({
6767
sanitizeOps: false,
6868
});
6969

70+
Deno.test({
71+
name: "findWorktree() returns a root path for worktree inside .git directory",
72+
fn: async () => {
73+
await using sbox = await prepare();
74+
// Create a worktree inside .git/workspaces/ directory
75+
await Deno.mkdir(join(".git", "workspaces"), { recursive: true });
76+
await $`git worktree add -b workspace-test .git/workspaces/test main`;
77+
78+
// Change to a subdirectory within the worktree
79+
await Deno.mkdir(join(".git", "workspaces", "test", "subdir"), {
80+
recursive: true,
81+
});
82+
Deno.chdir(join(".git", "workspaces", "test", "subdir"));
83+
84+
// findWorktree should return the worktree root, not the main repository root
85+
assertEquals(
86+
await findWorktree("."),
87+
join(sbox.path, ".git", "workspaces", "test"),
88+
);
89+
// An internal cache will be used for the following call
90+
assertEquals(
91+
await findWorktree("."),
92+
join(sbox.path, ".git", "workspaces", "test"),
93+
);
94+
},
95+
sanitizeResources: false,
96+
sanitizeOps: false,
97+
});
98+
99+
Deno.test({
100+
name: "findGitdir() returns a gitdir path for worktree inside .git directory",
101+
fn: async () => {
102+
await using sbox = await prepare();
103+
// Create a worktree inside .git/workspaces/ directory
104+
await Deno.mkdir(join(".git", "workspaces"), { recursive: true });
105+
await $`git worktree add -b workspace-test2 .git/workspaces/test2 main`;
106+
107+
// Change to the worktree
108+
Deno.chdir(join(".git", "workspaces", "test2"));
109+
110+
// findGitdir should return the correct gitdir for this worktree
111+
assertEquals(
112+
await findGitdir("."),
113+
join(sbox.path, ".git", "worktrees", "test2"),
114+
);
115+
// An internal cache will be used for the following call
116+
assertEquals(
117+
await findGitdir("."),
118+
join(sbox.path, ".git", "worktrees", "test2"),
119+
);
120+
},
121+
sanitizeResources: false,
122+
sanitizeOps: false,
123+
});
124+
70125
Deno.test({
71126
name:
72127
"findGitdir() throws an error if the path is not in a git working directory",

0 commit comments

Comments
 (0)