diff --git a/denops/gin/git/finder.ts b/denops/gin/git/finder.ts index 821dcb58..12fe4350 100644 --- a/denops/gin/git/finder.ts +++ b/denops/gin/git/finder.ts @@ -1,4 +1,4 @@ -import { resolve, SEPARATOR } from "jsr:@std/path@^1.0.0"; +import { resolve } from "jsr:@std/path@^1.0.0"; import { Cache } from "jsr:@lambdalisue/ttl-cache@^1.0.0"; import { execute } from "./process.ts"; import { decodeUtf8 } from "../util/text.ts"; @@ -7,6 +7,17 @@ const ttl = 30000; // seconds const cacheWorktree = new Cache(ttl); const cacheGitdir = new Cache(ttl); +/** + * Check if we've reached the filesystem root by comparing a path with its parent. + * This works cross-platform (Windows, Linux, Mac). + * + * @param currentPath - The path to check + * @returns true if the path is the filesystem root, false otherwise + */ +function isFilesystemRoot(currentPath: string): boolean { + return currentPath === resolve(currentPath, ".."); +} + /** * Find a root path of a git working directory. * @@ -18,12 +29,24 @@ export async function findWorktree(cwd: string): Promise { const result = cacheWorktree.get(path) ?? await (async () => { let result: string | Error; try { + // Search upward from current directory to find worktree root + let currentPath = path; + while (!isFilesystemRoot(currentPath)) { + if (await isWorktreeRoot(currentPath)) { + result = currentPath; + cacheWorktree.set(path, result); + return result; + } + currentPath = resolve(currentPath, ".."); + } + + // If no worktree found, use normal detection for regular repositories result = await revParse(path, [ "--show-toplevel", "--show-superproject-working-tree", ]); } catch (e) { - result = e; + result = e as Error; } cacheWorktree.set(path, result); return result; @@ -47,7 +70,7 @@ export async function findGitdir(cwd: string): Promise { try { result = await revParse(path, ["--git-dir"]); } catch (e) { - result = e; + result = e as Error; } cacheGitdir.set(path, result); return result; @@ -58,14 +81,22 @@ export async function findGitdir(cwd: string): Promise { return result; } -async function revParse(cwd: string, args: string[]): Promise { - const terms = cwd.split(SEPARATOR); - if (terms.includes(".git")) { - // `git rev-parse` does not work in a ".git" directory - // so use a parent directory of it instead. - const index = terms.indexOf(".git"); - cwd = terms.slice(0, index).join(SEPARATOR); +async function isWorktreeRoot(currentPath: string): Promise { + try { + const gitPath = resolve(currentPath, ".git"); + const stat = await Deno.stat(gitPath); + if (stat.isFile) { + // Found a .git file, verify it's a valid git worktree + await revParse(currentPath, ["--git-dir"]); + return true; + } + } catch { + // Either .git doesn't exist or it's not a valid worktree } + return false; +} + +async function revParse(cwd: string, args: string[]): Promise { const stdout = await execute(["rev-parse", ...args], { cwd }); const output = decodeUtf8(stdout); return resolve(output.split(/\n/, 2)[0]); diff --git a/denops/gin/git/finder_test.ts b/denops/gin/git/finder_test.ts index 90a543ff..1fa7f8ae 100644 --- a/denops/gin/git/finder_test.ts +++ b/denops/gin/git/finder_test.ts @@ -67,6 +67,61 @@ Deno.test({ sanitizeOps: false, }); +Deno.test({ + name: "findWorktree() returns a root path for worktree inside .git directory", + fn: async () => { + await using sbox = await prepare(); + // Create a worktree inside .git/workspaces/ directory + await Deno.mkdir(join(".git", "workspaces"), { recursive: true }); + await $`git worktree add -b workspace-test .git/workspaces/test main`; + + // Change to a subdirectory within the worktree + await Deno.mkdir(join(".git", "workspaces", "test", "subdir"), { + recursive: true, + }); + Deno.chdir(join(".git", "workspaces", "test", "subdir")); + + // findWorktree should return the worktree root, not the main repository root + assertEquals( + await findWorktree("."), + join(sbox.path, ".git", "workspaces", "test"), + ); + // An internal cache will be used for the following call + assertEquals( + await findWorktree("."), + join(sbox.path, ".git", "workspaces", "test"), + ); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "findGitdir() returns a gitdir path for worktree inside .git directory", + fn: async () => { + await using sbox = await prepare(); + // Create a worktree inside .git/workspaces/ directory + await Deno.mkdir(join(".git", "workspaces"), { recursive: true }); + await $`git worktree add -b workspace-test2 .git/workspaces/test2 main`; + + // Change to the worktree + Deno.chdir(join(".git", "workspaces", "test2")); + + // findGitdir should return the correct gitdir for this worktree + assertEquals( + await findGitdir("."), + join(sbox.path, ".git", "worktrees", "test2"), + ); + // An internal cache will be used for the following call + assertEquals( + await findGitdir("."), + join(sbox.path, ".git", "worktrees", "test2"), + ); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + Deno.test({ name: "findGitdir() throws an error if the path is not in a git working directory",