Skip to content

Commit 4990b44

Browse files
committed
feat: add Sapling (sl) VCS backend support
Sapling joins git and jj as a supported VCS mode. Set `vcs = "sl"` in config to use `hunk diff` and `hunk show` with Sapling revsets. Repos using `.sl` or `.hg` directories are auto-detected for repo-local config. CI workflows install Sapling for test coverage.
1 parent 585e0cf commit 4990b44

8 files changed

Lines changed: 693 additions & 5 deletions

File tree

src/core/loaders.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -661,6 +661,7 @@ function buildUntrackedDiffFile(
661661
sourcePrefix: string,
662662
agentContext: AgentContext | null,
663663
) {
664+
const absolutePath = join(repoRoot, filePath);
664665
const largeFileCheck = inspectLargeUntrackedFile(repoRoot, filePath);
665666
if (largeFileCheck.shouldSkip) {
666667
return buildDiffFile(
@@ -678,6 +679,40 @@ function buildUntrackedDiffFile(
678679
);
679680
}
680681

682+
if (input.options.vcs === "sl") {
683+
if (isProbablyBinaryFile(absolutePath)) {
684+
return buildDiffFile(
685+
createSkippedBinaryMetadata(filePath, "new"),
686+
`Binary file skipped: ${filePath}\n`,
687+
index,
688+
sourcePrefix,
689+
agentContext,
690+
{ isBinary: true, isUntracked: true },
691+
);
692+
}
693+
694+
const patch = createTwoFilesPatch(
695+
"/dev/null",
696+
escapeUntrackedPatchPath(filePath),
697+
"",
698+
fs.readFileSync(absolutePath, "utf8"),
699+
"",
700+
"",
701+
{ context: 3 },
702+
).replaceAll("\r\n", "\n");
703+
704+
return buildDiffFile(
705+
parseUntrackedPatchFile(patch, filePath),
706+
patch,
707+
index,
708+
sourcePrefix,
709+
agentContext,
710+
{
711+
isUntracked: true,
712+
},
713+
);
714+
}
715+
681716
const patch = normalizeUntrackedPatchHeaders(
682717
runGitUntrackedFileDiffText(input, filePath, { repoRoot }),
683718
filePath,

src/core/sl.test.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { afterEach, describe, expect, test } from "bun:test";
2+
import { mkdtempSync, realpathSync, rmSync } from "node:fs";
3+
import { tmpdir } from "node:os";
4+
import { join } from "node:path";
5+
import { buildSlDiffArgs, runSlText } from "./sl";
6+
7+
const tempDirs: string[] = [];
8+
9+
function cleanupTempDirs() {
10+
while (tempDirs.length > 0) {
11+
const dir = tempDirs.pop();
12+
if (dir) {
13+
rmSync(dir, { recursive: true, force: true });
14+
}
15+
}
16+
}
17+
18+
function createTempDir(prefix: string) {
19+
const dir = realpathSync(mkdtempSync(join(tmpdir(), prefix)));
20+
tempDirs.push(dir);
21+
return dir;
22+
}
23+
24+
function sl(cwd: string, ...cmd: string[]) {
25+
const proc = Bun.spawnSync(["sl", "--noninteractive", "--color", "never", ...cmd], {
26+
cwd,
27+
stdout: "pipe",
28+
stderr: "pipe",
29+
stdin: "ignore",
30+
});
31+
32+
if (proc.exitCode !== 0) {
33+
const stderr = Buffer.from(proc.stderr).toString("utf8");
34+
throw new Error(stderr.trim() || `sl ${cmd.join(" ")} failed`);
35+
}
36+
37+
return Buffer.from(proc.stdout).toString("utf8");
38+
}
39+
40+
function createTempSlRepo(prefix: string) {
41+
const dir = createTempDir(prefix);
42+
43+
sl(dir, "init", "--git");
44+
45+
return dir;
46+
}
47+
48+
afterEach(() => {
49+
cleanupTempDirs();
50+
});
51+
52+
describe("sl command helpers", () => {
53+
test("reports a friendly error when sl is not installed or not on PATH", () => {
54+
expect(() =>
55+
runSlText({
56+
input: {
57+
kind: "vcs",
58+
staged: false,
59+
options: { mode: "auto", vcs: "sl" },
60+
},
61+
args: ["root"],
62+
slExecutable: "definitely-not-a-real-sl-binary",
63+
}),
64+
).toThrow(
65+
'Sapling is required for `hunk diff` when `vcs = "sl"`, but `definitely-not-a-real-sl-binary` was not found in PATH.',
66+
);
67+
});
68+
69+
test("reports a friendly error outside a sl repository", () => {
70+
const dir = createTempDir("hunk-sl-nonrepo-");
71+
72+
expect(() =>
73+
runSlText({
74+
input: {
75+
kind: "vcs",
76+
staged: false,
77+
options: { mode: "auto", vcs: "sl" },
78+
},
79+
args: ["root"],
80+
cwd: dir,
81+
}),
82+
).toThrow('`hunk diff` must be run inside a Sapling repository when `vcs = "sl"`.');
83+
});
84+
85+
test("reports a friendly error for invalid revsets", () => {
86+
const dir = createTempSlRepo("hunk-sl-invalid-revset-");
87+
const input = {
88+
kind: "vcs" as const,
89+
range: "missing_revision",
90+
staged: false,
91+
options: { mode: "auto" as const, vcs: "sl" as const },
92+
};
93+
94+
expect(() =>
95+
runSlText({
96+
input,
97+
args: buildSlDiffArgs(input),
98+
cwd: dir,
99+
}),
100+
).toThrow("`hunk diff missing_revision` could not resolve Sapling revset `missing_revision`.");
101+
});
102+
});

0 commit comments

Comments
 (0)