/*
 * 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::HashMap;
use std::io::Write;
use std::sync::Arc;

use anyhow::format_err;
use anyhow::Error;
use blobrepo_errors::ErrorKind;
use blobstore::Blobstore;
use blobstore::Loadable;
use cloned::cloned;
use context::CoreContext;
use futures::channel::mpsc;
use futures::compat::Future01CompatExt;
use futures::future;
use futures::future::try_join_all;
use futures::future::BoxFuture;
use futures::future::FutureExt;
use futures::future::TryFutureExt;
use manifest::derive_manifest_with_io_sender;
use manifest::derive_manifests_for_simple_stack_of_commits;
use manifest::Entry;
use manifest::LeafInfo;
use manifest::ManifestChanges;
use manifest::Traced;
use manifest::TreeInfo;
use mercurial_types::blobs::ContentBlobMeta;
use mercurial_types::blobs::UploadHgFileContents;
use mercurial_types::blobs::UploadHgFileEntry;
use mercurial_types::blobs::UploadHgNodeHash;
use mercurial_types::blobs::UploadHgTreeEntry;
use mercurial_types::HgFileNodeId;
use mercurial_types::HgManifestId;
use mononoke_types::ChangesetId;
use mononoke_types::FileType;
use mononoke_types::MPath;
use mononoke_types::RepoPath;
use mononoke_types::TrackedFileChange;
use sorted_vector_map::SortedVectorMap;

use crate::derive_hg_changeset::store_file_change;

#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]
struct ParentIndex(usize);

pub async fn derive_simple_hg_manifest_stack_without_copy_info(
    ctx: CoreContext,
    blobstore: Arc<dyn Blobstore>,
    manifest_changes: Vec<ManifestChanges<TrackedFileChange>>,
    parent: Option<HgManifestId>,
) -> Result<HashMap<ChangesetId, HgManifestId>, Error> {
    let res = derive_manifests_for_simple_stack_of_commits(
        ctx.clone(),
        blobstore.clone(),
        parent.map(|p| Traced::assign(ParentIndex(0), p)),
        manifest_changes,
        {
            cloned!(blobstore, ctx);
            move |mut tree_info, _cs_id| {
                cloned!(blobstore, ctx);
                async move {
                    tree_info.parents = tree_info
                        .parents
                        .into_iter()
                        .map(|p| Traced::assign(ParentIndex(0), p.into_untraced()))
                        .collect();
                    create_hg_manifest(ctx.clone(), blobstore.clone(), None, tree_info).await
                }
            }
        },
        {
            cloned!(blobstore, ctx);
            move |leaf_info, _cs_id| {
                cloned!(blobstore, ctx);
                async move {
                    let LeafInfo {
                        leaf,
                        path,
                        parents,
                    } = leaf_info;

                    let parents: Vec<_> = parents
                        .into_iter()
                        .map(|p| Traced::assign(ParentIndex(0), p.into_untraced()))
                        .collect();
                    match leaf {
                        Some(leaf) => {
                            if leaf.copy_from().is_some() {
                                return Err(
                                    format_err!(
                                        "unsupported generation of stack of hg manifests: leaf {} has copy info {:?}",
                                        path,
                                        leaf.copy_from(),
                                    )
                                );
                            }
                            store_file_change(
                                ctx,
                                blobstore,
                                parents.get(0).map(|p| p.untraced().1),
                                None,
                                &path,
                                &leaf,
                                None, // copy_from should be empty
                            )
                            .map_ok(|res| ((), Traced::generate(res)))
                            .await
                        }
                        None => {
                            let (file_type, filenode) =
                                resolve_conflict(ctx, blobstore, path, &parents).await?;
                            Ok(((), Traced::generate((file_type, filenode))))
                        }
                    }
                }
            }
        },
    )
    .await?;

    Ok(res
        .into_iter()
        .map(|(key, value)| (key, value.into_untraced()))
        .collect())
}

/// Derive mercurial manifest from parent manifests and bonsai file changes.
pub async fn derive_hg_manifest(
    ctx: CoreContext,
    blobstore: Arc<dyn Blobstore>,
    parents: impl IntoIterator<Item = HgManifestId>,
    changes: impl IntoIterator<Item = (MPath, Option<(FileType, HgFileNodeId)>)> + 'static,
) -> Result<HgManifestId, Error> {
    let parents: Vec<_> = parents
        .into_iter()
        .enumerate()
        .map(|(i, m)| Traced::assign(ParentIndex(i), m))
        .collect();

    let tree_id = derive_manifest_with_io_sender(
        ctx.clone(),
        blobstore.clone(),
        parents.clone(),
        changes,
        {
            cloned!(ctx, blobstore);
            move |tree_info, sender| {
                create_hg_manifest(ctx.clone(), blobstore.clone(), Some(sender), tree_info)
            }
        },
        {
            cloned!(ctx, blobstore);
            move |leaf_info, _sender| create_hg_file(ctx.clone(), blobstore.clone(), leaf_info)
        },
    )
    .await?;

    match tree_id {
        Some(traced_tree_id) => Ok(traced_tree_id.into_untraced()),
        None => {
            // All files have been deleted, generate empty **root** manifest
            let tree_info = TreeInfo {
                path: None,
                parents,
                subentries: Default::default(),
            };
            let (_, traced_tree_id) = create_hg_manifest(ctx, blobstore, None, tree_info).await?;
            Ok(traced_tree_id.into_untraced())
        }
    }
}

/// This function is used as callback from `derive_manifest` to generate and store manifest
/// object from `TreeInfo`.
async fn create_hg_manifest(
    ctx: CoreContext,
    blobstore: Arc<dyn Blobstore>,
    sender: Option<mpsc::UnboundedSender<BoxFuture<'static, Result<(), Error>>>>,
    tree_info: TreeInfo<
        Traced<ParentIndex, HgManifestId>,
        Traced<ParentIndex, (FileType, HgFileNodeId)>,
        (),
    >,
) -> Result<((), Traced<ParentIndex, HgManifestId>), Error> {
    let TreeInfo {
        subentries,
        path,
        parents,
    } = tree_info;

    // See if any of the parents have the same hg manifest. If yes, then we can just reuse it
    // without recreating manifest again.
    // But note that we reuse only if manifest has more than on parent, and there are a few reasons for
    // it:
    // 1) If a commit have a single parent then create_hg_manifest function shouldn't normally be called -
    //    it can only happen if a file hasn't changed, but nevertheless there's an entry for this file
    //    in the bonsai. This should happen rarely, and recreating manifest in these cases shouldn't be
    //    a problem.
    // 2) It adds an additional read of parent manifests, and it can potentially be expensive if manifests
    //    are large.
    //    We'd rather not do it if we don't need to, and it seems that we don't really need to (see point 1)
    if parents.len() > 1 {
        let mut subentries_vec_map = BTreeMap::new();
        for (name, (_context, subentry)) in &subentries {
            let subentry = match subentry {
                Entry::Tree(manifest_id) => Entry::Tree(*manifest_id.untraced()),
                Entry::Leaf(leaf) => Entry::Leaf(*leaf.untraced()),
            };
            subentries_vec_map.insert(name.clone(), subentry);
        }

        let subentries_vec_map = SortedVectorMap::from(subentries_vec_map);

        let (p1_parent, p2_parent) = hg_parents(&parents);
        let loaded_parents = {
            let ctx = &ctx;
            let blobstore = &blobstore;

            future::try_join_all(p1_parent.into_iter().chain(p2_parent).map(|id| async move {
                let mf = id.load(ctx, blobstore).map_err(Error::from).await?;
                Result::<_, Error>::Ok((id, mf))
            }))
            .await?
        };

        if let Some((reuse_id, _)) = loaded_parents
            .into_iter()
            .find(|(_, p)| p.content().files == subentries_vec_map)
        {
            return Ok(((), Traced::generate(reuse_id)));
        }
    }

    let mut contents = Vec::new();
    for (name, (_context, subentry)) in subentries {
        contents.extend(name.as_ref());
        let subentry: Entry<_, _> = subentry.into();
        let (tag, hash) = match subentry {
            Entry::Tree(manifest_id) => ("t", manifest_id.into_nodehash()),
            Entry::Leaf((file_type, filenode_id)) => {
                let tag = match file_type {
                    FileType::Symlink => "l",
                    FileType::Executable => "x",
                    FileType::Regular => "",
                };
                (tag, filenode_id.into_nodehash())
            }
        };
        write!(&mut contents, "\0{}{}\n", hash, tag).expect("write to memory failed");
    }

    let path = match path {
        None => RepoPath::RootPath,
        Some(path) => RepoPath::DirectoryPath(path),
    };

    let (p1, p2) = hg_parents(&parents);

    let p1 = p1.map(|id| id.into_nodehash());
    let p2 = p2.map(|id| id.into_nodehash());

    let uploader = UploadHgTreeEntry {
        upload_node_id: UploadHgNodeHash::Generate,
        contents: contents.into(),
        p1,
        p2,
        path,
    }
    .upload(ctx, blobstore);

    let (mfid, upload_fut) = match uploader {
        Ok((mfid, fut)) => (mfid, fut.compat().map_ok(|_| ())),
        Err(e) => return Err(e),
    };

    match sender {
        Some(sender) => {
            sender
                .unbounded_send(upload_fut.boxed())
                .map_err(|err| format_err!("failed to send hg manifest future {}", err))?;
        }
        None => upload_fut.await?,
    }
    Ok(((), Traced::generate(mfid)))
}

/// This function is used as callback from `derive_manifest` to generate and store file entry
/// object from `LeafInfo`.
async fn create_hg_file(
    ctx: CoreContext,
    blobstore: Arc<dyn Blobstore>,
    leaf_info: LeafInfo<Traced<ParentIndex, (FileType, HgFileNodeId)>, (FileType, HgFileNodeId)>,
) -> Result<((), Traced<ParentIndex, (FileType, HgFileNodeId)>), Error> {
    let LeafInfo {
        leaf,
        path,
        parents,
    } = leaf_info;

    // TODO: move `Blobrepo::store_file_changes` logic in here
    match leaf {
        Some(leaf) => Ok(((), Traced::generate(leaf))),
        None => {
            // Leaf was not provided, try to resolve same-content different parents leaf. Since filenode
            // hashes include ancestry, this can be necessary if two identical files were created through
            // different paths in history.
            let (file_type, filenode) = resolve_conflict(ctx, blobstore, path, &parents).await?;
            Ok(((), Traced::generate((file_type, filenode))))
        }
    }
}

async fn resolve_conflict(
    ctx: CoreContext,
    blobstore: Arc<dyn Blobstore>,
    path: MPath,
    parents: &[Traced<ParentIndex, (FileType, HgFileNodeId)>],
) -> Result<(FileType, HgFileNodeId), Error> {
    let make_err = || {
        ErrorKind::UnresolvedConflicts(
            path.clone(),
            parents.iter().map(|p| *p.untraced()).collect::<Vec<_>>(),
        )
    };

    // First, if the file type is different across entries, we need to bail. This is a conflict.
    let file_type =
        unique_or_nothing(parents.iter().map(|p| p.untraced().0)).ok_or_else(make_err)?;

    // Assuming the file type is the same, then let's check that the contents are identical. To do
    // so, we'll load the envelopes.
    let envelopes = parents
        .iter()
        .map(|p| p.untraced().1.load(&ctx, &blobstore));

    let envelopes = try_join_all(envelopes).await?;

    let (content_id, content_size) =
        unique_or_nothing(envelopes.iter().map(|e| (e.content_id(), e.content_size())))
            .ok_or_else(make_err)?;

    // If we got here, then that means the file type and content is the same everywhere. In this
    // case, let's reuse a filenode.
    let (maybe_reuse_filenode, _) = hg_parents(parents);
    match maybe_reuse_filenode {
        Some((_ft, id)) => Ok((file_type, id)),
        None => {
            // This can only happen in the case of an octopus merge where neither p1 nor p2
            // contained this content. It would be nice if we could reuse p3 or later,
            // but Mercurial could be confused by a filenode whose linknode is not a Mercurial
            // ancestor of the commit. So don't risk it.
            let contents = ContentBlobMeta {
                id: content_id,
                size: content_size,
                copy_from: None,
            };
            let (filenode_id, _) = UploadHgFileEntry {
                upload_node_id: UploadHgNodeHash::Generate,
                contents: UploadHgFileContents::ContentUploaded(contents),
                p1: None,
                p2: None,
            }
            .upload_with_path(ctx, blobstore, path)
            .await?;
            Ok((file_type, filenode_id))
        }
    }
}

/// Extract hg-relevant parents from a set of Traced entries. This means we ignore any parents
/// except for p1 and p2.
fn hg_parents<T: Copy>(parents: &[Traced<ParentIndex, T>]) -> (Option<T>, Option<T>) {
    let mut parents = parents.iter().filter_map(|t| match t.id() {
        Some(ParentIndex(0)) | Some(ParentIndex(1)) => Some(t.untraced()),
        Some(_) | None => None,
    });

    (parents.next().copied(), parents.next().copied())
}

/// Take an iterator, if it has just one value, return it. Otherwise, return None.
fn unique_or_nothing<T: PartialEq>(iter: impl Iterator<Item = T>) -> Option<T> {
    let mut ret = None;

    for e in iter {
        if ret.is_none() {
            ret = Some(e);
            continue;
        }

        if ret.as_ref().expect("We just checked") == &e {
            continue;
        }

        return None;
    }

    ret
}


/*
 * 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 mod derive_hg_changeset;
pub mod derive_hg_manifest;
mod mapping;

pub use derive_hg_changeset::derive_hg_changeset;
pub use derive_hg_changeset::get_manifest_from_bonsai;
pub use derive_hg_changeset::DeriveHgChangeset;
pub use derive_hg_manifest::derive_hg_manifest;
pub use mapping::MappedHgChangesetId;


/*
 * 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 anyhow::anyhow;
use anyhow::bail;
use anyhow::Context;
use anyhow::Error;
use anyhow::Result;
use async_trait::async_trait;
use bonsai_hg_mapping::BonsaiHgMappingEntry;
use context::CoreContext;
use derived_data::batch::split_bonsais_in_linear_stacks;
use derived_data::batch::FileConflicts;
use derived_data::batch::SplitOptions;
use derived_data::batch::DEFAULT_STACK_FILE_CHANGES_LIMIT;
use derived_data::impl_bonsai_derived_via_manager;
use derived_data_manager::dependencies;
use derived_data_manager::BonsaiDerivable;
use derived_data_manager::DerivableType;
use derived_data_manager::DerivationContext;
use futures::future::try_join_all;
use mercurial_types::HgChangesetId;
use mononoke_types::BonsaiChangeset;
use mononoke_types::ChangesetId;
use slog::debug;
use stats::prelude::*;

define_stats! {
    prefix = "mononoke.derived_data.hgchangesets";
    new_parallel: timeseries(Rate, Sum),
}

use derived_data_service_if::types as thrift;

#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct MappedHgChangesetId(HgChangesetId);

impl MappedHgChangesetId {
    pub(crate) fn new(hg_changeset_id: HgChangesetId) -> Self {
        MappedHgChangesetId(hg_changeset_id)
    }

    pub fn hg_changeset_id(&self) -> HgChangesetId {
        self.0
    }
}

#[derive(Debug, Clone)]
pub struct HgChangesetDeriveOptions {
    pub set_committer_field: bool,
}

#[async_trait]
impl BonsaiDerivable for MappedHgChangesetId {
    const VARIANT: DerivableType = DerivableType::HgChangesets;

    type Dependencies = dependencies![];

    async fn derive_single(
        ctx: &CoreContext,
        derivation_ctx: &DerivationContext,
        bonsai: BonsaiChangeset,
        parents: Vec<Self>,
    ) -> Result<Self, Error> {
        if bonsai.is_snapshot() {
            bail!("Can't derive Hg changeset for snapshot")
        }
        let derivation_opts = get_hg_changeset_derivation_options(derivation_ctx);
        crate::derive_hg_changeset::derive_from_parents(
            ctx,
            derivation_ctx.blobstore(),
            bonsai,
            parents,
            &derivation_opts,
        )
        .await
    }

    async fn derive_batch(
        ctx: &CoreContext,
        derivation_ctx: &DerivationContext,
        bonsais: Vec<BonsaiChangeset>,
        _gap_size: Option<usize>,
    ) -> Result<HashMap<ChangesetId, Self>> {
        if bonsais.is_empty() {
            return Ok(HashMap::new());
        }

        STATS::new_parallel.add_value(1);
        let linear_stacks = split_bonsais_in_linear_stacks(
            &bonsais,
            SplitOptions {
                file_conflicts: FileConflicts::ChangeDelete,
                copy_info: true,
                file_changes_limit: DEFAULT_STACK_FILE_CHANGES_LIMIT,
            },
        )?;
        let mut res: HashMap<ChangesetId, Self> = HashMap::new();
        let batch_len = bonsais.len();

        let derivation_opts = get_hg_changeset_derivation_options(derivation_ctx);

        let mut bonsais = bonsais;
        for stack in linear_stacks {
            let derived_parents = try_join_all(
                stack
                    .parents
                    .into_iter()
                    .map(|p| derivation_ctx.fetch_unknown_dependency::<Self>(ctx, Some(&res), p)),
            )
            .await?;
            if let Some(item) = stack.stack_items.first() {
                debug!(
                    ctx.logger(),
                    "derive hgchangeset batch at {} (stack of {} from batch of {})",
                    item.cs_id.to_hex(),
                    stack.stack_items.len(),
                    batch_len,
                );
            }

            // after the line below `bonsais` will contain all the bonsais that we are
            // going to derive now, and `left_bonsais` will contain all the bonsais that
            // we are going to derive in the next step
            let left_bonsais = bonsais.split_off(stack.stack_items.len());
            if derived_parents.len() > 1 || bonsais.len() == 1 {
                // we can't derive stack for a merge commit or for a commit that contains renames,
                // so let's derive it without batching
                for bonsai in bonsais {
                    let parents = derivation_ctx
                        .fetch_unknown_parents(ctx, Some(&res), &bonsai)
                        .await?;
                    let cs_id = bonsai.get_changeset_id();
                    let derived = Self::derive_single(ctx, derivation_ctx, bonsai, parents).await?;
                    res.insert(cs_id, derived);
                }
            } else {
                let first = stack.stack_items.first().map(|item| item.cs_id);
                let last = stack.stack_items.last().map(|item| item.cs_id);
                let derived =
                    crate::derive_hg_changeset::derive_simple_hg_changeset_stack_without_copy_info(
                        ctx,
                        derivation_ctx.blobstore(),
                        bonsais,
                        derived_parents.get(0).cloned(),
                        &derivation_opts,
                    )
                    .await
                    .with_context(|| {
                        format!("failed deriving stack of {:?} to {:?}", first, last,)
                    })?;

                res.extend(derived.into_iter().map(|(csid, hg_cs_id)| (csid, hg_cs_id)));
            }
            bonsais = left_bonsais;
        }

        Ok(res)
    }

    async fn store_mapping(
        self,
        ctx: &CoreContext,
        derivation_ctx: &DerivationContext,
        changeset_id: ChangesetId,
    ) -> Result<()> {
        derivation_ctx
            .bonsai_hg_mapping()?
            .add(
                ctx,
                BonsaiHgMappingEntry {
                    hg_cs_id: self.0,
                    bcs_id: changeset_id,
                },
            )
            .await?;
        Ok(())
    }

    async fn fetch(
        ctx: &CoreContext,
        derivation_ctx: &DerivationContext,
        changeset_id: ChangesetId,
    ) -> Result<Option<Self>> {
        Ok(Self::fetch_batch(ctx, derivation_ctx, &[changeset_id])
            .await?
            .into_iter()
            .next()
            .map(|(_, hg_id)| hg_id))
    }

    async fn fetch_batch(
        ctx: &CoreContext,
        derivation_ctx: &DerivationContext,
        changeset_ids: &[ChangesetId],
    ) -> Result<HashMap<ChangesetId, Self>> {
        Ok(derivation_ctx
            .bonsai_hg_mapping()?
            .get(ctx, changeset_ids.to_vec().into())
            .await?
            .into_iter()
            .map(|entry| (entry.bcs_id, MappedHgChangesetId(entry.hg_cs_id)))
            .collect())
    }

    fn from_thrift(data: thrift::DerivedData) -> Result<Self> {
        if let thrift::DerivedData::hg_changeset(
            thrift::DerivedDataHgChangeset::mapped_hgchangeset_id(id),
        ) = data
        {
            HgChangesetId::from_thrift(id).map(Self)
        } else {
            Err(anyhow!(
                "Can't convert {} from provided thrift::DerivedData",
                Self::NAME.to_string(),
            ))
        }
    }

    fn into_thrift(data: Self) -> Result<thrift::DerivedData> {
        Ok(thrift::DerivedData::hg_changeset(
            thrift::DerivedDataHgChangeset::mapped_hgchangeset_id(data.0.into_thrift()),
        ))
    }
}

fn get_hg_changeset_derivation_options(
    derivation_ctx: &DerivationContext,
) -> HgChangesetDeriveOptions {
    HgChangesetDeriveOptions {
        set_committer_field: derivation_ctx.config().hg_set_committer_extra,
    }
}

impl_bonsai_derived_via_manager!(MappedHgChangesetId);

#[cfg(test)]
mod test {
    use bonsai_hg_mapping::BonsaiHgMapping;
    use bookmarks::BookmarkKey;
    use bookmarks::Bookmarks;
    use borrowed::borrowed;
    use changeset_fetcher::ChangesetFetcher;
    use changesets::Changesets;
    use cloned::cloned;
    use derived_data_manager::BatchDeriveOptions;
    use fbinit::FacebookInit;
    use filestore::FilestoreConfig;
    use fixtures::BranchEven;
    use fixtures::BranchUneven;
    use fixtures::BranchWide;
    use fixtures::Linear;
    use fixtures::ManyDiamonds;
    use fixtures::ManyFilesDirs;
    use fixtures::MergeEven;
    use fixtures::MergeUneven;
    use fixtures::TestRepoFixture;
    use fixtures::UnsharedMergeEven;
    use fixtures::UnsharedMergeUneven;
    use futures::compat::Stream01CompatExt;
    use futures::Future;
    use futures::Stream;
    use futures::TryFutureExt;
    use futures::TryStreamExt;
    use repo_blobstore::RepoBlobstore;
    use repo_derived_data::RepoDerivedData;
    use repo_derived_data::RepoDerivedDataRef;
    use repo_identity::RepoIdentity;
    use revset::AncestorsNodeStream;
    use tests_utils::CreateCommitContext;

    use super::*;
    use crate::DeriveHgChangeset;

    #[derive(Clone)]
    #[facet::container]
    struct TestRepo {
        #[facet]
        bonsai_hg_mapping: dyn BonsaiHgMapping,
        #[facet]
        bookmarks: dyn Bookmarks,
        #[facet]
        repo_blobstore: RepoBlobstore,
        #[facet]
        repo_derived_data: RepoDerivedData,
        #[facet]
        filestore_config: FilestoreConfig,
        #[facet]
        changeset_fetcher: dyn ChangesetFetcher,
        #[facet]
        changesets: dyn Changesets,
        #[facet]
        repo_identity: RepoIdentity,
    }

    fn all_commits_descendants_to_ancestors(
        ctx: CoreContext,
        repo: TestRepo,
    ) -> impl Stream<Item = Result<(ChangesetId, HgChangesetId), Error>> {
        let master_book = BookmarkKey::new("master").unwrap();
        repo.bookmarks
            .get(ctx.clone(), &master_book)
            .map_ok(move |maybe_bcs_id| {
                let bcs_id = maybe_bcs_id.unwrap();
                AncestorsNodeStream::new(ctx.clone(), &repo.changeset_fetcher, bcs_id.clone())
                    .compat()
                    .and_then(move |new_bcs_id| {
                        cloned!(ctx, repo);
                        async move {
                            let hg_cs_id = repo.derive_hg_changeset(&ctx, new_bcs_id).await?;
                            Result::<_, Error>::Ok((new_bcs_id, hg_cs_id))
                        }
                    })
            })
            .try_flatten_stream()
    }

    async fn verify_repo<F, Fut>(fb: FacebookInit, repo_func: F) -> Result<(), Error>
    where
        F: Fn() -> Fut,
        Fut: Future<Output = TestRepo>,
    {
        let ctx = CoreContext::test_mock(fb);
        let repo: TestRepo = repo_func().await;
        println!("Processing {}", repo.repo_identity.name());
        borrowed!(ctx, repo);

        let commits_desc_to_anc = all_commits_descendants_to_ancestors(ctx.clone(), repo.clone())
            .try_collect::<Vec<_>>()
            .await?;

        // Recreate repo from scratch and derive everything again
        let repo = repo_func().await;
        let options = BatchDeriveOptions::Parallel { gap_size: None };
        let csids = commits_desc_to_anc
            .clone()
            .into_iter()
            .rev()
            .map(|(cs_id, _)| cs_id)
            .collect::<Vec<_>>();
        let manager = repo.repo_derived_data().manager();

        manager
            .derive_exactly_batch::<MappedHgChangesetId>(ctx, csids.clone(), options, None)
            .await?;
        let batch_derived = manager
            .fetch_derived_batch::<MappedHgChangesetId>(ctx, csids, None)
            .await?;

        for (cs_id, hg_cs_id) in commits_desc_to_anc.into_iter().rev() {
            println!("{} {} {:?}", cs_id, hg_cs_id, batch_derived.get(&cs_id));
            assert_eq!(batch_derived.get(&cs_id).map(|x| x.0), Some(hg_cs_id));
        }

        Ok(())
    }

    #[fbinit::test]
    async fn test_batch_derive(fb: FacebookInit) -> Result<(), Error> {
        verify_repo(fb, || Linear::get_custom_test_repo::<TestRepo>(fb)).await?;
        verify_repo(fb, || BranchEven::get_custom_test_repo::<TestRepo>(fb)).await?;
        verify_repo(fb, || BranchUneven::get_custom_test_repo::<TestRepo>(fb)).await?;
        verify_repo(fb, || BranchWide::get_custom_test_repo::<TestRepo>(fb)).await?;
        verify_repo(fb, || ManyDiamonds::get_custom_test_repo::<TestRepo>(fb)).await?;
        verify_repo(fb, || ManyFilesDirs::get_custom_test_repo::<TestRepo>(fb)).await?;
        verify_repo(fb, || MergeEven::get_custom_test_repo::<TestRepo>(fb)).await?;
        verify_repo(fb, || MergeUneven::get_custom_test_repo::<TestRepo>(fb)).await?;
        verify_repo(fb, || {
            UnsharedMergeEven::get_custom_test_repo::<TestRepo>(fb)
        })
        .await?;
        verify_repo(fb, || {
            UnsharedMergeUneven::get_custom_test_repo::<TestRepo>(fb)
        })
        .await?;
        // Create a repo with a few empty commits in a row
        verify_repo(fb, || async {
            let repo: TestRepo = test_repo_factory::build_empty(fb).await.unwrap();
            let ctx = CoreContext::test_mock(fb);
            let root_empty = CreateCommitContext::new_root(&ctx, &repo)
                .commit()
                .await
                .unwrap();
            let first_empty = CreateCommitContext::new(&ctx, &repo, vec![root_empty])
                .commit()
                .await
                .unwrap();
            let second_empty = CreateCommitContext::new(&ctx, &repo, vec![first_empty])
                .commit()
                .await
                .unwrap();
            let first_non_empty = CreateCommitContext::new(&ctx, &repo, vec![second_empty])
                .add_file("file", "a")
                .commit()
                .await
                .unwrap();
            let third_empty = CreateCommitContext::new(&ctx, &repo, vec![first_non_empty])
                .delete_file("file")
                .commit()
                .await
                .unwrap();
            let fourth_empty = CreateCommitContext::new(&ctx, &repo, vec![third_empty])
                .commit()
                .await
                .unwrap();
            let fifth_empty = CreateCommitContext::new(&ctx, &repo, vec![fourth_empty])
                .commit()
                .await
                .unwrap();

            tests_utils::bookmark(&ctx, &repo, "master")
                .set_to(fifth_empty)
                .await
                .unwrap();
            repo
        })
        .await?;

        verify_repo(fb, || async {
            let repo: TestRepo = test_repo_factory::build_empty(fb).await.unwrap();
            let ctx = CoreContext::test_mock(fb);
            let root = CreateCommitContext::new_root(&ctx, &repo)
                .add_file("dir/subdir/to_replace", "one")
                .add_file("dir/subdir/file", "content")
                .add_file("somefile", "somecontent")
                .commit()
                .await
                .unwrap();
            let modify_unrelated = CreateCommitContext::new(&ctx, &repo, vec![root])
                .add_file("dir/subdir/file", "content2")
                .delete_file("somefile")
                .commit()
                .await
                .unwrap();
            let replace_file_with_dir =
                CreateCommitContext::new(&ctx, &repo, vec![modify_unrelated])
                    .delete_file("dir/subdir/to_replace")
                    .add_file("dir/subdir/to_replace/file", "newcontent")
                    .commit()
                    .await
                    .unwrap();

            tests_utils::bookmark(&ctx, &repo, "master")
                .set_to(replace_file_with_dir)
                .await
                .unwrap();
            repo
        })
        .await?;

        // Weird case - let's delete a file that was already replaced with a directory
        verify_repo(fb, || async {
            let repo: TestRepo = test_repo_factory::build_empty(fb).await.unwrap();
            let ctx = CoreContext::test_mock(fb);
            let root = CreateCommitContext::new_root(&ctx, &repo)
                .add_file("dir/subdir/to_replace", "one")
                .commit()
                .await
                .unwrap();
            let replace_file_with_dir = CreateCommitContext::new(&ctx, &repo, vec![root])
                .delete_file("dir/subdir/to_replace")
                .add_file("dir/subdir/to_replace/file", "newcontent")
                .commit()
                .await
                .unwrap();
            let noop_delete = CreateCommitContext::new(&ctx, &repo, vec![replace_file_with_dir])
                .delete_file("dir/subdir/to_replace")
                .commit()
                .await
                .unwrap();
            let second_noop_delete = CreateCommitContext::new(&ctx, &repo, vec![noop_delete])
                .delete_file("dir/subdir/to_replace")
                .commit()
                .await
                .unwrap();

            tests_utils::bookmark(&ctx, &repo, "master")
                .set_to(second_noop_delete)
                .await
                .unwrap();
            repo
        })
        .await?;

        // Add renames
        verify_repo(fb, || async {
            let repo: TestRepo = test_repo_factory::build_empty(fb).await.unwrap();
            let ctx = CoreContext::test_mock(fb);
            let root = CreateCommitContext::new_root(&ctx, &repo)
                .add_file("dir", "one")
                .commit()
                .await
                .unwrap();
            let renamed = CreateCommitContext::new(&ctx, &repo, vec![root])
                .add_file_with_copy_info("copied_dir", "one", (root, "dir"))
                .commit()
                .await
                .unwrap();
            let after_rename = CreateCommitContext::new(&ctx, &repo, vec![renamed])
                .add_file("new_file", "file")
                .commit()
                .await
                .unwrap();

            tests_utils::bookmark(&ctx, &repo, "master")
                .set_to(after_rename)
                .await
                .unwrap();
            repo
        })
        .await?;

        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::str::FromStr;

use anyhow::anyhow;
use anyhow::Error;
use blobrepo::BlobRepo;
use blobrepo_hg::file_history::get_file_history;
use blobstore::Loadable;
use context::CoreContext;
use derived_data::BonsaiDerived;
use fbinit::FacebookInit;
use filenodes_derivation::FilenodesOnlyPublic;
use fixtures::Linear;
use fixtures::TestRepoFixture;
use manifest::ManifestOps;
use mercurial_types::HgChangesetId;
use mononoke_types::MPath;
use repo_blobstore::RepoBlobstoreRef;
use tests_utils::resolve_cs_id;

#[fbinit::test]
async fn test_linear_get_file_history(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let repo = Linear::getrepo(fb).await;

    let master_cs_id = resolve_cs_id(&ctx, &repo, "master").await?;
    FilenodesOnlyPublic::derive(&ctx, &repo, master_cs_id).await?;

    let expected_linknodes = vec![
        HgChangesetId::from_str("2d7d4ba9ce0a6ffd222de7785b249ead9c51c536")?,
        HgChangesetId::from_str("3e0e761030db6e479a7fb58b12881883f9f8c63f")?,
        HgChangesetId::from_str("607314ef579bd2407752361ba1b0c1729d08b281")?,
        HgChangesetId::from_str("d0a361e9022d226ae52f689667bd7d212a19cfe0")?,
        HgChangesetId::from_str("cb15ca4a43a59acff5388cea9648c162afde8372")?,
        HgChangesetId::from_str("eed3a8c0ec67b6a6fe2eb3543334df3f0b4f202b")?,
        HgChangesetId::from_str("0ed509bf086fadcb8a8a5384dc3b550729b0fc17")?,
        HgChangesetId::from_str("a9473beb2eb03ddb1cccc3fbaeb8a4820f9cd157")?,
        HgChangesetId::from_str("3c15267ebf11807f3d772eb891272b911ec68759")?,
        HgChangesetId::from_str("a5ffa77602a066db7d5cfb9fb5823a0895717c5a")?,
    ];
    let expected_linknodes = expected_linknodes.into_iter().rev().collect::<Vec<_>>();

    assert_linknodes(
        &ctx,
        &repo,
        expected_linknodes,
        HgChangesetId::from_str("79a13814c5ce7330173ec04d279bf95ab3f652fb")?,
        MPath::new("files")?,
        None,
    )
    .await?;

    let expected_linknodes = vec![
        HgChangesetId::from_str("a5ffa77602a066db7d5cfb9fb5823a0895717c5a")?,
        HgChangesetId::from_str("3c15267ebf11807f3d772eb891272b911ec68759")?,
    ];
    assert_linknodes(
        &ctx,
        &repo,
        expected_linknodes,
        HgChangesetId::from_str("79a13814c5ce7330173ec04d279bf95ab3f652fb")?,
        MPath::new("files")?,
        Some(2),
    )
    .await?;

    let expected_linknodes = vec![
        HgChangesetId::from_str("79a13814c5ce7330173ec04d279bf95ab3f652fb")?,
        HgChangesetId::from_str("a5ffa77602a066db7d5cfb9fb5823a0895717c5a")?,
    ];
    assert_linknodes(
        &ctx,
        &repo,
        expected_linknodes,
        HgChangesetId::from_str("79a13814c5ce7330173ec04d279bf95ab3f652fb")?,
        MPath::new("10")?,
        None,
    )
    .await?;
    Ok(())
}

async fn assert_linknodes(
    ctx: &CoreContext,
    repo: &BlobRepo,
    expected_linknodes: Vec<HgChangesetId>,
    start_from: HgChangesetId,
    path: MPath,
    max_length: Option<u64>,
) -> Result<(), Error> {
    let root_mf_id = start_from
        .load(ctx, &repo.repo_blobstore().clone())
        .await?
        .manifestid();
    let (_, files_hg_id) = root_mf_id
        .find_entry(
            ctx.clone(),
            repo.repo_blobstore().clone(),
            Some(path.clone()),
        )
        .await?
        .ok_or_else(|| anyhow!("entry not found"))?
        .into_leaf()
        .ok_or_else(|| anyhow!("expected leaf"))?;

    let history = get_file_history(ctx.clone(), repo.clone(), files_hg_id, path, max_length)
        .await?
        .do_not_handle_disabled_filenodes()?;

    let actual_linknodes = history
        .into_iter()
        .map(|entry| *entry.linknode())
        .collect::<Vec<_>>();

    assert_eq!(expected_linknodes, actual_linknodes);
    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.
 */

