'use strict';

function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }

var _Map = _interopDefault(require('babel-runtime/core-js/map'));
var path = _interopDefault(require('path'));
var pify = _interopDefault(require('pify'));
var _extends = _interopDefault(require('babel-runtime/helpers/extends'));
var _Math$sign = _interopDefault(require('babel-runtime/core-js/math/sign'));
var buffer = require('buffer');
var openpgp = require('openpgp/dist/openpgp.min.js');
var _Object$keys = _interopDefault(require('babel-runtime/core-js/object/keys'));
var ini = _interopDefault(require('ini'));
var get = _interopDefault(require('lodash/get'));
var set = _interopDefault(require('lodash/set'));
var BufferCursor = _interopDefault(require('buffercursor'));
var pad = _interopDefault(require('pad'));
var gartal = require('gartal');
var _Symbol$iterator = _interopDefault(require('babel-runtime/core-js/symbol/iterator'));
var sortby = _interopDefault(require('lodash/sortBy'));
var shasum = _interopDefault(require('shasum'));
var systemfs = _interopDefault(require('fs'));
var _Promise = _interopDefault(require('babel-runtime/core-js/promise'));
var _Set = _interopDefault(require('babel-runtime/core-js/set'));
var AsyncLock = _interopDefault(require('async-lock'));
var ignore = _interopDefault(require('ignore'));
var pako = _interopDefault(require('pako'));
var simpleGet = _interopDefault(require('simple-get'));
var concat = _interopDefault(require('simple-concat'));
var stream = require('stream');
var stream__default = _interopDefault(stream);
var _Number$isNaN = _interopDefault(require('babel-runtime/core-js/number/is-nan'));
var thru = _interopDefault(require('thru'));
var listpack = _interopDefault(require('git-list-pack'));
var peek = _interopDefault(require('buffer-peek-stream'));
var applyDelta = _interopDefault(require('git-apply-delta'));
var marky = _interopDefault(require('marky'));
var crypto = _interopDefault(require('crypto'));

// @flow
function formatTimezoneOffset(minutes /*: number */) {
  let sign$$1 = _Math$sign(minutes) || 1;
  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 === 1 ? '-' : '+') + strHours + strMinutes;
}

function parseTimezoneOffset(offset) {
  let [, sign$$1, hours, minutes] = offset.match(/(\+|-)(\d\d)(\d\d)/);
  minutes = (sign$$1 === '-' ? 1 : -1) * Number(hours) * 60 + Number(minutes);
  return 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 {
  /*::
  _commit : string
  */
  constructor(commit /*: string|Buffer|Object */) {
    if (typeof commit === 'string') {
      this._commit = commit;
    } else if (buffer.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$$1 = GitCommit.justMessage(payload);
    let commit = normalize(headers + '\ngpgsig' + indent(signature) + '\n' + message$$1);
    return new GitCommit(commit);
  }

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

  toObject() {
    return buffer.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 _extends({ 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 = {};
    for (let h of hs) {
      let key$$1 = h.slice(0, h.indexOf(' '));
      let value = h.slice(h.indexOf(' ') + 1);
      obj[key$$1] = value;
    }
    obj.parent = obj.parent ? obj.parent.split(' ') : [];
    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) {
      headers += 'parent';
      for (let p of obj.parent) {
        headers += ' ' + p;
      }
      headers += '\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$$1 = commit.slice(commit.indexOf('-----END PGP SIGNATURE-----\n') + '-----END PGP SIGNATURE-----\n'.length);
    return normalize(headers + '\n' + message$$1);
  }

  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);
  }

  async sign(privateKeys /*: string */) {
    let commit = this.withoutSignature();
    let headers = GitCommit.justHeaders(this._commit);
    let message$$1 = 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(signature);
    let signedCommit = headers + '\n' + 'gpgsig' + indent(signature) + '\n' + message$$1;
    // return a new commit object
    return GitCommit.from(signedCommit);
  }

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

  async verify(publicKeys /*: string */) {
    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;
  }
}

const complexKeys = ['remote', 'branch'];

const isComplexKey = key$$1 => complexKeys.reduce((x, y) => x || key$$1.startsWith(y), false);

const splitComplexKey = key$$1 => key$$1.split('"').map(x => x.trim()).filter(x => x !== '');

// 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.ini = ini.decode(text);
    // Some mangling to make it easier to work with (honestly)
    for (let key$$1 of _Object$keys(this.ini)) {
      if (isComplexKey(key$$1)) {
        let parts = splitComplexKey(key$$1);
        if (parts.length === 2) {
          // just to be cautious
          set(this.ini, [parts[0], parts[1]], this.ini[key$$1]);
          delete this.ini[key$$1];
        }
      }
    }
  }
  static from(text) {
    return new GitConfig(text);
  }
  async get(path$$1) {
    return get(this.ini, path$$1);
  }
  async set(path$$1, value) {
    return set(this.ini, path$$1, value);
  }
  toString() {
    // de-mangle complex keys
    for (let key$$1 of _Object$keys(this.ini)) {
      if (isComplexKey(key$$1)) {
        for (let childkey of _Object$keys(this.ini[key$$1])) {
          let complexkey = `${key$$1} "${childkey}"`;
          this.ini[complexkey] = this.ini[key$$1][childkey];
          delete this.ini[key$$1][childkey];
        }
        delete this.ini[key$$1];
      }
    }
    let text = ini.encode(this.ini, { whitespace: true });
    return text;
  }
}

// @flow
/**
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.Buffer.from('0000', 'utf8');
  }

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

  static reader(buffer$$1 /*: Buffer */) {
    let buffercursor = new BufferCursor(buffer$$1);
    return 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$$1 /*: ReadableStream */) {
    return async function read() {
      let hexlength, length, bytes;
      try {
        hexlength = await gartal.readBytes(stream$$1, 4);
      } catch (err) {
        // No more file to read
        return null;
      }
      length = parseInt(hexlength.toString('utf8'), 16);
      // skip over flush packets
      if (length === 0) return read();
      // otherwise return the packet content
      bytes = await gartal.readBytes(stream$$1, length - 4);
      return bytes;
    };
  }
}

// @flow
/*::
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 /*: number */) /*: CacheEntryFlags */{
  return {
    assumeValid: Boolean(bits & 0b1000000000000000),
    extended: Boolean(bits & 0b0100000000000000),
    stage: (bits & 0b0011000000000000) >> 12,
    nameLength: bits & 0b0000111111111111
  };
}

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

function parseBuffer(buffer$$1) {
  // Verify shasum
  let shaComputed = shasum(buffer$$1.slice(0, -20));
  let shaClaimed = buffer$$1.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$$1);
  let _entries /*: Map<string, CacheEntry> */ = 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$$1.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 /*: any */) {
    this._dirty = false;
    if (buffer.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$$1) {
    return new GitIndex(buffer$$1);
  }
  get entries() /*: Array<CacheEntry> */{
    return sortby([...this._entries.values()], 'path');
  }
  *[_Symbol$iterator]() {
    for (let entry of this.entries) {
      yield entry;
    }
  }
  insert({
    filepath,
    stats,
    oid
  }) /*: {filepath: string, stats: Stats, oid: string } */{
    let entry = {
      ctime: stats.ctime,
      mtime: stats.mtime,
      dev: stats.dev,
      ino: stats.ino,
      mode: stats.mode,
      uid: stats.uid,
      gid: stats.gid,
      size: stats.size,
      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 /*: {filepath: string} */ }) {
    if (this._entries.has(filepath)) {
      this._entries.delete(filepath);
    } else {
      for (let key$$1 of this._entries.keys()) {
        if (key$$1.startsWith(filepath + '/')) {
          this._entries.delete(key$$1);
        }
      }
    }
    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.Buffer.alloc(12);
    let writer = new BufferCursor(header);
    writer.write('DIRC', 4, 'utf8');
    writer.writeUInt32BE(2);
    writer.writeUInt32BE(this.entries.length);
    let body = buffer.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.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);
      writer.writeUInt32BE(ctimeNanoseconds);
      writer.writeUInt32BE(mtimeSeconds);
      writer.writeUInt32BE(mtimeNanoseconds);
      writer.writeUInt32BE(entry.dev);
      writer.writeUInt32BE(entry.ino);
      writer.writeUInt32BE(entry.mode);
      writer.writeUInt32BE(entry.uid);
      writer.writeUInt32BE(entry.gid);
      writer.writeUInt32BE(entry.size);
      writer.write(entry.oid, 20, 'hex');
      writer.writeUInt16BE(renderCacheEntryFlags(entry.flags));
      writer.write(entry.path, entry.path.length, 'utf8');
      return written;
    }));
    let main = buffer.Buffer.concat([header, body]);
    let sum = shasum(main);
    return buffer.Buffer.concat([main, buffer.Buffer.from(sum, 'hex')]);
  }
}

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

