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

use bytes::Bytes;
use context::CoreContext;
use futures::stream;
use futures::stream::FuturesOrdered;
use futures::stream::StreamExt;
use futures::stream::TryStreamExt;
use futures::try_join;
use hooks::HookExecution;
use hooks::HookOutcome;
use itertools::Either;
use itertools::Itertools;
use maplit::btreeset;
use mononoke_api::CandidateSelectionHintArgs;
use mononoke_api::ChangesetContext;
use mononoke_api::ChangesetDiffItem;
use mononoke_api::ChangesetFileOrdering;
use mononoke_api::ChangesetHistoryOptions;
use mononoke_api::ChangesetId;
use mononoke_api::ChangesetPathContentContext;
use mononoke_api::ChangesetPathDiffContext;
use mononoke_api::ChangesetSpecifier;
use mononoke_api::CopyInfo;
use mononoke_api::MetadataDiff;
use mononoke_api::MononokeError;
use mononoke_api::MononokePath;
use mononoke_api::RepoContext;
use mononoke_api::UnifiedDiff;
use mononoke_api::UnifiedDiffMode;
use source_control as thrift;

use crate::commit_id::map_commit_identities;
use crate::commit_id::map_commit_identity;
use crate::errors;
use crate::errors::ServiceErrorResultExt;
use crate::from_request::check_range_and_convert;
use crate::from_request::validate_timestamp;
use crate::from_request::FromRequest;
use crate::history::collect_history;
use crate::into_response::AsyncIntoResponse;
use crate::into_response::AsyncIntoResponseWith;
use crate::into_response::IntoResponse;
use crate::source_control_impl::SourceControlServiceImpl;

// Magic number used when we want to limit concurrency with buffer_unordered.
const CONCURRENCY_LIMIT: usize = 100;

enum CommitComparePath {
    File(thrift::CommitCompareFile),
    Tree(thrift::CommitCompareTree),
}

impl CommitComparePath {
    /// The main path that this comparison applies to.
    fn path(&self) -> Result<&str, errors::ServiceError> {
        // Use the base path where available.  If it is not available, then
        // this is a deletion and the other path should be used.
        match self {
            CommitComparePath::File(file) => file
                .base_file
                .as_ref()
                .or(file.other_file.as_ref())
                .map(|file| file.path.as_str())
                .ok_or_else(|| {
                    errors::internal_error("programming error, file entry has no file").into()
                }),

            CommitComparePath::Tree(tree) => tree
                .base_tree
                .as_ref()
                .or(tree.other_tree.as_ref())
                .map(|tree| tree.path.as_str())
                .ok_or_else(|| {
                    errors::internal_error("programming error, tree entry has no tree").into()
                }),
        }
    }

    async fn from_path_diff(
        path_diff: ChangesetPathDiffContext,
    ) -> Result<Self, errors::ServiceError> {
        if path_diff.path().is_file().await? {
            let (base_file, other_file) = try_join!(
                path_diff.base().into_response(),
                path_diff.other().into_response()
            )?;
            let copy_info = path_diff.copy_info().into_response();
            Ok(CommitComparePath::File(thrift::CommitCompareFile {
                base_file,
                other_file,
                copy_info,
                ..Default::default()
            }))
        } else {
            let (base_tree, other_tree) = try_join!(
                path_diff.base().into_response(),
                path_diff.other().into_response()
            )?;
            Ok(CommitComparePath::Tree(thrift::CommitCompareTree {
                base_tree,
                other_tree,
                ..Default::default()
            }))
        }
    }
}

/// Helper for commit_compare to add mutable rename information if appropriate
async fn add_mutable_renames(
    base_changeset: &mut ChangesetContext,
    params: &thrift::CommitCompareParams,
) -> Result<(), errors::ServiceError> {
    if params.follow_mutable_file_history.unwrap_or(false) {
        if let Some(paths) = &params.paths {
            let paths: Vec<_> = paths
                .iter()
                .map(MononokePath::try_from)
                .collect::<Result<_, MononokeError>>()?;
            base_changeset
                .add_mutable_renames(paths.into_iter())
                .await?;
        }
    }
    Ok(())
}

struct CommitFileDiffsItem {
    path_diff_context: ChangesetPathDiffContext,
    placeholder: bool,
}

impl CommitFileDiffsItem {
    fn to_stopped_at_pair(&self) -> thrift::CommitFileDiffsStoppedAtPair {
        thrift::CommitFileDiffsStoppedAtPair {
            base_path: self.path_diff_context.base().map(|p| p.path().to_string()),
            other_path: self.path_diff_context.other().map(|p| p.path().to_string()),
            ..Default::default()
        }
    }

    async fn total_size(&self) -> Result<u64, errors::ServiceError> {
        if self.placeholder {
            Ok(0)
        } else {
            async fn file_size(
                path: Option<&ChangesetPathContentContext>,
            ) -> Result<u64, errors::ServiceError> {
                if let Some(path) = path {
                    if let Some(file) = path.file().await? {
                        return Ok(file.metadata().await?.total_size);
                    }
                }
                Ok(0)
            }
            let (base_size, other_size) = try_join!(
                file_size(self.path_diff_context.base()),
                file_size(self.path_diff_context.other())
            )?;
            Ok(base_size.saturating_add(other_size))
        }
    }

    async fn response_element(
        &self,
        format: thrift::DiffFormat,
        context_lines: usize,
    ) -> Result<CommitFileDiffsResponseElement, errors::ServiceError> {
        match format {
            thrift::DiffFormat::RAW_DIFF => self.raw_diff(context_lines).await,
            thrift::DiffFormat::METADATA_DIFF => self.metadata_diff().await,
            unknown => {
                Err(errors::invalid_request(format!("invalid diff format: {:?}", unknown)).into())
            }
        }
    }

    async fn raw_diff(
        &self,
        context_lines: usize,
    ) -> Result<CommitFileDiffsResponseElement, errors::ServiceError> {
        let mode = if self.placeholder {
            UnifiedDiffMode::OmitContent
        } else {
            UnifiedDiffMode::Inline
        };
        let diff = self
            .path_diff_context
            .unified_diff(context_lines, mode)
            .await?;
        Ok(CommitFileDiffsResponseElement::RawDiff { diff })
    }

    async fn metadata_diff(&self) -> Result<CommitFileDiffsResponseElement, errors::ServiceError> {
        let metadata_diff = self.path_diff_context.metadata_diff().await?;
        Ok(CommitFileDiffsResponseElement::MetadataDiff { metadata_diff })
    }
}

enum CommitFileDiffsResponseElement {
    RawDiff { diff: UnifiedDiff },
    MetadataDiff { metadata_diff: MetadataDiff },
}

impl CommitFileDiffsResponseElement {
    fn size(&self) -> usize {
        match self {
            Self::RawDiff { diff } => diff.raw_diff.len(),
            Self::MetadataDiff { .. } => 1,
        }
    }

    fn into_response_for_item(
        self,
        item: &CommitFileDiffsItem,
    ) -> thrift::CommitFileDiffsResponseElement {
        match self {
            Self::RawDiff { diff } => thrift::CommitFileDiffsResponseElement {
                base_path: item.path_diff_context.base().map(|p| p.path().to_string()),
                other_path: item.path_diff_context.other().map(|p| p.path().to_string()),
                diff: diff.into_response(),
                ..Default::default()
            },
            Self::MetadataDiff { metadata_diff } => thrift::CommitFileDiffsResponseElement {
                base_path: item.path_diff_context.base().map(|p| p.path().to_string()),
                other_path: item.path_diff_context.other().map(|p| p.path().to_string()),
                diff: metadata_diff.into_response(),
                ..Default::default()
            },
        }
    }
}

impl SourceControlServiceImpl {
    /// Returns the lowest common ancestor of two commits.
    ///
    /// In case of ambiguity (can happen with multiple merges of the same branches) returns the
    /// common ancestor with lowest id out of those with highest generation number.
    pub(crate) async fn commit_common_base_with(
        &self,
        ctx: CoreContext,
        commit: thrift::CommitSpecifier,
        params: thrift::CommitCommonBaseWithParams,
    ) -> Result<thrift::CommitLookupResponse, errors::ServiceError> {
        let (_repo, changeset, other_changeset) = self
            .repo_changeset_pair(ctx, &commit, &params.other_commit_id)
            .await?;
        let lca = changeset.common_base_with(other_changeset.id()).await?;
        Ok(thrift::CommitLookupResponse {
            exists: lca.is_some(),
            ids: if let Some(lca) = lca {
                Some(map_commit_identity(&lca, &params.identity_schemes).await?)
            } else {
                None
            },
            ..Default::default()
        })
    }

    /// Look up commit.
    pub(crate) async fn commit_lookup(
        &self,
        ctx: CoreContext,
        commit: thrift::CommitSpecifier,
        params: thrift::CommitLookupParams,
    ) -> Result<thrift::CommitLookupResponse, errors::ServiceError> {
        let repo = self.repo(ctx, &commit.repo).await?;
        match repo
            .changeset(ChangesetSpecifier::from_request(&commit.id)?)
            .await?
        {
            Some(cs) => {
                let ids = map_commit_identity(&cs, &params.identity_schemes).await?;
                Ok(thrift::CommitLookupResponse {
                    exists: true,
                    ids: Some(ids),
                    ..Default::default()
                })
            }
            None => Ok(thrift::CommitLookupResponse {
                exists: false,
                ids: None,
                ..Default::default()
            }),
        }
    }

