import path from 'path';
import { GitIndexManager, GitObjectManager, GitConfigManager, GitRemoteHTTP, GitRefManager, GitShallowManager, GitIgnoreManager } from './managers.js';
import { FileSystem, GitPktLine, GitCommit, GitTree, SignedGitCommit, GitPackIndex } from './models.js';
import { PassThrough } from 'stream';
import through2 from 'through2';
import pify from 'pify';
import concat from 'simple-concat';
import split2 from 'split2';
import { pkg, flatFileListToDirectoryStructure } from './utils.js';
import { Buffer } from 'buffer';
import pad from 'pad';
import pako from 'pako';
import createHash from 'sha.js';

/**
 * @external {FSModule} http://ghub.io/browserfs
 */

/**
 * Add a file to the git index (aka staging area)
 *
 * @param {Object} args - Arguments object
 * @param {FSModule} args.fs - The filesystem holding the git repo
 * @param {string} args.dir - The path to the [working tree](index.html#dir-vs-gitdir) directory
 * @param {string} [args.gitdir=path.join(dir, '.git')] - The path to the [git directory](index.html#dir-vs-gitdir)
 * @param {string} args.filepath - The path to the file to add to the index.
 * @returns {Promise<void>} - Resolves successfully once the git index has been updated.
 *
 * @example
 * let repo = {fs, dir: '<@.@>'}
 * await new Promise((resolve, reject) => fs.writeFile(
 *   '<@README.md@>',
 *   `<<@# TEST@>>`,
 *   (err) => err ? reject(err) : resolve()
 * ))
 * await git.add({...repo, filepath: '<@README.md@>'})
 * console.log('done')
 */
async function add({
  dir,
  gitdir = path.join(dir, '.git'),
  fs: _fs,
  filepath
}) {
  const fs = new FileSystem(_fs);
  const type = 'blob';
  const object = await fs.read(path.join(dir, filepath));
  if (object === null) throw new Error(`Could not read file '${filepath}'`);
  const oid = await GitObjectManager.write({ fs, gitdir, type, object });
  await GitIndexManager.acquire({ fs, filepath: `${gitdir}/index` }, async function (index) {
    let stats = await fs._lstat(path.join(dir, filepath));
    index.insert({ filepath, stats, oid });
  });
  // TODO: return oid?
}

/**
 * Initialize a new repository
 *
 * @param {Object} args - Arguments object
 * @param {FSModule} args.fs - The filesystem holding the git repo
 * @param {string} args.dir - The path to the [working tree](index.html#dir-vs-gitdir) directory
 * @param {string} [args.gitdir=path.join(dir, '.git')] - The path to the [git directory](index.html#dir-vs-gitdir)
 * @returns {Promise<void>} - Resolves successfully when filesystem operations are complete.
 *
 * @example
 * let repo = {fs, dir: '<@.@>'}
 * await git.init(repo)
 * console.log('done')
 */
async function init({ dir, gitdir = path.join(dir, '.git'), fs: _fs }) {
  const fs = new FileSystem(_fs);
  let folders = ['hooks', 'info', 'objects/info', 'objects/pack', 'refs/heads', 'refs/tags'];
  folders = folders.map(dir => gitdir + '/' + dir);
  for (let folder of folders) {
    await fs.mkdir(folder);
  }
  await fs.write(gitdir + '/config', '[core]\n' + '\trepositoryformatversion = 0\n' + '\tfilemode = false\n' + '\tbare = false\n' + '\tlogallrefupdates = true\n' + '\tsymlinks = false\n' + '\tignorecase = true\n');
  await fs.write(gitdir + '/HEAD', 'ref: refs/heads/master\n');
}

function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; }

/**
 * Read and/or write to the git config file(s)
 * @param {Object} args - Arguments object
 * @param {FSModule} args.fs - The filesystem holding the git repo
 * @param {string} args.dir - The path to the [working tree](index.html#dir-vs-gitdir) directory
 * @param {string} [args.gitdir=path.join(dir, '.git')] - The path to the [git directory](index.html#dir-vs-gitdir)
 * @param {string} args.path -  The key of the git config entry.
 * @param {string} [args.value] - A value to store at that path.
 * @returns {Promise<any>} - Resolves with the config value
 *
 * If no `value` is provided, it does a read.
 * If a `value` is provided, it does a write.
 *
 * @example
 * let repo = {fs, dir: '<@.@>'}
 *
 * // Write config value
 * await git.config({
 *   ...repo,
 *   path: '<@user.name@>',
 *   value: '<@Mr. Test@>'
 * })
 *
 * // Read config value
 * let value = await git.config({
 *   ...repo,
 *   path: '<@user.name@>'
 * })
 * console.log(value)
 */
