Skip to content
Open
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
1 change: 1 addition & 0 deletions @commitlint/top-level/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
},
"license": "MIT",
"devDependencies": {
"@commitlint/test": "^20.0.0",
"@commitlint/utils": "^20.0.0"
},
"dependencies": {
Expand Down
64 changes: 64 additions & 0 deletions @commitlint/top-level/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { test, expect } from "vitest";
import fs from "fs/promises";
import path from "node:path";
import os from "node:os";
import { git } from "@commitlint/test";

import toplevel from "./index.js";

test("should find git toplevel from repo root", async () => {
const cwd = await git.bootstrap();
const result = await toplevel(cwd);
expect(result).toBe(cwd);
});

test("should find git toplevel from subdirectory", async () => {
const cwd = await git.bootstrap();
const subdir = path.join(cwd, "subdir");
await fs.mkdir(subdir);

const result = await toplevel(subdir);
expect(result).toBe(cwd);
});

test("should prefer closer .git directory over farther .git file", async () => {
// Create a parent directory with a .git file (simulating dotfiles bare repo)
const parentDir = await fs.mkdtemp(path.join(os.tmpdir(), "parent-"));
const gitFileContent = `gitdir: ${path.join(parentDir, ".rcfiles")}`;
await fs.writeFile(path.join(parentDir, ".git"), gitFileContent);
await fs.mkdir(path.join(parentDir, ".rcfiles"));

// Create a child git repo inside the parent
const childDir = path.join(parentDir, "child-repo");
await fs.mkdir(childDir);
await fs.mkdir(path.join(childDir, ".git"));

// The toplevel should be the child repo, not the parent
const result = await toplevel(childDir);
expect(result).toBe(childDir);

// Cleanup
await fs.rm(parentDir, { recursive: true });
});

test("should handle .git file (submodule) correctly", async () => {
const cwd = await git.bootstrap();

// Create a submodule-like structure with a .git file
const submoduleDir = path.join(cwd, "submodule");
await fs.mkdir(submoduleDir);
const gitFileContent = `gitdir: ${path.join(cwd, ".git", "modules", "submodule")}`;
await fs.writeFile(path.join(submoduleDir, ".git"), gitFileContent);

const result = await toplevel(submoduleDir);
expect(result).toBe(submoduleDir);
});

test("should return undefined when no .git found", async () => {
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "no-git-"));

const result = await toplevel(tmpDir);
expect(result).toBeUndefined();

await fs.rm(tmpDir, { recursive: true });
});
5 changes: 5 additions & 0 deletions @commitlint/top-level/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,10 @@ async function searchDotGit(cwd?: string) {
const foundFile = await findUp(".git", { cwd, type: "file" });
const foundDir = await findUp(".git", { cwd, type: "directory" });

if (foundFile && foundDir) {
// Return whichever is deeper (closer to cwd) by comparing path lengths
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says "whichever is deeper (closer to cwd)" but the code returns the longer path. In the context of file paths, "deeper" typically means more nested (more path segments), not a longer string. This mismatch between comment and implementation could be confusing.

Suggested change
// Return whichever is deeper (closer to cwd) by comparing path lengths
// Return whichever has the longer path string

Copilot uses AI. Check for mistakes.
return foundFile.length > foundDir.length ? foundFile : foundDir;
Comment on lines +27 to +28
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While path length comparison works for determining proximity in typical cases, it's fragile and relies on filesystem conventions. Consider using path segment counting or explicit path comparison methods for more robust and maintainable code. For example, comparing foundFile.split(path.sep).length vs foundDir.split(path.sep).length would make the intent clearer and be more resilient to edge cases.

Suggested change
// Return whichever is deeper (closer to cwd) by comparing path lengths
return foundFile.length > foundDir.length ? foundFile : foundDir;
// Return whichever is deeper (closer to cwd) by comparing path segment counts
const fileDepth = path.resolve(foundFile).split(path.sep).length;
const dirDepth = path.resolve(foundDir).split(path.sep).length;
return fileDepth > dirDepth ? foundFile : foundDir;

Copilot uses AI. Check for mistakes.
}

return foundFile || foundDir;
}
Loading