All files / src/models git-isomorphic.js

66.07% Statements 111/168
60% Branches 9/15
57.14% Functions 8/14
66.07% Lines 111/168

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 10x 10x 193x 193x 25x 25x     25x 25x 193x 193x           193x 193x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 193x 193x                             193x 193x 6x 6x 6x 6x 6x 6x 6x 6x 6x 193x 193x 6x 6x 6x 6x 6x 6x   6x 193x 193x 6x 6x 6x 6x 6x 6x 6x 6x 193x 193x 1x 1x 1x 1x 1x 1x 1x 193x 193x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x     2x 2x           2x 193x 193x       193x 193x 193x 193x 193x 193x             193x 193x 193x 193x 193x 193x                         193x 193x 193x 193x               193x  
import * as git from 'isomorphic-git';
import _ from 'lodash';
import fs from 'node:fs';
import { config } from '../config/config.js';
import { slugify } from '../lib/slugify.js';
import { Git } from './git.js';
import * as http from './isomorphic-http-with-agent.js';
 
export class GitIsomorphic extends Git {
  constructor() {
    super('isomorphic');
  }
 
  async #getRepo() {
    const dir = await this._getRepoDir();
    if (await this._isLinkedWorktree(dir)) {
      throw new LinkedWorktreeNotSupportedError();
    }
    return { fs, dir, http };
  }
 
  #onAuth() {
    return {
      username: config.token,
      password: config.secret,
    };
  }
 
  async addRemote(remoteName, url) {
    this._debug('addRemote', remoteName, url);
    const repo = await this.#getRepo();
    const safeRemoteName = slugify(remoteName);
    const allRemotes = await git.listRemotes({ ...repo });
    const existingRemote = _.find(allRemotes, { remote: safeRemoteName });
    if (existingRemote == null) {
      // In some situations, we may end up with race conditions so we force it
      return git.addRemote({ ...repo, remote: safeRemoteName, url, force: true });
    }
  }
 
  async resolveFullCommitId(commitId) {
    this._debug('resolveFullCommitId', commitId);
    if (commitId == null) {
      return null;
    }
    try {
      const repo = await this.#getRepo();
      return await git.expandOid({ ...repo, oid: commitId });
    } catch (e) {
      if (e.code === 'ShortOidNotFound') {
        throw new Error(`Commit id ${commitId} is ambiguous`);
      }
      throw e;
    }
  }
 
  async getRemoteCommit(remoteUrl) {
    this._debug('getRemoteCommit', remoteUrl);
    const repo = await this.#getRepo();
    const remoteInfos = await git.getRemoteInfo({
      ...repo,
      onAuth: this.#onAuth,
      url: remoteUrl,
    });
    return _.get(remoteInfos, 'refs.heads.master');
  }
 
  async getFullBranch(branchName) {
    this._debug('getFullBranch', branchName);
    const repo = await this.#getRepo();
    if (branchName === '') {
      const currentBranch = await git.currentBranch({ ...repo, fullname: true });
      return currentBranch || 'HEAD';
    }
    return git.expandRef({ ...repo, ref: branchName });
  }
 
  async getBranchCommit(refspec) {
    this._debug('getBranchCommit', refspec);
    const repo = await this.#getRepo();
    const oid = await git.resolveRef({ ...repo, ref: refspec });
    // When a refspec refers to an annotated tag, the OID ref represents the annotation and not the commit directly,
    // that's why we need a call to `readCommit`.
    const res = await git.readCommit({ ...repo, ref: refspec, oid });
    return res.oid;
  }
 
  async isExistingTag(tag) {
    this._debug('isExistingTag', tag);
    const repo = await this.#getRepo();
    const tags = await git.listTags({
      ...repo,
    });
    return tags.includes(tag);
  }
 
  async push(remoteUrl, branchRefspec, force, remoteName) {
    const refspec = `${branchRefspec}:refs/heads/master`;
    this._debug('push', remoteUrl, refspec, force ? '--force' : '');
    const repo = await this.#getRepo();
    try {
      const push = await git.push({
        ...repo,
        onAuth: this.#onAuth,
        url: remoteUrl,
        ref: branchRefspec,
        remoteRef: 'master',
        remote: remoteName,
        force,
      });
      if (push.errors != null) {
        throw new Error(push.errors.join(', '));
      }
      return push;
    } catch (e) {
      if (e.code === 'PushRejectedNonFastForward') {
        throw new Error('Push rejected because it was not a simple fast-forward, use --force to override');
      }
      throw e;
    }
  }
 
  async completeBranches() {
    this._debug('completeBranches');
    return this.#getRepo().then((repo) => git.listBranches(repo));
  }
 
  /**
   * Check if the current directory is a git repository
   * @returns {Promise<boolean>}
   */
  async isInsideGitRepo() {
    this._debug('isInsideGitRepo');
    // Don't go through #getRepo: it rejects linked worktrees, which are still git repos
    return this._getRepoDir()
      .then(() => true)
      .catch(() => false);
  }
 
  /**
   * Check if the current git working directory is clean
   * @returns {Promise<boolean>}
   */
  async isGitWorkingDirectoryClean() {
    this._debug('isGitWorkingDirectoryClean');
    const repo = await this.#getRepo();
    const status = await git.statusMatrix({ ...repo });
    const isStatusEmpty =
      status.filter(([filepath, head, workdir]) => {
        // WARNING: isomorphic-git does not support global gitignore so we filter hidden files and dirs to reduce the amount of false positives
        const isHidden = filepath.startsWith('.');
        const isCleverJson = filepath === '.clever.json';
        return (!isHidden || isCleverJson) && head !== workdir;
      }).length === 0;
    return isStatusEmpty;
  }
}
 
export class LinkedWorktreeNotSupportedError extends Error {
  constructor() {
    super(
      "Linked git worktrees aren't supported by the default JS git backend.\n" +
        'Enable the system git backend (it uses your installed git):\n' +
        '    clever features enable system-git',
    );
    this.name = 'LinkedWorktreeNotSupportedError';
  }
}