function parseBuffer$1(buffer$$1) /*: Array<TreeEntry> */{
  let _entries = [];
  let cursor = 0;
  while (cursor < buffer$$1.length) {
    let space = buffer$$1.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$$1.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$$1.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$$1.slice(space + 1, nullchar).toString('utf8');
    let oid = buffer$$1.slice(nullchar + 1, nullchar + 21).toString('hex');
    cursor = nullchar + 21;
    _entries.push({ mode, path: path$$1, oid, type });
  }
  return _entries;
}

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

class GitTree {
  /*::
  _entries: Array<TreeEntry>
  */
  constructor(entries /*: any */) {
    if (buffer.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.Buffer.concat(this._entries.map(entry => {
      let mode = buffer.Buffer.from(entry.mode.replace(/^0/, ''));
      let space = buffer.Buffer.from(' ');
      let path$$1 = buffer.Buffer.from(entry.path, { encoding: 'utf8' });
      let nullchar = buffer.Buffer.from([0]);
      let oid = buffer.Buffer.from(entry.oid.match(/../g).map(n => parseInt(n, 16)));
      return buffer.Buffer.concat([mode, space, path$$1, nullchar, oid]);
    }));
  }
  entries() {
    return this._entries;
  }
  *[_Symbol$iterator]() {
    for (let entry of this._entries) {
      yield entry;
    }
  }
}



var models = Object.freeze({
	GitCommit: GitCommit,
	GitConfig: GitConfig,
	GitPktLine: GitPktLine,
	GitIndex: GitIndex,
	GitTree: GitTree
});

var fs = function () {
  return global.fs || systemfs;
};

async function rm(filepath) {
  try {
    await pify(fs().unlink)(filepath);
  } catch (err) {
    if (err.code !== 'ENOENT') throw err;
  }
}

// An async exists variant
async function exists(file, options) {
  return new _Promise(function (resolve, reject) {
    fs().stat(file, (err, stats) => {
      if (err) return err.code === 'ENOENT' ? resolve(false) : reject(err);
      resolve(true);
    });
  });
}

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

function flatFileListToDirectoryStructure(files /*: Array<{path: string}> */
) /*: Node|void */{
  const inodes /*: Map<string, Node> */ = new _Map();
  const mkdir = function (name) /*: Node|void */{
    if (!inodes.has(name)) {
      let dir /*: Node */ = {
        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) /*: Node|void */{
    if (!inodes.has(name)) {
      let file /*: Node */ = {
        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);
  };

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

const readdir = pify(fs().readdir);
const stat = pify(fs().stat);

// Based on an elegant concurrent recursive solution from SO
// https://stackoverflow.com/a/45130990/2168416
async function readDirAsFlatFileList(dir) {
  const subdirs = await readdir(dir);
  const files = await _Promise.all(subdirs.map(async subdir => {
    const res = dir + '/' + subdir;
    return (await stat(res)).isDirectory() ? readDirAsFlatFileList(res) : res;
  }));
  return files.reduce((a, f) => a.concat(f), []);
}

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

// @flow
// This is modeled after the lockfile strategy used by the git source code.
const delayedReleases = new _Map();

async function lock(filename /*: string */
, triesLeft /*: number */ = 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 pify(fs().mkdir)(`${filename}.lock`);
  } catch (err) {
    if (err.code === 'EEXIST') {
      await sleep(100);
      await lock(filename, triesLeft - 1);
    }
  }
}

async function unlock(filename /*: string */
, delayRelease /*: number */ = 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 pify(fs().rmdir)(`${filename}.lock`);
  }));
}

// @flow
async function mkdir(dirpath /*: string */) {
  try {
    await pify(fs().mkdir)(dirpath);
    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(dirpath);
      // Check to see if we've gone too far
      if (parent === '.' || parent === '/' || parent === dirpath) throw err;
      // Infinite recursion, what could go wrong?
      await mkdir(parent);
      await mkdir(dirpath);
    }
  }
}

async function mkdirs(dirlist /*: string[] */) {
  return _Promise.all(dirlist.map(mkdir));
}

// An async readFile variant that returns null instead of throwing errors
async function read(file, options) {
  return new _Promise(function (resolve, reject) {
    fs().readFile(file, options, (err, file) => err ? resolve(null) : resolve(file));
  });
}

// @flow
// An async writeFile variant that automatically creates missing directories,
// and returns null instead of throwing errors.
async function write(filepath /*: string */
, contents /*: string|Buffer */
, options /*: Object */ = {}) {
  try {
    await pify(fs().writeFile)(filepath, contents, options);
    return;
  } catch (err) {
    // Hmm. Let's try mkdirp and try again.
    await mkdir(path.dirname(filepath));
    await pify(fs().writeFile)(filepath, contents, options);
  }
}

var name = "isomorphic-git";
var version = "0.0.23";
var description = "A pure JavaScript implementation of git for node and browsers!";
var main = "dist/for-node/";
var browser = "dist/for-browserify/";
var module$1 = "dist/for-future/";
var unpkg = "dist/bundle.umd.min.js";
var bin = { "isogit": "./cli.js" };
var engines = { "node": ">=7.6.0" };
var scripts = { "start": "nps", "test": "nps test", "precommit": "nps format toc" };
var repository = { "type": "git", "url": "git+https://github.com/wmhilton/isomorphic-git.git" };
var keywords = ["git"];
var author = "William Hilton <wmhilton@gmail.com>";
var license = "Unlicense";
var bugs = { "url": "https://github.com/wmhilton/isomorphic-git/issues" };
var homepage = "https://github.com/wmhilton/isomorphic-git#readme";
var files = ["dist", "cli.js"];
var dependencies = { "async-lock": "^1.0.0", "await-stream-ready": "^1.0.1", "babel-runtime": "^6.26.0", "buffer": "^5.0.7", "buffer-peek-stream": "^1.0.1", "buffercursor": "0.0.12", "gartal": "^1.1.2", "git-apply-delta": "0.0.7", "git-list-pack": "0.0.10", "ignore": "^3.3.6", "ini": "^1.3.4", "marky": "^1.2.0", "minimisted": "^2.0.0", "openpgp": "^2.5.10", "pad": "^2.0.1", "pako": "^1.0.5", "pify": "^3.0.0", "shasum": "^1.0.2", "simple-concat": "^1.0.0", "simple-get": "^2.7.0", "thru": "git+https://github.com/wmhilton-contrib/thru.git#master" };
var devDependencies = { "babel-plugin-external-helpers": "^6.22.0", "babel-plugin-transform-es2015-modules-commonjs": "^6.24.1", "babel-plugin-transform-object-rest-spread": "^6.23.0", "babel-plugin-transform-runtime": "^6.23.0", "babel-preset-env": "^1.6.0", "babel-preset-flow": "^6.23.0", "ban-sensitive-files": "^1.9.0", "browserfs": "^1.4.3", "browserify": "^14.4.0", "browserify-shim": "^3.8.14", "codecov": "^3.0.0", "doctoc": "^1.3.0", "husky": "^0.14.3", "jest": "^21.2.1", "jest-fixtures": "^0.6.0", "jsonfile": "^4.0.0", "karma": "^1.7.1", "karma-browserify": "^5.1.1", "karma-chrome-launcher": "^2.2.0", "karma-firefox-launcher": "^1.0.1", "karma-sauce-launcher": "^1.2.0", "karma-tap": "^3.1.1", "lodash": "^4.17.4", "nock": "^9.0.17", "npm-run-all": "^4.1.1", "nps": "^5.7.1", "nps-utils": "^1.4.0", "parse-header-stream": "^1.1.1", "prettier-standard": "^7.0.1", "rollup": "^0.50.0", "rollup-plugin-babel": "^3.0.2", "rollup-plugin-json": "^2.3.0", "standard": "^10.0.3", "stream-equal": "^1.0.1", "tape": "^4.8.0", "uglify-es": "^3.1.2", "watchify": "^3.9.0" };
var ava = { "source": ["dist/for-node/*"] };
var browserify = { "transform": ["browserify-shim"] };
var testling = { "files": "testling/basic-test.js", "browsers": ["chrome/latest", "firefox/latest", "ie/latest"] };
var jest = { "testPathIgnorePatterns": ["__helpers__"], "testEnvironment": "node" };
var _package = {
	name: name,
	version: version,
	description: description,
	main: main,
	browser: browser,
	module: module$1,
	unpkg: unpkg,
	bin: bin,
	engines: engines,
	scripts: scripts,
	repository: repository,
	keywords: keywords,
	author: author,
	license: license,
	bugs: bugs,
	homepage: homepage,
	files: files,
	dependencies: dependencies,
	devDependencies: devDependencies,
	ava: ava,
	browserify: browserify,
	testling: testling,
	jest: jest,
	"browserify-shim": { "fs": "global:fs" }
};