#![cfg_attr(not(fbcode_build), allow(unused_crate_dependencies))]
mod file_history_test;
mod tracing_blobstore;
mod utils;

use std::collections::BTreeMap;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;

use ::manifest::Entry;
use ::manifest::Manifest;
use ::manifest::ManifestOps;
use anyhow::Error;
use assert_matches::assert_matches;
use async_trait::async_trait;
use blobrepo::BlobRepo;
use blobrepo_errors::ErrorKind;
use blobrepo_hg::repo_commit::compute_changed_files;
use blobrepo_hg::repo_commit::UploadEntries;
use blobstore::Loadable;
use blobstore::Storable;
use bonsai_hg_mapping::BonsaiHgMappingRef;
use bookmarks::BookmarksRef;
use bytes::Bytes;
use cloned::cloned;
use context::CoreContext;
use fbinit::FacebookInit;
use fixtures::create_bonsai_changeset;
use fixtures::ManyFilesDirs;
use fixtures::MergeUneven;
use fixtures::TestRepoFixture;
use futures::future::BoxFuture;
use futures::future::FutureExt;
use futures::future::TryFutureExt;
use memblob::Memblob;
use mercurial_derivation::get_manifest_from_bonsai;
use mercurial_derivation::DeriveHgChangeset;
use mercurial_types::blobs::ContentBlobMeta;
use mercurial_types::blobs::File;
use mercurial_types::blobs::HgBlobChangeset;
use mercurial_types::blobs::HgBlobManifest;
use mercurial_types::blobs::UploadHgFileContents;
use mercurial_types::blobs::UploadHgFileEntry;
use mercurial_types::blobs::UploadHgNodeHash;
use mercurial_types::FileType;
use mercurial_types::HgChangesetId;
use mercurial_types::HgFileEnvelope;
use mercurial_types::HgFileNodeId;
use mercurial_types::HgManifestId;
use mercurial_types::HgNodeHash;
use mercurial_types::HgParents;
use mercurial_types::MPath;
use mercurial_types::RepoPath;
use mercurial_types_mocks::nodehash::ONES_FNID;
use mononoke_types::blob::BlobstoreValue;
use mononoke_types::bonsai_changeset::BonsaiChangesetMut;
use mononoke_types::BonsaiChangeset;
use mononoke_types::ChangesetId;
use mononoke_types::DateTime;
use mononoke_types::FileChange;
use mononoke_types::FileContents;
use repo_blobstore::RepoBlobstoreArc;
use repo_blobstore::RepoBlobstoreRef;
use scuba_ext::MononokeScubaSampleBuilder;
use test_repo_factory::TestRepoFactory;
use tests_utils::CreateCommitContext;
use tracing_blobstore::TracingBlobstore;
use utils::create_changeset_no_parents;
use utils::create_changeset_one_parent;
use utils::string_to_nodehash;
use utils::to_leaf;
use utils::to_mpath;
use utils::to_tree;
use utils::upload_file_no_parents;
use utils::upload_file_one_parent;
use utils::upload_manifest_no_parents;
use utils::upload_manifest_one_parent;

