GitFOSS
.ts
TypeScript
(application/typescript)
// std
import { existsSync } from "node:fs";
import { join, resolve } from "node:path";
import { mkdir } from "node:fs/promises";
import { spawn } from "node:child_process";
// 1st-party
import type { ServiceMethodFactory } from "@ethicdevs/react-monolith";
// 3rd-party
import cuid from "cuid";
// generated via script[generate:prisma]
import type { Repository } from "@prisma/client";
// service
import type { ForkRepositoryDTO, RepositoryServiceDeps } from "./types";

const makeForkRepository: ServiceMethodFactory<
  RepositoryServiceDeps,
  [ForkRepositoryDTO],
  Promise<Repository>
> = ({ request }) => {
  return async ({ source, target }) => {
    if (
      source == null ||
      source.parentOrg == null ||
      source.repository == null
    ) {
      throw new Error(
        "Cannot fork repository: invalid source object (either it is `null`, or its `parentOrg` and/or `repository` properties are `null`)."
      );
    }

    if (
      target == null ||
      target.parentOrg == null ||
      target.repoSlug == null ||
      target.repoData == null
    ) {
      throw new Error(
        "Cannot fork repository: invalid target object (either it is `null`, or its `parentOrg`, `repoSlug`, and/or `repoData` properties are `null`)."
      );
    }

    let existingRepoWithSameSlugInSameTargetOrg =
      await request.prisma.repository.findFirst({
        where: {
          slug: target.repoSlug,
          organization: {
            id: target.parentOrg.id,
          },
        },
      });

    if (existingRepoWithSameSlugInSameTargetOrg != null) {
      throw new Error(
        "Cannot fork repository: a repository with the same slug already exists in this organization."
      );
    }

    // no longer needed, free it right away.
    existingRepoWithSameSlugInSameTargetOrg = null;

    console.log(`[..] creating target fork repository in database...`);

    const newRepo = await request.prisma.repository.create({
      data: {
        ...target.repoData,

        isFork: true,
        id: cuid(), // to generate one
        slug: target.repoSlug,
        forkedFromRepoId: source.repository.id,
        organizationId: target.parentOrg.id,

        createdAt: new Date(Date.now()),
        updatedAt: new Date(Date.now()),

        avatarUri: source.repository.avatarUri,
        keywords: source.repository.keywords,
        shortDescription: source.repository.shortDescription,
        websiteUrl: source.repository.websiteUrl,
      },
    });

    console.log(
      `[ok] created target fork repository in database with id "${newRepo.id}" and slug "${target.parentOrg.slug}/${newRepo.slug}" from source repo with id "${source.repository.id}" and slug "${source.parentOrg.slug}/${source.repository.slug}" !`
    );

    if (existsSync(target.parentOrgRepositoriesDir.toString()) === false) {
      console.log(`[..] creating organization directory...`);
      await mkdir(target.parentOrgRepositoriesDir.toString(), {
        recursive: true,
      });
      console.log(
        `[ok] created organization directory in:`,
        target.parentOrgRepositoriesDir.toString()
      );
    }

    const sourceRepositoryPathResolved = resolve(
      join(
        source.parentOrgRepositoriesDir.toString(),
        `${source.repository.slug}.git`
      )
    );

    if (existsSync(sourceRepositoryPathResolved) === false) {
      throw new Error(
        "Cannot fork repository: no .git/ folder exists for source repository in source organization."
      );
    }

    const targetRepositoryPathResolved = resolve(
      join(target.parentOrgRepositoriesDir.toString(), `${target.repoSlug}.git`)
    );

    if (existsSync(targetRepositoryPathResolved) === true) {
      throw new Error(
        "Cannot fork repository: a .git/ folder with the same name already exists in target organization."
      );
    }

    console.log(
      `[..] forking repository folder from:`,
      sourceRepositoryPathResolved,
      `to:`,
      targetRepositoryPathResolved
    );

    // could be much simpler... but does not work thanks to bug in libuv ->
    // https://github.com/nodejs/node/issues/36439#issuecomment-765403311
    // await copyFile(sourceRepositoryPathResolved, targetRepositoryPathResolved);

    const gitCopyForkRepoProcess = spawn(
      "/bin/cp",
      ["-rf", sourceRepositoryPathResolved, targetRepositoryPathResolved],
      {
        env: {
          LANG: "C",
        },
      }
    );

    const gitCopyForkRepoResult = await new Promise<string>(
      (resolve, reject) => {
        let buffer = [] as string[];
        gitCopyForkRepoProcess.stdout.on("data", (data) => buffer.push(data));
        gitCopyForkRepoProcess.stderr.on("data", (data) => {
          reject(new Error(Buffer.from(data).toString("utf-8")));
        });
        gitCopyForkRepoProcess.stdout.on("close", () => {
          resolve(buffer.join(""));
        });
      }
    );

    console.log(
      `[ok] finished execution of "cp -rf ${sourceRepositoryPathResolved} ${targetRepositoryPathResolved}" with result:\n\t`,
      gitCopyForkRepoResult
    );

    console.log(
      `[ok] forked repository folder from:`,
      sourceRepositoryPathResolved,
      `to:`,
      targetRepositoryPathResolved,
      "->",
      newRepo
    );

    return newRepo;
  };
};

export default makeForkRepository;