All files / src/models git-system.js

0% Statements 0/175
0% Branches 0/1
0% Functions 0/1
0% Lines 0/175

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 169 170 171 172 173 174 175 176                                                                                                                                                                                                                                                                                                                                                               
import { simpleGit } from 'simple-git';
import { config } from '../config/config.js';
import { slugify } from '../lib/slugify.js';
import { Git } from './git.js';

export class GitSystem extends Git {
  #gitAvailabilityChecked = false;

  constructor() {
    super('system');
  }

  async addRemote(remoteName, url) {
    this._debug('addRemote', remoteName, url);
    const git = await this.#getSimpleGit();
    const safeRemoteName = slugify(remoteName);
    const remotes = await git.getRemotes();
    const existingRemote = remotes.find((r) => r.name === safeRemoteName);
    if (existingRemote == null) {
      await git.addRemote(safeRemoteName, url);
    }
  }

  async #getSimpleGit() {
    await this.#checkGitAvailability();
    const dir = await this._getRepoDir();
    return simpleGit(dir);
  }

  async #checkGitAvailability() {
    if (this.#gitAvailabilityChecked) {
      return;
    }
    const git = simpleGit();
    const ver = await git.version();
    if (!ver.installed) {
      throw new GitNotFoundError();
    }
    this.#gitAvailabilityChecked = true;
  }

  async resolveFullCommitId(commitId) {
    this._debug('resolveFullCommitId', commitId);
    if (commitId == null) {
      return null;
    }
    const git = await this.#getSimpleGit();
    try {
      const fullOid = await git.revparse([commitId]);
      return fullOid.trim();
    } catch (e) {
      if (e.message.includes('unknown revision') || e.message.includes('ambiguous argument')) {
        throw new Error(`Commit id ${commitId} is ambiguous`);
      }
      throw e;
    }
  }

  async getRemoteCommit(remoteUrl) {
    const git = await this.#getSimpleGit();
    const authUrl = this.#buildAuthenticatedUrl(remoteUrl);
    this._debug('getRemoteCommit', this.#redactUrl(authUrl));
    try {
      const result = await git.listRemote(['--refs', authUrl.toString()]);
      // Parse output: "<sha>\trefs/heads/master"
      const lines = result.trim().split('\n');
      for (const line of lines) {
        const [sha, ref] = line.split('\t');
        if (ref === 'refs/heads/master') {
          return sha;
        }
      }
      return undefined;
    } catch {
      return undefined;
    }
  }

  #buildAuthenticatedUrl(url) {
    const urlObj = new URL(url);
    urlObj.username = config.token;
    urlObj.password = config.secret;
    return urlObj;
  }

  async getFullBranch(branchName) {
    this._debug('getFullBranch', branchName);
    const git = await this.#getSimpleGit();
    const ref = branchName === '' ? 'HEAD' : branchName;
    try {
      const fullRef = await git.revparse(['--symbolic-full-name', ref]);
      return fullRef.trim();
    } catch {
      // Not a symbolic ref (e.g. a commit hash), return as-is
      return ref;
    }
  }

  async getBranchCommit(refspec) {
    this._debug('getBranchCommit', refspec);
    const git = await this.#getSimpleGit();
    // Use rev-parse with ^{commit} to dereference tags to their commit
    const oid = await git.revparse([`${refspec}^{commit}`]);
    return oid.trim();
  }

  async isExistingTag(tag) {
    this._debug('isExistingTag', tag);
    const git = await this.#getSimpleGit();
    const tags = await git.tags();
    return tags.all.includes(tag);
  }

  #redactUrl(url) {
    const urlObj = typeof url === 'string' ? new URL(url) : url;
    const redacted = new URL(urlObj.toString());
    if (redacted.username) redacted.username = '***';
    if (redacted.password) redacted.password = '***';
    return redacted.toString();
  }

  async push(remoteUrl, branchRefspec, force) {
    const git = await this.#getSimpleGit();
    const authUrl = this.#buildAuthenticatedUrl(remoteUrl);
    const refspec = `${branchRefspec}:refs/heads/master`;
    this._debug('push', this.#redactUrl(authUrl), refspec, force ? '--force' : '');
    const options = ['--porcelain'];
    if (force) {
      options.push('--force');
    }
    try {
      await git.push(authUrl.toString(), refspec, options);
      return {};
    } catch (e) {
      if (e.message.includes('non-fast-forward') || e.message.includes('[rejected]')) {
        throw new Error('Push rejected because it was not a simple fast-forward, use --force to override');
      }
      throw e;
    }
  }

  async completeBranches() {
    this._debug('completeBranches');
    const git = await this.#getSimpleGit();
    const branches = await git.branchLocal();
    return branches.all;
  }

  async isInsideGitRepo() {
    this._debug('isInsideGitRepo');
    try {
      await this._getRepoDir();
      return true;
    } catch {
      return false;
    }
  }

  async isGitWorkingDirectoryClean() {
    this._debug('isGitWorkingDirectoryClean');
    const git = await this.#getSimpleGit();
    const status = await git.status();
    return status.isClean();
  }
}

class GitNotFoundError extends Error {
  constructor() {
    super(
      'The system git feature requires git to be installed and available in your PATH\n' +
        'Either install git or disable this feature with: clever features disable system-git',
    );
    this.name = 'GitNotFoundError';
  }
}