async fn get_content(
    ctx: &CoreContext,
    repo: &BlobRepo,
    id: HgFileNodeId,
) -> Result<bytes::Bytes, Error> {
    let content_id = id.load(ctx, repo.repo_blobstore()).await?.content_id();
    let content = filestore::fetch_concat(repo.repo_blobstore(), ctx, content_id);
    content.await
}

#[fbinit::test]
async fn upload_blob_no_parents(fb: FacebookInit) {
    let ctx = CoreContext::test_mock(fb);
    let repo: BlobRepo = test_repo_factory::build_empty(fb)
        .await
        .expect("Couldn't create repo");
    let expected_hash = HgFileNodeId::new(string_to_nodehash(
        "c3127cdbf2eae0f09653f9237d85c8436425b246",
    ));
    let fake_path = RepoPath::file("fake/file").expect("Can't generate fake RepoPath");

    // The blob does not exist...
    assert!(get_content(&ctx, &repo, expected_hash).await.is_err());

    // We upload it...
    let (fnid, future) = upload_file_no_parents(ctx.clone(), &repo, "blob", &fake_path);
    assert!(fnid == expected_hash);

    // The entry we're given is correct...
    let (fnid, path) = future.await.unwrap();
    assert!(path == fake_path);
    assert!(fnid == expected_hash);

    // And the blob now exists
    let bytes = get_content(&ctx, &repo, expected_hash).await.unwrap();
    assert!(bytes.as_ref() == &b"blob"[..]);
}