var _package$1 = Object.freeze({
	name: name,
	version: version,
	description: description,
	main: main,
	browser: browser,
	module: module$1,
	unpkg: unpkg,
	bin: bin,
	engines: engines,
	scripts: scripts,
	repository: repository,
	keywords: keywords,
	author: author,
	license: license,
	bugs: bugs,
	homepage: homepage,
	files: files,
	dependencies: dependencies,
	devDependencies: devDependencies,
	ava: ava,
	browserify: browserify,
	testling: testling,
	jest: jest,
	default: _package
});



var utils = Object.freeze({
	rm: rm,
	exists: exists,
	flatFileListToDirectoryStructure: flatFileListToDirectoryStructure,
	readDirAsFlatFileList: readDirAsFlatFileList,
	fs: fs,
	lock: lock,
	unlock: unlock,
	mkdirs: mkdirs,
	read: read,
	sleep: sleep,
	write: write,
	pkg: _package$1
});

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

// @flow
// TODO: Add file locks.
class GitShallowManager {
  static async read({ gitdir }) {
    let oids = new _Set();
    let text = await read(path.join(gitdir, 'shallow'), { encoding: 'utf8' });
    if (text === null) return oids;
    text.trim().split('\n').map(oid => oids.add(oid));
    return oids;
  }
  static async write({ gitdir, oids }) {
    let text = '';
    for (let oid of oids) {
      text += `${oid}\n`;
    }
    await write(path.join(gitdir, 'shallow'), text, {
      encoding: 'utf8'
    });
  }
}

// @flow
// import LockManager from 'travix-lock-manager'
// import Lock from '../utils'

// TODO: replace with an LRU cache?
const map /*: Map<string, GitIndex> */ = new _Map();
// const lm = new LockManager()
const lock$1 = new AsyncLock();

class GitIndexManager {
  static async acquire(filepath, closure) {
    await lock$1.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 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$$1 = index.toObject();
        await write(filepath, buffer$$1);
        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);
    });
  }
}

// @flow
// 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({
    gitdir,
    workdir,
    pathname
  }) /*: Promise<boolean> */{
    let pairs = [{
      gitignore: path.join(workdir, '.gitignore'),
      pathname
    }];
    let pieces = pathname.split('/');
    for (let i = 1; i < pieces.length; i++) {
      let dir = pieces.slice(0, i).join('/');
      let file = pieces.slice(i).join('/');
      pairs.push({
        gitignore: path.join(workdir, dir, '.gitignore'),
        pathname: file
      });
    }
    let ignoredStatus = false;
    for (let p of pairs) {
      let file;
      try {
        file = await 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.pathname);
      if (ign.ignores(parentdir)) return true;
      // If the file is currently ignored, test for UNignoring.
      if (ignoredStatus) {
        ignoredStatus = unign.ignores(p.pathname);
      } else {
        ignoredStatus = ign.ignores(p.pathname);
      }
    }
    return ignoredStatus;
  }
}

// @flow
function wrapObject({ type, object /*: {type: string, object: Buffer} */ }) {
  let buffer$$1 = buffer.Buffer.concat([buffer.Buffer.from(type + ' '), buffer.Buffer.from(object.byteLength.toString()), buffer.Buffer.from([0]), buffer.Buffer.from(object)]);
  let oid = shasum(buffer$$1);
  return {
    oid,
    file: buffer.Buffer.from(pako.deflate(buffer$$1))
  };
}

function unwrapObject({ oid, file /*: {oid: string, file: Buffer} */ }) {
  let inflated = buffer.Buffer.from(pako.inflate(file));
  if (oid) {
    let sha = shasum(inflated);
    if (sha !== oid) {
      throw new Error(`SHA check failed! Expected ${oid}, computed ${sha}`);
    }
  }
  let s = inflated.indexOf(32); // first space
  let i = inflated.indexOf(0); // first null value
  let type = inflated.slice(0, s).toString('utf8'); // get type of object
  let length = inflated.slice(s + 1, i).toString('utf8'); // get type of object
  let actualLength = inflated.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.Buffer.from(inflated.slice(i + 1))
  };
}

