import path from 'path';
import debug from 'debug';
import pify from 'pify';
import BufferCursor from 'buffercursor';
import sortby from 'lodash.sortby';
import shasum from 'shasum';
import crc32 from 'crc/lib/crc32.js';
import applyDelta from 'git-apply-delta';
import listpack from 'git-list-pack';
import { mark, stop } from 'marky';
import pako from 'pako';
import { PassThrough } from 'stream';
import pad from 'pad';
import streamSource from 'stream-source/index.node.js';
import ignore from 'ignore';
import AsyncLock from 'async-lock';
import concat from 'simple-concat';
import simpleGet from 'simple-get';
import createHash from 'sha.js';

/**
 * Use with push and fetch to set Basic Authentication headers.
 *
 * @link https://isomorphic-git.github.io/docs/utils_auth.html
 */
function auth (username, password) {
  // Allow specifying it as one argument (mostly for CLI inputability)
  if (password === undefined) {
    let i = username.indexOf(':');
    if (i > -1) {
      password = username.slice(i + 1);
      username = username.slice(0, i);
    } else {
      password = ''; // Enables the .auth(GITHUB_TOKEN) no-username shorthand
    }
  }
  return { username, password }
}

function calculateBasicAuthHeader ({ username, password }) {
  return `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`
}

/**
 * Use with push and fetch to set Basic Authentication headers.
 *
 * @link https://isomorphic-git.github.io/docs/utils_oauth2.html
 */
function oauth2 (company, token) {
  switch (company) {
    case 'github':
      return {
        username: token,
        password: 'x-oauth-basic'
      }
    case 'bitbucket':
      return {
        username: 'x-token-auth',
        password: token
      }
    case 'gitlab':
      return {
        username: 'oauth2',
        password: token
      }
    default:
      throw new Error(
        `I don't know how ${company} expects its Basic Auth headers to be formatted for OAuth2 usage. If you do, you can use the regular '.auth(username, password)' to set the basic auth header yourself.`
      )
  }
}

function calculateBasicAuthUsernamePasswordPair ({
  username,
  password,
  token,
  oauth2format
} = {}) {
  // This checks for the presense and/or absense of each of the 4 parameters,
  // converts that to a 4-bit binary representation, and then handles
  // every possible combination (2^4 or 16 cases) with a lookup table.
  const key = [!!username, !!password, !!token, !!oauth2format]
    .map(Number)
    .join('');
  // See the truth table on https://isomorphic-git.github.io/docs/authentication.html
  // prettier-ignore
  switch (key) {
    case '0000': return null
    case '1000': throw new Error(`Missing password or token`)
    case '0100': throw new Error(`Missing username`)
    case '1100': return { username, password }
    case '0010': return { username: token, password: '' } // Github's alternative format
    case '1010': return { username, password: token }
    case '0110': throw new Error(`Cannot mix 'password' with 'token'`)
    case '1110': throw new Error(`Cannot mix 'username' and 'password' with 'token'`)
    case '0001': throw new Error(`Missing token`)
    case '1001': throw new Error(`Cannot mix 'username' with 'oauth2format'. Missing token.`)
    case '0101': throw new Error(`Cannot mix 'password' with 'oauth2format'. Missing token.`)
    case '1101': throw new Error(`Cannot mix 'username' and 'password' with 'oauth2format'. Missing token.`)
    case '0011': return oauth2(oauth2format, token)
    case '1011': throw new Error(`Cannot mix 'username' with 'oauth2format' and 'token'`)
    case '0111': throw new Error(`Cannot mix 'password' with 'oauth2format' and 'token'`)
    case '1111': throw new Error(`Cannot mix 'username' and 'password' with 'oauth2format' and 'token'`)
  }
}

/*::
type Node = {
  type: string,
  fullpath: string,
  basename: string,
  metadata: Object, // mode, oid
  parent?: Node,
  children: Array<Node>
}
*/

function flatFileListToDirectoryStructure (files) {
  const inodes = new Map();
  const mkdir = function (name) {
    if (!inodes.has(name)) {
      let dir = {
        type: 'tree',
        fullpath: name,
        basename: path.basename(name),
        metadata: {},
        children: []
      };
      inodes.set(name, dir);
      // This recursively generates any missing parent folders.
      // We do it after we've added the inode to the set so that
      // we don't recurse infinitely trying to create the root '.' dirname.
      dir.parent = mkdir(path.dirname(name));
      if (dir.parent && dir.parent !== dir) dir.parent.children.push(dir);
    }
    return inodes.get(name)
  };

  const mkfile = function (name, metadata) {
    if (!inodes.has(name)) {
      let file = {
        type: 'blob',
        fullpath: name,
        basename: path.basename(name),
        metadata: metadata,
        // This recursively generates any missing parent folders.
        parent: mkdir(path.dirname(name)),
        children: []
      };
      if (file.parent) file.parent.children.push(file);
      inodes.set(name, file);
    }
    return inodes.get(name)
  };

  mkdir('.');
  for (let file of files) {
    mkfile(file.path, file);
  }
  return inodes.get('.')
}

const log = debug('isomorphic-git');

log.log = console.log.bind(console);

const pkg = {
  name: 'isomorphic-git',
  version: '0.19.3'
};

async function sleep (ms) {
  return new Promise((resolve, reject) => setTimeout(resolve, ms))
}



var utils = /*#__PURE__*/Object.freeze({
  auth: auth,
  flatFileListToDirectoryStructure: flatFileListToDirectoryStructure,
  log: log,
  oauth2: oauth2,
  pkg: pkg,
  sleep: sleep,
  calculateBasicAuthHeader: calculateBasicAuthHeader,
  calculateBasicAuthUsernamePasswordPair: calculateBasicAuthUsernamePasswordPair
});

const delayedReleases = new Map();
/**
 * This is just a collection of helper functions really. At least that's how it started.
 */
class FileSystem {
  constructor (fs) {
    if (typeof fs._readFile !== 'undefined') return fs
    this._readFile = pify(fs.readFile.bind(fs));
    this._writeFile = pify(fs.writeFile.bind(fs));
    this._mkdir = pify(fs.mkdir.bind(fs));
    this._rmdir = pify(fs.rmdir.bind(fs));
    this._unlink = pify(fs.unlink.bind(fs));
    this._stat = pify(fs.stat.bind(fs));
    this._lstat = pify(fs.lstat.bind(fs));
    this._readdir = pify(fs.readdir.bind(fs));
  }
  /**
   * Return true if a file exists, false if it doesn't exist.
   * Rethrows errors that aren't related to file existance.
   */
  async exists (filepath, options = {}) {
    try {
      await this._stat(filepath);
      return true
    } catch (err) {
      if (err.code === 'ENOENT' || err.code === 'ENOTDIR') {
        return false
      } else {
        console.log('Unhandled error in "FileSystem.exists()" function', err);
        throw err
      }
    }
  }
  /**
   * Return the contents of a file if it exists, otherwise returns null.
   */
  async read (filepath, options = {}) {
    try {
      let buffer = await this._readFile(filepath, options);
      return buffer
    } catch (err) {
      return null
    }
  }
  /**
   * Write a file (creating missing directories if need be) without throwing errors.
   */
  async write (filepath, contents, options = {}) {
    try {
      await this._writeFile(filepath, contents, options);
      return
    } catch (err) {
      // Hmm. Let's try mkdirp and try again.
      await this.mkdir(path.dirname(filepath));
      await this._writeFile(filepath, contents, options);
    }
  }
  /**
   * Make a directory (or series of nested directories) without throwing an error if it already exists.
   */
  async mkdir (filepath) {
    try {
      await this._mkdir(filepath);
      return
    } catch (err) {
      // If err is null then operation succeeded!
      if (err === null) return
      // If the directory already exists, that's OK!
      if (err.code === 'EEXIST') return
      // If we got a "no such file or directory error" backup and try again.
      if (err.code === 'ENOENT') {
        let parent = path.dirname(filepath);
        // Check to see if we've gone too far
        if (parent === '.' || parent === '/' || parent === filepath) throw err
        // Infinite recursion, what could go wrong?
        await this.mkdir(parent);
        await this._mkdir(filepath);
      }
    }
  }
  /**
   * Delete a file without throwing an error if it is already deleted.
   */
  async rm (filepath) {
    try {
      await this._unlink(filepath);
    } catch (err) {
      if (err.code !== 'ENOENT') throw err
    }
  }
  /**
   * Read a directory without throwing an error is the directory doesn't exist
   */
  async readdir (filepath) {
    try {
      return await this._readdir(filepath)
    } catch (err) {
      return []
    }
  }
  /**
   * Return a flast list of all the files nested inside a directory
   *
   * Based on an elegant concurrent recursive solution from SO
   * https://stackoverflow.com/a/45130990/2168416
   */
  async readdirDeep (dir) {
    const subdirs = await this._readdir(dir);
    const files = await Promise.all(
      subdirs.map(async subdir => {
        const res = dir + '/' + subdir;
        return (await this._stat(res)).isDirectory()
          ? this.readdirDeep(res)
          : res
      })
    );
    return files.reduce((a, f) => a.concat(f), [])
  }

  async lock (filename, triesLeft = 3) {
    // check to see if we still have it
    if (delayedReleases.has(filename)) {
      clearTimeout(delayedReleases.get(filename));
      delayedReleases.delete(filename);
      return
    }
    if (triesLeft === 0) {
      throw new Error(
        `Unable to acquire lockfile '${filename}'. Exhausted tries.`
      )
    }
    try {
      await this._mkdir(`${filename}.lock`);
    } catch (err) {
      if (err.code === 'EEXIST') {
        await sleep(100);
        await this.lock(filename, triesLeft - 1);
      }
    }
  }

  async unlock (filename, delayRelease = 50) {
    if (delayedReleases.has(filename)) {
      throw new Error('Cannot double-release lockfile')
    }
    // Basically, we lie and say it was deleted ASAP.
    // But really we wait a bit to see if you want to acquire it again.
    delayedReleases.set(
      filename,
      setTimeout(async () => {
        delayedReleases.delete(filename);
        await this._rmdir(`${filename}.lock`);
      }, delayRelease)
    );
  }
}

// The amount of work that went into crafting these cases to handl
// -0 (just so we don't lose that information when parsing and reconstructing)
// but can also default to +0 was extraordinary.

function simpleSign (n) {
  return Math.sign(n) || (Object.is(n, -0) ? -1 : 1)
}

function negateExceptForZero (n) {
  return n === 0 ? n : -n
}