#[fbinit::test]
async fn upload_blob_one_parent(fb: FacebookInit) {
    let ctx = CoreContext::test_mock(fb);
    let repo: BlobRepo = test_repo_factory::build_empty(fb)
        .await
        .expect("Couldn't create repo");
    let expected_hash = HgFileNodeId::new(string_to_nodehash(
        "c2d60b35a8e7e034042a9467783bbdac88a0d219",
    ));
    let fake_path = RepoPath::file("fake/file").expect("Can't generate fake RepoPath");

    let (p1, future) = upload_file_no_parents(ctx.clone(), &repo, "blob", &fake_path);

    // The blob does not exist...
    let _ = get_content(&ctx, &repo, expected_hash).await.unwrap_err();

    // We upload it...
    let (hash, future2) = upload_file_one_parent(ctx.clone(), &repo, "blob", &fake_path, p1);
    assert!(hash == expected_hash);

    // The entry we're given is correct...
    let ((fnid, path), _) = futures::try_join!(future2, future).unwrap();
    assert!(path == fake_path);
    assert!(fnid == expected_hash);

    // And the blob now exists
    let bytes = get_content(&ctx, &repo, expected_hash).await.unwrap();
    assert!(bytes.as_ref() == &b"blob"[..]);
}

#[fbinit::test]
async fn create_one_changeset(fb: FacebookInit) {
    let ctx = CoreContext::test_mock(fb);
    let repo: BlobRepo = test_repo_factory::build_empty(fb)
        .await
        .expect("Couldn't create repo");
    let fake_file_path = RepoPath::file("dir/file").expect("Can't generate fake RepoPath");
    let fake_dir_path = RepoPath::dir("dir").expect("Can't generate fake RepoPath");
    let path = RepoPath::file("dir/file")
        .expect("Can't generate fake RepoPath")
        .mpath()
        .unwrap()
        .clone();
    let expected_files = vec![path];
    let author: String = "author <author@fb.com>".into();

    let (filehash, file_future) =
        upload_file_no_parents(ctx.clone(), &repo, "blob", &fake_file_path);

    let (dirhash, manifest_dir_future) = upload_manifest_no_parents(
        ctx.clone(),
        &repo,
        format!("file\0{}\n", filehash),
        &fake_dir_path,
    );

    let (root_mfid, root_manifest_future) = upload_manifest_no_parents(
        ctx.clone(),
        &repo,
        format!("dir\0{}t\n", dirhash),
        &RepoPath::root(),
    );

    let commit = create_changeset_no_parents(
        fb,
        &repo,
        root_manifest_future.map_ok(Some).boxed(),
        vec![to_leaf(file_future), to_tree(manifest_dir_future)],
    );

    let bonsai_hg = commit.get_completed_changeset().await.unwrap();
    let cs = &bonsai_hg.1;
    assert!(cs.manifestid() == root_mfid);
    assert!(cs.user() == author.as_bytes());
    assert!(cs.parents().get_nodes() == (None, None));
    let files: Vec<_> = cs.files().into();
    assert!(
        files == expected_files,
        "Got {:?}, expected {:?}",
        files,
        expected_files
    );

    // And check the file blob is present
    let bytes = get_content(&ctx, &repo, filehash).await.unwrap();
    assert!(bytes.as_ref() == &b"blob"[..]);
}

#[fbinit::test]
async fn create_two_changesets(fb: FacebookInit) {
    let ctx = CoreContext::test_mock(fb);
    let repo: BlobRepo = test_repo_factory::build_empty(fb)
        .await
        .expect("Couldn't create repo");
    let fake_file_path = RepoPath::file("dir/file").expect("Can't generate fake RepoPath");
    let fake_dir_path = RepoPath::dir("dir").expect("Can't generate fake RepoPath");
    let utf_author: String = "\u{041F}\u{0451}\u{0442}\u{0440} <peter@fb.com>".into();

    let (filehash, file_future) =
        upload_file_no_parents(ctx.clone(), &repo, "blob", &fake_file_path);

    let (dirhash, manifest_dir_future) = upload_manifest_no_parents(
        ctx.clone(),
        &repo,
        format!("file\0{}\n", filehash),
        &fake_dir_path,
    );

    let (roothash, root_manifest_future) = upload_manifest_no_parents(
        ctx.clone(),
        &repo,
        format!("dir\0{}t\n", dirhash),
        &RepoPath::root(),
    );

    let commit1 = create_changeset_no_parents(
        fb,
        &repo,
        root_manifest_future.map_ok(Some).boxed(),
        vec![to_leaf(file_future), to_tree(manifest_dir_future)],
    );

    let fake_file_path_no_dir = RepoPath::file("file").expect("Can't generate fake RepoPath");
    let (filehash, file_future) =
        upload_file_no_parents(ctx.clone(), &repo, "blob", &fake_file_path_no_dir);
    let (roothash, root_manifest_future) = upload_manifest_one_parent(
        ctx.clone(),
        &repo,
        format!("file\0{}\n", filehash),
        &RepoPath::root(),
        roothash,
    );

    let commit2 = create_changeset_one_parent(
        fb,
        &repo,
        root_manifest_future.map_ok(Some).boxed(),
        vec![to_leaf(file_future)],
        commit1.clone(),
    );

    let (commit1, commit2) = futures::try_join!(
        commit1.get_completed_changeset(),
        commit2.get_completed_changeset(),
    )
    .unwrap();

    let commit1 = &commit1.1;
    let commit2 = &commit2.1;
    assert!(commit2.manifestid() == roothash);
    assert!(commit2.user() == utf_author.as_bytes());
    let files: Vec<_> = commit2.files().into();
    let expected_files = vec![MPath::new("dir/file").unwrap(), MPath::new("file").unwrap()];
    assert!(
        files == expected_files,
        "Got {:?}, expected {:?}",
        files,
        expected_files
    );

    assert!(commit1.parents().get_nodes() == (None, None));
    let commit1_id = Some(commit1.get_changeset_id().into_nodehash());
    let expected_parents = (commit1_id, None);
    assert!(commit2.parents().get_nodes() == expected_parents);
}

#[fbinit::test]
async fn check_bonsai_creation(fb: FacebookInit) {
    let ctx = CoreContext::test_mock(fb);
    let repo: BlobRepo = test_repo_factory::build_empty(fb)
        .await
        .expect("Couldn't create repo");
    let fake_file_path = RepoPath::file("dir/file").expect("Can't generate fake RepoPath");
    let fake_dir_path = RepoPath::dir("dir").expect("Can't generate fake RepoPath");

    let (filehash, file_future) =
        upload_file_no_parents(ctx.clone(), &repo, "blob", &fake_file_path);

    let (dirhash, manifest_dir_future) = upload_manifest_no_parents(
        ctx.clone(),
        &repo,
        format!("file\0{}\n", filehash),
        &fake_dir_path,
    );

    let (_, root_manifest_future) = upload_manifest_no_parents(
        ctx.clone(),
        &repo,
        format!("dir\0{}t\n", dirhash),
        &RepoPath::root(),
    );

    let commit = create_changeset_no_parents(
        fb,
        &repo,
        root_manifest_future.map_ok(Some).boxed(),
        vec![to_leaf(file_future), to_tree(manifest_dir_future)],
    );

    let commit = commit.get_completed_changeset().await.unwrap();
    let commit = &commit.1;
    let bonsai_cs_id = repo
        .bonsai_hg_mapping()
        .get_bonsai_from_hg(&ctx, commit.get_changeset_id())
        .await
        .unwrap();
    assert!(bonsai_cs_id.is_some());
    let bonsai = bonsai_cs_id
        .unwrap()
        .load(&ctx, repo.repo_blobstore())
        .await
        .unwrap();
    assert_eq!(
        bonsai
            .file_changes()
            .map(|fc| format!("{}", fc.0))
            .collect::<Vec<_>>(),
        vec![String::from("dir/file")]
    );
}

#[fbinit::test]
async fn check_bonsai_creation_with_rename(fb: FacebookInit) {
    let ctx = CoreContext::test_mock(fb);
    let repo: BlobRepo = test_repo_factory::build_empty(fb)
        .await
        .expect("Couldn't create repo");
    let parent = {
        let fake_file_path = RepoPath::file("file").expect("Can't generate fake RepoPath");

        let (filehash, file_future) =
            upload_file_no_parents(ctx.clone(), &repo, "blob", &fake_file_path);

        let (_, root_manifest_future) = upload_manifest_no_parents(
            ctx.clone(),
            &repo,
            format!("file\0{}\n", filehash),
            &RepoPath::root(),
        );

        create_changeset_no_parents(
            fb,
            &repo,
            root_manifest_future.map_ok(Some).boxed(),
            vec![to_leaf(file_future)],
        )
    };

    let child = {
        let fake_renamed_file_path =
            RepoPath::file("file_rename").expect("Can't generate fake RepoPath");

        let (filehash, file_future) = upload_file_no_parents(
            ctx.clone(),
            &repo,
            "\x01\ncopy: file\ncopyrev: c3127cdbf2eae0f09653f9237d85c8436425b246\x01\nblob",
            &fake_renamed_file_path,
        );

        let (_, root_manifest_future) = upload_manifest_no_parents(
            ctx.clone(),
            &repo,
            format!("file_rename\0{}\n", filehash),
            &RepoPath::root(),
        );

        create_changeset_one_parent(
            fb,
            &repo,
            root_manifest_future.map_ok(Some).boxed(),
            vec![to_leaf(file_future)],
            parent.clone(),
        )
    };

    let parent_cs = parent.get_completed_changeset().await.unwrap();
    let parent_cs = &parent_cs.1;
    let child_cs = child.get_completed_changeset().await.unwrap();
    let child_cs = &child_cs.1;

    let parent_bonsai_cs_id = repo
        .bonsai_hg_mapping()
        .get_bonsai_from_hg(&ctx, parent_cs.get_changeset_id())
        .await
        .unwrap()
        .unwrap();

    let bonsai_cs_id = repo
        .bonsai_hg_mapping()
        .get_bonsai_from_hg(&ctx, child_cs.get_changeset_id())
        .await
        .unwrap();
    let bonsai = bonsai_cs_id
        .unwrap()
        .load(&ctx, repo.repo_blobstore())
        .await
        .unwrap();
    let fc = bonsai.file_changes().collect::<BTreeMap<_, _>>();
    let file = MPath::new("file").unwrap();
    assert!(fc[&file].is_removed());
    let file_rename = MPath::new("file_rename").unwrap();
    assert!(fc[&file_rename].is_changed());
    assert_eq!(
        match &fc[&file_rename] {
            FileChange::Change(tc) => tc.copy_from(),
            _ => panic!(),
        },
        Some(&(file, parent_bonsai_cs_id))
    );
}

