'use strict';

Object.defineProperty(exports, '__esModule', { value: true });

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

var models_js = require('./models.js');
var path = _interopDefault(require('path'));
var AsyncLock = _interopDefault(require('async-lock'));
var ignore = _interopDefault(require('ignore'));
var buffer = require('buffer');
var shasum = _interopDefault(require('shasum'));
var pako = _interopDefault(require('pako'));
var simpleGet = _interopDefault(require('simple-get'));
var concat = _interopDefault(require('simple-concat'));
var pify = _interopDefault(require('pify'));
var utils_js = require('./utils.js');
var stream = require('stream');

// @flow

/** @ignore */
class GitConfigManager {
  static async get({ fs: _fs, gitdir }) {
    const fs = new models_js.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 models_js.GitConfig.from(text);
  }
  static async save({ fs: _fs, gitdir, config }) {
    const fs = new models_js.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'
    });
  }
}

// @flow
const lock = new AsyncLock();

/** @ignore */
class GitShallowManager {
  static async read({ fs: _fs, gitdir }) {
    const fs = new models_js.FileSystem(_fs);
    const filepath = path.join(gitdir, 'shallow');
    let oids = new Set();
    await lock.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 models_js.FileSystem(_fs);
    const filepath = path.join(gitdir, 'shallow');
    let text = [...oids].join('\n') + '\n';
    await lock.acquire(filepath, async function () {
      await fs.write(filepath, text, {
        encoding: 'utf8'
      });
    });
  }
}

// @flow
// 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();

/** @ignore */
class GitIndexManager {
  static async acquire({ fs: _fs, filepath }, closure) {
    const fs = new models_js.FileSystem(_fs);
    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 fs.read(filepath);
        index = models_js.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 fs.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

/** @ignore */
class GitIgnoreManager {
  static async isIgnored({
    fs: _fs,
    dir,
    gitdir = path.join(dir, '.git'),
    filepath
  }) /*: Promise<boolean> */{
    const fs = new models_js.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;
  }
}

const PackfileCache = new Map();

/** @ignore */
class GitObjectManager {
  static async read({ fs: _fs, gitdir, oid, format = 'content' }) {
    const fs = new models_js.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 models_js.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 models_js.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$$1 = buffer.Buffer.from(pako.inflate(file));
    if (format === 'wrapped') {
      return { format: 'wrapped', object: buffer$$1, source };
    }
    let { type, object } = models_js.GitObject.unwrap({ oid, buffer: buffer$$1 });
    if (format === 'content') return { type, format: 'content', object, source };
  }

  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({ fs: _fs, gitdir, type, object }) /*: Promise<string> */{
    const fs = new models_js.FileSystem(_fs);
    let { buffer: buffer$$1, oid } = models_js.GitObject.wrap({ type, object });
    let file = buffer.Buffer.from(pako.deflate(buffer$$1));
    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`];

/** @ignore */
class GitRefManager {
  /* ::
  updateRemoteRefs : ({
    gitdir: string,
    remote: string,
    refs: Map<string, string>,
    symrefs: Map<string, string>
  }) => Promise<void>
  */
  static async updateRemoteRefs({
    fs: _fs,
    gitdir,
    remote,
    refs,
    symrefs,
    tags,
    refspecs
  }) {
    const fs = new models_js.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 fot remote '${remote}'.
Make sure the config file has an entry like the following:
[remote "${remote}"]
fetch = +refs/heads/*:refs/remotes/origin/*`);
      }
    }
    const refspec = models_js.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 models_js.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 models_js.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 models_js.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, depth, map }) {
    if (depth !== undefined) {
      depth--;
      if (depth === -1) {
        return ref;
      }
    }
    // Is it a ref pointer?
    if (ref.startsWith('ref: ')) {
      ref = ref.slice('ref: '.length);
      return GitRefManager.resolveAgainstMap({ ref, depth, map });
    }
    // Is it a complete and valid SHA?
    if (ref.length === 40 && /[0-9a-f]{40}/.test(ref)) {
      return 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(), 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 models_js.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 models_js.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 models_js.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 models_js.FileSystem(_fs);
    let tags = await GitRefManager.listRefs({
      fs,
      gitdir,
      filepath: `refs/tags`
    });
    tags = tags.filter(x => !x.endsWith('^{}'));
    return tags;
  }
}

// @flow

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

/** @ignore */
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(`HTTP Error: ${res.statusCode} ${res.statusMessage}`);
    }
    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 = models_js.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;
    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 = await 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 { packetlines, packfile } = await this.stream({
      stream: stream$$1,
      service
    });
    // TODO: Someday, maybe we will make this a streaming parser.
    packfile = await pify(concat)(packfile);
    packetlines = await pify(concat)(packetlines);
    let result = {};
    // Parse the response!
    // I'm combining the side-band-64k and regular streams
    // because Github returns the first line in the sideband while
    // git-http-server returns it without the sideband.
    let response = '';
    let read = models_js.GitPktLine.reader(packfile);
    let line = await read();
    while (line !== null && line !== true) {
      response += line.toString('utf8') + '\n';
      line = await read();
    }
    response += packetlines.toString('utf8');

    let lines = response.toString('utf8').split('\n');
    // We're expecting "unpack {unpack-result}"
    line = lines.shift();
    if (!line.startsWith('unpack ')) {
      throw new Error(`Unparsable response from server! Expected 'unpack ok' or 'unpack [error message]' but got '${line}'`);
    }
    if (line === 'unpack ok') {
      result.ok = ['unpack'];
    } else {
      result.errors = [line.trim()];
    }
    for (let line of lines) {
      let status = line.slice(0, 2);
      let refAndMessage = line.slice(3);
      if (status === 'ok') {
        result.ok = result.ok || [];
        result.ok.push(refAndMessage);
      } else if (status === 'ng') {
        result.errors = result.errors || [];
        result.errors.push(refAndMessage);
      }
    }
    utils_js.log(result);
    return result;
  }
  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/${utils_js.pkg.name}@${utils_js.pkg.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
    });
    if (res.statusCode !== 200) {
      throw new Error(`HTTP Error: ${res.statusCode} ${res.statusMessage}`);
    }
    // Parse the response!
    let read = models_js.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();
      // 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
    };
  }
}

exports.GitConfigManager = GitConfigManager;
exports.GitShallowManager = GitShallowManager;
exports.GitIndexManager = GitIndexManager;
exports.GitIgnoreManager = GitIgnoreManager;
exports.GitObjectManager = GitObjectManager;
exports.GitRefManager = GitRefManager;
exports.GitRemoteHTTP = GitRemoteHTTP;
