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

use std::collections::HashMap;
use std::collections::HashSet;
use std::fmt;
use std::hash::Hash;
use std::hash::Hasher;
use std::sync::Arc;
use std::time::SystemTime;
use std::time::UNIX_EPOCH;

use acl_regions::build_disabled_acl_regions;
use acl_regions::AclRegions;
use anyhow::anyhow;
use anyhow::Error;
use blobrepo::AsBlobRepo;
use blobrepo::BlobRepo;
use blobrepo_hg::BlobRepoHg;
use blobstore::Loadable;
use blobstore_factory::MetadataSqlFactory;
use blobstore_factory::ReadOnlyStorage;
use bonsai_git_mapping::BonsaiGitMapping;
use bonsai_git_mapping::BonsaiGitMappingRef;
use bonsai_globalrev_mapping::BonsaiGlobalrevMapping;
use bonsai_globalrev_mapping::BonsaiGlobalrevMappingRef;
use bonsai_hg_mapping::BonsaiHgMapping;
use bonsai_hg_mapping::BonsaiHgMappingRef;
use bonsai_svnrev_mapping::BonsaiSvnrevMappingRef;
use bonsai_tag_mapping::BonsaiTagMapping;
use bookmarks::BookmarkCategory;
use bookmarks::BookmarkKey;
use bookmarks::BookmarkKind;
use bookmarks::BookmarkName;
use bookmarks::BookmarkPagination;
use bookmarks::BookmarkPrefix;
use bookmarks::BookmarkUpdateLog;
use bookmarks::BookmarkUpdateLogArc;
use bookmarks::BookmarkUpdateLogRef;
use bookmarks::Bookmarks;
use bookmarks::BookmarksArc;
use bookmarks::BookmarksRef;
pub use bookmarks::Freshness as BookmarkFreshness;
use bookmarks::Freshness;
use bytes::Bytes;
use cacheblob::InProcessLease;
use cacheblob::LeaseOps;
use changeset_fetcher::ChangesetFetcher;
use changeset_fetcher::ChangesetFetcherArc;
use changeset_fetcher::ChangesetFetcherRef;
use changeset_info::ChangesetInfo;
use changesets::Changesets;
use changesets::ChangesetsArc;
use changesets::ChangesetsRef;
use commit_graph::CommitGraph;
use commit_graph::CommitGraphRef;
use context::CoreContext;
use cross_repo_sync::types::Target;
use cross_repo_sync::CandidateSelectionHint;
use cross_repo_sync::CommitSyncContext;
use cross_repo_sync::CommitSyncRepos;
use cross_repo_sync::CommitSyncer;
use derived_data_manager::manager::derive::BatchDeriveOptions;
use derived_data_manager::BonsaiDerivable;
use derived_data_manager::DerivableType;
use ephemeral_blobstore::ArcRepoEphemeralStore;
use ephemeral_blobstore::Bubble;
use ephemeral_blobstore::BubbleId;
use ephemeral_blobstore::RepoEphemeralStore;
use ephemeral_blobstore::RepoEphemeralStoreArc;
use ephemeral_blobstore::RepoEphemeralStoreRef;
use ephemeral_blobstore::StorageLocation;
use fbinit::FacebookInit;
use filestore::Alias;
use filestore::FetchKey;
use filestore::FilestoreConfig;
use filestore::FilestoreConfigRef;
pub use filestore::StoreRequest;
use fsnodes::RootFsnodeId;
use futures::compat::Stream01CompatExt;
use futures::stream;
use futures::stream::Stream;
use futures::stream::StreamExt;
use futures::stream::TryStreamExt;
use futures::try_join;
use futures::Future;
use hooks::HookManager;
use hooks::HookManagerArc;
use itertools::Itertools;
use live_commit_sync_config::LiveCommitSyncConfig;
use mercurial_derivation::MappedHgChangesetId;
use mercurial_mutation::HgMutationStore;
use mercurial_types::Globalrev;
use metaconfig_types::HookManagerParams;
use metaconfig_types::InfinitepushNamespace;
use metaconfig_types::InfinitepushParams;
use metaconfig_types::RepoConfig;
use metaconfig_types::SourceControlServiceParams;
use mononoke_api_types::InnerRepo;
use mononoke_repos::MononokeRepos;
use mononoke_types::hash::Blake3;
use mononoke_types::hash::GitSha1;
use mononoke_types::hash::Sha1;
use mononoke_types::hash::Sha256;
use mononoke_types::ContentId;
use mononoke_types::Generation;
use mononoke_types::RepositoryId;
use mononoke_types::Svnrev;
use mononoke_types::Timestamp;
use mutable_counters::MutableCounters;
use mutable_renames::ArcMutableRenames;
use mutable_renames::MutableRenames;
use mutable_renames::MutableRenamesArc;
use mutable_renames::SqlMutableRenamesStore;
use phases::Phases;
use phases::PhasesArc;
use phases::PhasesRef;
use pushrebase_mutation_mapping::PushrebaseMutationMapping;
use regex::Regex;
use repo_authorization::AuthorizationContext;
use repo_blobstore::ArcRepoBlobstore;
use repo_blobstore::RepoBlobstore;
use repo_blobstore::RepoBlobstoreArc;
use repo_blobstore::RepoBlobstoreRef;
use repo_bookmark_attrs::RepoBookmarkAttrs;
use repo_cross_repo::RepoCrossRepo;
use repo_derived_data::RepoDerivedData;
use repo_derived_data::RepoDerivedDataArc;
use repo_derived_data::RepoDerivedDataRef;
use repo_identity::RepoIdentity;
use repo_identity::RepoIdentityArc;
use repo_identity::RepoIdentityRef;
use repo_lock::RepoLock;
use repo_permission_checker::RepoPermissionChecker;
use repo_sparse_profiles::ArcRepoSparseProfiles;
use repo_sparse_profiles::RepoSparseProfiles;
use repo_sparse_profiles::RepoSparseProfilesArc;
use revset::AncestorsNodeStream;
use segmented_changelog::CloneData;
use segmented_changelog::DisabledSegmentedChangelog;
use segmented_changelog::Location;
use segmented_changelog::SegmentedChangelog;
use segmented_changelog::SegmentedChangelogRef;
use skeleton_manifest::RootSkeletonManifestId;
use slog::debug;
use slog::error;
use sql_construct::SqlConstruct;
use sql_ext::facebook::MysqlOptions;
use stats::prelude::*;
use streaming_clone::StreamingClone;
use streaming_clone::StreamingCloneBuilder;
use synced_commit_mapping::ArcSyncedCommitMapping;
use synced_commit_mapping::SqlSyncedCommitMapping;
use test_repo_factory::TestRepoFactory;
use tunables::tunables;
use unbundle::PushRedirector;
use unbundle::PushRedirectorArgs;
use warm_bookmarks_cache::BookmarksCache;
use warm_bookmarks_cache::WarmBookmarksCacheBuilder;
use wireproto_handler::PushRedirectorBase;
use wireproto_handler::RepoHandlerBase;
use wireproto_handler::RepoHandlerBaseRef;

use crate::changeset::ChangesetContext;
use crate::errors::MononokeError;
use crate::file::FileContext;
use crate::file::FileId;
use crate::specifiers::ChangesetId;
use crate::specifiers::ChangesetPrefixSpecifier;
use crate::specifiers::ChangesetSpecifier;
use crate::specifiers::ChangesetSpecifierPrefixResolution;
use crate::specifiers::HgChangesetId;
use crate::tree::TreeContext;
use crate::tree::TreeId;
use crate::xrepo::CandidateSelectionHintArgs;

pub mod create_bookmark;
pub mod create_changeset;
pub mod delete_bookmark;
pub mod git;
pub mod land_stack;
pub mod move_bookmark;

pub use git::upload_git_object;

define_stats! {
    prefix = "mononoke.api";
    staleness: dynamic_singleton_counter(
        "staleness.secs.{}.{}",
        (repoid: ::mononoke_types::RepositoryId, bookmark: String)
    ),
    missing_from_cache: dynamic_singleton_counter(
        "missing_from_cache.{}.{}",
        (repoid: ::mononoke_types::RepositoryId, bookmark: String)
    ),
    missing_from_repo: dynamic_singleton_counter(
        "missing_from_repo.{}.{}",
        (repoid: ::mononoke_types::RepositoryId, bookmark: String)
    ),
}

#[facet::container]
#[derive(Clone)]
pub struct Repo {
    #[delegate(
        RepoBlobstore,
        RepoBookmarkAttrs,
        RepoDerivedData,
        RepoIdentity,
        dyn BonsaiTagMapping,
        dyn BonsaiGitMapping,
        dyn BonsaiGlobalrevMapping,
        dyn BonsaiHgMapping,
        dyn BookmarkUpdateLog,
        dyn Bookmarks,
        dyn ChangesetFetcher,
        dyn Changesets,
        dyn Phases,
        dyn PushrebaseMutationMapping,
        dyn HgMutationStore,
        dyn MutableCounters,
        dyn RepoPermissionChecker,
        dyn RepoLock,
        RepoConfig,
        dyn SegmentedChangelog,
        RepoEphemeralStore,
        MutableRenames,
        RepoCrossRepo,
        dyn AclRegions,
        RepoSparseProfiles,
        StreamingClone,
        CommitGraph,
    )]
    pub inner: InnerRepo,

    #[init(inner.repo_identity().name().to_string())]
    pub name: String,

    #[facet]
    pub warm_bookmarks_cache: dyn BookmarksCache,

    #[facet]
    pub hook_manager: HookManager,

    #[facet]
    pub repo_handler_base: RepoHandlerBase,

    #[facet]
    pub filestore_config: FilestoreConfig,
}

impl AsBlobRepo for Repo {
    fn as_blob_repo(&self) -> &BlobRepo {
        self.inner.as_blob_repo()
    }
}

#[derive(Clone)]
pub struct RepoContext {
    ctx: CoreContext,
    authz: Arc<AuthorizationContext>,
    repo: Arc<Repo>,
    push_redirector: Option<Arc<PushRedirector<Repo>>>,
}

impl fmt::Debug for RepoContext {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "RepoContext(repo={:?})", self.name())
    }
}

pub struct RepoContextBuilder {
    ctx: CoreContext,
    authz: Option<AuthorizationContext>,
    repo: Arc<Repo>,
    push_redirector: Option<Arc<PushRedirector<Repo>>>,
    bubble_id: Option<BubbleId>,
}

async fn maybe_push_redirector(
    ctx: &CoreContext,
    repo: &Arc<Repo>,
    repos: &MononokeRepos<Repo>,
) -> Result<Option<PushRedirector<Repo>>, MononokeError> {
    if tunables().disable_scs_pushredirect().unwrap_or_default() {
        return Ok(None);
    }
    let base = match repo.repo_handler_base().maybe_push_redirector_base.as_ref() {
        None => return Ok(None),
        Some(base) => base,
    };
    let live_commit_sync_config = repo.live_commit_sync_config();
    let enabled = live_commit_sync_config.push_redirector_enabled_for_public(repo.repoid());
    if enabled {
        let large_repo_id = base.common_commit_sync_config.large_repo_id;
        let large_repo = repos.get_by_id(large_repo_id.id()).ok_or_else(|| {
            MononokeError::InvalidRequest(format!("Large repo '{}' not found", large_repo_id))
        })?;
        Ok(Some(
            PushRedirectorArgs::new(
                large_repo,
                repo.clone(),
                base.synced_commit_mapping.clone(),
                base.target_repo_dbs.clone(),
            )
            .into_push_redirector(
                ctx,
                live_commit_sync_config,
                repo.inner_repo().repo_cross_repo.sync_lease().clone(),
            )
            .map_err(|e| MononokeError::InvalidRequest(e.to_string()))?,
        ))
    } else {
        Ok(None)
    }
}