#[fbinit::test]
async fn create_bad_changeset(fb: FacebookInit) {
    let ctx = CoreContext::test_mock(fb);
    let repo: BlobRepo = test_repo_factory::build_empty(fb)
        .await
        .expect("Couldn't create repo");
    let dirhash = string_to_nodehash("c2d60b35a8e7e034042a9467783bbdac88a0d219");

    let (_, root_manifest_future) = upload_manifest_no_parents(
        ctx,
        &repo,
        format!("dir\0{}t\n", dirhash),
        &RepoPath::root(),
    );

    let commit =
        create_changeset_no_parents(fb, &repo, root_manifest_future.map_ok(Some).boxed(), vec![]);

    commit
        .get_completed_changeset()
        .await
        .expect_err("This should fail");
}

#[fbinit::test]
async fn upload_entries_finalize_success(fb: FacebookInit) {
    let ctx = CoreContext::test_mock(fb);
    let repo: BlobRepo = test_repo_factory::build_empty(fb)
        .await
        .expect("Couldn't create repo");

    let fake_file_path = RepoPath::file("file").expect("Can't generate fake RepoPath");

    let (filehash, file_future) =
        upload_file_no_parents(ctx.clone(), &repo, "blob", &fake_file_path);

    let (roothash, root_manifest_future) = upload_manifest_no_parents(
        ctx.clone(),
        &repo,
        format!("file\0{}\n", filehash),
        &RepoPath::root(),
    );

    let (fnid, _) = file_future.await.unwrap();
    let (root_mfid, _) = root_manifest_future.await.unwrap();

    let entries = UploadEntries::new(
        repo.repo_blobstore().clone(),
        MononokeScubaSampleBuilder::with_discard(),
    );

    entries
        .process_root_manifest(&ctx, root_mfid)
        .await
        .unwrap();

    entries
        .process_one_entry(&ctx, Entry::Leaf(fnid), fake_file_path)
        .await
        .unwrap();

    (entries.finalize(&ctx, roothash, vec![])).await.unwrap();
}

#[fbinit::test]
async fn upload_entries_finalize_fail(fb: FacebookInit) {
    let ctx = CoreContext::test_mock(fb);
    let repo: BlobRepo = test_repo_factory::build_empty(fb)
        .await
        .expect("Couldn't create repo");

    let entries = UploadEntries::new(
        repo.repo_blobstore().clone(),
        MononokeScubaSampleBuilder::with_discard(),
    );

    let dirhash = string_to_nodehash("c2d60b35a8e7e034042a9467783bbdac88a0d219");
    let (_, root_manifest_future) = upload_manifest_no_parents(
        ctx.clone(),
        &repo,
        format!("dir\0{}t\n", dirhash),
        &RepoPath::root(),
    );
    let (root_mfid, _) = root_manifest_future.await.unwrap();

    entries
        .process_root_manifest(&ctx, root_mfid)
        .await
        .unwrap();

    let res = (entries.finalize(&ctx, root_mfid, vec![])).await;

    assert!(res.is_err());
}

#[fbinit::test]
async fn test_compute_changed_files_no_parents(fb: FacebookInit) {
    let ctx = CoreContext::test_mock(fb);
    let repo = ManyFilesDirs::getrepo(fb).await;
    let nodehash = string_to_nodehash("051946ed218061e925fb120dac02634f9ad40ae2");
    let expected = vec![
        MPath::new(b"1").unwrap(),
        MPath::new(b"2").unwrap(),
        MPath::new(b"dir1").unwrap(),
        MPath::new(b"dir2/file_1_in_dir2").unwrap(),
    ];

    let cs = HgChangesetId::new(nodehash)
        .load(&ctx, repo.repo_blobstore())
        .await
        .unwrap();

    let diff = (compute_changed_files(
        ctx.clone(),
        repo.repo_blobstore_arc(),
        cs.manifestid(),
        None,
        None,
    ))
    .await
    .unwrap();
    assert!(
        diff == expected,
        "Got {:?}, expected {:?}\n",
        diff,
        expected,
    );
}

#[fbinit::test]
async fn test_compute_changed_files_one_parent(fb: FacebookInit) {
    let ctx = CoreContext::test_mock(fb);
    // Note that this is a commit and its parent commit, so you can use:
    // hg log -T"{node}\n{files % '    MPath::new(b\"{file}\").unwrap(),\\n'}\\n" -r $HASH
    // to see how Mercurial would compute the files list and confirm that it's the same
    let repo = ManyFilesDirs::getrepo(fb).await;
    let nodehash = string_to_nodehash("051946ed218061e925fb120dac02634f9ad40ae2");
    let parenthash = string_to_nodehash("d261bc7900818dea7c86935b3fb17a33b2e3a6b4");
    let expected = vec![
        MPath::new(b"dir1").unwrap(),
        MPath::new(b"dir1/file_1_in_dir1").unwrap(),
        MPath::new(b"dir1/file_2_in_dir1").unwrap(),
        MPath::new(b"dir1/subdir1/file_1").unwrap(),
        MPath::new(b"dir1/subdir1/subsubdir1/file_1").unwrap(),
        MPath::new(b"dir1/subdir1/subsubdir2/file_1").unwrap(),
        MPath::new(b"dir1/subdir1/subsubdir2/file_2").unwrap(),
    ];

    let cs = (HgChangesetId::new(nodehash).load(&ctx, repo.repo_blobstore()))
        .await
        .unwrap();

    let parent_cs = (HgChangesetId::new(parenthash).load(&ctx, repo.repo_blobstore()))
        .await
        .unwrap();

    let diff = compute_changed_files(
        ctx.clone(),
        repo.repo_blobstore_arc(),
        cs.manifestid(),
        Some(parent_cs.manifestid()),
        None,
    )
    .await
    .unwrap();
    assert!(
        diff == expected,
        "Got {:?}, expected {:?}\n",
        diff,
        expected,
    );
}

fn make_bonsai_changeset(
    p0: Option<ChangesetId>,
    p1: Option<ChangesetId>,
    changes: Vec<(&'static str, FileChange)>,
) -> BonsaiChangeset {
    BonsaiChangesetMut {
        parents: p0.into_iter().chain(p1).collect(),
        author: "aslpavel".to_owned(),
        author_date: DateTime::from_timestamp(1528298184, 0).unwrap(),
        committer: None,
        committer_date: None,
        message: "[mononoke] awesome message".to_owned(),
        hg_extra: Default::default(),
        git_extra_headers: None,
        git_tree_hash: None,
        file_changes: changes
            .into_iter()
            .map(|(path, change)| (MPath::new(path).unwrap(), change))
            .collect(),
        is_snapshot: false,
        git_annotated_tag: None,
    }
    .freeze()
    .unwrap()
}

async fn make_file_change<'a>(
    ctx: &'a CoreContext,
    content: impl AsRef<[u8]>,
    repo: &'a BlobRepo,
) -> Result<FileChange, Error> {
    let content = content.as_ref();
    let content_size = content.len() as u64;
    let content_id = FileContents::new_bytes(Bytes::copy_from_slice(content))
        .into_blob()
        .store(ctx, repo.repo_blobstore())
        .await?;
    Ok(FileChange::tracked(
        content_id,
        FileType::Regular,
        content_size,
        None,
    ))
}

fn entry_nodehash(e: &Entry<HgManifestId, (FileType, HgFileNodeId)>) -> HgNodeHash {
    match e {
        Entry::Leaf((_, id)) => id.into_nodehash(),
        Entry::Tree(id) => id.into_nodehash(),
    }
}

async fn entry_content(
    ctx: &CoreContext,
    repo: &BlobRepo,
    e: &Entry<HgManifestId, (FileType, HgFileNodeId)>,
) -> Result<Bytes, Error> {
    let ret = match e {
        Entry::Leaf((_, id)) => {
            let envelope = id.load(ctx, repo.repo_blobstore()).await?;
            filestore::fetch_concat(&repo.repo_blobstore().clone(), ctx, envelope.content_id())
                .await?
        }
        Entry::Tree(..) => {
            return Err(Error::msg("entry_content was called on a Tree"));
        }
    };

    Ok(ret)
}

async fn entry_parents(
    ctx: &CoreContext,
    repo: &BlobRepo,
    e: &Entry<HgManifestId, (FileType, HgFileNodeId)>,
) -> Result<HgParents, Error> {
    let ret = match e {
        Entry::Leaf((_, id)) => {
            let envelope = id.load(ctx, repo.repo_blobstore()).await?;
            envelope.hg_parents()
        }
        Entry::Tree(id) => {
            let manifest = id.load(ctx, repo.repo_blobstore()).await?;
            manifest.hg_parents()
        }
    };

    Ok(ret)
}

#[fbinit::test]
async fn test_get_manifest_from_bonsai(fb: FacebookInit) {
    let ctx = CoreContext::test_mock(fb);
    let repo = MergeUneven::getrepo(fb).await;

    let get_entries = {
        cloned!(ctx, repo);
        move |ms_hash: HgManifestId| -> BoxFuture<
            'static,
            Result<HashMap<String, Entry<HgManifestId, (FileType, HgFileNodeId)>>, Error>,
        > {
            cloned!(ctx, repo);
            async move {
                let ms = ms_hash.load(&ctx, repo.repo_blobstore()).await?;
                let result = Manifest::list(&ms)
                    .map(|(name, entry)| {
                        (String::from_utf8(Vec::from(name.as_ref())).unwrap(), entry)
                    })
                    .collect::<HashMap<_, _>>();
                Ok(result)
            }
            .boxed()
        }
    };

    // #CONTENT
    // 1: 1
    // 2: 2
    // 3: 3
    // 4: 4
    // 5: 5
    // base: branch1
    // branch: 4
    let ms1 = HgChangesetId::new(string_to_nodehash(
        "264f01429683b3dd8042cb3979e8bf37007118bc",
    ))
    .load(&ctx, repo.repo_blobstore())
    .await
    .unwrap()
    .manifestid();

    // #CONTENT
    // base: base
    // branch: 4
    let ms2 = HgChangesetId::new(string_to_nodehash(
        "16839021e338500b3cf7c9b871c8a07351697d68",
    ))
    .load(&ctx, repo.repo_blobstore())
    .await
    .unwrap()
    .manifestid();

    // fails with conflict
    {
        let ms_hash = (get_manifest_from_bonsai(
            ctx.clone(),
            repo.repo_blobstore_arc(),
            make_bonsai_changeset(None, None, vec![]),
            vec![ms1, ms2],
        ))
        .await;
        assert!(
            ms_hash
                .expect_err("should have failed")
                .to_string()
                .contains("conflict")
        );
    }

    // resolves same content different parents for `branch` file
    {
        let ms_hash = (get_manifest_from_bonsai(
            ctx.clone(),
            repo.repo_blobstore_arc(),
            make_bonsai_changeset(None, None, vec![("base", FileChange::Deletion)]),
            vec![ms1, ms2],
        ))
        .await
        .expect("merge should have succeeded");
        let entries = get_entries(ms_hash).await.unwrap();

        assert!(entries.get("1").is_some());
        assert!(entries.get("2").is_some());
        assert!(entries.get("3").is_some());
        assert!(entries.get("4").is_some());
        assert!(entries.get("5").is_some());
        assert!(entries.get("base").is_none());

        // check trivial merge reuse of p1. This is different to Mercurial, but still OK.
        // It biases us towards looking at p1 history for a file whose content is identical
        // in p1 and p2.
        let ms1_entries = get_entries(ms1).await.unwrap();
        let br_expected = entry_nodehash(ms1_entries.get("branch").unwrap());

        let br = entry_nodehash(entries.get("branch").expect("trivial merge should succeed"));
        assert_eq!(br, br_expected);
    }

    // add file
    {
        let content_expected = &b"some awesome content"[..];
        let fc = make_file_change(&ctx, content_expected, &repo)
            .await
            .unwrap();
        let bcs = make_bonsai_changeset(
            None,
            None,
            vec![("base", FileChange::Deletion), ("new", fc)],
        );
        let ms_hash =
            (get_manifest_from_bonsai(ctx.clone(), repo.repo_blobstore_arc(), bcs, vec![ms1, ms2]))
                .await
                .expect("adding new file should not produce coflict");
        let entries = get_entries(ms_hash).await.unwrap();
        let new = entries.get("new").expect("new file should be in entries");
        let bytes = entry_content(&ctx, &repo, new).await.unwrap();
        assert_eq!(bytes.as_ref(), content_expected);

        let new_parents = (entry_parents(&ctx, &repo, new)).await.unwrap();
        assert_eq!(new_parents, HgParents::None);
    }
}

