Agents for developpement added + frontend add + backend added. Git viewer created + agent and template creator + layout and project creator
168 lines
4.3 KiB
TypeScript
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,
|
|
};
|
|
}
|