6.8 KB206 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 {ImportStack} from 'shared/types/stack';
9import type {PartialSelection} from '../partialSelection';
10import type {
11 ApplyUncommittedChangesPreviewsFuncType,
12 Dag,
13 UncommittedChangesPreviewContext,
14} from '../previews';
15import type {
16 ChangedFile,
17 CommandArg,
18 CommitInfo,
19 Hash,
20 RepoRelativePath,
21 UncommittedChanges,
22} from '../types';
23
24import {DagCommitInfo} from '../dag/dagCommitInfo';
25import {t} from '../i18n';
26import {readAtom} from '../jotaiUtils';
27import {uncommittedChangesWithPreviews} from '../previews';
28import {authorString} from '../serverAPIState';
29import {CommitBaseOperation} from './CommitBaseOperation';
30import {Operation} from './Operation';
31
32export class CommitOperation extends CommitBaseOperation {
33 private beforeCommitDate: Date;
34
35 /**
36 * @param message the commit message. The first line is used as the title.
37 * @param originalHeadHash the hash of the current head commit, needed to track when optimistic state is resolved.
38 * @param filesPathsToCommit if provided, only these file paths will be included in the commit operation. If undefined, ALL uncommitted changes are included. Paths should be relative to repo root.
39 */
40 constructor(
41 public message: string,
42 private originalHeadHash: Hash,
43 protected filesPathsToCommit?: Array<RepoRelativePath>,
44 ) {
45 super(message, filesPathsToCommit);
46
47 // New commit should have a greater date.
48 this.beforeCommitDate = new Date();
49
50 // When rendering optimistic state, we need to know the set of files that will be part of this commit.
51 // This is not necessarily the same as filePathsToCommit, since it may be undefined to represent "all files".
52 // This is done once at Operation creation time, not on each call to optimisticDag, since we
53 // only care about the list of changed files when the CommitOperation was enqueued.
54 this.optimisticChangedFiles = readAtom(uncommittedChangesWithPreviews).filter(changedFile => {
55 return filesPathsToCommit == null
56 ? true
57 : filesPathsToCommit.some(f => f === changedFile.path);
58 });
59 }
60
61 private optimisticChangedFiles: Array<ChangedFile>;
62
63 makeOptimisticUncommittedChangesApplier?(
64 context: UncommittedChangesPreviewContext,
65 ): ApplyUncommittedChangesPreviewsFuncType | undefined {
66 const filesToCommit = new Set(this.filesPathsToCommit);
67 // optimistic state is over when there's no uncommitted changes that we wanted to commit left
68 if (
69 context.uncommittedChanges.length === 0 ||
70 (filesToCommit.size > 0 &&
71 context.uncommittedChanges.every(change => !filesToCommit.has(change.path)))
72 ) {
73 return undefined;
74 }
75
76 const func: ApplyUncommittedChangesPreviewsFuncType = (changes: UncommittedChanges) => {
77 if (this.filesPathsToCommit != null) {
78 return changes.filter(change => !filesToCommit.has(change.path));
79 } else {
80 return [];
81 }
82 };
83 return func;
84 }
85
86 optimisticDag(dag: Dag): Dag {
87 const base = this.originalHeadHash;
88 const baseInfo = dag.get(base);
89 if (!baseInfo) {
90 return dag;
91 }
92
93 const [title] = this.message.split(/\n+/, 1);
94 const children = dag.children(base);
95 const hasWantedChild = children.toHashes().some(h => {
96 const info = dag.get(h);
97 return info?.title === title && info?.date > this.beforeCommitDate;
98 });
99 if (hasWantedChild) {
100 // A new commit was made on `base` with the the expected title.
101 // Consider the commit operation as completed.
102 return dag;
103 }
104
105 const now = new Date(Date.now());
106
107 // The fake optimistic commit can be resolved into a real commit by taking the
108 // first child of the given parent that's created after the commit operation was created.
109 const optimisticRevset = `first(sort((children(${base})-${base}) & date(">${now.toUTCString()}"),date))`;
110
111 // NOTE: We might want to check the "active bookmark" state
112 // and update bookmarks accordingly.
113 const hash = `OPTIMISTIC_COMMIT_${base}`;
114 const description = this.message.slice(title.length);
115 const author = readAtom(authorString);
116 const info = DagCommitInfo.fromCommitInfo({
117 author: author ?? baseInfo?.author ?? '',
118 description,
119 title,
120 bookmarks: [],
121 remoteBookmarks: [],
122 isDot: true,
123 parents: [base],
124 hash,
125 optimisticRevset,
126 phase: 'draft',
127 filePathsSample: this.optimisticChangedFiles.map(f => f.path),
128 totalFileCount: this.optimisticChangedFiles.length,
129 date: now,
130 });
131
132 return dag.replaceWith([base, hash], (h, _c) => {
133 if (h === base) {
134 return baseInfo?.set('isDot', false);
135 } else {
136 return info;
137 }
138 });
139 }
140}
141
142export class PartialCommitOperation extends Operation {
143 /**
144 * See also `CommitOperation`. This operation takes a `PartialSelection` and
145 * uses `debugimportstack` under the hood, to achieve `commit -i` effect.
146 */
147 constructor(
148 public message: string,
149 private originalHeadHash: Hash,
150 private selection: PartialSelection,
151 // We need "selected" or "all" files since `selection` only tracks deselected files.
152 private allFiles: Array<RepoRelativePath>,
153 ) {
154 super('PartialCommitOperation');
155 }
156
157 getArgs(): CommandArg[] {
158 return ['debugimportstack'];
159 }
160
161 getStdin(): string | undefined {
162 const files = this.selection.calculateImportStackFiles(this.allFiles);
163 const importStack: ImportStack = [
164 [
165 'commit',
166 {
167 mark: ':1',
168 text: this.message,
169 parents: [this.originalHeadHash],
170 files,
171 },
172 ],
173 ['reset', {mark: ':1'}],
174 ];
175 return JSON.stringify(importStack);
176 }
177
178 getDescriptionForDisplay() {
179 return {
180 description: t('Committing selected changes'),
181 tooltip: t(
182 'This operation does not have a traditional command line equivalent. \n' +
183 'You can use `commit -i` on the command line to select changes to commit.',
184 ),
185 };
186 }
187}
188
189/** Choose `PartialCommitOperation` or `CommitOperation` based on input. */
190export function getCommitOperation(
191 message: string,
192 originalHead: CommitInfo | undefined,
193 selection: PartialSelection,
194 allFiles: Array<RepoRelativePath>,
195): CommitOperation | PartialCommitOperation {
196 const originalHeadHash = originalHead?.hash ?? '.';
197 if (selection.hasChunkSelection()) {
198 return new PartialCommitOperation(message, originalHeadHash, selection, allFiles);
199 } else if (selection.isEverythingSelected(() => allFiles)) {
200 return new CommitOperation(message, originalHeadHash);
201 } else {
202 const selectedFiles = allFiles.filter(path => selection.isFullyOrPartiallySelected(path));
203 return new CommitOperation(message, originalHeadHash, selectedFiles);
204 }
205}
206