addons/isl/src/SuggestedRebase.tsxblame
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 {Operation} from './operations/Operation';
b69ab319import type {CommitInfo, ExactRevset, Hash, OptimisticRevset, SucceedableRevset} from './types';
b69ab3110
b69ab3111import {Button} from 'isl-components/Button';
b69ab3112import {Icon} from 'isl-components/Icon';
b69ab3113import {Kbd} from 'isl-components/Kbd';
b69ab3114import {Subtle} from 'isl-components/Subtle';
b69ab3115import {atom} from 'jotai';
b69ab3116import React from 'react';
b69ab3117import {useContextMenu} from 'shared/ContextMenu';
b69ab3118import {allCommands} from './ISLShortcuts';
b69ab3119import './SuggestedRebase.css';
b69ab3120import {tracker} from './analytics';
b69ab3121import {findPublicBaseAncestor} from './getCommitTree';
b69ab3122import {T} from './i18n';
b69ab3123import {atomFamilyWeak, readAtom} from './jotaiUtils';
b69ab3124import {BulkRebaseOperation} from './operations/BulkRebaseOperation';
b69ab3125import {RebaseAllDraftCommitsOperation} from './operations/RebaseAllDraftCommitsOperation';
b69ab3126import {RebaseOperation} from './operations/RebaseOperation';
b69ab3127import {useRunOperation} from './operationsState';
b69ab3128import {dagWithPreviews} from './previews';
b69ab3129import {RelativeDate} from './relativeDate';
b69ab3130import {commitsShownRange, latestDag} from './serverAPIState';
b69ab3131import {succeedableRevset} from './types';
b69ab3132
b69ab3133/**
b69ab3134 * Whether a given stack (from its base hash) is eligible for currently suggested rebase.
b69ab3135 * Determined by if the stack is old enough and worth rebasing (i.e. not obsolete or closed)
b69ab3136 */
b69ab3137export const showSuggestedRebaseForStack = atomFamilyWeak((hash: Hash) =>
b69ab3138 atom(get => {
b69ab3139 const dag = get(dagWithPreviews);
b69ab3140 const commit = dag.get(hash);
b69ab3141 if (commit == null) {
b69ab3142 return false;
b69ab3143 }
b69ab3144 const parentHash = commit.parents.at(0);
b69ab3145 const stackBase = dag.get(parentHash);
b69ab3146 if (stackBase == null) {
b69ab3147 return false;
b69ab3148 }
b69ab3149
b69ab3150 // If the public base is already on a remote bookmark or a stable commit, don't suggest rebasing it.
b69ab3151 if (
b69ab3152 stackBase.remoteBookmarks.length > 0 ||
b69ab3153 stackBase.bookmarks.length > 0 ||
b69ab3154 (stackBase.stableCommitMetadata?.length ?? 0) > 0
b69ab3155 ) {
b69ab3156 return false;
b69ab3157 }
b69ab3158
b69ab3159 // If all commits are obsoleted, do not suggest rebasing (but should suggest cleanup).
b69ab3160 const stack = dag.descendants(hash);
b69ab3161 if (stack.size === dag.obsolete(stack).size) {
b69ab3162 return false;
b69ab3163 }
b69ab3164
b69ab3165 return true;
b69ab3166 }),
b69ab3167);
b69ab3168
b69ab3169export const suggestedRebaseDestinations = atom(get => {
b69ab3170 const dag = get(latestDag);
b69ab3171 const publicBase = findPublicBaseAncestor(get(dagWithPreviews));
b69ab3172 const destinations: Array<[CommitInfo, React.ReactNode]> = dag
b69ab3173 .getBatch(dag.public_().toArray())
b69ab3174 .filter(
b69ab3175 commit => commit.remoteBookmarks.length > 0 || (commit.stableCommitMetadata?.length ?? 0) > 0,
b69ab3176 )
b69ab3177 .map((commit): [CommitInfo, string] => [
b69ab3178 commit,
b69ab3179 [
b69ab3180 ...commit.remoteBookmarks,
b69ab3181 ...(commit.stableCommitMetadata?.map(s => s.value) ?? []),
b69ab3182 ...commit.bookmarks,
b69ab3183 ].join(', '),
b69ab3184 ])
b69ab3185 .filter(([_commit, label]) => label.length > 0);
b69ab3186 if (publicBase) {
b69ab3187 const [modifiers, keycode] = allCommands.RebaseOntoCurrentStackBase;
b69ab3188 const publicBaseLabel = (
b69ab3189 <T
b69ab3190 replace={{
b69ab3191 $shortcut: (
b69ab3192 <Kbd modifiers={Array.isArray(modifiers) ? modifiers : [modifiers]} keycode={keycode} />
b69ab3193 ),
b69ab3194 }}>
b69ab3195 Current Stack Base ($shortcut)
b69ab3196 </T>
b69ab3197 );
b69ab3198 const existing = destinations.find(dest => dest[0].hash === publicBase.hash);
b69ab3199 if (existing != null) {
b69ab31100 existing[1] = (
b69ab31101 <>
b69ab31102 {publicBaseLabel}, {existing[1]}
b69ab31103 </>
b69ab31104 );
b69ab31105 } else {
b69ab31106 destinations.push([publicBase, publicBaseLabel]);
b69ab31107 }
b69ab31108 }
b69ab31109 destinations.sort((a, b) => b[0].date.valueOf() - a[0].date.valueOf());
b69ab31110
b69ab31111 return destinations;
b69ab31112});
b69ab31113
b69ab31114export function SuggestedRebaseButton({
b69ab31115 source,
b69ab31116 sources,
b69ab31117 afterRun,
b69ab31118}:
b69ab31119 | {
b69ab31120 source: SucceedableRevset | ExactRevset | OptimisticRevset;
b69ab31121 sources?: undefined;
b69ab31122 afterRun?: () => unknown;
b69ab31123 }
b69ab31124 | {
b69ab31125 source?: undefined;
b69ab31126 sources: Array<SucceedableRevset>;
b69ab31127 afterRun?: () => unknown;
b69ab31128 }
b69ab31129 | {
b69ab31130 source?: undefined;
b69ab31131 sources?: undefined;
b69ab31132 afterRun?: () => unknown;
b69ab31133 }) {
b69ab31134 const runOperation = useRunOperation();
b69ab31135 const isBulk = source == null;
b69ab31136 const isAllDraftCommits = sources == null && source == null;
b69ab31137 const showContextMenu = useContextMenu(() => {
b69ab31138 const destinations = readAtom(suggestedRebaseDestinations);
b69ab31139 return (
b69ab31140 destinations?.map(([dest, label]) => {
b69ab31141 return {
b69ab31142 label: (
b69ab31143 <span className="suggested-rebase-context-menu-option">
b69ab31144 <span>{label}</span>
b69ab31145 <Subtle>
b69ab31146 <RelativeDate date={dest.date} />
b69ab31147 </Subtle>
b69ab31148 </span>
b69ab31149 ),
b69ab31150 onClick: () => {
b69ab31151 runOperation(getSuggestedRebaseOperation(dest, source ?? sources));
b69ab31152 afterRun?.();
b69ab31153 },
b69ab31154 };
b69ab31155 }) ?? []
b69ab31156 );
b69ab31157 });
b69ab31158 return (
b69ab31159 <Button icon={!isBulk} onClick={showContextMenu}>
b69ab31160 <Icon icon="git-pull-request" slot="start" />
b69ab31161 {isAllDraftCommits ? (
b69ab31162 <T>Rebase all onto&hellip;</T>
b69ab31163 ) : isBulk ? (
b69ab31164 <T>Rebase selected commits onto...</T>
b69ab31165 ) : (
b69ab31166 <T>Rebase onto&hellip;</T>
b69ab31167 )}
b69ab31168 </Button>
b69ab31169 );
b69ab31170}
b69ab31171
b69ab31172/**
b69ab31173 * Returns an operation that will rebase the given source onto the given destination.
b69ab31174 * If source is undefined, rebase all draft commits.
b69ab31175 * If source is an Array of revsets, bulk rebase those commits.
b69ab31176 * If source is a single revset, rebase that commit.
b69ab31177 */
b69ab31178export function getSuggestedRebaseOperation(
b69ab31179 dest: CommitInfo,
b69ab31180 source: SucceedableRevset | ExactRevset | OptimisticRevset | Array<SucceedableRevset> | undefined,
b69ab31181): Operation {
b69ab31182 const destination = dest.remoteBookmarks?.[0] ?? dest.hash;
b69ab31183 const isBulk = source != null && Array.isArray(source);
b69ab31184 tracker.track('ClickSuggestedRebase', {
b69ab31185 extras: {destination, isBulk, locations: dest.stableCommitMetadata?.map(s => s.value)},
b69ab31186 });
b69ab31187
b69ab31188 const operation =
b69ab31189 source == null
b69ab31190 ? new RebaseAllDraftCommitsOperation(
b69ab31191 readAtom(commitsShownRange),
b69ab31192 succeedableRevset(destination),
b69ab31193 )
b69ab31194 : Array.isArray(source)
b69ab31195 ? new BulkRebaseOperation(source, succeedableRevset(destination))
b69ab31196 : new RebaseOperation(source, succeedableRevset(destination));
b69ab31197
b69ab31198 return operation;
b69ab31199}