Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions cspell-dict.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ exonum
flatpack
Flatpacked
gimu
gitdir
globby
globstar
gzipped
Expand Down Expand Up @@ -68,5 +69,7 @@ walk-throughs
webdeveric
wireapp
workdir
worktree
worktrees
zulip
zulipchat
27 changes: 26 additions & 1 deletion packages/cspell-gitignore/src/findRepoRoot.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import * as fs from 'node:fs/promises';
import * as path from 'node:path';

import { describe, expect, test } from 'vitest';
import { beforeAll, describe, expect, test } from 'vitest';

import { findRepoRoot } from './findRepoRoot.js';

const pathTemp = path.join(__dirname, '../temp/findRepoRoot');
const pathClone = path.join(pathTemp, 'clone');
const pathWorktree = path.join(pathClone, 'worktrees/wt');

Comment on lines +4 to +11
describe('helpers', () => {
test.each`
dir | expected
Expand All @@ -16,4 +21,24 @@ describe('helpers', () => {
const f = await findRepoRoot(dir);
expect(f ? path.join(f, '.') : f).toEqual(expected);
});

describe('git worktrees', () => {
// A worktree marks its root with a `.git` file, while the clone it belongs to has a
// `.git` directory. A worktree checked out inside its own clone has both above it.
beforeAll(async () => {
await fs.rm(pathTemp, { force: true, recursive: true });
await fs.mkdir(path.join(pathClone, '.git'), { recursive: true });
await fs.mkdir(pathWorktree, { recursive: true });
await fs.writeFile(path.join(pathWorktree, '.git'), 'gitdir: ../../.git/worktrees/wt\n');
});

test.each`
dir | expected
${pathWorktree} | ${pathWorktree}
${pathClone} | ${pathClone}
`('findRepoRoot $dir', async ({ dir, expected }) => {
const f = await findRepoRoot(dir);
expect(f ? path.join(f, '.') : f).toEqual(expected);
});
});
});
26 changes: 21 additions & 5 deletions packages/cspell-gitignore/src/findRepoRoot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,27 @@ import { getDefaultVirtualFs } from 'cspell-io';
* @returns resolves to `.git` root or undefined
*/
export async function findRepoRoot(directory: string | URL, vfs?: VFileSystem): Promise<string | undefined> {
directory = toFileDirURL(directory);
vfs = vfs || getDefaultVirtualFs().getFS(directory);
const foundDir = await vfs.findUp('.git', directory, { type: 'directory' });
const foundFile = await vfs.findUp('.git', directory, { type: 'file' });
const found = foundDir || foundFile;
const from = toFileDirURL(directory);
const fs = vfs || getDefaultVirtualFs().getFS(from);
const found = await fs.findUp((dir) => findDotGit(fs, dir), from);
if (!found) return undefined;
return toFilePathOrHref(new URL('.', found));
}

/**
* Look for `.git` in a single directory, without caring whether it is a file or a directory.
*
* A clone marks its root with a `.git` directory and a worktree marks its root with a `.git` file,
* so both have to count. A worktree checked out inside its own clone has one of each above it,
* which is why the nearest match has to win rather than one type of match.
* @param fs - file system to stat with.
* @param dir - directory to look in.
* @returns resolves to the url of `.git`, or undefined when the directory does not have one.
*/
function findDotGit(fs: VFileSystem, dir: URL): Promise<URL | undefined> {
const url = new URL('.git', dir);
return fs.stat(url).then(
() => url,
() => undefined,
);
}
Loading