addons/isl/src/operations/ImportStackOperation.tsblame
View source
b69ab311/**
b69ab312 * Copyright (c) Meta Platforms, Inc. and affiliates.
b69ab313 *
b69ab314 * This source code is licensed under the MIT license found in the
b69ab315 * LICENSE file in the root directory of this source tree.
b69ab316 */
b69ab317
b69ab318import type {Hash} from 'shared/types/common';
b69ab319import type {ExportStack, ImportCommit, ImportStack, Mark} from 'shared/types/stack';
b69ab3110import type {Dag} from '../previews';
b69ab3111
b69ab3112import {DagCommitInfo} from '../dag/dagCommitInfo';
b69ab3113import {HashSet} from '../dag/set';
b69ab3114import {t} from '../i18n';
b69ab3115import {CommitPreview} from '../previews';
b69ab3116import {Operation} from './Operation';
b69ab3117
b69ab3118export class ImportStackOperation extends Operation {
b69ab3119 static opName = 'StackEdit';
b69ab3120
b69ab3121 // Derived from importStack.
b69ab3122
b69ab3123 /** Commits sorted from the stack bottom to top. */
b69ab3124 private commits: Readonly<ImportCommit>[];
b69ab3125
b69ab3126 /** Parent of the first commit. */
b69ab3127 private firstParent: Hash | null;
b69ab3128
b69ab3129 /** Goto command from the importStack. */
b69ab3130 private gotoMark: Mark | undefined;
b69ab3131
b69ab3132 constructor(
b69ab3133 private importStack: Readonly<ImportStack>,
b69ab3134 protected originalStack: Readonly<ExportStack>,
b69ab3135 ) {
b69ab3136 super('ImportStackOperation');
b69ab3137
b69ab3138 let firstParent: Hash | null = null;
b69ab3139 const origHashes = new Set<Hash>();
b69ab3140 const gotoMark = importStack
b69ab3141 .flatMap(([op, value]) => (op === 'goto' || op === 'reset' ? value.mark : []))
b69ab3142 .at(-1);
b69ab3143 const commits = importStack.flatMap(a => (a[0] === 'commit' ? [a[1]] : []));
b69ab3144 commits.forEach(commit => {
b69ab3145 if (firstParent == null) {
b69ab3146 firstParent = commit.parents.at(0) ?? 'unexpected_parents';
b69ab3147 }
b69ab3148 (commit.predecessors ?? []).forEach(pred => {
b69ab3149 origHashes.add(pred);
b69ab3150 });
b69ab3151 });
b69ab3152
b69ab3153 this.commits = commits;
b69ab3154 this.firstParent = firstParent;
b69ab3155 this.gotoMark = gotoMark;
b69ab3156 }
b69ab3157
b69ab3158 getArgs() {
b69ab3159 return ['debugimportstack'];
b69ab3160 }
b69ab3161
b69ab3162 getStdin() {
b69ab3163 return JSON.stringify(this.importStack);
b69ab3164 }
b69ab3165
b69ab3166 getDescriptionForDisplay() {
b69ab3167 return {
b69ab3168 description: t('Applying stack changes'),
b69ab3169 tooltip: t(
b69ab3170 'This operation does not have a traditional command line equivalent. \n' +
b69ab3171 'You might use commit, amend, histedit, rebase, absorb, fold, or a combination of them for similar functionalities.',
b69ab3172 ),
b69ab3173 };
b69ab3174 }
b69ab3175
b69ab3176 optimisticDag(dag: Dag): Dag {
b69ab3177 const originalHashes = this.originalStack.map(c => c.node);
b69ab3178 // Replace the old stack with the new stack, followed by a rebase.
b69ab3179 // Note the rebase is actually not what the operation does, but we always
b69ab3180 // follow up with a rebase operation if needed.
b69ab3181 const toRebase = dag.descendants(dag.children(originalHashes.at(-1)));
b69ab3182 let toRemove = HashSet.fromHashes(originalHashes).subtract(dag.ancestors(this.firstParent));
b69ab3183 // If the "toRemove" part of the original stack is gone, consider as completed.
b69ab3184 // Note: We no longer do a rebase in this case, and requires the rebase preview
b69ab3185 // to be handled separately.
b69ab3186 if (dag.present(toRemove).size === 0) {
b69ab3187 return dag;
b69ab3188 }
b69ab3189 // It's possible that the new stack was actually created but the head commit
b69ab3190 // keeps the old stack from disappearing (so the above check returns false).
b69ab3191 // In this case, we hide the new stack (by using successors) temporarily.
b69ab3192 // Otherwise we need to figure out the "new head", which is not trivial.
b69ab3193 toRemove = toRemove.union(dag.successors(toRemove));
b69ab3194 const newStack = this.previewStack(dag);
b69ab3195 const newDag = dag.remove(toRemove).add(newStack).rebase(toRebase, newStack.at(-1)?.hash);
b69ab3196 return newDag;
b69ab3197 }
b69ab3198
b69ab3199 private previewStack(dag: Dag): Array<DagCommitInfo> {
b69ab31100 let parents = this.firstParent ? [this.firstParent] : [];
b69ab31101 const usedHashes = new Set<Hash>();
b69ab31102 return this.commits.map(commit => {
b69ab31103 const pred = commit.predecessors?.at(-1);
b69ab31104 const existingInfo = pred ? dag.get(pred) : undefined;
b69ab31105 // Pick a unique hash.
b69ab31106 let hash = existingInfo?.hash ?? `fake:${commit.mark}`;
b69ab31107 while (usedHashes.has(hash)) {
b69ab31108 hash = hash + '_';
b69ab31109 }
b69ab31110 usedHashes.add(hash);
b69ab31111 // Use existing CommitInfo as the "base" to build a new CommitInfo.
b69ab31112 const info = DagCommitInfo.fromCommitInfo({
b69ab31113 // "Default". Might be replaced by existingInfo.
b69ab31114 bookmarks: [],
b69ab31115 remoteBookmarks: [],
b69ab31116 filePathsSample: [],
b69ab31117 phase: 'draft',
b69ab31118 // Note: using `existingInfo` here might be not accurate.
b69ab31119 ...(existingInfo || {}),
b69ab31120 // Replace existingInfo.
b69ab31121 hash,
b69ab31122 parents,
b69ab31123 title: commit.text.trimStart().split('\n', 1).at(0) || '',
b69ab31124 author: commit.author ?? '',
b69ab31125 date: commit.date == null ? new Date() : new Date(commit.date[0] * 1000),
b69ab31126 description: commit.text,
b69ab31127 isDot: this.gotoMark ? commit.mark === this.gotoMark : (existingInfo?.isDot ?? false),
b69ab31128 totalFileCount: Object.keys(commit.files).length,
b69ab31129 closestPredecessors: commit.predecessors,
b69ab31130 previewType: CommitPreview.STACK_EDIT_DESCENDANT,
b69ab31131 });
b69ab31132 parents = [info.hash];
b69ab31133 return info;
b69ab31134 });
b69ab31135 }
b69ab31136}