addons/isl/src/previews.tsblame
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 {Dag} from './dag/dag';
b69ab319import type {CommitTreeWithPreviews} from './getCommitTree';
b69ab3110import type {Operation} from './operations/Operation';
b69ab3111import type {OperationInfo, OperationList} from './operationsState';
b69ab3112import type {ChangedFile, CommitInfo, Hash, MergeConflicts, UncommittedChanges} from './types';
b69ab3113
b69ab3114import {atom, useAtom, useAtomValue} from 'jotai';
b69ab3115import {useEffect} from 'react';
b69ab3116import {notEmpty, nullthrows} from 'shared/utils';
b69ab3117import {latestSuccessorsMapAtom} from './SuccessionTracker';
b69ab3118import {getTracker} from './analytics/globalTracker';
b69ab3119import {focusMode} from './atoms/FocusModeState';
b69ab3120import {YOU_ARE_HERE_VIRTUAL_COMMIT} from './dag/virtualCommit';
b69ab3121import {getCommitTree, walkTreePostorder} from './getCommitTree';
b69ab3122import {getOpName} from './operations/Operation';
b69ab3123import {operationBeingPreviewed, operationList, queuedOperations} from './operationsState';
b69ab3124import {
b69ab3125 latestCommits,
b69ab3126 latestCommitsData,
b69ab3127 latestDag,
b69ab3128 latestHeadCommit,
b69ab3129 latestUncommittedChanges,
b69ab3130 latestUncommittedChangesData,
b69ab3131 mergeConflicts,
b69ab3132} from './serverAPIState';
b69ab3133
b69ab3134export enum CommitPreview {
b69ab3135 REBASE_ROOT = 'rebase-root',
b69ab3136 REBASE_DESCENDANT = 'rebase-descendant',
b69ab3137 REBASE_OLD = 'rebase-old',
b69ab3138 REBASE_OPTIMISTIC_ROOT = 'rebase-optimistic-root',
b69ab3139 REBASE_OPTIMISTIC_DESCENDANT = 'rebase-optimistic-descendant',
b69ab3140 GOTO_DESTINATION = 'goto-destination',
b69ab3141 GOTO_PREVIOUS_LOCATION = 'goto-previous-location',
b69ab3142 HIDDEN_ROOT = 'hidden-root',
b69ab3143 HIDDEN_DESCENDANT = 'hidden-descendant',
b69ab3144 STACK_EDIT_ROOT = 'stack-edit-root',
b69ab3145 STACK_EDIT_DESCENDANT = 'stack-edit-descendant',
b69ab3146 FOLD_PREVIEW = 'fold-preview',
b69ab3147 FOLD = 'fold',
b69ab3148 // Commit being rendered in some other context than the commit tree,
b69ab3149 // such as the commit info sidebar
b69ab3150 NON_ACTIONABLE_COMMIT = 'non-actionable-commit',
b69ab3151}
b69ab3152
b69ab3153/**
b69ab3154 * Alter the set of Uncommitted Changes.
b69ab3155 */
b69ab3156export type ApplyUncommittedChangesPreviewsFuncType = (
b69ab3157 changes: UncommittedChanges,
b69ab3158) => UncommittedChanges;
b69ab3159
b69ab3160/**
b69ab3161 * Alter the set of Merge Conflicts.
b69ab3162 */
b69ab3163export type ApplyMergeConflictsPreviewsFuncType = (
b69ab3164 conflicts: MergeConflicts | undefined,
b69ab3165) => MergeConflicts | undefined;
b69ab3166
b69ab3167function applyPreviewsToChangedFiles(
b69ab3168 files: Array<ChangedFile>,
b69ab3169 list: OperationList,
b69ab3170 queued: Array<Operation>,
b69ab3171): Array<ChangedFile> {
b69ab3172 const currentOperation = list.currentOperation;
b69ab3173
b69ab3174 // gather operations from past, current, and queued commands which could have optimistic state appliers
b69ab3175 type Applier = (
b69ab3176 context: UncommittedChangesPreviewContext,
b69ab3177 ) => ApplyUncommittedChangesPreviewsFuncType | undefined;
b69ab3178 const appliersSources: Array<Applier> = [];
b69ab3179
b69ab3180 // previous commands
b69ab3181 for (const op of list.operationHistory) {
b69ab3182 if (op != null && !op.hasCompletedUncommittedChangesOptimisticState) {
b69ab3183 if (op.operation.makeOptimisticUncommittedChangesApplier != null) {
b69ab3184 appliersSources.push(
b69ab3185 op.operation.makeOptimisticUncommittedChangesApplier.bind(op.operation),
b69ab3186 );
b69ab3187 }
b69ab3188 }
b69ab3189 }
b69ab3190
b69ab3191 // currently running/last command
b69ab3192 if (
b69ab3193 currentOperation != null &&
b69ab3194 !currentOperation.hasCompletedUncommittedChangesOptimisticState &&
b69ab3195 // don't show optimistic state if we hit an error
b69ab3196 (currentOperation.exitCode == null || currentOperation.exitCode === 0)
b69ab3197 ) {
b69ab3198 if (currentOperation.operation.makeOptimisticUncommittedChangesApplier != null) {
b69ab3199 appliersSources.push(
b69ab31100 currentOperation.operation.makeOptimisticUncommittedChangesApplier.bind(
b69ab31101 currentOperation.operation,
b69ab31102 ),
b69ab31103 );
b69ab31104 }
b69ab31105 }
b69ab31106
b69ab31107 // queued commands
b69ab31108 for (const op of queued) {
b69ab31109 if (op != null) {
b69ab31110 if (op.makeOptimisticUncommittedChangesApplier != null) {
b69ab31111 appliersSources.push(op.makeOptimisticUncommittedChangesApplier.bind(op));
b69ab31112 }
b69ab31113 }
b69ab31114 }
b69ab31115
b69ab31116 // apply in order
b69ab31117 if (appliersSources.length) {
b69ab31118 let finalChanges = files;
b69ab31119
b69ab31120 for (const applierSource of appliersSources) {
b69ab31121 const context: UncommittedChangesPreviewContext = {
b69ab31122 uncommittedChanges: files,
b69ab31123 };
b69ab31124
b69ab31125 const applier = applierSource(context);
b69ab31126 if (applier == null) {
b69ab31127 continue;
b69ab31128 }
b69ab31129
b69ab31130 finalChanges = applier(finalChanges);
b69ab31131 }
b69ab31132 return finalChanges;
b69ab31133 }
b69ab31134
b69ab31135 return files;
b69ab31136}
b69ab31137
b69ab31138function applyPreviewsToMergeConflicts(
b69ab31139 conflicts: MergeConflicts,
b69ab31140 list: OperationList,
b69ab31141 queued: Array<Operation>,
b69ab31142): MergeConflicts | undefined {
b69ab31143 const currentOperation = list.currentOperation;
b69ab31144 if (conflicts.state !== 'loaded') {
b69ab31145 return conflicts;
b69ab31146 }
b69ab31147
b69ab31148 // gather operations from past, current, and queued commands which could have optimistic state appliers
b69ab31149 type Applier = (
b69ab31150 context: MergeConflictsPreviewContext,
b69ab31151 ) => ApplyMergeConflictsPreviewsFuncType | undefined;
b69ab31152 const appliersSources: Array<Applier> = [];
b69ab31153
b69ab31154 // previous commands
b69ab31155 for (const op of list.operationHistory) {
b69ab31156 if (op != null && !op.hasCompletedMergeConflictsOptimisticState) {
b69ab31157 if (op.operation.makeOptimisticMergeConflictsApplier != null) {
b69ab31158 appliersSources.push(op.operation.makeOptimisticMergeConflictsApplier.bind(op.operation));
b69ab31159 }
b69ab31160 }
b69ab31161 }
b69ab31162
b69ab31163 // currently running/last command
b69ab31164 if (
b69ab31165 currentOperation != null &&
b69ab31166 !currentOperation.hasCompletedMergeConflictsOptimisticState &&
b69ab31167 // don't show optimistic state if we hit an error
b69ab31168 (currentOperation.exitCode == null || currentOperation.exitCode === 0)
b69ab31169 ) {
b69ab31170 if (currentOperation.operation.makeOptimisticMergeConflictsApplier != null) {
b69ab31171 appliersSources.push(
b69ab31172 currentOperation.operation.makeOptimisticMergeConflictsApplier.bind(
b69ab31173 currentOperation.operation,
b69ab31174 ),
b69ab31175 );
b69ab31176 }
b69ab31177 }
b69ab31178
b69ab31179 // queued commands
b69ab31180 for (const op of queued) {
b69ab31181 if (op != null) {
b69ab31182 if (op.makeOptimisticMergeConflictsApplier != null) {
b69ab31183 appliersSources.push(op.makeOptimisticMergeConflictsApplier.bind(op));
b69ab31184 }
b69ab31185 }
b69ab31186 }
b69ab31187
b69ab31188 // apply in order
b69ab31189 if (appliersSources.length) {
b69ab31190 let finalChanges: MergeConflicts | undefined = conflicts;
b69ab31191
b69ab31192 for (const applierSource of appliersSources) {
b69ab31193 const context: MergeConflictsPreviewContext = {
b69ab31194 conflicts,
b69ab31195 };
b69ab31196
b69ab31197 const applier = applierSource(context);
b69ab31198 if (applier == null) {
b69ab31199 continue;
b69ab31200 }
b69ab31201
b69ab31202 finalChanges = applier(finalChanges);
b69ab31203 }
b69ab31204 return finalChanges;
b69ab31205 }
b69ab31206 return conflicts;
b69ab31207}
b69ab31208
b69ab31209export const uncommittedChangesWithPreviews = atom<Array<ChangedFile>>(get => {
b69ab31210 const list = get(operationList);
b69ab31211 const queued = get(queuedOperations);
b69ab31212 const uncommittedChanges = get(latestUncommittedChanges);
b69ab31213
b69ab31214 return applyPreviewsToChangedFiles(uncommittedChanges, list, queued);
b69ab31215});
b69ab31216
b69ab31217export const optimisticMergeConflicts = atom(get => {
b69ab31218 const list = get(operationList);
b69ab31219 const queued = get(queuedOperations);
b69ab31220 const conflicts = get(mergeConflicts);
b69ab31221 if (conflicts?.files == null) {
b69ab31222 return conflicts;
b69ab31223 }
b69ab31224
b69ab31225 return applyPreviewsToMergeConflicts(conflicts, list, queued);
b69ab31226});
b69ab31227
b69ab31228export type TreeWithPreviews = {
b69ab31229 trees: Array<CommitTreeWithPreviews>;
b69ab31230 treeMap: Map<Hash, CommitTreeWithPreviews>;
b69ab31231 headCommit?: CommitInfo;
b69ab31232};
b69ab31233
b69ab31234export type WithPreviewType = {
b69ab31235 previewType?: CommitPreview;
b69ab31236 /**
b69ab31237 * Insertion batch. Larger: later inserted.
b69ab31238 * All 'sl log' commits share a same initial number.
b69ab31239 * Later previews might have larger numbers.
b69ab31240 * Used for sorting.
b69ab31241 */
b69ab31242 seqNumber?: number;
b69ab31243};
b69ab31244
b69ab31245export type {Dag};
b69ab31246
b69ab31247export const dagWithPreviews = atom(get => {
b69ab31248 const originalDag = get(latestDag);
b69ab31249 const list = get(operationList);
b69ab31250 const queued = get(queuedOperations);
b69ab31251 const currentOperation = list.currentOperation;
b69ab31252 const history = list.operationHistory;
b69ab31253 const currentPreview = get(operationBeingPreviewed);
b69ab31254 let dag = originalDag;
b69ab31255
b69ab31256 const focus = get(focusMode);
b69ab31257 if (focus) {
b69ab31258 const current = dag.resolve('.');
b69ab31259 if (current) {
b69ab31260 const currentStack = dag.descendants(
b69ab31261 dag.ancestors(dag.draft(current.hash), {within: dag.draft()}),
b69ab31262 );
b69ab31263 const related = dag.descendants(
b69ab31264 dag.successors(currentStack).union(dag.predecessors(currentStack)),
b69ab31265 );
b69ab31266 const toKeep = currentStack
b69ab31267 .union(YOU_ARE_HERE_VIRTUAL_COMMIT.hash) // ensure we always show "You Are Here"
b69ab31268 .union(related);
b69ab31269 const toRemove = dag.draft().subtract(toKeep);
b69ab31270 dag = dag.remove(toRemove);
b69ab31271 }
b69ab31272 }
b69ab31273
b69ab31274 for (const op of optimisticOperations({history, queued, currentOperation})) {
b69ab31275 dag = op.optimisticDag(dag);
b69ab31276 }
b69ab31277 if (currentPreview) {
b69ab31278 dag = currentPreview.previewDag(dag);
b69ab31279 }
b69ab31280 return dag;
b69ab31281});
b69ab31282
b69ab31283export const treeWithPreviews = atom(get => {
b69ab31284 const dag = get(dagWithPreviews);
b69ab31285 const commits = [...dag.values()];
b69ab31286 const trees = getCommitTree(commits);
b69ab31287
b69ab31288 let headCommit = get(latestHeadCommit);
b69ab31289 // The headCommit might be changed by dag previews. Double check.
b69ab31290 if (headCommit && !dag.get(headCommit.hash)?.isDot) {
b69ab31291 headCommit = dag.resolve('.');
b69ab31292 }
b69ab31293 // Open-code latestCommitTreeMap to pick up tree changes done by `dag`.
b69ab31294 const treeMap = new Map<Hash, CommitTreeWithPreviews>();
b69ab31295 for (const tree of walkTreePostorder(trees)) {
b69ab31296 treeMap.set(tree.info.hash, tree);
b69ab31297 }
b69ab31298
b69ab31299 return {trees, treeMap, headCommit};
b69ab31300});
b69ab31301
b69ab31302/** Yield operations that might need optimistic state. */
b69ab31303function* optimisticOperations(props: {
b69ab31304 history: OperationInfo[];
b69ab31305 queued: Operation[];
b69ab31306 currentOperation?: OperationInfo;
b69ab31307}): Generator<Operation> {
b69ab31308 const {history, queued, currentOperation} = props;
b69ab31309
b69ab31310 // previous commands
b69ab31311 for (const op of history) {
b69ab31312 if (op != null && !op.hasCompletedOptimisticState) {
b69ab31313 yield op.operation;
b69ab31314 }
b69ab31315 }
b69ab31316
b69ab31317 // currently running/last command
b69ab31318 if (
b69ab31319 currentOperation != null &&
b69ab31320 !currentOperation.hasCompletedOptimisticState &&
b69ab31321 // don't show optimistic state if we hit an error
b69ab31322 (currentOperation.exitCode == null || currentOperation.exitCode === 0)
b69ab31323 ) {
b69ab31324 yield currentOperation.operation;
b69ab31325 }
b69ab31326
b69ab31327 // queued commands
b69ab31328 for (const op of queued) {
b69ab31329 if (op != null) {
b69ab31330 yield op;
b69ab31331 }
b69ab31332 }
b69ab31333}
b69ab31334
b69ab31335/**
b69ab31336 * Mark operations as completed when their optimistic applier is no longer needed.
b69ab31337 * Similarly marks uncommitted changes optimistic state resolved.
b69ab31338 * n.b. this must be a useEffect since React doesn't like setCurrentOperation getting called during render
b69ab31339 * when ongoingOperation is used elsewhere in the tree
b69ab31340 */
b69ab31341export function useMarkOperationsCompleted(): void {
b69ab31342 const fetchedCommits = useAtomValue(latestCommitsData);
b69ab31343 const commits = useAtomValue(latestCommits);
b69ab31344 const uncommittedChanges = useAtomValue(latestUncommittedChangesData);
b69ab31345 const conflicts = useAtomValue(mergeConflicts);
b69ab31346 const successorMap = useAtomValue(latestSuccessorsMapAtom);
b69ab31347
b69ab31348 const [list, setOperationList] = useAtom(operationList);
b69ab31349
b69ab31350 // Mark operations as completed when their optimistic applier is no longer needed
b69ab31351 // n.b. this must be a useEffect since React doesn't like setCurrentOperation getting called during render
b69ab31352 // when ongoingOperation is used elsewhere in the tree
b69ab31353 useEffect(() => {
b69ab31354 const toMarkResolved: Array<ReturnType<typeof shouldMarkOptimisticChangesResolved>> = [];
b69ab31355 const uncommittedContext = {
b69ab31356 uncommittedChanges: uncommittedChanges.files ?? [],
b69ab31357 };
b69ab31358 const mergeConflictsContext = {
b69ab31359 conflicts,
b69ab31360 };
b69ab31361 const currentOperation = list.currentOperation;
b69ab31362
b69ab31363 for (const operation of [...list.operationHistory, currentOperation]) {
b69ab31364 if (operation) {
b69ab31365 toMarkResolved.push(
b69ab31366 shouldMarkOptimisticChangesResolved(operation, uncommittedContext, mergeConflictsContext),
b69ab31367 );
b69ab31368 }
b69ab31369 }
b69ab31370 if (toMarkResolved.some(notEmpty)) {
b69ab31371 const operationHistory = [...list.operationHistory];
b69ab31372 const currentOperation =
b69ab31373 list.currentOperation == null ? undefined : {...list.currentOperation};
b69ab31374 for (let i = 0; i < toMarkResolved.length - 1; i++) {
b69ab31375 if (toMarkResolved[i]?.commits) {
b69ab31376 operationHistory[i] = {
b69ab31377 ...operationHistory[i],
b69ab31378 hasCompletedOptimisticState: true,
b69ab31379 };
b69ab31380 }
b69ab31381 if (toMarkResolved[i]?.files) {
b69ab31382 operationHistory[i] = {
b69ab31383 ...operationHistory[i],
b69ab31384 hasCompletedUncommittedChangesOptimisticState: true,
b69ab31385 };
b69ab31386 }
b69ab31387 if (toMarkResolved[i]?.conflicts) {
b69ab31388 operationHistory[i] = {
b69ab31389 ...operationHistory[i],
b69ab31390 hasCompletedMergeConflictsOptimisticState: true,
b69ab31391 };
b69ab31392 }
b69ab31393 }
b69ab31394 const markCurrentOpResolved = toMarkResolved[toMarkResolved.length - 1];
b69ab31395 if (markCurrentOpResolved && currentOperation != null) {
b69ab31396 if (markCurrentOpResolved.commits) {
b69ab31397 currentOperation.hasCompletedOptimisticState = true;
b69ab31398 }
b69ab31399 if (markCurrentOpResolved.files) {
b69ab31400 currentOperation.hasCompletedUncommittedChangesOptimisticState = true;
b69ab31401 }
b69ab31402 if (markCurrentOpResolved.conflicts) {
b69ab31403 currentOperation.hasCompletedMergeConflictsOptimisticState = true;
b69ab31404 }
b69ab31405 }
b69ab31406 setOperationList({operationHistory, currentOperation});
b69ab31407 }
b69ab31408
b69ab31409 function shouldMarkOptimisticChangesResolved(
b69ab31410 operation: OperationInfo,
b69ab31411 uncommittedChangesContext: UncommittedChangesPreviewContext,
b69ab31412 mergeConflictsContext: MergeConflictsPreviewContext,
b69ab31413 ): {commits: boolean; files: boolean; conflicts: boolean} | undefined {
b69ab31414 let files = false;
b69ab31415 let commits = false;
b69ab31416 let conflicts = false;
b69ab31417
b69ab31418 if (operation != null && !operation.hasCompletedUncommittedChangesOptimisticState) {
b69ab31419 if (operation.operation.makeOptimisticUncommittedChangesApplier != null) {
b69ab31420 const optimisticApplier =
b69ab31421 operation.operation.makeOptimisticUncommittedChangesApplier(uncommittedChangesContext);
b69ab31422 if (operation.exitCode != null) {
b69ab31423 if (optimisticApplier == null || operation.exitCode !== 0) {
b69ab31424 files = true;
b69ab31425 } else if (
b69ab31426 uncommittedChanges.fetchStartTimestamp > nullthrows(operation.endTime).valueOf()
b69ab31427 ) {
b69ab31428 getTracker()?.track('OptimisticFilesStateForceResolved', {extras: {}});
b69ab31429 files = true;
b69ab31430 }
b69ab31431 }
b69ab31432 } else if (operation.exitCode != null) {
b69ab31433 files = true;
b69ab31434 }
b69ab31435 }
b69ab31436
b69ab31437 if (operation != null && !operation.hasCompletedMergeConflictsOptimisticState) {
b69ab31438 if (operation.operation.makeOptimisticMergeConflictsApplier != null) {
b69ab31439 const optimisticApplier =
b69ab31440 operation.operation.makeOptimisticMergeConflictsApplier(mergeConflictsContext);
b69ab31441 if (operation.exitCode != null) {
b69ab31442 if (optimisticApplier == null || operation.exitCode !== 0) {
b69ab31443 conflicts = true;
b69ab31444 } else if (
b69ab31445 (mergeConflictsContext.conflicts?.fetchStartTimestamp ?? 0) >
b69ab31446 nullthrows(operation.endTime).valueOf()
b69ab31447 ) {
b69ab31448 getTracker()?.track('OptimisticConflictsStateForceResolved', {
b69ab31449 extras: {operation: getOpName(operation.operation)},
b69ab31450 });
b69ab31451 conflicts = true;
b69ab31452 }
b69ab31453 }
b69ab31454 } else if (operation.exitCode != null) {
b69ab31455 conflicts = true;
b69ab31456 }
b69ab31457 }
b69ab31458
b69ab31459 if (operation != null && !operation.hasCompletedOptimisticState) {
b69ab31460 const endTime = operation.endTime?.valueOf();
b69ab31461 if (endTime && fetchedCommits.fetchStartTimestamp >= endTime) {
b69ab31462 commits = true;
b69ab31463 }
b69ab31464 }
b69ab31465
b69ab31466 if (commits || files || conflicts) {
b69ab31467 return {commits, files, conflicts};
b69ab31468 }
b69ab31469 return undefined;
b69ab31470 }
b69ab31471 }, [
b69ab31472 list,
b69ab31473 setOperationList,
b69ab31474 commits,
b69ab31475 uncommittedChanges,
b69ab31476 conflicts,
b69ab31477 fetchedCommits,
b69ab31478 successorMap,
b69ab31479 ]);
b69ab31480}
b69ab31481
b69ab31482export type UncommittedChangesPreviewContext = {
b69ab31483 uncommittedChanges: UncommittedChanges;
b69ab31484};
b69ab31485
b69ab31486export type MergeConflictsPreviewContext = {
b69ab31487 conflicts: MergeConflicts | undefined;
b69ab31488};
b69ab31489
b69ab31490// eslint-disable-next-line @typescript-eslint/no-explicit-any
b69ab31491type Class<T> = new (...args: any[]) => T;
b69ab31492/**
b69ab31493 * React hook which looks in operation queue and history to see if a
b69ab31494 * particular operation is running or queued to run.
b69ab31495 * ```
b69ab31496 * const isRunning = useIsOperationRunningOrQueued(PullOperation);
b69ab31497 * ```
b69ab31498 */
b69ab31499export function useIsOperationRunningOrQueued(
b69ab31500 cls: Class<Operation>,
b69ab31501): 'running' | 'queued' | undefined {
b69ab31502 const list = useAtomValue(operationList);
b69ab31503 const queued = useAtomValue(queuedOperations);
b69ab31504 if (list.currentOperation?.operation instanceof cls && list.currentOperation?.exitCode == null) {
b69ab31505 return 'running';
b69ab31506 } else if (queued.some(op => op instanceof cls)) {
b69ab31507 return 'queued';
b69ab31508 }
b69ab31509 return undefined;
b69ab31510}
b69ab31511
b69ab31512export function useMostRecentPendingOperation(): Operation | undefined {
b69ab31513 const list = useAtomValue(operationList);
b69ab31514 const queued = useAtomValue(queuedOperations);
b69ab31515 if (queued.length > 0) {
b69ab31516 return queued.at(-1);
b69ab31517 }
b69ab31518 if (list.currentOperation?.exitCode == null) {
b69ab31519 return list.currentOperation?.operation;
b69ab31520 }
b69ab31521 return undefined;
b69ab31522}