#[fbinit::test]
async fn test_hg_commit_generation_simple(fb: FacebookInit) {
    let repo = fixtures::Linear::getrepo(fb).await;
    let bcs = create_bonsai_changeset(vec![]);

    let bcs_id = bcs.get_changeset_id();
    let ctx = CoreContext::test_mock(fb);
    blobrepo::save_bonsai_changesets(vec![bcs], ctx.clone(), &repo)
        .await
        .unwrap();
    let hg_cs_id = repo.derive_hg_changeset(&ctx, bcs_id).await.unwrap();

    assert_eq!(
        hg_cs_id,
        HgChangesetId::new(string_to_nodehash(
            "5c31d1196c64c93cb5bcf8bca3a24860f103d69f"
        ))
    );
    // make sure bonsai hg mapping is updated
    let map_bcs_id = repo
        .bonsai_hg_mapping()
        .get_bonsai_from_hg(&ctx, hg_cs_id)
        .await
        .unwrap();
    assert_eq!(map_bcs_id, Some(bcs_id));
}

#[fbinit::test]
async fn test_hg_commit_generation_stack(fb: FacebookInit) {
    let repo = fixtures::Linear::getrepo(fb).await;
    let mut changesets = vec![];
    let bcs = create_bonsai_changeset(vec![]);

    let mut prev_bcs_id = bcs.get_changeset_id();
    changesets.push(bcs.clone());

    // Create a large stack to make sure we don't have stackoverflow problems
    let stack_size = 10000;
    for _ in 1..stack_size {
        let new_bcs = create_bonsai_changeset(vec![prev_bcs_id]);
        prev_bcs_id = new_bcs.get_changeset_id();
        changesets.push(new_bcs);
    }

    let top_of_stack = changesets.last().unwrap().clone().get_changeset_id();
    let ctx = CoreContext::test_mock(fb);
    blobrepo::save_bonsai_changesets(changesets, ctx.clone(), &repo)
        .await
        .unwrap();

    let hg_cs_id = repo.derive_hg_changeset(&ctx, top_of_stack).await.unwrap();
    assert_eq!(
        hg_cs_id,
        HgChangesetId::new(string_to_nodehash(
            "b15a980d805db1646422dbf02016aa8a9f8aacd3",
        ))
    );
}

#[fbinit::test]
async fn test_hg_commit_generation_one_after_another(fb: FacebookInit) {
    let ctx = CoreContext::test_mock(fb);
    let repo = fixtures::Linear::getrepo(fb).await;

    let first_bcs = create_bonsai_changeset(vec![]);
    let first_bcs_id = first_bcs.get_changeset_id();

    let second_bcs = create_bonsai_changeset(vec![first_bcs_id]);
    let second_bcs_id = second_bcs.get_changeset_id();
    blobrepo::save_bonsai_changesets(vec![first_bcs, second_bcs], ctx.clone(), &repo)
        .await
        .unwrap();

    let hg_cs_id = repo.derive_hg_changeset(&ctx, first_bcs_id).await.unwrap();
    assert_eq!(
        hg_cs_id,
        HgChangesetId::new(string_to_nodehash(
            "5c31d1196c64c93cb5bcf8bca3a24860f103d69f",
        ))
    );

    let hg_cs_id = repo.derive_hg_changeset(&ctx, second_bcs_id).await.unwrap();
    assert_eq!(
        hg_cs_id,
        HgChangesetId::new(string_to_nodehash(
            "09e9a31873e07ad483aa64e4dfd2cc705de40276",
        ))
    );
}

#[fbinit::test]
async fn test_hg_commit_generation_diamond(fb: FacebookInit) {
    let ctx = CoreContext::test_mock(fb);
    let repo = fixtures::Linear::getrepo(fb).await;

    let last_bcs_id = fixtures::save_diamond_commits(&ctx, &repo, vec![])
        .await
        .unwrap();

    let hg_cs_id = repo.derive_hg_changeset(&ctx, last_bcs_id).await.unwrap();
    assert_eq!(
        hg_cs_id,
        HgChangesetId::new(string_to_nodehash(
            "5d69478d73e67e5270550e44f2acfd93f456d74a",
        ))
    );
}

#[fbinit::test]
async fn test_hg_commit_generation_many_diamond(fb: FacebookInit) {
    let ctx = CoreContext::test_mock(fb);
    let repo = fixtures::ManyDiamonds::getrepo(fb).await;
    let book = bookmarks::BookmarkKey::new("master").unwrap();
    let bcs_id = repo
        .bookmarks()
        .get(ctx.clone(), &book)
        .await
        .unwrap()
        .unwrap();

    let hg_cs_id = repo.derive_hg_changeset(&ctx, bcs_id).await.unwrap();
    assert_eq!(
        hg_cs_id,
        HgChangesetId::new(string_to_nodehash(
            "6b43556e77b7312cabd16ac5f0a85cd920d95272",
        ))
    );
}

#[fbinit::test]
async fn test_hg_commit_generation_uneven_branch(fb: FacebookInit) {
    let ctx = CoreContext::test_mock(fb);
    let repo: BlobRepo = test_repo_factory::build_empty(fb)
        .await
        .expect("Couldn't create repo");

    let root_bcs = fixtures::create_bonsai_changeset(vec![]);

    let large_branch_1 = fixtures::create_bonsai_changeset(vec![root_bcs.get_changeset_id()]);
    let large_branch_2 = fixtures::create_bonsai_changeset(vec![large_branch_1.get_changeset_id()]);

    let short_branch = fixtures::create_bonsai_changeset(vec![root_bcs.get_changeset_id()]);

    let merge = fixtures::create_bonsai_changeset(vec![
        short_branch.get_changeset_id(),
        large_branch_2.get_changeset_id(),
    ]);

    blobrepo::save_bonsai_changesets(
        vec![
            root_bcs,
            large_branch_1,
            large_branch_2,
            short_branch,
            merge.clone(),
        ],
        ctx.clone(),
        &repo,
    )
    .await
    .unwrap();

    let hg_cs_id = repo
        .derive_hg_changeset(&ctx, merge.get_changeset_id())
        .await
        .unwrap();
    assert_eq!(
        hg_cs_id,
        HgChangesetId::new(string_to_nodehash(
            "62b3de4cbd1bc4bf8422c6588234c28842476d3b",
        ))
    );
}

#[cfg(fbcode_build)]
#[fbinit::test]
async fn save_reproducibility_under_load(fb: FacebookInit) -> Result<(), Error> {
    use delayblob::DelayedBlobstore;
    use rand::Rng;
    use rand::SeedableRng;
    use rand_distr::Distribution;
    use rand_distr::Normal;
    use rand_xorshift::XorShiftRng;
    use sql::rusqlite::Connection as SqliteConnection;
    use sql::sqlite::SqliteCallbacks;
    use sql::sqlite::SqliteQueryType;

    let ctx = CoreContext::test_mock(fb);
    let blobstore = Arc::new(DelayedBlobstore::new(
        Memblob::default(),
        Normal::new(0.01, 0.005).expect("Normal::new failed"),
        Normal::new(0.005, 0.0025).expect("Normal::new failed"),
    ));

    struct DelayedSqliteCallbacks {
        db_put_dist: Normal<f64>,
        db_get_dist: Normal<f64>,
    }

    async fn delay(distribution: impl Distribution<f64>) {
        let seconds = rand::thread_rng().sample(distribution).abs();
        let duration = Duration::from_secs_f64(seconds);
        tokio::time::sleep(duration).await;
    }

    #[async_trait]
    impl SqliteCallbacks for DelayedSqliteCallbacks {
        async fn query_start(&self, query_type: SqliteQueryType) -> Result<(), Error> {
            match query_type {
                SqliteQueryType::Read => delay(self.db_get_dist).await,
                SqliteQueryType::Write => delay(self.db_put_dist).await,
                _ => {}
            }
            Ok(())
        }
    }

    let delayed_sqlite_callbacks = DelayedSqliteCallbacks {
        db_put_dist: Normal::new(0.002, 0.001).expect("Normal::new failed"),
        db_get_dist: Normal::new(0.002, 0.001).expect("Normal::new failed"),
    };

    cmdlib_caching::facebook::init_cachelib_from_settings(fb, Default::default(), false).unwrap();

    let repo: BlobRepo = TestRepoFactory::with_sqlite_connection_callbacks(
        fb,
        SqliteConnection::open_in_memory()?,
        SqliteConnection::open_in_memory()?,
        Some(Box::new(delayed_sqlite_callbacks)),
    )?
    .with_blobstore(blobstore)
    .build()
    .await?;

    let mut rng = XorShiftRng::seed_from_u64(1);

    let (csid, _) = tests_utils::random::create_random_stack(
        &ctx,
        &repo,
        &mut rng,
        None,
        std::iter::repeat(16).take(50),
    )
    .await?;
    let hgcsid = repo.derive_hg_changeset(&ctx, csid).await?;

    assert_eq!(hgcsid, "e9b73f926c993c5232139d4eefa6f77fa8c41279".parse()?);

    Ok(())
}

#[fbinit::test]
async fn test_filenode_lookup(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);

    let memblob = Memblob::default();
    let blobstore = Arc::new(TracingBlobstore::new(memblob));

    let repo: BlobRepo = TestRepoFactory::new(fb)?
        .with_blobstore(blobstore.clone())
        .build()
        .await?;

    let p1 = None;
    let p2 = None;

    let content_blob = FileContents::new_bytes(
        File::new(b"myblob".to_vec(), p1, p2)
            .file_contents()
            .into_bytes(),
    )
    .into_blob();
    let content_id = *content_blob.id();
    let content_len = content_blob.len() as u64;
    content_blob.store(&ctx, repo.repo_blobstore()).await?;

    let path = RepoPath::file("path/3")?;

    let content_key = format!("repo0000.content.blake2.{}", content_id.to_hex());

    let cbmeta = ContentBlobMeta {
        id: content_id,
        size: content_len,
        copy_from: None,
    };

    let cbmeta_copy = ContentBlobMeta {
        id: content_id,
        size: content_len,
        copy_from: Some((to_mpath(path.clone())?, ONES_FNID)),
    };

    // Clear our blobstore first.
    let _ = blobstore.tracing_gets();

    // First, upload. We expect 3 calls here:
    // - Filenode lookup: this will miss.
    // - File lookup (to compute metadata): this will hit.
    // - File lookup (to hash the contents): this will hit.

    let upload = UploadHgFileEntry {
        upload_node_id: UploadHgNodeHash::Generate,
        contents: UploadHgFileContents::ContentUploaded(cbmeta.clone()),
        p1,
        p2,
    };
    upload
        .upload(ctx.clone(), repo.repo_blobstore_arc(), None)
        .await?;

    let gets = blobstore.tracing_gets();
    assert_eq!(gets.len(), 3);
    assert!(gets[0].contains("filenode_lookup"));
    assert_eq!(gets[1], content_key);
    assert_eq!(gets[2], content_key);

    // Now, upload the content again. This time, we expect one call to the alias, and one call to
    // fetch the metadata (this is obviously a little inefficient if we need both, but the latter
    // call can now be reduced to peeking at the file contents).

    let upload = UploadHgFileEntry {
        upload_node_id: UploadHgNodeHash::Generate,
        contents: UploadHgFileContents::ContentUploaded(cbmeta.clone()),
        p1,
        p2,
    };

    upload
        .upload(ctx.clone(), repo.repo_blobstore_arc(), None)
        .await?;

    let gets = blobstore.tracing_gets();
    assert_eq!(gets.len(), 2);
    assert!(gets[0].contains("filenode_lookup"));
    assert_eq!(gets[1], content_key);

    // Finally, upload with different copy metadata. Reusing the filenode should not be possible,
    // so this should make 3 calls again.
    let upload = UploadHgFileEntry {
        upload_node_id: UploadHgNodeHash::Generate,
        contents: UploadHgFileContents::ContentUploaded(cbmeta_copy.clone()),
        p1,
        p2,
    };
    upload
        .upload(ctx.clone(), repo.repo_blobstore_arc(), None)
        .await?;

    let gets = blobstore.tracing_gets();
    assert_eq!(gets.len(), 3);
    assert!(gets[0].contains("filenode_lookup"));
    assert_eq!(gets[1], content_key);
    assert_eq!(gets[2], content_key);

    Ok(())
}