class GitObjectManager {
  static async read({ gitdir, oid /*: {gitdir: string, oid: string} */ }) {
    let file = await read(`${gitdir}/objects/${oid.slice(0, 2)}/${oid.slice(2)}`);
    if (!file) {
      // Check to see if it's in shallow commits.
      let text = await 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`);
      } else {
        throw new Error(`Failed to read git object with oid ${oid}`);
      }
    }
    let { type, object } = unwrapObject({ oid, file });
    return { type, object };
  }

  static async hash({ gitdir, type, object }) /*: Promise<string> */{
    let buffer$$1 = buffer.Buffer.concat([buffer.Buffer.from(type + ' '), buffer.Buffer.from(object.byteLength.toString()), buffer.Buffer.from([0]), buffer.Buffer.from(object)]);
    let oid = shasum(buffer$$1);
    return oid;
  }

  static async write({ gitdir, type, object }) /*: Promise<string> */{
    let { file, oid } = wrapObject({ type, object });
    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 exists(filepath))) await write(filepath, file);
    return oid;
  } /*: {gitdir: string, type: string, object: Buffer} */
}

// @flow
// This is a convenience wrapper for reading and writing files in the 'refs' directory.
class GitRefManager {
  /* ::
  updateRemoteRefs : ({
    gitdir: string,
    remote: string,
    refs: Map<string, string>,
    symrefs: Map<string, string>
  }) => Promise<void>
  */
  static async updateRemoteRefs({ gitdir, remote, refs, symrefs }) {
    // Validate input
    for (let [key$$1, value] of refs) {
      if (!value.match(/[0-9a-f]{40}/)) {
        throw new Error(`Unexpected ref contents: '${value}'`);
      }
    }
    // Combine refs and symrefs giving symrefs priority
    let actualRefsToWrite = new _Map();
    for (let [key$$1, value] of refs) actualRefsToWrite.set(key$$1, value);
    for (let [key$$1, value] of symrefs) {
      let branch = value.replace(/^refs\/heads\//, '');
      actualRefsToWrite.set(key$$1, `ref: refs/remotes/${remote}/${branch}`);
    }
    // Update files
    const normalizeValue = value => value.trim() + '\n';
    for (let [key$$1, value] of actualRefsToWrite) {
      // For some reason we trim these
      key$$1 = key$$1.replace(/^refs\/heads\//, '');
      key$$1 = key$$1.replace(/^refs\/tags\//, '');
      await write(path.join(gitdir, 'refs', 'remotes', remote, key$$1), normalizeValue(value), 'utf8');
    }
  }
  static async resolve({ gitdir, ref, depth }) {
    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({ gitdir, ref, depth });
    }
    // Is it a complete and valid SHA?
    if (ref.length === 40 && /[0-9a-f]{40}/.test(ref)) {
      return ref;
    }
    // Is it a special ref?
    if (ref === 'HEAD' || ref === 'MERGE_HEAD') {
      sha = await read(`${gitdir}/${ref}`, { encoding: 'utf8' });
      if (sha) return GitRefManager.resolve({ gitdir, ref: sha.trim(), depth });
    }
    // Is it a full ref?
    if (ref.startsWith('refs/')) {
      sha = await read(`${gitdir}/${ref}`, { encoding: 'utf8' });
      if (sha) return GitRefManager.resolve({ gitdir, ref: sha.trim(), depth });
    }
    // Is it a (local) branch?
    sha = await read(`${gitdir}/refs/heads/${ref}`, { encoding: 'utf8' });
    if (sha) return GitRefManager.resolve({ gitdir, ref: sha.trim(), depth });
    // Is it a tag?
    sha = await read(`${gitdir}/refs/tags/${ref}`, { encoding: 'utf8' });
    if (sha) return GitRefManager.resolve({ gitdir, ref: sha.trim(), depth });
    // Is it remote branch?
    sha = await read(`${gitdir}/refs/remotes/${ref}`, { encoding: 'utf8' });
    if (sha) return GitRefManager.resolve({ gitdir, ref: sha.trim(), depth });
    // Is it a packed ref? (This must be last because refs in heads have priority)
    let text = await read(`${gitdir}/packed-refs`, { encoding: 'utf8' });
    if (text && text.includes(ref)) {
      let candidates = text.trim().split('\n').filter(x => x.endsWith(ref)).filter(x => !x.startsWith('#'));
      if (candidates.length > 1) {
        throw new Error(`Could not resolve ambiguous reference ${ref}`);
      } else if (candidates.length === 1) {
        sha = candidates[0].split(' ')[0];
        return GitRefManager.resolve({ gitdir, ref: sha.trim(), depth });
      }
    }
    // Do we give up?
    throw new Error(`Could not resolve reference ${ref}`);
  }
}

// @flow
function basicAuth(auth) {
  return `Basic ${buffer.Buffer.from(auth.username + ':' + auth.password).toString('base64')}`;
}

class GitRemoteHTTP {
  /*::
  GIT_URL : string
  refs : Map<string, string>
  symrefs : Map<string, string>
  capabilities : Set<string>
  auth : { username : string, password : string }
  */
  constructor(url /*: string */) {
    // Auto-append the (necessary) .git if it's missing.
    if (!url.endsWith('.git')) url = url += '.git';
    this.GIT_URL = url;
  }
  async preparePull() {
    await this.discover('git-upload-pack');
  }
  async preparePush() {
    await this.discover('git-receive-pack');
  }
  async discover(service /*: string */) {
    this.capabilities = new _Set();
    this.refs = new _Map();
    this.symrefs = new _Map();
    let headers = {};
    // headers['Accept'] = `application/x-${service}-advertisement`
    if (this.auth) {
      headers['Authorization'] = basicAuth(this.auth);
    }
    let res = await pify(simpleGet)({
      method: 'GET',
      url: `${this.GIT_URL}/info/refs?service=${service}`,
      headers
    });
    if (res.statusCode !== 200) {
      throw new Error(`Bad status code from server: ${res.statusCode}`);
    }
    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 = read();
    // skip past any flushes
    while (lineOne === null) lineOne = 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 = read();
    // skip past any flushes
    while (lineTwo === null) lineTwo = read();
    // In the edge case of a brand new repo, zero refs (and zero capabilities)
    // are returned.
    if (lineTwo === true) return;
    let [firstRef, capabilities] = lineTwo.toString('utf8').trim().split('\0');
    capabilities.split(' ').map(x => this.capabilities.add(x));
    let [ref, name] = firstRef.split(' ');
    this.refs.set(name, ref);
    while (true) {
      let line = read();
      if (line === true) break;
      if (line !== null) {
        let [ref, name] = line.toString('utf8').trim().split(' ');
        this.refs.set(name, ref);
      }
    }
    // Symrefs are thrown into the "capabilities" unfortunately.
    for (let cap of this.capabilities) {
      if (cap.startsWith('symref=')) {
        let m = cap.match(/symref=([^:]+):(.*)/);
        if (m.length === 3) {
          this.symrefs.set(m[1], m[2]);
        }
      }
    }
  }
  async push(stream$$1 /*: ReadableStream */) {
    const service = 'git-receive-pack';
    let res = await this.stream({ stream: stream$$1, service });
    return res;
  }
  async pull(stream$$1 /*: ReadableStream */) {
    const service = 'git-upload-pack';
    let res = await this.stream({ stream: stream$$1, service });
    return res;
  }
  async stream({
    stream: stream$$1,
    service
  }) /*: Promise<{ packfile: ReadableStream, progress: ReadableStream }> */{
    let headers = {};
    headers['content-type'] = `application/x-${service}-request`;
    headers['accept'] = `application/x-${service}-result`;
    headers['user-agent'] = `git/${name}@${version}`;
    if (this.auth) {
      headers['authorization'] = basicAuth(this.auth);
    }
    let res = await pify(simpleGet)({
      method: 'POST',
      url: `${this.GIT_URL}/${service}`,
      body: stream$$1,
      headers
    });
    // Don't try to parse git pushes for now.
    if (service === 'git-receive-pack') return res;
    // Parse the response!
    let read = GitPktLine.streamReader(res);
    // And now for the ridiculous side-band-64k protocol
    let packetlines = new stream.PassThrough();
    let packfile = new stream.PassThrough();
    let progress = new stream.PassThrough();
    // TODO: Use a proper through stream?
    const nextBit = async function () {
      let line = await read();
      // A made up convention to signal there's no more to read.
      if (line === null) {
        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
    };
  } /*: {
    stream: ReadableStream,
    service: string
    } */
}



var managers = Object.freeze({
	GitConfigManager: GitConfigManager,
	GitShallowManager: GitShallowManager,
	GitIndexManager: GitIndexManager,
	GitIgnoreManager: GitIgnoreManager,
	GitObjectManager: GitObjectManager,
	GitRefManager: GitRefManager,
	GitRemoteHTTP: GitRemoteHTTP
});

async function add({ gitdir, workdir, filepath }) {
  const type = 'blob';
  const object = await read(path.join(workdir, filepath));
  if (object === null) throw new Error(`Could not read file '${filepath}'`);
  const oid = await GitObjectManager.write({ gitdir, type, object });
  await GitIndexManager.acquire(`${gitdir}/index`, async function (index) {
    let stats = await pify(fs().lstat)(path.join(workdir, filepath));
    index.insert({ filepath, stats, oid });
  });
  // TODO: return oid?
}

// @flow
async function init({ gitdir /*: { gitdir: string } */ }) {
  let folders = ['hooks', 'info', 'objects/info', 'objects/pack', 'refs/heads', 'refs/tags'];
  folders = folders.map(dir => gitdir + '/' + dir);
  await mkdirs(folders);
  await write(gitdir + '/config', '[core]\n' + '\trepositoryformatversion = 0\n' + '\tfilemode = false\n' + '\tbare = false\n' + '\tlogallrefupdates = true\n' + '\tsymlinks = false\n' + '\tignorecase = true\n');
  await write(gitdir + '/HEAD', 'ref: refs/heads/master\n');
  // await write(gitdir + '/refs/heads/master', '')
}

async function config(args) {
  let { gitdir, path: path$$1, value } = args;
  const config = await GitConfigManager.get({ gitdir });
  // This carefully distinguishes between
  // 1) there is no 'value' argument (do a "get")
  // 2) there is a 'value' argument with a value of undefined (do a "set")
  // Because setting a key to undefined is how we delete entries from the ini.
  if (value === undefined && !args.hasOwnProperty('value')) {
    const value = await config.get(path$$1);
    return value;
  } else {
    await config.set(path$$1, value);
    await GitConfigManager.save({ gitdir, config });
  }
}

// @flow
/*::
import type {Writable} from 'stream'
*/

const types = {
  1: 'commit',
  2: 'tree',
  3: 'blob',
  4: 'tag',
  6: 'ofs-delta',
  7: 'ref-delta'
};

function parseVarInt(buffer$$1 /*: Buffer */) {
  let n = 0;
  for (var i = 0; i < buffer$$1.byteLength; i++) {
    n = (buffer$$1[i] & 0b01111111) + (n << 7);
    if ((buffer$$1[i] & 0b10000000) === 0) {
      if (i !== buffer$$1.byteLength - 1) throw new Error('Invalid varint buffer');
      return n;
    }
  }
  throw new Error('Invalid varint buffer');
}

// TODO: Move this to 'plumbing'
async function unpack({
  gitdir,
  inputStream,
  onprogress /*: {gitdir: string, inputStream: ReadableStream, onprogress: Function} */
}) {
  return new _Promise(function (resolve, reject) {
    // Read header
    peek(inputStream, 12, (err, data, inputStream) => {
      if (err) return reject(err);
      let iden = data.slice(0, 4).toString('utf8');
      if (iden !== 'PACK') {
        throw new Error(`Packfile started with '${iden}'. Expected 'PACK'`);
      }
      let ver = data.slice(4, 8).toString('hex');
      if (ver !== '00000002') {
        throw new Error(`Unknown packfile version '${ver}'. Expected 00000002.`);
      }
      // Read a 4 byte (32-bit) int
      let numObjects = data.readInt32BE(8);
      if (onprogress !== undefined) {
        onprogress({ loaded: 0, total: numObjects, lengthComputable: true });
      }
      if (numObjects === 0) return resolve();
      // And on our merry way
      let totalTime = 0;
      let totalApplyDeltaTime = 0;
      let totalWriteFileTime = 0;
      let totalReadFileTime = 0;
      let offsetMap = new _Map();
      inputStream.pipe(listpack()).pipe(thru(async ({ data, type, reference, offset, num }, next) => {
        type = types[type];
        marky.mark(`${type} #${num} ${data.length}B`);
        if (type === 'ref-delta') {
          let oid = buffer.Buffer.from(reference).toString('hex');
          try {
            marky.mark(`readFile`);
            let { object, type } = await GitObjectManager.read({
              gitdir,
              oid
            });
            totalReadFileTime += marky.stop(`readFile`).duration;
            marky.mark(`applyDelta`);
            let result = applyDelta(data, object);
            totalApplyDeltaTime += marky.stop(`applyDelta`).duration;
            marky.mark(`writeFile`);
            let newoid = await GitObjectManager.write({
              gitdir,
              type,
              object: result
            });
            totalWriteFileTime += marky.stop(`writeFile`).duration;
            // console.log(`${type} ${newoid} ref-delta ${oid}`)
            offsetMap.set(offset, newoid);
          } catch (err) {
            throw new Error(`Could not find object ${reference} ${oid} that is referenced by a ref-delta object in packfile at byte offset ${offset}.`);
          }
        } else if (type === 'ofs-delta') {
          // Note: this might be not working because offsets might not be
          // guaranteed to be on object boundaries? In which case we'd need
          // to write the packfile to disk first, I think.
          // For now I've "solved" it by simply not advertising ofs-delta as a capability
          // during the HTTP request, so Github will only send ref-deltas not ofs-deltas.
          let absoluteOffset = offset - parseVarInt(reference);
          let referenceOid = offsetMap.get(absoluteOffset);
          // console.log(`${offset} ofs-delta ${absoluteOffset} ${referenceOid}`)
          let { type, object } = await GitObjectManager.read({
            gitdir,
            oid: referenceOid
          });
          let result = applyDelta(data, object);
          let oid = await GitObjectManager.write({
            gitdir,
            type,
            object: result
          });
          // console.log(`${offset} ${type} ${oid} ofs-delta ${referenceOid}`)
          offsetMap.set(offset, oid);
        } else {
          marky.mark(`writeFile`);
          let oid = await GitObjectManager.write({
            gitdir,
            type,
            object: data
          });
          totalWriteFileTime += marky.stop(`writeFile`).duration;
          // console.log(`${offset} ${type} ${oid}`)
          offsetMap.set(offset, oid);
        }
        if (onprogress !== undefined) {
          onprogress({
            loaded: numObjects - num,
            total: numObjects,
            lengthComputable: true
          });
        }
        let perfentry = marky.stop(`${type} #${num} ${data.length}B`);
        totalTime += perfentry.duration;
        if (num === 0) {
          console.log(`Total time unpacking objects: ${totalTime}`);
          console.log(`Total time applying deltas: ${totalApplyDeltaTime}`);
          console.log(`Total time reading files: ${totalReadFileTime}`);
          console.log(`Total time writing files: ${totalWriteFileTime}`);
          return resolve();
        }
        next(null);
      })).on('error', reject).on('finish', resolve);
    });
  });
}

