/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::collections::HashMap;
use std::collections::HashSet;
use std::fmt::Debug;
use std::hash::Hash;

use anyhow::Error;
use blobstore::StoreLoadable;
use cloned::cloned;
use context::CoreContext;
use futures::future;
use futures::future::try_join_all;
use futures::try_join;
use futures::FutureExt;
use futures::Stream;
use futures::TryFutureExt;
use futures::TryStreamExt;
use maplit::hashmap;
use maplit::hashset;
use mononoke_types::FileType;
use mononoke_types::MPath;
use tokio::task;

use crate::Entry;
use crate::Manifest;

pub(crate) type BonsaiEntry<ManifestId, FileId> = Entry<ManifestId, (FileType, FileId)>;

#[derive(Clone, Eq, Debug, Hash, PartialEq, PartialOrd, Ord)]
pub enum BonsaiDiffFileChange<FileId> {
    /// This file was changed (was added or modified) in this changeset.
    Changed(MPath, FileType, FileId),

    /// The file was marked changed, but one of the parent file ID was reused. This can happen in
    /// these situations:
    ///
    /// 1. The file type was changed without a corresponding change in file contents.
    /// 2. There's a merge and one of the parent nodes was picked as the resolution.
    ///
    /// This is separate from `Changed` because in these instances, if copy information is part of
    /// the node it wouldn't be recorded.
    ChangedReusedId(MPath, FileType, FileId),

    /// This file was deleted in this changeset.
    Deleted(MPath),
}