function formatTimezoneOffset (minutes) {
  let sign = simpleSign(negateExceptForZero(minutes));
  minutes = Math.abs(minutes);
  let hours = Math.floor(minutes / 60);
  minutes -= hours * 60;
  let strHours = String(hours);
  let strMinutes = String(minutes);
  if (strHours.length < 2) strHours = '0' + strHours;
  if (strMinutes.length < 2) strMinutes = '0' + strMinutes;
  return (sign === -1 ? '-' : '+') + strHours + strMinutes
}

function parseTimezoneOffset (offset) {
  let [, sign, hours, minutes] = offset.match(/(\+|-)(\d\d)(\d\d)/);
  minutes = (sign === '+' ? 1 : -1) * (Number(hours) * 60 + Number(minutes));
  return negateExceptForZero(minutes)
}

function parseAuthor (author) {
  let [, name, email, timestamp, offset] = author.match(
    /^(.*) <(.*)> (.*) (.*)$/
  );
  return {
    name: name,
    email: email,
    timestamp: Number(timestamp),
    timezoneOffset: parseTimezoneOffset(offset)
  }
}

function normalize (str) {
  // remove all <CR>
  str = str.replace(/\r/g, '');
  // no extra newlines up front
  str = str.replace(/^\n+/, '');
  // and a single newline at the end
  str = str.replace(/\n+$/, '') + '\n';
  return str
}

function indent (str) {
  return (
    str
      .trim()
      .split('\n')
      .map(x => ' ' + x)
      .join('\n') + '\n'
  )
}

function outdent (str) {
  return str
    .split('\n')
    .map(x => x.replace(/^ /, ''))
    .join('\n')
}

// TODO: Make all functions have static async signature?

class GitCommit {
  constructor (commit) {
    if (typeof commit === 'string') {
      this._commit = commit;
    } else if (Buffer.isBuffer(commit)) {
      this._commit = commit.toString('utf8');
    } else if (typeof commit === 'object') {
      this._commit = GitCommit.render(commit);
    } else {
      throw new Error('invalid type passed to GitCommit constructor')
    }
  }

  static fromPayloadSignature ({ payload, signature }) {
    let headers = GitCommit.justHeaders(payload);
    let message = GitCommit.justMessage(payload);
    let commit = normalize(
      headers + '\ngpgsig' + indent(signature) + '\n' + message
    );
    return new GitCommit(commit)
  }

  static from (commit) {
    return new GitCommit(commit)
  }

  toObject () {
    return Buffer.from(this._commit, 'utf8')
  }

  // Todo: allow setting the headers and message
  headers () {
    return this.parseHeaders()
  }

  // Todo: allow setting the headers and message
  message () {
    return GitCommit.justMessage(this._commit)
  }

  parse () {
    return Object.assign({ message: this.message() }, this.headers())
  }

  static justMessage (commit) {
    return normalize(commit.slice(commit.indexOf('\n\n') + 2))
  }

  static justHeaders (commit) {
    return commit.slice(0, commit.indexOf('\n\n'))
  }

  parseHeaders () {
    let headers = GitCommit.justHeaders(this._commit).split('\n');
    let hs = [];
    for (let h of headers) {
      if (h[0] === ' ') {
        // combine with previous header (without space indent)
        hs[hs.length - 1] += '\n' + h.slice(1);
      } else {
        hs.push(h);
      }
    }
    let obj = {
      parent: []
    };
    for (let h of hs) {
      let key = h.slice(0, h.indexOf(' '));
      let value = h.slice(h.indexOf(' ') + 1);
      if (Array.isArray(obj[key])) {
        obj[key].push(value);
      } else {
        obj[key] = value;
      }
    }
    if (obj.author) {
      obj.author = parseAuthor(obj.author);
    }
    if (obj.committer) {
      obj.committer = parseAuthor(obj.committer);
    }
    return obj
  }

  static renderHeaders (obj) {
    let headers = '';
    if (obj.tree) {
      headers += `tree ${obj.tree}\n`;
    } else {
      headers += `tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n`; // the null tree
    }
    if (obj.parent) {
      if (obj.parent.length === undefined) {
        throw new Error(`commit 'parent' property should be an array`)
      }
      for (let p of obj.parent) {
        headers += `parent ${p}\n`;
      }
    }
    let author = obj.author;
    headers += `author ${author.name} <${author.email}> ${
      author.timestamp
    } ${formatTimezoneOffset(author.timezoneOffset)}\n`;
    let committer = obj.committer || obj.author;
    headers += `committer ${committer.name} <${committer.email}> ${
      committer.timestamp
    } ${formatTimezoneOffset(committer.timezoneOffset)}\n`;
    if (obj.gpgsig) {
      headers += 'gpgsig' + indent(obj.gpgsig);
    }
    return headers
  }

  static render (obj) {
    return GitCommit.renderHeaders(obj) + '\n' + normalize(obj.message)
  }

  render () {
    return this._commit
  }

  withoutSignature () {
    let commit = normalize(this._commit);
    if (commit.indexOf('\ngpgsig') === -1) return commit
    let headers = commit.slice(0, commit.indexOf('\ngpgsig'));
    let message = commit.slice(
      commit.indexOf('-----END PGP SIGNATURE-----\n') +
        '-----END PGP SIGNATURE-----\n'.length
    );
    return normalize(headers + '\n' + message)
  }

  isolateSignature () {
    let signature = this._commit.slice(
      this._commit.indexOf('-----BEGIN PGP SIGNATURE-----'),
      this._commit.indexOf('-----END PGP SIGNATURE-----') +
        '-----END PGP SIGNATURE-----'.length
    );
    return outdent(signature)
  }
}

// This is straight from parse_unit_factor in config.c of canonical git
const num = val => {
  val = val.toLowerCase();
  let n = parseInt(val);
  if (val.endsWith('k')) n *= 1024;
  if (val.endsWith('m')) n *= 1024 * 1024;
  if (val.endsWith('g')) n *= 1024 * 1024 * 1024;
  return n
};

// This is straight from git_parse_maybe_bool_text in config.c of canonical git
const bool = val => {
  val = val.trim().toLowerCase();
  if (val === 'true' || val === 'yes' || val === 'on') return true
  if (val === 'false' || val === 'no' || val === 'off') return false
  throw Error(
    `Expected 'true', 'false', 'yes', 'no', 'on', or 'off', but got ${val}`
  )
};

const schema = {
  core: {
    _named: false,
    repositoryformatversion: String,
    filemode: bool,
    bare: bool,
    logallrefupdates: bool,
    symlinks: bool,
    ignorecase: bool,
    bigFileThreshold: num
  },
  remote: {
    _named: true,
    url: String,
    fetch: String
  },
  branch: {
    _named: true,
    remote: String,
    merge: String
  }
};

const isSection = line => line.trim().startsWith('[');

const extractSection = line => {
  const indices = [line.indexOf(']'), line.indexOf(' ')].filter(i => i > -1);
  return line.slice(line.indexOf('[') + 1, Math.min(...indices)).trim()
};

const isNamedSection = section => schema[section] && schema[section]._named;

const isKeyValuePair = line => line.includes('=');

const extractSectionName = line =>
  line.slice(line.indexOf('"') + 1, line.lastIndexOf('"'));

// Note: there are a LOT of edge cases that aren't covered (e.g. keys in sections that also
// have subsections, [include] directives, etc.
class GitConfig {
  constructor (text) {
    this.lines = text.split('\n');
  }
  static from (text) {
    return new GitConfig(text)
  }
  async get (path$$1, getall = false) {
    const parts = path$$1.split('.');
    const section = parts.shift();
    const sectionName = isNamedSection(section) ? parts.shift() : null;
    const key = parts.shift();

    let currentSection = '';
    let currentSectionName = null;
    let allValues = [];
    for (const line of this.lines) {
      // zero in on section
      if (isSection(line)) {
        currentSection = extractSection(line);
        if (isNamedSection(currentSection)) {
          currentSectionName = extractSectionName(line);
        }
      } else if (
        currentSection === section &&
        (sectionName === null || currentSectionName === sectionName)
      ) {
        if (isKeyValuePair(line)) {
          let [_key, _value] = line.split('=', 2);
          if (_key.trim() === key) {
            allValues.push(_value.trim());
          }
        }
      }
    }
    // Cast value to correct type
    let fn = schema[section] && schema[section][key];
    if (fn) {
      allValues = allValues.map(fn);
    }
    return getall ? allValues : allValues.pop()
  }
  async getall (path$$1) {
    return this.get(path$$1, true)
  }
  async append (path$$1, value) {
    return this.set(path$$1, value, true)
  }
  async set (path$$1, value, append = false) {
    const parts = path$$1.split('.');
    const section = parts.shift();
    const sectionName = isNamedSection(section) ? parts.shift() : null;
    const key = parts.shift();

    let currentSection = '';
    let currentSectionName = null;
    let lastSectionMatch = null;
    let lastMatch = null;
    for (let i = 0; i < this.lines.length; i++) {
      const line = this.lines[i];
      if (isSection(line)) {
        currentSection = extractSection(line);
        if (currentSection === section) {
          if (sectionName) {
            currentSectionName = extractSectionName(line);
          }
          if (currentSectionName === sectionName) {
            lastSectionMatch = i;
          }
        } else {
          currentSectionName = null;
        }
      } else if (
        currentSection === section &&
        (sectionName === null || currentSectionName === sectionName)
      ) {
        if (isKeyValuePair(line)) {
          let [_key] = line.split('=', 1);
          if (_key.trim() === key) {
            lastMatch = i;
          }
        }
      }
    }
    if (lastMatch !== null) {
      if (value === undefined) {
        this.lines.splice(lastMatch, 1);
      } else if (append) {
        this.lines.splice(lastMatch + 1, 0, [`\t${key} = ${value}`]);
      } else {
        this.lines[lastMatch] = `\t${key} = ${value}`;
      }
    } else if (lastSectionMatch !== null) {
      if (value !== undefined) {
        this.lines.splice(lastSectionMatch + 1, 0, [`\t${key} = ${value}`]);
      }
    } else if (value !== undefined) {
      if (sectionName) {
        this.lines.push(`[${section} "${sectionName}"]`);
      } else {
        this.lines.push(`[${section}]`);
      }
      this.lines.push([`\t${key} = ${value}`]);
    }
  }
  toString () {
    return this.lines.join('\n') + '\n'
  }
}

