From 3a735c6505cfe4aceb3dee12cbbdb70d1febff3a Mon Sep 17 00:00:00 2001 From: Caleb Owens Date: Thu, 9 Jan 2025 16:01:12 +0100 Subject: [PATCH] Added branch service --- apps/desktop/src/routes/+layout.svelte | 3 + apps/web/src/routes/+layout.svelte | 4 + .../shared/src/lib/branches/branchService.ts | 112 ++++++++++++++++++ 3 files changed, 119 insertions(+) create mode 100644 packages/shared/src/lib/branches/branchService.ts diff --git a/apps/desktop/src/routes/+layout.svelte b/apps/desktop/src/routes/+layout.svelte index db39bfca8a..eb3ccc838e 100644 --- a/apps/desktop/src/routes/+layout.svelte +++ b/apps/desktop/src/routes/+layout.svelte @@ -38,6 +38,7 @@ import { User, UserService } from '$lib/stores/user'; import * as events from '$lib/utils/events'; import { unsubscribe } from '$lib/utils/unsubscribe'; + import { BranchService as CloudBranchService } from '@gitbutler/shared/branches/branchService'; import { FeedService } from '@gitbutler/shared/feeds/service'; import { HttpClient } from '@gitbutler/shared/network/httpClient'; import { OrganizationService } from '@gitbutler/shared/organizations/organizationService'; @@ -70,6 +71,7 @@ const organizationService = new OrganizationService(data.cloud, appState.appDispatch); const cloudUserService = new CloudUserService(data.cloud, appState.appDispatch); const cloudProjectService = new CloudProjectService(data.cloud, appState.appDispatch); + const cloudBranchService = new CloudBranchService(data.cloud, appState.appDispatch); setContext(AppState, appState); setContext(AppDispatch, appState.appDispatch); @@ -79,6 +81,7 @@ setContext(OrganizationService, organizationService); setContext(CloudUserService, cloudUserService); setContext(CloudProjectService, cloudProjectService); + setContext(CloudBranchService, cloudBranchService); // Setters do not need to be reactive since `data` never updates setSecretsService(data.secretsService); diff --git a/apps/web/src/routes/+layout.svelte b/apps/web/src/routes/+layout.svelte index b6e0d2879e..8e477f3c00 100644 --- a/apps/web/src/routes/+layout.svelte +++ b/apps/web/src/routes/+layout.svelte @@ -15,6 +15,7 @@ import { goto } from '$app/navigation'; import { page } from '$app/stores'; import { env } from '$env/dynamic/public'; + import { BranchService } from '@gitbutler/shared/branches/branchService'; interface Props { children: Snippet; @@ -46,6 +47,9 @@ const newUserService = new NewUserService(httpClient, appState.appDispatch); setContext(NewUserService, newUserService); + const branchService = new BranchService(httpClient, appState.appDispatch); + setContext(BranchService, branchService); + $effect(() => { const token = get(authService.token) || $page.url.searchParams.get('gb_access_token'); if (token) { diff --git a/packages/shared/src/lib/branches/branchService.ts b/packages/shared/src/lib/branches/branchService.ts new file mode 100644 index 0000000000..43ed40356f --- /dev/null +++ b/packages/shared/src/lib/branches/branchService.ts @@ -0,0 +1,112 @@ +import { upsertBranch, upsertBranches } from '$lib/branches/branchesSlice'; +import { upsertPatches } from '$lib/branches/patchesSlice'; +import { + apiToBranch, + apiToPatch, + BranchStatus, + type ApiBranch, + type Branch, + type LoadableBranch, + type LoadablePatch +} from '$lib/branches/types'; +import { InterestStore, type Interest } from '$lib/interest/intrestStore'; +import { POLLING_GLACIALLY, POLLING_REGULAR } from '$lib/polling'; +import type { HttpClient } from '$lib/network/httpClient'; +import type { AppDispatch } from '$lib/redux/store.svelte'; + +type BranchUpdateParams = { + status: BranchStatus.Active | BranchStatus.Closed; + title: string; + description: string; +}; + +export class BranchService { + private readonly branchesInterests = new InterestStore<{ + repositoryId: string; + branchStatus: BranchStatus; + }>(POLLING_GLACIALLY); + private readonly branchInterests = new InterestStore<{ branchId: string }>(POLLING_REGULAR); + + constructor( + private readonly httpClient: HttpClient, + private readonly appDispatch: AppDispatch + ) {} + + getBranches(repositoryId: string, branchStatus: BranchStatus = BranchStatus.All): Interest { + return this.branchesInterests + .findOrCreateSubscribable({ repositoryId, branchStatus }, async () => { + try { + const apiBranches = await this.httpClient.get( + `patch_stack/${repositoryId}?status=${branchStatus}` + ); + const branches = apiBranches.map( + (api): LoadableBranch => ({ + status: 'found', + id: api.branch_id, + value: apiToBranch(api) + }) + ); + + const patches = apiBranches + .flatMap((branch) => branch.patches) + .map( + (api): LoadablePatch => ({ + status: 'found', + id: api.change_id, + value: apiToPatch(api) + }) + ); + + this.appDispatch.dispatch(upsertBranches(branches)); + this.appDispatch.dispatch(upsertPatches(patches)); + } catch (_error: unknown) { + /* empty */ + } + }) + .createInterest(); + } + + async createBranch(repositoryId: string, branchId: string, oplogSha: string): Promise { + const apiBranch = await this.httpClient.post(`patch_stack`, { + body: { branch_id: branchId, oplog_sha: oplogSha, project_id: repositoryId } + }); + const branch = apiToBranch(apiBranch); + + const patches = apiBranch.patches.map( + (api): LoadablePatch => ({ status: 'found', id: api.change_id, value: apiToPatch(api) }) + ); + + this.appDispatch.dispatch( + upsertBranch({ + status: 'found', + id: branch.branchId, + value: branch + }) + ); + this.appDispatch.dispatch(upsertPatches(patches)); + + return branch; + } + + async updateBranch(branchId: string, params: BranchUpdateParams): Promise { + const apiBranch = await this.httpClient.put(`patch_stack/${branchId}`, { + body: params + }); + const branch = apiToBranch(apiBranch); + + const patches = apiBranch.patches.map( + (api): LoadablePatch => ({ status: 'found', id: api.change_id, value: apiToPatch(api) }) + ); + + this.appDispatch.dispatch( + upsertBranch({ + status: 'found', + id: branch.branchId, + value: branch + }) + ); + this.appDispatch.dispatch(upsertPatches(patches)); + + return branch; + } +}