#[fbinit::test]
async fn test_content_uploaded_filenode_id(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);

    let repo: BlobRepo = test_repo_factory::build_empty(fb)
        .await
        .expect("Couldn't create repo");

    let p1 = None;
    let p2 = None;

    let content_blob = FileContents::new_bytes(
        File::new(b"myblob".to_vec(), p1, p2)
            .file_contents()
            .into_bytes(),
    )
    .into_blob();
    let content_id = *content_blob.id();
    let content_len = content_blob.len() as u64;
    content_blob.store(&ctx, repo.repo_blobstore()).await?;

    let path = RepoPath::file("path/2")?;

    let cbmeta = ContentBlobMeta {
        id: content_id,
        size: content_len,
        copy_from: Some((to_mpath(path.clone())?, ONES_FNID)),
    };

    let upload = UploadHgFileEntry {
        upload_node_id: UploadHgNodeHash::Checked(
            "47f917b28e191c4bb0de8927e716e1b976ec3ad0".parse()?,
        ),
        contents: UploadHgFileContents::ContentUploaded(cbmeta.clone()),
        p1,
        p2,
    };
    upload
        .upload(ctx.clone(), repo.repo_blobstore_arc(), None)
        .await?;

    Ok(())
}

struct TestHelper {
    ctx: CoreContext,
    repo: BlobRepo,
}

impl TestHelper {
    async fn new(fb: FacebookInit) -> Result<Self, Error> {
        let ctx = CoreContext::test_mock(fb);
        let repo: BlobRepo = test_repo_factory::build_empty(fb)
            .await
            .expect("Couldn't create repo");
        Ok(Self { ctx, repo })
    }

    fn new_commit(&self) -> CreateCommitContext<'_, BlobRepo> {
        CreateCommitContext::new_root(&self.ctx, &self.repo)
    }

    async fn lookup_changeset(&self, cs_id: ChangesetId) -> Result<HgBlobChangeset, Error> {
        let hg_cs_id = self.repo.derive_hg_changeset(&self.ctx, cs_id).await?;

        let hg_cs = hg_cs_id.load(&self.ctx, self.repo.repo_blobstore()).await?;

        Ok(hg_cs)
    }

    async fn root_manifest(&self, cs_id: ChangesetId) -> Result<HgBlobManifest, Error> {
        let hg_cs = self.lookup_changeset(cs_id).await?;

        let manifest = hg_cs
            .manifestid()
            .load(&self.ctx, self.repo.repo_blobstore())
            .await?;

        Ok(manifest)
    }

    async fn lookup_entry(
        &self,
        cs_id: ChangesetId,
        path: &str,
    ) -> Result<Entry<HgManifestId, (FileType, HgFileNodeId)>, Error> {
        let path = MPath::new(path)?;

        let hg_cs = self.lookup_changeset(cs_id).await?;

        let err = Error::msg(format!("Missing entry: {}", path));

        let entry = hg_cs
            .manifestid()
            .find_entry(
                self.ctx.clone(),
                self.repo.repo_blobstore().clone(),
                Some(path),
            )
            .await?
            .ok_or(err)?;

        Ok(entry)
    }

    async fn lookup_manifest(
        &self,
        cs_id: ChangesetId,
        path: &str,
    ) -> Result<HgBlobManifest, Error> {
        let id = self
            .lookup_entry(cs_id, path)
            .await?
            .into_tree()
            .ok_or_else(|| Error::msg(format!("Not a manifest: {}", path)))?;

        let manifest = id.load(&self.ctx, self.repo.repo_blobstore()).await?;

        Ok(manifest)
    }

    async fn lookup_filenode(
        &self,
        cs_id: ChangesetId,
        path: &str,
    ) -> Result<HgFileEnvelope, Error> {
        let (_, filenode) = self
            .lookup_entry(cs_id, path)
            .await?
            .into_leaf()
            .ok_or_else(|| Error::msg(format!("Not a filenode: {}", path)))?;

        let envelope = filenode.load(&self.ctx, self.repo.repo_blobstore()).await?;

        Ok(envelope)
    }
}

mod octopus_merges {
    use super::*;

    #[fbinit::test]
    async fn test_basic(fb: FacebookInit) -> Result<(), Error> {
        let ctx = CoreContext::test_mock(fb);
        let repo: BlobRepo = test_repo_factory::build_empty(fb)
            .await
            .expect("Couldn't create repo");

        let p1 = CreateCommitContext::new_root(&ctx, &repo)
            .add_file("foo", "foo")
            .commit()
            .await?;

        let p2 = CreateCommitContext::new_root(&ctx, &repo)
            .add_file("bar", "bar")
            .commit()
            .await?;

        let p3 = CreateCommitContext::new_root(&ctx, &repo)
            .add_file("qux", "qux")
            .commit()
            .await?;

        let commit = CreateCommitContext::new(&ctx, &repo, vec![p1, p2, p3])
            .commit()
            .await?;

        let hg_cs_id = repo.derive_hg_changeset(&ctx, commit).await?;

        let hg_cs = hg_cs_id.load(&ctx, repo.repo_blobstore()).await?;

        let hg_manifest = hg_cs.manifestid().load(&ctx, repo.repo_blobstore()).await?;

        // Do we get the same files?
        let files = Manifest::list(&hg_manifest);
        assert_eq!(files.count(), 3);

        Ok(())
    }

    #[fbinit::test]
    async fn test_basic_filenode_parents(fb: FacebookInit) -> Result<(), Error> {
        let helper = TestHelper::new(fb).await?;

        let p1 = helper.new_commit().add_file("foo", "foo").commit().await?;
        let p2 = helper.new_commit().add_file("bar", "bar").commit().await?;
        let p3 = helper.new_commit().add_file("qux", "qux").commit().await?;

        let commit = helper
            .new_commit()
            .add_parent(p1)
            .add_parent(p2)
            .add_parent(p3)
            .add_file("foo", "foo2")
            .add_file("bar", "bar2")
            .add_file("qux", "qux2")
            .commit()
            .await?;

        let foo = helper.lookup_filenode(commit, "foo").await?;
        let bar = helper.lookup_filenode(commit, "bar").await?;
        let qux = helper.lookup_filenode(commit, "qux").await?;

        // We expect the parents for foo and bar to be present, but qux should have it parent
        // dropped, because it's out of range for Mercurial.
        assert_eq!(
            foo.parents(),
            (
                Some(helper.lookup_filenode(p1, "foo").await?.node_id()),
                None
            )
        );

        assert_eq!(
            bar.parents(),
            (
                Some(helper.lookup_filenode(p2, "bar").await?.node_id()),
                None
            )
        );

        assert_eq!(qux.parents(), (None, None));

        Ok(())
    }

    #[fbinit::test]
    async fn test_many_filenode_parents(fb: FacebookInit) -> Result<(), Error> {
        let helper = TestHelper::new(fb).await?;

        let p1 = helper.new_commit().add_file("foo", "foo").commit().await?;
        let p2 = helper.new_commit().add_file("foo", "bar").commit().await?;
        let p3 = helper.new_commit().add_file("foo", "qux").commit().await?;

        let commit = helper
            .new_commit()
            .add_parent(p1)
            .add_parent(p2)
            .add_parent(p3)
            .add_file("foo", "foo2")
            .commit()
            .await?;

        let foo = helper.lookup_filenode(commit, "foo").await?;

        assert_eq!(
            foo.parents(),
            (
                Some(helper.lookup_filenode(p1, "foo").await?.node_id()),
                Some(helper.lookup_filenode(p2, "foo").await?.node_id())
            )
        );

        Ok(())
    }

    #[fbinit::test]
    async fn test_mixed_filenode_parents(fb: FacebookInit) -> Result<(), Error> {
        let helper = TestHelper::new(fb).await?;

        let p1 = helper.new_commit().commit().await?;
        let p2 = helper.new_commit().add_file("foo", "foo").commit().await?;
        let p3 = helper.new_commit().add_file("foo", "bar").commit().await?;

        let commit = helper
            .new_commit()
            .add_parent(p1)
            .add_parent(p2)
            .add_parent(p3)
            .add_file("foo", "foo2")
            .commit()
            .await?;

        let foo = helper.lookup_filenode(commit, "foo").await?;

        assert_eq!(
            foo.parents(),
            (
                Some(helper.lookup_filenode(p2, "foo").await?.node_id()),
                None
            )
        );

        Ok(())
    }

    #[fbinit::test]
    async fn test_strip_copy_from(fb: FacebookInit) -> Result<(), Error> {
        let helper = TestHelper::new(fb).await?;

        let p1 = helper.new_commit().commit().await?;
        let p2 = helper.new_commit().commit().await?;
        let p3 = helper.new_commit().add_file("foo", "foo").commit().await?;

        let commit = helper
            .new_commit()
            .add_parent(p1)
            .add_parent(p2)
            .add_parent(p3)
            .add_file_with_copy_info("foo", "bar", (p3, "foo"))
            .commit()
            .await?;

        let foo = helper.lookup_filenode(commit, "foo").await?;

        assert_eq!(foo.parents(), (None, None));

        Ok(())
    }

    #[fbinit::test]
    async fn test_mixed_manifest_parents(fb: FacebookInit) -> Result<(), Error> {
        let helper = TestHelper::new(fb).await?;

        let p1 = helper
            .new_commit()
            .add_file("foo/p1", "p1")
            .commit()
            .await?;

        let p2 = helper
            .new_commit()
            .add_file("foo/p2", "p2")
            .add_file("bar/p2", "p2")
            .commit()
            .await?;

        let p3 = helper
            .new_commit()
            .add_file("foo/p3", "p3")
            .add_file("bar/p3", "p3")
            .commit()
            .await?;

        let commit = helper
            .new_commit()
            .add_parent(p1)
            .add_parent(p2)
            .add_parent(p3)
            .commit()
            .await?;

        let foo = helper.lookup_manifest(commit, "foo").await?;
        let bar = helper.lookup_manifest(commit, "bar").await?;

        assert_eq!(
            foo.p1(),
            Some(helper.lookup_manifest(p1, "foo").await?.node_id())
        );

        assert_eq!(
            foo.p2(),
            Some(helper.lookup_manifest(p2, "foo").await?.node_id())
        );

        assert_eq!(
            bar.p1(),
            Some(helper.lookup_manifest(p2, "bar").await?.node_id())
        );

        assert_eq!(bar.p2(), None);

        Ok(())
    }

    #[fbinit::test]
    async fn test_step_parents_metadata(fb: FacebookInit) -> Result<(), Error> {
        let helper = TestHelper::new(fb).await?;

        let p1 = helper.new_commit().commit().await?;
        let p2 = helper.new_commit().commit().await?;
        let p3 = helper.new_commit().commit().await?;
        let p4 = helper.new_commit().commit().await?;

        let commit = helper
            .new_commit()
            .add_parent(p1)
            .add_parent(p2)
            .add_parent(p3)
            .add_parent(p4)
            .commit()
            .await?;

        let hg_cs = helper.lookup_changeset(commit).await?;

        assert_eq!(
            hg_cs.p1(),
            Some(
                helper
                    .lookup_changeset(p1)
                    .await?
                    .get_changeset_id()
                    .into_nodehash()
            )
        );

        assert_eq!(
            hg_cs.p2(),
            Some(
                helper
                    .lookup_changeset(p2)
                    .await?
                    .get_changeset_id()
                    .into_nodehash()
            )
        );

        let step_parents_key: Vec<u8> = "stepparents".into();
        let step_parents = hg_cs
            .extra()
            .get(&step_parents_key)
            .ok_or_else(|| Error::msg("stepparents are missing"))?;

        assert_eq!(
            std::str::from_utf8(step_parents)?,
            format!(
                "{},{}",
                helper
                    .lookup_changeset(p3)
                    .await?
                    .get_changeset_id()
                    .to_hex(),
                helper
                    .lookup_changeset(p4)
                    .await?
                    .get_changeset_id()
                    .to_hex()
            )
        );

        Ok(())
    }

    #[fbinit::test]
    async fn test_resolve_trivial_conflict(fb: FacebookInit) -> Result<(), Error> {
        let helper = TestHelper::new(fb).await?;

        let p1 = helper.new_commit().commit().await?;
        let p2 = helper.new_commit().add_file("foo", "foo").commit().await?;
        let p3 = helper
            .new_commit()
            .add_file("foo", "foo")
            .add_parent(helper.new_commit().add_file("foo", "bar").commit().await?)
            .commit()
            .await?;

        let commit = helper
            .new_commit()
            .add_parent(p1)
            .add_parent(p2)
            .add_parent(p3)
            .commit()
            .await?;

        let foo = helper.lookup_filenode(commit, "foo").await?;

        assert_eq!(
            foo.node_id(),
            helper.lookup_filenode(p2, "foo").await?.node_id()
        );

        Ok(())
    }

