| 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 | |
| 8 | import type {ImportAmendCommit, ImportStack} from 'shared/types/stack'; |
| 9 | import type {AmendRestackBehavior} from '../RestackBehavior'; |
| 10 | import type {PartialSelection} from '../partialSelection'; |
| 11 | import type { |
| 12 | ApplyUncommittedChangesPreviewsFuncType, |
| 13 | Dag, |
| 14 | UncommittedChangesPreviewContext, |
| 15 | } from '../previews'; |
| 16 | import type {CommandArg, CommitInfo, Hash, RepoRelativePath, UncommittedChanges} from '../types'; |
| 17 | |
| 18 | import {restackBehaviorAtom} from '../RestackBehavior'; |
| 19 | import {t} from '../i18n'; |
| 20 | import {readAtom} from '../jotaiUtils'; |
| 21 | import {authorString} from '../serverAPIState'; |
| 22 | import {Operation} from './Operation'; |
| 23 | |
| 24 | export class AmendOperation extends Operation { |
| 25 | /** |
| 26 | * @param filePathsToAmend if provided, only these file paths will be included in the amend operation. If undefined, ALL uncommitted changes are included. Paths should be relative to repo root. |
| 27 | * @param message if provided, update commit description to use this title & description |
| 28 | */ |
| 29 | constructor( |
| 30 | private filePathsToAmend?: Array<RepoRelativePath>, |
| 31 | public message?: string, |
| 32 | public author?: string, |
| 33 | ) { |
| 34 | super(filePathsToAmend ? 'AmendFileSubsetOperation' : 'AmendOperation'); |
| 35 | |
| 36 | this.restackBehavior = readAtom(restackBehaviorAtom); |
| 37 | } |
| 38 | |
| 39 | restackBehavior: AmendRestackBehavior; |
| 40 | |
| 41 | static opName = 'Amend'; |
| 42 | |
| 43 | getArgs() { |
| 44 | const args: Array<CommandArg> = [ |
| 45 | {type: 'config', key: 'amend.autorestack', value: this.restackBehavior}, |
| 46 | 'amend', |
| 47 | '--addremove', |
| 48 | ]; |
| 49 | if (this.filePathsToAmend) { |
| 50 | args.push( |
| 51 | ...this.filePathsToAmend.map(file => |
| 52 | // tag file arguments specially so the remote repo can convert them to the proper cwd-relative format. |
| 53 | ({ |
| 54 | type: 'repo-relative-file' as const, |
| 55 | path: file, |
| 56 | }), |
| 57 | ), |
| 58 | ); |
| 59 | } |
| 60 | |
| 61 | if (this.author) { |
| 62 | args.push('--user', this.author); |
| 63 | } |
| 64 | if (this.message) { |
| 65 | args.push('--message', this.message); |
| 66 | } |
| 67 | return args; |
| 68 | } |
| 69 | |
| 70 | makeOptimisticUncommittedChangesApplier?( |
| 71 | context: UncommittedChangesPreviewContext, |
| 72 | ): ApplyUncommittedChangesPreviewsFuncType | undefined { |
| 73 | const filesToAmend = new Set(this.filePathsToAmend); |
| 74 | if ( |
| 75 | context.uncommittedChanges.length === 0 || |
| 76 | (filesToAmend.size > 0 && |
| 77 | context.uncommittedChanges.every(change => !filesToAmend.has(change.path))) |
| 78 | ) { |
| 79 | return undefined; |
| 80 | } |
| 81 | |
| 82 | const func: ApplyUncommittedChangesPreviewsFuncType = (changes: UncommittedChanges) => { |
| 83 | if (this.filePathsToAmend != null) { |
| 84 | return changes.filter(change => !filesToAmend.has(change.path)); |
| 85 | } else { |
| 86 | return []; |
| 87 | } |
| 88 | }; |
| 89 | return func; |
| 90 | } |
| 91 | |
| 92 | // Bump the timestamp and update the commit message. |
| 93 | optimisticDag(dag: Dag): Dag { |
| 94 | const head = dag.resolve('.'); |
| 95 | if (head?.hash == null) { |
| 96 | return dag; |
| 97 | } |
| 98 | // XXX: amend's auto restack does not bump timestamp yet. We should fix that |
| 99 | // and remove includeDescendants here. |
| 100 | return dag.touch(head.hash, false /* includeDescendants */).replaceWith(head.hash, (_h, c) => { |
| 101 | if (this.message == null) { |
| 102 | return c; |
| 103 | } |
| 104 | const [title] = this.message.split(/\n+/, 1); |
| 105 | const description = this.message.slice(title.length); |
| 106 | // TODO: we should also update `filesSample` after amending. |
| 107 | // These files are visible in the commit info view during optimistic state. |
| 108 | return c?.merge({title, description}); |
| 109 | }); |
| 110 | } |
| 111 | } |
| 112 | |
| 113 | export class PartialAmendOperation extends Operation { |
| 114 | /** |
| 115 | * See also `AmendOperation`. This operation takes a `PartialSelection` and |
| 116 | * uses `debugimportstack` under the hood, to achieve `amend -i` effect. |
| 117 | */ |
| 118 | constructor( |
| 119 | public message: string | undefined, |
| 120 | private originalHeadHash: Hash, |
| 121 | private selection: PartialSelection, |
| 122 | // We need "selected" or "all" files since `selection` only tracks deselected files. |
| 123 | private allFiles: Array<RepoRelativePath>, |
| 124 | ) { |
| 125 | super('PartialAmendOperation'); |
| 126 | } |
| 127 | |
| 128 | getArgs(): CommandArg[] { |
| 129 | return ['debugimportstack']; |
| 130 | } |
| 131 | |
| 132 | getStdin(): string | undefined { |
| 133 | const files = this.selection.calculateImportStackFiles(this.allFiles); |
| 134 | const commitInfo: ImportAmendCommit = { |
| 135 | mark: ':1', |
| 136 | node: this.originalHeadHash, |
| 137 | files, |
| 138 | }; |
| 139 | if (this.message) { |
| 140 | commitInfo.text = this.message; |
| 141 | } |
| 142 | const importStack: ImportStack = [ |
| 143 | ['amend', commitInfo], |
| 144 | ['reset', {mark: ':1'}], |
| 145 | ]; |
| 146 | return JSON.stringify(importStack); |
| 147 | } |
| 148 | |
| 149 | getDescriptionForDisplay() { |
| 150 | return { |
| 151 | description: t('Amending selected changes'), |
| 152 | tooltip: t( |
| 153 | 'This operation does not have a traditional command line equivalent. \n' + |
| 154 | 'You can use `amend -i` on the command line to select changes to amend.', |
| 155 | ), |
| 156 | }; |
| 157 | } |
| 158 | } |
| 159 | |
| 160 | /** Choose `PartialAmendOperation` or `AmendOperation` based on input. */ |
| 161 | export function getAmendOperation( |
| 162 | message: string | undefined, |
| 163 | originalHead: CommitInfo | undefined, |
| 164 | selection: PartialSelection, |
| 165 | allFiles: Array<RepoRelativePath>, |
| 166 | ): AmendOperation | PartialAmendOperation { |
| 167 | const originalHeadHash = originalHead?.hash ?? '.'; |
| 168 | const intendedAuthor = readAtom(authorString); |
| 169 | const authorArg = |
| 170 | intendedAuthor != null && originalHead?.author !== intendedAuthor ? intendedAuthor : undefined; |
| 171 | if (selection.hasChunkSelection()) { |
| 172 | return new PartialAmendOperation(message, originalHeadHash, selection, allFiles); |
| 173 | } else if (selection.isEverythingSelected(() => allFiles)) { |
| 174 | return new AmendOperation(undefined, message, authorArg); |
| 175 | } else { |
| 176 | const selectedFiles = allFiles.filter(path => selection.isFullyOrPartiallySelected(path)); |
| 177 | return new AmendOperation(selectedFiles, message, authorArg); |
| 178 | } |
| 179 | } |
| 180 | |