impl RepoContextBuilder {
    pub(crate) async fn new(
        ctx: CoreContext,
        repo: Arc<Repo>,
        repos: &MononokeRepos<Repo>,
    ) -> Result<Self, MononokeError> {
        let push_redirector = maybe_push_redirector(&ctx, &repo, repos)
            .await?
            .map(Arc::new);
        Ok(RepoContextBuilder {
            ctx,
            authz: None,
            repo,
            push_redirector,
            bubble_id: None,
        })
    }

    pub async fn with_bubble<F, R>(mut self, bubble_fetcher: F) -> Result<Self, MononokeError>
    where
        F: FnOnce(RepoEphemeralStore) -> R,
        R: Future<Output = anyhow::Result<Option<BubbleId>>>,
    {
        self.bubble_id = bubble_fetcher(self.repo.repo_ephemeral_store().clone()).await?;
        Ok(self)
    }

    pub fn with_authorization_context(mut self, authz: AuthorizationContext) -> Self {
        self.authz = Some(authz);
        self
    }

    pub async fn build(self) -> Result<RepoContext, MononokeError> {
        let authz = Arc::new(
            self.authz
                .unwrap_or_else(|| AuthorizationContext::new(&self.ctx)),
        );
        RepoContext::new(
            self.ctx,
            authz,
            self.repo,
            self.bubble_id,
            self.push_redirector,
        )
        .await
    }
}

pub async fn open_synced_commit_mapping(
    fb: FacebookInit,
    config: RepoConfig,
    mysql_options: &MysqlOptions,
    readonly_storage: ReadOnlyStorage,
) -> Result<Arc<SqlSyncedCommitMapping>, Error> {
    let sql_factory = MetadataSqlFactory::new(
        fb,
        config.storage_config.metadata,
        mysql_options.clone(),
        readonly_storage,
    )
    .await?;

    Ok(Arc::new(
        sql_factory.open::<SqlSyncedCommitMapping>().await?,
    ))
}

impl Repo {
    /// Construct a new Repo based on an existing one with a bubble opened.
    pub fn with_bubble(&self, bubble: Bubble) -> Self {
        let blob_repo = self.inner.blob_repo.with_bubble(bubble);
        let inner = InnerRepo {
            blob_repo,
            ..self.inner.clone()
        };
        Self {
            name: self.name.clone(),
            inner,
            warm_bookmarks_cache: self.warm_bookmarks_cache.clone(),
            hook_manager: self.hook_manager.clone(),
            repo_handler_base: self.repo_handler_base.clone(),
            filestore_config: self.filestore_config.clone(),
        }
    }

    /// Construct a Repo from a test BlobRepo and commit_sync_config
    pub async fn new_test_xrepo(
        ctx: CoreContext,
        blob_repo: BlobRepo,
        live_commit_sync_config: Arc<dyn LiveCommitSyncConfig>,
        synced_commit_mapping: ArcSyncedCommitMapping,
    ) -> Result<Self, Error> {
        // TODO: Migrate more of this code to use the TestRepoFactory so that we can eventually
        // replace these test methods.
        let repo_factory: TestRepoFactory = TestRepoFactory::new(ctx.fb)?;

        let repo_id = blob_repo.repo_identity().id();

        let config = RepoConfig {
            infinitepush: InfinitepushParams {
                namespace: Some(InfinitepushNamespace::new(
                    Regex::new("scratch/.+").unwrap(),
                )),
                ..Default::default()
            },
            source_control_service: SourceControlServiceParams {
                permit_writes: true,
                ..Default::default()
            },
            hook_manager_params: Some(HookManagerParams {
                disable_acl_checker: true,
                ..Default::default()
            }),
            ..Default::default()
        };

        let name = blob_repo.repo_identity().name().to_string();
        let repo_blobstore = blob_repo.repo_blobstore_arc();
        let hook_manager = repo_factory.hook_manager(
            &blob_repo.repo_identity_arc(),
            &blob_repo.repo_derived_data_arc(),
            &blob_repo.bookmarks_arc(),
            &blob_repo.repo_blobstore_arc(),
        );
        let repo_cross_repo = Arc::new(RepoCrossRepo::new(
            synced_commit_mapping,
            live_commit_sync_config,
            Arc::new(InProcessLease::new()),
        ));
        let mutable_counters = repo_factory.mutable_counters(&blob_repo.repo_identity_arc())?;
        let repo_handler_base = repo_factory.repo_handler_base(
            &Arc::new(config.clone()),
            &repo_cross_repo,
            &blob_repo.repo_identity_arc(),
            &blob_repo.bookmarks_arc(),
            &blob_repo.bookmark_update_log_arc(),
            &mutable_counters,
        )?;

        let inner = InnerRepo {
            blob_repo,
            repo_config: Arc::new(config.clone()),
            segmented_changelog: Arc::new(DisabledSegmentedChangelog::new()),
            ephemeral_store: Arc::new(RepoEphemeralStore::disabled(repo_id)),
            mutable_renames: Arc::new(MutableRenames::new_test(
                repo_id,
                SqlMutableRenamesStore::with_sqlite_in_memory()?,
            )),
            repo_cross_repo,
            acl_regions: build_disabled_acl_regions(),
            sparse_profiles: Arc::new(RepoSparseProfiles::new(None)),
            streaming_clone: Arc::new(
                StreamingCloneBuilder::with_sqlite_in_memory()?.build(repo_id, repo_blobstore),
            ),
        };

        let mut warm_bookmarks_cache_builder = WarmBookmarksCacheBuilder::new(
            ctx.clone(),
            inner.bookmarks_arc(),
            inner.bookmark_update_log_arc(),
            inner.repo_identity_arc(),
        );
        warm_bookmarks_cache_builder
            .add_all_warmers(&inner.repo_derived_data_arc(), &inner.phases_arc())?;
        // We are constructing a test repo, so ensure the warm bookmark cache
        // is fully warmed, so that tests see up-to-date bookmarks.
        warm_bookmarks_cache_builder.wait_until_warmed();
        let warm_bookmarks_cache = warm_bookmarks_cache_builder.build().await?;
        let filestore_config = Arc::new(FilestoreConfig::no_chunking_filestore());

        Ok(Self {
            name: name.clone(),
            inner,
            warm_bookmarks_cache: Arc::new(warm_bookmarks_cache),
            hook_manager,
            repo_handler_base,
            filestore_config,
        })
    }

    /// The name of the underlying repo.
    pub fn name(&self) -> &String {
        &self.name
    }

    /// The internal id of the repo. Used for comparing the repo objects with each other.
    pub fn repoid(&self) -> RepositoryId {
        self.blob_repo().repo_identity().id()
    }

    /// The underlying `InnerRepo`.
    pub fn inner_repo(&self) -> &InnerRepo {
        &self.inner
    }

    /// The underlying `BlobRepo`.
    pub fn blob_repo(&self) -> &BlobRepo {
        &self.inner.blob_repo
    }

    /// `LiveCommitSyncConfig` instance to query current state of sync configs.
    pub fn live_commit_sync_config(&self) -> Arc<dyn LiveCommitSyncConfig> {
        self.inner.repo_cross_repo.live_commit_sync_config().clone()
    }

    /// The commit sync mapping for the referenced repository.
    pub fn synced_commit_mapping(&self) -> &ArcSyncedCommitMapping {
        self.inner.repo_cross_repo.synced_commit_mapping()
    }

    /// The commit sync lease for the referenced repository.
    pub fn x_repo_sync_lease(&self) -> &Arc<dyn LeaseOps> {
        self.inner.repo_cross_repo.sync_lease()
    }

    /// The warm bookmarks cache for the referenced repository.
    pub fn warm_bookmarks_cache(&self) -> &Arc<dyn BookmarksCache + Send + Sync> {
        &self.warm_bookmarks_cache
    }

    /// The configuration for the referenced repository.
    pub fn config(&self) -> &RepoConfig {
        &self.inner.repo_config
    }

    pub async fn report_monitoring_stats(&self, ctx: &CoreContext) -> Result<(), MononokeError> {
        match self.config().source_control_service_monitoring.as_ref() {
            None => {}
            Some(monitoring_config) => {
                for bookmark in monitoring_config.bookmarks_to_report_age.iter() {
                    self.report_bookmark_age_difference(ctx, bookmark).await?;
                }
            }
        }

        Ok(())
    }

    fn report_bookmark_missing_from_cache(&self, ctx: &CoreContext, bookmark: &BookmarkKey) {
        error!(
            ctx.logger(),
            "Monitored bookmark does not exist in the cache: {}, repo: {}",
            bookmark,
            self.repo_identity().name()
        );

        STATS::missing_from_cache.set_value(
            ctx.fb,
            1,
            (self.repo_identity().id(), bookmark.to_string()),
        );
    }

    fn report_bookmark_missing_from_repo(&self, ctx: &CoreContext, bookmark: &BookmarkKey) {
        error!(
            ctx.logger(),
            "Monitored bookmark does not exist in the repo: {}", bookmark
        );

        STATS::missing_from_repo.set_value(
            ctx.fb,
            1,
            (self.repo_identity().id(), bookmark.to_string()),
        );
    }

    fn report_bookmark_staleness(&self, ctx: &CoreContext, bookmark: &BookmarkKey, staleness: i64) {
        // Don't log if staleness is 0 to make output less spammy
        if staleness > 0 {
            debug!(
                ctx.logger(),
                "Reporting staleness of {} in repo {} to be {}s",
                bookmark,
                self.repo_identity().id(),
                staleness
            );
        }

        STATS::staleness.set_value(
            ctx.fb,
            staleness,
            (self.repo_identity().id(), bookmark.to_string()),
        );
    }

    async fn report_bookmark_age_difference(
        &self,
        ctx: &CoreContext,
        bookmark: &BookmarkKey,
    ) -> Result<(), MononokeError> {
        let repo = self.blob_repo();

        let maybe_bcs_id_from_service = self.warm_bookmarks_cache.get(ctx, bookmark).await?;
        let maybe_bcs_id_from_blobrepo = repo.bookmarks().get(ctx.clone(), bookmark).await?;

        if maybe_bcs_id_from_blobrepo.is_none() {
            self.report_bookmark_missing_from_repo(ctx, bookmark);
        }

        if maybe_bcs_id_from_service.is_none() {
            self.report_bookmark_missing_from_cache(ctx, bookmark);
        }

        if let (Some(service_bcs_id), Some(blobrepo_bcs_id)) =
            (maybe_bcs_id_from_service, maybe_bcs_id_from_blobrepo)
        {
            // We report the difference between current time (i.e. SystemTime::now())
            // and timestamp of the first child of bookmark value from cache (see graph below)
            //
            //       O <- bookmark value from blobrepo
            //       |
            //      ...
            //       |
            //       O <- first child of bookmark value from cache.
            //       |
            //       O <- bookmark value from cache, it's outdated
            //
            // This way of reporting shows for how long the oldest commit not in cache hasn't been
            // imported, and it should work correctly both for high and low commit rates.

            // Do not log if there's no lag to make output less spammy
            if blobrepo_bcs_id != service_bcs_id {
                debug!(
                    ctx.logger(),
                    "Reporting bookmark age difference for {}: latest {} value is {}, cache points to {}",
                    repo.repo_identity().id(),
                    bookmark,
                    blobrepo_bcs_id,
                    service_bcs_id,
                );
            }

            let difference = if blobrepo_bcs_id == service_bcs_id {
                0
            } else {
                let limit = 100;
                let maybe_child = self
                    .try_find_child(ctx, service_bcs_id, blobrepo_bcs_id, limit)
                    .await?;

                // If we can't find a child of a bookmark value from cache, then it might mean
                // that either cache is too far behind or there was a non-forward bookmark move.
                // Either way, we can't really do much about it here, so let's just find difference
                // between current timestamp and bookmark value from cache.
                let compare_bcs_id = maybe_child.unwrap_or(service_bcs_id);

                let compare_timestamp = compare_bcs_id
                    .load(ctx, repo.repo_blobstore())
                    .await?
                    .author_date()
                    .timestamp_secs();

                let current_timestamp = SystemTime::now()
                    .duration_since(UNIX_EPOCH)
                    .map_err(Error::from)?;
                let current_timestamp = current_timestamp.as_secs() as i64;
                current_timestamp - compare_timestamp
            };
            self.report_bookmark_staleness(ctx, bookmark, difference);
        }

        Ok(())
    }

