import { useQuery } from "@tanstack/react-query"; import { invoke } from "@tauri-apps/api/core"; import { createFastMutation } from "@yaakapp/app/hooks/useFastMutation"; import { queryClient } from "@yaakapp/app/lib/queryClient"; import { useMemo } from "react"; import { BranchDeleteResult, CloneResult, GitCommit, GitRemote, GitStatusSummary, PullResult, PushResult, } from "./bindings/gen_git"; import { showToast } from "@yaakapp/app/lib/toast"; export * from "./bindings/gen_git"; export * from "./bindings/gen_models"; export interface GitCredentials { username: string; password: string; } export type DivergedStrategy = "force_reset" | "merge" | "cancel"; export type UncommittedChangesStrategy = "reset" | "cancel"; export interface GitCallbacks { addRemote: () => Promise; promptCredentials: ( result: Extract, ) => Promise; promptDiverged: (result: Extract) => Promise; promptUncommittedChanges: () => Promise; forceSync: () => Promise; } const onSuccess = () => queryClient.invalidateQueries({ queryKey: ["git"] }); export function useGit(dir: string, callbacks: GitCallbacks, refreshKey?: string) { const mutations = useMemo(() => gitMutations(dir, callbacks), [dir, callbacks]); const fetchAll = useQuery({ queryKey: ["git", "fetch_all", dir, refreshKey], queryFn: () => invoke("cmd_git_fetch_all", { dir }), refetchInterval: 10 * 60_000, }); return [ { remotes: useQuery({ queryKey: ["git", "remotes", dir, refreshKey], queryFn: () => getRemotes(dir), placeholderData: (prev) => prev, }), log: useQuery({ queryKey: ["git", "log", dir, refreshKey], queryFn: () => invoke("cmd_git_log", { dir }), placeholderData: (prev) => prev, }), status: useQuery({ refetchOnMount: true, queryKey: ["git", "status", dir, refreshKey, fetchAll.dataUpdatedAt], queryFn: () => invoke("cmd_git_status", { dir }), placeholderData: (prev) => prev, }), }, mutations, ] as const; } export const gitMutations = (dir: string, callbacks: GitCallbacks) => { const push = async () => { const remotes = await getRemotes(dir); if (remotes.length === 0) { const remote = await callbacks.addRemote(); if (remote == null) throw new Error("No remote found"); } const result = await invoke("cmd_git_push", { dir }); if (result.type !== "needs_credentials") return result; // Needs credentials, prompt for them const creds = await callbacks.promptCredentials(result); if (creds == null) throw new Error("Canceled"); await invoke("cmd_git_add_credential", { remoteUrl: result.url, username: creds.username, password: creds.password, }); // Push again return invoke("cmd_git_push", { dir }); }; const handleError = (err: unknown) => { showToast({ id: err instanceof Error ? err.message : String(err), message: err instanceof Error ? err.message : String(err), color: "danger", timeout: 5000, }); }; return { init: createFastMutation({ mutationKey: ["git", "init"], mutationFn: () => invoke("cmd_git_initialize", { dir }), onSuccess, }), add: createFastMutation({ mutationKey: ["git", "add", dir], mutationFn: (args) => invoke("cmd_git_add", { dir, ...args }), onSuccess, }), addRemote: createFastMutation({ mutationKey: ["git", "add-remote"], mutationFn: (args) => invoke("cmd_git_add_remote", { dir, ...args }), onSuccess, }), rmRemote: createFastMutation({ mutationKey: ["git", "rm-remote", dir], mutationFn: (args) => invoke("cmd_git_rm_remote", { dir, ...args }), onSuccess, }), createBranch: createFastMutation({ mutationKey: ["git", "branch", dir], mutationFn: (args) => invoke("cmd_git_branch", { dir, ...args }), onSuccess, }), mergeBranch: createFastMutation({ mutationKey: ["git", "merge", dir], mutationFn: (args) => invoke("cmd_git_merge_branch", { dir, ...args }), onSuccess, }), deleteBranch: createFastMutation< BranchDeleteResult, string, { branch: string; force?: boolean } >({ mutationKey: ["git", "delete-branch", dir], mutationFn: (args) => invoke("cmd_git_delete_branch", { dir, ...args }), onSuccess, }), deleteRemoteBranch: createFastMutation({ mutationKey: ["git", "delete-remote-branch", dir], mutationFn: (args) => invoke("cmd_git_delete_remote_branch", { dir, ...args }), onSuccess, }), renameBranch: createFastMutation({ mutationKey: ["git", "rename-branch", dir], mutationFn: (args) => invoke("cmd_git_rename_branch", { dir, ...args }), onSuccess, }), checkout: createFastMutation({ mutationKey: ["git", "checkout", dir], mutationFn: (args) => invoke("cmd_git_checkout", { dir, ...args }), onSuccess, }), commit: createFastMutation({ mutationKey: ["git", "commit", dir], mutationFn: (args) => invoke("cmd_git_commit", { dir, ...args }), onSuccess, }), commitAndPush: createFastMutation({ mutationKey: ["git", "commit_push", dir], mutationFn: async (args) => { await invoke("cmd_git_commit", { dir, ...args }); return push(); }, onSuccess, }), push: createFastMutation({ mutationKey: ["git", "push", dir], mutationFn: push, onSuccess, }), pull: createFastMutation({ mutationKey: ["git", "pull", dir], async mutationFn() { const result = await invoke("cmd_git_pull", { dir }); if (result.type === "needs_credentials") { const creds = await callbacks.promptCredentials(result); if (creds == null) throw new Error("Canceled"); await invoke("cmd_git_add_credential", { remoteUrl: result.url, username: creds.username, password: creds.password, }); // Pull again after credentials return invoke("cmd_git_pull", { dir }); } if (result.type === "uncommitted_changes") { void callbacks .promptUncommittedChanges() .then(async (strategy) => { if (strategy === "cancel") return; await invoke("cmd_git_reset_changes", { dir }); return invoke("cmd_git_pull", { dir }); }) .then(async () => { await onSuccess(); await callbacks.forceSync(); }, handleError); } if (result.type === "diverged") { void callbacks .promptDiverged(result) .then((strategy) => { if (strategy === "cancel") return; if (strategy === "force_reset") { return invoke("cmd_git_pull_force_reset", { dir, remote: result.remote, branch: result.branch, }); } return invoke("cmd_git_pull_merge", { dir, remote: result.remote, branch: result.branch, }); }) .then(async () => { await onSuccess(); await callbacks.forceSync(); }, handleError); } return result; }, onSuccess, }), unstage: createFastMutation({ mutationKey: ["git", "unstage", dir], mutationFn: (args) => invoke("cmd_git_unstage", { dir, ...args }), onSuccess, }), resetChanges: createFastMutation({ mutationKey: ["git", "reset-changes", dir], mutationFn: () => invoke("cmd_git_reset_changes", { dir }), onSuccess, }), } as const; }; async function getRemotes(dir: string) { return invoke("cmd_git_remotes", { dir }); } /** * Clone a git repository, prompting for credentials if needed. */ export async function gitClone( url: string, dir: string, promptCredentials: (args: { url: string; error: string | null; }) => Promise, ): Promise { const result = await invoke("cmd_git_clone", { url, dir }); if (result.type !== "needs_credentials") return result; // Prompt for credentials const creds = await promptCredentials({ url: result.url, error: result.error }); if (creds == null) return { type: "cancelled" }; // Store credentials and retry await invoke("cmd_git_add_credential", { remoteUrl: result.url, username: creds.username, password: creds.password, }); return invoke("cmd_git_clone", { url, dir }); }