1
0
Fork 0
mirror of https://gitbruv.vercel.app/api/git/bruv/gitbruv.git synced 2025-12-20 23:24:09 +01:00
This commit is contained in:
Ahmet Kilinc 2025-12-20 03:41:20 +00:00
parent 698cf2bcd9
commit 1bdfde47d2

View file

@ -2,11 +2,9 @@ import { NextRequest, NextResponse } from "next/server";
import { db } from "@/db"; import { db } from "@/db";
import { users, repositories } from "@/db/schema"; import { users, repositories } from "@/db/schema";
import { eq, and } from "drizzle-orm"; import { eq, and } from "drizzle-orm";
import { spawn } from "child_process";
import path from "path";
import fs from "fs/promises";
import { withTempRepo } from "@/lib/r2-git-sync";
import { auth } from "@/lib/auth"; import { auth } from "@/lib/auth";
import git from "isomorphic-git";
import { createR2Fs, getRepoPrefix } from "@/lib/r2-fs";
async function authenticateUser(authHeader: string | null): Promise<{ id: string; username: string } | null> { async function authenticateUser(authHeader: string | null): Promise<{ id: string; username: string } | null> {
if (!authHeader || !authHeader.startsWith("Basic ")) { if (!authHeader || !authHeader.startsWith("Basic ")) {
@ -44,34 +42,6 @@ async function authenticateUser(authHeader: string | null): Promise<{ id: string
} }
} }
function runGitCommand(command: string, args: string[], cwd: string, input?: Buffer): Promise<{ stdout: Buffer; stderr: Buffer; code: number }> {
return new Promise((resolve) => {
const proc = spawn(command, args, {
cwd,
env: { ...process.env, GIT_DIR: cwd },
});
const stdout: Buffer[] = [];
const stderr: Buffer[] = [];
proc.stdout.on("data", (data) => stdout.push(data));
proc.stderr.on("data", (data) => stderr.push(data));
if (input) {
proc.stdin.write(input);
proc.stdin.end();
}
proc.on("close", (code) => {
resolve({
stdout: Buffer.concat(stdout),
stderr: Buffer.concat(stderr),
code: code || 0,
});
});
});
}
function parseGitPath(pathSegments: string[]): { username: string; repoName: string; action: string | null } | null { function parseGitPath(pathSegments: string[]): { username: string; repoName: string; action: string | null } | null {
if (pathSegments.length < 2) return null; if (pathSegments.length < 2) return null;
@ -96,6 +66,196 @@ function parseGitPath(pathSegments: string[]): { username: string; repoName: str
return { username, repoName, action }; return { username, repoName, action };
} }
async function getRefsAdvertisement(fs: any, gitdir: string, service: string): Promise<Buffer> {
const capabilities =
service === "git-upload-pack"
? [
"multi_ack",
"thin-pack",
"side-band",
"side-band-64k",
"ofs-delta",
"shallow",
"no-progress",
"include-tag",
"multi_ack_detailed",
"symref=HEAD:refs/heads/main",
]
: ["report-status", "delete-refs", "ofs-delta"];
const refs: { name: string; oid: string }[] = [];
try {
const branches = await git.listBranches({ fs, gitdir });
for (const branch of branches) {
const oid = await git.resolveRef({ fs, gitdir, ref: branch });
refs.push({ name: `refs/heads/${branch}`, oid });
}
} catch {}
try {
const tags = await git.listTags({ fs, gitdir });
for (const tag of tags) {
const oid = await git.resolveRef({ fs, gitdir, ref: tag });
refs.push({ name: `refs/tags/${tag}`, oid });
}
} catch {}
let head = "";
try {
head = await git.resolveRef({ fs, gitdir, ref: "HEAD" });
} catch {}
const lines: string[] = [];
if (refs.length === 0) {
const zeroId = "0".repeat(40);
const capsLine = `${zeroId} capabilities^{}\0${capabilities.join(" ")}\n`;
lines.push(capsLine);
} else {
const firstRef = head ? { name: "HEAD", oid: head } : refs[0];
const capsLine = `${firstRef.oid} ${firstRef.name}\0${capabilities.join(" ")}\n`;
lines.push(capsLine);
for (const ref of refs) {
if (ref.name !== firstRef.name || !head) {
lines.push(`${ref.oid} ${ref.name}\n`);
}
}
}
const packets: Buffer[] = [];
for (const line of lines) {
const len = (line.length + 4).toString(16).padStart(4, "0");
packets.push(Buffer.from(len + line));
}
packets.push(Buffer.from("0000"));
return Buffer.concat(packets);
}
async function handleUploadPack(fs: any, gitdir: string, body: Buffer): Promise<Buffer> {
const lines = parsePktLines(body);
const wants: string[] = [];
const haves: string[] = [];
for (const line of lines) {
if (line.startsWith("want ")) {
wants.push(line.slice(5, 45));
} else if (line.startsWith("have ")) {
haves.push(line.slice(5, 45));
}
}
if (wants.length === 0) {
return Buffer.from("0000");
}
try {
const packfile = await git.packObjects({
fs,
gitdir,
oids: wants,
});
const nakLine = "0008NAK\n";
if (!packfile.packfile) {
return Buffer.from(nakLine + "0000");
}
return Buffer.concat([Buffer.from(nakLine), Buffer.from(packfile.packfile)]);
} catch (err) {
console.error("Pack error:", err);
return Buffer.from("0000");
}
}
async function handleReceivePack(fs: any, gitdir: string, body: Buffer): Promise<Buffer> {
const packStart = body.indexOf(Buffer.from("PACK"));
if (packStart === -1) {
return Buffer.from("000eunpack ok\n0000");
}
const commandSection = body.slice(0, packStart);
const packData = body.slice(packStart);
const lines = parsePktLines(commandSection);
const updates: { oldOid: string; newOid: string; ref: string }[] = [];
for (const line of lines) {
const match = line.match(/^([0-9a-f]{40}) ([0-9a-f]{40}) (.+)$/);
if (match) {
updates.push({ oldOid: match[1], newOid: match[2], ref: match[3].replace("\0", "").split(" ")[0] });
}
}
try {
const packPath = `${gitdir}/objects/pack/pack-temp.pack`;
const packDir = `${gitdir}/objects/pack`;
await fs.promises.mkdir(packDir, { recursive: true }).catch(() => {});
await fs.promises.writeFile(packPath, packData);
const { oids } = await git.indexPack({ fs, dir: gitdir, gitdir, filepath: "objects/pack/pack-temp.pack" });
console.log("Indexed objects:", oids.length);
await fs.promises.unlink(packPath).catch(() => {});
for (const update of updates) {
const refPath = update.ref.startsWith("refs/") ? update.ref : `refs/heads/${update.ref}`;
if (update.newOid === "0".repeat(40)) {
await fs.promises.unlink(`${gitdir}/${refPath}`).catch(() => {});
} else {
const refDir = refPath.split("/").slice(0, -1).join("/");
await fs.promises.mkdir(`${gitdir}/${refDir}`, { recursive: true }).catch(() => {});
await fs.promises.writeFile(`${gitdir}/${refPath}`, update.newOid + "\n");
}
}
const response = ["unpack ok"];
for (const update of updates) {
response.push(`ok ${update.ref}`);
}
let result = "";
for (const line of response) {
const pktLine = line + "\n";
const len = (pktLine.length + 4).toString(16).padStart(4, "0");
result += len + pktLine;
}
result += "0000";
return Buffer.from(result);
} catch (err) {
console.error("Receive pack error:", err);
const errMsg = `ng unpack error ${err}\n`;
const len = (errMsg.length + 4).toString(16).padStart(4, "0");
return Buffer.from(len + errMsg + "0000");
}
}
function parsePktLines(data: Buffer): string[] {
const lines: string[] = [];
let offset = 0;
while (offset < data.length) {
const lenHex = data.slice(offset, offset + 4).toString("utf-8");
const len = parseInt(lenHex, 16);
if (len === 0) {
offset += 4;
continue;
}
if (len < 4) break;
const lineData = data.slice(offset + 4, offset + len);
lines.push(lineData.toString("utf-8").trim());
offset += len;
}
return lines;
}
export async function GET(request: NextRequest, { params }: { params: Promise<{ path: string[] }> }) { export async function GET(request: NextRequest, { params }: { params: Promise<{ path: string[] }> }) {
const { path: pathSegments } = await params; const { path: pathSegments } = await params;
const parsed = parseGitPath(pathSegments); const parsed = parseGitPath(pathSegments);
@ -136,9 +296,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
const serviceQuery = request.nextUrl.searchParams.get("service"); const serviceQuery = request.nextUrl.searchParams.get("service");
if (serviceQuery === "git-upload-pack" || serviceQuery === "git-receive-pack") { if (serviceQuery === "git-upload-pack" || serviceQuery === "git-receive-pack") {
const serviceName = serviceQuery; if (serviceQuery === "git-receive-pack") {
if (serviceName === "git-receive-pack") {
const user = await authenticateUser(request.headers.get("authorization")); const user = await authenticateUser(request.headers.get("authorization"));
if (!user || user.id !== repo.ownerId) { if (!user || user.id !== repo.ownerId) {
return new NextResponse("Unauthorized", { return new NextResponse("Unauthorized", {
@ -148,35 +306,22 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
} }
} }
const response = await withTempRepo(owner.id, repoName, async (tempDir) => { const repoPrefix = getRepoPrefix(owner.id, repoName);
const { stdout } = await runGitCommand("git", [serviceName.replace("git-", ""), "--advertise-refs", "."], tempDir); const fs = createR2Fs(repoPrefix);
const packet = `# service=${serviceName}\n`; const refs = await getRefsAdvertisement(fs, "/", serviceQuery);
const packet = `# service=${serviceQuery}\n`;
const packetLen = (packet.length + 4).toString(16).padStart(4, "0"); const packetLen = (packet.length + 4).toString(16).padStart(4, "0");
return Buffer.concat([Buffer.from(packetLen + packet + "0000"), stdout]); const response = Buffer.concat([Buffer.from(packetLen + packet + "0000"), refs]);
});
return new NextResponse(new Uint8Array(response), { return new NextResponse(new Uint8Array(response), {
headers: { headers: {
"Content-Type": `application/x-${serviceName}-advertisement`, "Content-Type": `application/x-${serviceQuery}-advertisement`,
"Cache-Control": "no-cache", "Cache-Control": "no-cache",
}, },
}); });
} }
const infoRefs = await withTempRepo(owner.id, repoName, async (tempDir) => {
await runGitCommand("git", ["update-server-info"], tempDir);
try {
return await fs.readFile(path.join(tempDir, "info", "refs"), "utf-8");
} catch {
return "";
}
});
return new NextResponse(infoRefs, {
headers: { "Content-Type": "text/plain" },
});
} }
return new NextResponse("Not found", { status: 404 }); return new NextResponse("Not found", { status: 404 });
@ -230,26 +375,19 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
} }
} }
const body = await request.arrayBuffer(); const body = Buffer.from(await request.arrayBuffer());
const input = Buffer.from(body); const repoPrefix = getRepoPrefix(owner.id, repoName);
const fs = createR2Fs(repoPrefix);
const serviceName = action.replace("git-", ""); let response: Buffer;
const shouldSyncBack = action === "git-receive-pack";
const { stdout, stderr, code } = await withTempRepo( if (action === "git-upload-pack") {
owner.id, response = await handleUploadPack(fs, "/", body);
repoName, } else {
async (tempDir) => { response = await handleReceivePack(fs, "/", body);
return await runGitCommand("git", [serviceName, "--stateless-rpc", "."], tempDir, input);
},
shouldSyncBack
);
if (code !== 0) {
console.error("Git error:", stderr.toString());
} }
return new NextResponse(new Uint8Array(stdout), { return new NextResponse(new Uint8Array(response), {
headers: { headers: {
"Content-Type": `application/x-${action}-result`, "Content-Type": `application/x-${action}-result`,
"Cache-Control": "no-cache", "Cache-Control": "no-cache",