addons/isl/src/DownloadCommitsMenu.tsxblame
View source
b69ab311/**
b69ab312 * Copyright (c) Meta Platforms, Inc. and affiliates.
b69ab313 *
b69ab314 * This source code is licensed under the MIT license found in the
b69ab315 * LICENSE file in the root directory of this source tree.
b69ab316 */
b69ab317
b69ab318import {Button} from 'isl-components/Button';
b69ab319import {Checkbox} from 'isl-components/Checkbox';
b69ab3110import {Divider} from 'isl-components/Divider';
b69ab3111import {Icon} from 'isl-components/Icon';
b69ab3112import {Kbd} from 'isl-components/Kbd';
b69ab3113import {KeyCode, Modifier} from 'isl-components/KeyboardShortcuts';
b69ab3114import {TextField} from 'isl-components/TextField';
b69ab3115import {Tooltip} from 'isl-components/Tooltip';
b69ab3116import {useAtom} from 'jotai';
b69ab3117import {useEffect, useRef, useState} from 'react';
b69ab3118import {nullthrows} from 'shared/utils';
b69ab3119import {Collapsable} from './Collapsable';
b69ab3120import {CommitCloudInfo} from './CommitCloud';
b69ab3121import {DropdownFields} from './DropdownFields';
b69ab3122import {GotoTimeContent} from './GotoTimeMenu';
b69ab3123import {useCommandEvent} from './ISLShortcuts';
b69ab3124import {Internal} from './Internal';
b69ab3125import {tracker} from './analytics';
b69ab3126import {findPublicBaseAncestor} from './getCommitTree';
b69ab3127import {t, T} from './i18n';
b69ab3128import {configBackedAtom, readAtom} from './jotaiUtils';
b69ab3129import {GotoOperation} from './operations/GotoOperation';
b69ab3130import {GraftOperation} from './operations/GraftOperation';
b69ab3131import {PullRevOperation} from './operations/PullRevOperation';
b69ab3132import {RebaseKeepOperation} from './operations/RebaseKeepOperation';
b69ab3133import {RebaseOperation} from './operations/RebaseOperation';
b69ab3134import {useRunOperation} from './operationsState';
b69ab3135import {dagWithPreviews} from './previews';
b69ab3136import {forceFetchCommit} from './serverAPIState';
b69ab3137import {exactRevset, succeedableRevset} from './types';
b69ab3138
b69ab3139import './DownloadCommitsMenu.css';
b69ab3140
b69ab3141export function DownloadCommitsTooltipButton() {
b69ab3142 const additionalToggles = useCommandEvent('ToggleDownloadCommitsDropdown');
b69ab3143 return (
b69ab3144 <Tooltip
b69ab3145 trigger="click"
b69ab3146 component={dismiss => <DownloadCommitsTooltip dismiss={dismiss} />}
b69ab3147 placement="bottom"
b69ab3148 additionalToggles={additionalToggles.asEventTarget()}
b69ab3149 group="topbar"
b69ab3150 title={
b69ab3151 <div>
b69ab3152 <T replace={{$shortcut: <Kbd modifiers={[Modifier.ALT]} keycode={KeyCode.D} />}}>
b69ab3153 Download commits and diffs ($shortcut)
b69ab3154 </T>
b69ab3155 </div>
b69ab3156 }>
b69ab3157 <Button icon data-testid="download-commits-tooltip-button">
b69ab3158 <Icon icon="cloud-download" />
b69ab3159 </Button>
b69ab3160 </Tooltip>
b69ab3161 );
b69ab3162}
b69ab3163
b69ab3164const downloadCommitRebaseType = configBackedAtom<'rebase_base' | 'rebase_ontop' | null>(
b69ab3165 'isl.download-commit-rebase-type',
b69ab3166 null,
b69ab3167);
b69ab3168
b69ab3169const downloadCommitShouldGoto = configBackedAtom<boolean>(
b69ab3170 'isl.download-commit-should-goto',
b69ab3171 false,
b69ab3172);
b69ab3173
b69ab3174function maybeSupportedByPull(name: string): boolean {
b69ab3175 return (
b69ab3176 !name.includes('(') /* likely a revset */ &&
b69ab3177 name.length < 42 /* likely an expression or rREPOhash Phabricator callsign */
b69ab3178 );
b69ab3179}
b69ab3180
b69ab3181function DownloadCommitsTooltip({dismiss}: {dismiss: () => unknown}) {
b69ab3182 const [enteredRevset, setEnteredRevset] = useState('');
b69ab3183 const runOperation = useRunOperation();
b69ab3184 const supportsDiffDownload = Internal.diffDownloadOperation != null;
b69ab3185 const downloadDiffTextArea = useRef(null);
b69ab3186 useEffect(() => {
b69ab3187 if (downloadDiffTextArea.current) {
b69ab3188 (downloadDiffTextArea.current as HTMLTextAreaElement).focus();
b69ab3189 }
b69ab3190 }, [downloadDiffTextArea]);
b69ab3191
b69ab3192 const [rebaseType, setRebaseType] = useAtom(downloadCommitRebaseType);
b69ab3193 const [shouldGoto, setShouldGoto] = useAtom(downloadCommitShouldGoto);
b69ab3194
b69ab3195 const doCommitDownload = async () => {
b69ab3196 tracker.track('ClickPullButton', {
b69ab3197 extras: {
b69ab3198 rebaseType,
b69ab3199 shouldGoto,
b69ab31100 },
b69ab31101 });
b69ab31102
b69ab31103 // We need to dismiss the tooltip now, since we don't want to leave it up until after the operations are run.
b69ab31104 dismiss();
b69ab31105
b69ab31106 // Typically, we'd just immediately use runOperation to queue up additional operations.
b69ab31107 // Unfortunately, we don't know if the result will be public or not,
b69ab31108 // and that changes how we'll rebase/graft the result. This means we can't use the queueing system.
b69ab31109 // This is not a correctness issue because we show no optimistically downloaded result to act on.
b69ab31110 // Worst case, the rebase/goto will be queued after some other unrelated actions which should be fine.
b69ab31111
b69ab31112 if (maybeSupportedByPull(enteredRevset)) {
b69ab31113 try {
b69ab31114 await runOperation(
b69ab31115 new PullRevOperation(exactRevset(enteredRevset)),
b69ab31116 /* throwOnError */ true,
b69ab31117 );
b69ab31118 } catch (err) {
b69ab31119 if (Internal.diffDownloadOperation != null) {
b69ab31120 // Note: try backup diff download system internally
b69ab31121 await runOperation(
b69ab31122 Internal.diffDownloadOperation(exactRevset(enteredRevset)),
b69ab31123 /* throwOnError */ true,
b69ab31124 );
b69ab31125 } else {
b69ab31126 // If there's no backup operation, respect the error and don't try further actions
b69ab31127 throw err;
b69ab31128 }
b69ab31129 }
b69ab31130 }
b69ab31131
b69ab31132 // Lookup the result of the pull
b69ab31133 const latest = await forceFetchCommit(enteredRevset).catch(() => null);
b69ab31134 if (!latest) {
b69ab31135 // We can't continue with the rebase/goto if the lookup failed.
b69ab31136 return;
b69ab31137 }
b69ab31138
b69ab31139 // Now we CAN queue up additional actions
b69ab31140
b69ab31141 const isPublic = latest?.phase === 'public';
b69ab31142 if (rebaseType != null) {
b69ab31143 const Op = isPublic
b69ab31144 ? // "graft" implicitly does "goto", "rebase --keep" does not
b69ab31145 shouldGoto
b69ab31146 ? GraftOperation
b69ab31147 : RebaseKeepOperation
b69ab31148 : RebaseOperation;
b69ab31149 const dest =
b69ab31150 rebaseType === 'rebase_ontop'
b69ab31151 ? '.'
b69ab31152 : nullthrows(findPublicBaseAncestor(readAtom(dagWithPreviews))?.hash);
b69ab31153 // Use exact revsets for sources, so that you can type a specific hash to download and not be surprised by succession.
b69ab31154 // Only use succession for destination, which may be in flux at the moment you start the download.
b69ab31155 runOperation(new Op(exactRevset(enteredRevset), succeedableRevset(dest)));
b69ab31156 }
b69ab31157
b69ab31158 if (
b69ab31159 shouldGoto &&
b69ab31160 // Goto for public commits will be handled by Graft, if a rebase/graft was performed.
b69ab31161 // Goto on max(latest_successors(revset)) would just yield the existing public commit,
b69ab31162 // but for non-landed commits, using succeedableRevset allows goto the newly rebased commit.
b69ab31163 // If no rebase was performed, we will use goto even for public commits.
b69ab31164 (!isPublic || rebaseType === null)
b69ab31165 ) {
b69ab31166 runOperation(
b69ab31167 new GotoOperation(
b69ab31168 // if not rebasing, just use the exact revset.
b69ab31169 rebaseType == null ? exactRevset(enteredRevset) : succeedableRevset(enteredRevset),
b69ab31170 ),
b69ab31171 );
b69ab31172 }
b69ab31173
b69ab31174 const fullRepoBranchGraftOperation = Internal.getFullRepoBranchGraftOperation?.(
b69ab31175 dagWithPreviews,
b69ab31176 latest,
b69ab31177 rebaseType,
b69ab31178 );
b69ab31179 if (fullRepoBranchGraftOperation != null) {
b69ab31180 runOperation(fullRepoBranchGraftOperation);
b69ab31181 }
b69ab31182 };
b69ab31183
b69ab31184 return (
b69ab31185 <DropdownFields
b69ab31186 title={<T>Download Commits</T>}
b69ab31187 icon="cloud-download"
b69ab31188 data-testid="download-commits-dropdown">
b69ab31189 <div className="download-commits-content">
b69ab31190 <div className="download-commits-input-row">
b69ab31191 <TextField
b69ab31192 width="100%"
b69ab31193 placeholder={
b69ab31194 supportsDiffDownload ? t('Hash, Diff Number, ...') : t('Hash, revset, pr123, ...')
b69ab31195 }
b69ab31196 value={enteredRevset}
b69ab31197 data-testid="download-commits-input"
b69ab31198 onInput={e => setEnteredRevset((e.target as unknown as {value: string})?.value ?? '')}
b69ab31199 onKeyDown={e => {
b69ab31200 if (e.key === 'Enter') {
b69ab31201 if (enteredRevset.trim().length > 0) {
b69ab31202 doCommitDownload();
b69ab31203 }
b69ab31204 }
b69ab31205 }}
b69ab31206 ref={downloadDiffTextArea}
b69ab31207 />
b69ab31208 <Button
b69ab31209 data-testid="download-commit-button"
b69ab31210 disabled={enteredRevset.trim().length === 0}
b69ab31211 onClick={doCommitDownload}>
b69ab31212 <T>Pull</T>
b69ab31213 </Button>
b69ab31214 </div>
b69ab31215 <div className="download-commits-input-row">
b69ab31216 <Tooltip title={t('After downloading this commit, also go there')}>
b69ab31217 <Checkbox checked={shouldGoto} onChange={setShouldGoto}>
b69ab31218 <T>Go to</T>
b69ab31219 </Checkbox>
b69ab31220 </Tooltip>
b69ab31221 <Tooltip
b69ab31222 title={t(
b69ab31223 'After downloading this commit, rebase it onto the public base of the current stack. Public commits will be copied instead of moved.',
b69ab31224 )}>
b69ab31225 <Checkbox
b69ab31226 checked={rebaseType === 'rebase_base'}
b69ab31227 onChange={checked => {
b69ab31228 setRebaseType(checked ? 'rebase_base' : null);
b69ab31229 }}>
b69ab31230 <T>Rebase to Stack Base</T>
b69ab31231 </Checkbox>
b69ab31232 </Tooltip>
b69ab31233 <Tooltip
b69ab31234 title={t(
b69ab31235 'After downloading this commit, rebase it on top of the current commit. Public commits will be copied instead of moved.',
b69ab31236 )}>
b69ab31237 <Checkbox
b69ab31238 checked={rebaseType === 'rebase_ontop'}
b69ab31239 onChange={checked => {
b69ab31240 setRebaseType(checked ? 'rebase_ontop' : null);
b69ab31241 }}>
b69ab31242 <T>Rebase onto Stack</T>
b69ab31243 </Checkbox>
b69ab31244 </Tooltip>
b69ab31245 </div>
b69ab31246 </div>
b69ab31247
b69ab31248 <Collapsable
b69ab31249 title={
b69ab31250 <>
b69ab31251 <Icon icon="clock" />
b69ab31252 <T>Go to time</T>
b69ab31253 </>
b69ab31254 }
b69ab31255 className="download-commits-expander">
b69ab31256 <GotoTimeContent dismiss={dismiss} />
b69ab31257 </Collapsable>
b69ab31258
b69ab31259 {Internal.supportsCommitCloud && (
b69ab31260 <>
b69ab31261 <Divider />
b69ab31262 <CommitCloudInfo />
b69ab31263 </>
b69ab31264 )}
b69ab31265 </DropdownFields>
b69ab31266 );
b69ab31267}