Skip to content

Conversation

@awojasinski
Copy link

@awojasinski awojasinski commented Jan 8, 2026

User description

Summary

This PR fixes #787 by replacing custom git root detection with native git commands that properly handle git worktrees.

Problem

When running commitlint from within a git worktree directory, users get the error:

could not find git root from undefined

This happens because the previous implementation used find-up to search for .git directories, which doesn't work correctly with worktrees where .git is a file pointing to the actual git directory.

Solution

1. @commitlint/top-level

  • Replaced find-up library with git rev-parse --show-toplevel
  • This correctly returns the working tree root for regular repos, submodules, and worktrees
  • Removed find-up dependency

2. @commitlint/read (get-edit-file-path)

  • Replaced manual .git file/directory detection with git rev-parse --git-dir
  • This correctly resolves to the actual git directory where COMMIT_EDITMSG lives, even in worktrees

Testing

Added comprehensive tests for git worktree support:

  • @commitlint/top-level: 5 new tests covering regular repos, subdirectories, non-git directories, and worktrees
  • @commitlint/read: 1 new test verifying reading commit messages from worktrees

All existing tests continue to pass.

Commits

  1. fix(top-level): use git rev-parse for worktree support - Core implementation change
  2. fix(read): use git rev-parse --git-dir for worktree support - Edit file path fix
  3. test: add git worktree tests for top-level and read - Test coverage

Fixes #787


PR Type

Bug fix


Description

  • Replace find-up with native git rev-parse commands for proper worktree support

  • Fix git root detection in @commitlint/top-level using git rev-parse --show-toplevel

  • Fix edit file path resolution in @commitlint/read using git rev-parse --git-dir

  • Add comprehensive test coverage for git worktree scenarios


Diagram Walkthrough

flowchart LR
  A["find-up library"] -->|replaced with| B["git rev-parse"]
  B -->|--show-toplevel| C["@commitlint/top-level"]
  B -->|--git-dir| D["@commitlint/read"]
  C -->|handles| E["Worktrees & Submodules"]
  D -->|resolves| E
Loading

File Walkthrough

Relevant files
Bug fix
index.ts
Replace find-up with git rev-parse for root detection       

@commitlint/top-level/src/index.ts

  • Replaced find-up library with git rev-parse --show-toplevel command
  • Simplified implementation from multi-step search to single git command
  • Added error handling to return undefined for non-git directories
  • Updated JSDoc to clarify worktree, submodule, and regular repo support
+19/-19 
get-edit-file-path.ts
Use git rev-parse for edit file path resolution                   

@commitlint/read/src/get-edit-file-path.ts

  • Replaced manual .git file/directory detection with git rev-parse
    --git-dir
  • Removed fs.lstat() and fs.readFile() calls for simpler implementation
  • Now correctly handles worktrees where .git is a file pointer
  • Uses execFile with promisified async wrapper for git command execution
+11/-13 
Tests
index.test.ts
Add comprehensive git worktree test coverage                         

@commitlint/top-level/src/index.test.ts

  • Added 5 new comprehensive tests for git worktree support
  • Tests cover regular repositories, subdirectories, non-git directories,
    and worktrees
  • Includes helper function initGitRepo() for test setup
  • Tests verify correct behavior from worktree subdirectories
+117/-0 
read.test.ts
Add git worktree read functionality test                                 

@commitlint/read/src/read.test.ts

  • Added new test for reading commit messages from git worktrees
  • Test creates main repo, branch, and worktree with actual git
    operations
  • Verifies read() function correctly retrieves edit commit message from
    worktree
  • Uses tmp, fs-extra, and tinyexec for test infrastructure
+48/-0   
Dependencies
package.json
Remove find-up, add test dependencies                                       

@commitlint/top-level/package.json

  • Removed find-up dependency from dependencies
  • Added test dependencies: fs-extra, tmp, and their type definitions
  • Moved @commitlint/utils to devDependencies section
+5/-4     
package.json
Add test utility dependencies                                                       

