From a5136cf156994833d6dc67a8237375ff826ef85e Mon Sep 17 00:00:00 2001 From: Jeremy Richards Date: Wed, 17 Dec 2025 19:59:00 -0700 Subject: [PATCH 1/2] fix(git): find the closer .git The previous method preferred finding `.git` files over `.git/` directories. That doesn't work if, for some reason, the user a `.git/` directory inside the repo with commitlint and that repo path is nested inside another git repo. Ignoring the fact that this isn't a standard use of git, it still works, and can be trivially supported by comparing the path lengths. Priority: Low Tests: None, but there were none in @commitlint before this, either. Risk: Low --- @commitlint/top-level/src/index.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/@commitlint/top-level/src/index.ts b/@commitlint/top-level/src/index.ts index 4996d4f8ee..ec72972ace 100644 --- a/@commitlint/top-level/src/index.ts +++ b/@commitlint/top-level/src/index.ts @@ -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 + return foundFile.length > foundDir.length ? foundFile : foundDir; + } + return foundFile || foundDir; } From 58dae8c2303e9ac2145f376ee800f178c6a00a92 Mon Sep 17 00:00:00 2001 From: Jeremy Richards Date: Wed, 17 Dec 2025 20:22:51 -0700 Subject: [PATCH 2/2] chore: add unit tests to @commitlint Add a unit test showing the previous change works as expected. --- @commitlint/top-level/package.json | 1 + @commitlint/top-level/src/index.test.ts | 64 +++++++++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 @commitlint/top-level/src/index.test.ts diff --git a/@commitlint/top-level/package.json b/@commitlint/top-level/package.json index 4c48c1557a..6b7483516f 100644 --- a/@commitlint/top-level/package.json +++ b/@commitlint/top-level/package.json @@ -36,6 +36,7 @@ }, "license": "MIT", "devDependencies": { + "@commitlint/test": "^20.0.0", "@commitlint/utils": "^20.0.0" }, "dependencies": { diff --git a/@commitlint/top-level/src/index.test.ts b/@commitlint/top-level/src/index.test.ts new file mode 100644 index 0000000000..5fa5615dfe --- /dev/null +++ b/@commitlint/top-level/src/index.test.ts @@ -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 }); +});