6.3 KB200 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 {Operation} from './operations/Operation';
9import type {CommitInfo, ExactRevset, Hash, OptimisticRevset, SucceedableRevset} from './types';
10
11import {Button} from 'isl-components/Button';
12import {Icon} from 'isl-components/Icon';
13import {Kbd} from 'isl-components/Kbd';
14import {Subtle} from 'isl-components/Subtle';
15import {atom} from 'jotai';
16import React from 'react';
17import {useContextMenu} from 'shared/ContextMenu';
18import {allCommands} from './ISLShortcuts';
19import './SuggestedRebase.css';
20import {tracker} from './analytics';
21import {findPublicBaseAncestor} from './getCommitTree';
22import {T} from './i18n';
23import {atomFamilyWeak, readAtom} from './jotaiUtils';
24import {BulkRebaseOperation} from './operations/BulkRebaseOperation';
25import {RebaseAllDraftCommitsOperation} from './operations/RebaseAllDraftCommitsOperation';
26import {RebaseOperation} from './operations/RebaseOperation';
27import {useRunOperation} from './operationsState';
28import {dagWithPreviews} from './previews';
29import {RelativeDate} from './relativeDate';
30import {commitsShownRange, latestDag} from './serverAPIState';
31import {succeedableRevset} from './types';
32
33/**
34 * Whether a given stack (from its base hash) is eligible for currently suggested rebase.
35 * Determined by if the stack is old enough and worth rebasing (i.e. not obsolete or closed)
36 */
37export const showSuggestedRebaseForStack = atomFamilyWeak((hash: Hash) =>
38 atom(get => {
39 const dag = get(dagWithPreviews);
40 const commit = dag.get(hash);
41 if (commit == null) {
42 return false;
43 }
44 const parentHash = commit.parents.at(0);
45 const stackBase = dag.get(parentHash);
46 if (stackBase == null) {
47 return false;
48 }
49
50 // If the public base is already on a remote bookmark or a stable commit, don't suggest rebasing it.
51 if (
52 stackBase.remoteBookmarks.length > 0 ||
53 stackBase.bookmarks.length > 0 ||
54 (stackBase.stableCommitMetadata?.length ?? 0) > 0
55 ) {
56 return false;
57 }
58
59 // If all commits are obsoleted, do not suggest rebasing (but should suggest cleanup).
60 const stack = dag.descendants(hash);
61 if (stack.size === dag.obsolete(stack).size) {
62 return false;
63 }
64
65 return true;
66 }),
67);
68
69export const suggestedRebaseDestinations = atom(get => {
70 const dag = get(latestDag);
71 const publicBase = findPublicBaseAncestor(get(dagWithPreviews));
72 const destinations: Array<[CommitInfo, React.ReactNode]> = dag
73 .getBatch(dag.public_().toArray())
74 .filter(
75 commit => commit.remoteBookmarks.length > 0 || (commit.stableCommitMetadata?.length ?? 0) > 0,
76 )
77 .map((commit): [CommitInfo, string] => [
78 commit,
79 [
80 ...commit.remoteBookmarks,
81 ...(commit.stableCommitMetadata?.map(s => s.value) ?? []),
82 ...commit.bookmarks,
83 ].join(', '),
84 ])
85 .filter(([_commit, label]) => label.length > 0);
86 if (publicBase) {
87 const [modifiers, keycode] = allCommands.RebaseOntoCurrentStackBase;
88 const publicBaseLabel = (
89 <T
90 replace={{
91 $shortcut: (
92 <Kbd modifiers={Array.isArray(modifiers) ? modifiers : [modifiers]} keycode={keycode} />
93 ),
94 }}>
95 Current Stack Base ($shortcut)
96 </T>
97 );
98 const existing = destinations.find(dest => dest[0].hash === publicBase.hash);
99 if (existing != null) {
100 existing[1] = (
101 <>
102 {publicBaseLabel}, {existing[1]}
103 </>
104 );
105 } else {
106 destinations.push([publicBase, publicBaseLabel]);
107 }
108 }
109 destinations.sort((a, b) => b[0].date.valueOf() - a[0].date.valueOf());
110
111 return destinations;
112});
113
114export function SuggestedRebaseButton({
115 source,
116 sources,
117 afterRun,
118}:
119 | {
120 source: SucceedableRevset | ExactRevset | OptimisticRevset;
121 sources?: undefined;
122 afterRun?: () => unknown;
123 }
124 | {
125 source?: undefined;
126 sources: Array<SucceedableRevset>;
127 afterRun?: () => unknown;
128 }
129 | {
130 source?: undefined;
131 sources?: undefined;
132 afterRun?: () => unknown;
133 }) {
134 const runOperation = useRunOperation();
135 const isBulk = source == null;
136 const isAllDraftCommits = sources == null && source == null;
137 const showContextMenu = useContextMenu(() => {
138 const destinations = readAtom(suggestedRebaseDestinations);
139 return (
140 destinations?.map(([dest, label]) => {
141 return {
142 label: (
143 <span className="suggested-rebase-context-menu-option">
144 <span>{label}</span>
145 <Subtle>
146 <RelativeDate date={dest.date} />
147 </Subtle>
148 </span>
149 ),
150 onClick: () => {
151 runOperation(getSuggestedRebaseOperation(dest, source ?? sources));
152 afterRun?.();
153 },
154 };
155 }) ?? []
156 );
157 });
158 return (
159 <Button icon={!isBulk} onClick={showContextMenu}>
160 <Icon icon="git-pull-request" slot="start" />
161 {isAllDraftCommits ? (
162 <T>Rebase all onto&hellip;</T>
163 ) : isBulk ? (
164 <T>Rebase selected commits onto...</T>
165 ) : (
166 <T>Rebase onto&hellip;</T>
167 )}
168 </Button>
169 );
170}
171
172/**
173 * Returns an operation that will rebase the given source onto the given destination.
174 * If source is undefined, rebase all draft commits.
175 * If source is an Array of revsets, bulk rebase those commits.
176 * If source is a single revset, rebase that commit.
177 */
178export function getSuggestedRebaseOperation(
179 dest: CommitInfo,
180 source: SucceedableRevset | ExactRevset | OptimisticRevset | Array<SucceedableRevset> | undefined,
181): Operation {
182 const destination = dest.remoteBookmarks?.[0] ?? dest.hash;
183 const isBulk = source != null && Array.isArray(source);
184 tracker.track('ClickSuggestedRebase', {
185 extras: {destination, isBulk, locations: dest.stableCommitMetadata?.map(s => s.value)},
186 });
187
188 const operation =
189 source == null
190 ? new RebaseAllDraftCommitsOperation(
191 readAtom(commitsShownRange),
192 succeedableRevset(destination),
193 )
194 : Array.isArray(source)
195 ? new BulkRebaseOperation(source, succeedableRevset(destination))
196 : new RebaseOperation(source, succeedableRevset(destination));
197
198 return operation;
199}
200