.ts
TypeScript
(application/typescript)
// std
import { existsSync } from "node:fs";
import { spawn } from "node:child_process";
// 1st-party
import type { ServiceMethodFactory } from "@ethicdevs/react-monolith";
// generated via script[generate:prisma]
import type { Repository } from "@prisma/client";
// app
import { Env } from "../../env";
// service
import type { RepositoryLog } from "../../types";
import type { RepositoryServiceDeps } from "./types";

const GIT_LOG_NEWLINEW_FINDER_REGEXP =
  /  \^@\^[a-z_]+\^@\^: \^@\^([^\^]+|)\^@\^,?/gim;

const makeGetRepositoryCommitLog: ServiceMethodFactory<
  RepositoryServiceDeps,
  [Repository, string | undefined, string | undefined, boolean | undefined],
  Promise<RepositoryLog[]>
> = ({ request }) => {
  return async (repo, path = "", ref = "HEAD", onlyLast = false) => {
    const parentOrg = await request.prisma.organization.findUnique({
      where: {
        id: repo.organizationId,
      },
    });

    if (parentOrg == null) {
      throw new Error(
        `Could not find the parent organization for project "${repo.slug}".`
      );
    }

    const repoPath = `${Env.GIT_REPOSITORIES_ROOT}/${parentOrg.slug}/${repo.slug}.git`;
    if (existsSync(repoPath) === false) {
      throw new Error(`Could not find a valid git repository at: ${repoPath}`);
    }

    var format =
      "{%n  ^@^commit^@^: ^@^%H^@^,%n  ^@^abbreviated_commit^@^: ^@^%h^@^,%n  ^@^tree^@^: ^@^%T^@^,%n  ^@^abbreviated_tree^@^: ^@^%t^@^,%n  ^@^parent^@^: ^@^%P^@^,%n  ^@^abbreviated_parent^@^: ^@^%p^@^,%n  ^@^refs^@^: ^@^%D^@^,%n  ^@^encoding^@^: ^@^%e^@^,%n  ^@^subject^@^: ^@^%s^@^,%n  ^@^sanitized_subject_line^@^: ^@^%f^@^,%n  ^@^body^@^: ^@^%b^@^,%n  ^@^commit_notes^@^: ^@^%N^@^,%n  ^@^verification_flag^@^: ^@^%G?^@^,%n  ^@^signer^@^: ^@^%GS^@^,%n  ^@^signer_key^@^: ^@^%GK^@^,%n  ^@^author^@^: {%n    ^@^name^@^: ^@^%aN^@^,%n    ^@^email^@^: ^@^%aE^@^,%n    ^@^date^@^: ^@^%aD^@^%n  },%n  ^@^commiter^@^: {%n    ^@^name^@^: ^@^%cN^@^,%n    ^@^email^@^: ^@^%cE^@^,%n    ^@^date^@^: ^@^%cD^@^%n  }%n},";

    const args = [
      "log",
      "--quiet",
      `--pretty=format:${format}`,
      onlyLast ? "-1" : null,
      ref,
      path != null && path.trim() !== "" ? "--" : null,
      path != null && path.trim() !== "" ? "-p" : null,
      path != null && path.trim() !== "" ? `${path}` : null,
    ].filter((x): x is string => x != null);
    const gitLogProcess = spawn("git", args, {
      cwd: repoPath,
    });

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

            const fieldsToEscape = escapedJson.match(
              GIT_LOG_NEWLINEW_FINDER_REGEXP
            );

            if (fieldsToEscape != null && Array.isArray(fieldsToEscape)) {
              fieldsToEscape.forEach((fieldTxt) => {
                escapedJson = escapedJson.replace(
                  fieldTxt,
                  fieldTxt.split("\n").join("\\n") // Escape newlines
                );
              });
            }

            escapedJson = escapedJson
              .replace(/"/gm, '\\"') // Escape double-quotes: " -> \"
              .replace(/\^@\^/gm, '"'); // Escaped double-quotes back to double quotes

            try {
              resolve(
                JSON.parse(
                  `[${escapedJson.substring(0, escapedJson.length - 1)}]`
                )
              );
            } catch (err) {
              // console.log("escapedJson:", escapedJson);
              reject(err);
            }
          });
        }
      );

      return gitLogResult as RepositoryLog[];
    } catch (err) {
      console.error("Cannot get git log", err);
      return [];
    }
  };
};

export default makeGetRepositoryCommitLog;