/*
 * 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.
 */

#![type_length_limit = "2097152"]

use std::fs::File;
use std::io::Write;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
use std::sync::Arc;

use anyhow::Context;
use anyhow::Error;
use async_trait::async_trait;
use clap::Parser;
use cloned::cloned;
use cmdlib_logging::ScribeLoggingArgs;
use connection_security_checker::ConnectionSecurityChecker;
use environment::WarmBookmarksCacheDerivedData;
use executor_lib::args::ShardedExecutorArgs;
use executor_lib::RepoShardedProcess;
use executor_lib::RepoShardedProcessExecutor;
use fb303_core::server::make_BaseService_server;
use fbinit::FacebookInit;
use megarepo_api::MegarepoApi;
use metaconfig_types::ShardedService;
use mononoke_api::repo::Repo;
use mononoke_api::CoreContext;
use mononoke_app::args::HooksAppExtension;
use mononoke_app::args::RepoFilterAppExtension;
use mononoke_app::args::ShutdownTimeoutArgs;
use mononoke_app::args::WarmBookmarksCacheExtension;
use mononoke_app::MononokeAppBuilder;
use mononoke_app::MononokeReposManager;
use panichandler::Fate;
use permission_checker::DefaultAclProvider;
use sharding_ext::RepoShard;
use slog::info;
use source_control::server::make_SourceControlService_server;
use srserver::service_framework::BuildModule;
use srserver::service_framework::ContextPropModule;
use srserver::service_framework::Fb303Module;
use srserver::service_framework::ProfileModule;
use srserver::service_framework::ServiceFramework;
use srserver::service_framework::ThriftStatsModule;
use srserver::ThriftServer;
use srserver::ThriftServerBuilder;
use tokio::task;

mod commit_id;
mod errors;
mod facebook;
mod from_request;
mod history;
mod into_response;
mod metadata;
mod methods;
mod monitoring;
mod scuba_common;
mod scuba_params;
mod scuba_response;
mod source_control_impl;
mod specifiers;

const SERVICE_NAME: &str = "mononoke_scs_server";
const SM_CLEANUP_TIMEOUT_SECS: u64 = 60;

/// Mononoke Source Control Service Server
#[derive(Parser)]
struct ScsServerArgs {
    #[clap(flatten)]
    shutdown_timeout_args: ShutdownTimeoutArgs,
    #[clap(flatten)]
    scribe_logging_args: ScribeLoggingArgs,
    /// Thrift host
    #[clap(long, short = 'H', default_value = "::")]
    host: String,
    /// Thrift port
    #[clap(long, short = 'p', default_value_t = 8367)]
    port: u16,
    /// Path for file in which to write the bound tcp address in rust std::net::SocketAddr format
    #[clap(long)]
    bound_address_file: Option<String>,
    #[clap(flatten)]
    sharded_executor_args: ShardedExecutorArgs,
}

/// Struct representing the Source Control Service process when sharding by
/// repo.
pub struct ScsServerProcess {
    repos_mgr: Arc<MononokeReposManager<Repo>>,
}

impl ScsServerProcess {
    fn new(repos_mgr: MononokeReposManager<Repo>) -> Self {
        let repos_mgr = Arc::new(repos_mgr);
        Self { repos_mgr }
    }
}

#[async_trait]
impl RepoShardedProcess for ScsServerProcess {
    async fn setup(&self, repo: &RepoShard) -> anyhow::Result<Arc<dyn RepoShardedProcessExecutor>> {
        let repo_name = repo.repo_name.as_str();
        let logger = self.repos_mgr.repo_logger(repo_name);
        info!(&logger, "Setting up repo {} in SCS service", repo_name);
        // Check if the input repo is already initialized. This can happen if the repo is a
        // shallow-sharded repo, in which case it would already be initialized during service startup.
        if self.repos_mgr.repos().get_by_name(repo_name).is_none() {
            // The input repo is a deep-sharded repo, so it needs to be added now.
            self.repos_mgr.add_repo(repo_name).await.with_context(|| {
                format!("Failure in setting up repo {} in SCS service", repo_name)
            })?;
            info!(&logger, "Completed repo {} setup in SCS service", repo_name);
        } else {
            info!(
                &logger,
                "Repo {} is already setup in SCS service", repo_name
            );
        }
        Ok(Arc::new(ScsServerProcessExecutor {
            repo_name: repo_name.to_string(),
            repos_mgr: self.repos_mgr.clone(),
        }))
    }
}

/// Struct representing the execution of the source control service for a
/// particular repo when sharding by repo.
pub struct ScsServerProcessExecutor {
    repo_name: String,
    repos_mgr: Arc<MononokeReposManager<Repo>>,
}

#[async_trait]
impl RepoShardedProcessExecutor for ScsServerProcessExecutor {
    async fn execute(&self) -> anyhow::Result<()> {
        info!(
            self.repos_mgr.logger(),
            "Serving repo {} in SCS service", &self.repo_name,
        );
        Ok(())
    }

    async fn stop(&self) -> anyhow::Result<()> {
        let config = self
            .repos_mgr
            .repo_config(&self.repo_name)
            .with_context(|| {
                format!(
                    "Failure in stopping repo {}. The config for repo doesn't exist",
                    &self.repo_name
                )
            })?;
        // Check if the current repo is a deep-sharded or shallow-sharded repo. If the
        // repo is deep-sharded, then remove it since SM wants some other host to serve it.
        // If repo is shallow-sharded, then keep it since regardless of SM sharding, shallow
        // sharded repos need to be present on each host.
        let is_deep_sharded = config
            .deep_sharding_config
            .and_then(|c| c.status.get(&ShardedService::SourceControlService).copied())
            .unwrap_or(false);
        if is_deep_sharded {
            self.repos_mgr.remove_repo(&self.repo_name);
            info!(
                self.repos_mgr.logger(),
                "No longer serving repo {} in SCS service.", &self.repo_name,
            );
        } else {
            info!(
                self.repos_mgr.logger(),
                "Continuing serving repo {} in SCS service because it's shallow-sharded.",
                &self.repo_name,
            );
        }
        Ok(())
    }
}

#[fbinit::main]
fn main(fb: FacebookInit) -> Result<(), Error> {
    panichandler::set_panichandler(Fate::Abort);

    let app = MononokeAppBuilder::new(fb)
        .with_warm_bookmarks_cache(WarmBookmarksCacheDerivedData::AllKinds)
        .with_app_extension(WarmBookmarksCacheExtension {})
        .with_app_extension(HooksAppExtension {})
        .with_app_extension(RepoFilterAppExtension {})
        .build::<ScsServerArgs>()?;

    let args: ScsServerArgs = app.args()?;

    let logger = app.logger().clone();
    let runtime = app.runtime();

    let exec = runtime.clone();
    let env = app.environment();

    let scuba_builder = env.scuba_sample_builder.clone();
    // Service name is used for shallow or deep sharding. If sharding itself is disabled, provide
    // service name as None while opening repos.
    let service_name = args
        .sharded_executor_args
        .sharded_service_name
        .as_ref()
        .map(|_| ShardedService::SourceControlService);
    let repos_mgr = runtime.block_on(app.open_managed_repos(service_name))?;
    let mononoke = Arc::new(repos_mgr.make_mononoke_api()?);
    let megarepo_api = Arc::new(runtime.block_on(MegarepoApi::new(&app, mononoke.clone()))?);

    let will_exit = Arc::new(AtomicBool::new(false));

    // Initialize the FB303 Thrift stack.

    let fb303_base = {
        cloned!(will_exit);
        move |proto| {
            make_BaseService_server(proto, facebook::BaseServiceImpl::new(will_exit.clone()))
        }
    };
    let acl_provider = DefaultAclProvider::new(fb);
    let security_checker = runtime.block_on(ConnectionSecurityChecker::new(
        acl_provider.as_ref(),
        &app.repo_configs().common,
    ))?;
    let source_control_server = source_control_impl::SourceControlServiceImpl::new(
        fb,
        mononoke.clone(),
        megarepo_api,
        logger.clone(),
        scuba_builder,
        args.scribe_logging_args.get_scribe(fb)?,
        security_checker,
        &app.repo_configs().common,
    );
    let service = {
        move |proto| {
            make_SourceControlService_server(
                proto,
                source_control_server.thrift_server(),
                fb303_base.clone(),
            )
        }
    };
    let monitoring_forever = {
        let monitoring_ctx = CoreContext::new_with_logger(fb, logger.clone());
        monitoring::monitoring_stats_submitter(monitoring_ctx, mononoke)
    };
    runtime.spawn(monitoring_forever);

    let thrift: ThriftServer = ThriftServerBuilder::new(fb)
        .with_name(SERVICE_NAME)
        .expect("failed to set name")
        .with_address(&args.host, args.port, false)?
        .with_tls()
        .expect("failed to enable TLS")
        .with_cancel_if_client_disconnected()
        .with_metadata(metadata::create_metadata())
        .with_factory(exec, move || service)
        .build();

    let mut service_framework = ServiceFramework::from_server(SERVICE_NAME, thrift)
        .context("Failed to create service framework server")?;

    service_framework.add_module(BuildModule)?;
    service_framework.add_module(ThriftStatsModule)?;
    service_framework.add_module(Fb303Module)?;
    service_framework.add_module(ProfileModule)?;
    service_framework.add_module(ContextPropModule)?;

    service_framework
        .serve_background()
        .expect("failed to start thrift service");

    let bound_addr = format!(
        "{}:{}",
        &args.host,
        service_framework.get_address()?.get_port()?
    );
    info!(logger, "Listening on {}", bound_addr);

    // Write out the bound address if requested, this is helpful in tests when using automatic binding with :0
    if let Some(bound_addr_path) = args.bound_address_file {
        let mut writer = File::create(bound_addr_path)?;
        writer.write_all(bound_addr.as_bytes())?;
        writer.write_all(b"\n")?;
    }

    if let Some(mut executor) = args.sharded_executor_args.build_executor(
        fb,
        runtime.clone(),
        app.logger(),
        || Arc::new(ScsServerProcess::new(repos_mgr)),
        false, // disable shard (repo) level healing
        SM_CLEANUP_TIMEOUT_SECS,
    )? {
        // The Sharded Process Executor needs to branch off and execute
        // on its own dedicated task spawned off the common tokio runtime.
        runtime.spawn({
            let logger = logger.clone();
            {
                cloned!(will_exit);
                async move { executor.block_and_execute(&logger, will_exit).await }
            }
        });
    }

    // Monitoring is provided by the `Fb303Module`, but we must still start
    // stats aggregation.
    app.start_stats_aggregation()?;

    app.wait_until_terminated(
        move || will_exit.store(true, Ordering::Relaxed),
        args.shutdown_timeout_args.shutdown_grace_period,
        async {
            // Note that async blocks are lazy, so this isn't called until first poll
            let _ = task::spawn_blocking(move || {
                // Calling `stop` blocks until the service has completed all requests.
                service_framework.stop();
            })
            .await;
        },
        args.shutdown_timeout_args.shutdown_timeout,
    )?;

    info!(logger, "Exiting...");
    Ok(())
}


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