@commitlint/read/package.json

  • Added fs-extra, tmp and their TypeScript type definitions as dev
    dependencies
  • Supports new worktree test implementation with file system utilities
+5/-1     

@qodo-code-review
Copy link

qodo-code-review bot commented Jan 8, 2026

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
PATH hijacking risk

Description: The code executes the external git binary via execFileAsync("git", ...), which can be
susceptible to PATH hijacking (executing an attacker-controlled git earlier in PATH) in
hostile environments.
index.ts [1-26]

Referred Code
import { execFile } from "node:child_process";
import { promisify } from "node:util";

const execFileAsync = promisify(execFile);

export default toplevel;

/**
 * Find the git root directory using git rev-parse.
 * This correctly handles git worktrees, submodules, and regular repositories.
 */
async function toplevel(cwd?: string): Promise<string | undefined> {
	try {
		const { stdout } = await execFileAsync("git", ["rev-parse", "--show-toplevel"], {
			cwd,
		});

		const topLevel = stdout.trim();
		if (topLevel) {
			return topLevel;
		}


 ... (clipped 5 lines)
Ticket Compliance
🟡
🎫 #787
🟢 Running commitlint from within a Git worktree directory should succeed (no could not find
git root from undefined error).
Git root detection should correctly handle worktrees where .git is a file pointer (not a
directory).
The CLI package behavior should be fixed (affected package: `cli`).
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Missing error handling: The new git rev-parse --git-dir call is awaited without a try/catch or contextual error
message, so failures (e.g., git not installed, non-git dir, permissions) may throw without
graceful fallback depending on how callers handle it.

Referred Code
// Use git rev-parse --git-dir to get the correct git directory
// This handles worktrees, submodules, and regular repositories correctly
const { stdout } = await execFileAsync("git", ["rev-parse", "--git-dir"], {
	cwd: top,
});

const gitDir = stdout.trim();
return path.resolve(top, gitDir, "COMMIT_EDITMSG");

Learn more about managing compliance generic rules or creating your own custom rules

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@codesandbox-ci
Copy link

codesandbox-ci bot commented Jan 8, 2026

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

@qodo-code-review
Copy link

qodo-code-review bot commented Jan 8, 2026

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
General
Ensure temporary test directories are cleaned

To prevent leftover artifacts, wrap the test logic in a try...finally block to
ensure the temporary directory is always cleaned up, even if the test fails.

@commitlint/read/src/read.test.ts [156-200]

 test("get edit commit message from git worktree", async () => {
 	const tmpDir = tmp.dirSync({ keep: false, unsafeCleanup: true });
-	const mainRepoDir = path.join(tmpDir.name, "main");
-	const worktreeDir = path.join(tmpDir.name, "worktree");
+	try {
+		const mainRepoDir = path.join(tmpDir.name, "main");
+		const worktreeDir = path.join(tmpDir.name, "worktree");
 
-	// Initialize main repo
-	await fsExtra.mkdirp(mainRepoDir);
-	await x("git", ["init"], { nodeOptions: { cwd: mainRepoDir } });
-	await x("git", ["config", "user.email", "test@example.com"], {
-		nodeOptions: { cwd: mainRepoDir },
-	});
-	await x("git", ["config", "user.name", "test"], {
-		nodeOptions: { cwd: mainRepoDir },
-	});
-	await x("git", ["config", "commit.gpgsign", "false"], {
-		nodeOptions: { cwd: mainRepoDir },
-	});
+		// Initialize main repo
+		await fsExtra.mkdirp(mainRepoDir);
+		await x("git", ["init"], { nodeOptions: { cwd: mainRepoDir } });
+		await x("git", ["config", "user.email", "test@example.com"], {
+			nodeOptions: { cwd: mainRepoDir },
+		});
+		await x("git", ["config", "user.name", "test"], {
+			nodeOptions: { cwd: mainRepoDir },
+		});
+		await x("git", ["config", "commit.gpgsign", "false"], {
+			nodeOptions: { cwd: mainRepoDir },
+		});
 
-	// Create initial commit in main repo
-	await fs.writeFile(path.join(mainRepoDir, "file.txt"), "content");
-	await x("git", ["add", "."], { nodeOptions: { cwd: mainRepoDir } });
-	await x("git", ["commit", "-m", "initial"], {
-		nodeOptions: { cwd: mainRepoDir },
-	});
+		// Create initial commit in main repo
+		await fs.writeFile(path.join(mainRepoDir, "file.txt"), "content");
+		await x("git", ["add", "."], { nodeOptions: { cwd: mainRepoDir } });
+		await x("git", ["commit", "-m", "initial"], {
+			nodeOptions: { cwd: mainRepoDir },
+		});
 
-	// Create a branch and worktree
-	await x("git", ["branch", "worktree-branch"], {
-		nodeOptions: { cwd: mainRepoDir },
-	});
-	await x("git", ["worktree", "add", worktreeDir, "worktree-branch"], {
-		nodeOptions: { cwd: mainRepoDir },
-	});
+		// Create a branch and worktree
+		await x("git", ["branch", "worktree-branch"], {
+			nodeOptions: { cwd: mainRepoDir },
+		});
+		await x("git", ["worktree", "add", worktreeDir, "worktree-branch"], {
+			nodeOptions: { cwd: mainRepoDir },
+		});
 
-	// Make a commit in the worktree
-	await fs.writeFile(path.join(worktreeDir, "worktree-file.txt"), "worktree");
-	await x("git", ["add", "."], { nodeOptions: { cwd: worktreeDir } });
-	await x("git", ["commit", "-m", "worktree commit"], {
-		nodeOptions: { cwd: worktreeDir },
-	});
+		// Make a commit in the worktree
+		await fs.writeFile(
+			path.join(worktreeDir, "worktree-file.txt"),
+			"worktree"
+		);
+		await x("git", ["add", "."], { nodeOptions: { cwd: worktreeDir } });
+		await x("git", ["commit", "-m", "worktree commit"], {
+			nodeOptions: { cwd: worktreeDir },
+		});
 
-	// Read the edit commit message from the worktree
-	const expected = ["worktree commit\n\n"];
-	const actual = await read({ edit: true, cwd: worktreeDir });
-	expect(actual).toEqual(expected);
+		// Read the edit commit message from the worktree
+		const expected = ["worktree commit\n\n"];
+		const actual = await read({ edit: true, cwd: worktreeDir });
+		expect(actual).toEqual(expected);
+	} finally {
+		tmpDir.removeCallback();
+	}
 });
  • Apply / Chat
Suggestion importance[1-10]: 5

__

Why: The suggestion correctly identifies a potential issue with test artifact cleanup and proposes a robust try...finally pattern, which is a good practice for improving test suite reliability.

Low
  • Update

@escapedcat
Copy link
Member

Thanks @adam-moss , for some reason the windows checks are failing. Would you mind having a look?

@awojasinski
Copy link
Author

@escapedcat looks like Windows is returning short paths. Let me handle that

@escapedcat
Copy link
Member

x_X

@escapedcat
Copy link
Member

😿

Replace custom find-up implementation with git rev-parse --show-toplevel.
This correctly handles git worktrees, submodules, and regular repositories.

Also improves Windows path normalization by using resolve() before realpathSync()
to handle Windows short path (8.3 format) vs long path discrepancies between
Git output and Node.js filesystem APIs.

Fixes conventional-changelog#787
Replace custom .git detection with git rev-parse --git-dir.
This correctly resolves the git directory in worktrees where .git is a file
pointing to the actual git directory.
Add comprehensive tests for git worktree support:
- Test toplevel() works correctly in worktrees
- Test toplevel() works from subdirectories in worktrees
- Test reading edit commit message from worktree
@escapedcat
Copy link
Member

😿

@awojasinski
Copy link
Author

I'll try to get my hands on some Windows machine so I can investigate it further. Until now I was just blindly trying to fix the issue

@escapedcat
Copy link
Member

I'll try to get my hands on some Windows machine so I can investigate it further. Until now I was just blindly trying to fix the issue

Thanks for trying! No rush

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

Error when working from within a branch worktree

2 participants