mirror of
https://github.com/block/buzz.git
synced 2026-11-11 20:01:09 +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>
137 lines
4.2 KiB
TypeScript
137 lines
4.2 KiB
TypeScript
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
import * as React from "react";
|
|
import { toast } from "sonner";
|
|
|
|
import type { Repository as Project } from "@/features/projects/hooks";
|
|
import {
|
|
createProjectRemoteBranch,
|
|
deleteProjectRemoteBranch,
|
|
} from "@/shared/api/projectGit";
|
|
|
|
/** Creates a remote branch from an observed branch commit. */
|
|
export function useCreateProjectRemoteBranchMutation(
|
|
project: Project | null | undefined,
|
|
) {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (input: {
|
|
sourceBranch: string;
|
|
expectedCommit: string;
|
|
newBranch: string;
|
|
}) => {
|
|
if (!project?.cloneUrls[0]) throw new Error("No project selected.");
|
|
return createProjectRemoteBranch({
|
|
cloneUrl: project.cloneUrls[0],
|
|
...input,
|
|
});
|
|
},
|
|
onSuccess: () =>
|
|
queryClient.invalidateQueries({
|
|
queryKey: ["project", project?.id ?? "none"],
|
|
}),
|
|
});
|
|
}
|
|
|
|
/** Deletes a remote branch only if it still points at the observed commit. */
|
|
export function useDeleteProjectRemoteBranchMutation(
|
|
project: Project | null | undefined,
|
|
) {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (input: { branch: string; expectedCommit: string }) => {
|
|
if (!project?.cloneUrls[0]) throw new Error("No project selected.");
|
|
return deleteProjectRemoteBranch({
|
|
cloneUrl: project.cloneUrls[0],
|
|
...input,
|
|
});
|
|
},
|
|
onSuccess: () =>
|
|
queryClient.invalidateQueries({
|
|
queryKey: ["project", project?.id ?? "none"],
|
|
}),
|
|
});
|
|
}
|
|
|
|
/** Coordinates branch dialogs and refreshes around the remote mutations. */
|
|
export function useProjectBranchActions(input: {
|
|
project: Project | null | undefined;
|
|
activeBranch: string | null;
|
|
activeBranchCommit: string | null;
|
|
activeRemoteBranch: { name: string; commit: string } | null;
|
|
defaultBranch: string | null;
|
|
deleteBranchReason: string | null;
|
|
refetchRepoState: () => Promise<unknown>;
|
|
rememberBranch: (branch: { name: string; commit: string }) => void;
|
|
forgetBranch: (branch: string) => void;
|
|
selectBranch: (branch: string | null) => void;
|
|
}) {
|
|
const [createOpen, setCreateOpen] = React.useState(false);
|
|
const [deleteOpen, setDeleteOpen] = React.useState(false);
|
|
const createMutation = useCreateProjectRemoteBranchMutation(input.project);
|
|
const deleteMutation = useDeleteProjectRemoteBranchMutation(input.project);
|
|
const createBranch = createMutation.mutateAsync;
|
|
const deleteBranch = deleteMutation.mutateAsync;
|
|
|
|
const handleCreate = React.useCallback(
|
|
async (newBranch: string) => {
|
|
if (!input.activeBranch || !input.activeBranchCommit) {
|
|
throw new Error("Refresh the source branch before creating a branch.");
|
|
}
|
|
const result = await createBranch({
|
|
sourceBranch: input.activeBranch,
|
|
expectedCommit: input.activeBranchCommit,
|
|
newBranch,
|
|
});
|
|
await input.refetchRepoState();
|
|
input.rememberBranch({ name: result.branch, commit: result.commit });
|
|
input.selectBranch(result.branch);
|
|
toast.success(result.message);
|
|
},
|
|
[
|
|
createBranch,
|
|
input.activeBranch,
|
|
input.activeBranchCommit,
|
|
input.refetchRepoState,
|
|
input.rememberBranch,
|
|
input.selectBranch,
|
|
],
|
|
);
|
|
const handleDelete = React.useCallback(async () => {
|
|
if (
|
|
!input.activeBranch ||
|
|
!input.activeRemoteBranch ||
|
|
input.deleteBranchReason
|
|
) {
|
|
throw new Error(input.deleteBranchReason ?? "Choose a remote branch.");
|
|
}
|
|
const result = await deleteBranch({
|
|
branch: input.activeBranch,
|
|
expectedCommit: input.activeRemoteBranch.commit,
|
|
});
|
|
input.forgetBranch(result.branch);
|
|
input.selectBranch(input.defaultBranch);
|
|
await input.refetchRepoState();
|
|
toast.success(result.message);
|
|
}, [
|
|
deleteBranch,
|
|
input.activeBranch,
|
|
input.activeRemoteBranch,
|
|
input.defaultBranch,
|
|
input.deleteBranchReason,
|
|
input.forgetBranch,
|
|
input.refetchRepoState,
|
|
input.selectBranch,
|
|
]);
|
|
|
|
return {
|
|
createOpen,
|
|
createPending: createMutation.isPending,
|
|
deleteOpen,
|
|
deletePending: deleteMutation.isPending,
|
|
handleCreate,
|
|
handleDelete,
|
|
setCreateOpen,
|
|
setDeleteOpen,
|
|
};
|
|
}
|