addons/isl/src/stackEdit/ui/StackEditConfirmButtons.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 {Hash} from '../../types';
b69ab319
b69ab3110import {Button} from 'isl-components/Button';
b69ab3111import {Icon} from 'isl-components/Icon';
b69ab3112import {DOCUMENTATION_DELAY, Tooltip} from 'isl-components/Tooltip';
b69ab3113import {useAtom, useAtomValue} from 'jotai';
b69ab3114import {useCallback} from 'react';
b69ab3115import serverAPI from '../../ClientToServerAPI';
b69ab3116import {
b69ab3117 editedCommitMessages,
b69ab3118 getDefaultEditedCommitMessage,
b69ab3119} from '../../CommitInfoView/CommitInfoState';
b69ab3120import {Internal} from '../../Internal';
b69ab3121import {tracker} from '../../analytics';
b69ab3122import {useFeatureFlagSync} from '../../featureFlags';
b69ab3123import {T, t} from '../../i18n';
b69ab3124import {writeAtom} from '../../jotaiUtils';
b69ab3125import {ImportStackOperation} from '../../operations/ImportStackOperation';
b69ab3126import {RebaseOperation} from '../../operations/RebaseOperation';
b69ab3127import {useRunOperation} from '../../operationsState';
b69ab3128import {latestDag, latestHeadCommit, repositoryInfo} from '../../serverAPIState';
b69ab3129import {exactRevset, succeedableRevset} from '../../types';
b69ab3130import {UndoDescription} from './StackEditSubTree';
b69ab3131import {
b69ab3132 bumpStackEditMetric,
b69ab3133 editingStackIntentionHashes,
b69ab3134 findStartEndRevs,
b69ab3135 sendStackEditMetrics,
b69ab3136 useStackEditState,
b69ab3137} from './stackEditState';
b69ab3138
b69ab3139import './StackEditSubTree.css';
b69ab3140
b69ab3141export function StackEditConfirmButtons(): React.ReactElement {
b69ab3142 const [[stackIntention], setStackIntentionHashes] = useAtom(editingStackIntentionHashes);
b69ab3143 const originalHead = useAtomValue(latestHeadCommit);
b69ab3144 const dag = useAtomValue(latestDag);
b69ab3145 const runOperation = useRunOperation();
b69ab3146 const stackEdit = useStackEditState();
b69ab3147
b69ab3148 const canUndo = stackEdit.canUndo();
b69ab3149 const canRedo = stackEdit.canRedo();
b69ab3150
b69ab3151 const handleUndo = () => {
b69ab3152 stackEdit.undo();
b69ab3153 bumpStackEditMetric('undo');
b69ab3154 };
b69ab3155
b69ab3156 const handleRedo = () => {
b69ab3157 stackEdit.redo();
b69ab3158 bumpStackEditMetric('redo');
b69ab3159 };
b69ab3160
b69ab3161 /**
b69ab3162 * Invalidate any unsaved edited commit messages for the original commits,
b69ab3163 * to prevent detected successions from persisting that state.
b69ab3164 * Splitting can cause the top of the stack to be an unexpected
b69ab3165 * successor, leading to wrong commit messages.
b69ab3166 * We already showed a confirm modal to "apply" your edits to split,
b69ab3167 * but we actually need to delete them now that we're really
b69ab3168 * doing the split/edit stack.
b69ab3169 */
b69ab3170 const invalidateUnsavedCommitMessages = useCallback((commits: Array<Hash>) => {
b69ab3171 for (const hash of commits) {
b69ab3172 writeAtom(editedCommitMessages(hash), getDefaultEditedCommitMessage());
b69ab3173 }
b69ab3174 }, []);
b69ab3175
b69ab3176 const handleSaveChanges = () => {
b69ab3177 const originalHash = originalHead?.hash;
b69ab3178 const stack = stackEdit.commitStack.applyAbsorbEdits();
b69ab3179 const isAbsorb = stackEdit.intention === 'absorb';
b69ab3180 const importStack = stack.calculateImportStack({
b69ab3181 goto: originalHash,
b69ab3182 rewriteDate: Date.now() / 1000,
b69ab3183 // Do not write anything to the working copy, for absorb (esp. with partial selection)
b69ab3184 skipWdir: isAbsorb,
b69ab3185 // Also, preserve dirty files. So if an absorb edit is left "unabsorbed" in the "wdir()",
b69ab3186 // it will be preserved without being dropped.
b69ab3187 preserveDirtyFiles: isAbsorb,
b69ab3188 });
b69ab3189 const op = new ImportStackOperation(importStack, stack.originalStack);
b69ab3190 runOperation(op);
b69ab3191 sendStackEditMetrics(stackEdit, true);
b69ab3192
b69ab3193 invalidateUnsavedCommitMessages(stack.originalStack.map(c => c.node));
b69ab3194
b69ab3195 // For standalone split, follow-up with a rebase.
b69ab3196 // Note: the rebase might fail with conflicted pending changes.
b69ab3197 // rebase is technically incorrect if the user edits the changes.
b69ab3198 // We should move the rebase logic to debugimportstack and make
b69ab3199 // it handle pending changes just fine.
b69ab31100 const stackTop = stack.originalStack.at(-1)?.node;
b69ab31101 if (stackIntention === 'split' && stackTop != null) {
b69ab31102 const children = dag.children(stackTop);
b69ab31103 if (children.size > 0) {
b69ab31104 const rebaseOp = new RebaseOperation(
b69ab31105 exactRevset(children.toArray().join('|')),
b69ab31106 succeedableRevset(stackTop) /* stack top of the new successor */,
b69ab31107 );
b69ab31108 runOperation(rebaseOp);
b69ab31109 }
b69ab31110 }
b69ab31111 // Exit stack editing.
b69ab31112 setStackIntentionHashes(['general', new Set()]);
b69ab31113 };
b69ab31114
b69ab31115 const handleCancel = () => {
b69ab31116 sendStackEditMetrics(stackEdit, false);
b69ab31117 setStackIntentionHashes(['general', new Set<Hash>()]);
b69ab31118 };
b69ab31119
b69ab31120 // Get the commit hash for AI Split feature
b69ab31121 const [startRev] = findStartEndRevs(stackEdit);
b69ab31122 const {commitStack} = stackEdit;
b69ab31123 const repo = useAtomValue(repositoryInfo);
b69ab31124 const repoPath = repo?.repoRoot;
b69ab31125 const enableDevmateSplit = useFeatureFlagSync(Internal.featureFlags?.DevmateSplitCommit) ?? false;
b69ab31126
b69ab31127 // Get the commit hash from the start of the split range
b69ab31128 const startCommit = startRev != null ? commitStack.get(startRev) : null;
b69ab31129 const splitCommitHash =
b69ab31130 startCommit?.originalNodes != null ? [...startCommit.originalNodes][0] : null;
b69ab31131
b69ab31132 const handleAISplit = () => {
b69ab31133 if (splitCommitHash == null) {
b69ab31134 return;
b69ab31135 }
b69ab31136 const numFilesInCommit = startCommit?.files?.size ?? 0;
b69ab31137
b69ab31138 // Bump the metric to track clicks for acceptance rate calculation
b69ab31139 bumpStackEditMetric('clickedAiSplit');
b69ab31140
b69ab31141 tracker.track('DevmateSplitWithDevmateButtonClicked', {
b69ab31142 extras: {
b69ab31143 action: 'SplitCommit',
b69ab31144 source: 'splitUI',
b69ab31145 commitHash: splitCommitHash,
b69ab31146 numFilesInCommit,
b69ab31147 stackIntention,
b69ab31148 },
b69ab31149 });
b69ab31150 serverAPI.postMessage({
b69ab31151 type: 'platform/splitCommitWithAI',
b69ab31152 diffCommit: splitCommitHash,
b69ab31153 repoPath,
b69ab31154 });
b69ab31155 };
b69ab31156
b69ab31157 let cancelTooltip = t('Discard stack editing changes');
b69ab31158 let confirmTooltip = t('Save stack editing changes');
b69ab31159 let confirmText = t('Save changes');
b69ab31160 switch (stackIntention) {
b69ab31161 case 'split':
b69ab31162 cancelTooltip = t('Cancel split');
b69ab31163 confirmTooltip = t('Apply split changes');
b69ab31164 confirmText = t('Split');
b69ab31165 break;
b69ab31166 case 'absorb':
b69ab31167 cancelTooltip = t('Cancel absorb');
b69ab31168 confirmTooltip = t('Apply absorb changes');
b69ab31169 confirmText = t('Absorb');
b69ab31170 break;
b69ab31171 }
b69ab31172
b69ab31173 // Show [AI Split] [Undo] [Redo] [Cancel] [Save changes].
b69ab31174 return (
b69ab31175 <>
b69ab31176 {stackIntention === 'split' &&
b69ab31177 enableDevmateSplit &&
b69ab31178 splitCommitHash != null &&
b69ab31179 Internal.AISplitButton && <Internal.AISplitButton onClick={handleAISplit} />}
b69ab31180 {enableDevmateSplit && splitCommitHash != null && <div className="stack-edit-spacer" />}
b69ab31181 <Tooltip
b69ab31182 component={() =>
b69ab31183 canUndo ? (
b69ab31184 <T replace={{$op: <UndoDescription op={stackEdit.undoOperationDescription()} />}}>
b69ab31185 Undo $op
b69ab31186 </T>
b69ab31187 ) : (
b69ab31188 <T>No operations to undo</T>
b69ab31189 )
b69ab31190 }
b69ab31191 placement="bottom">
b69ab31192 <Button icon disabled={!canUndo} onClick={handleUndo}>
b69ab31193 <Icon icon="discard" />
b69ab31194 </Button>
b69ab31195 </Tooltip>
b69ab31196 <Tooltip
b69ab31197 component={() =>
b69ab31198 canRedo ? (
b69ab31199 <T replace={{$op: <UndoDescription op={stackEdit.redoOperationDescription()} />}}>
b69ab31200 Redo $op
b69ab31201 </T>
b69ab31202 ) : (
b69ab31203 <T>No operations to redo</T>
b69ab31204 )
b69ab31205 }
b69ab31206 placement="bottom">
b69ab31207 <Button icon disabled={!canRedo} onClick={handleRedo}>
b69ab31208 <Icon icon="redo" />
b69ab31209 </Button>
b69ab31210 </Tooltip>
b69ab31211 <Tooltip title={cancelTooltip} delayMs={DOCUMENTATION_DELAY} placement="bottom">
b69ab31212 <Button className="cancel-edit-stack-button" onClick={handleCancel}>
b69ab31213 <T>Cancel</T>
b69ab31214 </Button>
b69ab31215 </Tooltip>
b69ab31216 <Tooltip title={confirmTooltip} delayMs={DOCUMENTATION_DELAY} placement="bottom">
b69ab31217 <Button
b69ab31218 className="confirm-edit-stack-button"
b69ab31219 data-testid="confirm-edit-stack-button"
b69ab31220 primary
b69ab31221 onClick={handleSaveChanges}>
b69ab31222 {confirmText}
b69ab31223 </Button>
b69ab31224 </Tooltip>
b69ab31225 </>
b69ab31226 );
b69ab31227}