    /// Try to find a changeset that's ancestor of `descendant` and direct child of
    /// `ancestor`. Returns None if this commit doesn't exist (for example if `ancestor` is not
    /// actually an ancestor of `descendant`) or if child is too far away from descendant.
    async fn try_find_child(
        &self,
        ctx: &CoreContext,
        ancestor: ChangesetId,
        descendant: ChangesetId,
        limit: u64,
    ) -> Result<Option<ChangesetId>, Error> {
        // This is a generation number beyond which we don't need to traverse
        let min_gen_num = self.fetch_gen_num(ctx, &ancestor).await?;

        let mut ancestors = AncestorsNodeStream::new(
            ctx.clone(),
            &self.blob_repo().changeset_fetcher_arc(),
            descendant,
        )
        .compat();

        let mut traversed = 0;
        while let Some(cs_id) = ancestors.next().await {
            traversed += 1;
            if traversed > limit {
                return Ok(None);
            }

            let cs_id = cs_id?;
            let parents = self
                .blob_repo()
                .changeset_fetcher()
                .get_parents(ctx, cs_id)
                .await?;

            if parents.contains(&ancestor) {
                return Ok(Some(cs_id));
            } else {
                let gen_num = self.fetch_gen_num(ctx, &cs_id).await?;
                if gen_num < min_gen_num {
                    return Ok(None);
                }
            }
        }

        Ok(None)
    }

    async fn fetch_gen_num(
        &self,
        ctx: &CoreContext,
        cs_id: &ChangesetId,
    ) -> Result<Generation, Error> {
        self.blob_repo()
            .changeset_fetcher()
            .get_generation_number(ctx, *cs_id)
            .await
    }
}

#[derive(Default)]
pub struct Stack {
    pub draft: Vec<ChangesetId>,
    pub public: Vec<ChangesetId>,
    pub leftover_heads: Vec<ChangesetId>,
}

pub struct BookmarkInfo {
    pub warm_changeset: ChangesetContext,
    pub fresh_changeset: ChangesetContext,
    pub last_update_timestamp: Timestamp,
}

/// A context object representing a query to a particular repo.
impl RepoContext {
    pub async fn new(
        ctx: CoreContext,
        authz: Arc<AuthorizationContext>,
        repo: Arc<Repo>,
        bubble_id: Option<BubbleId>,
        push_redirector: Option<Arc<PushRedirector<Repo>>>,
    ) -> Result<Self, MononokeError> {
        let ctx = ctx.with_mutated_scuba(|mut scuba| {
            scuba.add("permissions_model", format!("{:?}", authz));
            scuba
        });

        // Check the user is permitted to access this repo.
        authz.require_repo_metadata_read(&ctx, &repo.inner).await?;

        // Open the bubble if necessary.
        let repo = if let Some(bubble_id) = bubble_id {
            let bubble = repo.repo_ephemeral_store().open_bubble(bubble_id).await?;
            Arc::new(repo.with_bubble(bubble))
        } else {
            repo
        };

        Ok(Self {
            ctx,
            authz,
            repo,
            push_redirector,
        })
    }

    pub async fn new_test(ctx: CoreContext, repo: Arc<Repo>) -> Result<Self, MononokeError> {
        let authz = Arc::new(AuthorizationContext::new_bypass_access_control());
        RepoContext::new(ctx, authz, repo, None, None).await
    }

    /// The context for this query.
    pub fn ctx(&self) -> &CoreContext {
        &self.ctx
    }

    /// The name of the underlying repo.
    pub fn name(&self) -> &str {
        self.repo.name()
    }

    /// The internal id of the repo. Used for comparing the repo objects with each other.
    pub fn repoid(&self) -> RepositoryId {
        self.repo.repoid()
    }

    /// The authorization context of the request.
    pub fn authorization_context(&self) -> &AuthorizationContext {
        &self.authz
    }

    pub fn repo(&self) -> &Repo {
        self.repo.as_ref()
    }

    /// The underlying `InnerRepo`.
    pub fn inner_repo(&self) -> &InnerRepo {
        self.repo.inner_repo()
    }

    /// The underlying `BlobRepo`.
    pub fn blob_repo(&self) -> &BlobRepo {
        self.repo.blob_repo()
    }

    /// `LiveCommitSyncConfig` instance to query current state of sync configs.
    pub fn live_commit_sync_config(&self) -> Arc<dyn LiveCommitSyncConfig> {
        self.repo.live_commit_sync_config()
    }

    /// The ephemeral store for the referenced repository
    pub fn repo_ephemeral_store_arc(&self) -> ArcRepoEphemeralStore {
        self.repo.repo_ephemeral_store_arc()
    }

    /// The segmeneted changelog for the referenced repository.
    pub fn segmented_changelog(&self) -> &dyn SegmentedChangelog {
        self.repo.segmented_changelog()
    }

    /// The commit sync mapping for the referenced repository
    pub fn synced_commit_mapping(&self) -> &ArcSyncedCommitMapping {
        self.repo.synced_commit_mapping()
    }

    /// The warm bookmarks cache for the referenced repository.
    pub fn warm_bookmarks_cache(&self) -> &Arc<dyn BookmarksCache + Send + Sync> {
        self.repo.warm_bookmarks_cache()
    }

    /// The repo blobstore for the referenced repository.
    pub fn repo_blobstore(&self) -> ArcRepoBlobstore {
        self.repo.repo_blobstore_arc()
    }

    /// The hook manager for the referenced repository.
    pub fn hook_manager(&self) -> Arc<HookManager> {
        self.repo.hook_manager_arc()
    }

    /// The base for push redirection logic for this repo
    pub fn maybe_push_redirector_base(&self) -> Option<&PushRedirectorBase> {
        self.repo
            .repo_handler_base
            .maybe_push_redirector_base
            .as_ref()
            .map(AsRef::as_ref)
    }

    /// The configuration for the referenced repository.
    pub fn config(&self) -> &RepoConfig {
        self.repo.config()
    }

    pub fn mutable_renames(&self) -> ArcMutableRenames {
        self.repo.mutable_renames_arc()
    }

    pub fn sparse_profiles(&self) -> ArcRepoSparseProfiles {
        self.repo.repo_sparse_profiles_arc()
    }

    pub fn derive_changeset_info_enabled(&self) -> bool {
        self.blob_repo()
            .repo_derived_data()
            .config()
            .is_enabled(ChangesetInfo::NAME)
    }

    pub fn derive_hgchangesets_enabled(&self) -> bool {
        self.blob_repo()
            .repo_derived_data()
            .config()
            .is_enabled(MappedHgChangesetId::NAME)
    }

    /// Load bubble from id
    pub async fn open_bubble(&self, bubble_id: BubbleId) -> Result<Bubble, MononokeError> {
        Ok(self
            .repo
            .repo_ephemeral_store()
            .open_bubble(bubble_id)
            .await?)
    }

    // pub(crate) for testing
    pub(crate) async fn changesets(
        &self,
        bubble_id: Option<BubbleId>,
    ) -> Result<Arc<dyn Changesets>, MononokeError> {
        Ok(match bubble_id {
            Some(id) => Arc::new(self.open_bubble(id).await?.changesets(self.blob_repo())),
            None => self.blob_repo().changesets_arc(),
        })
    }

    /// Test whether a changeset exists in a particular storage location.
    pub async fn changeset_exists(
        &self,
        changeset_id: ChangesetId,
        storage_location: StorageLocation,
    ) -> Result<bool, MononokeError> {
        use StorageLocation::*;
        let bubble_id = match storage_location {
            Persistent => None,
            Bubble(id) => Some(id),
            UnknownBubble => match self
                .repo
                .repo_ephemeral_store()
                .bubble_from_changeset(&changeset_id)
                .await?
            {
                Some(id) => Some(id),
                None => return Ok(false),
            },
        };
        Ok(self
            .changesets(bubble_id)
            .await?
            .exists(&self.ctx, changeset_id)
            .await?)
    }

    /// Look up a changeset specifier to find the canonical bonsai changeset
    /// ID for a changeset.
    pub async fn resolve_specifier(
        &self,
        specifier: ChangesetSpecifier,
    ) -> Result<Option<ChangesetId>, MononokeError> {
        let id = match specifier {
            ChangesetSpecifier::Bonsai(cs_id) => self
                .changeset_exists(cs_id, StorageLocation::Persistent)
                .await?
                .then_some(cs_id),
            ChangesetSpecifier::EphemeralBonsai(cs_id, bubble_id) => self
                .changeset_exists(cs_id, StorageLocation::ephemeral(bubble_id))
                .await?
                .then_some(cs_id),
            ChangesetSpecifier::Hg(hg_cs_id) => {
                self.blob_repo()
                    .bonsai_hg_mapping()
                    .get_bonsai_from_hg(&self.ctx, hg_cs_id)
                    .await?
            }
            ChangesetSpecifier::Globalrev(rev) => {
                self.blob_repo()
                    .bonsai_globalrev_mapping()
                    .get_bonsai_from_globalrev(&self.ctx, rev)
                    .await?
            }
            ChangesetSpecifier::Svnrev(rev) => {
                self.blob_repo()
                    .bonsai_svnrev_mapping()
                    .get_bonsai_from_svnrev(&self.ctx, rev)
                    .await?
            }
            ChangesetSpecifier::GitSha1(git_sha1) => {
                self.blob_repo()
                    .bonsai_git_mapping()
                    .get_bonsai_from_git_sha1(&self.ctx, git_sha1)
                    .await?
            }
        };
        Ok(id)
    }

    /// Resolve a bookmark to a changeset.
    pub async fn resolve_bookmark(
        &self,
        bookmark: &BookmarkKey,
        freshness: BookmarkFreshness,
    ) -> Result<Option<ChangesetContext>, MononokeError> {
        let mut cs_id = match freshness {
            BookmarkFreshness::MaybeStale => {
                self.warm_bookmarks_cache().get(&self.ctx, bookmark).await?
            }
            BookmarkFreshness::MostRecent => None,
        };

        // If the bookmark wasn't found in the warm bookmarks cache, it might
        // be a scratch bookmark, so always do the look-up.
        if cs_id.is_none() {
            cs_id = self
                .blob_repo()
                .bookmarks()
                .get(self.ctx.clone(), bookmark)
                .await?
        }

        Ok(cs_id.map(|cs_id| ChangesetContext::new(self.clone(), cs_id)))
    }

    /// Resolve a changeset id by its prefix
    pub async fn resolve_changeset_id_prefix(
        &self,
        prefix: ChangesetPrefixSpecifier,
    ) -> Result<ChangesetSpecifierPrefixResolution, MononokeError> {
        const MAX_LIMIT_AMBIGUOUS_IDS: usize = 10;
        let resolved = match prefix {
            ChangesetPrefixSpecifier::Hg(prefix) => ChangesetSpecifierPrefixResolution::from(
                self.blob_repo()
                    .bonsai_hg_mapping()
                    .get_many_hg_by_prefix(&self.ctx, prefix, MAX_LIMIT_AMBIGUOUS_IDS)
                    .await?,
            ),
            ChangesetPrefixSpecifier::Bonsai(prefix) => ChangesetSpecifierPrefixResolution::from(
                self.blob_repo()
                    .changesets()
                    .get_many_by_prefix(&self.ctx, prefix, MAX_LIMIT_AMBIGUOUS_IDS)
                    .await?,
            ),
            ChangesetPrefixSpecifier::GitSha1(prefix) => ChangesetSpecifierPrefixResolution::from(
                self.blob_repo()
                    .bonsai_git_mapping()
                    .get_many_git_sha1_by_prefix(&self.ctx, prefix, MAX_LIMIT_AMBIGUOUS_IDS)
                    .await?,
            ),
            ChangesetPrefixSpecifier::Globalrev(prefix) => {
                ChangesetSpecifierPrefixResolution::from(
                    self.blob_repo()
                        .bonsai_globalrev_mapping()
                        .get_closest_globalrev(&self.ctx, prefix)
                        .await?,
                )
            }
        };
        Ok(resolved)
    }

