diff --git a/apps/desktop/src/components/branchesPage/BranchesViewStack.svelte b/apps/desktop/src/components/branchesPage/BranchesViewStack.svelte index 024ee380132..451581a2e02 100644 --- a/apps/desktop/src/components/branchesPage/BranchesViewStack.svelte +++ b/apps/desktop/src/components/branchesPage/BranchesViewStack.svelte @@ -30,7 +30,7 @@ const stackService = inject(STACK_SERVICE); - const stackQuery = $derived(stackService.allStackById(projectId, stackId)); + const stackQuery = $derived(stackService.stackById(projectId, stackId)); diff --git a/apps/desktop/src/lib/stacks/headInfoAdapters.test.ts b/apps/desktop/src/lib/stacks/headInfoAdapters.test.ts new file mode 100644 index 00000000000..82ffa9f83e1 --- /dev/null +++ b/apps/desktop/src/lib/stacks/headInfoAdapters.test.ts @@ -0,0 +1,151 @@ +import { transformWorkspaceDetails } from "$lib/stacks/headInfoAdapters"; +import { describe, expect, test } from "vitest"; +import type { Author, Commit, RefInfo, Segment, UpstreamCommit } from "@gitbutler/but-sdk"; + +const encoder = new TextEncoder(); + +function bytes(value: string): number[] { + return [...encoder.encode(value)]; +} + +const author: Author = { + name: "Ada", + email: "ada@example.com", + gravatarUrl: "", +}; + +const localCommit: Commit = { + id: "1111111111111111111111111111111111111111", + parentIds: ["0000000000000000000000000000000000000000"], + message: "Local commit", + hasConflicts: true, + state: { type: "LocalOnly" }, + createdAt: 1000, + author, + changeId: "I111", + gerritReviewUrl: null, +}; + +const upstreamCommit: UpstreamCommit = { + id: "2222222222222222222222222222222222222222", + message: "Remote commit", + createdAt: 2000, + author, + changeId: "I222", +}; + +function segment(overrides: Partial = {}): Segment { + return { + refName: { + fullNameBytes: bytes("refs/heads/feature/top"), + displayName: "feature/top", + }, + remoteTrackingRefName: { + fullNameBytes: bytes("refs/remotes/origin/feature/top"), + displayName: "feature/top", + remoteName: "origin", + }, + commits: [localCommit], + commitsOnRemote: [upstreamCommit], + commitsOutside: null, + metadata: { + refInfo: { + createdAt: null, + updatedAt: { seconds: 123, offset: 0 }, + }, + review: { + pullRequest: 7, + reviewId: "review-7", + }, + }, + isEntrypoint: true, + pushStatus: "unpushedCommits", + base: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + ...overrides, + }; +} + +function refInfo(stacks: RefInfo["stacks"]): RefInfo { + return { + workspaceRef: null, + stacks, + target: null, + isManagedRef: true, + isManagedCommit: true, + isEntrypoint: true, + }; +} + +describe("headInfoAdapters", () => { + test("maps head_info stacks to legacy stack entries and stack details", () => { + const result = transformWorkspaceDetails( + refInfo([ + { + id: "stack-1", + base: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + segments: [segment()], + }, + ]), + ); + + expect(result.stacks.ids).toEqual(["stack-1"]); + expect(result.stacks.entities["stack-1"]).toMatchObject({ + id: "stack-1", + tip: localCommit.id, + order: 0, + isCheckedOut: true, + heads: [ + { + name: "feature/top", + tip: localCommit.id, + reviewId: 7, + isCheckedOut: true, + }, + ], + }); + + const details = result.stackDetails["stack-1"]!; + expect(details.stackInfo).toMatchObject({ + derivedName: "feature/top", + pushStatus: "unpushedCommits", + isConflicted: true, + }); + expect(details.stackInfo.branchDetails[0]).toMatchObject({ + name: "feature/top", + reference: "refs/heads/feature/top", + remoteTrackingBranch: "refs/remotes/origin/feature/top", + prNumber: 7, + reviewId: "review-7", + tip: localCommit.id, + baseCommit: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + lastUpdatedAt: 123000, + commits: [localCommit], + upstreamCommits: [upstreamCommit], + isRemoteHead: false, + }); + expect(details.commits.ids).toEqual([localCommit.id]); + expect(details.upstreamCommits.ids).toEqual([upstreamCommit.id]); + }); + + test("uses the stack base as the tip for empty segments", () => { + const result = transformWorkspaceDetails( + refInfo([ + { + id: "stack-1", + base: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + segments: [ + segment({ + commits: [], + base: null, + }), + ], + }, + ]), + ); + + expect(result.stacks.entities["stack-1"]?.tip).toBe("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"); + expect(result.stackDetails["stack-1"]?.stackInfo.branchDetails[0]?.tip).toBe( + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + ); + }); +}); diff --git a/apps/desktop/src/lib/stacks/headInfoAdapters.ts b/apps/desktop/src/lib/stacks/headInfoAdapters.ts new file mode 100644 index 00000000000..2ed9b9de2a8 --- /dev/null +++ b/apps/desktop/src/lib/stacks/headInfoAdapters.ts @@ -0,0 +1,194 @@ +import { createEntityAdapter, type EntityState } from "@reduxjs/toolkit"; +import type { + BranchDetails, + Commit, + RefInfo, + Segment, + Stack as RefInfoStack, + StackDetails, + StackEntry, + UpstreamCommit, +} from "@gitbutler/but-sdk"; + +export type WorkspaceStackDetails = { + stackInfo: StackDetails; + branchDetails: EntityState; + commits: EntityState; + upstreamCommits: EntityState; +}; + +export type WorkspaceDetails = { + stacks: EntityState; + stackDetails: Record; +}; + +const NULL_SHA = "0000000000000000000000000000000000000000"; + +const stackAdapter = createEntityAdapter({ + selectId: (stack) => stack.id ?? stack.heads.at(0)?.name ?? stack.tip, +}); + +const branchDetailsAdapter = createEntityAdapter({ + selectId: (branch) => branch.name, +}); + +const commitAdapter = createEntityAdapter({ + selectId: (commit) => commit.id, +}); + +const upstreamCommitAdapter = createEntityAdapter({ + selectId: (commit) => commit.id, +}); + +function decodeBytes(bytes: number[]): string { + return new TextDecoder().decode(new Uint8Array(bytes)); +} + +function stackKey(stack: StackEntry): string { + return stack.id ?? stack.heads.at(0)?.name ?? stack.tip; +} + +function segmentName(segment: Segment): string { + if (!segment.refName) { + throw new Error("Cannot map anonymous head_info segment to legacy branch details"); + } + return segment.refName.displayName; +} + +function segmentReference(segment: Segment): string { + if (!segment.refName) { + throw new Error("Cannot map anonymous head_info segment to legacy branch details"); + } + return decodeBytes(segment.refName.fullNameBytes); +} + +function segmentTip(stack: RefInfoStack, segment: Segment): string { + return segment.commits.at(0)?.id ?? segment.base ?? stack.base ?? NULL_SHA; +} + +function branchLastUpdatedAt(segment: Segment): number | null { + const seconds = segment.metadata?.refInfo.updatedAt?.seconds; + return seconds === undefined ? null : seconds * 1000; +} + +function branchAuthors(segment: Segment): BranchDetails["authors"] { + const authors = [...segment.commits, ...segment.commitsOnRemote].map((commit) => commit.author); + return Array.from( + new Map(authors.map((author) => [JSON.stringify(author), author])).values(), + ).sort((a, b) => (a.name ?? "").localeCompare(b.name ?? "")); +} + +export function stackEntryFromHeadInfoStack(stack: RefInfoStack, index: number): StackEntry { + const heads = stack.segments.map((segment) => ({ + name: segmentName(segment), + tip: segmentTip(stack, segment), + reviewId: segment.metadata?.review.pullRequest ?? null, + isCheckedOut: segment.isEntrypoint, + })); + const tip = heads.at(0)?.tip ?? stack.base ?? NULL_SHA; + + return { + id: stack.id, + heads, + tip, + order: index, + isCheckedOut: heads.some((head) => head.isCheckedOut), + }; +} + +export function stackDetailsFromHeadInfoStack(stack: RefInfoStack): StackDetails { + const branchDetails = stack.segments.map((segment): BranchDetails => { + const baseCommit = segment.base ?? stack.base ?? NULL_SHA; + const remoteTrackingBranch = segment.remoteTrackingRefName + ? decodeBytes(segment.remoteTrackingRefName.fullNameBytes) + : null; + + return { + name: segmentName(segment), + reference: segmentReference(segment), + linkedWorktreeId: null, + remoteTrackingBranch, + prNumber: segment.metadata?.review.pullRequest ?? null, + reviewId: segment.metadata?.review.reviewId ?? null, + tip: segmentTip(stack, segment), + baseCommit, + pushStatus: segment.pushStatus, + lastUpdatedAt: branchLastUpdatedAt(segment), + authors: branchAuthors(segment), + isConflicted: segment.commits.some((commit) => commit.hasConflicts), + commits: segment.commits, + upstreamCommits: segment.commitsOnRemote, + isRemoteHead: segmentReference(segment).startsWith("refs/remotes/"), + }; + }); + const topmostBranch = branchDetails.at(0); + + if (!topmostBranch) { + throw new Error("Cannot map empty head_info stack to legacy stack details"); + } + + return { + derivedName: topmostBranch.name, + pushStatus: topmostBranch.pushStatus, + branchDetails, + isConflicted: topmostBranch.isConflicted, + }; +} + +export function transformWorkspaceDetails(response: RefInfo): WorkspaceDetails { + const stacks = response.stacks.map(stackEntryFromHeadInfoStack); + const stackDetails = Object.fromEntries( + response.stacks.map((stack, index) => { + const entry = stacks[index]!; + const details = stackDetailsFromHeadInfoStack(stack); + return [ + stackKey(entry), + { + stackInfo: details, + branchDetails: branchDetailsAdapter.addMany( + branchDetailsAdapter.getInitialState(), + details.branchDetails, + ), + commits: commitAdapter.addMany( + commitAdapter.getInitialState(), + details.branchDetails.flatMap((branch) => branch.commits), + ), + upstreamCommits: upstreamCommitAdapter.addMany( + upstreamCommitAdapter.getInitialState(), + details.branchDetails.flatMap((branch) => branch.upstreamCommits), + ), + }, + ]; + }), + ); + + return { + stacks: stackAdapter.addMany(stackAdapter.getInitialState(), stacks), + stackDetails, + }; +} + +export function selectWorkspaceStackDetails( + workspaceDetails: WorkspaceDetails, + stackId?: string, + branchName?: string, +): WorkspaceStackDetails | undefined { + if (stackId) return workspaceDetails.stackDetails[stackId]; + if (branchName) { + return Object.values(workspaceDetails.stackDetails).find((details) => + details.stackInfo.branchDetails.some((branch) => branch.name === branchName), + ); + } + return Object.values(workspaceDetails.stackDetails).at(0); +} + +export function selectWorkspaceStackById( + workspaceDetails: WorkspaceDetails, + stackId: string, +): StackEntry | undefined { + return stackAdapter.getSelectors().selectById(workspaceDetails.stacks, stackId); +} + +export function workspaceStackDetailTags(workspaceDetails: WorkspaceDetails): string[] { + return Object.keys(workspaceDetails.stackDetails); +} diff --git a/apps/desktop/src/lib/stacks/stackEndpoints.ts b/apps/desktop/src/lib/stacks/stackEndpoints.ts index 4a4a84228b3..416cf91725d 100644 --- a/apps/desktop/src/lib/stacks/stackEndpoints.ts +++ b/apps/desktop/src/lib/stacks/stackEndpoints.ts @@ -1,10 +1,16 @@ import { ConflictEntries, type ConflictEntriesObj } from "$lib/files/conflicts"; import { normalizeReferenceSubject } from "$lib/stacks/commitMovePlacement"; +import { + transformWorkspaceDetails, + workspaceStackDetailTags, + type WorkspaceDetails, +} from "$lib/stacks/headInfoAdapters"; import { createSelectByIds, createSelectNth } from "$lib/state/customSelectors"; import { invalidatesItem, invalidatesList, providesItem, + providesItems, providesList, ReduxTag, } from "$lib/state/tags"; @@ -20,7 +26,6 @@ import type { BackendEndpointBuilder } from "$lib/state/backendApi"; import type { AbsorptionTarget, CommitAbsorption, - StackDetails, BranchDetails, UpstreamCommit, Commit, @@ -39,6 +44,7 @@ import type { UncommitResult, InsertSide, RelativeTo, + RefInfo, } from "@gitbutler/but-sdk"; export type BranchParams = { @@ -140,10 +146,6 @@ export function normalizeCreateCommitOutcome(response: CommitCreateResult): Crea }; } -export function transformStacksResponse(response: Stack[]) { - return stackAdapter.addMany(stackAdapter.getInitialState(), response); -} - export function toCommitCreatePlacement(args: CreateCommitRequest): { relativeTo: RelativeTo; side: "above" | "below"; @@ -208,15 +210,15 @@ export const branchDetailsSelectors = branchDetailsAdapter.getSelectors(); export function buildStackEndpoints(build: BackendEndpointBuilder) { return { - stacks: build.query, { projectId: string; all?: boolean }>({ - extraOptions: { command: "stacks" }, - query: (args) => { - const filter = args.all ? "All" : undefined; - return { projectId: args.projectId, filter }; + workspaceDetails: build.query({ + extraOptions: { command: "head_info" }, + query: (args) => args, + providesTags: (result) => { + const stackIds = result ? workspaceStackDetailTags(result) : []; + return [providesList(ReduxTag.Stacks), ...providesItems(ReduxTag.StackDetails, stackIds)]; }, - providesTags: [providesList(ReduxTag.Stacks)], - transformResponse(response: Stack[]) { - return transformStacksResponse(response); + transformResponse(response: RefInfo) { + return transformWorkspaceDetails(response); }, }), createStack: build.mutation({ @@ -246,55 +248,10 @@ export function buildStackEndpoints(build: BackendEndpointBuilder) { // reload, however, so leaving it like this for now. // invalidatesTags: [invalidatesList(ReduxTag.Stacks)] }), - stackDetails: build.query< - { - stackInfo: StackDetails; - branchDetails: EntityState; - commits: EntityState; - upstreamCommits: EntityState; - }, - // TODO(single-branch): stackId is actually `stackId?` in the backend to be able to query details in single-branch mode. - // however, ideally all this goes away in favor of consuming `RefInfo` from the backend. - { projectId: string; stackId?: string } - >({ - extraOptions: { command: "stack_details" }, - query: (args) => args, - providesTags: (_result, _error, { stackId }) => [ - ...providesItem(ReduxTag.StackDetails, stackId || "undefined"), - ], - transformResponse(response: StackDetails) { - const branchDetailsEntity = branchDetailsAdapter.addMany( - branchDetailsAdapter.getInitialState(), - response.branchDetails, - ); - - // This is a list of all the commits across all branches in the stack. - // If you want to access the commits of a specific branch, use the - // `commits` property of the `BranchDetails` struct. - const commitsEntity = commitAdapter.addMany( - commitAdapter.getInitialState(), - response.branchDetails.flatMap((branch) => branch.commits), - ); - - // This is a list of all the upstream commits across all the branches in the stack. - // If you want to access the upstream commits of a specific branch, use the - // `upstreamCommits` property of the `BranchDetails` struct. - const upstreamCommitsEntity = upstreamCommitAdapter.addMany( - upstreamCommitAdapter.getInitialState(), - response.branchDetails.flatMap((branch) => branch.upstreamCommits), - ); - - return { - stackInfo: response, - branchDetails: branchDetailsEntity, - commits: commitsEntity, - upstreamCommits: upstreamCommitsEntity, - }; - }, - }), /** * Note: This is specifically for looking up branches outside of - * a stacking context. You almost certainly want `stackDetails` + * a stacking context. Stacked workspace branches should be read from + * the `head_info`-backed workspace details query. */ unstackedBranchDetails: build.query< { diff --git a/apps/desktop/src/lib/stacks/stackService.svelte.ts b/apps/desktop/src/lib/stacks/stackService.svelte.ts index 9cd4fe8b819..d1ad41a326e 100644 --- a/apps/desktop/src/lib/stacks/stackService.svelte.ts +++ b/apps/desktop/src/lib/stacks/stackService.svelte.ts @@ -1,6 +1,10 @@ import { getBranchNameFromRef } from "$lib/branches/branchUtils"; import { sortLikeFileTree } from "$lib/files/filetreeV3"; import { showToast } from "$lib/notifications/toasts"; +import { + selectWorkspaceStackById, + selectWorkspaceStackDetails, +} from "$lib/stacks/headInfoAdapters"; import { branchDetailsSelectors, changesSelectors, @@ -47,67 +51,60 @@ export class StackService { ) {} stacks(projectId: string) { - return this.backendApi.endpoints.stacks.useQuery( + return this.backendApi.endpoints.workspaceDetails.useQuery( { projectId }, { - transform: (stacks) => stackSelectors.selectAll(stacks), + transform: (workspaceDetails) => stackSelectors.selectAll(workspaceDetails.stacks), }, ); } async fetchStacks(projectId: string) { - return await this.backendApi.endpoints.stacks.fetch( + return await this.backendApi.endpoints.workspaceDetails.fetch( { projectId }, { - transform: (stacks) => stackSelectors.selectAll(stacks), + transform: (workspaceDetails) => stackSelectors.selectAll(workspaceDetails.stacks), }, ); } stackAt(projectId: string, index: number) { - return this.backendApi.endpoints.stacks.useQuery( + return this.backendApi.endpoints.workspaceDetails.useQuery( { projectId }, { - transform: (stacks) => stackSelectors.selectNth(stacks, index), + transform: (workspaceDetails) => stackSelectors.selectNth(workspaceDetails.stacks, index), }, ); } stackById(projectId: string, id: string) { - return this.backendApi.endpoints.stacks.useQuery( + return this.backendApi.endpoints.workspaceDetails.useQuery( { projectId }, { - transform: (stacks) => stackSelectors.selectById(stacks, id) ?? null, - }, - ); - } - - allStackById(projectId: string, id: string) { - return this.backendApi.endpoints.stacks.useQuery( - { projectId, all: true }, - { - transform: (stacks) => stackSelectors.selectById(stacks, id) ?? null, + transform: (workspaceDetails) => selectWorkspaceStackById(workspaceDetails, id) ?? null, }, ); } defaultBranch(projectId: string, stackId?: string) { if (!stackId) return null; - return this.backendApi.endpoints.stacks.useQuery( + return this.backendApi.endpoints.workspaceDetails.useQuery( { projectId }, { - transform: (stacks) => stackSelectors.selectById(stacks, stackId)?.heads[0]?.name ?? null, + transform: (workspaceDetails) => + selectWorkspaceStackById(workspaceDetails, stackId)?.heads[0]?.name ?? null, }, ); } branchDetails(projectId: string, stackId: string | undefined, branchName?: string) { - return this.backendApi.endpoints.stackDetails.useQuery( - { projectId, stackId }, + return this.backendApi.endpoints.workspaceDetails.useQuery( + { projectId }, { - transform: ({ branchDetails }) => { + transform: (workspaceDetails) => { + const details = selectWorkspaceStackDetails(workspaceDetails, stackId, branchName); return branchName - ? branchDetailsSelectors.selectById(branchDetails, branchName) + ? details && branchDetailsSelectors.selectById(details.branchDetails, branchName) : undefined; }, }, @@ -127,17 +124,23 @@ export class StackService { } branches(projectId: string, stackId?: string) { - return this.backendApi.endpoints.stackDetails.useQuery( - { projectId, stackId }, - { transform: ({ branchDetails }) => branchDetailsSelectors.selectAll(branchDetails) }, + return this.backendApi.endpoints.workspaceDetails.useQuery( + { projectId }, + { + transform: (workspaceDetails) => { + const details = selectWorkspaceStackDetails(workspaceDetails, stackId); + return details ? branchDetailsSelectors.selectAll(details.branchDetails) : []; + }, + }, ); } branchAt(projectId: string, stackId: string | undefined, index: number) { - return this.backendApi.endpoints.stackDetails.useQuery( - { projectId, stackId }, + return this.backendApi.endpoints.workspaceDetails.useQuery( + { projectId }, { - transform: ({ stackInfo }) => stackInfo.branchDetails[index], + transform: (workspaceDetails) => + selectWorkspaceStackDetails(workspaceDetails, stackId)?.stackInfo.branchDetails[index], }, ); } @@ -158,96 +161,137 @@ export class StackService { name: string, offset: number, ) { - return this.backendApi.endpoints.stackDetails.useQuery( - { projectId, stackId }, + return this.backendApi.endpoints.workspaceDetails.useQuery( + { projectId }, { - transform: ({ stackInfo, branchDetails }) => { - const names = stackInfo.branchDetails.map((branch) => branch.name); + transform: (workspaceDetails) => { + const details = selectWorkspaceStackDetails(workspaceDetails, stackId, name); + if (!details) return; + const names = details.stackInfo.branchDetails.map((branch) => branch.name); const index = names.indexOf(name); if (index === -1) return; const relativeName = names[index + offset]; if (!relativeName) return; - return branchDetailsSelectors.selectById(branchDetails, relativeName); + return branchDetailsSelectors.selectById(details.branchDetails, relativeName); }, }, ); } branchByName(projectId: string, stackId: string | undefined, name: string) { - return this.backendApi.endpoints.stackDetails.useQuery( - { projectId, stackId }, - { transform: ({ branchDetails }) => branchDetailsSelectors.selectById(branchDetails, name) }, + return this.backendApi.endpoints.workspaceDetails.useQuery( + { projectId }, + { + transform: (workspaceDetails) => { + const details = selectWorkspaceStackDetails(workspaceDetails, stackId, name); + return details && branchDetailsSelectors.selectById(details.branchDetails, name); + }, + }, ); } commits(projectId: string, stackId: string | undefined, branchName: string) { - return this.backendApi.endpoints.stackDetails.useQuery( - { projectId, stackId }, + return this.backendApi.endpoints.workspaceDetails.useQuery( + { projectId }, { - transform: ({ branchDetails }) => - branchDetailsSelectors.selectById(branchDetails, branchName)?.commits, + transform: (workspaceDetails) => { + const details = selectWorkspaceStackDetails(workspaceDetails, stackId, branchName); + return ( + details && branchDetailsSelectors.selectById(details.branchDetails, branchName)?.commits + ); + }, }, ); } fetchCommits(projectId: string, stackId: string | undefined, branchName: string) { - return this.backendApi.endpoints.stackDetails.fetch( - { projectId, stackId }, + return this.backendApi.endpoints.workspaceDetails.fetch( + { projectId }, { - transform: ({ branchDetails }) => - branchDetailsSelectors.selectById(branchDetails, branchName)?.commits, + transform: (workspaceDetails) => { + const details = selectWorkspaceStackDetails(workspaceDetails, stackId, branchName); + return ( + details && branchDetailsSelectors.selectById(details.branchDetails, branchName)?.commits + ); + }, }, ); } async fetchStackById(projectId: string, stackId: string) { - return await this.backendApi.endpoints.stacks.fetch( + return await this.backendApi.endpoints.workspaceDetails.fetch( { projectId }, { - transform: (stacks) => stackSelectors.selectById(stacks, stackId), + transform: (workspaceDetails) => selectWorkspaceStackById(workspaceDetails, stackId), }, ); } async fetchBranches(projectId: string, stackId: string) { - return await this.backendApi.endpoints.stackDetails.fetch( - { projectId, stackId }, + return await this.backendApi.endpoints.workspaceDetails.fetch( + { projectId }, { - transform: ({ branchDetails }) => branchDetailsSelectors.selectAll(branchDetails), + transform: (workspaceDetails) => { + const details = selectWorkspaceStackDetails(workspaceDetails, stackId); + return details ? branchDetailsSelectors.selectAll(details.branchDetails) : []; + }, }, ); } commitAt(projectId: string, stackId: string | undefined, branchName: string, index: number) { - return this.backendApi.endpoints.stackDetails.useQuery( - { projectId, stackId }, + return this.backendApi.endpoints.workspaceDetails.useQuery( + { projectId }, { - transform: ({ branchDetails }) => - branchDetailsSelectors.selectById(branchDetails, branchName)?.commits[index] ?? null, + transform: (workspaceDetails) => { + const details = selectWorkspaceStackDetails(workspaceDetails, stackId, branchName); + return ( + (details && + branchDetailsSelectors.selectById(details.branchDetails, branchName)?.commits[ + index + ]) ?? + null + ); + }, }, ); } allLocalCommits(projectId: string) { - const stacks = $derived(this.stacks(projectId)); - const stackIds = $derived(stacks.response?.map((s) => s.id).filter(isDefined) || []); - const args = $derived(stackIds?.map((stackId) => ({ projectId, stackId }))); const details = $derived( - this.backendApi.endpoints.stackDetails.useQueries(args, { - transform: ({ commits, stackInfo }) => ({ - commits: commitSelectors.selectAll(commits), - branches: stackInfo.branchDetails.map((b) => b.name), - baseCommitShas: stackInfo.branchDetails.map((b) => b.baseCommit), - stackInfo, - }), - }), + this.backendApi.endpoints.workspaceDetails.useQuery( + { projectId }, + { + transform: (workspaceDetails) => { + const stacks = stackSelectors.selectAll(workspaceDetails.stacks); + const stackIds = stacks.map((stack) => stack.id).filter(isDefined); + const detailsData = stackIds + .map((stackId) => workspaceDetails.stackDetails[stackId]) + .filter(isDefined); + return { + stackIds, + detailsData, + allCommits: detailsData.flatMap((details) => + commitSelectors.selectAll(details.commits), + ), + allBranches: detailsData.flatMap((details) => + details.stackInfo.branchDetails.map((branch) => branch.name), + ), + allBaseCommitShas: detailsData.flatMap((details) => + details.stackInfo.branchDetails.map((branch) => branch.baseCommit), + ), + }; + }, + }, + ), ); - const detailsData = $derived(details.current); - const allCommits = $derived(detailsData.flatMap((d) => d.data?.commits ?? [])); - const allBranches = $derived(detailsData.flatMap((d) => d.data?.branches ?? [])); - const allBaseCommitShas = $derived(detailsData.flatMap((d) => d.data?.baseCommitShas ?? [])); + const stackIds = $derived(details.response?.stackIds ?? []); + const detailsData = $derived(details.response?.detailsData ?? []); + const allCommits = $derived(details.response?.allCommits ?? []); + const allBranches = $derived(details.response?.allBranches ?? []); + const allBaseCommitShas = $derived(details.response?.allBaseCommitShas ?? []); $effect(() => { updateStaleProjectState( @@ -284,7 +328,7 @@ export class StackService { const nextSnapshot: Record = {}; stackIds.forEach((stackId, i) => { - const stackInfo = detailsData[i]?.data?.stackInfo; + const stackInfo = detailsData[i]?.stackInfo; if (!stackInfo) return; // Only run when the StackDetails object is actually new (different reference). // During a re-fetch, RTK Query keeps the cached data object unchanged while @@ -308,25 +352,33 @@ export class StackService { } commitById(projectId: string, stackId: string | undefined, commitId: string) { - return this.backendApi.endpoints.stackDetails.useQuery( - { projectId, stackId }, + return this.backendApi.endpoints.workspaceDetails.useQuery( + { projectId }, { - transform: ({ commits, upstreamCommits }) => - commitSelectors.selectById(commits, commitId) ?? - upstreamCommitSelectors.selectById(upstreamCommits, commitId), + transform: (workspaceDetails) => { + const details = selectWorkspaceStackDetails(workspaceDetails, stackId); + return ( + (details && + (commitSelectors.selectById(details.commits, commitId) ?? + upstreamCommitSelectors.selectById(details.upstreamCommits, commitId))) ?? + undefined + ); + }, }, ); } commitsByIds(projectId: string, stackId: string | undefined, commitIds: string[]) { - return this.backendApi.endpoints.stackDetails.useQuery( - { projectId, stackId }, + return this.backendApi.endpoints.workspaceDetails.useQuery( + { projectId }, { - transform: ({ commits, upstreamCommits }) => { + transform: (workspaceDetails) => { + const details = selectWorkspaceStackDetails(workspaceDetails, stackId); + if (!details) return []; const commitDetails = commitIds.map((id) => { return ( - commitSelectors.selectById(commits, id) ?? - upstreamCommitSelectors.selectById(upstreamCommits, id) + commitSelectors.selectById(details.commits, id) ?? + upstreamCommitSelectors.selectById(details.upstreamCommits, id) ); }); return commitDetails.filter(isDefined); @@ -336,25 +388,32 @@ export class StackService { } fetchCommitById(projectId: string, stackId: string, commitId: string) { - return this.backendApi.endpoints.stackDetails.fetch( - { projectId, stackId }, + return this.backendApi.endpoints.workspaceDetails.fetch( + { projectId }, { - transform: ({ commits, upstreamCommits }) => - commitSelectors.selectById(commits, commitId) ?? - upstreamCommitSelectors.selectById(upstreamCommits, commitId), + transform: (workspaceDetails) => { + const details = selectWorkspaceStackDetails(workspaceDetails, stackId); + return ( + details && + (commitSelectors.selectById(details.commits, commitId) ?? + upstreamCommitSelectors.selectById(details.upstreamCommits, commitId)) + ); + }, }, ); } fetchCommitsByIds(projectId: string, stackId: string, commitIds: string[]) { - return this.backendApi.endpoints.stackDetails.fetch( - { projectId, stackId }, + return this.backendApi.endpoints.workspaceDetails.fetch( + { projectId }, { - transform: ({ commits, upstreamCommits }) => { + transform: (workspaceDetails) => { + const details = selectWorkspaceStackDetails(workspaceDetails, stackId); + if (!details) return []; const commitDetails = commitIds.map((id) => { return ( - commitSelectors.selectById(commits, id) ?? - upstreamCommitSelectors.selectById(upstreamCommits, id) + commitSelectors.selectById(details.commits, id) ?? + upstreamCommitSelectors.selectById(details.upstreamCommits, id) ); }); return commitDetails.filter(isDefined); @@ -364,32 +423,46 @@ export class StackService { } upstreamCommits(projectId: string, stackId: string | undefined, branchName: string) { - return this.backendApi.endpoints.stackDetails.useQuery( - { projectId, stackId }, + return this.backendApi.endpoints.workspaceDetails.useQuery( + { projectId }, { - transform: ({ branchDetails }) => - branchDetailsSelectors.selectById(branchDetails, branchName)?.upstreamCommits, + transform: (workspaceDetails) => { + const details = selectWorkspaceStackDetails(workspaceDetails, stackId, branchName); + return ( + details && + branchDetailsSelectors.selectById(details.branchDetails, branchName)?.upstreamCommits + ); + }, }, ); } upstreamCommitAt(projectId: string, stackId: string, branchName: string, index: number) { - return this.backendApi.endpoints.stackDetails.useQuery( - { projectId, stackId }, + return this.backendApi.endpoints.workspaceDetails.useQuery( + { projectId }, { - transform: ({ branchDetails }) => - branchDetailsSelectors.selectById(branchDetails, branchName)?.upstreamCommits[index] ?? - null, + transform: (workspaceDetails) => { + const details = selectWorkspaceStackDetails(workspaceDetails, stackId, branchName); + return ( + (details && + branchDetailsSelectors.selectById(details.branchDetails, branchName)?.upstreamCommits[ + index + ]) ?? + null + ); + }, }, ); } fetchUpstreamCommitById(projectId: string, stackId: string, commitId: string) { - return this.backendApi.endpoints.stackDetails.fetch( - { projectId, stackId }, + return this.backendApi.endpoints.workspaceDetails.fetch( + { projectId }, { - transform: ({ upstreamCommits }) => - upstreamCommitSelectors.selectById(upstreamCommits, commitId), + transform: (workspaceDetails) => { + const details = selectWorkspaceStackDetails(workspaceDetails, stackId); + return details && upstreamCommitSelectors.selectById(details.upstreamCommits, commitId); + }, }, ); } @@ -741,11 +814,15 @@ export class StackService { stackId: string; branchName: string; }) { - const allCommits = await this.backendApi.endpoints.stackDetails.fetch( - { projectId, stackId }, + const allCommits = await this.backendApi.endpoints.workspaceDetails.fetch( + { projectId }, { - transform: ({ branchDetails }) => - branchDetailsSelectors.selectById(branchDetails, branchName)?.commits, + transform: (workspaceDetails) => { + const details = selectWorkspaceStackDetails(workspaceDetails, stackId, branchName); + return ( + details && branchDetailsSelectors.selectById(details.branchDetails, branchName)?.commits + ); + }, }, ); @@ -777,11 +854,13 @@ export class StackService { } isBranchConflicted(projectId: string, stackId: string, branchName: string) { - return this.backendApi.endpoints.stackDetails.useQuery( - { projectId, stackId }, + return this.backendApi.endpoints.workspaceDetails.useQuery( + { projectId }, { - transform: ({ branchDetails }) => { - const branch = branchDetailsSelectors.selectById(branchDetails, branchName); + transform: (workspaceDetails) => { + const details = selectWorkspaceStackDetails(workspaceDetails, stackId, branchName); + const branch = + details && branchDetailsSelectors.selectById(details.branchDetails, branchName); return branch?.isConflicted ?? false; }, }, diff --git a/apps/desktop/src/lib/telemetry/posthog.ts b/apps/desktop/src/lib/telemetry/posthog.ts index 00bb64084bd..fdf01d5282d 100644 --- a/apps/desktop/src/lib/telemetry/posthog.ts +++ b/apps/desktop/src/lib/telemetry/posthog.ts @@ -109,9 +109,7 @@ type EventDescription = { command: string; }; -const HIGH_VOLUME_EVENTS: EventDescription[] = [ - { name: "tauri_command", command: "stack_details" }, -]; +const HIGH_VOLUME_EVENTS: EventDescription[] = [{ name: "tauri_command", command: "head_info" }]; const MID_VOLUME_EVENTS: EventDescription[] = [ { name: "tauri_command", command: "get_base_branch_data" }, diff --git a/crates/gitbutler-tauri/permissions/default.toml b/crates/gitbutler-tauri/permissions/default.toml index a40936d86be..cfa07b6b333 100644 --- a/crates/gitbutler-tauri/permissions/default.toml +++ b/crates/gitbutler-tauri/permissions/default.toml @@ -186,8 +186,6 @@ commands.allow = [ "show_graph_svg", "show_in_finder", "snapshot_diff", - "stack_details", - "stacks", "stash_into_branch", "store_author_globally_if_unset", "store_github_enterprise_pat", diff --git a/crates/gitbutler-tauri/src/main.rs b/crates/gitbutler-tauri/src/main.rs index 73b7a97c4e3..1f7d5fe3316 100644 --- a/crates/gitbutler-tauri/src/main.rs +++ b/crates/gitbutler-tauri/src/main.rs @@ -405,8 +405,6 @@ fn main() -> anyhow::Result<()> { legacy::rules::tauri_update_workspace_rule::update_workspace_rule, legacy::rules::tauri_list_workspace_rules::list_workspace_rules, legacy::workspace::tauri_head_info::head_info, - legacy::workspace::tauri_stacks::stacks, - legacy::workspace::tauri_stack_details::stack_details, legacy::workspace::tauri_branch_details::branch_details, legacy::workspace::tauri_discard_worktree_changes::discard_worktree_changes, legacy::workspace::tauri_stash_into_branch::stash_into_branch, diff --git a/packages/but-sdk/src/test.ts b/packages/but-sdk/src/test.ts index 0d9142899ae..fcbdbd7017a 100644 --- a/packages/but-sdk/src/test.ts +++ b/packages/but-sdk/src/test.ts @@ -1,8 +1,8 @@ /* eslint-disable no-console */ -import { listProjectsStatelessNapi, stackDetailsNapi, stacksNapi } from "./generated/index.js"; +import { headInfo, listProjectsStateless } from "./generated/index.js"; async function main() { - const projects = await listProjectsStatelessNapi(); + const projects = await listProjectsStateless(); console.log(projects); if (projects.length === 0) { @@ -12,12 +12,8 @@ async function main() { const project = projects.at(0); if (!project) throw new Error("The world is wrong"); - const stacks = await stacksNapi(project.id, null); - for (const stack of stacks) { - const details = await stackDetailsNapi(project.id, stack.id); - console.log("This are the details for stack with id: " + stack.id); - console.log(details); - } + const info = await headInfo(project.id); + console.log(info); } main();