addons/isl/src/serverAPIState.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
ed2694f8import {startTransition} from 'react';
b69ab319import type {MessageBusStatus} from './MessageBus';
b69ab3110import type {
b69ab3111 AbsolutePath,
b69ab3112 ApplicationInfo,
b69ab3113 ChangedFile,
b69ab3114 CommitInfo,
b69ab3115 MergeConflicts,
b69ab3116 RepoInfo,
b69ab3117 SmartlogCommits,
b69ab3118 SubmodulesByRoot,
b69ab3119 SubscriptionKind,
b69ab3120 SubscriptionResultsData,
b69ab3121 UncommittedChanges,
b69ab3122 ValidatedRepoInfo,
b69ab3123} from './types';
b69ab3124
b69ab3125import {Set as ImSet} from 'immutable';
b69ab3126import {DEFAULT_DAYS_OF_COMMITS_TO_LOAD} from 'isl-server/src/constants';
b69ab3127import type {Atom} from 'jotai';
b69ab3128import {atom} from 'jotai';
b69ab3129import {reuseEqualObjects} from 'shared/deepEqualExt';
b69ab3130import {randomId} from 'shared/utils';
b69ab3131import {
b69ab3132 type BookmarksData,
b69ab3133 bookmarksDataStorage,
b69ab3134 recommendedBookmarksAtom,
b69ab3135 recommendedBookmarksAvailableAtom,
b69ab3136 REMOTE_MASTER_BOOKMARK,
b69ab3137} from './BookmarksData';
b69ab3138import serverAPI from './ClientToServerAPI';
b69ab3139import {hiddenMasterFeatureAvailableAtom, shouldHideMasterAtom} from './HiddenMasterData';
b69ab3140import type {InternalTypes} from './InternalTypes';
b69ab3141import {latestSuccessorsMapAtom, successionTracker} from './SuccessionTracker';
b69ab3142import {Dag, DagCommitInfo} from './dag/dag';
b69ab3143import {readInterestingAtoms, serializeAtomsState} from './debug/getInterestingAtoms';
b69ab3144import {atomFamilyWeak, configBackedAtom, readAtom, writeAtom} from './jotaiUtils';
b69ab3145import platform from './platform';
b69ab3146import {atomResetOnCwdChange, repositoryData} from './repositoryData';
b69ab3147import {registerCleanup, registerDisposable} from './utils';
b69ab3148
b69ab3149export {repositoryData};
b69ab3150
b69ab3151registerDisposable(
b69ab3152 repositoryData,
b69ab3153 serverAPI.onMessageOfType('repoInfo', event => {
b69ab3154 writeAtom(repositoryData, {info: event.info, cwd: event.cwd});
b69ab3155 }),
b69ab3156 import.meta.hot,
b69ab3157);
b69ab3158registerCleanup(
b69ab3159 repositoryData,
b69ab3160 serverAPI.onSetup(() =>
b69ab3161 serverAPI.postMessage({
b69ab3162 type: 'requestRepoInfo',
b69ab3163 }),
b69ab3164 ),
b69ab3165 import.meta.hot,
b69ab3166);
b69ab3167
b69ab3168export const repositoryInfoOrError = atom(
b69ab3169 get => {
b69ab3170 const data = get(repositoryData);
b69ab3171 return data?.info;
b69ab3172 },
b69ab3173 (
b69ab3174 get,
b69ab3175 set,
b69ab3176 update: RepoInfo | undefined | ((_prev: RepoInfo | undefined) => RepoInfo | undefined),
b69ab3177 ) => {
b69ab3178 const value = typeof update === 'function' ? update(get(repositoryData)?.info) : update;
b69ab3179 set(repositoryData, last => ({
b69ab3180 ...last,
b69ab3181 info: value,
b69ab3182 }));
b69ab3183 },
b69ab3184);
b69ab3185
b69ab3186/** ValidatedRepoInfo, or undefined on error. */
b69ab3187export const repositoryInfo = atom(
b69ab3188 get => {
b69ab3189 const info = get(repositoryInfoOrError);
b69ab3190 if (info?.type === 'success') {
b69ab3191 return info;
b69ab3192 }
b69ab3193 return undefined;
b69ab3194 },
b69ab3195 (
b69ab3196 get,
b69ab3197 set,
b69ab3198 update:
b69ab3199 | ValidatedRepoInfo
b69ab31100 | undefined
b69ab31101 | ((_prev: ValidatedRepoInfo | undefined) => ValidatedRepoInfo | undefined),
b69ab31102 ) => {
b69ab31103 const value = typeof update === 'function' ? update(get(repositoryInfo)) : update;
b69ab31104 set(repositoryData, last => ({
b69ab31105 ...last,
b69ab31106 info: value,
b69ab31107 }));
b69ab31108 },
b69ab31109);
b69ab31110
b69ab31111/** Main command name, like 'sl'. */
b69ab31112export const mainCommandName = atom(get => {
b69ab31113 const info = get(repositoryInfo);
b69ab31114 return info?.command ?? 'sl';
b69ab31115});
b69ab31116
b69ab31117/** List of repo roots. Useful when cwd is in nested submodules. */
b69ab31118export const repoRoots = atom(get => {
b69ab31119 const info = get(repositoryInfo);
b69ab31120 return info?.repoRoots;
b69ab31121});
b69ab31122
b69ab31123export const applicationinfo = atom<ApplicationInfo | undefined>(undefined);
b69ab31124registerDisposable(
b69ab31125 applicationinfo,
b69ab31126 serverAPI.onMessageOfType('applicationInfo', event => {
b69ab31127 writeAtom(applicationinfo, event.info);
b69ab31128 }),
b69ab31129 import.meta.hot,
b69ab31130);
b69ab31131registerCleanup(
b69ab31132 applicationinfo,
b69ab31133 serverAPI.onSetup(() =>
b69ab31134 serverAPI.postMessage({
b69ab31135 type: 'requestApplicationInfo',
b69ab31136 }),
b69ab31137 ),
b69ab31138 import.meta.hot,
b69ab31139);
b69ab31140
4fe1f34141export const watchmanStatus = atom<
4fe1f34142 'initializing' | 'reconnecting' | 'healthy' | 'ended' | 'errored' | 'unavailable' | undefined
4fe1f34143>(undefined);
4fe1f34144registerDisposable(
4fe1f34145 watchmanStatus,
4fe1f34146 serverAPI.onMessageOfType('watchmanStatus', event => {
4fe1f34147 writeAtom(watchmanStatus, event.status);
4fe1f34148 }),
4fe1f34149 import.meta.hot,
4fe1f34150);
4fe1f34151
b69ab31152export const reconnectingStatus = atom<MessageBusStatus>({type: 'initializing'});
b69ab31153registerDisposable(
b69ab31154 reconnectingStatus,
b69ab31155 platform.messageBus.onChangeStatus(status => {
b69ab31156 writeAtom(reconnectingStatus, status);
b69ab31157 }),
b69ab31158 import.meta.hot,
b69ab31159);
b69ab31160
b69ab31161export async function forceFetchCommit(revset: string): Promise<CommitInfo> {
b69ab31162 serverAPI.postMessage({
b69ab31163 type: 'fetchLatestCommit',
b69ab31164 revset,
b69ab31165 });
b69ab31166 const response = await serverAPI.nextMessageMatching(
b69ab31167 'fetchedLatestCommit',
b69ab31168 message => message.revset === revset,
b69ab31169 );
b69ab31170 if (response.info.error) {
b69ab31171 throw response.info.error;
b69ab31172 }
b69ab31173 return response.info.value;
b69ab31174}
b69ab31175
b69ab31176export const mostRecentSubscriptionIds: Record<SubscriptionKind, string> = {
b69ab31177 smartlogCommits: '',
b69ab31178 uncommittedChanges: '',
b69ab31179 mergeConflicts: '',
b69ab31180 submodules: '',
b69ab31181 subscribedFullRepoBranches: '',
b69ab31182};
b69ab31183
b69ab31184/**
b69ab31185 * Send a subscribeFoo message to the server on initialization,
b69ab31186 * and send an unsubscribe message on dispose.
b69ab31187 * Extract subscription response messages via a unique subscriptionID per effect call.
b69ab31188 */
b69ab31189function subscriptionEffect<K extends SubscriptionKind>(
b69ab31190 kind: K,
b69ab31191 onData: (data: SubscriptionResultsData[K]) => unknown,
b69ab31192): () => void {
b69ab31193 const subscriptionID = randomId();
b69ab31194 mostRecentSubscriptionIds[kind] = subscriptionID;
b69ab31195 const disposable = serverAPI.onMessageOfType('subscriptionResult', event => {
b69ab31196 if (event.subscriptionID !== subscriptionID || event.kind !== kind) {
b69ab31197 return;
b69ab31198 }
ed2694f199 // Defer off the current task so ongoing user interactions (e.g. drag) aren't blocked.
ed2694f200 // startTransition alone isn't enough since the message handler runs synchronously
ed2694f201 // on the same task as the drag mousemove handler.
ed2694f202 setTimeout(() => {
ed2694f203 startTransition(() => {
ed2694f204 onData(event.data as SubscriptionResultsData[K]);
ed2694f205 });
ed2694f206 }, 0);
b69ab31207 });
b69ab31208
b69ab31209 const disposeSubscription = serverAPI.onSetup(() => {
b69ab31210 serverAPI.postMessage({
b69ab31211 type: 'subscribe',
b69ab31212 kind,
b69ab31213 subscriptionID,
b69ab31214 });
b69ab31215
b69ab31216 return () =>
b69ab31217 serverAPI.postMessage({
b69ab31218 type: 'unsubscribe',
b69ab31219 kind,
b69ab31220 subscriptionID,
b69ab31221 });
b69ab31222 });
b69ab31223
b69ab31224 return () => {
b69ab31225 disposable.dispose();
b69ab31226 disposeSubscription();
b69ab31227 };
b69ab31228}
b69ab31229
b69ab31230export const latestUncommittedChangesData = atom<{
b69ab31231 fetchStartTimestamp: number;
b69ab31232 fetchCompletedTimestamp: number;
b69ab31233 files: UncommittedChanges;
b69ab31234 error?: Error;
b69ab31235}>({fetchStartTimestamp: 0, fetchCompletedTimestamp: 0, files: []});
b69ab31236// This is used by a test. Tests do not go through babel to rewrite source
b69ab31237// to insert debugLabel.
b69ab31238latestUncommittedChangesData.debugLabel = 'latestUncommittedChangesData';
b69ab31239
b69ab31240registerCleanup(
b69ab31241 latestUncommittedChangesData,
b69ab31242 subscriptionEffect('uncommittedChanges', data => {
b69ab31243 writeAtom(latestUncommittedChangesData, last => ({
b69ab31244 ...data,
b69ab31245 files:
b69ab31246 data.files.value ??
b69ab31247 // leave existing files in place if there was no error
b69ab31248 (last.error == null ? [] : last.files) ??
b69ab31249 [],
b69ab31250 error: data.files.error,
b69ab31251 }));
b69ab31252 }),
b69ab31253 import.meta.hot,
b69ab31254);
b69ab31255
b69ab31256/**
b69ab31257 * Latest fetched uncommitted file changes from the server, without any previews.
b69ab31258 * Prefer using `uncommittedChangesWithPreviews`, since it includes optimistic state
b69ab31259 * and previews.
b69ab31260 */
b69ab31261export const latestUncommittedChanges = atom<Array<ChangedFile>>(
b69ab31262 get => get(latestUncommittedChangesData).files,
b69ab31263);
b69ab31264
b69ab31265export const uncommittedChangesFetchError = atom(get => {
b69ab31266 return get(latestUncommittedChangesData).error;
b69ab31267});
b69ab31268
b69ab31269export const mergeConflicts = atom<MergeConflicts | undefined>(undefined);
b69ab31270registerCleanup(
b69ab31271 mergeConflicts,
b69ab31272 subscriptionEffect('mergeConflicts', data => {
b69ab31273 writeAtom(mergeConflicts, data);
b69ab31274 }),
b69ab31275);
b69ab31276
b69ab31277export const inMergeConflicts = atom(get => get(mergeConflicts) != undefined);
b69ab31278
b69ab31279export const latestCommitsData = atom<{
b69ab31280 fetchStartTimestamp: number;
b69ab31281 fetchCompletedTimestamp: number;
b69ab31282 commits: SmartlogCommits;
b69ab31283 error?: Error;
b69ab31284}>({fetchStartTimestamp: 0, fetchCompletedTimestamp: 0, commits: []});
b69ab31285
b69ab31286registerCleanup(
b69ab31287 latestCommitsData,
b69ab31288 subscriptionEffect('smartlogCommits', data => {
b69ab31289 const previousDag = readAtom(latestDag);
b69ab31290 writeAtom(latestCommitsData, last => {
b69ab31291 let commits = last.commits;
b69ab31292 const newCommits = data.commits.value;
b69ab31293 if (newCommits != null) {
b69ab31294 // leave existing commits in place if there was no error
b69ab31295 commits = reuseEqualObjects(commits, newCommits, c => c.hash);
b69ab31296 }
b69ab31297 return {
b69ab31298 ...data,
b69ab31299 commits,
b69ab31300 error: data.commits.error,
b69ab31301 };
b69ab31302 });
b69ab31303 if (data.commits.value) {
b69ab31304 successionTracker.findNewSuccessionsFromCommits(previousDag, data.commits.value);
b69ab31305 }
b69ab31306 }),
b69ab31307);
b69ab31308
b69ab31309export const latestUncommittedChangesTimestamp = atom(get => {
b69ab31310 return get(latestUncommittedChangesData).fetchCompletedTimestamp;
b69ab31311});
b69ab31312
b69ab31313/**
b69ab31314 * Lookup a commit by hash, *WITHOUT PREVIEWS*.
b69ab31315 * Generally, you'd want to look up WITH previews, which you can use dagWithPreviews for.
b69ab31316 */
b69ab31317export const commitByHash = atomFamilyWeak((hash: string) => atom(get => get(latestDag).get(hash)));
b69ab31318
b69ab31319export const latestCommits = atom(get => {
b69ab31320 return get(latestCommitsData).commits;
b69ab31321});
b69ab31322
b69ab31323/** The dag also includes a mutationDag to answer successor queries. */
b69ab31324export const latestDag = atom(get => {
b69ab31325 const commits = get(latestCommits);
b69ab31326 const successorMap = get(latestSuccessorsMapAtom);
b69ab31327 const bookmarksData = get(bookmarksDataStorage);
b69ab31328 const recommendedBookmarksAvailable = get(recommendedBookmarksAvailableAtom);
b69ab31329 const enableRecommended = bookmarksData.useRecommendedBookmark && recommendedBookmarksAvailable;
b69ab31330 const recommendedBookmarks = get(recommendedBookmarksAtom);
b69ab31331 const shouldHideMaster = get(shouldHideMasterAtom);
b69ab31332 const hiddenMasterFeatureAvailable = get(hiddenMasterFeatureAvailableAtom);
b69ab31333 const commitDag = undefined; // will be populated from `commits`
b69ab31334
b69ab31335 const dag = Dag.fromDag(commitDag, successorMap)
b69ab31336 .add(
b69ab31337 commits.map(c => {
b69ab31338 return DagCommitInfo.fromCommitInfo(
b69ab31339 filterBookmarks(
b69ab31340 bookmarksData,
b69ab31341 c,
b69ab31342 Boolean(enableRecommended),
b69ab31343 recommendedBookmarks,
b69ab31344 shouldHideMaster,
b69ab31345 hiddenMasterFeatureAvailable,
b69ab31346 ),
b69ab31347 );
b69ab31348 }),
b69ab31349 )
b69ab31350 .maybeForceConnectPublic();
b69ab31351 return dag;
b69ab31352});
b69ab31353
b69ab31354function filterBookmarks(
b69ab31355 bookmarksData: BookmarksData,
b69ab31356 commit: CommitInfo,
b69ab31357 enableRecommended: boolean,
b69ab31358 recommendedBookmarks: Set<string>,
b69ab31359 shouldHideMaster: boolean,
b69ab31360 hiddenMasterFeatureAvailable: boolean,
b69ab31361): CommitInfo {
b69ab31362 if (commit.phase !== 'public') {
b69ab31363 return commit;
b69ab31364 }
b69ab31365
b69ab31366 const hiddenBookmarks = new Set(bookmarksData.hiddenRemoteBookmarks);
b69ab31367
b69ab31368 const bookmarkFilter = (b: string) => {
b69ab31369 // When hidden master feature is available, handle remote/master visibility separately
b69ab31370 if (b === REMOTE_MASTER_BOOKMARK && hiddenMasterFeatureAvailable) {
b69ab31371 const visibility = bookmarksData.masterBookmarkVisibility;
b69ab31372 if (visibility === 'show') {
b69ab31373 return true;
b69ab31374 }
b69ab31375 if (visibility === 'hide') {
b69ab31376 return false;
b69ab31377 }
b69ab31378 // visibility === 'auto' or undefined - use sitevar config
b69ab31379 return !shouldHideMaster;
b69ab31380 }
b69ab31381
b69ab31382 // For all other bookmarks (and remote/master when feature is not available), hide if in hidden list
b69ab31383 if (hiddenBookmarks.has(b)) {
b69ab31384 return false;
b69ab31385 }
b69ab31386
b69ab31387 // If recommended bookmarks are enabled, hide all bookmarks except the recommended ones and master
b69ab31388 return !enableRecommended || recommendedBookmarks.has(b) || b === REMOTE_MASTER_BOOKMARK;
b69ab31389 };
b69ab31390
b69ab31391 return {
b69ab31392 ...commit,
b69ab31393 remoteBookmarks: commit.remoteBookmarks.filter(bookmarkFilter),
b69ab31394 bookmarks: commit.bookmarks.filter(bookmarkFilter),
b69ab31395 stableCommitMetadata: commit.stableCommitMetadata?.filter(b => !hiddenBookmarks.has(b.value)),
b69ab31396 };
b69ab31397}
b69ab31398
b69ab31399export const commitFetchError = atom(get => {
b69ab31400 return get(latestCommitsData).error;
b69ab31401});
b69ab31402
b69ab31403export const authorString = configBackedAtom<string | null>(
b69ab31404 'ui.username',
b69ab31405 null,
b69ab31406 true /* read-only */,
b69ab31407 true /* use raw value */,
b69ab31408);
b69ab31409
b69ab31410export const isFetchingCommits = atom(false);
b69ab31411registerDisposable(
b69ab31412 isFetchingCommits,
b69ab31413 serverAPI.onMessageOfType('subscriptionResult', () => {
b69ab31414 writeAtom(isFetchingCommits, false); // new commits OR error means the fetch is not running anymore
b69ab31415 }),
b69ab31416 import.meta.hot,
b69ab31417);
b69ab31418registerDisposable(
b69ab31419 isFetchingCommits,
b69ab31420 serverAPI.onMessageOfType('beganFetchingSmartlogCommitsEvent', () => {
b69ab31421 writeAtom(isFetchingCommits, true);
b69ab31422 }),
b69ab31423 import.meta.hot,
b69ab31424);
b69ab31425
b69ab31426export const isFetchingAdditionalCommits = atom(false);
b69ab31427registerDisposable(
b69ab31428 isFetchingAdditionalCommits,
b69ab31429 serverAPI.onMessageOfType('subscriptionResult', e => {
b69ab31430 if (e.kind === 'smartlogCommits') {
b69ab31431 writeAtom(isFetchingAdditionalCommits, false);
b69ab31432 }
b69ab31433 }),
b69ab31434 import.meta.hot,
b69ab31435);
b69ab31436registerDisposable(
b69ab31437 isFetchingAdditionalCommits,
b69ab31438 serverAPI.onMessageOfType('subscriptionResult', e => {
b69ab31439 if (e.kind === 'smartlogCommits') {
b69ab31440 writeAtom(isFetchingAdditionalCommits, false);
b69ab31441 }
b69ab31442 }),
b69ab31443 import.meta.hot,
b69ab31444);
b69ab31445registerDisposable(
b69ab31446 isFetchingAdditionalCommits,
b69ab31447 serverAPI.onMessageOfType('beganLoadingMoreCommits', () => {
b69ab31448 writeAtom(isFetchingAdditionalCommits, true);
b69ab31449 }),
b69ab31450 import.meta.hot,
b69ab31451);
b69ab31452
b69ab31453export const isFetchingUncommittedChanges = atom(false);
b69ab31454registerDisposable(
b69ab31455 isFetchingUncommittedChanges,
b69ab31456 serverAPI.onMessageOfType('subscriptionResult', e => {
b69ab31457 if (e.kind === 'uncommittedChanges') {
b69ab31458 writeAtom(isFetchingUncommittedChanges, false); // new files OR error means the fetch is not running anymore
b69ab31459 }
b69ab31460 }),
b69ab31461 import.meta.hot,
b69ab31462);
b69ab31463registerDisposable(
b69ab31464 isFetchingUncommittedChanges,
b69ab31465 serverAPI.onMessageOfType('beganFetchingUncommittedChangesEvent', () => {
b69ab31466 writeAtom(isFetchingUncommittedChanges, true);
b69ab31467 }),
b69ab31468 import.meta.hot,
b69ab31469);
b69ab31470
b69ab31471export const commitsShownRange = atomResetOnCwdChange<number | undefined>(
b69ab31472 DEFAULT_DAYS_OF_COMMITS_TO_LOAD,
b69ab31473);
b69ab31474registerDisposable(
b69ab31475 applicationinfo,
b69ab31476 serverAPI.onMessageOfType('commitsShownRange', event => {
b69ab31477 writeAtom(commitsShownRange, event.rangeInDays);
b69ab31478 }),
b69ab31479 import.meta.hot,
b69ab31480);
b69ab31481
b69ab31482/**
b69ab31483 * Latest head commit from original data from the server, without any previews.
b69ab31484 * Prefer using `dagWithPreviews.resolve('.')`, since it includes optimistic state
b69ab31485 * and previews.
b69ab31486 */
b69ab31487export const latestHeadCommit = atom(get => {
b69ab31488 const commits = get(latestCommits);
b69ab31489 return commits.find(commit => commit.isDot);
b69ab31490});
b69ab31491
b69ab31492/**
b69ab31493 * No longer in the "loading" state:
b69ab31494 * - Either the list of commits has successfully loaded
b69ab31495 * - or there was an error during the fetch
b69ab31496 */
b69ab31497export const haveCommitsLoadedYet = atom(get => {
b69ab31498 const data = get(latestCommitsData);
b69ab31499 return data.commits.length > 0 || data.error != null;
b69ab31500});
b69ab31501
b69ab31502export const haveRemotePath = atom(get => {
b69ab31503 const info = get(repositoryInfo);
b69ab31504 // codeReviewSystem.type is 'unknown' or other values if paths.default is present.
b69ab31505 return info?.type === 'success' && info.codeReviewSystem.type !== 'none';
b69ab31506});
b69ab31507
b69ab31508registerDisposable(
b69ab31509 serverAPI,
b69ab31510 serverAPI.onMessageOfType('getUiState', () => {
b69ab31511 const state = readInterestingAtoms();
b69ab31512 window.clientToServerAPI?.postMessage({
b69ab31513 type: 'gotUiState',
b69ab31514 state: JSON.stringify(serializeAtomsState(state), undefined, 2),
b69ab31515 });
b69ab31516 }),
b69ab31517 import.meta.hot,
b69ab31518);
b69ab31519
b69ab31520export const submodulesByRoot = atom<SubmodulesByRoot>(new Map());
b69ab31521
b69ab31522registerCleanup(
b69ab31523 submodulesByRoot,
b69ab31524 subscriptionEffect('submodules', fetchedSubmoduleMap => {
b69ab31525 writeAtom(submodulesByRoot, _prev_data => {
b69ab31526 // TODO: In the future we may add more granular client-server API
b69ab31527 // to update submodules. For now we just replace the whole map when the active repo updates.
b69ab31528 return fetchedSubmoduleMap;
b69ab31529 });
b69ab31530 }),
b69ab31531 import.meta.hot,
b69ab31532);
b69ab31533
b69ab31534export const submodulePathsByRoot = atomFamilyWeak<AbsolutePath, Atom<ImSet<string> | undefined>>(
b69ab31535 (root: AbsolutePath) =>
b69ab31536 atom(get => {
b69ab31537 const paths = get(submodulesByRoot)
b69ab31538 .get(root)
b69ab31539 ?.value?.map(m => m.path);
b69ab31540 return paths ? ImSet(paths) : undefined;
b69ab31541 }),
b69ab31542);
b69ab31543
b69ab31544export const subscribedFullRepoBranches = atom<Array<InternalTypes['FullRepoBranch']>>([]);
b69ab31545
b69ab31546registerCleanup(
b69ab31547 subscribedFullRepoBranches,
b69ab31548 subscriptionEffect('subscribedFullRepoBranches', data => {
b69ab31549 writeAtom(subscribedFullRepoBranches, _ => data);
b69ab31550 }),
b69ab31551);