    /// Look up a changeset by specifier.
    pub async fn changeset(
        &self,
        specifier: impl Into<ChangesetSpecifier>,
    ) -> Result<Option<ChangesetContext>, MononokeError> {
        let specifier = specifier.into();
        let changeset = self
            .resolve_specifier(specifier)
            .await?
            .map(|cs_id| ChangesetContext::new(self.clone(), cs_id));
        Ok(changeset)
    }

    pub async fn difference_of_unions_of_ancestors<'a>(
        &'a self,
        includes: Vec<ChangesetId>,
        excludes: Vec<ChangesetId>,
    ) -> Result<impl Stream<Item = Result<ChangesetContext, MononokeError>> + 'a, MononokeError>
    {
        let repo = self.clone();

        Ok(self
            .repo()
            .commit_graph()
            .ancestors_difference_stream(&self.ctx, includes, excludes)
            .await?
            .map_ok(move |cs_id| ChangesetContext::new(repo.clone(), cs_id))
            .map_err(|err| err.into()))
    }

    /// Get Mercurial ID for multiple changesets
    ///
    /// This is a more efficient version of:
    /// ```ignore
    /// let ids: Vec<ChangesetId> = ...;
    /// ids.into_iter().map(|id| {
    ///     let hg_id = repo
    ///         .changeset(ChangesetSpecifier::Bonsai(id))
    ///         .await
    ///         .hg_id();
    ///     (id, hg_id)
    /// });
    /// ```
    pub async fn many_changeset_hg_ids(
        &self,
        changesets: Vec<ChangesetId>,
    ) -> Result<Vec<(ChangesetId, HgChangesetId)>, MononokeError> {
        let mapping = self
            .blob_repo()
            .get_hg_bonsai_mapping(self.ctx.clone(), changesets)
            .await?
            .into_iter()
            .map(|(hg_cs_id, cs_id)| (cs_id, hg_cs_id))
            .collect();
        Ok(mapping)
    }

    /// Get changeset ID from Mercurial ID for multiple changesets
    pub async fn many_changeset_ids_from_hg(
        &self,
        changesets: Vec<HgChangesetId>,
    ) -> Result<Vec<(HgChangesetId, ChangesetId)>, MononokeError> {
        let mapping = self
            .blob_repo()
            .get_hg_bonsai_mapping(self.ctx.clone(), changesets)
            .await?;
        Ok(mapping)
    }

    /// Similar to many_changeset_hg_ids, but returning Git-SHA1s.
    pub async fn many_changeset_git_sha1s(
        &self,
        changesets: Vec<ChangesetId>,
    ) -> Result<Vec<(ChangesetId, GitSha1)>, MononokeError> {
        let mapping = self
            .blob_repo()
            .bonsai_git_mapping()
            .get(&self.ctx, changesets.into())
            .await?
            .into_iter()
            .map(|entry| (entry.bcs_id, entry.git_sha1))
            .collect();
        Ok(mapping)
    }

    /// Get changeset ID from Git-SHA1 for multiple changesets
    pub async fn many_changeset_ids_from_git_sha1(
        &self,
        changesets: Vec<GitSha1>,
    ) -> Result<Vec<(GitSha1, ChangesetId)>, MononokeError> {
        let mapping = self
            .blob_repo()
            .bonsai_git_mapping()
            .get(&self.ctx, changesets.into())
            .await?
            .into_iter()
            .map(|entry| (entry.git_sha1, entry.bcs_id))
            .collect();
        Ok(mapping)
    }

    /// Similar to many_changeset_hg_ids, but returning Globalrevs.
    pub async fn many_changeset_globalrev_ids(
        &self,
        changesets: Vec<ChangesetId>,
    ) -> Result<Vec<(ChangesetId, Globalrev)>, MononokeError> {
        let mapping = self
            .blob_repo()
            .bonsai_globalrev_mapping()
            .get(&self.ctx, changesets.into())
            .await?
            .into_iter()
            .map(|entry| (entry.bcs_id, entry.globalrev))
            .collect();
        Ok(mapping)
    }

    /// Get changeset ID from Globalrev for multiple changesets
    pub async fn many_changeset_ids_from_globalrev(
        &self,
        changesets: Vec<Globalrev>,
    ) -> Result<Vec<(Globalrev, ChangesetId)>, MononokeError> {
        let mapping = self
            .blob_repo()
            .bonsai_globalrev_mapping()
            .get(&self.ctx, changesets.into())
            .await?
            .into_iter()
            .map(|entry| (entry.globalrev, entry.bcs_id))
            .collect();
        Ok(mapping)
    }

    /// Similar to many_changeset_hg_ids, but returning Svnrevs.
    pub async fn many_changeset_svnrev_ids(
        &self,
        changesets: Vec<ChangesetId>,
    ) -> Result<Vec<(ChangesetId, Svnrev)>, MononokeError> {
        let mapping = self
            .blob_repo()
            .bonsai_svnrev_mapping()
            .get(&self.ctx, changesets.into())
            .await?
            .into_iter()
            .map(|entry| (entry.bcs_id, entry.svnrev))
            .collect();
        Ok(mapping)
    }

    pub async fn many_changeset_parents(
        &self,
        changesets: Vec<ChangesetId>,
    ) -> Result<HashMap<ChangesetId, Vec<ChangesetId>>, MononokeError> {
        let parents = self
            .blob_repo()
            .changesets()
            .get_many(&self.ctx, changesets)
            .await?
            .into_iter()
            .map(|entry| (entry.cs_id, entry.parents))
            .collect();
        Ok(parents)
    }

    /// Return comprehensive bookmark info including last update time
    /// Currently works only for public bookmarks.
    pub async fn bookmark_info(
        &self,
        bookmark: impl AsRef<str>,
    ) -> Result<Option<BookmarkInfo>, MononokeError> {
        // a non ascii bookmark name is an invalid request
        let bookmark = BookmarkKey::new(bookmark.as_ref())
            .map_err(|e| MononokeError::InvalidRequest(e.to_string()))?;

        let (maybe_warm_cs_id, maybe_log_entry) = try_join!(
            self.warm_bookmarks_cache().get(&self.ctx, &bookmark),
            async {
                let mut entries_stream = self
                    .repo
                    .blob_repo()
                    .bookmark_update_log()
                    .list_bookmark_log_entries(
                        self.ctx.clone(),
                        bookmark.clone(),
                        1,
                        None,
                        Freshness::MaybeStale,
                    );
                entries_stream.next().await.transpose()
            }
        )?;

        let maybe_warm_changeset =
            maybe_warm_cs_id.map(|cs_id| ChangesetContext::new(self.clone(), cs_id));

        let (_id, maybe_fresh_cs_id, _reason, timestamp) = maybe_log_entry
            .ok_or_else(|| anyhow!("Bookmark update log has no entries for queried bookmark!"))?;

        let fresh_cs_id = match maybe_fresh_cs_id {
            Some(cs_id) => cs_id,
            None => {
                return Ok(None);
            }
        };
        let fresh_changeset = ChangesetContext::new(self.clone(), fresh_cs_id);

        let last_update_timestamp = timestamp;

        // If the bookmark wasn't found in the warm bookmarks cache return
        // the fresh value for simplicity.
        let warm_changeset = maybe_warm_changeset.unwrap_or_else(|| fresh_changeset.clone());

        Ok(Some(BookmarkInfo {
            warm_changeset,
            fresh_changeset,
            last_update_timestamp,
        }))
    }

    /// Get a list of bookmarks.
    pub async fn list_bookmarks(
        &self,
        include_scratch: bool,
        prefix: Option<&str>,
        after: Option<&str>,
        limit: Option<u64>,
    ) -> Result<impl Stream<Item = Result<(String, ChangesetId), MononokeError>> + '_, MononokeError>
    {
        if include_scratch {
            if prefix.is_none() {
                return Err(MononokeError::InvalidRequest(
                    "prefix required to list scratch bookmarks".to_string(),
                ));
            }
            if limit.is_none() {
                return Err(MononokeError::InvalidRequest(
                    "limit required to list scratch bookmarks".to_string(),
                ));
            }
        }

        let prefix = match prefix {
            Some(prefix) => BookmarkPrefix::new(prefix).map_err(|e| {
                MononokeError::InvalidRequest(format!(
                    "invalid bookmark prefix '{}': {}",
                    prefix, e
                ))
            })?,
            None => BookmarkPrefix::empty(),
        };

        let pagination = match after {
            Some(after) => {
                let name = BookmarkName::new(after).map_err(|e| {
                    MononokeError::InvalidRequest(format!(
                        "invalid bookmark name '{}': {}",
                        after, e
                    ))
                })?;
                BookmarkPagination::After(name)
            }
            None => BookmarkPagination::FromStart,
        };

        if include_scratch {
            // Scratch bookmarks must be queried directly from the blobrepo as
            // they are not stored in the cache.  To maintain ordering with
            // public bookmarks, query all the bookmarks we are interested in.
            let blob_repo = self.blob_repo();
            let cache = self.warm_bookmarks_cache();
            let bookmarks = blob_repo
                .bookmarks()
                .list(
                    self.ctx.clone(),
                    BookmarkFreshness::MaybeStale,
                    &prefix,
                    BookmarkCategory::ALL,
                    BookmarkKind::ALL,
                    &pagination,
                    limit.unwrap_or(std::u64::MAX),
                )
                .try_filter_map(move |(bookmark, cs_id)| async move {
                    if bookmark.kind() == &BookmarkKind::Scratch {
                        Ok(Some((bookmark.into_key().into_string(), cs_id)))
                    } else {
                        // For non-scratch bookmarks, always return the value
                        // from the cache so that clients only ever see the
                        // warm value.  If the bookmark is newly created and
                        // has no warm value, this might mean we have to
                        // filter this bookmark out.
                        let bookmark_name = bookmark.into_key();
                        let maybe_cs_id = cache.get(&self.ctx, &bookmark_name).await?;
                        Ok(maybe_cs_id.map(|cs_id| (bookmark_name.into_string(), cs_id)))
                    }
                })
                .map_err(MononokeError::from)
                .boxed();
            Ok(bookmarks)
        } else {
            // Public bookmarks can be fetched from the warm bookmarks cache.
            let cache = self.warm_bookmarks_cache();
            Ok(
                stream::iter(cache.list(&self.ctx, &prefix, &pagination, limit).await?)
                    .map(|(bookmark, (cs_id, _kind))| Ok((bookmark.into_string(), cs_id)))
                    .boxed(),
            )
        }
    }

    /// Get a stack for the list of heads (up to the first public commit).
    ///
    /// Limit constrains the number of draft commits returned.
    /// Algo is designed to minimize number of db queries.
    /// Missing changesets are skipped.
    /// Changesets are returned in topological order (requested heads first)
    ///
    /// When the limit is reached returns the heads of which children were not processed to allow
    /// for continuation of the processing.
    pub async fn stack(
        &self,
        changesets: Vec<ChangesetId>,
        limit: usize,
    ) -> Result<Stack, MononokeError> {
        if limit == 0 {
            return Ok(Default::default());
        }

        // initialize visited
        let mut visited: HashSet<_> = changesets.iter().cloned().collect();

        let phases = self.blob_repo().phases();

        // get phases
        let public_phases = phases
            .get_public(&self.ctx, changesets.clone(), false)
            .await?;

        // partition
        let (mut public, mut draft): (Vec<_>, Vec<_>) = changesets
            .into_iter()
            .partition(|cs_id| public_phases.contains(cs_id));

        // initialize the queue
        let mut queue: Vec<_> = draft.to_vec();

        while !queue.is_empty() {
            // get the unique parents for all changesets in the queue & skip visited & update visited
            let parents: Vec<_> = self
                .blob_repo()
                .changesets()
                .get_many(&self.ctx, queue.clone())
                .await?
                .into_iter()
                .flat_map(|cs_entry| cs_entry.parents)
                .filter(|cs_id| !visited.contains(cs_id))
                .unique()
                .collect();

            visited.extend(parents.iter().cloned());

            // get phases for the parents
            let public_phases = phases.get_public(&self.ctx, parents.clone(), false).await?;

            // partition
            let (new_public, new_draft): (Vec<_>, Vec<_>) = parents
                .into_iter()
                .partition(|cs_id| public_phases.contains(cs_id));

            // respect the limit
            if draft.len() + new_draft.len() > limit {
                break;
            }

            // update queue and level
            queue = new_draft.clone();

            // update draft & public
            public.extend(new_public.into_iter());
            draft.extend(new_draft.into_iter());
        }

        Ok(Stack {
            draft,
            public,
            leftover_heads: queue,
        })
    }

    /// Get a Tree by id.  Returns `None` if the tree doesn't exist.
    pub async fn tree(&self, tree_id: TreeId) -> Result<Option<TreeContext>, MononokeError> {
        TreeContext::new_check_exists(self.clone(), tree_id).await
    }

    /// Get a File by id.  Returns `None` if the file doesn't exist.
    pub async fn file(&self, file_id: FileId) -> Result<Option<FileContext>, MononokeError> {
        FileContext::new_check_exists(self.clone(), FetchKey::Canonical(file_id)).await
    }

    /// Get a File by content sha-1.  Returns `None` if the file doesn't exist.
    pub async fn file_by_content_sha1(
        &self,
        hash: Sha1,
    ) -> Result<Option<FileContext>, MononokeError> {
        FileContext::new_check_exists(self.clone(), FetchKey::Aliased(Alias::Sha1(hash))).await
    }

    /// Get a File by content sha-256.  Returns `None` if the file doesn't exist.
    pub async fn file_by_content_sha256(
        &self,
        hash: Sha256,
    ) -> Result<Option<FileContext>, MononokeError> {
        FileContext::new_check_exists(self.clone(), FetchKey::Aliased(Alias::Sha256(hash))).await
    }

    /// Get a File by content git-sha-1.  Returns `None` if the file doesn't exist.
    pub async fn file_by_content_gitsha1(
        &self,
        hash: GitSha1,
    ) -> Result<Option<FileContext>, MononokeError> {
        FileContext::new_check_exists(self.clone(), FetchKey::Aliased(Alias::GitSha1(hash))).await
    }

    pub async fn upload_file_content(
        &self,
        content: Bytes,
        store_request: &StoreRequest,
    ) -> Result<ContentId, MononokeError> {
        let metadata = filestore::store(
            self.repo.repo_blobstore(),
            *self.repo.filestore_config(),
            &self.ctx,
            store_request,
            stream::once(async move { Ok(content) }),
        )
        .await?;
        Ok(metadata.content_id)
    }

    /// Get a File by content seeded-blake3. Returns `None` if the file doesn't exist.
    pub async fn file_by_content_seeded_blake3(
        &self,
        hash: Blake3,
    ) -> Result<Option<FileContext>, MononokeError> {
        FileContext::new_check_exists(self.clone(), FetchKey::Aliased(Alias::SeededBlake3(hash)))
            .await
    }

    fn target_repo(&self) -> Target<Repo> {
        Target(self.repo().clone())
    }

    async fn build_candidate_selection_hint(
        &self,
        maybe_args: Option<CandidateSelectionHintArgs>,
        other_repo_context: &Self,
    ) -> Result<CandidateSelectionHint<Repo>, MononokeError> {
        let args = match maybe_args {
            None => return Ok(CandidateSelectionHint::Only),
            Some(args) => args,
        };

        use CandidateSelectionHintArgs::*;
        match args {
            OnlyOrAncestorOfBookmark(bookmark) => {
                let repo = other_repo_context.target_repo();
                Ok(CandidateSelectionHint::OnlyOrAncestorOfBookmark(
                    Target(bookmark),
                    repo,
                ))
            }
            OnlyOrDescendantOfBookmark(bookmark) => {
                let repo = other_repo_context.target_repo();
                Ok(CandidateSelectionHint::OnlyOrDescendantOfBookmark(
                    Target(bookmark),
                    repo,
                ))
            }
            OnlyOrAncestorOfCommit(specifier) => {
                let repo = other_repo_context.target_repo();
                let cs_id = other_repo_context
                    .resolve_specifier(specifier)
                    .await?
                    .ok_or_else(|| {
                        MononokeError::InvalidRequest(format!(
                            "unknown commit specifier {}",
                            specifier
                        ))
                    })?;
                Ok(CandidateSelectionHint::OnlyOrAncestorOfCommit(
                    Target(cs_id),
                    repo,
                ))
            }
            OnlyOrDescendantOfCommit(specifier) => {
                let repo = other_repo_context.target_repo();
                let cs_id = other_repo_context
                    .resolve_specifier(specifier)
                    .await?
                    .ok_or_else(|| {
                        MononokeError::InvalidRequest(format!(
                            "unknown commit specifier {}",
                            specifier
                        ))
                    })?;
                Ok(CandidateSelectionHint::OnlyOrDescendantOfCommit(
                    Target(cs_id),
                    repo,
                ))
            }
            Exact(specifier) => {
                let cs_id = other_repo_context
                    .resolve_specifier(specifier)
                    .await?
                    .ok_or_else(|| {
                        MononokeError::InvalidRequest(format!(
                            "unknown commit specifier {}",
                            specifier
                        ))
                    })?;
                Ok(CandidateSelectionHint::Exact(Target(cs_id)))
            }
        }
    }

    /// Get the equivalent changeset from another repo - it will sync it if needed
    pub async fn xrepo_commit_lookup(
        &self,
        other: &Self,
        specifier: impl Into<ChangesetSpecifier>,
        maybe_candidate_selection_hint_args: Option<CandidateSelectionHintArgs>,
    ) -> Result<Option<ChangesetContext>, MononokeError> {
        let common_config = self
            .live_commit_sync_config()
            .get_common_config(self.blob_repo().repo_identity().id())
            .map_err(|e| {
                MononokeError::InvalidRequest(format!(
                    "Commits from {} are not configured to be remapped to another repo: {}",
                    self.repo.name, e
                ))
            })?;

        let candidate_selection_hint: CandidateSelectionHint<Repo> = self
            .build_candidate_selection_hint(maybe_candidate_selection_hint_args, other)
            .await?;

        let commit_sync_repos =
            CommitSyncRepos::new(self.repo().clone(), other.repo().clone(), &common_config)?;

        let specifier = specifier.into();
        let changeset = self.resolve_specifier(specifier).await?.ok_or_else(|| {
            MononokeError::InvalidRequest(format!("unknown commit specifier {}", specifier))
        })?;

        let commit_syncer = CommitSyncer::new(
            &self.ctx,
            self.synced_commit_mapping().clone(),
            commit_sync_repos,
            self.live_commit_sync_config(),
            self.repo.x_repo_sync_lease().clone(),
        );

        let maybe_cs_id = commit_syncer
            .sync_commit(
                &self.ctx,
                changeset,
                candidate_selection_hint,
                CommitSyncContext::ScsXrepoLookup,
                false,
            )
            .await?;
        Ok(maybe_cs_id.map(|cs_id| ChangesetContext::new(other.clone(), cs_id)))
    }

    /// Start a write to the repo.
    pub fn start_write(&self) -> Result<(), MononokeError> {
        if self.authz.is_service() {
            if !self.config().source_control_service.permit_service_writes {
                return Err(MononokeError::InvalidRequest(String::from(
                    "Service writes are disabled in configuration for this repo",
                )));
            }
        } else if !self.config().source_control_service.permit_writes {
            return Err(MononokeError::InvalidRequest(String::from(
                "Writes are disabled in configuration for this repo",
            )));
        }

        self.ctx
            .scuba()
            .clone()
            .log_with_msg("Write request start", None);

        Ok(())
    }

    /// Reads a value out of the underlying config, indicating if we support writes without parents in this repo.
    pub fn allow_no_parent_writes(&self) -> bool {
        return self
            .config()
            .source_control_service
            .permit_commits_without_parents;
    }

    /// A SegmentedChangelog client repository has a compressed shape of the commit graph but
    /// doesn't know the identifiers for all the commits in the graph. It only knows the
    /// identifiers for select commits called "known" commits. These repositories can query
    /// the server to get the identifiers of the commits they don't have using the location
    /// of the desired commit relative to one of the "known" commits.
    /// The current version has all parents of merge commits downloaded to clients so that
    /// locations can be expressed using only the unique descendant distance to one of these
    /// commits. The heads of the repo are also known.
    /// Let's assume our graph is `0 - a - b - c`.
    /// In this example our initial commit is `0`, then we have `a` the first commit, `b` second,
    /// `c` third.
    /// For `descendant = c` and `distance = 2` we want to return `a`.
    pub async fn location_to_changeset_id(
        &self,
        location: Location<ChangesetId>,
        count: u64,
    ) -> Result<Vec<ChangesetId>, MononokeError> {
        let segmented_changelog = self.repo.segmented_changelog();
        let ancestor = segmented_changelog
            .location_to_many_changeset_ids(&self.ctx, location, count)
            .await
            .map_err(MononokeError::from)?;
        Ok(ancestor)
    }

    /// A Segmented Changelog client needs to know how to translate between a commit hash,
    /// for example one that is provided by the user, and the information that it has locally,
    /// the shape of the graph, i.e. a location in the graph.
    pub async fn many_changeset_ids_to_locations(
        &self,
        master_heads: Vec<ChangesetId>,
        cs_ids: Vec<ChangesetId>,
    ) -> Result<HashMap<ChangesetId, Result<Location<ChangesetId>, MononokeError>>, MononokeError>
    {
        let segmented_changelog = self.repo.segmented_changelog();
        let result = segmented_changelog
            .many_changeset_ids_to_locations(&self.ctx, master_heads, cs_ids)
            .await
            .map(|ok| {
                ok.into_iter()
                    .map(|(k, v)| (k, v.map_err(Into::into)))
                    .collect::<HashMap<ChangesetId, Result<_, MononokeError>>>()
            })
            .map_err(MononokeError::from)?;
        Ok(result)
    }

    pub async fn segmented_changelog_clone_data(
        &self,
    ) -> Result<(CloneData<ChangesetId>, HashMap<ChangesetId, HgChangesetId>), MononokeError> {
        let segmented_changelog = self.repo.segmented_changelog();
        let clone_data = segmented_changelog
            .clone_data(&self.ctx)
            .await
            .map_err(MononokeError::from)?;
        Ok(clone_data)
    }

    pub async fn segmented_changelog_disabled(&self) -> Result<bool, MononokeError> {
        let segmented_changelog = self.repo.segmented_changelog();
        let disabled = segmented_changelog
            .disabled(&self.ctx)
            .await
            .map_err(MononokeError::from)?;
        Ok(disabled)
    }

    pub async fn segmented_changelog_pull_data(
        &self,
        common: Vec<ChangesetId>,
        missing: Vec<ChangesetId>,
    ) -> Result<CloneData<ChangesetId>, MononokeError> {
        let segmented_changelog = self.repo.segmented_changelog();
        let pull_data = segmented_changelog
            .pull_data(&self.ctx, common, missing)
            .await
            .map_err(MononokeError::from)?;
        Ok(pull_data)
    }

    pub async fn prepare_derived_data(
        &self,
        derivable_type: DerivableType,
        csids: Vec<ChangesetId>,
    ) -> Result<(), MononokeError> {
        // Simple initial implementation: does not support types with
        // dependencies, and only supports a single type at a time.
        match derivable_type {
            DerivableType::Fsnodes => {
                self.repo
                    .repo_derived_data()
                    .manager()
                    .derive_exactly_batch::<RootFsnodeId>(
                        self.ctx(),
                        csids,
                        BatchDeriveOptions::Parallel { gap_size: None },
                        None,
                    )
                    .await?;
            }
            DerivableType::SkeletonManifests => {
                self.repo
                    .repo_derived_data()
                    .manager()
                    .derive_exactly_batch::<RootSkeletonManifestId>(
                        self.ctx(),
                        csids,
                        BatchDeriveOptions::Parallel { gap_size: None },
                        None,
                    )
                    .await?;
            }
            _ => {
                return Err(MononokeError::InvalidRequest(format!(
                    "Unsupported derived data type for preparation: {}",
                    derivable_type
                )));
            }
        }
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use std::str::FromStr;

    use fixtures::Linear;
    use fixtures::MergeEven;
    use fixtures::TestRepoFixture;

    use super::*;

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

        let ancestor = ChangesetId::from_str(
            "c9f9a2a39195a583d523a4e5f6973443caeb0c66a315d5bf7db1b5775c725310",
        )?;
        let descendant = ChangesetId::from_str(
            "7785606eb1f26ff5722c831de402350cf97052dc44bc175da6ac0d715a3dbbf6",
        )?;

        let maybe_child = repo.try_find_child(&ctx, ancestor, descendant, 100).await?;
        let child = maybe_child.ok_or_else(|| anyhow!("didn't find child"))?;
        assert_eq!(
            child,
            ChangesetId::from_str(
                "98ef3234c2f1acdbb272715e8cfef4a6378e5443120677e0d87d113571280f79"
            )?
        );

        let maybe_child = repo.try_find_child(&ctx, ancestor, descendant, 1).await?;
        assert!(maybe_child.is_none());

        Ok(())
    }

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

        let ancestor = ChangesetId::from_str(
            "35fb4e0fb3747b7ca4d18281d059be0860d12407dc5dce5e02fb99d1f6a79d2a",
        )?;
        let descendant = ChangesetId::from_str(
            "567a25d453cafaef6550de955c52b91bf9295faf38d67b6421d5d2e532e5adef",
        )?;

        let maybe_child = repo.try_find_child(&ctx, ancestor, descendant, 100).await?;
        let child = maybe_child.ok_or_else(|| anyhow!("didn't find child"))?;
        assert_eq!(child, descendant);
        Ok(())
    }
}

