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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ All notable user-visible changes to Hunk are documented in this file.

### Fixed

- Fixed VCS auto-detection so a Git repository nested under a parent Jujutsu workspace still uses Git mode by default.

## [0.13.1] - 2026-05-19

### Fixed
Expand Down
8 changes: 8 additions & 0 deletions src/core/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,12 +246,16 @@ describe("config resolution", () => {
const jjRepo = createTempDir("hunk-config-jj-repo-");
const colocatedRepo = createTempDir("hunk-config-colocated-repo-");
const gitRepo = createTempDir("hunk-config-git-repo-");
const parentJjRepo = createTempDir("hunk-config-parent-jj-");
const gitRepoInsideParentJj = join(parentJjRepo, "git-project");
const plainDir = createTempDir("hunk-config-no-repo-");

createJjRepo(jjRepo);
createRepo(colocatedRepo);
createJjRepo(colocatedRepo);
createRepo(gitRepo);
createJjRepo(parentJjRepo);
createRepo(gitRepoInsideParentJj);

const input = {
kind: "vcs",
Expand All @@ -269,6 +273,10 @@ describe("config resolution", () => {
expect(
resolveConfiguredCliInput(input, { cwd: gitRepo, env: { HOME: home } }).input.options.vcs,
).toBe("git");
expect(
resolveConfiguredCliInput(input, { cwd: gitRepoInsideParentJj, env: { HOME: home } }).input
.options.vcs,
).toBe("git");
expect(
resolveConfiguredCliInput(input, { cwd: plainDir, env: { HOME: home } }).input.options.vcs,
).toBe("git");
Expand Down
12 changes: 12 additions & 0 deletions src/core/vcs/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,18 @@ describe("VCS adapter registry", () => {
expect(findVcsRepoRootCandidate(nested)).toBe(repo);
});

test("prefers the nearest checkout over a parent repository with higher adapter priority", () => {
const parent = createTempDir("hunk-vcs-parent-jj-");
const repo = join(parent, "project");
const nested = join(repo, "src", "nested");
mkdirSync(join(parent, ".jj"));
mkdirSync(join(repo, ".git"), { recursive: true });
mkdirSync(nested, { recursive: true });

expect(detectVcs(nested)).toEqual({ id: "git", repoRoot: repo });
expect(findVcsRepoRootCandidate(nested)).toBe(repo);
});

test("maps CLI inputs to neutral review operations", () => {
const diffInput = {
kind: "vcs",
Expand Down
24 changes: 19 additions & 5 deletions src/core/vcs/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { dirname, resolve } from "node:path";
import { dirname, relative, resolve } from "node:path";
import { HunkUserError } from "../errors";
import { gitAdapter } from "./git";
import { jjAdapter } from "./jj";
Expand All @@ -18,14 +18,28 @@ export function isVcsId(value: unknown): value is VcsId {
return vcsAdapters.some((adapter) => adapter.id === value);
}

/** Detect the nearest containing VCS checkout, using adapter order only to break same-root ties. */
export function detectVcs(cwd: string): VcsDetection | null {
const start = resolve(cwd);
let bestDetection: VcsDetection | null = null;
let bestDistance = Number.POSITIVE_INFINITY;

for (const adapter of vcsAdapters) {
const detected = adapter.detect(cwd);
if (detected) {
return detected;
const detected = adapter.detect(start);
if (!detected) {
continue;
}

const distance = relative(detected.repoRoot, start)
.split(/[\\/]+/)
.filter(Boolean).length;
if (distance < bestDistance) {
bestDetection = detected;
bestDistance = distance;
}
}
return null;

return bestDetection;
}

export function findVcsRepoRootCandidate(cwd = process.cwd()) {
Expand Down
Loading