pub use self::ffi::create_metadata;

#[cxx::bridge]
mod ffi {
    unsafe extern "C++" {
        include!("eden/mononoke/scs_server/metadata.h");

        #[namespace = "facebook::rust::srserver"]
        type RustThriftMetadata = srserver::RustThriftMetadata;

        #[namespace = "facebook::scm::service"]
        fn create_metadata() -> UniquePtr<RustThriftMetadata>;
    }
}


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

use std::sync::Arc;
use std::time::Duration;

use futures::StreamExt;
use mononoke_api::CoreContext;
use mononoke_api::Mononoke;
use slog::warn;

const SUBMIT_STATS_ONCE_PER_SECS: u64 = 10;

pub async fn monitoring_stats_submitter(ctx: CoreContext, mononoke: Arc<Mononoke>) {
    tokio_shim::time::interval_stream(Duration::from_secs(SUBMIT_STATS_ONCE_PER_SECS))
        .for_each(|_| async {
            if let Err(e) = mononoke.report_monitoring_stats(&ctx).await {
                warn!(ctx.logger(), "Failed to report monitoring stats: {:#?}", e);
            }
        })
        .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 scuba_ext::MononokeScubaSampleBuilder;
use source_control::MegarepoTarget as ThriftMegarepoTarget;

pub(crate) fn hex(v: &[u8]) -> String {
    faster_hex::hex_string(v)
}

pub(crate) enum Reported {
    Param,
    Response,
}

pub(crate) fn report_megarepo_target(
    target: &ThriftMegarepoTarget,
    scuba: &mut MononokeScubaSampleBuilder,
    reported: Reported,
) {
    match reported {
        Reported::Param => {
            scuba.add("param_megarepo_target_bookmark", target.bookmark.clone());
            scuba.add("param_megarepo_target_repo_id", target.repo_id);
        }
        Reported::Response => {
            scuba.add("response_megarepo_target_bookmark", target.bookmark.clone());
            scuba.add("response_megarepo_target_repo_id", target.repo_id);
        }
    }
}


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

use std::collections::BTreeSet;

use scuba_ext::MononokeScubaSampleBuilder;
use scuba_ext::ScubaValue;
use source_control as thrift;

use crate::commit_id::CommitIdExt;
use crate::scuba_common::hex;
use crate::scuba_common::report_megarepo_target;
use crate::scuba_common::Reported;

/// A trait for logging a thrift `Params` struct to scuba.
///
/// Common and meaningful parameters (e.g. `other_commit` or
/// `identity_schemes` are logged using that name.  Other parameters
/// should have their name prefixed by `param_` to make it clear that
/// the column is for a parameter.  The default implementation does
/// nothing.
pub(crate) trait AddScubaParams: Send + Sync {
    fn add_scuba_params(&self, _scuba: &mut MononokeScubaSampleBuilder) {}
}

impl AddScubaParams for BTreeSet<thrift::CommitIdentityScheme> {
    fn add_scuba_params(&self, scuba: &mut MononokeScubaSampleBuilder) {
        scuba.add(
            "identity_schemes",
            self.iter().map(ToString::to_string).collect::<ScubaValue>(),
        );
    }
}

impl AddScubaParams for thrift::ListReposParams {}

impl AddScubaParams for thrift::RepoCreateCommitParams {
    fn add_scuba_params(&self, scuba: &mut MononokeScubaSampleBuilder) {
        scuba.add(
            "param_parents",
            self.parents
                .iter()
                .map(CommitIdExt::to_string)
                .collect::<ScubaValue>(),
        );
        if let Some(date) = self.info.date.as_ref() {
            scuba.add("param_date", date.timestamp);
        }
        scuba.add("param_author", self.info.author.as_str());
        let deletes_count = self
            .changes
            .values()
            .filter(|change| matches!(change, thrift::RepoCreateCommitParamsChange::deleted(_)))
            .count();
        scuba.add("param_changes_count", self.changes.len() - deletes_count);
        scuba.add("param_deletes_count", deletes_count);
        self.identity_schemes.add_scuba_params(scuba);
        if let Some(service_identity) = self.service_identity.as_deref() {
            scuba.add("service_identity", service_identity);
        }
    }
}

impl AddScubaParams for thrift::RepoCreateStackParams {
    fn add_scuba_params(&self, scuba: &mut MononokeScubaSampleBuilder) {
        scuba.add(
            "param_parents",
            self.parents
                .iter()
                .map(CommitIdExt::to_string)
                .collect::<ScubaValue>(),
        );
        let deletes_count = self
            .commits
            .iter()
            .map(|commit| {
                commit
                    .changes
                    .values()
                    .filter(|change| {
                        matches!(change, thrift::RepoCreateCommitParamsChange::deleted(_))
                    })
                    .count()
            })
            .sum::<usize>();
        let changes_count = self
            .commits
            .iter()
            .map(|commit| commit.changes.len())
            .sum::<usize>();
        scuba.add("param_changes_count", changes_count - deletes_count);
        scuba.add("param_deletes_count", deletes_count);
        scuba.add("param_commit_count", self.commits.len());
        self.identity_schemes.add_scuba_params(scuba);
        if let Some(service_identity) = self.service_identity.as_deref() {
            scuba.add("service_identity", service_identity);
        }
    }
}

impl AddScubaParams for thrift::RepoCreateBookmarkParams {
    fn add_scuba_params(&self, scuba: &mut MononokeScubaSampleBuilder) {
        scuba.add("bookmark_name", self.bookmark.as_str());
        scuba.add("commit", self.target.to_string());
        if let Some(service_identity) = self.service_identity.as_deref() {
            scuba.add("service_identity", service_identity);
        }
    }
}

impl AddScubaParams for thrift::RepoMoveBookmarkParams {
    fn add_scuba_params(&self, scuba: &mut MononokeScubaSampleBuilder) {
        scuba.add("bookmark_name", self.bookmark.as_str());
        scuba.add("commit", self.target.to_string());
        if let Some(old_target) = &self.old_target {
            scuba.add("param_old_target", old_target.to_string());
        }
        scuba.add(
            "param_allow_non_fast_forward_move",
            self.allow_non_fast_forward_move as i32,
        );
        if let Some(service_identity) = self.service_identity.as_deref() {
            scuba.add("service_identity", service_identity);
        }
    }
}

impl AddScubaParams for thrift::RepoDeleteBookmarkParams {
    fn add_scuba_params(&self, scuba: &mut MononokeScubaSampleBuilder) {
        scuba.add("bookmark_name", self.bookmark.as_str());
        if let Some(old_target) = &self.old_target {
            scuba.add("param_old_target", old_target.to_string());
        }
        if let Some(service_identity) = self.service_identity.as_deref() {
            scuba.add("service_identity", service_identity);
        }
    }
}

impl AddScubaParams for thrift::RepoLandStackParams {
    fn add_scuba_params(&self, scuba: &mut MononokeScubaSampleBuilder) {
        scuba.add("bookmark_name", self.bookmark.as_str());
        scuba.add("commit", self.head.to_string());
        scuba.add("param_base", self.base.to_string());
        self.identity_schemes.add_scuba_params(scuba);
        if let Some(old_identity_schemes) = &self.old_identity_schemes {
            scuba.add(
                "old_identity_schemes",
                old_identity_schemes
                    .iter()
                    .map(ToString::to_string)
                    .collect::<ScubaValue>(),
            );
        }
        if let Some(service_identity) = self.service_identity.as_deref() {
            scuba.add("service_identity", service_identity);
        }
    }
}

impl AddScubaParams for thrift::RepoListBookmarksParams {
    fn add_scuba_params(&self, scuba: &mut MononokeScubaSampleBuilder) {
        scuba.add("param_include_scratch", self.include_scratch as i32);
        scuba.add("param_bookmark_prefix", self.bookmark_prefix.as_str());
        scuba.add("param_limit", self.limit);
        if let Some(after) = &self.after {
            scuba.add("param_after", after.as_str());
        }
        self.identity_schemes.add_scuba_params(scuba);
    }
}

impl AddScubaParams for thrift::RepoResolveBookmarkParams {
    fn add_scuba_params(&self, scuba: &mut MononokeScubaSampleBuilder) {
        scuba.add("bookmark_name", self.bookmark_name.as_str());
        self.identity_schemes.add_scuba_params(scuba);
    }
}

impl AddScubaParams for thrift::RepoBookmarkInfoParams {
    fn add_scuba_params(&self, scuba: &mut MononokeScubaSampleBuilder) {
        scuba.add("bookmark_name", self.bookmark_name.as_str());
        self.identity_schemes.add_scuba_params(scuba);
    }
}

impl AddScubaParams for thrift::RepoResolveCommitPrefixParams {
    fn add_scuba_params(&self, scuba: &mut MononokeScubaSampleBuilder) {
        scuba.add("param_prefix", self.prefix.as_str());
        scuba.add("param_prefix_scheme", self.prefix_scheme.to_string());
        self.identity_schemes.add_scuba_params(scuba);
    }
}

impl AddScubaParams for thrift::RepoStackInfoParams {}

impl AddScubaParams for thrift::RepoPrepareCommitsParams {}

impl AddScubaParams for thrift::RepoUploadFileContentParams {
    fn add_scuba_params(&self, scuba: &mut MononokeScubaSampleBuilder) {
        scuba.add("param_data_len", self.data.len());
    }
}

impl AddScubaParams for thrift::CommitCompareParams {
    fn add_scuba_params(&self, scuba: &mut MononokeScubaSampleBuilder) {
        if let Some(other_commit_id) = self.other_commit_id.as_ref() {
            scuba.add("other_commit", other_commit_id.to_string());
        }
        if let Some(paths) = &self.paths {
            scuba.add("param_paths", paths.iter().collect::<ScubaValue>());
        }
        if let Some(ordered_params) = &self.ordered_params {
            if let Some(after_path) = &ordered_params.after_path {
                scuba.add("param_after", after_path.as_str());
            }
            scuba.add("param_limit", ordered_params.limit);
        }
        scuba.add("param_skip_copies_renames", self.skip_copies_renames as i32);
        self.identity_schemes.add_scuba_params(scuba);
    }
}

impl AddScubaParams for thrift::CommitFileDiffsParams {
    fn add_scuba_params(&self, scuba: &mut MononokeScubaSampleBuilder) {
        scuba.add(
            "other_commit",
            self.other_commit_id.as_ref().map(|oci| oci.to_string()),
        );
        scuba.add("param_format", self.format.to_string());
        scuba.add("param_context", self.context);
    }
}

impl AddScubaParams for thrift::CommitFindFilesParams {
    fn add_scuba_params(&self, scuba: &mut MononokeScubaSampleBuilder) {
        scuba.add("param_limit", self.limit);
        if let Some(basenames) = &self.basenames {
            scuba.add("param_basenames", basenames.iter().collect::<ScubaValue>());
        }
        if let Some(suffixes) = &self.basename_suffixes {
            scuba.add("param_suffixes", suffixes.iter().collect::<ScubaValue>());
        }
        if let Some(prefixes) = &self.prefixes {
            scuba.add("param_prefixes", prefixes.iter().collect::<ScubaValue>());
        }
        if let Some(after) = &self.after {
            scuba.add("param_after", after.as_str());
        }
    }
}

impl AddScubaParams for thrift::CommitInfoParams {
    fn add_scuba_params(&self, scuba: &mut MononokeScubaSampleBuilder) {
        self.identity_schemes.add_scuba_params(scuba);
    }
}

impl AddScubaParams for thrift::CommitIsAncestorOfParams {
    fn add_scuba_params(&self, scuba: &mut MononokeScubaSampleBuilder) {
        scuba.add("other_commit", self.descendant_commit_id.to_string());
    }
}

impl AddScubaParams for thrift::CommitCommonBaseWithParams {
    fn add_scuba_params(&self, scuba: &mut MononokeScubaSampleBuilder) {
        scuba.add("other_commit", self.other_commit_id.to_string());
    }
}

impl AddScubaParams for thrift::CommitLookupParams {
    fn add_scuba_params(&self, scuba: &mut MononokeScubaSampleBuilder) {
        self.identity_schemes.add_scuba_params(scuba);
    }
}

impl AddScubaParams for thrift::CommitLookupPushrebaseHistoryParams {}

impl AddScubaParams for thrift::CommitHistoryParams {
    fn add_scuba_params(&self, scuba: &mut MononokeScubaSampleBuilder) {
        scuba.add("param_format", self.format.to_string());
        scuba.add("param_skip", self.skip);
        scuba.add("param_limit", self.limit);
        if let Some(before) = self.before_timestamp {
            scuba.add("param_before_timestamp", before);
        }
        if let Some(after) = self.after_timestamp {
            scuba.add("param_after_timestamp", after);
        }
        if let Some(descendants_of) = &self.descendants_of {
            scuba.add("param_descendants_of", descendants_of.to_string());
        }
        if let Some(exclude_changeset_and_ancestors) = &self.exclude_changeset_and_ancestors {
            scuba.add(
                "param_exclude_changeset_and_ancestors",
                exclude_changeset_and_ancestors.to_string(),
            );
        }
        self.identity_schemes.add_scuba_params(scuba);
    }
}

impl AddScubaParams for thrift::CommitListDescendantBookmarksParams {
    fn add_scuba_params(&self, scuba: &mut MononokeScubaSampleBuilder) {
        scuba.add("param_include_scratch", self.include_scratch as i32);
        scuba.add("param_bookmark_prefix", self.bookmark_prefix.as_str());
        scuba.add("param_limit", self.limit);
        if let Some(after) = &self.after {
            scuba.add("param_after", after.as_str());
        }
        self.identity_schemes.add_scuba_params(scuba);
    }
}

impl AddScubaParams for thrift::CommitRunHooksParams {
    fn add_scuba_params(&self, scuba: &mut MononokeScubaSampleBuilder) {
        scuba.add("bookmark_name", self.bookmark.as_str());
    }
}

impl AddScubaParams for thrift::CommitLookupXRepoParams {
    fn add_scuba_params(&self, scuba: &mut MononokeScubaSampleBuilder) {
        scuba.add("other_repo", self.other_repo.name.as_str());
        self.identity_schemes.add_scuba_params(scuba);
    }
}

impl AddScubaParams for thrift::CommitPathBlameParams {
    fn add_scuba_params(&self, scuba: &mut MononokeScubaSampleBuilder) {
        scuba.add("param_format", self.format.to_string());
        self.identity_schemes.add_scuba_params(scuba);
    }
}

impl AddScubaParams for thrift::CommitPathHistoryParams {
    fn add_scuba_params(&self, scuba: &mut MononokeScubaSampleBuilder) {
        scuba.add("param_format", self.format.to_string());
        scuba.add("param_skip", self.skip);
        scuba.add("param_limit", self.limit);
        if let Some(before) = self.before_timestamp {
            scuba.add("param_before_timestamp", before);
        }
        if let Some(after) = self.after_timestamp {
            scuba.add("param_after_timestamp", after);
        }
        scuba.add(
            "follow_history_across_deletions",
            self.follow_history_across_deletions,
        );
        if let Some(descendants_of) = &self.descendants_of {
            scuba.add("param_descendants_of", descendants_of.to_string());
        }
        if let Some(exclude_changeset_and_ancestors) = &self.exclude_changeset_and_ancestors {
            scuba.add(
                "param_exclude_changeset_and_ancestors",
                exclude_changeset_and_ancestors.to_string(),
            );
        }
        self.identity_schemes.add_scuba_params(scuba);
    }
}

impl AddScubaParams for thrift::CommitPathExistsParams {}

impl AddScubaParams for thrift::CommitPathInfoParams {}

impl AddScubaParams for thrift::RepoInfoParams {}

impl AddScubaParams for thrift::CommitMultiplePathInfoParams {
    fn add_scuba_params(&self, scuba: &mut MononokeScubaSampleBuilder) {
        scuba.add("param_paths", self.paths.iter().collect::<ScubaValue>());
    }
}

impl AddScubaParams for thrift::CommitPathLastChangedParams {
    fn add_scuba_params(&self, scuba: &mut MononokeScubaSampleBuilder) {
        self.identity_schemes.add_scuba_params(scuba);
    }
}

impl AddScubaParams for thrift::CommitMultiplePathLastChangedParams {
    fn add_scuba_params(&self, scuba: &mut MononokeScubaSampleBuilder) {
        scuba.add("param_paths", self.paths.iter().collect::<ScubaValue>());
        self.identity_schemes.add_scuba_params(scuba);
    }
}

impl AddScubaParams for thrift::CommitSparseProfileDeltaParams {
    fn add_scuba_params(&self, scuba: &mut MononokeScubaSampleBuilder) {
        scuba.add("other_commit", self.other_id.to_string());
        scuba.add(
            "param_paths",
            match &self.profiles {
                thrift::SparseProfiles::all_profiles(_) => {
                    ScubaValue::from("all sparse profiles".to_string())
                }
                thrift::SparseProfiles::profiles(profiles) => {
                    profiles.iter().collect::<ScubaValue>()
                }
                thrift::SparseProfiles::UnknownField(t) => {
                    ScubaValue::from(format!("unknown SparseProfiles type {}", t))
                }
            },
        );
    }
}

impl AddScubaParams for thrift::CommitSparseProfileSizeParams {
    fn add_scuba_params(&self, scuba: &mut MononokeScubaSampleBuilder) {
        scuba.add(
            "param_paths",
            match &self.profiles {
                thrift::SparseProfiles::all_profiles(_) => {
                    ScubaValue::from("all sparse profiles".to_string())
                }
                thrift::SparseProfiles::profiles(profiles) => {
                    profiles.iter().collect::<ScubaValue>()
                }
                thrift::SparseProfiles::UnknownField(t) => {
                    ScubaValue::from(format!("unknown SparseProfiles type {}", t))
                }
            },
        );
    }
}

impl AddScubaParams for thrift::FileContentChunkParams {
    fn add_scuba_params(&self, scuba: &mut MononokeScubaSampleBuilder) {
        scuba.add("param_offset", self.offset);
        scuba.add("param_size", self.size);
    }
}

impl AddScubaParams for thrift::FileExistsParams {}

impl AddScubaParams for thrift::FileInfoParams {}

impl AddScubaParams for thrift::FileDiffParams {
    fn add_scuba_params(&self, scuba: &mut MononokeScubaSampleBuilder) {
        scuba.add("other_file", hex(&self.other_file_id));
        scuba.add("param_format", self.format.to_string());
        scuba.add("param_context", self.context);
    }
}

impl AddScubaParams for thrift::TreeExistsParams {}

impl AddScubaParams for thrift::TreeListParams {
    fn add_scuba_params(&self, scuba: &mut MononokeScubaSampleBuilder) {
        scuba.add("param_offset", self.offset);
        scuba.add("param_limit", self.limit);
    }
}

impl AddScubaParams for thrift::MegarepoAddTargetToken {
    fn add_scuba_params(&self, scuba: &mut MononokeScubaSampleBuilder) {
        scuba.add("param_megarepo_token", self.id);
        report_megarepo_target(&self.target, scuba, Reported::Param);
    }
}

impl AddScubaParams for thrift::MegarepoAddBranchingTargetToken {
    fn add_scuba_params(&self, scuba: &mut MononokeScubaSampleBuilder) {
        scuba.add("param_megarepo_token", self.id);
        report_megarepo_target(&self.target, scuba, Reported::Param);
    }
}

impl AddScubaParams for thrift::MegarepoChangeConfigToken {
    fn add_scuba_params(&self, scuba: &mut MononokeScubaSampleBuilder) {
        scuba.add("param_megarepo_token", self.id);
        report_megarepo_target(&self.target, scuba, Reported::Param);
    }
}

impl AddScubaParams for thrift::MegarepoRemergeSourceToken {
    fn add_scuba_params(&self, scuba: &mut MononokeScubaSampleBuilder) {
        scuba.add("param_megarepo_token", self.id);
        report_megarepo_target(&self.target, scuba, Reported::Param);
    }
}

impl AddScubaParams for thrift::MegarepoSyncChangesetToken {
    fn add_scuba_params(&self, scuba: &mut MononokeScubaSampleBuilder) {
        scuba.add("param_megarepo_token", self.id);
        report_megarepo_target(&self.target, scuba, Reported::Param);
    }
}

impl AddScubaParams for thrift::MegarepoSyncChangesetParams {
    fn add_scuba_params(&self, scuba: &mut MononokeScubaSampleBuilder) {
        scuba.add("param_megarepo_source_name", self.source_name.clone());
        scuba.add("param_megarepo_cs_id", hex(&self.cs_id));
        report_megarepo_target(&self.target, scuba, Reported::Param);
    }
}

impl AddScubaParams for thrift::MegarepoRemergeSourceParams {
    fn add_scuba_params(&self, scuba: &mut MononokeScubaSampleBuilder) {
        scuba.add("param_megarepo_source_name", self.source_name.clone());
        scuba.add("param_megarepo_cs_id", hex(&self.cs_id));
        scuba.add("param_megarepo_target_location", hex(&self.target_location));
        scuba.add("param_megarepo_message", self.message.clone());
        report_megarepo_target(&self.target, scuba, Reported::Param);
    }
}

impl AddScubaParams for thrift::MegarepoChangeTargetConfigParams {
    fn add_scuba_params(&self, scuba: &mut MononokeScubaSampleBuilder) {
        scuba.add("param_megarepo_version", self.new_version.clone());
        scuba.add("param_megarepo_message", self.message.clone());
        scuba.add("param_megarepo_target_location", hex(&self.target_location));
        report_megarepo_target(&self.target, scuba, Reported::Param);
    }
}

impl AddScubaParams for thrift::MegarepoAddTargetParams {
    fn add_scuba_params(&self, scuba: &mut MononokeScubaSampleBuilder) {
        scuba.add(
            "param_megarepo_version",
            self.config_with_new_target.version.clone(),
        );
        scuba.add("param_megarepo_message", self.message.clone());
        report_megarepo_target(&self.config_with_new_target.target, scuba, Reported::Param);
    }
}

impl AddScubaParams for thrift::MegarepoAddBranchingTargetParams {
    fn add_scuba_params(&self, scuba: &mut MononokeScubaSampleBuilder) {
        scuba.add("param_megarepo_branching_point", hex(&self.branching_point));
        report_megarepo_target(&self.target, scuba, Reported::Param);
    }
}

impl AddScubaParams for thrift::MegarepoAddConfigParams {
    fn add_scuba_params(&self, scuba: &mut MononokeScubaSampleBuilder) {
        scuba.add("param_megarepo_version", self.new_config.version.clone());
        report_megarepo_target(&self.new_config.target, scuba, Reported::Param);
    }
}

impl AddScubaParams for thrift::MegarepoReadConfigParams {
    fn add_scuba_params(&self, scuba: &mut MononokeScubaSampleBuilder) {
        report_megarepo_target(&self.target, scuba, Reported::Param);
    }
}

impl AddScubaParams for thrift::UploadGitObjectParams {
    fn add_scuba_params(&self, scuba: &mut MononokeScubaSampleBuilder) {
        scuba.add("param_git_object_id", hex(&self.git_hash));
    }
}

impl AddScubaParams for thrift::CreateGitTreeParams {
    fn add_scuba_params(&self, scuba: &mut MononokeScubaSampleBuilder) {
        scuba.add("param_git_object_id", hex(&self.git_tree_hash));
    }
}

impl AddScubaParams for thrift::CreateGitTagParams {
    fn add_scuba_params(&self, scuba: &mut MononokeScubaSampleBuilder) {
        scuba.add("tagged_changeset_id", hex(&self.target_changeset));
    }
}


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

use crate::commit_id::CommitIdExt;
use crate::scuba_common::hex;
use crate::scuba_common::report_megarepo_target;
use crate::scuba_common::Reported;

/// A trait for logging a thrift `Response` struct to scuba.
pub(crate) trait AddScubaResponse: Send + Sync {
    fn add_scuba_response(&self, _scuba: &mut MononokeScubaSampleBuilder) {}
}

impl AddScubaResponse for bool {}

impl AddScubaResponse for Vec<thrift::Repo> {}

impl AddScubaResponse for thrift::RepoInfo {}

impl AddScubaResponse for thrift::RepoCreateCommitResponse {
    fn add_scuba_response(&self, scuba: &mut MononokeScubaSampleBuilder) {
        if let Some(id) = self.ids.get(&thrift::CommitIdentityScheme::BONSAI) {
            scuba.add("commit", id.to_string());
        }
    }
}

impl AddScubaResponse for thrift::RepoCreateStackResponse {
    fn add_scuba_response(&self, scuba: &mut MononokeScubaSampleBuilder) {
        if let Some(id) = self
            .commit_ids
            .last()
            .and_then(|id| id.get(&thrift::CommitIdentityScheme::BONSAI))
        {
            scuba.add("commit", id.to_string());
        }
        scuba.add("response_commit_count", self.commit_ids.len());
    }
}

impl AddScubaResponse for thrift::RepoCreateBookmarkResponse {}

impl AddScubaResponse for thrift::RepoMoveBookmarkResponse {}

impl AddScubaResponse for thrift::RepoDeleteBookmarkResponse {}

impl AddScubaResponse for thrift::RepoLandStackResponse {}

impl AddScubaResponse for thrift::RepoListBookmarksResponse {}

impl AddScubaResponse for thrift::RepoResolveBookmarkResponse {}

impl AddScubaResponse for thrift::RepoResolveCommitPrefixResponse {}

impl AddScubaResponse for thrift::RepoBookmarkInfoResponse {}

impl AddScubaResponse for thrift::RepoStackInfoResponse {}

impl AddScubaResponse for thrift::RepoPrepareCommitsResponse {}

impl AddScubaResponse for thrift::RepoUploadFileContentResponse {
    fn add_scuba_response(&self, scuba: &mut MononokeScubaSampleBuilder) {
        scuba.add("response_id", hex(&self.id));
    }
}

impl AddScubaResponse for thrift::CommitCompareResponse {}

impl AddScubaResponse for thrift::CommitFileDiffsResponse {
    fn add_scuba_response(&self, scuba: &mut MononokeScubaSampleBuilder) {
        let non_text_files = self
            .path_diffs
            .iter()
            .filter_map(|response| match &response.diff {
                thrift::Diff::metadata_diff(metadata) => match (
                    metadata.old_file_info.file_content_type,
                    metadata.new_file_info.file_content_type,
                ) {
                    (Some(old_file), Some(new_file)) => Some(vec![old_file, new_file]),
                    (Some(old_file), _) => Some(vec![old_file]),
                    (_, Some(new_file)) => Some(vec![new_file]),
                    _ => None,
                },
                _ => None,
            })
            .flatten()
            .filter(|content_type| content_type.0 > 1) // Non-text or binary
            .count();
        // Only log if there are any actual non-textual files
        if non_text_files > 0 {
            scuba.add("non_text_files", non_text_files);
        }
    }
}

impl AddScubaResponse for thrift::CommitFindFilesResponse {}

impl AddScubaResponse for thrift::CommitInfo {}

impl AddScubaResponse for thrift::CommitLookupResponse {}

impl AddScubaResponse for thrift::CommitLookupPushrebaseHistoryResponse {}

impl AddScubaResponse for thrift::CommitHistoryResponse {}

impl AddScubaResponse for thrift::CommitListDescendantBookmarksResponse {}

impl AddScubaResponse for thrift::CommitRunHooksResponse {}

impl AddScubaResponse for thrift::CommitPathBlameResponse {}

impl AddScubaResponse for thrift::CommitPathHistoryResponse {}

impl AddScubaResponse for thrift::CommitPathExistsResponse {}

impl AddScubaResponse for thrift::CommitPathInfoResponse {}

impl AddScubaResponse for thrift::CommitMultiplePathInfoResponse {}

impl AddScubaResponse for thrift::CommitPathLastChangedResponse {}

impl AddScubaResponse for thrift::CommitMultiplePathLastChangedResponse {}

impl AddScubaResponse for thrift::CommitSparseProfileDeltaResponse {}

impl AddScubaResponse for thrift::CommitSparseProfileSizeResponse {}

impl AddScubaResponse for thrift::FileChunk {}

impl AddScubaResponse for thrift::FileInfo {}

impl AddScubaResponse for thrift::FileDiffResponse {}

impl AddScubaResponse for thrift::TreeListResponse {}

// TODO: report cs_ids and actual error where possible
impl AddScubaResponse for thrift::MegarepoRemergeSourceResult {}

impl AddScubaResponse for thrift::MegarepoSyncChangesetResult {}

impl AddScubaResponse for thrift::MegarepoChangeTargetConfigResult {}

impl AddScubaResponse for thrift::MegarepoAddTargetResult {}

impl AddScubaResponse for thrift::MegarepoAddBranchingTargetResult {}

impl AddScubaResponse for thrift::MegarepoAddConfigResponse {}

impl AddScubaResponse for thrift::MegarepoReadConfigResponse {}

// Helper fn to report PollResponse types
fn report_maybe_result<R: AddScubaResponse>(
    maybe_result: &Option<R>,
    scuba: &mut MononokeScubaSampleBuilder,
) {
    match maybe_result {
        None => {
            scuba.add("megarepo_ready", false);
        }
        Some(resp) => {
            scuba.add("megarepo_ready", true);
            <R as AddScubaResponse>::add_scuba_response(resp, scuba);
        }
    }
}

impl AddScubaResponse for thrift::MegarepoRemergeSourcePollResponse {
    fn add_scuba_response(&self, scuba: &mut MononokeScubaSampleBuilder) {
        report_maybe_result(&self.result, scuba);
    }
}

impl AddScubaResponse for thrift::MegarepoSyncChangesetPollResponse {
    fn add_scuba_response(&self, scuba: &mut MononokeScubaSampleBuilder) {
        report_maybe_result(&self.result, scuba);
    }
}

impl AddScubaResponse for thrift::MegarepoChangeTargetConfigPollResponse {
    fn add_scuba_response(&self, scuba: &mut MononokeScubaSampleBuilder) {
        report_maybe_result(&self.result, scuba);
    }
}

impl AddScubaResponse for thrift::MegarepoAddTargetPollResponse {
    fn add_scuba_response(&self, scuba: &mut MononokeScubaSampleBuilder) {
        report_maybe_result(&self.result, scuba);
    }
}

impl AddScubaResponse for thrift::MegarepoAddTargetToken {
    fn add_scuba_response(&self, scuba: &mut MononokeScubaSampleBuilder) {
        scuba.add("megarepo_token", self.id);
        report_megarepo_target(&self.target, scuba, Reported::Response);
    }
}

impl AddScubaResponse for thrift::MegarepoAddBranchingTargetPollResponse {
    fn add_scuba_response(&self, scuba: &mut MononokeScubaSampleBuilder) {
        report_maybe_result(&self.result, scuba);
    }
}

impl AddScubaResponse for thrift::MegarepoAddBranchingTargetToken {
    fn add_scuba_response(&self, scuba: &mut MononokeScubaSampleBuilder) {
        scuba.add("megarepo_token", self.id);
        report_megarepo_target(&self.target, scuba, Reported::Response);
    }
}

impl AddScubaResponse for thrift::MegarepoChangeConfigToken {
    fn add_scuba_response(&self, scuba: &mut MononokeScubaSampleBuilder) {
        scuba.add("megarepo_token", self.id);
        report_megarepo_target(&self.target, scuba, Reported::Response);
    }
}

impl AddScubaResponse for thrift::MegarepoRemergeSourceToken {
    fn add_scuba_response(&self, scuba: &mut MononokeScubaSampleBuilder) {
        scuba.add("megarepo_token", self.id);
        report_megarepo_target(&self.target, scuba, Reported::Response);
    }
}

impl AddScubaResponse for thrift::MegarepoSyncChangesetToken {
    fn add_scuba_response(&self, scuba: &mut MononokeScubaSampleBuilder) {
        scuba.add("megarepo_token", self.id);
        report_megarepo_target(&self.target, scuba, Reported::Response);
    }
}

impl AddScubaResponse for thrift::UploadGitObjectResponse {}
impl AddScubaResponse for thrift::CreateGitTreeResponse {}
impl AddScubaResponse for thrift::CreateGitTagResponse {}


/*
 * 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::future::Future;
use std::net::IpAddr;
use std::pin::Pin;
use std::sync::Arc;

use connection_security_checker::ConnectionSecurityChecker;
use ephemeral_blobstore::BubbleId;
use ephemeral_blobstore::RepoEphemeralStore;
use fbinit::FacebookInit;
use futures::future::BoxFuture;
use futures::try_join;
use futures::FutureExt;
use futures_ext::FbFutureExt;
use futures_stats::FutureStats;
use futures_stats::TimedFutureExt;
use identity::Identity;
use login_objects_thrift::EnvironmentType;
use maplit::hashset;
use megarepo_api::MegarepoApi;
use metaconfig_types::CommonConfig;
use metadata::Metadata;
use mononoke_api::ChangesetContext;
use mononoke_api::ChangesetId;
use mononoke_api::ChangesetSpecifier;
use mononoke_api::CoreContext;
use mononoke_api::FileContext;
use mononoke_api::FileId;
use mononoke_api::Mononoke;
use mononoke_api::RepoContext;
use mononoke_api::SessionContainer;
use mononoke_api::TreeContext;
use mononoke_api::TreeId;
use mononoke_types::hash::Sha1;
use mononoke_types::hash::Sha256;
use once_cell::sync::Lazy;
use permission_checker::MononokeIdentity;
use permission_checker::MononokeIdentitySet;
use repo_authorization::AuthorizationContext;
use scribe_ext::Scribe;
use scuba_ext::MononokeScubaSampleBuilder;
use scuba_ext::ScubaValue;
use slog::Logger;
use source_control as thrift;
use source_control::server::SourceControlService;
use source_control::services::source_control_service as service;
use srserver::RequestContext;
use stats::prelude::*;
use time_ext::DurationExt;
use tunables::tunables;

use crate::commit_id::CommitIdExt;
use crate::errors;
use crate::errors::ServiceErrorResultExt;
use crate::errors::Status;
use crate::from_request::FromRequest;
use crate::scuba_params::AddScubaParams;
use crate::scuba_response::AddScubaResponse;
use crate::specifiers::SpecifierExt;

const FORWARDED_IDENTITIES_HEADER: &str = "scm_forwarded_identities";
const FORWARDED_CLIENT_IP_HEADER: &str = "scm_forwarded_client_ip";
const FORWARDED_CLIENT_DEBUG_HEADER: &str = "scm_forwarded_client_debug";
const FORWARDED_OTHER_CATS_HEADER: &str = "scm_forwarded_other_cats";

define_stats! {
    prefix = "mononoke.scs_server";
    total_request_start: timeseries(Rate, Sum),
    total_request_success: timeseries(Rate, Sum),
    total_request_internal_failure: timeseries(Rate, Sum),
    total_request_invalid: timeseries(Rate, Sum),
    total_request_cancelled: timeseries(Rate, Sum),

    // permille is used in canaries, because canaries do not allow for tracking formulas
    total_request_internal_failure_permille: timeseries(Average),
    total_request_invalid_permille: timeseries(Average),

    // Duration per method
    method_completion_time_ms: dynamic_histogram("method.{}.completion_time_ms", (method: String); 10, 0, 1_000, Average, Sum, Count; P 5; P 50 ; P 90),
}

static POPULAR_METHODS: Lazy<HashSet<&'static str>> = Lazy::new(|| hashset! {});

#[derive(Clone)]
pub(crate) struct SourceControlServiceImpl {
    pub(crate) fb: FacebookInit,
    pub(crate) mononoke: Arc<Mononoke>,
    pub(crate) megarepo_api: Arc<MegarepoApi>,
    pub(crate) logger: Logger,
    pub(crate) scuba_builder: MononokeScubaSampleBuilder,
    pub(crate) identity: Identity,
    pub(crate) scribe: Scribe,
    identity_proxy_checker: Arc<ConnectionSecurityChecker>,
}

pub(crate) struct SourceControlServiceThriftImpl(SourceControlServiceImpl);

impl SourceControlServiceImpl {
    pub fn new(
        fb: FacebookInit,
        mononoke: Arc<Mononoke>,
        megarepo_api: Arc<MegarepoApi>,
        logger: Logger,
        mut scuba_builder: MononokeScubaSampleBuilder,
        scribe: Scribe,
        identity_proxy_checker: ConnectionSecurityChecker,
        common_config: &CommonConfig,
    ) -> Self {
        scuba_builder.add_common_server_data();

        Self {
            fb,
            mononoke,
            megarepo_api,
            logger,
            scuba_builder,
            identity: Identity::new(
                common_config.internal_identity.id_type.as_str(),
                common_config.internal_identity.id_data.as_str(),
            ),
            scribe,
            identity_proxy_checker: Arc::new(identity_proxy_checker),
        }
    }

    pub(crate) fn thrift_server(&self) -> SourceControlServiceThriftImpl {
        SourceControlServiceThriftImpl(self.clone())
    }

    pub(crate) async fn create_ctx(
        &self,
        name: &str,
        req_ctxt: &RequestContext,
        specifier: Option<&dyn SpecifierExt>,
        params: &dyn AddScubaParams,
    ) -> Result<CoreContext, errors::ServiceError> {
        let session = self.create_session(req_ctxt).await?;
        let identities = session.metadata().identities();
        let mut scuba = self.create_scuba(name, req_ctxt, specifier, params, identities)?;
        scuba.add("session_uuid", session.metadata().session_id().to_string());

        let ctx = session.new_context_with_scribe(self.logger.clone(), scuba, self.scribe.clone());
        Ok(ctx)
    }

    /// Create and configure a scuba sample builder for a request.
    fn create_scuba(
        &self,
        name: &str,
        req_ctxt: &RequestContext,
        specifier: Option<&dyn SpecifierExt>,
        params: &dyn AddScubaParams,
        identities: &MononokeIdentitySet,
    ) -> Result<MononokeScubaSampleBuilder, errors::ServiceError> {
        let mut scuba = self.scuba_builder.clone().with_seq("seq");
        scuba.add("type", "thrift");
        scuba.add("method", name);
        if let Some(specifier) = specifier {
            if let Some(reponame) = specifier.scuba_reponame() {
                scuba.add("reponame", reponame);
            }
            if let Some(commit) = specifier.scuba_commit() {
                scuba.add("commit", commit);
            }
            if let Some(path) = specifier.scuba_path() {
                scuba.add("path", path);
            }
        }

        let sampling_rate = core::num::NonZeroU64::new(if POPULAR_METHODS.contains(name) {
            tunables()
                .scs_popular_methods_sampling_rate()
                .unwrap_or_default() as u64
        } else {
            tunables()
                .scs_other_methods_sampling_rate()
                .unwrap_or_default() as u64
        });
        if let Some(sampling_rate) = sampling_rate {
            scuba.sampled(sampling_rate);
        } else {
            scuba.unsampled();
        }

        params.add_scuba_params(&mut scuba);

        const CLIENT_HEADERS: &[&str] = &[
            "client_id",
            "client_type",
            "client_correlator",
            "proxy_client_id",
        ];
        for &header in CLIENT_HEADERS.iter() {
            let value = req_ctxt.header(header).map_err(errors::internal_error)?;
            if let Some(value) = value {
                scuba.add(header, value);
            }
        }

        scuba.add(
            "identities",
            identities
                .iter()
                .map(|id| id.to_string())
                .collect::<ScubaValue>(),
        );

        Ok(scuba)
    }

    async fn create_metadata(
        &self,
        req_ctxt: &RequestContext,
    ) -> Result<Metadata, errors::ServiceError> {
        let header = |h: &str| req_ctxt.header(h).map_err(errors::invalid_request);

        let tls_identities: MononokeIdentitySet = req_ctxt
            .identities()
            .map_err(errors::internal_error)?
            .entries()
            .into_iter()
            .map(MononokeIdentity::from_identity_ref)
            .collect();

        // Get any valid CAT identieies.
        let cats_identities: MononokeIdentitySet = req_ctxt
            .identities_cats(
                &self.identity,
                &[EnvironmentType::PROD, EnvironmentType::CORP],
            )
            .map_err(errors::internal_error)?
            .entries()
            .into_iter()
            .map(MononokeIdentity::from_identity_ref)
            .collect();

        let is_trusted = self
            .identity_proxy_checker
            .check_if_trusted(&tls_identities)
            .await;

        if is_trusted {
            if let (Some(forwarded_identities), Some(forwarded_ip)) = (
                header(FORWARDED_IDENTITIES_HEADER)?,
                header(FORWARDED_CLIENT_IP_HEADER)?,
            ) {
                let mut header_identities: MononokeIdentitySet =
                    serde_json::from_str(forwarded_identities.as_str())
                        .map_err(errors::invalid_request)?;
                let client_ip = Some(
                    forwarded_ip
                        .parse::<IpAddr>()
                        .map_err(errors::invalid_request)?,
                );
                let client_debug = header(FORWARDED_CLIENT_DEBUG_HEADER)?.is_some();

                header_identities.extend(cats_identities.into_iter());
                let mut metadata = Metadata::new(
                    None,
                    header_identities,
                    client_debug,
                    metadata::security::is_client_untrusted(|h| req_ctxt.header(h))
                        .map_err(errors::invalid_request)?,
                    client_ip,
                )
                .await;

                metadata.add_original_identities(tls_identities);

                if let Some(other_cats) = header(FORWARDED_OTHER_CATS_HEADER)? {
                    metadata.add_raw_encoded_cats(other_cats);
                }

                return Ok(metadata);
            }
        }

        Ok(Metadata::new(
            None,
            tls_identities.union(&cats_identities).cloned().collect(),
            false,
            metadata::security::is_client_untrusted(|h| req_ctxt.header(h))
                .map_err(errors::invalid_request)?,
            None,
        )
        .await)
    }

    /// Create and configure the session container for a request.
    async fn create_session(
        &self,
        req_ctxt: &RequestContext,
    ) -> Result<SessionContainer, errors::ServiceError> {
        let metadata = self.create_metadata(req_ctxt).await?;
        let session = SessionContainer::builder(self.fb)
            .metadata(Arc::new(metadata))
            .blobstore_maybe_read_qps_limiter(tunables().scs_request_read_qps().unwrap_or_default())
            .await
            .blobstore_maybe_write_qps_limiter(
                tunables().scs_request_write_qps().unwrap_or_default(),
            )
            .await
            .build();
        Ok(session)
    }

    /// Get the repo specified by a `thrift::RepoSpecifier`.
    pub(crate) async fn repo(
        &self,
        ctx: CoreContext,
        repo: &thrift::RepoSpecifier,
    ) -> Result<RepoContext, errors::ServiceError> {
        let authz = AuthorizationContext::new(&ctx);
        self.repo_impl(ctx, repo, authz, |_| async { Ok(None) })
            .await
    }

    /// Get the repo specified by a `thrift::RepoSpecifier` for access by a
    /// named service.
    pub(crate) async fn repo_for_service(
        &self,
        ctx: CoreContext,
        repo: &thrift::RepoSpecifier,
        service_name: Option<String>,
    ) -> Result<RepoContext, errors::ServiceError> {
        let authz = match service_name {
            Some(service_name) => AuthorizationContext::new_for_service_writes(service_name),
            None => AuthorizationContext::new(&ctx),
        };
        self.repo_impl(ctx, repo, authz, |_| async { Ok(None) })
            .await
    }

    pub async fn repo_impl<F, R>(
        &self,
        ctx: CoreContext,
        repo: &thrift::RepoSpecifier,
        authz: AuthorizationContext,
        bubble_fetcher: F,
    ) -> Result<RepoContext, errors::ServiceError>
    where
        F: FnOnce(RepoEphemeralStore) -> R,
        R: Future<Output = anyhow::Result<Option<BubbleId>>>,
    {
        let repo = self
            .mononoke
            .repo(ctx, &repo.name)
            .await?
            .ok_or_else(|| errors::repo_not_found(repo.description()))?
            .with_bubble(bubble_fetcher)
            .await?
            .with_authorization_context(authz)
            .build()
            .await?;
        Ok(repo)
    }

    fn bubble_fetcher_for_changeset(
        &self,
        specifier: ChangesetSpecifier,
    ) -> impl FnOnce(RepoEphemeralStore) -> BoxFuture<'static, anyhow::Result<Option<BubbleId>>>
    {
        move |ephemeral| async move { specifier.bubble_id(ephemeral).await }.boxed()
    }

    /// Get the repo and changeset specified by a `thrift::CommitSpecifier`.
    pub(crate) async fn repo_changeset(
        &self,
        ctx: CoreContext,
        commit: &thrift::CommitSpecifier,
    ) -> Result<(RepoContext, ChangesetContext), errors::ServiceError> {
        let changeset_specifier = ChangesetSpecifier::from_request(&commit.id)?;
        let authz = AuthorizationContext::new(&ctx);
        let repo = self
            .repo_impl(
                ctx,
                &commit.repo,
                authz,
                self.bubble_fetcher_for_changeset(changeset_specifier.clone()),
            )
            .await?;
        let changeset = repo
            .changeset(changeset_specifier)
            .await?
            .ok_or_else(|| errors::commit_not_found(commit.description()))?;
        Ok((repo, changeset))
    }

    /// Get the repo and pair of changesets specified by a `thrift::CommitSpecifier`
    /// and `thrift::CommitId` pair.
    pub(crate) async fn repo_changeset_pair(
        &self,
        ctx: CoreContext,
        commit: &thrift::CommitSpecifier,
        other_commit: &thrift::CommitId,
    ) -> Result<(RepoContext, ChangesetContext, ChangesetContext), errors::ServiceError> {
        let changeset_specifier =
            ChangesetSpecifier::from_request(&commit.id).context("invalid target commit id")?;
        let other_changeset_specifier = ChangesetSpecifier::from_request(other_commit)
            .context("invalid or missing other commit id")?;
        if other_changeset_specifier.in_bubble() {
            Err(errors::invalid_request(format!(
                "Can't compare against a snapshot: {}",
                other_changeset_specifier
            )))?
        }
        let authz = AuthorizationContext::new(&ctx);
        let repo = self
            .repo_impl(
                ctx,
                &commit.repo,
                authz,
                self.bubble_fetcher_for_changeset(changeset_specifier.clone()),
            )
            .await?;
        let (changeset, other_changeset) = try_join!(
            async {
                Ok::<_, errors::ServiceError>(
                    repo.changeset(changeset_specifier)
                        .await
                        .context("failed to resolve target commit")?
                        .ok_or_else(|| errors::commit_not_found(commit.description()))?,
                )
            },
            async {
                Ok::<_, errors::ServiceError>(
                    repo.changeset(other_changeset_specifier)
                        .await
                        .context("failed to resolve other commit")?
                        .ok_or_else(|| {
                            errors::commit_not_found(format!(
                                "repo={} commit={}",
                                commit.repo.name,
                                other_commit.to_string()
                            ))
                        })?,
                )
            },
        )?;
        Ok((repo, changeset, other_changeset))
    }

    /// Get the changeset id specified by a `thrift::CommitId`.
    pub(crate) async fn changeset_id(
        &self,
        repo: &RepoContext,
        id: &thrift::CommitId,
    ) -> Result<ChangesetId, errors::ServiceError> {
        let changeset_specifier = ChangesetSpecifier::from_request(id)?;
        Ok(repo
            .resolve_specifier(changeset_specifier)
            .await?
            .ok_or_else(|| {
                errors::commit_not_found(format!("repo={} commit={}", repo.name(), id.to_string()))
            })?)
    }

    /// Get the repo and tree specified by a `thrift::TreeSpecifier`.
    ///
    /// Returns `None` if the tree is specified by commit path and that path
    /// is not a directory in that commit.
    pub(crate) async fn repo_tree(
        &self,
        ctx: CoreContext,
        tree: &thrift::TreeSpecifier,
    ) -> Result<(RepoContext, Option<TreeContext>), errors::ServiceError> {
        let (repo, tree) = match tree {
            thrift::TreeSpecifier::by_commit_path(commit_path) => {
                let (repo, changeset) = self.repo_changeset(ctx, &commit_path.commit).await?;
                let path = changeset.path_with_content(&commit_path.path).await?;
                (repo, path.tree().await?)
            }
            thrift::TreeSpecifier::by_id(tree_id) => {
                let repo = self.repo(ctx, &tree_id.repo).await?;
                let tree_id = TreeId::from_request(&tree_id.id)?;
                let tree = repo
                    .tree(tree_id)
                    .await?
                    .ok_or_else(|| errors::tree_not_found(tree.description()))?;
                (repo, Some(tree))
            }
            thrift::TreeSpecifier::UnknownField(id) => {
                return Err(errors::invalid_request(format!(
                    "tree specifier type not supported: {}",
                    id
                ))
                .into());
            }
        };
        Ok((repo, tree))
    }

    /// Get the repo and file specified by a `thrift::FileSpecifier`.
    ///
    /// Returns `None` if the file is specified by commit path, and that path
    /// is not a file in that commit.
    pub(crate) async fn repo_file(
        &self,
        ctx: CoreContext,
        file: &thrift::FileSpecifier,
    ) -> Result<(RepoContext, Option<FileContext>), errors::ServiceError> {
        let (repo, file) = match file {
            thrift::FileSpecifier::by_commit_path(commit_path) => {
                let (repo, changeset) = self.repo_changeset(ctx, &commit_path.commit).await?;
                let path = changeset.path_with_content(&commit_path.path).await?;
                (repo, path.file().await?)
            }
            thrift::FileSpecifier::by_id(file_id) => {
                let repo = self.repo(ctx, &file_id.repo).await?;
                let file_id = FileId::from_request(&file_id.id)?;
                let file = repo
                    .file(file_id)
                    .await?
                    .ok_or_else(|| errors::file_not_found(file.description()))?;
                (repo, Some(file))
            }
            thrift::FileSpecifier::by_sha1_content_hash(hash) => {
                let repo = self.repo(ctx, &hash.repo).await?;
                let file_sha1 = Sha1::from_request(&hash.content_hash)?;
                let file = repo
                    .file_by_content_sha1(file_sha1)
                    .await?
                    .ok_or_else(|| errors::file_not_found(file.description()))?;
                (repo, Some(file))
            }
            thrift::FileSpecifier::by_sha256_content_hash(hash) => {
                let repo = self.repo(ctx, &hash.repo).await?;
                let file_sha256 = Sha256::from_request(&hash.content_hash)?;
                let file = repo
                    .file_by_content_sha256(file_sha256)
                    .await?
                    .ok_or_else(|| errors::file_not_found(file.description()))?;
                (repo, Some(file))
            }
            thrift::FileSpecifier::UnknownField(id) => {
                return Err(errors::invalid_request(format!(
                    "file specifier type not supported: {}",
                    id
                ))
                .into());
            }
        };
        Ok((repo, file))
    }
}

fn log_result<T: AddScubaResponse>(
    ctx: CoreContext,
    stats: &FutureStats,
    result: &Result<T, impl errors::LoggableError>,
) {
    let mut scuba = ctx.scuba().clone();

    let (status, error, invalid_request, internal_failure) = match result {
        Ok(response) => {
            response.add_scuba_response(&mut scuba);
            ("SUCCESS", None, 0, 0)
        }
        Err(err) => {
            let (status, desc) = err.status_and_description();
            match status {
                Status::RequestError => ("REQUEST_ERROR", Some(desc), 1, 0),
                Status::InternalError => ("INTERNAL_ERROR", Some(desc), 0, 1),
            }
        }
    };
    let success = if error.is_none() { 1 } else { 0 };

    STATS::total_request_success.add_value(success);
    STATS::total_request_internal_failure.add_value(internal_failure);
    STATS::total_request_invalid.add_value(invalid_request);
    STATS::total_request_cancelled.add_value(0);
    STATS::total_request_internal_failure_permille.add_value(internal_failure * 1000);
    STATS::total_request_invalid_permille.add_value(invalid_request * 1000);

    ctx.perf_counters().insert_perf_counters(&mut scuba);

    scuba.add_future_stats(stats);
    scuba.add("status", status);
    if let Some(error) = error {
        if !tunables().scs_error_log_sampling().unwrap_or_default() {
            scuba.unsampled();
        }
        scuba.add("error", error.as_str());
    }
    scuba.log_with_msg("Request complete", None);
}

fn log_cancelled(ctx: &CoreContext, stats: &FutureStats) {
    STATS::total_request_success.add_value(0);
    STATS::total_request_internal_failure.add_value(0);
    STATS::total_request_invalid.add_value(0);
    STATS::total_request_cancelled.add_value(1);

    let mut scuba = ctx.scuba().clone();
    ctx.perf_counters().insert_perf_counters(&mut scuba);
    scuba.add_future_stats(stats);
    scuba.add("status", "CANCELLED");
    scuba.log_with_msg("Request cancelled", None);
}

// Define a macro to construct a CoreContext based on the thrift parameters.
macro_rules! create_ctx {
    ( $service_impl:expr, $method_name:ident, $req_ctxt:ident, $params_name:ident ) => {
        $service_impl.create_ctx(stringify!($method_name), $req_ctxt, None, &$params_name)
    };

    ( $service_impl:expr, $method_name:ident, $req_ctxt:ident, $obj_name:ident, $params_name:ident ) => {
        $service_impl.create_ctx(
            stringify!($method_name),
            $req_ctxt,
            Some(&$obj_name),
            &$params_name,
        )
    };
}

// Define a macro that generates a non-async wrapper that delegates to the
// async implementation of the method.
//
// The implementations of the methods can be found in the `methods` module.
macro_rules! impl_thrift_methods {
    ( $( async fn $method_name:ident($( $param_name:ident : $param_type:ty, )*) -> Result<$ok_type:ty, $err_type:ty>; )* ) => {
        $(
            fn $method_name<'implementation, 'req_ctxt, 'async_trait>(
                &'implementation self,
                req_ctxt: &'req_ctxt RequestContext,
                $( $param_name: $param_type ),*
            ) -> Pin<Box<dyn Future<Output = Result<$ok_type, $err_type>> + Send + 'async_trait>>
            where
                'implementation: 'async_trait,
                'req_ctxt: 'async_trait,
                Self: Sync + 'async_trait,
            {
                let handler = async move {
                    let ctx = create_ctx!(self.0, $method_name, req_ctxt, $( $param_name ),*).await?;
                    ctx.scuba().clone().log_with_msg("Request start", None);
                    STATS::total_request_start.add_value(1);
                    let (stats, res) = (self.0)
                        .$method_name(ctx.clone(), $( $param_name ),* )
                        .timed()
                        .on_cancel_with_data(|stats| log_cancelled(&ctx, &stats))
                        .await;
                    log_result(ctx, &stats, &res);
                    let method = stringify!($method_name).to_string();
                    STATS::method_completion_time_ms.add_value(stats.completion_time.as_millis_unchecked() as i64, (method,));
                    res.map_err(Into::into)
                };
                Box::pin(handler)
            }
        )*
    }
}

impl SourceControlService for SourceControlServiceThriftImpl {
    type RequestContext = RequestContext;

    impl_thrift_methods! {
        async fn list_repos(
            params: thrift::ListReposParams,
        ) -> Result<Vec<thrift::Repo>, service::ListReposExn>;

        async fn repo_info(
            repo: thrift::RepoSpecifier,
            params: thrift::RepoInfoParams,
        ) -> Result<thrift::RepoInfo, service::RepoInfoExn>;

        async fn repo_resolve_bookmark(
            repo: thrift::RepoSpecifier,
            params: thrift::RepoResolveBookmarkParams,
        ) -> Result<thrift::RepoResolveBookmarkResponse, service::RepoResolveBookmarkExn>;

        async fn repo_resolve_commit_prefix(
            repo: thrift::RepoSpecifier,
            params: thrift::RepoResolveCommitPrefixParams,
        ) -> Result<thrift::RepoResolveCommitPrefixResponse, service::RepoResolveCommitPrefixExn>;

        async fn repo_list_bookmarks(
            repo: thrift::RepoSpecifier,
            params: thrift::RepoListBookmarksParams,
        ) -> Result<thrift::RepoListBookmarksResponse, service::RepoListBookmarksExn>;

        async fn commit_common_base_with(
            commit: thrift::CommitSpecifier,
            params: thrift::CommitCommonBaseWithParams,
        ) -> Result<thrift::CommitLookupResponse, service::CommitCommonBaseWithExn>;

        async fn commit_lookup(
            commit: thrift::CommitSpecifier,
            params: thrift::CommitLookupParams,
        ) -> Result<thrift::CommitLookupResponse, service::CommitLookupExn>;

        async fn commit_lookup_pushrebase_history(
            commit: thrift::CommitSpecifier,
            params: thrift::CommitLookupPushrebaseHistoryParams,
        ) -> Result<thrift::CommitLookupPushrebaseHistoryResponse, service::CommitLookupPushrebaseHistoryExn>;

        async fn commit_file_diffs(
            commit: thrift::CommitSpecifier,
            params: thrift::CommitFileDiffsParams,
        ) -> Result<thrift::CommitFileDiffsResponse, service::CommitFileDiffsExn>;

        async fn commit_info(
            commit: thrift::CommitSpecifier,
            params: thrift::CommitInfoParams,
        ) -> Result<thrift::CommitInfo, service::CommitInfoExn>;

        async fn commit_is_ancestor_of(
            commit: thrift::CommitSpecifier,
            params: thrift::CommitIsAncestorOfParams,
        ) -> Result<bool, service::CommitIsAncestorOfExn>;

        async fn commit_compare(
            commit: thrift::CommitSpecifier,
            params: thrift::CommitCompareParams,
        ) -> Result<thrift::CommitCompareResponse, service::CommitCompareExn>;

        async fn commit_find_files(
            commit: thrift::CommitSpecifier,
            params: thrift::CommitFindFilesParams,
        ) -> Result<thrift::CommitFindFilesResponse, service::CommitFindFilesExn>;

        async fn commit_history(
            commit: thrift::CommitSpecifier,
            params: thrift::CommitHistoryParams,
        ) -> Result<thrift::CommitHistoryResponse, service::CommitHistoryExn>;

        async fn commit_list_descendant_bookmarks(
            commit: thrift::CommitSpecifier,
            params: thrift::CommitListDescendantBookmarksParams,
        ) -> Result<thrift::CommitListDescendantBookmarksResponse, service::CommitListDescendantBookmarksExn>;

        async fn commit_run_hooks(
            commit: thrift::CommitSpecifier,
            params: thrift::CommitRunHooksParams,
        ) -> Result<thrift::CommitRunHooksResponse, service::CommitRunHooksExn>;

        async fn commit_lookup_xrepo(
            commit: thrift::CommitSpecifier,
            params: thrift::CommitLookupXRepoParams,
        ) -> Result<thrift::CommitLookupResponse, service::CommitLookupXrepoExn>;

        async fn commit_path_exists(
            commit_path: thrift::CommitPathSpecifier,
            params: thrift::CommitPathExistsParams,
        ) -> Result<thrift::CommitPathExistsResponse, service::CommitPathExistsExn>;

        async fn commit_path_info(
            commit_path: thrift::CommitPathSpecifier,
            params: thrift::CommitPathInfoParams,
        ) -> Result<thrift::CommitPathInfoResponse, service::CommitPathInfoExn>;

        async fn commit_multiple_path_info(
            commit_path: thrift::CommitSpecifier,
            params: thrift::CommitMultiplePathInfoParams,
        ) -> Result<thrift::CommitMultiplePathInfoResponse, service::CommitMultiplePathInfoExn>;

        async fn commit_path_blame(
            commit_path: thrift::CommitPathSpecifier,
            params: thrift::CommitPathBlameParams,
        ) -> Result<thrift::CommitPathBlameResponse, service::CommitPathBlameExn>;

        async fn commit_path_history(
            commit_path: thrift::CommitPathSpecifier,
            params: thrift::CommitPathHistoryParams,
        ) -> Result<thrift::CommitPathHistoryResponse, service::CommitPathHistoryExn>;

        async fn commit_path_last_changed(
            commit_path: thrift::CommitPathSpecifier,
            params: thrift::CommitPathLastChangedParams,
        ) -> Result<thrift::CommitPathLastChangedResponse, service::CommitPathLastChangedExn>;

        async fn commit_multiple_path_last_changed(
            commit_path: thrift::CommitSpecifier,
            params: thrift::CommitMultiplePathLastChangedParams,
        ) -> Result<thrift::CommitMultiplePathLastChangedResponse, service::CommitMultiplePathLastChangedExn>;

        async fn tree_exists(
            tree: thrift::TreeSpecifier,
            params: thrift::TreeExistsParams,
        ) -> Result<bool, service::TreeExistsExn>;

        async fn commit_sparse_profile_delta(
            commit: thrift::CommitSpecifier,
            params: thrift::CommitSparseProfileDeltaParams,
        ) -> Result<thrift::CommitSparseProfileDeltaResponse, service::CommitSparseProfileDeltaExn>;

        async fn commit_sparse_profile_size(
            commit: thrift::CommitSpecifier,
            params: thrift::CommitSparseProfileSizeParams,
        ) -> Result<thrift::CommitSparseProfileSizeResponse, service::CommitSparseProfileSizeExn>;

        async fn tree_list(
            tree: thrift::TreeSpecifier,
            params: thrift::TreeListParams,
        ) -> Result<thrift::TreeListResponse, service::TreeListExn>;

        async fn file_exists(
            file: thrift::FileSpecifier,
            _params: thrift::FileExistsParams,
        ) -> Result<bool, service::FileExistsExn>;

        async fn file_info(
            file: thrift::FileSpecifier,
            _params: thrift::FileInfoParams,
        ) -> Result<thrift::FileInfo, service::FileInfoExn>;

        async fn file_content_chunk(
            file: thrift::FileSpecifier,
            params: thrift::FileContentChunkParams,
        ) -> Result<thrift::FileChunk, service::FileContentChunkExn>;

        async fn file_diff(
            file: thrift::FileSpecifier,
            params: thrift::FileDiffParams,
        ) -> Result<thrift::FileDiffResponse, service::FileDiffExn>;

        async fn repo_create_commit(
            repo: thrift::RepoSpecifier,
            params: thrift::RepoCreateCommitParams,
        ) -> Result<thrift::RepoCreateCommitResponse, service::RepoCreateCommitExn>;

        async fn repo_create_stack(
            repo: thrift::RepoSpecifier,
            params: thrift::RepoCreateStackParams,
        ) -> Result<thrift::RepoCreateStackResponse, service::RepoCreateStackExn>;

        async fn repo_bookmark_info(
            repo: thrift::RepoSpecifier,
            params: thrift::RepoBookmarkInfoParams,
        ) -> Result<thrift::RepoBookmarkInfoResponse, service::RepoBookmarkInfoExn>;

        async fn repo_stack_info(
            repo: thrift::RepoSpecifier,
            params: thrift::RepoStackInfoParams,
        ) -> Result<thrift::RepoStackInfoResponse, service::RepoStackInfoExn>;

        async fn repo_create_bookmark(
            repo: thrift::RepoSpecifier,
            params: thrift::RepoCreateBookmarkParams,
        ) -> Result<thrift::RepoCreateBookmarkResponse, service::RepoCreateBookmarkExn>;

        async fn repo_move_bookmark(
            repo: thrift::RepoSpecifier,
            params: thrift::RepoMoveBookmarkParams,
        ) -> Result<thrift::RepoMoveBookmarkResponse, service::RepoMoveBookmarkExn>;

        async fn repo_delete_bookmark(
            repo: thrift::RepoSpecifier,
            params: thrift::RepoDeleteBookmarkParams,
        ) -> Result<thrift::RepoDeleteBookmarkResponse, service::RepoDeleteBookmarkExn>;

        async fn repo_land_stack(
            repo: thrift::RepoSpecifier,
            params: thrift::RepoLandStackParams,
        ) -> Result<thrift::RepoLandStackResponse, service::RepoLandStackExn>;

        async fn repo_prepare_commits(
            repo: thrift::RepoSpecifier,
            params: thrift::RepoPrepareCommitsParams,
        ) -> Result<thrift::RepoPrepareCommitsResponse, service::RepoPrepareCommitsExn>;

        async fn repo_upload_file_content(
            repo: thrift::RepoSpecifier,
            params: thrift::RepoUploadFileContentParams,
        ) -> Result<thrift::RepoUploadFileContentResponse, service::RepoUploadFileContentExn>;

        async fn megarepo_add_sync_target_config(
            params: thrift::MegarepoAddConfigParams,
        ) -> Result<thrift::MegarepoAddConfigResponse, service::MegarepoAddSyncTargetConfigExn>;

        async fn megarepo_read_target_config(
            params: thrift::MegarepoReadConfigParams,
        ) -> Result<thrift::MegarepoReadConfigResponse, service::MegarepoReadTargetConfigExn>;

        async fn megarepo_add_sync_target(
            params: thrift::MegarepoAddTargetParams,
        ) -> Result<thrift::MegarepoAddTargetToken, service::MegarepoAddSyncTargetExn>;

        async fn megarepo_add_sync_target_poll(
            params: thrift::MegarepoAddTargetToken,
        ) -> Result<thrift::MegarepoAddTargetPollResponse, service::MegarepoAddSyncTargetPollExn>;

        async fn megarepo_add_branching_sync_target(
            params: thrift::MegarepoAddBranchingTargetParams,
        ) -> Result<thrift::MegarepoAddBranchingTargetToken, service::MegarepoAddBranchingSyncTargetExn>;

        async fn megarepo_add_branching_sync_target_poll(
            params: thrift::MegarepoAddBranchingTargetToken,
        ) -> Result<thrift::MegarepoAddBranchingTargetPollResponse, service::MegarepoAddBranchingSyncTargetPollExn>;

        async fn megarepo_change_target_config(
            params: thrift::MegarepoChangeTargetConfigParams,
        ) -> Result<thrift::MegarepoChangeConfigToken, service::MegarepoChangeTargetConfigExn>;

        async fn megarepo_change_target_config_poll(
            token: thrift::MegarepoChangeConfigToken,
        ) -> Result<thrift::MegarepoChangeTargetConfigPollResponse, service::MegarepoChangeTargetConfigPollExn>;

        async fn megarepo_sync_changeset(
            params: thrift::MegarepoSyncChangesetParams,
        ) -> Result<thrift::MegarepoSyncChangesetToken, service::MegarepoSyncChangesetExn>;

        async fn megarepo_sync_changeset_poll(
            token: thrift::MegarepoSyncChangesetToken,
        ) -> Result<thrift::MegarepoSyncChangesetPollResponse, service::MegarepoSyncChangesetPollExn>;

        async fn megarepo_remerge_source(
            params: thrift::MegarepoRemergeSourceParams,
        ) -> Result<thrift::MegarepoRemergeSourceToken, service::MegarepoRemergeSourceExn>;

        async fn megarepo_remerge_source_poll(
            token: thrift::MegarepoRemergeSourceToken,
        ) -> Result<thrift::MegarepoRemergeSourcePollResponse, service::MegarepoRemergeSourcePollExn>;

        async fn upload_git_object(
            repo: thrift::RepoSpecifier,
            params: thrift::UploadGitObjectParams,
        ) -> Result<thrift::UploadGitObjectResponse, service::UploadGitObjectExn>;

        async fn create_git_tree(
            repo: thrift::RepoSpecifier,
            params: thrift::CreateGitTreeParams,
        ) -> Result<thrift::CreateGitTreeResponse, service::CreateGitTreeExn>;

        async fn create_git_tag(
            repo: thrift::RepoSpecifier,
            params: thrift::CreateGitTagParams,
        ) -> Result<thrift::CreateGitTagResponse, service::CreateGitTagExn>;
    }
}


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

use crate::commit_id::CommitIdExt;

pub(crate) trait SpecifierExt: Send + Sync {
    fn description(&self) -> String;

    fn scuba_reponame(&self) -> Option<String> {
        None
    }

    fn scuba_commit(&self) -> Option<String> {
        None
    }

    fn scuba_path(&self) -> Option<String> {
        None
    }
}

impl SpecifierExt for thrift::RepoSpecifier {
    fn description(&self) -> String {
        format!("repo={}", self.name)
    }

    fn scuba_reponame(&self) -> Option<String> {
        Some(self.name.clone())
    }
}

impl SpecifierExt for thrift::CommitSpecifier {
    fn description(&self) -> String {
        format!("repo={} commit={}", self.repo.name, self.id.to_string())
    }

    fn scuba_reponame(&self) -> Option<String> {
        self.repo.scuba_reponame()
    }

    fn scuba_commit(&self) -> Option<String> {
        Some(self.id.to_string())
    }
}

impl SpecifierExt for thrift::CommitPathSpecifier {
    fn description(&self) -> String {
        format!(
            "repo={} commit={} path={}",
            self.commit.repo.name,
            self.commit.id.to_string(),
            self.path
        )
    }

    fn scuba_reponame(&self) -> Option<String> {
        self.commit.scuba_reponame()
    }
    fn scuba_commit(&self) -> Option<String> {
        self.commit.scuba_commit()
    }
    fn scuba_path(&self) -> Option<String> {
        Some(self.path.clone())
    }
}

impl SpecifierExt for thrift::TreeSpecifier {
    fn description(&self) -> String {
        match self {
            thrift::TreeSpecifier::by_commit_path(commit_path) => commit_path.description(),
            thrift::TreeSpecifier::by_id(tree_id) => format!(
                "repo={} tree={}",
                tree_id.repo.name,
                hex_string(&tree_id.id)
            ),
            thrift::TreeSpecifier::UnknownField(n) => format!("unknown tree specifier type {}", n),
        }
    }

    fn scuba_reponame(&self) -> Option<String> {
        match self {
            thrift::TreeSpecifier::by_commit_path(commit_path) => commit_path.scuba_reponame(),
            thrift::TreeSpecifier::by_id(tree_id) => tree_id.repo.scuba_reponame(),
            thrift::TreeSpecifier::UnknownField(_) => None,
        }
    }

    fn scuba_commit(&self) -> Option<String> {
        match self {
            thrift::TreeSpecifier::by_commit_path(commit_path) => commit_path.scuba_commit(),
            thrift::TreeSpecifier::by_id(_tree_id) => None,
            thrift::TreeSpecifier::UnknownField(_) => None,
        }
    }

    fn scuba_path(&self) -> Option<String> {
        match self {
            thrift::TreeSpecifier::by_commit_path(commit_path) => commit_path.scuba_path(),
            thrift::TreeSpecifier::by_id(_tree_id) => None,
            thrift::TreeSpecifier::UnknownField(_) => None,
        }
    }
}

impl SpecifierExt for thrift::FileSpecifier {
    fn description(&self) -> String {
        match self {
            thrift::FileSpecifier::by_commit_path(commit_path) => commit_path.description(),
            thrift::FileSpecifier::by_id(file_id) => format!(
                "repo={} file={}",
                file_id.repo.name,
                hex_string(&file_id.id),
            ),
            thrift::FileSpecifier::by_sha1_content_hash(hash) => format!(
                "repo={} file_sha1={}",
                hash.repo.name,
                hex_string(&hash.content_hash),
            ),
            thrift::FileSpecifier::by_sha256_content_hash(hash) => format!(
                "repo={} file_sha256={}",
                hash.repo.name,
                hex_string(&hash.content_hash),
            ),
            thrift::FileSpecifier::UnknownField(n) => format!("unknown file specifier type {}", n),
        }
    }

    fn scuba_reponame(&self) -> Option<String> {
        match self {
            thrift::FileSpecifier::by_commit_path(commit_path) => commit_path.scuba_reponame(),
            thrift::FileSpecifier::by_id(file_id) => file_id.repo.scuba_reponame(),
            thrift::FileSpecifier::by_sha1_content_hash(hash) => hash.repo.scuba_reponame(),
            thrift::FileSpecifier::by_sha256_content_hash(hash) => hash.repo.scuba_reponame(),
            thrift::FileSpecifier::UnknownField(_) => None,
        }
    }
    fn scuba_commit(&self) -> Option<String> {
        match self {
            thrift::FileSpecifier::by_commit_path(commit_path) => commit_path.scuba_commit(),
            thrift::FileSpecifier::by_id(_file_id) => None,
            thrift::FileSpecifier::by_sha1_content_hash(_hash) => None,
            thrift::FileSpecifier::by_sha256_content_hash(_hash) => None,
            thrift::FileSpecifier::UnknownField(_) => None,
        }
    }
    fn scuba_path(&self) -> Option<String> {
        match self {
            thrift::FileSpecifier::by_commit_path(commit_path) => commit_path.scuba_path(),
            thrift::FileSpecifier::by_id(file_id) => Some(hex_string(&file_id.id)),
            thrift::FileSpecifier::by_sha1_content_hash(hash) => {
                Some(hex_string(&hash.content_hash))
            }
            thrift::FileSpecifier::by_sha256_content_hash(hash) => {
                Some(hex_string(&hash.content_hash))
            }
            thrift::FileSpecifier::UnknownField(_) => None,
        }
    }
}