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
4 changes: 2 additions & 2 deletions src/core/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ describe("config resolution", () => {
expect(fallbackResolved.input.options.excludeUntracked).toBe(false);
});

test("defaults to git VCS mode and accepts jj from config", () => {
test("defaults to git VCS mode and accepts registered VCS modes from config", () => {
const home = createTempDir("hunk-config-home-");
mkdirSync(join(home, ".config", "hunk"), { recursive: true });
writeFileSync(join(home, ".config", "hunk", "config.toml"), 'vcs = "jj"\n');
Expand All @@ -188,7 +188,7 @@ describe("config resolution", () => {
expect(configuredResolved.input.options.vcs).toBe("jj");
});

test("auto-detects jj checkouts before falling back to git mode", () => {
test("auto-detects registered VCS checkouts before falling back to git mode", () => {
const home = createTempDir("hunk-config-home-");
const jjRepo = createTempDir("hunk-config-jj-repo-");
const colocatedRepo = createTempDir("hunk-config-colocated-repo-");
Expand Down
35 changes: 7 additions & 28 deletions src/core/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import fs from "node:fs";
import { dirname, join, resolve } from "node:path";
import { join } from "node:path";
import { resolveGlobalConfigPath } from "./paths";
import { detectVcs, findVcsRepoRootCandidate, isVcsId } from "./vcs";
import type {
CliInput,
CommonOptions,
Expand Down Expand Up @@ -39,7 +40,7 @@ function normalizeLayoutMode(value: unknown): LayoutMode | undefined {

/** Accept only the VCS backends Hunk can load directly. */
function normalizeVcsMode(value: unknown): VcsMode | undefined {
return value === "git" || value === "jj" ? value : undefined;
return isVcsId(value) ? value : undefined;
}

/** Accept only plain booleans from config files. */
Expand Down Expand Up @@ -101,31 +102,9 @@ function resolveConfigLayer(source: Record<string, unknown>, input: CliInput): C
return resolved;
}

/** Return the first parent that looks like a repository root. */
function findRepoRoot(cwd = process.cwd()) {
let current = resolve(cwd);

for (;;) {
if (fs.existsSync(join(current, ".git")) || fs.existsSync(join(current, ".jj"))) {
return current;
}

const parent = dirname(current);
if (parent === current) {
return undefined;
}

current = parent;
}
}

/** Choose the VCS backend that best matches the discovered checkout. */
function detectRepoVcsMode(repoRoot?: string): VcsMode {
if (repoRoot && fs.existsSync(join(repoRoot, ".jj"))) {
return "jj";
}

return "git";
function detectRepoVcsMode(cwd: string): VcsMode {
return detectVcs(cwd)?.id ?? "git";
}

/** Parse one TOML config file into a plain object. */
Expand All @@ -147,13 +126,13 @@ export function resolveConfiguredCliInput(
input: CliInput,
{ cwd = process.cwd(), env = process.env }: ConfigResolutionOptions = {},
): HunkConfigResolution {
const repoRoot = findRepoRoot(cwd);
const repoRoot = findVcsRepoRootCandidate(cwd);
const repoConfigPath = repoRoot ? join(repoRoot, ".hunk", "config.toml") : undefined;
const userConfigPath = resolveGlobalConfigPath(env);

let resolvedOptions: CommonOptions = {
mode: DEFAULT_VIEW_PREFERENCES.mode,
vcs: detectRepoVcsMode(repoRoot),
vcs: detectRepoVcsMode(cwd),
// Keep the built-in theme default explicit so stdin-backed startup paths do not depend on
// renderer theme-mode detection for their initial palette.
theme: "graphite",
Expand Down
70 changes: 38 additions & 32 deletions src/core/jj.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { join } from "node:path";
import { buildJjDiffArgs, runJjText } from "./jj";

const tempDirs: string[] = [];
// Windows subprocess setup can exceed Bun's default 5s timeout while generating enough jj changes.
const JjAmbiguousPrefixTestTimeoutMs = 20_000;

function cleanupTempDirs() {
while (tempDirs.length > 0) {
Expand Down Expand Up @@ -131,36 +133,40 @@ describe("jj command helpers", () => {
).toThrow("`hunk diff missing_revision` could not resolve Jujutsu revset `missing_revision`.");
});

test("reports a friendly error for ambiguous change id prefixes", () => {
const dir = createTempJjRepo("hunk-jj-ambiguous-prefix-");
let prefix: string | undefined;

for (let index = 0; index < 32 && !prefix; index += 1) {
writeFileSync(join(dir, `file-${index}.txt`), `${index}\n`);
jj(dir, "commit", "-m", `commit ${index}`);

prefix = findDuplicatePrefix(
jj(dir, "log", "--no-graph", "-T", 'change_id ++ "\n"').trim().split("\n"),
);
}

if (!prefix) {
throw new Error("Expected generated jj changes to include an ambiguous prefix.");
}

const input = {
kind: "vcs" as const,
range: prefix,
staged: false,
options: { mode: "auto" as const, vcs: "jj" as const },
};

expect(() =>
runJjText({
input,
args: buildJjDiffArgs(input),
cwd: dir,
}),
).toThrow(`\`hunk diff ${prefix}\` could not resolve Jujutsu revset \`${prefix}\`.`);
});
test(
"reports a friendly error for ambiguous change id prefixes",
() => {
const dir = createTempJjRepo("hunk-jj-ambiguous-prefix-");
let prefix: string | undefined;

for (let index = 0; index < 32 && !prefix; index += 1) {
writeFileSync(join(dir, `file-${index}.txt`), `${index}\n`);
jj(dir, "commit", "-m", `commit ${index}`);

prefix = findDuplicatePrefix(
jj(dir, "log", "--no-graph", "-T", 'change_id ++ "\n"').trim().split("\n"),
);
}

if (!prefix) {
throw new Error("Expected generated jj changes to include an ambiguous prefix.");
}

const input = {
kind: "vcs" as const,
range: prefix,
staged: false,
options: { mode: "auto" as const, vcs: "jj" as const },
};

expect(() =>
runJjText({
input,
args: buildJjDiffArgs(input),
cwd: dir,
}),
).toThrow(`\`hunk diff ${prefix}\` could not resolve Jujutsu revset \`${prefix}\`.`);
},
JjAmbiguousPrefixTestTimeoutMs,
);
});
Loading
Loading