feat: add Git service for clone, pull, commit, push operations
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
import simpleGit, { SimpleGit } from "simple-git";
|
||||
import fs from "fs/promises";
|
||||
import { REPOS, type RepoName } from "../config/repos.js";
|
||||
import { getRepoPath, readConfig } from "./workspace.service.js";
|
||||
|
||||
function git(repoPath: string): SimpleGit {
|
||||
return simpleGit(repoPath);
|
||||
}
|
||||
|
||||
function getRepoDef(name: RepoName) {
|
||||
const def = REPOS.find((r) => r.name === name);
|
||||
if (!def) throw new Error(`Unknown repo: ${name}`);
|
||||
return def;
|
||||
}
|
||||
|
||||
export async function cloneRepo(
|
||||
repoUrl: string,
|
||||
targetDir: string,
|
||||
opts?: { depth?: number; branch?: string },
|
||||
) {
|
||||
const args: string[] = [];
|
||||
if (opts?.depth) args.push("--depth", String(opts.depth));
|
||||
if (opts?.branch) args.push("--branch", opts.branch);
|
||||
await simpleGit().clone(repoUrl, targetDir, args);
|
||||
}
|
||||
|
||||
export async function getRepoStatus(repoPath: string) {
|
||||
const g = git(repoPath);
|
||||
const status = await g.status();
|
||||
let ahead = 0;
|
||||
let behind = 0;
|
||||
try {
|
||||
const raw = await g.raw(["rev-list", "--left-right", "--count", `HEAD...@{u}`]);
|
||||
const parts = raw.trim().split(/\s+/);
|
||||
ahead = parseInt(parts[0], 10) || 0;
|
||||
behind = parseInt(parts[1], 10) || 0;
|
||||
} catch {
|
||||
// no upstream tracking set
|
||||
}
|
||||
return {
|
||||
modified: status.modified,
|
||||
staged: status.staged,
|
||||
untracked: status.not_added,
|
||||
ahead,
|
||||
behind,
|
||||
branch: status.current || "unknown",
|
||||
};
|
||||
}
|
||||
|
||||
export async function pull(repoPath: string) {
|
||||
const g = git(repoPath);
|
||||
return g.pull();
|
||||
}
|
||||
|
||||
export async function commitFiles(repoPath: string, message: string, files?: string[]) {
|
||||
const g = git(repoPath);
|
||||
if (files && files.length > 0) {
|
||||
await g.add(files);
|
||||
} else {
|
||||
await g.add("-A");
|
||||
}
|
||||
return g.commit(message);
|
||||
}
|
||||
|
||||
export async function push(repoPath: string) {
|
||||
const g = git(repoPath);
|
||||
return g.push();
|
||||
}
|
||||
|
||||
export async function fetchUpstream(repoPath: string): Promise<number> {
|
||||
const g = git(repoPath);
|
||||
await g.fetch("upstream");
|
||||
const branch = (await g.status()).current || "main";
|
||||
try {
|
||||
const raw = await g.raw(["rev-list", "--count", `HEAD..upstream/${branch}`]);
|
||||
return parseInt(raw.trim(), 10) || 0;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
export async function mergeUpstream(repoPath: string, branch: string) {
|
||||
const g = git(repoPath);
|
||||
return g.merge([`upstream/${branch}`]);
|
||||
}
|
||||
|
||||
export async function getDiff(repoPath: string, file?: string) {
|
||||
const g = git(repoPath);
|
||||
if (file) {
|
||||
return g.diff(["HEAD", "--", file]);
|
||||
}
|
||||
return g.diff(["HEAD"]);
|
||||
}
|
||||
|
||||
export async function setupClone(repoName: RepoName) {
|
||||
const config = await readConfig();
|
||||
const pat = config.githubPat;
|
||||
if (!pat) throw new Error("GitHub PAT not configured");
|
||||
|
||||
const repoDef = getRepoDef(repoName);
|
||||
const [upOwner, upRepo] = repoDef.upstream.split("/");
|
||||
|
||||
const { validatePat } = await import("./github.service.js");
|
||||
const { login } = await validatePat(pat);
|
||||
|
||||
const forkUrl = `https://${pat}@github.com/${login}/${upRepo}.git`;
|
||||
const upstreamUrl = `https://github.com/${upOwner}/${upRepo}.git`;
|
||||
const targetDir = getRepoPath(repoName);
|
||||
|
||||
try {
|
||||
await fs.access(targetDir);
|
||||
throw new Error(`Repo directory already exists: ${targetDir}`);
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Error && err.message.startsWith("Repo directory")) throw err;
|
||||
}
|
||||
|
||||
await cloneRepo(forkUrl, targetDir, { branch: repoDef.branch, depth: 1 });
|
||||
|
||||
const g = git(targetDir);
|
||||
await g.addRemote("upstream", upstreamUrl);
|
||||
await g.fetch("upstream");
|
||||
|
||||
return { path: targetDir, branch: repoDef.branch };
|
||||
}
|
||||
|
||||
export async function listReposWithStatus() {
|
||||
const results: Array<{
|
||||
name: string;
|
||||
branch: string;
|
||||
upstream: string;
|
||||
cloned: boolean;
|
||||
status?: Awaited<ReturnType<typeof getRepoStatus>>;
|
||||
}> = [];
|
||||
|
||||
for (const repo of REPOS) {
|
||||
const repoPath = getRepoPath(repo.name);
|
||||
let cloned = false;
|
||||
try {
|
||||
await fs.access(repoPath);
|
||||
cloned = true;
|
||||
} catch {
|
||||
// not cloned
|
||||
}
|
||||
|
||||
if (cloned) {
|
||||
try {
|
||||
const status = await getRepoStatus(repoPath);
|
||||
results.push({ name: repo.name, branch: repo.branch, upstream: repo.upstream, cloned, status });
|
||||
} catch {
|
||||
results.push({ name: repo.name, branch: repo.branch, upstream: repo.upstream, cloned });
|
||||
}
|
||||
} else {
|
||||
results.push({ name: repo.name, branch: repo.branch, upstream: repo.upstream, cloned });
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
Reference in New Issue
Block a user