import type { ReqHandler } from "@ethicdevs/react-monolith";
import { ResourceVisibility, PullRequestState } from "@prisma/client";
import { spawn } from "node:child_process";
import { mkdtemp, rm } from "fs/promises";
import os from "os";
import { join } from "node:path";
import { AppRoute, AppRouteParams } from "../../routes.defs";
import { makeOrganizationService } from "../../services/organization";
import { makePullRequestService } from "../../services/pullRequest";
import { makeRepositoryService } from "../../services/repository";
import { makeUsersService } from "../../services/user";
import { Env } from "../../env";
const deleteSourceBranchAction: ReqHandler<
AppRouteParams,
AppRoute.REPOSITORY_PULL_REQUEST_DELETE_SOURCE_BRANCH_ACTION
> = async (request, reply) => {
const { orgSlug, repoSlug, pullUid } = request.params as any;
const orgService = makeOrganizationService({ request });
const prService = makePullRequestService({ request });
const repoService = makeRepositoryService({ request });
const usersService = makeUsersService({ request });
const currentUser =
request.session.data.authenticated &&
request.session.data.curr_user_uid != null
? await usersService.getUserById(request.session.data.curr_user_uid)
: null;
const pullRequest = await prService.getPullRequestByUid(
orgSlug,
repoSlug,
pullUid,
);
if (pullRequest == null) {
return reply.status(404).callNotFound();
}
if (pullRequest.state !== PullRequestState.CLOSE_MERGED) {
return reply
.status(403)
.send({ error: "Deletion allowed only after PR is merged" });
}
const sourceRepo = await repoService.getRepositoryById(
pullRequest.sourceRepositoryId,
);
if (sourceRepo == null) {
return reply.status(404).callNotFound();
}
const sourceParentOrg = await orgService.getOrganizationById(
sourceRepo.organizationId,
);
if (sourceParentOrg == null) {
return reply.status(404).callNotFound();
}
if (sourceRepo.visibility === ResourceVisibility.PRIVATE) {
if (currentUser == null) {
return reply.status(404).callNotFound();
} else if (
(await repoService.canUserAccessRepository(currentUser, sourceRepo)) ===
false
) {
return reply.status(404).callNotFound();
}
}
const sourceBarePath = `${Env.GIT_REPOSITORIES_ROOT}/${sourceParentOrg.slug}/${sourceRepo.slug}.git`;
const tmpDir = await mkdtemp(join(os.tmpdir(), "gitfoss-delete-src-"));
try {
await new Promise<void>((resolve, reject) => {
const c = spawn("git", ["clone", sourceBarePath, tmpDir], {
cwd: undefined,
env: { LANG: "C" },
});
let err = "";
c.stderr.on("data", (d) => (err += d.toString()));
c.on("close", (code) =>
code === 0 ? resolve() : reject(new Error(err)),
);
});
await new Promise<void>((resolve, reject) => {
const c = spawn(
"git",
["config", "user.name", "GitFOSS Agent (system)"],
{
cwd: tmpDir,
env: { LANG: "C" },
},
);
let err = "";
c.stderr.on("data", (d) => (err += d.toString()));
c.on("close", (code) =>
code === 0 ? resolve() : reject(new Error(err)),
);
});
await new Promise<void>((resolve, reject) => {
const c = spawn("git", ["config", "user.email", "git@gitfoss.dev"], {
cwd: tmpDir,
env: { LANG: "C" },
});
let err = "";
c.stderr.on("data", (d) => (err += d.toString()));
c.on("close", (code) =>
code === 0 ? resolve() : reject(new Error(err)),
);
});
await new Promise<void>((resolve, reject) => {
const c = spawn(
"git",
["push", sourceBarePath, "--delete", pullRequest.sourceBranch],
{
cwd: tmpDir,
env: { LANG: "C" },
},
);
let err = "";
c.stderr.on("data", (d) => (err += d.toString()));
c.on("close", (code) =>
code === 0
? resolve()
: reject(new Error(err || "delete source branch failed")),
);
});
return reply
.status(200)
.send({ success: true, deletedBranch: pullRequest.sourceBranch });
} catch (err) {
console.error(
"Delete source branch action failed:",
(err as Error).message,
);
return reply.status(500).send({
error: "Delete source branch action failed",
detail: (err as Error).message,
});
} finally {
try {
await rm(tmpDir, { recursive: true, force: true });
} catch {
}
}
};
export default deleteSourceBranchAction;