impl PartialEq for RepoContext {
    fn eq(&self, other: &Self) -> bool {
        self.repoid() == other.repoid()
    }
}
impl Eq for RepoContext {}

impl Hash for RepoContext {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.repoid().hash(state);
    }
}


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

use std::collections::HashMap;
use std::collections::HashSet;
use std::ops::Not;
use std::sync::Arc;

use anyhow::anyhow;
use anyhow::Context;
use anyhow::Result;
use blobstore::Loadable;
use cloned::cloned;
use context::CoreContext;
use futures::stream;
use futures::try_join;
use futures::FutureExt;
use futures::StreamExt;
use futures::TryStreamExt;
use maplit::btreeset;
use metaconfig_types::SparseProfilesConfig;
use mononoke_types::fsnode::FsnodeEntry;
use mononoke_types::MPath;
use pathmatcher::DirectoryMatch;
use pathmatcher::Matcher;
use repo_blobstore::RepoBlobstoreRef;
use repo_sparse_profiles::RepoSparseProfiles;
use slog::debug;
use types::RepoPath;

use crate::errors::MononokeError;
use crate::ChangesetContext;
use crate::ChangesetDiffItem;
use crate::ChangesetFileOrdering;
use crate::ChangesetPathContentContext;
use crate::ChangesetPathDiffContext;
use crate::MononokePath;
use crate::PathEntry;

