import type { ServiceMethodFactory } from "@ethicdevs/react-monolith";
import { join } from "node:path";
import { mkdtemp, rm } from "fs/promises";
import { spawn } from "node:child_process";
import os from "os";
import type { Repository } from "@prisma/client";
import { Env } from "../../env";
import type {
MergePullRequestDTO,
MergePullRequestResult,
PullRequestServiceDeps,
} from "./types";
export const makeMergePullRequest: ServiceMethodFactory<
PullRequestServiceDeps,
[MergePullRequestDTO],
Promise<MergePullRequestResult>
> = ({ request }) => {
return async (dto) => {
const pr = await request.prisma.pullRequest.findUnique({
where: { id: dto.pullRequestId },
include: {
sourceRepository: {
include: {
organization: true,
},
},
targetRepository: {
include: {
organization: true,
},
},
},
});
if (pr == null) {
throw new Error("Pull request not found");
}
const sourceRepo: Repository | null = pr.sourceRepository ?? null;
const targetRepo: Repository | null = pr.targetRepository ?? null;
if (sourceRepo == null || targetRepo == null) {
throw new Error("Invalid pull request repositories");
}
const sourceOrgSlug = pr.sourceRepository.organization.slug ?? null;
const targetOrgSlug = pr.targetRepository.organization.slug ?? null;
if (!sourceOrgSlug || !targetOrgSlug) {
throw new Error("Could not resolve repository organizations");
}
const sourceBarePath = `${Env.GIT_REPOSITORIES_ROOT}/${sourceOrgSlug}/${sourceRepo.slug}.git`;
const targetBarePath = `${Env.GIT_REPOSITORIES_ROOT}/${targetOrgSlug}/${targetRepo.slug}.git`;
const tmpDir = await mkdtemp(join(os.tmpdir(), "gitfoss-merge-"));
try {
await new Promise<void>((resolve, reject) => {
const c = spawn("git", ["clone", targetBarePath, 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", ["remote", "add", "source", sourceBarePath], {
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", ["fetch", "source", pr.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)),
);
});
await new Promise<void>((resolve, reject) => {
const c = spawn("git", ["checkout", pr.targetBranch], {
cwd: tmpDir,
env: { LANG: "C" },
});
c.on("close", (code) =>
code === 0 ? resolve() : reject(new Error("checkout failed")),
);
});
const mergeArgs = ["merge", "--no-ff", `source/${pr.sourceBranch}`];
if (dto.mergeMessage && dto.mergeMessage.trim() !== "") {
mergeArgs.push("-m", dto.mergeMessage);
}
await new Promise<void>((resolve, reject) => {
const c = spawn("git", mergeArgs, {
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 || "merge failed")),
);
});
await new Promise<void>((resolve, reject) => {
const c = spawn(
"git",
["push", targetBarePath, `${pr.targetBranch}:${pr.targetBranch}`],
{
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 || "push failed")),
);
});
if (dto.deleteSourceBranch === true) {
await new Promise<void>((resolve, reject) => {
const c = spawn(
"git",
["push", sourceBarePath, "--delete", pr.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")),
);
});
}
const updatedPR = await request.prisma.pullRequest.update({
where: { id: pr.id },
data: {
state: (require("@prisma/client").PullRequestState as any)
.CLOSE_MERGED,
closedAt: new Date(),
},
});
return {
success: true,
updatedPullRequest: updatedPR,
};
export default makeMergePullRequest;