impl<FileId> BonsaiDiffFileChange<FileId> {
    pub fn path(&self) -> &MPath {
        match self {
            Self::Changed(path, ..) | Self::ChangedReusedId(path, ..) | Self::Deleted(path) => path,
        }
    }
    pub fn file_id(&self) -> Option<&FileId> {
        match self {
            Self::Changed(_, _, fid) | Self::ChangedReusedId(_, _, fid) => Some(fid),
            Self::Deleted(_) => None,
        }
    }
    pub fn file_type(&self) -> Option<&FileType> {
        match self {
            Self::Changed(_, ft, _) | Self::ChangedReusedId(_, ft, _) => Some(ft),
            Self::Deleted(_) => None,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct CompositeEntry<ManifestId, FileId>
where
    FileId: Hash + Eq,
    ManifestId: Hash + Eq,
{
    manifests: HashSet<ManifestId>,
    files: HashSet<(FileType, FileId)>,
}

impl<ManifestId, FileId> CompositeEntry<ManifestId, FileId>
where
    FileId: Hash + Eq,
    ManifestId: Hash + Eq,
{
    fn empty() -> Self {
        Self {
            manifests: hashset! {},
            files: hashset! {},
        }
    }

    fn insert(&mut self, entry: BonsaiEntry<ManifestId, FileId>) {
        match entry {
            Entry::Leaf(new_id) => {
                self.files.insert(new_id);
            }
            Entry::Tree(new_id) => {
                self.manifests.insert(new_id);
            }
        }
    }

    fn into_parts(
        self,
    ) -> (
        Option<HashSet<ManifestId>>,
        Option<HashSet<(FileType, FileId)>>,
    ) {
        let manifests = if self.manifests.is_empty() {
            None
        } else {
            Some(self.manifests)
        };

        let files = if self.files.is_empty() {
            None
        } else {
            Some(self.files)
        };

        (manifests, files)
    }
}

/// WorkEntry describes the work to be performed by the bounded_traversal to produce a Bonsai diff.
/// It maps a path to consider to the contents of the Manifest for which to produce Bonsai changes
/// at this path and the contents of the parents at this path.
type WorkEntry<ManifestId, FileId> = HashMap<
    MPath,
    (
        Option<BonsaiEntry<ManifestId, FileId>>,
        CompositeEntry<ManifestId, FileId>,
    ),
>;

/// Identify further work to be performed for this Bonsai diff.
async fn recurse_trees<ManifestId, FileId, Store>(
    ctx: &CoreContext,
    store: &Store,
    path: Option<&MPath>,
    node: Option<ManifestId>,
    parents: HashSet<ManifestId>,
) -> Result<WorkEntry<ManifestId, FileId>, Error>
where
    FileId: Hash + Eq,
    ManifestId: Hash + Eq + StoreLoadable<Store>,
    <ManifestId as StoreLoadable<Store>>::Value:
        Manifest<TreeId = ManifestId, LeafId = (FileType, FileId)>,
{
    // If there is a single parent, and it's unchanged, then we don't need to recurse.
    if parents.len() == 1 && node.as_ref() == parents.iter().next() {
        return Ok(HashMap::new());
    }

    let node = async {
        let r = match node {
            Some(node) => Some(node.load(ctx, store).await?),
            None => None,
        };
        Ok(r)
    };

    let parents = try_join_all(
        parents
            .into_iter()
            .map(|id| async move { id.load(ctx, store).await }),
    );

    let (node, parents) = try_join!(node, parents)?;

    let mut ret = HashMap::new();

    if let Some(node) = node {
        for (path, entry) in node.list() {
            ret.entry(path).or_insert((None, CompositeEntry::empty())).0 = Some(entry);
        }
    }

    for parent in parents {
        for (path, entry) in parent.list() {
            ret.entry(path)
                .or_insert((None, CompositeEntry::empty()))
                .1
                .insert(entry);
        }
    }

    let ret = ret
        .into_iter()
        .map(|(p, e)| {
            let p = MPath::join_opt_element(path, &p);
            (p, e)
        })
        .collect();

    Ok(ret)
}

fn resolve_file_over_files<FileId>(
    path: MPath,
    node: (FileType, FileId),
    parents: HashSet<(FileType, FileId)>,
) -> Option<BonsaiDiffFileChange<FileId>>
where
    FileId: Hash + Eq,
{
    if parents.len() == 1 && parents.contains(&node) {
        return None;
    }

    Some(resolve_file_over_mixed(path, node, parents))
}

fn resolve_file_over_mixed<FileId>(
    path: MPath,
    node: (FileType, FileId),
    parents: HashSet<(FileType, FileId)>,
) -> BonsaiDiffFileChange<FileId>
where
    FileId: Hash + Eq,
{
    if parents.iter().any(|e| e.1 == node.1) {
        return BonsaiDiffFileChange::ChangedReusedId(path, node.0, node.1);
    }

    BonsaiDiffFileChange::Changed(path, node.0, node.1)
}

async fn bonsai_diff_unfold<ManifestId, FileId, Store>(
    ctx: &CoreContext,
    store: &Store,
    path: MPath,
    node: Option<BonsaiEntry<ManifestId, FileId>>,
    parents: CompositeEntry<ManifestId, FileId>,
) -> Result<
    (
        Option<BonsaiDiffFileChange<FileId>>,
        WorkEntry<ManifestId, FileId>,
    ),
    Error,
>
where
    FileId: Hash + Eq,
    ManifestId: Hash + Eq + StoreLoadable<Store>,
    <ManifestId as StoreLoadable<Store>>::Value:
        Manifest<TreeId = ManifestId, LeafId = (FileType, FileId)>,
{
    let res = match node {
        Some(Entry::Tree(node)) => {
            let (manifests, files) = parents.into_parts();

            // We have a tree here, so we'll need to compare it to the matching manifests in the parent
            // manifests.
            let recurse = recurse_trees(
                ctx,
                store,
                Some(&path),
                Some(node),
                manifests.unwrap_or_default(),
            )
            .await?;

            // If the parent manifests contained a file at the path where we have a tree, then we
            // must indicate that this path is being deleted, since otherwise we'd have an invalid
            // Bonsai changeset that contains files that are children of a file!
            let change = if files.is_some() {
                Some(BonsaiDiffFileChange::Deleted(path))
            } else {
                None
            };

            (change, recurse)
        }
        Some(Entry::Leaf(node)) => {
            // We have a file here. We won't need to recurse into the parents: the presence of this
            // file implicitly deletes all descendent files (if any exist).
            let recurse = hashmap! {};

            let change = match parents.into_parts() {
                // At least one of our parents has a manifest here. We must emit a file to delete
                // it.
                (Some(_trees), Some(files)) => Some(resolve_file_over_mixed(path, node, files)),
                // Our parents have only files at this path. We might not need to emit anything if
                // they have the same files.
                (None, Some(files)) => resolve_file_over_files(path, node, files),
                // We don't have files in the parents. Regardless of whether we have manifests, we'll
                // need to emit this file, so let's do so.
                (_, None) => Some(BonsaiDiffFileChange::Changed(path, node.0, node.1)),
            };

            (change, recurse)
        }
        None => {
            // We don't have anything at this path, but our parents do: we need to recursively
            // delete everything in this tree.
            let (manifests, files) = parents.into_parts();

            let recurse =
                recurse_trees(ctx, store, Some(&path), None, manifests.unwrap_or_default()).await?;

            let change = if files.is_some() {
                Some(BonsaiDiffFileChange::Deleted(path))
            } else {
                None
            };

            (change, recurse)
        }
    };

    Ok(res)
}

pub fn bonsai_diff<ManifestId, FileId, Store>(
    ctx: CoreContext,
    store: Store,
    node: ManifestId,
    parents: HashSet<ManifestId>,
) -> impl Stream<Item = Result<BonsaiDiffFileChange<FileId>, Error>>
where
    FileId: Hash + Eq + Send + Sync + 'static,
    ManifestId: Hash + Eq + StoreLoadable<Store> + Send + Sync + 'static,
    Store: Send + Sync + Clone + 'static,
    <ManifestId as StoreLoadable<Store>>::Value:
        Manifest<TreeId = ManifestId, LeafId = (FileType, FileId)> + Send + Sync,
{
    // NOTE: `async move` blocks are used below to own CoreContext and Store for the (static)
    // lifetime of the stream we're returning here (recurse_trees and bonsai_diff_unfold don't
    // require those).
    {
        cloned!(ctx, store);
        async move { recurse_trees(&ctx, &store, None, Some(node), parents).await }
    }
    .map_ok(|seed| {
        bounded_traversal::bounded_traversal_stream(256, seed, move |(path, (node, parents))| {
            cloned!(ctx, store);
            async move {
                task::spawn(
                    async move { bonsai_diff_unfold(&ctx, &store, path, node, parents).await },
                )
                .await?
            }
            .boxed()
        })
    })
    .try_flatten_stream()
    .try_filter_map(future::ok)
}

#[cfg(test)]
mod test {
    use borrowed::borrowed;
    use fbinit::FacebookInit;

    use super::*;
    use crate::tests::ctx;
    use crate::tests::dir;
    use crate::tests::element;
    use crate::tests::file;
    use crate::tests::path;
    use crate::tests::ManifestStore;
    use crate::tests::TestFileId;
    use crate::tests::TestManifestIdStr;
    use crate::tests::TestManifestStr;

    impl<ManifestId, FileId> CompositeEntry<ManifestId, FileId>
    where
        FileId: Hash + Eq,
        ManifestId: Hash + Eq,
    {
        fn files(files: HashSet<(FileType, FileId)>) -> Self {
            Self {
                manifests: hashset! {},
                files,
            }
        }

        fn manifests(manifests: HashSet<ManifestId>) -> Self {
            Self {
                manifests,
                files: hashset! {},
            }
        }
    }

    fn changed(
        path: MPath,
        ty: FileType,
        name: &'static str,
    ) -> Option<BonsaiDiffFileChange<TestFileId>> {
        Some(BonsaiDiffFileChange::Changed(path, ty, TestFileId(name)))
    }

    fn reused(
        path: MPath,
        ty: FileType,
        name: &'static str,
    ) -> Option<BonsaiDiffFileChange<TestFileId>> {
        Some(BonsaiDiffFileChange::ChangedReusedId(
            path,
            ty,
            TestFileId(name),
        ))
    }

    fn deleted(path: MPath) -> Option<BonsaiDiffFileChange<TestFileId>> {
        Some(BonsaiDiffFileChange::Deleted(path))
    }

    #[fbinit::test]
    async fn test_unfold_file_from_files(fb: FacebookInit) -> Result<(), Error> {
        let store = ManifestStore::default();
        let root = path("a");

        let node = Some(file(FileType::Regular, "1"));
        let mut parents = CompositeEntry::empty();
        let ctx = ctx(fb);
        borrowed!(ctx);

        // Start with no parent. The file should be added.
        let (change, work) =
            bonsai_diff_unfold(ctx, &store, root.clone(), node, parents.clone()).await?;
        assert_eq!(change, changed(root.clone(), FileType::Regular, "1"));
        assert_eq!(work, hashmap! {});

        // Add the same file in a parent
        parents.insert(file(FileType::Regular, "1"));
        let (change, work) =
            bonsai_diff_unfold(ctx, &store, root.clone(), node, parents.clone()).await?;
        assert_eq!(change, None);
        assert_eq!(work, hashmap! {});

        // Add the file again in a different parent
        parents.insert(file(FileType::Regular, "1"));
        let (change, work) =
            bonsai_diff_unfold(ctx, &store, root.clone(), node, parents.clone()).await?;
        assert_eq!(change, None);
        assert_eq!(work, hashmap! {});

        // Add a different file
        parents.insert(file(FileType::Regular, "2"));
        let (change, work) =
            bonsai_diff_unfold(ctx, &store, root.clone(), node, parents.clone()).await?;
        assert_eq!(change, reused(root.clone(), FileType::Regular, "1"));
        assert_eq!(work, hashmap! {});

        Ok(())
    }

    #[fbinit::test]
    async fn test_unfold_file_mode_change(fb: FacebookInit) -> Result<(), Error> {
        let store = ManifestStore::default();
        let root = path("a");

        let node = Some(file(FileType::Regular, "1"));
        let mut parents = CompositeEntry::empty();
        let ctx = ctx(fb);
        borrowed!(ctx);

        // Add a parent with a different mode. We can reuse it.
        parents.insert(file(FileType::Executable, "1"));
        let (change, work) =
            bonsai_diff_unfold(ctx, &store, root.clone(), node, parents.clone()).await?;
        assert_eq!(change, reused(root.clone(), FileType::Regular, "1"));
        assert_eq!(work, hashmap! {});

        Ok(())
    }

    #[fbinit::test]
    async fn test_unfold_file_from_dirs(fb: FacebookInit) -> Result<(), Error> {
        let store = ManifestStore::default();
        let root = path("a");

        let node = Some(file(FileType::Regular, "1"));
        let mut parents = CompositeEntry::empty();
        let ctx = ctx(fb);
        borrowed!(ctx);

        // Add a conflicting directory. We need to delete it.
        parents.insert(dir("1"));
        let (change, work) =
            bonsai_diff_unfold(ctx, &store, root.clone(), node, parents.clone()).await?;
        assert_eq!(change, changed(root.clone(), FileType::Regular, "1"));
        assert_eq!(work, hashmap! {});

        // Add another parent with the same file. We can reuse it but we still need to emit it.
        parents.insert(file(FileType::Regular, "1"));
        let (change, work) =
            bonsai_diff_unfold(ctx, &store, root.clone(), node, parents.clone()).await?;
        assert_eq!(change, reused(root.clone(), FileType::Regular, "1"));
        assert_eq!(work, hashmap! {});

        // Add a different file. Same as above.
        parents.insert(file(FileType::Regular, "2"));
        let (change, work) =
            bonsai_diff_unfold(ctx, &store, root.clone(), node, parents.clone()).await?;
        assert_eq!(change, reused(root.clone(), FileType::Regular, "1"));
        assert_eq!(work, hashmap! {});

        Ok(())
    }

    #[fbinit::test]
    async fn test_unfold_dir_from_dirs(fb: FacebookInit) -> Result<(), Error> {
        let root = path("a");

        let store = ManifestStore(hashmap! {
            TestManifestIdStr("1") => TestManifestStr(hashmap! {
                element("p1") => file(FileType::Regular, "1"),
            }),
            TestManifestIdStr("2") => TestManifestStr(hashmap! {
                element("p1") => file(FileType::Executable, "2"),
                element("p2") => dir("2"),
            })
        });

        let node = Some(dir("1"));
        let mut parents = CompositeEntry::empty();
        let ctx = ctx(fb);
        borrowed!(ctx);

        // No parents. We need to recurse in this directory.
        let (change, work) =
            bonsai_diff_unfold(ctx, &store, root.clone(), node, parents.clone()).await?;
        assert_eq!(change, None);
        assert_eq!(
            work,
            hashmap! {
                path("a/p1") => (Some(file(FileType::Regular, "1")), CompositeEntry::empty()),
            }
        );

        // Identical parent. We are done.
        parents.insert(dir("1"));
        let (change, work) =
            bonsai_diff_unfold(ctx, &store, root.clone(), node, parents.clone()).await?;
        assert_eq!(change, None);
        assert_eq!(work, hashmap! {});

        // One parent differs. Recurse on the 2 paths reachable from those manifests, and with the
        // contents listed in both parent manifests.
        parents.insert(dir("2"));
        let (change, work) =
            bonsai_diff_unfold(ctx, &store, root.clone(), node, parents.clone()).await?;
        assert_eq!(change, None);
        assert_eq!(
            work,
            hashmap! {
                path("a/p1") => (
                    Some(file(FileType::Regular, "1")),
                    CompositeEntry::files(hashset! {
                        (FileType::Regular, TestFileId("1")),
                        (FileType::Executable, TestFileId("2"))
                    })
                ),
                path("a/p2") => (
                    None,
                    CompositeEntry::manifests(hashset! { TestManifestIdStr("2") })
                ),
            }
        );

        Ok(())
    }

    #[fbinit::test]
    async fn test_unfold_dir_from_files(fb: FacebookInit) -> Result<(), Error> {
        let root = path("a");

        let store =
            ManifestStore(hashmap! { TestManifestIdStr("1") => TestManifestStr(hashmap! {}) });

        let node = Some(dir("1"));
        let mut parents = CompositeEntry::empty();
        let ctx = ctx(fb);
        borrowed!(ctx);

        // Parent has a file. Delete it.
        parents.insert(file(FileType::Regular, "1"));
        let (change, work) =
            bonsai_diff_unfold(ctx, &store, root.clone(), node, parents.clone()).await?;
        assert_eq!(change, deleted(root));
        assert_eq!(work, hashmap! {});

        Ok(())
    }

    #[fbinit::test]
    async fn test_unfold_missing_from_files(fb: FacebookInit) -> Result<(), Error> {
        let store = ManifestStore::default();
        let root = path("a");

        let mut parents = CompositeEntry::empty();
        let ctx = ctx(fb);
        borrowed!(ctx);

        // Parent has a file, delete it.
        parents.insert(file(FileType::Regular, "1"));
        let (change, work) =
            bonsai_diff_unfold(ctx, &store, root.clone(), None, parents.clone()).await?;
        assert_eq!(change, deleted(root));
        assert_eq!(work, hashmap! {});

        Ok(())
    }

    #[fbinit::test]
    async fn test_unfold_missing_from_dirs(fb: FacebookInit) -> Result<(), Error> {
        let root = path("a");

        let store = ManifestStore(hashmap! {
            TestManifestIdStr("1") => TestManifestStr(hashmap! {
                element("p1") => dir("2"),
            }),
            TestManifestIdStr("2") => TestManifestStr(hashmap! {
                element("p2") => dir("3"),
            })
        });

        let mut parents = CompositeEntry::empty();
        let ctx = ctx(fb);
        borrowed!(ctx);

        // Parent has a directory, recurse into it.
        parents.insert(dir("1"));
        let (change, work) =
            bonsai_diff_unfold(ctx, &store, root.clone(), None, parents.clone()).await?;
        assert_eq!(change, None);
        assert_eq!(
            work,
            hashmap! {
                path("a/p1") => (
                    None,
                    CompositeEntry::manifests(hashset! { TestManifestIdStr("2") })
                ),
            }
        );

        // Multiple parents have multiple directories. Recurse into all of them.
        parents.insert(dir("2"));
        let (change, work) =
            bonsai_diff_unfold(ctx, &store, root.clone(), None, parents.clone()).await?;
        assert_eq!(change, None);
        assert_eq!(
            work,
            hashmap! {
                path("a/p1") => (
                    None,
                    CompositeEntry::manifests(hashset! { TestManifestIdStr("2") })
                ),
                path("a/p2") => (
                    None,
                    CompositeEntry::manifests(hashset! { TestManifestIdStr("3") })
                ),
            }
        );

        Ok(())
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::collections::BTreeMap;
use std::collections::HashSet;
use std::fmt;
use std::future::Future;
use std::hash::Hash;
use std::sync::Arc;

use anyhow::format_err;
use anyhow::Error;
use borrowed::borrowed;
use cloned::cloned;
use context::CoreContext;
use futures::channel::mpsc;
use futures::future;
use futures::future::FutureExt;
use futures::future::TryFutureExt;
use futures::stream::StreamExt;
use futures::stream::TryStreamExt;
use mononoke_types::MPath;
use mononoke_types::MPathElement;

use crate::AsyncManifest as Manifest;
use crate::Entry;
use crate::PathTree;
use crate::StoreLoadable;

/// Information passed to `create_tree` function when tree node is constructed
///
/// `Ctx` is any additional data which is useful for particular implementation of
/// manifest. It is `Some` for subentries for which `create_{tree|leaf}` was called to
/// generate them, and `None` if subentry was reused from one of its parents.
pub struct TreeInfo<TreeId, LeafId, Ctx> {
    pub path: Option<MPath>,
    pub parents: Vec<TreeId>,
    pub subentries: BTreeMap<MPathElement, (Option<Ctx>, Entry<TreeId, LeafId>)>,
}

/// Information passed to `create_leaf` function when leaf node is constructed
pub struct LeafInfo<LeafId, Leaf> {
    pub path: MPath,
    pub parents: Vec<LeafId>,
    /// Leaf value, if it is not provided it means we have leaves only conflict
    /// which can potentially be resolved by `create_leaf`, in case of mercurial
    /// multiple leaves with the same content can be successfully resolved.
    pub leaf: Option<Leaf>,
}

/// Derive a new manifest from parents and a set of changes. The types have to match in the
/// following way here:
///
/// - We'll walk and merge the manifests from the parents. Those must be Manifests where trees are
/// TreeId and leaves are LeafId.
/// - We'll create new leaves (for the diff) through create_leaf (which should merge changes), and
/// new tres through create_tree, which will receive entries consisting of existing trees and tres
/// merged with new leaves and trees (and should produce a new tree).
/// - To make this work, create_tree must return the same kind of TreeId as the ones that exist in
/// the tree currently. That said, this constraint is marginally relaxed for leaves: create_leaf
/// can return an IntermediateLeafId that must, and that is the also the type that create_tree will
/// receive for leaves (to make this work, IntermediateLeafId must implement From<LeafId> so that
/// leaves that are to be reused from the existing tree can be turned into IntermediateLeafId).
///
/// Note that for most use cases, IntermediateLeafId and LeafId should probably be the same type.
/// That said, this distinction can be useful in cases where the leaves aren't actually objects
/// that exist in the blobstore, and are just contained in trees. This is notably the case with
/// Fsnodes, where leaves are FsnodeFiles and are actually stored in their parent manifest.
pub fn derive_manifest<TreeId, LeafId, IntermediateLeafId, Leaf, T, TFut, L, LFut, Ctx, Store>(
    ctx: CoreContext,
    store: Store,
    parents: impl IntoIterator<Item = TreeId>,
    changes: impl IntoIterator<Item = (MPath, Option<Leaf>)>,
    create_tree: T,
    create_leaf: L,
) -> impl Future<Output = Result<Option<TreeId>, Error>>
where
    Store: Sync + Send + Clone + 'static,
    LeafId: Send + Clone + Eq + Hash + fmt::Debug + 'static,
    Leaf: Send + 'static,
    IntermediateLeafId: Send + From<LeafId> + 'static + fmt::Debug + Clone + Eq + Hash,
    TreeId: StoreLoadable<Store> + Clone + Eq + Hash + fmt::Debug + Send + Sync + 'static,
    TreeId::Value: Manifest<Store, TreeId = TreeId, LeafId = LeafId>,
    <TreeId as StoreLoadable<Store>>::Value: Send + Sync,
    T: Fn(TreeInfo<TreeId, IntermediateLeafId, Ctx>) -> TFut + Send + Sync + 'static,
    TFut: Future<Output = Result<(Ctx, TreeId), Error>> + Send + 'static,
    L: Fn(LeafInfo<IntermediateLeafId, Leaf>) -> LFut + Send + Sync + 'static,
    LFut: Future<Output = Result<(Ctx, IntermediateLeafId), Error>> + Send + 'static,
    Ctx: Send + 'static,
{
    derive_manifest_inner(ctx, store, parents, changes, create_tree, create_leaf)
}

/// Construct a new manifest from parent manifests and a list of changes from a bonsai commit.
/// The manifest is constructed in accordance with bonsai semantic (see below for explanation).
///
/// Parent manifests should have been constructed for each parent of this bonsai commit
/// (note that there can be more than 2 parents).
///
/// In the context of this function "manifest" means a set of leaves and trees. Manifest is a
/// recursive structure that starts with a root tree which can point to other trees or leaves.
/// Each leaf represents a file in a repository, and each tree represents a directory.
///
/// Note that while `derive_manifest` can be used to construct Mercurial manifests
/// (e.g. Manifests where leafs are filenodes), it's not limited to them. Leafs can be arbitrary
/// ids e.g. SHA-1 content of a file, unode ids. Tree ids can also be arbitrary.
///
/// ## Bonsai Semantic
/// Bonsai semantic is a set of rules about how to apply changes to manifests. Each change is
/// either `Some(Leaf)` meaning that a new file exists in the new commit (either new file is
/// created or reused from one of the parents), `None` meaning that the file was deleted from
/// one of the parents and no longer exist in the new bonsai commit.
/// Also see: [Bonsai changeset actions](https://fb.quip.com/A2kqArd9Nb90)
///
/// Changes are applied recursively starting from the root of parent manifests.
/// Here is how changes affect conflict resolution
/// 1. If no change ends on the current path or any subpaths e.g. we are just merging parent
///    manifests.
///    - If all entries in parent manifests are identical (regardless of whether they are trees
///      or leaves) then we are just reusing the current entry.
///    - If all parents entries are trees then recurse into all of them and continue merging.
///    - If all parent entries are leaves then `create_leaf` is called with `None` leaf, this
///      can result in successful merge (for example, mercurial can merge different entries with
///      same content).
///    - If we have a mix of leaves/trees in parent entries - we have a broken commit and this is
///      an unresolved conflict error.
/// 2. If no change ends on the current path BUT there are changes on the subpaths (e.g. we are
///    on "A/", but there's "A/file.txt").
///    - If all parents entries are trees then recurse into all of them and continue merging
///    - If we have a single leaf in parent entries - we have a broken commit and this is an
///      unresolved conflict error.
/// 3. Current path have `None` change associated with it.
///   - Only trees: invalid changes.
///   - Only leaves: all leaves are removed.
///   - Mix of leaves/trees: all leaves are removed, recurse into the trees.
/// 4. Current path have `Some(leaf)` change associated with it.
///   - _: all the trees are removed in favour of this leaf.
///
pub fn derive_manifest_inner<
    TreeId,
    LeafId,
    IntermediateLeafId,
    Leaf,
    T,
    TFut,
    L,
    LFut,
    Ctx,
    Store,
>(
    ctx: CoreContext,
    store: Store,
    parents: impl IntoIterator<Item = TreeId>,
    changes: impl IntoIterator<Item = (MPath, Option<Leaf>)>,
    create_tree: T,
    create_leaf: L,
) -> impl Future<Output = Result<Option<TreeId>, Error>>
where
    Store: Sync + Send + Clone + 'static,
    LeafId: Send + Clone + Eq + Hash + fmt::Debug + 'static,
    Leaf: Send + 'static,
    IntermediateLeafId: Send + From<LeafId> + 'static + Eq + Hash + Clone + fmt::Debug,
    TreeId: StoreLoadable<Store> + Clone + Eq + Hash + fmt::Debug + Send + Sync + 'static,
    TreeId::Value: Manifest<Store, TreeId = TreeId, LeafId = LeafId>,
    <TreeId as StoreLoadable<Store>>::Value: Send + Sync,
    T: Fn(TreeInfo<TreeId, IntermediateLeafId, Ctx>) -> TFut + Send + Sync + 'static,
    TFut: Future<Output = Result<(Ctx, TreeId), Error>> + Send + 'static,
    L: Fn(LeafInfo<IntermediateLeafId, Leaf>) -> LFut + Send + Sync + 'static,
    LFut: Future<Output = Result<(Ctx, IntermediateLeafId), Error>> + Send + 'static,
    Ctx: Send + 'static,
{
    bounded_traversal::bounded_traversal(
        256,
        MergeNode {
            name: None,
            path: None,
            changes: PathTree::from_iter(
                changes
                    .into_iter()
                    .map(|(path, change)| (path, Some(Change::from(change)))),
            ),
            parents: parents.into_iter().map(Entry::Tree).collect(),
        },
        // unfold, all merge logic happens in this unfold function
        move |merge_node: MergeNode<_, IntermediateLeafId, _>| {
            merge(ctx.clone(), store.clone(), merge_node).boxed()
        },
        // fold, this function only creates entries from merge result and already merged subentries
        {
            let create_tree = Arc::new(create_tree);
            let create_leaf = Arc::new(create_leaf);
            move |merge_result: MergeResult<_, IntermediateLeafId, _>, subentries| {
                let create_tree = create_tree.clone();
                let create_leaf = create_leaf.clone();
                async move {
                    tokio::spawn(async move {
                        match merge_result {
                            MergeResult::Reuse { name, entry } => {
                                Ok(Some((name, None, convert_to_intermediate_entry(entry))))
                            }
                            MergeResult::Delete => Ok(None),
                            MergeResult::CreateTree {
                                name,
                                path,
                                parents,
                            } => {
                                let subentries: BTreeMap<_, _> = subentries
                                    .flatten()
                                    .filter_map(
                                        |(name, context, entry): (
                                            Option<MPathElement>,
                                            Option<Ctx>,
                                            _,
                                        )| {
                                            name.map(move |name| (name, (context, entry)))
                                        },
                                    )
                                    .collect();
                                if subentries.is_empty() {
                                    Ok(None)
                                } else {
                                    let (context, tree_id) = create_tree(TreeInfo {
                                        path: path.clone(),
                                        parents,
                                        subentries,
                                    })
                                    .await?;
                                    Ok(Some((name, Some(context), Entry::Tree(tree_id))))
                                }
                            }
                            MergeResult::CreateLeaf {
                                leaf,
                                name,
                                path,
                                parents,
                            } => {
                                let (context, leaf_id) = create_leaf(LeafInfo {
                                    leaf,
                                    path: path.clone(),
                                    parents,
                                })
                                .await?;
                                Ok(Some((name, Some(context), Entry::Leaf(leaf_id))))
                            }
                        }
                    })
                    .await?
                }
                .boxed()
            }
        },
    )
    .map_ok(|result: Option<_>| result.and_then(|(_, _, entry)| entry.into_tree()))
}

fn convert_to_intermediate_entry<TreeId, LeafId, IntermediateLeafId>(
    e: Entry<TreeId, LeafId>,
) -> Entry<TreeId, IntermediateLeafId>
where
    IntermediateLeafId: From<LeafId>,
{
    match e {
        Entry::Tree(t) => Entry::Tree(t),
        Entry::Leaf(l) => Entry::Leaf(l.into()),
    }
}

type BoxFuture<T, E> = future::BoxFuture<'static, Result<T, E>>;

/// A convenience wrapper around `derive_manifest` that allows for the tree and leaf creation
/// closures to send IO work onto a channel that is then fed into a buffered stream. NOTE: don't
/// send computationally expensive work as it will block the task.
///
/// The sender is commonly used to write blobs to the blobstore concurrently.
///
/// `derive_manifest_with_work_sender` guarantees that all work is completed before it returns, but
/// it does not guarantee the order in which the work is completed.
pub fn derive_manifest_with_io_sender<
    TreeId,
    LeafId,
    IntermediateLeafId,
    Leaf,
    T,
    TFut,
    L,
    LFut,
    Ctx,
    Store,
>(
    ctx: CoreContext,
    store: Store,
    parents: impl IntoIterator<Item = TreeId>,
    changes: impl IntoIterator<Item = (MPath, Option<Leaf>)>,
    create_tree_with_sender: T,
    create_leaf_with_sender: L,
) -> impl Future<Output = Result<Option<TreeId>, Error>>
where
    LeafId: Send + Clone + Eq + Hash + fmt::Debug + 'static,
    Leaf: Send + 'static,
    IntermediateLeafId: Send + From<LeafId> + 'static + Eq + Hash + fmt::Debug + Clone,
    Store: Sync + Send + Clone + 'static,
    TreeId: StoreLoadable<Store> + Clone + Eq + Hash + fmt::Debug + Send + Sync + 'static,
    TreeId::Value: Manifest<Store, TreeId = TreeId, LeafId = LeafId>,
    <TreeId as StoreLoadable<Store>>::Value: Send + Sync,
    T: Fn(
            TreeInfo<TreeId, IntermediateLeafId, Ctx>,
            mpsc::UnboundedSender<BoxFuture<(), Error>>,
        ) -> TFut
        + Send
        + Sync
        + 'static,
    TFut: Future<Output = Result<(Ctx, TreeId), Error>> + Send + 'static,
    L: Fn(LeafInfo<IntermediateLeafId, Leaf>, mpsc::UnboundedSender<BoxFuture<(), Error>>) -> LFut
        + Send
        + Sync
        + 'static,
    LFut: Future<Output = Result<(Ctx, IntermediateLeafId), Error>> + Send + 'static,
    Ctx: Send + 'static,
{
    let (sender, receiver) = mpsc::unbounded();

    let derive = derive_manifest_inner(
        ctx,
        store,
        parents,
        changes,
        {
            cloned!(sender);
            move |tree_info| create_tree_with_sender(tree_info, sender.clone())
        },
        {
            cloned!(sender);
            move |leaf_info| create_leaf_with_sender(leaf_info, sender.clone())
        },
    );
    let process = receiver
        .buffer_unordered(1024)
        .try_for_each(|_| future::ok(()));

    future::try_join(derive, process).map_ok(|(res, ())| res)
}

// Change is isomorphic to Option, but it makes it easier to understand merge logic
enum Change<LeafId> {
    Add(LeafId),
    Remove,
}

impl<Leaf> From<Option<Leaf>> for Change<Leaf> {
    fn from(change: Option<Leaf>) -> Self {
        change.map_or(Change::Remove, Change::Add)
    }
}

enum MergeResult<TreeId, LeafId, Leaf> {
    Delete,
    Reuse {
        name: Option<MPathElement>,
        entry: Entry<TreeId, LeafId>,
    },
    CreateLeaf {
        leaf: Option<Leaf>,
        name: Option<MPathElement>,
        path: MPath,
        parents: Vec<LeafId>,
    },
    CreateTree {
        name: Option<MPathElement>,
        path: Option<MPath>,
        parents: Vec<TreeId>,
    },
}

/// This node represents unmerged state of `parents.len() + 1` way merge
/// between changes and parents.
struct MergeNode<TreeId, LeafId, Leaf> {
    name: Option<MPathElement>, // name of this node in parent manifest
    path: Option<MPath>,        // path to this node from root of the manifest
    changes: PathTree<Option<Change<Leaf>>>, // changes associated with current subtree
    parents: Vec<Entry<TreeId, LeafId>>, // unmerged parents of current node
}

async fn merge<TreeId, LeafId, IntermediateLeafId, Leaf, Store>(
    ctx: CoreContext,
    store: Store,
    node: MergeNode<TreeId, IntermediateLeafId, Leaf>,
) -> Result<
    (
        MergeResult<TreeId, IntermediateLeafId, Leaf>,
        Vec<MergeNode<TreeId, IntermediateLeafId, Leaf>>,
    ),
    Error,
>
where
    Store: Sync + Send + Clone + 'static,
    IntermediateLeafId: Send + From<LeafId> + 'static + fmt::Debug + Clone + Eq + Hash,
    LeafId: Clone + Eq + Hash + fmt::Debug,
    TreeId: StoreLoadable<Store> + Clone + Eq + Hash + fmt::Debug + Sync,
    TreeId::Value: Manifest<Store, TreeId = TreeId, LeafId = LeafId>,
{
    let MergeNode {
        name,
        path,
        changes: PathTree {
            value: change,
            subentries,
        },
        mut parents,
    } = node;

    // Deduplicate entries in parents list, **preserving order** of entries.
    // Essentially performing a trivial merge between identical entries.
    {
        let mut visited = HashSet::new();
        parents.retain(|parent| visited.insert(parent.clone()));
    }

    // Apply change
    // If we create `parent_subtrees` (none of the return statement have been reached), this
    // indicates that file/tree conflict if any, has been resolved in favour of tree merge.
    let parent_subtrees = match change {
        None => match parents.as_slice() {
            // Changes does not have entry associated with current path
            [parent_entry] => {
                // Only one tree/leaf is left
                if !subentries.is_empty() {
                    match parent_entry {
                        Entry::Leaf(_) => {
                            // Current entry is a leaf but we still have changes that needs
                            // to be applied to its subentries, we cannot resolve this merge.
                            let error = format_err!(
                                "Can not apply changes to a leaf:\npath: {:?}\nparents: {:?}",
                                path,
                                parents
                            );
                            return Err(error);
                        }
                        Entry::Tree(tree_id) => {
                            // We have tree entry, and changes that needs to be applied
                            // to its subentries, we cannot reuse this entry and have to recurse
                            vec![tree_id.clone()]
                        }
                    }
                } else {
                    // We have single entry and do not have any changes associated with it subentries,
                    // it is safe to reuse current entry as is.
                    return Ok((
                        MergeResult::Reuse {
                            name,
                            entry: parent_entry.clone(),
                        },
                        Vec::new(),
                    ));
                }
            }
            _ => {
                // Split entries int leaves and trees.
                let mut leaves = Vec::new();
                let mut trees = Vec::new();
                for entry in parents.iter() {
                    match entry {
                        Entry::Leaf(leaf) => leaves.push(leaf.clone()),
                        Entry::Tree(tree) => trees.push(tree.clone()),
                    }
                }

                if leaves.is_empty() {
                    // We do not have any leaves at this point, and should proceed with
                    // merging of trees
                    trees
                } else if trees.is_empty() && subentries.is_empty() {
                    // We have leaves only but their ids are not equal to each other,
                    // this should immediately indicate conflict, as mercurial can successfully
                    // merge these leaves if they have identical content.
                    return Ok((
                        MergeResult::CreateLeaf {
                            leaf: None,
                            name,
                            path: path.expect("leaf can not have empty path"),
                            parents: leaves,
                        },
                        Vec::new(),
                    ));
                } else {
                    // We can get here in two cases:
                    //   - we have mix of trees and leaves.
                    //   - all of the parents are leaves, but we have changes that need to be
                    //     applied to it current nodes subentries.
                    // both of this situation result in unresolvable conflict.
                    let error = format_err!(
                        "Unresolved conflict at:\npath: {:?}\nparents: {:?}",
                        path,
                        parents
                    );
                    return Err(error);
                }
            }
        },
        Some(Change::Remove) => {
            // Remove associated Leaf entr{y|ies}, leaving only trees.
            // This case is used to either remove leaf entry or resolve file/tree conflict
            // in favour of tree merge.
            parents.into_iter().filter_map(Entry::into_tree).collect()
        }
        Some(Change::Add(leaf)) => {
            // Replace current merge node with a leaf, and stop traversal.
            // This case is used ot either replace leaf entry or resolve file/tree conflict
            // in favour or file.
            return Ok((
                MergeResult::CreateLeaf {
                    leaf: Some(leaf),
                    name,
                    path: path.expect("leaf can not have empty path"),
                    parents: parents.into_iter().filter_map(Entry::into_leaf).collect(),
                },
                Vec::new(),
            ));
        }
    };

    if parent_subtrees.is_empty() && subentries.is_empty() {
        // All elements of this merge tree have been deleted.
        // Nothing left to do apart from inidicating that this node needs to be removed
        // from its parent.
        return Ok((MergeResult::Delete, Vec::new()));
    }

    // Fetch parent trees and merge them.
    borrowed!(ctx, store);
    let manifests = future::try_join_all(parent_subtrees.iter().map(move |tree_id| {
        cloned!(ctx);
        async move { tree_id.load(&ctx, store).await }
    }))
    .await?;

    let mut deps: BTreeMap<MPathElement, _> = Default::default();
    // add subentries from all parents
    for manifest in manifests {
        // TODO(T123518092): Do this concurrently where possible. Also, skip
        // it altogether if possible, instead using lookup.
        let mut stream = manifest.list(ctx, store).await?;
        while let Some((name, entry)) = stream.try_next().await? {
            let subentry = deps.entry(name.clone()).or_insert_with(|| MergeNode {
                path: Some(MPath::join_opt_element(path.as_ref(), &name)),
                name: Some(name),
                changes: Default::default(),
                parents: Default::default(),
            });
            subentry.parents.push(convert_to_intermediate_entry(entry));
        }
    }
    // add subentries from changes
    for (name, change) in subentries {
        let subentry = deps.entry(name.clone()).or_insert_with(|| MergeNode {
            path: Some(MPath::join_opt_element(path.as_ref(), &name)),
            name: Some(name),
            changes: Default::default(),
            parents: Default::default(),
        });
        subentry.changes = change;
    }

    Ok((
        MergeResult::CreateTree {
            name,
            path,
            parents: parent_subtrees,
        },
        deps.into_values().collect(),
    ))
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::collections::BTreeMap;
use std::fmt;
use std::hash::Hash;
use std::sync::Arc;

use anyhow::anyhow;
use anyhow::Context;
use anyhow::Error;
use blobstore::StoreLoadable;
use cloned::cloned;
use context::CoreContext;
use futures::Future;
use futures::FutureExt;
use mononoke_types::ChangesetId;
use mononoke_types::MPath;
use mononoke_types::MPathElement;

use crate::Entry;
use crate::LeafInfo;
use crate::Manifest;
use crate::PathTree;
use crate::TreeInfo;

pub struct ManifestChanges<Leaf> {
    pub cs_id: ChangesetId,
    pub changes: Vec<(MPath, Option<Leaf>)>,
}

// Function that can derive manifests for a "simple" stack of commits. But what does "simple" mean?
// In this case "simple" means:
// 1) There are no merges i.e. we get just a Linear stack of commits
//    (`changes` parameter  should be sorted from ancestors to descendant)
// 2) Paths that commits modify should not be prefixes of each other i.e.
//    stack shouldn't have file changes ("dir/A" => "dir") and ("dir" => "file").
//    The exception applies only to changes that were modified in the same commit
//    i.e. if a single commit has ("dir" => None) and ("dir/A" => "content") then this
//    commit can be derived by derive_manifests_for_simple_stack_of_commits.
#[allow(unused)]
pub async fn derive_manifests_for_simple_stack_of_commits<
    'caller,
    TreeId,
    LeafId,
    IntermediateLeafId,
    Leaf,
    T,
    TFut,
    L,
    LFut,
    Ctx,
    Store,
>(
    ctx: CoreContext,
    store: Store,
    parent: Option<TreeId>,
    changes: Vec<ManifestChanges<Leaf>>,
    create_tree: T,
    create_leaf: L,
) -> Result<BTreeMap<ChangesetId, TreeId>, Error>
where
    Store: Sync + Send + Clone + 'static,
    LeafId: Send + Clone + Eq + Hash + fmt::Debug + 'static + Sync,
    IntermediateLeafId: Clone + Send + From<LeafId> + 'static + Sync,
    Leaf: Send + 'static,
    TreeId: StoreLoadable<Store> + Clone + Eq + Hash + fmt::Debug + Send + Sync + 'static,
    TreeId::Value: Manifest<TreeId = TreeId, LeafId = LeafId> + Sync,
    <TreeId as StoreLoadable<Store>>::Value: Send,
    T: Fn(TreeInfo<TreeId, IntermediateLeafId, Ctx>, ChangesetId) -> TFut + Send + Sync + 'static,
    TFut: Future<Output = Result<(Ctx, TreeId), Error>> + Send + 'caller,
    L: Fn(LeafInfo<IntermediateLeafId, Leaf>, ChangesetId) -> LFut + Send + Sync + 'static,
    LFut: Future<Output = Result<(Ctx, IntermediateLeafId), Error>> + Send + 'caller,
    Ctx: Clone + Send + Sync + 'static,
{
    Deriver {
        ctx,
        store,
        parent,
        changes,
        create_tree,
        create_leaf,
        _marker: std::marker::PhantomData,
    }
    .derive()
    .await
}

// Stack of changes for a single file path - it can be either a tree or a leaf
struct EntryStack<TreeId, LeafId, Ctx> {
    parent: Option<Entry<TreeId, LeafId>>,
    values: Vec<(ChangesetId, Option<Ctx>, Option<Entry<TreeId, LeafId>>)>,
}

// This struct is not necessary, it just exists so that we don't need to repeat
// a long list of generic restrictions for each function
struct Deriver<TreeId, Leaf, IntermediateLeafId, T, L, Store> {
    ctx: CoreContext,
    store: Store,
    parent: Option<TreeId>,
    changes: Vec<ManifestChanges<Leaf>>,
    create_tree: T,
    create_leaf: L,
    _marker: std::marker::PhantomData<IntermediateLeafId>,
}

fn convert_to_intermediate_entry<TreeId, LeafId, IntermediateLeafId>(
    e: Entry<TreeId, LeafId>,
) -> Entry<TreeId, IntermediateLeafId>
where
    IntermediateLeafId: From<LeafId>,
{
    match e {
        Entry::Tree(t) => Entry::Tree(t),
        Entry::Leaf(l) => Entry::Leaf(l.into()),
    }
}

impl<'caller, TreeId, LeafId, IntermediateLeafId, Leaf, T, TFut, L, LFut, Ctx, Store>
    Deriver<TreeId, Leaf, IntermediateLeafId, T, L, Store>
where
    Store: Sync + Send + Clone + 'static,
    LeafId: Send + Clone + Eq + Hash + fmt::Debug + 'static + Sync,
    IntermediateLeafId: Clone + Send + From<LeafId> + 'static + Sync,
    Leaf: Send + 'static,
    TreeId: StoreLoadable<Store> + Clone + Eq + Hash + fmt::Debug + Send + Sync + 'static,
    TreeId::Value: Manifest<TreeId = TreeId, LeafId = LeafId> + Sync,
    <TreeId as StoreLoadable<Store>>::Value: Send,
    T: Fn(TreeInfo<TreeId, IntermediateLeafId, Ctx>, ChangesetId) -> TFut + Send + Sync + 'static,
    TFut: Future<Output = Result<(Ctx, TreeId), Error>> + Send + 'caller,
    L: Fn(LeafInfo<IntermediateLeafId, Leaf>, ChangesetId) -> LFut + Send + Sync + 'static,
    LFut: Future<Output = Result<(Ctx, IntermediateLeafId), Error>> + Send + 'caller,
    Ctx: Clone + Send + Sync + 'static,
{
    async fn derive(self) -> Result<BTreeMap<ChangesetId, TreeId>, Error> {
        let Deriver {
            ctx,
            store,
            parent,
            changes,
            create_tree,
            create_leaf,
            ..
        } = self;

        // This is a tree of paths, and for each path we store a list of
        // (ChangesetId, Option<Leaf>) i.e. for each path we store
        // changesets where it was modified and what this modification was.
        // Each list of modifications is ordered in the same order as the commits
        // in the stack.
        let mut path_tree = PathTree::<Vec<(ChangesetId, Option<Leaf>)>>::default();

        let mut stack_of_commits = vec![];
        for mf_changes in changes {
            for (path, leaf) in mf_changes.changes {
                path_tree.insert_and_merge(Some(path), (mf_changes.cs_id, leaf));
            }
            stack_of_commits.push(mf_changes.cs_id);
        }

        struct UnfoldState<TreeId, LeafId, Leaf> {
            path: Option<MPath>,
            name: Option<MPathElement>,
            parent: Option<Entry<TreeId, LeafId>>,
            path_tree: PathTree<Vec<(ChangesetId, Leaf)>>,
        }

        enum FoldState<TreeId, LeafId, Leaf> {
            Reuse(
                Option<MPath>,
                Option<MPathElement>,
                Option<Entry<TreeId, LeafId>>,
            ),
            CreateLeaves(
                Option<MPath>,
                MPathElement,
                Option<Entry<TreeId, LeafId>>,
                Vec<Leaf>,
            ),
            CreateTrees(
                Option<MPath>,
                Option<MPathElement>,
                Option<Entry<TreeId, LeafId>>,
                // We might have a single file deletion, this field represents it
                Option<ChangesetId>,
            ),
        }

        let stack_of_commits = Arc::new(stack_of_commits);
        let (_, entry_stack) = bounded_traversal::bounded_traversal(
            256,
            UnfoldState {
                path: None,
                name: None,
                parent: parent.clone().map(Entry::Tree),
                path_tree,
            },
            // Unfold - during this traversal we visit all changed paths and decide what to do with them
            {
                cloned!(ctx, store);
                move |
                    UnfoldState {
                        path,
                        name,
                        parent,
                        path_tree,
                    },
                | {
                    cloned!(ctx, store);
                    async move {
                        let PathTree {
                            value: changes,
                            subentries,
                        } = path_tree;

                        if !changes.is_empty() && subentries.is_empty() {
                            // We have a stack of changes for a given leaf
                            let name = name.ok_or_else(|| anyhow!("unexpected empty path for leaf"))?;
                            Ok((FoldState::CreateLeaves(path, name, parent, changes), vec![]))
                        } else if !subentries.is_empty() {
                            // We need to recurse now - recurse into all parent entries and all subentries

                            let maybe_file_deletion = if !changes.is_empty() {
                                // So a file was changed and also some other subdirectories were
                                // changed. In most cases that would just mean that we are not dealing
                                // with the simple stack and we should just error out, however there's
                                // one exception. If a file was replaced with a directory in a single
                                // commit then we might have both deletion of a file and a few
                                // subentries with the same prefix (e.g. "R file; A file/subentry").
                                // Let's check if that's indeed the case.

                                let mut only_single_deletion = None;
                                if changes.len() == 1 {
                                    let (cs_id, file_change) = &changes[0];
                                    if file_change.is_none() {
                                        only_single_deletion = Some(*cs_id);
                                    }
                                }

                                if only_single_deletion.is_none() {
                                    return Err(anyhow!(
                                        "unsupported stack derive - {:?} is a prefix of other paths",
                                        path
                                    ));
                                }

                                only_single_deletion
                            } else {
                                None
                            };

                            let mut deps: BTreeMap<MPathElement, _> = Default::default();
                            if let Some(Entry::Tree(tree_id)) = &parent {
                                let mf = tree_id.load(&ctx, &store).await?;
                                for (name, entry) in mf.list() {
                                    let subentry =
                                        deps.entry(name.clone()).or_insert_with(|| UnfoldState {
                                            path: Some(MPath::join_opt_element(path.as_ref(), &name)),
                                            name: Some(name),
                                            parent: Default::default(),
                                            path_tree: Default::default(),
                                        });
                                    subentry.parent = Some(entry);
                                }
                            }

                            for (name, path_tree) in subentries {
                                let subentry =
                                    deps.entry(name.clone()).or_insert_with(|| UnfoldState {
                                        path: Some(MPath::join_opt_element(path.as_ref(), &name)),
                                        name: Some(name),
                                        parent: Default::default(),
                                        path_tree: Default::default(),
                                    });
                                subentry.path_tree = path_tree;
                            }

                            let deps = deps.into_values().collect();
                            Ok((
                                FoldState::CreateTrees(path, name, parent, maybe_file_deletion),
                                deps,
                            ))
                        } else if path.is_none() && parent.is_none() {
                            // This is a weird case - we got an empty commit with no parent.
                            // In that case  we want to create an empty root tree for this commit
                            Ok((FoldState::CreateTrees(None, None, None, None), vec![]))
                        } else {
                            // No changes, no subentries - just reuse the entry
                            Ok((FoldState::Reuse(path, name, parent.map(convert_to_intermediate_entry)), vec![]))
                        }
                    }
                    .boxed()
                }
            },
            // Fold - actually create the entries
            {
                let create_leaf = Arc::new(create_leaf);
                let create_tree = Arc::new(create_tree);
                cloned!(ctx, store, stack_of_commits);
                move |fold_state, subentries| {
                    cloned!(ctx, create_leaf, create_tree, stack_of_commits, store);
                    async move {
                        let subentries: BTreeMap<MPathElement, _> = subentries
                            .filter_map(|(maybe_path, val): (Option<_>, _)| {
                                maybe_path.map(|path| (path, val))
                            })
                            .collect();

                        match fold_state {
                            FoldState::CreateLeaves(path, name, parent, leaves) => {
                                if !subentries.is_empty() {
                                    anyhow::bail!(
                                        "Can't create entries for {:?} - have unexpected subentries",
                                        path,
                                    );
                                }

                                let path = path.clone().context("unexpected empty path for leaf")?;
                                let entry_stack = Self::create_leaves(
                                    &ctx,
                                    path,
                                    parent.map(convert_to_intermediate_entry),
                                    leaves,
                                    create_leaf,
                                    create_tree,
                                    store,
                                )
                                .await?;
                                Ok((Some(name), entry_stack))
                            }
                            FoldState::CreateTrees(path, name, parent, maybe_file_deletion) => {
                                let entry_stack = Self::create_trees(
                                    path,
                                    parent.map(convert_to_intermediate_entry),
                                    subentries,
                                    create_tree,
                                    stack_of_commits,
                                    maybe_file_deletion,
                                )
                                .await?;
                                Ok((name, entry_stack))
                            }
                            FoldState::Reuse(_, name, maybe_entry) => {
                                let entry_stack = EntryStack {
                                    parent: maybe_entry.map(convert_to_intermediate_entry),
                                    values: vec![],
                                };
                                Ok((name, entry_stack))
                            }
                        }
                    }
                    .boxed()
                }
            },
        )
        .await?;

        let derived: BTreeMap<_, _> = entry_stack
            .values
            .into_iter()
            .filter_map(|(cs_id, _, maybe_entry)| {
                let maybe_tree_id = match maybe_entry {
                    Some(entry) => Some(entry.into_tree()?),
                    None => None,
                };

                Some((cs_id, maybe_tree_id))
            })
            .collect();

        let mut parent = parent;
        // Make sure that we have entries for every commit
        let mut res = BTreeMap::new();
        for cs_id in stack_of_commits.iter() {
            let new_mf_id = match derived.get(cs_id) {
                Some(maybe_tree_id) => {
                    parent = maybe_tree_id.clone();
                    maybe_tree_id.clone()
                }
                None => parent.clone(),
            };
            let new_mf_id =
                new_mf_id.ok_or_else(|| anyhow!("unexpected empty manifest for {}", cs_id))?;
            res.insert(*cs_id, new_mf_id);
        }

        Ok(res)
    }

    async fn create_leaves(
        ctx: &CoreContext,
        path: MPath,
        parent: Option<Entry<TreeId, IntermediateLeafId>>,
        changes: Vec<(ChangesetId, Option<Leaf>)>,
        create_leaf: Arc<L>,
        create_tree: Arc<T>,
        store: Store,
    ) -> Result<EntryStack<TreeId, IntermediateLeafId, Ctx>, Error> {
        let mut entry_stack = EntryStack {
            parent: parent.clone(),
            values: vec![],
        };
        let mut parent = parent;

        for (cs_id, maybe_leaf) in changes {
            match maybe_leaf {
                Some(leaf) => {
                    let (upload_ctx, leaf_id) = create_leaf(
                        LeafInfo {
                            leaf: Some(leaf),
                            path: path.clone(),
                            // Note that a parent can be a tree here - in that case
                            // directory is implicitly deleted and replaced with a file
                            parents: parent.and_then(Entry::into_leaf).into_iter().collect(),
                        },
                        cs_id,
                    )
                    .await?;

                    entry_stack.values.push((
                        cs_id,
                        Some(upload_ctx),
                        Some(Entry::Leaf(leaf_id.clone())),
                    ));
                    parent = Some(Entry::Leaf(leaf_id));
                }
                None => {
                    if let Some(Entry::Tree(tree_id)) = parent {
                        // This is a weird logic in derive_manifest()
                        // If file is deleted and parent entry is a tree then
                        // we ignore the deletion and create a new object with
                        // existing parent tree entry as a parent.
                        // This is strange, but it's worth replicating what we have
                        // derive_manifest()
                        let parent_mf = tree_id.load(ctx, &store).await?;
                        let subentries = parent_mf
                            .list()
                            .map(|(path, entry)| {
                                (path, (None, convert_to_intermediate_entry(entry)))
                            })
                            .collect();
                        let (upload_ctx, tree_id) = create_tree(
                            TreeInfo {
                                path: Some(path.clone()),
                                parents: vec![tree_id],
                                subentries,
                            },
                            cs_id,
                        )
                        .await?;
                        entry_stack.values.push((
                            cs_id,
                            Some(upload_ctx),
                            Some(Entry::Tree(tree_id.clone())),
                        ));
                        parent = Some(Entry::Tree(tree_id));
                    } else {
                        entry_stack.values.push((cs_id, None, None));
                        parent = None;
                    }
                }
            }
        }

        Ok(entry_stack)
    }

    async fn create_trees(
        path: Option<MPath>,
        parent: Option<Entry<TreeId, IntermediateLeafId>>,
        stack_sub_entries: BTreeMap<MPathElement, EntryStack<TreeId, IntermediateLeafId, Ctx>>,
        create_tree: Arc<T>,
        stack_of_commits: Arc<Vec<ChangesetId>>,
        maybe_file_deletion: Option<ChangesetId>,
    ) -> Result<EntryStack<TreeId, IntermediateLeafId, Ctx>, Error> {
        // These are all sub entries for the commit we are currently processing.
        // We start with parent entries, and then apply delta changes on top.
        let mut cur_sub_entries: BTreeMap<MPathElement, (Option<Ctx>, Entry<_, _>)> =
            BTreeMap::new();

        // `stack_sub_entries` is a mapping from (name -> list of changes).
        // We want to pivot it into (Changeset id -> Map(name, entry)),
        // so that we know how tree directory changed
        let mut delta_sub_entries: BTreeMap<ChangesetId, BTreeMap<_, _>> = BTreeMap::new();

        for (path_elem, entry_stack) in stack_sub_entries {
            let EntryStack { values, parent } = entry_stack;
            if let Some(parent) = parent {
                cur_sub_entries.insert(path_elem.clone(), (None, parent));
            }

            for (cs_id, ctx, maybe_entry) in values {
                delta_sub_entries
                    .entry(cs_id)
                    .or_default()
                    .insert(path_elem.clone(), (ctx, maybe_entry));
            }
        }

        // Before we start with the actual logic of creating trees we have one corner case
        // to deal with. A file can be replaced with a directory in a single commit. In that
        // case we have a maybe_file_deletion set to the the commit where the file was deleted,
        // and a parent should either be non-existent or a leaf. A case like that can only be
        // the first in the stack of changes this tree because we use a simple stack.
        // So let's check these two things if file deletion is set:
        // 1) Check that parent is a leaf or non-existent
        // 2) Check that deletion is the first change
        match (&parent, maybe_file_deletion) {
            (Some(Entry::Leaf(_)), Some(deletion_cs_id)) | (None, Some(deletion_cs_id)) => {
                for cs_id in stack_of_commits.iter() {
                    if cs_id == &deletion_cs_id {
                        // File is deleted in this commit, and no previous commits had subentries.
                        // That's good, we can exit
                        break;
                    }

                    // There are subentries before the deletion - we don't support this, so
                    // let's exit.
                    if delta_sub_entries.contains_key(cs_id) {
                        return Err(anyhow!(
                            "Unexpected file deletion of {:?} in {}",
                            path,
                            deletion_cs_id
                        ));
                    }
                }
            }
            (Some(Entry::Tree(_)), Some(deletion_cs_id)) => {
                // Something is odd here - parent is a tree but we try to delete it as a file
                return Err(anyhow!(
                    "Unexpected file deletion of {:?} in {}",
                    path,
                    deletion_cs_id
                ));
            }
            (Some(Entry::Leaf(_)), None) => {
                return Err(anyhow!("Unexpected file parent for a directory {:?}", path,));
            }
            (Some(Entry::Tree(_)), None) | (None, None) => {
                // Simple cases, nothing to do here
            }
        };

        let mut entry_stack = EntryStack {
            values: vec![],
            parent: parent.clone(),
        };
        let mut parent = parent.and_then(|e| e.into_tree());

        for cs_id in stack_of_commits.iter() {
            let delta = match delta_sub_entries.remove(cs_id) {
                Some(delta) => delta,
                None => {
                    // This directory hasn't been changed in `cs_id`, just continue...
                    if path.is_none() && cur_sub_entries.is_empty() && parent.is_none() {
                        // ... unless it's an empty root tree with no parents.
                        // That means we have an empty commit with no parents,
                        // and for that case let's create a new root object to match what
                        // derive_manifest() function is doing.
                        let (ctx, tree_id) = create_tree(
                            TreeInfo {
                                path: path.clone(),
                                parents: parent.clone().into_iter().collect(),
                                subentries: Default::default(),
                            },
                            *cs_id,
                        )
                        .await?;
                        parent = Some(tree_id.clone());
                        entry_stack
                            .values
                            .push((*cs_id, Some(ctx), Some(Entry::Tree(tree_id))));
                    }
                    continue;
                }
            };

            // Let's apply delta to get the subentries for a given commit
            for (path_elem, (ctx, maybe_entry)) in delta {
                match maybe_entry {
                    Some(entry) => {
                        cur_sub_entries.insert(path_elem, (ctx, entry));
                    }
                    None => {
                        cur_sub_entries.remove(&path_elem);
                    }
                }
            }

            // Finally let's create or delete the directory
            if !cur_sub_entries.is_empty() {
                let (ctx, tree_id) = create_tree(
                    TreeInfo {
                        path: path.clone(),
                        parents: parent.clone().into_iter().collect(),
                        subentries: cur_sub_entries.clone(),
                    },
                    *cs_id,
                )
                .await?;

                parent = Some(tree_id.clone());
                entry_stack
                    .values
                    .push((*cs_id, Some(ctx), Some(Entry::Tree(tree_id))));
            } else if path.is_none() {
                // Everything is deleted in the repo - let's create a new root
                // object
                let (ctx, tree_id) = create_tree(
                    TreeInfo {
                        path: path.clone(),
                        parents: parent.clone().into_iter().collect(),
                        subentries: Default::default(),
                    },
                    *cs_id,
                )
                .await?;
                parent = Some(tree_id.clone());
                entry_stack
                    .values
                    .push((*cs_id, Some(ctx), Some(Entry::Tree(tree_id))));
            } else {
                parent = None;
                entry_stack.values.push((*cs_id, None, None));
            }
        }

        Ok(entry_stack)
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::collections::HashSet;
use std::hash::Hash;

use anyhow::Error;
use blobstore::StoreLoadable;
use cloned::cloned;
use context::CoreContext;
use futures::future;
use futures::stream;
use futures::Stream;
use futures::StreamExt;
use futures::TryFutureExt;
use futures::TryStreamExt;
use mononoke_types::FileType;
use mononoke_types::MPath;

use crate::Entry;
use crate::Manifest;
use crate::ManifestOps;

/// Get implicit directory deletes
/// These may happen when a path, occupied by a dir in the parent manifest
/// becomes occupied by a file in the child. In this case all files in the
/// replaced directory are implicitly deleted.
/// For example, consider the following manifest:
/// ```ignore
/// p1/
///   p2/   <- a dir
///     p3  <- a file
///     p4  <- a file
/// ```
/// Now a child adds the following file: `/p1/p2`
/// New manifest looks like:
/// ```ignore
/// p1/
///   p2    <- f file
/// ```
/// Dir `/p1/p2` was implicitly delted (meaning files `/p1/p2/p3` and
/// `/p1/p2/p4` were implicitly delted)
fn get_implicit_dir_deletes<ManifestId, FileId, Store>(
    ctx: CoreContext,
    store: Store,
    path_added_in_a_child: MPath,
    parent: ManifestId,
) -> impl Stream<Item = Result<MPath, Error>>
where
    FileId: Hash + Eq + Send + Sync + Unpin + 'static,
    ManifestId: Hash + Eq + StoreLoadable<Store> + Send + Sync + ManifestOps<Store> + 'static,
    Store: Send + Sync + Clone + 'static,
    <ManifestId as StoreLoadable<Store>>::Value:
        Manifest<TreeId = ManifestId, LeafId = (FileType, FileId)> + Send + Sync,
    <<ManifestId as StoreLoadable<Store>>::Value as Manifest>::LeafId: Send + Copy + Eq,
{
    parent
        .find_entry(
            ctx.clone(),
            store.clone(),
            Some(path_added_in_a_child.clone()),
        )
        .map_ok({
            cloned!(ctx, store, path_added_in_a_child);
            move |maybe_entry| {
                match maybe_entry {
                    // No such path in parent or such path used to be a leaf.
                    // No implicit delete in both cases
                    None | Some(Entry::Leaf(_)) => stream::empty().left_stream(),
                    Some(Entry::Tree(tree)) => tree
                        .list_leaf_entries(ctx, store)
                        .map_ok({
                            cloned!(path_added_in_a_child);
                            move |(relative_path, _)| path_added_in_a_child.join(&relative_path)
                        })
                        .right_stream(),
                }
            }
        })
        .try_flatten_stream()
}

/// Get implicit deletes from a single parent manifest,
/// caused by introducing new paths into a child
fn get_implicit_deletes_single_parent<ManifestId, FileId, Store, I>(
    ctx: CoreContext,
    store: Store,
    paths_added_in_a_child: I,
    parent: ManifestId,
) -> impl Stream<Item = Result<MPath, Error>>
where
    FileId: Hash + Eq + Send + Sync + Unpin + 'static,
    ManifestId: Hash + Eq + StoreLoadable<Store> + Send + Sync + ManifestOps<Store> + 'static,
    Store: Send + Sync + Clone + 'static,
    <ManifestId as StoreLoadable<Store>>::Value:
        Manifest<TreeId = ManifestId, LeafId = (FileType, FileId)> + Send + Sync,
    <<ManifestId as StoreLoadable<Store>>::Value as Manifest>::LeafId: Send + Copy + Eq,
    I: IntoIterator<Item = MPath>,
{
    let individual_path_streams = paths_added_in_a_child.into_iter().map({
        cloned!(ctx, store, parent);
        move |path_added_in_a_child| {
            get_implicit_dir_deletes(
                ctx.clone(),
                store.clone(),
                path_added_in_a_child,
                parent.clone(),
            )
        }
    });
    stream::iter(individual_path_streams).flatten()
}

/// Get implicit deletes in parent manifests,
/// caused by introducing new paths into a child
pub fn get_implicit_deletes<ManifestId, FileId, Store, I, M>(
    ctx: &CoreContext,
    store: Store,
    paths_added_in_a_child: I,
    parents: M,
) -> impl Stream<Item = Result<MPath, Error>>
where
    FileId: Hash + Eq + Send + Sync + Unpin + 'static,
    ManifestId: Hash + Eq + StoreLoadable<Store> + Send + Sync + ManifestOps<Store> + 'static,
    Store: Send + Sync + Clone + 'static,
    <ManifestId as StoreLoadable<Store>>::Value:
        Manifest<TreeId = ManifestId, LeafId = (FileType, FileId)> + Send + Sync,
    <<ManifestId as StoreLoadable<Store>>::Value as Manifest>::LeafId: Send + Copy + Eq,
    I: IntoIterator<Item = MPath> + Clone,
    M: IntoIterator<Item = ManifestId>,
{
    let individual_parent_streams = parents.into_iter().map({
        cloned!(ctx, store, paths_added_in_a_child);
        move |parent| {
            get_implicit_deletes_single_parent(
                ctx.clone(),
                store.clone(),
                paths_added_in_a_child.clone(),
                parent,
            )
        }
    });
    // Note that identical implicit deletes may exist against
    // multiple parents, so to ensure uniqueness, we must allocate
    // some additional memory
    stream::iter(individual_parent_streams)
        .flatten()
        .try_filter({
            let mut seen = HashSet::new();
            move |item| future::ready(seen.insert(item.clone()))
        })
}

#[cfg(test)]
mod test {
    use std::fmt::Debug;

    use fbinit::FacebookInit;
    use maplit::hashmap;

    use super::*;
    use crate::tests::ctx;
    use crate::tests::dir;
    use crate::tests::element;
    use crate::tests::file;
    use crate::tests::path;
    use crate::tests::ManifestStore;
    use crate::tests::TestManifestIdStr;
    use crate::tests::TestManifestStr;

    fn ensure_unordered_eq<T: Debug + Hash + PartialEq + Eq, I: IntoIterator<Item = T>>(
        v1: I,
        v2: I,
    ) {
        let hs1: HashSet<_> = v1.into_iter().collect();
        let hs2: HashSet<_> = v2.into_iter().collect();
        assert_eq!(hs1, hs2);
    }

    #[fbinit::test]
    async fn test_implicit_dir_deletes(fb: FacebookInit) -> Result<(), Error> {
        // Parent manifest looks like:
        // p1/
        //   p2/
        //     p3
        //     p4
        //   p5/
        //     p6
        let root_manifest = TestManifestIdStr("1");
        let store = ManifestStore(hashmap! {
            root_manifest => TestManifestStr(hashmap! {
                element("p1") => dir("2"),
            }),
            TestManifestIdStr("2") => TestManifestStr(hashmap! {
                element("p2") => dir("3"),
                element("p5") => dir("5"),
            }),
            TestManifestIdStr("3") => TestManifestStr(hashmap! {
                element("p3") => file(FileType::Regular, "3"),
                element("p4") => file(FileType::Regular, "4"),
            }),
            TestManifestIdStr("5") => TestManifestStr(hashmap! {
                element("p6") => file(FileType::Regular, "6"),
            }),
        });
        // Child adds a file at /p1/p2
        let implicitly_deleted_files: Vec<_> =
            get_implicit_dir_deletes(ctx(fb), store.clone(), path("p1/p2"), root_manifest)
                .try_collect()
                .await?;
        // We expect the entire /p1/p2 dir to be implicitly deleted
        // (and we enumerate all files: /p1/p2/p3 and /p1/p2/p4)
        ensure_unordered_eq(
            implicitly_deleted_files,
            vec![path("p1/p2/p3"), path("p1/p2/p4")],
        );

        // Adding an unrelated file should not cause any implicit deletes
        let implicitly_deleted_files: Vec<_> =
            get_implicit_dir_deletes(ctx(fb), store.clone(), path("p1/p200"), root_manifest)
                .try_collect()
                .await?;
        assert_eq!(implicitly_deleted_files, vec![]);

        Ok(())
    }

    #[fbinit::test]
    async fn test_implicit_deletes(fb: FacebookInit) -> Result<(), Error> {
        // Parent 1 manifest looks like:
        // p1/
        //   p2/
        //     p3
        //     p4
        //   p5/
        //     p6
        //   p7
        // p8
        // Parent 2 manifest looks like:
        // p1/
        //   p2/
        //     p3
        //     p4
        //   p5/
        //     p6
        //   p9/
        //     p10
        // p11
        let root_manifest_1 = TestManifestIdStr("1_1");
        let root_manifest_2 = TestManifestIdStr("1_2");
        let store = ManifestStore(hashmap! {
            root_manifest_1 => TestManifestStr(hashmap! {
                element("p1") => dir("2_1"),
                element("p8") => file(FileType::Regular, "8_1"),
            }),
            TestManifestIdStr("2_1") => TestManifestStr(hashmap! {
                element("p2") => dir("3"),
                element("p5") => dir("5"),
                element("p7") => file(FileType::Regular, "7_1"),
            }),
            root_manifest_2 => TestManifestStr(hashmap! {
                element("p1") => dir("2_2"),
                element("p11") => file(FileType::Regular, "11_1"),
            }),
            TestManifestIdStr("2_2") => TestManifestStr(hashmap! {
                element("p2") => dir("3"),
                element("p5") => dir("5"),
                element("p9") => dir("9_2")
            }),
            TestManifestIdStr("9_2") => TestManifestStr(hashmap! {
                element("p10") => file(FileType::Executable, "10_2"),
            }),
            TestManifestIdStr("3") => TestManifestStr(hashmap! {
                element("p3") => file(FileType::Regular, "3"),
                element("p4") => file(FileType::Regular, "4"),
            }),
            TestManifestIdStr("5") => TestManifestStr(hashmap! {
                element("p6") => file(FileType::Regular, "6"),
            }),
        });
        let c = ctx(fb);
        // Child adds files at /p1/p2, /p1/p7/p12 and /p1/p9
        let implicitly_deleted_files: Vec<_> = get_implicit_deletes(
            &c,
            store.clone(),
            vec![path("p1/p2"), path("p1/p7/p12"), path("p1/p9")],
            vec![root_manifest_1, root_manifest_2],
        )
        .try_collect()
        .await?;

        ensure_unordered_eq(
            implicitly_deleted_files,
            vec![path("p1/p2/p3"), path("p1/p2/p4"), path("p1/p9/p10")],
        );

        // Result should not depend on the order of parents
        let implicitly_deleted_files: Vec<_> = get_implicit_deletes(
            &c,
            store,
            vec![path("p1/p2"), path("p1/p7/p12"), path("p1/p9")],
            vec![root_manifest_2, root_manifest_1],
        )
        .try_collect()
        .await?;

        ensure_unordered_eq(
            implicitly_deleted_files,
            vec![path("p1/p2/p3"), path("p1/p2/p4"), path("p1/p9/p10")],
        );
        Ok(())
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

pub use blobstore::StoreLoadable;
pub use derive_batch::derive_manifests_for_simple_stack_of_commits;
pub use derive_batch::ManifestChanges;

pub use crate::bonsai::bonsai_diff;
pub use crate::bonsai::BonsaiDiffFileChange;
pub use crate::derive::derive_manifest;
pub use crate::derive::derive_manifest_with_io_sender;
pub use crate::derive::LeafInfo;
pub use crate::derive::TreeInfo;
pub use crate::implicit_deletes::get_implicit_deletes;
pub use crate::ops::find_intersection_of_diffs;
pub use crate::ops::find_intersection_of_diffs_and_parents;
pub use crate::ops::Diff;
pub use crate::ops::ManifestOps;
pub use crate::ordered_ops::After;
pub use crate::ordered_ops::ManifestOrderedOps;
pub use crate::select::PathOrPrefix;
pub use crate::types::AsyncManifest;
pub use crate::types::AsyncOrderedManifest;
pub use crate::types::Entry;
pub use crate::types::Manifest;
pub use crate::types::OrderedManifest;
pub use crate::types::PathTree;
pub use crate::types::Traced;

mod bonsai;
mod derive;
mod derive_batch;
mod implicit_deletes;
mod ops;
mod ordered_ops;
mod select;
mod types;

#[cfg(test)]
mod tests;