// This struct contains matchers which will be consulted in various scenarious
// Clients can request analysis either for ALL profiles or Set of interested
//  1. If client requests exact list of profiles, then `exact_profiles_matcher`
//     will be used to check for the config changed and profiles will be
//     located using `changeset::find_files()`
//  2. If client asks to analyse all profiles - we check the configuration:
//     a. If list of monitored_profiles is set, then we use
//        `monitoring_profiles_only_matcher`
//     b. If monitored_profiles is None, then profiles would be checked
//        in configured location with respect of optional excludes.
pub struct SparseProfileMonitoring {
    sql_sparse_profiles: Arc<RepoSparseProfiles>,
    sparse_config: SparseProfilesConfig,
    exact_profiles_matcher: pathmatcher::TreeMatcher,
    profiles_location_with_excludes_matcher: pathmatcher::TreeMatcher,
    monitoring_profiles_only_matcher: Option<pathmatcher::TreeMatcher>,
    monitoring_profiles: MonitoringProfiles,
}

impl SparseProfileMonitoring {
    pub fn new(
        repo_name: &str,
        sql_sparse_profiles: Arc<RepoSparseProfiles>,
        maybe_sparse_config: Option<SparseProfilesConfig>,
        monitoring_profiles: MonitoringProfiles,
    ) -> Result<Self, MononokeError> {
        let sparse_config = maybe_sparse_config.ok_or_else(|| {
            MononokeError::from(anyhow!(
                "There isn't sparse profiles monitoring config in repo {}",
                repo_name
            ))
        })?;
        let rules: Vec<_> = vec![format!("{}/**", sparse_config.sparse_profiles_location)]
            .into_iter()
            .chain(
                sparse_config
                    .excluded_paths
                    .iter()
                    .map(|s| format!("!{}/{}", sparse_config.sparse_profiles_location, s)),
            )
            .collect();
        let profiles_location_with_excludes_matcher =
            pathmatcher::TreeMatcher::from_rules(rules.iter(), true).context(format!(
                "Couldn't create profiles config matcher for repo {} from rules {:?}",
                repo_name, rules,
            ))?;
        let exact_profiles_matcher = match monitoring_profiles {
            MonitoringProfiles::Exact { ref profiles } => {
                pathmatcher::TreeMatcher::from_rules(profiles.iter(), true).context(format!(
                    "Couldn't create exact profiles matcher for repo {} from rules {:?}",
                    repo_name, rules,
                ))?
            }
            // In that case exact_proifles_matcher will not be used, however making it Option
            // brings in some unnecessary complexity, so I just cloned existing matcher.
            MonitoringProfiles::All => profiles_location_with_excludes_matcher.clone(),
        };
        let monitoring_profiles_only_matcher = if sparse_config.monitored_profiles.is_empty() {
            None
        } else {
            Some(
                pathmatcher::TreeMatcher::from_rules(
                    sparse_config
                        .monitored_profiles
                        .iter()
                        .map(|p| format!("{}/{}", sparse_config.sparse_profiles_location, p)),
                    true,
                )
                .context(format!(
                    "Couldn't create monitored profiles matcher for repo {} from rules {:?}",
                    repo_name, sparse_config.monitored_profiles,
                ))?,
            )
        };
        Ok(Self {
            sql_sparse_profiles,
            sparse_config,
            exact_profiles_matcher,
            profiles_location_with_excludes_matcher,
            monitoring_profiles_only_matcher,
            monitoring_profiles,
        })
    }

    pub async fn get_monitoring_profiles(
        &self,
        changeset: &ChangesetContext,
    ) -> Result<Vec<MPath>, MononokeError> {
        match &self.monitoring_profiles {
            MonitoringProfiles::All => {
                let prefixes = vec![MononokePath::try_from(
                    &self.sparse_config.sparse_profiles_location,
                )?];
                let files = changeset
                    .find_files(Some(prefixes), None, None, ChangesetFileOrdering::Unordered)
                    .await?;
                files
                    .try_filter_map(|path| async move {
                        let matcher = self
                            .monitoring_profiles_only_matcher
                            .as_ref()
                            .map_or_else(|| &self.profiles_location_with_excludes_matcher, |m| m);
                        Ok(match matcher.matches(path.to_string()) {
                            // Since None in MononokePath is a root repo directory
                            // and we are returning list of profiles
                            // we can safely filter that out.
                            true => path.into_mpath(),
                            false => None,
                        })
                    })
                    .try_collect()
                    .await
            }
            MonitoringProfiles::Exact { profiles } => {
                let prefixes = profiles
                    .iter()
                    .map(MononokePath::try_from)
                    .collect::<Result<Vec<_>, MononokeError>>()?;
                changeset
                    .find_files(Some(prefixes), None, None, ChangesetFileOrdering::Unordered)
                    .await?
                    .map(|p| {
                        p.and_then(|path| {
                            path.into_mpath().ok_or_else(|| {
                                MononokeError::from(anyhow!(
                                    "Provided root diretory as monitored profile."
                                ))
                            })
                        })
                    })
                    .try_collect()
                    .await
            }
        }
    }

    fn is_profile_config_change(&self, path: &MononokePath) -> bool {
        let matcher = match self.monitoring_profiles {
            MonitoringProfiles::Exact { .. } => &self.exact_profiles_matcher,
            MonitoringProfiles::All => &self.profiles_location_with_excludes_matcher,
        };
        matcher.matches(path.to_string())
    }

    pub async fn get_profile_size(
        &self,
        ctx: &CoreContext,
        changeset: &ChangesetContext,
        paths: Vec<MPath>,
    ) -> Result<Out, MononokeError> {
        let cs_id = changeset.id();
        let maybe_sizes = self
            .sql_sparse_profiles
            .get_profiles_sizes(cs_id, paths.iter().map(MPath::to_string).collect())
            .await?;
        let (paths_to_calculate, mut sizes) = match maybe_sizes {
            None => (paths, HashMap::new()),
            Some(sizes) => {
                let processed_paths = sizes
                    .iter()
                    .map(|(path, _)| MPath::try_from(<String as AsRef<str>>::as_ref(path)))
                    .collect::<Result<HashSet<_>>>()?;
                (
                    paths
                        .into_iter()
                        .filter(|path| processed_paths.contains(path).not())
                        .collect(),
                    sizes.into_iter().collect(),
                )
            }
        };
        if paths_to_calculate.is_empty().not() {
            let matchers = create_matchers(changeset, paths_to_calculate).await?;
            let other_sizes = calculate_size(ctx, changeset, matchers).await?;
            let res = self
                .sql_sparse_profiles
                .insert_profiles_sizes(cs_id, other_sizes.clone())
                .await?;
            if let Some(false) = res {
                debug!(
                    ctx.logger(),
                    "Failed to insert sizes into DB for cs_id {}", cs_id
                );
            }
            sizes.extend(other_sizes);
        }
        Ok(sizes)
    }
}

pub(crate) async fn fetch(path: String, changeset: &ChangesetContext) -> Result<Option<Vec<u8>>> {
    let path: &str = &path;
    let path = MPath::try_from(path)?;
    let path_with_content = changeset.path_with_content(path.clone()).await?;
    let file_ctx = path_with_content
        .file()
        .await?
        .ok_or_else(|| anyhow!("Sparse profile {} not found", path))?;
    file_ctx
        .content_concat()
        .await
        .with_context(|| format!("Couldn't fetch content of {path}"))
        .map(|b| Some(b.to_vec()))
}

async fn create_matchers(
    changeset: &ChangesetContext,
    paths: Vec<MPath>,
) -> Result<HashMap<String, Arc<dyn Matcher + Send + Sync>>> {
    stream::iter(paths)
        .map(|path| async move {
            let content = format!("%include {path}");
            let dummy_source = "repo_root".to_string();
            let profile = sparse::Root::from_bytes(content.as_bytes(), dummy_source)
                .with_context(|| format!("while constructing Profile for source {path}"))?;
            let matcher = profile
                .matcher(|path| fetch(path, changeset))
                .await
                .with_context(|| format!("While constructing matcher for source {path}"))?;
            anyhow::Ok((
                path.to_string(),
                Arc::new(matcher) as Arc<dyn Matcher + Send + Sync>,
            ))
        })
        .buffer_unordered(100)
        .try_collect()
        .await
}

type Out = HashMap<String, u64>;

