Skip to content

Commit cca193e

Browse files
committed
Render uncommon diff entries correctly
1 parent 1fc9bcc commit cca193e

3 files changed

Lines changed: 141 additions & 0 deletions

File tree

‎app/page.tsx‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ type DiffFile = {
3030
patch: string;
3131
snippet: string;
3232
sourceUrl?: string;
33+
comparisonUrl?: string;
3334
summary: FileSummary;
3435
noteReady?: boolean;
3536
};
@@ -577,6 +578,16 @@ export default function Home() {
577578
Open file ↗
578579
</a>
579580
) : null}
581+
{currentFile.comparisonUrl ? (
582+
<a
583+
className="text-button"
584+
href={currentFile.comparisonUrl}
585+
target="_blank"
586+
rel="noreferrer"
587+
>
588+
Open comparison ↗
589+
</a>
590+
) : null}
580591
</div>
581592
</div>
582593

‎scripts/build-diff-data.mjs‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -646,6 +646,9 @@ function resolveCheckoutTarget() {
646646
const repository = githubRepository(remoteUrl);
647647
const headLabel = branch || currentHead;
648648
const hasCommittedChanges = mergeBaseOid !== currentHead;
649+
const hasUncommittedChanges = Boolean(
650+
tryRepo(['status', '--porcelain=v1', '-z']),
651+
);
649652
const isDefaultBranchCheckout = branch === defaultBranch.name;
650653

651654
return {
@@ -659,6 +662,7 @@ function resolveCheckoutTarget() {
659662
remote,
660663
sourceRepositoryUrl: repository?.webUrl,
661664
baseRepositoryUrl: repository?.webUrl,
665+
hasUncommittedChanges,
662666
target: {
663667
kind: 'checkout',
664668
...(remote ? { remote: remote.name } : {}),
@@ -812,6 +816,13 @@ function githubFileUrl(repositoryUrl, ref, path) {
812816
return `${repositoryUrl}/blob/${encodeURIComponent(ref)}/${filePath}`;
813817
}
814818

819+
function githubComparisonUrl(repositoryUrl, base, head) {
820+
if (!repositoryUrl || !base || !head || head === 'WORKTREE') {
821+
return undefined;
822+
}
823+
return `${repositoryUrl}/compare/${encodeURIComponent(base)}...${encodeURIComponent(head)}`;
824+
}
825+
815826
function build() {
816827
const localWorkspace =
817828
tryRepo(['rev-parse', '--is-inside-work-tree']) === 'true';
@@ -887,6 +898,11 @@ function build() {
887898
file.status === 'deleted' ? target.base : target.head,
888899
file.path,
889900
);
901+
const comparisonUrl = githubComparisonUrl(
902+
target.hasUncommittedChanges ? undefined : target.sourceRepositoryUrl,
903+
target.base,
904+
target.head,
905+
);
890906
return {
891907
path: file.path,
892908
...(file.oldPath ? { oldPath: file.oldPath } : {}),
@@ -899,6 +915,7 @@ function build() {
899915
patch: textPatch,
900916
snippet: binary ? '' : compactSnippet(textPatch),
901917
...(sourceUrl ? { sourceUrl } : {}),
918+
...(comparisonUrl ? { comparisonUrl } : {}),
902919
};
903920
});
904921

‎tests/remote-targets.test.mjs‎

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,13 @@ test("names worktree-only checkout changes without comparing a branch to itself"
141141

142142
try {
143143
await writeFile(join(fixture.repo, "working.txt"), "working tree work\n");
144+
git(
145+
fixture.repo,
146+
"remote",
147+
"set-url",
148+
"origin",
149+
"https://github.com/example/diffsplain.git",
150+
);
144151

145152
run(fixture.repo, ["--checkout", "--output", output]);
146153
const payload = JSON.parse(await readFile(output, "utf8"));
@@ -152,6 +159,10 @@ test("names worktree-only checkout changes without comparing a branch to itself"
152159
payload.change.summary,
153160
"Shows staged, unstaged, and untracked changes in the current checkout.",
154161
);
162+
assert.ok(
163+
payload.files.every((file) => file.comparisonUrl === undefined),
164+
"uncommitted work must not link to a commit-only comparison",
165+
);
155166

156167
git(fixture.repo, "add", "working.txt");
157168
git(fixture.repo, "commit", "-qm", "local main work");
@@ -161,6 +172,10 @@ test("names worktree-only checkout changes without comparing a branch to itself"
161172

162173
assert.notEqual(committed.repo.base, committed.repo.head);
163174
assert.equal(committed.change.title, "Local changes on main");
175+
assert.match(
176+
committed.files[0].comparisonUrl,
177+
/^https:\/\/github\.com\/example\/diffsplain\/compare\//,
178+
);
164179
} finally {
165180
await rm(fixture.root, { recursive: true, force: true });
166181
}
@@ -199,6 +214,104 @@ test("builds a remote repo target without a local checkout", async () => {
199214
}
200215
});
201216

217+
test("renders uncommon range entries with the right content and GitHub links", async () => {
218+
const fixture = await makeRemoteRepo();
219+
const output = join(fixture.root, "uncommon-range.json");
220+
221+
try {
222+
await writeFile(join(fixture.repo, "deleted.txt"), "remove me\n");
223+
await writeFile(join(fixture.repo, "moved-from.txt"), "move me\n");
224+
await writeFile(join(fixture.repo, "changed.bin"), Buffer.from([0, 1]));
225+
await writeFile(
226+
join(fixture.repo, "long.txt"),
227+
Array.from({ length: 240 }, (_, index) => `before ${index}\n`).join(""),
228+
);
229+
git(fixture.repo, "add", ".");
230+
git(fixture.repo, "commit", "-qm", "uncommon base");
231+
const base = git(fixture.repo, "rev-parse", "HEAD");
232+
233+
await writeFile(join(fixture.repo, "changed.bin"), Buffer.from([0, 2]));
234+
await writeFile(join(fixture.repo, "added.bin"), Buffer.from([0, 4]));
235+
await rm(join(fixture.repo, "deleted.txt"));
236+
git(fixture.repo, "mv", "moved-from.txt", "moved-to.txt");
237+
await writeFile(
238+
join(fixture.repo, "long.txt"),
239+
Array.from({ length: 240 }, (_, index) => `after ${index}\n`).join(""),
240+
);
241+
git(fixture.repo, "add", ".");
242+
git(fixture.repo, "commit", "-qm", "uncommon changes");
243+
const head = git(fixture.repo, "rev-parse", "HEAD");
244+
git(fixture.repo, "remote", "set-url", "origin", "https://github.com/example/diffsplain.git");
245+
const before = checkoutState(fixture.repo);
246+
247+
run(fixture.repo, ["--base", base, "--head", head, "--output", output]);
248+
const payload = JSON.parse(await readFile(output, "utf8"));
249+
const files = Object.fromEntries(payload.files.map((file) => [file.path, file]));
250+
const source = (ref, path) =>
251+
`https://github.com/example/diffsplain/blob/${ref}/${path}`;
252+
const comparison = `https://github.com/example/diffsplain/compare/${base}...${head}`;
253+
254+
assert.deepEqual(
255+
payload.files.map((file) => file.path),
256+
["added.bin", "changed.bin", "deleted.txt", "long.txt", "moved-to.txt"],
257+
);
258+
assert.equal(files["added.bin"].status, "binary");
259+
assert.equal(files["added.bin"].isBinary, true);
260+
assert.equal(files["added.bin"].patch, "");
261+
assert.equal(files["added.bin"].sourceUrl, source(head, "added.bin"));
262+
assert.equal(files["added.bin"].comparisonUrl, comparison);
263+
assert.equal(files["changed.bin"].status, "binary");
264+
assert.equal(files["changed.bin"].isBinary, true);
265+
assert.equal(files["changed.bin"].patch, "");
266+
assert.equal(files["changed.bin"].sourceUrl, source(head, "changed.bin"));
267+
assert.equal(files["changed.bin"].comparisonUrl, comparison);
268+
assert.equal(files["deleted.txt"].status, "deleted");
269+
assert.equal(files["deleted.txt"].isBinary, false);
270+
assert.match(files["deleted.txt"].patch, /-remove me/);
271+
assert.equal(files["deleted.txt"].sourceUrl, source(base, "deleted.txt"));
272+
assert.equal(files["deleted.txt"].comparisonUrl, comparison);
273+
assert.equal(files["moved-to.txt"].status, "renamed");
274+
assert.equal(files["moved-to.txt"].oldPath, "moved-from.txt");
275+
assert.match(files["moved-to.txt"].patch, /similarity index 100%/);
276+
assert.equal(files["moved-to.txt"].sourceUrl, source(head, "moved-to.txt"));
277+
assert.equal(files["moved-to.txt"].comparisonUrl, comparison);
278+
assert.equal(files["long.txt"].status, "modified");
279+
assert.equal(files["long.txt"].isBinary, false);
280+
assert.equal(files["long.txt"].isTruncated, true);
281+
assert.ok(files["long.txt"].snippet.split("\n").length <= 180);
282+
assert.match(files["long.txt"].snippet, /^@@ /m);
283+
assert.equal(files["long.txt"].sourceUrl, source(head, "long.txt"));
284+
assert.equal(files["long.txt"].comparisonUrl, comparison);
285+
assert.deepEqual(checkoutState(fixture.repo), before);
286+
} finally {
287+
await rm(fixture.root, { recursive: true, force: true });
288+
}
289+
});
290+
291+
test("keeps links out of worktree entries and leaves the checkout untouched", async () => {
292+
const fixture = await makeRemoteRepo();
293+
const output = join(fixture.root, "uncommon-worktree.json");
294+
295+
try {
296+
await writeFile(join(fixture.repo, "worktree.bin"), Buffer.from([0, 1]));
297+
const before = checkoutState(fixture.repo);
298+
299+
run(fixture.repo, ["--worktree", "--output", output]);
300+
const payload = JSON.parse(await readFile(output, "utf8"));
301+
const [file] = payload.files;
302+
303+
assert.equal(file.path, "worktree.bin");
304+
assert.equal(file.status, "binary");
305+
assert.equal(file.isBinary, true);
306+
assert.equal(file.patch, "");
307+
assert.equal(file.sourceUrl, undefined);
308+
assert.equal(file.comparisonUrl, undefined);
309+
assert.deepEqual(checkoutState(fixture.repo), before);
310+
} finally {
311+
await rm(fixture.root, { recursive: true, force: true });
312+
}
313+
});
314+
202315
test("builds a pull request range through gh without changing the checkout", async () => {
203316
const fixture = await makeRemoteRepo();
204317
const bin = join(fixture.root, "bin");

0 commit comments

Comments
 (0)