| b69ab31 | | | 1 | /** |
| b69ab31 | | | 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. |
| b69ab31 | | | 3 | * |
| b69ab31 | | | 4 | * This source code is licensed under the MIT license found in the |
| b69ab31 | | | 5 | * LICENSE file in the root directory of this source tree. |
| b69ab31 | | | 6 | */ |
| b69ab31 | | | 7 | |
| b69ab31 | | | 8 | import type {DagCommitInfo} from './dag/dag'; |
| b69ab31 | | | 9 | import type {Hash} from './types'; |
| b69ab31 | | | 10 | |
| b69ab31 | | | 11 | import {Button} from 'isl-components/Button'; |
| b69ab31 | | | 12 | import {Icon} from 'isl-components/Icon'; |
| b69ab31 | | | 13 | import {DOCUMENTATION_DELAY, Tooltip} from 'isl-components/Tooltip'; |
| b69ab31 | | | 14 | import {useAtom, useAtomValue} from 'jotai'; |
| b69ab31 | | | 15 | import {type ContextMenuItem, useContextMenu} from 'shared/ContextMenu'; |
| b69ab31 | | | 16 | import {CleanupButton, isStackEligibleForCleanup} from './Cleanup'; |
| b69ab31 | | | 17 | import {Row} from './ComponentUtils'; |
| b69ab31 | | | 18 | import {shouldShowSubmitStackConfirmation, useShowConfirmSubmitStack} from './ConfirmSubmitStack'; |
| b69ab31 | | | 19 | import {HighlightCommitsWhileHovering} from './HighlightedCommits'; |
| b69ab31 | | | 20 | import {OperationDisabledButton} from './OperationDisabledButton'; |
| b69ab31 | | | 21 | import {RebaseOrphanedStackButton} from './RebaseOntoSuccessor'; |
| b69ab31 | | | 22 | import {showSuggestedRebaseForStack, SuggestedRebaseButton} from './SuggestedRebase'; |
| b69ab31 | | | 23 | import {allDiffSummaries, codeReviewProvider} from './codeReview/CodeReviewInfo'; |
| b69ab31 | | | 24 | import {SyncStatus, syncStatusAtom} from './codeReview/syncStatus'; |
| b69ab31 | | | 25 | import {T, t} from './i18n'; |
| b69ab31 | | | 26 | import {IconStack} from './icons/IconStack'; |
| b69ab31 | | | 27 | import {useRunOperation} from './operationsState'; |
| b69ab31 | | | 28 | import {useUncommittedSelection} from './partialSelection'; |
| b69ab31 | | | 29 | import {dagWithPreviews} from './previews'; |
| b69ab31 | | | 30 | import {latestUncommittedChangesData} from './serverAPIState'; |
| b69ab31 | | | 31 | import {useConfirmUnsavedEditsBeforeSplit} from './stackEdit/ui/ConfirmUnsavedEditsBeforeSplit'; |
| b69ab31 | | | 32 | import {StackEditIcon} from './stackEdit/ui/StackEditIcon'; |
| b69ab31 | | | 33 | import {editingStackIntentionHashes, loadingStackState} from './stackEdit/ui/stackEditState'; |
| b69ab31 | | | 34 | import {succeedableRevset} from './types'; |
| b69ab31 | | | 35 | |
| b69ab31 | | | 36 | import './StackActions.css'; |
| b69ab31 | | | 37 | |
| b69ab31 | | | 38 | /** |
| b69ab31 | | | 39 | * Actions at the bottom of a stack of commits that acts on the whole stack, |
| b69ab31 | | | 40 | * like submitting, hiding, editing the stack. |
| b69ab31 | | | 41 | */ |
| b69ab31 | | | 42 | export function StackActions({hash}: {hash: Hash}): React.ReactElement | null { |
| b69ab31 | | | 43 | const reviewProvider = useAtomValue(codeReviewProvider); |
| b69ab31 | | | 44 | const diffMap = useAtomValue(allDiffSummaries); |
| b69ab31 | | | 45 | const stackHashes = useAtomValue(editingStackIntentionHashes)[1]; |
| b69ab31 | | | 46 | const loadingState = useAtomValue(loadingStackState); |
| b69ab31 | | | 47 | const suggestedRebase = useAtomValue(showSuggestedRebaseForStack(hash)); |
| b69ab31 | | | 48 | const dag = useAtomValue(dagWithPreviews); |
| b69ab31 | | | 49 | const runOperation = useRunOperation(); |
| b69ab31 | | | 50 | const syncStatusMap = useAtomValue(syncStatusAtom); |
| b69ab31 | | | 51 | |
| b69ab31 | | | 52 | // buttons at the bottom of the stack |
| b69ab31 | | | 53 | const actions = []; |
| b69ab31 | | | 54 | // additional actions hidden behind [...] menu. |
| b69ab31 | | | 55 | // Non-empty only when actions is non-empty. |
| b69ab31 | | | 56 | const moreActions: Array<ContextMenuItem> = []; |
| b69ab31 | | | 57 | const confirmShouldSubmit = useShowConfirmSubmitStack(); |
| b69ab31 | | | 58 | const contextMenu = useContextMenu(() => moreActions); |
| b69ab31 | | | 59 | |
| b69ab31 | | | 60 | const isStackEditingActivated = |
| b69ab31 | | | 61 | stackHashes.size > 0 && |
| b69ab31 | | | 62 | loadingState.state === 'hasValue' && |
| b69ab31 | | | 63 | dag |
| b69ab31 | | | 64 | .descendants(hash) |
| b69ab31 | | | 65 | .toSeq() |
| b69ab31 | | | 66 | .some(h => stackHashes.has(h)); |
| b69ab31 | | | 67 | |
| b69ab31 | | | 68 | const showCleanupButton = |
| b69ab31 | | | 69 | reviewProvider == null || diffMap?.value == null |
| b69ab31 | | | 70 | ? false |
| b69ab31 | | | 71 | : isStackEligibleForCleanup(hash, dag, diffMap.value, reviewProvider); |
| b69ab31 | | | 72 | |
| b69ab31 | | | 73 | const info = dag.get(hash); |
| b69ab31 | | | 74 | |
| b69ab31 | | | 75 | if (info == null) { |
| b69ab31 | | | 76 | return null; |
| b69ab31 | | | 77 | } |
| b69ab31 | | | 78 | |
| b69ab31 | | | 79 | if (reviewProvider !== null && !isStackEditingActivated) { |
| b69ab31 | | | 80 | const reviewActions = |
| b69ab31 | | | 81 | diffMap.value == null |
| b69ab31 | | | 82 | ? {} |
| b69ab31 | | | 83 | : reviewProvider?.getSupportedStackActions(hash, dag, diffMap.value); |
| b69ab31 | | | 84 | const resubmittableStack = reviewActions?.resubmittableStack; |
| b69ab31 | | | 85 | const submittableStack = reviewActions?.submittableStack; |
| b69ab31 | | | 86 | const MIN_STACK_SIZE_TO_SUGGEST_SUBMIT = 2; // don't show "submit stack" on single commits... they're not really "stacks". |
| b69ab31 | | | 87 | |
| b69ab31 | | | 88 | const locallyChangedCommits = resubmittableStack?.filter( |
| b69ab31 | | | 89 | c => syncStatusMap?.get(c.hash) === SyncStatus.LocalIsNewer, |
| b69ab31 | | | 90 | ); |
| b69ab31 | | | 91 | |
| b69ab31 | | | 92 | const willShowConfirmationModal = shouldShowSubmitStackConfirmation(); |
| b69ab31 | | | 93 | |
| b69ab31 | | | 94 | // any existing diffs -> show resubmit stack, |
| b69ab31 | | | 95 | if ( |
| b69ab31 | | | 96 | resubmittableStack != null && |
| b69ab31 | | | 97 | resubmittableStack.length >= MIN_STACK_SIZE_TO_SUGGEST_SUBMIT |
| b69ab31 | | | 98 | ) { |
| b69ab31 | | | 99 | const TooltipContent = () => { |
| b69ab31 | | | 100 | return ( |
| b69ab31 | | | 101 | <div className="resubmit-stack-tooltip"> |
| b69ab31 | | | 102 | <T replace={{$cmd: reviewProvider?.submitCommandName()}}> |
| b69ab31 | | | 103 | Submit new version of commits in this stack for review with $cmd. |
| b69ab31 | | | 104 | </T> |
| b69ab31 | | | 105 | {willShowConfirmationModal && ( |
| b69ab31 | | | 106 | <div> |
| b69ab31 | | | 107 | <T>Draft mode and update message can be configured before submitting.</T> |
| b69ab31 | | | 108 | </div> |
| b69ab31 | | | 109 | )} |
| b69ab31 | | | 110 | {locallyChangedCommits != null && locallyChangedCommits.length > 0 && ( |
| b69ab31 | | | 111 | <div> |
| b69ab31 | | | 112 | <Icon icon="circle-filled" color={'blue'} /> |
| b69ab31 | | | 113 | <T count={locallyChangedCommits.length}>someCommitsUpdatedLocallyAddendum</T> |
| b69ab31 | | | 114 | </div> |
| b69ab31 | | | 115 | )} |
| b69ab31 | | | 116 | </div> |
| b69ab31 | | | 117 | ); |
| b69ab31 | | | 118 | }; |
| b69ab31 | | | 119 | let icon = <Icon icon="cloud-upload" slot="start" />; |
| b69ab31 | | | 120 | if (locallyChangedCommits != null && locallyChangedCommits.length > 0) { |
| b69ab31 | | | 121 | icon = ( |
| b69ab31 | | | 122 | <IconStack slot="start"> |
| b69ab31 | | | 123 | <Icon icon="cloud-upload" /> |
| b69ab31 | | | 124 | <Icon icon="circle-large-filled" color={'blue'} /> |
| b69ab31 | | | 125 | </IconStack> |
| b69ab31 | | | 126 | ); |
| b69ab31 | | | 127 | } |
| b69ab31 | | | 128 | actions.push( |
| b69ab31 | | | 129 | <Tooltip key="resubmit-stack" component={() => <TooltipContent />} placement="bottom"> |
| b69ab31 | | | 130 | <HighlightCommitsWhileHovering toHighlight={resubmittableStack}> |
| b69ab31 | | | 131 | <OperationDisabledButton |
| b69ab31 | | | 132 | // Use the diffId in the key so that only this "resubmit stack" button shows the spinner. |
| b69ab31 | | | 133 | contextKey={`resubmit-stack-on-${info.diffId}`} |
| b69ab31 | | | 134 | kind="icon" |
| b69ab31 | | | 135 | icon={icon} |
| b69ab31 | | | 136 | runOperation={async () => { |
| b69ab31 | | | 137 | const confirmation = await confirmShouldSubmit('resubmit', resubmittableStack); |
| b69ab31 | | | 138 | if (!confirmation) { |
| b69ab31 | | | 139 | return []; |
| b69ab31 | | | 140 | } |
| b69ab31 | | | 141 | return reviewProvider.submitOperation(resubmittableStack, { |
| b69ab31 | | | 142 | draft: confirmation.submitAsDraft, |
| b69ab31 | | | 143 | updateMessage: confirmation.updateMessage, |
| b69ab31 | | | 144 | publishWhenReady: confirmation.publishWhenReady, |
| b69ab31 | | | 145 | }); |
| b69ab31 | | | 146 | }}> |
| b69ab31 | | | 147 | <T>Resubmit stack</T> |
| b69ab31 | | | 148 | </OperationDisabledButton> |
| b69ab31 | | | 149 | </HighlightCommitsWhileHovering> |
| b69ab31 | | | 150 | </Tooltip>, |
| b69ab31 | | | 151 | ); |
| b69ab31 | | | 152 | // any non-submitted diffs -> "submit all commits this stack" in hidden group |
| b69ab31 | | | 153 | if ( |
| b69ab31 | | | 154 | submittableStack != null && |
| b69ab31 | | | 155 | submittableStack.length > 0 && |
| b69ab31 | | | 156 | submittableStack.length > resubmittableStack.length |
| b69ab31 | | | 157 | ) { |
| b69ab31 | | | 158 | moreActions.push({ |
| b69ab31 | | | 159 | label: ( |
| b69ab31 | | | 160 | <HighlightCommitsWhileHovering key="submit-entire-stack" toHighlight={submittableStack}> |
| b69ab31 | | | 161 | <Row> |
| b69ab31 | | | 162 | <Icon icon="cloud-upload" slot="start" /> |
| b69ab31 | | | 163 | <T>Submit entire stack</T> |
| b69ab31 | | | 164 | </Row> |
| b69ab31 | | | 165 | </HighlightCommitsWhileHovering> |
| b69ab31 | | | 166 | ), |
| b69ab31 | | | 167 | onClick: async () => { |
| b69ab31 | | | 168 | const confirmation = await confirmShouldSubmit('submit-all', submittableStack); |
| b69ab31 | | | 169 | if (!confirmation) { |
| b69ab31 | | | 170 | return []; |
| b69ab31 | | | 171 | } |
| b69ab31 | | | 172 | runOperation( |
| b69ab31 | | | 173 | reviewProvider.submitOperation(submittableStack, { |
| b69ab31 | | | 174 | draft: confirmation.submitAsDraft, |
| b69ab31 | | | 175 | updateMessage: confirmation.updateMessage, |
| b69ab31 | | | 176 | publishWhenReady: confirmation.publishWhenReady, |
| b69ab31 | | | 177 | }), |
| b69ab31 | | | 178 | ); |
| b69ab31 | | | 179 | }, |
| b69ab31 | | | 180 | }); |
| b69ab31 | | | 181 | } |
| b69ab31 | | | 182 | // NO non-submitted diffs -> nothing in hidden group |
| b69ab31 | | | 183 | } else if ( |
| b69ab31 | | | 184 | submittableStack != null && |
| b69ab31 | | | 185 | submittableStack.length >= MIN_STACK_SIZE_TO_SUGGEST_SUBMIT |
| b69ab31 | | | 186 | ) { |
| b69ab31 | | | 187 | // We need to associate this operation with the stack we're submitting, |
| b69ab31 | | | 188 | // but during submitting, we'll amend the original commit, so hash is not accurate. |
| b69ab31 | | | 189 | // Parent is close, but if you had multiple stacks rebased to the same public commit, |
| b69ab31 | | | 190 | // all those stacks would render the same key and show the same spinner. |
| b69ab31 | | | 191 | // So parent hash + title heuristic lets us almost always show the spinner for only this stack. |
| b69ab31 | | | 192 | const contextKey = `submit-stack-on-${info.parents.at(0)}-${info.title.replace(/ /g, '_')}`; |
| b69ab31 | | | 193 | |
| b69ab31 | | | 194 | const tooltip = t( |
| b69ab31 | | | 195 | willShowConfirmationModal |
| b69ab31 | | | 196 | ? 'Submit commits in this stack for review with $cmd.\n\nDraft mode and update message can be configured before submitting.' |
| b69ab31 | | | 197 | : 'Submit commits in this stack for review with $cmd.', |
| b69ab31 | | | 198 | {replace: {$cmd: reviewProvider?.submitCommandName()}}, |
| b69ab31 | | | 199 | ); |
| b69ab31 | | | 200 | // NO existing diffs -> show submit stack () |
| b69ab31 | | | 201 | actions.push( |
| b69ab31 | | | 202 | <Tooltip key="submit-stack" title={tooltip} placement="bottom"> |
| b69ab31 | | | 203 | <HighlightCommitsWhileHovering toHighlight={submittableStack}> |
| b69ab31 | | | 204 | <OperationDisabledButton |
| b69ab31 | | | 205 | contextKey={contextKey} |
| b69ab31 | | | 206 | kind="icon" |
| b69ab31 | | | 207 | icon={<Icon icon="cloud-upload" slot="start" />} |
| b69ab31 | | | 208 | runOperation={async () => { |
| b69ab31 | | | 209 | const allCommits = submittableStack; |
| b69ab31 | | | 210 | const confirmation = await confirmShouldSubmit('submit', allCommits); |
| b69ab31 | | | 211 | if (!confirmation) { |
| b69ab31 | | | 212 | return []; |
| b69ab31 | | | 213 | } |
| b69ab31 | | | 214 | return reviewProvider.submitOperation(submittableStack, { |
| b69ab31 | | | 215 | draft: confirmation.submitAsDraft, |
| b69ab31 | | | 216 | updateMessage: confirmation.updateMessage, |
| b69ab31 | | | 217 | publishWhenReady: confirmation.publishWhenReady, |
| b69ab31 | | | 218 | }); |
| b69ab31 | | | 219 | }}> |
| b69ab31 | | | 220 | <T>Submit stack</T> |
| b69ab31 | | | 221 | </OperationDisabledButton> |
| b69ab31 | | | 222 | </HighlightCommitsWhileHovering> |
| b69ab31 | | | 223 | </Tooltip>, |
| b69ab31 | | | 224 | ); |
| b69ab31 | | | 225 | } |
| b69ab31 | | | 226 | } |
| b69ab31 | | | 227 | |
| b69ab31 | | | 228 | const hasChildren = dag.childHashes(hash).size > 0; |
| b69ab31 | | | 229 | if (hasChildren) { |
| b69ab31 | | | 230 | actions.push(<StackEditButton key="edit-stack" info={info} />); |
| b69ab31 | | | 231 | } |
| b69ab31 | | | 232 | |
| b69ab31 | | | 233 | if (showCleanupButton) { |
| b69ab31 | | | 234 | actions.push(<CleanupButton key="cleanup" commit={info} hasChildren={hasChildren} />); |
| b69ab31 | | | 235 | // cleanup button implies no need to rebase this stack |
| b69ab31 | | | 236 | } else if (suggestedRebase) { |
| b69ab31 | | | 237 | // FIXME: Support optimistic commits, requires CommitInfo instead of just Hash |
| b69ab31 | | | 238 | actions.push(<SuggestedRebaseButton key="suggested-rebase" source={succeedableRevset(hash)} />); |
| b69ab31 | | | 239 | } |
| b69ab31 | | | 240 | |
| b69ab31 | | | 241 | actions.push(<RebaseOrphanedStackButton key="rebase-orphaned" hash={hash} />); |
| b69ab31 | | | 242 | |
| b69ab31 | | | 243 | if (actions.length === 0) { |
| b69ab31 | | | 244 | return null; |
| b69ab31 | | | 245 | } |
| b69ab31 | | | 246 | const moreActionsButton = |
| b69ab31 | | | 247 | moreActions.length === 0 ? null : ( |
| b69ab31 | | | 248 | <Button key="more-actions" icon onClick={contextMenu}> |
| b69ab31 | | | 249 | <Icon icon="ellipsis" /> |
| b69ab31 | | | 250 | </Button> |
| b69ab31 | | | 251 | ); |
| b69ab31 | | | 252 | return ( |
| b69ab31 | | | 253 | <div className="commit-tree-stack-actions" data-testid="commit-tree-stack-actions"> |
| b69ab31 | | | 254 | {actions} |
| b69ab31 | | | 255 | {moreActionsButton} |
| b69ab31 | | | 256 | </div> |
| b69ab31 | | | 257 | ); |
| b69ab31 | | | 258 | } |
| b69ab31 | | | 259 | |
| b69ab31 | | | 260 | function StackEditButton({info}: {info: DagCommitInfo}): React.ReactElement | null { |
| b69ab31 | | | 261 | const uncommitted = useAtomValue(latestUncommittedChangesData); |
| b69ab31 | | | 262 | const dag = useAtomValue(dagWithPreviews); |
| b69ab31 | | | 263 | const [[, stackHashes], setStackIntentionHashes] = useAtom(editingStackIntentionHashes); |
| b69ab31 | | | 264 | const loadingState = useAtomValue(loadingStackState); |
| b69ab31 | | | 265 | const confirmUnsavedEditsBeforeSplit = useConfirmUnsavedEditsBeforeSplit(); |
| b69ab31 | | | 266 | |
| b69ab31 | | | 267 | const set = dag.nonObsolete(dag.descendants(info.hash)); |
| b69ab31 | | | 268 | if (set.size <= 1) { |
| b69ab31 | | | 269 | return null; |
| b69ab31 | | | 270 | } |
| b69ab31 | | | 271 | |
| b69ab31 | | | 272 | const stackCommits = dag.getBatch(set.toArray()); |
| b69ab31 | | | 273 | const isEditing = stackHashes.size > 0 && set.toSeq().some(h => stackHashes.has(h)); |
| b69ab31 | | | 274 | |
| b69ab31 | | | 275 | const isPreview = info.previewType != null; |
| b69ab31 | | | 276 | const isLoading = isEditing && loadingState.state === 'loading'; |
| b69ab31 | | | 277 | const isError = isEditing && loadingState.state === 'hasError'; |
| b69ab31 | | | 278 | const isLinear = |
| b69ab31 | | | 279 | dag.merge(set).size === 0 && dag.heads(set).size === 1 && dag.roots(set).size === 1; |
| b69ab31 | | | 280 | const isDirty = stackCommits.some(c => c.isDot) && uncommitted.files.length > 0; |
| b69ab31 | | | 281 | const hasPublic = stackCommits.some(c => c.phase === 'public'); |
| b69ab31 | | | 282 | const disabled = isDirty || !isLinear || isLoading || isError || isPreview || hasPublic; |
| b69ab31 | | | 283 | const title = isError |
| b69ab31 | | | 284 | ? t(`Failed to load stack: ${loadingState.error}`) |
| b69ab31 | | | 285 | : isLoading |
| b69ab31 | | | 286 | ? loadingState.exportedStack === undefined |
| b69ab31 | | | 287 | ? t('Reading stack content') |
| b69ab31 | | | 288 | : t('Analyzing stack content') |
| b69ab31 | | | 289 | : isDirty |
| b69ab31 | | | 290 | ? t( |
| b69ab31 | | | 291 | 'Cannot edit stack when there are uncommitted changes.\nCommit or amend your changes first.', |
| b69ab31 | | | 292 | ) |
| b69ab31 | | | 293 | : isPreview |
| b69ab31 | | | 294 | ? t('Cannot edit pending changes') |
| b69ab31 | | | 295 | : hasPublic |
| b69ab31 | | | 296 | ? t('Cannot edit public commits') |
| b69ab31 | | | 297 | : isLinear |
| b69ab31 | | | 298 | ? t('Reorder, fold, or drop commits') |
| b69ab31 | | | 299 | : t('Cannot edit non-linear stack'); |
| b69ab31 | | | 300 | const highlight = disabled ? [] : stackCommits; |
| b69ab31 | | | 301 | const tooltipDelay = disabled && !isLoading ? undefined : DOCUMENTATION_DELAY; |
| b69ab31 | | | 302 | const icon = isLoading ? <Icon icon="loading" slot="start" /> : <StackEditIcon slot="start" />; |
| b69ab31 | | | 303 | |
| b69ab31 | | | 304 | return ( |
| b69ab31 | | | 305 | <HighlightCommitsWhileHovering key="submit-stack" toHighlight={highlight}> |
| b69ab31 | | | 306 | <Tooltip title={title} delayMs={tooltipDelay} placement="bottom"> |
| b69ab31 | | | 307 | <Button |
| b69ab31 | | | 308 | className={`edit-stack-button${disabled ? ' disabled' : ''}`} |
| b69ab31 | | | 309 | disabled={disabled} |
| b69ab31 | | | 310 | icon |
| b69ab31 | | | 311 | onClick={async () => { |
| b69ab31 | | | 312 | if (!(await confirmUnsavedEditsBeforeSplit(stackCommits, 'edit_stack'))) { |
| b69ab31 | | | 313 | return; |
| b69ab31 | | | 314 | } |
| b69ab31 | | | 315 | setStackIntentionHashes(['general', new Set<Hash>(set)]); |
| b69ab31 | | | 316 | }}> |
| b69ab31 | | | 317 | {icon} |
| b69ab31 | | | 318 | <T>Edit stack</T> |
| b69ab31 | | | 319 | </Button> |
| b69ab31 | | | 320 | </Tooltip> |
| b69ab31 | | | 321 | </HighlightCommitsWhileHovering> |
| b69ab31 | | | 322 | ); |
| b69ab31 | | | 323 | } |
| b69ab31 | | | 324 | |
| b69ab31 | | | 325 | export function AbsorbButton() { |
| b69ab31 | | | 326 | const selection = useUncommittedSelection(); |
| b69ab31 | | | 327 | const dag = useAtomValue(dagWithPreviews); |
| b69ab31 | | | 328 | const [[, stackHashes], setStackIntentionHashes] = useAtom(editingStackIntentionHashes); |
| b69ab31 | | | 329 | |
| b69ab31 | | | 330 | let disableReason = null; |
| b69ab31 | | | 331 | const dot = dag.resolve('.'); |
| b69ab31 | | | 332 | const stack = dag.ancestors(dot == null ? [] : [dot.hash], {within: dag.draft()}); |
| b69ab31 | | | 333 | if (dot == null) { |
| b69ab31 | | | 334 | disableReason = t('Absorb requires a working copy'); |
| b69ab31 | | | 335 | } else if (stackHashes.size > 0) { |
| b69ab31 | | | 336 | disableReason = t('Cannot initialize absorb while editing a stack'); |
| b69ab31 | | | 337 | } else if (dot.phase === 'public') { |
| b69ab31 | | | 338 | disableReason = t('Absorb only works for draft commits'); |
| b69ab31 | | | 339 | } else if (stack.size <= 1) { |
| b69ab31 | | | 340 | disableReason = t('Absorb works for a stack of more than one commit. Use "Amend" instead.'); |
| b69ab31 | | | 341 | } else if (dag.merge(stack).size > 0) { |
| b69ab31 | | | 342 | disableReason = t('Absorb does not work for merge commits'); |
| b69ab31 | | | 343 | } else if (selection.isNothingSelected()) { |
| b69ab31 | | | 344 | disableReason = t('No files are selected. Absorb requires at least one file.'); |
| b69ab31 | | | 345 | } |
| b69ab31 | | | 346 | |
| b69ab31 | | | 347 | const tooltipText = |
| b69ab31 | | | 348 | disableReason ?? |
| b69ab31 | | | 349 | t( |
| b69ab31 | | | 350 | 'Absorb changes into the stack. Amend each change to the commit that introduced the surrounding line changes. You can customize the amend destination in a dialog.', |
| b69ab31 | | | 351 | ); |
| b69ab31 | | | 352 | |
| b69ab31 | | | 353 | return ( |
| b69ab31 | | | 354 | <Tooltip title={tooltipText}> |
| b69ab31 | | | 355 | <Button |
| b69ab31 | | | 356 | icon |
| b69ab31 | | | 357 | key="absorb" |
| b69ab31 | | | 358 | disabled={disableReason != null} |
| b69ab31 | | | 359 | onClick={() => { |
| b69ab31 | | | 360 | // Since "wdir()" might race with the "dot" and "stack" here, |
| b69ab31 | | | 361 | // use ".%public()", avoid "stack" to avoid inconsistent state |
| b69ab31 | | | 362 | // (ex. "wdir()" is no longer the "stack" top). |
| b69ab31 | | | 363 | setStackIntentionHashes(['absorb', new Set<string>(['wdir()', '(.%public())'])]); |
| b69ab31 | | | 364 | }}> |
| b69ab31 | | | 365 | <Icon slot="start" icon="replace-all" /> |
| b69ab31 | | | 366 | <T>Absorb</T> |
| b69ab31 | | | 367 | </Button> |
| b69ab31 | | | 368 | </Tooltip> |
| b69ab31 | | | 369 | ); |
| b69ab31 | | | 370 | } |