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
12 changes: 12 additions & 0 deletions src/bundled/github/GitHub.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,18 @@
.description {
margin-top: var(--space-4);
}
.views {
margin-top: var(--space-4);
}
.tabCounts {
margin-inline-start: var(--space-1);
font-variant-numeric: tabular-nums;
}
.files {
display: grid;
gap: var(--space-3);
margin-top: var(--space-4);
}
/* A Solo chat's linked pull request, in the sidebar's chat icon slot. */
.soloPull {
display: inline-flex;
Expand Down
158 changes: 157 additions & 1 deletion src/bundled/github/GitHubPanel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ import {
import { StrictMode } from "react";
import { afterEach, expect, it, vi } from "vitest";
import userEvent from "@testing-library/user-event";
import { GhUnavailable, GitHubError, type GitHubCli } from "./gh";
import {
GhUnavailable,
GitHubError,
OutputTooLarge,
type GitHubCli,
} from "./gh";
import { GitHubPanel, REFRESH_MS } from "./index";
import { createPullStates } from "./pull-states";
import { ToastProvider } from "../../shared/design-system/ui/Toast";
Expand Down Expand Up @@ -1104,6 +1109,23 @@ it("places each resolution action in its matching checklist row", async () => {
const reviewMutation =
"mutation($input:AddPullRequestReviewInput!){addPullRequestReview(input:$input){clientMutationId}}";

const patch = `diff --git a/src/app.ts b/src/app.ts
index 1111111..2222222 100644
--- a/src/app.ts
+++ b/src/app.ts
@@ -1,2 +1,2 @@
const a = 1;
-const b = 2;
+const b = 3;
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..3333333
--- /dev/null
+++ b/README.md
@@ -0,0 +1 @@
+Hello
`;

it("approves someone else's pull request without a comment, at the head it showed", async () => {
const user = userEvent.setup();
let approved = false;
Expand Down Expand Up @@ -1163,6 +1185,140 @@ it("approves someone else's pull request without a comment, at the head it showe
]);
});

it("shows a pull request's changes under Changes, with its line counts", async () => {
const user = userEvent.setup();
const gh = vi.fn(async () => pullData());
const diff = vi.fn(async () => patch);
render(
<GitHubPanel
target={pull}
close={() => {}}
gh={gh as unknown as GitHubCli}
diff={diff}
/>,
{ wrapper: ToastProvider },
);
await screen.findByRole("heading", { name: "Private change #9" });
expect(screen.getByRole("tab", { name: "Summary" })).toHaveAttribute(
"aria-selected",
"true",
);
expect(diff).not.toHaveBeenCalled();
const changes = screen.getByRole("tab", { name: /^Changes/ });
expect(changes).toHaveTextContent("+3 −1");
await user.click(changes);
expect(
await screen.findByRole("region", { name: "src/app.ts" }),
).toHaveTextContent("+1−1");
expect(screen.getByRole("region", { name: "README.md" })).toHaveTextContent(
"New file",
);
expect(
screen.queryByRole("heading", { name: "Private change #9" }),
).not.toBeInTheDocument();
// Reviewing stays a click away while reading the changes.
expect(
screen.getByRole("button", { name: "Submit review" }),
).toBeInTheDocument();
expect(diff).toHaveBeenCalledWith(
{
repository: "block/private",
base: "main",
head: "abc123",
merged: undefined,
},
expect.any(AbortSignal),
);
await user.click(screen.getByRole("tab", { name: "Summary" }));
expect(
screen.getByRole("heading", { name: "Private change #9" }),
).toBeInTheDocument();
expect(
screen.queryByRole("region", { name: "src/app.ts" }),
).not.toBeInTheDocument();
await user.click(changes);
expect(screen.getByRole("region", { name: "src/app.ts" })).toBeVisible();
expect(diff).toHaveBeenCalledTimes(1);
});

it("reads a merged pull request's changes by its number", async () => {
const user = userEvent.setup();
const gh = vi.fn(async () => pullData({ state: "MERGED" }));
const diff = vi.fn(async () => patch);
render(
<GitHubPanel
target={pull}
close={() => {}}
gh={gh as unknown as GitHubCli}
diff={diff}
/>,
);
await screen.findByRole("heading", { name: "Private change #9" });
await user.click(screen.getByRole("tab", { name: /^Changes/ }));
expect(
await screen.findByRole("region", { name: "src/app.ts" }),
).toBeInTheDocument();
expect(diff).toHaveBeenCalledWith(
{ repository: "block/private", base: "main", head: "abc123", merged: 9 },
expect.any(AbortSignal),
);
});

it("points to GitHub when the changes cannot be shown", async () => {
const user = userEvent.setup();
const gh = vi.fn(async () => pullData());
const diff = vi.fn(async () => {
throw new OutputTooLarge();
});
render(
<GitHubPanel
target={pull}
close={() => {}}
gh={gh as unknown as GitHubCli}
diff={diff}
/>,
);
await screen.findByRole("heading", { name: "Private change #9" });
await user.click(screen.getByRole("tab", { name: /^Changes/ }));
expect(await screen.findByRole("alert")).toHaveTextContent(
"This diff is too large to show here.",
);
expect(screen.getByRole("link", { name: "Open on GitHub" })).toHaveAttribute(
"href",
`${pull}/files`,
);
});

it("shows a file the parser cannot render as raw text beside the rest", async () => {
const user = userEvent.setup();
const gh = vi.fn(async () => pullData({ viewerDidAuthor: true }));
const binary = `diff --git a/logo.png b/logo.png
GIT binary patch
literal 1
Ic${"$"}@
`;
render(
<GitHubPanel
target={pull}
close={() => {}}
gh={gh as unknown as GitHubCli}
diff={async () => patch + binary}
/>,
);
await screen.findByRole("heading", { name: "Private change #9" });
// The viewer's own pull request takes no review, but its changes show.
expect(
screen.queryByRole("button", { name: "Submit review" }),
).not.toBeInTheDocument();
await user.click(screen.getByRole("tab", { name: /^Changes/ }));
expect(
await screen.findByRole("region", { name: "src/app.ts" }),
).toBeInTheDocument();
expect(screen.getByRole("region", { name: "Raw diff" })).toHaveTextContent(
"GIT binary patch",
);
});

it("keeps a refused review's draft with GitHub's reason, and clears it once it is in", async () => {
const user = userEvent.setup();
let refuse = true;
Expand Down
87 changes: 87 additions & 0 deletions src/bundled/github/PullFiles.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { useEffect, useMemo, useState } from "react";
import { ArrowSquareOutIcon } from "../../shared/design-system/icons/index";
import { LoadingRegion } from "../../shared/design-system/ui/LoadingRegion";
import { DiffViewer } from "../diffs/DiffViewer";
import { parsePatch } from "../diffs/parse";
import type { GitHubDiff } from "./gh";
import { readable } from "./object";
import styles from "./GitHub.module.css";

/** One patch per file, so a file the parser cannot render falls back to raw
* text without taking the rest of the diff with it. */
export function splitPatch(content: string): string[] {
return content.split(/^(?=diff --git )/m).filter((part) => part.trim());
}

/** A pull request's Files changed, read for the base and head it names. */
export function PullFiles({
url,
pull,
diff,
}: {
url: string;
pull: Parameters<GitHubDiff>[0];
diff: GitHubDiff;
}) {
const { repository, base, head, merged } = pull;
const [result, setResult] = useState<string | Error>();
useEffect(() => {
const controller = new AbortController();
diff({ repository, base, head, merged }, controller.signal).then(
(content) => {
if (!controller.signal.aborted) setResult(content);
},
(failure) => {
if (!controller.signal.aborted) setResult(readable(failure));
},
);
return () => controller.abort();
}, [diff, repository, base, head, merged]);
const parts = useMemo(
() =>
typeof result === "string"
? splitPatch(result).map((content) => ({
content,
files: parsePatch(content),
}))
: undefined,
[result],
);
return (
<LoadingRegion
pending={result === undefined}
label="Loading changes from GitHub…"
feedback={
result instanceof Error ? (
<div role="alert">
<p>{result.message}</p>
<a
className={styles.external}
href={`${url}/files`}
target="_blank"
rel="noreferrer"
>
Open on GitHub <ArrowSquareOutIcon size={14} />
</a>
</div>
) : undefined
}
>
{parts &&
(parts.length ? (
<div className={styles.files}>
{parts.map((part, index) => (
<DiffViewer
// biome-ignore lint/suspicious/noArrayIndexKey: A file's patch can repeat; its place in this diff cannot.
key={index}
content={part.content}
files={part.files}
/>
))}
</div>
) : (
<p className={styles.files}>No file changes.</p>
))}
</LoadingRegion>
);
}
3 changes: 2 additions & 1 deletion src/bundled/github/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ Preview GitHub repositories, pull requests, issues, and commits without leaving
icon, kept current while shown. Click any github.com link to open it in a
side panel.
2. On a pull request, switch between **Discussion** and **Checks**.
3. Choose **Open on GitHub** to see the full page.
3. Switch a pull request from **Summary** to **Changes** to read its diff.
4. Choose **Open on GitHub** to see the full page.
Loading
Loading