addons/isl/src/operations/CommitOperation.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 {ImportStack} from 'shared/types/stack';
b69ab319import type {PartialSelection} from '../partialSelection';
b69ab3110import type {
b69ab3111 ApplyUncommittedChangesPreviewsFuncType,
b69ab3112 Dag,
b69ab3113 UncommittedChangesPreviewContext,
b69ab3114} from '../previews';
b69ab3115import type {
b69ab3116 ChangedFile,
b69ab3117 CommandArg,
b69ab3118 CommitInfo,
b69ab3119 Hash,
b69ab3120 RepoRelativePath,
b69ab3121 UncommittedChanges,
b69ab3122} from '../types';
b69ab3123
b69ab3124import {DagCommitInfo} from '../dag/dagCommitInfo';
b69ab3125import {t} from '../i18n';
b69ab3126import {readAtom} from '../jotaiUtils';
b69ab3127import {uncommittedChangesWithPreviews} from '../previews';
b69ab3128import {authorString} from '../serverAPIState';
b69ab3129import {CommitBaseOperation} from './CommitBaseOperation';
b69ab3130import {Operation} from './Operation';
b69ab3131
b69ab3132export class CommitOperation extends CommitBaseOperation {
b69ab3133 private beforeCommitDate: Date;
b69ab3134
b69ab3135 /**
b69ab3136 * @param message the commit message. The first line is used as the title.
b69ab3137 * @param originalHeadHash the hash of the current head commit, needed to track when optimistic state is resolved.
b69ab3138 * @param filesPathsToCommit if provided, only these file paths will be included in the commit operation. If undefined, ALL uncommitted changes are included. Paths should be relative to repo root.
b69ab3139 */
b69ab3140 constructor(
b69ab3141 public message: string,
b69ab3142 private originalHeadHash: Hash,
b69ab3143 protected filesPathsToCommit?: Array<RepoRelativePath>,
b69ab3144 ) {
b69ab3145 super(message, filesPathsToCommit);
b69ab3146
b69ab3147 // New commit should have a greater date.
b69ab3148 this.beforeCommitDate = new Date();
b69ab3149
b69ab3150 // When rendering optimistic state, we need to know the set of files that will be part of this commit.
b69ab3151 // This is not necessarily the same as filePathsToCommit, since it may be undefined to represent "all files".
b69ab3152 // This is done once at Operation creation time, not on each call to optimisticDag, since we
b69ab3153 // only care about the list of changed files when the CommitOperation was enqueued.
b69ab3154 this.optimisticChangedFiles = readAtom(uncommittedChangesWithPreviews).filter(changedFile => {
b69ab3155 return filesPathsToCommit == null
b69ab3156 ? true
b69ab3157 : filesPathsToCommit.some(f => f === changedFile.path);
b69ab3158 });
b69ab3159 }
b69ab3160
b69ab3161 private optimisticChangedFiles: Array<ChangedFile>;
b69ab3162
b69ab3163 makeOptimisticUncommittedChangesApplier?(
b69ab3164 context: UncommittedChangesPreviewContext,
b69ab3165 ): ApplyUncommittedChangesPreviewsFuncType | undefined {
b69ab3166 const filesToCommit = new Set(this.filesPathsToCommit);
b69ab3167 // optimistic state is over when there's no uncommitted changes that we wanted to commit left
b69ab3168 if (
b69ab3169 context.uncommittedChanges.length === 0 ||
b69ab3170 (filesToCommit.size > 0 &&
b69ab3171 context.uncommittedChanges.every(change => !filesToCommit.has(change.path)))
b69ab3172 ) {
b69ab3173 return undefined;
b69ab3174 }
b69ab3175
b69ab3176 const func: ApplyUncommittedChangesPreviewsFuncType = (changes: UncommittedChanges) => {
b69ab3177 if (this.filesPathsToCommit != null) {
b69ab3178 return changes.filter(change => !filesToCommit.has(change.path));
b69ab3179 } else {
b69ab3180 return [];
b69ab3181 }
b69ab3182 };
b69ab3183 return func;
b69ab3184 }
b69ab3185
b69ab3186 optimisticDag(dag: Dag): Dag {
b69ab3187 const base = this.originalHeadHash;
b69ab3188 const baseInfo = dag.get(base);
b69ab3189 if (!baseInfo) {
b69ab3190 return dag;
b69ab3191 }
b69ab3192
b69ab3193 const [title] = this.message.split(/\n+/, 1);
b69ab3194 const children = dag.children(base);
b69ab3195 const hasWantedChild = children.toHashes().some(h => {
b69ab3196 const info = dag.get(h);
b69ab3197 return info?.title === title && info?.date > this.beforeCommitDate;
b69ab3198 });
b69ab3199 if (hasWantedChild) {
b69ab31100 // A new commit was made on `base` with the the expected title.
b69ab31101 // Consider the commit operation as completed.
b69ab31102 return dag;
b69ab31103 }
b69ab31104
b69ab31105 const now = new Date(Date.now());
b69ab31106
b69ab31107 // The fake optimistic commit can be resolved into a real commit by taking the
b69ab31108 // first child of the given parent that's created after the commit operation was created.
b69ab31109 const optimisticRevset = `first(sort((children(${base})-${base}) & date(">${now.toUTCString()}"),date))`;
b69ab31110
b69ab31111 // NOTE: We might want to check the "active bookmark" state
b69ab31112 // and update bookmarks accordingly.
b69ab31113 const hash = `OPTIMISTIC_COMMIT_${base}`;
b69ab31114 const description = this.message.slice(title.length);
b69ab31115 const author = readAtom(authorString);
b69ab31116 const info = DagCommitInfo.fromCommitInfo({
b69ab31117 author: author ?? baseInfo?.author ?? '',
b69ab31118 description,
b69ab31119 title,
b69ab31120 bookmarks: [],
b69ab31121 remoteBookmarks: [],
b69ab31122 isDot: true,
b69ab31123 parents: [base],
b69ab31124 hash,
b69ab31125 optimisticRevset,
b69ab31126 phase: 'draft',
b69ab31127 filePathsSample: this.optimisticChangedFiles.map(f => f.path),
b69ab31128 totalFileCount: this.optimisticChangedFiles.length,
b69ab31129 date: now,
b69ab31130 });
b69ab31131
b69ab31132 return dag.replaceWith([base, hash], (h, _c) => {
b69ab31133 if (h === base) {
b69ab31134 return baseInfo?.set('isDot', false);
b69ab31135 } else {
b69ab31136 return info;
b69ab31137 }
b69ab31138 });
b69ab31139 }
b69ab31140}
b69ab31141
b69ab31142export class PartialCommitOperation extends Operation {
b69ab31143 /**
b69ab31144 * See also `CommitOperation`. This operation takes a `PartialSelection` and
b69ab31145 * uses `debugimportstack` under the hood, to achieve `commit -i` effect.
b69ab31146 */
b69ab31147 constructor(
b69ab31148 public message: string,
b69ab31149 private originalHeadHash: Hash,
b69ab31150 private selection: PartialSelection,
b69ab31151 // We need "selected" or "all" files since `selection` only tracks deselected files.
b69ab31152 private allFiles: Array<RepoRelativePath>,
b69ab31153 ) {
b69ab31154 super('PartialCommitOperation');
b69ab31155 }
b69ab31156
b69ab31157 getArgs(): CommandArg[] {
b69ab31158 return ['debugimportstack'];
b69ab31159 }
b69ab31160
b69ab31161 getStdin(): string | undefined {
b69ab31162 const files = this.selection.calculateImportStackFiles(this.allFiles);
b69ab31163 const importStack: ImportStack = [
b69ab31164 [
b69ab31165 'commit',
b69ab31166 {
b69ab31167 mark: ':1',
b69ab31168 text: this.message,
b69ab31169 parents: [this.originalHeadHash],
b69ab31170 files,
b69ab31171 },
b69ab31172 ],
b69ab31173 ['reset', {mark: ':1'}],
b69ab31174 ];
b69ab31175 return JSON.stringify(importStack);
b69ab31176 }
b69ab31177
b69ab31178 getDescriptionForDisplay() {
b69ab31179 return {
b69ab31180 description: t('Committing selected changes'),
b69ab31181 tooltip: t(
b69ab31182 'This operation does not have a traditional command line equivalent. \n' +
b69ab31183 'You can use `commit -i` on the command line to select changes to commit.',
b69ab31184 ),
b69ab31185 };
b69ab31186 }
b69ab31187}
b69ab31188
b69ab31189/** Choose `PartialCommitOperation` or `CommitOperation` based on input. */
b69ab31190export function getCommitOperation(
b69ab31191 message: string,
b69ab31192 originalHead: CommitInfo | undefined,
b69ab31193 selection: PartialSelection,
b69ab31194 allFiles: Array<RepoRelativePath>,
b69ab31195): CommitOperation | PartialCommitOperation {
b69ab31196 const originalHeadHash = originalHead?.hash ?? '.';
b69ab31197 if (selection.hasChunkSelection()) {
b69ab31198 return new PartialCommitOperation(message, originalHeadHash, selection, allFiles);
b69ab31199 } else if (selection.isEverythingSelected(() => allFiles)) {
b69ab31200 return new CommitOperation(message, originalHeadHash);
b69ab31201 } else {
b69ab31202 const selectedFiles = allFiles.filter(path => selection.isFullyOrPartiallySelected(path));
b69ab31203 return new CommitOperation(message, originalHeadHash, selectedFiles);
b69ab31204 }
b69ab31205}