async function config(_ref) {
  let {
    dir,
    gitdir = path.join(dir, '.git'),
    fs: _fs,
    all = false,
    append = false
  } = _ref,
      args = _objectWithoutProperties(_ref, ['dir', 'gitdir', 'fs', 'all', 'append']);

  const fs = new FileSystem(_fs);
  let { path: path$$1, value } = args;
  const config = await GitConfigManager.get({ fs, 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')) {
    if (all) {
      return config.getall(path$$1);
    } else {
      return config.get(path$$1);
    }
  } else {
    if (append) {
      await config.append(path$$1, value);
    } else {
      await config.set(path$$1, value);
    }
    await GitConfigManager.save({ fs, gitdir, config });
  }
}

/**
 * Fetch commits
 *
 * @link https://isomorphic-git.github.io/docs/fetch.html
 */
async function fetch({
  dir,
  gitdir = path.join(dir, '.git'),
  fs: _fs,
  emitter,
  ref = 'HEAD',
  refs,
  remote,
  url,
  authUsername,
  authPassword,
  depth,
  since,
  exclude,
  relative,
  tags,
  singleBranch,
  onprogress // deprecated
}) {
  if (onprogress !== undefined) {
    console.warn('The `onprogress` callback has been deprecated. Please use the more generic `emitter` EventEmitter argument instead.');
  }
  const fs = new FileSystem(_fs);
  let response = await fetchPackfile({
    gitdir,
    fs,
    ref,
    refs,
    remote,
    url,
    authUsername,
    authPassword,
    depth,
    since,
    exclude,
    relative,
    tags,
    singleBranch
  });
  // Note: progress messages are designed to be written directly to the terminal,
  // so they are often sent with just a carriage return to overwrite the last line of output.
  // But there are also messages delimited with newlines.
  // I also include CRLF just in case.
  response.progress.pipe(split2(/(\r\n)|\r|\n/)).on('data', line => {
    if (emitter) {
      emitter.emit('message', line.trim());
    }
    let matches = line.match(/\((\d+?)\/(\d+?)\)/);
    if (matches && emitter) {
      emitter.emit('progress', {
        loaded: parseInt(matches[1], 10),
        total: parseInt(matches[2], 10),
        lengthComputable: true
      });
    }
  });
  let packfile = await pify(concat)(response.packfile);
  let packfileSha = packfile.slice(-20).toString('hex');
  await fs.write(path.join(gitdir, `objects/pack/pack-${packfileSha}.pack`), packfile);
  // TODO: Return more metadata?
  return {
    defaultBranch: response.HEAD,
    fetchHead: response.FETCH_HEAD
  };
}

async function fetchPackfile({
  gitdir,
  fs: _fs,
  ref,
  refs = [ref],
  remote,
  url,
  authUsername,
  authPassword,
  depth = null,
  since = null,
  exclude = [],
  relative = false,
  tags = false,
  singleBranch = false
}) {
  const fs = new FileSystem(_fs);
  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({
      fs,
      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`);
  }
  // TODO: Don't add other refs if singleBranch is specified.
  await GitRefManager.updateRemoteRefs({
    fs,
    gitdir,
    remote,
    refs: remoteHTTP.refs,
    symrefs: remoteHTTP.symrefs,
    tags
  });
  // 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 ofs-delta agent=git/${pkg.name}@${pkg.version}${relative ? ' deepen-relative' : ''}`;
  let packstream = new PassThrough();
  // Figure out the SHA for the requested ref
  let oid = GitRefManager.resolveAgainstMap({
    ref,
    map: remoteHTTP.refs
  });
  // Start requesting oids from the remote by their SHAs
  let wants = singleBranch ? [oid] : remoteHTTP.refs.values();
  wants = [...new Set(wants)]; // remove duplicates
  let firstLineCapabilities = ` ${capabilities}`;
  for (const want of wants) {
    packstream.write(GitPktLine.encode(`want ${want}${firstLineCapabilities}\n`));
    firstLineCapabilities = '';
  }
  let oids = await GitShallowManager.read({ fs, 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`));
  }
  let haves = [];
  for (let ref of refs) {
    try {
      ref = await GitRefManager.expand({ fs, gitdir, ref });
      // TODO: Actually, should we test whether we have the object using readObject?
      if (!ref.startsWith('refs/tags')) {
        let have = await GitRefManager.resolve({ fs, gitdir, ref });
        haves.push(have);
      }
    } catch (err) {}
  }
  for (const have of haves) {
    packstream.write(GitPktLine.encode(`have ${have}\n`));
  }
  packstream.write(GitPktLine.flush());
  packstream.end(GitPktLine.encode(`done\n`));
  let response = await remoteHTTP.pull(packstream);
  // Apply all the 'shallow' and 'unshallow' commands
  response.packetlines.pipe(through2(async (data, enc, 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({ fs, 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({ fs, gitdir, oids });
    }
    next(null, data);
  }));
  // We need this value later for the `clone` command.
  response.HEAD = remoteHTTP.symrefs.get('HEAD');
  response.FETCH_HEAD = oid;
  return response;
}

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

async function checkout({
  dir,
  gitdir = path.join(dir, '.git'),
  fs: _fs,
  remote = 'origin',
  ref
}) {
  const fs = new FileSystem(_fs);
  if (ref === undefined) {
    throw new Error('Cannot checkout ref "undefined"');
  }
  // Get tree oid
  let oid;
  try {
    oid = await GitRefManager.resolve({ fs, gitdir, ref });
    // TODO: Figure out what to do if both 'ref' and 'remote' are specified, ref already exists,
    // and is configured to track a different remote.
  } catch (err) {
    // If `ref` doesn't exist, create a new remote tracking branch
    // Figure out the commit to checkout
    let remoteRef = `${remote}/${ref}`;
    oid = await GitRefManager.resolve({
      fs,
      gitdir,
      ref: remoteRef
    });
    // Set up remote tracking branch
    await config({
      gitdir,
      fs,
      path: `branch.${ref}.remote`,
      value: `${remote}`
    });
    await config({
      gitdir,
      fs,
      path: `branch.${ref}.merge`,
      value: `refs/heads/${ref}`
    });
    // Create a new branch that points at that same commit
    await fs.write(`${gitdir}/refs/heads/${ref}`, oid + '\n');
  }
  let commit = {};
  try {
    commit = await GitObjectManager.read({ fs, gitdir, oid });
  } catch (err) {
    throw new Error(`Failed to checkout ref '${ref}' because commit ${oid} is not available locally. Do a git fetch to make the branch available locally.`);
  }
  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({ fs, 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({ fs, filepath: `${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 fs.rm(path.join(dir, entry.path));
      } catch (err) {}
    }
    index.clear();
    // Write files. TODO: Write them atomically
    await writeTreeToDisk({ fs, gitdir, dir, index, prefix: '', tree });
    // Update HEAD TODO: Handle non-branch cases
    await fs.write(`${gitdir}/HEAD`, `ref: refs/heads/${ref}`);
  });
}

/**
 * Clone a repository
 *
 * @link https://isomorphic-git.github.io/docs/clone.html
 */
async function clone({
  dir,
  gitdir = path.join(dir, '.git'),
  fs: _fs,
  emitter,
  url,
  ref,
  remote,
  authUsername,
  authPassword,
  depth,
  since,
  exclude,
  relative,
  singleBranch,
  onprogress
}) {
  if (onprogress !== undefined) {
    console.warn('The `onprogress` callback has been deprecated. Please use the more generic `emitter` EventEmitter argument instead.');
  }
  const fs = new FileSystem(_fs);
  remote = remote || 'origin';
  await init({ gitdir, fs });
  // Add remote
  await config({
    gitdir,
    fs,
    path: `remote.${remote}.url`,
    value: url
  });
  await config({
    gitdir,
    fs,
    path: `remote.${remote}.fetch`,
    value: `+refs/heads/*:refs/remotes/${remote}/*`
  });
  // Fetch commits
  let { defaultBranch } = await fetch({
    gitdir,
    fs,
    emitter,
    ref,
    remote,
    authUsername,
    authPassword,
    depth,
    since,
    exclude,
    relative,
    singleBranch,
    tags: true
  });
  ref = ref || defaultBranch;
  ref = ref.replace('refs/heads/', '');
  // Checkout that branch
  await checkout({
    dir,
    gitdir,
    fs,
    ref,
    remote
  });
}

async function constructTree({ fs, 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({ fs, 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({
    fs,
    gitdir,
    type: 'tree',
    object: tree.toObject()
  });
  return oid;
}

/**
 * Create a new commit
 * @param {Object} args - Arguments object
 * @param {FSModule} args.fs - The filesystem holding the git repo
 * @param {string} args.dir - The path to the [working tree](index.html#dir-vs-gitdir) directory
 * @param {string} [args.gitdir=path.join(dir, '.git')] - The path to the [git directory](index.html#dir-vs-gitdir)
 * @param {string} args.message - The commit message to use.
 * @param {Object} [args.author] - The details about the commit author.
 * @param {string} [args.author.name=undefined] - Default is `user.name` config.
 * @param {string} [args.author.email=undefined] - Default is `user.email` config.
 * @param {Date} [args.author.date=new Date()] - Set the author timestamp field. Default is the current date.
 * @param {number} [args.author.timestamp=undefined] - Set the author timestamp field. This is an alternative to using `date` using an integer number of seconds since the Unix epoch instead of a JavaScript date object.
 * @param {Object} [args.committer=author] - The details about the commit committer, in the same format as the author parameter. If not specified, the author details are used.
 * @returns {Promise<string>} - The object ID of the newly created commit.
 *
 * @example
 * let repo = {fs, dir: '<@.@>'}
 * let sha = await git.commit({
 *   ...repo,
 *   author: {
 *     name: '<@Mr. Test@>',
 *     email: '<@mrtest@example.com@>'
 *   },
 *   message: '<@Added the a.txt file@>'
 * })
 * console.log(sha)
 */
async function commit({
  dir,
  gitdir = path.join(dir, '.git'),
  fs: _fs,
  message,
  author,
  committer
}) {
  const fs = new FileSystem(_fs);
  // Fill in missing arguments with default values
  if (author === undefined) author = {};
  if (author.name === undefined) {
    author.name = await config({ fs, gitdir, path: 'user.name' });
  }
  if (author.email === undefined) {
    author.email = await config({ fs, gitdir, path: 'user.email' });
  }
  committer = committer || author;
  let authorDateTime = author.date || new Date();
  let committerDateTime = committer.date || authorDateTime;
  let oid;
  await GitIndexManager.acquire({ fs, filepath: `${gitdir}/index` }, async function (index) {
    const inode = flatFileListToDirectoryStructure(index.entries);
    const treeRef = await constructTree({ fs, gitdir, inode });
    let parents;
    try {
      let parent = await GitRefManager.resolve({ fs, 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
    });
    oid = await GitObjectManager.write({
      fs,
      gitdir,
      type: 'commit',
      object: comm.toObject()
    });
    // Update branch pointer
    const branch = await GitRefManager.resolve({
      fs,
      gitdir,
      ref: 'HEAD',
      depth: 2
    });
    await fs.write(path.join(gitdir, branch), oid + '\n');
  });
  return oid;
}

/**
 * List all the tracked files in a repo
 *
 * @param {Object} args - Arguments object
 * @param {FSModule} args.fs - The filesystem holding the git repo
 * @param {string} args.dir - The path to the [working tree](index.html#dir-vs-gitdir) directory
 * @param {string} [args.gitdir=path.join(dir, '.git')] - The path to the [git directory](index.html#dir-vs-gitdir)
 * @returns {Promise<string[]>} - Resolves successfully with an array of file paths.
 *
 * @example
 * let repo = {fs, dir: '<@.@>'}
 * let files = await git.listFiles(repo)
 * console.log(files)
 */
async function listFiles({
  dir,
  gitdir = path.join(dir, '.git'),
  fs: _fs
}) {
  const fs = new FileSystem(_fs);
  let filenames;
  await GitIndexManager.acquire({ fs, filepath: `${gitdir}/index` }, async function (index) {
    filenames = index.entries.map(x => x.path);
  });
  return filenames;
}

/**
 * List branches
 *
 * @param {Object} args - Arguments object
 * @param {FSModule} args.fs - The filesystem holding the git repo
 * @param {string} args.dir - The path to the [working tree](index.html#dir-vs-gitdir) directory
 * @param {string} [args.gitdir=path.join(dir, '.git')] - The path to the [git directory](index.html#dir-vs-gitdir)
 * @param {string} [remote=undefined] - If specified, lists the branches for that remote. Otherwise lists local branches.
 * @returns {Promise<string[]>} - Resolves successfully with an array of branch names.
 *
 * @example
 * let repo = {fs, dir: '<@.@>'}
 * let branches = await git.listBranches(repo)
 * console.log(branches)
 */
async function listBranches({
  dir,
  gitdir = path.join(dir, '.git'),
  fs: _fs,
  remote = undefined
}) {
  const fs = new FileSystem(_fs);
  return GitRefManager.listBranches({ fs, gitdir, remote });
}

/**
 * List all local tags
 *
 * @param {Object} args - Arguments object
 * @param {FSModule} args.fs - The filesystem holding the git repo
 * @param {string} args.dir - The path to the [working tree](index.html#dir-vs-gitdir) directory
 * @param {string} [args.gitdir=path.join(dir, '.git')] - The path to the [git directory](index.html#dir-vs-gitdir)
 * @returns {Promise<string[]>} - Resolves successfully with an array of branch names.
 *
 * @example
 * let repo = {fs, dir: '<@.@>'}
 * let tags = await git.listTags(repo)
 * console.log(tags)
 */
async function listTags({
  dir,
  gitdir = path.join(dir, '.git'),
  fs: _fs
}) {
  const fs = new FileSystem(_fs);
  return GitRefManager.listTags({ fs, gitdir });
}

var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };

async function log({
  dir,
  gitdir = path.join(dir, '.git'),
  fs: _fs,
  ref = 'HEAD',
  depth,
  since // Date
}) {
  const fs = new FileSystem(_fs);
  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({ fs, gitdir, ref });
  let { type, object } = await GitObjectManager.read({ fs, 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({ fs, 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;
}

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

  /**
   *
   * If there were no errors, then there will be no `errors` property.
   * There can be a mix of `ok` messages and `errors` messages.
   *
   * @typedef {Object} PushResponse
   * @property {Array<string>} [ok] - The first item is "unpack" if the overall operation was successful. The remaining items are the names of refs that were updated successfully.
   * @property {Array<string>} [errors] - If the overall operation threw and error, the first item will be "unpack {Overall error message}". The remaining items are individual refs that failed to be updated in the format "{ref name} {error message}".
   */

  /**
   * Push a branch
   *
   * @param {Object} args - Arguments object
   * @param {FSModule} args.fs - The filesystem holding the git repo
   * @param {string} args.dir - The path to the [working tree](index.html#dir-vs-gitdir) directory
   * @param {string} [args.gitdir=path.join(dir, '.git')] - The path to the [git directory](index.html#dir-vs-gitdir)
   * @param {string} [args.ref=undefined] - Which branch to push. By default this is the currently checked out branch of the repository.
   * @param {string} [args.remote='origin'] - If URL is not specified, determines which remote to use.
   * @param {string} [args.url=undefined] - The URL of the remote git server. The default is the value set in the git config for that remote.
   * @param {string} [args.authUsername=undefined] - The username to use with Basic Auth
   * @param {string} [args.authPassword=undefined] - The password to use with Basic Auth
   * @returns {Promise<PushResponse>} - Resolves successfully when push completes with a detailed description of the operation from the server.
   *
   * @example
   * let repo = {fs, dir: '<@.@>'}
   * let pushResponse = await git.push({
   *   ...repo,
   *   remote: '<@origin@>',
   *   ref: '<@master@>',
   *   authUsername: <@process.env.GITHUB_TOKEN@>,
   *   authPassword: <@process.env.GITHUB_TOKEN@>
   * })
   * console.log(pushResponse)
   */
};async function push({
  fs: _fs,
  dir,
  gitdir = path.join(dir, '.git'),
  ref,
  remote = 'origin',
  url,
  authUsername,
  authPassword
}) {
  const fs = new FileSystem(_fs);
  // TODO: Figure out how pushing tags works. (This only works for branches.)
  if (url === undefined) {
    url = await config({ fs, gitdir, path: `remote.${remote}.url` });
  }
  let fullRef = ref.startsWith('refs/') ? ref : `refs/heads/${ref}`;
  let oid = await GitRefManager.resolve({ fs, 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({
    fs,
    gitdir,
    start: [oid],
    finish: httpRemote.refs.values()
  });
  let objects = await listObjects({ fs, gitdir, oids: commits });
  let packstream = new PassThrough();
  let oldoid = httpRemote.refs.get(fullRef) || '0000000000000000000000000000000000000000';
  packstream.write(GitPktLine.encode(`${oldoid} ${oid} ${fullRef}\0 report-status\n`));
  packstream.write(GitPktLine.flush());
  pack({
    fs,
    gitdir,
    oids: [...objects],
    outputStream: packstream
  });
  let response = await httpRemote.push(packstream);
  return response;
}

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

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

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

/** @ignore */
async function listObjects({
  dir,
  gitdir = path.join(dir, '.git'),
  fs: _fs,
  oids
}) {
  const fs = new FileSystem(_fs);
  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({ fs, gitdir, oid });
    if (type === 'commit') {
      let commit = GitCommit.from(object);
      let tree = commit.headers().tree;
      await walk(tree);
    } else if (type === 'tree') {
      let tree = GitTree.from(object);
      for (let entry /*: 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;
}

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

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

/**
 * Remove a file from the git index (aka staging area)
 *
 * Note that this does NOT delete the file in the working directory.
 *
 * @param {Object} args - Arguments object
 * @param {FSModule} args.fs - The filesystem holding the git repo
 * @param {string} args.dir - The path to the [working tree](index.html#dir-vs-gitdir) directory
 * @param {string} [args.gitdir=path.join(dir, '.git')] - The path to the [git directory](index.html#dir-vs-gitdir)
 * @param {string} args.filepath - The path to the file to remove to the index.
 * @returns {Promise<void>} - Resolves successfully once the git index has been updated.
 *
 * @example
 * let repo = {fs, dir: '<@.@>'}
 * await git.remove({...repo, filepath: '<@README.md@>'})
 * console.log('done')
 */
async function remove({
  dir,
  gitdir = path.join(dir, '.git'),
  fs: _fs,
  filepath
}) {
  const fs = new FileSystem(_fs);
  await GitIndexManager.acquire({ fs, filepath: `${gitdir}/index` }, async function (index) {
    index.delete({ filepath });
  });
  // TODO: return oid?
}

/**
 * Verify a signed commit
 *
 * It is up to you to figure out what the commit's public key *should* be.
 * I would use the "author" or "committer" name and email, and look up
 * that person's public key from a trusted source such as the Github API.
 *
 * The function returns false if any of the signatures on a signed git commit are invalid.
 * Otherwise, it returns an array of the key ids that were used to sign it.
 *
 * The {@link publicKeys} argument is a single string in ASCII armor format. However, it is plural "keys" because
 * you can technically have multiple public keys in a single ASCII armor string. While I haven't tested it, it
 * should support verifying a single commit signed with multiple keys. Hence why the returned result is an array of key ids.
 *
 * @param {Object} args - Arguments object
 * @param {FSModule} args.fs - The filesystem holding the git repo
 * @param {string} args.dir - The path to the [working tree](index.html#dir-vs-gitdir) directory
 * @param {string} [args.gitdir=path.join(dir, '.git')] - The path to the [git directory](index.html#dir-vs-gitdir)
 * @param {string} args.ref - A reference to the commit to verify
 * @param {string} args.publicKeys - A PGP public key in ASCII armor format.
 * @returns {Promise<false|Array<string>>} - The key ids used to sign the commit, in hex format.
 *
 * @example
 * let repo = {fs, dir: '.'}
 * let keyids = await git.verify({
 *   ...repo,
 *   ref: '<@HEAD@>',
 *   publicKeys: `<<@
 * -----BEGIN PGP PUBLIC KEY BLOCK-----
 * ...
 * @>>`
 * })
 * console.log(keyids)
 */
async function verify({
  dir,
  gitdir = path.join(dir, '.git'),
  fs: _fs,
  ref,
  publicKeys,
  openpgp
}) {
  const fs = new FileSystem(_fs);
  const oid = await GitRefManager.resolve({ fs, gitdir, ref });
  const { type, object } = await GitObjectManager.read({ fs, gitdir, oid });
  if (type !== 'commit') {
    throw new Error(`'ref' is not pointing to a 'commit' object but a '${type}' object`);
  }
  let commit = SignedGitCommit.from(object);
  let keys = await commit.listSigningKeys(openpgp);
  let validity = await commit.verify(openpgp, publicKeys);
  if (!validity) return false;
  return keys;
}

/*::
import type { Stats } from 'fs'
import type { CacheEntry } from '../models/GitIndex'
*/

function cacheIsStale({ entry, stats /*: {
                                     entry: CacheEntry,
                                     stats: Stats
                                     } */
}) {
  // Comparison based on the description in Paragraph 4 of
  // https://www.kernel.org/pub/software/scm/git/docs/technical/racy-git.txt
  return entry.mode !== stats.mode || entry.mtime.valueOf() !== stats.mtime.valueOf() || entry.ctime.valueOf() !== stats.ctime.valueOf() || entry.uid !== stats.uid || entry.gid !== stats.gid || entry.ino !== stats.ino >> 0 || entry.size !== stats.size;
}

async function getOidAtPath({ fs, gitdir, tree, path: path$$1 }) {
  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({
        fs,
        gitdir,
        oid: entry.oid
      });
      if (type === 'tree') {
        let tree = GitTree.from(object);
        return getOidAtPath({ fs, gitdir, tree, path: path$$1 });
      }
      if (type === 'blob') {
        throw new Error(`Blob found where tree expected.`);
      }
    }
  }
  return null;
}

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

/**
 * Tell whether a file has been changed
 *
 * The possible resolve values are:
 *
 * - `"ignored"` file ignored by a .gitignore rule
 * - `"unmodified"` file unchanged from HEAD commit
 * - `"*modified"` file has modifications, not yet staged
 * - `"*deleted"` file has been removed, but the removal is not yet staged
 * - `"*added"` file is untracked, not yet staged
 * - `"absent"` file not present in HEAD commit, staging area, or working dir
 * - `"modified"` file has modifications, staged
 * - `"deleted"` file has been removed, staged
 * - `"added"` previously untracked file, staged
 * - `"*unmodified"` working dir and HEAD commit match, but index differs
 * - `"*absent"` file not present in working dir or HEAD commit, but present in the index
 *
 * @param {Object} args - Arguments object
 * @param {FSModule} args.fs - The filesystem holding the git repo
 * @param {string} args.dir - The path to the [working tree](index.html#dir-vs-gitdir) directory
 * @param {string} [args.gitdir=path.join(dir, '.git')] - The path to the [git directory](index.html#dir-vs-gitdir)
 * @param {string} args.filepath - The path to the file to query.
 * @returns {Promise<string>} - Resolves successfully with the file's git status.
 *
 * @example
 * let repo = {fs, dir: '<@.@>'}
 * let status = await git.status({...repo, filepath: '<@README.md@>'})
 * console.log(status)
 */
async function status({
  dir,
  gitdir = path.join(dir, '.git'),
  fs: _fs,
  filepath
}) {
  const fs = new FileSystem(_fs);
  let ignored = await GitIgnoreManager.isIgnored({
    gitdir,
    dir,
    filepath,
    fs
  });
  if (ignored) {
    return 'ignored';
  }
  let headTree = await getHeadTree({ fs, gitdir });
  let treeOid = await getOidAtPath({
    fs,
    gitdir,
    tree: headTree,
    path: filepath
  });
  let indexEntry = null;
  // Acquire a lock on the index
  await GitIndexManager.acquire({ fs, filepath: `${gitdir}/index` }, async function (index) {
    for (let entry of index) {
      if (entry.path === filepath) {
        indexEntry = entry;
        break;
      }
    }
  });
  let stats = null;
  try {
    stats = await fs._lstat(path.join(dir, filepath));
  } catch (err) {
    if (err.code !== 'ENOENT') {
      throw err;
    }
  }

  let H = treeOid !== null; // head
  let I = indexEntry !== null; // index
  let W = stats !== null; // working dir

  const getWorkdirOid = async () => {
    if (I && !cacheIsStale({ entry: indexEntry, stats })) {
      return indexEntry.oid;
    } else {
      let object = await fs.read(path.join(dir, filepath));
      let workdirOid = await GitObjectManager.hash({
        gitdir,
        type: 'blob',
        object
      });
      return workdirOid;
    }
  };

  if (!H && !W && !I) return 'absent'; // ---
  if (!H && !W && I) return '*absent'; // -A-
  if (!H && W && !I) return '*added'; // --A
  if (!H && W && I) {
    let workdirOid = await getWorkdirOid();
    return workdirOid === indexEntry.oid ? 'added' : '*added'; // -AA : -AB
  }
  if (H && !W && !I) return 'deleted'; // A--
  if (H && !W && I) {
    return treeOid === indexEntry.oid ? '*deleted' : '*deleted'; // AA- : AB-
  }
  if (H && W && !I) {
    let workdirOid = await getWorkdirOid();
    return workdirOid === treeOid ? '*undeleted' : '*undeletemodified'; // A-A : A-B
  }
  if (H && W && I) {
    let workdirOid = await getWorkdirOid();
    if (workdirOid === treeOid) {
      return workdirOid === indexEntry.oid ? 'unmodified' : '*unmodified'; // AAA : ABA
    } else {
      return workdirOid === indexEntry.oid ? 'modified' : '*modified'; // ABB : AAB
    }
  }
  /*
  ---
  -A-
  --A
  -AA
  -AB
  A--
  AA-
  AB-
  A-A
  A-B
  AAA
  ABA
  ABB
  AAB
  */
}

/**
 * Find the root git directory
 * @param {Object} args - Arguments object
 * @param {FSModule} args.fs - The filesystem holding the git repo
 * @param {string} args.filepath - The file directory to start searching in.
 * @returns {Promise<string>} - a directory name
 * @throws {Error} - Error('Unable to find git root')
 *
 * Starting at `filepath`, will walk upwards until it finds a directory that contains a directory called '.git'.
 *
 * @example
 * let gitroot = await git.findRoot({
 *   fs,
 *   filepath: '<@/path/to/some/gitrepo/path/to/some/file.txt@>'
 * })
 * console.log(gitroot) // '/path/to/some/gitrepo'
 */
async function findRoot({ fs: _fs, filepath }) {
  const fs = new FileSystem(_fs);
  return _findRoot(fs, filepath);
}

async function _findRoot(fs, filepath) {
  if (await fs.exists(path.join(filepath, '.git'))) {
    return filepath;
  } else {
    let parent = path.dirname(filepath);
    if (parent === filepath) throw new Error('Unable to find git root');
    return _findRoot(fs, parent);
  }
}

var version = "0.0.0-development";

/**
 * Return the version number of 'isomorphic-git'
 *
 * I don't know why you might need this. I added it just so I could check that I was getting
 * the correct version of the library and not a cached version.
 *
 * TODO: Semantic-release broke this, now it always says '0.0.0-development'. Need to add a
 * prepublishOnly script to find & replace that with the actual version number.
 *
 * @returns {string} version - the version string taken from package.json at publication time
 * @example
 * console.log(git.version())
 */
function version$1() {
  return version;
}

/**
 * Create the .idx file for a given .pack file
 *
 * @param {Object} args - Arguments object
 * @param {FSModule} args.fs - The filesystem holding the git repo
 * @param {string} args.dir - The path to the [working tree](index.html#dir-vs-gitdir) directory
 * @param {string} [args.gitdir=path.join(dir, '.git')] - The path to the [git directory](index.html#dir-vs-gitdir)
 * @param {string} args.filepath - The path to the .pack file to index.
 * @returns {Promise<void>} - Resolves successfully once the .idx file been written.
 *
 * @example
 * let repo = {fs, dir: '<@.@>'}
 * await git.indexPack({...repo, filepath: '<@pack-9cbd243a1caa4cb4bef976062434a958d82721a9.pack@>'})
 */
async function indexPack({
  dir,
  gitdir = path.join(dir, '.git'),
  fs: _fs,
  filepath
}) {
  const fs = new FileSystem(_fs);
  const pack = await fs.read(path.join(dir, filepath));
  const idx = await GitPackIndex.fromPack({ pack });
  await fs.write(filepath.replace(/\.pack$/, '.idx'), idx.toBuffer());
}

/**
 * Create a signed commit
 *
 * OpenPGP.js is a huge library and if you don't need to create or verify signed commits
 * you shouldn't be forced to include that weighty feature in your bundle. That's why this
 * is its own function.
 *
 * It creates a signed version of whatever commit HEAD currently points to, and then updates the current branch,
 * leaving the original commit dangling.
 *
 * The {@link privateKeys} argument is a single string in ASCII armor format. However, it is plural "keys" because
 * you can technically have multiple private keys in a single ASCII armor string. The openpgp.sign() function accepts
 * multiple keys, so while I haven't tested it, it should support signing a single commit with multiple keys.
 *
 * @param {Object} args - Arguments object
 * @param {FSModule} args.fs - The filesystem holding the git repo
 * @param {string} args.dir - The path to the [working tree](index.html#dir-vs-gitdir) directory
 * @param {string} [args.gitdir=path.join(dir, '.git')] - The path to the [git directory](index.html#dir-vs-gitdir)
 * @param {string} args.privateKeys - A PGP private key in ASCII armor format.
 * @returns {Promise<string>} - The object ID of the newly created commit.
 *
 * @example
 * let repo = {fs, dir: '<@.@>'}
 * let sha = await git.sign({
 *   ...repo,
 *   privateKeys: `<<@
 * -----BEGIN PGP PRIVATE KEY BLOCK-----
 * ...
 * @>>`
 * })
 * console.log(sha)
 */
async function sign({
  dir,
  gitdir = path.join(dir, '.git'),
  fs: _fs,
  privateKeys,
  openpgp
}) {
  const fs = new FileSystem(_fs);
  const oid = await GitRefManager.resolve({ fs, gitdir, ref: 'HEAD' });
  const { type, object } = await GitObjectManager.read({ fs, gitdir, oid });
  if (type !== 'commit') {
    throw new Error(`HEAD is not pointing to a 'commit' object but a '${type}' object`);
  }
  let commit = SignedGitCommit.from(object);
  commit = await commit.sign(openpgp, privateKeys);
  const newOid = await GitObjectManager.write({
    fs,
    gitdir,
    type: 'commit',
    object: commit.toObject()
  });
  // Update branch pointer
  // TODO: Use an updateBranch function instead of this.
  const branch = await GitRefManager.resolve({
    fs,
    gitdir,
    ref: 'HEAD',
    depth: 2
  });
  await fs.write(path.join(gitdir, branch), newOid + '\n');
}

/**
 * Get the value of a symbolic ref or resolve a ref to its object id.
 * @param {Object} args - Arguments object
 * @param {FSModule} args.fs - The filesystem holding the git repo
 * @param {string} args.dir - The path to the [working tree](index.html#dir-vs-gitdir) directory
 * @param {string} [args.gitdir=path.join(dir, '.git')] - The path to the [git directory](index.html#dir-vs-gitdir)
 * @param {string} args.ref - Which ref to resolve.
 * @param {number} [args.depth=undefined] - How many symbolic references to follow before returning.
 * @returns {Promise<string>} - Resolves successfully with the SHA, or the value of another symbolic ref.
 *
 * @example
 * let repo = {fs, dir: '<@.@>'}
 * let currentCommit = await git.resolveRef({...repo, ref: '<@HEAD@>'})
 * console.log(currentCommit)
 * let currentBranch = await git.resolveRef({...repo, ref: '<@HEAD@>', depth: 1})
 * console.log(currentBranch)
 */
async function resolveRef({
  dir,
  gitdir = path.join(dir, '.git'),
  fs: _fs,
  ref,
  depth
}) {
  const fs = new FileSystem(_fs);
  return GitRefManager.resolve({
    fs,
    gitdir,
    ref,
    depth
  });
}

async function readObject({
  dir,
  gitdir = path.join(dir, '.git'),
  fs: _fs,
  oid,
  format = 'parsed'
}) {
  const fs = new FileSystem(_fs);
  // GitObjectManager does not know how to parse content, so we tweak that parameter before passing it.
  const _format = format === 'parsed' ? 'content' : format;
  let result = await GitObjectManager.read({ fs, gitdir, oid, format: _format });
  if (format === 'parsed') {
    switch (result.type) {
      case 'commit':
        result.object = GitCommit.from(result.object).parse();
        break;
      case 'tree':
        result.object = { entries: GitTree.from(result.object).entries() };
        break;
      case 'blob':
        break;
      case 'tag':
        throw new Error('TODO: Parsing annotated tag objects still needs to be implemented!!');
      default:
        throw new Error(`Unrecognized git object type: '${result.type}'`);
    }
    result.format = 'parsed';
  }
  return result;
}

async function getRemoteInfo({ url }) {
  const remote = new GitRemoteHTTP(url);
  await remote.preparePull();
  const result = {};
  // Note: remote.capabilities, remote.refs, and remote.symrefs are Set and Map objects,
  // but one of the objectives of the public API is to always return JSON-compatible objects
  // so we must JSONify them.
  result.capabilities = [...remote.capabilities];
  // Convert the flat list into an object tree, because I figure 99% of the time
  // that will be easier to use.
  for (const [ref, oid] of remote.refs) {
    let parts = ref.split('/');
    let last = parts.pop();
    let o = result;
    for (let part of parts) {
      o[part] = o[part] || {};
      o = o[part];
    }
    o[last] = oid;
  }
  // Merge symrefs on top of refs to more closely match actual git repo layouts
  for (const [symref, ref] of remote.symrefs) {
    let parts = symref.split('/');
    let last = parts.pop();
    let o = result;
    for (let part of parts) {
      o[part] = o[part] || {};
      o = o[part];
    }
    o[last] = ref;
  }
  return result;
}

// import diff3 from 'node-diff3'

function compareAge(a, b) {
  return a.committer.timestamp - b.committer.timestamp;
}

async function findMergeBase({ gitdir, fs, refs }) {
  // Where is async flatMap when you need it?
  let commits = [];
  for (const ref of refs) {
    let list = await log({ gitdir, fs, ref, depth: 1 });
    commits.push(list[0]);
  }
  // Are they actually the same commit?
  if (commits.every(commit => commit.oid === commits[0].oid)) {
    return commits[0].oid;
  }
  // Is the oldest commit an ancestor of the others?
  let sorted = commits.sort(compareAge);
  let candidate = sorted[0];
  let since = candidate.timestamp - 1;
  for (const ref of refs) {
    let list = await log({ gitdir, fs, ref, since });
    if (!list.find(commit => commit.oid === candidate.oid)) {
      candidate = null;
      break;
    }
  }
  if (candidate) return candidate.oid;
  // Is...
  throw new Error('Non-trivial merge not implemented at this time');
}

async function merge({
  dir,
  gitdir = path.join(dir, '.git'),
  fs: _fs,
  ours,
  theirs,
  fastForwardOnly
}) {
  const fs = new FileSystem(_fs);
  let ourOid = await GitRefManager.resolve({
    fs,
    gitdir,
    ref: ours
  });
  let theirOid = await GitRefManager.resolve({
    fs,
    gitdir,
    ref: theirs
  });
  // find most recent common ancestor of ref a and ref b
  let baseOid = await findMergeBase({ gitdir, fs, refs: [ourOid, theirOid] });
  // handle fast-forward case
  if (baseOid === theirOid) {
    console.log(`'${theirs}' is already merged into '${ours}'`);
    return {
      oid: ourOid,
      alreadyMerged: true
    };
  }
  if (baseOid === ourOid) {
    console.log(`Performing a fast-forward merge...`);
    await GitRefManager.writeRef({ fs, gitdir, ref: ours, value: theirOid });
    return {
      oid: theirOid,
      fastForward: true
    };
  } else {
    // not a simple fast-forward
    if (fastForwardOnly) {
      throw new Error('A simple fast-forward merge was not possible.');
    }
    throw new Error('Non-fast-forward merges are not supported yet.');
  }
}

// import diff3 from 'node-diff3'

async function pull({
  dir,
  gitdir = path.join(dir, '.git'),
  fs: _fs,
  ref,
  fastForwardOnly = false,
  emitter,
  authUsername,
  authPassword,
  singleBranch
}) {
  const fs = new FileSystem(_fs);
  // If ref is undefined, use 'HEAD'
  if (!ref) {
    ref = await GitRefManager.resolve({
      fs,
      gitdir,
      ref: 'HEAD',
      depth: 1
    });
  }
  console.log(`Using ref=${ref}`);
  // Fetch from the correct remote.
  let remote = await config({
    gitdir,
    fs,
    path: `branch.${ref}.remote`
  });
  let { fetchHead } = await fetch({
    dir,
    gitdir,
    fs,
    emitter,
    ref,
    remote,
    authUsername,
    authPassword,
    singleBranch
  });
  // Merge the remote tracking branch into the local one.
  await merge({
    gitdir,
    fs,
    ours: ref,
    theirs: fetchHead,
    fastForwardOnly
  });
  await checkout({
    dir,
    gitdir,
    fs,
    ref
  });
}

export { add, clone, checkout, commit, fetch, init, listFiles, listBranches, listTags, log, push, remove, config, verify, status, findRoot, version$1 as version, indexPack, sign, resolveRef, readObject, getRemoteInfo, merge, pull };