    /// Get diff.
    pub(crate) async fn commit_file_diffs(
        &self,
        ctx: CoreContext,
        commit: thrift::CommitSpecifier,
        params: thrift::CommitFileDiffsParams,
    ) -> Result<thrift::CommitFileDiffsResponse, errors::ServiceError> {
        // Check the path count limit
        if params.paths.len() as i64 > thrift::consts::COMMIT_FILE_DIFFS_PATH_COUNT_LIMIT {
            Err(errors::diff_input_too_many_paths(params.paths.len()))?;
        }

        // Resolve the CommitSpecfier into ChangesetContext
        let (base_commit, other_commit) = match params.other_commit_id {
            Some(other_commit_id) => {
                let (_repo, base_commit, other_commit) = self
                    .repo_changeset_pair(ctx, &commit, &other_commit_id)
                    .await?;
                (base_commit, Some(other_commit))
            }
            None => {
                let (_repo, base_commit) = self.repo_changeset(ctx, &commit).await?;
                (base_commit, None)
            }
        };

        // Resolve the paths into ChangesetPathContentContext
        // To make it more efficient we do a batch request
        // to resolve all paths into path contexts
        let mut base_commit_paths = vec![];
        let mut other_commit_paths = vec![];
        let paths = params
            .paths
            .into_iter()
            .map(|path_pair| {
                Ok((
                    match path_pair.base_path {
                        Some(path) => {
                            let mpath = MononokePath::try_from(&path)
                                .context("invalid base commit path")?;
                            base_commit_paths.push(mpath.clone());
                            Some(mpath)
                        }
                        None => None,
                    },
                    match &other_commit {
                        Some(_other_commit) => match path_pair.other_path {
                            Some(path) => {
                                let mpath = MononokePath::try_from(&path)
                                    .context("invalid other commit path")?;
                                other_commit_paths.push(mpath.clone());
                                Some(mpath)
                            }
                            None => None,
                        },
                        None => None,
                    },
                    CopyInfo::from_request(&path_pair.copy_info)?,
                    path_pair.generate_placeholder_diff.unwrap_or(false),
                ))
            })
            .collect::<Result<Vec<_>, errors::ServiceError>>()?;

        let (base_commit_contexts, other_commit_contexts) = try_join!(
            async {
                let base_commit_paths = base_commit
                    .paths_with_content(base_commit_paths.into_iter())
                    .await?;
                let base_commit_contexts = base_commit_paths
                    .map_ok(|path_context| (path_context.path().clone(), path_context))
                    .try_collect::<HashMap<_, _>>()
                    .await?;
                Ok::<_, MononokeError>(base_commit_contexts)
            },
            async {
                match &other_commit {
                    None => Ok(None),
                    Some(other_commit) => {
                        let other_commit_paths = other_commit
                            .paths_with_content(other_commit_paths.into_iter())
                            .await?;
                        let other_commit_contexts = other_commit_paths
                            .map_ok(|path_context| (path_context.path().clone(), path_context))
                            .try_collect::<HashMap<_, _>>()
                            .await?;
                        Ok::<_, MononokeError>(Some(other_commit_contexts))
                    }
                }
            }
        )?;

        let items = paths
            .into_iter()
            .map(|(base_path, other_path, copy_info, placeholder)| {
                let base_path = match base_path {
                    Some(base_path) => {
                        Some(base_commit_contexts.get(&base_path).ok_or_else(|| {
                            errors::invalid_request(format!(
                                "{} not found in {:?}",
                                base_path, commit
                            ))
                        })?)
                    }
                    None => None,
                };

                let other_path = match other_path {
                    Some(other_path) => match &other_commit_contexts {
                        Some(other_commit_contexts) => {
                            Some(other_commit_contexts.get(&other_path).ok_or_else(|| {
                                errors::invalid_request(format!(
                                    "{} not found in {:?}",
                                    other_path, other_commit
                                ))
                            })?)
                        }
                        None => None,
                    },
                    None => None,
                };

                let path_diff_context = ChangesetPathDiffContext::new(
                    base_path.cloned(),
                    other_path.cloned(),
                    copy_info,
                )?;
                Ok(CommitFileDiffsItem {
                    path_diff_context,
                    placeholder,
                })
            })
            .collect::<Result<Vec<_>, errors::ServiceError>>()?;

        // Check the total file size limit
        let total_input_size = stream::iter(items.iter())
            .map(|item| item.total_size())
            .boxed() // Prevents compiler error
            .buffered(100)
            .try_fold(
                0u64,
                |acc, size| async move { Ok(acc.saturating_add(size)) },
            )
            .await?;

        if total_input_size > thrift::consts::COMMIT_FILE_DIFFS_SIZE_LIMIT as u64 {
            Err(errors::diff_input_too_big(total_input_size))?;
        }

        let context = check_range_and_convert("context", params.context, 0..)?;
        let diff_size_limit: Option<usize> = params
            .diff_size_limit
            .map(|limit| check_range_and_convert("diff_size_limit", limit, 0..))
            .transpose()?;
        let mut size_so_far = 0usize;
        let mut stopped_at_pair = None;

        let path_diffs = stream::iter(items)
            .map(|item| async move {
                let element = item.response_element(params.format, context).await?;
                Ok::<_, errors::ServiceError>((item, element))
            })
            .boxed() // Prevents compiler error
            .buffered(20)
            .try_take_while(|(item, element)| {
                let mut limit_reached = false;
                if let Some(diff_size_limit) = diff_size_limit {
                    size_so_far = size_so_far.saturating_add(element.size());
                    if size_so_far > diff_size_limit {
                        limit_reached = true;
                        stopped_at_pair = Some(item.to_stopped_at_pair());
                    }
                }
                async move { Ok(!limit_reached) }
            })
            .map_ok(|(item, element)| element.into_response_for_item(&item))
            .try_collect()
            .await?;

        Ok(thrift::CommitFileDiffsResponse {
            path_diffs,
            stopped_at_pair,
            ..Default::default()
        })
    }

    /// Get commit info.
    pub(crate) async fn commit_info(
        &self,
        ctx: CoreContext,
        commit: thrift::CommitSpecifier,
        params: thrift::CommitInfoParams,
    ) -> Result<thrift::CommitInfo, errors::ServiceError> {
        let (_repo, changeset) = self.repo_changeset(ctx, &commit).await?;
        changeset.into_response_with(&params.identity_schemes).await
    }

    /// Returns `true` if this commit is an ancestor of `other_commit`.
    pub(crate) async fn commit_is_ancestor_of(
        &self,
        ctx: CoreContext,
        commit: thrift::CommitSpecifier,
        params: thrift::CommitIsAncestorOfParams,
    ) -> Result<bool, errors::ServiceError> {
        let (_repo, changeset, other_changeset) = self
            .repo_changeset_pair(ctx, &commit, &params.descendant_commit_id)
            .await?;
        let is_ancestor_of = changeset.is_ancestor_of(other_changeset.id()).await?;
        Ok(is_ancestor_of)
    }

    /// Given a base changeset, find the "other" changeset from parent information
    /// including mutable history if appropriate
    ///
    /// This is entirely a heuristic to guess the "right" thing if the client
    /// doesn't provide an "other" changeset - errors would normally be fed back
    /// to a human and not handled automatically.
    async fn find_commit_compare_parent(
        &self,
        repo: &RepoContext,
        base_changeset: &mut ChangesetContext,
        params: &thrift::CommitCompareParams,
    ) -> Result<Option<ChangesetContext>, errors::ServiceError> {
        let commit_parents = base_changeset.parents().await?;
        let mut other_changeset_id = commit_parents.get(0).copied();

        if params.follow_mutable_file_history.unwrap_or(false) {
            let mutable_parents = base_changeset.mutable_parents();

            // If there are multiple choices to make, then bail - the user needs to be
            // clear to avoid the ambiguity
            if mutable_parents.len() > 1 {
                return Err(errors::invalid_request(
                    "multiple different mutable parents in supplied paths",
                )
                .into());
            }
            if let Some(Some(parent)) = mutable_parents.into_iter().next() {
                other_changeset_id = Some(parent);
            }
        }

        match other_changeset_id {
            None => Ok(None),
            Some(other_changeset_id) => {
                let other_changeset = repo
                    .changeset(ChangesetSpecifier::Bonsai(other_changeset_id))
                    .await?
                    .ok_or_else(|| errors::internal_error("other changeset is missing"))?;
                Ok(Some(other_changeset))
            }
        }
    }

