async def export_tree(self,
                          tree,
                          dest,
                          previous_tree=None,
                          *,
                          force=False,
                          previous_index_file=None):
        '''This method is the core of `peru sync`. If the contents of "dest"
        match "previous_tree", then export_tree() updates them to match "tree".
        If not, it raises an error and doesn't touch any files.

        Because it's important for the no-op `peru sync` to be fast, we make an
        extra optimization for this case. The caller passes in the path to the
        index file used during the last sync, which should already reflect
        "previous_tree". That allows us to skip the read-tree and update-index
        calls, so all we have to do is a single diff-files operation to check
        for cleanliness.

        It's difficult to predict all the different states the index file might
        end up in under different error conditions, not only now but also in
        past and future git versions. For safety and simplicity, if any
        operation returns an error code, we delete the supplied index file.
        Right now this includes expected errors, like "sync would overwrite
        existing files," and unexpected errors, like "index is on fire."'''

        tree = tree or (await self.get_empty_tree())
        previous_tree = previous_tree or (await self.get_empty_tree())

        makedirs(dest)

        with contextlib.ExitStack() as stack:

            # If the caller gave us an index file, create a git session around
            # it. Otherwise, create a clean one. Note that because we delete
            # the index file whenever there are errors, we also allow the
            # caller to pass in a path to a nonexistent file. In that case we
            # have to pay the cost to recreate it.
            did_refresh = False
            if previous_index_file:
                session = GitSession(self.trees_path, previous_index_file,
                                     dest)
                stack.enter_context(delete_if_error(previous_index_file))
                if not os.path.exists(previous_index_file):
                    did_refresh = True
                    await session.read_tree_and_stats_into_index(previous_tree)
            else:
                session = stack.enter_context(self.clean_git_session(dest))
                did_refresh = True
                await session.read_tree_and_stats_into_index(previous_tree)

            # The fast path. If the previous tree is the same as the current
            # one, and no files have changed at all, short-circuit.
            if previous_tree == tree:
                if (await session.working_copy_matches_index()):
                    return

            # Everything below is the slow path. Some files have changed, or
            # the tree has changed, or both. If we didn't refresh the index
            # file above, we must do so now.
            if not did_refresh:
                await session.read_tree_and_stats_into_index(previous_tree)
            modified = await session.get_modified_files_skipping_deletes()
            if modified and not force:
                raise DirtyWorkingCopyError(
                    'Imported files have been modified ' +
                    '(use --force to overwrite):\n\n' +
                    _format_file_lines(modified))

            # Do all the file updates and deletions needed to produce `tree`.
            try:
                await session.read_tree_updating_working_copy(tree, force)
            except GitError:
                # Give a more informative error if we failed because files that
                # are new in `tree` already existed in the working copy.
                new_files = await session.get_new_files_in_tree(
                    previous_tree, tree)
                existing_new_files = [
                    f for f in new_files
                    if f and os.path.exists(os.path.join(dest, f))
                ]
                existing_new_files.sort()
                if existing_new_files:
                    raise DirtyWorkingCopyError(
                        'Imports would overwrite preexisting files '
                        '(use --force to write anyway):\n\n' +
                        _format_file_lines(existing_new_files))
                else:
                    # We must've failed for some other reason. Let the error
                    # keep going.
                    raise

            # Recreate any missing files.
            await session.checkout_files_from_index()
This method is the core of `peru sync`. If the contents of "dest"
        match "previous_tree", then export_tree() updates them to match "tree".
        If not, it raises an error and doesn't touch any files.

        Because it's important for the no-op `peru sync` to be fast, we make an
        extra optimization for this case. The caller passes in the path to the
        index file used during the last sync, which should already reflect
        "previous_tree". That allows us to skip the read-tree and update-index
        calls, so all we have to do is a single diff-files operation to check
        for cleanliness.

        It's difficult to predict all the different states the index file might
        end up in under different error conditions, not only now but also in
        past and future git versions. For safety and simplicity, if any
        operation returns an error code, we delete the supplied index file.
        Right now this includes expected errors, like "sync would overwrite
        existing files," and unexpected errors, like "index is on fire."