XasCode/terrafile-backend-lib

View on GitHub
src/backend/moduleSources/common/cloneRepo.ts

Summary

Maintainability
C
1 day
Test Coverage
B
82%
import path from 'path';
import chalk from '@xascode/chalk';

import { ExecFileException } from 'child_process';
import { ExecResult, Path, RepoLocation, SourceParts, Status, FsHelpers } from '../../types';
import gitCloner from '@jestaubach/cloner-git';

const defaultGitCloner = gitCloner.use(gitCloner.default);

function determineRef(ref: string): string[] {
  const commit = ref;
  const branchOrTag = ref;
  return ref?.length === 40 ? [``, commit] : [branchOrTag, ``];
}

function insertGit(source: Path): Path {
  const parts = source.split(`?ref=`);
  return parts.length < 2 || source.includes(`.git`) ? source : [parts[0], `.git`, `?ref=`, ...parts.slice(1)].join(``);
}

function sourceParts(source: Path): SourceParts {
  const tempSource = insertGit(source);
  const [beforeGit, afterGit] = tempSource.split(`.git`);
  const newSource = `${beforeGit}${source.includes(`.git`) ? `.git` : ``}`;
  const newAfterGit = afterGit || ``;
  const [beforeQref, afterQref] = newAfterGit.split(`?ref=`);
  const [, afterPathSep] = beforeQref.split(`//`);
  const newPathPart = afterPathSep ? `//${afterPathSep}` : ``;
  return [newSource, newPathPart, afterQref];
}

function getPartsFromHttp(source: Path): RepoLocation {
  const [repo, repoDir, ref] = sourceParts(source);
  const [branchOrTag, commit] = determineRef(ref);
  return [repo, repoDir, branchOrTag, commit];
}

async function cloneRepo(
  [repo, repoDir, branchOrTag]: RepoLocation,
  fullDest: Path,
  cloner: (_: string[], __?: Path) => Promise<ExecResult>,
): Promise<ExecResult> {
  const cloneCmd = [
    `clone`,
    ...(repoDir ? [`--depth`, `1`, `--filter=blob:none`, `--sparse`] : []),
    ...(branchOrTag ? [`--branch=${branchOrTag}`] : []),
    `${repo}`,
    fullDest,
  ];
  return cloner(cloneCmd);
}

async function scopeRepo(
  [, repoDir]: RepoLocation,
  fullDest: Path,
  cloner: (_: string[], __?: Path) => Promise<ExecResult>,
): Promise<ExecResult> {
  const sparseCmd = [`sparse-checkout`, `set`, repoDir.slice(1)];
  if (repoDir) {
    return cloner(sparseCmd, fullDest);
  }
  return {} as ExecResult;
}

async function checkoutCommit(
  [, , , commit]: RepoLocation,
  fullDest: Path,
  cloner: (_: string[], __?: Path) => Promise<ExecResult>,
): Promise<ExecResult> {
  const commitCmd = [`checkout`, commit];
  if (commit) {
    return cloner(commitCmd, fullDest);
  }
  return {} as ExecResult;
}

const tempDirName = `__temp__`;

function renameFullDestToTempDir([, repoDir]: RepoLocation, fullDest: Path, fsHelpers: FsHelpers): ExecResult {
  if (repoDir) {
    const tempDir = `${fullDest}${tempDirName}`;
    let retVal = null;
    try {
      retVal = fsHelpers.renameDir(fullDest, tempDir);
    } catch (err) {
      console.log(`caught error in renameDir`);
    }
    if (!retVal.success) {
      const err = `failed to rename '${fullDest}' to '${tempDir}'`;
      console.log(chalk.red(`    ! Failed - ${err}`));
      return {
        error: { name: ``, message: err, code: -1 } as ExecFileException,
        stdout: ``,
        stderr: ``,
      };
    }
  }
  return {} as ExecResult;
}

function moveFromTempRepoDirToFullDest([, repoDir]: RepoLocation, fullDest: Path, fsHelpers: FsHelpers): ExecResult {
  if (repoDir) {
    const tempDir = `${fullDest}${tempDirName}`;
    const src = fsHelpers.getAbsolutePath(`${tempDir}${path.sep}${repoDir}`).value;
    const retVal = fsHelpers.copyDirAbs(src, fullDest);
    if (!retVal.success) {
      const err = `failed to copy '${src}' to '${fullDest}'`;
      console.log(chalk.red(`    ! Failed - ${err}`));
      return {
        error: { name: ``, message: err, code: -1 } as ExecFileException,
        stdout: ``,
        stderr: ``,
      };
    }
  }
  return {} as ExecResult;
}

function removeTempDir([, repoDir]: RepoLocation, fullDest: Path, fsHelpers: FsHelpers): ExecResult {
  if (repoDir) {
    const tempDir = `${fullDest}${tempDirName}`;
    const retVal = fsHelpers.rimrafDir(tempDir);
    if (!retVal.success) {
      const err = `failed to delete '${tempDir}'`;
      console.log(chalk.red(`    ! Failed - ${err}`));
      return {
        error: { name: ``, message: err, code: -1 } as ExecFileException,
        stdout: ``,
        stderr: ``,
      };
    }
  }
  return {} as ExecResult;
}

async function cloneRepoToDest(
  repoUrl: Path,
  fullDest: Path,
  cloner: (_: string[], __?: Path) => Promise<ExecResult>,
  fsHelpers: FsHelpers,
): Promise<Status> {
  const retVal = {
    success: false,
    contents: null,
    error: `Error cloning repo to destination ${repoUrl} - ${fullDest}`,
  } as Status;
  const useCloner = cloner || defaultGitCloner;
  const repoSourceParts: RepoLocation = getPartsFromHttp(repoUrl);
  const successfulCloning =
    !(await cloneRepo(repoSourceParts, fullDest, useCloner)).error &&
    !(await scopeRepo(repoSourceParts, fullDest, useCloner)).error &&
    !(await checkoutCommit(repoSourceParts, fullDest, useCloner)).error;
  if (!successfulCloning) {
    console.log(chalk.yellow(`      ! Failed - cloning '${repoUrl}' to '${fullDest}'; is network available?`));
    return retVal;
  }
  const successfulMoving =
    !renameFullDestToTempDir(repoSourceParts, fullDest, fsHelpers).error &&
    !moveFromTempRepoDirToFullDest(repoSourceParts, fullDest, fsHelpers).error &&
    !removeTempDir(repoSourceParts, fullDest, fsHelpers).error;
  if (!successfulMoving) {
    const src = `${fullDest}${path.sep}${repoSourceParts[1]}`;
    console.log(
      chalk.yellow(
        `      ! Failed - moving '${src}' to '${fullDest}'; if destination already exists, remove and try again.`,
      ),
    );
    return retVal;
  }
  retVal.success = true;
  retVal.error = null;
  retVal.contents = [{ source: `${repoUrl}` } as unknown as [string, Record<string, string>]];
  return retVal;
}

export { cloneRepoToDest, getPartsFromHttp };