// @flow
async function fetchPackfile({
  gitdir,
  ref,
  remote,
  url,
  authUsername,
  authPassword,
  depth = null,
  since = null,
  exclude = [],
  relative = false
}) {
  if (depth !== null) {
    if (_Number$isNaN(parseInt(depth))) {
      throw new Error(`Invalid value for depth argument: ${depth}`);
    }
    depth = parseInt(depth);
  }
  remote = remote || 'origin';
  if (url === undefined) {
    url = await config({
      gitdir,
      path: `remote.${remote}.url`
    });
  }
  let remoteHTTP = new GitRemoteHTTP(url);
  if (authUsername !== undefined && authPassword !== undefined) {
    remoteHTTP.auth = {
      username: authUsername,
      password: authPassword
    };
  }
  await remoteHTTP.preparePull();
  // Check server supports shallow cloning
  if (depth !== null && !remoteHTTP.capabilities.has('shallow')) {
    throw new Error(`Remote does not support shallow fetches`);
  }
  if (since !== null && !remoteHTTP.capabilities.has('deepen-since')) {
    throw new Error(`Remote does not support shallow fetches by date`);
  }
  if (exclude.length > 0 && !remoteHTTP.capabilities.has('deepen-not')) {
    throw new Error(`Remote does not support shallow fetches excluding commits reachable by refs`);
  }
  if (relative === true && !remoteHTTP.capabilities.has('deepen-relative')) {
    throw new Error(`Remote does not support shallow fetches relative to the current shallow depth`);
  }
  await GitRefManager.updateRemoteRefs({
    gitdir,
    remote,
    refs: remoteHTTP.refs,
    symrefs: remoteHTTP.symrefs
  });
  let want = await GitRefManager.resolve({
    gitdir,
    ref: `refs/remotes/${remote}/${ref}`
  });
  // Note: I removed "ofs-delta" from the capabilities list and now
  // Github uses all ref-deltas when I fetch packfiles instead of all ofs-deltas. Nice!
  const capabilities = `multi_ack_detailed no-done side-band-64k thin-pack agent=git/${name}@${version}${relative ? ' deepen-relative' : ''}`;
  let packstream = new stream__default.PassThrough();
  packstream.write(GitPktLine.encode(`want ${want} ${capabilities}\n`));
  let oids = await GitShallowManager.read({ gitdir });
  if (oids.size > 0 && remoteHTTP.capabilities.has('shallow')) {
    for (let oid of oids) {
      packstream.write(GitPktLine.encode(`shallow ${oid}\n`));
    }
  }
  if (depth !== null) {
    packstream.write(GitPktLine.encode(`deepen ${depth}\n`));
  }
  if (since !== null) {
    packstream.write(GitPktLine.encode(`deepen-since ${Math.floor(since.valueOf() / 1000)}\n`));
  }
  for (let x of exclude) {
    packstream.write(GitPktLine.encode(`deepen-not ${x}\n`));
  }
  packstream.write(GitPktLine.flush());
  let have = null;
  try {
    have = await GitRefManager.resolve({ gitdir, ref });
  } catch (err) {}
  if (have) {
    packstream.write(GitPktLine.encode(`have ${have}\n`));
    packstream.write(GitPktLine.flush());
  }
  packstream.end(GitPktLine.encode(`done\n`));
  let response = await remoteHTTP.pull(packstream);
  response.packetlines.pipe(thru(async (data, next) => {
    let line = data.toString('utf8');
    if (line.startsWith('shallow')) {
      let oid = line.slice(-41).trim();
      if (oid.length !== 40) {
        throw new Error(`non-40 character 'shallow' oid: ${oid}`);
      }
      oids.add(oid);
      await GitShallowManager.write({ gitdir, oids });
    } else if (line.startsWith('unshallow')) {
      let oid = line.slice(-41).trim();
      if (oid.length !== 40) {
        throw new Error(`non-40 character 'shallow' oid: ${oid}`);
      }
      oids.delete(oid);
      await GitShallowManager.write({ gitdir, oids });
    }
    next(null, data);
  }));
  return response;
}

