6.1 KB226 lines
Blame
1/**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8import type {ReactNode} from 'react';
9import type {Operation} from '../../operations/Operation';
10import type {
11 CodeReviewSystem,
12 CommitInfo,
13 DiffId,
14 DiffSummary,
15 PreferredSubmitCommand,
16} from '../../types';
17import type {UICodeReviewProvider} from '../UICodeReviewProvider';
18import type {SyncStatus} from '../syncStatus';
19
20import {Icon} from 'isl-components/Icon';
21import {Tooltip} from 'isl-components/Tooltip';
22import {PullRequestReviewDecision, PullRequestState} from 'isl-server/src/github/generated/graphql';
23import {MS_PER_DAY} from 'shared/constants';
24import {OSSCommitMessageFieldSchema} from '../../CommitInfoView/OSSCommitMessageFieldsSchema';
25import {Internal} from '../../Internal';
26import {t, T} from '../../i18n';
27import {GhStackSubmitOperation} from '../../operations/GhStackSubmitOperation';
28import {PrSubmitOperation} from '../../operations/PrSubmitOperation';
29
30import './GitHubPRBadge.css';
31
32export class GithubUICodeReviewProvider implements UICodeReviewProvider {
33 name = 'github';
34 label = t('GitHub');
35
36 constructor(
37 public system: CodeReviewSystem & {type: 'github'},
38 private preferredSubmitCommand: PreferredSubmitCommand,
39 ) {}
40 cliName?: string | undefined;
41
42 DiffBadgeContent({
43 diff,
44 children,
45 }: {
46 diff?: DiffSummary;
47 children?: ReactNode;
48 }): JSX.Element | null {
49 if (diff != null && diff?.type !== 'github') {
50 return null;
51 }
52 return (
53 <div className="github-diff-info">
54 <div
55 className={
56 'github-diff-status' + (diff?.state ? ` github-diff-status-${diff.state}` : '')
57 }>
58 <Tooltip title={t('Click to open Pull Request in GitHub')} delayMs={500}>
59 {diff && <Icon className="github-diff-badge-icon" icon={iconForPRState(diff.state)} />}
60 {diff?.state && <PRStateLabel state={diff.state} />}
61 {children}
62 </Tooltip>
63 </div>
64 {diff?.reviewDecision && <ReviewDecision decision={diff.reviewDecision} />}
65 </div>
66 );
67 }
68
69 formatDiffNumber(diffId: DiffId): string {
70 return `#${diffId}`;
71 }
72
73 getSyncStatuses(
74 _commits: CommitInfo[],
75 _allDiffSummaries: Map<string, DiffSummary>,
76 ): Map<string, SyncStatus> {
77 // TODO: support finding the sync status for GitHub PRs
78 return new Map();
79 }
80
81 RepoInfo = () => {
82 return (
83 <span>
84 {this.system.hostname !== 'github.com' ? this.system.hostname : ''} {this.system.owner}/
85 {this.system.repo}
86 </span>
87 );
88 };
89
90 getRemoteTrackingBranch(): string | null {
91 return null;
92 }
93
94 getRemoteTrackingBranchFromDiffSummary(): string | null {
95 return null;
96 }
97
98 isSplitSuggestionSupported(): boolean {
99 return false;
100 }
101 submitOperation(
102 _commits: Array<CommitInfo>,
103 options: {draft?: boolean; updateMessage?: string; publishWhenReady?: boolean},
104 ): Operation {
105 if (this.preferredSubmitCommand === 'ghstack') {
106 return new GhStackSubmitOperation(options);
107 } else if (this.preferredSubmitCommand === 'pr') {
108 return new PrSubmitOperation(options);
109 } else {
110 throw new Error('Not yet implemented');
111 }
112 }
113
114 submitCommandName() {
115 return `sl ${this.preferredSubmitCommand}`;
116 }
117
118 getSupportedStackActions() {
119 return {};
120 }
121
122 getSubmittableDiffs() {
123 return [];
124 }
125
126 isDiffClosed(diff: DiffSummary & {type: 'github'}): boolean {
127 return diff.state === PullRequestState.Closed;
128 }
129
130 isDiffEligibleForCleanup(diff: DiffSummary & {type: 'github'}): boolean {
131 return diff.state === PullRequestState.Closed;
132 }
133
134 getUpdateDiffActions(_summary: DiffSummary) {
135 return [];
136 }
137
138 commitMessageFieldsSchema =
139 Internal.CommitMessageFieldSchemaForGitHub ?? OSSCommitMessageFieldSchema;
140
141 supportSubmittingAsDraft = 'newDiffsOnly' as const;
142 supportsUpdateMessage = false;
143 submitDisabledReason = () =>
144 Internal.submitForGitHubDisabledReason?.(this.preferredSubmitCommand);
145 supportBranchingPrs = true;
146
147 branchNameForRemoteBookmark(bookmark: string) {
148 // TODO: is "origin" really always the prefix for remote bookmarks in git?
149 const originPrefix = 'origin/';
150 const branchName = bookmark.startsWith(originPrefix)
151 ? bookmark.slice(originPrefix.length)
152 : bookmark;
153 return branchName;
154 }
155
156 enableMessageSyncing = false;
157
158 supportsSuggestedReviewers = false;
159
160 supportsComparingSinceLastSubmit = false;
161
162 supportsRenderingMarkup = false;
163
164 gotoDistanceWarningAgeCutoff = 30 * MS_PER_DAY;
165}
166
167type BadgeState = PullRequestState | 'ERROR' | 'DRAFT' | 'MERGE_QUEUED';
168
169function iconForPRState(state?: BadgeState) {
170 switch (state) {
171 case 'ERROR':
172 return 'error';
173 case 'DRAFT':
174 case 'MERGE_QUEUED':
175 return 'git-pull-request';
176 case PullRequestState.Open:
177 return 'git-pull-request';
178 case PullRequestState.Merged:
179 return 'git-merge';
180 case PullRequestState.Closed:
181 return 'git-pull-request-closed';
182 default:
183 return 'git-pull-request';
184 }
185}
186
187function PRStateLabel({state}: {state: BadgeState}) {
188 switch (state) {
189 case PullRequestState.Open:
190 return <T>Open</T>;
191 case PullRequestState.Merged:
192 return <T>Merged</T>;
193 case PullRequestState.Closed:
194 return <T>Closed</T>;
195 case 'DRAFT':
196 return <T>Draft</T>;
197 case 'ERROR':
198 return <T>Error</T>;
199 case 'MERGE_QUEUED':
200 return <T>Merge Queued</T>;
201 default:
202 return <T>{state}</T>;
203 }
204}
205
206function reviewDecisionLabel(decision: PullRequestReviewDecision) {
207 switch (decision) {
208 case PullRequestReviewDecision.Approved:
209 return <T>Approved</T>;
210 case PullRequestReviewDecision.ChangesRequested:
211 return <T>Changes Requested</T>;
212 case PullRequestReviewDecision.ReviewRequired:
213 return <T>Review Required</T>;
214 default:
215 return <T>{decision}</T>;
216 }
217}
218
219function ReviewDecision({decision}: {decision: PullRequestReviewDecision}) {
220 return (
221 <div className={`github-review-decision github-review-decision-${decision}`}>
222 {reviewDecisionLabel(decision)}
223 </div>
224 );
225}
226