.ts
TypeScript
(application/typescript)
// std
import { join } from "node:path";
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 type { RepositoryFile } from "../../types";
import { Const } from "../../const";
import { Env } from "../../env";
// service
import type { RepositoryServiceDeps } from "./types";
import { default as makeGetRepositoryCommitLog } from "./getRepositoryCommitLog";

const GIT_LS_TREE_REGEXP =
  /^([\d]+)[\s]+(blob|tree)[\s]+([a-z0-9]+)[\s]+(.*)$/i;

const makeGetRepositoryFiles: ServiceMethodFactory<
  RepositoryServiceDeps,
  [Repository, string | undefined, string | undefined],
  Promise<RepositoryFile[]>
> = (deps) => {
  const { request } = deps;
  return async (repo, path = "", ref = Const.DEFAULT_HEAD_REF) => {
    const getRepositoryCommitLog = makeGetRepositoryCommitLog(deps);
    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}".`
      );
    }

    try {
      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}`
        );
      }

      const gitLsTreeProcess = spawn("git", ["ls-tree", `${ref}:${path}`], {
        cwd: repoPath,
        env: {
          LANG: "C",
        },
      });

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

      const files = gitLsTreeResult.split("\n");
      const repoFilesP: (RepositoryFile | null)[] = await Promise.all(
        files.map(async (line) => {
          const matches = GIT_LS_TREE_REGEXP.exec(line);
          if (matches == null || Array.isArray(matches) === false) {
            return null;
          }
          const [_, permissions, type, id, name] = matches;
          const commitLogs = await getRepositoryCommitLog(
            repo,
            join(path, name),
            ref,
            true
          );
          return {
            id,
            name,
            permissions,
            type,
            lastCommit: commitLogs.length >= 1 ? commitLogs[0] : null,
          } as RepositoryFile;
        })
      );

      const repoFiles = repoFilesP
        .filter((x): x is RepositoryFile => x != null)
        .sort((a, b) => {
          if (a.type === "blob" && b.type === "tree") {
            return 1;
          } else if (a.type === "blob" && b.type === "blob") {
            return 0;
          } else if (a.type === "tree" && b.type === "tree") {
            return 0;
          } else {
            return -1;
          }
        });

      return repoFiles;
    } catch (_) {
      return [];
    }
  };
};

export default makeGetRepositoryFiles;