9.4 KB268 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 {Button} from 'isl-components/Button';
9import {Checkbox} from 'isl-components/Checkbox';
10import {Divider} from 'isl-components/Divider';
11import {Icon} from 'isl-components/Icon';
12import {Kbd} from 'isl-components/Kbd';
13import {KeyCode, Modifier} from 'isl-components/KeyboardShortcuts';
14import {TextField} from 'isl-components/TextField';
15import {Tooltip} from 'isl-components/Tooltip';
16import {useAtom} from 'jotai';
17import {useEffect, useRef, useState} from 'react';
18import {nullthrows} from 'shared/utils';
19import {Collapsable} from './Collapsable';
20import {CommitCloudInfo} from './CommitCloud';
21import {DropdownFields} from './DropdownFields';
22import {GotoTimeContent} from './GotoTimeMenu';
23import {useCommandEvent} from './ISLShortcuts';
24import {Internal} from './Internal';
25import {tracker} from './analytics';
26import {findPublicBaseAncestor} from './getCommitTree';
27import {t, T} from './i18n';
28import {configBackedAtom, readAtom} from './jotaiUtils';
29import {GotoOperation} from './operations/GotoOperation';
30import {GraftOperation} from './operations/GraftOperation';
31import {PullRevOperation} from './operations/PullRevOperation';
32import {RebaseKeepOperation} from './operations/RebaseKeepOperation';
33import {RebaseOperation} from './operations/RebaseOperation';
34import {useRunOperation} from './operationsState';
35import {dagWithPreviews} from './previews';
36import {forceFetchCommit} from './serverAPIState';
37import {exactRevset, succeedableRevset} from './types';
38
39import './DownloadCommitsMenu.css';
40
41export function DownloadCommitsTooltipButton() {
42 const additionalToggles = useCommandEvent('ToggleDownloadCommitsDropdown');
43 return (
44 <Tooltip
45 trigger="click"
46 component={dismiss => <DownloadCommitsTooltip dismiss={dismiss} />}
47 placement="bottom"
48 additionalToggles={additionalToggles.asEventTarget()}
49 group="topbar"
50 title={
51 <div>
52 <T replace={{$shortcut: <Kbd modifiers={[Modifier.ALT]} keycode={KeyCode.D} />}}>
53 Download commits and diffs ($shortcut)
54 </T>
55 </div>
56 }>
57 <Button icon data-testid="download-commits-tooltip-button">
58 <Icon icon="cloud-download" />
59 </Button>
60 </Tooltip>
61 );
62}
63
64const downloadCommitRebaseType = configBackedAtom<'rebase_base' | 'rebase_ontop' | null>(
65 'isl.download-commit-rebase-type',
66 null,
67);
68
69const downloadCommitShouldGoto = configBackedAtom<boolean>(
70 'isl.download-commit-should-goto',
71 false,
72);
73
74function maybeSupportedByPull(name: string): boolean {
75 return (
76 !name.includes('(') /* likely a revset */ &&
77 name.length < 42 /* likely an expression or rREPOhash Phabricator callsign */
78 );
79}
80
81function DownloadCommitsTooltip({dismiss}: {dismiss: () => unknown}) {
82 const [enteredRevset, setEnteredRevset] = useState('');
83 const runOperation = useRunOperation();
84 const supportsDiffDownload = Internal.diffDownloadOperation != null;
85 const downloadDiffTextArea = useRef(null);
86 useEffect(() => {
87 if (downloadDiffTextArea.current) {
88 (downloadDiffTextArea.current as HTMLTextAreaElement).focus();
89 }
90 }, [downloadDiffTextArea]);
91
92 const [rebaseType, setRebaseType] = useAtom(downloadCommitRebaseType);
93 const [shouldGoto, setShouldGoto] = useAtom(downloadCommitShouldGoto);
94
95 const doCommitDownload = async () => {
96 tracker.track('ClickPullButton', {
97 extras: {
98 rebaseType,
99 shouldGoto,
100 },
101 });
102
103 // We need to dismiss the tooltip now, since we don't want to leave it up until after the operations are run.
104 dismiss();
105
106 // Typically, we'd just immediately use runOperation to queue up additional operations.
107 // Unfortunately, we don't know if the result will be public or not,
108 // and that changes how we'll rebase/graft the result. This means we can't use the queueing system.
109 // This is not a correctness issue because we show no optimistically downloaded result to act on.
110 // Worst case, the rebase/goto will be queued after some other unrelated actions which should be fine.
111
112 if (maybeSupportedByPull(enteredRevset)) {
113 try {
114 await runOperation(
115 new PullRevOperation(exactRevset(enteredRevset)),
116 /* throwOnError */ true,
117 );
118 } catch (err) {
119 if (Internal.diffDownloadOperation != null) {
120 // Note: try backup diff download system internally
121 await runOperation(
122 Internal.diffDownloadOperation(exactRevset(enteredRevset)),
123 /* throwOnError */ true,
124 );
125 } else {
126 // If there's no backup operation, respect the error and don't try further actions
127 throw err;
128 }
129 }
130 }
131
132 // Lookup the result of the pull
133 const latest = await forceFetchCommit(enteredRevset).catch(() => null);
134 if (!latest) {
135 // We can't continue with the rebase/goto if the lookup failed.
136 return;
137 }
138
139 // Now we CAN queue up additional actions
140
141 const isPublic = latest?.phase === 'public';
142 if (rebaseType != null) {
143 const Op = isPublic
144 ? // "graft" implicitly does "goto", "rebase --keep" does not
145 shouldGoto
146 ? GraftOperation
147 : RebaseKeepOperation
148 : RebaseOperation;
149 const dest =
150 rebaseType === 'rebase_ontop'
151 ? '.'
152 : nullthrows(findPublicBaseAncestor(readAtom(dagWithPreviews))?.hash);
153 // Use exact revsets for sources, so that you can type a specific hash to download and not be surprised by succession.
154 // Only use succession for destination, which may be in flux at the moment you start the download.
155 runOperation(new Op(exactRevset(enteredRevset), succeedableRevset(dest)));
156 }
157
158 if (
159 shouldGoto &&
160 // Goto for public commits will be handled by Graft, if a rebase/graft was performed.
161 // Goto on max(latest_successors(revset)) would just yield the existing public commit,
162 // but for non-landed commits, using succeedableRevset allows goto the newly rebased commit.
163 // If no rebase was performed, we will use goto even for public commits.
164 (!isPublic || rebaseType === null)
165 ) {
166 runOperation(
167 new GotoOperation(
168 // if not rebasing, just use the exact revset.
169 rebaseType == null ? exactRevset(enteredRevset) : succeedableRevset(enteredRevset),
170 ),
171 );
172 }
173
174 const fullRepoBranchGraftOperation = Internal.getFullRepoBranchGraftOperation?.(
175 dagWithPreviews,
176 latest,
177 rebaseType,
178 );
179 if (fullRepoBranchGraftOperation != null) {
180 runOperation(fullRepoBranchGraftOperation);
181 }
182 };
183
184 return (
185 <DropdownFields
186 title={<T>Download Commits</T>}
187 icon="cloud-download"
188 data-testid="download-commits-dropdown">
189 <div className="download-commits-content">
190 <div className="download-commits-input-row">
191 <TextField
192 width="100%"
193 placeholder={
194 supportsDiffDownload ? t('Hash, Diff Number, ...') : t('Hash, revset, pr123, ...')
195 }
196 value={enteredRevset}
197 data-testid="download-commits-input"
198 onInput={e => setEnteredRevset((e.target as unknown as {value: string})?.value ?? '')}
199 onKeyDown={e => {
200 if (e.key === 'Enter') {
201 if (enteredRevset.trim().length > 0) {
202 doCommitDownload();
203 }
204 }
205 }}
206 ref={downloadDiffTextArea}
207 />
208 <Button
209 data-testid="download-commit-button"
210 disabled={enteredRevset.trim().length === 0}
211 onClick={doCommitDownload}>
212 <T>Pull</T>
213 </Button>
214 </div>
215 <div className="download-commits-input-row">
216 <Tooltip title={t('After downloading this commit, also go there')}>
217 <Checkbox checked={shouldGoto} onChange={setShouldGoto}>
218 <T>Go to</T>
219 </Checkbox>
220 </Tooltip>
221 <Tooltip
222 title={t(
223 'After downloading this commit, rebase it onto the public base of the current stack. Public commits will be copied instead of moved.',
224 )}>
225 <Checkbox
226 checked={rebaseType === 'rebase_base'}
227 onChange={checked => {
228 setRebaseType(checked ? 'rebase_base' : null);
229 }}>
230 <T>Rebase to Stack Base</T>
231 </Checkbox>
232 </Tooltip>
233 <Tooltip
234 title={t(
235 'After downloading this commit, rebase it on top of the current commit. Public commits will be copied instead of moved.',
236 )}>
237 <Checkbox
238 checked={rebaseType === 'rebase_ontop'}
239 onChange={checked => {
240 setRebaseType(checked ? 'rebase_ontop' : null);
241 }}>
242 <T>Rebase onto Stack</T>
243 </Checkbox>
244 </Tooltip>
245 </div>
246 </div>
247
248 <Collapsable
249 title={
250 <>
251 <Icon icon="clock" />
252 <T>Go to time</T>
253 </>
254 }
255 className="download-commits-expander">
256 <GotoTimeContent dismiss={dismiss} />
257 </Collapsable>
258
259 {Internal.supportsCommitCloud && (
260 <>
261 <Divider />
262 <CommitCloudInfo />
263 </>
264 )}
265 </DropdownFields>
266 );
267}
268