    /// Diff two commits
    pub(crate) async fn commit_compare(
        &self,
        ctx: CoreContext,
        commit: thrift::CommitSpecifier,
        params: thrift::CommitCompareParams,
    ) -> Result<thrift::CommitCompareResponse, errors::ServiceError> {
        let (base_changeset, other_changeset) = match &params.other_commit_id {
            Some(id) => {
                let (_repo, mut base_changeset, other_changeset) =
                    self.repo_changeset_pair(ctx, &commit, id).await?;
                add_mutable_renames(&mut base_changeset, &params).await?;
                (base_changeset, Some(other_changeset))
            }
            None => {
                let (repo, mut base_changeset) = self.repo_changeset(ctx, &commit).await?;
                add_mutable_renames(&mut base_changeset, &params).await?;
                let other_changeset = self
                    .find_commit_compare_parent(&repo, &mut base_changeset, &params)
                    .await?;
                (base_changeset, other_changeset)
            }
        };

        let mut last_path = None;
        let mut diff_items: BTreeSet<_> = params
            .compare_items
            .into_iter()
            .filter_map(|item| match item {
                thrift::CommitCompareItem::FILES => Some(ChangesetDiffItem::FILES),
                thrift::CommitCompareItem::TREES => Some(ChangesetDiffItem::TREES),
                _ => None,
            })
            .collect();

        if diff_items.is_empty() {
            diff_items = btreeset! { ChangesetDiffItem::FILES };
        }

        let paths: Option<Vec<MononokePath>> = match params.paths {
            None => None,
            Some(paths) => Some(
                paths
                    .iter()
                    .map(|path| path.try_into())
                    .collect::<Result<Vec<_>, _>>()?,
            ),
        };
        let (diff_files, diff_trees) = match params.ordered_params {
            None => {
                let diff = match other_changeset {
                    Some(ref other_changeset) => {
                        base_changeset
                            .diff_unordered(
                                other_changeset,
                                !params.skip_copies_renames,
                                paths,
                                diff_items,
                            )
                            .await?
                    }
                    None => {
                        base_changeset
                            .diff_root_unordered(paths, diff_items)
                            .await?
                    }
                };
                stream::iter(diff)
                    .map(CommitComparePath::from_path_diff)
                    .buffer_unordered(CONCURRENCY_LIMIT)
                    .try_collect::<Vec<_>>()
                    .await?
                    .into_iter()
                    .partition_map(|diff| match diff {
                        CommitComparePath::File(entry) => Either::Left(entry),
                        CommitComparePath::Tree(entry) => Either::Right(entry),
                    })
            }
            Some(ordered_params) => {
                let limit: usize = check_range_and_convert(
                    "limit",
                    ordered_params.limit,
                    0..=source_control::COMMIT_COMPARE_ORDERED_MAX_LIMIT,
                )?;
                let after = ordered_params
                    .after_path
                    .map(|after| {
                        MononokePath::try_from(&after).map_err(|e| {
                            errors::invalid_request(format!(
                                "invalid continuation path '{}': {}",
                                after, e
                            ))
                        })
                    })
                    .transpose()?;
                let diff = match other_changeset {
                    Some(ref other_changeset) => {
                        base_changeset
                            .diff(
                                other_changeset,
                                !params.skip_copies_renames,
                                paths,
                                diff_items,
                                ChangesetFileOrdering::Ordered { after },
                                Some(limit),
                            )
                            .await?
                    }
                    None => {
                        base_changeset
                            .diff_root(
                                paths,
                                diff_items,
                                ChangesetFileOrdering::Ordered { after },
                                Some(limit),
                            )
                            .await?
                    }
                };
                let diff_items = diff
                    .into_iter()
                    .map(CommitComparePath::from_path_diff)
                    .collect::<FuturesOrdered<_>>()
                    .try_collect::<Vec<_>>()
                    .await?;
                if diff_items.len() >= limit {
                    if let Some(item) = diff_items.last() {
                        last_path = Some(item.path()?.to_string());
                    }
                }
                diff_items.into_iter().partition_map(|diff| match diff {
                    CommitComparePath::File(entry) => Either::Left(entry),
                    CommitComparePath::Tree(entry) => Either::Right(entry),
                })
            }
        };

        let other_commit_ids = match other_changeset {
            None => None,
            Some(other_changeset) => {
                Some(map_commit_identity(&other_changeset, &params.identity_schemes).await?)
            }
        };
        Ok(thrift::CommitCompareResponse {
            diff_files,
            diff_trees,
            other_commit_ids,
            last_path,
            ..Default::default()
        })
    }

    /// Returns files that match the criteria
    pub(crate) async fn commit_find_files(
        &self,
        ctx: CoreContext,
        commit: thrift::CommitSpecifier,
        params: thrift::CommitFindFilesParams,
    ) -> Result<thrift::CommitFindFilesResponse, errors::ServiceError> {
        let (_repo, changeset) = self.repo_changeset(ctx, &commit).await?;
        let limit: usize = check_range_and_convert(
            "limit",
            params.limit,
            0..=source_control::COMMIT_FIND_FILES_MAX_LIMIT,
        )?;
        let prefixes: Option<Vec<_>> = match params.prefixes {
            Some(prefixes) => Some(
                prefixes
                    .into_iter()
                    .map(|prefix| {
                        MononokePath::try_from(&prefix).map_err(|e| {
                            errors::invalid_request(format!("invalid prefix '{}': {}", prefix, e))
                        })
                    })
                    .collect::<Result<Vec<_>, _>>()?,
            ),
            None => None,
        };
        let ordering = match &params.after {
            Some(after) => {
                let after = Some(MononokePath::try_from(after).map_err(|e| {
                    errors::invalid_request(format!("invalid continuation path '{}': {}", after, e))
                })?);
                ChangesetFileOrdering::Ordered { after }
            }
            None => ChangesetFileOrdering::Unordered,
        };

        let files = changeset
            .find_files(
                prefixes,
                params.basenames,
                params.basename_suffixes,
                ordering,
            )
            .await?
            .take(limit)
            .map_ok(|path| path.to_string())
            .try_collect()
            .await?;
        Ok(thrift::CommitFindFilesResponse {
            files,
            ..Default::default()
        })
    }

    /// Returns the history of a commit
    pub(crate) async fn commit_history(
        &self,
        ctx: CoreContext,
        commit: thrift::CommitSpecifier,
        params: thrift::CommitHistoryParams,
    ) -> Result<thrift::CommitHistoryResponse, errors::ServiceError> {
        let (repo, changeset) = self.repo_changeset(ctx, &commit).await?;
        let (descendants_of, exclude_changeset_and_ancestors) = try_join!(
            async {
                if let Some(descendants_of) = &params.descendants_of {
                    Ok::<_, errors::ServiceError>(Some(
                        self.changeset_id(&repo, descendants_of).await?,
                    ))
                } else {
                    Ok(None)
                }
            },
            async {
                if let Some(exclude_changeset_and_ancestors) =
                    &params.exclude_changeset_and_ancestors
                {
                    Ok::<_, errors::ServiceError>(Some(
                        self.changeset_id(&repo, exclude_changeset_and_ancestors)
                            .await?,
                    ))
                } else {
                    Ok(None)
                }
            }
        )?;

        let limit: usize = check_range_and_convert("limit", params.limit, 0..)?;
        let skip: usize = check_range_and_convert("skip", params.skip, 0..)?;

        // Time filter equal to zero might be mistaken by users for an unset, like None.
        // We will consider negative timestamps as invalid and zeros as unset.
        let after_timestamp = validate_timestamp(params.after_timestamp, "after_timestamp")?;
        let before_timestamp = validate_timestamp(params.before_timestamp, "before_timestamp")?;

        if let (Some(ats), Some(bts)) = (after_timestamp, before_timestamp) {
            if bts < ats {
                return Err(errors::invalid_request(format!(
                    "after_timestamp ({}) cannot be greater than before_timestamp ({})",
                    ats, bts,
                ))
                .into());
            }
        }

        if skip > 0 && (after_timestamp.is_some() || before_timestamp.is_some()) {
            return Err(errors::invalid_request(
                "Time filters cannot be applied if skip is not 0".to_string(),
            )
            .into());
        }

        let history_stream = changeset
            .history(ChangesetHistoryOptions {
                until_timestamp: after_timestamp,
                descendants_of,
                exclude_changeset_and_ancestors,
            })
            .await?;
        let history = collect_history(
            history_stream,
            skip,
            limit,
            before_timestamp,
            after_timestamp,
            params.format,
            &params.identity_schemes,
        )
        .await?;

        Ok(thrift::CommitHistoryResponse {
            history,
            ..Default::default()
        })
    }

    pub(crate) async fn commit_list_descendant_bookmarks(
        &self,
        ctx: CoreContext,
        commit: thrift::CommitSpecifier,
        params: thrift::CommitListDescendantBookmarksParams,
    ) -> Result<thrift::CommitListDescendantBookmarksResponse, errors::ServiceError> {
        let limit = match check_range_and_convert(
            "limit",
            params.limit,
            0..=source_control::COMMIT_LIST_DESCENDANT_BOOKMARKS_MAX_LIMIT,
        )? {
            0 => None,
            limit => Some(limit),
        };
        let prefix = if !params.bookmark_prefix.is_empty() {
            Some(params.bookmark_prefix)
        } else {
            None
        };
        let (repo, changeset) = self.repo_changeset(ctx, &commit).await?;
        let bookmarks = repo
            .list_bookmarks(
                params.include_scratch,
                prefix.as_deref(),
                params.after.as_deref(),
                limit,
            )
            .await?
            .try_collect::<Vec<_>>()
            .await?;
        let continue_after = match limit {
            Some(limit) if bookmarks.len() as u64 >= limit => {
                bookmarks.last().map(|bookmark| bookmark.0.clone())
            }
            _ => None,
        };

        async fn filter_descendant(
            changeset: Arc<ChangesetContext>,
            bookmark: (String, ChangesetId),
        ) -> Result<Option<(String, ChangesetId)>, MononokeError> {
            if changeset.is_ancestor_of(bookmark.1).await? {
                Ok(Some(bookmark))
            } else {
                Ok(None)
            }
        }

        let bookmarks = stream::iter(bookmarks)
            .map({
                // Wrap `changeset` in `Arc` so that cloning it to send to
                // the tasks is cheap.
                let changeset = Arc::new(changeset);
                move |bookmark| {
                    let changeset = changeset.clone();
                    async move {
                        tokio::task::spawn(filter_descendant(changeset, bookmark))
                            .await
                            .map_err(anyhow::Error::from)?
                    }
                }
            })
            .buffered(20)
            .try_fold(Vec::new(), |mut bookmarks, maybe_bookmark| async move {
                if let Some(bookmark) = maybe_bookmark {
                    bookmarks.push(bookmark);
                }
                Ok(bookmarks)
            })
            .await?;

        let ids = bookmarks.iter().map(|(_name, cs_id)| *cs_id).collect();
        let id_mapping = map_commit_identities(&repo, ids, &params.identity_schemes).await?;

        let bookmarks = bookmarks
            .into_iter()
            .map(|(name, cs_id)| (name, id_mapping.get(&cs_id).cloned().unwrap_or_default()))
            .collect();

        Ok(thrift::CommitListDescendantBookmarksResponse {
            bookmarks,
            continue_after,
            ..Default::default()
        })
    }

