| 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 { |
| 9 | ApplyUncommittedChangesPreviewsFuncType, |
| 10 | Dag, |
| 11 | UncommittedChangesPreviewContext, |
| 12 | } from '../previews'; |
| 13 | import type {ChangedFile, CommitInfo, UncommittedChanges} from '../types'; |
| 14 | |
| 15 | import {Operation} from './Operation'; |
| 16 | |
| 17 | export 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 | |