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

use std::collections::HashSet;
use std::time::Duration;

use anyhow::anyhow;
use anyhow::Result;
use async_requests::tokens::MegarepoAddBranchingTargetToken;
use async_requests::tokens::MegarepoAddTargetToken;
use async_requests::tokens::MegarepoChangeTargetConfigToken;
use async_requests::tokens::MegarepoRemergeSourceToken;
use async_requests::tokens::MegarepoSyncChangesetToken;
use async_requests::types::IntoApiFormat;
use async_requests::types::IntoConfigFormat;
use async_requests::types::ThriftParams;
use async_requests::types::Token;
use context::CoreContext;
use megarepo_config::SyncTargetConfig;
use mononoke_api::ChangesetSpecifier;
use mononoke_api::MononokeError;
use mononoke_types::RepositoryId;
use repo_authorization::RepoWriteOperation;
use slog::warn;
use source_control as thrift;

use crate::errors;
use crate::from_request::FromRequest;
use crate::source_control_impl::SourceControlServiceImpl;

impl SourceControlServiceImpl {
    fn verify_repos_by_config(
        &self,
        config: &SyncTargetConfig,
    ) -> Result<(), errors::ServiceError> {
        let known_repo_ids: HashSet<RepositoryId> =
            self.mononoke.known_repo_ids().into_iter().collect();

        let repo_ids_in_cfg = {
            let mut repo_ids_in_cfg = Vec::new();
            repo_ids_in_cfg.push(config.target.repo_id);
            repo_ids_in_cfg.extend(config.sources.iter().map(|src| src.repo_id));
            repo_ids_in_cfg
        };

        for repo_id_in_cfg in repo_ids_in_cfg {
            if !known_repo_ids.contains(&RepositoryId::new(repo_id_in_cfg as i32)) {
                return Err(errors::ServiceError::from(errors::repo_not_found(format!(
                    "{}",
                    repo_id_in_cfg
                ))));
            }
        }

        Ok(())
    }

    async fn check_write_allowed(
        &self,
        ctx: &CoreContext,
        target_repo_id: RepositoryId,
    ) -> Result<(), errors::ServiceError> {
        let target_repo = self
            .mononoke
            .repo_by_id(ctx.clone(), target_repo_id)
            .await
            .map_err(errors::invalid_request)?
            .ok_or_else(|| errors::invalid_request(anyhow!("repo not found {}", target_repo_id)))?
            .build()
            .await?;
        // Check that source control service writes are enabled
        target_repo.start_write()?;
        // Check that we are allowed to write to the target repo
        target_repo
            .authorization_context()
            .require_repo_write(
                ctx,
                target_repo.inner_repo(),
                RepoWriteOperation::MegarepoSync,
            )
            .await
            .map_err(MononokeError::from)?;
        Ok(())
    }

    pub(crate) async fn megarepo_add_sync_target_config(
        &self,
        ctx: CoreContext,
        params: thrift::MegarepoAddConfigParams,
    ) -> Result<thrift::MegarepoAddConfigResponse, errors::ServiceError> {
        let target = params
            .new_config
            .target
            .clone()
            .into_config_format(&self.mononoke)?;
        let target_repo_id = RepositoryId::new(target.repo_id.try_into().unwrap());
        self.check_write_allowed(&ctx, target_repo_id).await?;

        let new_config = params.new_config.into_config_format(&self.mononoke)?;
        self.verify_repos_by_config(&new_config)?;
        let megarepo_configs = self.megarepo_api.configs();
        megarepo_configs
            .add_config_version(ctx.clone(), new_config.clone())
            .await?;

        // We've seen cases where config is not readable immediately after
        // it was written. Let's try reading a few times before returning
        let mut latest_error = None;
        for _ in 0..10 {
            let res = megarepo_configs.get_config_by_version(
                ctx.clone(),
                new_config.target.clone(),
                new_config.version.clone(),
            );
            match res {
                Ok(_) => {
                    latest_error = None;
                    break;
                }
                Err(err) => {
                    latest_error = Some(err);
                    warn!(
                        ctx.logger(),
                        "failed to read just written config version {}, retrying...",
                        new_config.version
                    );

                    tokio::time::sleep(Duration::from_secs(5)).await;
                }
            }
        }

        if let Some(err) = latest_error {
            return Err(errors::internal_error(format!(
                "Failed to read just written config version {}, error: {:?}",
                new_config.version, err
            ))
            .into());
        }

        Ok(thrift::MegarepoAddConfigResponse {
            ..Default::default()
        })
    }

    pub(crate) async fn megarepo_read_target_config(
        &self,
        ctx: CoreContext,
        params: thrift::MegarepoReadConfigParams,
    ) -> Result<thrift::MegarepoReadConfigResponse, errors::ServiceError> {
        let target = params.target.clone().into_config_format(&self.mononoke)?;
        let repo = self
            .megarepo_api
            .target_repo(&ctx, &target)
            .await
            .map_err(|err| {
                errors::invalid_request(anyhow!(
                    "can't open target repo {}: {}",
                    target.repo_id,
                    err
                ))
            })?;
        let changeset = repo
            .changeset(ChangesetSpecifier::from_request(&params.commit)?)
            .await?
            .ok_or_else(|| errors::invalid_request(anyhow!("commit not found")))?;
        let (_commit_remapping_state, target_config) = self
            .megarepo_api
            .get_target_sync_config(&ctx, &target, &changeset.id())
            .await?;

        Ok(thrift::MegarepoReadConfigResponse {
            config: target_config.into_api_format(&self.mononoke)?,
            ..Default::default()
        })
    }

    pub(crate) async fn megarepo_add_sync_target(
        &self,
        ctx: CoreContext,
        params: thrift::MegarepoAddTargetParams,
    ) -> Result<thrift::MegarepoAddTargetToken, errors::ServiceError> {
        let target = params.target().clone().into_config_format(&self.mononoke)?;
        let target_repo_id = RepositoryId::new(target.repo_id.try_into().unwrap());
        self.check_write_allowed(&ctx, target_repo_id).await?;
        let config_with_new_target = params
            .config_with_new_target
            .clone()
            .into_config_format(&self.mononoke)?;
        self.verify_repos_by_config(&config_with_new_target)?;

        let token = self
            .megarepo_api
            .async_method_request_queue(&ctx, &target)
            .await?
            .enqueue(ctx, &self.mononoke, params)
            .await
            .map_err(|e| errors::internal_error(format!("Failed to enqueue the request: {}", e)))?;

        Ok(token.into_thrift())
    }

    pub(crate) async fn megarepo_add_sync_target_poll(
        &self,
        ctx: CoreContext,
        token: thrift::MegarepoAddTargetToken,
    ) -> Result<thrift::MegarepoAddTargetPollResponse, errors::ServiceError> {
        let token = MegarepoAddTargetToken(token);
        let target = token.target().clone().into_config_format(&self.mononoke)?;
        let target_repo_id = RepositoryId::new(target.repo_id.try_into().unwrap());
        self.check_write_allowed(&ctx, target_repo_id).await?;

        let poll_response = self
            .megarepo_api
            .async_method_request_queue(&ctx, &target)
            .await?
            .poll(ctx, token)
            .await?;

        Ok(poll_response)
    }

    pub(crate) async fn megarepo_add_branching_sync_target(
        &self,
        ctx: CoreContext,
        params: thrift::MegarepoAddBranchingTargetParams,
    ) -> Result<thrift::MegarepoAddBranchingTargetToken, errors::ServiceError> {
        let target = params.target().clone().into_config_format(&self.mononoke)?;
        let target_repo_id = RepositoryId::new(target.repo_id.try_into().unwrap());
        self.check_write_allowed(&ctx, target_repo_id).await?;

        let token = self
            .megarepo_api
            .async_method_request_queue(&ctx, &target)
            .await?
            .enqueue(ctx, &self.mononoke, params)
            .await
            .map_err(|e| errors::internal_error(format!("Failed to enqueue the request: {}", e)))?;

        Ok(token.into_thrift())
    }

    pub(crate) async fn megarepo_add_branching_sync_target_poll(
        &self,
        ctx: CoreContext,
        token: thrift::MegarepoAddBranchingTargetToken,
    ) -> Result<thrift::MegarepoAddBranchingTargetPollResponse, errors::ServiceError> {
        let token = MegarepoAddBranchingTargetToken(token);
        let target = token.target().clone().into_config_format(&self.mononoke)?;
        let target_repo_id = RepositoryId::new(target.repo_id.try_into().unwrap());
        self.check_write_allowed(&ctx, target_repo_id).await?;

        let poll_response = self
            .megarepo_api
            .async_method_request_queue(&ctx, &target)
            .await?
            .poll(ctx, token)
            .await?;

        Ok(poll_response)
    }

    pub(crate) async fn megarepo_change_target_config(
        &self,
        ctx: CoreContext,
        params: thrift::MegarepoChangeTargetConfigParams,
    ) -> Result<thrift::MegarepoChangeConfigToken, errors::ServiceError> {
        let target = params.target().clone().into_config_format(&self.mononoke)?;
        let target_repo_id = RepositoryId::new(target.repo_id.try_into().unwrap());
        self.check_write_allowed(&ctx, target_repo_id).await?;

        let token = self
            .megarepo_api
            .async_method_request_queue(&ctx, &target)
            .await?
            .enqueue(ctx, &self.mononoke, params)
            .await
            .map_err(|e| errors::internal_error(format!("Failed to enqueue the request: {}", e)))?;

        Ok(token.into_thrift())
    }

    pub(crate) async fn megarepo_change_target_config_poll(
        &self,
        ctx: CoreContext,
        token: thrift::MegarepoChangeConfigToken,
    ) -> Result<thrift::MegarepoChangeTargetConfigPollResponse, errors::ServiceError> {
        let token = MegarepoChangeTargetConfigToken(token);
        let target = token.target().clone().into_config_format(&self.mononoke)?;
        let target_repo_id = RepositoryId::new(target.repo_id.try_into().unwrap());
        self.check_write_allowed(&ctx, target_repo_id).await?;

        let poll_response = self
            .megarepo_api
            .async_method_request_queue(&ctx, &target)
            .await?
            .poll(ctx, token)
            .await?;

        Ok(poll_response)
    }

    pub(crate) async fn megarepo_sync_changeset(
        &self,
        ctx: CoreContext,
        params: thrift::MegarepoSyncChangesetParams,
    ) -> Result<thrift::MegarepoSyncChangesetToken, errors::ServiceError> {
        let target = params.target().clone().into_config_format(&self.mononoke)?;
        let target_repo_id = RepositoryId::new(target.repo_id.try_into().unwrap());
        self.check_write_allowed(&ctx, target_repo_id).await?;

        let token = self
            .megarepo_api
            .async_method_request_queue(&ctx, &target)
            .await?
            .enqueue(ctx, &self.mononoke, params)
            .await
            .map_err(|e| errors::internal_error(format!("Failed to enqueue the request: {}", e)))?;

        Ok(token.into_thrift())
    }