    #[fbinit::test]
    async fn test_fail_to_resolve_conflict_on_content(fb: FacebookInit) -> Result<(), Error> {
        let helper = TestHelper::new(fb).await?;

        let p1 = helper.new_commit().commit().await?;
        let p2 = helper.new_commit().add_file("foo", "foo").commit().await?;
        let p3 = helper.new_commit().add_file("foo", "bar").commit().await?;

        let commit = helper
            .new_commit()
            .add_parent(p1)
            .add_parent(p2)
            .add_parent(p3)
            .commit()
            .await?;

        let root = helper.root_manifest(commit).await;

        let err = root
            .map(|_| ())
            .expect_err("Derivation should fail on conflict");

        assert_matches!(
            err.downcast_ref::<ErrorKind>(),
            Some(ErrorKind::UnresolvedConflicts(_, _))
        );

        Ok(())
    }

    #[fbinit::test]
    async fn test_fail_to_resolve_conflict_on_type(fb: FacebookInit) -> Result<(), Error> {
        let helper = TestHelper::new(fb).await?;

        let p1 = helper.new_commit().commit().await?;
        let p2 = helper
            .new_commit()
            .add_file_with_type("foo", "foo", FileType::Regular)
            .commit()
            .await?;
        let p3 = helper
            .new_commit()
            .add_file_with_type("foo", "foo", FileType::Executable)
            .commit()
            .await?;

        let commit = helper
            .new_commit()
            .add_parent(p1)
            .add_parent(p2)
            .add_parent(p3)
            .commit()
            .await?;

        let root = helper.root_manifest(commit).await;

        let err = root
            .map(|_| ())
            .expect_err("Derivation should fail on conflict");

        assert_matches!(
            err.downcast_ref::<ErrorKind>(),
            Some(ErrorKind::UnresolvedConflicts(_, _))
        );

        Ok(())
    }

    #[fbinit::test]
    async fn test_changeset_file_changes(fb: FacebookInit) -> Result<(), Error> {
        let helper = TestHelper::new(fb).await?;

        let p1 = helper.new_commit().commit().await?;
        let p2 = helper.new_commit().add_file("p2", "p2").commit().await?;
        let p3 = helper.new_commit().add_file("p3", "p3").commit().await?;
        let p4 = helper.new_commit().add_file("p4", "p4").commit().await?;

        let commit = helper
            .new_commit()
            .add_parent(p1)
            .add_parent(p2)
            .add_parent(p3)
            .add_parent(p4)
            .commit()
            .await?;

        let cs = helper.lookup_changeset(commit).await?;
        assert_eq!(cs.files(), &vec![MPath::new("p3")?, MPath::new("p4")?][..]);

        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::sync::Arc;
use std::sync::Mutex;

use anyhow::Result;
use async_trait::async_trait;
use blobstore::Blobstore;
use blobstore::BlobstoreGetData;
use blobstore::BlobstoreIsPresent;
use context::CoreContext;
use mononoke_types::BlobstoreBytes;

#[derive(Debug)]
pub struct TracingBlobstore<T> {
    inner: T,
    gets: Arc<Mutex<Vec<String>>>,
}

impl<T: std::fmt::Display> std::fmt::Display for TracingBlobstore<T> {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "TracingBlobstore<{}>", &self.inner)
    }
}

impl<T> TracingBlobstore<T> {
    pub fn new(inner: T) -> Self {
        let gets = Arc::new(Mutex::new(vec![]));
        Self { inner, gets }
    }
}

impl<T> TracingBlobstore<T> {
    pub fn tracing_gets(&self) -> Vec<String> {
        let mut gets = self.gets.lock().expect("poisoned lock");
        std::mem::take(&mut *gets)
    }
}

#[async_trait]
impl<T: Blobstore> Blobstore for TracingBlobstore<T> {
    async fn get<'a>(
        &'a self,
        ctx: &'a CoreContext,
        key: &'a str,
    ) -> Result<Option<BlobstoreGetData>> {
        self.gets
            .lock()
            .expect("poisoned lock")
            .push(key.to_owned());
        self.inner.get(ctx, key).await
    }

    async fn put<'a>(
        &'a self,
        ctx: &'a CoreContext,
        key: String,
        value: BlobstoreBytes,
    ) -> Result<()> {
        self.inner.put(ctx, key, value).await
    }

    async fn is_present<'a>(
        &'a self,
        ctx: &'a CoreContext,
        key: &'a str,
    ) -> Result<BlobstoreIsPresent> {
        self.inner.is_present(ctx, key).await
    }
}


/*
 * 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 ::manifest::Entry;
use anyhow::Error;
use ascii::AsAsciiStr;
use blobrepo::BlobRepo;
use blobrepo_hg::ChangesetHandle;
use blobrepo_hg::CreateChangeset;
use bytes::Bytes;
use context::CoreContext;
use fbinit::FacebookInit;
use filestore::FilestoreConfigRef;
use futures::compat::Future01CompatExt;
use futures::future::BoxFuture;
use futures::stream::FuturesUnordered;
use futures::FutureExt;
use futures::StreamExt;
use futures::TryFutureExt;
use mercurial_types::blobs::ChangesetMetadata;
use mercurial_types::blobs::UploadHgFileContents;
use mercurial_types::blobs::UploadHgFileEntry;
use mercurial_types::blobs::UploadHgNodeHash;
use mercurial_types::blobs::UploadHgTreeEntry;
use mercurial_types::HgBlobNode;
use mercurial_types::HgFileNodeId;
use mercurial_types::HgManifestId;
use mercurial_types::HgNodeHash;
use mercurial_types::MPath;
use mercurial_types::RepoPath;
use mononoke_types::DateTime;
use repo_blobstore::RepoBlobstoreArc;
use scuba_ext::MononokeScubaSampleBuilder;

pub fn upload_file_no_parents<B>(
    ctx: CoreContext,
    repo: &BlobRepo,
    data: B,
    path: &RepoPath,
) -> (
    HgFileNodeId,
    BoxFuture<'static, Result<(HgFileNodeId, RepoPath), Error>>,
)
where
    B: Into<Bytes>,
{
    upload_hg_file_entry(ctx, repo, data.into(), path.clone(), None, None)
}

pub fn upload_file_one_parent<B>(
    ctx: CoreContext,
    repo: &BlobRepo,
    data: B,
    path: &RepoPath,
    p1: HgFileNodeId,
) -> (
    HgFileNodeId,
    BoxFuture<'static, Result<(HgFileNodeId, RepoPath), Error>>,
)
where
    B: Into<Bytes>,
{
    upload_hg_file_entry(ctx, repo, data.into(), path.clone(), Some(p1), None)
}

pub fn upload_manifest_no_parents<B>(
    ctx: CoreContext,
    repo: &BlobRepo,
    data: B,
    path: &RepoPath,
) -> (
    HgManifestId,
    BoxFuture<'static, Result<(HgManifestId, RepoPath), Error>>,
)
where
    B: Into<Bytes>,
{
    upload_hg_tree_entry(ctx, repo, data.into(), path.clone(), None, None)
}

pub fn upload_manifest_one_parent<B>(
    ctx: CoreContext,
    repo: &BlobRepo,
    data: B,
    path: &RepoPath,
    p1: HgManifestId,
) -> (
    HgManifestId,
    BoxFuture<'static, Result<(HgManifestId, RepoPath), Error>>,
)
where
    B: Into<Bytes>,
{
    upload_hg_tree_entry(ctx, repo, data.into(), path.clone(), Some(p1), None)
}

fn upload_hg_tree_entry(
    ctx: CoreContext,
    repo: &BlobRepo,
    contents: Bytes,
    path: RepoPath,
    p1: Option<HgManifestId>,
    p2: Option<HgManifestId>,
) -> (
    HgManifestId,
    BoxFuture<'static, Result<(HgManifestId, RepoPath), Error>>,
) {
    let upload = UploadHgTreeEntry {
        upload_node_id: UploadHgNodeHash::Generate,
        contents,
        p1: p1.map(|mfid| mfid.into_nodehash()),
        p2: p2.map(|mfid| mfid.into_nodehash()),
        path,
    };
    let (hgcsid, fut) = upload.upload(ctx, repo.repo_blobstore_arc()).unwrap();
    (hgcsid, fut.compat().boxed())
}

fn upload_hg_file_entry(
    ctx: CoreContext,
    repo: &BlobRepo,
    contents: Bytes,
    path: RepoPath,
    p1: Option<HgFileNodeId>,
    p2: Option<HgFileNodeId>,
) -> (
    HgFileNodeId,
    BoxFuture<'static, Result<(HgFileNodeId, RepoPath), Error>>,
) {
    // Ideally the node id returned from upload.upload would be used, but that isn't immediately
    // available -- so compute it ourselves.
    let node_id = HgBlobNode::new(
        contents.clone(),
        p1.map(HgFileNodeId::into_nodehash),
        p2.map(HgFileNodeId::into_nodehash),
    )
    .nodeid();

    let upload = UploadHgFileEntry {
        upload_node_id: UploadHgNodeHash::Checked(node_id),
        contents: UploadHgFileContents::RawBytes(contents, *repo.filestore_config()),
        p1,
        p2,
    };
    let path = path.into_mpath().expect("expected a path to be present");

    let upload_fut = upload.upload_with_path(ctx, repo.repo_blobstore_arc(), path);
    (HgFileNodeId::new(node_id), upload_fut.boxed())
}

pub fn create_changeset_no_parents(
    fb: FacebookInit,
    repo: &BlobRepo,
    root_manifest: BoxFuture<'static, Result<Option<(HgManifestId, RepoPath)>, Error>>,
    other_nodes: Vec<
        BoxFuture<'static, Result<(Entry<HgManifestId, HgFileNodeId>, RepoPath), Error>>,
    >,
) -> ChangesetHandle {
    let cs_metadata = ChangesetMetadata {
        user: "author <author@fb.com>".into(),
        time: DateTime::from_timestamp(0, 0).expect("valid timestamp"),
        extra: BTreeMap::new(),
        message: "Test commit".into(),
    };
    let create_changeset = CreateChangeset {
        expected_nodeid: None,
        expected_files: None,
        p1: None,
        p2: None,
        root_manifest,
        sub_entries: other_nodes
            .into_iter()
            .collect::<FuturesUnordered<_>>()
            .boxed(),
        cs_metadata,
        create_bonsai_changeset_hook: None,
        upload_to_blobstore_only: false,
    };
    create_changeset.create(
        CoreContext::test_mock(fb),
        repo,
        MononokeScubaSampleBuilder::with_discard(),
    )
}

pub fn create_changeset_one_parent(
    fb: FacebookInit,
    repo: &BlobRepo,
    root_manifest: BoxFuture<'static, Result<Option<(HgManifestId, RepoPath)>, Error>>,
    other_nodes: Vec<
        BoxFuture<'static, Result<(Entry<HgManifestId, HgFileNodeId>, RepoPath), Error>>,
    >,
    p1: ChangesetHandle,
) -> ChangesetHandle {
    let cs_metadata = ChangesetMetadata {
        user: "\u{041F}\u{0451}\u{0442}\u{0440} <peter@fb.com>".into(),
        time: DateTime::from_timestamp(1234, 0).expect("valid timestamp"),
        extra: BTreeMap::new(),
        message: "Child commit".into(),
    };
    let create_changeset = CreateChangeset {
        expected_nodeid: None,
        expected_files: None,
        p1: Some(p1),
        p2: None,
        root_manifest: root_manifest.boxed(),
        sub_entries: other_nodes
            .into_iter()
            .collect::<FuturesUnordered<_>>()
            .boxed(),
        cs_metadata,
        create_bonsai_changeset_hook: None,
        upload_to_blobstore_only: false,
    };
    create_changeset.create(
        CoreContext::test_mock(fb),
        repo,
        MononokeScubaSampleBuilder::with_discard(),
    )
}

pub fn string_to_nodehash(hash: &str) -> HgNodeHash {
    HgNodeHash::from_ascii_str(hash.as_ascii_str().unwrap()).unwrap()
}

pub fn to_mpath(path: RepoPath) -> Result<MPath, Error> {
    let bad_mpath = Error::msg("RepoPath did not convert to MPath");
    path.into_mpath().ok_or(bad_mpath)
}

pub fn to_leaf(
    fut: BoxFuture<'static, Result<(HgFileNodeId, RepoPath), Error>>,
) -> BoxFuture<'static, Result<(Entry<HgManifestId, HgFileNodeId>, RepoPath), Error>> {
    fut.map_ok(|(id, path)| (Entry::Leaf(id), path)).boxed()
}

pub fn to_tree(
    fut: BoxFuture<'static, Result<(HgManifestId, RepoPath), Error>>,
) -> BoxFuture<'static, Result<(Entry<HgManifestId, HgFileNodeId>, RepoPath), Error>> {
    fut.map_ok(|(id, path)| (Entry::Tree(id), path)).boxed()
}