mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-15 00:08:15 +00: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 <30753807+imacross@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { fragmentsOf, withFragments } from "./document";
|
||||
import { FRAGMENTS } from "./fragments";
|
||||
import * as Q from "./queries";
|
||||
import { GHGraphQLError, graphqlData, parseRest } from "./client";
|
||||
import { timelineItems } from "./requests";
|
||||
|
||||
const definitions = (text: string) => [...text.matchAll(/^\s*fragment\s+(\S+)\s+on\s/gm)].map((m) => m[1]!);
|
||||
const spreads = (text: string) => new Set([...text.matchAll(/^\s*\.\.\.([A-Za-z_]\w*)/gm)].map((m) => m[1]!));
|
||||
|
||||
describe("GraphQL documents", () => {
|
||||
it("ports every IntelliJ fragment and query", () => {
|
||||
expect(Object.keys(FRAGMENTS)).toHaveLength(39);
|
||||
expect(Object.keys(Q)).toHaveLength(40);
|
||||
for (const [name, text] of Object.entries(FRAGMENTS)) expect(definitions(text), name).toEqual([name]);
|
||||
});
|
||||
|
||||
it.each(Object.entries(Q))("sends %s with every fragment it needs, once", (_name, doc) => {
|
||||
const text = withFragments(doc);
|
||||
const defined = definitions(text);
|
||||
expect(new Set(defined).size).toBe(defined.length);
|
||||
for (const s of spreads(text)) expect(defined, s).toContain(s);
|
||||
// Only fragments that are spread somewhere are added.
|
||||
for (const d of fragmentsOf(doc)) expect(spreads(text).has(d), d).toBe(true);
|
||||
});
|
||||
|
||||
it("resolves fragments transitively, like GHGQLQueryLoader", () => {
|
||||
const names = fragmentsOf(Q.issueSearch);
|
||||
expect(names).toEqual(
|
||||
expect.arrayContaining([
|
||||
"pageInfo",
|
||||
"pullRequestInfoShort",
|
||||
"rates",
|
||||
"nodeInfo",
|
||||
"actor",
|
||||
"actorInfo",
|
||||
"userInfo",
|
||||
"user",
|
||||
"labelInfo",
|
||||
"pullRequestReviewer",
|
||||
"teamInfo",
|
||||
"pullRequestReview",
|
||||
"reactions",
|
||||
"reaction",
|
||||
"enterpriseUserAccountInfo",
|
||||
"organizationInfo",
|
||||
]),
|
||||
);
|
||||
expect(names).not.toContain("repository");
|
||||
// Fragments go first, the operation last.
|
||||
expect(withFragments(Q.issueSearch).trim()).toMatch(/^fragment[\s\S]*\nquery issueSearch[\s\S]*\}$/);
|
||||
});
|
||||
|
||||
it("does not add fragments a document defines itself", () => {
|
||||
const text = withFragments(Q.pullRequestTimeline);
|
||||
expect(definitions(text).filter((d) => d === "gitRefName")).toHaveLength(1);
|
||||
expect(fragmentsOf(Q.pullRequestTimeline)).not.toContain("gitRefName");
|
||||
expect(fragmentsOf(Q.pullRequestTimeline)).not.toContain("issueCommentInfo");
|
||||
});
|
||||
|
||||
it("treats inline fragments as selections, not spreads", () => {
|
||||
expect(fragmentsOf(Q.findPendingReview)).toEqual(expect.arrayContaining(["pullRequestPendingReview", "nodeInfo"]));
|
||||
expect(fragmentsOf(`query { node(id: "x") { ... on User { id } } }`)).toEqual([]);
|
||||
});
|
||||
|
||||
it("fails on an unknown fragment", () => {
|
||||
expect(() => withFragments(`query {\n viewer {\n ...nope\n }\n}`)).toThrow(/nope/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("client", () => {
|
||||
afterEach(() => vi.restoreAllMocks());
|
||||
|
||||
it("returns data and throws GraphQL errors", () => {
|
||||
expect(graphqlData<{ a: number }>('{"data":{"a":1}}')).toEqual({ a: 1 });
|
||||
expect(() => graphqlData('{"errors":[{"message":"bad"},{"message":"worse"}]}')).toThrow(GHGraphQLError);
|
||||
expect(() => graphqlData('{"errors":[{"message":"bad"}]}')).toThrow("bad");
|
||||
});
|
||||
|
||||
it("flattens slurped REST pages and maps empty bodies to null", () => {
|
||||
expect(parseRest("[[1,2],[3]]", true)).toEqual([1, 2, 3]);
|
||||
expect(parseRest('[{"a":1}]', false)).toEqual([{ a: 1 }]);
|
||||
expect(parseRest(" ", false)).toBeNull();
|
||||
});
|
||||
|
||||
it("sends documents with their fragments through gh", async () => {
|
||||
const ipc = await import("@/platform/ipc");
|
||||
const ghApi = vi
|
||||
.spyOn(ipc.commands, "ghApi")
|
||||
.mockResolvedValue({ status: "ok", data: '{"data":{"repository":null}}' } as never);
|
||||
const { graphql } = await import("./client");
|
||||
await graphql(Q.findRepository, { repoOwner: "o", repoName: "r", cursor: undefined });
|
||||
const req = ghApi.mock.calls[0]![0];
|
||||
expect(req).toMatchObject({ method: "POST", endpoint: "graphql", paginate: false });
|
||||
const body = JSON.parse(req.body!) as { query: string; variables: Record<string, unknown> };
|
||||
expect(body.variables).toEqual({ repoOwner: "o", repoName: "r" });
|
||||
expect(definitions(body.query)).toEqual(expect.arrayContaining(["repository", "repositoryOwnerName", "rates"]));
|
||||
});
|
||||
});
|
||||
|
||||
describe("timeline items", () => {
|
||||
it("drops IntelliJ's ignored types and marks unknown ones", () => {
|
||||
const items = timelineItems([
|
||||
{ __typename: "IssueComment" },
|
||||
{ __typename: "MentionedEvent" },
|
||||
{ __typename: "SubscribedEvent" },
|
||||
{ __typename: "AutoMergeEnabledEvent" },
|
||||
]);
|
||||
expect(items.map((i) => i.__typename)).toEqual(["IssueComment", "Unknown"]);
|
||||
expect(items[1]).toEqual({ __typename: "Unknown", type: "AutoMergeEnabledEvent" });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* GitHub API access through gh (`commands.ghApi`, which runs `gh api` in the project root
|
||||
* with gh's authentication), like IntelliJ's `GithubApiRequestExecutor`:
|
||||
*
|
||||
* const data = await graphql<{ repository: … }>(findRepository, { repoOwner, repoName });
|
||||
* const page = await rest<GHLabelDto[]>("GET", `${repoPath()}/labels?per_page=100`);
|
||||
*
|
||||
* Documents are sent with the fragments they spread (`withFragments`). Requests address
|
||||
* the connected repository explicitly (`repoPath()`, `repoVariables()`), not gh's
|
||||
* `{owner}/{repo}` placeholders, since a project can have several GitHub remotes.
|
||||
*/
|
||||
import { call, commands } from "@/platform/ipc";
|
||||
import { withFragments } from "./document";
|
||||
import type { GHRepositoryCoordinates } from "./types";
|
||||
|
||||
export type GHApiMethod = "GET" | "POST" | "PATCH" | "PUT" | "DELETE";
|
||||
|
||||
/** A GraphQL response with `errors` (IntelliJ's `GithubConfusingException`). */
|
||||
export class GHGraphQLError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
readonly errors: ReadonlyArray<{ message: string; type?: string; path?: unknown[] }>,
|
||||
) {
|
||||
super(message);
|
||||
this.name = "GHGraphQLError";
|
||||
}
|
||||
}
|
||||
|
||||
export const errorMessage = (e: unknown) => (e instanceof Error ? e.message : String(e));
|
||||
|
||||
/** `data` of a GraphQL response body; throws on `errors` (gh usually fails the call first). */
|
||||
export function graphqlData<T>(text: string): T {
|
||||
const json = JSON.parse(text) as { data?: T; errors?: Array<{ message: string; type?: string; path?: unknown[] }> };
|
||||
if (json.errors?.length) throw new GHGraphQLError(json.errors.map((e) => e.message).join("\n"), json.errors);
|
||||
if (json.data === undefined) throw new GHGraphQLError("GitHub returned no data", []);
|
||||
return json.data;
|
||||
}
|
||||
|
||||
/** Runs a document from `queries.ts` with `variables` (undefined values are dropped). */
|
||||
export async function graphql<T>(doc: string, variables: Record<string, unknown> = {}): Promise<T> {
|
||||
const body = JSON.stringify({ query: withFragments(doc), variables: definedOnly(variables) });
|
||||
const text = await call(commands.ghApi({ method: "POST", endpoint: "graphql", body, paginate: false }));
|
||||
return graphqlData<T>(text);
|
||||
}
|
||||
|
||||
function definedOnly(v: Record<string, unknown>): Record<string, unknown> {
|
||||
return Object.fromEntries(Object.entries(v).filter(([, x]) => x !== undefined));
|
||||
}
|
||||
|
||||
/**
|
||||
* A REST call (`endpoint` relative to the API root, e.g. `repos/o/r/pulls/1/files`).
|
||||
* `paginate` follows every `Link` page and concatenates array pages. Empty bodies
|
||||
* (`204 No Content`) come back as `null`.
|
||||
*/
|
||||
export async function rest<T>(
|
||||
method: GHApiMethod,
|
||||
endpoint: string,
|
||||
body?: unknown,
|
||||
opts: { paginate?: boolean } = {},
|
||||
): Promise<T> {
|
||||
const text = await call(
|
||||
commands.ghApi({
|
||||
method,
|
||||
endpoint,
|
||||
body: body === undefined ? null : JSON.stringify(body),
|
||||
paginate: opts.paginate ?? false,
|
||||
}),
|
||||
);
|
||||
return parseRest<T>(text, opts.paginate ?? false);
|
||||
}
|
||||
|
||||
/** Parses a REST body; `--slurp` pages (an array of pages) are flattened when they are arrays. */
|
||||
export function parseRest<T>(text: string, paginated: boolean): T {
|
||||
if (!text.trim()) return null as T;
|
||||
const json = JSON.parse(text) as unknown;
|
||||
if (paginated && Array.isArray(json) && json.every(Array.isArray)) return (json as unknown[][]).flat() as T;
|
||||
return json as T;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// The connected repository
|
||||
|
||||
let current: (GHRepositoryCoordinates & { id: string | null }) | null = null;
|
||||
|
||||
/** Set by `data/repository.ts` when the tool window connects (null: disconnected). */
|
||||
export function setCurrentRepository(repo: (GHRepositoryCoordinates & { id: string | null }) | null) {
|
||||
current = repo;
|
||||
}
|
||||
|
||||
/** The connected repository: `owner`, `name` and its GraphQL node `id` (once loaded). */
|
||||
export function currentRepository(): GHRepositoryCoordinates & { id: string | null } {
|
||||
if (!current) throw new Error("Not connected to a GitHub repository");
|
||||
return current;
|
||||
}
|
||||
|
||||
/** `repos/owner/name`, the prefix of the connected repository's REST endpoints. */
|
||||
export function repoPath(repo: GHRepositoryCoordinates = currentRepository()): string {
|
||||
return `repos/${repo.owner}/${repo.name}`;
|
||||
}
|
||||
|
||||
/** The `$repoOwner` / `$repoName` variables of the repository documents. */
|
||||
export function repoVariables(repo: GHRepositoryCoordinates = currentRepository()) {
|
||||
return { repoOwner: repo.owner, repoName: repo.name };
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* GraphQL document assembly, like IntelliJ's `CachingGraphQLQueryLoader`: a document is
|
||||
* sent with every fragment it spreads (`...name`, where the fragment is named after its
|
||||
* file), transitively. Fragments the document defines itself are not added again.
|
||||
*/
|
||||
import { FRAGMENTS } from "./fragments";
|
||||
|
||||
interface Block {
|
||||
body: string;
|
||||
/** Fragments spread in the block and not defined in it. */
|
||||
dependencies: Set<string>;
|
||||
/** Fragments the block defines itself. */
|
||||
defines: Set<string>;
|
||||
}
|
||||
|
||||
const FRAGMENT_DEFINITION = /^fragment\s+(\S+)\s+on\s+.*\{$/;
|
||||
const SPREAD = /^\.\.\.([A-Za-z_]\w*)/;
|
||||
|
||||
function readBlock(text: string): Block {
|
||||
const dependencies = new Set<string>();
|
||||
const defines = new Set<string>();
|
||||
const lines: string[] = [];
|
||||
for (const raw of text.split("\n")) {
|
||||
const line = raw.trim();
|
||||
lines.push(line);
|
||||
const def = FRAGMENT_DEFINITION.exec(line);
|
||||
if (def) defines.add(def[1]!);
|
||||
// `... on User {` is an inline fragment, not a spread.
|
||||
const spread = SPREAD.exec(line);
|
||||
if (spread) dependencies.add(spread[1]!);
|
||||
}
|
||||
for (const d of defines) dependencies.delete(d);
|
||||
return { body: lines.join("\n").trim(), dependencies, defines };
|
||||
}
|
||||
|
||||
const fragmentBlocks = new Map<string, Block>();
|
||||
const documents = new Map<string, { text: string; fragments: string[] }>();
|
||||
|
||||
function fragmentBlock(name: string): Block {
|
||||
let block = fragmentBlocks.get(name);
|
||||
if (!block) {
|
||||
const text = FRAGMENTS[name];
|
||||
if (text === undefined) throw new Error(`Couldn't find file for fragment ${name}`);
|
||||
block = readBlock(text);
|
||||
fragmentBlocks.set(name, block);
|
||||
}
|
||||
return block;
|
||||
}
|
||||
|
||||
function resolve(doc: string): { text: string; fragments: string[] } {
|
||||
const cached = documents.get(doc);
|
||||
if (cached) return cached;
|
||||
const { body, dependencies, defines } = readBlock(doc);
|
||||
const found = new Map<string, Block>();
|
||||
const visit = (names: Iterable<string>) => {
|
||||
for (const name of names) {
|
||||
if (found.has(name) || defines.has(name)) continue;
|
||||
const block = fragmentBlock(name);
|
||||
found.set(name, block);
|
||||
visit(block.dependencies);
|
||||
}
|
||||
};
|
||||
visit(dependencies);
|
||||
const blocks = [...found.values()].reverse();
|
||||
const resolved = { text: [...blocks.map((f) => f.body), body].join("\n"), fragments: [...found.keys()] };
|
||||
documents.set(doc, resolved);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
/** `doc` preceded by the fragments it needs, ready to send (cached per document). */
|
||||
export function withFragments(doc: string): string {
|
||||
return resolve(doc).text;
|
||||
}
|
||||
|
||||
/** Names of the fragments `withFragments(doc)` adds, in discovery order. */
|
||||
export function fragmentsOf(doc: string): string[] {
|
||||
return resolve(doc).fragments;
|
||||
}
|
||||
@@ -0,0 +1,505 @@
|
||||
/**
|
||||
* GraphQL fragments of IntelliJ's GitHub plugin (`resources/graphql/fragment`, plus
|
||||
* collaboration-tools' `pageInfo`), verbatim. Each one is named after its file, the
|
||||
* name it is spread with (`...name`); `withFragments` adds the ones a document needs.
|
||||
*/
|
||||
export const FRAGMENTS: Readonly<Record<string, string>> = {
|
||||
actor: `
|
||||
fragment actor on Actor {
|
||||
...actorInfo
|
||||
|
||||
... on User {
|
||||
...userInfo
|
||||
}
|
||||
... on EnterpriseUserAccount {
|
||||
...enterpriseUserAccountInfo
|
||||
}
|
||||
... on Organization {
|
||||
...organizationInfo
|
||||
}
|
||||
}
|
||||
`,
|
||||
actorInfo: `
|
||||
# Just the data that should be present for any type of Actor.
|
||||
fragment actorInfo on Actor {
|
||||
__typename
|
||||
...nodeInfo
|
||||
login
|
||||
url
|
||||
avatarUrl
|
||||
}
|
||||
`,
|
||||
checkRun: `
|
||||
fragment checkRun on CheckRun {
|
||||
name
|
||||
conclusion
|
||||
isRequired(pullRequestNumber: $number) # See: findPullRequestMergeability
|
||||
detailsUrl
|
||||
url
|
||||
}
|
||||
`,
|
||||
checkSuite: `
|
||||
fragment checkSuite on CheckSuite {
|
||||
checkRuns(first: 100) {
|
||||
pageInfo {
|
||||
...pageInfo
|
||||
}
|
||||
nodes {
|
||||
...checkRun
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
comment: `
|
||||
fragment comment on Comment {
|
||||
...nodeInfo
|
||||
author {
|
||||
...actor
|
||||
}
|
||||
body
|
||||
createdAt
|
||||
...reactions
|
||||
}
|
||||
`,
|
||||
commit: `
|
||||
fragment commit on Commit {
|
||||
...commitShort
|
||||
committer {
|
||||
...gitActorInfo
|
||||
}
|
||||
messageBody
|
||||
committedDate
|
||||
parents(first: 100) {
|
||||
nodes {
|
||||
...commitHash
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
commitHash: `
|
||||
fragment commitHash on Commit {
|
||||
...nodeInfo
|
||||
oid
|
||||
abbreviatedOid
|
||||
}
|
||||
`,
|
||||
commitShort: `
|
||||
fragment commitShort on Commit {
|
||||
...commitHash
|
||||
url
|
||||
messageHeadline
|
||||
author {
|
||||
...gitActorInfo
|
||||
}
|
||||
}
|
||||
`,
|
||||
commitStatusContext: `
|
||||
fragment commitStatusContext on StatusContext {
|
||||
context
|
||||
state
|
||||
isRequired(pullRequestNumber: $number) # See: findPullRequestMergeability
|
||||
targetUrl
|
||||
}
|
||||
`,
|
||||
commitStatusRollup: `
|
||||
fragment commitStatusRollup on StatusCheckRollup {
|
||||
contexts(first: $pageSize, after: $cursor) {
|
||||
pageInfo {
|
||||
...pageInfo
|
||||
}
|
||||
nodes {
|
||||
__typename
|
||||
... on CheckRun {
|
||||
checkSuite {
|
||||
workflowRun {
|
||||
workflow {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
name
|
||||
title
|
||||
status
|
||||
conclusion
|
||||
url
|
||||
}
|
||||
... on StatusContext {
|
||||
context
|
||||
description
|
||||
state
|
||||
targetUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
commitStatusRollupShort: `
|
||||
fragment commitStatusRollupShort on StatusCheckRollup {
|
||||
state
|
||||
}
|
||||
`,
|
||||
commitWithCheckStatuses: `
|
||||
fragment commitWithCheckStatuses on Commit {
|
||||
...commitHash
|
||||
status {
|
||||
contexts {
|
||||
...commitStatusContext
|
||||
}
|
||||
}
|
||||
|
||||
checkSuites(first: 100) {
|
||||
pageInfo {
|
||||
...pageInfo
|
||||
}
|
||||
nodes {
|
||||
...checkSuite
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
enterpriseUserAccountInfo: `
|
||||
fragment enterpriseUserAccountInfo on EnterpriseUserAccount {
|
||||
name
|
||||
}
|
||||
`,
|
||||
gitActorInfo: `
|
||||
fragment gitActorInfo on GitActor {
|
||||
name
|
||||
email
|
||||
avatarUrl
|
||||
date
|
||||
user {
|
||||
...user
|
||||
}
|
||||
}
|
||||
`,
|
||||
gitRefName: `
|
||||
fragment gitRefName on Ref {
|
||||
name
|
||||
}
|
||||
`,
|
||||
labelInfo: `
|
||||
fragment labelInfo on Label {
|
||||
...nodeInfo
|
||||
url
|
||||
name
|
||||
color
|
||||
}
|
||||
`,
|
||||
nodeInfo: `
|
||||
fragment nodeInfo on Node {
|
||||
id
|
||||
}
|
||||
`,
|
||||
organizationInfo: `
|
||||
fragment organizationInfo on Organization {
|
||||
name
|
||||
}
|
||||
`,
|
||||
pageInfo: `
|
||||
fragment pageInfo on PageInfo {
|
||||
startCursor
|
||||
hasPreviousPage
|
||||
endCursor
|
||||
hasNextPage
|
||||
}
|
||||
`,
|
||||
pullRequestBranchMatch: `
|
||||
fragment pullRequestBranchMatch on PullRequest {
|
||||
...nodeInfo
|
||||
number
|
||||
title
|
||||
headRefName
|
||||
}
|
||||
`,
|
||||
pullRequestCommitWithCheckStatuses: `
|
||||
fragment pullRequestCommitWithCheckStatuses on PullRequestCommit {
|
||||
...nodeInfo
|
||||
commit {
|
||||
...commitWithCheckStatuses
|
||||
}
|
||||
}
|
||||
`,
|
||||
pullRequestInfo: `
|
||||
fragment pullRequestInfo on PullRequest {
|
||||
...pullRequestInfoShort
|
||||
|
||||
body
|
||||
|
||||
author {
|
||||
...actor
|
||||
}
|
||||
createdAt
|
||||
|
||||
reviewDecision
|
||||
|
||||
baseRefName
|
||||
baseRefOid
|
||||
baseRepository {
|
||||
...repository
|
||||
}
|
||||
|
||||
headRef {
|
||||
id
|
||||
}
|
||||
headRefName
|
||||
headRefOid
|
||||
headRepository {
|
||||
...repository
|
||||
}
|
||||
|
||||
viewerCanDeleteHeadRef
|
||||
}
|
||||
`,
|
||||
pullRequestInfoShort: `
|
||||
fragment pullRequestInfoShort on PullRequest {
|
||||
...nodeInfo
|
||||
url
|
||||
number
|
||||
|
||||
title
|
||||
state
|
||||
isDraft
|
||||
|
||||
author {
|
||||
...actor
|
||||
}
|
||||
createdAt
|
||||
updatedAt
|
||||
|
||||
isReadByViewer
|
||||
|
||||
assignees(first: 100) {
|
||||
nodes {
|
||||
...user
|
||||
}
|
||||
}
|
||||
labels(first: 100) {
|
||||
nodes {
|
||||
...labelInfo
|
||||
}
|
||||
}
|
||||
reviewRequests(first: 100) {
|
||||
nodes {
|
||||
... on ReviewRequest {
|
||||
requestedReviewer {
|
||||
...pullRequestReviewer
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
reviewThreads(last: 100) {
|
||||
nodes {
|
||||
isResolved
|
||||
isOutdated
|
||||
}
|
||||
}
|
||||
reviews(last: 100) {
|
||||
nodes {
|
||||
...pullRequestReview
|
||||
}
|
||||
}
|
||||
...reactions
|
||||
|
||||
mergeable
|
||||
|
||||
viewerCanUpdate
|
||||
viewerCanReact
|
||||
viewerDidAuthor
|
||||
}
|
||||
`,
|
||||
pullRequestMergeability: `
|
||||
fragment pullRequestMergeability on PullRequest {
|
||||
mergeable
|
||||
canBeRebased
|
||||
mergeStateStatus
|
||||
baseRef {
|
||||
refUpdateRule {
|
||||
...refUpdateRule
|
||||
}
|
||||
}
|
||||
|
||||
commits(last: 1) {
|
||||
pageInfo {
|
||||
...pageInfo
|
||||
}
|
||||
nodes {
|
||||
...pullRequestCommitWithCheckStatuses
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
pullRequestPendingReview: `
|
||||
fragment pullRequestPendingReview on PullRequestReview {
|
||||
...nodeInfo
|
||||
state
|
||||
comments(first: 0) {
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
`,
|
||||
pullRequestReview: `
|
||||
fragment pullRequestReview on PullRequestReview {
|
||||
__typename
|
||||
...nodeInfo
|
||||
url
|
||||
author {
|
||||
...actor
|
||||
}
|
||||
body
|
||||
state
|
||||
createdAt
|
||||
viewerCanUpdate
|
||||
}
|
||||
`,
|
||||
pullRequestReviewComment: `
|
||||
fragment pullRequestReviewComment on PullRequestReviewComment {
|
||||
...comment
|
||||
url
|
||||
state
|
||||
commit {
|
||||
...commitHash
|
||||
}
|
||||
originalCommit {
|
||||
...commitHash
|
||||
}
|
||||
diffHunk
|
||||
pullRequestReview {
|
||||
...nodeInfo
|
||||
}
|
||||
viewerCanDelete
|
||||
viewerCanUpdate
|
||||
viewerCanReact
|
||||
}
|
||||
`,
|
||||
pullRequestReviewer: `
|
||||
fragment pullRequestReviewer on RequestedReviewer {
|
||||
... on Actor {
|
||||
...actorInfo
|
||||
}
|
||||
... on User {
|
||||
...userInfo
|
||||
}
|
||||
... on Team {
|
||||
...teamInfo
|
||||
}
|
||||
|
||||
# For future enthusiasts: be careful with adding ... on Bot, as it's been added only in GHE 3.11
|
||||
}
|
||||
`,
|
||||
pullRequestReviewThread: `
|
||||
fragment pullRequestReviewThread on PullRequestReviewThread {
|
||||
...nodeInfo
|
||||
isResolved
|
||||
isOutdated
|
||||
path
|
||||
subjectType
|
||||
diffSide
|
||||
line
|
||||
originalLine
|
||||
startDiffSide
|
||||
startLine
|
||||
originalStartLine
|
||||
comments(first: 100) {
|
||||
nodes {
|
||||
...pullRequestReviewComment
|
||||
}
|
||||
}
|
||||
viewerCanReply
|
||||
viewerCanResolve
|
||||
viewerCanUnresolve
|
||||
}
|
||||
`,
|
||||
pullRequestTemplate: `
|
||||
fragment pullRequestTemplate on PullRequestTemplate {
|
||||
body
|
||||
}
|
||||
`,
|
||||
rates: `
|
||||
fragment rates on RateLimit {
|
||||
cost
|
||||
}
|
||||
`,
|
||||
reaction: `
|
||||
fragment reaction on Reaction {
|
||||
content
|
||||
user {
|
||||
...user
|
||||
}
|
||||
}
|
||||
`,
|
||||
reactions: `
|
||||
fragment reactions on Reactable {
|
||||
reactions(first: 25) {
|
||||
pageInfo {
|
||||
...pageInfo
|
||||
}
|
||||
nodes {
|
||||
...reaction
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
refUpdateRule: `
|
||||
fragment refUpdateRule on RefUpdateRule {
|
||||
allowsDeletions
|
||||
allowsForcePushes
|
||||
pattern
|
||||
requiredApprovingReviewCount
|
||||
requiredStatusCheckContexts
|
||||
requiresLinearHistory
|
||||
requiresSignatures
|
||||
viewerCanPush
|
||||
}
|
||||
`,
|
||||
repository: `
|
||||
fragment repository on Repository {
|
||||
...nodeInfo
|
||||
owner {
|
||||
...repositoryOwnerName
|
||||
}
|
||||
nameWithOwner
|
||||
viewerPermission
|
||||
|
||||
mergeCommitAllowed
|
||||
squashMergeAllowed
|
||||
rebaseMergeAllowed
|
||||
|
||||
defaultBranchRef {
|
||||
...gitRefName
|
||||
}
|
||||
isFork
|
||||
url
|
||||
sshUrl
|
||||
}
|
||||
`,
|
||||
repositoryOwnerName: `
|
||||
fragment repositoryOwnerName on RepositoryOwner {
|
||||
__typename
|
||||
...nodeInfo
|
||||
login
|
||||
}
|
||||
`,
|
||||
teamInfo: `
|
||||
fragment teamInfo on Team {
|
||||
__typename
|
||||
...nodeInfo
|
||||
slug
|
||||
url
|
||||
teamAvatarUrl: avatarUrl
|
||||
teamName: name
|
||||
combinedSlug
|
||||
}
|
||||
`,
|
||||
user: `
|
||||
fragment user on User {
|
||||
...actorInfo
|
||||
...userInfo
|
||||
}
|
||||
`,
|
||||
userInfo: `
|
||||
fragment userInfo on User {
|
||||
name
|
||||
}
|
||||
`,
|
||||
};
|
||||
@@ -0,0 +1,852 @@
|
||||
/**
|
||||
* GraphQL documents of IntelliJ's GitHub plugin (`resources/graphql/query`), verbatim and
|
||||
* named after their files. They spread fragments from `fragments.ts`, which
|
||||
* `withFragments` (in `document.ts`) appends before a document is sent.
|
||||
*/
|
||||
|
||||
export const addPullRequestReviewThread = `
|
||||
mutation ($body: String!, $line: Int!, $path: String!, $pullRequestReviewId: ID, $side: DiffSide, $startSide: DiffSide, $startLine: Int) {
|
||||
addPullRequestReviewThread(input: {body: $body, line: $line, path: $path, pullRequestReviewId:$pullRequestReviewId, side: $side, startSide: $startSide, startLine: $startLine}) {
|
||||
thread {
|
||||
...pullRequestReviewThread
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const addReaction = `
|
||||
mutation ($id: ID!, $reaction: ReactionContent!) {
|
||||
addReaction(input: {
|
||||
subjectId: $id,
|
||||
content: $reaction
|
||||
}) {
|
||||
reaction {
|
||||
...reaction
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const addReviewComment = `
|
||||
mutation ($reviewId: ID!, $body: String!, $commit: GitObjectID, $file: String, $position: Int, $inReplyTo: ID) {
|
||||
addPullRequestReviewComment(input: {pullRequestReviewId: $reviewId, body: $body, commitOID: $commit, path: $file, position: $position, inReplyTo: $inReplyTo}) {
|
||||
comment {
|
||||
...pullRequestReviewComment
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const createPullRequest = `
|
||||
mutation ($repositoryId: ID!, $baseRefName: String!, $headRefName: String!, $title: String!, $body: String, $draft: Boolean = false) {
|
||||
createPullRequest(input: {
|
||||
repositoryId: $repositoryId,
|
||||
baseRefName: $baseRefName,
|
||||
headRefName: $headRefName,
|
||||
title: $title,
|
||||
body: $body,
|
||||
draft: $draft
|
||||
}) {
|
||||
pullRequest {
|
||||
...pullRequestInfoShort
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const createReview = `
|
||||
mutation ($pullRequestId: ID!, $event: PullRequestReviewEvent, $body: String, $commitOid: GitObjectID, $threads: [DraftPullRequestReviewThread]) {
|
||||
addPullRequestReview(input: {pullRequestId: $pullRequestId, event: $event, body: $body, commitOID: $commitOid, threads: $threads}) {
|
||||
pullRequestReview {
|
||||
...pullRequestPendingReview
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const deleteIssueComment = `
|
||||
mutation ($id: ID!) {
|
||||
deleteIssueComment(input: {id: $id}) {
|
||||
__typename
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const deleteRef = `
|
||||
mutation ($refId: ID!) {
|
||||
deleteRef(input: {refId: $refId}) {
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const deleteReview = `
|
||||
mutation ($reviewId: ID!){
|
||||
deletePullRequestReview(input: {pullRequestReviewId: $reviewId}) {
|
||||
__typename
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const deleteReviewComment = `
|
||||
mutation ($id: ID!) {
|
||||
deletePullRequestReviewComment(input: {id: $id}) {
|
||||
pullRequestReview {
|
||||
...pullRequestPendingReview
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const findMentionableUsers = `
|
||||
query($repoOwner: String!, $repoName: String!, $pageSize: Int = 100, $cursor: String) {
|
||||
repository(owner: $repoOwner, name: $repoName) {
|
||||
mentionableUsers(first: $pageSize, after: $cursor) {
|
||||
pageInfo {
|
||||
...pageInfo
|
||||
}
|
||||
nodes {
|
||||
...user
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rateLimit {
|
||||
...rates
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const findOrganizationTeams = `
|
||||
query($organization: String!, $pageSize: Int = 100, $cursor: String) {
|
||||
organization(login: $organization) {
|
||||
teams(first: $pageSize, after: $cursor) {
|
||||
pageInfo {
|
||||
...pageInfo
|
||||
}
|
||||
nodes {
|
||||
...teamInfo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rateLimit {
|
||||
...rates
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const findPendingReview = `
|
||||
query($pullRequestId: ID!) {
|
||||
node(id: $pullRequestId) {
|
||||
... on PullRequest {
|
||||
reviews(states: [PENDING], first: 1) {
|
||||
nodes {
|
||||
...pullRequestPendingReview
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rateLimit {
|
||||
...rates
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const findPullRequest = `
|
||||
query($repoOwner: String!, $repoName: String!, $number: Int!) {
|
||||
repository(owner: $repoOwner, name: $repoName) {
|
||||
pullRequest(number: $number) {
|
||||
...pullRequestInfo
|
||||
}
|
||||
}
|
||||
|
||||
rateLimit {
|
||||
...rates
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const findPullRequestId = `
|
||||
query($repoOwner: String!, $repoName: String!, $number: Int!) {
|
||||
repository(owner: $repoOwner, name: $repoName) {
|
||||
pullRequest(number: $number) {
|
||||
...nodeInfo
|
||||
number
|
||||
}
|
||||
}
|
||||
|
||||
rateLimit {
|
||||
...rates
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const findPullRequestMergeability = `
|
||||
query($repoOwner: String!, $repoName: String!, $number: Int!) {
|
||||
repository(owner: $repoOwner, name: $repoName) {
|
||||
pullRequest(number: $number) {
|
||||
...pullRequestMergeability
|
||||
}
|
||||
}
|
||||
|
||||
rateLimit {
|
||||
...rates
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const findPullRequestsByHeadBranch = `
|
||||
query findPullRequestsByHeadBranch($query: String!, $pageSize: Int = 100, $cursor: String) {
|
||||
search(first: $pageSize, after: $cursor, type: ISSUE_ADVANCED, query: $query) {
|
||||
pageInfo {
|
||||
...pageInfo
|
||||
}
|
||||
nodes {
|
||||
...pullRequestBranchMatch
|
||||
}
|
||||
}
|
||||
|
||||
rateLimit {
|
||||
...rates
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const findRepository = `
|
||||
query($repoOwner: String!, $repoName: String!) {
|
||||
repository(owner: $repoOwner, name: $repoName) {
|
||||
...repository
|
||||
}
|
||||
|
||||
rateLimit {
|
||||
...rates
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const findUser = `
|
||||
query($login: String!) {
|
||||
user(login: $login) {
|
||||
__typename
|
||||
...user
|
||||
}
|
||||
|
||||
rateLimit {
|
||||
...rates
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const getProtectionRules = `
|
||||
query($repoOwner: String!, $repoName: String!, $pageSize: Int = 100, $cursor: String) {
|
||||
repository(owner: $repoOwner, name: $repoName) {
|
||||
branchProtectionRules(first: $pageSize, after: $cursor) {
|
||||
pageInfo {
|
||||
...pageInfo
|
||||
}
|
||||
nodes {
|
||||
...nodeInfo
|
||||
pattern
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rateLimit {
|
||||
...rates
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const getPullRequestTemplates = `
|
||||
query($repoOwner: String!, $repoName: String!) {
|
||||
repository(owner: $repoOwner, name: $repoName) {
|
||||
pullRequestTemplates {
|
||||
...pullRequestTemplate
|
||||
}
|
||||
}
|
||||
|
||||
rateLimit {
|
||||
...rates
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const getRepositoryCommitStatusContexts = `
|
||||
query($repoOwner: String!, $repoName: String!, $oid: GitObjectID!, $pageSize: Int, $cursor: String) {
|
||||
repository(owner: $repoOwner, name: $repoName) {
|
||||
object(oid: $oid) {
|
||||
... on Commit {
|
||||
statusCheckRollup {
|
||||
...commitStatusRollup
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rateLimit {
|
||||
...rates
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const getRepositoryCommitStatusShort = `
|
||||
query($repoOwner: String!, $repoName: String!, $oid: GitObjectID!) {
|
||||
repository(owner: $repoOwner, name: $repoName) {
|
||||
object(oid: $oid) {
|
||||
... on Commit {
|
||||
statusCheckRollup {
|
||||
...commitStatusRollupShort
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rateLimit {
|
||||
...rates
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// `ISSUE_ADVANCED` rather than IntelliJ's `ISSUE`: only the advanced search understands
|
||||
// `OR`, `AND` and parentheses in the typed query.
|
||||
export const issueSearch = `
|
||||
query issueSearch($query: String!, $pageSize: Int = 100, $cursor: String) {
|
||||
search(first: $pageSize, after: $cursor, type: ISSUE_ADVANCED, query: $query) {
|
||||
pageInfo {
|
||||
...pageInfo
|
||||
}
|
||||
nodes {
|
||||
...pullRequestInfoShort
|
||||
}
|
||||
}
|
||||
|
||||
rateLimit {
|
||||
...rates
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const markFileAsViewed = `
|
||||
mutation ($pullRequestId: ID!, $path: String!) {
|
||||
markFileAsViewed(input: { pullRequestId: $pullRequestId, path: $path }) {
|
||||
__typename
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const markPullRequestReadyForReview = `
|
||||
mutation ($pullRequestId: ID!) {
|
||||
markPullRequestReadyForReview(input: {pullRequestId: $pullRequestId}) {
|
||||
__typename
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const metrics = `
|
||||
query metrics(
|
||||
$allPRCountQuery: String!,
|
||||
$openPRCountQuery: String!,
|
||||
$openAuthoredPRCountQuery: String!,
|
||||
$openAssigneePRCountQuery: String!,
|
||||
$openReviewedPRCountQuery: String!,
|
||||
$openReviewAssignedPRCountQuery: String!,
|
||||
) {
|
||||
allPRCount: search(type: ISSUE, query: $allPRCountQuery) {
|
||||
issueCount
|
||||
}
|
||||
|
||||
openPRCount: search(type: ISSUE, query: $openPRCountQuery) {
|
||||
issueCount
|
||||
}
|
||||
|
||||
openAuthoredPRCount: search(type: ISSUE, query: $openAuthoredPRCountQuery) {
|
||||
issueCount
|
||||
}
|
||||
|
||||
openAssigneePRCount: search(type: ISSUE, query: $openAssigneePRCountQuery) {
|
||||
issueCount
|
||||
}
|
||||
|
||||
openReviewedPRCount: search(type: ISSUE, query: $openReviewedPRCountQuery) {
|
||||
issueCount
|
||||
}
|
||||
|
||||
openReviewAssignedPRCount: search(type: ISSUE, query: $openReviewAssignedPRCountQuery) {
|
||||
issueCount
|
||||
}
|
||||
|
||||
rateLimit {
|
||||
...rates
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const pullRequestCommits = `
|
||||
query($repoOwner: String!, $repoName: String!, $number: Int!, $pageSize: Int = 100, $cursor: String) {
|
||||
repository(owner: $repoOwner, name: $repoName) {
|
||||
pullRequest(number: $number) {
|
||||
commits(first: $pageSize, after: $cursor) {
|
||||
pageInfo {
|
||||
...pageInfo
|
||||
}
|
||||
nodes {
|
||||
...nodeInfo
|
||||
commit {
|
||||
...commit
|
||||
}
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rateLimit {
|
||||
...rates
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const pullRequestFiles = `
|
||||
query($repoOwner: String!, $repoName: String!, $number: Int!, $pageSize: Int = 100, $cursor: String) {
|
||||
repository(owner: $repoOwner, name: $repoName) {
|
||||
pullRequest(number: $number) {
|
||||
files(first: $pageSize, after: $cursor) {
|
||||
pageInfo {
|
||||
...pageInfo
|
||||
}
|
||||
nodes {
|
||||
path
|
||||
viewerViewedState
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rateLimit {
|
||||
...rates
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const pullRequestParticipants = `
|
||||
query($repoOwner: String!, $repoName: String!, $number: Int!, $pageSize: Int = 100, $cursor: String) {
|
||||
repository(owner: $repoOwner, name: $repoName) {
|
||||
pullRequest(number: $number) {
|
||||
participants(first: $pageSize, after: $cursor) {
|
||||
pageInfo {
|
||||
...pageInfo
|
||||
}
|
||||
nodes {
|
||||
...user
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rateLimit {
|
||||
...rates
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const pullRequestReviewThreads = `
|
||||
query($repoOwner: String!, $repoName: String!, $number: Int!, $pageSize: Int = 100, $cursor: String) {
|
||||
repository(owner: $repoOwner, name: $repoName) {
|
||||
pullRequest(number: $number) {
|
||||
reviewThreads(first: $pageSize, after: $cursor) {
|
||||
pageInfo {
|
||||
...pageInfo
|
||||
}
|
||||
nodes {
|
||||
...pullRequestReviewThread
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rateLimit {
|
||||
...rates
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const pullRequestTimeline = `
|
||||
fragment issueCommentInfo on IssueComment {
|
||||
...comment
|
||||
viewerCanUpdate
|
||||
viewerCanDelete
|
||||
viewerCanReact
|
||||
}
|
||||
|
||||
fragment prCommitInfo on PullRequestCommit {
|
||||
...nodeInfo
|
||||
commit {
|
||||
...commitShort
|
||||
}
|
||||
url
|
||||
}
|
||||
|
||||
fragment reviewDismissedEvent on ReviewDismissedEvent {
|
||||
actor {
|
||||
...actor
|
||||
}
|
||||
createdAt
|
||||
dismissalMessageHTML
|
||||
review {
|
||||
author {
|
||||
...actor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fragment readyForReviewEvent on ReadyForReviewEvent {
|
||||
actor {
|
||||
...actor
|
||||
}
|
||||
createdAt
|
||||
}
|
||||
|
||||
fragment convertToDraftEvent on ConvertToDraftEvent {
|
||||
actor {
|
||||
...actor
|
||||
}
|
||||
createdAt
|
||||
}
|
||||
|
||||
fragment renamedEvent on RenamedTitleEvent {
|
||||
actor {
|
||||
...actor
|
||||
}
|
||||
previousTitle
|
||||
currentTitle
|
||||
createdAt
|
||||
}
|
||||
|
||||
fragment closedEvent on ClosedEvent {
|
||||
actor {
|
||||
...actor
|
||||
}
|
||||
createdAt
|
||||
}
|
||||
|
||||
fragment reopenedEvent on ReopenedEvent {
|
||||
actor {
|
||||
...actor
|
||||
}
|
||||
createdAt
|
||||
}
|
||||
|
||||
fragment mergedEvent on MergedEvent {
|
||||
actor {
|
||||
...actor
|
||||
}
|
||||
createdAt
|
||||
mergedEventCommit: commit {
|
||||
...commitShort
|
||||
}
|
||||
mergeRefName
|
||||
}
|
||||
|
||||
fragment assignedEvent on AssignedEvent {
|
||||
actor {
|
||||
...actor
|
||||
}
|
||||
createdAt
|
||||
user {
|
||||
...user
|
||||
}
|
||||
}
|
||||
|
||||
fragment unassignedEvent on UnassignedEvent {
|
||||
actor {
|
||||
...actor
|
||||
}
|
||||
createdAt
|
||||
user {
|
||||
...user
|
||||
}
|
||||
}
|
||||
|
||||
fragment labeledEvent on LabeledEvent {
|
||||
actor {
|
||||
...actor
|
||||
}
|
||||
createdAt
|
||||
label {
|
||||
...labelInfo
|
||||
}
|
||||
}
|
||||
|
||||
fragment unlabeledEvent on UnlabeledEvent {
|
||||
actor {
|
||||
...actor
|
||||
}
|
||||
createdAt
|
||||
label {
|
||||
...labelInfo
|
||||
}
|
||||
}
|
||||
|
||||
fragment reviewRequestedEvent on ReviewRequestedEvent {
|
||||
actor {
|
||||
...actor
|
||||
}
|
||||
createdAt
|
||||
requestedReviewer {
|
||||
...pullRequestReviewer
|
||||
}
|
||||
}
|
||||
|
||||
fragment reviewRequestRemovedEvent on ReviewRequestRemovedEvent {
|
||||
actor {
|
||||
...actor
|
||||
}
|
||||
createdAt
|
||||
requestedReviewer {
|
||||
...pullRequestReviewer
|
||||
}
|
||||
}
|
||||
|
||||
fragment gitRefName on Ref {
|
||||
name
|
||||
}
|
||||
|
||||
fragment baseRefForcePushedEvent on BaseRefForcePushedEvent {
|
||||
actor {
|
||||
...actor
|
||||
}
|
||||
createdAt
|
||||
ref {
|
||||
...gitRefName
|
||||
}
|
||||
}
|
||||
|
||||
fragment headRefForcePushedEvent on HeadRefForcePushedEvent {
|
||||
actor {
|
||||
...actor
|
||||
}
|
||||
createdAt
|
||||
ref {
|
||||
...gitRefName
|
||||
}
|
||||
beforeCommit {
|
||||
...commitHash
|
||||
}
|
||||
afterCommit {
|
||||
...commitHash
|
||||
}
|
||||
}
|
||||
|
||||
fragment headRefDeletedEvent on HeadRefDeletedEvent {
|
||||
actor {
|
||||
...actor
|
||||
}
|
||||
createdAt
|
||||
headRefName
|
||||
}
|
||||
|
||||
fragment headRefRestoredEvent on HeadRefRestoredEvent {
|
||||
actor {
|
||||
...actor
|
||||
}
|
||||
createdAt
|
||||
}
|
||||
|
||||
fragment baseRefChangedEvent on BaseRefChangedEvent {
|
||||
actor {
|
||||
...actor
|
||||
}
|
||||
createdAt
|
||||
}
|
||||
|
||||
fragment referenceSubject on ReferencedSubject {
|
||||
__typename
|
||||
... on PullRequest {
|
||||
title
|
||||
number
|
||||
prState: state
|
||||
isDraft
|
||||
url
|
||||
}
|
||||
... on Issue {
|
||||
title
|
||||
number
|
||||
issueState: state
|
||||
url
|
||||
}
|
||||
}
|
||||
|
||||
fragment crossReferencedEvent on CrossReferencedEvent {
|
||||
actor {
|
||||
...actor
|
||||
}
|
||||
createdAt
|
||||
source {
|
||||
...referenceSubject
|
||||
}
|
||||
}
|
||||
|
||||
fragment connectedEvent on ConnectedEvent {
|
||||
actor {
|
||||
...actor
|
||||
}
|
||||
createdAt
|
||||
subject {
|
||||
...referenceSubject
|
||||
}
|
||||
}
|
||||
|
||||
fragment disconnectedEvent on DisconnectedEvent {
|
||||
actor {
|
||||
...actor
|
||||
}
|
||||
createdAt
|
||||
subject {
|
||||
...referenceSubject
|
||||
}
|
||||
}
|
||||
|
||||
query($repoOwner: String!, $repoName: String!, $number: Int!, $pageSize: Int = 100, $cursor: String, $since: DateTime) {
|
||||
repository(owner: $repoOwner, name: $repoName) {
|
||||
pullRequest(number: $number) {
|
||||
timelineItems(first: $pageSize, after: $cursor, since: $since) {
|
||||
pageInfo {
|
||||
...pageInfo
|
||||
}
|
||||
nodes {
|
||||
__typename
|
||||
...prCommitInfo
|
||||
...pullRequestReview
|
||||
...issueCommentInfo
|
||||
|
||||
...renamedEvent
|
||||
|
||||
...closedEvent
|
||||
...reopenedEvent
|
||||
...mergedEvent
|
||||
|
||||
...assignedEvent
|
||||
...unassignedEvent
|
||||
|
||||
...labeledEvent
|
||||
...unlabeledEvent
|
||||
|
||||
...reviewRequestedEvent
|
||||
...reviewRequestRemovedEvent
|
||||
|
||||
...reviewDismissedEvent
|
||||
|
||||
...readyForReviewEvent
|
||||
...convertToDraftEvent
|
||||
|
||||
...baseRefChangedEvent
|
||||
...baseRefForcePushedEvent
|
||||
|
||||
...headRefForcePushedEvent
|
||||
...headRefDeletedEvent
|
||||
...headRefRestoredEvent
|
||||
|
||||
...crossReferencedEvent
|
||||
...connectedEvent
|
||||
...disconnectedEvent
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rateLimit {
|
||||
...rates
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const removeReaction = `
|
||||
mutation ($id: ID!, $reaction: ReactionContent!) {
|
||||
removeReaction(input: {
|
||||
subjectId: $id,
|
||||
content: $reaction
|
||||
}) {
|
||||
reaction {
|
||||
...reaction
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const resolveReviewThread = `
|
||||
mutation ($threadId: ID!) {
|
||||
resolveReviewThread(input: {threadId: $threadId}) {
|
||||
thread {
|
||||
...pullRequestReviewThread
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const submitReview = `
|
||||
mutation ($reviewId: ID!, $event: PullRequestReviewEvent!, $body: String){
|
||||
submitPullRequestReview(input: {pullRequestReviewId: $reviewId, event: $event, body: $body}) {
|
||||
__typename
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const unmarkFileAsViewed = `
|
||||
mutation ($pullRequestId: ID!, $path: String!) {
|
||||
unmarkFileAsViewed(input: { pullRequestId: $pullRequestId, path: $path }) {
|
||||
__typename
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const unresolveReviewThread = `
|
||||
mutation ($threadId: ID!) {
|
||||
unresolveReviewThread(input: {threadId: $threadId}) {
|
||||
thread {
|
||||
...pullRequestReviewThread
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const updateIssueComment = `
|
||||
mutation ($id: ID!, $body: String!) {
|
||||
updateIssueComment(input: {id: $id, body: $body}) {
|
||||
issueComment {
|
||||
...comment
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const updatePullRequest = `
|
||||
mutation ($pullRequestId: ID!, $title: String, $body: String) {
|
||||
updatePullRequest(input: {pullRequestId: $pullRequestId, title: $title, body: $body}) {
|
||||
pullRequest {
|
||||
...pullRequestInfo
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const updateReview = `
|
||||
mutation ($reviewId: ID!, $body: String!){
|
||||
updatePullRequestReview(input: {pullRequestReviewId: $reviewId, body: $body}) {
|
||||
pullRequestReview {
|
||||
...pullRequestReview
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const updateReviewComment = `
|
||||
mutation ($id: ID!, $body: String!) {
|
||||
updatePullRequestReviewComment(input: {pullRequestReviewCommentId: $id, body: $body}) {
|
||||
pullRequestReviewComment {
|
||||
...pullRequestReviewComment
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,619 @@
|
||||
/**
|
||||
* Typed GitHub requests, one per IntelliJ `GHGQLRequests` / `GithubApiRequests` call the
|
||||
* plugin makes. Everything addresses the connected repository (`client.ts`) unless a
|
||||
* repository is passed. Mutations only send; `data/operations.ts` refreshes what they
|
||||
* change.
|
||||
*/
|
||||
import { graphql, repoPath, repoVariables, rest } from "./client";
|
||||
import * as Q from "./queries";
|
||||
import type {
|
||||
GHCollaborator,
|
||||
GHCommitFile,
|
||||
GHCommitStatusRollupContext,
|
||||
GHConnection,
|
||||
GHDiffSide,
|
||||
GHComment,
|
||||
GHLabel,
|
||||
GHPRTimelineItem,
|
||||
GHPullRequest,
|
||||
GHPullRequestBranchMatch,
|
||||
GHPullRequestChangedFile,
|
||||
GHPullRequestCommit,
|
||||
GHPullRequestDraftReviewThread,
|
||||
GHPullRequestMergeabilityData,
|
||||
GHPullRequestPendingReview,
|
||||
GHPullRequestReview,
|
||||
GHPullRequestReviewComment,
|
||||
GHPullRequestReviewEvent,
|
||||
GHPullRequestReviewThread,
|
||||
GHPullRequestShort,
|
||||
GHReaction,
|
||||
GHReactionContent,
|
||||
GHRepository,
|
||||
GHRepositoryCoordinates,
|
||||
GHRepositoryPullRequestTemplate,
|
||||
GHTeam,
|
||||
GHUser,
|
||||
} from "./types";
|
||||
|
||||
/** IntelliJ's `PR_PAGE_SIZE`: list rows per `issueSearch` page. */
|
||||
export const PR_PAGE_SIZE = 50;
|
||||
|
||||
type Pr = { repository: { pullRequest: Record<string, unknown> | null } | null };
|
||||
|
||||
/** `repository.pullRequest.<field>` of a pull request document, or null when the PR is gone. */
|
||||
async function prField<T>(doc: string, number: number, field: string, vars: Record<string, unknown> = {}) {
|
||||
const data = await graphql<Pr>(doc, { ...repoVariables(), number, ...vars });
|
||||
return (data.repository?.pullRequest?.[field] as T | undefined) ?? null;
|
||||
}
|
||||
|
||||
/** Every page of a cursor-paged connection, in order. */
|
||||
export async function allPages<T>(page: (cursor: string | null) => Promise<GHConnection<T> | null>): Promise<T[]> {
|
||||
const out: T[] = [];
|
||||
let cursor: string | null = null;
|
||||
for (;;) {
|
||||
const c: GHConnection<T> | null = await page(cursor);
|
||||
if (!c) return out;
|
||||
out.push(...c.nodes);
|
||||
if (!c.pageInfo.hasNextPage || !c.pageInfo.endCursor) return out;
|
||||
cursor = c.pageInfo.endCursor;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Repository and users
|
||||
|
||||
export async function findRepository(repo: GHRepositoryCoordinates): Promise<GHRepository | null> {
|
||||
const data = await graphql<{ repository: GHRepository | null }>(Q.findRepository, repoVariables(repo));
|
||||
return data.repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* A user by login (`findUser`).
|
||||
* @public
|
||||
*/
|
||||
export async function findUser(login: string): Promise<GHUser | null> {
|
||||
return (await graphql<{ user: GHUser | null }>(Q.findUser, { login })).user;
|
||||
}
|
||||
|
||||
interface RestUser {
|
||||
login: string;
|
||||
node_id: string;
|
||||
avatar_url: string;
|
||||
html_url: string;
|
||||
name?: string | null;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
/** A REST user as the GraphQL `user` fragment shape. */
|
||||
export function restUser(u: RestUser): GHUser {
|
||||
return {
|
||||
__typename: "User",
|
||||
id: u.node_id,
|
||||
login: u.login,
|
||||
url: u.html_url,
|
||||
avatarUrl: u.avatar_url,
|
||||
name: u.name ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
/** The signed-in user (REST `user`, IntelliJ's `CurrentUser.get`). */
|
||||
export async function currentUser(): Promise<GHUser> {
|
||||
return restUser(await rest<RestUser>("GET", "user"));
|
||||
}
|
||||
|
||||
/** Page `page` (1-based) of a REST list; `next` is null on the last page. */
|
||||
async function restPage<T, R>(endpoint: string, page: number, map: (r: R) => T) {
|
||||
const sep = endpoint.includes("?") ? "&" : "?";
|
||||
const items = (await rest<R[] | null>("GET", `${endpoint}${sep}per_page=100&page=${page}`)) ?? [];
|
||||
return { items: items.map(map), next: items.length === 100 ? page + 1 : null };
|
||||
}
|
||||
|
||||
export function collaboratorsPage(page: number) {
|
||||
return restPage<GHCollaborator, RestUser & { permissions?: GHCollaborator["permissions"] }>(
|
||||
`${repoPath()}/collaborators`,
|
||||
page,
|
||||
(u) => ({
|
||||
...restUser(u),
|
||||
permissions: u.permissions ?? { admin: false, maintain: false, push: false, triage: false, pull: true },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function contributorsPage(page: number) {
|
||||
return restPage<GHUser, RestUser>(`${repoPath()}/contributors`, page, restUser);
|
||||
}
|
||||
|
||||
/** Users issues and pull requests can be assigned to. */
|
||||
export function assigneesPage(page: number) {
|
||||
return restPage<GHUser, RestUser>(`${repoPath()}/assignees`, page, restUser);
|
||||
}
|
||||
|
||||
interface RestLabel {
|
||||
node_id: string;
|
||||
url: string;
|
||||
name: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
const restLabel = (l: RestLabel): GHLabel => ({ id: l.node_id, url: l.url, name: l.name, color: l.color });
|
||||
|
||||
export function labelsPage(page: number) {
|
||||
return restPage<GHLabel, RestLabel>(`${repoPath()}/labels`, page, restLabel);
|
||||
}
|
||||
|
||||
export async function mentionableUsersPage(cursor: string | null): Promise<GHConnection<GHUser> | null> {
|
||||
const data = await graphql<{ repository: { mentionableUsers: GHConnection<GHUser> } | null }>(
|
||||
Q.findMentionableUsers,
|
||||
{ ...repoVariables(), cursor },
|
||||
);
|
||||
return data.repository?.mentionableUsers ?? null;
|
||||
}
|
||||
|
||||
export async function organizationTeamsPage(organization: string, cursor: string | null) {
|
||||
const data = await graphql<{ organization: { teams: GHConnection<GHTeam> } | null }>(Q.findOrganizationTeams, {
|
||||
organization,
|
||||
cursor,
|
||||
});
|
||||
return data.organization?.teams ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* A page of the branch protection rules (`getProtectionRules`).
|
||||
* @public
|
||||
*/
|
||||
export async function protectionRulesPage(cursor: string | null) {
|
||||
const data = await graphql<{
|
||||
repository: { branchProtectionRules: GHConnection<{ id: string; pattern: string }> } | null;
|
||||
}>(Q.getProtectionRules, { ...repoVariables(), cursor });
|
||||
return data.repository?.branchProtectionRules ?? null;
|
||||
}
|
||||
|
||||
export async function pullRequestTemplates(): Promise<GHRepositoryPullRequestTemplate[]> {
|
||||
const data = await graphql<{ repository: { pullRequestTemplates: GHRepositoryPullRequestTemplate[] | null } | null }>(
|
||||
Q.getPullRequestTemplates,
|
||||
repoVariables(),
|
||||
);
|
||||
return data.repository?.pullRequestTemplates ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* A page of the commit's status check rollup (`getRepositoryCommitStatusContexts`).
|
||||
* @public
|
||||
*/
|
||||
export async function commitStatusContextsPage(oid: string, cursor: string | null, pageSize = 100) {
|
||||
const data = await graphql<{
|
||||
repository: {
|
||||
object: { statusCheckRollup: { contexts: GHConnection<GHCommitStatusRollupContext> } | null } | null;
|
||||
};
|
||||
}>(Q.getRepositoryCommitStatusContexts, { ...repoVariables(), oid, pageSize, cursor });
|
||||
return data.repository.object?.statusCheckRollup?.contexts ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The commit's overall status check state (`getRepositoryCommitStatusShort`).
|
||||
* @public
|
||||
*/
|
||||
export async function commitStatusShort(oid: string): Promise<string | null> {
|
||||
const data = await graphql<{ repository: { object: { statusCheckRollup: { state: string } | null } | null } }>(
|
||||
Q.getRepositoryCommitStatusShort,
|
||||
{ ...repoVariables(), oid },
|
||||
);
|
||||
return data.repository.object?.statusCheckRollup?.state ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The counts of IntelliJ's usage metrics, by query (each a full search string).
|
||||
* @public
|
||||
*/
|
||||
export async function metrics(queries: {
|
||||
allPRCountQuery: string;
|
||||
openPRCountQuery: string;
|
||||
openAuthoredPRCountQuery: string;
|
||||
openAssigneePRCountQuery: string;
|
||||
openReviewedPRCountQuery: string;
|
||||
openReviewAssignedPRCountQuery: string;
|
||||
}): Promise<Record<string, number>> {
|
||||
const data = await graphql<Record<string, { issueCount: number }>>(Q.metrics, queries);
|
||||
return Object.fromEntries(
|
||||
Object.entries(data)
|
||||
.filter(([k]) => k !== "rateLimit")
|
||||
.map(([k, v]) => [k, v.issueCount]),
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Pull request queries
|
||||
|
||||
/** One `issueSearch` page: `query` is the full search string (`type:pr repo:o/r …`). */
|
||||
export async function searchPullRequests(query: string, cursor: string | null, pageSize = PR_PAGE_SIZE) {
|
||||
const data = await graphql<{ search: GHConnection<GHPullRequestShort | Record<string, never>> }>(Q.issueSearch, {
|
||||
query,
|
||||
pageSize,
|
||||
cursor,
|
||||
});
|
||||
// `type: ISSUE` may return issues (no fields selected for them: empty objects).
|
||||
const nodes = data.search.nodes.filter((n): n is GHPullRequestShort => "number" in n);
|
||||
return { pageInfo: data.search.pageInfo, nodes };
|
||||
}
|
||||
|
||||
/**
|
||||
* Open pull requests whose head matches the `head:` terms of `query` (`findPullRequestsByHeadBranch`).
|
||||
* @public
|
||||
*/
|
||||
export async function searchPullRequestsByHeadBranches(query: string, pageSize = 100) {
|
||||
const data = await graphql<{ search: GHConnection<GHPullRequestBranchMatch> }>(Q.findPullRequestsByHeadBranch, {
|
||||
query,
|
||||
pageSize,
|
||||
});
|
||||
return data.search.nodes.filter((n) => "number" in n);
|
||||
}
|
||||
|
||||
export async function findPullRequest(number: number): Promise<GHPullRequest | null> {
|
||||
const data = await graphql<{ repository: { pullRequest: GHPullRequest | null } | null }>(Q.findPullRequest, {
|
||||
...repoVariables(),
|
||||
number,
|
||||
});
|
||||
return data.repository?.pullRequest ?? null;
|
||||
}
|
||||
|
||||
export async function findPullRequestId(number: number): Promise<string | null> {
|
||||
return prField<string>(Q.findPullRequestId, number, "id");
|
||||
}
|
||||
|
||||
export async function findPullRequestMergeability(number: number): Promise<GHPullRequestMergeabilityData | null> {
|
||||
const data = await graphql<{ repository: { pullRequest: GHPullRequestMergeabilityData | null } | null }>(
|
||||
Q.findPullRequestMergeability,
|
||||
{ ...repoVariables(), number },
|
||||
);
|
||||
return data.repository?.pullRequest ?? null;
|
||||
}
|
||||
|
||||
export function pullRequestCommitsPage(number: number, cursor: string | null) {
|
||||
return prField<GHConnection<GHPullRequestCommit>>(Q.pullRequestCommits, number, "commits", { cursor });
|
||||
}
|
||||
|
||||
export function pullRequestFilesPage(number: number, cursor: string | null) {
|
||||
return prField<GHConnection<GHPullRequestChangedFile>>(Q.pullRequestFiles, number, "files", { cursor });
|
||||
}
|
||||
|
||||
export function pullRequestParticipantsPage(number: number, cursor: string | null) {
|
||||
return prField<GHConnection<GHUser>>(Q.pullRequestParticipants, number, "participants", { cursor });
|
||||
}
|
||||
|
||||
export function pullRequestReviewThreadsPage(number: number, cursor: string | null) {
|
||||
return prField<GHConnection<GHPullRequestReviewThread>>(Q.pullRequestReviewThreads, number, "reviewThreads", {
|
||||
cursor,
|
||||
});
|
||||
}
|
||||
|
||||
/** Timeline item types IntelliJ drops (`GHPRTimelineItem.IGNORED_TYPES`). */
|
||||
const IGNORED_TIMELINE_TYPES = new Set([
|
||||
"ReferencedEvent",
|
||||
"MentionedEvent",
|
||||
"CommentDeletedEvent",
|
||||
"LockedEvent",
|
||||
"UnlockedEvent",
|
||||
"SubscribedEvent",
|
||||
"UnsubscribedEvent",
|
||||
]);
|
||||
|
||||
/** Item types the timeline document selects fields for. */
|
||||
const TIMELINE_TYPES = new Set([
|
||||
"PullRequestCommit",
|
||||
"PullRequestReview",
|
||||
"IssueComment",
|
||||
"RenamedTitleEvent",
|
||||
"ClosedEvent",
|
||||
"ReopenedEvent",
|
||||
"MergedEvent",
|
||||
"AssignedEvent",
|
||||
"UnassignedEvent",
|
||||
"LabeledEvent",
|
||||
"UnlabeledEvent",
|
||||
"ReviewRequestedEvent",
|
||||
"ReviewRequestRemovedEvent",
|
||||
"ReviewDismissedEvent",
|
||||
"ReadyForReviewEvent",
|
||||
"ConvertToDraftEvent",
|
||||
"BaseRefChangedEvent",
|
||||
"BaseRefForcePushedEvent",
|
||||
"HeadRefForcePushedEvent",
|
||||
"HeadRefDeletedEvent",
|
||||
"HeadRefRestoredEvent",
|
||||
"CrossReferencedEvent",
|
||||
"ConnectedEvent",
|
||||
"DisconnectedEvent",
|
||||
]);
|
||||
|
||||
/** Timeline nodes as items: ignored types dropped, others IntelliJ can't show marked `Unknown`. */
|
||||
export function timelineItems(nodes: ReadonlyArray<{ __typename: string }>): GHPRTimelineItem[] {
|
||||
return nodes.flatMap((n): GHPRTimelineItem[] => {
|
||||
if (IGNORED_TIMELINE_TYPES.has(n.__typename)) return [];
|
||||
if (TIMELINE_TYPES.has(n.__typename)) return [n as GHPRTimelineItem];
|
||||
return [{ __typename: "Unknown", type: n.__typename }];
|
||||
});
|
||||
}
|
||||
|
||||
/** One page (100) of timeline items, after `cursor`. */
|
||||
export async function pullRequestTimelinePage(number: number, cursor: string | null, since?: string) {
|
||||
const c = await prField<GHConnection<{ __typename: string }>>(Q.pullRequestTimeline, number, "timelineItems", {
|
||||
cursor,
|
||||
since,
|
||||
});
|
||||
return c && { pageInfo: c.pageInfo, nodes: timelineItems(c.nodes) };
|
||||
}
|
||||
|
||||
interface PendingReviewNode {
|
||||
id: string;
|
||||
state: GHPullRequestPendingReview["state"];
|
||||
comments: { totalCount: number };
|
||||
}
|
||||
|
||||
const pendingReview = (r: PendingReviewNode): GHPullRequestPendingReview => ({
|
||||
id: r.id,
|
||||
state: r.state,
|
||||
commentsCount: r.comments.totalCount,
|
||||
});
|
||||
|
||||
/** The viewer's pending (not yet submitted) review of the pull request with node id `pullRequestId`. */
|
||||
export async function findPendingReview(pullRequestId: string): Promise<GHPullRequestPendingReview | null> {
|
||||
const data = await graphql<{ node: { reviews?: { nodes: PendingReviewNode[] } } | null }>(Q.findPendingReview, {
|
||||
pullRequestId,
|
||||
});
|
||||
const r = data.node?.reviews?.nodes[0];
|
||||
return r ? pendingReview(r) : null;
|
||||
}
|
||||
|
||||
interface RestCommitFile {
|
||||
sha: string | null;
|
||||
filename: string;
|
||||
status: GHCommitFile["status"];
|
||||
additions: number;
|
||||
deletions: number;
|
||||
changes: number;
|
||||
patch?: string;
|
||||
previous_filename?: string;
|
||||
}
|
||||
|
||||
const commitFile = (f: RestCommitFile): GHCommitFile => ({
|
||||
sha: f.sha,
|
||||
filename: f.filename,
|
||||
status: f.status,
|
||||
additions: f.additions,
|
||||
deletions: f.deletions,
|
||||
changes: f.changes,
|
||||
patch: f.patch ?? null,
|
||||
previousFilename: f.previous_filename ?? null,
|
||||
});
|
||||
|
||||
/** The pull request's changed files (REST `pulls/{n}/files`, every page). */
|
||||
export async function pullRequestChangedFiles(number: number): Promise<GHCommitFile[]> {
|
||||
const files = await rest<RestCommitFile[]>("GET", `${repoPath()}/pulls/${number}/files?per_page=100`, undefined, {
|
||||
paginate: true,
|
||||
});
|
||||
return (files ?? []).map(commitFile);
|
||||
}
|
||||
|
||||
/** The files a commit changes (REST `commits/{sha}`). */
|
||||
export async function commitChangedFiles(sha: string): Promise<GHCommitFile[]> {
|
||||
const commit = await rest<{ files?: RestCommitFile[] }>("GET", `${repoPath()}/commits/${sha}`);
|
||||
return (commit?.files ?? []).map(commitFile);
|
||||
}
|
||||
|
||||
/** The files changed from the merge base of `base` and `head` to `head` (REST `compare/{base}...{head}`). */
|
||||
export async function compareChangedFiles(base: string, head: string): Promise<GHCommitFile[]> {
|
||||
const r = await rest<{ files?: RestCommitFile[] }>("GET", `${repoPath()}/compare/${base}...${head}`);
|
||||
return (r?.files ?? []).map(commitFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open pull requests from `head` (`owner:branch`) into `base`, REST `Repos.PullRequests.find`.
|
||||
*/
|
||||
export async function findOpenPullRequestsByHead(
|
||||
head: string,
|
||||
base?: string,
|
||||
): Promise<Array<{ number: number; html_url: string }>> {
|
||||
const q = new URLSearchParams({ state: "open", head, ...(base ? { base } : {}), per_page: "1" });
|
||||
return (await rest<Array<{ number: number; html_url: string }>>("GET", `${repoPath()}/pulls?${q}`)) ?? [];
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Pull request mutations (GraphQL)
|
||||
|
||||
export async function createPullRequest(input: {
|
||||
repositoryId: string;
|
||||
baseRefName: string;
|
||||
/** `owner:branch` for a branch of a fork. */
|
||||
headRefName: string;
|
||||
title: string;
|
||||
body: string | null;
|
||||
draft: boolean;
|
||||
}): Promise<GHPullRequestShort> {
|
||||
const data = await graphql<{ createPullRequest: { pullRequest: GHPullRequestShort } }>(Q.createPullRequest, input);
|
||||
return data.createPullRequest.pullRequest;
|
||||
}
|
||||
|
||||
export async function updatePullRequest(pullRequestId: string, patch: { title?: string; body?: string }) {
|
||||
const data = await graphql<{ updatePullRequest: { pullRequest: GHPullRequest } }>(Q.updatePullRequest, {
|
||||
pullRequestId,
|
||||
...patch,
|
||||
});
|
||||
return data.updatePullRequest.pullRequest;
|
||||
}
|
||||
|
||||
export async function markPullRequestReadyForReview(pullRequestId: string): Promise<void> {
|
||||
await graphql(Q.markPullRequestReadyForReview, { pullRequestId });
|
||||
}
|
||||
|
||||
export async function markFileAsViewed(pullRequestId: string, path: string): Promise<void> {
|
||||
await graphql(Q.markFileAsViewed, { pullRequestId, path });
|
||||
}
|
||||
|
||||
export async function unmarkFileAsViewed(pullRequestId: string, path: string): Promise<void> {
|
||||
await graphql(Q.unmarkFileAsViewed, { pullRequestId, path });
|
||||
}
|
||||
|
||||
export async function deleteRef(refId: string): Promise<void> {
|
||||
await graphql(Q.deleteRef, { refId });
|
||||
}
|
||||
|
||||
/** `addPullRequestReview`: a pending review without `event`, else a submitted one. */
|
||||
export async function createReview(input: {
|
||||
pullRequestId: string;
|
||||
event?: GHPullRequestReviewEvent | null;
|
||||
body?: string | null;
|
||||
commitOid?: string | null;
|
||||
threads?: GHPullRequestDraftReviewThread[] | null;
|
||||
}): Promise<GHPullRequestPendingReview> {
|
||||
const data = await graphql<{ addPullRequestReview: { pullRequestReview: PendingReviewNode } }>(Q.createReview, input);
|
||||
return pendingReview(data.addPullRequestReview.pullRequestReview);
|
||||
}
|
||||
|
||||
export async function submitReview(reviewId: string, event: GHPullRequestReviewEvent, body?: string | null) {
|
||||
await graphql(Q.submitReview, { reviewId, event, body });
|
||||
}
|
||||
|
||||
export async function deleteReview(reviewId: string): Promise<void> {
|
||||
await graphql(Q.deleteReview, { reviewId });
|
||||
}
|
||||
|
||||
export async function updateReview(reviewId: string, body: string): Promise<GHPullRequestReview> {
|
||||
const data = await graphql<{ updatePullRequestReview: { pullRequestReview: GHPullRequestReview } }>(Q.updateReview, {
|
||||
reviewId,
|
||||
body,
|
||||
});
|
||||
return data.updatePullRequestReview.pullRequestReview;
|
||||
}
|
||||
|
||||
/** `addPullRequestReviewComment`: a reply (`inReplyTo`), or a comment on a commit position. */
|
||||
export async function addReviewComment(input: {
|
||||
reviewId: string;
|
||||
body: string;
|
||||
commit?: string | null;
|
||||
file?: string | null;
|
||||
position?: number | null;
|
||||
inReplyTo?: string | null;
|
||||
}): Promise<GHPullRequestReviewComment> {
|
||||
const data = await graphql<{ addPullRequestReviewComment: { comment: GHPullRequestReviewComment } }>(
|
||||
Q.addReviewComment,
|
||||
input,
|
||||
);
|
||||
return data.addPullRequestReviewComment.comment;
|
||||
}
|
||||
|
||||
export async function updateReviewComment(id: string, body: string): Promise<GHPullRequestReviewComment> {
|
||||
const data = await graphql<{
|
||||
updatePullRequestReviewComment: { pullRequestReviewComment: GHPullRequestReviewComment };
|
||||
}>(Q.updateReviewComment, { id, body });
|
||||
return data.updatePullRequestReviewComment.pullRequestReviewComment;
|
||||
}
|
||||
|
||||
/** Deletes a review comment; returns its (pending) review, when it still exists. */
|
||||
export async function deleteReviewComment(id: string): Promise<GHPullRequestPendingReview | null> {
|
||||
const data = await graphql<{ deletePullRequestReviewComment: { pullRequestReview: PendingReviewNode | null } }>(
|
||||
Q.deleteReviewComment,
|
||||
{ id },
|
||||
);
|
||||
const r = data.deletePullRequestReviewComment.pullRequestReview;
|
||||
return r ? pendingReview(r) : null;
|
||||
}
|
||||
|
||||
export async function addReviewThread(input: {
|
||||
pullRequestReviewId: string;
|
||||
body: string;
|
||||
path: string;
|
||||
line: number;
|
||||
side: GHDiffSide;
|
||||
startLine?: number | null;
|
||||
startSide?: GHDiffSide | null;
|
||||
}): Promise<GHPullRequestReviewThread> {
|
||||
const data = await graphql<{ addPullRequestReviewThread: { thread: GHPullRequestReviewThread } }>(
|
||||
Q.addPullRequestReviewThread,
|
||||
input,
|
||||
);
|
||||
return data.addPullRequestReviewThread.thread;
|
||||
}
|
||||
|
||||
export async function resolveReviewThread(threadId: string): Promise<GHPullRequestReviewThread> {
|
||||
const data = await graphql<{ resolveReviewThread: { thread: GHPullRequestReviewThread } }>(Q.resolveReviewThread, {
|
||||
threadId,
|
||||
});
|
||||
return data.resolveReviewThread.thread;
|
||||
}
|
||||
|
||||
export async function unresolveReviewThread(threadId: string): Promise<GHPullRequestReviewThread> {
|
||||
const data = await graphql<{ unresolveReviewThread: { thread: GHPullRequestReviewThread } }>(
|
||||
Q.unresolveReviewThread,
|
||||
{ threadId },
|
||||
);
|
||||
return data.unresolveReviewThread.thread;
|
||||
}
|
||||
|
||||
export async function updateIssueComment(id: string, body: string): Promise<GHComment> {
|
||||
const data = await graphql<{ updateIssueComment: { issueComment: GHComment } }>(Q.updateIssueComment, { id, body });
|
||||
return data.updateIssueComment.issueComment;
|
||||
}
|
||||
|
||||
export async function deleteIssueComment(id: string): Promise<void> {
|
||||
await graphql(Q.deleteIssueComment, { id });
|
||||
}
|
||||
|
||||
export async function addReaction(id: string, reaction: GHReactionContent): Promise<GHReaction> {
|
||||
const data = await graphql<{ addReaction: { reaction: GHReaction } }>(Q.addReaction, { id, reaction });
|
||||
return data.addReaction.reaction;
|
||||
}
|
||||
|
||||
export async function removeReaction(id: string, reaction: GHReactionContent): Promise<GHReaction> {
|
||||
const data = await graphql<{ removeReaction: { reaction: GHReaction } }>(Q.removeReaction, { id, reaction });
|
||||
return data.removeReaction.reaction;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Pull request mutations (REST)
|
||||
|
||||
/** Adds (`POST`) or removes (`DELETE`) requested reviewers: user logins and team slugs. */
|
||||
export async function changeRequestedReviewers(
|
||||
number: number,
|
||||
method: "POST" | "DELETE",
|
||||
reviewers: { logins: string[]; teamSlugs: string[] },
|
||||
): Promise<void> {
|
||||
await rest(method, `${repoPath()}/pulls/${number}/requested_reviewers`, {
|
||||
reviewers: reviewers.logins,
|
||||
team_reviewers: reviewers.teamSlugs,
|
||||
});
|
||||
}
|
||||
|
||||
/** Closes or reopens (`PullRequests.update(state)`). */
|
||||
export async function updatePullRequestState(number: number, state: "open" | "closed"): Promise<void> {
|
||||
await rest("PATCH", `${repoPath()}/pulls/${number}`, { state });
|
||||
}
|
||||
|
||||
/** `PUT pulls/{n}/merge` at head `sha` (rebase takes no message). */
|
||||
export async function mergePullRequest(
|
||||
number: number,
|
||||
method: "merge" | "squash" | "rebase",
|
||||
sha: string,
|
||||
message?: { title: string; body: string },
|
||||
): Promise<void> {
|
||||
await rest("PUT", `${repoPath()}/pulls/${number}/merge`, {
|
||||
merge_method: method,
|
||||
sha,
|
||||
...(message ? { commit_title: message.title, commit_message: message.body } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
/** Adds a conversation comment (`Issues.Comments.create`). */
|
||||
export async function createIssueComment(number: number, body: string): Promise<void> {
|
||||
await rest("POST", `${repoPath()}/issues/${number}/comments`, { body });
|
||||
}
|
||||
|
||||
/** Replaces the assignees (`Issues.updateAssignees`). */
|
||||
export async function updateAssignees(number: number, logins: string[]): Promise<void> {
|
||||
await rest("PATCH", `${repoPath()}/issues/${number}`, { assignees: logins });
|
||||
}
|
||||
|
||||
/** Replaces the labels (`Issues.Labels.replace`). */
|
||||
export async function replaceLabels(number: number, names: string[]): Promise<void> {
|
||||
await rest("PUT", `${repoPath()}/issues/${number}/labels`, { labels: names });
|
||||
}
|
||||
@@ -0,0 +1,499 @@
|
||||
/**
|
||||
* Shapes of the GitHub data the documents in `queries.ts` return, named after IntelliJ's
|
||||
* DTOs (`GHPullRequestShort`, `GHPullRequest`, …). GraphQL connections keep their
|
||||
* `nodes`; `data/model.ts` derives what the UI shows (reviewer states, unread, counters).
|
||||
* REST results are mapped to the same shapes (`GHUser`, `GHLabel`) where IntelliJ does.
|
||||
*/
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Common
|
||||
|
||||
/** `pageInfo` fragment. */
|
||||
export interface GHPageInfo {
|
||||
startCursor: string | null;
|
||||
hasPreviousPage: boolean;
|
||||
endCursor: string | null;
|
||||
hasNextPage: boolean;
|
||||
}
|
||||
|
||||
export interface GHConnection<T> {
|
||||
pageInfo: GHPageInfo;
|
||||
nodes: T[];
|
||||
}
|
||||
|
||||
export interface GHNodes<T> {
|
||||
nodes: T[];
|
||||
}
|
||||
|
||||
/** `actorInfo`: what every actor has. `name` comes with users, organizations and enterprise accounts. */
|
||||
export interface GHActor {
|
||||
__typename: "User" | "Bot" | "Organization" | "EnterpriseUserAccount" | "Mannequin" | (string & {});
|
||||
id: string;
|
||||
login: string;
|
||||
url: string;
|
||||
avatarUrl: string;
|
||||
name?: string | null;
|
||||
}
|
||||
|
||||
/** `user` fragment. */
|
||||
export interface GHUser extends GHActor {
|
||||
__typename: "User";
|
||||
name: string | null;
|
||||
}
|
||||
|
||||
/** `teamInfo` fragment (aliased `teamAvatarUrl`, `teamName`). */
|
||||
export interface GHTeam {
|
||||
__typename: "Team";
|
||||
id: string;
|
||||
slug: string;
|
||||
url: string;
|
||||
teamAvatarUrl: string | null;
|
||||
teamName: string;
|
||||
/** `org/slug`. */
|
||||
combinedSlug: string;
|
||||
}
|
||||
|
||||
/** `pullRequestReviewer`: a user, a team or (GitHub Apps) another actor. */
|
||||
export type GHPullRequestRequestedReviewer = GHUser | GHTeam | GHActor;
|
||||
|
||||
/** `labelInfo` fragment. `color` is hex without `#`. */
|
||||
export interface GHLabel {
|
||||
id: string;
|
||||
url: string;
|
||||
name: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
export type GHReactionContent =
|
||||
| "THUMBS_UP"
|
||||
| "THUMBS_DOWN"
|
||||
| "LAUGH"
|
||||
| "HOORAY"
|
||||
| "CONFUSED"
|
||||
| "HEART"
|
||||
| "ROCKET"
|
||||
| "EYES";
|
||||
|
||||
export interface GHReaction {
|
||||
content: GHReactionContent;
|
||||
user: GHUser | null;
|
||||
}
|
||||
|
||||
/** `reactions` fragment (first 25). */
|
||||
export interface GHReactable {
|
||||
reactions: GHConnection<GHReaction>;
|
||||
}
|
||||
|
||||
/** `gitActorInfo`: commit author / committer. */
|
||||
export interface GHGitActor {
|
||||
name: string | null;
|
||||
email: string | null;
|
||||
avatarUrl: string;
|
||||
date: string | null;
|
||||
user: GHUser | null;
|
||||
}
|
||||
|
||||
/** `commitHash` fragment. */
|
||||
export interface GHCommitHash {
|
||||
id: string;
|
||||
oid: string;
|
||||
abbreviatedOid: string;
|
||||
}
|
||||
|
||||
/** `commitShort` fragment. */
|
||||
export interface GHCommitShort extends GHCommitHash {
|
||||
url: string;
|
||||
messageHeadline: string;
|
||||
author: GHGitActor | null;
|
||||
}
|
||||
|
||||
/** `commit` fragment. */
|
||||
export interface GHCommit extends GHCommitShort {
|
||||
committer: GHGitActor | null;
|
||||
messageBody: string;
|
||||
committedDate: string;
|
||||
parents: GHNodes<GHCommitHash>;
|
||||
}
|
||||
|
||||
/** `pullRequestCommits` node. */
|
||||
export interface GHPullRequestCommit {
|
||||
id: string;
|
||||
commit: GHCommit;
|
||||
url: string;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Repository
|
||||
|
||||
export type GHRepositoryPermissionLevel = "READ" | "TRIAGE" | "WRITE" | "MAINTAIN" | "ADMIN";
|
||||
|
||||
/** `repository` fragment. */
|
||||
export interface GHRepository {
|
||||
id: string;
|
||||
owner: { __typename: "User" | "Organization" | (string & {}); id: string; login: string };
|
||||
nameWithOwner: string;
|
||||
viewerPermission: GHRepositoryPermissionLevel | null;
|
||||
mergeCommitAllowed: boolean;
|
||||
squashMergeAllowed: boolean;
|
||||
rebaseMergeAllowed: boolean;
|
||||
defaultBranchRef: { name: string } | null;
|
||||
isFork: boolean;
|
||||
url: string;
|
||||
sshUrl: string;
|
||||
}
|
||||
|
||||
/** Where a request goes: `owner/name` on github.com. */
|
||||
export interface GHRepositoryCoordinates {
|
||||
owner: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
/** REST collaborator (`repos/{o}/{r}/collaborators`) mapped to a user, with its permissions. */
|
||||
export interface GHCollaborator extends GHUser {
|
||||
permissions: { admin: boolean; maintain: boolean; push: boolean; triage: boolean; pull: boolean };
|
||||
}
|
||||
|
||||
/** `pullRequestTemplate` fragment. */
|
||||
export interface GHRepositoryPullRequestTemplate {
|
||||
body: string | null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Pull requests
|
||||
|
||||
export type GHPullRequestState = "OPEN" | "CLOSED" | "MERGED";
|
||||
export type GHPullRequestMergeableState = "MERGEABLE" | "CONFLICTING" | "UNKNOWN";
|
||||
export type GHPullRequestReviewDecision = "APPROVED" | "CHANGES_REQUESTED" | "REVIEW_REQUIRED";
|
||||
export type GHPullRequestReviewState = "PENDING" | "COMMENTED" | "APPROVED" | "CHANGES_REQUESTED" | "DISMISSED";
|
||||
export type GHPullRequestReviewEvent = "COMMENT" | "APPROVE" | "REQUEST_CHANGES" | "DISMISS";
|
||||
export type GHPullRequestMergeStateStatus =
|
||||
| "BEHIND"
|
||||
| "BLOCKED"
|
||||
| "CLEAN"
|
||||
| "DIRTY"
|
||||
| "DRAFT"
|
||||
| "HAS_HOOKS"
|
||||
| "UNKNOWN"
|
||||
| "UNSTABLE";
|
||||
|
||||
/** `pullRequestReview` fragment. */
|
||||
export interface GHPullRequestReview {
|
||||
__typename: "PullRequestReview";
|
||||
id: string;
|
||||
url: string;
|
||||
author: GHActor | null;
|
||||
body: string;
|
||||
state: GHPullRequestReviewState;
|
||||
createdAt: string;
|
||||
viewerCanUpdate: boolean;
|
||||
}
|
||||
|
||||
/** `pullRequestInfoShort` fragment: a list row (`issueSearch`). */
|
||||
export interface GHPullRequestShort extends GHReactable {
|
||||
id: string;
|
||||
url: string;
|
||||
number: number;
|
||||
title: string;
|
||||
state: GHPullRequestState;
|
||||
isDraft: boolean;
|
||||
author: GHActor | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
isReadByViewer: boolean;
|
||||
assignees: GHNodes<GHUser>;
|
||||
labels: GHNodes<GHLabel>;
|
||||
reviewRequests: GHNodes<{ requestedReviewer: GHPullRequestRequestedReviewer | null }>;
|
||||
reviewThreads: GHNodes<{ isResolved: boolean; isOutdated: boolean }>;
|
||||
reviews: GHNodes<GHPullRequestReview>;
|
||||
mergeable: GHPullRequestMergeableState;
|
||||
viewerCanUpdate: boolean;
|
||||
viewerCanReact: boolean;
|
||||
viewerDidAuthor: boolean;
|
||||
}
|
||||
|
||||
/** `pullRequestInfo` fragment: the details (`findPullRequest`). */
|
||||
export interface GHPullRequest extends GHPullRequestShort {
|
||||
body: string;
|
||||
reviewDecision: GHPullRequestReviewDecision | null;
|
||||
baseRefName: string;
|
||||
baseRefOid: string;
|
||||
baseRepository: GHRepository | null;
|
||||
/** Null once the head branch is deleted. */
|
||||
headRef: { id: string } | null;
|
||||
headRefName: string;
|
||||
headRefOid: string;
|
||||
headRepository: GHRepository | null;
|
||||
viewerCanDeleteHeadRef: boolean;
|
||||
}
|
||||
|
||||
/** `pullRequestBranchMatch` fragment. */
|
||||
export interface GHPullRequestBranchMatch {
|
||||
id: string;
|
||||
number: number;
|
||||
title: string;
|
||||
headRefName: string;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mergeability (`findPullRequestMergeability`)
|
||||
|
||||
/** `refUpdateRule` fragment. */
|
||||
export interface GHRefUpdateRule {
|
||||
allowsDeletions: boolean;
|
||||
allowsForcePushes: boolean;
|
||||
pattern: string;
|
||||
requiredApprovingReviewCount: number | null;
|
||||
requiredStatusCheckContexts: Array<string | null>;
|
||||
requiresLinearHistory: boolean;
|
||||
requiresSignatures: boolean;
|
||||
viewerCanPush: boolean;
|
||||
}
|
||||
|
||||
export type GHCommitStatusContextState = "ERROR" | "EXPECTED" | "FAILURE" | "PENDING" | "SUCCESS";
|
||||
export type GHCommitCheckSuiteConclusion =
|
||||
| "ACTION_REQUIRED"
|
||||
| "CANCELLED"
|
||||
| "FAILURE"
|
||||
| "NEUTRAL"
|
||||
| "SKIPPED"
|
||||
| "STALE"
|
||||
| "STARTUP_FAILURE"
|
||||
| "SUCCESS"
|
||||
| "TIMED_OUT";
|
||||
|
||||
/** `commitStatusContext` fragment. */
|
||||
export interface GHCommitStatusContext {
|
||||
context: string;
|
||||
state: GHCommitStatusContextState;
|
||||
isRequired: boolean;
|
||||
targetUrl: string | null;
|
||||
}
|
||||
|
||||
/** `checkRun` fragment. */
|
||||
export interface GHCheckRun {
|
||||
name: string;
|
||||
conclusion: GHCommitCheckSuiteConclusion | null;
|
||||
isRequired: boolean;
|
||||
detailsUrl: string | null;
|
||||
url: string;
|
||||
}
|
||||
|
||||
/** `commitWithCheckStatuses` fragment. */
|
||||
export interface GHCommitWithCheckStatuses extends GHCommitHash {
|
||||
status: { contexts: GHCommitStatusContext[] } | null;
|
||||
checkSuites: GHConnection<{ checkRuns: GHConnection<GHCheckRun> | null }> | null;
|
||||
}
|
||||
|
||||
/** `pullRequestMergeability` fragment. */
|
||||
export interface GHPullRequestMergeabilityData {
|
||||
mergeable: GHPullRequestMergeableState;
|
||||
canBeRebased: boolean;
|
||||
mergeStateStatus: GHPullRequestMergeStateStatus;
|
||||
baseRef: { refUpdateRule: GHRefUpdateRule | null } | null;
|
||||
commits: GHConnection<{ id: string; commit: GHCommitWithCheckStatuses }>;
|
||||
}
|
||||
|
||||
/** `statusCheckRollup` (`getRepositoryCommitStatusContexts`). */
|
||||
export type GHCommitStatusRollupContext =
|
||||
| {
|
||||
__typename: "CheckRun";
|
||||
checkSuite: { workflowRun: { workflow: { name: string } } | null } | null;
|
||||
name: string;
|
||||
title: string | null;
|
||||
status: string;
|
||||
conclusion: GHCommitCheckSuiteConclusion | null;
|
||||
url: string;
|
||||
}
|
||||
| {
|
||||
__typename: "StatusContext";
|
||||
context: string;
|
||||
description: string | null;
|
||||
state: GHCommitStatusContextState;
|
||||
targetUrl: string | null;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Files, reviews, threads
|
||||
|
||||
export type GHPullRequestFileViewedState = "VIEWED" | "UNVIEWED" | "DISMISSED";
|
||||
|
||||
/** `pullRequestFiles` node. */
|
||||
export interface GHPullRequestChangedFile {
|
||||
path: string;
|
||||
viewerViewedState: GHPullRequestFileViewedState;
|
||||
}
|
||||
|
||||
/** REST `pulls/{n}/files` / `commits/{sha}` entry (IntelliJ's `GHCommitFile`), camel-cased. */
|
||||
export interface GHCommitFile {
|
||||
sha: string | null;
|
||||
filename: string;
|
||||
status: "added" | "removed" | "modified" | "renamed" | "copied" | "changed" | "unchanged";
|
||||
additions: number;
|
||||
deletions: number;
|
||||
changes: number;
|
||||
/** Absent for binary and very large files. */
|
||||
patch: string | null;
|
||||
previousFilename: string | null;
|
||||
}
|
||||
|
||||
/** `pullRequestPendingReview` fragment, flattened like IntelliJ's `GHPullRequestPendingReview`. */
|
||||
export interface GHPullRequestPendingReview {
|
||||
id: string;
|
||||
state: GHPullRequestReviewState;
|
||||
commentsCount: number;
|
||||
}
|
||||
|
||||
/** `comment` fragment. */
|
||||
export interface GHComment extends GHReactable {
|
||||
id: string;
|
||||
author: GHActor | null;
|
||||
body: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export type GHPullRequestReviewCommentState = "PENDING" | "SUBMITTED";
|
||||
|
||||
/** `pullRequestReviewComment` fragment. */
|
||||
export interface GHPullRequestReviewComment extends GHComment {
|
||||
url: string;
|
||||
state: GHPullRequestReviewCommentState;
|
||||
commit: GHCommitHash | null;
|
||||
originalCommit: GHCommitHash | null;
|
||||
diffHunk: string;
|
||||
pullRequestReview: { id: string } | null;
|
||||
viewerCanDelete: boolean;
|
||||
viewerCanUpdate: boolean;
|
||||
viewerCanReact: boolean;
|
||||
}
|
||||
|
||||
export type GHDiffSide = "LEFT" | "RIGHT";
|
||||
|
||||
/** `pullRequestReviewThread` fragment. Lines are 1-based; `line` is null when outdated. */
|
||||
export interface GHPullRequestReviewThread {
|
||||
id: string;
|
||||
isResolved: boolean;
|
||||
isOutdated: boolean;
|
||||
path: string;
|
||||
subjectType: "LINE" | "FILE";
|
||||
diffSide: GHDiffSide;
|
||||
line: number | null;
|
||||
originalLine: number | null;
|
||||
startDiffSide: GHDiffSide | null;
|
||||
startLine: number | null;
|
||||
originalStartLine: number | null;
|
||||
comments: GHNodes<GHPullRequestReviewComment>;
|
||||
viewerCanReply: boolean;
|
||||
viewerCanResolve: boolean;
|
||||
viewerCanUnresolve: boolean;
|
||||
}
|
||||
|
||||
/** `DraftPullRequestReviewThread` input of `createReview`. */
|
||||
export interface GHPullRequestDraftReviewThread {
|
||||
body: string;
|
||||
path: string;
|
||||
line: number;
|
||||
side: GHDiffSide;
|
||||
startLine?: number | null;
|
||||
startSide?: GHDiffSide | null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Timeline (`pullRequestTimeline`)
|
||||
|
||||
interface GHTimelineEvent {
|
||||
actor: GHActor | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
/** `issueCommentInfo`. */
|
||||
export interface GHIssueComment extends GHComment {
|
||||
__typename: "IssueComment";
|
||||
viewerCanUpdate: boolean;
|
||||
viewerCanDelete: boolean;
|
||||
viewerCanReact: boolean;
|
||||
}
|
||||
|
||||
/** `referenceSubject`. */
|
||||
export type GHReferencedSubject =
|
||||
| {
|
||||
__typename: "PullRequest";
|
||||
title: string;
|
||||
number: number;
|
||||
prState: GHPullRequestState;
|
||||
isDraft: boolean;
|
||||
url: string;
|
||||
}
|
||||
| { __typename: "Issue"; title: string; number: number; issueState: "OPEN" | "CLOSED"; url: string };
|
||||
|
||||
export type GHPRTimelineItem =
|
||||
| { __typename: "PullRequestCommit"; id: string; commit: GHCommitShort; url: string }
|
||||
| GHPullRequestReview
|
||||
| GHIssueComment
|
||||
| ({ __typename: "RenamedTitleEvent"; previousTitle: string; currentTitle: string } & GHTimelineEvent)
|
||||
| ({ __typename: "ClosedEvent" } & GHTimelineEvent)
|
||||
| ({ __typename: "ReopenedEvent" } & GHTimelineEvent)
|
||||
| ({ __typename: "MergedEvent"; mergedEventCommit: GHCommitShort | null; mergeRefName: string } & GHTimelineEvent)
|
||||
| ({ __typename: "AssignedEvent"; user: GHUser | null } & GHTimelineEvent)
|
||||
| ({ __typename: "UnassignedEvent"; user: GHUser | null } & GHTimelineEvent)
|
||||
| ({ __typename: "LabeledEvent"; label: GHLabel } & GHTimelineEvent)
|
||||
| ({ __typename: "UnlabeledEvent"; label: GHLabel } & GHTimelineEvent)
|
||||
| ({ __typename: "ReviewRequestedEvent"; requestedReviewer: GHPullRequestRequestedReviewer | null } & GHTimelineEvent)
|
||||
| ({
|
||||
__typename: "ReviewRequestRemovedEvent";
|
||||
requestedReviewer: GHPullRequestRequestedReviewer | null;
|
||||
} & GHTimelineEvent)
|
||||
| ({
|
||||
__typename: "ReviewDismissedEvent";
|
||||
dismissalMessageHTML: string | null;
|
||||
review: { author: GHActor | null } | null;
|
||||
} & GHTimelineEvent)
|
||||
| ({ __typename: "ReadyForReviewEvent" } & GHTimelineEvent)
|
||||
| ({ __typename: "ConvertToDraftEvent" } & GHTimelineEvent)
|
||||
| ({ __typename: "BaseRefChangedEvent" } & GHTimelineEvent)
|
||||
| ({ __typename: "BaseRefForcePushedEvent"; ref: { name: string } | null } & GHTimelineEvent)
|
||||
| ({
|
||||
__typename: "HeadRefForcePushedEvent";
|
||||
ref: { name: string } | null;
|
||||
beforeCommit: GHCommitHash | null;
|
||||
afterCommit: GHCommitHash | null;
|
||||
} & GHTimelineEvent)
|
||||
| ({ __typename: "HeadRefDeletedEvent"; headRefName: string } & GHTimelineEvent)
|
||||
| ({ __typename: "HeadRefRestoredEvent" } & GHTimelineEvent)
|
||||
| ({ __typename: "CrossReferencedEvent"; source: GHReferencedSubject } & GHTimelineEvent)
|
||||
| ({ __typename: "ConnectedEvent"; subject: GHReferencedSubject } & GHTimelineEvent)
|
||||
| ({ __typename: "DisconnectedEvent"; subject: GHReferencedSubject } & GHTimelineEvent)
|
||||
/** Types the document does not select fields for (ReferencedEvent, MentionedEvent…). */
|
||||
| { __typename: "Unknown"; type: string };
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Derived state (`data/model.ts`)
|
||||
|
||||
/** IntelliJ's `GHPRReviewerState`. */
|
||||
export type GHPRReviewerState = "APPROVED" | "CHANGES_REQUESTED" | "COMMENTED" | "NEEDS_REVIEW" | "RE_REQUESTED";
|
||||
|
||||
export type CodeReviewCIJobState = "FAILED" | "PENDING" | "SKIPPED" | "SUCCESS";
|
||||
|
||||
/** IntelliJ's `CodeReviewCIJob`. */
|
||||
export interface CodeReviewCIJob {
|
||||
name: string;
|
||||
status: CodeReviewCIJobState;
|
||||
isRequired: boolean;
|
||||
detailsUrl: string | null;
|
||||
}
|
||||
|
||||
/** IntelliJ's `GHPRMergeabilityState`, plus the raw GitHub values it was built from. */
|
||||
export interface GHPRMergeabilityState {
|
||||
/** Null while GitHub is still checking (mergeable UNKNOWN). */
|
||||
hasConflicts: boolean | null;
|
||||
ciJobs: CodeReviewCIJob[];
|
||||
canBeMerged: boolean;
|
||||
canBeRebased: boolean;
|
||||
/** The viewer may not push to the base branch. */
|
||||
isRestricted: boolean;
|
||||
/** Approvals still required by branch protection (0 when nothing blocks on reviews). */
|
||||
requiredApprovingReviewsCount: number;
|
||||
/** `BLOCKING_BEHIND` / `BLOCKING_FAILING` when the merge box blocks on them, else null. */
|
||||
checksState: "BLOCKING_BEHIND" | "BLOCKING_FAILING" | null;
|
||||
mergeable: GHPullRequestMergeableState;
|
||||
mergeStateStatus: GHPullRequestMergeStateStatus;
|
||||
}
|
||||
Reference in New Issue
Block a user