addons/isl/src/Commit.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 type {ReactNode} from 'react';
b69ab319import type {ContextMenuItem} from 'shared/ContextMenu';
b69ab3110import type {UICodeReviewProvider} from './codeReview/UICodeReviewProvider';
b69ab3111import type {DagCommitInfo} from './dag/dag';
083fd5f12import type {CommitInfo, Hash, SuccessorInfo} from './types';
b69ab3113import {succeedableRevset, WarningCheckResult} from './types';
b69ab3114
b69ab3115import * as stylex from '@stylexjs/stylex';
b69ab3116import {Button} from 'isl-components/Button';
b69ab3117import {Icon} from 'isl-components/Icon';
b69ab3118import {Subtle} from 'isl-components/Subtle';
b69ab3119import {Tooltip} from 'isl-components/Tooltip';
b69ab3120import {atom, useAtomValue, useSetAtom} from 'jotai';
b69ab3121import React, {memo, useEffect} from 'react';
b69ab3122import {ComparisonType} from 'shared/Comparison';
b69ab3123import {contextMenuState, useContextMenu} from 'shared/ContextMenu';
b69ab3124import {MS_PER_DAY} from 'shared/constants';
b69ab3125import {useAutofocusRef} from 'shared/hooks';
b69ab3126import {notEmpty, nullthrows} from 'shared/utils';
b69ab3127import {spacing} from '../../components/theme/tokens.stylex';
b69ab3128import {AllBookmarksTruncated, Bookmark, Bookmarks, createBookmarkAtCommit} from './Bookmark';
b69ab3129import {openBrowseUrlForHash, supportsBrowseUrlForHash} from './BrowseRepo';
b69ab3130import {hasUnsavedEditedCommitMessage} from './CommitInfoView/CommitInfoState';
b69ab3131import {showComparison} from './ComparisonView/atoms';
b69ab3132import {Row} from './ComponentUtils';
b69ab3133import {DragToRebase} from './DragToRebase';
b69ab3134import {EducationInfoTip} from './Education';
b69ab3135import {HighlightCommitsWhileHovering} from './HighlightedCommits';
b69ab3136import {Internal} from './Internal';
b69ab3137import {SubmitSelectionButton} from './SubmitSelectionButton';
b69ab3138import {SubmitSingleCommitButton} from './SubmitSingleCommitButton';
b69ab3139import {getSuggestedRebaseOperation, suggestedRebaseDestinations} from './SuggestedRebase';
b69ab3140import {UncommitButton} from './UncommitButton';
b69ab3141import {UncommittedChanges} from './UncommittedChanges';
b69ab3142import {tracker} from './analytics';
b69ab3143import {clipboardLinkHtml} from './clipboard';
b69ab3144import {
b69ab3145 allDiffSummaries,
b69ab3146 branchingDiffInfos,
083fd5f47 canopySignalForCommit,
b69ab3148 codeReviewProvider,
b69ab3149 diffSummary,
e7069e150 ensureCanopySignalsFetched,
b69ab3151 latestCommitMessageTitle,
b69ab3152} from './codeReview/CodeReviewInfo';
083fd5f53import {DiffBadge, DiffFollower, DiffInfo, SignalSummaryIcon} from './codeReview/DiffBadge';
b69ab3154import {submitAsDraft} from './codeReview/DraftCheckbox';
b69ab3155import {SyncStatus, syncStatusAtom} from './codeReview/syncStatus';
b69ab3156import {useFeatureFlagSync} from './featureFlags';
b69ab3157import {FoldButton, useRunFoldPreview} from './fold';
b69ab3158import {findPublicBaseAncestor} from './getCommitTree';
b69ab3159import {t, T} from './i18n';
b69ab3160import {IconStack} from './icons/IconStack';
b69ab3161import {IrrelevantCwdIcon} from './icons/IrrelevantCwdIcon';
b69ab3162import {atomFamilyWeak, localStorageBackedAtom, readAtom, writeAtom} from './jotaiUtils';
b69ab3163import {CONFLICT_SIDE_LABELS} from './mergeConflicts/consts';
b69ab3164import {getAmendToOperation, isAmendToAllowedForCommit} from './operationUtils';
b69ab3165import {GotoOperation} from './operations/GotoOperation';
b69ab3166import {HideOperation} from './operations/HideOperation';
b69ab3167import {
b69ab3168 inlineProgressByHash,
b69ab3169 operationBeingPreviewed,
b69ab3170 useRunOperation,
b69ab3171 useRunPreviewedOperation,
b69ab3172} from './operationsState';
b69ab3173import platform from './platform';
b69ab3174import {CommitPreview, dagWithPreviews, uncommittedChangesWithPreviews} from './previews';
b69ab3175import {RelativeDate, relativeDate} from './relativeDate';
b69ab3176import {repoRelativeCwd, useIsIrrelevantToCwd} from './repositoryData';
b69ab3177import {isNarrowCommitTree} from './responsive';
b69ab3178import {
b69ab3179 actioningCommit,
b69ab3180 selectedCommitInfos,
b69ab3181 selectedCommits,
b69ab3182 useCommitCallbacks,
b69ab3183} from './selection';
9e488cc84import {inMergeConflicts, mergeConflicts, repositoryInfo} from './serverAPIState';
b69ab3185import {SmartActionsDropdown} from './smartActions/SmartActionsDropdown';
b69ab3186import {SmartActionsMenu} from './smartActions/SmartActionsMenu';
b69ab3187import {useConfirmUnsavedEditsBeforeSplit} from './stackEdit/ui/ConfirmUnsavedEditsBeforeSplit';
b69ab3188import {SplitButton} from './stackEdit/ui/SplitButton';
b69ab3189import {editingStackIntentionHashes} from './stackEdit/ui/stackEditState';
b69ab3190import {latestSuccessorUnlessExplicitlyObsolete} from './successionUtils';
b69ab3191import {copyAndShowToast} from './toast';
b69ab3192import {showModal} from './useModal';
b69ab3193import {short} from './utils';
b69ab3194
b69ab3195export const rebaseOffWarmWarningEnabled = localStorageBackedAtom<boolean>(
b69ab3196 'isl.rebase-off-warm-warning-enabled',
b69ab3197 true,
b69ab3198);
b69ab3199
b69ab31100export const distantRebaseWarningEnabled = localStorageBackedAtom<boolean>(
b69ab31101 'isl.distant-rebase-warning-enabled',
b69ab31102 true,
b69ab31103);
b69ab31104
b69ab31105export const rebaseOntoMasterWarningEnabled = localStorageBackedAtom<boolean>(
b69ab31106 'isl.rebase-onto-master-warning-enabled',
b69ab31107 true,
b69ab31108);
b69ab31109
b69ab31110/**
b69ab31111 * Some preview types should not allow actions on top of them
b69ab31112 * For example, you can't click goto on the preview of dragging a rebase,
b69ab31113 * but you can goto on the optimistic form of a running rebase.
b69ab31114 */
b69ab31115function previewPreventsActions(preview?: CommitPreview): boolean {
b69ab31116 switch (preview) {
b69ab31117 case CommitPreview.REBASE_OLD:
b69ab31118 case CommitPreview.REBASE_DESCENDANT:
b69ab31119 case CommitPreview.REBASE_ROOT:
b69ab31120 case CommitPreview.HIDDEN_ROOT:
b69ab31121 case CommitPreview.HIDDEN_DESCENDANT:
b69ab31122 case CommitPreview.FOLD:
b69ab31123 case CommitPreview.FOLD_PREVIEW:
b69ab31124 case CommitPreview.NON_ACTIONABLE_COMMIT:
b69ab31125 return true;
b69ab31126 }
b69ab31127 return false;
b69ab31128}
b69ab31129
b69ab31130const commitLabelForCommit = atomFamilyWeak((hash: string) =>
b69ab31131 atom(get => {
b69ab31132 const conflicts = get(mergeConflicts);
b69ab31133 const {localShort, incomingShort} = CONFLICT_SIDE_LABELS;
b69ab31134 const hashes = conflicts?.hashes;
b69ab31135 if (hash === hashes?.other) {
b69ab31136 return incomingShort;
b69ab31137 } else if (hash === hashes?.local) {
b69ab31138 return localShort;
b69ab31139 }
b69ab31140 return null;
b69ab31141 }),
b69ab31142);
b69ab31143
b69ab31144export const Commit = memo(
b69ab31145 ({
b69ab31146 commit,
b69ab31147 previewType,
b69ab31148 hasChildren,
b69ab31149 }: {
b69ab31150 commit: DagCommitInfo | CommitInfo;
b69ab31151 previewType?: CommitPreview;
b69ab31152 hasChildren: boolean;
b69ab31153 }) => {
b69ab31154 const isPublic = commit.phase === 'public';
b69ab31155 const isObsoleted = commit.successorInfo != null;
b69ab31156 const hasUncommittedChanges = (readAtom(uncommittedChangesWithPreviews).length ?? 0) > 0;
b69ab31157
b69ab31158 const isIrrelevantToCwd = useIsIrrelevantToCwd(commit);
b69ab31159
b69ab31160 const handlePreviewedOperation = useRunPreviewedOperation();
b69ab31161 const runOperation = useRunOperation();
b69ab31162 const setEditStackIntentionHashes = useSetAtom(editingStackIntentionHashes);
b69ab31163
b69ab31164 const inlineProgress = useAtomValue(inlineProgressByHash(commit.hash));
b69ab31165
b69ab31166 const {isSelected, onDoubleClickToShowDrawer} = useCommitCallbacks(commit);
b69ab31167 const actionsPrevented = previewPreventsActions(previewType);
b69ab31168
b69ab31169 const isActioning = useAtomValue(actioningCommit) === commit.hash;
b69ab31170 const isContextMenuOpen = useAtomValue(contextMenuState) != null;
b69ab31171
b69ab31172 useEffect(() => {
b69ab31173 if (!isContextMenuOpen && isActioning) {
b69ab31174 writeAtom(actioningCommit, null);
b69ab31175 }
b69ab31176 }, [isContextMenuOpen, isActioning]);
b69ab31177
b69ab31178 const inConflicts = useAtomValue(inMergeConflicts);
b69ab31179
b69ab31180 const isNarrow = useAtomValue(isNarrowCommitTree);
b69ab31181
b69ab31182 const title = useAtomValue(latestCommitMessageTitle(commit.hash));
b69ab31183
b69ab31184 const commitLabel = useAtomValue(commitLabelForCommit(commit.hash));
b69ab31185
b69ab31186 const clipboardCopy = (text: string, url?: string) =>
b69ab31187 copyAndShowToast(text, url == null ? undefined : clipboardLinkHtml(text, url));
b69ab31188
b69ab31189 const confirmUnsavedEditsBeforeSplit = useConfirmUnsavedEditsBeforeSplit();
b69ab31190 async function handleSplit() {
b69ab31191 if (!(await confirmUnsavedEditsBeforeSplit([commit], 'split'))) {
b69ab31192 return;
b69ab31193 }
b69ab31194 setEditStackIntentionHashes(['split', new Set([commit.hash])]);
b69ab31195 tracker.track('SplitOpenFromCommitContextMenu');
b69ab31196 }
b69ab31197
b69ab31198 const makeContextMenuOptions = () => {
b69ab31199 const syncStatus = readAtom(syncStatusAtom)?.get(commit.hash);
b69ab31200
b69ab31201 const items: Array<ContextMenuItem & {loggingLabel?: string}> = [
b69ab31202 {
b69ab31203 label: <T replace={{$hash: short(commit?.hash)}}>Copy Commit Hash "$hash"</T>,
b69ab31204 onClick: () => clipboardCopy(commit.hash),
b69ab31205 loggingLabel: 'Copy Commit Hash',
b69ab31206 },
b69ab31207 ];
b69ab31208 if (isPublic && readAtom(supportsBrowseUrlForHash)) {
b69ab31209 items.push({
b69ab31210 label: (
b69ab31211 <Row>
b69ab31212 <T>Browse Repo At This Commit</T>
b69ab31213 <Icon icon="link-external" />
b69ab31214 </Row>
b69ab31215 ),
b69ab31216 onClick: () => {
b69ab31217 openBrowseUrlForHash(commit.hash);
b69ab31218 },
b69ab31219 loggingLabel: 'Browse Repo At This Commit',
b69ab31220 });
b69ab31221 }
b69ab31222 const selectedDiffIDs = readAtom(selectedCommitInfos)
b69ab31223 .map(info => info.diffId)
b69ab31224 .filter(notEmpty);
b69ab31225 if (selectedDiffIDs.length > 1) {
b69ab31226 items.push({
b69ab31227 label: (
b69ab31228 <T replace={{$count: selectedDiffIDs.length < 10 ? selectedDiffIDs.length : '9+'}}>
b69ab31229 Copy $count Selected Diff Numbers
b69ab31230 </T>
b69ab31231 ),
b69ab31232 onClick: () => {
b69ab31233 const text = selectedDiffIDs.join(' ');
b69ab31234 clipboardCopy(text);
b69ab31235 },
b69ab31236 loggingLabel: 'Copy Selected Diff Numbers',
b69ab31237 });
b69ab31238 } else if (!isPublic && commit.diffId != null) {
b69ab31239 items.push({
b69ab31240 label: <T replace={{$number: commit.diffId}}>Copy Diff Number "$number"</T>,
b69ab31241 onClick: () => {
b69ab31242 const info = readAtom(diffSummary(commit.diffId));
b69ab31243 const url = info?.value?.url;
b69ab31244 clipboardCopy(commit.diffId ?? '', url);
b69ab31245 },
b69ab31246 loggingLabel: 'Copy Diff Number',
b69ab31247 });
b69ab31248 }
b69ab31249 if (!isPublic) {
b69ab31250 items.push({
b69ab31251 label: <T>View Changes in Commit</T>,
b69ab31252 onClick: () => showComparison({type: ComparisonType.Committed, hash: commit.hash}),
b69ab31253 loggingLabel: 'View Changes in Commit',
b69ab31254 });
b69ab31255
b69ab31256 const provider = readAtom(codeReviewProvider);
b69ab31257 if (provider != null) {
b69ab31258 const selectedInfos = readAtom(selectedCommitInfos);
b69ab31259 const diffSummaries = readAtom(allDiffSummaries);
b69ab31260 const dag = readAtom(dagWithPreviews);
b69ab31261
b69ab31262 const isMultiSelect =
b69ab31263 selectedInfos.length > 1 && selectedInfos.some(c => c.hash === commit.hash);
b69ab31264 const commits = isMultiSelect
b69ab31265 ? dag.getBatch(dag.sortAsc(dag.present(new Set(selectedInfos.map(c => c.hash)))))
b69ab31266 : [commit];
b69ab31267 const submittable =
b69ab31268 (diffSummaries?.value != null
b69ab31269 ? provider.getSubmittableDiffs(commits, diffSummaries.value)
b69ab31270 : undefined) ?? [];
b69ab31271
b69ab31272 if (submittable.length > 0) {
8d8e815273 const actionLabel = provider.submitButtonLabel ?? 'Submit';
b69ab31274 items.push({
b69ab31275 label:
b69ab31276 submittable.length > 1 ? (
8d8e815277 <T replace={{$count: submittable.length, $action: actionLabel}}>
8d8e815278 $action $count Commits
8d8e815279 </T>
b69ab31280 ) : (
8d8e815281 <T replace={{$action: actionLabel}}>$action Commit</T>
b69ab31282 ),
b69ab31283 onClick: () => {
b69ab31284 runOperation(
b69ab31285 provider.submitOperation(submittable, {
b69ab31286 draft: readAtom(submitAsDraft),
b69ab31287 }),
b69ab31288 );
b69ab31289 },
8d8e815290 loggingLabel:
8d8e815291 submittable.length > 1
8d8e815292 ? `${actionLabel} Multiple Commits`
8d8e815293 : `${actionLabel} Commit`,
b69ab31294 });
b69ab31295 }
b69ab31296 }
b69ab31297 }
b69ab31298 if (!isPublic && syncStatus != null && syncStatus !== SyncStatus.InSync) {
b69ab31299 const provider = readAtom(codeReviewProvider);
b69ab31300 if (provider?.supportsComparingSinceLastSubmit) {
b69ab31301 items.push({
b69ab31302 label: <T replace={{$provider: provider?.label ?? 'remote'}}>Compare with $provider</T>,
b69ab31303 onClick: () => {
b69ab31304 showComparison({type: ComparisonType.SinceLastCodeReviewSubmit, hash: commit.hash});
b69ab31305 },
b69ab31306 loggingLabel: 'Compare with Provider',
b69ab31307 });
b69ab31308 }
b69ab31309 }
b69ab31310 if (!isPublic && commit.diffId != null) {
b69ab31311 const provider = readAtom(codeReviewProvider);
b69ab31312 const summary = readAtom(diffSummary(commit.diffId));
b69ab31313 if (summary.value) {
b69ab31314 const actions = provider?.getUpdateDiffActions(summary.value);
b69ab31315 if (actions != null && actions.length > 0) {
b69ab31316 items.push({
b69ab31317 label: <T replace={{$number: commit.diffId}}>Update Diff $number</T>,
b69ab31318 type: 'submenu',
b69ab31319 children: actions,
b69ab31320 });
b69ab31321 }
b69ab31322 }
b69ab31323 }
b69ab31324 if (!isPublic && !actionsPrevented && !inConflicts) {
b69ab31325 const suggestedRebases = readAtom(suggestedRebaseDestinations);
b69ab31326 items.push({
b69ab31327 label: 'Rebase onto',
b69ab31328 type: 'submenu',
b69ab31329 children:
b69ab31330 suggestedRebases?.map(([dest, name]) => ({
b69ab31331 label: name,
b69ab31332 onClick: async () => {
b69ab31333 const operation = getSuggestedRebaseOperation(
b69ab31334 dest,
b69ab31335 latestSuccessorUnlessExplicitlyObsolete(commit),
b69ab31336 );
b69ab31337
b69ab31338 const shouldProceed = await runWarningChecks([
b69ab31339 () => maybeWarnAboutRebaseOntoMaster(dest),
b69ab31340 () => maybeWarnAboutRebaseOffWarm(dest),
b69ab31341 ]);
b69ab31342
b69ab31343 if (shouldProceed) {
b69ab31344 runOperation(operation);
b69ab31345 }
b69ab31346 },
b69ab31347 })) ?? [],
b69ab31348 });
b69ab31349 if (isAmendToAllowedForCommit(commit)) {
b69ab31350 items.push({
b69ab31351 label: <T>Amend changes to here</T>,
b69ab31352 onClick: () => runOperation(getAmendToOperation(commit)),
b69ab31353 });
b69ab31354 }
b69ab31355 if (!isObsoleted) {
b69ab31356 items.push({
b69ab31357 label: hasUncommittedChanges ? (
b69ab31358 <span className="context-menu-disabled-option">
b69ab31359 <T>Split... </T>
b69ab31360 <Subtle>
b69ab31361 <T>(disabled due to uncommitted changes)</T>
b69ab31362 </Subtle>
b69ab31363 </span>
b69ab31364 ) : (
b69ab31365 <T>Split...</T>
b69ab31366 ),
b69ab31367 onClick: hasUncommittedChanges ? () => null : handleSplit,
b69ab31368 loggingLabel: 'Split',
b69ab31369 });
b69ab31370 }
b69ab31371 items.push({
b69ab31372 label: <T>Create Bookmark...</T>,
b69ab31373 onClick: () => {
b69ab31374 createBookmarkAtCommit(commit);
b69ab31375 },
b69ab31376 loggingLabel: 'Create Bookmark',
b69ab31377 });
b69ab31378 items.push({
b69ab31379 label: hasChildren ? <T>Hide Commit and Descendants</T> : <T>Hide Commit</T>,
b69ab31380 onClick: () =>
b69ab31381 writeAtom(
b69ab31382 operationBeingPreviewed,
b69ab31383 new HideOperation(latestSuccessorUnlessExplicitlyObsolete(commit)),
b69ab31384 ),
b69ab31385 loggingLabel: 'Hide Commit',
b69ab31386 });
b69ab31387 }
b69ab31388 if (!actionsPrevented && !commit.isDot) {
b69ab31389 items.push({
b69ab31390 label: <T>Goto</T>,
b69ab31391 onClick: async () => {
b69ab31392 await gotoAction(runOperation, commit);
b69ab31393 },
b69ab31394 loggingLabel: 'Goto',
b69ab31395 });
b69ab31396 }
b69ab31397 if (commit.fullRepoBranch != null) {
b69ab31398 const fullRepoBranchItems = Internal.getFullRepoBranchMergeContextMenuItems?.(
b69ab31399 commit.fullRepoBranch,
b69ab31400 );
b69ab31401 if (fullRepoBranchItems != null && fullRepoBranchItems instanceof Array) {
b69ab31402 items.push(...fullRepoBranchItems);
b69ab31403 }
b69ab31404 }
b69ab31405 return items;
b69ab31406 };
b69ab31407
b69ab31408 const contextMenu = useContextMenu((): Array<ContextMenuItem> => {
b69ab31409 return makeContextMenuOptions().map((item: ContextMenuItem & {loggingLabel?: string}) => {
b69ab31410 if (item.type == null && notEmpty(item.loggingLabel)) {
b69ab31411 return {
b69ab31412 ...item,
b69ab31413 onClick: () => {
b69ab31414 tracker.track('CommitContextMenuItemClick', {
b69ab31415 extras: {choice: item.loggingLabel},
b69ab31416 });
b69ab31417 item.onClick?.();
b69ab31418 },
b69ab31419 };
b69ab31420 }
b69ab31421 return item;
b69ab31422 });
b69ab31423 });
b69ab31424
b69ab31425 const commitActions = [];
b69ab31426
b69ab31427 if (previewType === CommitPreview.REBASE_ROOT) {
b69ab31428 commitActions.push(
b69ab31429 <React.Fragment key="rebase">
b69ab31430 <Button onClick={() => handlePreviewedOperation(/* cancel */ true)}>
b69ab31431 <T>Cancel</T>
b69ab31432 </Button>
b69ab31433 <Button
b69ab31434 primary
b69ab31435 onClick={() => {
b69ab31436 return handleRebaseConfirmation(commit, handlePreviewedOperation);
b69ab31437 }}>
b69ab31438 <T>Run Rebase</T>
b69ab31439 </Button>
b69ab31440 </React.Fragment>,
b69ab31441 );
b69ab31442 } else if (previewType === CommitPreview.HIDDEN_ROOT) {
b69ab31443 commitActions.push(
b69ab31444 <React.Fragment key="hide">
b69ab31445 <Button onClick={() => handlePreviewedOperation(/* cancel */ true)}>
b69ab31446 <T>Cancel</T>
b69ab31447 </Button>
b69ab31448 <ConfirmHideButton onClick={() => handlePreviewedOperation(/* cancel */ false)} />
b69ab31449 </React.Fragment>,
b69ab31450 );
b69ab31451 } else if (previewType === CommitPreview.FOLD_PREVIEW) {
b69ab31452 commitActions.push(<ConfirmCombineButtons key="fold" />);
b69ab31453 }
b69ab31454
b69ab31455 if (!isPublic && !actionsPrevented && isSelected) {
b69ab31456 commitActions.push(
b69ab31457 <SubmitSelectionButton key="submit-selection-btn" commit={commit} />,
b69ab31458 <FoldButton key="fold-button" commit={commit} />,
b69ab31459 );
b69ab31460 }
b69ab31461
b69ab31462 if (!actionsPrevented && !commit.isDot) {
b69ab31463 commitActions.push(
b69ab31464 <span className="goto-button" key="goto-button">
b69ab31465 <Tooltip
b69ab31466 title={t(
b69ab31467 'Update files in the working copy to match this commit. Mark this commit as the "current commit".',
b69ab31468 )}
b69ab31469 delayMs={250}>
b69ab31470 <Button
b69ab31471 aria-label={t('Go to commit "$title"', {replace: {$title: commit.title}})}
b69ab31472 xstyle={styles.gotoButton}
b69ab31473 onClick={async event => {
b69ab31474 event.stopPropagation(); // don't toggle selection by letting click propagate onto selection target.
b69ab31475 await gotoAction(runOperation, commit);
b69ab31476 }}>
b69ab31477 <T>Goto</T>
b69ab31478 <Icon icon="newline" />
b69ab31479 </Button>
b69ab31480 </Tooltip>
b69ab31481 </span>,
b69ab31482 );
b69ab31483 }
b69ab31484
b69ab31485 if (!isPublic && !actionsPrevented && commit.isDot && !inConflicts) {
b69ab31486 commitActions.push(<SubmitSingleCommitButton key="submit" />);
b69ab31487 commitActions.push(<UncommitButton key="uncommit" />);
b69ab31488 }
b69ab31489
b69ab31490 if (!isPublic && !actionsPrevented && commit.isDot && !isObsoleted && !inConflicts) {
b69ab31491 commitActions.push(
b69ab31492 <SplitButton icon key="split" trackerEventName="SplitOpenFromHeadCommit" commit={commit} />,
b69ab31493 );
b69ab31494 }
b69ab31495
b69ab31496 const useV2SmartActions = useFeatureFlagSync(Internal.featureFlags?.SmartActionsRedesign);
b69ab31497 if (!isPublic && !actionsPrevented) {
b69ab31498 if (useV2SmartActions) {
b69ab31499 if (commit.isDot && !hasUncommittedChanges && !inConflicts) {
b69ab31500 commitActions.push(<SmartActionsDropdown key="smartActions" commit={commit} />);
b69ab31501 }
b69ab31502 } else {
b69ab31503 commitActions.push(<SmartActionsMenu key="smartActions" commit={commit} />);
b69ab31504 }
b69ab31505 }
b69ab31506
ab83ad3507 if (!actionsPrevented) {
b69ab31508 commitActions.push(
b69ab31509 <OpenCommitInfoButton
b69ab31510 key="open-sidebar"
b69ab31511 revealCommit={onDoubleClickToShowDrawer}
b69ab31512 commit={commit}
b69ab31513 />,
b69ab31514 );
b69ab31515 }
b69ab31516
b69ab31517 if ((commit as DagCommitInfo).isYouAreHere) {
b69ab31518 return (
b69ab31519 <div className="head-commit-info">
b69ab31520 <UncommittedChanges place="main" />
b69ab31521 </div>
b69ab31522 );
b69ab31523 }
b69ab31524
b69ab31525 return (
b69ab31526 <div
b69ab31527 className={
b69ab31528 'commit' +
b69ab31529 (commit.isDot ? ' head-commit' : '') +
b69ab31530 (commit.successorInfo != null ? ' obsolete' : '') +
b69ab31531 (isIrrelevantToCwd ? ' irrelevant' : '')
b69ab31532 }
b69ab31533 onContextMenu={e => {
b69ab31534 writeAtom(actioningCommit, commit.hash);
b69ab31535 contextMenu(e);
b69ab31536 }}
b69ab31537 data-testid={`commit-${commit.hash}`}>
b69ab31538 <div
b69ab31539 className={
b69ab31540 'commit-rows' +
b69ab31541 (isSelected ? ' commit-row-selected' : '') +
b69ab31542 (isActioning ? ' commit-row-actioning' : '')
b69ab31543 }
b69ab31544 data-testid={isSelected ? 'selected-commit' : undefined}>
b69ab31545 <DragToRebase
b69ab31546 className={
b69ab31547 'commit-details' + (previewType != null ? ` commit-preview-${previewType}` : '')
b69ab31548 }
b69ab31549 commit={commit}
b69ab31550 previewType={previewType}>
b69ab31551 {!isPublic && isIrrelevantToCwd && (
b69ab31552 <Tooltip
b69ab31553 title={
b69ab31554 <T
b69ab31555 replace={{
b69ab31556 $prefix: <pre>{commit.maxCommonPathPrefix}</pre>,
b69ab31557 $cwd: <pre>{readAtom(repoRelativeCwd)}</pre>,
b69ab31558 }}>
b69ab31559 This commit only contains files within: $prefix These are irrelevant to your
b69ab31560 current working directory: $cwd
b69ab31561 </T>
b69ab31562 }>
b69ab31563 <IrrelevantCwdIcon />
b69ab31564 </Tooltip>
b69ab31565 )}
ab83ad3566 <span className="commit-title">
ab83ad3567 {commitLabel && <CommitLabel>{commitLabel}</CommitLabel>}
ab83ad3568 <span>{title}</span>
ab83ad3569 <CommitDate date={commit.date} />
ab83ad3570 </span>
b69ab31571 <UnsavedEditedMessageIndicator commit={commit} />
b69ab31572 {!isPublic && <BranchingPrs bookmarks={commit.remoteBookmarks} />}
b69ab31573 <AllBookmarksTruncated
b69ab31574 local={commit.bookmarks}
b69ab31575 remote={
b69ab31576 isPublic
b69ab31577 ? commit.remoteBookmarks
b69ab31578 : /* draft commits with remote bookmarks are probably branching PRs, rendered above. */ []
b69ab31579 }
b69ab31580 stable={commit?.stableCommitMetadata ?? []}
b69ab31581 fullRepoBranch={commit.fullRepoBranch}
b69ab31582 />
b69ab31583 {isNarrow ? commitActions : null}
b69ab31584 </DragToRebase>
b69ab31585 <DivIfChildren className="commit-second-row">
e625552586 {!isPublic ? (
b69ab31587 <DiffInfo commit={commit} hideActions={actionsPrevented || inlineProgress != null} />
b69ab31588 ) : null}
083fd5f589 {isPublic ? <CanopySignal hash={commit.hash} /> : null}
b69ab31590 {commit.successorInfo != null ? (
b69ab31591 <SuccessorInfoToDisplay successorInfo={commit.successorInfo} />
b69ab31592 ) : null}
b69ab31593 {inlineProgress && <InlineProgressSpan message={inlineProgress} />}
b69ab31594 {commit.isFollower ? <DiffFollower commit={commit} /> : null}
b69ab31595 </DivIfChildren>
b69ab31596 {!isNarrow ? commitActions : null}
b69ab31597 </div>
b69ab31598 </div>
b69ab31599 );
b69ab31600 },
b69ab31601 (prevProps, nextProps) => {
b69ab31602 const prevCommit = prevProps.commit;
b69ab31603 const nextCommit = nextProps.commit;
b69ab31604 const commitEqual =
b69ab31605 'equals' in nextCommit ? nextCommit.equals(prevCommit) : nextCommit === prevCommit;
b69ab31606 return (
b69ab31607 commitEqual &&
b69ab31608 nextProps.previewType === prevProps.previewType &&
b69ab31609 nextProps.hasChildren === prevProps.hasChildren
b69ab31610 );
b69ab31611 },
b69ab31612);
b69ab31613
b69ab31614function BranchingPrs({bookmarks}: {bookmarks: ReadonlyArray<string>}) {
b69ab31615 const provider = useAtomValue(codeReviewProvider);
b69ab31616 if (provider == null || !provider.supportBranchingPrs) {
b69ab31617 // If we don't have a provider, just render them as bookmarks so they don't get hidden.
b69ab31618 return <Bookmarks bookmarks={bookmarks} kind="remote" />;
b69ab31619 }
b69ab31620 return bookmarks.map(bookmark => (
b69ab31621 <BranchingPr key={bookmark} provider={provider} bookmark={bookmark} />
b69ab31622 ));
b69ab31623}
b69ab31624
b69ab31625function BranchingPr({bookmark, provider}: {bookmark: string; provider: UICodeReviewProvider}) {
b69ab31626 const branchName = nullthrows(provider.branchNameForRemoteBookmark)(bookmark);
b69ab31627 const info = useAtomValue(branchingDiffInfos(branchName));
b69ab31628 return (
b69ab31629 <>
b69ab31630 <Bookmark kind="remote">{bookmark}</Bookmark>
b69ab31631 {info.value == null ? null : (
b69ab31632 <DiffBadge diff={info.value} provider={provider} url={info.value.url} />
b69ab31633 )}
b69ab31634 </>
b69ab31635 );
b69ab31636}
b69ab31637
b69ab31638const styles = stylex.create({
b69ab31639 commitLabel: {
b69ab31640 fontVariant: 'all-petite-caps',
b69ab31641 opacity: '0.8',
b69ab31642 fontWeight: 'bold',
b69ab31643 fontSize: '90%',
b69ab31644 },
b69ab31645 gotoButton: {
b69ab31646 gap: spacing.half,
b69ab31647 },
b69ab31648});
b69ab31649
b69ab31650function CommitLabel({children}: {children?: ReactNode}) {
b69ab31651 return <div {...stylex.props(styles.commitLabel)}>{children}</div>;
b69ab31652}
b69ab31653
b69ab31654export function InlineProgressSpan(props: {message: string}) {
b69ab31655 return (
b69ab31656 <span className="commit-inline-operation-progress">
b69ab31657 <Icon icon="loading" /> <T>{props.message}</T>
b69ab31658 </span>
b69ab31659 );
b69ab31660}
b69ab31661
b69ab31662function OpenCommitInfoButton({
b69ab31663 commit,
b69ab31664 revealCommit,
b69ab31665}: {
b69ab31666 commit: CommitInfo;
b69ab31667 revealCommit: () => unknown;
b69ab31668}) {
b69ab31669 return (
b69ab31670 <Tooltip title={t("Open commit's details in sidebar")} delayMs={250}>
b69ab31671 <Button
b69ab31672 icon
b69ab31673 onClick={e => {
b69ab31674 revealCommit();
b69ab31675 e.stopPropagation();
b69ab31676 e.preventDefault();
b69ab31677 }}
b69ab31678 className="open-commit-info-button"
b69ab31679 aria-label={t('Open commit "$title"', {replace: {$title: commit.title}})}
b69ab31680 data-testid="open-commit-info-button">
b69ab31681 <Icon icon="chevron-right" />
b69ab31682 </Button>
b69ab31683 </Tooltip>
b69ab31684 );
b69ab31685}
b69ab31686
b69ab31687function ConfirmHideButton({onClick}: {onClick: () => unknown}) {
b69ab31688 const ref = useAutofocusRef() as React.MutableRefObject<null>;
b69ab31689 return (
b69ab31690 <Button ref={ref} primary onClick={onClick}>
b69ab31691 <T>Hide</T>
b69ab31692 </Button>
b69ab31693 );
b69ab31694}
b69ab31695
b69ab31696function ConfirmCombineButtons() {
b69ab31697 const ref = useAutofocusRef() as React.MutableRefObject<null>;
b69ab31698 const [cancel, run] = useRunFoldPreview();
b69ab31699
b69ab31700 return (
b69ab31701 <>
b69ab31702 <Button onClick={cancel}>
b69ab31703 <T>Cancel</T>
b69ab31704 </Button>
b69ab31705 <Button ref={ref} primary onClick={run}>
b69ab31706 <T>Run Combine</T>
b69ab31707 </Button>
b69ab31708 </>
b69ab31709 );
b69ab31710}
b69ab31711
b69ab31712function CommitDate({date}: {date: Date}) {
b69ab31713 return (
b69ab31714 <span className="commit-date" title={date.toLocaleString()}>
b69ab31715 <RelativeDate date={date} useShortVariant />
b69ab31716 </span>
b69ab31717 );
b69ab31718}
b69ab31719
b69ab31720function DivIfChildren({
b69ab31721 children,
b69ab31722 ...props
b69ab31723}: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>) {
b69ab31724 if (!children || (Array.isArray(children) && children.filter(notEmpty).length === 0)) {
b69ab31725 return null;
b69ab31726 }
b69ab31727 return <div {...props}>{children}</div>;
b69ab31728}
b69ab31729
b69ab31730function UnsavedEditedMessageIndicator({commit}: {commit: CommitInfo}) {
b69ab31731 const isEdted = useAtomValue(hasUnsavedEditedCommitMessage(commit.hash));
b69ab31732 if (!isEdted) {
b69ab31733 return null;
b69ab31734 }
b69ab31735 return (
b69ab31736 <div className="unsaved-message-indicator" data-testid="unsaved-message-indicator">
b69ab31737 <Tooltip title={t('This commit has unsaved changes to its message')}>
b69ab31738 <IconStack>
b69ab31739 <Icon icon="output" />
b69ab31740 <Icon icon="circle-large-filled" />
b69ab31741 </IconStack>
b69ab31742 </Tooltip>
b69ab31743 </div>
b69ab31744 );
b69ab31745}
b69ab31746
b69ab31747export function SuccessorInfoToDisplay({successorInfo}: {successorInfo: SuccessorInfo}) {
b69ab31748 const successorType = successorInfo.type;
b69ab31749 const inner: JSX.Element = {
b69ab31750 pushrebase: <T>Landed as a newer commit</T>,
b69ab31751 land: <T>Landed as a newer commit</T>,
b69ab31752 amend: <T>Amended as a newer commit</T>,
b69ab31753 rebase: <T>Rebased as a newer commit</T>,
b69ab31754 split: <T>Split as a newer commit</T>,
b69ab31755 fold: <T>Folded as a newer commit</T>,
b69ab31756 histedit: <T>Histedited as a newer commit</T>,
b69ab31757 }[successorType] ?? <T>Rewritten as a newer commit</T>;
b69ab31758 const isSuccessorPublic = successorType === 'land' || successorType === 'pushrebase';
b69ab31759 return (
b69ab31760 <Row style={{gap: 'var(--halfpad)'}}>
b69ab31761 <HighlightCommitsWhileHovering toHighlight={[successorInfo.hash]}>
b69ab31762 {inner}
b69ab31763 </HighlightCommitsWhileHovering>
b69ab31764 <EducationInfoTip>
b69ab31765 <ObsoleteTip isSuccessorPublic={isSuccessorPublic} />
b69ab31766 </EducationInfoTip>
b69ab31767 </Row>
b69ab31768 );
b69ab31769}
b69ab31770
b69ab31771function ObsoleteTipInner(props: {isSuccessorPublic?: boolean}) {
b69ab31772 const tips: string[] = props.isSuccessorPublic
b69ab31773 ? [
b69ab31774 t('Avoid editing (e.g., amend, rebase) this obsoleted commit. It cannot be landed again.'),
b69ab31775 t(
b69ab31776 'The new commit was landed in a public branch and became immutable. It cannot be edited or hidden.',
b69ab31777 ),
b69ab31778 t('If you want to make changes, create a new commit.'),
b69ab31779 ]
b69ab31780 : [
b69ab31781 t(
b69ab31782 'Avoid editing (e.g., amend, rebase) this obsoleted commit. You should use the new commit instead.',
b69ab31783 ),
b69ab31784 t(
b69ab31785 'If you do edit, there will be multiple new versions. They look like duplications and there is no easy way to de-duplicate (e.g. merge all edits back into one commit).',
b69ab31786 ),
b69ab31787 t(
b69ab31788 'To revert to this obsoleted commit, simply hide the new one. It will remove the "obsoleted" status.',
b69ab31789 ),
b69ab31790 ];
b69ab31791
b69ab31792 return (
b69ab31793 <div style={{maxWidth: '60vw'}}>
b69ab31794 <T>This commit is "obsoleted" because a newer version exists.</T>
b69ab31795 <ul>
b69ab31796 {tips.map((tip, i) => (
b69ab31797 <li key={i}>{tip}</li>
b69ab31798 ))}
b69ab31799 </ul>
b69ab31800 </div>
b69ab31801 );
b69ab31802}
b69ab31803
b69ab31804async function maybeWarnAboutOldDestination(dest: CommitInfo): Promise<WarningCheckResult> {
b69ab31805 const provider = readAtom(codeReviewProvider);
b69ab31806 // Cutoff age is determined by the code review provider since internal repos have different requirements than GitHub-backed repos.
b69ab31807 const MAX_AGE_CUTOFF_MS = provider?.gotoDistanceWarningAgeCutoff ?? 30 * MS_PER_DAY;
b69ab31808
b69ab31809 const dag = readAtom(dagWithPreviews);
b69ab31810 const currentBase = findPublicBaseAncestor(dag);
b69ab31811 const destBase = findPublicBaseAncestor(dag, dest.hash);
b69ab31812 if (!currentBase || !destBase) {
b69ab31813 // can't determine if we can show warning
b69ab31814 return WarningCheckResult.PASS;
b69ab31815 }
b69ab31816
b69ab31817 const ageDiff = currentBase.date.valueOf() - destBase.date.valueOf();
b69ab31818 if (ageDiff < MAX_AGE_CUTOFF_MS) {
b69ab31819 // Either destination base is within time limit or destination base is newer than the current base.
b69ab31820 // No need to warn.
b69ab31821 return WarningCheckResult.PASS;
b69ab31822 }
b69ab31823
b69ab31824 const confirmed = await platform.confirm(
b69ab31825 t(
b69ab31826 Internal.warnAboutOldGotoReason ??
b69ab31827 'The destination commit is $age older than the current commit. ' +
b69ab31828 "Going here may be slow. It's often faster to rebase the commit to a newer base before going. " +
b69ab31829 'Do you want to `goto` anyway?',
b69ab31830 {
b69ab31831 replace: {
b69ab31832 $age: relativeDate(destBase.date, {reference: currentBase.date, useRelativeForm: true}),
b69ab31833 },
b69ab31834 },
b69ab31835 ),
b69ab31836 );
b69ab31837 return confirmed ? WarningCheckResult.BYPASS : WarningCheckResult.FAIL;
b69ab31838}
b69ab31839
b69ab31840async function maybeWarnAboutRebaseOffWarm(dest: CommitInfo): Promise<WarningCheckResult> {
b69ab31841 const isRebaseOffWarmWarningEnabled = readAtom(rebaseOffWarmWarningEnabled);
b69ab31842 if (!isRebaseOffWarmWarningEnabled || dest.stableCommitMetadata == null) {
b69ab31843 return WarningCheckResult.PASS;
b69ab31844 }
b69ab31845
b69ab31846 const dag = readAtom(dagWithPreviews);
b69ab31847 const src = findPublicBaseAncestor(dag);
b69ab31848 const destBase = findPublicBaseAncestor(dag, dest.hash);
b69ab31849
b69ab31850 if (!src || !destBase) {
b69ab31851 return WarningCheckResult.PASS;
b69ab31852 }
b69ab31853
b69ab31854 if (Internal.maybeWarnAboutRebaseOffWarm?.(src, destBase)) {
b69ab31855 const buttons = [
b69ab31856 t('Opt Out of Future Warnings'),
b69ab31857 {label: t('Cancel'), primary: true},
b69ab31858 t('Continue Anyway'),
b69ab31859 ];
b69ab31860 const answer = await showModal({
b69ab31861 type: 'confirm',
b69ab31862 buttons,
b69ab31863 title: <T>Move off Warm Commit</T>,
b69ab31864 message: t(
b69ab31865 Internal.warnAboutRebaseOffWarmReason ??
b69ab31866 "The commit you're on is a warmed up commit. Moving off will cause slower builds and performance.\n" +
b69ab31867 "It's recommended to rebase your changes onto the warmed up commit instead.\n" +
b69ab31868 "If you need fresher changes, it's recommended to reserve a new OD and work off the warm commit.\n" +
b69ab31869 'Do you want to continue anyway?',
b69ab31870 ),
b69ab31871 });
b69ab31872 const userEnv = (await Internal.getDevEnvType?.()) ?? 'NotImplemented';
b69ab31873 const cwd = readAtom(repoRelativeCwd);
b69ab31874 tracker.track('WarnAboutRebaseOffWarm', {
b69ab31875 extras: {
b69ab31876 userAction: answer,
b69ab31877 envType: userEnv,
b69ab31878 cwd,
b69ab31879 },
b69ab31880 });
b69ab31881 if (answer === buttons[0]) {
b69ab31882 writeAtom(rebaseOffWarmWarningEnabled, false);
b69ab31883 return WarningCheckResult.PASS;
b69ab31884 }
b69ab31885 return answer === buttons[2] ? WarningCheckResult.BYPASS : WarningCheckResult.FAIL;
b69ab31886 }
b69ab31887
b69ab31888 return WarningCheckResult.PASS;
b69ab31889}
b69ab31890
b69ab31891async function maybeWarnAboutRebaseOntoMaster(commit: CommitInfo): Promise<WarningCheckResult> {
b69ab31892 const isRebaseOntoMasterWarningEnabled = readAtom(rebaseOntoMasterWarningEnabled);
b69ab31893 if (!isRebaseOntoMasterWarningEnabled) {
b69ab31894 return WarningCheckResult.PASS;
b69ab31895 }
b69ab31896
b69ab31897 const dag = readAtom(dagWithPreviews);
b69ab31898 const src = findPublicBaseAncestor(dag);
b69ab31899 const destBase = findPublicBaseAncestor(dag, commit.hash);
b69ab31900
b69ab31901 if (!src || !destBase) {
b69ab31902 // can't determine if we can show warning
b69ab31903 return WarningCheckResult.PASS;
b69ab31904 }
b69ab31905
b69ab31906 if (Internal.maybeWarnAboutRebaseOntoMaster?.(src, destBase)) {
b69ab31907 const buttons = [
b69ab31908 t('Opt Out of Future Warnings'),
b69ab31909 {label: t('Cancel'), primary: true},
b69ab31910 t('Continue Anyway'),
b69ab31911 ];
b69ab31912 const answer = await showModal({
b69ab31913 type: 'confirm',
b69ab31914 buttons,
b69ab31915 title: <T>Rebase onto Master Warning</T>,
b69ab31916 message: t(
b69ab31917 Internal.warnAboutRebaseOntoMasterReason ??
b69ab31918 'You are about to rebase directly onto master/main. ' +
b69ab31919 'This is generally not recommended as it can cause unexpected failures and slower builds. ' +
b69ab31920 'Consider rebasing onto a stable or warm branch instead. ' +
b69ab31921 'Do you want to continue anyway?',
b69ab31922 ),
b69ab31923 });
b69ab31924 const userEnv = (await Internal.getDevEnvType?.()) ?? 'NotImplemented';
b69ab31925 const cwd = readAtom(repoRelativeCwd);
b69ab31926 tracker.track('WarnAboutRebaseOntoMaster', {
b69ab31927 extras: {
b69ab31928 userAction: answer,
b69ab31929 envType: userEnv,
b69ab31930 cwd,
b69ab31931 },
b69ab31932 });
b69ab31933 if (answer === buttons[0]) {
b69ab31934 writeAtom(rebaseOntoMasterWarningEnabled, false);
b69ab31935 return WarningCheckResult.PASS;
b69ab31936 }
b69ab31937 return answer === buttons[2] ? WarningCheckResult.BYPASS : WarningCheckResult.FAIL;
b69ab31938 }
b69ab31939
b69ab31940 return WarningCheckResult.PASS;
b69ab31941}
b69ab31942
b69ab31943async function gotoAction(runOperation: ReturnType<typeof useRunOperation>, commit: CommitInfo) {
b69ab31944 const shouldProceed = await runWarningChecks([
b69ab31945 () => maybeWarnAboutRebaseOntoMaster(commit),
b69ab31946 () => maybeWarnAboutOldDestination(commit),
b69ab31947 () => maybeWarnAboutRebaseOffWarm(commit),
b69ab31948 ]);
b69ab31949
b69ab31950 if (!shouldProceed) {
b69ab31951 return;
b69ab31952 }
b69ab31953
b69ab31954 const dest =
b69ab31955 // If the commit has a remote bookmark, use that instead of the hash. This is easier to read in the command history
b69ab31956 // and works better with optimistic state
b69ab31957 commit.remoteBookmarks.length > 0
b69ab31958 ? succeedableRevset(commit.remoteBookmarks[0])
b69ab31959 : latestSuccessorUnlessExplicitlyObsolete(commit);
b69ab31960 runOperation(new GotoOperation(dest));
b69ab31961 // Instead of propagating, ensure we remove the selection, so we view the new head commit by default
b69ab31962 // (since the head commit is the default thing shown in the sidebar)
b69ab31963 writeAtom(selectedCommits, new Set());
b69ab31964}
b69ab31965
b69ab31966const ObsoleteTip = React.memo(ObsoleteTipInner);
b69ab31967
b69ab31968/**
b69ab31969 * Runs a series of validation checks sequentially. Returns true if all checks pass
b69ab31970 * or the user manually bypassed a warning, otherwise returns false if any check fails.
b69ab31971 */
b69ab31972async function runWarningChecks(
b69ab31973 checks: Array<() => Promise<WarningCheckResult>>,
b69ab31974): Promise<boolean> {
b69ab31975 for (const check of checks) {
b69ab31976 // eslint-disable-next-line no-await-in-loop
b69ab31977 const result = await check();
b69ab31978 if (result !== WarningCheckResult.PASS) {
b69ab31979 return result === WarningCheckResult.BYPASS;
b69ab31980 }
b69ab31981 }
b69ab31982 return true;
b69ab31983}
b69ab31984
b69ab31985async function handleRebaseConfirmation(
b69ab31986 commit: CommitInfo,
b69ab31987 handlePreviewedOperation: (cancel: boolean) => void,
b69ab31988): Promise<void> {
b69ab31989 const shouldProceed = await runWarningChecks([
b69ab31990 () => maybeWarnAboutRebaseOntoMaster(commit),
b69ab31991 () => maybeWarnAboutDistantRebase(commit),
b69ab31992 () => maybeWarnAboutRebaseOffWarm(commit),
b69ab31993 ]);
b69ab31994
b69ab31995 if (!shouldProceed) {
b69ab31996 return;
b69ab31997 }
b69ab31998
b69ab31999 handlePreviewedOperation(/* cancel */ false);
b69ab311000
b69ab311001 const dag = readAtom(dagWithPreviews);
b69ab311002 const onto = dag.get(commit.parents[0]);
b69ab311003 if (onto) {
b69ab311004 tracker.track('ConfirmDragAndDropRebase', {
b69ab311005 extras: {
b69ab311006 remoteBookmarks: onto.remoteBookmarks,
b69ab311007 locations: onto.stableCommitMetadata?.map(s => s.value),
b69ab311008 },
b69ab311009 });
b69ab311010 }
b69ab311011}
b69ab311012
b69ab311013async function maybeWarnAboutDistantRebase(commit: CommitInfo): Promise<WarningCheckResult> {
b69ab311014 const isDistantRebaseWarningEnabled = readAtom(distantRebaseWarningEnabled);
b69ab311015 if (!isDistantRebaseWarningEnabled) {
b69ab311016 return WarningCheckResult.PASS;
b69ab311017 }
b69ab311018 const dag = readAtom(dagWithPreviews);
b69ab311019 const onto = dag.get(commit.parents[0]);
b69ab311020 if (!onto) {
b69ab311021 return WarningCheckResult.PASS; // If there's no target commit, proceed without warning
b69ab311022 }
b69ab311023 const currentBase = findPublicBaseAncestor(dag);
b69ab311024 const destBase = findPublicBaseAncestor(dag, onto.hash);
b69ab311025 if (!currentBase || !destBase) {
b69ab311026 // can't determine if we can show warning
b69ab311027 return WarningCheckResult.PASS;
b69ab311028 }
b69ab311029
b69ab311030 if (Internal.maybeWarnAboutDistantRebase?.(currentBase, destBase)) {
b69ab311031 const buttons = [
b69ab311032 t('Opt Out of Future Warnings'),
b69ab311033 {label: t('Cancel'), primary: true},
b69ab311034 t('Rebase Anyway'),
b69ab311035 ];
b69ab311036 const answer = await showModal({
b69ab311037 type: 'confirm',
b69ab311038 buttons,
b69ab311039 title: <T>Distant Rebase Warning</T>,
b69ab311040 message: t(
b69ab311041 Internal.warnAboutDistantRebaseReason ??
b69ab311042 'The target commit is $age away from your current commit. ' +
b69ab311043 'Rebasing across a large time gap may cause slower builds and performance. ' +
b69ab311044 "It's recommended to rebase the destination commit(s) to the nearest stable or warm commit first and then attempt this rebase. " +
b69ab311045 'Do you want to `rebase` anyway?',
b69ab311046 {
b69ab311047 replace: {
b69ab311048 $age: relativeDate(onto.date, {reference: commit.date, useRelativeForm: true}),
b69ab311049 },
b69ab311050 },
b69ab311051 ),
b69ab311052 });
b69ab311053 const userEnv = (await Internal.getDevEnvType?.()) ?? 'NotImplemented';
b69ab311054 const cwd = readAtom(repoRelativeCwd);
b69ab311055 tracker.track('WarnAboutDistantRebase', {
b69ab311056 extras: {
b69ab311057 userAction: answer,
b69ab311058 envType: userEnv,
b69ab311059 cwd,
b69ab311060 },
b69ab311061 });
b69ab311062 if (answer === buttons[0]) {
b69ab311063 writeAtom(distantRebaseWarningEnabled, false);
b69ab311064 return WarningCheckResult.PASS;
b69ab311065 }
b69ab311066 return answer === buttons[2] ? WarningCheckResult.BYPASS : WarningCheckResult.FAIL;
b69ab311067 }
b69ab311068
b69ab311069 return WarningCheckResult.PASS;
b69ab311070}
083fd5f1071
083fd5f1072function CanopySignal({hash}: {hash: Hash}) {
e7069e11073 useEffect(() => {
e7069e11074 ensureCanopySignalsFetched();
e7069e11075 }, []);
9e488cc1076 const info = useAtomValue(canopySignalForCommit(hash));
9e488cc1077 const repoInfo = useAtomValue(repositoryInfo);
9e488cc1078 if (!info) {
083fd5f1079 return null;
083fd5f1080 }
9e488cc1081 const handleClick =
e7069e11082 info.commitId != null && repoInfo?.codeReviewSystem.type === 'grove'
9e488cc1083 ? () => {
9e488cc1084 const {owner, repo, apiUrl} = repoInfo.codeReviewSystem as {
9e488cc1085 apiUrl: string;
9e488cc1086 owner: string;
9e488cc1087 repo: string;
9e488cc1088 };
9e488cc1089 const baseUrl = apiUrl.replace('/api', '').replace(/^https?:\/\//, '');
9e488cc1090 const canopyHost = `canopy.${baseUrl}`;
e7069e11091 const url = `https://${canopyHost}/${owner}/${repo}/runs/${info.commitId}`;
9e488cc1092 platform.openExternalLink(url);
9e488cc1093 }
9e488cc1094 : undefined;
9e488cc1095 return (
9e488cc1096 <span onClick={handleClick} style={handleClick ? {cursor: 'pointer'} : undefined}>
9e488cc1097 <SignalSummaryIcon signal={info.signal} />
9e488cc1098 </span>
9e488cc1099 );
083fd5f1100}