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

//! Helper library for rendering commit info

use std::collections::BTreeMap;
use std::collections::HashSet;
use std::io::Write;

use anyhow::Error;
use chrono::DateTime;
use chrono::FixedOffset;
use chrono::Local;
use chrono::TimeZone;
use serde::Serialize;
use source_control::types as thrift;

use crate::args::commit_id::map_commit_ids;
use crate::library::commit_id::render_commit_id;

#[derive(Serialize)]
pub(crate) struct CommitInfo {
    pub r#type: String, // For JSON output, always "commit".
    pub ids: BTreeMap<String, String>,
    pub parents: Vec<BTreeMap<String, String>>,
    pub message: String,
    pub date: DateTime<FixedOffset>,
    pub timestamp: i64,
    pub timezone: i32,
    pub author: String,
    pub generation: i64,
    pub extra: BTreeMap<String, String>,
    pub extra_hex: BTreeMap<String, String>,
    pub git_extra_headers: Option<BTreeMap<String, String>>,
}

impl TryFrom<&thrift::CommitInfo> for CommitInfo {
    type Error = Error;

    fn try_from(commit: &thrift::CommitInfo) -> Result<CommitInfo, Error> {
        let ids = map_commit_ids(commit.ids.values());
        let parents = commit
            .parents
            .iter()
            .map(|ids| map_commit_ids(ids.values()))
            .collect();
        let message = commit.message.clone();
        let author = commit.author.clone();
        // The commit date is recorded as a timestamp plus timezone pair, where
        // the timezone is seconds east of UTC.
        let timestamp = commit.date;
        let timezone = commit.tz;
        let date = FixedOffset::east_opt(timezone)
            .unwrap()
            .timestamp_opt(timestamp, 0)
            .unwrap();
        // Extras are binary data, but usually we want to render them as
        // strings. In the case that they are not UTF-8 strings, they're
        // probably a commit hash, so we should hex-encode them. Record extras
        // that are valid UTF-8 as strings, and hex encode the rest.
        let mut extra = BTreeMap::new();
        let mut extra_hex = BTreeMap::new();
        for (name, value) in commit.extra.iter() {
            match std::str::from_utf8(value) {
                Ok(value) => extra.insert(name.clone(), value.to_string()),
                Err(_) => extra_hex.insert(name.clone(), faster_hex::hex_string(value)),
            };
        }
        // Only parse the headers which are valid string cause only those will be displayable to the user
        let git_extra_headers = commit.git_extra_headers.as_ref().map(|headers| {
            headers
                .iter()
                .filter_map(|(k, v)| {
                    match (
                        String::from_utf8(k.0.to_vec()),
                        String::from_utf8(v.to_vec()),
                    ) {
                        (Ok(key), Ok(value)) => Some((key, value)),
                        _ => None,
                    }
                })
                .collect()
        });
        Ok(CommitInfo {
            r#type: "commit".to_string(),
            ids,
            parents,
            message,
            date,
            timestamp,
            timezone,
            author,
            generation: commit.generation,
            extra,
            extra_hex,
            git_extra_headers,
        })
    }
}

#[allow(dead_code)]
pub(crate) fn render_commit_summary(
    commit: &CommitInfo,
    requested: &str,
    schemes: &HashSet<String>,
    w: &mut dyn Write,
) -> Result<(), Error> {
    render_commit_id(
        Some(("Commit", "    ")),
        "\n",
        requested,
        &commit.ids,
        schemes,
        w,
    )?;
    write!(w, "\n")?;
    let date = commit.date.to_string();
    let local_date = commit.date.with_timezone(&Local).to_string();
    if date != local_date {
        write!(w, "Date: {} ({})\n", date, local_date)?;
    } else {
        write!(w, "Date: {}\n", date)?;
    }
    write!(w, "Author: {}\n", commit.author)?;
    write!(
        w,
        "Summary: {}\n",
        commit.message.lines().next().unwrap_or("")
    )?;
    Ok(())
}

