.ts
TypeScript
(application/typescript)
// 1st-party
import type { ServiceMethodFactory } from "@ethicdevs/react-monolith";
// 3rd-party
import { join } from "node:path";
import { mkdtemp, rm } from "fs/promises";
import { spawn } from "node:child_process";
import os from "os";
// generated via script[generate:prisma]
import type { Repository } from "@prisma/client";
// app
import { Env } from "../../env";
import type {
  MergePullRequestDTO,
  MergePullRequestResult,
  PullRequestServiceDeps,
  // PullRequestServiceAPI,
} from "./types";

// Implementation of a merge operation for a pull request using a temporary working copy
// This service is deliberately conservative and uses the standard merge workflow by default.
// It does not attempt to squash or rebase unless you extend the DTO.

export const makeMergePullRequest: ServiceMethodFactory<
  PullRequestServiceDeps,
  [MergePullRequestDTO],
  Promise<MergePullRequestResult>
> = ({ request }) => {
  return async (dto) => {
    // fetch pull request with related repos and their organizations to resolve paths
    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");
    }

    // bare repo paths
    const sourceBarePath = `${Env.GIT_REPOSITORIES_ROOT}/${sourceOrgSlug}/${sourceRepo.slug}.git`;
    const targetBarePath = `${Env.GIT_REPOSITORIES_ROOT}/${targetOrgSlug}/${targetRepo.slug}.git`;

    // ensure bare repos exist
    // Best-effort check; actual fs check is optional here, rely on git failing gracefully otherwise

    // create a temporary working directory
    const tmpDir = await mkdtemp(join(os.tmpdir(), "gitfoss-merge-"));

    try {
      // clone target bare repo into working copy
      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)),
        );
      });

      // setup committer identity
      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)),
        );
      });

      // add source as remote
      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)),
        );
      });

      // fetch source branch
      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)),
        );
      });

      // checkout target branch
      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")),
        );
      });

      // merge
      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")),
        );
      });

      // push merged result back to target bare repo
      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")),
        );
      });

      // Optional: delete source branch on the source repo if requested
      if (dto.deleteSourceBranch === true) {
        try {
          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")),
            );
          });
        } catch (err) {
          // Do not fail the merge if deletion fails
          // log error for visibility
          // @ts-ignore
          console.error(
            "Failed to delete source branch after merge:",
            (err as Error).message,
          );
        }
      }

      // update PR as merged
      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,
      };
    } catch (err) {
      throw err;
    } finally {
      // cleanup
      try {
        await rm(tmpDir, { recursive: true, force: true });
      } catch {
        // ignore cleanup errors
      }
    }
  };
};

export default makeMergePullRequest;