/** Unified hook for git repository operations. * * Centralizes branch fetching, status, history, and git actions * so components don't duplicate this logic. */ import { useState, useEffect, useCallback } from "react"; import { listRepositoryBranches, getRepositoryStatus, getRepositoryHistory, getCommitDetail, commitChanges, pushRepository, pullRepository, fetchRepository, checkoutBranch, createBranch, deleteBranch, mergeBranches, type Branch, type CommitHistoryResponse, type CommitDetail, } from "../api/git_repositories"; export interface GitStatus { branch: string; modified: string[]; added: string[]; deleted: string[]; untracked: string[]; renamed: string[]; ahead: number; behind: number; } export interface UseGitRepoResult { /** Available branch names. */ branches: string[]; /** The repo's default branch. */ defaultBranch: string; /** Current working-directory status. */ status: GitStatus | null; /** Commit history. */ history: CommitHistoryResponse | null; /** Selected commit detail. */ commitDetail: CommitDetail | null; /** True while any async operation is in flight. */ loading: boolean; /** Error message from the last failed operation. */ error: string | null; /** Refresh branches list. */ refreshBranches: () => Promise; /** Refresh working-directory status. */ refreshStatus: () => Promise; /** Refresh commit history. */ refreshHistory: (branch?: string, limit?: number) => Promise; /** Fetch a single commit's details. */ loadCommitDetail: (hash: string) => Promise; /** Stage + commit changes. */ commit: (message: string, files?: string[]) => Promise; /** Push current branch (or named branch) to remote. */ push: (branch?: string) => Promise; /** Pull from remote. */ pull: (branch?: string) => Promise; /** Fetch from remote. */ fetch: () => Promise; /** Checkout an existing branch. */ checkout: (branch: string) => Promise; /** Create and checkout a new branch. */ createBranch: (name: string, baseBranch?: string) => Promise; /** Delete a branch. */ deleteBranch: (name: string, force?: boolean) => Promise; /** Merge source into current (or target) branch. */ merge: ( sourceBranch: string, targetBranch?: string, message?: string, ) => Promise; /** Clear the current error. */ clearError: () => void; } export function useGitRepo( projectId: string | undefined, repoId: string | undefined, ): UseGitRepoResult { const [branches, setBranches] = useState([]); const [defaultBranch, setDefaultBranch] = useState(""); const [status, setStatus] = useState(null); const [history, setHistory] = useState(null); const [commitDetail, setCommitDetail] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const extractError = (err: unknown): string => { if (typeof err === "object" && err !== null) { const e = err as Record; const response = e.response as Record | undefined; const data = response?.data as Record | undefined; if (typeof data?.detail === "string") return data.detail; if (typeof data?.message === "string") return data.message; if (typeof e.message === "string") return e.message; } return "Git operation failed"; }; const withLoading = useCallback( async (fn: () => Promise): Promise => { setLoading(true); setError(null); try { return await fn(); } catch (err) { setError(extractError(err)); throw err; } finally { setLoading(false); } }, [], ); const refreshBranches = useCallback(async () => { if (!projectId || !repoId) return; const data = await withLoading(() => listRepositoryBranches(projectId, repoId), ); setBranches(data.branches.map((b: Branch) => b.name)); setDefaultBranch(data.default_branch ?? ""); }, [projectId, repoId, withLoading]); const refreshStatus = useCallback(async () => { if (!projectId || !repoId) return; const data = await withLoading(() => getRepositoryStatus(projectId, repoId), ); setStatus({ branch: data.branch, modified: data.modified, added: data.added, deleted: data.deleted, untracked: data.untracked, renamed: data.renamed ?? [], ahead: data.ahead, behind: data.behind, }); }, [projectId, repoId, withLoading]); const refreshHistory = useCallback( async (branch?: string, limit = 50) => { if (!projectId || !repoId) return; const data = await withLoading(() => getRepositoryHistory(projectId, repoId, branch, limit), ); setHistory(data); }, [projectId, repoId, withLoading], ); const loadCommitDetail = useCallback( async (hash: string) => { if (!projectId || !repoId) return; const data = await withLoading(() => getCommitDetail(projectId, repoId, hash), ); setCommitDetail(data); }, [projectId, repoId, withLoading], ); const commit = useCallback( async (message: string, files?: string[]) => { if (!projectId || !repoId) return; await withLoading(() => commitChanges(projectId, repoId, message, files)); await refreshStatus(); }, [projectId, repoId, withLoading, refreshStatus], ); const push = useCallback( async (branch?: string) => { if (!projectId || !repoId) return; await withLoading(() => pushRepository(projectId, repoId, branch)); await refreshStatus(); }, [projectId, repoId, withLoading, refreshStatus], ); const pull = useCallback( async (branch?: string) => { if (!projectId || !repoId) return; await withLoading(() => pullRepository(projectId, repoId, branch)); await refreshStatus(); }, [projectId, repoId, withLoading, refreshStatus], ); const fetch = useCallback(async () => { if (!projectId || !repoId) return; await withLoading(() => fetchRepository(projectId, repoId)); await refreshStatus(); }, [projectId, repoId, withLoading, refreshStatus]); const checkout = useCallback( async (branch: string) => { if (!projectId || !repoId) return; await withLoading(() => checkoutBranch(projectId, repoId, branch)); await refreshStatus(); await refreshBranches(); }, [projectId, repoId, withLoading, refreshStatus, refreshBranches], ); const createBranchFn = useCallback( async (name: string, baseBranch = "HEAD") => { if (!projectId || !repoId) return; await withLoading(() => createBranch(projectId, repoId, name, baseBranch), ); await refreshBranches(); await refreshStatus(); }, [projectId, repoId, withLoading, refreshBranches, refreshStatus], ); const deleteBranchFn = useCallback( async (name: string, force = false) => { if (!projectId || !repoId) return; await withLoading(() => deleteBranch(projectId, repoId, name, force)); await refreshBranches(); }, [projectId, repoId, withLoading, refreshBranches], ); const merge = useCallback( async (sourceBranch: string, targetBranch?: string, message?: string) => { if (!projectId || !repoId) return; await withLoading(() => mergeBranches(projectId, repoId, sourceBranch, targetBranch, message), ); await refreshStatus(); await refreshHistory(); }, [projectId, repoId, withLoading, refreshStatus, refreshHistory], ); // Auto-refresh branches when projectId/repoId become valid useEffect(() => { if (projectId && repoId) { void refreshBranches(); } else { setBranches([]); setDefaultBranch(""); } }, [projectId, repoId, refreshBranches]); return { branches, defaultBranch, status, history, commitDetail, loading, error, refreshBranches, refreshStatus, refreshHistory, loadCommitDetail, commit, push, pull, fetch, checkout, createBranch: createBranchFn, deleteBranch: deleteBranchFn, merge, clearError: () => setError(null), }; }