const MAX_UINT32 = 2 ** 32;
/*::
import type {Stats} from 'fs'

type CacheEntryFlags = {
  assumeValid: boolean,
  extended: boolean,
  stage: number,
  nameLength: number
}

type CacheEntry = {
  ctime: Date,
  ctimeNanoseconds?: number,
  mtime: Date,
  mtimeNanoseconds?: number,
  dev: number,
  ino: number,
  mode: number,
  uid: number,
  gid: number,
  size: number,
  oid: string,
  flags: CacheEntryFlags,
  path: string
}
*/

// Extract 1-bit assume-valid, 1-bit extended flag, 2-bit merge state flag, 12-bit path length flag
function parseCacheEntryFlags (bits) {
  return {
    assumeValid: Boolean(bits & 0b1000000000000000),
    extended: Boolean(bits & 0b0100000000000000),
    stage: (bits & 0b0011000000000000) >> 12,
    nameLength: bits & 0b0000111111111111
  }
}

function renderCacheEntryFlags (flags) {
  return (
    (flags.assumeValid ? 0b1000000000000000 : 0) +
    (flags.extended ? 0b0100000000000000 : 0) +
    ((flags.stage & 0b11) << 12) +
    (flags.nameLength & 0b111111111111)
  )
}

function parseBuffer (buffer) {
  // Verify shasum
  let shaComputed = shasum(buffer.slice(0, -20));
  let shaClaimed = buffer.slice(-20).toString('hex');
  if (shaClaimed !== shaComputed) {
    throw new Error(
      `Invalid checksum in GitIndex buffer: expected ${shaClaimed} but saw ${shaComputed}`
    )
  }
  let reader = new BufferCursor(buffer);
  let _entries = new Map();
  let magic = reader.toString('utf8', 4);
  if (magic !== 'DIRC') {
    throw new Error(`Inavlid dircache magic file number: ${magic}`)
  }
  let version = reader.readUInt32BE();
  if (version !== 2) throw new Error(`Unsupported dircache version: ${version}`)
  let numEntries = reader.readUInt32BE();
  let i = 0;
  while (!reader.eof() && i < numEntries) {
    let entry = {};
    let ctimeSeconds = reader.readUInt32BE();
    let ctimeNanoseconds = reader.readUInt32BE();
    entry.ctime = new Date(ctimeSeconds * 1000 + ctimeNanoseconds / 1000000);
    entry.ctimeNanoseconds = ctimeNanoseconds;
    let mtimeSeconds = reader.readUInt32BE();
    let mtimeNanoseconds = reader.readUInt32BE();
    entry.mtime = new Date(mtimeSeconds * 1000 + mtimeNanoseconds / 1000000);
    entry.mtimeNanoseconds = mtimeNanoseconds;
    entry.dev = reader.readUInt32BE();
    entry.ino = reader.readUInt32BE();
    entry.mode = reader.readUInt32BE();
    entry.uid = reader.readUInt32BE();
    entry.gid = reader.readUInt32BE();
    entry.size = reader.readUInt32BE();
    entry.oid = reader.slice(20).toString('hex');
    let flags = reader.readUInt16BE();
    entry.flags = parseCacheEntryFlags(flags);
    // TODO: handle if (version === 3 && entry.flags.extended)
    let pathlength = buffer.indexOf(0, reader.tell() + 1) - reader.tell();
    if (pathlength < 1) throw new Error(`Got a path length of: ${pathlength}`)
    entry.path = reader.toString('utf8', pathlength);
    // The next bit is awkward. We expect 1 to 8 null characters
    let tmp = reader.readUInt8();
    if (tmp !== 0) {
      throw new Error(`Expected 1-8 null characters but got '${tmp}'`)
    }
    let numnull = 1;
    while (!reader.eof() && reader.readUInt8() === 0 && numnull < 9) numnull++;
    reader.seek(reader.tell() - 1);
    // end of awkward part
    _entries.set(entry.path, entry);
    i++;
  }
  return _entries
}

class GitIndex {
  /*::
   _entries: Map<string, CacheEntry>
   _dirty: boolean // Used to determine if index needs to be saved to filesystem
   */
  constructor (index) {
    this._dirty = false;
    if (Buffer.isBuffer(index)) {
      this._entries = parseBuffer(index);
    } else if (index === null) {
      this._entries = new Map();
    } else {
      throw new Error('invalid type passed to GitIndex constructor')
    }
  }
  static from (buffer) {
    return new GitIndex(buffer)
  }
  get entries () {
    return sortby([...this._entries.values()], 'path')
  }
  * [Symbol.iterator] () {
    for (let entry of this.entries) {
      yield entry;
    }
  }
  insert ({ filepath, stats, oid }) {
    let entry = {
      ctime: stats.ctime,
      mtime: stats.mtime,
      dev: stats.dev % MAX_UINT32,
      ino: stats.ino % MAX_UINT32,
      // We provide a fallback value for `mode` here because not all fs
      // implementations assign it, but we use it in GitTree.
      // '100644' is for a "regular non-executable file"
      mode: (stats.mode || 0o100644) % MAX_UINT32,
      uid: stats.uid % MAX_UINT32,
      gid: stats.gid % MAX_UINT32,
      size: stats.size % MAX_UINT32,
      path: filepath,
      oid: oid,
      flags: {
        assumeValid: false,
        extended: false,
        stage: 0,
        nameLength: filepath.length < 0xfff ? filepath.length : 0xfff
      }
    };
    this._entries.set(entry.path, entry);
    this._dirty = true;
  }
  delete ({ filepath }) {
    if (this._entries.has(filepath)) {
      this._entries.delete(filepath);
    } else {
      for (let key of this._entries.keys()) {
        if (key.startsWith(filepath + '/')) {
          this._entries.delete(key);
        }
      }
    }
    this._dirty = true;
  }
  clear () {
    this._entries.clear();
    this._dirty = true;
  }
  render () {
    return this.entries
      .map(entry => `${entry.mode.toString(8)} ${entry.oid}    ${entry.path}`)
      .join('\n')
  }
  toObject () {
    let header = Buffer.alloc(12);
    let writer = new BufferCursor(header);
    writer.write('DIRC', 4, 'utf8');
    writer.writeUInt32BE(2);
    writer.writeUInt32BE(this.entries.length);
    let body = Buffer.concat(
      this.entries.map(entry => {
        // the fixed length + the filename + at least one null char => align by 8
        let length = Math.ceil((62 + entry.path.length + 1) / 8) * 8;
        let written = Buffer.alloc(length);
        let writer = new BufferCursor(written);
        let ctimeMilliseconds = entry.ctime.valueOf();
        let ctimeSeconds = Math.floor(ctimeMilliseconds / 1000);
        let ctimeNanoseconds =
          entry.ctimeNanoseconds ||
          ctimeMilliseconds * 1000000 - ctimeSeconds * 1000000 * 1000;
        let mtimeMilliseconds = entry.mtime.valueOf();
        let mtimeSeconds = Math.floor(mtimeMilliseconds / 1000);
        let mtimeNanoseconds =
          entry.mtimeNanoseconds ||
          mtimeMilliseconds * 1000000 - mtimeSeconds * 1000000 * 1000;
        writer.writeUInt32BE(ctimeSeconds % MAX_UINT32);
        writer.writeUInt32BE(ctimeNanoseconds % MAX_UINT32);
        writer.writeUInt32BE(mtimeSeconds % MAX_UINT32);
        writer.writeUInt32BE(mtimeNanoseconds % MAX_UINT32);
        writer.writeUInt32BE(entry.dev % MAX_UINT32);
        writer.writeUInt32BE(entry.ino % MAX_UINT32);
        writer.writeUInt32BE(entry.mode % MAX_UINT32);
        writer.writeUInt32BE(entry.uid % MAX_UINT32);
        writer.writeUInt32BE(entry.gid % MAX_UINT32);
        writer.writeUInt32BE(entry.size % MAX_UINT32);
        writer.write(entry.oid, 20, 'hex');
        writer.writeUInt16BE(renderCacheEntryFlags(entry.flags));
        writer.write(entry.path, entry.path.length, 'utf8');
        return written
      })
    );
    let main = Buffer.concat([header, body]);
    let sum = shasum(main);
    return Buffer.concat([main, Buffer.from(sum, 'hex')])
  }
}

class GitObject {
  static hash ({ type, object }) {
    let buffer = Buffer.concat([
      Buffer.from(`${type} ${object.byteLength.toString()}\0`),
      Buffer.from(object)
    ]);
    let oid = shasum(buffer);
    return oid
  }
  static wrap ({ type, object }) {
    let buffer = Buffer.concat([
      Buffer.from(`${type} ${object.byteLength.toString()}\0`),
      object
    ]);
    let oid = shasum(buffer);
    return {
      oid,
      buffer
    }
  }
  static unwrap ({ oid, buffer }) {
    if (oid) {
      let sha = shasum(buffer);
      if (sha !== oid) {
        throw new Error(`SHA check failed! Expected ${oid}, computed ${sha}`)
      }
    }
    let s = buffer.indexOf(32); // first space
    let i = buffer.indexOf(0); // first null value
    let type = buffer.slice(0, s).toString('utf8'); // get type of object
    let length = buffer.slice(s + 1, i).toString('utf8'); // get type of object
    let actualLength = buffer.length - (i + 1);
    // verify length
    if (parseInt(length) !== actualLength) {
      throw new Error(
        `Length mismatch: expected ${length} bytes but got ${actualLength} instead.`
      )
    }
    return {
      type,
      object: Buffer.from(buffer.slice(i + 1))
    }
  }
}

function buffer2stream (buffer) {
  let stream = new PassThrough();
  stream.end(buffer);
  return stream
}

function decodeVarInt (reader) {
  let bytes = [];
  let byte = 0;
  let multibyte = 0;
  do {
    byte = reader.readUInt8();
    // We keep bits 6543210
    const lastSeven = byte & 0b01111111;
    bytes.push(lastSeven);
    // Whether the next byte is part of the variable-length encoded number
    // is encoded in bit 7
    multibyte = byte & 0b10000000;
  } while (multibyte)
  // Now that all the bytes are in big-endian order,
  // alternate shifting the bits left by 7 and OR-ing the next byte.
  // And... do a weird increment-by-one thing that I don't quite understand.
  return bytes.reduce((a, b) => ((a + 1) << 7) | b, -1)
}

