addons/isl/src/fold.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 {CommitInfo, 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 {useCallback} from 'react';
b69ab3115import {editedCommitMessages} from './CommitInfoView/CommitInfoState';
b69ab3116import {
b69ab3117 applyEditedFields,
b69ab3118 commitMessageFieldsSchema,
b69ab3119 commitMessageFieldsToString,
b69ab3120 mergeManyCommitMessageFields,
b69ab3121 parseCommitMessageFields,
b69ab3122} from './CommitInfoView/CommitMessageFields';
b69ab3123import {T, t} from './i18n';
b69ab3124import {readAtom, writeAtom} from './jotaiUtils';
b69ab3125import {
b69ab3126 FOLD_COMMIT_PREVIEW_HASH_PREFIX,
b69ab3127 FoldOperation,
b69ab3128 getFoldRangeCommitHash,
b69ab3129} from './operations/FoldOperation';
b69ab3130import {operationBeingPreviewed, useRunPreviewedOperation} from './operationsState';
b69ab3131import {type Dag, dagWithPreviews} from './previews';
b69ab3132import {selectedCommits} from './selection';
b69ab3133import {firstOfIterable} from './utils';
b69ab3134
b69ab3135/**
b69ab3136 * If the selected commits are linear, contiguous, and non-branching, they may be folded together.
b69ab3137 * This selector gives the range of commits that can be folded, if any,
b69ab3138 * so a button may be shown to do the fold.
b69ab3139 */
b69ab3140export const foldableSelection = atom(get => {
b69ab3141 const selection = get(selectedCommits);
b69ab3142 if (selection.size < 2) {
b69ab3143 return undefined;
b69ab3144 }
b69ab3145 const dag = get(dagWithPreviews);
b69ab3146 const foldable = getFoldableRange(selection, dag);
b69ab3147 return foldable;
b69ab3148});
b69ab3149
b69ab3150/**
b69ab3151 * Given a set of selected commits, order them into an array from bottom to top.
b69ab3152 * If commits are not contiguous, returns undefined.
b69ab3153 * This selection must be linear and contiguous: no branches out are allowed.
b69ab3154 * This constitutes a set of commits that may be "folded"/combined into a single commit via `sl fold`.
b69ab3155 */
b69ab3156export function getFoldableRange(selection: Set<Hash>, dag: Dag): Array<CommitInfo> | undefined {
b69ab3157 const set = dag.present(selection);
b69ab3158 if (set.size <= 1) {
b69ab3159 return undefined;
b69ab3160 }
b69ab3161 const head = dag.heads(set);
b69ab3162 if (
b69ab3163 head.size !== 1 ||
b69ab3164 dag.roots(set).size !== 1 ||
b69ab3165 dag.merge(set).size > 0 ||
b69ab3166 dag.public_(set).size > 0 ||
b69ab3167 // only head can have other children
b69ab3168 dag.children(set.subtract(head)).subtract(set).size > 0
b69ab3169 ) {
b69ab3170 return undefined;
b69ab3171 }
b69ab3172 return dag.getBatch(dag.sortAsc(set, {gap: false}));
b69ab3173}
b69ab3174
b69ab3175export function FoldButton({commit}: {commit?: CommitInfo}) {
b69ab3176 const foldable = useAtomValue(foldableSelection);
b69ab3177 const onClick = useCallback(() => {
b69ab3178 if (foldable == null) {
b69ab3179 return;
b69ab3180 }
b69ab3181 const schema = readAtom(commitMessageFieldsSchema);
b69ab3182 if (schema == null) {
b69ab3183 return;
b69ab3184 }
b69ab3185 const messageFields = mergeManyCommitMessageFields(
b69ab3186 schema,
b69ab3187 foldable.map(commit => parseCommitMessageFields(schema, commit.title, commit.description)),
b69ab3188 );
b69ab3189 const message = commitMessageFieldsToString(schema, messageFields);
b69ab3190 writeAtom(operationBeingPreviewed, new FoldOperation(foldable, message));
b69ab3191 writeAtom(selectedCommits, new Set([getFoldRangeCommitHash(foldable, /* isPreview */ true)]));
b69ab3192 }, [foldable]);
b69ab3193 if (foldable == null || (commit != null && foldable?.[0]?.hash !== commit.hash)) {
b69ab3194 return null;
b69ab3195 }
b69ab3196 return (
b69ab3197 <Tooltip title={t('Combine selected commits into one commit')}>
b69ab3198 <Button onClick={onClick}>
b69ab3199 <Icon icon="fold" slot="start" />
b69ab31100 <T replace={{$count: foldable.length}}>Combine $count commits</T>
b69ab31101 </Button>
b69ab31102 </Tooltip>
b69ab31103 );
b69ab31104}
b69ab31105
b69ab31106/**
b69ab31107 * Make a new copy of the FoldOperation with the latest edited message for the combined preview.
b69ab31108 * This allows running the fold operation to use the newly typed message.
b69ab31109 */
b69ab31110export function updateFoldedMessageWithEditedMessage(): FoldOperation | undefined {
b69ab31111 const beingPreviewed = readAtom(operationBeingPreviewed);
b69ab31112 if (beingPreviewed != null && beingPreviewed instanceof FoldOperation) {
b69ab31113 const range = beingPreviewed.getFoldRange();
b69ab31114 const combinedHash = getFoldRangeCommitHash(range, /* isPreview */ true);
b69ab31115 const [existingTitle, existingMessage] = beingPreviewed.getFoldedMessage();
b69ab31116 const editedMessage = readAtom(editedCommitMessages(combinedHash));
b69ab31117
b69ab31118 const schema = readAtom(commitMessageFieldsSchema);
b69ab31119 if (schema == null) {
b69ab31120 return undefined;
b69ab31121 }
b69ab31122
b69ab31123 const old = parseCommitMessageFields(schema, existingTitle, existingMessage);
b69ab31124 const message = editedMessage == null ? old : applyEditedFields(old, editedMessage);
b69ab31125
b69ab31126 const newMessage = commitMessageFieldsToString(schema, message);
b69ab31127
b69ab31128 return new FoldOperation(range, newMessage);
b69ab31129 }
b69ab31130}
b69ab31131
b69ab31132export function useRunFoldPreview(): [cancel: () => unknown, run: () => unknown] {
b69ab31133 const handlePreviewedOperation = useRunPreviewedOperation();
b69ab31134 const run = useCallback(() => {
b69ab31135 const foldOperation = updateFoldedMessageWithEditedMessage();
b69ab31136 if (foldOperation == null) {
b69ab31137 return;
b69ab31138 }
b69ab31139 handlePreviewedOperation(/* isCancel */ false, foldOperation);
b69ab31140 // select the optimistic commit instead of the preview commit
b69ab31141 writeAtom(selectedCommits, last =>
b69ab31142 last.size === 1 && firstOfIterable(last.values())?.startsWith(FOLD_COMMIT_PREVIEW_HASH_PREFIX)
b69ab31143 ? new Set([getFoldRangeCommitHash(foldOperation.getFoldRange(), /* isPreview */ false)])
b69ab31144 : last,
b69ab31145 );
b69ab31146 }, [handlePreviewedOperation]);
b69ab31147 return [
b69ab31148 () => {
b69ab31149 handlePreviewedOperation(/* isCancel */ true);
b69ab31150 },
b69ab31151 run,
b69ab31152 ];
b69ab31153}