4.9 KB137 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 {Hash} from 'shared/types/common';
9import type {ExportStack, ImportCommit, ImportStack, Mark} from 'shared/types/stack';
10import type {Dag} from '../previews';
11
12import {DagCommitInfo} from '../dag/dagCommitInfo';
13import {HashSet} from '../dag/set';
14import {t} from '../i18n';
15import {CommitPreview} from '../previews';
16import {Operation} from './Operation';
17
18export class ImportStackOperation extends Operation {
19 static opName = 'StackEdit';
20
21 // Derived from importStack.
22
23 /** Commits sorted from the stack bottom to top. */
24 private commits: Readonly<ImportCommit>[];
25
26 /** Parent of the first commit. */
27 private firstParent: Hash | null;
28
29 /** Goto command from the importStack. */
30 private gotoMark: Mark | undefined;
31
32 constructor(
33 private importStack: Readonly<ImportStack>,
34 protected originalStack: Readonly<ExportStack>,
35 ) {
36 super('ImportStackOperation');
37
38 let firstParent: Hash | null = null;
39 const origHashes = new Set<Hash>();
40 const gotoMark = importStack
41 .flatMap(([op, value]) => (op === 'goto' || op === 'reset' ? value.mark : []))
42 .at(-1);
43 const commits = importStack.flatMap(a => (a[0] === 'commit' ? [a[1]] : []));
44 commits.forEach(commit => {
45 if (firstParent == null) {
46 firstParent = commit.parents.at(0) ?? 'unexpected_parents';
47 }
48 (commit.predecessors ?? []).forEach(pred => {
49 origHashes.add(pred);
50 });
51 });
52
53 this.commits = commits;
54 this.firstParent = firstParent;
55 this.gotoMark = gotoMark;
56 }
57
58 getArgs() {
59 return ['debugimportstack'];
60 }
61
62 getStdin() {
63 return JSON.stringify(this.importStack);
64 }
65
66 getDescriptionForDisplay() {
67 return {
68 description: t('Applying stack changes'),
69 tooltip: t(
70 'This operation does not have a traditional command line equivalent. \n' +
71 'You might use commit, amend, histedit, rebase, absorb, fold, or a combination of them for similar functionalities.',
72 ),
73 };
74 }
75
76 optimisticDag(dag: Dag): Dag {
77 const originalHashes = this.originalStack.map(c => c.node);
78 // Replace the old stack with the new stack, followed by a rebase.
79 // Note the rebase is actually not what the operation does, but we always
80 // follow up with a rebase operation if needed.
81 const toRebase = dag.descendants(dag.children(originalHashes.at(-1)));
82 let toRemove = HashSet.fromHashes(originalHashes).subtract(dag.ancestors(this.firstParent));
83 // If the "toRemove" part of the original stack is gone, consider as completed.
84 // Note: We no longer do a rebase in this case, and requires the rebase preview
85 // to be handled separately.
86 if (dag.present(toRemove).size === 0) {
87 return dag;
88 }
89 // It's possible that the new stack was actually created but the head commit
90 // keeps the old stack from disappearing (so the above check returns false).
91 // In this case, we hide the new stack (by using successors) temporarily.
92 // Otherwise we need to figure out the "new head", which is not trivial.
93 toRemove = toRemove.union(dag.successors(toRemove));
94 const newStack = this.previewStack(dag);
95 const newDag = dag.remove(toRemove).add(newStack).rebase(toRebase, newStack.at(-1)?.hash);
96 return newDag;
97 }
98
99 private previewStack(dag: Dag): Array<DagCommitInfo> {
100 let parents = this.firstParent ? [this.firstParent] : [];
101 const usedHashes = new Set<Hash>();
102 return this.commits.map(commit => {
103 const pred = commit.predecessors?.at(-1);
104 const existingInfo = pred ? dag.get(pred) : undefined;
105 // Pick a unique hash.
106 let hash = existingInfo?.hash ?? `fake:${commit.mark}`;
107 while (usedHashes.has(hash)) {
108 hash = hash + '_';
109 }
110 usedHashes.add(hash);
111 // Use existing CommitInfo as the "base" to build a new CommitInfo.
112 const info = DagCommitInfo.fromCommitInfo({
113 // "Default". Might be replaced by existingInfo.
114 bookmarks: [],
115 remoteBookmarks: [],
116 filePathsSample: [],
117 phase: 'draft',
118 // Note: using `existingInfo` here might be not accurate.
119 ...(existingInfo || {}),
120 // Replace existingInfo.
121 hash,
122 parents,
123 title: commit.text.trimStart().split('\n', 1).at(0) || '',
124 author: commit.author ?? '',
125 date: commit.date == null ? new Date() : new Date(commit.date[0] * 1000),
126 description: commit.text,
127 isDot: this.gotoMark ? commit.mark === this.gotoMark : (existingInfo?.isDot ?? false),
128 totalFileCount: Object.keys(commit.files).length,
129 closestPredecessors: commit.predecessors,
130 previewType: CommitPreview.STACK_EDIT_DESCENDANT,
131 });
132 parents = [info.hash];
133 return info;
134 });
135 }
136}
137