дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 03:05:40 +02:00
feat(github): pull request tool window, timeline and review flow (#477)
Replaces the Pull Requests tool window with a full review workflow on top of gh's authentication. - List with search, quick filters and paging; a tab per pull request and a New Pull Request tab. - Details: commit picker, changes tree with viewed state, CI and reviewer status, merge, close and reopen. - Timeline editor: comments, review threads, reactions and suggested changes. - Review in the diff viewer and, for the checked-out branch, in the editor (review mode). - New generic vcs extension points: registerDiffViewerProvider, lineInlays, reviewGutter, branch widget extensions; new vcs_merge_base and gh_api commands. - CSP: img-src allows https://avatars.githubusercontent.com for avatars. Fixed while testing against a real repository: remotes behind an SSH host alias, mergeability stuck on UNKNOWN, Apply Locally staying disabled after a checkout, relative links navigating the app's page, and review inlays wider than the pane. Co-authored-by: imacross <[email protected]>
Цей коміт міститься в:
1 джерело
696377acc2
коміт
059fd8a69b
192 files changed
+23668
-2076
No files matched your search
@@ -1,19 +1,308 @@
|
||||
/**
|
||||
* GitHub pull requests through the gh CLI: the Pull Requests tool window (list,
|
||||
* details, files diffed in the diff viewer with review comments, commits,
|
||||
* conversation), checkout, create, review and comment.
|
||||
* GitHub pull requests, ported from IntelliJ's GitHub plugin: the Pull Requests tool window
|
||||
* (list, `#N` details tabs, New Pull Request), the timeline editor, review in the diff
|
||||
* viewer and review mode. GitHub is reached through gh (`api/`), cached per repository
|
||||
* and pull request (`data/`); shared components live in `ui/`, the shell in `toolwindow/`.
|
||||
*
|
||||
* Action ids map IntelliJ's (`Github.PullRequest.Show` → `github.pullRequest.show`);
|
||||
* `github.pullRequests`, `.createPullRequest`, `.checkoutPullRequest`, `.openInBrowser`
|
||||
* and `.refresh` are the Git › GitHub menu's.
|
||||
*/
|
||||
import { registerActions } from "@/platform/actions";
|
||||
import "./ui/palette.css";
|
||||
import { registerActions, type Action } from "@/platform/actions";
|
||||
import { getContext, setContext } from "@/platform/context";
|
||||
import { useProject } from "@/platform/project";
|
||||
import { registerOverlay, registerToolWindow } from "@/workbench/contributions";
|
||||
import { GithubDialogHost } from "./dialogs";
|
||||
import { checkoutAction, createPullRequest, openInBrowser, showPullRequests, WINDOW } from "./ops";
|
||||
import { PullRequestsHeader, PullRequestsView } from "./PullRequestsView";
|
||||
import { refreshAll, resetGithub } from "./store";
|
||||
import { lazyComponent } from "@/ui";
|
||||
import { maybeEditorService, registerEditorKind, registerOverlay, registerToolWindow } from "@/workbench/contributions";
|
||||
import { activateReviewDiff, toggleReviewDiffViewed } from "./diff";
|
||||
import { reloadList } from "./data/list";
|
||||
import { checkoutPullRequest, clearDiffCache, copyUrl, openInBrowser, openInWorktree } from "./data/local";
|
||||
import { deleteMergedBranch } from "./data/operations";
|
||||
import { cachedDetails, loadPullRequestData, reloadPullRequest } from "./data/pullRequest";
|
||||
import { refreshConnection, resetConnection, useGhRepository } from "./data/repository";
|
||||
import { showSubmitReview } from "./details/SubmitReviewPopup";
|
||||
import { activateReviewMode } from "./reviewMode";
|
||||
import { githubHandler, handlerContextKey } from "./toolwindow/handlers";
|
||||
import {
|
||||
openCreatePullRequest,
|
||||
openPullRequest,
|
||||
selectOpenedPullRequest,
|
||||
showPullRequests,
|
||||
WINDOW,
|
||||
} from "./toolwindow/open";
|
||||
import {
|
||||
changeRepository,
|
||||
PullRequestsHeaderActions,
|
||||
PullRequestsTabs,
|
||||
PullRequestsToolWindow,
|
||||
usePullRequestsHeaderMenu,
|
||||
} from "./toolwindow/PullRequestsToolWindow";
|
||||
import { contextPullRequest, resetTabs } from "./toolwindow/store";
|
||||
import {
|
||||
activeTimelinePullRequest,
|
||||
openTimeline,
|
||||
TIMELINE_KIND,
|
||||
timelineIcon,
|
||||
timelineTitle,
|
||||
} from "./toolwindow/timelineInput";
|
||||
import { GithubDialogHost, numberDialog } from "./ui/dialogs";
|
||||
|
||||
const WHEN = "projectOpen";
|
||||
const CONNECTED = "projectOpen && githubConnected";
|
||||
const IN_WINDOW = `activeToolWindow == '${WINDOW}'`;
|
||||
|
||||
/** The pull request an action applies to: its argument, the timeline editor, the active tab or the list selection. */
|
||||
function targetPullRequest(arg: unknown): number | null {
|
||||
if (typeof arg === "number") return arg;
|
||||
return activeTimelinePullRequest() ?? contextPullRequest();
|
||||
}
|
||||
|
||||
async function withPullRequest(arg: unknown, run: (number: number) => unknown) {
|
||||
const n = targetPullRequest(arg);
|
||||
if (n !== null) await run(n);
|
||||
}
|
||||
|
||||
async function prUrl(number: number): Promise<string> {
|
||||
return cachedDetails(number)?.url ?? (await loadPullRequestData(number, "details")).url;
|
||||
}
|
||||
|
||||
function actions(): Action[] {
|
||||
return [
|
||||
// Git › GitHub
|
||||
{
|
||||
// Github.View.Pull.Request
|
||||
id: "github.pullRequests",
|
||||
title: "View Pull Requests",
|
||||
category: "GitHub",
|
||||
icon: "toolwindow-pull-requests",
|
||||
keywords: ["github", "pr", "review", "merge request", "code review"],
|
||||
when: WHEN,
|
||||
run: () => void showPullRequests(),
|
||||
},
|
||||
{
|
||||
// Github.Create.Pull.Request
|
||||
id: "github.createPullRequest",
|
||||
title: "Create Pull Request…",
|
||||
category: "GitHub",
|
||||
icon: "plus",
|
||||
keywords: ["github", "pr", "new pull request", "start pull request", "merge request", "code review"],
|
||||
when: WHEN,
|
||||
keyWhen: IN_WINDOW,
|
||||
run: () => openCreatePullRequest(),
|
||||
},
|
||||
{
|
||||
id: "github.checkoutPullRequest",
|
||||
title: "Checkout Pull Request…",
|
||||
description: "Checks out the selected pull request, a number given as argument, or asks for one.",
|
||||
category: "GitHub",
|
||||
icon: "branch",
|
||||
keywords: ["github", "pr"],
|
||||
when: WHEN,
|
||||
run: async ({ arg }) => {
|
||||
if (!(await showPullRequests(false))) return;
|
||||
const n = targetPullRequest(arg) ?? (await numberDialog("Checkout Pull Request"));
|
||||
if (n) await checkoutPullRequest(n);
|
||||
},
|
||||
},
|
||||
{
|
||||
// Github.Open.In.Browser
|
||||
id: "github.openInBrowser",
|
||||
title: "Open on GitHub",
|
||||
category: "GitHub",
|
||||
icon: "external-link",
|
||||
when: WHEN,
|
||||
run: async () => {
|
||||
if (!(await showPullRequests(false))) return;
|
||||
const n = targetPullRequest(undefined);
|
||||
const url = n !== null ? await prUrl(n) : useGhRepository.getState().connection?.repository.url;
|
||||
if (url) await openInBrowser(url);
|
||||
},
|
||||
},
|
||||
{
|
||||
// Github.PullRequest.List.Reload
|
||||
id: "github.refresh",
|
||||
title: "Refresh Pull Requests List",
|
||||
category: "GitHub",
|
||||
icon: "refresh",
|
||||
keywords: ["refresh list", "reload"],
|
||||
when: WHEN,
|
||||
keyWhen: `${IN_WINDOW} && githubPrTab == 'list'`,
|
||||
run: async () => {
|
||||
await refreshConnection();
|
||||
if (useGhRepository.getState().connection) await reloadList();
|
||||
},
|
||||
},
|
||||
// Tool window
|
||||
{
|
||||
// GHPRSwitchRemoteAction
|
||||
id: "github.changeRepository",
|
||||
title: "Change Repository or Account…",
|
||||
category: "GitHub",
|
||||
when: CONNECTED,
|
||||
run: () => changeRepository(),
|
||||
},
|
||||
{
|
||||
// pull.request.select.action
|
||||
id: "github.selectOpenedPullRequest",
|
||||
title: "Select Opened Pull Request",
|
||||
category: "GitHub",
|
||||
icon: "locate",
|
||||
when: `${CONNECTED} && githubTimelineActive`,
|
||||
run: () => selectOpenedPullRequest(),
|
||||
},
|
||||
// A pull request
|
||||
{
|
||||
// Github.PullRequest.Show
|
||||
id: "github.pullRequest.show",
|
||||
title: "View Pull Request",
|
||||
description: "View pull request information and timeline",
|
||||
category: "GitHub",
|
||||
when: CONNECTED,
|
||||
run: ({ arg }) => withPullRequest(arg, (n) => openPullRequest(n)),
|
||||
},
|
||||
{
|
||||
// Github.PullRequest.Timeline.Show
|
||||
id: "github.pullRequest.showTimeline",
|
||||
title: "View Timeline",
|
||||
description: "View pull request timeline",
|
||||
category: "GitHub",
|
||||
icon: "github-timeline",
|
||||
when: CONNECTED,
|
||||
run: ({ arg }) => withPullRequest(arg, (n) => openTimeline(n, { focus: true })),
|
||||
},
|
||||
{
|
||||
// Github.PullRequest.Details.Reload
|
||||
id: "github.pullRequest.refresh",
|
||||
title: "Refresh Pull Request",
|
||||
category: "GitHub",
|
||||
icon: "refresh",
|
||||
when: CONNECTED,
|
||||
keyWhen: `${IN_WINDOW} && githubPrTab == 'pr'`,
|
||||
run: ({ arg }) => withPullRequest(arg, reloadPullRequest),
|
||||
},
|
||||
{
|
||||
// Github.PullRequest.Timeline.Update
|
||||
id: "github.pullRequest.timeline.refresh",
|
||||
title: "Refresh Timeline",
|
||||
description: "Check for new timeline events",
|
||||
category: "GitHub",
|
||||
icon: "refresh",
|
||||
when: `${CONNECTED} && githubTimelineActive`,
|
||||
keyWhen: "editorAreaFocus",
|
||||
run: () => withPullRequest(undefined, reloadPullRequest),
|
||||
},
|
||||
{
|
||||
// Github.PullRequest.Open.Link
|
||||
id: "github.pullRequest.openInBrowser",
|
||||
title: "View Pull Request in Browser",
|
||||
category: "GitHub",
|
||||
icon: "external-link",
|
||||
keywords: ["open pull request in github"],
|
||||
when: CONNECTED,
|
||||
run: ({ arg }) => withPullRequest(arg, async (n) => openInBrowser(await prUrl(n))),
|
||||
},
|
||||
{
|
||||
// Github.PullRequest.Copy.Link
|
||||
id: "github.pullRequest.copyUrl",
|
||||
title: "Copy Pull Request URL",
|
||||
category: "GitHub",
|
||||
icon: "copy",
|
||||
when: CONNECTED,
|
||||
run: ({ arg }) => withPullRequest(arg, async (n) => copyUrl(await prUrl(n))),
|
||||
},
|
||||
{
|
||||
// Github.PullRequest.CheckoutBranch
|
||||
id: "github.pullRequest.checkoutBranch",
|
||||
title: "Checkout Branch",
|
||||
description: "Fetch and checkout the pull request branch",
|
||||
category: "GitHub",
|
||||
icon: "branch",
|
||||
when: CONNECTED,
|
||||
run: ({ arg }) => withPullRequest(arg, checkoutPullRequest),
|
||||
},
|
||||
{
|
||||
// Github.PullRequest.OpenInWorktree
|
||||
id: "github.pullRequest.openInWorktree",
|
||||
title: "Open in Worktree",
|
||||
description: "Fetch and open the pull request branch in a Git worktree",
|
||||
category: "GitHub",
|
||||
when: CONNECTED,
|
||||
run: ({ arg }) => withPullRequest(arg, openInWorktree),
|
||||
},
|
||||
{
|
||||
// GitHub.Timeline.DeleteMergedBranch
|
||||
id: "github.pullRequest.deleteMergedBranch",
|
||||
title: "Delete Branch",
|
||||
description: "Delete the merged branch on GitHub",
|
||||
category: "GitHub",
|
||||
icon: "trash",
|
||||
hidden: true,
|
||||
when: CONNECTED,
|
||||
run: ({ arg }) =>
|
||||
withPullRequest(arg, async (n) => {
|
||||
const ref = (await loadPullRequestData(n, "details")).headRef;
|
||||
if (ref) await deleteMergedBranch(n, ref.id);
|
||||
}),
|
||||
},
|
||||
// Implemented by other parts (`provideGithubHandler`)
|
||||
{
|
||||
// Github.PullRequest.Review.Submit
|
||||
id: "github.pullRequest.submitReview",
|
||||
title: "Submit Review…",
|
||||
category: "GitHub",
|
||||
// The details panel's popup; without it (the branch popup in review mode) one of its own.
|
||||
when: `${CONNECTED} && (${handlerContextKey("submitReview")} || githubCurrentBranchPullRequest)`,
|
||||
run: ({ arg }) =>
|
||||
withPullRequest(arg, (n) => {
|
||||
const handler = githubHandler("submitReview");
|
||||
if (handler) handler(n);
|
||||
else void showSubmitReview(n);
|
||||
}),
|
||||
},
|
||||
{
|
||||
// Github.PullRequest.Changed.MarkViewed.Toggle
|
||||
id: "github.pullRequest.toggleViewed",
|
||||
title: "Toggle viewed/not viewed state",
|
||||
description: "Toggle viewed/not viewed state for the selection or file",
|
||||
category: "GitHub",
|
||||
// The file of the focused review diff, else the details panel's selection.
|
||||
when: `${CONNECTED} && (${handlerContextKey("toggleViewed")} || githubReviewDiff)`,
|
||||
run: () => {
|
||||
if (!(getContext().diffEditorFocus && toggleReviewDiffViewed())) githubHandler("toggleViewed")?.();
|
||||
},
|
||||
},
|
||||
{
|
||||
// Github.Pull.Request.Show.In.Toolwindow
|
||||
id: "github.pullRequest.showInToolWindow",
|
||||
title: "Show Pull Request in the Tool Window",
|
||||
category: "GitHub",
|
||||
icon: "toolwindow-pull-requests",
|
||||
when: `${CONNECTED} && ${handlerContextKey("showCurrentBranchPullRequest")}`,
|
||||
run: () => githubHandler("showCurrentBranchPullRequest")?.(),
|
||||
},
|
||||
{
|
||||
// Github.Pull.Request.Branch.Update
|
||||
id: "github.pullRequest.updateBranch",
|
||||
title: "Update to Enable Review Mode…",
|
||||
category: "GitHub",
|
||||
icon: "github-warning",
|
||||
when: `${CONNECTED} && ${handlerContextKey("updateReviewBranch")}`,
|
||||
run: () => githubHandler("updateReviewBranch")?.(),
|
||||
},
|
||||
{
|
||||
// Github.Pull.Request.Review.In.Editor.Toggle
|
||||
id: "github.pullRequest.reviewMode",
|
||||
title: "Review Mode",
|
||||
category: "GitHub",
|
||||
when: `${CONNECTED} && ${handlerContextKey("toggleReviewMode")}`,
|
||||
toggled: () => githubHandler("isReviewModeOn")?.() ?? false,
|
||||
run: () => githubHandler("toggleReviewMode")?.(),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function activate(): () => void {
|
||||
setContext("githubPrTab", "list");
|
||||
const disposers = [
|
||||
registerToolWindow({
|
||||
id: WINDOW,
|
||||
@@ -22,59 +311,39 @@ export function activate(): () => void {
|
||||
anchor: "left",
|
||||
order: 2,
|
||||
stripe: "onUse",
|
||||
component: PullRequestsView,
|
||||
headerContent: PullRequestsHeader,
|
||||
component: PullRequestsToolWindow,
|
||||
headerContent: PullRequestsTabs,
|
||||
headerActions: PullRequestsHeaderActions,
|
||||
headerMenu: usePullRequestsHeaderMenu,
|
||||
}),
|
||||
registerEditorKind({
|
||||
kind: TIMELINE_KIND,
|
||||
component: lazyComponent(() => import("./timeline/TimelineEditor").then((m) => m.TimelineEditor)),
|
||||
title: timelineTitle,
|
||||
icon: timelineIcon,
|
||||
}),
|
||||
registerOverlay({ id: "github.dialogs", component: GithubDialogHost }),
|
||||
registerActions([
|
||||
{
|
||||
id: "github.pullRequests",
|
||||
title: "View Pull Requests",
|
||||
category: "GitHub",
|
||||
icon: "toolwindow-pull-requests",
|
||||
keywords: ["github", "pr", "review"],
|
||||
when: WHEN,
|
||||
run: () => showPullRequests(),
|
||||
},
|
||||
{
|
||||
id: "github.createPullRequest",
|
||||
title: "Create Pull Request…",
|
||||
category: "GitHub",
|
||||
icon: "plus",
|
||||
keywords: ["github", "pr"],
|
||||
when: WHEN,
|
||||
run: () => createPullRequest(),
|
||||
},
|
||||
{
|
||||
id: "github.checkoutPullRequest",
|
||||
title: "Checkout Pull Request…",
|
||||
description: "Checks out the selected pull request, a number given as argument, or asks for one.",
|
||||
category: "GitHub",
|
||||
icon: "branch",
|
||||
keywords: ["github", "pr"],
|
||||
when: WHEN,
|
||||
run: ({ arg }) => checkoutAction(arg),
|
||||
},
|
||||
{
|
||||
id: "github.openInBrowser",
|
||||
title: "Open on GitHub",
|
||||
category: "GitHub",
|
||||
icon: "external-link",
|
||||
when: WHEN,
|
||||
run: () => openInBrowser(),
|
||||
},
|
||||
{
|
||||
id: "github.refresh",
|
||||
title: "Refresh Pull Requests",
|
||||
category: "GitHub",
|
||||
icon: "refresh",
|
||||
when: WHEN,
|
||||
run: () => refreshAll(),
|
||||
},
|
||||
]),
|
||||
useProject.subscribe((s, prev) => {
|
||||
if (s.project?.root !== prev.project?.root) resetGithub();
|
||||
registerActions(actions()),
|
||||
useGhRepository.subscribe((s, prev) => {
|
||||
setContext("githubConnected", !!s.connection);
|
||||
setContext("githubTimelineActive", activeTimelinePullRequest() !== null);
|
||||
const a = s.connection?.coordinates;
|
||||
const b = prev.connection?.coordinates;
|
||||
if (a?.owner !== b?.owner || a?.name !== b?.name) {
|
||||
resetTabs();
|
||||
clearDiffCache();
|
||||
}
|
||||
}),
|
||||
useProject.subscribe((s, prev) => {
|
||||
if (s.project?.root !== prev.project?.root) resetConnection();
|
||||
}),
|
||||
activateReviewDiff(),
|
||||
activateReviewMode(),
|
||||
];
|
||||
const editors = maybeEditorService();
|
||||
const stopEditors = editors?.onActiveInputChange?.(() =>
|
||||
setContext("githubTimelineActive", activeTimelinePullRequest() !== null),
|
||||
);
|
||||
if (stopEditors) disposers.push(stopEditors);
|
||||
return () => disposers.reverse().forEach((d) => d());
|
||||
}
|
||||
Reference in new issue
Заблокувати користувача