async function mergeTree({
  fs,
  cache,
  dir,
  gitdir = join(dir, '.git'),
  index,
  ourOid,
  baseOid,
  theirOid,
  ourName = 'ours',
  baseName = 'base',
  theirName = 'theirs',
  dryRun = false,
  abortOnConflict = true,
  mergeDriver,
}) {
  const ourTree = TREE({ ref: ourOid })
  const baseTree = TREE({ ref: baseOid })
  const theirTree = TREE({ ref: theirOid })

  const unmergedFiles = []
  const bothModified = []
  const deleteByUs = []
  const deleteByTheirs = []

  const results = await _walk({
    fs,
    cache,
    dir,
    gitdir,
    trees: [ourTree, baseTree, theirTree],
    map: async function(filepath, [ours, base, theirs]) {
      const path = basename(filepath)
      // What we did, what they did
      const ourChange = await modified(ours, base)
      const theirChange = await modified(theirs, base)
      switch (`${ourChange}-${theirChange}`) {
        case 'false-false': {
          return {
            mode: await base.mode(),
            path,
            oid: await base.oid(),
            type: await base.type(),
          }
        }
        case 'false-true': {
          // if directory is deleted in theirs but not in ours we return our directory
          if (!theirs && (await ours.type()) === 'tree') {
            return {
              mode: await ours.mode(),
              path,
              oid: await ours.oid(),
              type: await ours.type(),
            }
          }

          return theirs
            ? {
                mode: await theirs.mode(),
                path,
                oid: await theirs.oid(),
                type: await theirs.type(),
              }
            : undefined
        }
        case 'true-false': {
          // if directory is deleted in ours but not in theirs we return their directory
          if (!ours && (await theirs.type()) === 'tree') {
            return {
              mode: await theirs.mode(),
              path,
              oid: await theirs.oid(),
              type: await theirs.type(),
            }
          }

          return ours
            ? {
                mode: await ours.mode(),
                path,
                oid: await ours.oid(),
                type: await ours.type(),
              }
            : undefined
        }
        case 'true-true': {
          // Modifications
          if (
            ours &&
            base &&
            theirs &&
            (await ours.type()) === 'blob' &&
            (await base.type()) === 'blob' &&
            (await theirs.type()) === 'blob'
          ) {
            return mergeBlobs({
              fs,
              gitdir,
              path,
              ours,
              base,
              theirs,
              ourName,
              baseName,
              theirName,
              mergeDriver,
            }).then(async r => {
              if (!r.cleanMerge) {
                unmergedFiles.push(filepath)
                bothModified.push(filepath)
                if (!abortOnConflict) {
                  const baseOid = await base.oid()
                  const ourOid = await ours.oid()
                  const theirOid = await theirs.oid()

                  index.delete({ filepath })

                  index.insert({ filepath, oid: baseOid, stage: 1 })
                  index.insert({ filepath, oid: ourOid, stage: 2 })
                  index.insert({ filepath, oid: theirOid, stage: 3 })
                }
              } else if (!abortOnConflict) {
                index.insert({ filepath, oid: r.mergeResult.oid, stage: 0 })
              }
              return r.mergeResult
            })
          }

          // deleted by us
          if (
            base &&
            !ours &&
            theirs &&
            (await base.type()) === 'blob' &&
            (await theirs.type()) === 'blob'
          ) {
            unmergedFiles.push(filepath)
            deleteByUs.push(filepath)
            if (!abortOnConflict) {
              const baseOid = await base.oid()
              const theirOid = await theirs.oid()

              index.delete({ filepath })

              index.insert({ filepath, oid: baseOid, stage: 1 })
              index.insert({ filepath, oid: theirOid, stage: 3 })
            }

            return {
              mode: await theirs.mode(),
              oid: await theirs.oid(),
              type: 'blob',
              path,
            }
          }

          // deleted by theirs
          if (
            base &&
            ours &&
            !theirs &&
            (await base.type()) === 'blob' &&
            (await ours.type()) === 'blob'
          ) {
            unmergedFiles.push(filepath)
            deleteByTheirs.push(filepath)
            if (!abortOnConflict) {
              const baseOid = await base.oid()
              const ourOid = await ours.oid()

              index.delete({ filepath })

              index.insert({ filepath, oid: baseOid, stage: 1 })
              index.insert({ filepath, oid: ourOid, stage: 2 })
            }

            return {
              mode: await ours.mode(),
              oid: await ours.oid(),
              type: 'blob',
              path,
            }
          }

          // deleted by both
          if (base && !ours && !theirs && (await base.type()) === 'blob') {
            return undefined
          }

          // all other types of conflicts fail
          // TODO: Merge conflicts involving additions
          throw new MergeNotSupportedError()
        }
      }
    },
    /**
     * @param {TreeEntry} [parent]
     * @param {Array<TreeEntry>} children
     */
    reduce:
      unmergedFiles.length !== 0 && (!dir || abortOnConflict)
        ? undefined
        : async (parent, children) => {
            const entries = children.filter(Boolean) // remove undefineds

            // if the parent was deleted, the children have to go
            if (!parent) return

            // automatically delete directories if they have been emptied
            if (parent && parent.type === 'tree' && entries.length === 0) return

            if (entries.length > 0) {
              const tree = new GitTree(entries)
              const object = tree.toObject()
              const oid = await writeObject({
                fs,
                gitdir,
                type: 'tree',
                object,
                dryRun,
              })
              parent.oid = oid
            }
            return parent
          },
  })

  if (unmergedFiles.length !== 0) {
    if (dir && !abortOnConflict) {
      await _walk({
        fs,
        cache,
        dir,
        gitdir,
        trees: [TREE({ ref: results.oid })],
        map: async function(filepath, [entry]) {
          const path = `${dir}/${filepath}`
          if ((await entry.type()) === 'blob') {
            const mode = await entry.mode()
            const content = new TextDecoder().decode(await entry.content())
            await fs.write(path, content, { mode })
          }
          return true
        },
      })
    }
    return new MergeConflictError(
      unmergedFiles,
      bothModified,
      deleteByUs,
      deleteByTheirs
    )
  }

  return results.oid
}
Create a merged tree

@param {Object} args
@param {import('../models/FileSystem.js').FileSystem} args.fs
@param {object} args.cache
@param {string} [args.dir] - The [working tree](dir-vs-gitdir.md) directory path
@param {string} [args.gitdir=join(dir,'.git')] - [required] The [git directory](dir-vs-gitdir.md) path
@param {string} args.ourOid - The SHA-1 object id of our tree
@param {string} args.baseOid - The SHA-1 object id of the base tree
@param {string} args.theirOid - The SHA-1 object id of their tree
@param {string} [args.ourName='ours'] - The name to use in conflicted files for our hunks
@param {string} [args.baseName='base'] - The name to use in conflicted files (in diff3 format) for the base hunks
@param {string} [args.theirName='theirs'] - The name to use in conflicted files for their hunks
@param {boolean} [args.dryRun=false]
@param {boolean} [args.abortOnConflict=false]
@param {MergeDriverCallback} [args.mergeDriver]

@returns {Promise<string>} - The SHA-1 object id of the merged tree