Files
headquarter/apps/web/src/hooks/use-git-repo.ts
T
alex 020f832eed refactor: rename frontend API files to kebab-case
Renamed:
- git_repositories.ts → git-repositories.ts
- ssh_keys.ts → ssh-keys.ts
- tool_types.ts → tool-types.ts
- tool_types.test.ts → tool-types.test.ts

Updated all imports across components, pages, and hooks.

Quality gates: verified no remaining old imports.
2026-06-04 12:33:10 +02:00

281 lines
7.8 KiB
TypeScript

/** 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<void>;
/** Refresh working-directory status. */
refreshStatus: () => Promise<void>;
/** Refresh commit history. */
refreshHistory: (branch?: string, limit?: number) => Promise<void>;
/** Fetch a single commit's details. */
loadCommitDetail: (hash: string) => Promise<void>;
/** Stage + commit changes. */
commit: (message: string, files?: string[]) => Promise<void>;
/** Push current branch (or named branch) to remote. */
push: (branch?: string) => Promise<void>;
/** Pull from remote. */
pull: (branch?: string) => Promise<void>;
/** Fetch from remote. */
fetch: () => Promise<void>;
/** Checkout an existing branch. */
checkout: (branch: string) => Promise<void>;
/** Create and checkout a new branch. */
createBranch: (name: string, baseBranch?: string) => Promise<void>;
/** Delete a branch. */
deleteBranch: (name: string, force?: boolean) => Promise<void>;
/** Merge source into current (or target) branch. */
merge: (
sourceBranch: string,
targetBranch?: string,
message?: string,
) => Promise<void>;
/** Clear the current error. */
clearError: () => void;
}
export function useGitRepo(
projectId: string | undefined,
repoId: string | undefined,
): UseGitRepoResult {
const [branches, setBranches] = useState<string[]>([]);
const [defaultBranch, setDefaultBranch] = useState("");
const [status, setStatus] = useState<GitStatus | null>(null);
const [history, setHistory] = useState<CommitHistoryResponse | null>(null);
const [commitDetail, setCommitDetail] = useState<CommitDetail | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const extractError = (err: unknown): string => {
if (typeof err === "object" && err !== null) {
const e = err as Record<string, unknown>;
const response = e.response as Record<string, unknown> | undefined;
const data = response?.data as Record<string, unknown> | 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 <T>(fn: () => Promise<T>): Promise<T> => {
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),
};
}