addons/isl/src/CommitTreeList.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 {RenderGlyphResult} from './RenderDag';
b69ab319import type {DagCommitInfo} from './dag/dag';
b69ab3110import type {ExtendedGraphRow} from './dag/render';
b69ab3111import type {Hash} from './types';
b69ab3112
b69ab3113import {Button} from 'isl-components/Button';
b69ab3114import {ErrorNotice} from 'isl-components/ErrorNotice';
b69ab3115import {ErrorShortMessages} from 'isl-server/src/constants';
b69ab3116import {atom, useAtomValue} from 'jotai';
b69ab3117import {Commit, InlineProgressSpan} from './Commit';
b69ab3118import {Center, LargeSpinner} from './ComponentUtils';
b69ab3119import {FetchingAdditionalCommitsRow} from './FetchAdditionalCommitsButton';
b69ab3120import {isHighlightedCommit} from './HighlightedCommits';
b69ab3121import {RegularGlyph, RenderDag, YouAreHereGlyph} from './RenderDag';
b69ab3122import {StackActions} from './StackActions';
b69ab3123import {YOU_ARE_HERE_VIRTUAL_COMMIT} from './dag/virtualCommit';
b69ab3124import {T, t} from './i18n';
b69ab3125import {atomFamilyWeak, localStorageBackedAtom} from './jotaiUtils';
b69ab3126import {CreateEmptyInitialCommitOperation} from './operations/CreateEmptyInitialCommitOperation';
b69ab3127import {inlineProgressByHash, useRunOperation} from './operationsState';
b69ab3128import {dagWithPreviews, treeWithPreviews, useMarkOperationsCompleted} from './previews';
b69ab3129import {hideIrrelevantCwdStacks, isIrrelevantToCwd, repoRelativeCwd} from './repositoryData';
b69ab3130import {isNarrowCommitTree} from './responsive';
b69ab3131import {
b69ab3132 selectedCommits,
b69ab3133 useArrowKeysToChangeSelection,
b69ab3134 useBackspaceToHideSelected,
b69ab3135 useCommitCallbacks,
b69ab3136 useShortcutToRebaseSelected,
b69ab3137} from './selection';
b69ab3138import {commitFetchError, latestUncommittedChangesData} from './serverAPIState';
b69ab3139import {MaybeEditStackModal} from './stackEdit/ui/EditStackModal';
b69ab3140
b69ab3141import './CommitTreeList.css';
b69ab3142
b69ab3143type DagCommitListProps = {
b69ab3144 isNarrow: boolean;
b69ab3145};
b69ab3146
b69ab3147const dagWithYouAreHere = atom(get => {
b69ab3148 let dag = get(dagWithPreviews);
b69ab3149 // Insert a virtual "You are here" as a child of ".".
b69ab3150 const dot = dag.resolve('.');
b69ab3151 if (dot != null) {
b69ab3152 dag = dag.add([YOU_ARE_HERE_VIRTUAL_COMMIT.set('parents', [dot.hash])]);
b69ab3153 }
b69ab3154 return dag;
b69ab3155});
b69ab3156
b69ab3157export const condenseObsoleteStacks = localStorageBackedAtom<boolean | null>(
b69ab3158 'isl.condense-obsolete-stacks',
b69ab3159 true,
b69ab3160);
b69ab3161
b69ab3162const renderSubsetUnionSelection = atom(get => {
b69ab3163 const dag = get(dagWithYouAreHere);
b69ab3164 const condense = get(condenseObsoleteStacks);
b69ab3165 let subset = dag.subsetForRendering(undefined, /* condenseObsoleteStacks */ condense !== false);
b69ab3166 // If selectedCommits includes commits unknown to dag (ex. in tests), ignore them to avoid errors.
b69ab3167 const selection = dag.present(get(selectedCommits));
b69ab3168
b69ab3169 const hideIrrelevant = get(hideIrrelevantCwdStacks);
b69ab3170 if (hideIrrelevant) {
b69ab3171 const cwd = get(repoRelativeCwd);
b69ab3172 subset = dag.filter(commit => commit.isDot || !isIrrelevantToCwd(commit, cwd), subset);
b69ab3173 }
b69ab3174
b69ab3175 return subset.union(selection);
b69ab3176});
b69ab3177
b69ab3178function DagCommitList(props: DagCommitListProps) {
b69ab3179 const {isNarrow} = props;
b69ab3180
b69ab3181 const dag = useAtomValue(dagWithYouAreHere);
b69ab3182 const subset = useAtomValue(renderSubsetUnionSelection);
b69ab3183
b69ab3184 return (
b69ab3185 <RenderDag
b69ab3186 dag={dag}
b69ab3187 subset={subset}
b69ab3188 className={'commit-tree-root ' + (isNarrow ? ' commit-tree-narrow' : '')}
b69ab3189 data-testid="commit-tree-root"
b69ab3190 renderCommit={renderCommit}
b69ab3191 renderCommitExtras={renderCommitExtras}
b69ab3192 renderGlyph={renderGlyph}
b69ab3193 useExtraCommitRowProps={useExtraCommitRowProps}
b69ab3194 />
b69ab3195 );
b69ab3196}
b69ab3197
b69ab3198function renderCommit(info: DagCommitInfo) {
b69ab3199 return <DagCommitBody info={info} />;
b69ab31100}
b69ab31101
b69ab31102function renderCommitExtras(info: DagCommitInfo, row: ExtendedGraphRow) {
b69ab31103 if (row.termLine != null && (info.parents.length > 0 || (info.ancestors?.size ?? 0) > 0)) {
b69ab31104 // Root (no parents) in the displayed DAG, but not root in the full DAG.
b69ab31105 return <MaybeFetchingAdditionalCommitsRow hash={info.hash} />;
b69ab31106 } else if (info.phase === 'draft') {
b69ab31107 // Draft but parents are not drafts. Likely a stack root. Show stack buttons.
b69ab31108 return <MaybeStackActions hash={info.hash} />;
b69ab31109 }
b69ab31110 return null;
b69ab31111}
b69ab31112
b69ab31113function renderGlyph(info: DagCommitInfo): RenderGlyphResult {
b69ab31114 if (info.isYouAreHere) {
b69ab31115 return ['replace-tile', <YouAreHereGlyphWithProgress info={info} />];
b69ab31116 } else {
b69ab31117 return ['inside-tile', <HighlightedGlyph info={info} />];
b69ab31118 }
b69ab31119}
b69ab31120
b69ab31121function useExtraCommitRowProps(info: DagCommitInfo): React.HTMLAttributes<HTMLDivElement> | void {
b69ab31122 const {isSelected, onClickToSelect, onDoubleClickToShowDrawer} = useCommitCallbacks(info);
b69ab31123
b69ab31124 return {
b69ab31125 onClick: onClickToSelect,
b69ab31126 onDoubleClick: onDoubleClickToShowDrawer,
b69ab31127 className: isSelected ? 'commit-row-selected' : '',
b69ab31128 };
b69ab31129}
b69ab31130
b69ab31131function YouAreHereGlyphWithProgress({info}: {info: DagCommitInfo}) {
b69ab31132 const inlineProgress = useAtomValue(inlineProgressByHash(info.hash));
b69ab31133 return (
b69ab31134 <YouAreHereGlyph info={info}>
b69ab31135 {inlineProgress && <InlineProgressSpan message={inlineProgress} />}
b69ab31136 </YouAreHereGlyph>
b69ab31137 );
b69ab31138}
b69ab31139
b69ab31140const dagHasChildren = atomFamilyWeak((key: string) => {
b69ab31141 return atom(get => {
b69ab31142 const dag = get(dagWithPreviews);
b69ab31143 return dag.children(key).size > 0;
b69ab31144 });
b69ab31145});
b69ab31146
b69ab31147function DagCommitBody({info}: {info: DagCommitInfo}) {
b69ab31148 const hasChildren = useAtomValue(dagHasChildren(info.hash));
b69ab31149 return (
b69ab31150 <Commit
b69ab31151 commit={info}
b69ab31152 key={info.hash}
b69ab31153 previewType={info.previewType}
b69ab31154 hasChildren={hasChildren}
b69ab31155 />
b69ab31156 );
b69ab31157}
b69ab31158
b69ab31159const dagHasParents = atomFamilyWeak((key: string) => {
b69ab31160 return atom(get => {
b69ab31161 const dag = get(dagWithPreviews);
b69ab31162 return dag.parents(key).size > 0;
b69ab31163 });
b69ab31164});
b69ab31165
b69ab31166const dagIsDraftStackRoot = atomFamilyWeak((key: string) => {
b69ab31167 return atom(get => {
b69ab31168 const dag = get(dagWithPreviews);
b69ab31169 return dag.draft(dag.parents(key)).size === 0;
b69ab31170 });
b69ab31171});
b69ab31172
b69ab31173function MaybeFetchingAdditionalCommitsRow({hash}: {hash: Hash}) {
b69ab31174 const hasParents = useAtomValue(dagHasParents(hash));
b69ab31175 return hasParents ? null : <FetchingAdditionalCommitsRow />;
b69ab31176}
b69ab31177
b69ab31178function MaybeStackActions({hash}: {hash: Hash}) {
b69ab31179 const isDraftStackRoot = useAtomValue(dagIsDraftStackRoot(hash));
b69ab31180 return isDraftStackRoot ? <StackActions hash={hash} /> : null;
b69ab31181}
b69ab31182
b69ab31183function HighlightedGlyph({info}: {info: DagCommitInfo}) {
b69ab31184 const highlighted = useAtomValue(isHighlightedCommit(info.hash));
b69ab31185
b69ab31186 const highlightCircle = highlighted ? (
b69ab31187 <circle cx={0} cy={0} r={8} fill="transparent" stroke="var(--focus-border)" strokeWidth={4} />
b69ab31188 ) : null;
b69ab31189
b69ab31190 return (
b69ab31191 <>
b69ab31192 {highlightCircle}
b69ab31193 <RegularGlyph info={info} />
b69ab31194 </>
b69ab31195 );
b69ab31196}
b69ab31197
b69ab31198export function CommitTreeList() {
b69ab31199 // Make sure we trigger subscription to changes to uncommitted changes *before* we have a tree to render,
b69ab31200 // so we don't miss the first returned uncommitted changes message.
b69ab31201 // TODO: This is a little ugly, is there a better way to tell recoil to start the subscription immediately?
b69ab31202 // Or should we queue/cache messages?
b69ab31203 useAtomValue(latestUncommittedChangesData);
b69ab31204 useMarkOperationsCompleted();
b69ab31205
b69ab31206 useArrowKeysToChangeSelection();
b69ab31207 useBackspaceToHideSelected();
b69ab31208 useShortcutToRebaseSelected();
b69ab31209
b69ab31210 const isNarrow = useAtomValue(isNarrowCommitTree);
b69ab31211
b69ab31212 const {trees} = useAtomValue(treeWithPreviews);
b69ab31213 const fetchError = useAtomValue(commitFetchError);
b69ab31214 return fetchError == null && trees.length === 0 ? (
b69ab31215 <Center>
b69ab31216 <LargeSpinner />
b69ab31217 </Center>
b69ab31218 ) : (
b69ab31219 <>
b69ab31220 {fetchError ? <CommitFetchError error={fetchError} /> : null}
b69ab31221 <DagCommitList isNarrow={isNarrow} />
b69ab31222 <MaybeEditStackModal />
b69ab31223 </>
b69ab31224 );
b69ab31225}
b69ab31226
b69ab31227function CommitFetchError({error}: {error: Error}) {
b69ab31228 const runOperation = useRunOperation();
b69ab31229 if (error.message === ErrorShortMessages.NoCommitsFetched) {
b69ab31230 return (
b69ab31231 <ErrorNotice
b69ab31232 title={t('No commits found')}
b69ab31233 description={t('If this is a new repository, try adding an initial commit first.')}
b69ab31234 error={error}
b69ab31235 buttons={[
b69ab31236 <Button
b69ab31237 onClick={() => {
b69ab31238 runOperation(new CreateEmptyInitialCommitOperation());
b69ab31239 }}>
b69ab31240 <T>Create empty initial commit</T>
b69ab31241 </Button>,
b69ab31242 ]}
b69ab31243 />
b69ab31244 );
b69ab31245 }
b69ab31246 return <ErrorNotice title={t('Failed to fetch commits')} error={error} />;
b69ab31247}