Files
buzz/desktop/src/features/projects/useProjectsRepoSnapshots.ts
thomaspblock e30db7028f feat(projects): support multiple repositories (#4671)
## Summary
- adopt the finalized NIP-MP project model so one project can enumerate
and switch between multiple NIP-34 repositories
- add project and repository navigation, activity summaries,
existing-repository attachment, and repository access-channel management
- preserve privacy-safe activation provenance for agent-authored
patches, pull requests, issues, and associated commits

## Test plan
- [x] Run desktop typecheck and unit tests
- [x] Run focused NIP-MP, repository access, and provenance tests
- [x] Run Rust formatting and desktop lint checks
- [x] Run the complete pre-push suite after merging current `main`
- [ ] Manually verify project creation, repository attachment,
switching, and access repair on staging
- [ ] Manually verify public-channel and private-agent origin labels on
newly created Git activity

Related: [#4695](https://github.com/block/buzz/pull/4695)

---------

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-04 17:30:12 -04:00

117 lines
3.7 KiB
TypeScript

import { useQuery } from "@tanstack/react-query";
import * as React from "react";
import {
getProjectLocalRepoSnapshot,
getProjectRepoSnapshot,
} from "@/shared/api/projectGit";
import type { ProjectRepoSnapshot } from "@/shared/api/types";
import type { Project } from "./hooks";
import { selectProjectRepository } from "./projectModels";
import {
type ProjectRepoUnavailableReason,
projectRepoUnavailableReason,
} from "./lib/projectRepoAvailability";
// Remote snapshots are backed by a blobless `git clone` per repository, so the
// overview scan is deliberately throttled and cached for a long time.
const OVERVIEW_SNAPSHOT_CONCURRENCY = 3;
function snapshotHasData(snapshot: ProjectRepoSnapshot | null | undefined) {
return Boolean(
snapshot && (snapshot.files.length > 0 || snapshot.latestCommit),
);
}
/**
* Local checkouts are instant (no network, no clone) and keep working when
* the relay's git storage is empty or unreachable, so they are preferred.
* Only repositories without usable local data fall back to a remote clone.
*/
async function fetchProjectSnapshot(
project: Project,
reposDir: string | null | undefined,
): Promise<ProjectRepoSnapshot | null> {
const repository = selectProjectRepository(project, null);
if (!repository) return null;
try {
const local = await getProjectLocalRepoSnapshot({
reposDir,
projectDtag: repository.dtag,
cloneUrl: repository.cloneUrls[0] ?? null,
defaultBranch: repository.defaultBranch,
baseBranch: repository.defaultBranch,
});
if (snapshotHasData(local?.snapshot)) return local?.snapshot ?? null;
} catch {
// Best-effort: fall through to the remote snapshot.
}
const cloneUrl = repository.cloneUrls[0];
if (!cloneUrl) return null;
return getProjectRepoSnapshot({
cloneUrl,
defaultBranch: repository.defaultBranch,
baseBranch: repository.defaultBranch,
});
}
async function fetchProjectsRepoSnapshots(
projects: Project[],
reposDir: string | null | undefined,
): Promise<{
snapshots: Record<string, ProjectRepoSnapshot>;
unavailable: Record<string, ProjectRepoUnavailableReason>;
}> {
const snapshots: Record<string, ProjectRepoSnapshot> = {};
const unavailable: Record<string, ProjectRepoUnavailableReason> = {};
const queue = [...projects];
const workers = Array.from(
{ length: Math.min(OVERVIEW_SNAPSHOT_CONCURRENCY, queue.length) },
async () => {
for (;;) {
const project = queue.shift();
if (!project) return;
try {
const snapshot = await fetchProjectSnapshot(project, reposDir);
if (snapshot) {
snapshots[project.id] = snapshot;
} else {
unavailable[project.id] = "missing";
}
} catch (error) {
unavailable[project.id] = projectRepoUnavailableReason(error);
}
}
},
);
await Promise.all(workers);
return { snapshots, unavailable };
}
/**
* Fetches repo snapshots for a set of projects (throttled, failure-tolerant)
* for community-wide aggregates like the overview language breakdown.
* Prefers local checkouts under `reposDir`; falls back to remote clones.
* Callers should pre-filter and cap `projects` — up to one git clone per entry.
*/
export function useProjectsRepoSnapshotsQuery(
projects: Project[],
reposDir?: string | null,
) {
const projectIds = React.useMemo(
() => projects.map((project) => project.id).sort(),
[projects],
);
return useQuery({
enabled: projects.length > 0,
queryKey: ["projects", "repo-snapshots", reposDir ?? "default", projectIds],
queryFn: () => fetchProjectsRepoSnapshots(projects, reposDir),
staleTime: 15 * 60_000,
retry: 0,
});
}