async fn calculate_size<'a>(
    ctx: &'a CoreContext,
    changeset: &'a ChangesetContext,
    matchers: HashMap<String, Arc<dyn Matcher + Send + Sync>>,
) -> Result<Out, MononokeError> {
    let root_fsnode_id = changeset.root_fsnode_id().await?;
    let root: Option<MPath> = None;
    bounded_traversal::bounded_traversal(
        256,
        (root, *root_fsnode_id.fsnode_id(), matchers),
        |(path, fsnode_id, matchers)| {
            cloned!(ctx, matchers);
            let blobstore = changeset.repo().blob_repo().repo_blobstore();
            async move {
                let mut sizes: Out = HashMap::new();
                let mut next: HashMap<_, HashMap<_, _>> = HashMap::new();
                let fsnode = fsnode_id.load(&ctx, blobstore).await?;
                for (base_name, entry) in fsnode.list() {
                    let path = MPath::join_opt_element(path.as_ref(), base_name);
                    let path_vec = path.to_vec();
                    let repo_path = RepoPath::from_utf8(&path_vec)?;
                    match entry {
                        FsnodeEntry::File(leaf) => {
                            for (source, matcher) in &matchers {
                                if matcher.matches_file(repo_path)? {
                                    *sizes.entry(source.to_string()).or_insert(0) += leaf.size();
                                }
                            }
                        }
                        FsnodeEntry::Directory(tree) => {
                            for (source, matcher) in &matchers {
                                match matcher.matches_directory(repo_path)? {
                                    DirectoryMatch::Everything => {
                                        *sizes.entry(source.to_string()).or_insert(0) +=
                                            tree.summary().descendant_files_total_size;
                                    }
                                    DirectoryMatch::ShouldTraverse => {
                                        next.entry((Some(path.clone()), *tree.id()))
                                            .or_default()
                                            .insert(source.clone(), matcher.clone());
                                    }
                                    DirectoryMatch::Nothing => {}
                                }
                            }
                        }
                    }
                }

                anyhow::Ok((
                    sizes,
                    next.into_iter()
                        .map(|((path, fsnode_id), matchers)| (path, fsnode_id, matchers)),
                ))
            }
            .boxed()
        },
        |sizes, children| {
            async move {
                let t = children.fold(HashMap::new(), fold_maps);
                Ok(fold_maps(t, sizes))
            }
            .boxed()
        },
    )
    .await
    .map_err(MononokeError::from)
}

fn fold_maps(mut a: Out, b: Out) -> Out {
    for (source, size) in b {
        *a.entry(source).or_insert(0) += size;
    }
    a
}

async fn get_entry_size(content: &ChangesetPathContentContext) -> Result<u64, MononokeError> {
    let path = content.path();
    match content.entry().await? {
        PathEntry::File(file, _) => Ok(file.metadata().await?.total_size),
        PathEntry::Tree(_) => Err(MononokeError::from(anyhow!(
            "Got Tree entry for the diff, while requested Files only. Path {}",
            path
        ))),
        PathEntry::NotPresent => Ok(0),
    }
}

async fn get_bonsai_size_change(
    current: &ChangesetContext,
    other: &ChangesetContext,
) -> Result<Vec<BonsaiSizeChange>> {
    let diff_items = btreeset! { ChangesetDiffItem::FILES };
    let diff = current
        .diff_unordered(other, true, None, diff_items)
        .await?;
    let res = stream::iter(diff)
        .map(|diff| async move {
            match diff {
                ChangesetPathDiffContext::Added(content) => {
                    anyhow::Ok(vec![BonsaiSizeChange::Added {
                        path: content.path().clone(),
                        size_change: get_entry_size(&content).await?,
                    }])
                }
                ChangesetPathDiffContext::Removed(content) => {
                    anyhow::Ok(vec![BonsaiSizeChange::Removed {
                        path: content.path().clone(),
                        size_change: get_entry_size(&content).await?,
                    }])
                }
                ChangesetPathDiffContext::Changed(new, old) => {
                    let (new_size, old_size) =
                        try_join!(get_entry_size(&new), get_entry_size(&old))?;

                    let new_size = i64::try_from(new_size).with_context(|| {
                        format!(
                            "Size of the file {} can't be converted back to i64",
                            new.path()
                        )
                    })?;
                    let old_size = i64::try_from(old_size).with_context(|| {
                        format!(
                            "Size of the file {} can't be converted back to i64",
                            old.path()
                        )
                    })?;
                    let size_change = new_size - old_size;
                    anyhow::Ok(vec![BonsaiSizeChange::Changed {
                        path: new.path().clone(),
                        size_change,
                    }])
                }
                ChangesetPathDiffContext::Copied(to, _from) => {
                    anyhow::Ok(vec![BonsaiSizeChange::Added {
                        path: to.path().clone(),
                        size_change: get_entry_size(&to).await?,
                    }])
                }
                ChangesetPathDiffContext::Moved(to, from) => anyhow::Ok(vec![
                    BonsaiSizeChange::Added {
                        path: to.path().clone(),
                        size_change: get_entry_size(&to).await?,
                    },
                    BonsaiSizeChange::Removed {
                        path: from.path().clone(),
                        size_change: get_entry_size(&from).await?,
                    },
                ]),
            }
        })
        .buffered(100)
        .try_collect::<Vec<_>>()
        .await?;
    Ok(res.into_iter().flatten().collect())
}

fn match_path(matcher: &dyn Matcher, path: &MononokePath) -> Result<bool> {
    // None here means repo root which is empty RepoPath
    let maybe_path_vec = path.as_mpath().map(|path| path.to_vec());
    let path_vec = maybe_path_vec.unwrap_or_default();
    matcher.matches_file(RepoPath::from_utf8(&path_vec)?)
}

pub async fn get_profile_delta_size(
    ctx: &CoreContext,
    monitor: &SparseProfileMonitoring,
    current: &ChangesetContext,
    other: &ChangesetContext,
    paths: Vec<MPath>,
) -> Result<HashMap<String, ProfileSizeChange>, MononokeError> {
    let matchers = create_matchers(current, paths).await?;
    calculate_delta_size(ctx, monitor, current, other, matchers).await
}

pub async fn calculate_delta_size<'a>(
    ctx: &'a CoreContext,
    monitor: &'a SparseProfileMonitoring,
    current: &'a ChangesetContext,
    other: &'a ChangesetContext,
    matchers: HashMap<String, Arc<dyn Matcher + Send + Sync>>,
) -> Result<HashMap<String, ProfileSizeChange>, MononokeError> {
    let diff_change = get_bonsai_size_change(current, other).await?;
    let (sparse_config_change, other_changes): (Vec<_>, Vec<_>) = diff_change
        .into_iter()
        .partition(|entry| monitor.is_profile_config_change(entry.path()));
    let mut sizes: HashMap<_, _> = other_changes
        .iter()
        .try_fold(HashMap::new(), |mut sizes, entry| {
            let (path, size_change) = match entry {
                BonsaiSizeChange::Added { path, size_change } => (path, *size_change as i64),
                BonsaiSizeChange::Removed { path, size_change } => (path, -(*size_change as i64)),
                BonsaiSizeChange::Changed { path, size_change } => (path, *size_change),
            };
            for (source, matcher) in &matchers {
                if match_path(matcher, path)? {
                    *sizes.entry(source).or_insert(0) += size_change;
                }
            }
            anyhow::Ok(sizes)
        })?
        .into_iter()
        .map(|(source, size)| (source.clone(), ProfileSizeChange::Changed(size)))
        .collect();
    let profile_configs_change =
        calculate_profile_config_change(ctx, monitor, current, other, sparse_config_change).await?;
    sizes.extend(profile_configs_change.into_iter());
    Ok(sizes
        .into_iter()
        .filter(|(_, size)| {
            !(*size == ProfileSizeChange::Added(0)
                || *size == ProfileSizeChange::Removed(0)
                || *size == ProfileSizeChange::Changed(0))
        })
        .collect())
}

async fn calculate_profile_config_change<'a>(
    ctx: &'a CoreContext,
    monitor: &'a SparseProfileMonitoring,
    current: &'a ChangesetContext,
    other: &'a ChangesetContext,
    // This should be changes to the sparse profile configs only
    changes: Vec<BonsaiSizeChange>,
) -> Result<HashMap<String, ProfileSizeChange>, MononokeError> {
    let mut result = HashMap::new();
    if changes.is_empty() {
        return Ok(result);
    }
    let mut changed = vec![];
    let mut raw_increase = vec![];
    let mut raw_decrease = vec![];
    for entry in changes {
        match entry {
            BonsaiSizeChange::Added {
                path,
                size_change: _,
            } => {
                if let Some(path) = path.into_mpath() {
                    raw_increase.push(path);
                }
            }
            BonsaiSizeChange::Removed {
                path,
                size_change: _,
            } => {
                if let Some(path) = path.into_mpath() {
                    raw_decrease.push(path);
                }
            }
            BonsaiSizeChange::Changed {
                path,
                size_change: _,
            } => {
                if let Some(path) = path.into_mpath() {
                    changed.push(path);
                }
            }
        }
    }
    // profiles which need current total sizes
    let current_paths: Vec<_> = changed
        .iter()
        .cloned()
        .chain(raw_increase.iter().cloned())
        .collect();
    // profiles which need previous commit total sizes (need diff or been removed)
    let previous_paths: Vec<_> = changed
        .iter()
        .cloned()
        .chain(raw_decrease.iter().cloned())
        .collect();

    let (current_sizes, previous_sizes) = try_join!(
        monitor.get_profile_size(ctx, current, current_paths),
        monitor.get_profile_size(ctx, other, previous_paths)
    )?;
    for path in raw_increase {
        let size = current_sizes.get(&path.to_string()).ok_or_else(|| {
            anyhow!(
                "Size for the {} wasn't calculated for current cs: {}",
                path,
                current.id()
            )
        })?;
        result.insert(path.to_string(), ProfileSizeChange::Added(*size));
    }
    for path in raw_decrease {
        let size = previous_sizes.get(&path.to_string()).ok_or_else(|| {
            anyhow!(
                "Size for the {} wasn't calculated for previous cs: {}",
                path,
                other.id()
            )
        })?;
        result.insert(path.to_string(), ProfileSizeChange::Removed(*size));
    }
    for path in changed {
        let new_size = *current_sizes.get(&path.to_string()).ok_or_else(|| {
            anyhow!(
                "Size for the {} wasn't calculated for cs: {}",
                path,
                current.id()
            )
        })? as i64;
        let old_size = *previous_sizes.get(&path.to_string()).ok_or_else(|| {
            anyhow!(
                "Size for the {} wasn't calculated for cs: {}",
                path,
                other.id()
            )
        })? as i64;
        result.insert(
            path.to_string(),
            ProfileSizeChange::Changed(new_size - old_size),
        );
    }
    Ok(result)
}

#[derive(Debug)]
enum BonsaiSizeChange {
    Added {
        path: MononokePath,
        size_change: u64,
    },
    Removed {
        path: MononokePath,
        size_change: u64,
    },
    Changed {
        path: MononokePath,
        size_change: i64,
    },
}

impl BonsaiSizeChange {
    fn path(&self) -> &MononokePath {
        match self {
            BonsaiSizeChange::Added {
                path,
                size_change: _,
            }
            | BonsaiSizeChange::Removed {
                path,
                size_change: _,
            }
            | BonsaiSizeChange::Changed {
                path,
                size_change: _,
            } => path,
        }
    }
}

#[derive(Debug, PartialEq)]
pub enum ProfileSizeChange {
    Added(u64),
    Removed(u64),
    Changed(i64),
}

pub enum MonitoringProfiles {
    All,
    Exact { profiles: Vec<String> },
}


