mirror of
https://github.com/block/buzz.git
synced 2026-11-01 15:01:12 +00:00
## 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>
117 lines
3.7 KiB
TypeScript
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,
|
|
});
|
|
}
|