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