import path from 'path';
import pify from 'pify';
import { GitConfigManager, GitIgnoreManager, GitIndexManager, GitObjectManager, GitRefManager, GitRemoteHTTP, GitShallowManager } from './managers.js';
import { flatFileListToDirectoryStructure, fs, mkdirs, pkg, read, readDirAsFlatFileList, rm, write } from './utils.js';
import stream from 'stream';
import thru from 'thru';
import { Buffer } from 'buffer';
import listpack from 'git-list-pack';
import peek from 'buffer-peek-stream';
import applyDelta from 'git-apply-delta';
import marky from 'marky';
import { GitCommit, GitPktLine, GitTree } from './models.js';
import pad from 'pad';
import pako from 'pako';
import crypto from 'crypto';
import { HKP } from 'openpgp/dist/openpgp.min.js';

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.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/${pkg.name}@${pkg.version}${relative ? ' deepen-relative' : ''}`;
  let packstream = new stream.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,
  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
    });
    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$$1(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$$1(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$$1(pad(2, byte.toString(16), '0'), 'hex');
      length = length >>> 7;
    }
    // Lastly, we can compress and write the object.
    write$$1(Buffer.from(pako.deflate(object)));
  }

  write$$1('PACK');
  write$$1('00000002', 'hex');
  // Write a 4 byte (32-bit) int
  write$$1(pad(8, oids.length.toString(16), '0'), 'hex');
  for (let oid of oids) {
    let { type, object } = await GitObjectManager.read({ gitdir, oid });
    writeObject({ write: write$$1, 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.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;
}

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

// @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 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;
}

var version$1 = "0.0.23";

function version() {
  return version$1;
}

export { add, clone, checkout, commit, fetch, init, list, listCommits, listObjects, log, pack, push, remove, config, unpack, verify, status, findRoot, listBranches, version };
