@volter/twin-github 0.1.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -36
- package/client/github-mirror.css +277 -319
- package/client/github-mirror.d.ts +418 -0
- package/client/github-mirror.js +485 -0
- package/client/github-mirror.tsx +153 -357
- package/client/pulls-rest.ts +159 -0
- package/client/pulls-workspace.tsx +841 -0
- package/dist/client/github-mirror.bundle.js +239 -0
- package/dist/client/github-mirror.css +916 -0
- package/dist/client/github-mirror.d.ts +418 -0
- package/dist/client/github-mirror.js +485 -0
- package/dist/client/github-mirror.tsx +1315 -0
- package/dist/client/pulls-rest.d.ts +42 -0
- package/dist/client/pulls-rest.js +140 -0
- package/dist/client/pulls-rest.ts +159 -0
- package/dist/client/pulls-workspace.bundle.js +22 -0
- package/dist/client/pulls-workspace.d.ts +114 -0
- package/dist/client/pulls-workspace.js +418 -0
- package/dist/client/pulls-workspace.tsx +841 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +40 -0
- package/dist/src/generated/graphql-sdl.gen.json +1 -0
- package/dist/src/generated/graphql.gen.json +1 -0
- package/dist/src/generated/surface.gen.json +1 -0
- package/dist/src/generated/ui.gen.json +1 -0
- package/dist/src/github-budget.d.ts +69 -0
- package/dist/src/github-budget.js +172 -0
- package/dist/src/github-capabilities.d.ts +5 -0
- package/dist/src/github-capabilities.js +4468 -0
- package/dist/src/github-conformance.d.ts +43 -0
- package/dist/src/github-conformance.js +76 -0
- package/dist/src/github-connector.d.ts +307 -0
- package/dist/src/github-connector.js +1398 -0
- package/dist/src/github-events.d.ts +41 -0
- package/dist/src/github-events.js +230 -0
- package/dist/src/github-git-http.d.ts +49 -0
- package/dist/src/github-git-http.js +185 -0
- package/dist/src/github-git-plane.d.ts +114 -0
- package/dist/src/github-git-plane.js +407 -0
- package/dist/src/github-mirror-state.d.ts +2 -0
- package/dist/src/github-mirror-state.js +335 -0
- package/dist/src/github-mirror-ui.d.ts +20 -0
- package/dist/src/github-mirror-ui.js +101 -0
- package/dist/src/github-server.d.ts +14 -0
- package/dist/src/github-server.js +240 -0
- package/dist/src/github-shared.d.ts +12 -0
- package/dist/src/github-shared.js +21 -0
- package/dist/src/github-twin.d.ts +1411 -0
- package/dist/src/github-twin.js +4084 -0
- package/dist/src/github-ui-conformance.d.ts +4 -0
- package/dist/src/github-ui-conformance.js +105 -0
- package/dist/src/github-ui-structure.d.ts +18 -0
- package/dist/src/github-ui-structure.js +251 -0
- package/dist/src/graphql-wire.d.ts +22 -0
- package/dist/src/graphql-wire.js +89 -0
- package/dist/src/index.d.ts +17 -0
- package/dist/src/index.js +101 -0
- package/dist/src/manifest.d.ts +8 -0
- package/dist/src/manifest.js +597 -0
- package/dist/src/npm-registry.d.ts +7 -0
- package/dist/src/npm-registry.js +47 -0
- package/dist/src/screens/app-installation.d.ts +3 -0
- package/dist/src/screens/app-installation.js +166 -0
- package/dist/src/screens/app-manifest.d.ts +3 -0
- package/dist/src/screens/app-manifest.js +81 -0
- package/dist/src/screens/oauth.d.ts +15 -0
- package/dist/src/screens/oauth.js +257 -0
- package/dist/src/screens/session.d.ts +8 -0
- package/dist/src/screens/session.js +159 -0
- package/dist/src/semantics/actions.d.ts +2 -0
- package/dist/src/semantics/actions.js +413 -0
- package/dist/src/semantics/activity.d.ts +4 -0
- package/dist/src/semantics/activity.js +161 -0
- package/dist/src/semantics/apps.d.ts +2 -0
- package/dist/src/semantics/apps.js +144 -0
- package/dist/src/semantics/branches.d.ts +2 -0
- package/dist/src/semantics/branches.js +136 -0
- package/dist/src/semantics/checks.d.ts +2 -0
- package/dist/src/semantics/checks.js +176 -0
- package/dist/src/semantics/code-scanning-upload.d.ts +2 -0
- package/dist/src/semantics/code-scanning-upload.js +97 -0
- package/dist/src/semantics/codespaces.d.ts +2 -0
- package/dist/src/semantics/codespaces.js +58 -0
- package/dist/src/semantics/commits.d.ts +2 -0
- package/dist/src/semantics/commits.js +109 -0
- package/dist/src/semantics/contents.d.ts +2 -0
- package/dist/src/semantics/contents.js +131 -0
- package/dist/src/semantics/deployments.d.ts +2 -0
- package/dist/src/semantics/deployments.js +127 -0
- package/dist/src/semantics/gists.d.ts +2 -0
- package/dist/src/semantics/gists.js +86 -0
- package/dist/src/semantics/git.d.ts +2 -0
- package/dist/src/semantics/git.js +235 -0
- package/dist/src/semantics/graphql.d.ts +7 -0
- package/dist/src/semantics/graphql.js +512 -0
- package/dist/src/semantics/index.d.ts +5 -0
- package/dist/src/semantics/index.js +62 -0
- package/dist/src/semantics/issues.d.ts +2 -0
- package/dist/src/semantics/issues.js +456 -0
- package/dist/src/semantics/keys.d.ts +2 -0
- package/dist/src/semantics/keys.js +66 -0
- package/dist/src/semantics/labels.d.ts +2 -0
- package/dist/src/semantics/labels.js +100 -0
- package/dist/src/semantics/meta.d.ts +12 -0
- package/dist/src/semantics/meta.js +144 -0
- package/dist/src/semantics/notifications.d.ts +4 -0
- package/dist/src/semantics/notifications.js +62 -0
- package/dist/src/semantics/orgs.d.ts +2 -0
- package/dist/src/semantics/orgs.js +420 -0
- package/dist/src/semantics/packages.d.ts +2 -0
- package/dist/src/semantics/packages.js +55 -0
- package/dist/src/semantics/pages.d.ts +2 -0
- package/dist/src/semantics/pages.js +176 -0
- package/dist/src/semantics/projects.d.ts +2 -0
- package/dist/src/semantics/projects.js +239 -0
- package/dist/src/semantics/pulls.d.ts +2 -0
- package/dist/src/semantics/pulls.js +462 -0
- package/dist/src/semantics/push-reactions.d.ts +101 -0
- package/dist/src/semantics/push-reactions.js +513 -0
- package/dist/src/semantics/releases.d.ts +19 -0
- package/dist/src/semantics/releases.js +231 -0
- package/dist/src/semantics/repo-invitations.d.ts +2 -0
- package/dist/src/semantics/repo-invitations.js +98 -0
- package/dist/src/semantics/repos.d.ts +17 -0
- package/dist/src/semantics/repos.js +390 -0
- package/dist/src/semantics/rulesets.d.ts +2 -0
- package/dist/src/semantics/rulesets.js +111 -0
- package/dist/src/semantics/search.d.ts +2 -0
- package/dist/src/semantics/search.js +84 -0
- package/dist/src/semantics/security.d.ts +2 -0
- package/dist/src/semantics/security.js +177 -0
- package/dist/src/semantics/shared.d.ts +61 -0
- package/dist/src/semantics/shared.js +151 -0
- package/dist/src/semantics/users.d.ts +2 -0
- package/dist/src/semantics/users.js +255 -0
- package/dist/src/semantics/webhooks.d.ts +2 -0
- package/dist/src/semantics/webhooks.js +107 -0
- package/dist/test-fixtures/github-a11y-reference.pr-list.SOURCE.md +56 -0
- package/dist/test-fixtures/github-a11y-reference.pr-list.json +1447 -0
- package/dist/test-fixtures/github-comment-schema.SOURCE.md +11 -0
- package/dist/test-fixtures/github-comment-schema.json +702 -0
- package/dist/test-fixtures/github-known-deviations.json +94 -0
- package/dist/test-fixtures/github-openapi-operations.SOURCE.md +76 -0
- package/dist/test-fixtures/github-openapi-operations.json +362 -0
- package/dist/test-fixtures/github-pull-schema.SOURCE.md +37 -0
- package/dist/test-fixtures/github-pull-schema.json +3601 -0
- package/dist/test-fixtures/github-review-schema.SOURCE.md +12 -0
- package/dist/test-fixtures/github-review-schema.json +236 -0
- package/package.json +20 -11
- package/src/cli.ts +7 -6
- package/src/generated/graphql-sdl.gen.json +1 -0
- package/src/generated/graphql.gen.json +1 -0
- package/src/generated/surface.gen.json +1 -0
- package/src/generated/ui.gen.json +1 -0
- package/src/github-a11y-snapshot.uitest.ts +5 -5
- package/src/github-budget.ts +4 -4
- package/src/github-capabilities.ts +1993 -556
- package/src/github-conformance.ts +12 -7
- package/src/github-connector.ts +108 -96
- package/src/github-events.ts +223 -95
- package/src/github-git-http.ts +47 -83
- package/src/github-git-plane.ts +247 -385
- package/src/github-journey.uitest.ts +28 -44
- package/src/github-mirror-state.ts +23 -61
- package/src/github-mirror-ui.ts +16 -10
- package/src/github-server.ts +158 -60
- package/src/github-shared.ts +1 -1
- package/src/github-twin.ts +1525 -4494
- package/src/github-ui-conformance.ts +7 -8
- package/src/github-ui-structure.ts +19 -18
- package/src/graphql-wire.ts +115 -0
- package/src/index.ts +47 -7
- package/src/manifest.ts +605 -0
- package/src/npm-registry.ts +43 -0
- package/src/screens/app-installation.tsx +217 -0
- package/src/screens/app-manifest.tsx +97 -0
- package/src/screens/oauth.tsx +258 -0
- package/src/screens/session.tsx +186 -0
- package/src/semantics/actions.ts +395 -0
- package/src/semantics/activity.ts +171 -0
- package/src/semantics/apps.ts +133 -0
- package/src/semantics/branches.ts +133 -0
- package/src/semantics/checks.ts +163 -0
- package/src/semantics/code-scanning-upload.ts +92 -0
- package/src/semantics/codespaces.ts +58 -0
- package/src/semantics/commits.ts +109 -0
- package/src/semantics/contents.ts +112 -0
- package/src/semantics/deployments.ts +116 -0
- package/src/semantics/gists.ts +85 -0
- package/src/semantics/git.ts +226 -0
- package/src/semantics/graphql.ts +505 -0
- package/src/semantics/index.ts +68 -0
- package/src/semantics/issues.ts +434 -0
- package/src/semantics/keys.ts +66 -0
- package/src/semantics/labels.ts +91 -0
- package/src/semantics/meta.ts +139 -0
- package/src/semantics/notifications.ts +58 -0
- package/src/semantics/orgs.ts +383 -0
- package/src/semantics/packages.ts +59 -0
- package/src/semantics/pages.ts +154 -0
- package/src/semantics/projects.ts +246 -0
- package/src/semantics/pulls.ts +421 -0
- package/src/semantics/push-reactions.ts +471 -0
- package/src/semantics/releases.ts +213 -0
- package/src/semantics/repo-invitations.ts +112 -0
- package/src/semantics/repos.ts +373 -0
- package/src/semantics/rulesets.ts +107 -0
- package/src/semantics/search.ts +83 -0
- package/src/semantics/security.ts +153 -0
- package/src/semantics/shared.ts +181 -0
- package/src/semantics/users.ts +252 -0
- package/src/semantics/webhooks.ts +101 -0
- package/test-fixtures/github-known-deviations.json +7 -8
- package/test-fixtures/github-openapi-operations.json +46 -227
- package/src/github-graphql.ts +0 -398
|
@@ -0,0 +1,841 @@
|
|
|
1
|
+
// The pull-request WORKSPACE: GitHub's repository header, its pull request list (/{owner}/{repo}/pulls) and a pull
|
|
2
|
+
// request's page (/{owner}/{repo}/pull/{n}, with its Commits, Checks and Files changed tabs). Authored, never
|
|
3
|
+
// captured (docs/contributing/architecture.md, "Screens"): the glyphs are Primer's published Octicons (MIT), the rest
|
|
4
|
+
// is written here. Everything shown was read through GitHub's REST API (pulls-rest.ts) and every control writes
|
|
5
|
+
// through it: a comment (POST /issues/{n}/comments), a merge (PUT /pulls/{n}/merge), close and reopen
|
|
6
|
+
// (PATCH /pulls/{n} state).
|
|
7
|
+
import React, { useState } from "react";
|
|
8
|
+
import type { ChangedFile, Comment, Commit, PullRequest, Review } from "./github-mirror.tsx";
|
|
9
|
+
import type { Check, RepoFacts, StateEvent } from "./pulls-rest.ts";
|
|
10
|
+
|
|
11
|
+
// ── Octicons (16px viewBox unless noted) ────────────────────────────────────────────────────────────────────────
|
|
12
|
+
const ICONS: Record<string, string> = {
|
|
13
|
+
"mark-github": "M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z",
|
|
14
|
+
"git-pull-request": "M1.5 3.25a2.25 2.25 0 1 1 3 2.122v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.25 2.25 0 0 1 1.5 3.25Zm5.677-.177L9.573.677A.25.25 0 0 1 10 .854V2.5h1A2.5 2.5 0 0 1 13.5 5v5.628a2.251 2.251 0 1 1-1.5 0V5a1 1 0 0 0-1-1h-1v1.646a.25.25 0 0 1-.427.177L7.177 3.427a.25.25 0 0 1 0-.354ZM3.75 2.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm0 9.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm8.25.75a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0Z",
|
|
15
|
+
"git-merge": "M5.45 5.154A4.25 4.25 0 0 0 9.25 7.5h1.378a2.251 2.251 0 1 1 0 1.5H9.25A5.734 5.734 0 0 1 5 7.123v3.505a2.25 2.25 0 1 1-1.5 0V5.372a2.25 2.25 0 1 1 1.95-.218ZM4.25 13.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm8.5-4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5ZM5 3.25a.75.75 0 1 0 0 .005V3.25Z",
|
|
16
|
+
"git-pull-request-closed": "M3.25 1A2.25 2.25 0 0 1 4 5.372v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.251 2.251 0 0 1 3.25 1Zm9.5 5.5a.75.75 0 0 1 .75.75v3.378a2.251 2.251 0 1 1-1.5 0V7.25a.75.75 0 0 1 .75-.75Zm-2.03-5.273a.75.75 0 0 1 1.06 0l.97.97.97-.97a.748.748 0 0 1 1.265.332.75.75 0 0 1-.205.729l-.97.97.97.97a.751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018l-.97-.97-.97.97a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734l.97-.97-.97-.97a.75.75 0 0 1 0-1.06ZM2.5 3.25a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0ZM3.25 12a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm9.5 0a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Z",
|
|
17
|
+
"git-pull-request-draft": "M3.25 1A2.25 2.25 0 0 1 4 5.372v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.251 2.251 0 0 1 3.25 1Zm9.5 14a2.25 2.25 0 1 1 0-4.5 2.25 2.25 0 0 1 0 4.5ZM2.5 3.25a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0ZM3.25 12a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm9.5 0a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5ZM14 7.5a1.25 1.25 0 1 1-2.5 0 1.25 1.25 0 0 1 2.5 0Zm0-4.25a1.25 1.25 0 1 1-2.5 0 1.25 1.25 0 0 1 2.5 0Z",
|
|
18
|
+
check: "M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z",
|
|
19
|
+
x: "M3.72 3.72a.75.75 0 0 1 1.06 0L8 6.94l3.22-3.22a.749.749 0 0 1 1.275.326.749.749 0 0 1-.215.734L9.06 8l3.22 3.22a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215L8 9.06l-3.22 3.22a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042L6.94 8 3.72 4.78a.75.75 0 0 1 0-1.06Z",
|
|
20
|
+
"dot-fill": "M8 4a4 4 0 1 1 0 8 4 4 0 0 1 0-8Z",
|
|
21
|
+
comment: "M1 2.75C1 1.784 1.784 1 2.75 1h10.5c.966 0 1.75.784 1.75 1.75v7.5A1.75 1.75 0 0 1 13.25 12H9.06l-2.573 2.573A1.458 1.458 0 0 1 4 13.543V12H2.75A1.75 1.75 0 0 1 1 10.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h4.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z",
|
|
22
|
+
"comment-discussion": "M1.75 1h8.5c.966 0 1.75.784 1.75 1.75v5.5A1.75 1.75 0 0 1 10.25 10H7.061l-2.574 2.573A1.458 1.458 0 0 1 2 11.543V10h-.25A1.75 1.75 0 0 1 0 8.25v-5.5C0 1.784.784 1 1.75 1ZM1.5 2.75v5.5c0 .138.112.25.25.25h1a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h3.5a.25.25 0 0 0 .25-.25v-5.5a.25.25 0 0 0-.25-.25h-8.5a.25.25 0 0 0-.25.25Zm13 2a.25.25 0 0 0-.25-.25h-.5a.75.75 0 0 1 0-1.5h.5c.966 0 1.75.784 1.75 1.75v5.5A1.75 1.75 0 0 1 14.25 12H14v1.543a1.458 1.458 0 0 1-2.487 1.03L9.22 12.28a.749.749 0 0 1 .326-1.275.749.749 0 0 1 .734.215l2.22 2.22v-2.19a.75.75 0 0 1 .75-.75h1a.25.25 0 0 0 .25-.25Z",
|
|
23
|
+
code: "m11.28 3.22 4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734L13.94 8l-3.72-3.72a.749.749 0 0 1 .326-1.275.749.749 0 0 1 .734.215Zm-6.56 0a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042L2.06 8l3.72 3.72a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215L.47 8.53a.75.75 0 0 1 0-1.06Z",
|
|
24
|
+
"issue-opened": "M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Z",
|
|
25
|
+
play: "M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Zm4.879-2.773 4.264 2.559a.25.25 0 0 1 0 .428l-4.264 2.559A.25.25 0 0 1 6 10.559V5.442a.25.25 0 0 1 .379-.215Z",
|
|
26
|
+
table: "M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v12.5A1.75 1.75 0 0 1 14.25 16H1.75A1.75 1.75 0 0 1 0 14.25ZM6.5 6.5v8h7.75a.25.25 0 0 0 .25-.25V6.5Zm8-1.5V1.75a.25.25 0 0 0-.25-.25H6.5V5Zm-13 1.5v7.75c0 .138.112.25.25.25H5v-8ZM5 5V1.5H1.75a.25.25 0 0 0-.25.25V5Z",
|
|
27
|
+
shield: "M7.467.133a1.748 1.748 0 0 1 1.066 0l5.25 1.68A1.75 1.75 0 0 1 15 3.48V7c0 1.566-.32 3.182-1.303 4.682-.983 1.498-2.585 2.813-5.032 3.855a1.697 1.697 0 0 1-1.33 0c-2.447-1.042-4.049-2.357-5.032-3.855C1.32 10.182 1 8.566 1 7V3.48a1.755 1.755 0 0 1 1.217-1.667Zm.61 1.429a.25.25 0 0 0-.153 0l-5.25 1.68a.25.25 0 0 0-.174.238V7c0 1.358.275 2.666 1.057 3.86.784 1.194 2.121 2.34 4.366 3.297a.196.196 0 0 0 .154 0c2.245-.956 3.582-2.104 4.366-3.298C13.225 9.666 13.5 8.36 13.5 7V3.48a.251.251 0 0 0-.174-.237l-5.25-1.68Z",
|
|
28
|
+
graph: "M1.5 1.75V13.5h13.75a.75.75 0 0 1 0 1.5H.75a.75.75 0 0 1-.75-.75V1.75a.75.75 0 0 1 1.5 0Zm14.28 2.53-5.25 5.25a.75.75 0 0 1-1.06 0L7 7.06 4.28 9.78a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042l3.25-3.25a.75.75 0 0 1 1.06 0L10 7.94l4.72-4.72a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042Z",
|
|
29
|
+
gear: "M8 0a8.2 8.2 0 0 1 .701.031C9.444.095 9.99.645 10.16 1.29l.288 1.107c.018.066.079.158.212.224.231.114.454.243.668.386.123.082.233.09.299.071l1.103-.303c.644-.176 1.392.021 1.82.63.27.385.506.792.704 1.218.315.675.111 1.422-.364 1.891l-.814.806c-.049.048-.098.147-.088.294.016.257.016.515 0 .772-.01.147.038.246.088.294l.814.806c.475.469.679 1.216.364 1.891a7.977 7.977 0 0 1-.704 1.217c-.428.61-1.176.807-1.82.63l-1.102-.302c-.067-.019-.177-.011-.3.071a5.909 5.909 0 0 1-.668.386c-.133.066-.194.158-.211.224l-.29 1.106c-.168.646-.715 1.196-1.458 1.26a8.006 8.006 0 0 1-1.402 0c-.743-.064-1.289-.614-1.458-1.26l-.289-1.106c-.018-.066-.079-.158-.212-.224a5.738 5.738 0 0 1-.668-.386c-.123-.082-.233-.09-.299-.071l-1.103.303c-.644.176-1.392-.021-1.82-.63a8.12 8.12 0 0 1-.704-1.218c-.315-.675-.111-1.422.363-1.891l.815-.806c.05-.048.098-.147.088-.294a6.214 6.214 0 0 1 0-.772c.01-.147-.038-.246-.088-.294l-.815-.806C.635 6.045.431 5.298.746 4.623a7.92 7.92 0 0 1 .704-1.217c.428-.61 1.176-.807 1.82-.63l1.102.302c.067.019.177.011.3-.071.214-.143.437-.272.668-.386.133-.066.194-.158.211-.224l.29-1.106C6.009.645 6.556.095 7.299.03 7.53.01 7.764 0 8 0Zm-.571 1.525c-.036.003-.108.036-.137.146l-.289 1.105c-.147.561-.549.967-.998 1.189-.173.086-.34.183-.5.29-.417.278-.97.423-1.529.27l-1.103-.303c-.109-.03-.175.016-.195.045-.22.312-.412.644-.573.99-.014.031-.021.11.059.19l.815.806c.411.406.562.957.53 1.456a4.709 4.709 0 0 0 0 .582c.032.499-.119 1.05-.53 1.456l-.815.806c-.081.08-.073.159-.059.19.162.346.353.677.573.989.02.03.085.076.195.046l1.102-.303c.56-.153 1.113-.008 1.53.27.161.107.328.204.501.29.447.222.85.629.997 1.189l.289 1.105c.029.109.101.143.137.146a6.6 6.6 0 0 0 1.142 0c.036-.003.108-.036.137-.146l.289-1.105c.147-.561.549-.967.998-1.189.173-.086.34-.183.5-.29.417-.278.97-.423 1.529-.27l1.103.303c.109.029.175-.016.195-.045.22-.313.411-.644.573-.99.014-.031.021-.11-.059-.19l-.815-.806c-.411-.406-.562-.957-.53-1.456a4.709 4.709 0 0 0 0-.582c-.032-.499.119-1.05.53-1.456l.815-.806c.081-.08.073-.159.059-.19a6.464 6.464 0 0 0-.573-.989c-.02-.03-.085-.076-.195-.046l-1.102.303c-.56.153-1.113.008-1.53-.27a4.44 4.44 0 0 0-.501-.29c-.447-.222-.85-.629-.997-1.189l-.289-1.105c-.029-.11-.101-.143-.137-.146a6.6 6.6 0 0 0-1.142 0ZM11 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0ZM9.5 8a1.5 1.5 0 1 0-3.001.001A1.5 1.5 0 0 0 9.5 8Z",
|
|
30
|
+
"git-commit": "M11.93 8.5a4.002 4.002 0 0 1-7.86 0H.75a.75.75 0 0 1 0-1.5h3.32a4.002 4.002 0 0 1 7.86 0h3.32a.75.75 0 0 1 0 1.5Zm-1.43-.75a2.5 2.5 0 1 0-5 0 2.5 2.5 0 0 0 5 0Z",
|
|
31
|
+
"file-diff": "M1 1.75C1 .784 1.784 0 2.75 0h7.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16H2.75A1.75 1.75 0 0 1 1 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25V4.664a.25.25 0 0 0-.073-.177l-2.914-2.914a.25.25 0 0 0-.177-.073ZM8 3.25a.75.75 0 0 1 .75.75v1.5h1.5a.75.75 0 0 1 0 1.5h-1.5v1.5a.75.75 0 0 1-1.5 0V7h-1.5a.75.75 0 0 1 0-1.5h1.5V4A.75.75 0 0 1 8 3.25Zm-3 8a.75.75 0 0 1 .75-.75h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1-.75-.75Z",
|
|
32
|
+
checklist: "M2.5 1.75v11.5c0 .138.112.25.25.25h3.17a.75.75 0 0 1 0 1.5H2.75A1.75 1.75 0 0 1 1 13.25V1.75C1 .784 1.784 0 2.75 0h8.5C12.216 0 13 .784 13 1.75v7.736a.75.75 0 0 1-1.5 0V1.75a.25.25 0 0 0-.25-.25h-8.5a.25.25 0 0 0-.25.25Zm13.274 9.537v-.001l-4.557 4.45a.75.75 0 0 1-1.055-.008l-1.943-1.95a.75.75 0 0 1 1.062-1.058l1.419 1.425 4.026-3.932a.75.75 0 1 1 1.048 1.074ZM4.75 4h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1 0-1.5ZM4 7.75A.75.75 0 0 1 4.75 7h2a.75.75 0 0 1 0 1.5h-2A.75.75 0 0 1 4 7.75Z",
|
|
33
|
+
eye: "M8 2c1.981 0 3.671.992 4.933 2.078 1.27 1.091 2.187 2.345 2.637 3.023a1.62 1.62 0 0 1 0 1.798c-.45.678-1.367 1.932-2.637 3.023C11.67 13.008 9.981 14 8 14c-1.981 0-3.671-.992-4.933-2.078C1.797 10.83.88 9.576.43 8.898a1.62 1.62 0 0 1 0-1.798c.45-.677 1.367-1.931 2.637-3.022C4.33 2.992 6.019 2 8 2ZM1.679 7.932a.12.12 0 0 0 0 .136c.411.622 1.241 1.75 2.366 2.717C5.176 11.758 6.527 12.5 8 12.5c1.473 0 2.825-.742 3.955-1.715 1.124-.967 1.954-2.096 2.366-2.717a.12.12 0 0 0 0-.136c-.412-.621-1.242-1.75-2.366-2.717C10.824 4.242 9.473 3.5 8 3.5c-1.473 0-2.825.742-3.955 1.715-1.124.967-1.954 2.096-2.366 2.717ZM8 10a2 2 0 1 1-.001-3.999A2 2 0 0 1 8 10Z",
|
|
34
|
+
search: "M10.68 11.74a6 6 0 0 1-7.922-8.982 6 6 0 0 1 8.982 7.922l3.04 3.04a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215ZM11.5 7a4.499 4.499 0 1 0-8.997 0A4.499 4.499 0 0 0 11.5 7Z",
|
|
35
|
+
"three-bars": "M1 2.75A.75.75 0 0 1 1.75 2h12.5a.75.75 0 0 1 0 1.5H1.75A.75.75 0 0 1 1 2.75Zm0 5A.75.75 0 0 1 1.75 7h12.5a.75.75 0 0 1 0 1.5H1.75A.75.75 0 0 1 1 7.75ZM1.75 12h12.5a.75.75 0 0 1 0 1.5H1.75a.75.75 0 0 1 0-1.5Z",
|
|
36
|
+
inbox: "M2.8 2.06A1.75 1.75 0 0 1 4.41 1h7.18c.7 0 1.333.417 1.61 1.06l2.74 6.395c.04.093.06.194.06.295v4.5A1.75 1.75 0 0 1 14.25 15H1.75A1.75 1.75 0 0 1 0 13.25v-4.5c0-.101.02-.202.06-.295Zm1.61.44a.25.25 0 0 0-.23.152L1.887 8H4.75a.75.75 0 0 1 .6.3L6.625 10h2.75l1.275-1.7a.75.75 0 0 1 .6-.3h2.863L11.82 2.652a.25.25 0 0 0-.23-.152Zm10.09 7h-2.875l-1.275 1.7a.75.75 0 0 1-.6.3h-3.5a.75.75 0 0 1-.6-.3L4.375 9.5H1.5v3.75c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25Z",
|
|
37
|
+
plus: "M7.75 2a.75.75 0 0 1 .75.75V7h4.25a.75.75 0 0 1 0 1.5H8.5v4.25a.75.75 0 0 1-1.5 0V8.5H2.75a.75.75 0 0 1 0-1.5H7V2.75A.75.75 0 0 1 7.75 2Z",
|
|
38
|
+
"triangle-down": "m4.427 7.427 3.396 3.396a.25.25 0 0 0 .354 0l3.396-3.396A.25.25 0 0 0 11.396 7H4.604a.25.25 0 0 0-.177.427Z",
|
|
39
|
+
"chevron-down": "M12.78 5.22a.749.749 0 0 1 0 1.06l-4.25 4.25a.749.749 0 0 1-1.06 0L3.22 6.28a.749.749 0 1 1 1.06-1.06L8 8.939l3.72-3.719a.749.749 0 0 1 1.06 0Z",
|
|
40
|
+
tag: "M1 7.775V2.75C1 1.784 1.784 1 2.75 1h5.025c.464 0 .91.184 1.238.513l6.25 6.25a1.75 1.75 0 0 1 0 2.474l-5.026 5.026a1.75 1.75 0 0 1-2.474 0l-6.25-6.25A1.752 1.752 0 0 1 1 7.775Zm1.5 0c0 .066.026.13.073.177l6.25 6.25a.25.25 0 0 0 .354 0l5.025-5.025a.25.25 0 0 0 0-.354l-6.25-6.25a.25.25 0 0 0-.177-.073H2.75a.25.25 0 0 0-.25.25ZM6 5a1 1 0 1 1 0 2 1 1 0 0 1 0-2Z",
|
|
41
|
+
milestone: "M7.75 0a.75.75 0 0 1 .75.75V3h3.634c.414 0 .814.147 1.13.414l2.07 1.75a1.75 1.75 0 0 1 0 2.672l-2.07 1.75a1.75 1.75 0 0 1-1.13.414H8.5v5.25a.75.75 0 0 1-1.5 0V10H2.75A1.75 1.75 0 0 1 1 8.25v-3.5C1 3.784 1.784 3 2.75 3H7V.75A.75.75 0 0 1 7.75 0Zm4.384 8.5a.25.25 0 0 0 .161-.06l2.07-1.75a.248.248 0 0 0 0-.38l-2.07-1.75a.25.25 0 0 0-.161-.06H2.75a.25.25 0 0 0-.25.25v3.5c0 .138.112.25.25.25h9.384Z",
|
|
42
|
+
copy: "M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Zm5-5C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z",
|
|
43
|
+
"kebab-horizontal": "M8 9a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM1.5 9a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Zm13 0a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z",
|
|
44
|
+
"skip": "M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Z",
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export function Octicon({ name, size = 16, className }: { name: string; size?: number; className?: string }) {
|
|
48
|
+
return (
|
|
49
|
+
<svg className={`octicon octicon-${name}${className ? ` ${className}` : ""}`} viewBox="0 0 16 16" width={size} height={size} aria-hidden="true" fill="currentColor">
|
|
50
|
+
<path d={ICONS[name] ?? ""} />
|
|
51
|
+
</svg>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── Small shared pieces ─────────────────────────────────────────────────────────────────────────────────────────
|
|
56
|
+
function hashOf(text: string): number {
|
|
57
|
+
let h = 2166136261;
|
|
58
|
+
for (let i = 0; i < text.length; i++) h = Math.imul(h ^ text.charCodeAt(i), 16777619);
|
|
59
|
+
return h >>> 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** A person's default picture, as GitHub draws one for an account without an upload: a 5x5 mirrored identicon. */
|
|
63
|
+
export function Avatar({ login, size = 20, src }: { login?: string; size?: number; src?: string }) {
|
|
64
|
+
if (src) return <img className="gh-avatar" src={src} width={size} height={size} alt={`@${login ?? ""}`} />;
|
|
65
|
+
const h = hashOf(login || "ghost");
|
|
66
|
+
const hue = h % 360;
|
|
67
|
+
const cells: React.ReactNode[] = [];
|
|
68
|
+
for (let y = 0; y < 5; y++) for (let x = 0; x < 3; x++) {
|
|
69
|
+
if ((h >> (y * 3 + x)) & 1) {
|
|
70
|
+
cells.push(<rect key={`${x}-${y}`} x={1 + x} y={1 + y} width={1} height={1} />);
|
|
71
|
+
if (x < 2) cells.push(<rect key={`${4 - x}-${y}m`} x={5 - x} y={1 + y} width={1} height={1} />);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return (
|
|
75
|
+
<svg className="gh-avatar" width={size} height={size} viewBox="0 0 7 7" role="img" aria-label={`@${login ?? "ghost"}`} shapeRendering="crispEdges">
|
|
76
|
+
<rect width="7" height="7" fill="#f0f0f0" />
|
|
77
|
+
<g fill={`hsl(${hue} 55% 58%)`}>{cells}</g>
|
|
78
|
+
</svg>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** GitHub's relative time: "now", "3 minutes ago", "yesterday", then "on Sep 24". */
|
|
83
|
+
export function timeAgo(iso?: string, now = Date.now()): string {
|
|
84
|
+
if (!iso) return "";
|
|
85
|
+
const t = Date.parse(iso);
|
|
86
|
+
if (!Number.isFinite(t)) return "";
|
|
87
|
+
const s = Math.max(0, Math.round((now - t) / 1000));
|
|
88
|
+
if (s < 45) return "now";
|
|
89
|
+
const m = Math.round(s / 60);
|
|
90
|
+
if (m < 60) return m === 1 ? "1 minute ago" : `${m} minutes ago`;
|
|
91
|
+
const h = Math.round(m / 60);
|
|
92
|
+
if (h < 24) return h === 1 ? "1 hour ago" : `${h} hours ago`;
|
|
93
|
+
const d = Math.round(h / 24);
|
|
94
|
+
if (d === 1) return "yesterday";
|
|
95
|
+
if (d < 30) return `${d} days ago`;
|
|
96
|
+
return `on ${new Date(t).toLocaleDateString("en-US", { month: "short", day: "numeric", ...(new Date(t).getFullYear() !== new Date(now).getFullYear() ? { year: "numeric" } : {}) })}`;
|
|
97
|
+
}
|
|
98
|
+
function RelativeTime({ iso }: { iso?: string }) {
|
|
99
|
+
return <time dateTime={iso} title={iso ? new Date(iso).toLocaleString("en-US", { dateStyle: "medium", timeStyle: "short" }) : undefined}>{timeAgo(iso)}</time>;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** A label pill in the label's own colour, its text dark or light by the colour's lightness. */
|
|
103
|
+
export function Label({ name, color }: { name: string; color?: string }) {
|
|
104
|
+
const hex = (color || "ededed").replace(/^#/, "");
|
|
105
|
+
const [r, g, b] = [0, 2, 4].map((i) => parseInt(hex.slice(i, i + 2), 16) || 0);
|
|
106
|
+
const light = (r! * 299 + g! * 587 + b! * 114) / 1000 > 150;
|
|
107
|
+
return <span className="gh-label" style={{ background: `#${hex}`, color: light ? "#1f2328" : "#ffffff" }}>{name}</span>;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** A body as GitHub renders its Markdown, for what a comment ordinarily holds: paragraphs, inline code, bold, links. */
|
|
111
|
+
function Markdown({ text }: { text?: string }) {
|
|
112
|
+
const inline = (line: string, key: string): React.ReactNode[] => line.split(/(`[^`]+`|\*\*[^*]+\*\*|https?:\/\/\S+)/g).map((part, i) => {
|
|
113
|
+
if (/^`[^`]+`$/.test(part)) return <code key={`${key}-${i}`}>{part.slice(1, -1)}</code>;
|
|
114
|
+
if (/^\*\*[^*]+\*\*$/.test(part)) return <strong key={`${key}-${i}`}>{part.slice(2, -2)}</strong>;
|
|
115
|
+
if (/^https?:\/\//.test(part)) return <a key={`${key}-${i}`} href={part} target="_blank" rel="noreferrer">{part}</a>;
|
|
116
|
+
return part;
|
|
117
|
+
});
|
|
118
|
+
const paragraphs = (text ?? "").trim().split(/\n{2,}/).filter(Boolean);
|
|
119
|
+
if (!paragraphs.length) return <p className="gh-muted"><em>No description provided.</em></p>;
|
|
120
|
+
return <>{paragraphs.map((p, i) => <p key={i}>{p.split("\n").flatMap((line, j) => (j ? [<br key={`br-${i}-${j}`} />] : []).concat(inline(line, `${i}-${j}`) as any))}</p>)}</>;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
type PullState = "open" | "merged" | "closed" | "draft";
|
|
124
|
+
export function pullState(pr: PullRequest): PullState {
|
|
125
|
+
if (pr.merged) return "merged";
|
|
126
|
+
if (pr.state === "closed") return "closed";
|
|
127
|
+
return pr.draft ? "draft" : "open";
|
|
128
|
+
}
|
|
129
|
+
const STATE_ICON: Record<PullState, string> = { open: "git-pull-request", merged: "git-merge", closed: "git-pull-request-closed", draft: "git-pull-request-draft" };
|
|
130
|
+
const STATE_WORD: Record<PullState, string> = { open: "Open", merged: "Merged", closed: "Closed", draft: "Draft" };
|
|
131
|
+
|
|
132
|
+
export function StateBadge({ pr }: { pr: PullRequest }) {
|
|
133
|
+
const s = pullState(pr);
|
|
134
|
+
return <span className={`state-badge gh-state gh-state-${s}`}><Octicon name={STATE_ICON[s]} />{STATE_WORD[s]}</span>;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const CI_ICON: Record<string, string> = { success: "check", failure: "x", error: "x", pending: "dot-fill" };
|
|
138
|
+
/** The CI rollup beside a title: a green check, a red cross or an amber dot. */
|
|
139
|
+
export function CiStatus({ status }: { status?: string }) {
|
|
140
|
+
if (!status) return null;
|
|
141
|
+
const word = status === "success" ? "All checks have passed" : status === "pending" ? "Some checks haven't completed yet" : "Some checks were not successful";
|
|
142
|
+
return <span className={`ci-status ${status}`} title={`checks: ${status}`} aria-label={word}><Octicon name={CI_ICON[status] ?? "dot-fill"} /></span>;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** What the latest review by each reviewer says, as GitHub decides a pull request's review state. */
|
|
146
|
+
export function reviewDecision(pr: PullRequest, reviews: Review[]): "approved" | "changes_requested" | "review_required" | undefined {
|
|
147
|
+
const latest = new Map<string, string>();
|
|
148
|
+
for (const r of reviews) {
|
|
149
|
+
if (r.repo !== pr.repo || Number(r.number) !== Number(pr.number) || !r.reviewer) continue;
|
|
150
|
+
const st = String(r.state ?? "").toLowerCase();
|
|
151
|
+
if (st === "approved" || st === "changes_requested" || st === "dismissed") latest.set(r.reviewer, st);
|
|
152
|
+
}
|
|
153
|
+
const states = [...latest.values()];
|
|
154
|
+
if (states.includes("changes_requested")) return "changes_requested";
|
|
155
|
+
if (states.includes("approved")) return "approved";
|
|
156
|
+
return (pr.requestedReviewers ?? []).length ? "review_required" : undefined;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── The repository header ──────────────────────────────────────────────────────────────────────────────────────
|
|
160
|
+
export type RepoTab = "code" | "issues" | "pulls" | "actions" | "projects" | "security" | "insights" | "settings" | "discussions";
|
|
161
|
+
|
|
162
|
+
export function RepoHeader({ repo, viewer, active, facts, openPulls, projects, onTab, onHome, onInbox, onSearch }: {
|
|
163
|
+
repo: string; viewer?: string; active?: RepoTab; facts?: RepoFacts; openPulls: number; projects: number;
|
|
164
|
+
onTab: (tab: RepoTab) => void; onHome: () => void; onInbox: () => void; onSearch: () => void;
|
|
165
|
+
}) {
|
|
166
|
+
const all = repo === "all" || !repo;
|
|
167
|
+
const [owner, name] = all ? ["", ""] : repo.split("/");
|
|
168
|
+
const tabs: Array<{ id: RepoTab; label: string; icon: string; count?: number }> = [
|
|
169
|
+
{ id: "code", label: "Code", icon: "code" },
|
|
170
|
+
{ id: "issues", label: "Issues", icon: "issue-opened", count: facts?.openIssues ?? 0 },
|
|
171
|
+
{ id: "pulls", label: "Pull requests", icon: "git-pull-request", count: openPulls },
|
|
172
|
+
...(facts?.hasDiscussions ? [{ id: "discussions" as RepoTab, label: "Discussions", icon: "comment-discussion" }] : []),
|
|
173
|
+
{ id: "actions", label: "Actions", icon: "play" },
|
|
174
|
+
{ id: "projects", label: "Projects", icon: "table", ...(projects ? { count: projects } : {}) },
|
|
175
|
+
{ id: "security", label: "Security", icon: "shield" },
|
|
176
|
+
{ id: "insights", label: "Insights", icon: "graph" },
|
|
177
|
+
{ id: "settings", label: "Settings", icon: "gear" },
|
|
178
|
+
];
|
|
179
|
+
return (
|
|
180
|
+
<header className="gh-header">
|
|
181
|
+
<div className="gh-globalbar">
|
|
182
|
+
<button className="gh-iconbtn" aria-label="Open global navigation menu"><Octicon name="three-bars" /></button>
|
|
183
|
+
<a className="gh-logo" href="/" aria-label="Homepage" onClick={(e) => { e.preventDefault(); onHome(); }}><Octicon name="mark-github" size={32} /></a>
|
|
184
|
+
<nav className="gh-context" aria-label="Page context">
|
|
185
|
+
{all ? <strong>Pull requests</strong> : (<>
|
|
186
|
+
<a href={`/${owner}`} onClick={(e) => e.preventDefault()}>{owner}</a>
|
|
187
|
+
<span className="gh-context-sep">/</span>
|
|
188
|
+
<a href={`/${repo}`} onClick={(e) => { e.preventDefault(); onTab("code"); }}><strong>{name}</strong></a>
|
|
189
|
+
</>)}
|
|
190
|
+
</nav>
|
|
191
|
+
<div className="gh-spacer" />
|
|
192
|
+
<button className="gh-search" onClick={onSearch}><Octicon name="search" /><span>Type <kbd>/</kbd> to search</span></button>
|
|
193
|
+
<span className="gh-divider" />
|
|
194
|
+
<button className="gh-iconbtn gh-iconbtn-wide" aria-label="Create something new"><Octicon name="plus" /><Octicon name="triangle-down" /></button>
|
|
195
|
+
<button className="gh-iconbtn" aria-label="Your issues" onClick={() => onTab("issues")}><Octicon name="issue-opened" /></button>
|
|
196
|
+
<button className="gh-iconbtn" aria-label="Your pull requests" onClick={onHome}><Octicon name="git-pull-request" /></button>
|
|
197
|
+
<button className="gh-iconbtn" aria-label="You have no unread notifications" onClick={onInbox}><Octicon name="inbox" /></button>
|
|
198
|
+
<span className="gh-viewer"><Avatar login={viewer} size={32} /></span>
|
|
199
|
+
</div>
|
|
200
|
+
{all ? null : (
|
|
201
|
+
<nav className="gh-localbar" aria-label="Repository">
|
|
202
|
+
{tabs.map((t) => (
|
|
203
|
+
<a key={t.id} href={`/${repo}${t.id === "code" ? "" : `/${t.id}`}`} className={`gh-tab${active === t.id ? " selected" : ""}`} aria-current={active === t.id ? "page" : undefined}
|
|
204
|
+
onClick={(e) => { e.preventDefault(); onTab(t.id); }}>
|
|
205
|
+
<Octicon name={t.icon} />
|
|
206
|
+
<span>{t.label}</span>
|
|
207
|
+
{t.count ? <span className="Counter">{t.count}</span> : null}
|
|
208
|
+
</a>
|
|
209
|
+
))}
|
|
210
|
+
</nav>
|
|
211
|
+
)}
|
|
212
|
+
</header>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** The page footer every GitHub page ends with. */
|
|
217
|
+
export function SiteFooter() {
|
|
218
|
+
return (
|
|
219
|
+
<footer className="gh-footer">
|
|
220
|
+
<span className="gh-footer-brand"><Octicon name="mark-github" size={24} /> © {new Date().getFullYear()} GitHub, Inc.</span>
|
|
221
|
+
<ul>{["Terms", "Privacy", "Security", "Status", "Community", "Docs", "Contact", "Manage cookies", "Do not share my personal information"].map((t) => <li key={t}>{t}</li>)}</ul>
|
|
222
|
+
</footer>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ── The list page ──────────────────────────────────────────────────────────────────────────────────────────────
|
|
227
|
+
function rowMeta(pr: PullRequest): React.ReactNode {
|
|
228
|
+
const s = pullState(pr);
|
|
229
|
+
if (s === "merged") return <>#{pr.number} by <a className="gh-muted-link">{pr.author}</a> was merged <RelativeTime iso={pr.mergedAt} /></>;
|
|
230
|
+
if (s === "closed") return <>#{pr.number} by <a className="gh-muted-link">{pr.author}</a> was closed <RelativeTime iso={pr.closedAt ?? pr.updatedAt} /></>;
|
|
231
|
+
return <>#{pr.number} opened <RelativeTime iso={pr.createdAt} /> by <a className="gh-muted-link">{pr.author}</a>{s === "draft" ? " • Draft" : ""}</>;
|
|
232
|
+
}
|
|
233
|
+
const DECISION_WORD = { approved: "Approved", changes_requested: "Changes requested", review_required: "Review required" } as const;
|
|
234
|
+
|
|
235
|
+
/** The list's rows: each pull request's state icon, title, CI, labels, meta line, assignees and comment count. */
|
|
236
|
+
export function PrList({ prs, reviews, labelColors = {}, showRepo, onSelect }: {
|
|
237
|
+
prs: PullRequest[]; reviews: Review[]; labelColors?: Record<string, string>; showRepo?: boolean; selectedKey?: string; onSelect?: (key: string) => void;
|
|
238
|
+
}) {
|
|
239
|
+
return (
|
|
240
|
+
<div className="gh-rows">
|
|
241
|
+
{prs.map((pr) => {
|
|
242
|
+
const s = pullState(pr);
|
|
243
|
+
const decision = s === "open" || s === "draft" ? reviewDecision(pr, reviews) : undefined;
|
|
244
|
+
const comments = pr.commentsCount ?? 0;
|
|
245
|
+
return (
|
|
246
|
+
<div key={`${pr.repo}#${pr.number}`} className="pr-row" data-number={pr.number}>
|
|
247
|
+
<span className={`pr-state-icon ${s}`} aria-label={`${STATE_WORD[s]} pull request`}><Octicon name={STATE_ICON[s]} /></span>
|
|
248
|
+
<div className="pr-row-main">
|
|
249
|
+
<div className="pr-row-title-line">
|
|
250
|
+
{showRepo ? <span className="pr-row-repo">{pr.repo}</span> : null}
|
|
251
|
+
<a className="pr-row-title" href={`/${pr.repo}/pull/${pr.number}`} onClick={(e) => { e.preventDefault(); onSelect?.(`${pr.repo}#${pr.number}`); }}>{pr.title}</a>
|
|
252
|
+
<CiStatus status={pr.ciStatus} />
|
|
253
|
+
{(pr.labels ?? []).map((l) => <Label key={l.name} name={l.name} color={labelColors[`${pr.repo}:${l.name}`] ?? l.color} />)}
|
|
254
|
+
</div>
|
|
255
|
+
<div className="pr-row-meta">
|
|
256
|
+
{rowMeta(pr)}
|
|
257
|
+
{decision ? <> • <a className={`gh-muted-link decision-${decision}`}>{DECISION_WORD[decision]}</a></> : null}
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
260
|
+
<div className="pr-row-side">
|
|
261
|
+
<span className="pr-row-assignees">{(pr.assignees ?? []).map((a) => <Avatar key={a} login={a} size={20} />)}</span>
|
|
262
|
+
<span className="pr-row-comments">{comments ? <a className="gh-muted-link" aria-label={`${comments} comments`}><Octicon name="comment" /> {comments}</a> : null}</span>
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
);
|
|
266
|
+
})}
|
|
267
|
+
</div>
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function PullsListPage({ repo, prs, reviews, facts, listState, onListState, onSelect }: {
|
|
272
|
+
repo: string; prs: PullRequest[]; reviews: Review[]; facts?: RepoFacts; listState: "open" | "closed";
|
|
273
|
+
onListState: (s: "open" | "closed") => void; onSelect: (key: string) => void;
|
|
274
|
+
}) {
|
|
275
|
+
const open = prs.filter((p) => pullState(p) === "open" || pullState(p) === "draft");
|
|
276
|
+
const closed = prs.filter((p) => pullState(p) === "merged" || pullState(p) === "closed");
|
|
277
|
+
const shown = listState === "open" ? open : closed;
|
|
278
|
+
const labelColors = Object.fromEntries((facts?.labels ?? []).map((l) => [`${repo}:${l.name}`, l.color ?? "ededed"]));
|
|
279
|
+
for (const p of prs) for (const l of p.labels ?? []) if (l.color && !labelColors[`${p.repo}:${l.name}`]) labelColors[`${p.repo}:${l.name}`] = l.color;
|
|
280
|
+
const all = repo === "all" || !repo;
|
|
281
|
+
return (
|
|
282
|
+
<div className="gh-container gh-list-page">
|
|
283
|
+
<div className="gh-subnav">
|
|
284
|
+
<div className="gh-filter">
|
|
285
|
+
<button className="gh-btn gh-filter-btn">Filters <Octicon name="triangle-down" /></button>
|
|
286
|
+
<label className="gh-filter-input"><Octicon name="search" /><input aria-label="Search all pull requests" value={`is:pr is:${listState} `} readOnly /></label>
|
|
287
|
+
</div>
|
|
288
|
+
{all ? null : (
|
|
289
|
+
<div className="gh-subnav-links">
|
|
290
|
+
<a className="gh-subnav-item"><Octicon name="tag" /> Labels <span className="Counter">{facts?.labels.length ?? 0}</span></a>
|
|
291
|
+
<a className="gh-subnav-item"><Octicon name="milestone" /> Milestones <span className="Counter">{facts?.milestones ?? 0}</span></a>
|
|
292
|
+
</div>
|
|
293
|
+
)}
|
|
294
|
+
{all ? null : <button className="gh-btn gh-btn-primary">New pull request</button>}
|
|
295
|
+
</div>
|
|
296
|
+
<div className="gh-box">
|
|
297
|
+
<div className="gh-box-header">
|
|
298
|
+
<div className="gh-states" role="group" aria-label="Pull request state">
|
|
299
|
+
<button className={`gh-states-link${listState === "open" ? " selected" : ""}`} aria-pressed={listState === "open"} onClick={() => onListState("open")}>
|
|
300
|
+
<Octicon name="git-pull-request" /> {open.length} Open
|
|
301
|
+
</button>
|
|
302
|
+
<button className={`gh-states-link${listState === "closed" ? " selected" : ""}`} aria-pressed={listState === "closed"} onClick={() => onListState("closed")}>
|
|
303
|
+
<Octicon name="check" /> {closed.length} Closed
|
|
304
|
+
</button>
|
|
305
|
+
</div>
|
|
306
|
+
<div className="gh-box-filters">
|
|
307
|
+
{["Author", "Label", "Projects", "Milestones", "Reviews", "Assignee", "Sort"].map((f) => <span key={f} className="gh-select-menu">{f} <Octicon name="triangle-down" /></span>)}
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
{shown.length ? <PrList prs={shown} reviews={reviews} labelColors={labelColors} showRepo={all} onSelect={onSelect} /> : (
|
|
311
|
+
<div className="gh-blankslate">
|
|
312
|
+
<Octicon name="git-pull-request" size={24} />
|
|
313
|
+
<h3>{listState === "open" ? "There aren’t any open pull requests." : "There aren’t any closed pull requests."}</h3>
|
|
314
|
+
<p>You could search <a>all of GitHub</a> or try an <a>advanced search</a>.</p>
|
|
315
|
+
</div>
|
|
316
|
+
)}
|
|
317
|
+
</div>
|
|
318
|
+
<p className="gh-protip"><strong>ProTip!</strong> Filter pull requests by the default branch with <a>base:main</a>.</p>
|
|
319
|
+
</div>
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ── The pull request page ──────────────────────────────────────────────────────────────────────────────────────
|
|
324
|
+
export type PullTab = "conversation" | "commits" | "checks" | "files";
|
|
325
|
+
export type PullActions = {
|
|
326
|
+
comment: (body: string) => Promise<void>;
|
|
327
|
+
merge: (title: string, message: string) => Promise<void>;
|
|
328
|
+
close: (body?: string) => Promise<void>;
|
|
329
|
+
reopen: () => Promise<void>;
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
function shortSha(sha?: string) { return sha ? sha.slice(0, 7) : ""; }
|
|
333
|
+
|
|
334
|
+
function DiffStat({ additions = 0, deletions = 0 }: { additions?: number; deletions?: number }) {
|
|
335
|
+
const total = additions + deletions;
|
|
336
|
+
const greens = total ? Math.round((additions / total) * 5) : 0;
|
|
337
|
+
const reds = total ? Math.min(5 - greens, Math.round((deletions / total) * 5) || (deletions ? 1 : 0)) : 0;
|
|
338
|
+
return (
|
|
339
|
+
<span className="gh-diffstat">
|
|
340
|
+
<span className="gh-add">+{additions}</span> <span className="gh-del">−{deletions}</span>
|
|
341
|
+
<span className="gh-diffstat-blocks">{Array.from({ length: 5 }, (_, i) => <span key={i} className={`gh-block ${i < greens ? "add" : i < greens + reds ? "del" : "neutral"}`} />)}</span>
|
|
342
|
+
</span>
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function PullHeader({ pr, commits, checks, files, tab, onTab }: { pr: PullRequest; commits: number; checks: number; files: number; tab: PullTab; onTab: (t: PullTab) => void }) {
|
|
347
|
+
const s = pullState(pr);
|
|
348
|
+
const n = pr.commitsCount ?? commits;
|
|
349
|
+
const commitWord = `${n} commit${n === 1 ? "" : "s"}`;
|
|
350
|
+
const [owner] = pr.repo.split("/");
|
|
351
|
+
const ref = (branch?: string) => <span className="gh-commit-ref" title={`${owner}:${branch}`}>{branch}</span>;
|
|
352
|
+
const tabs: Array<{ id: PullTab; label: string; icon: string; count: number }> = [
|
|
353
|
+
{ id: "conversation", label: "Conversation", icon: "comment-discussion", count: (pr.commentsCount ?? 0) + (pr.reviewCount ?? 0) + (pr.reviewCommentsCount ?? 0) },
|
|
354
|
+
{ id: "commits", label: "Commits", icon: "git-commit", count: n },
|
|
355
|
+
{ id: "checks", label: "Checks", icon: "checklist", count: checks },
|
|
356
|
+
{ id: "files", label: "Files changed", icon: "file-diff", count: pr.changedFiles ?? files },
|
|
357
|
+
];
|
|
358
|
+
return (
|
|
359
|
+
<div className="gh-pull-header">
|
|
360
|
+
<div className="gh-pull-title-row">
|
|
361
|
+
<h1 className="gh-pull-title"><bdi>{pr.title}</bdi> <span className="gh-pull-number">#{pr.number}</span></h1>
|
|
362
|
+
<div className="gh-pull-title-actions">
|
|
363
|
+
<button className="gh-btn gh-btn-sm">Edit</button>
|
|
364
|
+
<button className="gh-btn gh-btn-sm gh-btn-primary"><Octicon name="code" /> Code <Octicon name="triangle-down" /></button>
|
|
365
|
+
</div>
|
|
366
|
+
</div>
|
|
367
|
+
<div className="gh-pull-meta state-line">
|
|
368
|
+
<StateBadge pr={pr} />
|
|
369
|
+
<span className="gh-pull-meta-text">
|
|
370
|
+
{s === "merged" ? (
|
|
371
|
+
<><a className="gh-author">{pr.mergedBy ?? pr.author}</a> merged {commitWord} into {ref(pr.baseBranch)} from {ref(pr.headBranch)} <RelativeTime iso={pr.mergedAt} /></>
|
|
372
|
+
) : (
|
|
373
|
+
<><a className="gh-author">{pr.author}</a> wants to merge {commitWord} into {ref(pr.baseBranch)} from {ref(pr.headBranch)}</>
|
|
374
|
+
)}
|
|
375
|
+
<button className="gh-copy" aria-label="Copy head branch name to clipboard" onClick={() => void navigator.clipboard?.writeText(pr.headBranch ?? "")}><Octicon name="copy" /></button>
|
|
376
|
+
</span>
|
|
377
|
+
</div>
|
|
378
|
+
<nav className="gh-pull-tabs" aria-label="Pull request tabs">
|
|
379
|
+
{tabs.map((t) => (
|
|
380
|
+
<a key={t.id} href="#" className={`gh-pull-tab${tab === t.id ? " selected" : ""}`} aria-current={tab === t.id ? "page" : undefined} onClick={(e) => { e.preventDefault(); onTab(t.id); }}>
|
|
381
|
+
<Octicon name={t.icon} /> {t.label} <span className="Counter">{t.count}</span>
|
|
382
|
+
</a>
|
|
383
|
+
))}
|
|
384
|
+
<span className="gh-pull-tabs-fill" />
|
|
385
|
+
<DiffStat additions={pr.additions} deletions={pr.deletions} />
|
|
386
|
+
</nav>
|
|
387
|
+
</div>
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function association(login: string | undefined, pr: PullRequest, given?: string): string[] {
|
|
392
|
+
const out: string[] = [];
|
|
393
|
+
if (given && given !== "NONE") out.push(given === "OWNER" ? "Owner" : given === "MEMBER" ? "Member" : given === "COLLABORATOR" ? "Collaborator" : given === "CONTRIBUTOR" ? "Contributor" : "");
|
|
394
|
+
else if (login && login === pr.repo.split("/")[0]) out.push("Owner");
|
|
395
|
+
if (login && login === pr.author) out.push("Author");
|
|
396
|
+
return out.filter(Boolean);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function CommentBox({ login, when, body, pr, viewer, verb = "commented", given }: { login?: string; when?: string; body?: string; pr: PullRequest; viewer?: string; verb?: string; given?: string }) {
|
|
400
|
+
return (
|
|
401
|
+
<div className="timeline-item gh-comment-item">
|
|
402
|
+
<span className="gh-timeline-avatar"><Avatar login={login} size={40} /></span>
|
|
403
|
+
<article className={`comment gh-comment${login && login === viewer ? " gh-comment-self" : ""}`}>
|
|
404
|
+
<header className="gh-comment-header">
|
|
405
|
+
<span><a className="gh-author">{login}</a> <span className="gh-muted">{verb} <RelativeTime iso={when} /></span></span>
|
|
406
|
+
<span className="gh-comment-badges">
|
|
407
|
+
{association(login, pr, given).map((b) => <span key={b} className="gh-role">{b}</span>)}
|
|
408
|
+
<button className="gh-iconbtn gh-iconbtn-sm" aria-label="Show options"><Octicon name="kebab-horizontal" /></button>
|
|
409
|
+
</span>
|
|
410
|
+
</header>
|
|
411
|
+
<div className="gh-comment-body gh-markdown"><Markdown text={body} /></div>
|
|
412
|
+
</article>
|
|
413
|
+
</div>
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function TimelineEvent({ icon, tone, children }: { icon: string; tone?: string; children: React.ReactNode }) {
|
|
418
|
+
return (
|
|
419
|
+
<div className="gh-event">
|
|
420
|
+
<span className={`gh-event-badge${tone ? ` ${tone}` : ""}`}><Octicon name={icon} /></span>
|
|
421
|
+
<div className="gh-event-body">{children}</div>
|
|
422
|
+
</div>
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
type Entry = { at: number; key: string; node: React.ReactNode };
|
|
427
|
+
|
|
428
|
+
/** The Conversation tab's timeline, the merge box and the comment form. */
|
|
429
|
+
export function Conversation({ pr, reviews, commits, comments, reviewComments, checks = [], stateEvents, viewer, actions }: {
|
|
430
|
+
pr?: PullRequest; reviews: Review[]; commits: Commit[]; comments: Comment[]; reviewComments: Comment[]; files?: ChangedFile[];
|
|
431
|
+
checks?: Check[]; stateEvents?: StateEvent[]; viewer?: string; actions?: PullActions;
|
|
432
|
+
}) {
|
|
433
|
+
if (!pr) return null;
|
|
434
|
+
const s = pullState(pr);
|
|
435
|
+
const entries: Entry[] = [];
|
|
436
|
+
const at = (iso?: string) => { const t = Date.parse(iso ?? ""); return Number.isFinite(t) ? t : 0; };
|
|
437
|
+
if (commits.length) {
|
|
438
|
+
// commits pushed together read as one event: "<login> added N commits"
|
|
439
|
+
const by = commits[0]!.authorLogin ?? commits[0]!.author;
|
|
440
|
+
entries.push({
|
|
441
|
+
at: at(commits[0]!.date), key: "commits", node: (
|
|
442
|
+
<div className="gh-commits-event">
|
|
443
|
+
<TimelineEvent icon="git-commit">
|
|
444
|
+
<Avatar login={by} size={20} /> <a className="gh-author">{by}</a> <span className="gh-muted">added {commits.length} commit{commits.length === 1 ? "" : "s"} <RelativeTime iso={commits[0]!.date} /></span>
|
|
445
|
+
</TimelineEvent>
|
|
446
|
+
{commits.map((c) => (
|
|
447
|
+
<div className="gh-commit-line" key={c.sha}>
|
|
448
|
+
<Avatar login={c.authorLogin ?? c.author} size={16} />
|
|
449
|
+
<span className="gh-commit-message">{(c.message ?? "").split("\n")[0]}</span>
|
|
450
|
+
<span className="gh-commit-status" />
|
|
451
|
+
<code className="gh-sha">{shortSha(c.sha)}</code>
|
|
452
|
+
</div>
|
|
453
|
+
))}
|
|
454
|
+
</div>
|
|
455
|
+
),
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
comments.forEach((c, i) => entries.push({ at: at(c.createdAt), key: `c-${c.id ?? i}`, node: <CommentBox login={c.author} when={c.createdAt} body={c.body} pr={pr} viewer={viewer} given={c.authorAssociation} /> }));
|
|
459
|
+
reviews.forEach((r, i) => {
|
|
460
|
+
const st = String(r.state ?? "").toLowerCase();
|
|
461
|
+
const word = st === "approved" ? "approved these changes" : st === "changes_requested" ? "requested changes" : st === "dismissed" ? "reviewed (dismissed)" : "reviewed";
|
|
462
|
+
const tone = st === "approved" ? "approved" : st === "changes_requested" ? "changes" : "";
|
|
463
|
+
entries.push({
|
|
464
|
+
at: at(r.submittedAt), key: `r-${r.id ?? i}`, node: (
|
|
465
|
+
<div className="review-card gh-review">
|
|
466
|
+
<TimelineEvent icon={st === "approved" ? "check" : st === "changes_requested" ? "file-diff" : "eye"} tone={tone}>
|
|
467
|
+
<Avatar login={r.reviewer} size={20} /> <a className="gh-author">{r.reviewer}</a> <span className="gh-muted">{word} <RelativeTime iso={r.submittedAt} /></span>
|
|
468
|
+
</TimelineEvent>
|
|
469
|
+
{r.body ? <div className="gh-review-body"><CommentBox login={r.reviewer} when={r.submittedAt} body={r.body} pr={pr} viewer={viewer} verb="left a comment" /></div> : null}
|
|
470
|
+
</div>
|
|
471
|
+
),
|
|
472
|
+
});
|
|
473
|
+
});
|
|
474
|
+
reviewComments.forEach((c, i) => entries.push({ at: at(c.createdAt), key: `rc-${c.id ?? i}`, node: <CommentBox login={c.author} when={c.createdAt} body={c.body} pr={pr} viewer={viewer} verb={`commented on ${c.path ?? "a file"}`} /> }));
|
|
475
|
+
if (s === "merged") entries.push({
|
|
476
|
+
at: at(pr.mergedAt), key: "merged", node: (
|
|
477
|
+
<TimelineEvent icon="git-merge" tone="merged">
|
|
478
|
+
<Avatar login={pr.mergedBy ?? pr.author} size={20} /> <a className="gh-author">{pr.mergedBy ?? pr.author}</a> <span className="gh-muted">merged commit</span> <code className="gh-sha">{shortSha(pr.mergeCommit)}</code> <span className="gh-muted">into</span> <span className="gh-commit-ref">{pr.baseBranch}</span> <span className="gh-muted"><RelativeTime iso={pr.mergedAt} /></span>
|
|
479
|
+
</TimelineEvent>
|
|
480
|
+
),
|
|
481
|
+
});
|
|
482
|
+
// closed and reopened, from the timeline GitHub keeps (a merge closes too; the merged event says it)
|
|
483
|
+
const changes = stateEvents ?? (s === "closed" ? [{ event: "closed", at: pr.closedAt ?? pr.updatedAt }] : []);
|
|
484
|
+
changes.filter((e) => !(s === "merged" && e.event === "closed")).forEach((e, i) => {
|
|
485
|
+
const who = e.actor ?? pr.author;
|
|
486
|
+
entries.push({
|
|
487
|
+
at: at(e.at), key: `state-${i}`, node: (
|
|
488
|
+
<TimelineEvent icon={e.event === "closed" ? "git-pull-request-closed" : "git-pull-request"} tone={e.event === "closed" ? "closed" : "reopened"}>
|
|
489
|
+
<Avatar login={who} size={20} /> <a className="gh-author">{who}</a> <span className="gh-muted">{e.event === "closed" ? "closed" : "reopened"} this <RelativeTime iso={e.at} /></span>
|
|
490
|
+
</TimelineEvent>
|
|
491
|
+
),
|
|
492
|
+
});
|
|
493
|
+
});
|
|
494
|
+
entries.sort((a, b) => a.at - b.at);
|
|
495
|
+
return (
|
|
496
|
+
<section className="conversation gh-conversation">
|
|
497
|
+
<div className="gh-timeline">
|
|
498
|
+
<CommentBox login={pr.author} when={pr.createdAt} body={pr.body} pr={pr} viewer={viewer} />
|
|
499
|
+
{entries.map((e) => <React.Fragment key={e.key}>{e.node}</React.Fragment>)}
|
|
500
|
+
</div>
|
|
501
|
+
<MergeBox pr={pr} reviews={reviews} checks={checks} actions={actions} />
|
|
502
|
+
<CommentForm pr={pr} viewer={viewer} actions={actions} />
|
|
503
|
+
</section>
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function MergeSection({ ok, tone, icon, title, detail, children }: { ok?: boolean; tone?: string; icon: string; title: string; detail?: string; children?: React.ReactNode }) {
|
|
508
|
+
return (
|
|
509
|
+
<div className="gh-merge-section">
|
|
510
|
+
<span className={`gh-merge-section-icon ${tone ?? (ok ? "ok" : "warn")}`}><Octicon name={icon} size={16} /></span>
|
|
511
|
+
<div className="gh-merge-section-text">
|
|
512
|
+
<strong>{title}</strong>
|
|
513
|
+
{detail ? <div className="gh-muted">{detail}</div> : null}
|
|
514
|
+
{children}
|
|
515
|
+
</div>
|
|
516
|
+
</div>
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function MergeBox({ pr, reviews, checks, actions }: { pr: PullRequest; reviews: Review[]; checks: Check[]; actions?: PullActions }) {
|
|
521
|
+
const [confirming, setConfirming] = useState(false);
|
|
522
|
+
const [title, setTitle] = useState("");
|
|
523
|
+
const [message, setMessage] = useState("");
|
|
524
|
+
const [busy, setBusy] = useState(false);
|
|
525
|
+
const [error, setError] = useState("");
|
|
526
|
+
const s = pullState(pr);
|
|
527
|
+
if (s === "merged") {
|
|
528
|
+
return (
|
|
529
|
+
<div className="gh-mergebox">
|
|
530
|
+
<span className="gh-mergebox-icon merged"><Octicon name="git-merge" size={24} /></span>
|
|
531
|
+
<div className="gh-mergebox-panel">
|
|
532
|
+
<MergeSection tone="merged" icon="git-merge" title="Pull request successfully merged and closed" detail={`You’re all set — the ${pr.headBranch} branch can be safely deleted.`} />
|
|
533
|
+
</div>
|
|
534
|
+
</div>
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
if (s === "closed") {
|
|
538
|
+
return (
|
|
539
|
+
<div className="gh-mergebox">
|
|
540
|
+
<span className="gh-mergebox-icon closed"><Octicon name="git-pull-request-closed" size={24} /></span>
|
|
541
|
+
<div className="gh-mergebox-panel">
|
|
542
|
+
<MergeSection tone="closed" icon="git-pull-request-closed" title="Closed with unmerged commits" detail="This pull request is closed, but the branch has unmerged commits. You can reopen it to continue the review." />
|
|
543
|
+
</div>
|
|
544
|
+
</div>
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
const decision = reviewDecision(pr, reviews);
|
|
548
|
+
const approvals = new Set(reviews.filter((r) => String(r.state).toLowerCase() === "approved").map((r) => r.reviewer)).size;
|
|
549
|
+
const failing = checks.filter((c) => ["failure", "error", "timed_out", "cancelled", "action_required"].includes(c.state));
|
|
550
|
+
const pending = checks.filter((c) => c.state === "pending" || c.state === "queued" || c.state === "in_progress");
|
|
551
|
+
const passed = checks.length - failing.length - pending.length;
|
|
552
|
+
const conflicts = pr.mergeable === false;
|
|
553
|
+
const plural = (n: number, w: string) => `${n} ${w}${n === 1 ? "" : "s"}`;
|
|
554
|
+
async function run(work: () => Promise<void>) {
|
|
555
|
+
setBusy(true); setError("");
|
|
556
|
+
try { await work(); setConfirming(false); } catch (e) { setError(e instanceof Error ? e.message : String(e)); } finally { setBusy(false); }
|
|
557
|
+
}
|
|
558
|
+
return (
|
|
559
|
+
<div className="gh-mergebox">
|
|
560
|
+
<span className={`gh-mergebox-icon ${conflicts || failing.length ? "neutral" : "open"}`}><Octicon name="git-merge" size={24} /></span>
|
|
561
|
+
<div className="gh-mergebox-panel">
|
|
562
|
+
{decision === undefined ? null : decision === "approved" ? (
|
|
563
|
+
<MergeSection ok icon="check" title="Changes approved" detail={`${plural(approvals, "approving review")} by reviewers with write access.`} />
|
|
564
|
+
) : decision === "changes_requested" ? (
|
|
565
|
+
<MergeSection tone="bad" icon="file-diff" title="Changes requested" detail="A reviewer has requested changes to this pull request." />
|
|
566
|
+
) : (
|
|
567
|
+
<MergeSection tone="warn" icon="dot-fill" title="Review requested" detail={`Review has been requested on this pull request. ${(pr.requestedReviewers ?? []).join(", ")} ${(pr.requestedReviewers ?? []).length === 1 ? "is" : "are"} requested.`} />
|
|
568
|
+
)}
|
|
569
|
+
{checks.length ? (
|
|
570
|
+
<MergeSection
|
|
571
|
+
ok={!failing.length && !pending.length}
|
|
572
|
+
tone={failing.length ? "bad" : pending.length ? "warn" : "ok"}
|
|
573
|
+
icon={failing.length ? "x" : pending.length ? "dot-fill" : "check"}
|
|
574
|
+
title={failing.length ? "Some checks were not successful" : pending.length ? "Some checks haven’t completed yet" : "All checks have passed"}
|
|
575
|
+
detail={[failing.length ? `${failing.length} failing` : "", pending.length ? `${pending.length} pending` : "", passed ? plural(passed, "successful check") : ""].filter(Boolean).join(", ")}
|
|
576
|
+
>
|
|
577
|
+
<ul className="gh-checks-list">
|
|
578
|
+
{checks.map((c) => (
|
|
579
|
+
<li key={c.name}>
|
|
580
|
+
<span className={`ci-status ${c.state === "success" ? "success" : c.state === "pending" ? "pending" : "failure"}`}><Octicon name={CI_ICON[c.state] ?? (c.state === "success" ? "check" : "x")} /></span>
|
|
581
|
+
<strong>{c.name}</strong>{c.description ? <span className="gh-muted"> — {c.description}</span> : null}
|
|
582
|
+
{c.targetUrl ? <a className="gh-check-details" href={c.targetUrl} target="_blank" rel="noreferrer">Details</a> : null}
|
|
583
|
+
</li>
|
|
584
|
+
))}
|
|
585
|
+
</ul>
|
|
586
|
+
</MergeSection>
|
|
587
|
+
) : null}
|
|
588
|
+
{conflicts
|
|
589
|
+
? <MergeSection tone="bad" icon="x" title="This branch has conflicts that must be resolved" detail="Resolve conflicts then push again." />
|
|
590
|
+
: <MergeSection ok icon="check" title="No conflicts with base branch" detail="Merging can be performed automatically." />}
|
|
591
|
+
<div className="gh-merge-actions">
|
|
592
|
+
{confirming ? (
|
|
593
|
+
<div className="gh-merge-confirm">
|
|
594
|
+
<input className="gh-input" aria-label="Commit title" value={title} onChange={(e) => setTitle(e.target.value)} />
|
|
595
|
+
<textarea className="gh-input" aria-label="Commit message" value={message} onChange={(e) => setMessage(e.target.value)} rows={3} />
|
|
596
|
+
<div className="gh-merge-confirm-buttons">
|
|
597
|
+
<button className="gh-btn gh-btn-primary" disabled={busy} onClick={() => void run(() => actions!.merge(title, message))}>{busy ? "Merging…" : "Confirm merge"}</button>
|
|
598
|
+
<button className="gh-btn" disabled={busy} onClick={() => setConfirming(false)}>Cancel</button>
|
|
599
|
+
</div>
|
|
600
|
+
</div>
|
|
601
|
+
) : (
|
|
602
|
+
<div className="gh-merge-button-row">
|
|
603
|
+
<span className="gh-btn-group">
|
|
604
|
+
<button className="gh-btn gh-btn-primary" disabled={conflicts || !actions} onClick={() => {
|
|
605
|
+
const [owner] = pr.repo.split("/");
|
|
606
|
+
setTitle(`Merge pull request #${pr.number} from ${owner}/${pr.headBranch}`);
|
|
607
|
+
setMessage(pr.title ?? "");
|
|
608
|
+
setConfirming(true);
|
|
609
|
+
}}>Merge pull request</button>
|
|
610
|
+
<button className="gh-btn gh-btn-primary gh-btn-caret" aria-label="Select merge method" disabled={conflicts || !actions}><Octicon name="triangle-down" /></button>
|
|
611
|
+
</span>
|
|
612
|
+
<span className="gh-muted gh-merge-hint">You can also merge this with the command line.</span>
|
|
613
|
+
</div>
|
|
614
|
+
)}
|
|
615
|
+
{error ? <div className="gh-flash-error" role="alert">{error}</div> : null}
|
|
616
|
+
</div>
|
|
617
|
+
</div>
|
|
618
|
+
</div>
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function CommentForm({ pr, viewer, actions }: { pr: PullRequest; viewer?: string; actions?: PullActions }) {
|
|
623
|
+
const [body, setBody] = useState("");
|
|
624
|
+
const [busy, setBusy] = useState(false);
|
|
625
|
+
const [error, setError] = useState("");
|
|
626
|
+
const s = pullState(pr);
|
|
627
|
+
async function run(work: () => Promise<void>, clear = true) {
|
|
628
|
+
setBusy(true); setError("");
|
|
629
|
+
try { await work(); if (clear) setBody(""); } catch (e) { setError(e instanceof Error ? e.message : String(e)); } finally { setBusy(false); }
|
|
630
|
+
}
|
|
631
|
+
return (
|
|
632
|
+
<div className="timeline-item gh-comment-form-item">
|
|
633
|
+
<span className="gh-timeline-avatar"><Avatar login={viewer} size={40} /></span>
|
|
634
|
+
<div className="gh-comment-form">
|
|
635
|
+
<h2 className="gh-comment-form-title">Add a comment</h2>
|
|
636
|
+
<div className="gh-comment-form-box">
|
|
637
|
+
<div className="gh-comment-form-tabs"><span className="selected">Write</span><span>Preview</span></div>
|
|
638
|
+
<textarea className="gh-comment-textarea" aria-label="Markdown value" placeholder="Use Markdown to format your comment" value={body} onChange={(e) => setBody(e.target.value)} />
|
|
639
|
+
<div className="gh-comment-form-hint"><Octicon name="file-diff" /> Markdown is supported</div>
|
|
640
|
+
</div>
|
|
641
|
+
{error ? <div className="gh-flash-error" role="alert">{error}</div> : null}
|
|
642
|
+
<div className="gh-comment-form-actions">
|
|
643
|
+
{s === "open" || s === "draft" ? (
|
|
644
|
+
<button className="gh-btn" disabled={busy || !actions} onClick={() => void run(() => actions!.close(body.trim() || undefined))}>
|
|
645
|
+
<Octicon name="git-pull-request-closed" className="gh-closed-fg" /> {body.trim() ? "Close with comment" : "Close pull request"}
|
|
646
|
+
</button>
|
|
647
|
+
) : s === "closed" ? (
|
|
648
|
+
<button className="gh-btn" disabled={busy || !actions} onClick={() => void run(() => actions!.reopen(), false)}>Reopen pull request</button>
|
|
649
|
+
) : null}
|
|
650
|
+
<button className="gh-btn gh-btn-primary" disabled={busy || !body.trim() || !actions} onClick={() => void run(() => actions!.comment(body.trim()))}>Comment</button>
|
|
651
|
+
</div>
|
|
652
|
+
</div>
|
|
653
|
+
</div>
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/** The sidebar: Reviewers, Assignees, Labels, Projects, Milestone, Development, Participants. */
|
|
658
|
+
export function Aside({ pr, reviews, comments = [], labelColors = {} }: { pr?: PullRequest; reviews: Review[]; commits?: Commit[]; files?: ChangedFile[]; comments?: Comment[]; labelColors?: Record<string, string> }) {
|
|
659
|
+
if (!pr) return <aside className="sidebar" />;
|
|
660
|
+
const latest = new Map<string, string>();
|
|
661
|
+
for (const r of reviews) if (r.reviewer) latest.set(r.reviewer, String(r.state ?? "").toLowerCase());
|
|
662
|
+
const requested = (pr.requestedReviewers ?? []).filter((l) => !latest.has(l));
|
|
663
|
+
const participants = [...new Set([pr.author, ...reviews.map((r) => r.reviewer), ...comments.map((c) => c.author)].filter(Boolean) as string[])];
|
|
664
|
+
return (
|
|
665
|
+
<aside className="sidebar gh-sidebar">
|
|
666
|
+
<section className="gh-sidebar-item">
|
|
667
|
+
<h3>Reviewers<Octicon name="gear" /></h3>
|
|
668
|
+
{latest.size || requested.length ? (
|
|
669
|
+
<ul className="gh-sidebar-list">
|
|
670
|
+
{[...latest].map(([login, st]) => (
|
|
671
|
+
<li key={login}><Avatar login={login} size={20} /> <a className="gh-author">{login}</a>
|
|
672
|
+
<span className={`gh-review-state ${st}`} title={st}><Octicon name={st === "approved" ? "check" : st === "changes_requested" ? "file-diff" : "comment"} /></span></li>
|
|
673
|
+
))}
|
|
674
|
+
{requested.map((login) => (
|
|
675
|
+
<li key={login}><Avatar login={login} size={20} /> <a className="gh-author">{login}</a>
|
|
676
|
+
<span className="gh-review-state pending" title="Awaiting requested review"><Octicon name="dot-fill" /></span></li>
|
|
677
|
+
))}
|
|
678
|
+
</ul>
|
|
679
|
+
) : <p className="gh-muted">No reviews</p>}
|
|
680
|
+
</section>
|
|
681
|
+
<section className="gh-sidebar-item">
|
|
682
|
+
<h3>Assignees<Octicon name="gear" /></h3>
|
|
683
|
+
{(pr.assignees ?? []).length ? <ul className="gh-sidebar-list">{(pr.assignees ?? []).map((a) => <li key={a}><Avatar login={a} size={20} /> <a className="gh-author">{a}</a></li>)}</ul>
|
|
684
|
+
: <p className="gh-muted">No one—<a>assign yourself</a></p>}
|
|
685
|
+
</section>
|
|
686
|
+
<section className="gh-sidebar-item">
|
|
687
|
+
<h3>Labels<Octicon name="gear" /></h3>
|
|
688
|
+
{(pr.labels ?? []).length ? <div className="gh-sidebar-labels">{(pr.labels ?? []).map((l) => <Label key={l.name} name={l.name} color={labelColors[`${pr.repo}:${l.name}`] ?? l.color} />)}</div> : <p className="gh-muted">None yet</p>}
|
|
689
|
+
</section>
|
|
690
|
+
<section className="gh-sidebar-item">
|
|
691
|
+
<h3>Projects<Octicon name="gear" /></h3>
|
|
692
|
+
<p className="gh-muted">None yet</p>
|
|
693
|
+
</section>
|
|
694
|
+
<section className="gh-sidebar-item">
|
|
695
|
+
<h3>Milestone<Octicon name="gear" /></h3>
|
|
696
|
+
<p className="gh-muted">{pr.milestone ?? "No milestone"}</p>
|
|
697
|
+
</section>
|
|
698
|
+
<section className="gh-sidebar-item">
|
|
699
|
+
<h3>Development<Octicon name="gear" /></h3>
|
|
700
|
+
<p className="gh-muted">Successfully merging this pull request may close these issues.</p>
|
|
701
|
+
<p className="gh-muted">None yet</p>
|
|
702
|
+
</section>
|
|
703
|
+
<section className="gh-sidebar-item">
|
|
704
|
+
<h3 className="gh-sidebar-plain">{participants.length} participant{participants.length === 1 ? "" : "s"}</h3>
|
|
705
|
+
<div className="gh-participants">{participants.map((p) => <Avatar key={p} login={p} size={26} />)}</div>
|
|
706
|
+
</section>
|
|
707
|
+
</aside>
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// ── Commits, Checks and Files changed tabs ─────────────────────────────────────────────────────────────────────
|
|
712
|
+
function CommitsTab({ commits }: { commits: Commit[] }) {
|
|
713
|
+
const days = new Map<string, Commit[]>();
|
|
714
|
+
for (const c of commits) {
|
|
715
|
+
const day = c.date ? new Date(c.date).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }) : "";
|
|
716
|
+
days.set(day, [...(days.get(day) ?? []), c]);
|
|
717
|
+
}
|
|
718
|
+
if (!commits.length) return <div className="gh-blankslate"><h3>No commits</h3></div>;
|
|
719
|
+
return (
|
|
720
|
+
<div className="gh-commits-tab">
|
|
721
|
+
{[...days].map(([day, list]) => (
|
|
722
|
+
<div className="gh-commit-group" key={day}>
|
|
723
|
+
<h3 className="gh-commit-group-title"><Octicon name="git-commit" /> Commits on {day}</h3>
|
|
724
|
+
<div className="gh-box">
|
|
725
|
+
{list.map((c) => (
|
|
726
|
+
<div className="gh-commit-row" key={c.sha}>
|
|
727
|
+
<div>
|
|
728
|
+
<div className="gh-commit-row-title">{(c.message ?? "").split("\n")[0]}</div>
|
|
729
|
+
<div className="gh-commit-row-meta"><Avatar login={c.authorLogin ?? c.author} size={16} /> <a className="gh-author">{c.authorLogin ?? c.author}</a> <span className="gh-muted">committed <RelativeTime iso={c.date} /></span></div>
|
|
730
|
+
</div>
|
|
731
|
+
<div className="gh-commit-row-side">
|
|
732
|
+
<code className="gh-sha gh-sha-btn">{shortSha(c.sha)}</code>
|
|
733
|
+
<button className="gh-iconbtn gh-iconbtn-sm" aria-label="Copy full SHA"><Octicon name="copy" /></button>
|
|
734
|
+
<button className="gh-iconbtn gh-iconbtn-sm" aria-label="Browse the repository at this point"><Octicon name="code" /></button>
|
|
735
|
+
</div>
|
|
736
|
+
</div>
|
|
737
|
+
))}
|
|
738
|
+
</div>
|
|
739
|
+
</div>
|
|
740
|
+
))}
|
|
741
|
+
</div>
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function ChecksTab({ checks }: { checks: Check[] }) {
|
|
746
|
+
if (!checks.length) return <div className="gh-blankslate"><Octicon name="checklist" size={24} /><h3>No checks</h3><p>This commit has no checks yet.</p></div>;
|
|
747
|
+
return (
|
|
748
|
+
<div className="gh-box gh-checks-tab">
|
|
749
|
+
<div className="gh-box-header"><strong>{checks.length} check{checks.length === 1 ? "" : "s"}</strong></div>
|
|
750
|
+
{checks.map((c) => (
|
|
751
|
+
<div className="gh-check-row" key={c.name}>
|
|
752
|
+
<span className={`ci-status ${c.state === "success" ? "success" : c.state === "pending" ? "pending" : "failure"}`}><Octicon name={CI_ICON[c.state] ?? "x"} /></span>
|
|
753
|
+
<strong>{c.name}</strong>
|
|
754
|
+
<span className="gh-muted">{c.description}</span>
|
|
755
|
+
<span className="gh-spacer" />
|
|
756
|
+
{c.targetUrl ? <a href={c.targetUrl} target="_blank" rel="noreferrer">Details</a> : null}
|
|
757
|
+
</div>
|
|
758
|
+
))}
|
|
759
|
+
</div>
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
type DiffLine = { kind: "hunk" | "add" | "del" | "ctx" | "note"; text: string; oldNo?: number; newNo?: number };
|
|
764
|
+
function diffLines(patch: string): DiffLine[] {
|
|
765
|
+
const out: DiffLine[] = [];
|
|
766
|
+
let oldNo = 0, newNo = 0;
|
|
767
|
+
for (const line of patch.split("\n")) {
|
|
768
|
+
const m = /^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(line);
|
|
769
|
+
if (m) { oldNo = Number(m[1]); newNo = Number(m[2]); out.push({ kind: "hunk", text: line }); continue; }
|
|
770
|
+
if (line.startsWith("\\")) { out.push({ kind: "note", text: line }); continue; }
|
|
771
|
+
if (line.startsWith("+")) out.push({ kind: "add", text: line.slice(1), newNo: newNo++ });
|
|
772
|
+
else if (line.startsWith("-")) out.push({ kind: "del", text: line.slice(1), oldNo: oldNo++ });
|
|
773
|
+
else out.push({ kind: "ctx", text: line.slice(1), oldNo: oldNo++, newNo: newNo++ });
|
|
774
|
+
}
|
|
775
|
+
return out;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/** The Files changed tab: each file's header (diffstat, path) and its unified diff with both line-number gutters. */
|
|
779
|
+
export function FilesDiff({ files }: { files: ChangedFile[] }) {
|
|
780
|
+
if (!files.length) return <div className="gh-blankslate"><h3>No changes to show</h3></div>;
|
|
781
|
+
return (
|
|
782
|
+
<section className="files-diff gh-files" data-testid="files-diff">
|
|
783
|
+
<div className="gh-files-toolbar">
|
|
784
|
+
<span className="gh-select-menu">Changes from all commits <Octicon name="triangle-down" /></span>
|
|
785
|
+
<span className="gh-select-menu">File filter <Octicon name="triangle-down" /></span>
|
|
786
|
+
<span className="gh-select-menu">Conversations <Octicon name="triangle-down" /></span>
|
|
787
|
+
<span className="gh-spacer" />
|
|
788
|
+
<span className="gh-muted">0 / {files.length} files viewed</span>
|
|
789
|
+
<button className="gh-btn gh-btn-sm gh-btn-primary">Review changes <Octicon name="triangle-down" /></button>
|
|
790
|
+
</div>
|
|
791
|
+
{files.map((file) => (
|
|
792
|
+
<article className="diff-file gh-file" key={file.filename}>
|
|
793
|
+
<header className="gh-file-header">
|
|
794
|
+
<Octicon name="chevron-down" />
|
|
795
|
+
<span className="gh-file-count">{(file.additions ?? 0) + (file.deletions ?? 0)}</span>
|
|
796
|
+
<DiffStat additions={file.additions} deletions={file.deletions} />
|
|
797
|
+
<span className="gh-file-name">{file.filename}</span>
|
|
798
|
+
<button className="gh-iconbtn gh-iconbtn-sm" aria-label="Copy file name to clipboard"><Octicon name="copy" /></button>
|
|
799
|
+
<span className="gh-spacer" />
|
|
800
|
+
<label className="gh-viewed"><input type="checkbox" readOnly /> Viewed</label>
|
|
801
|
+
</header>
|
|
802
|
+
{file.patch ? (
|
|
803
|
+
<table className="gh-diff-table" data-testid="diff-hunk">
|
|
804
|
+
<tbody>
|
|
805
|
+
{diffLines(file.patch).map((l, i) => l.kind === "hunk" || l.kind === "note" ? (
|
|
806
|
+
<tr key={i} className={`gh-diff-${l.kind}`}><td className="gh-diff-num" colSpan={2} /><td className="gh-diff-code">{l.text}</td></tr>
|
|
807
|
+
) : (
|
|
808
|
+
<tr key={i} className={`gh-diff-${l.kind}`}>
|
|
809
|
+
<td className="gh-diff-num">{l.oldNo ?? ""}</td>
|
|
810
|
+
<td className="gh-diff-num">{l.newNo ?? ""}</td>
|
|
811
|
+
<td className="gh-diff-code"><span className="gh-diff-marker">{l.kind === "add" ? "+" : l.kind === "del" ? "-" : " "}</span>{l.text || "\u00a0"}</td>
|
|
812
|
+
</tr>
|
|
813
|
+
))}
|
|
814
|
+
</tbody>
|
|
815
|
+
</table>
|
|
816
|
+
) : <div className="gh-file-nodiff gh-muted">{file.status === "removed" ? "This file was deleted." : "Binary file not shown."}</div>}
|
|
817
|
+
</article>
|
|
818
|
+
))}
|
|
819
|
+
</section>
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
export function PullPage({ pr, reviews, commits, comments, reviewComments, files, checks, stateEvents, viewer, labelColors, tab, onTab, actions }: {
|
|
824
|
+
pr?: PullRequest; reviews: Review[]; commits: Commit[]; comments: Comment[]; reviewComments: Comment[]; files: ChangedFile[]; checks: Check[]; stateEvents: StateEvent[];
|
|
825
|
+
viewer?: string; labelColors: Record<string, string>; tab: PullTab; onTab: (t: PullTab) => void; actions: PullActions;
|
|
826
|
+
}) {
|
|
827
|
+
if (!pr) return <div className="gh-container"><div className="gh-blankslate"><h3>Loading pull request…</h3></div></div>;
|
|
828
|
+
return (
|
|
829
|
+
<div className="gh-container gh-pull-page">
|
|
830
|
+
<PullHeader pr={pr} commits={commits.length} checks={checks.length} files={files.length} tab={tab} onTab={onTab} />
|
|
831
|
+
{tab === "conversation" ? (
|
|
832
|
+
<div className="gh-pull-layout">
|
|
833
|
+
<Conversation pr={pr} reviews={reviews} commits={commits} comments={comments} reviewComments={reviewComments} checks={checks} stateEvents={stateEvents} viewer={viewer} actions={actions} />
|
|
834
|
+
<Aside pr={pr} reviews={reviews} comments={comments} labelColors={labelColors} />
|
|
835
|
+
</div>
|
|
836
|
+
) : tab === "commits" ? <CommitsTab commits={commits} />
|
|
837
|
+
: tab === "checks" ? <ChecksTab checks={checks} />
|
|
838
|
+
: <FilesDiff files={files} />}
|
|
839
|
+
</div>
|
|
840
|
+
);
|
|
841
|
+
}
|