4.0 KB116 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 {DagCommitInfo} from '../dag/dagCommitInfo';
9import type {Dag} from '../previews';
10import type {ExactRevset, Hash, OptimisticRevset, SucceedableRevset} from '../types';
11
12import deepEqual from 'fast-deep-equal';
13import {t} from '../i18n';
14import {CommitPreview} from '../previews';
15import {latestSuccessor} from '../successionUtils';
16import {Operation} from './Operation';
17
18export class RebaseOperation extends Operation {
19 constructor(
20 private source: SucceedableRevset | ExactRevset | OptimisticRevset,
21 private destination: SucceedableRevset | ExactRevset | OptimisticRevset,
22 ) {
23 super('RebaseOperation');
24 }
25
26 static opName = 'Rebase';
27
28 equals(other?: Operation | null): boolean {
29 return (
30 other instanceof RebaseOperation &&
31 deepEqual([this.source, this.destination], [other.source, other.destination])
32 );
33 }
34
35 getArgs() {
36 return ['rebase', '-s', this.source, '-d', this.destination];
37 }
38
39 getInitialInlineProgress(): Array<[string, string]> {
40 // TODO: successions
41 return [[this.source.revset, t('rebasing...')]];
42 }
43
44 previewDag(dag: Dag): Dag {
45 const srcHash = dag.resolve(latestSuccessor(dag, this.source))?.hash;
46 const destHash = dag.resolve(latestSuccessor(dag, this.destination))?.hash;
47 if (srcHash == null || destHash == null) {
48 return dag;
49 }
50 const src = dag.descendants(srcHash);
51 const srcHashes = src.toHashes().toArray();
52 const prefix = `${REBASE_PREVIEW_HASH_PREFIX}:${destHash}:`;
53 const rewriteHash = (h: Hash) => (src.contains(h) ? prefix + h : h);
54 const date = new Date();
55 const newCommits = srcHashes.flatMap(h => {
56 const info = dag.get(h);
57 if (info == null) {
58 return [];
59 }
60 const isRoot = info.hash === srcHash;
61 const newInfo: DagCommitInfo = info.merge({
62 parents: isRoot ? [destHash] : info.parents,
63 date,
64 seqNumber: undefined,
65 previewType: isRoot ? CommitPreview.REBASE_ROOT : CommitPreview.REBASE_DESCENDANT,
66 });
67 return [newInfo];
68 });
69 // Rewrite REBASE_OLD commits to use fake hash so they won't conflict with
70 // the rebased commits. Insert new commits with the original hash so they
71 // can be interacted.
72 return dag
73 .replaceWith(src, (h, c) =>
74 c?.merge({
75 hash: rewriteHash(h),
76 parents: c.parents.map(rewriteHash),
77 previewType: CommitPreview.REBASE_OLD,
78 }),
79 )
80 .add(newCommits);
81 }
82
83 optimisticDag(dag: Dag): Dag {
84 const src = dag.resolve(latestSuccessor(dag, this.source))?.hash;
85 const dest = dag.resolve(latestSuccessor(dag, this.destination))?.hash;
86 // src already on dest?
87 if (src && dest && dag.parentHashes(src).includes(dest)) {
88 // The stack might be partially rebased while the rebase is ongoing.
89 // For example, graph
90 // a--b--c--d--e z
91 // Rebasing b (and descendants) to z, we might observe:
92 // a--bOld--cOld--d--e z--bNew--cNew
93
94 // bOld
95 const srcOrig = dag.resolve(latestSuccessor(dag.remove(src), this.source))?.hash;
96 // bOld + cOld + d + e
97 const toRebase = dag.descendants(srcOrig);
98 // bNew + cNew + d + e
99 const successors = dag.followSuccessors(toRebase);
100 // d + e
101 const remainingToRebase = toRebase.intersect(successors);
102 // cNew (simplified, does not handle merges)
103 const newDest = dag.heads(dag.descendants(src).intersect(successors)).toHashes().first();
104
105 // To test this in a real repo, try adding these configs to slow down rebases:
106 // --config "hooks.pretxncommit.slow=sleep 3"
107 // --config "rebase.experimental.inmemory=false"
108 // and goto the src stack top before rebasing.
109 return dag.rebase(remainingToRebase, newDest);
110 }
111 return dag.rebase(dag.descendants(src), dest);
112 }
113}
114
115export const REBASE_PREVIEW_HASH_PREFIX = 'OPTIMISTIC_REBASE_PREVIEW';
116