// I'm pretty much copying this one from the git C source code,
// because it makes no sense.
function otherVarIntDecode (reader, startWith) {
  let result = startWith;
  let shift = 4;
  let byte = null;
  do {
    byte = reader.readUInt8();
    result |= (byte & 0b01111111) << shift;
    shift += 7;
  } while (byte & 0b10000000)
  return result
}

class GitPackIndex {
  constructor (stuff) {
    Object.assign(this, stuff);
    this.offsetCache = {};
  }
  static async fromIdx ({ idx, getExternalRefDelta }) {
    let reader = new BufferCursor(idx);
    let magic = reader.slice(4).toString('hex');
    // Check for IDX v2 magic number
    if (magic !== 'ff744f63') {
      return // undefined
    }
    let version = reader.readUInt32BE();
    if (version !== 2) {
      throw new Error(
        `Unable to read version ${version} packfile IDX. (Only version 2 supported)`
      )
    }
    // Verify checksums
    let shaComputed = shasum(idx.slice(0, -20));
    let shaClaimed = idx.slice(-20).toString('hex');
    if (shaClaimed !== shaComputed) {
      throw new Error(
        `Invalid checksum in IDX buffer: expected ${shaClaimed} but saw ${shaComputed}`
      )
    }
    if (idx.byteLength > 2048 * 1024 * 1024) {
      throw new Error(
        `To keep implementation simple, I haven't implemented the layer 5 feature needed to support packfiles > 2GB in size.`
      )
    }
    let fanout = [];
    for (let i = 0; i < 256; i++) {
      fanout.push(reader.readUInt32BE());
    }
    let size = fanout[255];
    // For now we'll parse the whole thing. We can optimize later if we need to.
    let hashes = [];
    for (let i = 0; i < size; i++) {
      hashes.push(reader.slice(20).toString('hex'));
    }
    let crcs = {};
    for (let i = 0; i < size; i++) {
      crcs[hashes[i]] = reader.readUInt32BE();
    }
    let offsets = {};
    for (let i = 0; i < size; i++) {
      offsets[hashes[i]] = reader.readUInt32BE();
    }
    let packfileSha = reader.slice(20).toString('hex');
    return new GitPackIndex({
      hashes,
      crcs,
      offsets,
      packfileSha,
      getExternalRefDelta
    })
  }
  static async fromPack ({ pack, getExternalRefDelta }) {
    const listpackTypes = {
      1: 'commit',
      2: 'tree',
      3: 'blob',
      4: 'tag',
      6: 'ofs-delta',
      7: 'ref-delta'
    };
    let offsetToObject = {};

    // Older packfiles do NOT use the shasum of the pack itself,
    // so it is recommended to just use whatever bytes are in the trailer.
    // Source: https://github.com/git/git/commit/1190a1acf800acdcfd7569f87ac1560e2d077414
    // let packfileSha = shasum(pack.slice(0, -20))
    let packfileSha = pack.slice(-20).toString('hex');

    let hashes = [];
    let crcs = {};
    let offsets = {};
    let totalObjectCount = null;
    let lastPercent = null;
    let times = {
      hash: 0,
      readSlice: 0,
      offsets: 0,
      crcs: 0,
      sort: 0
    };
    let histogram = {
      commit: 0,
      tree: 0,
      blob: 0,
      tag: 0,
      'ofs-delta': 0,
      'ref-delta': 0
    };
    let bytesProcessed = 0;

    log('Indexing objects');
    log(
      `percent\tmilliseconds\tbytesProcessed\tcommits\ttrees\tblobs\ttags\tofs-deltas\tref-deltas`
    );
    mark('total');
    mark('offsets');
    mark('percent');
    await new Promise((resolve, reject) => {
      buffer2stream(pack)
        .pipe(listpack())
        .on('data', async ({ data, type, reference, offset, num }) => {
          if (totalObjectCount === null) totalObjectCount = num;
          let percent = Math.floor(
            (totalObjectCount - num) * 100 / totalObjectCount
          );
          if (percent !== lastPercent) {
            log(
              `${percent}%\t${Math.floor(
                stop('percent').duration
              )}\t${bytesProcessed}\t${histogram.commit}\t${histogram.tree}\t${
                histogram.blob
              }\t${histogram.tag}\t${histogram['ofs-delta']}\t${
                histogram['ref-delta']
              }`
            );

            histogram = {
              commit: 0,
              tree: 0,
              blob: 0,
              tag: 0,
              'ofs-delta': 0,
              'ref-delta': 0
            };
            bytesProcessed = 0;
            mark('percent');
          }
          lastPercent = percent;
          // Change type from a number to a meaningful string
          type = listpackTypes[type];

          histogram[type]++;
          bytesProcessed += data.byteLength;

          if (['commit', 'tree', 'blob', 'tag'].includes(type)) {
            offsetToObject[offset] = {
              type,
              offset
            };
          } else if (type === 'ofs-delta') {
            offsetToObject[offset] = {
              type,
              offset
            };
          } else if (type === 'ref-delta') {
            offsetToObject[offset] = {
              type,
              offset
            };
          }
          if (num === 0) resolve();
        });
    });
    times['offsets'] = Math.floor(stop('offsets').duration);

    log('Computing CRCs');
    mark('crcs');
    // We need to know the lengths of the slices to compute the CRCs.
    let offsetArray = Object.keys(offsetToObject).map(Number);
    for (let [i, start] of offsetArray.entries()) {
      let end =
        i + 1 === offsetArray.length ? pack.byteLength - 20 : offsetArray[i + 1];
      let o = offsetToObject[start];
      let crc = crc32(pack.slice(start, end));
      o.end = end;
      o.crc = crc;
    }
    times['crcs'] = Math.floor(stop('crcs').duration);

    // We don't have the hashes yet. But we can generate them using the .readSlice function!
    const p = new GitPackIndex({
      pack,
      packfileSha,
      crcs,
      hashes,
      offsets,
      getExternalRefDelta
    });

    // Resolve deltas and compute the oids
    log('Resolving deltas');
    log(`percent2\tmilliseconds2\tcallsToReadSlice\tcallsToGetExternal`);
    mark('percent');
    lastPercent = null;
    let count = 0;
    let callsToReadSlice = 0;
    let callsToGetExternal = 0;
    let timeByDepth = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
    let objectsByDepth = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
    for (let offset in offsetToObject) {
      offset = Number(offset);
      let percent = Math.floor(count++ * 100 / totalObjectCount);
      if (percent !== lastPercent) {
        log(
          `${percent}%\t${Math.floor(
            stop('percent').duration
          )}\t${callsToReadSlice}\t${callsToGetExternal}`
        );
        mark('percent');
        callsToReadSlice = 0;
        callsToGetExternal = 0;
      }
      lastPercent = percent;

      let o = offsetToObject[offset];
      if (o.oid) continue
      try {
        p.readDepth = 0;
        p.externalReadDepth = 0;
        mark('readSlice');
        let { type, object } = await p.readSlice({ start: offset });
        let time = stop('readSlice').duration;
        times.readSlice += time;
        callsToReadSlice += p.readDepth;
        callsToGetExternal += p.externalReadDepth;
        timeByDepth[p.readDepth] += time;
        objectsByDepth[p.readDepth] += 1;
        mark('hash');
        let oid = GitObject.hash({ type, object });
        times.hash += stop('hash').duration;
        o.oid = oid;
        hashes.push(oid);
        offsets[oid] = offset;
        crcs[oid] = o.crc;
      } catch (err) {
        log('ERROR', err);
        continue
      }
    }

    mark('sort');
    hashes.sort();
    times['sort'] = Math.floor(stop('sort').duration);
    let totalElapsedTime = stop('total').duration;
    times.hash = Math.floor(times.hash);
    times.readSlice = Math.floor(times.readSlice);
    times.misc = Math.floor(
      Object.values(times).reduce((a, b) => a - b, totalElapsedTime)
    );
    log(Object.keys(times).join('\t'));
    log(Object.values(times).join('\t'));
    log('by depth:');
    log([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].join('\t'));
    log(objectsByDepth.slice(0, 12).join('\t'));
    log(
      timeByDepth
        .map(Math.floor)
        .slice(0, 12)
        .join('\t')
    );
    return p
  }
  toBuffer () {
    let buffers = [];
    let write = (str, encoding) => {
      buffers.push(Buffer.from(str, encoding));
    };
    // Write out IDX v2 magic number
    write('ff744f63', 'hex');
    // Write out version number 2
    write('00000002', 'hex');
    // Write fanout table
    let fanoutBuffer = new BufferCursor(Buffer.alloc(256 * 4));
    for (let i = 0; i < 256; i++) {
      let count = 0;
      for (let hash of this.hashes) {
        if (parseInt(hash.slice(0, 2), 16) <= i) count++;
      }
      fanoutBuffer.writeUInt32BE(count);
    }
    buffers.push(fanoutBuffer.buffer);
    // Write out hashes
    for (let hash of this.hashes) {
      write(hash, 'hex');
    }
    // Write out crcs
    let crcsBuffer = new BufferCursor(Buffer.alloc(this.hashes.length * 4));
    for (let hash of this.hashes) {
      crcsBuffer.writeUInt32BE(this.crcs[hash]);
    }
    buffers.push(crcsBuffer.buffer);
    // Write out offsets
    let offsetsBuffer = new BufferCursor(Buffer.alloc(this.hashes.length * 4));
    for (let hash of this.hashes) {
      offsetsBuffer.writeUInt32BE(this.offsets[hash]);
    }
    buffers.push(offsetsBuffer.buffer);
    // Write out packfile checksum
    write(this.packfileSha, 'hex');
    // Write out shasum
    let totalBuffer = Buffer.concat(buffers);
    let sha = shasum(totalBuffer);
    let shaBuffer = Buffer.alloc(20);
    shaBuffer.write(sha, 'hex');
    return Buffer.concat([totalBuffer, shaBuffer])
  }
  async load ({ pack }) {
    this.pack = pack;
  }
  async unload () {
    this.pack = null;
  }
  async read ({ oid }) {
    if (!this.offsets[oid]) {
      if (this.getExternalRefDelta) {
        this.externalReadDepth++;
        return this.getExternalRefDelta(oid)
      } else {
        throw new Error(`Could not read object ${oid} from packfile`)
      }
    }
    let start = this.offsets[oid];
    return this.readSlice({ start })
  }
  async readSlice ({ start }) {
    if (this.offsetCache[start]) return this.offsetCache[start]
    this.readDepth++;
    const types = {
      0b0010000: 'commit',
      0b0100000: 'tree',
      0b0110000: 'blob',
      0b1000000: 'tag',
      0b1100000: 'ofs_delta',
      0b1110000: 'ref_delta'
    };
    if (!this.pack) {
      throw new Error(
        'Tried to read from a GitPackIndex with no packfile loaded into memory'
      )
    }
    let raw = this.pack.slice(start);
    let reader = new BufferCursor(raw);
    let byte = reader.readUInt8();
    // Object type is encoded in bits 654
    let btype = byte & 0b1110000;
    let type = types[btype];
    if (type === undefined) {
      throw new Error('Unrecognized type: 0b' + btype.toString(2))
    }
    // The length encoding get complicated.
    // Last four bits of length is encoded in bits 3210
    let lastFour = byte & 0b1111;
    let length = lastFour;
    // Whether the next byte is part of the variable-length encoded number
    // is encoded in bit 7
    let multibyte = byte & 0b10000000;
    if (multibyte) {
      length = otherVarIntDecode(reader, lastFour);
    }
    let base = null;
    let object = null;
    // Handle deltified objects
    if (type === 'ofs_delta') {
      let offset = decodeVarInt(reader);
      let baseOffset = start - offset
      ;({ object: base, type } = await this.readSlice({ start: baseOffset }));
    }
    if (type === 'ref_delta') {
      let oid = reader.slice(20).toString('hex')
      ;({ object: base, type } = await this.read({ oid }));
    }
    // Handle undeltified objects
    let buffer = raw.slice(reader.tell());
    object = Buffer.from(pako.inflate(buffer));
    // Assert that the object length is as expected.
    if (object.byteLength !== length) {
      throw new Error(
        `Packfile told us object would have length ${length} but it had length ${
          object.byteLength
        }`
      )
    }
    if (base) {
      object = Buffer.from(applyDelta(object, base));
    }
    // Cache the result based on depth.
    if (this.readDepth > 3) {
      // hand tuned for speed / memory usage tradeoff
      this.offsetCache[start] = { type, object };
    }
    return { type, format: 'content', object }
  }
}

