5.8 KB180 lines
Blame
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
8import type {ImportAmendCommit, ImportStack} from 'shared/types/stack';
9import type {AmendRestackBehavior} from '../RestackBehavior';
10import type {PartialSelection} from '../partialSelection';
11import type {
12 ApplyUncommittedChangesPreviewsFuncType,
13 Dag,
14 UncommittedChangesPreviewContext,
15} from '../previews';
16import type {CommandArg, CommitInfo, Hash, RepoRelativePath, UncommittedChanges} from '../types';
17
18import {restackBehaviorAtom} from '../RestackBehavior';
19import {t} from '../i18n';
20import {readAtom} from '../jotaiUtils';
21import {authorString} from '../serverAPIState';
22import {Operation} from './Operation';
23
24export 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
113export 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. */
161export 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