5.0 KB159 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 './types';
9
10import {Button} from 'isl-components/Button';
11import {Icon} from 'isl-components/Icon';
12import {Tooltip} from 'isl-components/Tooltip';
13import {atom, useAtomValue} from 'jotai';
14import {HighlightCommitsWhileHovering} from './HighlightedCommits';
15import {Internal} from './Internal';
16import {tracker} from './analytics';
17import {useFeatureFlagSync} from './featureFlags';
18import {t, T} from './i18n';
19import {atomFamilyWeak} from './jotaiUtils';
20import {RebaseOperation} from './operations/RebaseOperation';
21import {useRunOperation} from './operationsState';
22import {dagWithPreviews} from './previews';
23import {succeedableRevset} from './types';
24
25/**
26 * For a given obsolete commit, returns info about its orphaned (non-obsolete)
27 * children and the latest successor to rebase them onto, or null if the button
28 * should not be shown.
29 */
30export const orphanedChildrenForCommit = atomFamilyWeak((hash: Hash) =>
31 atom(get => {
32 const dag = get(dagWithPreviews);
33 const commit = dag.get(hash);
34 if (commit == null || commit.successorInfo == null) {
35 return null;
36 }
37
38 // Don't show for landed commits — they use the Cleanup workflow instead.
39 const successorType = commit.successorInfo.type;
40 if (successorType === 'land' || successorType === 'pushrebase') {
41 return null;
42 }
43
44 // Follow the successor chain to find the latest non-obsolete successor.
45 const successors = dag.followSuccessors(hash);
46 // followSuccessors returns a set; remove the original hash to get just successors.
47 const successorHashes = successors
48 .toHashes()
49 .toArray()
50 .filter(h => h !== hash);
51 if (successorHashes.length === 0) {
52 return null;
53 }
54
55 // Pick the first successor. If it's not in the DAG or is itself obsolete, bail out.
56 const successorHash = successorHashes[0];
57 const successorCommit = dag.get(successorHash);
58 if (successorCommit == null || successorCommit.successorInfo != null) {
59 return null;
60 }
61
62 // Find non-obsolete children of this obsolete commit.
63 const children = dag.children(hash);
64 const nonObsoleteChildren = dag.nonObsolete(children);
65 if (nonObsoleteChildren.size === 0) {
66 return null;
67 }
68
69 return {
70 orphanedChildren: nonObsoleteChildren.toHashes().toArray(),
71 successorHash,
72 };
73 }),
74);
75
76/**
77 * For a given stack root hash, aggregates all orphaned children across all
78 * obsolete commits in the stack. Returns the list of rebase operations needed
79 * (each mapping an orphaned child to its target successor), or null if no
80 * orphaned commits exist in the stack.
81 */
82export const orphanedChildrenForStack = atomFamilyWeak((hash: Hash) =>
83 atom(get => {
84 const dag = get(dagWithPreviews);
85 const stackHashes = dag.descendants(hash).toHashes().toArray();
86
87 const rebaseEntries: Array<{orphanedChild: Hash; successorHash: Hash}> = [];
88 const allOrphaned: Hash[] = [];
89 const allSuccessors: Hash[] = [];
90
91 for (const h of stackHashes) {
92 const info = get(orphanedChildrenForCommit(h));
93 if (info != null) {
94 for (const child of info.orphanedChildren) {
95 rebaseEntries.push({orphanedChild: child, successorHash: info.successorHash});
96 allOrphaned.push(child);
97 }
98 if (!allSuccessors.includes(info.successorHash)) {
99 allSuccessors.push(info.successorHash);
100 }
101 }
102 }
103
104 if (rebaseEntries.length === 0) {
105 return null;
106 }
107
108 return {
109 rebaseEntries,
110 allOrphaned,
111 allSuccessors,
112 };
113 }),
114);
115
116export function RebaseOrphanedStackButton({hash}: {hash: Hash}) {
117 const featureEnabled = useFeatureFlagSync(Internal.featureFlags?.RebaseOntoSuccessor);
118 const info = useAtomValue(orphanedChildrenForStack(hash));
119 const runOperation = useRunOperation();
120
121 if (!featureEnabled) {
122 return null;
123 }
124
125 if (info == null) {
126 return null;
127 }
128
129 const {rebaseEntries, allOrphaned, allSuccessors} = info;
130
131 const handleClick = async () => {
132 tracker.track('ClickRebaseOntoSuccessor', {
133 extras: {
134 sources: allOrphaned,
135 dest: allSuccessors.join(','),
136 numOrphans: allOrphaned.length,
137 },
138 });
139 for (const {orphanedChild, successorHash} of rebaseEntries) {
140 runOperation(
141 new RebaseOperation(succeedableRevset(orphanedChild), succeedableRevset(successorHash)),
142 );
143 }
144 };
145
146 return (
147 <HighlightCommitsWhileHovering toHighlight={[...allOrphaned, ...allSuccessors]}>
148 <Tooltip
149 title={t('Rebase orphaned commits onto the latest successors of their obsolete parents')}
150 placement="bottom">
151 <Button icon onClick={handleClick} data-testid="rebase-onto-successor-button">
152 <Icon icon="git-pull-request" slot="start" />
153 <T>Rebase orphaned commits</T>
154 </Button>
155 </Tooltip>
156 </HighlightCommitsWhileHovering>
157 );
158}
159