/**
pkt-line Format
---------------

Much (but not all) of the payload is described around pkt-lines.

A pkt-line is a variable length binary string.  The first four bytes
of the line, the pkt-len, indicates the total length of the line,
in hexadecimal.  The pkt-len includes the 4 bytes used to contain
the length's hexadecimal representation.

A pkt-line MAY contain binary data, so implementors MUST ensure
pkt-line parsing/formatting routines are 8-bit clean.

A non-binary line SHOULD BE terminated by an LF, which if present
MUST be included in the total length. Receivers MUST treat pkt-lines
with non-binary data the same whether or not they contain the trailing
LF (stripping the LF if present, and not complaining when it is
missing).

The maximum length of a pkt-line's data component is 65516 bytes.
Implementations MUST NOT send pkt-line whose length exceeds 65520
(65516 bytes of payload + 4 bytes of length data).

Implementations SHOULD NOT send an empty pkt-line ("0004").

A pkt-line with a length field of 0 ("0000"), called a flush-pkt,
is a special case and MUST be handled differently than an empty
pkt-line ("0004").

----
  pkt-line     =  data-pkt / flush-pkt

  data-pkt     =  pkt-len pkt-payload
  pkt-len      =  4*(HEXDIG)
  pkt-payload  =  (pkt-len - 4)*(OCTET)

  flush-pkt    = "0000"
----

Examples (as C-style strings):

----
  pkt-line          actual value
  ---------------------------------
  "0006a\n"         "a\n"
  "0005a"           "a"
  "000bfoobar\n"    "foobar\n"
  "0004"            ""
----
*/

// I'm really using this more as a namespace.
// There's not a lot of "state" in a pkt-line

class GitPktLine {
  static flush () {
    return Buffer.from('0000', 'utf8')
  }

  static encode (line) {
    if (typeof line === 'string') {
      line = Buffer.from(line);
    }
    let length = line.length + 4;
    let hexlength = pad(4, length.toString(16), '0');
    return Buffer.concat([Buffer.from(hexlength, 'utf8'), line])
  }

  static reader (buffer) {
    let buffercursor = new BufferCursor(buffer);
    return async function read () {
      if (buffercursor.eof()) return true
      let length = parseInt(buffercursor.slice(4).toString('utf8'), 16);
      if (length === 0) return null
      return buffercursor.slice(length - 4).buffer
    }
  }

  static streamReader (stream) {
    const bufferstream = streamSource(stream);
    return async function read () {
      try {
        let length = await bufferstream.slice(4);
        if (length === null) return true
        length = parseInt(length.toString('utf8'), 16);
        if (length === 0) return null
        let buffer = await bufferstream.slice(length - 4);
        if (buffer === null) return true
        return buffer
      } catch (err) {
        console.log('error', err);
        return true
      }
    }
  }
}

class GitRefSpec {
  constructor ({ remotePath, localPath, force, matchPrefix }) {
    Object.assign(this, {
      remotePath,
      localPath,
      force,
      matchPrefix
    });
  }
  static from (refspec) {
    const [
      forceMatch,
      remotePath,
      remoteGlobMatch,
      localPath,
      localGlobMatch
    ] = refspec.match(/^(\+?)(.*?)(\*?):(.*?)(\*?)$/).slice(1);
    const force = forceMatch === '+';
    const remoteIsGlob = remoteGlobMatch === '*';
    const localIsGlob = localGlobMatch === '*';
    // validate
    // TODO: Make this check more nuanced, and depend on whether this is a fetch refspec or a push refspec
    if (remoteIsGlob !== localIsGlob) throw new Error('Invalid refspec')
    return new GitRefSpec({
      remotePath,
      localPath,
      force,
      matchPrefix: remoteIsGlob
    })
    // TODO: We need to run resolveRef on both paths to expand them to their full name.
  }
  translate (remoteBranch) {
    if (this.matchPrefix) {
      if (remoteBranch.startsWith(this.remotePath)) {
        return this.localPath + remoteBranch.replace(this.remotePath, '')
      }
    } else {
      if (remoteBranch === this.remotePath) return this.localPath
    }
    return null
  }
}

class GitRefSpecSet {
  constructor (rules = []) {
    this.rules = rules;
  }
  static from (refspecs) {
    const rules = [];
    for (const refspec of refspecs) {
      rules.push(GitRefSpec.from(refspec)); // might throw
    }
    return new GitRefSpecSet(rules)
  }
  add (refspec) {
    const rule = GitRefSpec.from(refspec); // might throw
    this.rules.push(rule);
  }
  translate (remoteRefs) {
    const result = [];
    for (const rule of this.rules) {
      for (const remoteRef of remoteRefs) {
        const localRef = rule.translate(remoteRef);
        if (localRef) {
          result.push([remoteRef, localRef]);
        }
      }
    }
    return result
  }
  translateOne (remoteRef) {
    let result = null;
    for (const rule of this.rules) {
      const localRef = rule.translate(remoteRef);
      if (localRef) {
        result = localRef;
      }
    }
    return result
  }
}

/*::
type TreeEntry = {
  mode: string,
  path: string,
  oid: string,
  type?: string
}
*/

function parseBuffer$1 (buffer) {
  let _entries = [];
  let cursor = 0;
  while (cursor < buffer.length) {
    let space = buffer.indexOf(32, cursor);
    if (space === -1) {
      throw new Error(
        `GitTree: Error parsing buffer at byte location ${cursor}: Could not find the next space character.`
      )
    }
    let nullchar = buffer.indexOf(0, cursor);
    if (nullchar === -1) {
      throw new Error(
        `GitTree: Error parsing buffer at byte location ${cursor}: Could not find the next null character.`
      )
    }
    let mode = buffer.slice(cursor, space).toString('utf8');
    if (mode === '40000') mode = '040000'; // makes it line up neater in printed output
    let type = mode === '040000' ? 'tree' : 'blob';
    let path$$1 = buffer.slice(space + 1, nullchar).toString('utf8');
    let oid = buffer.slice(nullchar + 1, nullchar + 21).toString('hex');
    cursor = nullchar + 21;
    _entries.push({ mode, path: path$$1, oid, type });
  }
  return _entries
}

function limitModeToAllowed (mode) {
  if (typeof mode === 'number') {
    mode = mode.toString(8);
  }
  // tree
  if (mode.match(/^0?4.*/)) return '40000' // Directory
  if (mode.match(/^1006.*/)) return '100644' // Regular non-executable file
  if (mode.match(/^1007.*/)) return '100755' // Regular executable file
  if (mode.match(/^120.*/)) return '120000' // Symbolic link
  if (mode.match(/^160.*/)) return '160000' // Commit (git submodule reference)
  throw new Error(`Could not understand file mode: ${mode}`)
}

function nudgeIntoShape (entry) {
  if (!entry.oid && entry.sha) {
    entry.oid = entry.sha; // Github
  }
  entry.mode = limitModeToAllowed(entry.mode); // index
  if (!entry.type) {
    entry.type = 'blob'; // index
  }
  return entry
}

class GitTree {
  /*::
  _entries: Array<TreeEntry>
  */
  constructor (entries) {
    if (Buffer.isBuffer(entries)) {
      this._entries = parseBuffer$1(entries);
    } else if (Array.isArray(entries)) {
      this._entries = entries.map(nudgeIntoShape);
    } else {
      throw new Error('invalid type passed to GitTree constructor')
    }
  }
  static from (tree) {
    return new GitTree(tree)
  }
  render () {
    return this._entries
      .map(entry => `${entry.mode} ${entry.type} ${entry.oid}    ${entry.path}`)
      .join('\n')
  }
  toObject () {
    return Buffer.concat(
      this._entries.map(entry => {
        let mode = Buffer.from(entry.mode.replace(/^0/, ''));
        let space = Buffer.from(' ');
        let path$$1 = Buffer.from(entry.path, { encoding: 'utf8' });
        let nullchar = Buffer.from([0]);
        let oid = Buffer.from(entry.oid.match(/../g).map(n => parseInt(n, 16)));
        return Buffer.concat([mode, space, path$$1, nullchar, oid])
      })
    )
  }
  entries () {
    return this._entries
  }
  * [Symbol.iterator] () {
    for (let entry of this._entries) {
      yield entry;
    }
  }
}

function normalize$1 (str) {
  // remove all <CR>
  str = str.replace(/\r/g, '');
  // no extra newlines up front
  str = str.replace(/^\n+/, '');
  // and a single newline at the end
  str = str.replace(/\n+$/, '') + '\n';
  return str
}