async function fetch({
  gitdir,
  ref = 'HEAD',
  remote,
  url,
  authUsername,
  authPassword,
  depth,
  since,
  exclude,
  relative,
  onprogress
}) {
  let response = await fetchPackfile({
    gitdir,
    ref,
    remote,
    url,
    authUsername,
    authPassword,
    depth,
    since,
    exclude,
    relative
  });
  await unpack({ gitdir, inputStream: response.packfile, onprogress });
}

async function writeTreeToDisk({ gitdir, workdir, index, prefix, tree }) {
  for (let entry of tree) {
    let { type, object } = await GitObjectManager.read({
      gitdir,
      oid: entry.oid
    });
    let entrypath = prefix === '' ? entry.path : `${prefix}/${entry.path}`;
    let filepath = path.join(workdir, prefix, entry.path);
    switch (type) {
      case 'blob':
        await write(filepath, object);
        let stats = await pify(fs().lstat)(filepath);
        index.insert({
          filepath: entrypath,
          stats,
          oid: entry.oid
        });
        break;
      case 'tree':
        let tree = GitTree.from(object);
        await writeTreeToDisk({
          gitdir,
          workdir,
          index,
          prefix: entrypath,
          tree
        });
        break;
      default:
        throw new Error(`Unexpected object type ${type} found in tree for '${entrypath}'`);
    }
  }
}

async function checkout({ workdir, gitdir, remote, ref }) {
  // Get tree oid
  let oid;
  if (remote) {
    let remoteRef;
    if (ref === undefined) {
      remoteRef = await GitRefManager.resolve({
        gitdir,
        ref: `${remote}/HEAD`,
        depth: 2
      });
      ref = path.basename(remoteRef);
    } else {
      remoteRef = `${remote}/${ref}`;
    }
    oid = await GitRefManager.resolve({ gitdir, ref: remoteRef });
    // Make the remote ref our own!
    await write(`${gitdir}/refs/heads/${ref}`, oid + '\n');
  } else {
    if (ref === undefined) {
      throw new Error('Cannot checkout ref "undefined"');
    }
    oid = await GitRefManager.resolve({ gitdir, ref });
  }
  let commit = await GitObjectManager.read({ gitdir, oid });
  if (commit.type !== 'commit') {
    throw new Error(`Unexpected type: ${commit.type}`);
  }
  let comm = GitCommit.from(commit.object.toString('utf8'));
  let sha = comm.headers().tree;
  // Get top-level tree
  let { type, object } = await GitObjectManager.read({ gitdir, oid: sha });
  if (type !== 'tree') throw new Error(`Unexpected type: ${type}`);
  let tree = GitTree.from(object);
  // Acquire a lock on the index
  await GitIndexManager.acquire(`${gitdir}/index`, async function (index) {
    // TODO: Big optimization possible here.
    // Instead of deleting and rewriting everything, only delete files
    // that are not present in the new branch, and only write files that
    // are not in the index or are in the index but have the wrong SHA.
    for (let entry of index) {
      try {
        await rm(path.join(workdir, entry.path));
      } catch (err) {}
    }
    index.clear();
    // Write files. TODO: Write them atomically
    await writeTreeToDisk({ gitdir, workdir, index, prefix: '', tree });
    // Update HEAD TODO: Handle non-branch cases
    write(`${gitdir}/HEAD`, `ref: refs/heads/${ref}`);
  });
}

async function clone({
  workdir,
  gitdir,
  url,
  remote,
  ref,
  authUsername,
  authPassword,
  depth,
  since,
  exclude,
  relative,
  onprogress
}) {
  remote = remote || 'origin';
  await init({ gitdir });
  // Add remote
  await config({
    gitdir,
    path: `remote.${remote}.url`,
    value: url
  });
  // Fetch commits
  await fetch({
    gitdir,
    ref,
    remote,
    authUsername,
    authPassword,
    depth,
    since,
    exclude,
    relative,
    onprogress
  });
  // Checkout branch
  await checkout({
    workdir,
    gitdir,
    ref,
    remote
  });
}

async function constructTree({ gitdir, inode }) /*: string */{
  // use depth first traversal
  let children = inode.children;
  for (let inode of children) {
    if (inode.type === 'tree') {
      inode.metadata.mode = '040000';
      inode.metadata.oid = await constructTree({ gitdir, inode });
    }
  }
  let entries = children.map(inode => ({
    mode: inode.metadata.mode,
    path: inode.basename,
    oid: inode.metadata.oid,
    type: inode.type
  }));
  const tree = GitTree.from(entries);
  let oid = await GitObjectManager.write({
    gitdir,
    type: 'tree',
    object: tree.toObject()
  });
  return oid;
}

async function commit({
  gitdir,
  author,
  committer,
  message: message$$1,
  privateKeys
}) {
  // Fill in missing arguments with default values
  if (author === undefined) author = {};
  if (author.name === undefined) {
    author.name = await config({ gitdir, path: 'user.name' });
  }
  if (author.email === undefined) {
    author.email = await config({ gitdir, path: 'user.email' });
  }
  committer = committer || author;
  let authorDateTime = author.date || new Date();
  let committerDateTime = committer.date || authorDateTime;
  let oid;
  await GitIndexManager.acquire(`${gitdir}/index`, async function (index) {
    const inode = flatFileListToDirectoryStructure(index.entries);
    const treeRef = await constructTree({ gitdir, inode });
    let parents;
    try {
      let parent = await GitRefManager.resolve({ gitdir, ref: 'HEAD' });
      parents = [parent];
    } catch (err) {
      // Probably an initial commit
      parents = [];
    }
    let comm = GitCommit.from({
      tree: treeRef,
      parent: parents,
      author: {
        name: author.name,
        email: author.email,
        timestamp: author.timestamp || Math.floor(authorDateTime.valueOf() / 1000),
        timezoneOffset: author.timezoneOffset || 0
      },
      committer: {
        name: committer.name,
        email: committer.email,
        timestamp: committer.timestamp || Math.floor(committerDateTime.valueOf() / 1000),
        timezoneOffset: committer.timezoneOffset || 0
      },
      message: message$$1
    });
    if (privateKeys) {
      comm = await comm.sign(privateKeys);
    }
    oid = await GitObjectManager.write({
      gitdir,
      type: 'commit',
      object: comm.toObject()
    });
    // Update branch pointer
    const branch = await GitRefManager.resolve({
      gitdir,
      ref: 'HEAD',
      depth: 2
    });
    await write(path.join(gitdir, branch), oid + '\n');
  });
  return oid;
}

async function list({ gitdir }) {
  let filenames;
  await GitIndexManager.acquire(`${gitdir}/index`, async function (index) {
    filenames = index.entries.map(x => x.path);
  });
  return filenames;
}

// @flow
/*::
import type {Writable} from 'stream'
*/