    pub(crate) async fn commit_run_hooks(
        &self,
        ctx: CoreContext,
        commit: thrift::CommitSpecifier,
        params: thrift::CommitRunHooksParams,
    ) -> Result<thrift::CommitRunHooksResponse, errors::ServiceError> {
        let (_repo, changeset) = self.repo_changeset(ctx, &commit).await?;
        let pushvars: Option<HashMap<String, Bytes>> = params
            .pushvars
            .map(|p| p.into_iter().map(|(k, v)| (k, Bytes::from(v))).collect());
        let outcomes = changeset
            .run_hooks(params.bookmark, pushvars.as_ref())
            .await?;

        let mut outcomes_map = BTreeMap::new();

        for outcome in outcomes {
            let (name, execution) = match outcome {
                HookOutcome::FileHook(id, exec) => (id.hook_name, exec),
                HookOutcome::ChangesetHook(id, exec) => (id.hook_name, exec),
            };

            match execution {
                HookExecution::Accepted => {
                    outcomes_map.entry(name).or_insert_with(|| {
                        thrift::HookOutcome::accepted(thrift::HookOutcomeAccepted {
                            ..Default::default()
                        })
                    });
                }
                HookExecution::Rejected(rej) => {
                    let rejection = thrift::HookOutcomeRejected {
                        description: rej.description.to_string(),
                        long_description: rej.long_description,
                        ..Default::default()
                    };

                    match outcomes_map
                        .entry(name)
                        .or_insert_with(|| thrift::HookOutcome::rejections(vec![]))
                    {
                        thrift::HookOutcome::rejections(rejs) => rejs.push(rejection),
                        obj => *obj = thrift::HookOutcome::rejections(vec![rejection]),
                    }
                }
            }
        }

        Ok(thrift::CommitRunHooksResponse {
            outcomes: outcomes_map,
            ..Default::default()
        })
    }