function indent$1 (str) {
  return (
    str
      .trim()
      .split('\n')
      .map(x => ' ' + x)
      .join('\n') + '\n'
  )
}

class SignedGitCommit extends GitCommit {
  static from (commit) {
    return new SignedGitCommit(commit)
  }
  async sign (openpgp, privateKeys) {
    let commit = this.withoutSignature();
    let headers = GitCommit.justHeaders(this._commit);
    let message = GitCommit.justMessage(this._commit);
    let privKeyObj = openpgp.key.readArmored(privateKeys).keys;
    let { signature } = await openpgp.sign({
      data: openpgp.util.str2Uint8Array(commit),
      privateKeys: privKeyObj,
      detached: true,
      armor: true
    });
    // renormalize the line endings to the one true line-ending
    signature = normalize$1(signature);
    let signedCommit =
      headers + '\n' + 'gpgsig' + indent$1(signature) + '\n' + message;
    // return a new commit object
    return GitCommit.from(signedCommit)
  }

  async listSigningKeys (openpgp) {
    let msg = openpgp.message.readSignedContent(
      this.withoutSignature(),
      this.isolateSignature()
    );
    return msg.getSigningKeyIds().map(keyid => keyid.toHex())
  }

  async verify (openpgp, publicKeys) {
    let pubKeyObj = openpgp.key.readArmored(publicKeys).keys;
    let msg = openpgp.message.readSignedContent(
      this.withoutSignature(),
      this.isolateSignature()
    );
    let results = msg.verify(pubKeyObj);
    let validity = results.reduce((a, b) => a.valid && b.valid, { valid: true });
    return validity
  }
}



var models = /*#__PURE__*/Object.freeze({
  FileSystem: FileSystem,
  GitCommit: GitCommit,
  GitConfig: GitConfig,
  GitIndex: GitIndex,
  GitObject: GitObject,
  GitPackIndex: GitPackIndex,
  GitPktLine: GitPktLine,
  GitRefSpec: GitRefSpec,
  GitRefSpecSet: GitRefSpecSet,
  GitTree: GitTree,
  SignedGitCommit: SignedGitCommit
});

class GitConfigManager {
  static async get ({ fs: _fs, gitdir }) {
    const fs = new FileSystem(_fs);
    // We can improve efficiency later if needed.
    // TODO: read from full list of git config files
    let text = await fs.read(`${gitdir}/config`, { encoding: 'utf8' });
    return GitConfig.from(text)
  }
  static async save ({ fs: _fs, gitdir, config }) {
    const fs = new FileSystem(_fs);
    // We can improve efficiency later if needed.
    // TODO: handle saving to the correct global/user/repo location
    await fs.write(`${gitdir}/config`, config.toString(), {
      encoding: 'utf8'
    });
  }
}

// I'm putting this in a Manager because I reckon it could benefit
// from a LOT of cacheing.

// TODO: Implement .git/info/exclude

class GitIgnoreManager {
  static async isIgnored ({
    fs: _fs,
    dir,
    gitdir = path.join(dir, '.git'),
    filepath
  }) {
    const fs = new FileSystem(_fs);
    let pairs = [
      {
        gitignore: path.join(dir, '.gitignore'),
        filepath
      }
    ];
    let pieces = filepath.split('/');
    for (let i = 1; i < pieces.length; i++) {
      let folder = pieces.slice(0, i).join('/');
      let file = pieces.slice(i).join('/');
      pairs.push({
        gitignore: path.join(dir, folder, '.gitignore'),
        filepath: file
      });
    }
    let ignoredStatus = false;
    for (let p of pairs) {
      let file;
      try {
        file = await fs.read(p.gitignore, 'utf8');
      } catch (err) {
        if (err.code === 'NOENT') continue
      }
      let ign = ignore().add(file);
      let unign = ignore().add(`**\n${file}`);
      // If the parent directory is excluded, we are done.
      // "It is not possible to re-include a file if a parent directory of that file is excluded. Git doesn’t list excluded directories for performance reasons, so any patterns on contained files have no effect, no matter where they are defined."
      // source: https://git-scm.com/docs/gitignore
      let parentdir = path.dirname(p.filepath);
      if (ign.ignores(parentdir)) return true
      // If the file is currently ignored, test for UNignoring.
      if (ignoredStatus) {
        ignoredStatus = unign.ignores(p.filepath);
      } else {
        ignoredStatus = ign.ignores(p.filepath);
      }
    }
    return ignoredStatus
  }
}

// import LockManager from 'travix-lock-manager'

// import Lock from '../utils'

// TODO: replace with an LRU cache?
const map = new Map();
// const lm = new LockManager()
const lock = new AsyncLock();

class GitIndexManager {
  static async acquire ({ fs: _fs, filepath }, closure) {
    const fs = new FileSystem(_fs);
    await lock.acquire(filepath, async function () {
      let index = map.get(filepath);
      if (index === undefined) {
        // Acquire a file lock while we're reading the index
        // to make sure other processes aren't writing to it
        // simultaneously, which could result in a corrupted index.
        // const fileLock = await Lock(filepath)
        const rawIndexFile = await fs.read(filepath);
        index = GitIndex.from(rawIndexFile);
        // cache the GitIndex object so we don't need to re-read it
        // every time.
        // TODO: save the stat data for the index so we know whether
        // the cached file is stale (modified by an outside process).
        map.set(filepath, index);
        // await fileLock.cancel()
      }
      await closure(index);
      if (index._dirty) {
        // Acquire a file lock while we're writing the index file
        // let fileLock = await Lock(filepath)
        const buffer = index.toObject();
        await fs.write(filepath, buffer);
        index._dirty = false;
      }
      // For now, discard our cached object so that external index
      // manipulation is picked up. TODO: use lstat and compare
      // file times to determine if our cached object should be
      // discarded.
      map.delete(filepath);
    });
  }
}

const PackfileCache = new Map();

class GitObjectManager {
  static async read ({ fs: _fs, gitdir, oid, format = 'content' }) {
    const fs = new FileSystem(_fs);
    // Look for it in the loose object directory.
    let file = await fs.read(
      `${gitdir}/objects/${oid.slice(0, 2)}/${oid.slice(2)}`
    );
    let source = `./objects/${oid.slice(0, 2)}/${oid.slice(2)}`;
    // Check to see if it's in a packfile.
    if (!file) {
      // Curry the current read method so that the packfile un-deltification
      // process can acquire external ref-deltas.
      const getExternalRefDelta = oid =>
        GitObjectManager.read({ fs: _fs, gitdir, oid });
      // Iterate through all the .pack files
      let list = await fs.readdir(path.join(gitdir, '/objects/pack'));
      list = list.filter(x => x.endsWith('.pack'));
      for (let filename of list) {
        // Try to get the packfile from the in-memory cache
        let p = PackfileCache.get(filename);
        if (!p) {
          // If not there, load it from a .idx file
          const idxName = filename.replace(/pack$/, 'idx');
          if (await fs.exists(`${gitdir}/objects/pack/${idxName}`)) {
            const idx = await fs.read(`${gitdir}/objects/pack/${idxName}`);
            p = await GitPackIndex.fromIdx({ idx, getExternalRefDelta });
          } else {
            // If the .idx file isn't available, generate one.
            const pack = await fs.read(`${gitdir}/objects/pack/${filename}`);
            p = await GitPackIndex.fromPack({ pack, getExternalRefDelta });
            // Save .idx file
            await fs.write(`${gitdir}/objects/pack/${idxName}`, p.toBuffer());
          }
          PackfileCache.set(filename, p);
        }
        // console.log(p)
        // If the packfile DOES have the oid we're looking for...
        if (p.hashes.includes(oid)) {
          // Make sure the packfile is loaded in memory
          if (!p.pack) {
            const pack = await fs.read(`${gitdir}/objects/pack/${filename}`);
            await p.load({ pack });
          }
          // Get the resolved git object from the packfile
          let result = await p.read({ oid, getExternalRefDelta });
          result.source = `./objects/pack/${filename}`;
          return result
        }
      }
    }
    // Check to see if it's in shallow commits.
    if (!file) {
      let text = await fs.read(`${gitdir}/shallow`, { encoding: 'utf8' });
      if (text !== null && text.includes(oid)) {
        throw new Error(
          `Failed to read git object with oid ${oid} because it is a shallow commit`
        )
      }
    }
    // Finally
    if (!file) {
      throw new Error(`Failed to read git object with oid ${oid}`)
    }
    if (format === 'deflated') {
      return { format: 'deflated', object: file, source }
    }
    let buffer = Buffer.from(pako.inflate(file));
    if (format === 'wrapped') {
      return { format: 'wrapped', object: buffer, source }
    }
    let { type, object } = GitObject.unwrap({ oid, buffer });
    if (format === 'content') return { type, format: 'content', object, source }
  }

  static async hash ({ gitdir, type, object }) {
    let buffer = Buffer.concat([
      Buffer.from(type + ' '),
      Buffer.from(object.byteLength.toString()),
      Buffer.from([0]),
      Buffer.from(object)
    ]);
    let oid = shasum(buffer);
    return oid
  }

  static async write ({ fs: _fs, gitdir, type, object }) {
    const fs = new FileSystem(_fs);
    let { buffer, oid } = GitObject.wrap({ type, object });
    let file = Buffer.from(pako.deflate(buffer));
    let filepath = `${gitdir}/objects/${oid.slice(0, 2)}/${oid.slice(2)}`;
    // Don't overwrite existing git objects - this helps avoid EPERM errors.
    // Although I don't know how we'd fix corrupted objects then. Perhaps delete them
    // on read?
    if (!await fs.exists(filepath)) await fs.write(filepath, file);
    return oid
  }
}

// This is a convenience wrapper for reading and writing files in the 'refs' directory.

// @see https://git-scm.com/docs/git-rev-parse.html#_specifying_revisions
const refpaths = ref => [
  `${ref}`,
  `refs/${ref}`,
  `refs/tags/${ref}`,
  `refs/heads/${ref}`,
  `refs/remotes/${ref}`,
  `refs/remotes/${ref}/HEAD`
];

