mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-05 16:04:43 +00:00
* refactor(forge): forge-neutral foundation (GitHub-only) Decouple git-hosting from GitHub behind a neutral abstraction (issue #1616), GitHub-only for now; existing GitHub behaviour is unchanged. - Forge manifest, neutral ForgeService contract, forge registry + resolver, and a client forge-module registry. - GitHub code renamed to the neutral shape; PR/Issue attachment wording preserved. - forge.search.response enums parse tolerantly (unknown kind/auth state degrade instead of breaking the client). - createPullRequest reports typed CLI/auth errors instead of a generic message. - forge-resolver host/remote caches are LRU-bounded. - Forge host trust is explicit: only a known cloud host or a CLI-authenticated host is ever talked to; an unauthenticated GitHub Enterprise host fails resolution instead of routing to github.com. - Docs: forge-providers guide, glossary and i18n forge-copy conventions, architecture and rpc-namespacing terminology. - Vitest React Native mocks (unistyles, svg, linking, lucide) consolidated into shared aliased test-stubs. * feat(forge): GitLab adapter, forge-aware UI, pipelines and approvals GitLab adapter over the glab CLI on the neutral contracts: MR status, forge-aware UI, pipeline tree, and N-of-M approvals. - threadIsResolved is part of the neutral timeline item. - Pipeline load failures show an error instead of an empty section. - Manual pipeline jobs render as pending. - Fork/detached MR head pipelines are fetched by MR iid (glab ci get --merge-request). * feat(forge): Gitea family adapter (Gitea, Forgejo, Codeberg) One adapter over the tea CLI serving Gitea, Forgejo, and Codeberg on the neutral contracts. - CI status aggregates commit statuses and Actions runs together. - Gitea's terminal "warning" state maps to failure on server and client. - Gitea Actions check details are reachable from the PR pane by workflowRunId. * refactor(forge): localize compatibility handling * test(forge): expect normalized GitLab facts --------- Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
91 lines
3.0 KiB
TypeScript
91 lines
3.0 KiB
TypeScript
import type { QueryClient } from "@tanstack/react-query";
|
|
import type { CheckoutStatusResponse, CheckoutStatusUpdate } from "@getpaseo/protocol/messages";
|
|
import equal from "fast-deep-equal/es6";
|
|
import {
|
|
checkoutPrStatusQueryKey,
|
|
checkoutStatusQueryKey,
|
|
invalidatePrPaneTimelineForCheckout,
|
|
} from "@/git/query-keys";
|
|
import { type CheckoutPrStatusPayload, normalizeCheckoutPrStatusPayload } from "@/git/pr-status";
|
|
import { expireStaleDiffModeOverrides } from "@/review/store";
|
|
|
|
export type CheckoutStatusPayload = CheckoutStatusResponse["payload"];
|
|
export type { CheckoutPrStatusPayload } from "@/git/pr-status";
|
|
|
|
export interface CheckoutStatusClient {
|
|
getCheckoutStatus: (cwd: string) => Promise<CheckoutStatusPayload>;
|
|
}
|
|
|
|
// Checkout status enters the app through exactly two doors: daemon pushes
|
|
// (applyCheckoutStatusUpdateFromEvent) and query fetches (fetchCheckoutStatus). Both run
|
|
// the dirty-state reactions, so they hold regardless of which screens are mounted.
|
|
|
|
export async function fetchCheckoutStatus({
|
|
client,
|
|
serverId,
|
|
cwd,
|
|
}: {
|
|
client: CheckoutStatusClient;
|
|
serverId: string;
|
|
cwd: string;
|
|
}): Promise<CheckoutStatusPayload> {
|
|
const payload = await client.getCheckoutStatus(cwd);
|
|
expireStaleDiffModeOverrides({ serverId, cwd, isDirty: payload.isGit && payload.isDirty });
|
|
return payload;
|
|
}
|
|
|
|
export function applyCheckoutStatusUpdateFromEvent({
|
|
queryClient,
|
|
serverId,
|
|
message,
|
|
}: {
|
|
queryClient: QueryClient;
|
|
serverId: string;
|
|
message: CheckoutStatusUpdate;
|
|
}): void {
|
|
const { payload } = message;
|
|
const prStatus = payload.prStatus
|
|
? normalizeCheckoutPrStatusPayload(payload.prStatus)
|
|
: undefined;
|
|
const cachePayload = prStatus ? { ...payload, prStatus } : payload;
|
|
queryClient.setQueryData(checkoutStatusQueryKey(serverId, payload.cwd), cachePayload);
|
|
expireStaleDiffModeOverrides({
|
|
serverId,
|
|
cwd: payload.cwd,
|
|
isDirty: payload.isGit && payload.isDirty,
|
|
});
|
|
|
|
if (!prStatus) {
|
|
return;
|
|
}
|
|
|
|
const previous = queryClient.getQueryData<CheckoutPrStatusPayload>(
|
|
checkoutPrStatusQueryKey(serverId, prStatus.cwd),
|
|
);
|
|
queryClient.setQueryData(checkoutPrStatusQueryKey(serverId, prStatus.cwd), prStatus);
|
|
|
|
// The PR activity timeline has no push channel; mark it stale when the pushed PR status
|
|
// meaningfully changed. Active panes refetch immediately, evicted ones on next mount.
|
|
if (hasPrStatusChanged(previous, prStatus)) {
|
|
void invalidatePrPaneTimelineForCheckout(queryClient, { serverId, cwd: prStatus.cwd });
|
|
}
|
|
}
|
|
|
|
// requestId changes on every emission and carries no PR state.
|
|
function prStatusWithoutVolatileFields(
|
|
prStatus: CheckoutPrStatusPayload,
|
|
): Omit<CheckoutPrStatusPayload, "requestId"> {
|
|
const { requestId: _requestId, ...rest } = prStatus;
|
|
return rest;
|
|
}
|
|
|
|
function hasPrStatusChanged(
|
|
previous: CheckoutPrStatusPayload | undefined,
|
|
next: CheckoutPrStatusPayload,
|
|
): boolean {
|
|
if (!previous) {
|
|
return true;
|
|
}
|
|
return !equal(prStatusWithoutVolatileFields(previous), prStatusWithoutVolatileFields(next));
|
|
}
|