diff --git a/CHANGELOG.md b/CHANGELOG.md index 87a3d243..3ca3615c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/core/config.test.ts b/src/core/config.test.ts index 91786986..37881d1c 100644 --- a/src/core/config.test.ts +++ b/src/core/config.test.ts @@ -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", @@ -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"); diff --git a/src/core/vcs/index.test.ts b/src/core/vcs/index.test.ts index 3517e7f8..bb288162 100644 --- a/src/core/vcs/index.test.ts +++ b/src/core/vcs/index.test.ts @@ -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", diff --git a/src/core/vcs/index.ts b/src/core/vcs/index.ts index ea1cb0fc..f1209337 100644 --- a/src/core/vcs/index.ts +++ b/src/core/vcs/index.ts @@ -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"; @@ -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()) {