addons/isl/src/RebaseOntoSuccessor.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 {Hash} from './types';
b69ab319
b69ab3110import {Button} from 'isl-components/Button';
b69ab3111import {Icon} from 'isl-components/Icon';
b69ab3112import {Tooltip} from 'isl-components/Tooltip';
b69ab3113import {atom, useAtomValue} from 'jotai';
b69ab3114import {HighlightCommitsWhileHovering} from './HighlightedCommits';
b69ab3115import {Internal} from './Internal';
b69ab3116import {tracker} from './analytics';
b69ab3117import {useFeatureFlagSync} from './featureFlags';
b69ab3118import {t, T} from './i18n';
b69ab3119import {atomFamilyWeak} from './jotaiUtils';
b69ab3120import {RebaseOperation} from './operations/RebaseOperation';
b69ab3121import {useRunOperation} from './operationsState';
b69ab3122import {dagWithPreviews} from './previews';
b69ab3123import {succeedableRevset} from './types';
b69ab3124
b69ab3125/**
b69ab3126 * For a given obsolete commit, returns info about its orphaned (non-obsolete)
b69ab3127 * children and the latest successor to rebase them onto, or null if the button
b69ab3128 * should not be shown.
b69ab3129 */
b69ab3130export const orphanedChildrenForCommit = atomFamilyWeak((hash: Hash) =>
b69ab3131 atom(get => {
b69ab3132 const dag = get(dagWithPreviews);
b69ab3133 const commit = dag.get(hash);
b69ab3134 if (commit == null || commit.successorInfo == null) {
b69ab3135 return null;
b69ab3136 }
b69ab3137
b69ab3138 // Don't show for landed commits — they use the Cleanup workflow instead.
b69ab3139 const successorType = commit.successorInfo.type;
b69ab3140 if (successorType === 'land' || successorType === 'pushrebase') {
b69ab3141 return null;
b69ab3142 }
b69ab3143
b69ab3144 // Follow the successor chain to find the latest non-obsolete successor.
b69ab3145 const successors = dag.followSuccessors(hash);
b69ab3146 // followSuccessors returns a set; remove the original hash to get just successors.
b69ab3147 const successorHashes = successors
b69ab3148 .toHashes()
b69ab3149 .toArray()
b69ab3150 .filter(h => h !== hash);
b69ab3151 if (successorHashes.length === 0) {
b69ab3152 return null;
b69ab3153 }
b69ab3154
b69ab3155 // Pick the first successor. If it's not in the DAG or is itself obsolete, bail out.
b69ab3156 const successorHash = successorHashes[0];
b69ab3157 const successorCommit = dag.get(successorHash);
b69ab3158 if (successorCommit == null || successorCommit.successorInfo != null) {
b69ab3159 return null;
b69ab3160 }
b69ab3161
b69ab3162 // Find non-obsolete children of this obsolete commit.
b69ab3163 const children = dag.children(hash);
b69ab3164 const nonObsoleteChildren = dag.nonObsolete(children);
b69ab3165 if (nonObsoleteChildren.size === 0) {
b69ab3166 return null;
b69ab3167 }
b69ab3168
b69ab3169 return {
b69ab3170 orphanedChildren: nonObsoleteChildren.toHashes().toArray(),
b69ab3171 successorHash,
b69ab3172 };
b69ab3173 }),
b69ab3174);
b69ab3175
b69ab3176/**
b69ab3177 * For a given stack root hash, aggregates all orphaned children across all
b69ab3178 * obsolete commits in the stack. Returns the list of rebase operations needed
b69ab3179 * (each mapping an orphaned child to its target successor), or null if no
b69ab3180 * orphaned commits exist in the stack.
b69ab3181 */
b69ab3182export const orphanedChildrenForStack = atomFamilyWeak((hash: Hash) =>
b69ab3183 atom(get => {
b69ab3184 const dag = get(dagWithPreviews);
b69ab3185 const stackHashes = dag.descendants(hash).toHashes().toArray();
b69ab3186
b69ab3187 const rebaseEntries: Array<{orphanedChild: Hash; successorHash: Hash}> = [];
b69ab3188 const allOrphaned: Hash[] = [];
b69ab3189 const allSuccessors: Hash[] = [];
b69ab3190
b69ab3191 for (const h of stackHashes) {
b69ab3192 const info = get(orphanedChildrenForCommit(h));
b69ab3193 if (info != null) {
b69ab3194 for (const child of info.orphanedChildren) {
b69ab3195 rebaseEntries.push({orphanedChild: child, successorHash: info.successorHash});
b69ab3196 allOrphaned.push(child);
b69ab3197 }
b69ab3198 if (!allSuccessors.includes(info.successorHash)) {
b69ab3199 allSuccessors.push(info.successorHash);
b69ab31100 }
b69ab31101 }
b69ab31102 }
b69ab31103
b69ab31104 if (rebaseEntries.length === 0) {
b69ab31105 return null;
b69ab31106 }
b69ab31107
b69ab31108 return {
b69ab31109 rebaseEntries,
b69ab31110 allOrphaned,
b69ab31111 allSuccessors,
b69ab31112 };
b69ab31113 }),
b69ab31114);
b69ab31115
b69ab31116export function RebaseOrphanedStackButton({hash}: {hash: Hash}) {
b69ab31117 const featureEnabled = useFeatureFlagSync(Internal.featureFlags?.RebaseOntoSuccessor);
b69ab31118 const info = useAtomValue(orphanedChildrenForStack(hash));
b69ab31119 const runOperation = useRunOperation();
b69ab31120
b69ab31121 if (!featureEnabled) {
b69ab31122 return null;
b69ab31123 }
b69ab31124
b69ab31125 if (info == null) {
b69ab31126 return null;
b69ab31127 }
b69ab31128
b69ab31129 const {rebaseEntries, allOrphaned, allSuccessors} = info;
b69ab31130
b69ab31131 const handleClick = async () => {
b69ab31132 tracker.track('ClickRebaseOntoSuccessor', {
b69ab31133 extras: {
b69ab31134 sources: allOrphaned,
b69ab31135 dest: allSuccessors.join(','),
b69ab31136 numOrphans: allOrphaned.length,
b69ab31137 },
b69ab31138 });
b69ab31139 for (const {orphanedChild, successorHash} of rebaseEntries) {
b69ab31140 runOperation(
b69ab31141 new RebaseOperation(succeedableRevset(orphanedChild), succeedableRevset(successorHash)),
b69ab31142 );
b69ab31143 }
b69ab31144 };
b69ab31145
b69ab31146 return (
b69ab31147 <HighlightCommitsWhileHovering toHighlight={[...allOrphaned, ...allSuccessors]}>
b69ab31148 <Tooltip
b69ab31149 title={t('Rebase orphaned commits onto the latest successors of their obsolete parents')}
b69ab31150 placement="bottom">
b69ab31151 <Button icon onClick={handleClick} data-testid="rebase-onto-successor-button">
b69ab31152 <Icon icon="git-pull-request" slot="start" />
b69ab31153 <T>Rebase orphaned commits</T>
b69ab31154 </Button>
b69ab31155 </Tooltip>
b69ab31156 </HighlightCommitsWhileHovering>
b69ab31157 );
b69ab31158}