diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0f2ff188..250f9dcf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -72,6 +72,11 @@ jobs: - run: git config --global core.autocrlf false if: runner.os == 'Windows' + - run: | + git config --global user.email "github-action@example.com" + git config --global user.name "GitHub Action" + git version + - uses: actions/checkout@v4 - uses: denoland/setup-deno@v1.1.4 diff --git a/deno.jsonc b/deno.jsonc index fff1ecd8..8b698044 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -5,7 +5,7 @@ ], "tasks": { "check": "deno check ./**/*.ts", - "test": "deno test -A --parallel --shuffle --doc", + "test": "deno test -A --shuffle --doc", "test:coverage": "deno task test --coverage=.coverage", "coverage": "deno coverage .coverage", "update": "deno run --allow-env --allow-read --allow-write --allow-run=git,deno --allow-net=jsr.io,registry.npmjs.org jsr:@molt/cli **/*.ts", diff --git a/denops/gin/command/chaperon/util.ts b/denops/gin/command/chaperon/util.ts index 14314df7..8653d136 100644 --- a/denops/gin/command/chaperon/util.ts +++ b/denops/gin/command/chaperon/util.ts @@ -1,5 +1,6 @@ import * as fs from "jsr:@std/fs@^1.0.0"; import * as path from "jsr:@std/path@^1.0.0"; +import { findGitdir } from "../../git/finder.ts"; const beginMarker = `${"<".repeat(7)} `; const endMarker = `${">".repeat(7)} `; @@ -29,8 +30,9 @@ export type AliasHead = typeof validAliasHeads[number]; export async function getInProgressAliasHead( worktree: string, ): Promise { + const gitdir = await findGitdir(worktree); for (const head of validAliasHeads) { - if (await fs.exists(path.join(worktree, ".git", head))) { + if (await fs.exists(path.join(gitdir, head))) { return head; } } diff --git a/denops/gin/component/worktree.ts b/denops/gin/component/worktree.ts index 10793955..b1e97d2e 100644 --- a/denops/gin/component/worktree.ts +++ b/denops/gin/component/worktree.ts @@ -2,7 +2,7 @@ import type { Denops } from "jsr:@denops/std@^7.0.0"; import { Cache } from "jsr:@lambdalisue/ttl-cache@^1.0.0"; import * as path from "jsr:@std/path@^1.0.0"; import { findWorktreeFromDenops } from "../git/worktree.ts"; -import { find } from "../git/finder.ts"; +import { findWorktree } from "../git/finder.ts"; type Data = string; @@ -13,7 +13,7 @@ async function getData( ): Promise { return cache.get("data") ?? await (async () => { const worktree = await findWorktreeFromDenops(denops); - const result = await find(worktree); + const result = await findWorktree(worktree); cache.set("data", result); return result; })(); diff --git a/denops/gin/git/finder.ts b/denops/gin/git/finder.ts index 7eec0ae9..821dcb58 100644 --- a/denops/gin/git/finder.ts +++ b/denops/gin/git/finder.ts @@ -4,17 +4,28 @@ import { execute } from "./process.ts"; import { decodeUtf8 } from "../util/text.ts"; const ttl = 30000; // seconds -const cache = new Cache(ttl); +const cacheWorktree = new Cache(ttl); +const cacheGitdir = new Cache(ttl); -export async function find(cwd: string): Promise { - const result = cache.get(cwd) ?? await (async () => { +/** + * Find a root path of a git working directory. + * + * @param cwd - A current working directory. + * @returns A root path of a git working directory. + */ +export async function findWorktree(cwd: string): Promise { + const path = await Deno.realPath(cwd); + const result = cacheWorktree.get(path) ?? await (async () => { let result: string | Error; try { - result = await findInternal(cwd); + result = await revParse(path, [ + "--show-toplevel", + "--show-superproject-working-tree", + ]); } catch (e) { result = e; } - cache.set(cwd, result); + cacheWorktree.set(path, result); return result; })(); if (result instanceof Error) { @@ -23,7 +34,31 @@ export async function find(cwd: string): Promise { return result; } -async function findInternal(cwd: string): Promise { +/** + * Find a .git directory of a git working directory. + * + * @param cwd - A current working directory. + * @returns A root path of a git working directory. + */ +export async function findGitdir(cwd: string): Promise { + const path = await Deno.realPath(cwd); + const result = cacheGitdir.get(path) ?? await (async () => { + let result: string | Error; + try { + result = await revParse(path, ["--git-dir"]); + } catch (e) { + result = e; + } + cacheGitdir.set(path, result); + return result; + })(); + if (result instanceof Error) { + throw result; + } + 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 @@ -31,16 +66,7 @@ async function findInternal(cwd: string): Promise { const index = terms.indexOf(".git"); cwd = terms.slice(0, index).join(SEPARATOR); } - const stdout = await execute( - [ - "rev-parse", - "--show-toplevel", - "--show-superproject-working-tree", - ], - { - cwd, - }, - ); + 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 bfc54c00..90a543ff 100644 --- a/denops/gin/git/finder_test.ts +++ b/denops/gin/git/finder_test.ts @@ -1,34 +1,93 @@ import { assertEquals, assertRejects } from "jsr:@std/assert@^1.0.0"; -import * as path from "jsr:@std/path@^1.0.0"; -import { find } from "./finder.ts"; +import { sandbox } from "jsr:@lambdalisue/sandbox@^2.0.0"; +import $ from "jsr:@david/dax@^0.42.0"; +import { join } from "jsr:@std/path@^1.0.0"; +import { findGitdir, findWorktree } from "./finder.ts"; import { ExecuteError } from "./process.ts"; Deno.test({ - name: "find() returns a root path of a git working directory", + name: "findWorktree() returns a root path of a git working directory", fn: async () => { - const exp = path.resolve( - path.fromFileUrl(import.meta.url), - "../../../../", + await using sbox = await prepare(); + Deno.chdir(join("a", "b", "c")); + assertEquals(await findWorktree("."), sbox.path); + // An internal cache will be used for the following call + assertEquals(await findWorktree("."), sbox.path); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: + "findWorktree() throws an error if the path is not in a git working directory", + fn: async () => { + await assertRejects(async () => { + await findWorktree("/"); + }, ExecuteError); + // An internal cache will be used for the following call + await assertRejects(async () => { + await findWorktree("/"); + }, ExecuteError); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "findGitdir() returns a root path of a git working directory", + fn: async () => { + await using sbox = await prepare(); + Deno.chdir(join("a", "b", "c")); + assertEquals(await findGitdir("."), join(sbox.path, ".git")); + // An internal cache will be used for the following call + assertEquals(await findGitdir("."), join(sbox.path, ".git")); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "findGitdir() returns a worktree path of a git working directory", + fn: async () => { + await using sbox = await prepare(); + await $`git worktree add -b test test main`; + Deno.chdir("test"); + assertEquals( + await findGitdir("."), + join(sbox.path, ".git", "worktrees", "test"), ); - assertEquals(await find("."), exp); // An internal cache will be used for the following call - assertEquals(await find("."), exp); + assertEquals( + await findGitdir("."), + join(sbox.path, ".git", "worktrees", "test"), + ); }, sanitizeResources: false, sanitizeOps: false, }); Deno.test({ - name: "find() throws an error if the path is not in a git working directory", + name: + "findGitdir() throws an error if the path is not in a git working directory", fn: async () => { await assertRejects(async () => { - await find("/"); + await findGitdir("/"); }, ExecuteError); // An internal cache will be used for the following call await assertRejects(async () => { - await find("/"); + await findGitdir("/"); }, ExecuteError); }, sanitizeResources: false, sanitizeOps: false, }); + +async function prepare(): ReturnType { + const sbox = await sandbox(); + await $`git init`; + await $`git commit --allow-empty -m 'Initial commit' --no-gpg-sign`; + await $`git switch -c main`; + await Deno.mkdir(join("a", "b", "c"), { recursive: true }); + return sbox; +} diff --git a/denops/gin/git/worktree.ts b/denops/gin/git/worktree.ts index 20afd04d..be68803c 100644 --- a/denops/gin/git/worktree.ts +++ b/denops/gin/git/worktree.ts @@ -5,7 +5,7 @@ import * as fn from "jsr:@denops/std@^7.0.0/function"; import * as path from "jsr:@std/path@^1.0.0"; import { GIN_BUFFER_PROTOCOLS } from "../global.ts"; import { expand } from "../util/expand.ts"; -import { find } from "./finder.ts"; +import { findWorktree } from "./finder.ts"; /** * Find a git worktree from a suspected directory @@ -32,7 +32,7 @@ async function findWorktreeFromSuspect( console.debug(`Trying to find a git repository from '${c}'`); } try { - return await find(c); + return await findWorktree(c); } catch { // Fail silently }