8.0 KB248 lines
Blame
1/**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8import type {RenderGlyphResult} from './RenderDag';
9import type {DagCommitInfo} from './dag/dag';
10import type {ExtendedGraphRow} from './dag/render';
11import type {Hash} from './types';
12
13import {Button} from 'isl-components/Button';
14import {ErrorNotice} from 'isl-components/ErrorNotice';
15import {ErrorShortMessages} from 'isl-server/src/constants';
16import {atom, useAtomValue} from 'jotai';
17import {Commit, InlineProgressSpan} from './Commit';
18import {Center, LargeSpinner} from './ComponentUtils';
19import {FetchingAdditionalCommitsRow} from './FetchAdditionalCommitsButton';
20import {isHighlightedCommit} from './HighlightedCommits';
21import {RegularGlyph, RenderDag, YouAreHereGlyph} from './RenderDag';
22import {StackActions} from './StackActions';
23import {YOU_ARE_HERE_VIRTUAL_COMMIT} from './dag/virtualCommit';
24import {T, t} from './i18n';
25import {atomFamilyWeak, localStorageBackedAtom} from './jotaiUtils';
26import {CreateEmptyInitialCommitOperation} from './operations/CreateEmptyInitialCommitOperation';
27import {inlineProgressByHash, useRunOperation} from './operationsState';
28import {dagWithPreviews, treeWithPreviews, useMarkOperationsCompleted} from './previews';
29import {hideIrrelevantCwdStacks, isIrrelevantToCwd, repoRelativeCwd} from './repositoryData';
30import {isNarrowCommitTree} from './responsive';
31import {
32 selectedCommits,
33 useArrowKeysToChangeSelection,
34 useBackspaceToHideSelected,
35 useCommitCallbacks,
36 useShortcutToRebaseSelected,
37} from './selection';
38import {commitFetchError, latestUncommittedChangesData} from './serverAPIState';
39import {MaybeEditStackModal} from './stackEdit/ui/EditStackModal';
40
41import './CommitTreeList.css';
42
43type DagCommitListProps = {
44 isNarrow: boolean;
45};
46
47const dagWithYouAreHere = atom(get => {
48 let dag = get(dagWithPreviews);
49 // Insert a virtual "You are here" as a child of ".".
50 const dot = dag.resolve('.');
51 if (dot != null) {
52 dag = dag.add([YOU_ARE_HERE_VIRTUAL_COMMIT.set('parents', [dot.hash])]);
53 }
54 return dag;
55});
56
57export const condenseObsoleteStacks = localStorageBackedAtom<boolean | null>(
58 'isl.condense-obsolete-stacks',
59 true,
60);
61
62const renderSubsetUnionSelection = atom(get => {
63 const dag = get(dagWithYouAreHere);
64 const condense = get(condenseObsoleteStacks);
65 let subset = dag.subsetForRendering(undefined, /* condenseObsoleteStacks */ condense !== false);
66 // If selectedCommits includes commits unknown to dag (ex. in tests), ignore them to avoid errors.
67 const selection = dag.present(get(selectedCommits));
68
69 const hideIrrelevant = get(hideIrrelevantCwdStacks);
70 if (hideIrrelevant) {
71 const cwd = get(repoRelativeCwd);
72 subset = dag.filter(commit => commit.isDot || !isIrrelevantToCwd(commit, cwd), subset);
73 }
74
75 return subset.union(selection);
76});
77
78function DagCommitList(props: DagCommitListProps) {
79 const {isNarrow} = props;
80
81 const dag = useAtomValue(dagWithYouAreHere);
82 const subset = useAtomValue(renderSubsetUnionSelection);
83
84 return (
85 <RenderDag
86 dag={dag}
87 subset={subset}
88 className={'commit-tree-root ' + (isNarrow ? ' commit-tree-narrow' : '')}
89 data-testid="commit-tree-root"
90 renderCommit={renderCommit}
91 renderCommitExtras={renderCommitExtras}
92 renderGlyph={renderGlyph}
93 useExtraCommitRowProps={useExtraCommitRowProps}
94 />
95 );
96}
97
98function renderCommit(info: DagCommitInfo) {
99 return <DagCommitBody info={info} />;
100}
101
102function renderCommitExtras(info: DagCommitInfo, row: ExtendedGraphRow) {
103 if (row.termLine != null && (info.parents.length > 0 || (info.ancestors?.size ?? 0) > 0)) {
104 // Root (no parents) in the displayed DAG, but not root in the full DAG.
105 return <MaybeFetchingAdditionalCommitsRow hash={info.hash} />;
106 } else if (info.phase === 'draft') {
107 // Draft but parents are not drafts. Likely a stack root. Show stack buttons.
108 return <MaybeStackActions hash={info.hash} />;
109 }
110 return null;
111}
112
113function renderGlyph(info: DagCommitInfo): RenderGlyphResult {
114 if (info.isYouAreHere) {
115 return ['replace-tile', <YouAreHereGlyphWithProgress info={info} />];
116 } else {
117 return ['inside-tile', <HighlightedGlyph info={info} />];
118 }
119}
120
121function useExtraCommitRowProps(info: DagCommitInfo): React.HTMLAttributes<HTMLDivElement> | void {
122 const {isSelected, onClickToSelect, onDoubleClickToShowDrawer} = useCommitCallbacks(info);
123
124 return {
125 onClick: onClickToSelect,
126 onDoubleClick: onDoubleClickToShowDrawer,
127 className: isSelected ? 'commit-row-selected' : '',
128 };
129}
130
131function YouAreHereGlyphWithProgress({info}: {info: DagCommitInfo}) {
132 const inlineProgress = useAtomValue(inlineProgressByHash(info.hash));
133 return (
134 <YouAreHereGlyph info={info}>
135 {inlineProgress && <InlineProgressSpan message={inlineProgress} />}
136 </YouAreHereGlyph>
137 );
138}
139
140const dagHasChildren = atomFamilyWeak((key: string) => {
141 return atom(get => {
142 const dag = get(dagWithPreviews);
143 return dag.children(key).size > 0;
144 });
145});
146
147function DagCommitBody({info}: {info: DagCommitInfo}) {
148 const hasChildren = useAtomValue(dagHasChildren(info.hash));
149 return (
150 <Commit
151 commit={info}
152 key={info.hash}
153 previewType={info.previewType}
154 hasChildren={hasChildren}
155 />
156 );
157}
158
159const dagHasParents = atomFamilyWeak((key: string) => {
160 return atom(get => {
161 const dag = get(dagWithPreviews);
162 return dag.parents(key).size > 0;
163 });
164});
165
166const dagIsDraftStackRoot = atomFamilyWeak((key: string) => {
167 return atom(get => {
168 const dag = get(dagWithPreviews);
169 return dag.draft(dag.parents(key)).size === 0;
170 });
171});
172
173function MaybeFetchingAdditionalCommitsRow({hash}: {hash: Hash}) {
174 const hasParents = useAtomValue(dagHasParents(hash));
175 return hasParents ? null : <FetchingAdditionalCommitsRow />;
176}
177
178function MaybeStackActions({hash}: {hash: Hash}) {
179 const isDraftStackRoot = useAtomValue(dagIsDraftStackRoot(hash));
180 return isDraftStackRoot ? <StackActions hash={hash} /> : null;
181}
182
183function HighlightedGlyph({info}: {info: DagCommitInfo}) {
184 const highlighted = useAtomValue(isHighlightedCommit(info.hash));
185
186 const highlightCircle = highlighted ? (
187 <circle cx={0} cy={0} r={8} fill="transparent" stroke="var(--focus-border)" strokeWidth={4} />
188 ) : null;
189
190 return (
191 <>
192 {highlightCircle}
193 <RegularGlyph info={info} />
194 </>
195 );
196}
197
198export function CommitTreeList() {
199 // Make sure we trigger subscription to changes to uncommitted changes *before* we have a tree to render,
200 // so we don't miss the first returned uncommitted changes message.
201 // TODO: This is a little ugly, is there a better way to tell recoil to start the subscription immediately?
202 // Or should we queue/cache messages?
203 useAtomValue(latestUncommittedChangesData);
204 useMarkOperationsCompleted();
205
206 useArrowKeysToChangeSelection();
207 useBackspaceToHideSelected();
208 useShortcutToRebaseSelected();
209
210 const isNarrow = useAtomValue(isNarrowCommitTree);
211
212 const {trees} = useAtomValue(treeWithPreviews);
213 const fetchError = useAtomValue(commitFetchError);
214 return fetchError == null && trees.length === 0 ? (
215 <Center>
216 <LargeSpinner />
217 </Center>
218 ) : (
219 <>
220 {fetchError ? <CommitFetchError error={fetchError} /> : null}
221 <DagCommitList isNarrow={isNarrow} />
222 <MaybeEditStackModal />
223 </>
224 );
225}
226
227function CommitFetchError({error}: {error: Error}) {
228 const runOperation = useRunOperation();
229 if (error.message === ErrorShortMessages.NoCommitsFetched) {
230 return (
231 <ErrorNotice
232 title={t('No commits found')}
233 description={t('If this is a new repository, try adding an initial commit first.')}
234 error={error}
235 buttons={[
236 <Button
237 onClick={() => {
238 runOperation(new CreateEmptyInitialCommitOperation());
239 }}>
240 <T>Create empty initial commit</T>
241 </Button>,
242 ]}
243 />
244 );
245 }
246 return <ErrorNotice title={t('Failed to fetch commits')} error={error} />;
247}
248