3.3 KB87 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 {
9 ApplyUncommittedChangesPreviewsFuncType,
10 Dag,
11 UncommittedChangesPreviewContext,
12} from '../previews';
13import type {ChangedFile, CommitInfo, UncommittedChanges} from '../types';
14
15import {Operation} from './Operation';
16
17export class UncommitOperation extends Operation {
18 /**
19 * @param originalDotCommit the current dot commit, needed to track when optimistic state is resolved
20 * @param changedFiles the files that are in the commit to be uncommitted. Must be fetched before running, as the CommitInfo object itself does not have file statuses.
21 */
22 constructor(
23 private originalDotCommit: CommitInfo,
24 private changedFiles: Array<ChangedFile>,
25 ) {
26 super('UncommitOperation');
27 }
28
29 static opName = 'Uncommit';
30
31 getArgs() {
32 const args = ['uncommit'];
33 return args;
34 }
35
36 optimisticDag(dag: Dag): Dag {
37 const {hash, parents} = this.originalDotCommit;
38 const p1 = parents.at(0);
39 const commitHasChildren = (dag.children(hash)?.size ?? 0) > 0;
40 if (
41 p1 == null || commitHasChildren
42 ? // If the commit has children, then we know the uncommit is done when it's no longer the dot commit
43 dag.get(hash)?.isDot !== true
44 : // If the commit does not have children, if `hash` disappears and `p1` still exists, then uncommit is completed.
45 dag.get(hash) == null || dag.get(p1) == null
46 ) {
47 return dag;
48 }
49 return commitHasChildren
50 ? // Set `isDot` on `p1` and not `hash`
51 dag.replaceWith([p1 as string, hash], (h, c) => {
52 return c?.set('isDot', h === p1);
53 })
54 : // Hide `hash` and set `isDot` on `p1`.
55 dag.replaceWith([p1 as string, hash], (h, c) => {
56 if (h === hash) {
57 return undefined;
58 } else {
59 return c?.set('isDot', true);
60 }
61 });
62 }
63
64 makeOptimisticUncommittedChangesApplier?(
65 context: UncommittedChangesPreviewContext,
66 ): ApplyUncommittedChangesPreviewsFuncType | undefined {
67 const preexistingChanges = new Set(context.uncommittedChanges.map(change => change.path));
68
69 if (this.changedFiles.every(file => preexistingChanges.has(file.path))) {
70 // once every file to uncommit appears in the output, the uncommit has reflected in the latest fetch.
71 // TODO: we'll eventually limit how many uncommitted changes we pull in. When this happens, it's
72 // possible the list of files won't include any of the changes being uncommitted (though this would be rare).
73 // We should probably return undefined if the number of uncommitted changes >= max fetched.
74 return undefined;
75 }
76
77 const func: ApplyUncommittedChangesPreviewsFuncType = (changes: UncommittedChanges) => {
78 // You could have uncommitted changes before uncommitting, so we need to include
79 // files from the commit AND the existing uncommitted changes.
80 // But it's also possible to have changed a file changed by the commit, so we need to de-dupe.
81 const newChanges = this.changedFiles.filter(file => !preexistingChanges.has(file.path));
82 return [...changes, ...newChanges];
83 };
84 return func;
85 }
86}
87