    pub(crate) async fn megarepo_sync_changeset_poll(
        &self,
        ctx: CoreContext,
        token: thrift::MegarepoSyncChangesetToken,
    ) -> Result<thrift::MegarepoSyncChangesetPollResponse, errors::ServiceError> {
        let token = MegarepoSyncChangesetToken(token);
        let target = token.target().clone().into_config_format(&self.mononoke)?;
        let target_repo_id = RepositoryId::new(target.repo_id.try_into().unwrap());
        self.check_write_allowed(&ctx, target_repo_id).await?;
        let poll_response = self
            .megarepo_api
            .async_method_request_queue(&ctx, &target)
            .await?
            .poll(ctx, token)
            .await?;

        Ok(poll_response)
    }

    pub(crate) async fn megarepo_remerge_source(
        &self,
        ctx: CoreContext,
        params: thrift::MegarepoRemergeSourceParams,
    ) -> Result<thrift::MegarepoRemergeSourceToken, errors::ServiceError> {
        let target = params.target().clone().into_config_format(&self.mononoke)?;
        let target_repo_id = RepositoryId::new(target.repo_id.try_into().unwrap());
        self.check_write_allowed(&ctx, target_repo_id).await?;

        let token = self
            .megarepo_api
            .async_method_request_queue(&ctx, &target)
            .await?
            .enqueue(ctx, &self.mononoke, params)
            .await
            .map_err(|e| errors::internal_error(format!("Failed to enqueue the request: {}", e)))?;

        Ok(token.into_thrift())
    }

    pub(crate) async fn megarepo_remerge_source_poll(
        &self,
        ctx: CoreContext,
        token: thrift::MegarepoRemergeSourceToken,
    ) -> Result<thrift::MegarepoRemergeSourcePollResponse, errors::ServiceError> {
        let token = MegarepoRemergeSourceToken(token);
        let target = token.target().clone().into_config_format(&self.mononoke)?;
        let target_repo_id = RepositoryId::new(target.repo_id.try_into().unwrap());
        self.check_write_allowed(&ctx, target_repo_id).await?;

        let poll_response = self
            .megarepo_api
            .async_method_request_queue(&ctx, &target)
            .await?
            .poll(ctx, token)
            .await?;

        Ok(poll_response)
    }
}