/*
 * 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::fmt;

use anyhow::Context;
use anyhow::Result;
use edenapi_types::HgId;
use ephemeral_blobstore::BubbleId;
use ephemeral_blobstore::RepoEphemeralStore;

/// A changeset ID.  This is the canonical ID for a changeset.
pub type ChangesetId = mononoke_types::ChangesetId;

/// A Mercurial changeset ID.
pub type HgChangesetId = mercurial_types::HgChangesetId;

/// A Globalrev.
pub type Globalrev = mercurial_types::Globalrev;

/// A Git SHA-1 hash.
pub type GitSha1 = mononoke_types::hash::GitSha1;

/// A SVN revision number.
pub type Svnrev = mononoke_types::Svnrev;

/// A changeset specifier.  This is anything that may be used to specify a
/// unique changeset, such as its bonsai changeset ID, a changeset hash in an
/// alternative hashing scheme, a globally unique hash prefix, or an
/// externally-assigned numerical ID.
#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Debug, Hash)]
pub enum ChangesetSpecifier {
    Bonsai(ChangesetId),
    EphemeralBonsai(ChangesetId, Option<BubbleId>),
    Hg(HgChangesetId),
    Globalrev(Globalrev),
    GitSha1(GitSha1),
    Svnrev(Svnrev),
}

impl From<ChangesetId> for ChangesetSpecifier {
    fn from(id: ChangesetId) -> Self {
        Self::Bonsai(id)
    }
}

impl From<HgChangesetId> for ChangesetSpecifier {
    fn from(id: HgChangesetId) -> Self {
        Self::Hg(id)
    }
}

impl From<HgId> for ChangesetSpecifier {
    fn from(id: HgId) -> Self {
        Self::Hg(HgChangesetId::from(id))
    }
}

impl From<Globalrev> for ChangesetSpecifier {
    fn from(id: Globalrev) -> Self {
        Self::Globalrev(id)
    }
}

impl From<Svnrev> for ChangesetSpecifier {
    fn from(id: Svnrev) -> Self {
        Self::Svnrev(id)
    }
}

impl From<GitSha1> for ChangesetSpecifier {
    fn from(id: GitSha1) -> Self {
        Self::GitSha1(id)
    }
}

impl ChangesetSpecifier {
    pub fn in_bubble(&self) -> bool {
        use ChangesetSpecifier::*;
        match self {
            EphemeralBonsai(_, _) => true,
            Bonsai(_) | Hg(_) | Globalrev(_) | GitSha1(_) | Svnrev(_) => false,
        }
    }

    pub async fn bubble_id(
        &self,
        ephemeral_blobstore: RepoEphemeralStore,
    ) -> Result<Option<BubbleId>> {
        use ChangesetSpecifier::*;
        Ok(match self {
            EphemeralBonsai(cs_id, bubble_id) => Some(match bubble_id {
                Some(id) => id.clone(),
                None => ephemeral_blobstore
                    .bubble_from_changeset(cs_id)
                    .await?
                    .with_context(|| format!("changeset {} does not belong to bubble", cs_id))?,
            }),
            Bonsai(_) | Hg(_) | Globalrev(_) | GitSha1(_) | Svnrev(_) => None,
        })
    }
}

/// A prefix of canonical ID for a changeset (Bonsai).
pub type ChangesetIdPrefix = mononoke_types::ChangesetIdPrefix;

/// A prefix of a Mercurial changeset ID.
pub type HgChangesetIdPrefix = mercurial_types::HgChangesetIdPrefix;
pub type GitSha1Prefix = git_types::GitSha1Prefix;

/// This is prefix that may be used to resolve a changeset
#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Debug, Hash)]
pub enum ChangesetPrefixSpecifier {
    Bonsai(ChangesetIdPrefix),
    Hg(HgChangesetIdPrefix),
    GitSha1(GitSha1Prefix),
    Globalrev(Globalrev),
}

impl From<HgChangesetIdPrefix> for ChangesetPrefixSpecifier {
    fn from(prefix: HgChangesetIdPrefix) -> Self {
        Self::Hg(prefix)
    }
}

impl From<GitSha1Prefix> for ChangesetPrefixSpecifier {
    fn from(prefix: GitSha1Prefix) -> Self {
        Self::GitSha1(prefix)
    }
}

impl From<ChangesetIdPrefix> for ChangesetPrefixSpecifier {
    fn from(prefix: ChangesetIdPrefix) -> Self {
        Self::Bonsai(prefix)
    }
}

impl From<Globalrev> for ChangesetPrefixSpecifier {
    fn from(prefix: Globalrev) -> Self {
        Self::Globalrev(prefix)
    }
}

/// This is the result of resolving changesets by prefix
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Debug, Hash)]
pub enum ChangesetSpecifierPrefixResolution {
    NoMatch,
    Single(ChangesetSpecifier),
    Multiple(Vec<ChangesetSpecifier>),
    TooMany(Vec<ChangesetSpecifier>),
}

impl ChangesetSpecifierPrefixResolution {
    pub fn into_list(self) -> Vec<ChangesetSpecifier> {
        use ChangesetSpecifierPrefixResolution::*;
        match self {
            NoMatch => Vec::new(),
            Single(x) => vec![x],
            Multiple(v) => v,
            TooMany(v) => v,
        }
    }
}

impl From<mercurial_types::HgChangesetIdsResolvedFromPrefix>
    for ChangesetSpecifierPrefixResolution
{
    fn from(resolved: mercurial_types::HgChangesetIdsResolvedFromPrefix) -> Self {
        use mercurial_types::HgChangesetIdsResolvedFromPrefix::*;
        use ChangesetSpecifier::*;
        match resolved {
            Single(id) => Self::Single(Hg(id)),
            Multiple(ids) => Self::Multiple(ids.into_iter().map(Hg).collect()),
            TooMany(ids) => Self::TooMany(ids.into_iter().map(Hg).collect()),
            NoMatch => Self::NoMatch,
        }
    }
}

impl From<git_types::GitSha1sResolvedFromPrefix> for ChangesetSpecifierPrefixResolution {
    fn from(resolved: git_types::GitSha1sResolvedFromPrefix) -> Self {
        use git_types::GitSha1sResolvedFromPrefix::*;
        use ChangesetSpecifier::*;
        match resolved {
            Single(id) => Self::Single(GitSha1(id)),
            Multiple(ids) => Self::Multiple(ids.into_iter().map(GitSha1).collect()),
            TooMany(ids) => Self::TooMany(ids.into_iter().map(GitSha1).collect()),
            NoMatch => Self::NoMatch,
        }
    }
}

impl From<mononoke_types::ChangesetIdsResolvedFromPrefix> for ChangesetSpecifierPrefixResolution {
    fn from(resolved: mononoke_types::ChangesetIdsResolvedFromPrefix) -> Self {
        use mononoke_types::ChangesetIdsResolvedFromPrefix::*;
        use ChangesetSpecifier::*;
        match resolved {
            Single(id) => Self::Single(Bonsai(id)),
            Multiple(ids) => Self::Multiple(ids.into_iter().map(Bonsai).collect()),
            TooMany(ids) => Self::TooMany(ids.into_iter().map(Bonsai).collect()),
            NoMatch => Self::NoMatch,
        }
    }
}

impl From<Option<Globalrev>> for ChangesetSpecifierPrefixResolution {
    fn from(resolved: Option<Globalrev>) -> Self {
        use ChangesetSpecifier::*;
        match resolved {
            Some(globalrev) => Self::Single(Globalrev(globalrev)),
            None => Self::NoMatch,
        }
    }
}

impl fmt::Display for ChangesetSpecifier {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            ChangesetSpecifier::Bonsai(cs_id) => write!(f, "changeset {}", cs_id),
            ChangesetSpecifier::EphemeralBonsai(cs_id, bubble_id) => {
                write!(
                    f,
                    "ephemeral changeset {} in bubble {}",
                    cs_id,
                    bubble_id.map_or_else(|| "unknown".to_string(), |b| b.to_string())
                )
            }
            ChangesetSpecifier::Hg(hg_cs_id) => write!(f, "hg changeset {}", hg_cs_id),
            ChangesetSpecifier::Globalrev(rev) => write!(f, "globalrev {}", rev.id()),
            ChangesetSpecifier::GitSha1(git_sha1) => write!(f, "git sha1 {}", git_sha1),
            ChangesetSpecifier::Svnrev(rev) => write!(f, "svn rev {}", rev.id()),
        }
    }
}


/*
 * 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::fmt;

use anyhow::Error;
use blobstore::Loadable;
use blobstore::LoadableError;
use cloned::cloned;
use futures_lazy_shared::LazyShared;
use mononoke_types::fsnode::Fsnode;
// An entry within a tree list (either a file or subdirectory).
pub use mononoke_types::fsnode::FsnodeEntry as TreeEntry;
// Summary information about the files in a tree.
pub use mononoke_types::fsnode::FsnodeSummary as TreeSummary;
// Trees are identified by their FsnodeId.
pub use mononoke_types::FsnodeId as TreeId;
use repo_blobstore::RepoBlobstoreRef;

use crate::errors::MononokeError;
use crate::repo::RepoContext;

#[derive(Clone)]
pub struct TreeContext {
    repo: RepoContext,
    id: TreeId,
    fsnode: LazyShared<Result<Fsnode, MononokeError>>,
}

impl fmt::Debug for TreeContext {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(
            f,
            "TreeContext(repo={:?} id={:?})",
            self.repo().name(),
            self.id()
        )
    }
}

impl TreeContext {
    /// Create a new TreeContext. The tree must exist in the repo and have
    /// had its derived data generated, and the user must be known to have
    /// permission to access the file.
    ///
    /// To construct a `TreeContext` for a tree that might not exist, use
    /// `new_check_exists`.
    pub(crate) fn new_authorized(repo: RepoContext, id: TreeId) -> Self {
        Self {
            repo,
            id,
            fsnode: LazyShared::new_empty(),
        }
    }

    /// Create a new TreeContext using an ID that might not exist. Returns
    /// `None` if the tree doesn't exist.
    pub(crate) async fn new_check_exists(
        repo: RepoContext,
        id: TreeId,
    ) -> Result<Option<Self>, MononokeError> {
        // Access to an arbitrary tree requires full access to the repo,
        // as we do not know which path it corresponds to.
        repo.authorization_context()
            .require_full_repo_read(repo.ctx(), repo.inner_repo())
            .await?;
        // Try to load the fsnode immediately to see if it exists. Unlike
        // `new`, if the fsnode is missing, we simply return `Ok(None)`.
        match id.load(repo.ctx(), repo.blob_repo().repo_blobstore()).await {
            Ok(fsnode) => Ok(Some(Self {
                repo,
                id,
                fsnode: LazyShared::new_ready(Ok(fsnode)),
            })),
            Err(LoadableError::Missing(_)) => Ok(None),
            Err(e) => Err(MononokeError::from(Error::from(e))),
        }
    }

    /// The `RepoContext` for this query.
    pub(crate) fn repo(&self) -> &RepoContext {
        &self.repo
    }

    async fn fsnode(&self) -> Result<Fsnode, MononokeError> {
        self.fsnode
            .get_or_init(|| {
                cloned!(self.repo, self.id);
                async move {
                    id.load(repo.ctx(), repo.blob_repo().repo_blobstore())
                        .await
                        .map_err(Error::from)
                        .map_err(MononokeError::from)
                }
            })
            .await
    }

    pub fn id(&self) -> &TreeId {
        &self.id
    }

    pub async fn summary(&self) -> Result<TreeSummary, MononokeError> {
        let summary = self.fsnode().await?.summary().clone();
        Ok(summary)
    }

    pub async fn list(&self) -> Result<impl Iterator<Item = (String, TreeEntry)>, MononokeError> {
        let fsnode = self.fsnode().await?;
        let entries = fsnode
            .into_subentries()
            .into_iter()
            .map(|(elem, entry)| (String::from_utf8_lossy(elem.as_ref()).to_string(), entry));
        Ok(entries)
    }
}


/*
 * 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 bookmarks::BookmarkKey;

use crate::specifiers::ChangesetSpecifier;

/// An aux struct to build `CandidateSelectionHint`
pub enum CandidateSelectionHintArgs {
    OnlyOrAncestorOfBookmark(BookmarkKey),
    OnlyOrDescendantOfBookmark(BookmarkKey),
    OnlyOrAncestorOfCommit(ChangesetSpecifier),
    OnlyOrDescendantOfCommit(ChangesetSpecifier),
    Exact(ChangesetSpecifier),
}