class GitRefManager {
  static async updateRemoteRefs ({
    fs: _fs,
    gitdir,
    remote,
    refs,
    symrefs,
    tags,
    refspecs
  }) {
    const fs = new FileSystem(_fs);
    // Validate input
    for (let value of refs.values()) {
      if (!value.match(/[0-9a-f]{40}/)) {
        throw new Error(`Unexpected ref contents: '${value}'`)
      }
    }
    const config = await GitConfigManager.get({ fs, gitdir });
    if (!refspecs) {
      refspecs = await config.getall(`remote.${remote}.fetch`);
      if (refspecs.length === 0) {
        throw new Error(
          `Could not find a fetch refspec for remote '${remote}'.
Make sure the config file has an entry like the following:
[remote "${remote}"]
fetch = +refs/heads/*:refs/remotes/origin/*`
        )
      }
      // There's some interesting behavior with HEAD that doesn't follow the refspec.
      refspecs.unshift(`+HEAD:refs/remotes/${remote}/HEAD`);
    }
    const refspec = GitRefSpecSet.from(refspecs);
    let actualRefsToWrite = new Map();
    // Add all tags if the fetch tags argument is true.
    if (tags) {
      for (const serverRef of refs.keys()) {
        if (serverRef.startsWith('refs/tags') && !serverRef.endsWith('^{}')) {
          const filename = path.join(gitdir, serverRef);
          // Git's behavior is to only fetch tags that do not conflict with tags already present.
          if (!await fs.exists(filename)) {
            // If there is a dereferenced an annotated tag value available, prefer that.
            const oid = refs.get(serverRef + '^{}') || refs.get(serverRef);
            actualRefsToWrite.set(serverRef, oid);
          }
        }
      }
    }
    // Combine refs and symrefs giving symrefs priority
    let refTranslations = refspec.translate([...refs.keys()]);
    for (let [serverRef, translatedRef] of refTranslations) {
      let value = refs.get(serverRef);
      actualRefsToWrite.set(translatedRef, value);
    }
    let symrefTranslations = refspec.translate([...symrefs.keys()]);
    for (let [serverRef, translatedRef] of symrefTranslations) {
      let value = symrefs.get(serverRef);
      let symtarget = refspec.translateOne(value);
      if (symtarget) {
        actualRefsToWrite.set(translatedRef, `ref: ${symtarget}`);
      }
    }
    // Update files
    // TODO: For large repos with a history of thousands of pull requests
    // (i.e. gitlab-ce) it would be vastly more efficient to write them
    // to .git/packed-refs.
    // The trick is to make sure we a) don't write a packed ref that is
    // already shadowed by a loose ref and b) don't loose any refs already
    // in packed-refs. Doing this efficiently may be difficult. A
    // solution that might work is
    // a) load the current packed-refs file
    // b) add actualRefsToWrite, overriding the existing values if present
    // c) enumerate all the loose refs currently in .git/refs/remotes/${remote}
    // d) overwrite their value with the new value.
    // Examples of refs we need to avoid writing in loose format for efficieny's sake
    // are .git/refs/remotes/origin/refs/remotes/remote_mirror_3059
    // and .git/refs/remotes/origin/refs/merge-requests
    const normalizeValue = value => value.trim() + '\n';
    for (let [key, value] of actualRefsToWrite) {
      await fs.write(path.join(gitdir, key), normalizeValue(value), 'utf8');
    }
  }
  // TODO: make this less crude?
  static async writeRef ({ fs: _fs, gitdir, ref, value }) {
    const fs = new FileSystem(_fs);
    // Validate input
    if (!value.match(/[0-9a-f]{40}/)) {
      throw new Error(`Unexpected ref contents: '${value}'`)
    }
    const normalizeValue = value => value.trim() + '\n';
    await fs.write(path.join(gitdir, ref), normalizeValue(value), 'utf8');
  }
  static async resolve ({ fs: _fs, gitdir, ref, depth }) {
    const fs = new FileSystem(_fs);
    if (depth !== undefined) {
      depth--;
      if (depth === -1) {
        return ref
      }
    }
    let sha;
    // Is it a ref pointer?
    if (ref.startsWith('ref: ')) {
      ref = ref.slice('ref: '.length);
      return GitRefManager.resolve({ fs, gitdir, ref, depth })
    }
    // Is it a complete and valid SHA?
    if (ref.length === 40 && /[0-9a-f]{40}/.test(ref)) {
      return ref
    }
    // We need to alternate between the file system and the packed-refs
    let packedMap = await GitRefManager.packedRefs({ fs, gitdir });
    // Look in all the proper paths, in this order
    const allpaths = refpaths(ref);
    for (let ref of allpaths) {
      sha =
        (await fs.read(`${gitdir}/${ref}`, { encoding: 'utf8' })) ||
        packedMap.get(ref);
      if (sha) {
        return GitRefManager.resolve({ fs, gitdir, ref: sha.trim(), depth })
      }
    }
    // Do we give up?
    throw new Error(`Could not resolve reference ${ref}`)
  }
  static async expand ({ fs: _fs, gitdir, ref }) {
    const fs = new FileSystem(_fs);
    // Is it a complete and valid SHA?
    if (ref.length === 40 && /[0-9a-f]{40}/.test(ref)) {
      return ref
    }
    // We need to alternate between the file system and the packed-refs
    let packedMap = await GitRefManager.packedRefs({ fs, gitdir });
    // Look in all the proper paths, in this order
    const allpaths = refpaths(ref);
    for (let ref of allpaths) {
      if (await fs.exists(`${gitdir}/${ref}`)) return ref
      if (packedMap.has(ref)) return ref
    }
    // Do we give up?
    throw new Error(`Could not expand ref ${ref}`)
  }
  static resolveAgainstMap ({ ref, fullref = ref, depth, map }) {
    if (depth !== undefined) {
      depth--;
      if (depth === -1) {
        return { fullref, oid: ref }
      }
    }
    // Is it a ref pointer?
    if (ref.startsWith('ref: ')) {
      ref = ref.slice('ref: '.length);
      return GitRefManager.resolveAgainstMap({ ref, fullref, depth, map })
    }
    // Is it a complete and valid SHA?
    if (ref.length === 40 && /[0-9a-f]{40}/.test(ref)) {
      return { fullref, oid: ref }
    }
    // Look in all the proper paths, in this order
    const allpaths = refpaths(ref);
    for (let ref of allpaths) {
      let sha = map.get(ref);
      if (sha) {
        return GitRefManager.resolveAgainstMap({
          ref: sha.trim(),
          fullref: ref,
          depth,
          map
        })
      }
    }
    // Do we give up?
    throw new Error(`Could not resolve reference ${ref}`)
  }
  static async packedRefs ({ fs: _fs, gitdir }) {
    const refs = new Map();
    const fs = new FileSystem(_fs);
    const text = await fs.read(`${gitdir}/packed-refs`, { encoding: 'utf8' });
    if (!text) return refs
    const lines = text
      .trim()
      .split('\n')
      .filter(line => !/^\s*#/.test(line));
    let key = null;
    for (let line of lines) {
      const i = line.indexOf(' ');
      if (line.startsWith('^')) {
        // This is a oid for the commit associated with the annotated tag immediately preceding this line.
        // Trim off the '^'
        const value = line.slice(1, i);
        // The tagname^{} syntax is based on the output of `git show-ref --tags -d`
        refs.set(key + '^{}', value);
      } else {
        // This is an oid followed by the ref name
        const value = line.slice(0, i);
        key = line.slice(i + 1);
        refs.set(key, value);
      }
    }
    return refs
  }
  // List all the refs that match the `filepath` prefix
  static async listRefs ({ fs: _fs, gitdir, filepath }) {
    const fs = new FileSystem(_fs);
    let packedMap = GitRefManager.packedRefs({ fs, gitdir });
    let files = null;
    try {
      files = await fs.readdirDeep(`${gitdir}/${filepath}`);
      files = files.map(x => x.replace(`${gitdir}/${filepath}/`, ''));
    } catch (err) {
      files = [];
    }

    for (let key of (await packedMap).keys()) {
      // filter by prefix
      if (key.startsWith(filepath)) {
        // remove prefix
        key = key.replace(filepath + '/', '');
        // Don't include duplicates; the loose files have precedence anyway
        if (!files.includes(key)) {
          files.push(key);
        }
      }
    }
    return files
  }
  static async listBranches ({ fs: _fs, gitdir, remote }) {
    const fs = new FileSystem(_fs);
    if (remote) {
      return GitRefManager.listRefs({
        fs,
        gitdir,
        filepath: `refs/remotes/${remote}`
      })
    } else {
      return GitRefManager.listRefs({ fs, gitdir, filepath: `refs/heads` })
    }
  }
  static async listTags ({ fs: _fs, gitdir }) {
    const fs = new FileSystem(_fs);
    let tags = await GitRefManager.listRefs({
      fs,
      gitdir,
      filepath: `refs/tags`
    });
    return tags.filter(x => !x.endsWith('^{}'))
  }
}

class GitRemoteConnection {
  static async discover (service, res) {
    const capabilities = new Set();
    const refs = new Map();
    const symrefs = new Map();

    let data = await pify(concat)(res);
    // There is probably a better way to do this, but for now
    // let's just throw the result parser inline here.
    let read = GitPktLine.reader(data);
    let lineOne = await read();
    // skip past any flushes
    while (lineOne === null) lineOne = await read();
    if (lineOne === true) throw new Error('Bad response from git server.')
    if (lineOne.toString('utf8') !== `# service=${service}\n`) {
      throw new Error(
        `Expected '# service=${service}\\n' but got '${lineOne.toString(
          'utf8'
        )}'`
      )
    }
    let lineTwo = await read();
    // skip past any flushes
    while (lineTwo === null) lineTwo = await read();
    // In the edge case of a brand new repo, zero refs (and zero capabilities)
    // are returned.
    if (lineTwo === true) return { capabilities, refs, symrefs }
    let [firstRef, capabilitiesLine] = lineTwo
      .toString('utf8')
      .trim()
      .split('\0');
    capabilitiesLine.split(' ').map(x => capabilities.add(x));
    let [ref, name] = firstRef.split(' ');
    refs.set(name, ref);
    while (true) {
      let line = await read();
      if (line === true) break
      if (line !== null) {
        let [ref, name] = line
          .toString('utf8')
          .trim()
          .split(' ');
        refs.set(name, ref);
      }
    }
    // Symrefs are thrown into the "capabilities" unfortunately.
    for (let cap of capabilities) {
      if (cap.startsWith('symref=')) {
        let m = cap.match(/symref=([^:]+):(.*)/);
        if (m.length === 3) {
          symrefs.set(m[1], m[2]);
        }
      }
    }
    return { capabilities, refs, symrefs }
  }
  static async stream ({ res }) {
    // Parse the response!
    let read = GitPktLine.streamReader(res);
    // And now for the ridiculous side-band-64k protocol
    let packetlines = new PassThrough();
    let packfile = new PassThrough();
    let progress = new PassThrough();
    // TODO: Use a proper through stream?
    const nextBit = async function () {
      let line = await read();
      // Skip over flush packets
      if (line === null) return nextBit()
      // A made up convention to signal there's no more to read.
      if (line === true) {
        packetlines.end();
        progress.end();
        packfile.end();
        return
      }
      // Examine first byte to determine which output "stream" to use
      switch (line[0]) {
        case 1: // pack data
          packfile.write(line.slice(1));
          break
        case 2: // progress message
          progress.write(line.slice(1));
          break
        case 3: // fatal error message just before stream aborts
          let error = line.slice(1);
          progress.write(error);
          packfile.destroy(new Error(error.toString('utf8')));
          return
        default:
          // Not part of the side-band-64k protocol
          packetlines.write(line.slice(0));
      }
      // Careful not to blow up the stack.
      // I think Promises in a tail-call position should be OK.
      nextBit();
    };
    nextBit();
    return {
      packetlines,
      packfile,
      progress
    }
  }
}

class GitRemoteHTTP {
  static async capabilities () {
    return ['discover', 'connect']
  }
  static async discover ({ service, url, noGitSuffix, auth: auth$$1 }) {
    // Auto-append the (necessary) .git if it's missing.
    if (!url.endsWith('.git') && !noGitSuffix) url = url += '.git';
    let headers = {};
    // headers['Accept'] = `application/x-${service}-advertisement`
    headers['user-agent'] = `git/${pkg.name}@${pkg.version}`;
    auth$$1 = calculateBasicAuthUsernamePasswordPair(auth$$1);
    if (auth$$1) {
      headers['Authorization'] = calculateBasicAuthHeader(auth$$1);
    }
    let res = await pify(simpleGet)({
      method: 'GET',
      url: `${url}/info/refs?service=${service}`,
      headers
    });
    if (res.statusCode !== 200) {
      throw new Error(`HTTP Error: ${res.statusCode} ${res.statusMessage}`)
    }
    return GitRemoteConnection.discover(service, res)
  }
  static async connect ({ service, url, noGitSuffix, auth: auth$$1, stream }) {
    // Auto-append the (necessary) .git if it's missing.
    if (!url.endsWith('.git') && !noGitSuffix) url = url += '.git';
    let headers = {};
    headers['content-type'] = `application/x-${service}-request`;
    headers['accept'] = `application/x-${service}-result`;
    headers['user-agent'] = `git/${pkg.name}@${pkg.version}`;
    auth$$1 = calculateBasicAuthUsernamePasswordPair(auth$$1);
    if (auth$$1) {
      headers['Authorization'] = calculateBasicAuthHeader(auth$$1);
    }
    let res = await pify(simpleGet)({
      method: 'POST',
      url: `${url}/${service}`,
      body: stream,
      headers
    });
    if (res.statusCode !== 200) {
      throw new Error(`HTTP Error: ${res.statusCode} ${res.statusMessage}`)
    }
    return GitRemoteConnection.stream({ res })
  }
}

// For now, to remain API compatible, we'll pre-register the GitRemoteHTTP helper

const remoteHelpers = new Map();
remoteHelpers.set('http', GitRemoteHTTP);
remoteHelpers.set('https', GitRemoteHTTP);

function parseRemoteUrl ({ url }) {
  let matches = url.match(/(\w+)(:\/\/|::)(.*)/);
  if (matches === null) return
  /*
   * When git encounters a URL of the form <transport>://<address>, where <transport> is
   * a protocol that it cannot handle natively, it automatically invokes git remote-<transport>
   * with the full URL as the second argument.
   *
   * @see https://git-scm.com/docs/git-remote-helpers
   */
  if (matches[2] === '://') {
    return {
      transport: matches[1],
      address: matches[0]
    }
  }
  /*
   * A URL of the form <transport>::<address> explicitly instructs git to invoke
   * git remote-<transport> with <address> as the second argument.
   *
   * @see https://git-scm.com/docs/git-remote-helpers
   */
  if (matches[2] === '::') {
    return {
      transport: matches[1],
      address: matches[3]
    }
  }
}

class GitRemoteManager {
  static getRemoteHelperFor ({ url }) {
    let parts = parseRemoteUrl({ url });
    if (!parts) {
      throw new Error(`Cannot determine protocol of remote URL: "${url}"`)
    }
    if (remoteHelpers.has(parts.transport)) {
      return remoteHelpers.get(parts.transport)
    }
    throw new Error(
      `Git remote "${url}" uses an unrecognized transport protocol: "${
        parts.transport
      }"`
    )
  }
}

const lock$1 = new AsyncLock();

class GitShallowManager {
  static async read ({ fs: _fs, gitdir }) {
    const fs = new FileSystem(_fs);
    const filepath = path.join(gitdir, 'shallow');
    let oids = new Set();
    await lock$1.acquire(filepath, async function () {
      let text = await fs.read(filepath, { encoding: 'utf8' });
      if (text === null) return
      text
        .trim()
        .split('\n')
        .map(oid => oids.add(oid));
    });
    return oids
  }
  static async write ({ fs: _fs, gitdir, oids }) {
    const fs = new FileSystem(_fs);
    const filepath = path.join(gitdir, 'shallow');
    let text = [...oids].join('\n') + '\n';
    await lock$1.acquire(filepath, async function () {
      await fs.write(filepath, text, {
        encoding: 'utf8'
      });
    });
  }
}



var managers = /*#__PURE__*/Object.freeze({
  GitConfigManager: GitConfigManager,
  GitIgnoreManager: GitIgnoreManager,
  GitIndexManager: GitIndexManager,
  GitObjectManager: GitObjectManager,
  GitRefManager: GitRefManager,
  GitRemoteConnection: GitRemoteConnection,
  GitRemoteHTTP: GitRemoteHTTP,
  GitRemoteManager: GitRemoteManager,
  GitShallowManager: GitShallowManager
});

const types = {
  commit: 0b0010000,
  tree: 0b0100000,
  blob: 0b0110000,
  tag: 0b1000000,
  ofs_delta: 0b1100000,
  ref_delta: 0b1110000
};

async function listCommits ({
  dir,
  gitdir = path.join(dir, '.git'),
  fs: _fs,
  start,
  finish
}) {
  const fs = new FileSystem(_fs);
  let startingSet = new Set();
  let finishingSet = new Set();
  for (let ref of start) {
    startingSet.add(await GitRefManager.resolve({ fs, gitdir, ref }));
  }
  for (let ref of finish) {
    // We may not have these refs locally so we must try/catch
    try {
      let oid = await GitRefManager.resolve({ fs, gitdir, ref });
      finishingSet.add(oid);
    } catch (err) {}
  }
  let visited = new Set();

  // Because git commits are named by their hash, there is no
  // way to construct a cycle. Therefore we won't worry about
  // setting a default recursion limit.
  async function walk (oid) {
    visited.add(oid);
    let { type, object } = await GitObjectManager.read({ fs, gitdir, oid });
    if (type !== 'commit') {
      throw new Error(`Expected type commit but type is ${type}`)
    }
    let commit = GitCommit.from(object);
    let parents = commit.headers().parent;
    for (oid of parents) {
      if (!finishingSet.has(oid) && !visited.has(oid)) {
        await walk(oid);
      }
    }
  }

  // Let's go walking!
  for (let oid of startingSet) {
    await walk(oid);
  }
  return visited
}

async function listObjects ({
  dir,
  gitdir = path.join(dir, '.git'),
  fs: _fs,
  oids
}) {
  const fs = new FileSystem(_fs);
  let visited = new Set();

  // We don't do the purest simplest recursion, because we can
  // avoid reading Blob objects entirely since the Tree objects
  // tell us which oids are Blobs and which are Trees.
  async function walk (oid) {
    visited.add(oid);
    let { type, object } = await GitObjectManager.read({ fs, gitdir, oid });
    if (type === 'commit') {
      let commit = GitCommit.from(object);
      let tree = commit.headers().tree;
      await walk(tree);
    } else if (type === 'tree') {
      let tree = GitTree.from(object);
      for (let entry of tree) {
        visited.add(entry.oid);
        // only recurse for trees
        if (entry.type === 'tree') {
          await walk(entry.oid);
        }
      }
    }
  }

  // Let's go walking!
  for (let oid of oids) {
    await walk(oid);
  }
  return visited
}

async function pack ({
  dir,
  gitdir = path.join(dir, '.git'),
  fs: _fs,
  oids,
  outputStream
}) {
  const fs = new FileSystem(_fs);
  let hash = createHash('sha1');
  function write (chunk, enc) {
    outputStream.write(chunk, enc);
    hash.update(chunk, enc);
  }
  function writeObject ({ stype, object }) {
    let lastFour, multibyte, length;
    // Object type is encoded in bits 654
    let type = types[stype];
    if (type === undefined) throw new Error('Unrecognized type: ' + stype)
    // The length encoding get complicated.
    length = object.length;
    // Whether the next byte is part of the variable-length encoded number
    // is encoded in bit 7
    multibyte = length > 0b1111 ? 0b10000000 : 0b0;
    // Last four bits of length is encoded in bits 3210
    lastFour = length & 0b1111;
    // Discard those bits
    length = length >>> 4;
    // The first byte is then (1-bit multibyte?), (3-bit type), (4-bit least sig 4-bits of length)
    let byte = (multibyte | type | lastFour).toString(16);
    write(byte, 'hex');
    // Now we keep chopping away at length 7-bits at a time until its zero,
    // writing out the bytes in what amounts to little-endian order.
    while (multibyte) {
      multibyte = length > 0b01111111 ? 0b10000000 : 0b0;
      byte = multibyte | (length & 0b01111111);
      write(pad(2, byte.toString(16), '0'), 'hex');
      length = length >>> 7;
    }
    // Lastly, we can compress and write the object.
    write(Buffer.from(pako.deflate(object)));
  }

  write('PACK');
  write('00000002', 'hex');
  // Write a 4 byte (32-bit) int
  write(pad(8, oids.length.toString(16), '0'), 'hex');
  for (let oid of oids) {
    let { type, object } = await GitObjectManager.read({ fs, gitdir, oid });
    writeObject({ write, object, stype: type });
  }
  // Write SHA1 checksum
  let digest = hash.digest();
  outputStream.end(digest);
  return outputStream
}

export { managers, models, utils, listCommits, listObjects, pack };