/*
 * 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 context::CoreContext;
use source_control as thrift;

use crate::errors;
use crate::source_control_impl::SourceControlServiceImpl;

pub(crate) mod commit;
pub(crate) mod commit_lookup_pushrebase_history;
pub(crate) mod commit_path;
pub(crate) mod commit_sparse_profile_info;
pub(crate) mod file;
pub(crate) mod git;
pub(crate) mod megarepo;
pub(crate) mod repo;
pub(crate) mod tree;

impl SourceControlServiceImpl {
    pub(crate) async fn list_repos(
        &self,
        _ctx: CoreContext,
        _params: thrift::ListReposParams,
    ) -> Result<Vec<thrift::Repo>, errors::ServiceError> {
        let mut repo_names: Vec<_> = self.mononoke.repo_names_in_tier.clone();
        repo_names.sort();
        let rsp = repo_names
            .into_iter()
            .map(|repo_name| thrift::Repo {
                name: repo_name,
                ..Default::default()
            })
            .collect();
        Ok(rsp)
    }
}


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

use std::collections::BTreeMap;

use bookmarks::BookmarkKey;
use bytes::Bytes;
use context::CoreContext;
use derived_data_manager::DerivableType;
use futures::future::try_join_all;
use futures::stream;
use futures::stream::FuturesOrdered;
use futures::stream::StreamExt;
use futures::stream::TryStreamExt;
use futures::try_join;
use maplit::btreemap;
use metaconfig_types::CommitIdentityScheme;
use mononoke_api::BookmarkFreshness;
use mononoke_api::ChangesetId;
use mononoke_api::ChangesetPrefixSpecifier;
use mononoke_api::ChangesetSpecifier;
use mononoke_api::ChangesetSpecifierPrefixResolution;
use mononoke_api::CreateChange;
use mononoke_api::CreateChangeFile;
use mononoke_api::CreateCopyInfo;
use mononoke_api::CreateInfo;
use mononoke_api::FileId;
use mononoke_api::FileType;
use mononoke_api::MononokeError;
use mononoke_api::MononokePath;
use mononoke_api::RepoContext;
use mononoke_api::StoreRequest;
use mononoke_types::hash::GitSha1;
use mononoke_types::hash::Sha1;
use mononoke_types::hash::Sha256;
use repo_authorization::AuthorizationContext;
use source_control as thrift;

use crate::commit_id::map_commit_identities;
use crate::commit_id::map_commit_identity;
use crate::commit_id::CommitIdExt;
use crate::errors;
use crate::errors::ServiceErrorResultExt;
use crate::from_request::check_range_and_convert;
use crate::from_request::convert_pushvars;
use crate::from_request::FromRequest;
use crate::into_response::AsyncIntoResponseWith;
use crate::source_control_impl::SourceControlServiceImpl;

mod land_stack;

impl SourceControlServiceImpl {
    /// Detailed repo info.
    ///
    /// Returns detailed information about a repository.
    pub(crate) async fn repo_info(
        &self,
        ctx: CoreContext,
        repo: thrift::RepoSpecifier,
        _params: thrift::RepoInfoParams,
    ) -> Result<thrift::RepoInfo, errors::ServiceError> {
        let authz = AuthorizationContext::new_bypass_access_control();
        let repo = self
            .repo_impl(ctx, &repo, authz, |_| async { Ok(None) })
            .await?;
        let repo_name = repo.name();

        let default_commit_identity_scheme_conf = &repo.config().default_commit_identity_scheme;

        let default_commit_identity_scheme = match default_commit_identity_scheme_conf {
            CommitIdentityScheme::HG => thrift::CommitIdentityScheme::HG,
            CommitIdentityScheme::GIT => thrift::CommitIdentityScheme::GIT,
            CommitIdentityScheme::BONSAI => thrift::CommitIdentityScheme::BONSAI,
            CommitIdentityScheme::UNKNOWN => thrift::CommitIdentityScheme::UNKNOWN,
        };

        Ok(thrift::RepoInfo {
            name: repo_name.to_string(),
            default_commit_identity_scheme,
            ..Default::default()
        })
    }

    /// Resolve a bookmark to a changeset.
    ///
    /// Returns whether the bookmark exists, and the IDs of the changeset in
    /// the requested indentity schemes.
    pub(crate) async fn repo_resolve_bookmark(
        &self,
        ctx: CoreContext,
        repo: thrift::RepoSpecifier,
        params: thrift::RepoResolveBookmarkParams,
    ) -> Result<thrift::RepoResolveBookmarkResponse, errors::ServiceError> {
        let repo = self.repo(ctx, &repo).await?;
        match repo
            .resolve_bookmark(
                &BookmarkKey::new(&params.bookmark_name).map_err(Into::<MononokeError>::into)?,
                BookmarkFreshness::MaybeStale,
            )
            .await?
        {
            Some(cs) => {
                let ids = map_commit_identity(&cs, &params.identity_schemes).await?;
                Ok(thrift::RepoResolveBookmarkResponse {
                    exists: true,
                    ids: Some(ids),
                    ..Default::default()
                })
            }
            None => Ok(thrift::RepoResolveBookmarkResponse {
                exists: false,
                ids: None,
                ..Default::default()
            }),
        }
    }

    /// Resolve a prefix and its identity scheme to a changeset.
    ///
    /// Returns the IDs of the changeset in the requested identity schemes.
    /// Suggestions for ambiguous prefixes are not provided for now.
    pub(crate) async fn repo_resolve_commit_prefix(
        &self,
        ctx: CoreContext,
        repo: thrift::RepoSpecifier,
        params: thrift::RepoResolveCommitPrefixParams,
    ) -> Result<thrift::RepoResolveCommitPrefixResponse, errors::ServiceError> {
        use ChangesetSpecifierPrefixResolution::*;
        type Response = thrift::RepoResolveCommitPrefixResponse;
        type ResponseType = thrift::RepoResolveCommitPrefixResponseType;

        let same_request_response_schemes = params.identity_schemes.len() == 1
            && params.identity_schemes.contains(&params.prefix_scheme);

        let prefix = ChangesetPrefixSpecifier::from_request(&params)?;
        let repo = self.repo(ctx, &repo).await?;

        // If the response requires exactly the same identity scheme as in the request,
        // the general case works but we don't need to pay extra overhead to resolve
        // ChangesetSpecifier to a changeset.

        match repo.resolve_changeset_id_prefix(prefix).await? {
            Single(ChangesetSpecifier::Bonsai(cs_id)) if same_request_response_schemes => {
                Ok(Response {
                    ids: Some(btreemap! {
                        params.prefix_scheme => thrift::CommitId::bonsai(cs_id.as_ref().into())
                    }),
                    resolved_type: ResponseType::RESOLVED,
                    ..Default::default()
                })
            }
            Single(ChangesetSpecifier::Hg(cs_id)) if same_request_response_schemes => {
                Ok(Response {
                    ids: Some(btreemap! {
                        params.prefix_scheme => thrift::CommitId::hg(cs_id.as_ref().into())
                    }),
                    resolved_type: ResponseType::RESOLVED,
                    ..Default::default()
                })
            }
            Single(ChangesetSpecifier::GitSha1(cs_id)) if same_request_response_schemes => {
                Ok(Response {
                    ids: Some(btreemap! {
                        params.prefix_scheme => thrift::CommitId::git(cs_id.as_ref().into())
                    }),
                    resolved_type: ResponseType::RESOLVED,
                    ..Default::default()
                })
            }
            Single(cs_id) => match &repo.changeset(cs_id).await? {
                None => Err(errors::internal_error(
                    "unexpected failure to resolve an existing commit",
                )
                .into()),
                Some(cs) => Ok(Response {
                    ids: Some(map_commit_identity(cs, &params.identity_schemes).await?),
                    resolved_type: ResponseType::RESOLVED,
                    ..Default::default()
                }),
            },
            NoMatch => Ok(Response {
                resolved_type: ResponseType::NOT_FOUND,
                ..Default::default()
            }),
            _ => Ok(Response {
                resolved_type: ResponseType::AMBIGUOUS,
                ..Default::default()
            }),
        }
    }

    /// Comprehensive bookmark info.
    ///
    /// Returns value of the bookmark (both fresh and warm) and the timestamp of
    /// last update.
    pub(crate) async fn repo_bookmark_info(
        &self,
        ctx: CoreContext,
        repo: thrift::RepoSpecifier,
        params: thrift::RepoBookmarkInfoParams,
    ) -> Result<thrift::RepoBookmarkInfoResponse, errors::ServiceError> {
        let repo = self.repo(ctx, &repo).await?;
        let info = repo.bookmark_info(params.bookmark_name).await?;
        Ok(thrift::RepoBookmarkInfoResponse {
            info: match info {
                Some(info) => Some(info.into_response_with(&params.identity_schemes).await?),
                None => None,
            },
            ..Default::default()
        })
    }

    /// List bookmarks.
    pub(crate) async fn repo_list_bookmarks(
        &self,
        ctx: CoreContext,
        repo: thrift::RepoSpecifier,
        params: thrift::RepoListBookmarksParams,
    ) -> Result<thrift::RepoListBookmarksResponse, errors::ServiceError> {
        let limit = match check_range_and_convert(
            "limit",
            params.limit,
            0..=source_control::REPO_LIST_BOOKMARKS_MAX_LIMIT,
        )? {
            0 => None,
            limit => Some(limit),
        };
        let prefix = if !params.bookmark_prefix.is_empty() {
            Some(params.bookmark_prefix)
        } else {
            None
        };
        let repo = self.repo(ctx, &repo).await?;
        let bookmarks = repo
            .list_bookmarks(
                params.include_scratch,
                prefix.as_deref(),
                params.after.as_deref(),
                limit,
            )
            .await?
            .try_collect::<Vec<_>>()
            .await?;
        let continue_after = match limit {
            Some(limit) if bookmarks.len() as u64 >= limit => {
                bookmarks.last().map(|bookmark| bookmark.0.clone())
            }
            _ => None,
        };
        let ids = bookmarks.iter().map(|(_name, cs_id)| *cs_id).collect();
        let id_mapping = map_commit_identities(&repo, ids, &params.identity_schemes).await?;
        let bookmarks = bookmarks
            .into_iter()
            .map(|(name, cs_id)| match id_mapping.get(&cs_id) {
                Some(ids) => (name, ids.clone()),
                None => (name, BTreeMap::new()),
            })
            .collect();
        Ok(thrift::RepoListBookmarksResponse {
            bookmarks,
            continue_after,
            ..Default::default()
        })
    }

    async fn convert_create_commit_parents(
        repo: &RepoContext,
        parents: &[thrift::CommitId],
    ) -> Result<Vec<ChangesetId>, errors::ServiceError> {
        let parents: Vec<_> = parents
            .iter()
            .map(|parent| async move {
                let changeset_specifier = ChangesetSpecifier::from_request(parent)
                    .context("invalid commit id for parent")?;
                let changeset = repo
                    .changeset(changeset_specifier)
                    .await?
                    .ok_or_else(|| errors::commit_not_found(parent.to_string()))?;
                Ok::<_, errors::ServiceError>(changeset.id())
            })
            .collect::<FuturesOrdered<_>>()
            .try_collect()
            .await?;

        if parents.is_empty() && !repo.allow_no_parent_writes() {
            return Err(errors::invalid_request(
                "this repo does not permit commits without a parent",
            )
            .into());
        }

        Ok(parents)
    }

    async fn convert_create_commit_change(
        repo: &RepoContext,
        change: thrift::RepoCreateCommitParamsChange,
    ) -> Result<CreateChange, errors::ServiceError> {
        let change = match change {
            thrift::RepoCreateCommitParamsChange::changed(c) => {
                let file_type = FileType::from_request(&c.r#type)?;
                let copy_info = c
                    .copy_info
                    .as_ref()
                    .map(CreateCopyInfo::from_request)
                    .transpose()?;
                match c.content {
                    thrift::RepoCreateCommitParamsFileContent::id(id) => {
                        let file_id = FileId::from_request(&id)?;
                        let file = repo
                            .file(file_id)
                            .await?
                            .ok_or_else(|| errors::file_not_found(file_id.to_string()))?;
                        CreateChange::Tracked(
                            CreateChangeFile::Existing {
                                file_id: file.id().await?,
                                file_type,
                                maybe_size: None,
                            },
                            copy_info,
                        )
                    }
                    thrift::RepoCreateCommitParamsFileContent::content_sha1(sha) => {
                        let sha = Sha1::from_request(&sha)?;
                        let file = repo
                            .file_by_content_sha1(sha)
                            .await?
                            .ok_or_else(|| errors::file_not_found(sha.to_string()))?;
                        CreateChange::Tracked(
                            CreateChangeFile::Existing {
                                file_id: file.id().await?,
                                file_type,
                                maybe_size: None,
                            },
                            copy_info,
                        )
                    }
                    thrift::RepoCreateCommitParamsFileContent::content_sha256(sha) => {
                        let sha = Sha256::from_request(&sha)?;
                        let file = repo
                            .file_by_content_sha256(sha)
                            .await?
                            .ok_or_else(|| errors::file_not_found(sha.to_string()))?;
                        CreateChange::Tracked(
                            CreateChangeFile::Existing {
                                file_id: file.id().await?,
                                file_type,
                                maybe_size: None,
                            },
                            copy_info,
                        )
                    }
                    thrift::RepoCreateCommitParamsFileContent::content_gitsha1(sha) => {
                        let sha = GitSha1::from_request(&sha)?;
                        let file = repo
                            .file_by_content_gitsha1(sha)
                            .await?
                            .ok_or_else(|| errors::file_not_found(sha.to_string()))?;
                        CreateChange::Tracked(
                            CreateChangeFile::Existing {
                                file_id: file.id().await?,
                                file_type,
                                maybe_size: None,
                            },
                            copy_info,
                        )
                    }
                    thrift::RepoCreateCommitParamsFileContent::data(data) => CreateChange::Tracked(
                        CreateChangeFile::New {
                            bytes: Bytes::from(data),
                            file_type,
                        },
                        copy_info,
                    ),
                    thrift::RepoCreateCommitParamsFileContent::UnknownField(t) => {
                        return Err(errors::invalid_request(format!(
                            "file content type not supported: {}",
                            t
                        ))
                        .into());
                    }
                }
            }
            thrift::RepoCreateCommitParamsChange::deleted(_d) => CreateChange::Deletion,
            thrift::RepoCreateCommitParamsChange::UnknownField(t) => {
                return Err(errors::invalid_request(format!(
                    "file change type not supported: {}",
                    t
                ))
                .into());
            }
        };
        Ok(change)
    }

    async fn convert_create_commit_changes(
        repo: &RepoContext,
        changes: BTreeMap<String, thrift::RepoCreateCommitParamsChange>,
    ) -> Result<BTreeMap<MononokePath, CreateChange>, errors::ServiceError> {
        let changes = changes
            .into_iter()
            .map(|(path, change)| async move {
                let path = MononokePath::try_from(&path).map_err(|e| {
                    errors::invalid_request(format!("invalid path '{}': {}", path, e))
                })?;
                let change = Self::convert_create_commit_change(repo, change).await?;
                Ok::<_, errors::ServiceError>((path, change))
            })
            .collect::<FuturesOrdered<_>>()
            .try_collect()
            .await?;

        Ok(changes)
    }

    /// Create a new commit.
    pub(crate) async fn repo_create_commit(
        &self,
        ctx: CoreContext,
        repo: thrift::RepoSpecifier,
        params: thrift::RepoCreateCommitParams,
    ) -> Result<thrift::RepoCreateCommitResponse, errors::ServiceError> {
        let repo = self
            .repo_for_service(ctx, &repo, params.service_identity.clone())
            .await?;

        let parents = Self::convert_create_commit_parents(&repo, &params.parents).await?;
        let info = CreateInfo::from_request(&params.info)?;
        let changes = Self::convert_create_commit_changes(&repo, params.changes).await?;
        let bubble = None;

        let changeset = repo
            .create_changeset(parents, info, changes, bubble)
            .await?;

        // If you ask for a git identity back, then we'll assume that you supplied one to us
        // and set it. Later, when we can derive a git commit hash, this'll become more
        // open, because we'll only do the check if you ask for a hash different to the
        // one we would derive
        if params
            .identity_schemes
            .contains(&thrift::CommitIdentityScheme::GIT)
        {
            repo.set_git_mapping_from_changeset(&changeset).await?;
        }
        let ids = map_commit_identity(&changeset, &params.identity_schemes).await?;
        Ok(thrift::RepoCreateCommitResponse {
            ids,
            ..Default::default()
        })
    }

    /// Create a new stack of commits.
    pub(crate) async fn repo_create_stack(
        &self,
        ctx: CoreContext,
        repo: thrift::RepoSpecifier,
        params: thrift::RepoCreateStackParams,
    ) -> Result<thrift::RepoCreateStackResponse, errors::ServiceError> {
        let repo = self
            .repo_for_service(ctx, &repo, params.service_identity.clone())
            .await?;
        let repo = &repo;

        let stack_parents = Self::convert_create_commit_parents(repo, &params.parents).await?;
        let info_stack = params
            .commits
            .iter()
            .map(|commit| CreateInfo::from_request(&commit.info))
            .collect::<Result<Vec<_>, _>>()?;
        let changes_stack =
            stream::iter(
                params.commits.into_iter().map({
                    |commit| async move {
                        Self::convert_create_commit_changes(repo, commit.changes).await
                    }
                }),
            )
            .buffered(10)
            .try_collect::<Vec<_>>()
            .await?;
        let bubble = None;

        let stack = repo
            .create_changeset_stack(stack_parents, info_stack, changes_stack, bubble)
            .await?;

        // Prepare derived data if we were asked to.  Simple implementation
        // that doesn't support parallel derivation or dependencies between types.
        if let Some(prepare_types) = &params.prepare_derived_data_types {
            let csids = stack.iter().map(|c| c.id()).collect::<Vec<_>>();
            for derived_data_type in prepare_types {
                let derivable_type = DerivableType::from_request(derived_data_type)?;
                repo.prepare_derived_data(derivable_type, csids.clone())
                    .await?;
            }
        }

        // If you ask for a git identity back, then we'll assume that you supplied one to us
        // and set it. Later, when we can derive a git commit hash, this'll become more
        // open, because we'll only do the check if you ask for a hash different to the
        // one we would derive
        if params
            .identity_schemes
            .contains(&thrift::CommitIdentityScheme::GIT)
        {
            for changeset in stack.iter() {
                repo.set_git_mapping_from_changeset(changeset).await?;
            }
        }
        let identity_schemes = &params.identity_schemes;
        let commit_ids = stream::iter(stack.into_iter().map(|changeset| async move {
            map_commit_identity(&changeset, identity_schemes).await
        }))
        .buffered(10)
        .try_collect::<Vec<_>>()
        .await?;
        Ok(thrift::RepoCreateStackResponse {
            commit_ids,
            ..Default::default()
        })
    }

    /// Build stacks for the given list of heads.
    ///
    /// Returns the IDs of the changeset in the requested identity schemes.
    /// Draft nodes and first public roots.
    /// The changesets are returned in topological order.
    ///
    /// Best effort: missing changesets are skipped,
    ///              building stack up to provided limit.
    pub(crate) async fn repo_stack_info(
        &self,
        ctx: CoreContext,
        repo: thrift::RepoSpecifier,
        params: thrift::RepoStackInfoParams,
    ) -> Result<thrift::RepoStackInfoResponse, errors::ServiceError> {
        let repo = self.repo(ctx, &repo).await?;

        // Check the limit
        let limit = check_range_and_convert(
            "limit",
            params.limit,
            0..=thrift::consts::REPO_STACK_INFO_MAX_LIMIT,
        )?;

        // parse changeset specifiers from params
        let head_specifiers = params
            .heads
            .iter()
            .map(ChangesetSpecifier::from_request)
            .collect::<Result<Vec<_>, _>>()?;

        // convert changeset specifiers to bonsai changeset ids
        // missing changesets are skipped
        #[allow(clippy::filter_map_identity)]
        let heads_ids = try_join_all(
            head_specifiers
                .into_iter()
                .map(|specifier| repo.resolve_specifier(specifier)),
        )
        .await?
        .into_iter()
        .filter_map(std::convert::identity)
        .collect::<Vec<_>>();

        // get stack
        let stack = repo.stack(heads_ids, limit).await?;

        // resolve draft changesets & public changesets
        let (draft_commits, public_parents, leftover_heads) = try_join!(
            try_join_all(
                stack
                    .draft
                    .into_iter()
                    .map(|cs_id| repo.changeset(ChangesetSpecifier::Bonsai(cs_id))),
            ),
            try_join_all(
                stack
                    .public
                    .into_iter()
                    .map(|cs_id| repo.changeset(ChangesetSpecifier::Bonsai(cs_id))),
            ),
            try_join_all(
                stack
                    .leftover_heads
                    .into_iter()
                    .map(|cs_id| repo.changeset(ChangesetSpecifier::Bonsai(cs_id))),
            ),
        )?;

        if draft_commits.len() <= params.heads.len() && !leftover_heads.is_empty() {
            Err(errors::limit_too_low(limit))?;
        }

        // generate response
        match (
            draft_commits.into_iter().collect::<Option<Vec<_>>>(),
            public_parents.into_iter().collect::<Option<Vec<_>>>(),
            leftover_heads.into_iter().collect::<Option<Vec<_>>>(),
        ) {
            (Some(draft_commits), Some(public_parents), Some(leftover_heads)) => {
                let (mut draft_commits, public_parents, leftover_heads) = try_join!(
                    try_join_all(
                        draft_commits
                            .into_iter()
                            .map(|cs| cs.into_response_with(&params.identity_schemes)),
                    ),
                    try_join_all(
                        public_parents
                            .into_iter()
                            .map(|cs| cs.into_response_with(&params.identity_schemes)),
                    ),
                    leftover_heads.into_response_with(&params.identity_schemes),
                )?;

                // Need to return the draft commits in topological order to meet the API definition
                // at https://fburl.com/code/a017qoam.
                draft_commits.sort_by_key(|commit| commit.generation);
                draft_commits.reverse();

                Ok(thrift::RepoStackInfoResponse {
                    draft_commits,
                    public_parents,
                    leftover_heads,
                    ..Default::default()
                })
            }
            _ => Err(
                errors::internal_error("unexpected failure to resolve an existing commit").into(),
            ),
        }
    }

    pub(crate) async fn repo_create_bookmark(
        &self,
        ctx: CoreContext,
        repo: thrift::RepoSpecifier,
        params: thrift::RepoCreateBookmarkParams,
    ) -> Result<thrift::RepoCreateBookmarkResponse, errors::ServiceError> {
        let repo = self
            .repo_for_service(ctx, &repo, params.service_identity)
            .await?;
        let target = &params.target;
        let changeset = repo
            .changeset(ChangesetSpecifier::from_request(target)?)
            .await?
            .ok_or_else(|| errors::commit_not_found(target.to_string()))?;
        let pushvars = convert_pushvars(params.pushvars);

        repo.create_bookmark(
            &BookmarkKey::new(&params.bookmark).map_err(Into::<MononokeError>::into)?,
            changeset.id(),
            pushvars.as_ref(),
        )
        .await?;
        Ok(thrift::RepoCreateBookmarkResponse {
            ..Default::default()
        })
    }

    pub(crate) async fn repo_move_bookmark(
        &self,
        ctx: CoreContext,
        repo: thrift::RepoSpecifier,
        params: thrift::RepoMoveBookmarkParams,
    ) -> Result<thrift::RepoMoveBookmarkResponse, errors::ServiceError> {
        let repo = self
            .repo_for_service(ctx, &repo, params.service_identity)
            .await?;
        let target = &params.target;
        let changeset = repo
            .changeset(ChangesetSpecifier::from_request(target)?)
            .await?
            .ok_or_else(|| errors::commit_not_found(target.to_string()))?;
        let old_changeset_id = match &params.old_target {
            Some(old_target) => Some(
                repo.changeset(ChangesetSpecifier::from_request(old_target)?)
                    .await
                    .context("failed to resolve old target")?
                    .ok_or_else(|| errors::commit_not_found(old_target.to_string()))?
                    .id(),
            ),
            None => None,
        };
        let pushvars = convert_pushvars(params.pushvars);

        repo.move_bookmark(
            &BookmarkKey::new(&params.bookmark).map_err(Into::<MononokeError>::into)?,
            changeset.id(),
            old_changeset_id,
            params.allow_non_fast_forward_move,
            pushvars.as_ref(),
        )
        .await?;
        Ok(thrift::RepoMoveBookmarkResponse {
            ..Default::default()
        })
    }

    pub(crate) async fn repo_delete_bookmark(
        &self,
        ctx: CoreContext,
        repo: thrift::RepoSpecifier,
        params: thrift::RepoDeleteBookmarkParams,
    ) -> Result<thrift::RepoDeleteBookmarkResponse, errors::ServiceError> {
        let repo = self
            .repo_for_service(ctx, &repo, params.service_identity)
            .await?;
        let old_changeset_id = match &params.old_target {
            Some(old_target) => Some(
                repo.changeset(ChangesetSpecifier::from_request(old_target)?)
                    .await?
                    .ok_or_else(|| errors::commit_not_found(old_target.to_string()))?
                    .id(),
            ),
            None => None,
        };
        let pushvars = convert_pushvars(params.pushvars);

        repo.delete_bookmark(
            &BookmarkKey::new(&params.bookmark).map_err(Into::<MononokeError>::into)?,
            old_changeset_id,
            pushvars.as_ref(),
        )
        .await?;

        Ok(thrift::RepoDeleteBookmarkResponse {
            ..Default::default()
        })
    }

    /// Prepare commits for future operations.
    ///
    /// Perform any necessary pre-processing on the mononoke side to ensure that the commits
    /// are ready to be used later without incurring a performance penalty for repeated
    /// preparation.
    ///
    /// For now, concretely, "preparing" means deriving the fsnode data for a batch of commits.
    ///
    /// * The provided batch of commits must be in topological order.
    /// * The dependencies and ancestors of all commits in the batch must have already been derived.
    ///
    /// If these conditions are not met, an error will be returned.
    pub(crate) async fn repo_prepare_commits(
        &self,
        ctx: CoreContext,
        repo: thrift::RepoSpecifier,
        params: thrift::RepoPrepareCommitsParams,
    ) -> Result<thrift::RepoPrepareCommitsResponse, errors::ServiceError> {
        let repo = self.repo(ctx, &repo).await?;
        // Convert thrift commit ids to bonsai changeset ids
        let changesets = try_join_all(
            params
                .commits
                .iter()
                .map(ChangesetSpecifier::from_request)
                .collect::<Result<Vec<_>, _>>()?
                .into_iter()
                .map(|specifier| repo.changeset(specifier)),
        )
        .await?;
        let cs_ids = std::iter::zip(params.commits, changesets)
            .map(|(commit, cs)| {
                cs.map(|cs| cs.id())
                    .ok_or_else(|| errors::commit_not_found(commit.to_string()))
            })
            .collect::<Result<Vec<_>, _>>()?;

        // Derive data of the requested type for the batch of desired commits
        let derivable_type = DerivableType::from_request(&params.derived_data_type)?;
        repo.prepare_derived_data(derivable_type, cs_ids).await?;

        Ok(thrift::RepoPrepareCommitsResponse {
            ..Default::default()
        })
    }

    pub(crate) async fn repo_upload_file_content(
        &self,
        ctx: CoreContext,
        repo: thrift::RepoSpecifier,
        params: thrift::RepoUploadFileContentParams,
    ) -> Result<thrift::RepoUploadFileContentResponse, errors::ServiceError> {
        let repo = self
            .repo_for_service(ctx, &repo, params.service_identity)
            .await?;
        let mut store_request = StoreRequest::new(
            params
                .data
                .len()
                .try_into()
                .expect("usize should convert to u64"),
        );
        if let Some(expected_content_sha1) = &params.expected_content_sha1 {
            store_request.sha1 = Some(Sha1::from_request(expected_content_sha1)?);
        }
        if let Some(expected_content_sha256) = &params.expected_content_sha256 {
            store_request.sha256 = Some(Sha256::from_request(expected_content_sha256)?);
        }
        if params.expected_content_seeded_blake3.is_some() {
            return Err(errors::invalid_request(
                "Seeded blake3 not yet implemented for file upload",
            )
            .into());
        }

        let id = repo
            .upload_file_content(params.data.into(), &store_request)
            .await?
            .as_ref()
            .to_vec();
        Ok(thrift::RepoUploadFileContentResponse {
            id,
            ..Default::default()
        })
    }
}


/*
 * 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 context::CoreContext;
use source_control as thrift;

use crate::errors;
use crate::from_request::check_range_and_convert;
use crate::into_response::IntoResponse;
use crate::source_control_impl::SourceControlServiceImpl;

impl SourceControlServiceImpl {
    /// Determine whether a tree exists.
    pub(crate) async fn tree_exists(
        &self,
        ctx: CoreContext,
        tree: thrift::TreeSpecifier,
        _params: thrift::TreeExistsParams,
    ) -> Result<bool, errors::ServiceError> {
        let (_repo, tree) = self.repo_tree(ctx, &tree).await?;
        Ok(tree.is_some())
    }

    /// List the contents of a directory.
    pub(crate) async fn tree_list(
        &self,
        ctx: CoreContext,
        tree: thrift::TreeSpecifier,
        params: thrift::TreeListParams,
    ) -> Result<thrift::TreeListResponse, errors::ServiceError> {
        let (_repo, tree) = self.repo_tree(ctx, &tree).await?;
        let offset: usize = check_range_and_convert("offset", params.offset, 0..)?;
        let limit: usize = check_range_and_convert(
            "limit",
            params.limit,
            0..=source_control::TREE_LIST_MAX_LIMIT,
        )?;
        if let Some(tree) = tree {
            let summary = tree.summary().await?;
            let entries = tree
                .list()
                .await?
                .skip(offset)
                .take(limit)
                .map(IntoResponse::into_response)
                .collect();
            let response = thrift::TreeListResponse {
                entries,
                count: (summary.child_files_count + summary.child_dirs_count) as i64,
                ..Default::default()
            };
            Ok(response)
        } else {
            // Listing a path that is not a directory just returns an empty list.
            Ok(thrift::TreeListResponse {
                entries: Vec::new(),
                count: 0,
                ..Default::default()
            })
        }
    }
}


/*
 * 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_movement::describe_hook_rejections;
use bookmarks_movement::BookmarkKindRestrictions;
use bookmarks_movement::HookRejection;
use borrowed::borrowed;
use context::CoreContext;
use hooks::PushAuthoredBy;
use mononoke_api::ChangesetSpecifier;
use mononoke_api::MononokeError;
use pushrebase::PushrebaseConflict;
use service::RepoLandStackExn;
use source_control as thrift;
use source_control::services::source_control_service as service;

use crate::commit_id::CommitIdExt;
use crate::errors;
use crate::errors::LoggableError;
use crate::errors::ServiceErrorResultExt;
use crate::errors::Status;
use crate::from_request::convert_pushvars;
use crate::from_request::FromRequest;
use crate::into_response::AsyncIntoResponseWith;
use crate::source_control_impl::SourceControlServiceImpl;

enum LandStackError {
    Service(errors::ServiceError),
    PushrebaseConflicts(Vec<PushrebaseConflict>),
    HookRejections(Vec<HookRejection>),
}

impl From<errors::ServiceError> for LandStackError {
    fn from(e: errors::ServiceError) -> Self {
        Self::Service(e)
    }
}

impl From<MononokeError> for LandStackError {
    fn from(e: MononokeError) -> Self {
        match e {
            MononokeError::HookFailure(rejections) => Self::HookRejections(rejections),
            MononokeError::PushrebaseConflicts(conflicts) => Self::PushrebaseConflicts(conflicts),
            e => Self::Service(e.into()),
        }
    }
}

impl From<anyhow::Error> for LandStackError {
    fn from(e: anyhow::Error) -> Self {
        Self::Service(errors::internal_error(e).into())
    }
}

impl From<thrift::RequestError> for LandStackError {
    fn from(e: thrift::RequestError) -> Self {
        Self::Service(e.into())
    }
}

fn reason_rejections(rejections: &Vec<HookRejection>) -> String {
    format!(
        "Hooks failed:\n{}",
        describe_hook_rejections(rejections.as_slice())
    )
}

fn reason_conflicts(conflicts: &Vec<PushrebaseConflict>) -> String {
    format!("Conflicts while pushrebasing: {:?}", conflicts)
}

fn convert_rejection(rejection: HookRejection) -> thrift::HookRejection {
    thrift::HookRejection {
        hook_name: rejection.hook_name,
        cs_id: Vec::from(rejection.cs_id.as_ref()),
        reason: thrift::HookOutcomeRejected {
            description: rejection.reason.description.to_string(),
            long_description: rejection.reason.long_description,
            ..Default::default()
        },
        ..Default::default()
    }
}

impl From<LandStackError> for RepoLandStackExn {
    fn from(e: LandStackError) -> RepoLandStackExn {
        match e {
            LandStackError::Service(e) => e.into(),
            LandStackError::HookRejections(rejections) => {
                RepoLandStackExn::hook_rejections(thrift::HookRejectionsException {
                    reason: reason_rejections(&rejections),
                    rejections: rejections.into_iter().map(convert_rejection).collect(),
                    ..Default::default()
                })
            }
            LandStackError::PushrebaseConflicts(conflicts) => {
                RepoLandStackExn::pushrebase_conflicts(thrift::PushrebaseConflictsException {
                    reason: reason_conflicts(&conflicts),
                    conflicts: conflicts
                        .into_iter()
                        .map(|c| thrift::PushrebaseConflict {
                            left: c.left.to_string(),
                            right: c.right.to_string(),
                            ..Default::default()
                        })
                        .collect(),
                    ..Default::default()
                })
            }
        }
    }
}

impl LoggableError for LandStackError {
    fn status_and_description(&self) -> (Status, String) {
        match self {
            Self::Service(svc) => svc.status_and_description(),
            Self::HookRejections(rejections) => {
                (Status::RequestError, reason_rejections(rejections))
            }
            Self::PushrebaseConflicts(conflicts) => {
                (Status::RequestError, reason_conflicts(conflicts))
            }
        }
    }
}

impl SourceControlServiceImpl {
    async fn impl_repo_land_stack(
        &self,
        ctx: CoreContext,
        repo: thrift::RepoSpecifier,
        params: thrift::RepoLandStackParams,
    ) -> Result<thrift::RepoLandStackResponse, LandStackError> {
        let push_authored_by = if params.service_identity.is_some() {
            PushAuthoredBy::Service
        } else {
            PushAuthoredBy::User
        };
        let repo = self
            .repo_for_service(ctx, &repo, params.service_identity)
            .await?;
        borrowed!(params.head, params.base);
        let head = repo
            .changeset(ChangesetSpecifier::from_request(head)?)
            .await
            .context("failed to resolve head commit")?
            .ok_or_else(|| errors::commit_not_found(head.to_string()))?;
        let base = repo
            .changeset(ChangesetSpecifier::from_request(base)?)
            .await
            .context("failed to resolve base commit")?
            .ok_or_else(|| errors::commit_not_found(base.to_string()))?;
        let pushvars = convert_pushvars(params.pushvars);
        let bookmark_restrictions =
            BookmarkKindRestrictions::from_request(&params.bookmark_restrictions)?;

        let pushrebase_outcome = repo
            .land_stack(
                &params.bookmark,
                head.id(),
                base.id(),
                pushvars.as_ref(),
                bookmark_restrictions,
                push_authored_by,
            )
            .await?
            .into_response_with(&(
                repo.clone(),
                params.identity_schemes,
                params.old_identity_schemes,
            ))
            .await?;

        Ok(thrift::RepoLandStackResponse {
            pushrebase_outcome,
            ..Default::default()
        })
    }

    pub(crate) async fn repo_land_stack(
        &self,
        ctx: CoreContext,
        repo: thrift::RepoSpecifier,
        params: thrift::RepoLandStackParams,
    ) -> Result<thrift::RepoLandStackResponse, impl Into<service::RepoLandStackExn> + LoggableError>
    {
        self.impl_repo_land_stack(ctx, repo, params).await
    }
}


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

use context::CoreContext;
use criterion::Criterion;
use mononoke_types::hash::Blake2;
use mononoke_types::ChangesetId;
use segmented_changelog::ConcurrentMemIdMap;
use segmented_changelog::DagId;
use segmented_changelog::IdMap;
use tokio::runtime::Runtime;

async fn insert(ctx: &CoreContext, idmap: &dyn IdMap, low: u64, high: u64) {
    let mut cs_id_bytes = [0u8; 32];
    for i in low..high {
        cs_id_bytes[0] = (i & 255) as u8;
        cs_id_bytes[1] = (i >> 8) as u8;
        let cs_id = ChangesetId::from(Blake2::from_byte_array(cs_id_bytes.clone()));
        // no good reason to join these futures
        idmap
            .insert(ctx, DagId(i), cs_id)
            .await
            .expect("failed to insert");
    }
}

async fn find(ctx: &CoreContext, idmap: &dyn IdMap, v: DagId) {
    let _ = idmap
        .find_changeset_id(ctx, v)
        .await
        .expect("failed_find_changeset_id");
}

fn insert_benchmark(c: &mut Criterion, runtime: &Runtime, ctx: &CoreContext) {
    c.bench_function("insert 0..10000", |b| {
        b.iter(|| {
            runtime.block_on(async {
                let idmap = ConcurrentMemIdMap::new();
                insert(ctx, &idmap, 0, 10000).await;
            });
        })
    });
}

fn read_benchmark(c: &mut Criterion, runtime: &Runtime, ctx: &CoreContext) {
    let idmap = ConcurrentMemIdMap::new();
    runtime.block_on(async {
        insert(ctx, &idmap, 0, 10000).await;
    });
    c.bench_function("read 0..10000", |b| {
        b.iter(|| {
            runtime.block_on(async {
                for chunk in (0..10000u64).collect::<Vec<_>>().chunks(1000) {
                    let mut f = vec![];
                    for j in chunk {
                        f.push(find(ctx, &idmap, DagId(*j)));
                    }
                    let _ = futures::future::join_all(f).await;
                }
            });
        })
    });
}

fn read_write_benchmark(c: &mut Criterion, runtime: &Runtime, ctx: &CoreContext) {
    let idmap = ConcurrentMemIdMap::new();
    let mx = 10000;
    runtime.block_on(async {
        insert(ctx, &idmap, 0, mx).await;
    });
    c.bench_function("read 0..10000; write 1..100", |b| {
        b.iter(|| {
            runtime.block_on(async {
                for i in 0..100 {
                    let mut read = vec![];
                    for j in 0..100 {
                        read.push(find(ctx, &idmap, DagId(i * 100 + j)));
                    }
                    let _ = futures::future::join(
                        futures::future::join_all(read),
                        insert(ctx, &idmap, mx + i, mx + i + 1),
                    )
                    .await;
                }
            });
        })
    });
}

#[fbinit::main]
fn main(fb: fbinit::FacebookInit) {
    let runtime = Runtime::new().expect("failed to initialize runtime");
    let ctx = CoreContext::test_mock(fb);

    let mut criterion = Criterion::default().sample_size(10);

    insert_benchmark(&mut criterion, &runtime, &ctx);
    read_benchmark(&mut criterion, &runtime, &ctx);
    read_write_benchmark(&mut criterion, &runtime, &ctx);

    criterion.final_summary();
}


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

use std::sync::Arc;

use anyhow::Context;
use anyhow::Result;
use blobstore::Blobstore;
use bookmarks::ArcBookmarks;
use caching_ext::CacheHandlerFactory;
use changeset_fetcher::ArcChangesetFetcher;
use context::CoreContext;
use metaconfig_types::SegmentedChangelogConfig;
use mononoke_types::RepositoryId;
use repo_identity::RepoIdentity;
use sql_construct::SqlConstruct;
use sql_construct::SqlConstructFromMetadataDatabaseConfig;
use sql_ext::replication::NoReplicaLagMonitor;
use sql_ext::SqlConnections;

use crate::iddag::IdDagSaveStore;
use crate::idmap::CacheHandlers;
use crate::idmap::ConcurrentMemIdMap;
use crate::idmap::IdMapFactory;
use crate::manager::SegmentedChangelogManager;
use crate::manager::SegmentedChangelogType;
use crate::on_demand::OnDemandUpdateSegmentedChangelog;
use crate::periodic_reload::PeriodicReloadSegmentedChangelog;
use crate::seedheads_from_config;
use crate::version_store::SegmentedChangelogVersionStore;
use crate::CloneHints;
use crate::DisabledSegmentedChangelog;
use crate::InProcessIdDag;
use crate::JobType;
use crate::SegmentedChangelog;

#[derive(Clone)]
pub struct SegmentedChangelogSqlConnections(pub SqlConnections);

impl SqlConstruct for SegmentedChangelogSqlConnections {
    const LABEL: &'static str = "segmented_changelog";

    const CREATION_QUERY: &'static str = include_str!("../schemas/sqlite-segmented-changelog.sql");

    fn from_sql_connections(connections: SqlConnections) -> Self {
        Self(connections)
    }
}

impl SqlConstructFromMetadataDatabaseConfig for SegmentedChangelogSqlConnections {}

pub fn new_test_segmented_changelog(
    ctx: CoreContext,
    repo_id: RepositoryId,
    config: &SegmentedChangelogConfig,
    changeset_fetcher: ArcChangesetFetcher,
    bookmarks: ArcBookmarks,
) -> Result<Arc<dyn SegmentedChangelog + Send + Sync>> {
    if !config.enabled {
        return Ok(Arc::new(DisabledSegmentedChangelog::new()));
    }
    let seed_heads = seedheads_from_config(&ctx, config, JobType::Server)
        .context("finding segmented changelog heads")?;
    Ok(Arc::new(OnDemandUpdateSegmentedChangelog::new(
        ctx,
        repo_id,
        InProcessIdDag::new_in_process(),
        Arc::new(ConcurrentMemIdMap::new()),
        changeset_fetcher,
        bookmarks,
        seed_heads,
        None,
    )?))
}

pub async fn new_server_segmented_changelog_manager<'a>(
    ctx: &'a CoreContext,
    repo_identity: &'a RepoIdentity,
    config: SegmentedChangelogConfig,
    connections: SegmentedChangelogSqlConnections,
    changeset_fetcher: ArcChangesetFetcher,
    bookmarks: ArcBookmarks,
    blobstore: Arc<dyn Blobstore>,
    cache_handler_factory: Option<CacheHandlerFactory>,
) -> Result<SegmentedChangelogManager> {
    let repo_id = repo_identity.id();
    let seed_heads = seedheads_from_config(ctx, &config, JobType::Server)
        .context("finding segmented changelog heads")?;
    let replica_lag_monitor = Arc::new(NoReplicaLagMonitor());
    let mut idmap_factory = IdMapFactory::new(connections.0.clone(), replica_lag_monitor, repo_id);
    if let Some(cache_handler_factory) = cache_handler_factory {
        idmap_factory =
            idmap_factory.with_cache_handlers(CacheHandlers::new(cache_handler_factory));
    }
    let sc_version_store = SegmentedChangelogVersionStore::new(connections.0.clone(), repo_id);
    let iddag_save_store = IdDagSaveStore::new(repo_id, blobstore.clone());
    let clone_hints = CloneHints::new(connections.0, repo_id, blobstore);
    let manager = SegmentedChangelogManager::new(
        repo_id,
        sc_version_store,
        iddag_save_store,
        idmap_factory,
        changeset_fetcher,
        bookmarks,
        seed_heads,
        SegmentedChangelogType::OnDemand {
            update_to_master_bookmark_period: config.update_to_master_bookmark_period,
        },
        Some(clone_hints),
    );
    Ok(manager)
}

pub async fn new_server_segmented_changelog<'a>(
    ctx: &'a CoreContext,
    repo_identity: &'a RepoIdentity,
    config: SegmentedChangelogConfig,
    connections: SegmentedChangelogSqlConnections,
    changeset_fetcher: ArcChangesetFetcher,
    bookmarks: ArcBookmarks,
    blobstore: Arc<dyn Blobstore>,
    cache_handler_factory: Option<CacheHandlerFactory>,
) -> Result<Arc<dyn SegmentedChangelog + Send + Sync>> {
    if !config.enabled {
        return Ok(Arc::new(DisabledSegmentedChangelog::new()));
    }
    if config.skip_dag_load_at_startup {
        let repo_id = repo_identity.id();
        let seed_heads = seedheads_from_config(ctx, &config, JobType::Server)
            .context("finding segmented changelog heads")?;
        // This is a special case. We build Segmented Changelog using an in process iddag and idmap
        // and update then on demand.
        // All other configuration is ignored, for example there won't be periodic updates
        // following a bookmark.
        return Ok(Arc::new(OnDemandUpdateSegmentedChangelog::new(
            ctx.clone(),
            repo_id,
            InProcessIdDag::new_in_process(),
            Arc::new(ConcurrentMemIdMap::new()),
            changeset_fetcher,
            bookmarks,
            seed_heads,
            None,
        )?));
    }
    let reload_dag_save_period = config.reload_dag_save_period;
    let manager = new_server_segmented_changelog_manager(
        ctx,
        repo_identity,
        config,
        connections,
        changeset_fetcher,
        bookmarks,
        blobstore,
        cache_handler_factory,
    )
    .await?;
    let name = repo_identity.name().to_string();
    let sc = match reload_dag_save_period {
        None => {
            let (sc, _sc_version) = manager.load(ctx).await?;
            sc
        }
        Some(reload_period) => Arc::new(
            PeriodicReloadSegmentedChangelog::start_from_manager(ctx, reload_period, manager, name)
                .await?,
        ),
    };
    Ok(sc)
}


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

use anyhow::Context;
use anyhow::Error;
use anyhow::Result;
use blobstore::Blobstore;
use blobstore::BlobstoreBytes;
use bonsai_hg_mapping::BonsaiHgMapping;
use context::CoreContext;
use futures::stream;
use futures::stream::StreamExt;
use futures::stream::TryStreamExt;
use mercurial_types::HgChangesetId;
use mononoke_types::hash;
use mononoke_types::ChangesetId;
use mononoke_types::RepositoryId;
use serde_derive::Deserialize;
use serde_derive::Serialize;
use slog::debug;
use slog::info;
use sql_ext::mononoke_queries;
use sql_ext::SqlConnections;

use crate::idmap::IdMap;
use crate::types::IdMapVersion;
use crate::update::ServerNameDag;
use crate::DagId;

/// Number of hint entries to store in a single chunk.
///
/// Each chunk will have exactly this many hints in it - we do not save partial chunks.
const HINTS_PER_CHUNK: usize = 5000;

mononoke_queries! {
    read SelectChunks(repo_id: RepositoryId, version: u64) -> (Vec<u8>) {
        "SELECT blob_name
        FROM segmented_changelog_clone_hints
        WHERE repo_id = {repo_id}
        AND idmap_version = {version}"
    }

    write InsertChunks(values: (repo_id: RepositoryId, version: u64, blob_name: &str)) {
        none,
        "INSERT INTO segmented_changelog_clone_hints
        (repo_id, idmap_version, blob_name)
        VALUES {values}"
    }
}

#[derive(Clone)]
pub struct CloneHints {
    inner: Arc<CloneHintsInner>,
}

struct CloneHintsInner {
    connections: SqlConnections,
    repo_id: RepositoryId,
    blobstore: Arc<dyn Blobstore>,
}

impl CloneHints {
    pub fn new(
        connections: SqlConnections,
        repo_id: RepositoryId,
        blobstore: Arc<dyn Blobstore>,
    ) -> Self {
        let inner = Arc::new(CloneHintsInner {
            connections,
            repo_id,
            blobstore,
        });
        Self { inner }
    }

    pub(crate) async fn fetch(
        &self,
        ctx: &CoreContext,
        idmap_version: IdMapVersion,
    ) -> Result<HashMap<DagId, (ChangesetId, HgChangesetId)>> {
        let hint_chunk_names = SelectChunks::query(
            &self.inner.connections.read_connection,
            &self.inner.repo_id,
            &idmap_version.0,
        )
        .await?;

        let mut hints = HashMap::new();
        stream::iter(hint_chunk_names)
            .map(move |(name,)| async move {
                self.inner
                    .blobstore
                    .get(
                        ctx,
                        std::str::from_utf8(&name).expect("Name should be UTF-8"),
                    )
                    .await
            })
            .buffer_unordered(100)
            .try_fold(&mut hints, |hints, blob| async move {
                if let Some(blob) = blob {
                    let blob = blob.as_raw_bytes();
                    let values: Vec<Hint> = mincode::deserialize(blob)?;
                    for hint in values {
                        hints.insert(
                            DagId(hint.dag_id),
                            (
                                ChangesetId::from_bytes(hint.cs_id)?,
                                HgChangesetId::from_bytes(&hint.hgcs_id)?,
                            ),
                        );
                    }
                }
                Ok(hints)
            })
            .await?;

        Ok(hints)
    }

    pub(crate) async fn add_hints(
        &self,
        ctx: &CoreContext,
        namedag: &ServerNameDag,
        idmap_version: IdMapVersion,
        bonsai_hg_mapping: &dyn BonsaiHgMapping,
    ) -> Result<()> {
        info!(
            ctx.logger(),
            "Adding hints for idmap_version {}", idmap_version.0
        );

        // Similar to `export_pull_data` in the dag crate.
        let id_dag = namedag.dag();
        let all_ids = id_dag.all().context("error computing all() ids")?;
        let flat_segments = id_dag
            .idset_to_flat_segments(all_ids)
            .context("error getting segments for all()")?;
        let ids: Vec<_> = flat_segments.parents_head_and_roots().into_iter().collect();
        let universal_ids_len = ids.len();
        let existing_hints = self.fetch(ctx, idmap_version).await?;
        let new_ids: Vec<_> = ids
            .into_iter()
            .filter(|id| !existing_hints.contains_key(id))
            .collect();

        if new_ids.len() < HINTS_PER_CHUNK {
            info!(
                ctx.logger(),
                "idmap_version {} has a full set of hints ({} unhinted IDs is less than chunk size of {})",
                idmap_version.0,
                new_ids.len(),
                HINTS_PER_CHUNK
            );
            return Ok(());
        }

        debug!(
            ctx.logger(),
            "Found {} universally known IDs, {} in existing hints, {} to find",
            universal_ids_len,
            existing_hints.len(),
            new_ids.len()
        );

        let idmap_entries = namedag
            .map()
            .as_inner()
            .find_many_changeset_ids(ctx, new_ids.clone())
            .await
            .context("error retrieving mappings for dag universal ids")?;

        let csids: Vec<_> = idmap_entries.values().copied().collect();
        let hg_mapping: HashMap<_, _> = bonsai_hg_mapping
            .get(ctx, csids.into())
            .await
            .context("error converting from bonsai to hg")?
            .into_iter()
            .map(|mapping| (mapping.bcs_id, mapping.hg_cs_id))
            .collect();

        let new_hints: Vec<Hint> = new_ids
            .into_iter()
            .filter_map(|dag_id| {
                let cs_id = idmap_entries.get(&dag_id)?;
                let hgcs_id = hg_mapping.get(cs_id)?;
                let dag_id = dag_id.0;
                let cs_id = cs_id.blake2().into_inner();
                let hgcs_id = hgcs_id.as_bytes().try_into().ok()?;
                Some(Hint {
                    dag_id,
                    cs_id,
                    hgcs_id,
                })
            })
            .collect();

        debug!(ctx.logger(), "Uploading {} hint entries", new_hints.len());

        let chunks = new_hints
            .chunks_exact(HINTS_PER_CHUNK)
            .map(|chunk| {
                let chunk: Vec<_> = chunk.iter().collect();
                mincode::serialize(&chunk)
            })
            .collect::<Result<Vec<_>, _>>()?;

        let hint_blob_keys: Vec<_> = stream::iter(chunks.into_iter().map(|chunk| async move {
            let chunk_hash = {
                let mut context = hash::Context::new(b"segmented_clone");
                context.update(&chunk);
                context.finish()
            };
            let chunk_key = format!("segmented_clone_v1_idmapv{}.{}", idmap_version, chunk_hash);
            let blob = BlobstoreBytes::from_bytes(chunk);
            self.inner
                .blobstore
                .put(ctx, chunk_key.clone(), blob)
                .await?;
            debug!(ctx.logger(), "Uploaded hint entry {}", &chunk_key);
            Ok::<_, Error>(chunk_key)
        }))
        .buffer_unordered(100)
        .try_collect()
        .await?;

        let hint_blob_keys: Vec<&str> = hint_blob_keys.iter().map(|s| s.as_str()).collect();

        let hint_blob_values: Vec<_> = hint_blob_keys
            .iter()
            .map(|key| (&self.inner.repo_id, &idmap_version.0, key))
            .collect();
        InsertChunks::query(
            &self.inner.connections.write_connection,
            &hint_blob_values[..],
        )
        .await?;

        info!(ctx.logger(), "Hints uploaded",);
        Ok(())
    }
}

#[derive(Serialize, Deserialize)]
struct Hint {
    dag_id: u64,
    cs_id: [u8; 32],
    hgcs_id: [u8; 20],
}


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

use std::sync::Arc;

use anyhow::format_err;
use anyhow::Context;
use anyhow::Result;
use blobstore::Blobstore;
use context::CoreContext;
use mononoke_types::ChangesetId;
use mononoke_types::RepositoryId;
use sql_ext::replication::ReplicaLagMonitor;

use crate::iddag::IdDagSaveStore;
use crate::idmap::IdMap;
use crate::idmap::SqlIdMap;
use crate::types::SegmentedChangelogVersion;
use crate::version_store::SegmentedChangelogVersionStore;
use crate::InProcessIdDag;
use crate::SegmentedChangelogSqlConnections;

pub async fn copy_segmented_changelog(
    ctx: &CoreContext,
    repo_id: RepositoryId,
    connections: SegmentedChangelogSqlConnections,
    blobstore: Arc<dyn Blobstore>,
    replica_lag_monitor: Arc<dyn ReplicaLagMonitor>,
    heads: Vec<ChangesetId>,
) -> Result<()> {
    let iddag_save_store = IdDagSaveStore::new(repo_id, blobstore);
    let sc_version_store = SegmentedChangelogVersionStore::new(connections.0.clone(), repo_id);
    let sc_version = sc_version_store
        .get(ctx)
        .await
        .with_context(|| {
            format!(
                "repo {}: error loading segmented changelog version",
                repo_id
            )
        })?
        .ok_or_else(|| {
            format_err!(
                "repo {}: segmented changelog metadata not found, maybe repo is not seeded",
                repo_id
            )
        })?;

    let old_iddag = iddag_save_store
        .load(ctx, sc_version.iddag_version)
        .await
        .with_context(|| format!("repo {}: failed to load iddag", repo_id))?;

    let idmap = SqlIdMap::new(
        connections.0,
        replica_lag_monitor,
        repo_id,
        sc_version.idmap_version,
    );

    let dag_limit = idmap
        .find_many_dag_ids(ctx, heads.clone())
        .await?
        .into_values()
        .max()
        .with_context(|| format!("repo {}: no valid heads in {:?}", repo_id, heads))?;

    let new_idmap_version = sc_version.idmap_version.bump();
    let _new_idmap = idmap.copy(dag_limit, new_idmap_version).await?;

    // Build an IdDag - we can use the old IdDag's shape to speed things up,
    // as we know that the new IdDag is a subset of the old one.
    let mut new_iddag = InProcessIdDag::new_in_process();
    let get_parents = |id| old_iddag.parent_ids(id);
    new_iddag.build_segments(dag_limit, &get_parents)?;

    let iddag_version = iddag_save_store
        .save(ctx, &new_iddag)
        .await
        .with_context(|| format!("repo {}: error saving iddag", repo_id))?;

    let sc_version = SegmentedChangelogVersion::new(iddag_version, new_idmap_version);
    sc_version_store
        .set(ctx, sc_version)
        .await
        .with_context(|| {
            format!(
                "repo {}: error updating segmented changelog version store",
                repo_id
            )
        })?;

    Ok(())
}


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

//! Segmented Changelog
//!
//! This represents an implementation for the core commit graph that we have
//! in a given repository. It provides algorithms over the commit graph.
use std::collections::HashMap;

use anyhow::format_err;
use anyhow::Result;
use async_trait::async_trait;
use context::CoreContext;
use mononoke_types::ChangesetId;

mod builder;
mod clone_hints;
mod copy;
mod iddag;
mod idmap;
mod logging;
mod manager;
mod on_demand;
mod owned;
mod parents;
mod periodic_reload;
mod read_only;
mod sql_types;
mod tailer;
pub mod types;
mod update;
mod version_store;

#[cfg(test)]
mod tests;

use mercurial_types::HgChangesetId;
pub use segmented_changelog_types::dag;
pub use segmented_changelog_types::ArcSegmentedChangelog;
pub use segmented_changelog_types::CloneData;
pub use segmented_changelog_types::DagId;
pub use segmented_changelog_types::DagIdSet;
pub use segmented_changelog_types::FirstAncestorConstraint;
pub use segmented_changelog_types::FlatSegment;
pub use segmented_changelog_types::Group;
pub use segmented_changelog_types::InProcessIdDag;
pub use segmented_changelog_types::Location;
pub use segmented_changelog_types::MismatchedHeadsError;
pub use segmented_changelog_types::PreparedFlatSegments;
pub use segmented_changelog_types::SegmentedChangelog;
pub use segmented_changelog_types::SegmentedChangelogArc;
pub use segmented_changelog_types::SegmentedChangelogRef;

pub use crate::builder::new_server_segmented_changelog;
pub use crate::builder::new_server_segmented_changelog_manager;
pub use crate::builder::new_test_segmented_changelog;
pub use crate::builder::SegmentedChangelogSqlConnections;
pub use crate::clone_hints::CloneHints;
pub use crate::copy::copy_segmented_changelog;
// public for benchmarking
pub use crate::idmap::ConcurrentMemIdMap;
pub use crate::idmap::IdMap;
pub use crate::manager::ArcSegmentedChangelogManager;
pub use crate::manager::SegmentedChangelogManager;
pub use crate::tailer::OperationMode;
pub use crate::tailer::SegmentedChangelogTailer;
pub use crate::update::seedheads_from_config;
pub use crate::update::JobType;
pub use crate::update::SeedHead;

// TODO(T74420661): use `thiserror` to represent error case

pub struct DisabledSegmentedChangelog;

impl DisabledSegmentedChangelog {
    pub fn new() -> Self {
        Self {}
    }
}

#[async_trait]
impl SegmentedChangelog for DisabledSegmentedChangelog {
    async fn location_to_many_changeset_ids(
        &self,
        _ctx: &CoreContext,
        _location: Location<ChangesetId>,
        _count: u64,
    ) -> Result<Vec<ChangesetId>> {
        // TODO(T74420661): use `thiserror` to represent error case
        Err(format_err!(
            "Segmented Changelog is not enabled for this repo",
        ))
    }

    async fn clone_data(
        &self,
        _ctx: &CoreContext,
    ) -> Result<(CloneData<ChangesetId>, HashMap<ChangesetId, HgChangesetId>)> {
        Err(format_err!(
            "Segmented Changelog is not enabled for this repo",
        ))
    }

    async fn pull_data(
        &self,
        _ctx: &CoreContext,
        _common: Vec<ChangesetId>,
        _missing: Vec<ChangesetId>,
    ) -> Result<CloneData<ChangesetId>> {
        Err(format_err!(
            "Segmented Changelog is not enabled for this repo",
        ))
    }

    async fn many_changeset_ids_to_locations(
        &self,
        _ctx: &CoreContext,
        _master_heads: Vec<ChangesetId>,
        _cs_ids: Vec<ChangesetId>,
    ) -> Result<HashMap<ChangesetId, Result<Location<ChangesetId>>>> {
        Err(format_err!(
            "Segmented Changelog is not enabled for this repo",
        ))
    }

    async fn disabled(&self, _ctx: &CoreContext) -> Result<bool> {
        Ok(true)
    }

    async fn is_ancestor(
        &self,
        _ctx: &CoreContext,
        _ancestor: ChangesetId,
        _descendant: ChangesetId,
    ) -> Result<Option<bool>> {
        // None means inconclusive result, it can be returned safely
        Ok(None)
    }
}

#[macro_export]
macro_rules! segmented_changelog_delegate {
    ($type:ident, |&$self:ident, $ctx:ident: &CoreContext,| $delegate:block) => {
        // the difference in the pattern is the extra comma after Context
        segmented_changelog_delegate!($type, |&$self, $ctx: &CoreContext| $delegate);
    };
    ($type:ident, |&$self:ident, $ctx:ident: &CoreContext| $delegate:block) => {
        #[async_trait]
        impl SegmentedChangelog for $type {
            async fn location_to_many_changeset_ids(
                &$self,
                $ctx: &CoreContext,
                location: Location<ChangesetId>,
                count: u64,
            ) -> Result<Vec<ChangesetId>> {
                let delegate = $delegate;
                delegate
                    .location_to_many_changeset_ids($ctx, location, count)
                    .await
            }

            async fn clone_data(
                &$self,
                $ctx: &CoreContext
            ) -> Result<(CloneData<ChangesetId>, HashMap<ChangesetId, ::mercurial_types::HgChangesetId>)> {
                let delegate = $delegate;
                delegate.clone_data($ctx).await
            }

            async fn pull_data(
                &$self,
                $ctx: &CoreContext,
                common: Vec<ChangesetId>,
                missing: Vec<ChangesetId>,
            ) -> Result<CloneData<ChangesetId>> {
                let delegate = $delegate;
                delegate.pull_data($ctx, common, missing).await
            }

            async fn many_changeset_ids_to_locations(
                &$self,
                $ctx: &CoreContext,
                master_heads: Vec<ChangesetId>,
                cs_ids: Vec<ChangesetId>,
            ) -> Result<HashMap<ChangesetId, Result<Location<ChangesetId>>>> {
                let delegate = $delegate;
                delegate
                    .many_changeset_ids_to_locations($ctx, master_heads, cs_ids)
                    .await
            }

            async fn disabled(&$self, $ctx: &CoreContext) -> Result<bool> {
                let delegate = $delegate;
                delegate.disabled($ctx).await
            }

            async fn is_ancestor(
                &$self,
                $ctx: &CoreContext,
                ancestor: ChangesetId,
                descendant: ChangesetId,
            ) -> Result<Option<bool>> {
                let delegate = $delegate;
                delegate.is_ancestor($ctx, ancestor, descendant).await
            }

            async fn build_up_to_heads(&$self, $ctx: &CoreContext, heads: &[ChangesetId]) -> Result<bool> {
                let delegate = $delegate;
                delegate.build_up_to_heads($ctx, heads).await
            }
        }
    };
}


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

use context::CoreContext;
use mononoke_types::RepositoryId;
use scuba_ext::MononokeScubaSampleBuilder;

use crate::types::IdDagVersion;
use crate::types::SegmentedChangelogVersion;

const SCUBA_TABLE: &str = "segmented_changelog_version";

pub fn log_new_iddag_version(
    ctx: &CoreContext,
    repo_id: RepositoryId,
    iddag_version: IdDagVersion,
) {
    if let Some(mut sample_builder) = new_sample_builder(ctx) {
        sample_builder
            .add("type", "iddag")
            .add("repo_id", repo_id.id())
            .add("iddag_version", format!("{}", iddag_version.0))
            .log(); // note that logging may fail
    }
}

pub fn log_new_segmented_changelog_version(
    ctx: &CoreContext,
    repo_id: RepositoryId,
    sc_version: SegmentedChangelogVersion,
) {
    slog::info!(
        ctx.logger(),
        "segmented changelog version saved, idmap_version: {}, iddag_version: {}",
        sc_version.idmap_version,
        sc_version.iddag_version,
    );
    if let Some(mut sample_builder) = new_sample_builder(ctx) {
        sample_builder
            .add("type", "segmented_changelog")
            .add("repo_id", repo_id.id())
            .add("idmap_version", sc_version.idmap_version.0)
            .add("iddag_version", format!("{}", sc_version.iddag_version.0))
            .log(); // note that logging may fail
    }
}

fn new_sample_builder(ctx: &CoreContext) -> Option<MononokeScubaSampleBuilder> {
    // We construct a completely new scuba sample builder to log to the version scuba table so we
    // check the context to verify if we are in an environment where we are allowed to log.
    // We want to avoid logging from tests.
    if ctx.scuba().is_discard() {
        return None;
    }
    let mut sample_builder = MononokeScubaSampleBuilder::new(ctx.fb, SCUBA_TABLE).ok()?;
    sample_builder.add_common_server_data();
    Some(sample_builder)
}


/*
 * 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::sync::Arc;
use std::time::Duration;

use anyhow::format_err;
use anyhow::Context;
use anyhow::Result;
use async_trait::async_trait;
use bookmarks::Bookmarks;
use changeset_fetcher::ArcChangesetFetcher;
use context::CoreContext;
use futures_stats::TimedFutureExt;
use mononoke_types::ChangesetId;
use mononoke_types::RepositoryId;

use crate::iddag::IdDagSaveStore;
use crate::idmap::IdMapFactory;
use crate::on_demand::OnDemandUpdateSegmentedChangelog;
use crate::owned::OwnedSegmentedChangelog;
use crate::segmented_changelog_delegate;
use crate::types::SegmentedChangelogVersion;
use crate::version_store::SegmentedChangelogVersionStore;
use crate::CloneData;
use crate::CloneHints;
use crate::Location;
use crate::SeedHead;
use crate::SegmentedChangelog;

pub enum SegmentedChangelogType {
    OnDemand {
        update_to_master_bookmark_period: Option<Duration>,
    },
    #[cfg(test)]
    Owned,
}

#[facet::facet]
pub struct SegmentedChangelogManager {
    repo_id: RepositoryId,
    sc_version_store: SegmentedChangelogVersionStore,
    iddag_save_store: IdDagSaveStore,
    idmap_factory: IdMapFactory,
    changeset_fetcher: ArcChangesetFetcher,
    bookmarks: Arc<dyn Bookmarks>,
    seed_heads: Vec<SeedHead>,
    segmented_changelog_type: SegmentedChangelogType,
    clone_hints: Option<CloneHints>,
}

impl SegmentedChangelogManager {
    pub fn new(
        repo_id: RepositoryId,
        sc_version_store: SegmentedChangelogVersionStore,
        iddag_save_store: IdDagSaveStore,
        idmap_factory: IdMapFactory,
        changeset_fetcher: ArcChangesetFetcher,
        bookmarks: Arc<dyn Bookmarks>,
        seed_heads: Vec<SeedHead>,
        segmented_changelog_type: SegmentedChangelogType,
        clone_hints: Option<CloneHints>,
    ) -> Self {
        Self {
            repo_id,
            sc_version_store,
            iddag_save_store,
            idmap_factory,
            changeset_fetcher,
            bookmarks,
            seed_heads,
            segmented_changelog_type,
            clone_hints,
        }
    }

    pub async fn load(
        &self,
        ctx: &CoreContext,
    ) -> Result<(
        Arc<dyn SegmentedChangelog + Send + Sync>,
        SegmentedChangelogVersion,
    )> {
        let monitored = async {
            let (asc, sc_version): (Arc<dyn SegmentedChangelog + Send + Sync>, _) =
                match self.segmented_changelog_type {
                    SegmentedChangelogType::OnDemand {
                        update_to_master_bookmark_period,
                    } => {
                        let (on_demand, sc_version) = self.load_ondemand_update(ctx).await?;
                        let on_demand: Arc<dyn SegmentedChangelog + Send + Sync> =
                            match update_to_master_bookmark_period {
                                None => on_demand,
                                Some(period) => Arc::new(
                                    on_demand.with_periodic_update_to_master_bookmark(ctx, period),
                                ),
                            };
                        (on_demand, sc_version)
                    }
                    #[cfg(test)]
                    SegmentedChangelogType::Owned => {
                        let (sc, sc_version) = self.load_owned(ctx).await?;
                        (Arc::new(sc), sc_version)
                    }
                };
            Ok((asc, sc_version))
        };

        let (stats, ret) = monitored.timed().await;
        let mut scuba = ctx.scuba().clone();
        scuba.add_future_stats(&stats);
        scuba.add("repo_id", self.repo_id.id());
        scuba.add("success", ret.is_ok());
        let msg = ret.as_ref().err().map(|err| format!("{:?}", err));
        scuba.log_with_msg("segmented_changelog_load", msg);

        ret
    }

    async fn load_ondemand_update(
        &self,
        ctx: &CoreContext,
    ) -> Result<(
        Arc<OnDemandUpdateSegmentedChangelog>,
        SegmentedChangelogVersion,
    )> {
        let (owned, sc_version) = self.load_owned(ctx).await.with_context(|| {
            format!("repo {}: failed to load segmented changelog", self.repo_id)
        })?;
        Ok((
            Arc::new(OnDemandUpdateSegmentedChangelog::new(
                ctx.clone(),
                self.repo_id,
                owned.iddag,
                owned.idmap,
                Arc::clone(&self.changeset_fetcher),
                Arc::clone(&self.bookmarks),
                self.seed_heads.clone(),
                self.clone_hints.clone(),
            )?),
            sc_version,
        ))
    }

    // public for builder only
    pub async fn load_owned(
        &self,
        ctx: &CoreContext,
    ) -> Result<(OwnedSegmentedChangelog, SegmentedChangelogVersion)> {
        let sc_version = self.latest_version(ctx).await?;
        let iddag = self
            .iddag_save_store
            .load(ctx, sc_version.iddag_version)
            .await
            .with_context(|| format!("repo {}: failed to load iddag", self.repo_id))?;
        let idmap = self
            .idmap_factory
            .for_server(ctx, sc_version.idmap_version, &iddag)?;
        slog::debug!(
            ctx.logger(),
            "segmented changelog dag successfully loaded - repo_id: {}, idmap_version: {}, \
            iddag_version: {} ",
            self.repo_id,
            sc_version.idmap_version,
            sc_version.iddag_version,
        );
        let owned = OwnedSegmentedChangelog::new(iddag, idmap);
        Ok((owned, sc_version))
    }

    pub async fn latest_version(&self, ctx: &CoreContext) -> Result<SegmentedChangelogVersion> {
        self.sc_version_store
            .get(ctx)
            .await
            .with_context(|| {
                format!(
                    "repo {}: error loading segmented changelog version",
                    self.repo_id
                )
            })?
            .ok_or_else(|| {
                format_err!(
                    "repo {}: segmented changelog metadata not found, maybe repo is not seeded",
                    self.repo_id
                )
            })
    }

    /// Checks if given changeset is indexed by given segmented changelog version.
    pub async fn check_if_changeset_indexed(
        &self,
        ctx: &CoreContext,
        sc_version: &SegmentedChangelogVersion,
        cs_id: ChangesetId,
    ) -> Result<bool> {
        let iddag = self
            .iddag_save_store
            .load(ctx, sc_version.iddag_version)
            .await
            .with_context(|| format!("repo {}: failed to load iddag", self.repo_id))?;
        let idmap = self
            .idmap_factory
            .for_server(ctx, sc_version.idmap_version, &iddag)?;
        let result = idmap.find_dag_id(ctx, cs_id).await?;
        Ok(result.is_some())
    }
}

segmented_changelog_delegate!(SegmentedChangelogManager, |&self, ctx: &CoreContext| {
    // using load_owned for backwards compatibility until we deprecate upload algorithm
    // we would then remove this implementation for SegmentedChangelog
    let (sc, _sc_version) = self.load_owned(ctx).await.with_context(|| {
        format!(
            "repo {}: error loading segmented changelog from save",
            self.repo_id
        )
    })?;
    sc
});