1
0
Fork 0
mirror of https://gitbruv.vercel.app/api/git/bruv/gitbruv.git synced 2025-12-20 23:24:09 +01:00
gitbruv/actions/repositories.ts
2025-12-20 04:23:17 +00:00

303 lines
7.5 KiB
TypeScript

"use server";
import { db } from "@/db";
import { repositories, users } from "@/db/schema";
import { getSession } from "@/lib/session";
import { eq, and, desc } from "drizzle-orm";
import { revalidatePath } from "next/cache";
import git from "isomorphic-git";
import { createR2Fs, getRepoPrefix } from "@/lib/r2-fs";
export async function createRepository(data: { name: string; description?: string; visibility: "public" | "private" }) {
const session = await getSession();
if (!session?.user) {
throw new Error("Unauthorized");
}
const normalizedName = data.name.toLowerCase().replace(/\s+/g, "-");
if (!/^[a-zA-Z0-9_.-]+$/.test(normalizedName)) {
throw new Error("Invalid repository name");
}
const existing = await db.query.repositories.findFirst({
where: and(eq(repositories.ownerId, session.user.id), eq(repositories.name, normalizedName)),
});
if (existing) {
throw new Error("Repository already exists");
}
const [repo] = await db
.insert(repositories)
.values({
name: normalizedName,
description: data.description || null,
visibility: data.visibility,
ownerId: session.user.id,
})
.returning();
const repoPrefix = getRepoPrefix(session.user.id, `${normalizedName}.git`);
const fs = createR2Fs(repoPrefix);
await fs.writeFile("/HEAD", "ref: refs/heads/main\n");
await fs.writeFile(
"/config",
`[core]
\trepositoryformatversion = 0
\tfilemode = true
\tbare = true
`
);
await fs.writeFile("/description", "Unnamed repository; edit this file to name the repository.\n");
const username = (session.user as { username?: string }).username;
revalidatePath(`/${username}`);
revalidatePath("/");
return repo;
}
export async function getRepository(owner: string, name: string) {
const user = await db.query.users.findFirst({
where: eq(users.username, owner),
});
if (!user) {
return null;
}
const repo = await db.query.repositories.findFirst({
where: and(eq(repositories.ownerId, user.id), eq(repositories.name, name)),
});
if (!repo) {
return null;
}
const session = await getSession();
if (repo.visibility === "private") {
if (!session?.user || session.user.id !== repo.ownerId) {
return null;
}
}
return {
...repo,
owner: {
id: user.id,
username: user.username,
name: user.name,
image: user.image,
},
};
}
export async function getUserRepositories(username: string) {
const user = await db.query.users.findFirst({
where: eq(users.username, username),
});
if (!user) {
return [];
}
const session = await getSession();
const isOwner = session?.user?.id === user.id;
const repos = await db.query.repositories.findMany({
where: isOwner ? eq(repositories.ownerId, user.id) : and(eq(repositories.ownerId, user.id), eq(repositories.visibility, "public")),
orderBy: [desc(repositories.updatedAt)],
});
return repos;
}
export async function deleteRepository(repoId: string) {
const session = await getSession();
if (!session?.user) {
throw new Error("Unauthorized");
}
const repo = await db.query.repositories.findFirst({
where: eq(repositories.id, repoId),
});
if (!repo) {
throw new Error("Repository not found");
}
if (repo.ownerId !== session.user.id) {
throw new Error("Unauthorized");
}
const { r2DeletePrefix } = await import("@/lib/r2");
const repoPrefix = getRepoPrefix(session.user.id, `${repo.name}.git`);
try {
await r2DeletePrefix(repoPrefix);
} catch {}
await db.delete(repositories).where(eq(repositories.id, repoId));
const username = (session.user as { username?: string }).username;
revalidatePath(`/${username}`);
revalidatePath("/");
}
export async function getRepoFileTree(owner: string, repoName: string, branch: string, dirPath: string = "") {
const user = await db.query.users.findFirst({
where: eq(users.username, owner),
});
if (!user) {
return null;
}
const repoPrefix = getRepoPrefix(user.id, `${repoName}.git`);
const fs = createR2Fs(repoPrefix);
try {
const commits = await git.log({
fs,
gitdir: "/",
ref: branch,
depth: 1,
});
if (commits.length === 0) {
return { files: [], isEmpty: true };
}
const commitOid = commits[0].oid;
const { tree } = await git.readTree({
fs,
gitdir: "/",
oid: commitOid,
});
let targetTree = tree;
if (dirPath) {
const parts = dirPath.split("/").filter(Boolean);
for (const part of parts) {
const entry = targetTree.find((e) => e.path === part && e.type === "tree");
if (!entry) {
return { files: [], isEmpty: false };
}
const subTree = await git.readTree({
fs,
gitdir: "/",
oid: entry.oid,
});
targetTree = subTree.tree;
}
}
const entries = await Promise.all(
targetTree.map(async (entry) => {
const filePath = dirPath ? `${dirPath}/${entry.path}` : entry.path;
let lastCommit: { message: string; timestamp: number } | null = null;
try {
const fileCommits = await git.log({
fs,
gitdir: "/",
ref: branch,
filepath: filePath,
depth: 1,
});
if (fileCommits.length > 0) {
lastCommit = {
message: fileCommits[0].commit.message.split("\n")[0],
timestamp: fileCommits[0].commit.committer.timestamp * 1000,
};
}
} catch {}
return {
name: entry.path,
type: entry.type as "blob" | "tree",
oid: entry.oid,
path: filePath,
lastCommit,
};
})
);
entries.sort((a, b) => {
if (a.type === "tree" && b.type !== "tree") return -1;
if (a.type !== "tree" && b.type === "tree") return 1;
return a.name.localeCompare(b.name);
});
return { files: entries, isEmpty: false };
} catch (err: unknown) {
const error = err as { code?: string };
if (error.code !== "NotFoundError") {
console.error("getRepoFileTree error:", err);
}
return { files: [], isEmpty: true };
}
}
export async function getRepoFile(owner: string, repoName: string, branch: string, filePath: string) {
const user = await db.query.users.findFirst({
where: eq(users.username, owner),
});
if (!user) {
return null;
}
const repoPrefix = getRepoPrefix(user.id, `${repoName}.git`);
const fs = createR2Fs(repoPrefix);
try {
const commits = await git.log({
fs,
gitdir: "/",
ref: branch,
depth: 1,
});
if (commits.length === 0) {
return null;
}
const commitOid = commits[0].oid;
const parts = filePath.split("/").filter(Boolean);
const fileName = parts.pop()!;
let currentTree = (await git.readTree({ fs, gitdir: "/", oid: commitOid })).tree;
for (const part of parts) {
const entry = currentTree.find((e) => e.path === part && e.type === "tree");
if (!entry) return null;
currentTree = (await git.readTree({ fs, gitdir: "/", oid: entry.oid })).tree;
}
const fileEntry = currentTree.find((e) => e.path === fileName && e.type === "blob");
if (!fileEntry) return null;
const { blob } = await git.readBlob({
fs,
gitdir: "/",
oid: fileEntry.oid,
});
const decoder = new TextDecoder("utf-8");
const content = decoder.decode(blob);
return {
content,
oid: fileEntry.oid,
path: filePath,
};
} catch (err) {
console.error("getRepoFile error:", err);
return null;
}
}