Files
IdeaSDK/frontend/src/features/git/useGit.ts
Blomios 307ae71857 feat: add main features
Agents for developpement added + frontend add + backend added. Git viewer created + agent and template creator + layout and project creator
2026-06-06 01:27:01 +02:00

168 lines
4.3 KiB
TypeScript

/**
* `useGit` — view-model hook for the Git feature (L8).
*
* Owns the git feature state for a given project and exposes the actions the
* UI triggers. Consumes {@link GitGateway} exclusively through `useGateways().git`;
* never touches `invoke()` or `@tauri-apps/api`.
*/
import { useCallback, useEffect, useState } from "react";
import type { GitBranches, GitCommit, GitFileStatus, GatewayError } from "@/domain";
import { useGateways } from "@/app/di";
/** What the Git UI needs from this hook. */
export interface GitViewModel {
/** Files in the working tree (staged + unstaged). */
files: GitFileStatus[];
/** Branch info: list + current. */
branches: GitBranches;
/** Recent commit log. */
log: GitCommit[];
/** Whether a request is in flight. */
busy: boolean;
/** Last error message, or `null`. */
error: string | null;
/** Refresh all git state. */
refresh: () => Promise<void>;
/** Stage a file. */
stage: (path: string) => Promise<void>;
/** Unstage a file. */
unstage: (path: string) => Promise<void>;
/** Commit staged files with the given message. */
commit: (message: string) => Promise<void>;
/** Checkout (or create) a branch. */
checkout: (branch: string) => Promise<void>;
}
function describeError(e: unknown): string {
if (e && typeof e === "object" && "message" in e) {
return String((e as GatewayError).message);
}
return String(e);
}
const EMPTY_BRANCHES: GitBranches = { branches: [], current: null };
const LOG_LIMIT = 20;
export function useGit(projectId: string): GitViewModel {
const { git } = useGateways();
const [files, setFiles] = useState<GitFileStatus[]>([]);
const [branches, setBranches] = useState<GitBranches>(EMPTY_BRANCHES);
const [log, setLog] = useState<GitCommit[]>([]);
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
const refresh = useCallback(async () => {
setBusy(true);
setError(null);
try {
const [fileList, branchInfo, commitLog] = await Promise.all([
git.status(projectId),
git.branches(projectId),
git.log(projectId, LOG_LIMIT),
]);
setFiles(fileList);
setBranches(branchInfo);
setLog(commitLog);
} catch (e) {
setError(describeError(e));
} finally {
setBusy(false);
}
}, [git, projectId]);
useEffect(() => {
void refresh();
}, [refresh]);
const stage = useCallback(
async (path: string) => {
setBusy(true);
setError(null);
try {
await git.stage(projectId, path);
setFiles((prev) =>
prev.map((f) => (f.path === path ? { ...f, staged: true } : f)),
);
} catch (e) {
setError(describeError(e));
} finally {
setBusy(false);
}
},
[git, projectId],
);
const unstage = useCallback(
async (path: string) => {
setBusy(true);
setError(null);
try {
await git.unstage(projectId, path);
setFiles((prev) =>
prev.map((f) => (f.path === path ? { ...f, staged: false } : f)),
);
} catch (e) {
setError(describeError(e));
} finally {
setBusy(false);
}
},
[git, projectId],
);
const commit = useCallback(
async (message: string) => {
setBusy(true);
setError(null);
try {
const newCommit = await git.commit(projectId, message);
// Remove committed (staged) files from the list.
setFiles((prev) => prev.filter((f) => !f.staged));
setLog((prev) => [newCommit, ...prev].slice(0, LOG_LIMIT));
} catch (e) {
setError(describeError(e));
} finally {
setBusy(false);
}
},
[git, projectId],
);
const checkout = useCallback(
async (branch: string) => {
setBusy(true);
setError(null);
try {
await git.checkout(projectId, branch);
setBranches((prev) => ({
branches: prev.branches.includes(branch)
? prev.branches
: [...prev.branches, branch],
current: branch,
}));
} catch (e) {
setError(describeError(e));
} finally {
setBusy(false);
}
},
[git, projectId],
);
return {
files,
branches,
log,
busy,
error,
refresh,
stage,
unstage,
commit,
checkout,
};
}