| b69ab31 | | | 1 | /** |
| b69ab31 | | | 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. |
| b69ab31 | | | 3 | * |
| b69ab31 | | | 4 | * This source code is licensed under the MIT license found in the |
| b69ab31 | | | 5 | * LICENSE file in the root directory of this source tree. |
| b69ab31 | | | 6 | */ |
| b69ab31 | | | 7 | |
| ed2694f | | | 8 | import {startTransition} from 'react'; |
| b69ab31 | | | 9 | import type {MessageBusStatus} from './MessageBus'; |
| b69ab31 | | | 10 | import type { |
| b69ab31 | | | 11 | AbsolutePath, |
| b69ab31 | | | 12 | ApplicationInfo, |
| b69ab31 | | | 13 | ChangedFile, |
| b69ab31 | | | 14 | CommitInfo, |
| b69ab31 | | | 15 | MergeConflicts, |
| b69ab31 | | | 16 | RepoInfo, |
| b69ab31 | | | 17 | SmartlogCommits, |
| b69ab31 | | | 18 | SubmodulesByRoot, |
| b69ab31 | | | 19 | SubscriptionKind, |
| b69ab31 | | | 20 | SubscriptionResultsData, |
| b69ab31 | | | 21 | UncommittedChanges, |
| b69ab31 | | | 22 | ValidatedRepoInfo, |
| b69ab31 | | | 23 | } from './types'; |
| b69ab31 | | | 24 | |
| b69ab31 | | | 25 | import {Set as ImSet} from 'immutable'; |
| b69ab31 | | | 26 | import {DEFAULT_DAYS_OF_COMMITS_TO_LOAD} from 'isl-server/src/constants'; |
| b69ab31 | | | 27 | import type {Atom} from 'jotai'; |
| b69ab31 | | | 28 | import {atom} from 'jotai'; |
| b69ab31 | | | 29 | import {reuseEqualObjects} from 'shared/deepEqualExt'; |
| b69ab31 | | | 30 | import {randomId} from 'shared/utils'; |
| b69ab31 | | | 31 | import { |
| b69ab31 | | | 32 | type BookmarksData, |
| b69ab31 | | | 33 | bookmarksDataStorage, |
| b69ab31 | | | 34 | recommendedBookmarksAtom, |
| b69ab31 | | | 35 | recommendedBookmarksAvailableAtom, |
| b69ab31 | | | 36 | REMOTE_MASTER_BOOKMARK, |
| b69ab31 | | | 37 | } from './BookmarksData'; |
| b69ab31 | | | 38 | import serverAPI from './ClientToServerAPI'; |
| b69ab31 | | | 39 | import {hiddenMasterFeatureAvailableAtom, shouldHideMasterAtom} from './HiddenMasterData'; |
| b69ab31 | | | 40 | import type {InternalTypes} from './InternalTypes'; |
| b69ab31 | | | 41 | import {latestSuccessorsMapAtom, successionTracker} from './SuccessionTracker'; |
| b69ab31 | | | 42 | import {Dag, DagCommitInfo} from './dag/dag'; |
| b69ab31 | | | 43 | import {readInterestingAtoms, serializeAtomsState} from './debug/getInterestingAtoms'; |
| b69ab31 | | | 44 | import {atomFamilyWeak, configBackedAtom, readAtom, writeAtom} from './jotaiUtils'; |
| b69ab31 | | | 45 | import platform from './platform'; |
| b69ab31 | | | 46 | import {atomResetOnCwdChange, repositoryData} from './repositoryData'; |
| b69ab31 | | | 47 | import {registerCleanup, registerDisposable} from './utils'; |
| b69ab31 | | | 48 | |
| b69ab31 | | | 49 | export {repositoryData}; |
| b69ab31 | | | 50 | |
| b69ab31 | | | 51 | registerDisposable( |
| b69ab31 | | | 52 | repositoryData, |
| b69ab31 | | | 53 | serverAPI.onMessageOfType('repoInfo', event => { |
| b69ab31 | | | 54 | writeAtom(repositoryData, {info: event.info, cwd: event.cwd}); |
| b69ab31 | | | 55 | }), |
| b69ab31 | | | 56 | import.meta.hot, |
| b69ab31 | | | 57 | ); |
| b69ab31 | | | 58 | registerCleanup( |
| b69ab31 | | | 59 | repositoryData, |
| b69ab31 | | | 60 | serverAPI.onSetup(() => |
| b69ab31 | | | 61 | serverAPI.postMessage({ |
| b69ab31 | | | 62 | type: 'requestRepoInfo', |
| b69ab31 | | | 63 | }), |
| b69ab31 | | | 64 | ), |
| b69ab31 | | | 65 | import.meta.hot, |
| b69ab31 | | | 66 | ); |
| b69ab31 | | | 67 | |
| b69ab31 | | | 68 | export const repositoryInfoOrError = atom( |
| b69ab31 | | | 69 | get => { |
| b69ab31 | | | 70 | const data = get(repositoryData); |
| b69ab31 | | | 71 | return data?.info; |
| b69ab31 | | | 72 | }, |
| b69ab31 | | | 73 | ( |
| b69ab31 | | | 74 | get, |
| b69ab31 | | | 75 | set, |
| b69ab31 | | | 76 | update: RepoInfo | undefined | ((_prev: RepoInfo | undefined) => RepoInfo | undefined), |
| b69ab31 | | | 77 | ) => { |
| b69ab31 | | | 78 | const value = typeof update === 'function' ? update(get(repositoryData)?.info) : update; |
| b69ab31 | | | 79 | set(repositoryData, last => ({ |
| b69ab31 | | | 80 | ...last, |
| b69ab31 | | | 81 | info: value, |
| b69ab31 | | | 82 | })); |
| b69ab31 | | | 83 | }, |
| b69ab31 | | | 84 | ); |
| b69ab31 | | | 85 | |
| b69ab31 | | | 86 | /** ValidatedRepoInfo, or undefined on error. */ |
| b69ab31 | | | 87 | export const repositoryInfo = atom( |
| b69ab31 | | | 88 | get => { |
| b69ab31 | | | 89 | const info = get(repositoryInfoOrError); |
| b69ab31 | | | 90 | if (info?.type === 'success') { |
| b69ab31 | | | 91 | return info; |
| b69ab31 | | | 92 | } |
| b69ab31 | | | 93 | return undefined; |
| b69ab31 | | | 94 | }, |
| b69ab31 | | | 95 | ( |
| b69ab31 | | | 96 | get, |
| b69ab31 | | | 97 | set, |
| b69ab31 | | | 98 | update: |
| b69ab31 | | | 99 | | ValidatedRepoInfo |
| b69ab31 | | | 100 | | undefined |
| b69ab31 | | | 101 | | ((_prev: ValidatedRepoInfo | undefined) => ValidatedRepoInfo | undefined), |
| b69ab31 | | | 102 | ) => { |
| b69ab31 | | | 103 | const value = typeof update === 'function' ? update(get(repositoryInfo)) : update; |
| b69ab31 | | | 104 | set(repositoryData, last => ({ |
| b69ab31 | | | 105 | ...last, |
| b69ab31 | | | 106 | info: value, |
| b69ab31 | | | 107 | })); |
| b69ab31 | | | 108 | }, |
| b69ab31 | | | 109 | ); |
| b69ab31 | | | 110 | |
| b69ab31 | | | 111 | /** Main command name, like 'sl'. */ |
| b69ab31 | | | 112 | export const mainCommandName = atom(get => { |
| b69ab31 | | | 113 | const info = get(repositoryInfo); |
| b69ab31 | | | 114 | return info?.command ?? 'sl'; |
| b69ab31 | | | 115 | }); |
| b69ab31 | | | 116 | |
| b69ab31 | | | 117 | /** List of repo roots. Useful when cwd is in nested submodules. */ |
| b69ab31 | | | 118 | export const repoRoots = atom(get => { |
| b69ab31 | | | 119 | const info = get(repositoryInfo); |
| b69ab31 | | | 120 | return info?.repoRoots; |
| b69ab31 | | | 121 | }); |
| b69ab31 | | | 122 | |
| b69ab31 | | | 123 | export const applicationinfo = atom<ApplicationInfo | undefined>(undefined); |
| b69ab31 | | | 124 | registerDisposable( |
| b69ab31 | | | 125 | applicationinfo, |
| b69ab31 | | | 126 | serverAPI.onMessageOfType('applicationInfo', event => { |
| b69ab31 | | | 127 | writeAtom(applicationinfo, event.info); |
| b69ab31 | | | 128 | }), |
| b69ab31 | | | 129 | import.meta.hot, |
| b69ab31 | | | 130 | ); |
| b69ab31 | | | 131 | registerCleanup( |
| b69ab31 | | | 132 | applicationinfo, |
| b69ab31 | | | 133 | serverAPI.onSetup(() => |
| b69ab31 | | | 134 | serverAPI.postMessage({ |
| b69ab31 | | | 135 | type: 'requestApplicationInfo', |
| b69ab31 | | | 136 | }), |
| b69ab31 | | | 137 | ), |
| b69ab31 | | | 138 | import.meta.hot, |
| b69ab31 | | | 139 | ); |
| b69ab31 | | | 140 | |
| 4fe1f34 | | | 141 | export const watchmanStatus = atom< |
| 4fe1f34 | | | 142 | 'initializing' | 'reconnecting' | 'healthy' | 'ended' | 'errored' | 'unavailable' | undefined |
| 4fe1f34 | | | 143 | >(undefined); |
| 4fe1f34 | | | 144 | registerDisposable( |
| 4fe1f34 | | | 145 | watchmanStatus, |
| 4fe1f34 | | | 146 | serverAPI.onMessageOfType('watchmanStatus', event => { |
| 4fe1f34 | | | 147 | writeAtom(watchmanStatus, event.status); |
| 4fe1f34 | | | 148 | }), |
| 4fe1f34 | | | 149 | import.meta.hot, |
| 4fe1f34 | | | 150 | ); |
| 4fe1f34 | | | 151 | |
| b69ab31 | | | 152 | export const reconnectingStatus = atom<MessageBusStatus>({type: 'initializing'}); |
| b69ab31 | | | 153 | registerDisposable( |
| b69ab31 | | | 154 | reconnectingStatus, |
| b69ab31 | | | 155 | platform.messageBus.onChangeStatus(status => { |
| b69ab31 | | | 156 | writeAtom(reconnectingStatus, status); |
| b69ab31 | | | 157 | }), |
| b69ab31 | | | 158 | import.meta.hot, |
| b69ab31 | | | 159 | ); |
| b69ab31 | | | 160 | |
| b69ab31 | | | 161 | export async function forceFetchCommit(revset: string): Promise<CommitInfo> { |
| b69ab31 | | | 162 | serverAPI.postMessage({ |
| b69ab31 | | | 163 | type: 'fetchLatestCommit', |
| b69ab31 | | | 164 | revset, |
| b69ab31 | | | 165 | }); |
| b69ab31 | | | 166 | const response = await serverAPI.nextMessageMatching( |
| b69ab31 | | | 167 | 'fetchedLatestCommit', |
| b69ab31 | | | 168 | message => message.revset === revset, |
| b69ab31 | | | 169 | ); |
| b69ab31 | | | 170 | if (response.info.error) { |
| b69ab31 | | | 171 | throw response.info.error; |
| b69ab31 | | | 172 | } |
| b69ab31 | | | 173 | return response.info.value; |
| b69ab31 | | | 174 | } |
| b69ab31 | | | 175 | |
| b69ab31 | | | 176 | export const mostRecentSubscriptionIds: Record<SubscriptionKind, string> = { |
| b69ab31 | | | 177 | smartlogCommits: '', |
| b69ab31 | | | 178 | uncommittedChanges: '', |
| b69ab31 | | | 179 | mergeConflicts: '', |
| b69ab31 | | | 180 | submodules: '', |
| b69ab31 | | | 181 | subscribedFullRepoBranches: '', |
| b69ab31 | | | 182 | }; |
| b69ab31 | | | 183 | |
| b69ab31 | | | 184 | /** |
| b69ab31 | | | 185 | * Send a subscribeFoo message to the server on initialization, |
| b69ab31 | | | 186 | * and send an unsubscribe message on dispose. |
| b69ab31 | | | 187 | * Extract subscription response messages via a unique subscriptionID per effect call. |
| b69ab31 | | | 188 | */ |
| b69ab31 | | | 189 | function subscriptionEffect<K extends SubscriptionKind>( |
| b69ab31 | | | 190 | kind: K, |
| b69ab31 | | | 191 | onData: (data: SubscriptionResultsData[K]) => unknown, |
| b69ab31 | | | 192 | ): () => void { |
| b69ab31 | | | 193 | const subscriptionID = randomId(); |
| b69ab31 | | | 194 | mostRecentSubscriptionIds[kind] = subscriptionID; |
| b69ab31 | | | 195 | const disposable = serverAPI.onMessageOfType('subscriptionResult', event => { |
| b69ab31 | | | 196 | if (event.subscriptionID !== subscriptionID || event.kind !== kind) { |
| b69ab31 | | | 197 | return; |
| b69ab31 | | | 198 | } |
| ed2694f | | | 199 | // Defer off the current task so ongoing user interactions (e.g. drag) aren't blocked. |
| ed2694f | | | 200 | // startTransition alone isn't enough since the message handler runs synchronously |
| ed2694f | | | 201 | // on the same task as the drag mousemove handler. |
| ed2694f | | | 202 | setTimeout(() => { |
| ed2694f | | | 203 | startTransition(() => { |
| ed2694f | | | 204 | onData(event.data as SubscriptionResultsData[K]); |
| ed2694f | | | 205 | }); |
| ed2694f | | | 206 | }, 0); |
| b69ab31 | | | 207 | }); |
| b69ab31 | | | 208 | |
| b69ab31 | | | 209 | const disposeSubscription = serverAPI.onSetup(() => { |
| b69ab31 | | | 210 | serverAPI.postMessage({ |
| b69ab31 | | | 211 | type: 'subscribe', |
| b69ab31 | | | 212 | kind, |
| b69ab31 | | | 213 | subscriptionID, |
| b69ab31 | | | 214 | }); |
| b69ab31 | | | 215 | |
| b69ab31 | | | 216 | return () => |
| b69ab31 | | | 217 | serverAPI.postMessage({ |
| b69ab31 | | | 218 | type: 'unsubscribe', |
| b69ab31 | | | 219 | kind, |
| b69ab31 | | | 220 | subscriptionID, |
| b69ab31 | | | 221 | }); |
| b69ab31 | | | 222 | }); |
| b69ab31 | | | 223 | |
| b69ab31 | | | 224 | return () => { |
| b69ab31 | | | 225 | disposable.dispose(); |
| b69ab31 | | | 226 | disposeSubscription(); |
| b69ab31 | | | 227 | }; |
| b69ab31 | | | 228 | } |
| b69ab31 | | | 229 | |
| b69ab31 | | | 230 | export const latestUncommittedChangesData = atom<{ |
| b69ab31 | | | 231 | fetchStartTimestamp: number; |
| b69ab31 | | | 232 | fetchCompletedTimestamp: number; |
| b69ab31 | | | 233 | files: UncommittedChanges; |
| b69ab31 | | | 234 | error?: Error; |
| b69ab31 | | | 235 | }>({fetchStartTimestamp: 0, fetchCompletedTimestamp: 0, files: []}); |
| b69ab31 | | | 236 | // This is used by a test. Tests do not go through babel to rewrite source |
| b69ab31 | | | 237 | // to insert debugLabel. |
| b69ab31 | | | 238 | latestUncommittedChangesData.debugLabel = 'latestUncommittedChangesData'; |
| b69ab31 | | | 239 | |
| b69ab31 | | | 240 | registerCleanup( |
| b69ab31 | | | 241 | latestUncommittedChangesData, |
| b69ab31 | | | 242 | subscriptionEffect('uncommittedChanges', data => { |
| b69ab31 | | | 243 | writeAtom(latestUncommittedChangesData, last => ({ |
| b69ab31 | | | 244 | ...data, |
| b69ab31 | | | 245 | files: |
| b69ab31 | | | 246 | data.files.value ?? |
| b69ab31 | | | 247 | // leave existing files in place if there was no error |
| b69ab31 | | | 248 | (last.error == null ? [] : last.files) ?? |
| b69ab31 | | | 249 | [], |
| b69ab31 | | | 250 | error: data.files.error, |
| b69ab31 | | | 251 | })); |
| b69ab31 | | | 252 | }), |
| b69ab31 | | | 253 | import.meta.hot, |
| b69ab31 | | | 254 | ); |
| b69ab31 | | | 255 | |
| b69ab31 | | | 256 | /** |
| b69ab31 | | | 257 | * Latest fetched uncommitted file changes from the server, without any previews. |
| b69ab31 | | | 258 | * Prefer using `uncommittedChangesWithPreviews`, since it includes optimistic state |
| b69ab31 | | | 259 | * and previews. |
| b69ab31 | | | 260 | */ |
| b69ab31 | | | 261 | export const latestUncommittedChanges = atom<Array<ChangedFile>>( |
| b69ab31 | | | 262 | get => get(latestUncommittedChangesData).files, |
| b69ab31 | | | 263 | ); |
| b69ab31 | | | 264 | |
| b69ab31 | | | 265 | export const uncommittedChangesFetchError = atom(get => { |
| b69ab31 | | | 266 | return get(latestUncommittedChangesData).error; |
| b69ab31 | | | 267 | }); |
| b69ab31 | | | 268 | |
| b69ab31 | | | 269 | export const mergeConflicts = atom<MergeConflicts | undefined>(undefined); |
| b69ab31 | | | 270 | registerCleanup( |
| b69ab31 | | | 271 | mergeConflicts, |
| b69ab31 | | | 272 | subscriptionEffect('mergeConflicts', data => { |
| b69ab31 | | | 273 | writeAtom(mergeConflicts, data); |
| b69ab31 | | | 274 | }), |
| b69ab31 | | | 275 | ); |
| b69ab31 | | | 276 | |
| b69ab31 | | | 277 | export const inMergeConflicts = atom(get => get(mergeConflicts) != undefined); |
| b69ab31 | | | 278 | |
| b69ab31 | | | 279 | export const latestCommitsData = atom<{ |
| b69ab31 | | | 280 | fetchStartTimestamp: number; |
| b69ab31 | | | 281 | fetchCompletedTimestamp: number; |
| b69ab31 | | | 282 | commits: SmartlogCommits; |
| b69ab31 | | | 283 | error?: Error; |
| b69ab31 | | | 284 | }>({fetchStartTimestamp: 0, fetchCompletedTimestamp: 0, commits: []}); |
| b69ab31 | | | 285 | |
| b69ab31 | | | 286 | registerCleanup( |
| b69ab31 | | | 287 | latestCommitsData, |
| b69ab31 | | | 288 | subscriptionEffect('smartlogCommits', data => { |
| b69ab31 | | | 289 | const previousDag = readAtom(latestDag); |
| b69ab31 | | | 290 | writeAtom(latestCommitsData, last => { |
| b69ab31 | | | 291 | let commits = last.commits; |
| b69ab31 | | | 292 | const newCommits = data.commits.value; |
| b69ab31 | | | 293 | if (newCommits != null) { |
| b69ab31 | | | 294 | // leave existing commits in place if there was no error |
| b69ab31 | | | 295 | commits = reuseEqualObjects(commits, newCommits, c => c.hash); |
| b69ab31 | | | 296 | } |
| b69ab31 | | | 297 | return { |
| b69ab31 | | | 298 | ...data, |
| b69ab31 | | | 299 | commits, |
| b69ab31 | | | 300 | error: data.commits.error, |
| b69ab31 | | | 301 | }; |
| b69ab31 | | | 302 | }); |
| b69ab31 | | | 303 | if (data.commits.value) { |
| b69ab31 | | | 304 | successionTracker.findNewSuccessionsFromCommits(previousDag, data.commits.value); |
| b69ab31 | | | 305 | } |
| b69ab31 | | | 306 | }), |
| b69ab31 | | | 307 | ); |
| b69ab31 | | | 308 | |
| b69ab31 | | | 309 | export const latestUncommittedChangesTimestamp = atom(get => { |
| b69ab31 | | | 310 | return get(latestUncommittedChangesData).fetchCompletedTimestamp; |
| b69ab31 | | | 311 | }); |
| b69ab31 | | | 312 | |
| b69ab31 | | | 313 | /** |
| b69ab31 | | | 314 | * Lookup a commit by hash, *WITHOUT PREVIEWS*. |
| b69ab31 | | | 315 | * Generally, you'd want to look up WITH previews, which you can use dagWithPreviews for. |
| b69ab31 | | | 316 | */ |
| b69ab31 | | | 317 | export const commitByHash = atomFamilyWeak((hash: string) => atom(get => get(latestDag).get(hash))); |
| b69ab31 | | | 318 | |
| b69ab31 | | | 319 | export const latestCommits = atom(get => { |
| b69ab31 | | | 320 | return get(latestCommitsData).commits; |
| b69ab31 | | | 321 | }); |
| b69ab31 | | | 322 | |
| b69ab31 | | | 323 | /** The dag also includes a mutationDag to answer successor queries. */ |
| b69ab31 | | | 324 | export const latestDag = atom(get => { |
| b69ab31 | | | 325 | const commits = get(latestCommits); |
| b69ab31 | | | 326 | const successorMap = get(latestSuccessorsMapAtom); |
| b69ab31 | | | 327 | const bookmarksData = get(bookmarksDataStorage); |
| b69ab31 | | | 328 | const recommendedBookmarksAvailable = get(recommendedBookmarksAvailableAtom); |
| b69ab31 | | | 329 | const enableRecommended = bookmarksData.useRecommendedBookmark && recommendedBookmarksAvailable; |
| b69ab31 | | | 330 | const recommendedBookmarks = get(recommendedBookmarksAtom); |
| b69ab31 | | | 331 | const shouldHideMaster = get(shouldHideMasterAtom); |
| b69ab31 | | | 332 | const hiddenMasterFeatureAvailable = get(hiddenMasterFeatureAvailableAtom); |
| b69ab31 | | | 333 | const commitDag = undefined; // will be populated from `commits` |
| b69ab31 | | | 334 | |
| b69ab31 | | | 335 | const dag = Dag.fromDag(commitDag, successorMap) |
| b69ab31 | | | 336 | .add( |
| b69ab31 | | | 337 | commits.map(c => { |
| b69ab31 | | | 338 | return DagCommitInfo.fromCommitInfo( |
| b69ab31 | | | 339 | filterBookmarks( |
| b69ab31 | | | 340 | bookmarksData, |
| b69ab31 | | | 341 | c, |
| b69ab31 | | | 342 | Boolean(enableRecommended), |
| b69ab31 | | | 343 | recommendedBookmarks, |
| b69ab31 | | | 344 | shouldHideMaster, |
| b69ab31 | | | 345 | hiddenMasterFeatureAvailable, |
| b69ab31 | | | 346 | ), |
| b69ab31 | | | 347 | ); |
| b69ab31 | | | 348 | }), |
| b69ab31 | | | 349 | ) |
| b69ab31 | | | 350 | .maybeForceConnectPublic(); |
| b69ab31 | | | 351 | return dag; |
| b69ab31 | | | 352 | }); |
| b69ab31 | | | 353 | |
| b69ab31 | | | 354 | function filterBookmarks( |
| b69ab31 | | | 355 | bookmarksData: BookmarksData, |
| b69ab31 | | | 356 | commit: CommitInfo, |
| b69ab31 | | | 357 | enableRecommended: boolean, |
| b69ab31 | | | 358 | recommendedBookmarks: Set<string>, |
| b69ab31 | | | 359 | shouldHideMaster: boolean, |
| b69ab31 | | | 360 | hiddenMasterFeatureAvailable: boolean, |
| b69ab31 | | | 361 | ): CommitInfo { |
| b69ab31 | | | 362 | if (commit.phase !== 'public') { |
| b69ab31 | | | 363 | return commit; |
| b69ab31 | | | 364 | } |
| b69ab31 | | | 365 | |
| b69ab31 | | | 366 | const hiddenBookmarks = new Set(bookmarksData.hiddenRemoteBookmarks); |
| b69ab31 | | | 367 | |
| b69ab31 | | | 368 | const bookmarkFilter = (b: string) => { |
| b69ab31 | | | 369 | // When hidden master feature is available, handle remote/master visibility separately |
| b69ab31 | | | 370 | if (b === REMOTE_MASTER_BOOKMARK && hiddenMasterFeatureAvailable) { |
| b69ab31 | | | 371 | const visibility = bookmarksData.masterBookmarkVisibility; |
| b69ab31 | | | 372 | if (visibility === 'show') { |
| b69ab31 | | | 373 | return true; |
| b69ab31 | | | 374 | } |
| b69ab31 | | | 375 | if (visibility === 'hide') { |
| b69ab31 | | | 376 | return false; |
| b69ab31 | | | 377 | } |
| b69ab31 | | | 378 | // visibility === 'auto' or undefined - use sitevar config |
| b69ab31 | | | 379 | return !shouldHideMaster; |
| b69ab31 | | | 380 | } |
| b69ab31 | | | 381 | |
| b69ab31 | | | 382 | // For all other bookmarks (and remote/master when feature is not available), hide if in hidden list |
| b69ab31 | | | 383 | if (hiddenBookmarks.has(b)) { |
| b69ab31 | | | 384 | return false; |
| b69ab31 | | | 385 | } |
| b69ab31 | | | 386 | |
| b69ab31 | | | 387 | // If recommended bookmarks are enabled, hide all bookmarks except the recommended ones and master |
| b69ab31 | | | 388 | return !enableRecommended || recommendedBookmarks.has(b) || b === REMOTE_MASTER_BOOKMARK; |
| b69ab31 | | | 389 | }; |
| b69ab31 | | | 390 | |
| b69ab31 | | | 391 | return { |
| b69ab31 | | | 392 | ...commit, |
| b69ab31 | | | 393 | remoteBookmarks: commit.remoteBookmarks.filter(bookmarkFilter), |
| b69ab31 | | | 394 | bookmarks: commit.bookmarks.filter(bookmarkFilter), |
| b69ab31 | | | 395 | stableCommitMetadata: commit.stableCommitMetadata?.filter(b => !hiddenBookmarks.has(b.value)), |
| b69ab31 | | | 396 | }; |
| b69ab31 | | | 397 | } |
| b69ab31 | | | 398 | |
| b69ab31 | | | 399 | export const commitFetchError = atom(get => { |
| b69ab31 | | | 400 | return get(latestCommitsData).error; |
| b69ab31 | | | 401 | }); |
| b69ab31 | | | 402 | |
| b69ab31 | | | 403 | export const authorString = configBackedAtom<string | null>( |
| b69ab31 | | | 404 | 'ui.username', |
| b69ab31 | | | 405 | null, |
| b69ab31 | | | 406 | true /* read-only */, |
| b69ab31 | | | 407 | true /* use raw value */, |
| b69ab31 | | | 408 | ); |
| b69ab31 | | | 409 | |
| b69ab31 | | | 410 | export const isFetchingCommits = atom(false); |
| b69ab31 | | | 411 | registerDisposable( |
| b69ab31 | | | 412 | isFetchingCommits, |
| b69ab31 | | | 413 | serverAPI.onMessageOfType('subscriptionResult', () => { |
| b69ab31 | | | 414 | writeAtom(isFetchingCommits, false); // new commits OR error means the fetch is not running anymore |
| b69ab31 | | | 415 | }), |
| b69ab31 | | | 416 | import.meta.hot, |
| b69ab31 | | | 417 | ); |
| b69ab31 | | | 418 | registerDisposable( |
| b69ab31 | | | 419 | isFetchingCommits, |
| b69ab31 | | | 420 | serverAPI.onMessageOfType('beganFetchingSmartlogCommitsEvent', () => { |
| b69ab31 | | | 421 | writeAtom(isFetchingCommits, true); |
| b69ab31 | | | 422 | }), |
| b69ab31 | | | 423 | import.meta.hot, |
| b69ab31 | | | 424 | ); |
| b69ab31 | | | 425 | |
| b69ab31 | | | 426 | export const isFetchingAdditionalCommits = atom(false); |
| b69ab31 | | | 427 | registerDisposable( |
| b69ab31 | | | 428 | isFetchingAdditionalCommits, |
| b69ab31 | | | 429 | serverAPI.onMessageOfType('subscriptionResult', e => { |
| b69ab31 | | | 430 | if (e.kind === 'smartlogCommits') { |
| b69ab31 | | | 431 | writeAtom(isFetchingAdditionalCommits, false); |
| b69ab31 | | | 432 | } |
| b69ab31 | | | 433 | }), |
| b69ab31 | | | 434 | import.meta.hot, |
| b69ab31 | | | 435 | ); |
| b69ab31 | | | 436 | registerDisposable( |
| b69ab31 | | | 437 | isFetchingAdditionalCommits, |
| b69ab31 | | | 438 | serverAPI.onMessageOfType('subscriptionResult', e => { |
| b69ab31 | | | 439 | if (e.kind === 'smartlogCommits') { |
| b69ab31 | | | 440 | writeAtom(isFetchingAdditionalCommits, false); |
| b69ab31 | | | 441 | } |
| b69ab31 | | | 442 | }), |
| b69ab31 | | | 443 | import.meta.hot, |
| b69ab31 | | | 444 | ); |
| b69ab31 | | | 445 | registerDisposable( |
| b69ab31 | | | 446 | isFetchingAdditionalCommits, |
| b69ab31 | | | 447 | serverAPI.onMessageOfType('beganLoadingMoreCommits', () => { |
| b69ab31 | | | 448 | writeAtom(isFetchingAdditionalCommits, true); |
| b69ab31 | | | 449 | }), |
| b69ab31 | | | 450 | import.meta.hot, |
| b69ab31 | | | 451 | ); |
| b69ab31 | | | 452 | |
| b69ab31 | | | 453 | export const isFetchingUncommittedChanges = atom(false); |
| b69ab31 | | | 454 | registerDisposable( |
| b69ab31 | | | 455 | isFetchingUncommittedChanges, |
| b69ab31 | | | 456 | serverAPI.onMessageOfType('subscriptionResult', e => { |
| b69ab31 | | | 457 | if (e.kind === 'uncommittedChanges') { |
| b69ab31 | | | 458 | writeAtom(isFetchingUncommittedChanges, false); // new files OR error means the fetch is not running anymore |
| b69ab31 | | | 459 | } |
| b69ab31 | | | 460 | }), |
| b69ab31 | | | 461 | import.meta.hot, |
| b69ab31 | | | 462 | ); |
| b69ab31 | | | 463 | registerDisposable( |
| b69ab31 | | | 464 | isFetchingUncommittedChanges, |
| b69ab31 | | | 465 | serverAPI.onMessageOfType('beganFetchingUncommittedChangesEvent', () => { |
| b69ab31 | | | 466 | writeAtom(isFetchingUncommittedChanges, true); |
| b69ab31 | | | 467 | }), |
| b69ab31 | | | 468 | import.meta.hot, |
| b69ab31 | | | 469 | ); |
| b69ab31 | | | 470 | |
| b69ab31 | | | 471 | export const commitsShownRange = atomResetOnCwdChange<number | undefined>( |
| b69ab31 | | | 472 | DEFAULT_DAYS_OF_COMMITS_TO_LOAD, |
| b69ab31 | | | 473 | ); |
| b69ab31 | | | 474 | registerDisposable( |
| b69ab31 | | | 475 | applicationinfo, |
| b69ab31 | | | 476 | serverAPI.onMessageOfType('commitsShownRange', event => { |
| b69ab31 | | | 477 | writeAtom(commitsShownRange, event.rangeInDays); |
| b69ab31 | | | 478 | }), |
| b69ab31 | | | 479 | import.meta.hot, |
| b69ab31 | | | 480 | ); |
| b69ab31 | | | 481 | |
| b69ab31 | | | 482 | /** |
| b69ab31 | | | 483 | * Latest head commit from original data from the server, without any previews. |
| b69ab31 | | | 484 | * Prefer using `dagWithPreviews.resolve('.')`, since it includes optimistic state |
| b69ab31 | | | 485 | * and previews. |
| b69ab31 | | | 486 | */ |
| b69ab31 | | | 487 | export const latestHeadCommit = atom(get => { |
| b69ab31 | | | 488 | const commits = get(latestCommits); |
| b69ab31 | | | 489 | return commits.find(commit => commit.isDot); |
| b69ab31 | | | 490 | }); |
| b69ab31 | | | 491 | |
| b69ab31 | | | 492 | /** |
| b69ab31 | | | 493 | * No longer in the "loading" state: |
| b69ab31 | | | 494 | * - Either the list of commits has successfully loaded |
| b69ab31 | | | 495 | * - or there was an error during the fetch |
| b69ab31 | | | 496 | */ |
| b69ab31 | | | 497 | export const haveCommitsLoadedYet = atom(get => { |
| b69ab31 | | | 498 | const data = get(latestCommitsData); |
| b69ab31 | | | 499 | return data.commits.length > 0 || data.error != null; |
| b69ab31 | | | 500 | }); |
| b69ab31 | | | 501 | |
| b69ab31 | | | 502 | export const haveRemotePath = atom(get => { |
| b69ab31 | | | 503 | const info = get(repositoryInfo); |
| b69ab31 | | | 504 | // codeReviewSystem.type is 'unknown' or other values if paths.default is present. |
| b69ab31 | | | 505 | return info?.type === 'success' && info.codeReviewSystem.type !== 'none'; |
| b69ab31 | | | 506 | }); |
| b69ab31 | | | 507 | |
| b69ab31 | | | 508 | registerDisposable( |
| b69ab31 | | | 509 | serverAPI, |
| b69ab31 | | | 510 | serverAPI.onMessageOfType('getUiState', () => { |
| b69ab31 | | | 511 | const state = readInterestingAtoms(); |
| b69ab31 | | | 512 | window.clientToServerAPI?.postMessage({ |
| b69ab31 | | | 513 | type: 'gotUiState', |
| b69ab31 | | | 514 | state: JSON.stringify(serializeAtomsState(state), undefined, 2), |
| b69ab31 | | | 515 | }); |
| b69ab31 | | | 516 | }), |
| b69ab31 | | | 517 | import.meta.hot, |
| b69ab31 | | | 518 | ); |
| b69ab31 | | | 519 | |
| b69ab31 | | | 520 | export const submodulesByRoot = atom<SubmodulesByRoot>(new Map()); |
| b69ab31 | | | 521 | |
| b69ab31 | | | 522 | registerCleanup( |
| b69ab31 | | | 523 | submodulesByRoot, |
| b69ab31 | | | 524 | subscriptionEffect('submodules', fetchedSubmoduleMap => { |
| b69ab31 | | | 525 | writeAtom(submodulesByRoot, _prev_data => { |
| b69ab31 | | | 526 | // TODO: In the future we may add more granular client-server API |
| b69ab31 | | | 527 | // to update submodules. For now we just replace the whole map when the active repo updates. |
| b69ab31 | | | 528 | return fetchedSubmoduleMap; |
| b69ab31 | | | 529 | }); |
| b69ab31 | | | 530 | }), |
| b69ab31 | | | 531 | import.meta.hot, |
| b69ab31 | | | 532 | ); |
| b69ab31 | | | 533 | |
| b69ab31 | | | 534 | export const submodulePathsByRoot = atomFamilyWeak<AbsolutePath, Atom<ImSet<string> | undefined>>( |
| b69ab31 | | | 535 | (root: AbsolutePath) => |
| b69ab31 | | | 536 | atom(get => { |
| b69ab31 | | | 537 | const paths = get(submodulesByRoot) |
| b69ab31 | | | 538 | .get(root) |
| b69ab31 | | | 539 | ?.value?.map(m => m.path); |
| b69ab31 | | | 540 | return paths ? ImSet(paths) : undefined; |
| b69ab31 | | | 541 | }), |
| b69ab31 | | | 542 | ); |
| b69ab31 | | | 543 | |
| b69ab31 | | | 544 | export const subscribedFullRepoBranches = atom<Array<InternalTypes['FullRepoBranch']>>([]); |
| b69ab31 | | | 545 | |
| b69ab31 | | | 546 | registerCleanup( |
| b69ab31 | | | 547 | subscribedFullRepoBranches, |
| b69ab31 | | | 548 | subscriptionEffect('subscribedFullRepoBranches', data => { |
| b69ab31 | | | 549 | writeAtom(subscribedFullRepoBranches, _ => data); |
| b69ab31 | | | 550 | }), |
| b69ab31 | | | 551 | ); |