    /// Do a cross-repo lookup to see if a commit exists under a different hash in another repo
    pub(crate) async fn commit_lookup_xrepo(
        &self,
        ctx: CoreContext,
        commit: thrift::CommitSpecifier,
        params: thrift::CommitLookupXRepoParams,
    ) -> Result<thrift::CommitLookupResponse, errors::ServiceError> {
        let repo = self.repo(ctx.clone(), &commit.repo).await?;
        let other_repo = self.repo(ctx, &params.other_repo).await?;
        let candidate_selection_hint = match params.candidate_selection_hint {
            Some(ref hint) => Some(CandidateSelectionHintArgs::from_request(hint)?),
            None => None,
        };

        match repo
            .xrepo_commit_lookup(
                &other_repo,
                ChangesetSpecifier::from_request(&commit.id)?,
                candidate_selection_hint,
            )
            .await?
        {
            Some(cs) => {
                let ids = map_commit_identity(&cs, &params.identity_schemes).await?;
                Ok(thrift::CommitLookupResponse {
                    exists: true,
                    ids: Some(ids),
                    ..Default::default()
                })
            }
            None => Ok(thrift::CommitLookupResponse {
                exists: false,
                ids: None,
                ..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::iter::once;
use std::sync::Arc;

use context::CoreContext;
use metaconfig_types::CommonCommitSyncConfig;
use mononoke_api::Mononoke;
use mononoke_api::RepoContext;
use mononoke_types::ChangesetId;
use phases::PhasesRef;
use pushrebase_mutation_mapping::PushrebaseMutationMappingRef;
use source_control as thrift;
use synced_commit_mapping::SyncedCommitSourceRepo;

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

#[derive(Debug, Clone)]
struct RepoChangeset(String, ChangesetId);

impl RepoChangeset {
    fn into_thrift(self) -> thrift::CommitSpecifier {
        let RepoChangeset(name, cs_id) = self;
        thrift::CommitSpecifier {
            repo: thrift::RepoSpecifier {
                name,
                ..Default::default()
            },
            id: thrift::CommitId::bonsai(cs_id.as_ref().to_vec()),
            ..Default::default()
        }
    }
}

// Struct that represents current pushrebase history. Provides a simple
// interface which allows extending the history by looking into different
// mappings.
struct RepoChangesetsPushrebaseHistory {
    ctx: CoreContext,
    mononoke: Arc<Mononoke>,
    head: RepoChangeset,
    changesets: Vec<RepoChangeset>,
}

impl RepoChangesetsPushrebaseHistory {
    fn new(
        ctx: CoreContext,
        mononoke: Arc<Mononoke>,
        repo_name: String,
        changeset: ChangesetId,
    ) -> Self {
        Self {
            ctx,
            mononoke,
            head: RepoChangeset(repo_name, changeset),
            changesets: vec![],
        }
    }

    fn last(&self) -> RepoChangeset {
        match self.changesets.last() {
            Some(rc) => rc.clone(),
            None => self.head.clone(),
        }
    }

    async fn repo(&self, repo_name: &String) -> Result<RepoContext, errors::ServiceError> {
        let repo = self
            .mononoke
            .repo(self.ctx.clone(), repo_name)
            .await?
            .ok_or_else(|| errors::repo_not_found(repo_name.clone()))?
            .build()
            .await?;
        Ok(repo)
    }

    async fn ensure_head_is_public(&self) -> Result<(), errors::ServiceError> {
        let RepoChangeset(repo_name, bcs_id) = &self.head;
        let repo = self.repo(repo_name).await?;
        let is_public = repo
            .blob_repo()
            .phases()
            .get_public(&self.ctx, vec![*bcs_id], true /* ephemeral_derive */)
            .await
            .map_err(errors::internal_error)?
            .contains(bcs_id);
        if !is_public {
            return Err(errors::invalid_request(format!(
                "changeset {} is not public, and only public commits could be pushrebased",
                bcs_id,
            ))
            .into());
        }
        Ok(())
    }

    async fn try_traverse_pushrebase(&mut self) -> Result<bool, errors::ServiceError> {
        let RepoChangeset(repo_name, bcs_id) = self.last();
        let repo = self.repo(&repo_name).await?;
        let bcs_ids = repo
            .blob_repo()
            .pushrebase_mutation_mapping()
            .get_prepushrebase_ids(&self.ctx, bcs_id)
            .await
            .map_err(errors::internal_error)?;
        let mut iter = bcs_ids.iter();
        match (iter.next(), iter.next()) {
            (None, _) => Ok(false),
            (Some(bcs_id), None) => {
                self.changesets
                    .push(RepoChangeset(repo_name.clone(), *bcs_id));
                Ok(true)
            }
            (Some(_), Some(_)) => Err(errors::internal_error(format!(
                "pushrebase mapping is ambiguous in repo {} for {}: {:?} (expected only one)",
                repo_name, bcs_id, bcs_ids,
            ))
            .into()),
        }
    }

    async fn try_traverse_commit_sync(&mut self) -> Result<bool, errors::ServiceError> {
        let RepoChangeset(repo_name, bcs_id) = self.last();
        let repo = self.repo(&repo_name).await?;

        let maybe_common_commit_sync_config = repo
            .live_commit_sync_config()
            .get_common_config_if_exists(repo.repoid())
            .map_err(errors::internal_error)?;

        if let Some(config) = maybe_common_commit_sync_config {
            self.try_traverse_commit_sync_inner(repo, bcs_id, config)
                .await
        } else {
            Ok(false)
        }
    }

    // Helper function that wraps traversing logic from try_traverse_commit_sync.
    // Only needed for better code readability
    async fn try_traverse_commit_sync_inner(
        &mut self,
        repo: RepoContext,
        bcs_id: ChangesetId,
        config: CommonCommitSyncConfig,
    ) -> Result<bool, errors::ServiceError> {
        let mut synced_changesets = vec![];
        let (target_repo_ids, expected_sync_origin) = if config.large_repo_id == repo.repoid() {
            (
                config.small_repos.keys().copied().collect(),
                SyncedCommitSourceRepo::Small,
            )
        } else {
            (vec![config.large_repo_id], SyncedCommitSourceRepo::Large)
        };

        for target_repo_id in target_repo_ids.into_iter() {
            let entries = repo
                .synced_commit_mapping()
                .get(&self.ctx, repo.repoid(), bcs_id, target_repo_id)
                .await
                .map_err(errors::internal_error)?;
            if let Some(target_repo_name) = self.mononoke.repo_name_from_id(target_repo_id) {
                synced_changesets.extend(entries.into_iter().filter_map(
                    |(cs, _, maybe_source_repo)| {
                        let traverse = match maybe_source_repo {
                            // source_repo information can be absent e.g. for old commits but
                            // let's still traverse the mapping because in most cases we will
                            // get the correct result.
                            None => true,
                            Some(source_repo) if source_repo == expected_sync_origin => true,
                            _ => false,
                        };
                        if traverse {
                            Some(RepoChangeset(target_repo_name.clone(), cs))
                        } else {
                            None
                        }
                    },
                ));
            }
        }

        let mut iter = synced_changesets.iter();

        match (iter.next(), iter.next()) {
            (None, _) => Ok(false),
            (Some(rc), None) => {
                self.changesets.push(rc.clone());
                Ok(true)
            }
            (Some(_), Some(_)) => Err(errors::internal_error(format!(
                "commit sync mapping is ambiguous in repo {} for {}: {:?} (expected only one)",
                repo.name(),
                bcs_id,
                synced_changesets,
            ))
            .into()),
        }
    }

    fn into_thrift(self) -> thrift::CommitLookupPushrebaseHistoryResponse {
        let origin = self.last().into_thrift();
        let history: Vec<_> = once(self.head)
            .chain(self.changesets.into_iter())
            .map(|rc| rc.into_thrift())
            .collect();
        thrift::CommitLookupPushrebaseHistoryResponse {
            history,
            origin,
            ..Default::default()
        }
    }
}

impl SourceControlServiceImpl {
    // Look up commit history over Pushrebase mutations
    pub(crate) async fn commit_lookup_pushrebase_history(
        &self,
        ctx: CoreContext,
        commit: thrift::CommitSpecifier,
        _params: thrift::CommitLookupPushrebaseHistoryParams,
    ) -> Result<thrift::CommitLookupPushrebaseHistoryResponse, errors::ServiceError> {
        let (repo, changeset) = self.repo_changeset(ctx.clone(), &commit).await?;
        let mut history = RepoChangesetsPushrebaseHistory::new(
            ctx,
            self.mononoke.clone(),
            repo.name().to_string(),
            changeset.id(),
        );

        // Ensure that commit is public
        history.ensure_head_is_public().await?;

        let mut pushrebased = false;
        if history.try_traverse_pushrebase().await? {
            pushrebased = true;
        } else if history.try_traverse_commit_sync().await? {
            pushrebased = history.try_traverse_pushrebase().await?;
        }

        if pushrebased {
            history.try_traverse_commit_sync().await?;
        }

        Ok(history.into_thrift())
    }
}


/*
 * 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::borrow::Cow;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::collections::HashSet;

use borrowed::borrowed;
use bytes::Bytes;
use context::CoreContext;
use dedupmap::DedupMap;
use futures::future;
use futures::stream::TryStreamExt;
use futures::try_join;
use maplit::btreeset;
use mononoke_api::ChangesetPathHistoryOptions;
use mononoke_api::ChangesetSpecifier;
use mononoke_api::MononokeError;
use mononoke_api::MononokePath;
use mononoke_api::PathEntry;
use source_control as thrift;

use crate::commit_id::map_commit_identities;
use crate::commit_id::map_commit_identity;
use crate::errors;
use crate::from_request::check_range_and_convert;
use crate::from_request::validate_timestamp;
use crate::history::collect_history;
use crate::into_response::IntoResponse;
use crate::source_control_impl::SourceControlServiceImpl;

const BLAME_TITLE_MAX_LENGTH: usize = 128;

impl SourceControlServiceImpl {
    /// Determine whether anything exists at this path.
    pub(crate) async fn commit_path_exists(
        &self,
        ctx: CoreContext,
        commit_path: thrift::CommitPathSpecifier,
        _params: thrift::CommitPathExistsParams,
    ) -> Result<thrift::CommitPathExistsResponse, errors::ServiceError> {
        let (_repo, changeset) = self.repo_changeset(ctx, &commit_path.commit).await?;
        let path = changeset.path(&commit_path.path).await?;
        Ok(thrift::CommitPathExistsResponse {
            exists: path.exists().await?,
            file_exists: path.is_file().await?,
            tree_exists: path.is_tree().await?,
            ..Default::default()
        })
    }

    /// Returns information about the file or directory at a path in a commit.
    pub(crate) async fn commit_path_info(
        &self,
        ctx: CoreContext,
        commit_path: thrift::CommitPathSpecifier,
        _params: thrift::CommitPathInfoParams,
    ) -> Result<thrift::CommitPathInfoResponse, errors::ServiceError> {
        let (_repo, changeset) = self.repo_changeset(ctx, &commit_path.commit).await?;
        let path = changeset.path_with_content(&commit_path.path).await?;
        let response = match path.entry().await? {
            PathEntry::NotPresent => thrift::CommitPathInfoResponse {
                exists: false,
                r#type: None,
                info: None,
                ..Default::default()
            },
            PathEntry::Tree(tree) => {
                let summary = tree.summary().await?;
                let tree_info = thrift::TreeInfo {
                    id: tree.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::CommitPathInfoResponse {
                    exists: true,
                    r#type: Some(thrift::EntryType::TREE),
                    info: Some(thrift::EntryInfo::tree(tree_info)),
                    ..Default::default()
                }
            }
            PathEntry::File(file, file_type) => {
                let metadata = file.metadata().await?;
                let file_info = thrift::FileInfo {
                    id: metadata.content_id.as_ref().to_vec(),
                    file_size: metadata.total_size as i64,
                    content_sha1: metadata.sha1.as_ref().to_vec(),
                    content_sha256: metadata.sha256.as_ref().to_vec(),
                    ..Default::default()
                };
                thrift::CommitPathInfoResponse {
                    exists: true,
                    r#type: Some(file_type.into_response()),
                    info: Some(thrift::EntryInfo::file(file_info)),
                    ..Default::default()
                }
            }
        };
        Ok(response)
    }

    pub(crate) async fn commit_multiple_path_info(
        &self,
        ctx: CoreContext,
        commit: thrift::CommitSpecifier,
        params: thrift::CommitMultiplePathInfoParams,
    ) -> Result<thrift::CommitMultiplePathInfoResponse, errors::ServiceError> {
        let (_repo, changeset) = self.repo_changeset(ctx, &commit).await?;
        let mut paths = vec![];
        for path in params.paths {
            let strpath = path.as_str();
            let mpath = MononokePath::try_from(strpath)?;
            paths.push(mpath);
        }

        let result = changeset
            .paths_with_content(paths.into_iter())
            .await?
            .map_ok(|context| async move {
                let context_path = context.path().to_string();

                match context.entry().await? {
                    PathEntry::NotPresent => {
                        let not_present_elem = thrift::CommitPathInfoResponse {
                            exists: false,
                            r#type: None,
                            info: None,
                            ..Default::default()
                        };
                        Result::<_, errors::ServiceError>::Ok((context_path, not_present_elem))
                    }
                    PathEntry::Tree(tree) => {
                        let summary = tree.summary().await?;
                        let tree_elem = thrift::CommitPathInfoResponse {
                            exists: true,
                            r#type: Some(thrift::EntryType::TREE),
                            info: Some(thrift::EntryInfo::tree(
                                (*tree.id(), summary).into_response(),
                            )),
                            ..Default::default()
                        };
                        Result::<_, errors::ServiceError>::Ok((context_path, tree_elem))
                    }
                    PathEntry::File(file, file_type) => {
                        let metadata = file.metadata().await?;
                        let file_elem = thrift::CommitPathInfoResponse {
                            exists: true,
                            r#type: Some(file_type.into_response()),
                            info: Some(thrift::EntryInfo::file(metadata.into_response())),
                            ..Default::default()
                        };
                        Result::<_, errors::ServiceError>::Ok((context_path, file_elem))
                    }
                }
            })
            .map_err(errors::ServiceError::from)
            .try_buffer_unordered(100)
            .try_collect::<BTreeMap<_, _>>()
            .await?;

        Ok(thrift::CommitMultiplePathInfoResponse {
            path_info: result,
            ..Default::default()
        })
    }

    pub(crate) async fn commit_path_blame(
        &self,
        ctx: CoreContext,
        commit_path: thrift::CommitPathSpecifier,
        params: thrift::CommitPathBlameParams,
    ) -> Result<thrift::CommitPathBlameResponse, errors::ServiceError> {
        match params.format {
            thrift::BlameFormat::COMPACT => {
                self.commit_path_blame_compact(ctx, commit_path, params)
                    .await
            }
            other_format => Err(errors::invalid_request(format!(
                "unsupported blame format {}",
                other_format
            ))
            .into()),
        }
    }

    async fn commit_path_blame_compact(
        &self,
        ctx: CoreContext,
        commit_path: thrift::CommitPathSpecifier,
        params: thrift::CommitPathBlameParams,
    ) -> Result<thrift::CommitPathBlameResponse, errors::ServiceError> {
        let (repo, changeset) = self.repo_changeset(ctx, &commit_path.commit).await?;
        borrowed!(repo);
        let path = changeset.path_with_history(&commit_path.path).await?;

        let options = params.format_options.unwrap_or_else(|| {
            btreeset! { thrift::BlameFormatOption::INCLUDE_CONTENTS }
        });
        let option_include_contents =
            options.contains(&thrift::BlameFormatOption::INCLUDE_CONTENTS);
        let option_include_title = options.contains(&thrift::BlameFormatOption::INCLUDE_TITLE);
        let option_include_message = options.contains(&thrift::BlameFormatOption::INCLUDE_MESSAGE);
        let option_include_parent = options.contains(&thrift::BlameFormatOption::INCLUDE_PARENT);
        let option_include_commit_numbers =
            options.contains(&thrift::BlameFormatOption::INCLUDE_COMMIT_NUMBERS);

        let follow_mutable_file_history = params.follow_mutable_file_history.unwrap_or(false);

        // Changeset ids in the order they will be returned.
        let mut indexed_csids = Vec::new();

        // Mapped commit ids in that same order.
        let mut commit_ids = Vec::new();

        // The small number suitable for each commit, in that same order.
        let mut commit_numbers = Vec::new();

        // The index into these vectors of each changeset.
        let mut commit_id_indexes = HashMap::new();

        let mut paths = DedupMap::new();
        let mut authors = DedupMap::new();
        let mut dates = DedupMap::new();
        let mut titles = DedupMap::new();
        let mut messages = DedupMap::new();

        // Fetch the blame, and optionally its associated content.
        let (blame, content) = if option_include_contents {
            path.blame_with_content(follow_mutable_file_history).await?
        } else {
            (path.blame(follow_mutable_file_history).await?, Bytes::new())
        };

        // Map all the changeset IDs into the requested identity schemes.  Keep a mapping of
        // which bonsai changeset ID corresponds to which mapped commit ID index, so we can look
        // them up later.
        let csids_and_nums = blame
            .changeset_ids()
            .map_err(|e| MononokeError::InvalidRequest(e.to_string()))?
            .collect::<Vec<_>>();
        let csids = csids_and_nums
            .iter()
            .map(|(csid, _)| *csid)
            .collect::<Vec<_>>();
        let mut mapped_commit_ids =
            map_commit_identities(repo, csids.clone(), &params.identity_schemes).await?;
        for (id, num) in csids_and_nums {
            if let Some(mapped_ids) = mapped_commit_ids.remove(&id) {
                let index = commit_ids.len();
                commit_ids.push(mapped_ids);
                commit_numbers.push(num as i32);
                commit_id_indexes.insert(id, index);
                indexed_csids.push(id);
            }
        }

        // Collect author and date fields from the commit info.
        let info: HashMap<_, _> = future::try_join_all(csids.iter().map(move |csid| async move {
            let changeset = repo
                .changeset(ChangesetSpecifier::Bonsai(*csid))
                .await?
                .ok_or_else(|| {
                    MononokeError::InvalidRequest(format!("failed to resolve commit: {}", csid))
                })?;
            let (date, author, message) = try_join!(
                changeset.author_date(),
                changeset.author(),
                changeset.message(),
            )?;
            let title: String = message
                .chars()
                .take(BLAME_TITLE_MAX_LENGTH)
                .take_while(|ch| *ch != '\n')
                .collect();

            Ok::<_, MononokeError>((*csid, (author, date, message, title)))
        }))
        .await?
        .into_iter()
        .collect();

        // Collect parent information for each changeset if requested.
        let parent_commit_ids = if option_include_parent {
            let changeset_parents = repo.many_changeset_parents(csids.clone()).await?;
            let all_parent_csids = changeset_parents
                .iter()
                .flat_map(|(_, parents)| parents)
                .collect::<HashSet<_>>()
                .into_iter()
                .copied()
                .collect::<Vec<_>>();
            let parent_commit_ids_map =
                map_commit_identities(repo, all_parent_csids, &params.identity_schemes).await?;
            let mut parent_commit_ids = Vec::with_capacity(indexed_csids.len());
            for csid in indexed_csids {
                let parents = changeset_parents.get(&csid).ok_or_else(|| {
                    errors::internal_error(format!("missing parents for {}", csid))
                })?;
                let mut changeset_parent_commit_ids = Vec::with_capacity(parents.len());
                for parent in parents {
                    changeset_parent_commit_ids.push(
                        parent_commit_ids_map
                            .get(parent)
                            .ok_or_else(|| {
                                errors::internal_error(format!(
                                    "missing parent commit ids for {}",
                                    parent
                                ))
                            })?
                            .clone(),
                    );
                }
                parent_commit_ids.push(changeset_parent_commit_ids);
            }
            Some(parent_commit_ids)
        } else {
            None
        };

        let mut content_iter = content.as_ref().split(|c| *c == b'\n');

        let lines = blame
            .lines()
            .map_err(|e| MononokeError::InvalidRequest(e.to_string()))?
            .enumerate()
            .map(|(line, blame_line)| -> Result<_, thrift::RequestError> {
                let commit_id_index =
                    commit_id_indexes
                        .get(blame_line.changeset_id)
                        .ok_or_else(|| {
                            errors::commit_not_found(format!(
                                "failed to resolve commit: {}",
                                blame_line.changeset_id
                            ))
                        })?;
                let (author, date, message, title) =
                    info.get(blame_line.changeset_id).ok_or_else(|| {
                        errors::commit_not_found(format!(
                            "failed to resolve commit: {}",
                            blame_line.changeset_id
                        ))
                    })?;
                let mut thrift_blame_line = thrift::BlameCompactLine {
                    line: (line + 1) as i32,
                    contents: None,
                    commit_id_index: *commit_id_index as i32,
                    path_index: paths.insert(&blame_line.path.to_string()) as i32,
                    author_index: authors.insert(author) as i32,
                    date_index: dates.insert(Cow::Borrowed(date)) as i32,
                    origin_line: (blame_line.origin_offset + 1) as i32,
                    title_index: None,
                    message_index: None,
                    ..Default::default()
                };
                if option_include_contents {
                    if let Some(content_line) = content_iter.next() {
                        thrift_blame_line.contents =
                            Some(String::from_utf8_lossy(content_line).into_owned());
                    }
                }
                if option_include_title {
                    thrift_blame_line.title_index = Some(titles.insert(title) as i32);
                }
                if option_include_message {
                    thrift_blame_line.message_index = Some(messages.insert(message) as i32);
                }
                if option_include_parent {
                    if let Some(parent) = &blame_line.parent {
                        thrift_blame_line.parent_index = Some(parent.parent_index as i32);
                        thrift_blame_line.parent_start_line = Some((parent.offset + 1) as i32);
                        thrift_blame_line.parent_range_length = Some(parent.length as i32);
                        thrift_blame_line.parent_path_index = parent
                            .renamed_from_path
                            .map(|path| paths.insert(&path.to_string()) as i32);
                    }
                }
                Ok(thrift_blame_line)
            })
            .collect::<Result<Vec<_>, _>>()?;

        let paths = paths.into_items();
        let authors = authors.into_items();
        let titles = Some(titles.into_items()).filter(|titles| !titles.is_empty());
        let messages = Some(messages.into_items()).filter(|messages| !messages.is_empty());
        let commit_numbers = option_include_commit_numbers.then_some(commit_numbers);
        let approx_commit_count = option_include_commit_numbers
            .then(|| blame.changeset_count().ok())
            .flatten()
            .map(|c| c as i32);
        let distinct_range_count = Some(
            blame
                .range_count()
                .map_err(|e| MononokeError::InvalidRequest(e.to_string()))? as i32,
        );
        let dates = dates
            .into_items()
            .into_iter()
            .map(|date| thrift::DateTime {
                timestamp: date.timestamp(),
                tz: date.offset().local_minus_utc(),
                ..Default::default()
            })
            .collect();
        let blame = thrift::BlameCompact {
            lines,
            commit_ids,
            paths,
            authors,
            dates,
            titles,
            messages,
            parent_commit_ids,
            commit_numbers,
            approx_commit_count,
            distinct_range_count,
            ..Default::default()
        };

        Ok(thrift::CommitPathBlameResponse {
            blame: thrift::Blame::blame_compact(blame),
            ..Default::default()
        })
    }

    pub(crate) async fn commit_path_history(
        &self,
        ctx: CoreContext,
        commit_path: thrift::CommitPathSpecifier,
        params: thrift::CommitPathHistoryParams,
    ) -> Result<thrift::CommitPathHistoryResponse, errors::ServiceError> {
        let (repo, changeset) = self.repo_changeset(ctx, &commit_path.commit).await?;
        let path = changeset.path_with_history(&commit_path.path).await?;
        let (descendants_of, exclude_changeset_and_ancestors) = try_join!(
            async {
                if let Some(descendants_of) = &params.descendants_of {
                    Ok::<_, errors::ServiceError>(Some(
                        self.changeset_id(&repo, descendants_of).await?,
                    ))
                } else {
                    Ok(None)
                }
            },
            async {
                if let Some(exclude_changeset_and_ancestors) =
                    &params.exclude_changeset_and_ancestors
                {
                    Ok::<_, errors::ServiceError>(Some(
                        self.changeset_id(&repo, exclude_changeset_and_ancestors)
                            .await?,
                    ))
                } else {
                    Ok(None)
                }
            }
        )?;

        let limit: usize = check_range_and_convert("limit", params.limit, 0..)?;
        let skip: usize = check_range_and_convert("skip", params.skip, 0..)?;

        // Time filter equal to zero might be mistaken by users for an unset, like None.
        // We will consider negative timestamps as invalid and zeros as unset.
        let after_timestamp = validate_timestamp(params.after_timestamp, "after_timestamp")?;
        let before_timestamp = validate_timestamp(params.before_timestamp, "before_timestamp")?;

        if let (Some(ats), Some(bts)) = (after_timestamp, before_timestamp) {
            if bts < ats {
                return Err(errors::invalid_request(format!(
                    "after_timestamp ({}) cannot be greater than before_timestamp ({})",
                    ats, bts,
                ))
                .into());
            }
        }

        if skip > 0 && (after_timestamp.is_some() || before_timestamp.is_some()) {
            return Err(errors::invalid_request(
                "Time filters cannot be applied if skip is not 0".to_string(),
            )
            .into());
        }

        let history_stream = path
            .history(ChangesetPathHistoryOptions {
                until_timestamp: after_timestamp.clone(),
                descendants_of,
                exclude_changeset_and_ancestors,
                follow_history_across_deletions: params.follow_history_across_deletions,
                follow_mutable_file_history: params.follow_mutable_file_history.unwrap_or(false),
            })
            .await?;
        let history = collect_history(
            history_stream,
            skip,
            limit,
            before_timestamp,
            after_timestamp,
            params.format,
            &params.identity_schemes,
        )
        .await?;

        Ok(thrift::CommitPathHistoryResponse {
            history,
            ..Default::default()
        })
    }

    pub(crate) async fn commit_path_last_changed(
        &self,
        ctx: CoreContext,
        commit_path: thrift::CommitPathSpecifier,
        params: thrift::CommitPathLastChangedParams,
    ) -> Result<thrift::CommitPathLastChangedResponse, errors::ServiceError> {
        let (_repo, changeset) = self.repo_changeset(ctx, &commit_path.commit).await?;
        let path = changeset.path_with_history(&commit_path.path).await?;
        match path.last_modified().await? {
            Some(last_modified) => {
                let last_modified =
                    map_commit_identity(&last_modified, &params.identity_schemes).await?;
                Ok(thrift::CommitPathLastChangedResponse {
                    last_change: Some(thrift::CommitPathLastChange {
                        exists: true,
                        last_changed_commit: last_modified,
                        ..Default::default()
                    }),
                    ..Default::default()
                })
            }
            None => match path.last_deleted().await? {
                Some(last_deleted) => {
                    let last_deleted =
                        map_commit_identity(&last_deleted, &params.identity_schemes).await?;
                    Ok(thrift::CommitPathLastChangedResponse {
                        last_change: Some(thrift::CommitPathLastChange {
                            exists: false,
                            last_changed_commit: last_deleted,
                            ..Default::default()
                        }),
                        ..Default::default()
                    })
                }
                None => Ok(thrift::CommitPathLastChangedResponse {
                    last_change: None,
                    ..Default::default()
                }),
            },
        }
    }

    pub(crate) async fn commit_multiple_path_last_changed(
        &self,
        ctx: CoreContext,
        commit: thrift::CommitSpecifier,
        params: thrift::CommitMultiplePathLastChangedParams,
    ) -> Result<thrift::CommitMultiplePathLastChangedResponse, errors::ServiceError> {
        let (repo, changeset) = self.repo_changeset(ctx, &commit).await?;
        let mut paths = HashSet::with_capacity(params.paths.len());
        for path in params.paths {
            let strpath = path.as_str();
            let mpath = MononokePath::try_from(strpath)?;
            paths.insert(mpath);
        }

        let path_last_modified = changeset
            .paths_with_history(paths.iter().cloned())
            .await?
            .map_ok(|context| async move {
                let context_path = context.path().clone();
                let last_modified = context.last_modified().await?;
                Ok::<_, errors::ServiceError>((context_path, last_modified))
            })
            .map_err(errors::ServiceError::from)
            .try_buffer_unordered(100)
            .try_filter_map(|(path, maybe_last_changed)| async move {
                Ok(maybe_last_changed.map(move |last_changed| (path, last_changed.id())))
            })
            .try_collect::<BTreeMap<_, _>>()
            .await?;

        paths.retain(|path| !path_last_modified.contains_key(path));

        let path_last_deleted = changeset
            .deleted_paths(paths.into_iter())
            .await?
            .map_ok(|context| async move {
                let context_path = context.path().clone();
                let last_deleted = context.last_deleted().await?;
                Ok::<_, errors::ServiceError>((context_path, last_deleted))
            })
            .map_err(errors::ServiceError::from)
            .try_buffer_unordered(100)
            .try_filter_map(|(path, maybe_last_changed)| async move {
                Ok(maybe_last_changed.map(move |last_changed| (path, last_changed.id())))
            })
            .try_collect::<BTreeMap<_, _>>()
            .await?;

        let changesets = path_last_modified
            .values()
            .chain(path_last_deleted.values())
            .collect::<HashSet<_>>()
            .into_iter()
            .copied()
            .collect::<Vec<_>>();

        let commit_identities =
            map_commit_identities(&repo, changesets, &params.identity_schemes).await?;

        let path_last_modified = path_last_modified
            .into_iter()
            .map(|(path, last_changed)| (true, path, last_changed));
        let path_last_deleted = path_last_deleted
            .into_iter()
            .map(|(path, last_changed)| (false, path, last_changed));
        let path_last_change = path_last_modified
            .chain(path_last_deleted)
            .map(|(exists, path, last_changed)| {
                let last_changed_commit = commit_identities
                    .get(&last_changed)
                    .cloned()
                    .unwrap_or_default();
                let last_change = thrift::CommitPathLastChange {
                    exists,
                    last_changed_commit,
                    ..Default::default()
                };

                (path.to_string(), last_change)
            })
            .collect();

        Ok(thrift::CommitMultiplePathLastChangedResponse {
            path_last_change,
            ..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 anyhow::Result;
use context::CoreContext;
use itertools::Itertools;
use mononoke_api::sparse_profile::get_profile_delta_size;
use mononoke_api::sparse_profile::MonitoringProfiles;
use mononoke_api::sparse_profile::ProfileSizeChange;
use mononoke_api::sparse_profile::SparseProfileMonitoring;
use source_control as thrift;

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

pub(crate) trait SparseProfilesExt {
    fn to_string(&self) -> String;
}

impl SparseProfilesExt for thrift::SparseProfiles {
    fn to_string(&self) -> String {
        match self {
            thrift::SparseProfiles::all_profiles(_) => "all sparse profiles".to_string(),
            thrift::SparseProfiles::profiles(profiles) => profiles
                .iter()
                .format_with("\n", |item, f| f(&item))
                .to_string(),
            thrift::SparseProfiles::UnknownField(t) => format!("unknown SparseProfiles type {}", t),
        }
    }
}

impl SourceControlServiceImpl {
    pub(crate) async fn commit_sparse_profile_size(
        &self,
        ctx: CoreContext,
        commit: thrift::CommitSpecifier,
        params: thrift::CommitSparseProfileSizeParams,
    ) -> Result<thrift::CommitSparseProfileSizeResponse, errors::ServiceError> {
        let (repo, changeset) = self.repo_changeset(ctx.clone(), &commit).await?;
        let profiles = convert_profiles_params(params.profiles).await?;
        let monitor = SparseProfileMonitoring::new(
            repo.name(),
            repo.sparse_profiles(),
            repo.config().sparse_profiles_config.clone(),
            profiles,
        )?;
        let profiles = monitor.get_monitoring_profiles(&changeset).await?;
        let sizes_hashmap = monitor.get_profile_size(&ctx, &changeset, profiles).await?;
        let sizes = sizes_hashmap
            .into_iter()
            .map(|(source, size)| {
                (
                    source,
                    thrift::SparseProfileSize {
                        size: size as i64,
                        ..Default::default()
                    },
                )
            })
            .collect();
        Ok(thrift::CommitSparseProfileSizeResponse {
            profiles_size: thrift::SparseProfileSizes {
                sizes,
                ..Default::default()
            },
            ..Default::default()
        })
    }

    pub(crate) async fn commit_sparse_profile_delta(
        &self,
        ctx: CoreContext,
        commit: thrift::CommitSpecifier,
        params: thrift::CommitSparseProfileDeltaParams,
    ) -> Result<thrift::CommitSparseProfileDeltaResponse, errors::ServiceError> {
        let (repo, changeset, other) = self
            .repo_changeset_pair(ctx.clone(), &commit, &params.other_id)
            .await?;
        let profiles = convert_profiles_params(params.profiles).await?;
        let monitor = SparseProfileMonitoring::new(
            repo.name(),
            repo.sparse_profiles(),
            repo.config().sparse_profiles_config.clone(),
            profiles,
        )?;
        let profiles = monitor.get_monitoring_profiles(&changeset).await?;
        let sizes_hashmap =
            get_profile_delta_size(&ctx, &monitor, &changeset, &other, profiles).await;
        let sizes = sizes_hashmap?
            .into_iter()
            .map(|(source, change)| {
                (
                    source,
                    thrift::SparseProfileChange {
                        change: convert(change),
                        ..Default::default()
                    },
                )
            })
            .collect();
        Ok(thrift::CommitSparseProfileDeltaResponse {
            changed_sparse_profiles: Some(thrift::SparseProfileDeltaSizes {
                size_changes: sizes,
                ..Default::default()
            }),
            ..Default::default()
        })
    }
}

async fn convert_profiles_params(
    params_profiles: thrift::SparseProfiles,
) -> Result<MonitoringProfiles, errors::ServiceError> {
    match params_profiles {
        thrift::SparseProfiles::all_profiles(_) => Ok(MonitoringProfiles::All),
        thrift::SparseProfiles::profiles(profiles) => Ok(MonitoringProfiles::Exact { profiles }),
        thrift::SparseProfiles::UnknownField(_) => Err(errors::ServiceError::Request(
            errors::not_available("Not implemented".to_string()),
        )),
    }
}

fn convert(change: ProfileSizeChange) -> thrift::SparseProfileChangeElement {
    match change {
        ProfileSizeChange::Added(size) => {
            thrift::SparseProfileChangeElement::added(thrift::SparseProfileAdded {
                size: size as i64,
                ..Default::default()
            })
        }
        ProfileSizeChange::Removed(size) => {
            thrift::SparseProfileChangeElement::removed(thrift::SparseProfileRemoved {
                previous_size: size as i64,
                ..Default::default()
            })
        }
        ProfileSizeChange::Changed(size) => {
            thrift::SparseProfileChangeElement::changed(thrift::SparseProfileSizeChanged {
                size_change: size,
                ..Default::default()
            })
        }
    }
}


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

use context::CoreContext;
use mononoke_api::headerless_unified_diff;
use mononoke_api::FileId;
use source_control as thrift;

use crate::errors;
use crate::errors::ServiceErrorResultExt;
use crate::from_request::check_range_and_convert;
use crate::from_request::FromRequest;
use crate::into_response::IntoResponse;
use crate::source_control_impl::SourceControlServiceImpl;
use crate::specifiers::SpecifierExt;

impl SourceControlServiceImpl {
    /// Test whether a file exists.
    pub(crate) async fn file_exists(
        &self,
        ctx: CoreContext,
        file: thrift::FileSpecifier,
        _params: thrift::FileExistsParams,
    ) -> Result<bool, errors::ServiceError> {
        let (_repo, file) = self.repo_file(ctx, &file).await?;
        Ok(file.is_some())
    }

    /// Get file info.
    pub(crate) async fn file_info(
        &self,
        ctx: CoreContext,
        file: thrift::FileSpecifier,
        _params: thrift::FileInfoParams,
    ) -> Result<thrift::FileInfo, errors::ServiceError> {
        match self.repo_file(ctx, &file).await? {
            (_repo, Some(file)) => Ok(file.metadata().await?.into_response()),
            (_repo, None) => Err(errors::file_not_found(file.description()).into()),
        }
    }

    /// Get a chunk of file content.
    pub(crate) async fn file_content_chunk(
        &self,
        ctx: CoreContext,
        file: thrift::FileSpecifier,
        params: thrift::FileContentChunkParams,
    ) -> Result<thrift::FileChunk, errors::ServiceError> {
        let offset: u64 = check_range_and_convert("offset", params.offset, 0..)?;
        let size: u64 = check_range_and_convert(
            "size",
            params.size,
            0..=source_control::FILE_CONTENT_CHUNK_SIZE_LIMIT,
        )?;
        match self.repo_file(ctx, &file).await? {
            (_repo, Some(file)) => {
                let metadata = file.metadata().await?;
                let data = file.content_range_concat(offset, size).await?;
                Ok(thrift::FileChunk {
                    offset: params.offset,
                    file_size: metadata.total_size as i64,
                    data: Vec::from(data.as_ref()),
                    ..Default::default()
                })
            }
            (_repo, None) => Err(errors::file_not_found(file.description()).into()),
        }
    }

    /// Compare a file with another file.
    pub(crate) async fn file_diff(
        &self,
        ctx: CoreContext,
        file: thrift::FileSpecifier,
        params: thrift::FileDiffParams,
    ) -> Result<thrift::FileDiffResponse, errors::ServiceError> {
        let context_lines = params.context as usize;

        let (repo, base_file) = self.repo_file(ctx, &file).await?;
        let base_file = base_file
            .ok_or_else(|| errors::file_not_found(file.description()))
            .context("failed to resolve target file")?;

        let other_file_id = FileId::from_request(&params.other_file_id)?;
        let other_file = repo
            .file(other_file_id)
            .await?
            .ok_or_else(|| errors::file_not_found(other_file_id.to_string()))
            .context("failed to resolve other file")?;

        let diff = headerless_unified_diff(&other_file, &base_file, context_lines)
            .await?
            .into_response();

        Ok(thrift::FileDiffResponse {
            diff,
            ..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 bytes::Bytes;
use context::CoreContext;
use mononoke_api::errors::MononokeError;
use mononoke_api::repo::git::GitError;
use mononoke_api::ChangesetId;
use mononoke_types::bonsai_changeset::BonsaiAnnotatedTag;
use mononoke_types::bonsai_changeset::BonsaiAnnotatedTagTarget;
use source_control as thrift;

use crate::errors::invalid_request;
use crate::errors::ServiceErrorResultExt;
use crate::errors::{self};
use crate::source_control_impl::SourceControlServiceImpl;

impl SourceControlServiceImpl {
    /// Upload raw git object to Mononoke data store for back-and-forth translation.
    /// Not to be used for uploading raw file content blobs.
    pub(crate) async fn upload_git_object(
        &self,
        ctx: CoreContext,
        repo: thrift::RepoSpecifier,
        params: thrift::UploadGitObjectParams,
    ) -> Result<thrift::UploadGitObjectResponse, errors::ServiceError> {
        let repo_ctx = self
            .repo_for_service(ctx, &repo, params.service_identity.clone())
            .await
            .with_context(|| format!("Error in opening repo using specifier {:?}", repo))?;
        // Validate that the request sender has an internal service identity with the right permission.
        repo_ctx
            .authorization_context()
            .require_git_import_operations(repo_ctx.ctx(), repo_ctx.inner_repo())
            .await
            .map_err(MononokeError::from)?;
        // Validate that the bytes correspond to a valid git hash.
        let git_hash = git_hash::oid::try_from_bytes(&params.git_hash)
            .map_err(|_| GitError::InvalidHash(format!("{:x?}", params.git_hash)))?;
        repo_ctx
            .upload_git_object(git_hash, params.raw_content)
            .await?;
        Ok(thrift::UploadGitObjectResponse {
            ..Default::default()
        })
    }

    /// Create Mononoke counterpart of git tree object in the form of a bonsai changeset.
    /// The raw git tree object must already be stored in Mononoke stores before invoking
    /// this endpoint.
    pub(crate) async fn create_git_tree(
        &self,
        ctx: CoreContext,
        repo: thrift::RepoSpecifier,
        params: thrift::CreateGitTreeParams,
    ) -> Result<thrift::CreateGitTreeResponse, errors::ServiceError> {
        let repo_ctx = self
            .repo_for_service(ctx, &repo, params.service_identity.clone())
            .await
            .with_context(|| format!("Error in opening repo using specifier {:?}", repo))?;
        // Validate that the request sender has an internal service identity with the right permission.
        repo_ctx
            .authorization_context()
            .require_git_import_operations(repo_ctx.ctx(), repo_ctx.inner_repo())
            .await
            .map_err(MononokeError::from)?;
        // Validate that the provided byte content constitutes a hash.
        let git_tree_hash = git_hash::oid::try_from_bytes(&params.git_tree_hash)
            .map_err(|_| GitError::InvalidHash(format!("{:x?}", params.git_tree_hash)))?;
        repo_ctx.create_git_tree(git_tree_hash).await?;
        Ok(thrift::CreateGitTreeResponse {
            ..Default::default()
        })
    }

    /// Create Mononoke counterpart of git tag object in the form of a bonsai changeset.
    /// The raw git tag object must already be stored in Mononoke stores before invoking
    /// this endpoint.
    pub(crate) async fn create_git_tag(
        &self,
        ctx: CoreContext,
        repo: thrift::RepoSpecifier,
        params: thrift::CreateGitTagParams,
    ) -> Result<thrift::CreateGitTagResponse, errors::ServiceError> {
        let repo_ctx = self
            .repo_for_service(ctx, &repo, params.service_identity.clone())
            .await
            .with_context(|| format!("Error in opening repo using specifier {:?}", repo))?;
        // Validate that the request sender has an internal service identity with the right permission.
        repo_ctx
            .authorization_context()
            .require_git_import_operations(repo_ctx.ctx(), repo_ctx.inner_repo())
            .await
            .map_err(MononokeError::from)?;
        let author_date = params
            .author_date
            .as_ref()
            .map(|date| mononoke_types::DateTime::from_timestamp(date.timestamp, date.tz))
            .transpose()
            .map_err(|err| {
                invalid_request(format!(
                    "Error in creating author date from {:?}. Cause: {:#}",
                    params.author_date, err
                ))
            })?
            .map(|date| date.into());

        let target_changeset_id =
            ChangesetId::from_bytes(&params.target_changeset).map_err(|err| {
                invalid_request(format!(
                    "Error in creating ChangesetId from {:?}. Cause: {:#}",
                    params.target_changeset, err
                ))
            })?;
        let annotated_tag = BonsaiAnnotatedTag {
            target: BonsaiAnnotatedTagTarget::Changeset(target_changeset_id),
            pgp_signature: params.pgp_signature.map(Bytes::from),
        };
        let changeset_context = repo_ctx
            .create_annotated_tag(
                params.tag_name,
                params.author,
                author_date,
                params.annotation,
                annotated_tag,
            )
            .await?;
        Ok(thrift::CreateGitTagResponse {
            created_changeset_id: changeset_context.id().as_ref().to_vec(),
            ..Default::default()
        })
    }
}