const types$1 = {
  commit: 0b0010000,
  tree: 0b0100000,
  blob: 0b0110000,
  tag: 0b1000000
  // TODO: Move this to 'plumbing'
};async function pack({
  oids,
  gitdir,
  outputStream /*: {oids: Array<string>, gitdir: string, outputStream: Writable} */
}) {
  let hash = crypto.createHash('sha1');
  let stream$$1 = outputStream;
  function write(chunk, enc) {
    stream$$1.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$1[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.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({ gitdir, oid });
    writeObject({ write, object, stype: type });
  }
  // Write SHA1 checksum
  let digest = hash.digest();
  stream$$1.end(digest);
  return stream$$1;
}

// @flow
async function listCommits({
  gitdir,
  start,
  finish /*: {
         gitdir: string,
         start: Array<string>,
         finish: Array<string>
         } */
}) {
  let startingSet = new _Set();
  let finishingSet = new _Set();
  for (let ref of start) {
    startingSet.add((await GitRefManager.resolve({ 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({ gitdir, ref });
      finishingSet.add(oid);
    } catch (err) {}
  }
  let visited = new _Set(); /*: Set<string> */

  // 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({ 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({ gitdir, oids /*: {
                                                 gitdir: string,
                                                 oids: Array<string>
                                                 } */
}) {
  let visited /*: Set<string> */ = 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({ 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 /*: TreeEntry */ 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 push({
  gitdir,
  ref,
  remote,
  url,
  authUsername,
  authPassword
}) {
  // TODO: Figure out how pushing tags works. (This only works for branches.)
  remote = remote || 'origin';
  if (url === undefined) {
    url = await config({
      gitdir,
      path: `remote.${remote}.url`
    });
  }
  let fullRef = ref.startsWith('refs/') ? ref : `refs/heads/${ref}`;
  let oid = await GitRefManager.resolve({ gitdir, ref });
  let httpRemote = new GitRemoteHTTP(url);
  if (authUsername !== undefined && authPassword !== undefined) {
    httpRemote.auth = {
      username: authUsername,
      password: authPassword
    };
  }
  await httpRemote.preparePush();
  let commits = await listCommits({
    gitdir,
    start: [oid],
    finish: httpRemote.refs.values()
  });
  let objects = await listObjects({ gitdir, oids: commits });
  let packstream = new stream__default.PassThrough();
  let oldoid = httpRemote.refs.get(fullRef) || '0000000000000000000000000000000000000000';
  packstream.write(GitPktLine.encode(`${oldoid} ${oid} ${fullRef}\0 report-status\n`));
  packstream.write(GitPktLine.flush());
  pack({
    gitdir,
    oids: [...objects],
    outputStream: packstream
  });
  let response = await httpRemote.push(packstream);
  return response;
}

// @flow
async function log({
  gitdir,
  ref = 'HEAD',
  depth,
  since // Date
}) {
  let sinceTimestamp = since === undefined ? undefined : Math.floor(since.valueOf() / 1000);
  // TODO: In the future, we may want to have an API where we return a
  // async iterator that emits commits.
  let commits = [];
  let start = await GitRefManager.resolve({ gitdir, ref });
  let { type, object } = await GitObjectManager.read({ gitdir, oid: start });
  if (type !== 'commit') {
    throw new Error(`The given ref ${ref} did not resolve to a commit but to a ${type}`);
  }
  let currentCommit = _extends({ oid: start }, GitCommit.from(object).parse());
  commits.push(currentCommit);
  while (true) {
    if (depth !== undefined && commits.length === depth) break;
    if (currentCommit.parent.length === 0) break;
    let oid = currentCommit.parent[0];
    let gitobject;
    try {
      gitobject = await GitObjectManager.read({ gitdir, oid });
    } catch (err) {
      commits.push({
        oid,
        error: err
      });
      break;
    }
    let { type, object } = gitobject;
    if (type !== 'commit') {
      commits.push({
        oid,
        error: new Error(`Invalid commit parent ${oid} is of type ${type}`)
      });
      break;
    }
    currentCommit = _extends({ oid }, GitCommit.from(object).parse());
    if (sinceTimestamp !== undefined && currentCommit.author.timestamp <= sinceTimestamp) {
      break;
    }
    commits.push(currentCommit);
  }
  return commits;
}

async function remove({ gitdir, filepath }) {
  await GitIndexManager.acquire(`${gitdir}/index`, async function (index) {
    index.delete({ filepath });
  });
  // TODO: return oid?
}

const HttpKeyServer = new openpgp.HKP();

async function verify({ gitdir, ref, publicKeys }) {
  const oid = await GitRefManager.resolve({ gitdir, ref });
  const { type, object } = await GitObjectManager.read({ gitdir, oid });
  if (type !== 'commit') {
    throw new Error(`git.verify() was expecting a ref type 'commit' but got type '${type}'`);
  }
  let commit = GitCommit.from(object);
  let author = commit.headers().author;
  let keys = await commit.listSigningKeys();
  if (!publicKeys) {
    let keyArray = await _Promise.all(keys.map(id => HttpKeyServer.lookup({ keyId: id })));
    publicKeys = keyArray.join('\n');
  }
  let validity = await commit.verify(publicKeys);
  if (!validity) return false;
  return { author, keys };
}

// @flow
async function getOidAtPath({
  gitdir,
  tree,
  path: path$$1 /*: {
       gitdir: string,
       tree: GitTree,
       path: string|Array<string>
       } */
}) {
  if (typeof path$$1 === 'string') path$$1 = path$$1.split('/');
  let dirname = path$$1.shift();
  for (let entry of tree) {
    if (entry.path === dirname) {
      if (path$$1.length === 0) {
        return entry.oid;
      }
      let { type, object } = await GitObjectManager.read({
        gitdir,
        oid: entry.oid
      });
      if (type === 'tree') {
        let tree = GitTree.from(object);
        return getOidAtPath({ gitdir, tree, path: path$$1 });
      }
      if (type === 'blob') {
        throw new Error(`Blob found where tree expected.`);
      }
    }
  }
  return null;
}

async function getHeadTree({ gitdir }) {
  // Get the tree from the HEAD commit.
  let oid = await GitRefManager.resolve({ gitdir, ref: 'HEAD' });
  let { object: cobject } = await GitObjectManager.read({ gitdir, oid });
  let commit = GitCommit.from(cobject);
  let { object: tobject } = await GitObjectManager.read({
    gitdir,
    oid: commit.parseHeaders().tree
  });
  let tree = GitTree.from(tobject).entries();
  return tree;
}

// For now we're just diffing individual files
async function status({
  workdir,
  gitdir,
  pathname /*: {
           workdir: string,
           gitdir: string,
           pathname: string
           } */
}) {
  let ignored = await GitIgnoreManager.isIgnored({ gitdir, workdir, pathname });
  if (ignored) {
    return 'ignored';
  }
  let headTree = await getHeadTree({ gitdir });
  let treeOid = await getOidAtPath({ gitdir, tree: headTree, path: pathname });
  let indexEntry = null;
  // Acquire a lock on the index
  await GitIndexManager.acquire(`${gitdir}/index`, async function (index) {
    for (let entry of index) {
      if (entry.path === pathname) {
        indexEntry = entry;
        break;
      }
    }
  });
  let stats = null;
  try {
    stats = await pify(fs().lstat)(path.join(workdir, pathname));
  } catch (err) {
    if (err.code !== 'ENOENT') {
      throw err;
    }
  }

  if (treeOid === null && stats === null) {
    if (indexEntry === null) {
      return 'absent';
    } else {
      return '*absent';
    }
  } else if (treeOid !== null && stats === null) {
    if (indexEntry === null) {
      return 'deleted';
    } else {
      return '*deleted';
    }
  } else if (treeOid === null && stats !== null) {
    if (indexEntry === null) {
      return '*added';
    } else {
      let object = await read(path.join(workdir, pathname));
      let workdirOid = await GitObjectManager.hash({
        gitdir,
        type: 'blob',
        object
      });
      return workdirOid === indexEntry.oid ? 'added' : '*added';
    }
  } else {
    let object = await read(path.join(workdir, pathname));
    let workdirOid = await GitObjectManager.hash({
      gitdir,
      type: 'blob',
      object
    });
    if (workdirOid === treeOid) {
      if (indexEntry === null) return '*unmodified';
      return workdirOid === indexEntry.oid ? 'unmodified' : '*unmodified';
    }
    if (indexEntry === null) return '*modified';
    return workdirOid === indexEntry.oid ? 'modified' : '*modified';
  }
}

async function test(filepath) {
  try {
    await pify(fs().lstat)(path.join(filepath, '.git'));
    return true;
  } catch (err) {
    return false;
  }
}
// TODO: Detect base repositories?
async function findRoot(filepath) {
  if (await test(filepath)) return filepath;
  let parent = path.dirname(filepath);
  if (parent === filepath) throw new Error('Unable to find git root');
  return findRoot(parent);
}

// Note: this function will get more interesting once we add support for
// packed-refs.
async function listBranches({ gitdir }) {
  let files = await readDirAsFlatFileList(`${gitdir}/refs/heads`);
  files = files.map(x => x.replace(`${gitdir}/refs/heads/`, ''));
  let text = await read(`${gitdir}/packed-refs`, { encoding: 'utf8' });
  if (text) {
    let refs = text.trim().split('\n').filter(x => x.includes('refs/heads')).map(x => x.replace(/^.+ refs\/heads\//, '').trim()).filter(x => !files.includes(x)); // remove duplicates
    files = files.concat(refs);
  }
  return files;
}

function version$1() {
  return version;
}



var commands = Object.freeze({
	add: add,
	clone: clone,
	checkout: checkout,
	commit: commit,
	fetch: fetch,
	init: init,
	list: list,
	listCommits: listCommits,
	listObjects: listObjects,
	log: log,
	pack: pack,
	push: push,
	remove: remove,
	config: config,
	unpack: unpack,
	verify: verify,
	status: status,
	findRoot: findRoot,
	listBranches: listBranches,
	version: version$1
});

function git(dir) {
  return dir === undefined ? new Git() : new Git().workdir(dir).gitdir(`${dir}/.git`);
}
git.commands = commands;
git.managers = managers;
git.models = models;
git.utils = utils;

const extend = (self, array) => {
  for (let fn of array) {
    self[fn] = val => self.set(fn, val);
  }
};

// This is so git().relative() is the same as git().relative(true)
// but you can still undo the flag later with git().relative(false)
const extendBool = (self, array) => {
  for (let fn of array) {
    self[fn] = val => self.set(fn, val !== false);
  }
};

// The class is merely a fluent command/query builder
class Git extends _Map {
  // @constructor
  constructor(parent) {
    super(parent);
    extend(this, ['workdir', 'gitdir', 'remote', 'branch', 'author', 'email', 'datetime', 'depth', 'since', 'exclude', 'timestamp', 'signingKey', 'verificationKey', 'username', 'password', 'url', 'outputStream', 'inputStream', 'onprogress']);
    extendBool(this, ['relative']);
  }
  version() {
    return version$1();
  }
  /**
   * @param {string} username
   * @param {string} password
   * @returns {Git} this
   *
   * Use with {@link #gitpush .push} and {@link #gitpull .pull} to set Basic Authentication headers.
   * This works for basic username / password auth, or the newer username / token auth
   * that is often required if 2FA is enabled.
   */
  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
      }
    }
    this.set('username', username);
    this.set('password', password);
    return this;
  }
  /**
   * @param {string} company
   * @param {string} token
   * @returns {Git} this
   *
   * Use with {@link #gitpush .push} and {@link #gitpull .pull} to set Basic Authentication headers.
   * This for is for *actual* OAuth2 tokens (not "personal access tokens").
   * Unfortunately, all the major git hosting companies have chosen different conventions!
   * Lucky for you, I already looked up and codified it for you.
   *
   * - oauth2('github', token) - Github uses `token` as the username, and 'x-oauth-basic' as the password.
   * - oauth2('bitbucket', token) - Bitbucket uses 'x-token-auth' as the username, and `token` as the password.
   * - oauth2('gitlab', token) - Gitlab uses 'oauth2' as the username, and `token` as the password.
   *
   * I will gladly accept pull requests for more companies' conventions.
   */
  oauth2(company, token) {
    switch (company) {
      case 'github':
        this.set('username', token);
        this.set('password', 'x-oauth-basic');
        break;
      case 'bitbucket':
        this.set('username', 'x-token-auth');
        this.set('password', token);
        break;
      case 'gitlab':
        this.set('username', 'oauth2');
        this.set('password', token);
        break;
      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.`);
    }
    return this;
  }
  async findRoot(dir) {
    return findRoot(dir);
  }
  async init() {
    await init({
      gitdir: this.get('gitdir')
    });
  }
  async fetch(ref) {
    await fetch({
      gitdir: this.get('gitdir'),
      ref,
      url: this.get('url'),
      remote: this.get('remote'),
      authUsername: this.get('username'),
      authPassword: this.get('password'),
      depth: this.get('depth'),
      since: this.get('since'),
      exclude: this.get('exclude'),
      relative: this.get('relative'),
      onprogress: this.get('onprogress')
    });
  }
  async checkout(ref) {
    await checkout({
      workdir: this.get('workdir'),
      gitdir: this.get('gitdir'),
      ref,
      remote: this.get('remote')
    });
  }
  async clone(url) {
    await clone({
      workdir: this.get('workdir'),
      gitdir: this.get('gitdir'),
      url,
      remote: this.get('remote'),
      ref: this.get('branch'),
      authUsername: this.get('username'),
      authPassword: this.get('password'),
      depth: this.get('depth'),
      since: this.get('since'),
      exclude: this.get('exclude'),
      relative: this.get('relative'),
      onprogress: this.get('onprogress')
    });
  }
  async list() {
    return list({
      gitdir: this.get('gitdir')
    });
  }
  async log(ref) {
    return log({
      gitdir: this.get('gitdir'),
      ref,
      depth: this.get('depth'),
      since: this.get('since')
    });
  }
  async add(filepath) {
    return add({
      gitdir: this.get('gitdir'),
      workdir: this.get('workdir'),
      filepath
    });
  }
  async remove(filepath) {
    return remove({
      gitdir: this.get('gitdir'),
      filepath
    });
  }
  async commit(message$$1) {
    return commit({
      gitdir: this.get('gitdir'),
      author: {
        name: this.get('author'),
        email: this.get('email'),
        timestamp: this.get('timestamp'),
        date: this.get('datetime')
      },
      committer: {
        name: this.get('author'),
        email: this.get('email'),
        timestamp: this.get('timestamp'),
        date: this.get('datetime')
      },
      message: message$$1,
      privateKeys: this.get('signingKey')
    });
  }
  async verify(ref) {
    return verify({
      gitdir: this.get('gitdir'),
      publicKeys: this.get('verificationKey'),
      ref
    });
  }
  async pack(oids) {
    return pack({
      gitdir: this.get('gitdir'),
      outputStream: this.get('outputStream'),
      oids
    });
  }
  async unpack(oids) {
    return unpack({
      gitdir: this.get('gitdir'),
      inputStream: this.get('inputStream')
    });
  }
  async push(ref) {
    return push({
      gitdir: this.get('gitdir'),
      ref,
      remote: this.get('remote'),
      url: this.get('url'),
      authUsername: this.get('username'),
      authPassword: this.get('password')
    });
  }
  async pull(ref) {
    return fetch({
      gitdir: this.get('gitdir'),
      ref,
      remote: this.get('remote'),
      authUsername: this.get('username'),
      authPassword: this.get('password')
    });
  }
  async config(path$$1, value) {
    if (arguments.length === 1) {
      return config({
        gitdir: this.get('gitdir'),
        path: path$$1
      });
    } else {
      return config({
        gitdir: this.get('gitdir'),
        path: path$$1,
        value
      });
    }
  }
  async status(pathname) {
    return status({
      gitdir: this.get('gitdir'),
      workdir: this.get('workdir'),
      pathname
    });
  }
  async listBranches() {
    return listBranches({
      gitdir: this.get('gitdir')
    });
  }
}

module.exports = git;
//# sourceMappingURL=index.js.map