pub(crate) fn render_commit_info(
    commit: &CommitInfo,
    requested: &str,
    schemes: &HashSet<String>,
    w: &mut dyn Write,
) -> Result<(), Error> {
    render_commit_id(
        Some(("Commit", "    ")),
        "\n",
        requested,
        &commit.ids,
        schemes,
        w,
    )?;
    write!(w, "\n")?;
    for (i, parent) in commit.parents.iter().enumerate() {
        let header = if commit.parents.len() == 1 {
            "Parent".to_string()
        } else {
            format!("Parent-{}", i)
        };
        render_commit_id(
            Some((&header, "    ")),
            "\n",
            &format!("Parent {} of {}", i, requested),
            parent,
            schemes,
            w,
        )?;
        write!(w, "\n")?;
    }
    let date = commit.date.to_string();
    let local_date = commit.date.with_timezone(&Local).to_string();
    if date != local_date {
        write!(w, "Date: {} ({})\n", date, local_date)?;
    } else {
        write!(w, "Date: {}\n", date)?;
    }
    write!(w, "Author: {}\n", commit.author)?;
    write!(w, "Generation: {}\n", commit.generation)?;
    if !commit.extra.is_empty() {
        write!(w, "Extra:\n")?;
        for (name, value) in commit.extra.iter() {
            write!(w, "    {}={}\n", name, value)?;
        }
    }
    if !commit.extra_hex.is_empty() {
        write!(w, "Extra-Binary:\n")?;
        for (name, value) in commit.extra_hex.iter() {
            write!(w, "    {}={}\n", name, value)?;
        }
    }
    if let Some(ref headers) = commit.git_extra_headers {
        write!(w, "Git Extra Headers:\n")?;
        for (key, value) in headers {
            write!(w, "    {}={}\n", key, value)?;
        }
    }
    write!(w, "\n{}\n", commit.message)?;
    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.
 */

//! Helper library for rendering commit ids

use std::collections::BTreeMap;
use std::collections::HashSet;
use std::io::Write;

use anyhow::bail;
use anyhow::Error;
use itertools::Itertools;

/// Render a Commit ID, potentially in multiple schemes.
///
/// If the commit ID is in a single scheme, then it is rendered as-is. If the
/// commit ID is in multiple schemes, then it is rendered in each scheme,
/// prefixed by the name of the scheme, separated by the separator.
///
/// If header is not None, then the ID is prefixed by the header, and any
/// commit IDs with multiple schemes are indented with the indent.
pub(crate) fn render_commit_id(
    header: Option<(&str, &str)>,
    separator: &str,
    requested: &str,
    ids: &BTreeMap<String, String>,
    schemes: &HashSet<String>,
    w: &mut dyn Write,
) -> Result<(), Error> {
    let ids: BTreeMap<_, _> = ids
        .iter()
        .filter(|(scheme, _id)| schemes.contains(*scheme))
        .collect();

    if let Ok(scheme) = schemes.iter().exactly_one() {
        if let Ok(id) = ids.values().exactly_one() {
            if let Some((header, _indent)) = header {
                write!(w, "{}: ", header)?;
            }
            write!(w, "{}", id)?;
        } else {
            bail!("{} does not have a '{}' id", requested, scheme);
        }
    } else {
        if ids.is_empty() {
            let mut schemes: Vec<_> = schemes.iter().map(AsRef::as_ref).collect();
            schemes.sort_unstable();
            bail!(
                "{} does not have any '{}' ids",
                requested,
                schemes.as_slice().join("', '")
            );
        }
        let mut prefix = "";
        if let Some((header, indent)) = header {
            write!(w, "{}:{}", header, separator)?;
            prefix = indent;
        }
        for (i, (scheme, id)) in ids.iter().enumerate() {
            if i > 0 {
                write!(w, "{}", separator)?;
            }
            write!(w, "{}{}={}", prefix, scheme, 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.
 */

//! Helper library for returning file diffs

use std::io::Write;

use anyhow::Result;
use cloned::cloned;
use futures::stream;
use futures::stream::StreamExt;
use futures::Stream;
use serde::Serialize;
use source_control as thrift;

use crate::connection::Connection;
use crate::render::Render;

#[derive(Serialize)]
struct FileInfo {
    file_type: Option<String>,
    file_content_type: Option<String>,
    file_generated_status: Option<String>,
}

impl From<&thrift::MetadataDiffFileInfo> for FileInfo {
    fn from(file_info: &thrift::MetadataDiffFileInfo) -> Self {
        Self {
            file_type: file_info
                .file_type
                .as_ref()
                .map(thrift::MetadataDiffFileType::to_string),
            file_content_type: file_info
                .file_content_type
                .as_ref()
                .map(thrift::MetadataDiffFileContentType::to_string),
            file_generated_status: file_info
                .file_generated_status
                .as_ref()
                .map(thrift::FileGeneratedStatus::to_string),
        }
    }
}

#[derive(Serialize)]
struct LinesCount {
    added_lines_count: i64,
    deleted_lines_count: i64,
    significant_added_lines_count: i64,
    significant_deleted_lines_count: i64,
    first_added_line_number: Option<i64>,
}

impl From<&thrift::MetadataDiffLinesCount> for LinesCount {
    fn from(lines_count: &thrift::MetadataDiffLinesCount) -> Self {
        Self {
            added_lines_count: lines_count.added_lines_count,
            deleted_lines_count: lines_count.deleted_lines_count,
            significant_added_lines_count: lines_count.significant_added_lines_count,
            significant_deleted_lines_count: lines_count.significant_deleted_lines_count,
            first_added_line_number: lines_count.first_added_line_number,
        }
    }
}

#[derive(Serialize)]
struct MetadataDiffElement {
    old_path: String,
    new_path: String,
    old_file_info: FileInfo,
    new_file_info: FileInfo,
    lines_count: Option<LinesCount>,
}

#[derive(Serialize)]
enum DiffOutputElement {
    RawDiff(Vec<u8>),
    MetadataDiff(MetadataDiffElement),
}

#[derive(Serialize)]
struct DiffOutput {
    diffs: Vec<DiffOutputElement>,
    stopped_at_pair: Option<thrift::CommitFileDiffsStoppedAtPair>,
}

fn render_file_info(
    tag: &str,
    path: &String,
    file_info: &FileInfo,
    w: &mut dyn Write,
) -> Result<()> {
    write!(w, "{} {},", tag, path)?;
    if let Some(file_type) = &file_info.file_type {
        write!(w, " file type: {},", file_type)?;
    }
    if let Some(content_type) = &file_info.file_content_type {
        write!(w, " content type: {},", content_type)?;
    }
    if let Some(generated_status) = &file_info.file_generated_status {
        write!(w, " generated status: {},", generated_status)?;
    }
    write!(w, "\n")?;
    Ok(())
}

impl Render for DiffOutput {
    type Args = ();

    fn render(&self, _args: &Self::Args, w: &mut dyn Write) -> Result<()> {
        for diff in &self.diffs {
            match diff {
                DiffOutputElement::RawDiff(diff) => write!(w, "{}", String::from_utf8_lossy(diff))?,
                DiffOutputElement::MetadataDiff(diff) => {
                    render_file_info("---", &diff.old_path, &diff.old_file_info, w)?;
                    render_file_info("+++", &diff.new_path, &diff.new_file_info, w)?;

                    if let Some(lines_count) = &diff.lines_count {
                        if let Some(first_added_line_number) = lines_count.first_added_line_number {
                            write!(w, "first added line number: {}, ", first_added_line_number)?;
                        }
                        writeln!(
                            w,
                            "{} significant lines ({} added, {} deleted), {} total ({} added, {} deleted)",
                            lines_count.significant_added_lines_count
                                + lines_count.significant_deleted_lines_count,
                            lines_count.significant_added_lines_count,
                            lines_count.significant_deleted_lines_count,
                            lines_count.added_lines_count + lines_count.deleted_lines_count,
                            lines_count.added_lines_count,
                            lines_count.deleted_lines_count,
                        )?
                    }
                    write!(w, "\n")?;
                }
            }
        }

        if let Some(stopped_at_pair) = &self.stopped_at_pair {
            writeln!(
                w,
                "stopped at (base_path: {}, other_path: {})",
                stopped_at_pair
                    .base_path
                    .as_ref()
                    .unwrap_or(&String::from("")),
                stopped_at_pair
                    .other_path
                    .as_ref()
                    .unwrap_or(&String::from(""))
            )?;
        }

        Ok(())
    }

    fn render_json(&self, _args: &Self::Args, w: &mut dyn Write) -> Result<()> {
        Ok(serde_json::to_writer(w, self)?)
    }
}

async fn make_file_diff_request(
    connection: &Connection,
    commit: &thrift::CommitSpecifier,
    other_commit_id: Option<thrift::CommitId>,
    paths: Vec<thrift::CommitFileDiffsParamsPathPair>,
    diff_size_limit: Option<i64>,
    diff_format: thrift::DiffFormat,
) -> Result<DiffOutput> {
    let params = thrift::CommitFileDiffsParams {
        other_commit_id,
        paths,
        format: diff_format,
        context: 3,
        diff_size_limit,
        ..Default::default()
    };

    let response = connection.commit_file_diffs(commit, &params).await?;
    let diffs: Vec<_> = response
        .path_diffs
        .into_iter()
        .filter_map(|path_diff| match path_diff {
            thrift::CommitFileDiffsResponseElement {
                diff: thrift::Diff::raw_diff(diff),
                ..
            } => Some(DiffOutputElement::RawDiff(
                diff.raw_diff.unwrap_or_else(Vec::new),
            )),
            thrift::CommitFileDiffsResponseElement {
                diff: thrift::Diff::metadata_diff(diff),
                base_path,
                other_path,
                ..
            } => Some(DiffOutputElement::MetadataDiff(MetadataDiffElement {
                old_path: other_path
                    .map_or_else(|| "/dev/null".to_string(), |path| format!("a/{}", path)),
                new_path: base_path
                    .map_or_else(|| "/dev/null".to_string(), |path| format!("b/{}", path)),
                old_file_info: FileInfo::from(&diff.old_file_info),
                new_file_info: FileInfo::from(&diff.new_file_info),
                lines_count: diff.lines_count.as_ref().map(LinesCount::from),
            })),
            _ => None,
        })
        .collect();

    Ok(DiffOutput {
        diffs,
        stopped_at_pair: response.stopped_at_pair,
    })
}

/// Given the paths and sizes of files to diff returns the stream of renderable
/// structs. The sizes are used to avoid hitting size limit when doing batch requests.
pub(crate) fn diff_files(
    connection: &Connection,
    commit: thrift::CommitSpecifier,
    other_commit_id: Option<thrift::CommitId>,
    paths_sizes: impl IntoIterator<Item = (thrift::CommitFileDiffsParamsPathPair, i64)>,
    diff_size_limit: Option<i64>,
    diff_format: thrift::DiffFormat,
) -> impl Stream<Item = Result<impl Render<Args = ()>>> {
    let mut size_sum: i64 = 0;
    let mut path_count: i64 = 0;
    let mut paths = Vec::new();
    let mut requests = Vec::new();
    cloned!(connection);
    for (path, size) in paths_sizes {
        if size + size_sum > thrift::COMMIT_FILE_DIFFS_SIZE_LIMIT
            || path_count + 1 > thrift::COMMIT_FILE_DIFFS_PATH_COUNT_LIMIT
        {
            requests.push(paths);
            paths = Vec::new();
            size_sum = 0;
            path_count = 0;
        }
        paths.push(path);
        path_count += 1;
        size_sum += size;
    }
    requests.push(paths);
    stream::iter(requests).then(move |paths| {
        let connection = connection.clone();
        let commit = commit.clone();
        let other_commit_id = other_commit_id.clone();
        async move {
            make_file_diff_request(
                &connection,
                &commit,
                other_commit_id,
                paths,
                diff_size_limit,
                diff_format,
            )
            .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.
 */

//! Library contating code shared between commands.

pub(crate) mod bookmark;
pub(crate) mod commit;
pub(crate) mod commit_id;
pub(crate) mod diff;
pub(crate) mod path_tree;

use chrono::DateTime;
use chrono::FixedOffset;
use chrono::TimeZone;
use source_control as thrift;

pub fn datetime(datetime: &thrift::DateTime) -> DateTime<FixedOffset> {
    FixedOffset::east_opt(datetime.tz)
        .unwrap()
        .timestamp_opt(datetime.timestamp, 0)
        .unwrap()
}


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

use std::collections::BTreeMap;
use std::iter::Extend;

#[derive(Debug)]
pub(crate) enum PathItem {
    // Requested item.  Either a file, or a whole directory tree.
    Target,

    // Requested item, but only if it is a directory.
    TargetDir,

    // Directory with requested items inside.
    Dir(PathTree),
}

#[derive(Default, Debug)]
pub(crate) struct PathTree {
    elems: BTreeMap<String, PathItem>,
}

impl PathTree {
    pub fn new() -> Self {
        PathTree {
            elems: BTreeMap::new(),
        }
    }

    pub fn insert(&mut self, path: &str) {
        if let Some((elem, rest)) = path.split_once('/') {
            if rest.is_empty() {
                // Path ending in `/` - include the target if it is a
                // directory.
                match self.elems.get_mut(elem) {
                    Some(PathItem::Target) | Some(PathItem::TargetDir) => {
                        // This target is already requested.
                    }
                    Some(item @ PathItem::Dir(_)) => {
                        // Requesting both a path and something under that path
                        // is the same as requesting just the path.  Upgrade this
                        // directory to a full target.
                        *item = PathItem::TargetDir;
                    }
                    None => {
                        self.elems.insert(elem.to_string(), PathItem::TargetDir);
                    }
                }
            } else {
                // Path with some more elements to come.
                match self.elems.get_mut(elem) {
                    Some(PathItem::Target) | Some(PathItem::TargetDir) => {
                        // Requesting both a path and something under that path
                        // is the same as requesting just the path.
                    }
                    Some(PathItem::Dir(tree)) => tree.insert(rest),
                    None => {
                        let mut tree = PathTree::new();
                        tree.insert(rest);
                        self.elems.insert(elem.to_string(), PathItem::Dir(tree));
                    }
                }
            }
        } else {
            match self.elems.get_mut(path) {
                Some(PathItem::Target) => {
                    // This target is already requested.
                }
                Some(item @ (PathItem::Dir(_) | PathItem::TargetDir)) => {
                    // Requesting both a path and something under that path
                    // is the same as requesting just the path.  Upgrade this
                    // directory to a full target.
                    *item = PathItem::Target;
                }
                None => {
                    self.elems.insert(path.to_string(), PathItem::Target);
                }
            }
        }
    }

    pub fn remove(&mut self, path: &str) -> Option<PathItem> {
        self.elems.remove(path)
    }
}

impl<'a> Extend<&'a str> for PathTree {
    fn extend<T>(&mut self, items: T)
    where
        T: IntoIterator<Item = &'a str>,
    {
        for item in items {
            self.insert(item);
        }
    }
}

impl Extend<String> for PathTree {
    fn extend<T>(&mut self, items: T)
    where
        T: IntoIterator<Item = String>,
    {
        for item in items {
            self.insert(&item);
        }
    }
}


// @generated by autocargo
use std::env;
use std::fs;
use std::path::Path;

use thrift_compiler::Config;
use thrift_compiler::GenContext;

#[rustfmt::skip]
fn main() {
    // Rerun if this gets rewritten.
    println!("cargo:rerun-if-changed=thrift_build.rs");

    let out_dir = env::var_os("OUT_DIR").expect("OUT_DIR env not provided");
    let out_dir: &Path = out_dir.as_ref();
    fs::write(
        out_dir.join("cratemap"),
        "fb303_core fb303_core
megarepo_configs megarepo_configs
source_control crate",
    ).expect("Failed to write cratemap");

    let conf = {
        let mut conf = Config::from_env(GenContext::Lib).expect("Failed to instantiate thrift_compiler::Config");

        let path_from_manifest_to_base: &Path = "../../../..".as_ref();
        let cargo_manifest_dir =
            env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not provided");
        let cargo_manifest_dir: &Path = cargo_manifest_dir.as_ref();
        let base_path = cargo_manifest_dir
            .join(path_from_manifest_to_base)
            .canonicalize()
            .expect("Failed to canonicalize base_path");
        // TODO: replace canonicalize() with std::path::absolute() when
        // https://github.com/rust-lang/rust/pull/91673 is available (~Rust 1.60)
        // and remove this block.
        #[cfg(windows)]
        let base_path = Path::new(
            base_path
                .as_path()
                .to_string_lossy()
                .trim_start_matches(r"\\?\"),
            )
            .to_path_buf();

        conf.base_path(base_path);

        let options = "types_crate=source_control__types,serde";
        if !options.is_empty() {
            conf.options(options);
        }

        let include_srcs = vec![

        ];
        conf.include_srcs(include_srcs);

        conf
    };

    conf
        .run(&[
            "source_control.thrift"
        ])
        .expect("Failed while running thrift compilation");
}


// @generated by autocargo
::codegen_includer_proc_macro::include!();


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

use std::collections::BTreeMap;
use std::collections::BTreeSet;

use cloned::cloned;
use faster_hex::hex_string;
use futures_util::future;
use futures_util::FutureExt;
use mononoke_api::ChangesetContext;
use mononoke_api::ChangesetId;
use mononoke_api::MononokeError;
use mononoke_api::RepoContext;
use source_control as thrift;

/// Generate a mapping for a commit's identity into the requested identity
/// schemes.
pub(crate) async fn map_commit_identity(
    changeset_ctx: &ChangesetContext,
    schemes: &BTreeSet<thrift::CommitIdentityScheme>,
) -> Result<BTreeMap<thrift::CommitIdentityScheme, thrift::CommitId>, MononokeError> {
    let mut ids = BTreeMap::new();
    ids.insert(
        thrift::CommitIdentityScheme::BONSAI,
        thrift::CommitId::bonsai(changeset_ctx.id().as_ref().into()),
    );
    let mut scheme_identities = vec![];
    if schemes.contains(&thrift::CommitIdentityScheme::HG) {
        let identity = async {
            if let Some(hg_id) = changeset_ctx.hg_id().await? {
                let result: Result<Option<_>, MononokeError> = Ok(Some((
                    thrift::CommitIdentityScheme::HG,
                    thrift::CommitId::hg(hg_id.as_ref().into()),
                )));
                result
            } else {
                Ok(None)
            }
        };
        scheme_identities.push(identity.boxed());
    }
    if schemes.contains(&thrift::CommitIdentityScheme::GLOBALREV) {
        let identity = async {
            if let Some(globalrev) = changeset_ctx.globalrev().await? {
                let result: Result<Option<_>, MononokeError> = Ok(Some((
                    thrift::CommitIdentityScheme::GLOBALREV,
                    thrift::CommitId::globalrev(globalrev.id() as i64),
                )));
                result
            } else {
                Ok(None)
            }
        };
        scheme_identities.push(identity.boxed());
    }
    if schemes.contains(&thrift::CommitIdentityScheme::SVNREV) {
        let identity = async {
            if let Some(svnrev) = changeset_ctx.svnrev().await? {
                let result: Result<Option<_>, MononokeError> = Ok(Some((
                    thrift::CommitIdentityScheme::SVNREV,
                    thrift::CommitId::svnrev(svnrev.id() as i64),
                )));
                result
            } else {
                Ok(None)
            }
        };
        scheme_identities.push(identity.boxed());
    }
    if schemes.contains(&thrift::CommitIdentityScheme::GIT) {
        let identity = async {
            if let Some(git_sha1) = changeset_ctx.git_sha1().await? {
                let result: Result<Option<_>, MononokeError> = Ok(Some((
                    thrift::CommitIdentityScheme::GIT,
                    thrift::CommitId::git(git_sha1.as_ref().into()),
                )));
                result
            } else {
                Ok(None)
            }
        };
        scheme_identities.push(identity.boxed());
    }
    let scheme_identities = future::try_join_all(scheme_identities).await?;
    for (scheme, id) in scheme_identities.into_iter().flatten() {
        ids.insert(scheme, id);
    }
    Ok(ids)
}

/// Generate mappings for multiple commits' identities into the requested
/// identity schemes.
pub(crate) async fn map_commit_identities(
    repo_ctx: &RepoContext,
    ids: Vec<ChangesetId>,
    schemes: &BTreeSet<thrift::CommitIdentityScheme>,
) -> Result<
    BTreeMap<ChangesetId, BTreeMap<thrift::CommitIdentityScheme, thrift::CommitId>>,
    MononokeError,
> {
    let mut result = BTreeMap::new();
    for id in ids.iter() {
        let mut idmap = BTreeMap::new();
        idmap.insert(
            thrift::CommitIdentityScheme::BONSAI,
            thrift::CommitId::bonsai(id.as_ref().into()),
        );
        result.insert(*id, idmap);
    }
    let mut scheme_identities = vec![];
    if schemes.contains(&thrift::CommitIdentityScheme::HG) {
        let ids = ids.clone();
        let identities = async {
            let bonsai_hg_ids = repo_ctx
                .many_changeset_hg_ids(ids)
                .await?
                .into_iter()
                .map(|(cs_id, hg_cs_id)| {
                    (
                        cs_id,
                        thrift::CommitIdentityScheme::HG,
                        thrift::CommitId::hg(hg_cs_id.as_ref().into()),
                    )
                })
                .collect::<Vec<_>>();
            let result: Result<_, MononokeError> = Ok(bonsai_hg_ids);
            result
        };
        scheme_identities.push(identities.boxed());
    }
    if schemes.contains(&thrift::CommitIdentityScheme::GIT) {
        cloned!(ids);
        let identities = async {
            let bonsai_git_shas = repo_ctx
                .many_changeset_git_sha1s(ids)
                .await?
                .into_iter()
                .map(|(cs_id, git_sha1)| {
                    (
                        cs_id,
                        thrift::CommitIdentityScheme::GIT,
                        thrift::CommitId::git(git_sha1.as_ref().into()),
                    )
                })
                .collect::<Vec<_>>();
            let result: Result<_, MononokeError> = Ok(bonsai_git_shas);
            result
        };
        scheme_identities.push(identities.boxed());
    }
    if schemes.contains(&thrift::CommitIdentityScheme::GLOBALREV) {
        cloned!(ids);
        let identities = async {
            let bonsai_globalrev_ids = repo_ctx
                .many_changeset_globalrev_ids(ids)
                .await?
                .into_iter()
                .map(|(cs_id, globalrev)| {
                    (
                        cs_id,
                        thrift::CommitIdentityScheme::GLOBALREV,
                        thrift::CommitId::globalrev(globalrev.id() as i64),
                    )
                })
                .collect::<Vec<_>>();
            let result: Result<_, MononokeError> = Ok(bonsai_globalrev_ids);
            result
        };
        scheme_identities.push(identities.boxed());
    }
    if schemes.contains(&thrift::CommitIdentityScheme::SVNREV) {
        let identities = async {
            let bonsai_svnrev_ids = repo_ctx
                .many_changeset_svnrev_ids(ids)
                .await?
                .into_iter()
                .map(|(cs_id, svnrev)| {
                    (
                        cs_id,
                        thrift::CommitIdentityScheme::SVNREV,
                        thrift::CommitId::svnrev(svnrev.id() as i64),
                    )
                })
                .collect::<Vec<_>>();
            let result: Result<_, MononokeError> = Ok(bonsai_svnrev_ids);
            result
        };
        scheme_identities.push(identities.boxed());
    }
    let scheme_identities = future::try_join_all(scheme_identities).await?;
    for ids in scheme_identities {
        for (cs_id, commit_identity_scheme, commit_id) in ids {
            result
                .entry(cs_id)
                .or_insert_with(BTreeMap::new)
                .insert(commit_identity_scheme, commit_id);
        }
    }
    Ok(result)
}

/// Trait to extend CommitId with useful functions.
pub(crate) trait CommitIdExt {
    fn scheme(&self) -> thrift::CommitIdentityScheme;
    fn to_string(&self) -> String;
}

impl CommitIdExt for thrift::CommitId {
    /// Returns the commit identity scheme of a commit ID.
    fn scheme(&self) -> thrift::CommitIdentityScheme {
        match self {
            thrift::CommitId::bonsai(_) => thrift::CommitIdentityScheme::BONSAI,
            thrift::CommitId::ephemeral_bonsai(_) => thrift::CommitIdentityScheme::EPHEMERAL_BONSAI,
            thrift::CommitId::hg(_) => thrift::CommitIdentityScheme::HG,
            thrift::CommitId::git(_) => thrift::CommitIdentityScheme::GIT,
            thrift::CommitId::globalrev(_) => thrift::CommitIdentityScheme::GLOBALREV,
            thrift::CommitId::svnrev(_) => thrift::CommitIdentityScheme::SVNREV,
            thrift::CommitId::UnknownField(t) => (*t).into(),
        }
    }

    /// Convert a `thrift::CommitId` to a string for display. This would normally
    /// be implemented as `Display for thrift::CommitId`, but it is defined in
    /// the generated crate.
    fn to_string(&self) -> String {
        match self {
            thrift::CommitId::bonsai(id) => hex_string(id),
            thrift::CommitId::ephemeral_bonsai(ephemeral) => format!(
                "{} (bubble {})",
                hex_string(&ephemeral.bonsai_id),
                ephemeral.bubble_id
            ),
            thrift::CommitId::hg(id) => hex_string(id),
            thrift::CommitId::git(id) => hex_string(id),
            thrift::CommitId::globalrev(rev) => rev.to_string(),
            thrift::CommitId::svnrev(rev) => rev.to_string(),
            thrift::CommitId::UnknownField(t) => format!("unknown id type ({})", t),
        }
    }
}


/*
 * 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::backtrace::BacktraceStatus;
use std::error::Error as StdError;

use megarepo_error::MegarepoError;
use mononoke_api::repo::git::GitError;
use mononoke_api::MononokeError;
use source_control as thrift;
use source_control::services::source_control_service as service;

pub(crate) enum ServiceError {
    Request(thrift::RequestError),
    Internal(thrift::InternalError),
}

impl From<thrift::RequestError> for ServiceError {
    fn from(e: thrift::RequestError) -> Self {
        Self::Request(e)
    }
}

impl From<thrift::InternalError> for ServiceError {
    fn from(e: thrift::InternalError) -> Self {
        Self::Internal(e)
    }
}

#[derive(Clone, Copy)]
pub(crate) enum Status {
    RequestError,
    InternalError,
}

/// Error can be logged to SCS scuba table
pub(crate) trait LoggableError {
    fn status_and_description(&self) -> (Status, String);
}

impl LoggableError for ServiceError {
    fn status_and_description(&self) -> (Status, String) {
        match self {
            Self::Request(err) => (Status::RequestError, format!("{:?}", err)),
            Self::Internal(err) => (Status::InternalError, format!("{:?}", err)),
        }
    }
}

impl ServiceError {
    pub fn context(self, context: &str) -> Self {
        match self {
            Self::Request(thrift::RequestError { kind, reason, .. }) => {
                let reason = format!("{}: {}", context, reason);
                Self::Request(thrift::RequestError {
                    kind,
                    reason,
                    ..Default::default()
                })
            }
            Self::Internal(thrift::InternalError {
                reason,
                backtrace,
                source_chain,
                ..
            }) => {
                let reason = format!("{}: {}", context, reason);
                Self::Internal(thrift::InternalError {
                    reason,
                    backtrace,
                    source_chain,
                    ..Default::default()
                })
            }
        }
    }
}

pub(crate) trait ServiceErrorResultExt<T> {
    fn context(self, context: &str) -> Result<T, ServiceError>;
    fn with_context(self, context_fn: impl FnOnce() -> String) -> Result<T, ServiceError>;
}

impl<T, E> ServiceErrorResultExt<T> for Result<T, E>
where
    E: Into<ServiceError>,
{
    fn context(self, context: &str) -> Result<T, ServiceError> {
        match self {
            Ok(v) => Ok(v),
            Err(e) => Err(e.into().context(context)),
        }
    }

    fn with_context(self, context_fn: impl FnOnce() -> String) -> Result<T, ServiceError> {
        match self {
            Ok(v) => Ok(v),
            Err(e) => Err(e.into().context(context_fn().as_str())),
        }
    }
}

impl From<MegarepoError> for ServiceError {
    fn from(e: MegarepoError) -> Self {
        match e {
            MegarepoError::RequestError(e) => Self::Request(thrift::RequestError {
                kind: thrift::RequestErrorKind::INVALID_REQUEST,
                reason: format!("{}", e),
                ..Default::default()
            }),
            MegarepoError::InternalError(error) => {
                let reason = error.to_string();
                let backtrace = match error.backtrace().status() {
                    BacktraceStatus::Captured => Some(error.backtrace().to_string()),
                    _ => None,
                };
                let mut source_chain = Vec::new();
                let mut error: &dyn StdError = &error;
                while let Some(source) = error.source() {
                    source_chain.push(source.to_string());
                    error = source;
                }

                Self::Internal(thrift::InternalError {
                    reason,
                    backtrace,
                    source_chain,
                    ..Default::default()
                })
            }
        }
    }
}

impl From<MononokeError> for ServiceError {
    fn from(e: MononokeError) -> Self {
        match e {
            MononokeError::InvalidRequest(reason) => Self::Request(thrift::RequestError {
                kind: thrift::RequestErrorKind::INVALID_REQUEST,
                reason,
                ..Default::default()
            }),
            error @ MononokeError::MergeConflicts { .. } => Self::Request(thrift::RequestError {
                kind: thrift::RequestErrorKind::MERGE_CONFLICTS,
                reason: error.to_string(),
                ..Default::default()
            }),
            error @ MononokeError::ServicePermissionDenied { .. } => {
                Self::Request(thrift::RequestError {
                    kind: thrift::RequestErrorKind::PERMISSION_DENIED,
                    reason: error.to_string(),
                    ..Default::default()
                })
            }
            error @ MononokeError::AuthorizationError(_) => Self::Request(thrift::RequestError {
                kind: thrift::RequestErrorKind::PERMISSION_DENIED,
                reason: error.to_string(),
                ..Default::default()
            }),
            error @ MononokeError::NotAvailable(_) => Self::Request(thrift::RequestError {
                kind: thrift::RequestErrorKind::NOT_AVAILABLE,
                reason: error.to_string(),
                ..Default::default()
            }),
            error @ MononokeError::HookFailure(_) => Self::Request(thrift::RequestError {
                kind: thrift::RequestErrorKind::INVALID_REQUEST,
                reason: error.to_string(),
                ..Default::default()
            }),
            error @ MononokeError::PushrebaseConflicts(_) => Self::Request(thrift::RequestError {
                kind: thrift::RequestErrorKind::INVALID_REQUEST,
                reason: error.to_string(),
                ..Default::default()
            }),
            MononokeError::InternalError(error) => {
                let reason = format!("{:#}", error);
                let backtrace = match error.backtrace().status() {
                    BacktraceStatus::Captured => Some(error.backtrace().to_string()),
                    _ => None,
                };
                let mut source_chain = Vec::new();
                let mut error: &dyn StdError = &error;
                while let Some(source) = error.source() {
                    source_chain.push(source.to_string());
                    error = source;
                }
                Self::Internal(thrift::InternalError {
                    reason,
                    backtrace,
                    source_chain,
                    ..Default::default()
                })
            }
        }
    }
}

macro_rules! impl_into_thrift_error {
    ($t:ty) => {
        impl From<ServiceError> for $t {
            fn from(e: ServiceError) -> Self {
                match e {
                    ServiceError::Request(e) => e.into(),
                    ServiceError::Internal(e) => e.into(),
                }
            }
        }
    };
}

impl_into_thrift_error!(service::ListReposExn);
impl_into_thrift_error!(service::RepoInfoExn);
impl_into_thrift_error!(service::RepoResolveBookmarkExn);
impl_into_thrift_error!(service::RepoResolveCommitPrefixExn);
impl_into_thrift_error!(service::RepoListBookmarksExn);
impl_into_thrift_error!(service::RepoCreateCommitExn);
impl_into_thrift_error!(service::RepoCreateStackExn);
impl_into_thrift_error!(service::RepoCreateBookmarkExn);
impl_into_thrift_error!(service::RepoMoveBookmarkExn);
impl_into_thrift_error!(service::RepoDeleteBookmarkExn);
impl_into_thrift_error!(service::RepoLandStackExn);
impl_into_thrift_error!(service::RepoBookmarkInfoExn);
impl_into_thrift_error!(service::RepoStackInfoExn);
impl_into_thrift_error!(service::RepoPrepareCommitsExn);
impl_into_thrift_error!(service::RepoUploadFileContentExn);
impl_into_thrift_error!(service::CommitCommonBaseWithExn);
impl_into_thrift_error!(service::CommitFileDiffsExn);
impl_into_thrift_error!(service::CommitLookupExn);
impl_into_thrift_error!(service::CommitLookupPushrebaseHistoryExn);
impl_into_thrift_error!(service::CommitInfoExn);
impl_into_thrift_error!(service::CommitCompareExn);
impl_into_thrift_error!(service::CommitIsAncestorOfExn);
impl_into_thrift_error!(service::CommitFindFilesExn);
impl_into_thrift_error!(service::CommitHistoryExn);
impl_into_thrift_error!(service::CommitListDescendantBookmarksExn);
impl_into_thrift_error!(service::CommitRunHooksExn);
impl_into_thrift_error!(service::CommitPathExistsExn);
impl_into_thrift_error!(service::CommitPathInfoExn);
impl_into_thrift_error!(service::CommitMultiplePathInfoExn);
impl_into_thrift_error!(service::CommitPathBlameExn);
impl_into_thrift_error!(service::CommitPathHistoryExn);
impl_into_thrift_error!(service::CommitPathLastChangedExn);
impl_into_thrift_error!(service::CommitMultiplePathLastChangedExn);
impl_into_thrift_error!(service::CommitSparseProfileDeltaExn);
impl_into_thrift_error!(service::CommitSparseProfileSizeExn);
impl_into_thrift_error!(service::TreeExistsExn);
impl_into_thrift_error!(service::TreeListExn);
impl_into_thrift_error!(service::FileExistsExn);
impl_into_thrift_error!(service::FileInfoExn);
impl_into_thrift_error!(service::FileContentChunkExn);
impl_into_thrift_error!(service::FileDiffExn);
impl_into_thrift_error!(service::CommitLookupXrepoExn);
impl_into_thrift_error!(service::MegarepoAddSyncTargetConfigExn);
impl_into_thrift_error!(service::MegarepoReadTargetConfigExn);
impl_into_thrift_error!(service::MegarepoAddSyncTargetExn);
impl_into_thrift_error!(service::MegarepoAddSyncTargetPollExn);
impl_into_thrift_error!(service::MegarepoAddBranchingSyncTargetExn);
impl_into_thrift_error!(service::MegarepoAddBranchingSyncTargetPollExn);
impl_into_thrift_error!(service::MegarepoChangeTargetConfigExn);
impl_into_thrift_error!(service::MegarepoChangeTargetConfigPollExn);
impl_into_thrift_error!(service::MegarepoSyncChangesetExn);
impl_into_thrift_error!(service::MegarepoSyncChangesetPollExn);
impl_into_thrift_error!(service::MegarepoRemergeSourceExn);
impl_into_thrift_error!(service::MegarepoRemergeSourcePollExn);
impl_into_thrift_error!(service::UploadGitObjectExn);
impl_into_thrift_error!(service::CreateGitTreeExn);
impl_into_thrift_error!(service::CreateGitTagExn);

pub(crate) fn invalid_request(reason: impl ToString) -> thrift::RequestError {
    thrift::RequestError {
        kind: thrift::RequestErrorKind::INVALID_REQUEST,
        reason: reason.to_string(),
        ..Default::default()
    }
}

pub(crate) fn internal_error(error: impl ToString) -> thrift::InternalError {
    thrift::InternalError {
        reason: error.to_string(),
        backtrace: None,
        source_chain: Vec::new(),
        ..Default::default()
    }
}

pub(crate) fn repo_not_found(repo: String) -> thrift::RequestError {
    thrift::RequestError {
        kind: thrift::RequestErrorKind::REPO_NOT_FOUND,
        reason: format!("repo not found ({})", repo),
        ..Default::default()
    }
}

pub(crate) fn commit_not_found(commit: String) -> thrift::RequestError {
    thrift::RequestError {
        kind: thrift::RequestErrorKind::COMMIT_NOT_FOUND,
        reason: format!("commit not found ({})", commit),
        ..Default::default()
    }
}

pub(crate) fn file_not_found(file: String) -> thrift::RequestError {
    thrift::RequestError {
        kind: thrift::RequestErrorKind::FILE_NOT_FOUND,
        reason: format!("file not found ({})", file),
        ..Default::default()
    }
}

pub(crate) fn tree_not_found(tree: String) -> thrift::RequestError {
    thrift::RequestError {
        kind: thrift::RequestErrorKind::TREE_NOT_FOUND,
        reason: format!("tree not found ({})", tree),
        ..Default::default()
    }
}

pub(crate) fn limit_too_low(limit: usize) -> thrift::RequestError {
    thrift::RequestError {
        kind: thrift::RequestErrorKind::INVALID_REQUEST,
        reason: format!(
            "the limit param value of {} is not enough for the method to make any progress",
            limit,
        ),
        ..Default::default()
    }
}

pub(crate) fn diff_input_too_big(total_size: u64) -> thrift::RequestError {
    thrift::RequestError {
        kind: thrift::RequestErrorKind::INVALID_REQUEST_INPUT_TOO_BIG,
        reason: format!(
            "only {} bytes of files (in total) can be diffed in one request, you asked for {} bytes",
            thrift::consts::COMMIT_FILE_DIFFS_SIZE_LIMIT,
            total_size,
        ),
        ..Default::default()
    }
}

pub(crate) fn diff_input_too_many_paths(path_count: usize) -> thrift::RequestError {
    thrift::RequestError {
        kind: thrift::RequestErrorKind::INVALID_REQUEST_TOO_MANY_PATHS,
        reason: format!(
            "only at most {} paths can be diffed in one request, you asked for {}",
            thrift::consts::COMMIT_FILE_DIFFS_PATH_COUNT_LIMIT,
            path_count,
        ),
        ..Default::default()
    }
}

pub(crate) fn not_available(reason: String) -> thrift::RequestError {
    thrift::RequestError {
        kind: thrift::RequestErrorKind::NOT_AVAILABLE,
        reason,
        ..Default::default()
    }
}

#[allow(unused)]
pub(crate) fn not_implemented(reason: String) -> thrift::RequestError {
    thrift::RequestError {
        kind: thrift::RequestErrorKind::NOT_IMPLEMENTED,
        reason,
        ..Default::default()
    }
}

impl From<GitError> for ServiceError {
    fn from(error: GitError) -> Self {
        match error {
            // Storage failure is a internal error with system generated error message.
            // Convert it into thrift::InternalError before sending across service boundary.
            GitError::StorageFailure(reason, error) => {
                let backtrace = match error.backtrace().status() {
                    BacktraceStatus::Captured => Some(error.backtrace().to_string()),
                    _ => None,
                };
                let mut source_chain = Vec::new();
                let mut error: &dyn StdError = &error;
                while let Some(source) = error.source() {
                    source_chain.push(source.to_string());
                    error = source;
                }
                Self::Internal(thrift::InternalError {
                    reason,
                    backtrace,
                    source_chain,
                    ..Default::default()
                })
            }
            // All other kind of errors associated with git operations are a result of
            // invalid / bad user input. Categorize them under INVALID_REQUEST and include
            // the actual cause in the reason string.
            _ => Self::Request(thrift::RequestError {
                kind: thrift::RequestErrorKind::INVALID_REQUEST,
                reason: error.to_string(),
                ..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 std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
use std::sync::Arc;

use async_trait::async_trait;
use fb303_core::fb303_status;
use fb303_core::server::BaseService;
use fb303_core::services::base_service::GetNameExn;
use fb303_core::services::base_service::GetStatusDetailsExn;
use fb303_core::services::base_service::GetStatusExn;

#[derive(Clone)]
pub struct BaseServiceImpl {
    will_exit: Arc<AtomicBool>,
}

impl BaseServiceImpl {
    pub fn new(will_exit: Arc<AtomicBool>) -> Self {
        Self { will_exit }
    }
}

#[async_trait]
impl BaseService for BaseServiceImpl {
    async fn getName(&self) -> Result<String, GetNameExn> {
        Ok("Mononoke Source Control Service Server".to_string())
    }

    async fn getStatus(&self) -> Result<fb303_status, GetStatusExn> {
        if !self.will_exit.load(Ordering::Relaxed) {
            Ok(fb303_status::ALIVE)
        } else {
            Ok(fb303_status::STOPPING)
        }
    }

    async fn getStatusDetails(&self) -> Result<String, GetStatusDetailsExn> {
        if !self.will_exit.load(Ordering::Relaxed) {
            Ok("Alive and running.".to_string())
        } else {
            Ok("Shutting down.".to_string())
        }
    }
}


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

use std::collections::BTreeMap;
use std::collections::HashMap;
use std::fmt::Debug;
use std::fmt::Display;
use std::ops::RangeBounds;
use std::str::FromStr;

use bookmarks_movement::BookmarkKindRestrictions;
use bytes::Bytes;
use chrono::DateTime;
use chrono::FixedOffset;
use chrono::Local;
use chrono::TimeZone;
use derived_data_manager::DerivableType;
use ephemeral_blobstore::BubbleId;
use faster_hex::hex_string;
use hooks::CrossRepoPushSource;
use mononoke_api::specifiers::GitSha1;
use mononoke_api::specifiers::GitSha1Prefix;
use mononoke_api::specifiers::Globalrev;
use mononoke_api::specifiers::Svnrev;
use mononoke_api::BookmarkKey;
use mononoke_api::CandidateSelectionHintArgs;
use mononoke_api::ChangesetId;
use mononoke_api::ChangesetIdPrefix;
use mononoke_api::ChangesetPrefixSpecifier;
use mononoke_api::ChangesetSpecifier;
use mononoke_api::CopyInfo;
use mononoke_api::CreateCopyInfo;
use mononoke_api::CreateInfo;
use mononoke_api::FileId;
use mononoke_api::FileType;
use mononoke_api::HgChangesetId;
use mononoke_api::HgChangesetIdPrefix;
use mononoke_api::MononokePath;
use mononoke_api::TreeId;
use mononoke_types::hash::Sha1;
use mononoke_types::hash::Sha256;
use source_control as thrift;

use crate::commit_id::CommitIdExt;
use crate::errors;

/// Convert an item from a thrift request to the internal type.
pub(crate) trait FromRequest<T: ?Sized> {
    fn from_request(t: &T) -> Result<Self, thrift::RequestError>
    where
        Self: Sized;
}

impl FromRequest<str> for BookmarkKey {
    fn from_request(bookmark: &str) -> Result<BookmarkKey, thrift::RequestError> {
        BookmarkKey::new(bookmark).map_err(|e| {
            errors::invalid_request(format!(
                "failed parsing bookmark out of {}: {:?}",
                bookmark, e
            ))
        })
    }
}

impl FromRequest<thrift::CrossRepoPushSource> for CrossRepoPushSource {
    fn from_request(
        push_source: &thrift::CrossRepoPushSource,
    ) -> Result<Self, thrift::RequestError> {
        match push_source {
            &thrift::CrossRepoPushSource::NATIVE_TO_THIS_REPO => Ok(Self::NativeToThisRepo),
            &thrift::CrossRepoPushSource::PUSH_REDIRECTED => Ok(Self::PushRedirected),
            other => Err(errors::invalid_request(format!(
                "Unknown CrossRepoPushSource: {}",
                other
            ))),
        }
    }
}

impl FromRequest<thrift::BookmarkKindRestrictions> for BookmarkKindRestrictions {
    fn from_request(
        push_source: &thrift::BookmarkKindRestrictions,
    ) -> Result<Self, thrift::RequestError> {
        match push_source {
            &thrift::BookmarkKindRestrictions::ANY_KIND => Ok(Self::AnyKind),
            &thrift::BookmarkKindRestrictions::ONLY_SCRATCH => Ok(Self::OnlyScratch),
            &thrift::BookmarkKindRestrictions::ONLY_PUBLISHING => Ok(Self::OnlyPublishing),
            other => Err(errors::invalid_request(format!(
                "Unknown BookmarkKindRestrictions: {}",
                other
            ))),
        }
    }
}

impl FromRequest<thrift::CandidateSelectionHint> for CandidateSelectionHintArgs {
    fn from_request(hint: &thrift::CandidateSelectionHint) -> Result<Self, thrift::RequestError> {
        match hint {
            thrift::CandidateSelectionHint::bookmark_ancestor(bookmark) => {
                let bookmark = BookmarkKey::from_request(bookmark)?;
                Ok(CandidateSelectionHintArgs::OnlyOrAncestorOfBookmark(
                    bookmark,
                ))
            }
            thrift::CandidateSelectionHint::bookmark_descendant(bookmark) => {
                let bookmark = BookmarkKey::from_request(bookmark)?;
                Ok(CandidateSelectionHintArgs::OnlyOrDescendantOfBookmark(
                    bookmark,
                ))
            }
            thrift::CandidateSelectionHint::commit_ancestor(commit) => {
                let changeset_specifier = ChangesetSpecifier::from_request(commit)?;
                Ok(CandidateSelectionHintArgs::OnlyOrAncestorOfCommit(
                    changeset_specifier,
                ))
            }
            thrift::CandidateSelectionHint::commit_descendant(commit) => {
                let changeset_specifier = ChangesetSpecifier::from_request(commit)?;
                Ok(CandidateSelectionHintArgs::OnlyOrDescendantOfCommit(
                    changeset_specifier,
                ))
            }
            thrift::CandidateSelectionHint::exact(commit) => {
                let changeset_specifier = ChangesetSpecifier::from_request(commit)?;
                Ok(CandidateSelectionHintArgs::Exact(changeset_specifier))
            }
            thrift::CandidateSelectionHint::UnknownField(f) => Err(errors::invalid_request(
                format!("unsupported candidate selection hint: {:?}", f),
            )),
        }
    }
}

impl FromRequest<thrift::CommitId> for ChangesetSpecifier {
    fn from_request(commit: &thrift::CommitId) -> Result<Self, thrift::RequestError> {
        match commit {
            thrift::CommitId::bonsai(id) => {
                let cs_id = ChangesetId::from_bytes(id).map_err(|e| {
                    errors::invalid_request(format!(
                        "invalid commit id (scheme={} {}): {}",
                        commit.scheme(),
                        commit.to_string(),
                        e
                    ))
                })?;
                Ok(ChangesetSpecifier::Bonsai(cs_id))
            }
            thrift::CommitId::hg(id) => {
                let hg_cs_id = HgChangesetId::from_bytes(id).map_err(|e| {
                    errors::invalid_request(format!(
                        "invalid commit id (scheme={} {}): {}",
                        commit.scheme(),
                        commit.to_string(),
                        e
                    ))
                })?;
                Ok(ChangesetSpecifier::Hg(hg_cs_id))
            }
            thrift::CommitId::globalrev(rev) => {
                let rev = Globalrev::new((*rev).try_into().map_err(|_| {
                    errors::invalid_request(format!("cannot parse globalrev {} to u64", rev))
                })?);
                Ok(ChangesetSpecifier::Globalrev(rev))
            }
            thrift::CommitId::git(git_sha1) => {
                let git_sha1 = GitSha1::from_bytes(git_sha1).map_err(|e| {
                    errors::invalid_request(format!(
                        "invalid commit id (scheme={} {}): {}",
                        commit.scheme(),
                        commit.to_string(),
                        e
                    ))
                })?;
                Ok(ChangesetSpecifier::GitSha1(git_sha1))
            }
            thrift::CommitId::svnrev(rev) => {
                let rev = Svnrev::new((*rev).try_into().map_err(|_| {
                    errors::invalid_request(format!("cannot parse svn revision {} to u64", rev))
                })?);
                Ok(ChangesetSpecifier::Svnrev(rev))
            }
            thrift::CommitId::ephemeral_bonsai(ephemeral) => {
                let cs_id = ChangesetId::from_bytes(&ephemeral.bonsai_id).map_err(|e| {
                    errors::invalid_request(format!(
                        "invalid commit id (scheme={} {}): {}",
                        commit.scheme(),
                        commit.to_string(),
                        e
                    ))
                })?;
                let bubble_id = if ephemeral.bubble_id == 0 {
                    None
                } else {
                    Some(BubbleId::try_from(ephemeral.bubble_id).map_err(|_| {
                        errors::invalid_request(format!(
                            "invalid bubble id {}",
                            ephemeral.bubble_id
                        ))
                    })?)
                };
                Ok(ChangesetSpecifier::EphemeralBonsai(cs_id, bubble_id))
            }
            thrift::CommitId::UnknownField(_) => Err(errors::invalid_request(format!(
                "unsupported commit identity scheme ({})",
                commit.scheme()
            ))),
        }
    }
}

impl FromRequest<thrift::CopyInfo> for CopyInfo {
    fn from_request(copy_info: &thrift::CopyInfo) -> Result<Self, thrift::RequestError> {
        match *copy_info {
            thrift::CopyInfo::NONE => Ok(CopyInfo::None),
            thrift::CopyInfo::COPY => Ok(CopyInfo::Copy),
            thrift::CopyInfo::MOVE => Ok(CopyInfo::Move),
            val => Err(errors::invalid_request(format!(
                "unsupported copy info ({})",
                val
            ))),
        }
    }
}

impl FromRequest<thrift::RepoResolveCommitPrefixParams> for ChangesetPrefixSpecifier {
    fn from_request(
        params: &thrift::RepoResolveCommitPrefixParams,
    ) -> Result<Self, thrift::RequestError> {
        match params.prefix_scheme {
            thrift::CommitIdentityScheme::HG => {
                let prefix = HgChangesetIdPrefix::from_str(&params.prefix).map_err(|e| {
                    errors::invalid_request(format!(
                        "invalid commit id prefix (scheme={} {}): {}",
                        params.prefix_scheme, params.prefix, e
                    ))
                })?;
                Ok(ChangesetPrefixSpecifier::from(prefix))
            }
            thrift::CommitIdentityScheme::GIT => {
                let prefix = GitSha1Prefix::from_str(&params.prefix).map_err(|e| {
                    errors::invalid_request(format!(
                        "invalid commit id prefix (scheme={} {}): {}",
                        params.prefix_scheme, params.prefix, e
                    ))
                })?;
                Ok(ChangesetPrefixSpecifier::from(prefix))
            }
            thrift::CommitIdentityScheme::BONSAI => {
                let prefix = ChangesetIdPrefix::from_str(&params.prefix).map_err(|e| {
                    errors::invalid_request(format!(
                        "invalid commit id prefix (scheme={} {}): {}",
                        params.prefix_scheme, params.prefix, e
                    ))
                })?;
                Ok(ChangesetPrefixSpecifier::from(prefix))
            }
            thrift::CommitIdentityScheme::GLOBALREV => {
                let rev = params.prefix.parse().map_err(|e| {
                    errors::invalid_request(format!(
                        "invalid commit id prefix (scheme={} {}): {}",
                        params.prefix_scheme, params.prefix, e
                    ))
                })?;
                Ok(ChangesetPrefixSpecifier::from(Globalrev::new(rev)))
            }
            _ => Err(errors::invalid_request(format!(
                "unsupported prefix identity scheme ({})",
                params.prefix_scheme
            ))),
        }
    }
}

macro_rules! impl_from_request_binary_id(
    ($t:ty, $name:expr) => {
        impl FromRequest<Vec<u8>> for $t {
            fn from_request(id: &Vec<u8>) -> Result<Self, thrift::RequestError> {
                <$t>::from_bytes(id).map_err(|e| {
                    errors::invalid_request(format!(
                        "invalid {} ({}): {}",
                        $name,
                        hex_string(&id),
                        e,
                    ))})
            }
        }
    }
);

impl_from_request_binary_id!(TreeId, "tree id");
impl_from_request_binary_id!(FileId, "file id");
impl_from_request_binary_id!(Sha1, "sha-1");
impl_from_request_binary_id!(Sha256, "sha-256");
impl_from_request_binary_id!(GitSha1, "git-sha-1");

impl FromRequest<thrift::RepoCreateCommitParamsFileType> for FileType {
    fn from_request(
        file_type: &thrift::RepoCreateCommitParamsFileType,
    ) -> Result<Self, thrift::RequestError> {
        match *file_type {
            thrift::RepoCreateCommitParamsFileType::FILE => Ok(FileType::Regular),
            thrift::RepoCreateCommitParamsFileType::EXEC => Ok(FileType::Executable),
            thrift::RepoCreateCommitParamsFileType::LINK => Ok(FileType::Symlink),
            val => Err(errors::invalid_request(format!(
                "unsupported file type ({})",
                val
            ))),
        }
    }
}

impl FromRequest<thrift::RepoCreateCommitParamsFileCopyInfo> for CreateCopyInfo {
    fn from_request(
        copy_info: &thrift::RepoCreateCommitParamsFileCopyInfo,
    ) -> Result<Self, thrift::RequestError> {
        let path = MononokePath::try_from(&copy_info.path).map_err(|e| {
            errors::invalid_request(format!(
                "invalid copy-from path '{}': {}",
                copy_info.path, e
            ))
        })?;
        let parent_index = usize::try_from(copy_info.parent_index).map_err(|e| {
            errors::invalid_request(format!(
                "invalid copy-from parent index '{}': {}",
                copy_info.parent_index, e
            ))
        })?;
        Ok(CreateCopyInfo::new(path, parent_index))
    }
}

impl FromRequest<thrift::RepoCreateCommitParamsCommitInfo> for CreateInfo {
    fn from_request(
        info: &thrift::RepoCreateCommitParamsCommitInfo,
    ) -> Result<Self, thrift::RequestError> {
        let author = info.author.clone();
        let author_date = info.date.as_ref().map_or_else(
            || {
                let now = Local::now();
                Ok(now.with_timezone(now.offset()))
            },
            <DateTime<FixedOffset>>::from_request,
        )?;
        let committer = info.committer.clone();
        let committer_date = info
            .committer_date
            .as_ref()
            .map(<DateTime<FixedOffset>>::from_request)
            .transpose()?;
        let message = info.message.clone();
        let extra = info.extra.clone();
        let git_extra_headers = info.git_extra_headers.as_ref().map(|headers| {
            headers
                .iter()
                .map(|(k, v)| (k.0.clone(), v.clone()))
                .collect()
        });

        Ok(CreateInfo {
            author,
            author_date,
            committer,
            committer_date,
            message,
            extra,
            git_extra_headers,
        })
    }
}

impl FromRequest<thrift::DateTime> for DateTime<FixedOffset> {
    fn from_request(date: &thrift::DateTime) -> Result<Self, thrift::RequestError> {
        Ok(FixedOffset::east_opt(date.tz)
            .unwrap()
            .timestamp_opt(date.timestamp, 0)
            .unwrap())
    }
}

impl FromRequest<thrift::DerivedDataType> for DerivableType {
    fn from_request(data_type: &thrift::DerivedDataType) -> Result<Self, thrift::RequestError> {
        match *data_type {
            thrift::DerivedDataType::FSNODE | thrift::DerivedDataType::FSNODE_NEW => {
                Ok(DerivableType::Fsnodes)
            }
            thrift::DerivedDataType::SKELETON_MANIFEST => Ok(DerivableType::SkeletonManifests),
            val => Err(errors::invalid_request(format!(
                "unsupported derived data type ({})",
                val
            ))),
        }
    }
}

/// Check that an input value is in range for the request, and convert it to
/// the internal type.  Returns a invalid request error if the number was out
/// of range, and an internal error if the conversion failed.
pub(crate) fn check_range_and_convert<F, T, B>(
    name: &'static str,
    value: F,
    range: B,
) -> Result<T, errors::ServiceError>
where
    F: Copy + Display + PartialOrd,
    T: TryFrom<F>,
    B: Debug + RangeBounds<F>,
    <T as TryFrom<F>>::Error: Display,
{
    if range.contains(&value) {
        T::try_from(value).map_err(|e| {
            let msg = format!("failed to convert {} ({}): {}", name, value, e);
            errors::internal_error(msg).into()
        })
    } else {
        let msg = format!("{} ({}) out of range ({:?})", name, value, range);
        Err(errors::invalid_request(msg).into())
    }
}

pub(crate) fn validate_timestamp(
    ts: Option<i64>,
    name: &str,
) -> Result<Option<i64>, errors::ServiceError> {
    match ts {
        None | Some(0) => Ok(None),
        Some(ts) if ts < 0 => {
            Err(errors::invalid_request(format!("{} ({}) cannot be negative", name, ts)).into())
        }
        Some(ts) => Ok(Some(ts)),
    }
}

/// Convert a pushvars map from thrift's representation to the one used
/// internally in mononoke.
pub(crate) fn convert_pushvars(
    pushvars: Option<BTreeMap<String, Vec<u8>>>,
) -> Option<HashMap<String, Bytes>> {
    pushvars.map(|pushvars| {
        pushvars
            .into_iter()
            .map(|(name, value)| (name, Bytes::from(value)))
            .collect()
    })
}


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

use std::collections::BTreeMap;
use std::collections::BTreeSet;

use futures::stream;
use futures::stream::Stream;
use futures::stream::StreamExt;
use futures::stream::TryStreamExt;
use mononoke_api::ChangesetContext;
use mononoke_api::MononokeError;
use source_control as thrift;

use crate::errors;
use crate::into_response::AsyncIntoResponseWith;

pub(crate) async fn collect_history(
    history_stream: impl Stream<Item = Result<ChangesetContext, MononokeError>>,
    skip: usize,
    limit: usize,
    before_timestamp: Option<i64>,
    after_timestamp: Option<i64>,
    format: thrift::HistoryFormat,
    identity_schemes: &BTreeSet<thrift::CommitIdentityScheme>,
) -> Result<thrift::History, errors::ServiceError> {
    let history_stream = history_stream
        .map_err(errors::ServiceError::from)
        .skip(skip);

    let history = if before_timestamp.is_some() || after_timestamp.is_some() {
        history_stream
            .map(move |changeset| async move {
                let changeset = changeset?;
                if after_timestamp.is_some() || before_timestamp.is_some() {
                    let date = changeset.author_date().await?;

                    if let Some(after) = after_timestamp {
                        if after > date.timestamp() {
                            return Ok(None);
                        }
                    }
                    if let Some(before) = before_timestamp {
                        if before < date.timestamp() {
                            return Ok(None);
                        }
                    }
                }

                Ok(Some(changeset))
            })
            // to check the date we need to fetch changeset first, that can be expensive
            // better to try doing it in parallel
            .buffered(100)
            .try_filter_map(|maybe_changeset| async move {
                Ok::<_, errors::ServiceError>(maybe_changeset)
            })
            .take(limit)
            .left_stream()
    } else {
        history_stream.take(limit).right_stream()
    };

    match format {
        thrift::HistoryFormat::COMMIT_INFO => {
            let commit_infos: Vec<_> = history
                .map(|changeset| async {
                    match changeset {
                        Ok(cs) => cs.into_response_with(identity_schemes).await,
                        Err(err) => Err(err),
                    }
                })
                .buffered(100)
                .try_collect()
                .await?;
            Ok(thrift::History::commit_infos(commit_infos))
        }
        thrift::HistoryFormat::COMMIT_ID => {
            let identity_schemes = identity_schemes.clone();
            let commit_ids: Vec<BTreeMap<thrift::CommitIdentityScheme, thrift::CommitId>> = history
                .chunks(100)
                // TryStreamExt doesn't have the try_chunks method yet so we have to do it by mapping
                .map(|chunk| chunk.into_iter().collect::<Result<Vec<_>, _>>())
                .and_then(move |changesets: Vec<ChangesetContext>| {
                    let identity_schemes = identity_schemes.clone();
                    async move {
                        Ok(stream::iter(
                            changesets
                                .into_response_with(&identity_schemes)
                                .await?
                                .into_iter()
                                .map(Ok::<_, errors::ServiceError>)
                                .collect::<Vec<_>>(),
                        ))
                    }
                })
                .try_flatten()
                .try_collect()
                .await?;
            Ok(thrift::History::commit_ids(commit_ids))
        }
        other_format => Err(errors::invalid_request(format!(
            "unsupported history format {}",
            other_format
        ))
        .into()),
    }
}


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

use std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::collections::HashSet;

use async_trait::async_trait;
use futures::future::try_join_all;
use futures::try_join;
use itertools::Itertools;
use maplit::btreemap;
use mononoke_api::BookmarkInfo;
use mononoke_api::ChangesetContext;
use mononoke_api::ChangesetId;
use mononoke_api::ChangesetPathContentContext;
use mononoke_api::CopyInfo;
use mononoke_api::FileContentType;
use mononoke_api::FileGeneratedStatus;
use mononoke_api::FileMetadata;
use mononoke_api::FileType;
use mononoke_api::HeaderlessUnifiedDiff;
use mononoke_api::MetadataDiff;
use mononoke_api::MetadataDiffFileInfo;
use mononoke_api::MetadataDiffLinesCount;
use mononoke_api::MononokeError;
use mononoke_api::PushrebaseOutcome;
use mononoke_api::RepoContext;
use mononoke_api::TreeEntry;
use mononoke_api::TreeId;
use mononoke_api::TreeSummary;
use mononoke_api::UnifiedDiff;
use source_control as thrift;

use crate::commit_id::map_commit_identities;
use crate::commit_id::map_commit_identity;
use crate::errors;

/// Convert an item into a thrift type suitable for inclusion in a thrift
/// response.
pub(crate) trait IntoResponse<T> {
    fn into_response(self) -> T;
}

/// Asynchronously convert an item into a thrift type suitable for inclusion
/// in a thrift response.
#[async_trait]
pub(crate) trait AsyncIntoResponse<T> {
    async fn into_response(self) -> Result<T, errors::ServiceError>;
}

/// Asynchronously convert an item into a thrift type suitable for inclusion
/// in a thrift response, with additional data required for the conversion.
#[async_trait]
pub(crate) trait AsyncIntoResponseWith<T> {
    /// The type of additional data that must be provided to convert this
    /// value into a response value.
    type Additional;

    async fn into_response_with(
        self,
        additional: &Self::Additional,
    ) -> Result<T, errors::ServiceError>;
}

#[async_trait]
impl<T, A: AsyncIntoResponse<T> + Send> AsyncIntoResponse<Option<T>> for Option<A> {
    async fn into_response(self) -> Result<Option<T>, errors::ServiceError> {
        match self {
            Some(value) => Ok(Some(value.into_response().await?)),
            None => Ok(None),
        }
    }
}

impl IntoResponse<thrift::EntryType> for FileType {
    fn into_response(self) -> thrift::EntryType {
        match self {
            FileType::Regular => thrift::EntryType::FILE,
            FileType::Executable => thrift::EntryType::EXEC,
            FileType::Symlink => thrift::EntryType::LINK,
        }
    }
}

impl IntoResponse<Option<thrift::MetadataDiffFileType>> for Option<FileType> {
    fn into_response(self) -> Option<thrift::MetadataDiffFileType> {
        match self {
            None => None,
            Some(FileType::Regular) => Some(thrift::MetadataDiffFileType::FILE),
            Some(FileType::Executable) => Some(thrift::MetadataDiffFileType::EXEC),
            Some(FileType::Symlink) => Some(thrift::MetadataDiffFileType::LINK),
        }
    }
}

impl IntoResponse<thrift::TreeEntry> for (String, TreeEntry) {
    fn into_response(self) -> thrift::TreeEntry {
        let (name, entry) = self;
        let (r#type, info) = match entry {
            TreeEntry::Directory(dir) => {
                let summary = dir.summary();
                let info = thrift::TreeInfo {
                    id: dir.id().as_ref().to_vec(),
                    simple_format_sha1: summary.simple_format_sha1.as_ref().to_vec(),
                    simple_format_sha256: summary.simple_format_sha256.as_ref().to_vec(),
                    child_files_count: summary.child_files_count as i64,
                    child_files_total_size: summary.child_files_total_size as i64,
                    child_dirs_count: summary.child_dirs_count as i64,
                    descendant_files_count: summary.descendant_files_count as i64,
                    descendant_files_total_size: summary.descendant_files_total_size as i64,
                    ..Default::default()
                };
                (thrift::EntryType::TREE, thrift::EntryInfo::tree(info))
            }
            TreeEntry::File(file) => {
                let info = thrift::FileInfo {
                    id: file.content_id().as_ref().to_vec(),
                    file_size: file.size() as i64,
                    content_sha1: file.content_sha1().as_ref().to_vec(),
                    content_sha256: file.content_sha256().as_ref().to_vec(),
                    ..Default::default()
                };
                (
                    file.file_type().into_response(),
                    thrift::EntryInfo::file(info),
                )
            }
        };
        thrift::TreeEntry {
            name,
            r#type,
            info,
            ..Default::default()
        }
    }
}

impl IntoResponse<thrift::FileInfo> for FileMetadata {
    fn into_response(self) -> thrift::FileInfo {
        thrift::FileInfo {
            id: self.content_id.as_ref().to_vec(),
            file_size: self.total_size as i64,
            content_sha1: self.sha1.as_ref().to_vec(),
            content_sha256: self.sha256.as_ref().to_vec(),
            content_git_sha1: self.git_sha1.as_ref().to_vec(),
            content_seeded_blake3: self.seeded_blake3.as_ref().to_vec(),
            is_binary: self.is_binary,
            is_ascii: self.is_ascii,
            is_utf8: self.is_utf8,
            ends_in_newline: self.ends_in_newline,
            newline_count: self.newline_count as i64,
            first_line: self.first_line,
            is_generated: self.is_generated,
            is_partially_generated: self.is_partially_generated,
            ..Default::default()
        }
    }
}

impl IntoResponse<thrift::TreeInfo> for (TreeId, TreeSummary) {
    fn into_response(self) -> thrift::TreeInfo {
        let (id, summary) = self;
        thrift::TreeInfo {
            id: id.as_ref().to_vec(),
            simple_format_sha1: summary.simple_format_sha1.as_ref().to_vec(),
            simple_format_sha256: summary.simple_format_sha256.as_ref().to_vec(),
            child_files_count: summary.child_files_count as i64,
            child_files_total_size: summary.child_files_total_size as i64,
            child_dirs_count: summary.child_dirs_count as i64,
            descendant_files_count: summary.descendant_files_count as i64,
            descendant_files_total_size: summary.descendant_files_total_size as i64,
            ..Default::default()
        }
    }
}

impl IntoResponse<thrift::CopyInfo> for CopyInfo {
    fn into_response(self) -> thrift::CopyInfo {
        match self {
            CopyInfo::None => thrift::CopyInfo::NONE,
            CopyInfo::Copy => thrift::CopyInfo::COPY,
            CopyInfo::Move => thrift::CopyInfo::MOVE,
        }
    }
}

impl IntoResponse<Option<thrift::MetadataDiffFileContentType>> for Option<FileContentType> {
    fn into_response(self) -> Option<thrift::MetadataDiffFileContentType> {
        match self {
            None => None,
            Some(FileContentType::Text) => Some(thrift::MetadataDiffFileContentType::TEXT),
            Some(FileContentType::NonUtf8) => Some(thrift::MetadataDiffFileContentType::NON_UTF8),
            Some(FileContentType::Binary) => Some(thrift::MetadataDiffFileContentType::BINARY),
        }
    }
}

impl IntoResponse<Option<thrift::FileGeneratedStatus>> for Option<FileGeneratedStatus> {
    fn into_response(self) -> Option<thrift::FileGeneratedStatus> {
        match self {
            None => None,
            Some(FileGeneratedStatus::FullyGenerated) => {
                Some(thrift::FileGeneratedStatus::FULLY_GENERATED)
            }
            Some(FileGeneratedStatus::PartiallyGenerated) => {
                Some(thrift::FileGeneratedStatus::PARTIALLY_GENERATED)
            }
            Some(FileGeneratedStatus::NotGenerated) => {
                Some(thrift::FileGeneratedStatus::NOT_GENERATED)
            }
        }
    }
}

impl IntoResponse<thrift::MetadataDiffFileInfo> for MetadataDiffFileInfo {
    fn into_response(self) -> thrift::MetadataDiffFileInfo {
        thrift::MetadataDiffFileInfo {
            file_type: self.file_type.into_response(),
            file_content_type: self.file_content_type.into_response(),
            file_generated_status: self.file_generated_status.into_response(),
            ..Default::default()
        }
    }
}

impl IntoResponse<Option<thrift::MetadataDiffLinesCount>> for Option<MetadataDiffLinesCount> {
    fn into_response(self) -> Option<thrift::MetadataDiffLinesCount> {
        self.map(|lines_count| thrift::MetadataDiffLinesCount {
            added_lines_count: lines_count.added_lines_count as i64,
            deleted_lines_count: lines_count.deleted_lines_count as i64,
            significant_added_lines_count: lines_count.significant_added_lines_count as i64,
            significant_deleted_lines_count: lines_count.significant_deleted_lines_count as i64,
            first_added_line_number: lines_count
                .first_added_line_number
                .map(|line_number| line_number as i64),
            ..Default::default()
        })
    }
}

impl IntoResponse<thrift::Diff> for MetadataDiff {
    fn into_response(self) -> thrift::Diff {
        thrift::Diff::metadata_diff(thrift::MetadataDiff {
            old_file_info: self.old_file_info.into_response(),
            new_file_info: self.new_file_info.into_response(),
            lines_count: self.lines_count.into_response(),
            ..Default::default()
        })
    }
}

impl IntoResponse<thrift::Diff> for UnifiedDiff {
    fn into_response(self) -> thrift::Diff {
        thrift::Diff::raw_diff(thrift::RawDiff {
            raw_diff: Some(self.raw_diff),
            is_binary: self.is_binary,
            ..Default::default()
        })
    }
}

impl IntoResponse<thrift::Diff> for HeaderlessUnifiedDiff {
    fn into_response(self) -> thrift::Diff {
        thrift::Diff::raw_diff(thrift::RawDiff {
            raw_diff: Some(self.raw_diff),
            is_binary: self.is_binary,
            ..Default::default()
        })
    }
}

#[async_trait]
impl AsyncIntoResponse<thrift::FilePathInfo> for &ChangesetPathContentContext {
    async fn into_response(self) -> Result<thrift::FilePathInfo, errors::ServiceError> {
        let (meta, type_) = try_join!(
            async {
                Ok::<_, errors::ServiceError>(
                    self.file()
                        .await?
                        .ok_or_else(|| errors::internal_error("programming error: not a file"))?
                        .metadata()
                        .await?,
                )
            },
            async {
                Ok::<_, errors::ServiceError>(
                    self.file_type()
                        .await?
                        .ok_or_else(|| errors::internal_error("programming error: not a file"))?,
                )
            },
        )?;
        Ok(thrift::FilePathInfo {
            path: self.path().to_string(),
            r#type: type_.into_response(),
            info: meta.into_response(),
            ..Default::default()
        })
    }
}

#[async_trait]
impl AsyncIntoResponse<thrift::TreePathInfo> for &ChangesetPathContentContext {
    async fn into_response(self) -> Result<thrift::TreePathInfo, errors::ServiceError> {
        let tree = self
            .tree()
            .await?
            .ok_or_else(|| errors::internal_error("programming error: not a tree"))?;

        let summary = (tree.id().clone(), tree.summary().await?);
        Ok(thrift::TreePathInfo {
            path: self.path().to_string(),
            info: summary.into_response(),
            ..Default::default()
        })
    }
}

#[async_trait]
impl AsyncIntoResponseWith<thrift::CommitInfo> for ChangesetContext {
    /// The additional data is the set of commit identity schemes to be
    /// returned in the response.
    type Additional = BTreeSet<thrift::CommitIdentityScheme>;

    async fn into_response_with(
        self,
        identity_schemes: &BTreeSet<thrift::CommitIdentityScheme>,
    ) -> Result<thrift::CommitInfo, errors::ServiceError> {
        async fn map_parent_identities(
            changeset: &ChangesetContext,
            identity_schemes: &BTreeSet<thrift::CommitIdentityScheme>,
        ) -> Result<Vec<BTreeMap<thrift::CommitIdentityScheme, thrift::CommitId>>, MononokeError>
        {
            let parents = changeset.parents().await?;
            let parent_id_mapping =
                map_commit_identities(changeset.repo(), parents.clone(), identity_schemes).await?;
            Ok(parents
                .iter()
                .map(|parent_id| {
                    parent_id_mapping
                        .get(parent_id)
                        .cloned()
                        .unwrap_or_default()
                })
                .collect())
        }

        let (ids, message, date, author, parents, hg_extra, git_extra_headers, generation) = try_join!(
            map_commit_identity(&self, identity_schemes),
            self.message(),
            self.author_date(),
            self.author(),
            map_parent_identities(&self, identity_schemes),
            self.hg_extras(),
            self.git_extra_headers(),
            self.generation(),
        )?;
        Ok(thrift::CommitInfo {
            ids,
            message,
            date: date.timestamp(),
            tz: date.offset().local_minus_utc(),
            author,
            parents,
            extra: hg_extra.into_iter().collect(),
            git_extra_headers: git_extra_headers.map(|headers| {
                headers
                    .into_iter()
                    .map(|(k, v)| (thrift::small_binary(k), v))
                    .collect()
            }),
            generation: generation.value() as i64,
            ..Default::default()
        })
    }
}

#[async_trait]
impl AsyncIntoResponseWith<Vec<BTreeMap<thrift::CommitIdentityScheme, thrift::CommitId>>>
    for Vec<ChangesetContext>
{
    /// The additional data is the set of commit identity schemes to be
    /// returned in the response.
    type Additional = BTreeSet<thrift::CommitIdentityScheme>;

    async fn into_response_with(
        self,
        identity_schemes: &BTreeSet<thrift::CommitIdentityScheme>,
    ) -> Result<Vec<BTreeMap<thrift::CommitIdentityScheme, thrift::CommitId>>, errors::ServiceError>
    {
        let res = try_join_all({
            let changesets_grouped_by_repo = self
                .into_iter()
                .map(|c| c.into_repo_and_id())
                .into_group_map();

            changesets_grouped_by_repo
                .into_iter()
                .map(|(repo, changesets)| async move {
                    let id_map =
                        map_commit_identities(&repo, changesets.clone(), identity_schemes).await?;

                    changesets
                        .iter()
                        .map(move |id| {
                            id_map.get(id).cloned().ok_or_else(|| {
                                errors::internal_error(
                                    "programming error, id is missing from the map",
                                )
                                .into()
                            })
                        })
                        .collect::<Result<
                            Vec<BTreeMap<thrift::CommitIdentityScheme, thrift::CommitId>>,
                            errors::ServiceError,
                        >>()
                })
        })
        .await?
        .into_iter()
        .flatten()
        .collect();
        Ok(res)
    }
}

fn to_i64(val: usize) -> Result<i64, errors::ServiceError> {
    val.try_into()
        .map_err(|_| errors::internal_error("usize too big for i64").into())
}

#[async_trait]
impl AsyncIntoResponseWith<thrift::PushrebaseOutcome> for PushrebaseOutcome {
    /// The additional data is the repo context, the set of commit identity
    /// schemes to be returned in the response, and optionally a different set
    /// of commit identity schemes to use for the old commit ids.
    type Additional = (
        RepoContext,
        BTreeSet<thrift::CommitIdentityScheme>,
        Option<BTreeSet<thrift::CommitIdentityScheme>>,
    );

    async fn into_response_with(
        self,
        additional: &Self::Additional,
    ) -> Result<thrift::PushrebaseOutcome, errors::ServiceError> {
        let (repo, identity_schemes, old_identity_schemes) = additional;
        let mut new_ids = HashSet::new();
        let mut old_ids = HashSet::new();
        new_ids.insert(self.head);
        for rebase in self.rebased_changesets.iter() {
            old_ids.insert(rebase.id_old);
            new_ids.insert(rebase.id_new);
        }
        let old_identity_schemes = old_identity_schemes.as_ref().unwrap_or(identity_schemes);
        let (old_id_map, new_id_map) = try_join!(
            map_commit_identities(repo, old_ids.into_iter().collect(), old_identity_schemes),
            map_commit_identities(repo, new_ids.into_iter().collect(), identity_schemes),
        )?;

        // Map IDs using one of the maps we just fetched.  If we couldn't
        // perform the look-up then just return the bonsai ID only.
        fn try_get(
            map: &BTreeMap<ChangesetId, BTreeMap<thrift::CommitIdentityScheme, thrift::CommitId>>,
            cs_id: ChangesetId,
        ) -> BTreeMap<thrift::CommitIdentityScheme, thrift::CommitId> {
            match map.get(&cs_id) {
                Some(ids) => ids.clone(),
                None => btreemap! {
                    thrift::CommitIdentityScheme::BONSAI =>
                        thrift::CommitId::bonsai(cs_id.as_ref().into()),
                },
            }
        }

        let old_bookmark_value = self
            .old_bookmark_value
            .map_or_else(BTreeMap::new, |old_bookmark_value| {
                try_get(&new_id_map, old_bookmark_value)
            });
        let head = try_get(&new_id_map, self.head);
        let rebased_commits: Vec<_> = self
            .rebased_changesets
            .iter()
            .map(|rebase| {
                let old_ids = try_get(&old_id_map, rebase.id_old);
                let new_ids = try_get(&new_id_map, rebase.id_new);
                thrift::PushrebaseRebasedCommit {
                    old_ids,
                    new_ids,
                    ..Default::default()
                }
            })
            .collect();

        Ok(thrift::PushrebaseOutcome {
            old_bookmark_value,
            head,
            rebased_commits,
            pushrebase_distance: to_i64(self.pushrebase_distance.0)?,
            retry_num: to_i64(self.retry_num.0)?,
            ..Default::default()
        })
    }
}

#[async_trait]
impl AsyncIntoResponseWith<thrift::BookmarkInfo> for BookmarkInfo {
    /// The additional data is the set of commit identity schemes to be
    /// returned in the response.
    type Additional = BTreeSet<thrift::CommitIdentityScheme>;

    async fn into_response_with(
        self,
        identity_schemes: &BTreeSet<thrift::CommitIdentityScheme>,
    ) -> Result<thrift::BookmarkInfo, errors::ServiceError> {
        let (warm_ids, fresh_ids) = try_join!(
            map_commit_identity(&self.warm_changeset, identity_schemes),
            map_commit_identity(&self.fresh_changeset, identity_schemes),
        )?;
        Ok(thrift::BookmarkInfo {
            warm_ids,
            fresh_ids,
            last_update_timestamp_ns: self.last_update_timestamp.timestamp_nanos(),
            ..Default::default()
        })
    }
}