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

use std::collections::HashMap;

use bookmarks::BookmarkKey;
use bookmarks::BookmarkUpdateReason;
use bookmarks_movement::CreateBookmarkOp;
use bytes::Bytes;
use hooks::HookManagerRef;
use mononoke_types::ChangesetId;
use tunables::tunables;

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

impl RepoContext {
    /// Create a bookmark.
    pub async fn create_bookmark(
        &self,
        bookmark: &BookmarkKey,
        target: ChangesetId,
        pushvars: Option<&HashMap<String, Bytes>>,
    ) -> Result<(), MononokeError> {
        self.start_write()?;

        fn make_create_op<'a>(
            bookmark: &'a BookmarkKey,
            target: ChangesetId,
            pushvars: Option<&'a HashMap<String, Bytes>>,
        ) -> CreateBookmarkOp<'a> {
            let mut op = CreateBookmarkOp::new(bookmark, target, BookmarkUpdateReason::ApiRequest)
                .with_pushvars(pushvars);
            if !tunables()
                .disable_commit_scribe_logging_scs()
                .unwrap_or_default()
            {
                op = op.log_new_public_commits_to_scribe();
            }
            op
        }
        if let Some(redirector) = self.push_redirector.as_ref() {
            let large_bookmark = redirector.small_to_large_bookmark(bookmark).await?;
            if &large_bookmark == bookmark {
                return Err(MononokeError::InvalidRequest(format!(
                    "Cannot create shared bookmark '{}' from small repo",
                    bookmark.name()
                )));
            }
            let ctx = self.ctx();
            let target = redirector
                .get_small_to_large_commit_equivalent(ctx, target)
                .await?;
            make_create_op(&large_bookmark, target, pushvars)
                .run(
                    self.ctx(),
                    self.authorization_context(),
                    redirector.repo.inner_repo(),
                    redirector.repo.hook_manager(),
                )
                .await?;
            // Wait for bookmark to catch up on small repo
            redirector.backsync_latest(ctx).await?;
        } else {
            make_create_op(bookmark, target, pushvars)
                .run(
                    self.ctx(),
                    self.authorization_context(),
                    self.inner_repo(),
                    self.hook_manager().as_ref(),
                )
                .await?;
        }
        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.
 */

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

use anyhow::anyhow;
use blobstore::Loadable;
use borrowed::borrowed;
use bytes::Bytes;
use changesets::ChangesetsRef;
use chrono::DateTime;
use chrono::FixedOffset;
use context::CoreContext;
use ephemeral_blobstore::Bubble;
use filestore::FetchKey;
use filestore::FilestoreConfig;
use filestore::FilestoreConfigRef;
use filestore::StoreRequest;
use futures::stream;
use futures::stream::Stream;
use futures::stream::TryStreamExt;
use futures::try_join;
use futures::StreamExt;
use futures_stats::TimedFutureExt;
use itertools::Itertools;
use manifest::PathTree;
use metaconfig_types::RepoConfigRef;
use mononoke_types::fsnode::FsnodeEntry;
use mononoke_types::BonsaiChangeset;
use mononoke_types::BonsaiChangesetMut;
use mononoke_types::ChangesetId;
use mononoke_types::DateTime as MononokeDateTime;
use mononoke_types::FileChange;
use mononoke_types::MPath;
use mononoke_types::MPathElement;
use repo_authorization::RepoWriteOperation;
use repo_blobstore::RepoBlobstore;
use repo_blobstore::RepoBlobstoreRef;
use repo_identity::RepoIdentityRef;
use repo_update_logger::log_new_commits;
use repo_update_logger::CommitInfo;
use smallvec::SmallVec;
use sorted_vector_map::SortedVectorMap;

use crate::changeset::ChangesetContext;
use crate::errors::MononokeError;
use crate::file::FileId;
use crate::file::FileType;
use crate::path::MononokePath;
use crate::repo::RepoContext;
use crate::specifiers::ChangesetSpecifier;

#[derive(Clone)]
pub struct CreateCopyInfo {
    path: MononokePath,
    parent_index: usize,
}

impl CreateCopyInfo {
    pub fn new(path: MononokePath, parent_index: usize) -> Self {
        CreateCopyInfo { path, parent_index }
    }

    async fn check_valid(
        &self,
        stack_changes: Option<&PathTree<CreateChangeType>>,
        stack_parents: &[ChangesetContext],
    ) -> Result<(), MononokeError> {
        if let Some(stack_changes) = stack_changes {
            // Since this is a stacked commit, there is only one parent.
            if self.parent_index > 0 {
                return Err(MononokeError::InvalidRequest(format!(
                    "Parent index '{}' out of range for stacked commit",
                    self.parent_index,
                )));
            }
            // Check if the copy-from path was added or removed in the stack.
            match stack_changes.get(self.path.as_mpath()) {
                None | Some(CreateChangeType::None) => {}
                Some(CreateChangeType::Change) => {
                    return Ok(());
                }
                Some(CreateChangeType::Deletion) => {
                    return Err(MononokeError::InvalidRequest(String::from(
                        "Copy-from path references a file deleted earler in the stack",
                    )));
                }
            }
            // Check if the copy-from path was deleted by a prefix change.
            for prefix in self.path.prefixes() {
                if stack_changes.get(prefix.as_mpath()) == Some(&CreateChangeType::Change) {
                    return Err(MononokeError::InvalidRequest(String::from(
                        "Copy-from path references a file in a directory deleted earler in the stack",
                    )));
                }
            }
            // The copy-from path wasn't touched in the stack, check it was in
            // at least one of the stack's parents.
            for parent_ctx in stack_parents {
                if parent_ctx
                    .path_with_content(self.path.clone())
                    .await?
                    .is_file()
                    .await?
                {
                    return Ok(());
                }
            }
        } else {
            // This is the root of the stack.  Check the specific parent.
            let parent_ctx = stack_parents.get(self.parent_index).ok_or_else(|| {
                MononokeError::InvalidRequest(format!(
                    "Parent index '{}' out of range for commit with {} parent(s)",
                    self.parent_index,
                    stack_parents.len()
                ))
            })?;
            // Check the file exists in that parent.
            if parent_ctx
                .path_with_content(self.path.clone())
                .await?
                .is_file()
                .await?
            {
                return Ok(());
            }
        };

        Err(MononokeError::InvalidRequest(String::from(
            "Copy-from path must reference a file",
        )))
    }

    fn into_file_change(
        self,
        parent_ids: &[ChangesetId],
    ) -> Result<(MPath, ChangesetId), MononokeError> {
        let mpath = self.path.into_mpath().ok_or_else(|| {
            MononokeError::InvalidRequest(String::from("Copy-from path cannot be the root"))
        })?;
        let parent_id = parent_ids.get(self.parent_index).ok_or_else(|| {
            MononokeError::InvalidRequest(format!(
                "Parent index '{}' out of range for commit with {} parent(s)",
                self.parent_index,
                parent_ids.len()
            ))
        })?;
        Ok((mpath, *parent_id))
    }
}

/// Description of a change to make to a file.
#[derive(Clone)]
pub enum CreateChange {
    /// The file is created or modified to contain new data.
    Tracked(CreateChangeFile, Option<CreateCopyInfo>),

    /// A new file is modified in the working copy
    Untracked(CreateChangeFile),

    /// The file is not present in the working copy
    UntrackedDeletion,

    /// The file is marked as deleted
    Deletion,
}

#[derive(Clone)]
pub enum CreateChangeFile {
    // Upload content from bytes
    New {
        bytes: Bytes,
        file_type: FileType,
    },
    // Use already uploaded content
    Existing {
        file_id: FileId,
        file_type: FileType,
        // If not present, will be fetched from the blobstore
        maybe_size: Option<u64>,
    },
}

// Enum for recording whether a path is not changed, changed or deleted.
#[derive(Copy, Clone, Default, Eq, PartialEq, Debug)]
enum CreateChangeType {
    #[default]
    None,
    Change,
    Deletion,
}

impl CreateChangeType {
    fn is_modification(&self) -> bool {
        match self {
            Self::None => false,
            Self::Change => true,
            Self::Deletion => true,
        }
    }
}

impl CreateChange {
    async fn resolve(
        &mut self,
        ctx: &CoreContext,
        filestore_config: FilestoreConfig,
        repo_blobstore: RepoBlobstore,
        stack_changes: Option<&PathTree<CreateChangeType>>,
        stack_parents: &[ChangesetContext],
    ) -> Result<(), MononokeError> {
        let file = match self {
            CreateChange::Tracked(file, copy_info) => {
                if let Some(copy_info) = copy_info {
                    copy_info.check_valid(stack_changes, stack_parents).await?;
                }
                file
            }
            CreateChange::Untracked(file) => file,
            CreateChange::UntrackedDeletion | CreateChange::Deletion => return Ok(()),
        };
        match file {
            CreateChangeFile::New { bytes, file_type } => {
                let meta = filestore::store(
                    &repo_blobstore,
                    filestore_config,
                    ctx,
                    &StoreRequest::new(bytes.len() as u64),
                    stream::once(async move { Ok(bytes.clone()) }),
                )
                .await?;
                let file_type = *file_type;
                *file = CreateChangeFile::Existing {
                    file_id: meta.content_id,
                    file_type,
                    maybe_size: Some(meta.total_size),
                };
            }
            CreateChangeFile::Existing {
                file_id,
                maybe_size,
                ..
            } => {
                if maybe_size.is_none() {
                    let size = filestore::get_metadata(
                        &repo_blobstore,
                        ctx,
                        &FetchKey::Canonical(*file_id),
                    )
                    .await?
                    .ok_or_else(|| {
                        MononokeError::InvalidRequest(format!(
                            "File id '{}' is not available in this repo",
                            file_id
                        ))
                    })?
                    .total_size;
                    *maybe_size = Some(size);
                }
            }
        }
        Ok(())
    }

    pub fn into_file_change(self, parent_ids: &[ChangesetId]) -> Result<FileChange, MononokeError> {
        match self {
            CreateChange::Tracked(
                CreateChangeFile::Existing {
                    file_id,
                    file_type,
                    maybe_size: Some(size),
                },
                copy_info,
            ) => Ok(FileChange::tracked(
                file_id,
                file_type,
                size,
                copy_info
                    .map(|copy_info| copy_info.into_file_change(parent_ids))
                    .transpose()?,
            )),
            CreateChange::Untracked(CreateChangeFile::Existing {
                file_id,
                file_type,
                maybe_size: Some(size),
            }) => Ok(FileChange::untracked(file_id, file_type, size)),
            CreateChange::UntrackedDeletion => Ok(FileChange::UntrackedDeletion),
            CreateChange::Deletion => Ok(FileChange::Deletion),
            _ => Err(anyhow!("Programming error: create change must be resolved first").into()),
        }
    }

    fn change_type(&self) -> CreateChangeType {
        match self {
            CreateChange::Deletion | CreateChange::UntrackedDeletion => CreateChangeType::Deletion,
            CreateChange::Tracked(..) | CreateChange::Untracked(..) => CreateChangeType::Change,
        }
    }
}

/// Commit info for a newly created commit.
pub struct CreateInfo {
    pub author: String,
    pub author_date: DateTime<FixedOffset>,
    pub committer: Option<String>,
    pub committer_date: Option<DateTime<FixedOffset>>,
    pub message: String,
    pub extra: BTreeMap<String, Vec<u8>>,
    pub git_extra_headers: Option<BTreeMap<SmallVec<[u8; 24]>, Bytes>>,
}

/// Verify that all deleted files existed in at least one of the parents.
async fn verify_deleted_files_existed_in_a_parent(
    parent_ctxs: &[ChangesetContext],
    stack_changes: Option<&PathTree<CreateChangeType>>,
    mut deleted_files: BTreeSet<MononokePath>,
) -> Result<(), MononokeError> {
    async fn get_matching_files<'a>(
        parent_ctx: &'a ChangesetContext,
        files: &'a BTreeSet<MononokePath>,
    ) -> Result<impl Stream<Item = Result<MononokePath, MononokeError>> + 'a, MononokeError> {
        Ok(parent_ctx
            .paths(files.iter().cloned())
            .await?
            .try_filter_map(|changeset_path| async move {
                if changeset_path.is_file().await? {
                    Ok(Some(changeset_path.path().clone()))
                } else {
                    Ok(None)
                }
            })
            .boxed())
    }

    if let Some(stack_changes) = stack_changes {
        // Ignore files that were created or modified earlier in the stack.
        deleted_files.retain(|deleted_file| {
            stack_changes.get(deleted_file.as_mpath()) != Some(&CreateChangeType::Change)
        });

        for deleted_file in deleted_files.iter() {
            // It's an error if this file was already deleted, or if any of
            // its path prefixes were created (this implicitly deletes the
            // directory).
            if stack_changes.get(deleted_file.as_mpath()) == Some(&CreateChangeType::Deletion) {
                return Err(MononokeError::InvalidRequest(format!(
                    "Deleted file '{}' was deleted earlier in the stack",
                    deleted_file
                )));
            }
            for prefix in deleted_file.prefixes() {
                if let Some(CreateChangeType::Change) = stack_changes.get(prefix.as_mpath()) {
                    return Err(MononokeError::InvalidRequest(format!(
                        "Deleted file '{}' was deleted earlier in the stack through replacement of '{}'",
                        deleted_file, prefix
                    )));
                }
            }
        }
    }

    // Filter the deleted files to those that existed in a parent.
    let deleted_files = &deleted_files;
    let parent_files: BTreeSet<_> = stream::iter(
        parent_ctxs
            .iter()
            .map(|parent_ctx| async move { get_matching_files(parent_ctx, deleted_files).await }),
    )
    .boxed()
    .buffered(10)
    .try_flatten()
    .try_collect()
    .await?;

    // Quickly check if all deleted files existed by comparing set lengths.
    if deleted_files.len() == parent_files.len() {
        Ok(())
    } else {
        // At least one deleted file didn't exist. Find out which ones to
        // give a good error message.
        let non_existent_path = deleted_files
            .difference(&parent_files)
            .next()
            .expect("at least one file did not exist");
        let path_count = deleted_files.len().saturating_sub(parent_files.len());
        if path_count == 1 {
            Err(MononokeError::InvalidRequest(format!(
                "Deleted file '{}' does not exist in any parent",
                non_existent_path
            )))
        } else {
            Err(MononokeError::InvalidRequest(format!(
                "{} deleted files ('{}', ...) do not exist in any parent",
                path_count, non_existent_path
            )))
        }
    }
}

/// Returns `true` if any prefix of the path has a change.  Use for
/// detecting when a directory is replaced by a file.
fn is_prefix_changed(path: &MononokePath, paths: &PathTree<CreateChangeType>) -> bool {
    path.prefixes()
        .any(|prefix| paths.get(prefix.as_mpath()) == Some(&CreateChangeType::Change))
}

/// Verify that any files in `prefix_paths` that exist in any of
/// `parent_ctxs`, as modified by the existing stack changes, have been marked
/// as deleted in `path_changes`.
async fn verify_prefix_files_deleted(
    parent_ctxs: &[ChangesetContext],
    stack_changes: Option<&PathTree<CreateChangeType>>,
    mut prefix_paths: BTreeSet<MononokePath>,
    path_changes: &PathTree<CreateChangeType>,
) -> Result<(), MononokeError> {
    if let Some(stack_changes) = stack_changes {
        // Remove any prefix paths that have already been deleted earlier in the stack.
        prefix_paths.retain(|prefix_path| {
            stack_changes.get(prefix_path.as_mpath()) != Some(&CreateChangeType::Deletion)
        });
        // Check that any prefix path added earlier in the stack is being deleted.
        for prefix_path in prefix_paths.iter() {
            if stack_changes.get(prefix_path.as_mpath()) == Some(&CreateChangeType::Change)
                && path_changes.get(prefix_path.as_mpath()) != Some(&CreateChangeType::Deletion)
            {
                return Err(MononokeError::InvalidRequest(format!(
                    concat!(
                        "Creating files inside '{}' requires deleting the file ",
                        "added earlier in the stack at that path"
                    ),
                    prefix_path
                )));
            }
        }
    }
    // Check that any prefix path that exists in any parent is being deleted.
    borrowed!(prefix_paths);
    stream::iter(parent_ctxs.iter().map(Ok))
        .try_for_each_concurrent(10, |parent_ctx| async move {
            parent_ctx
                .paths(prefix_paths.iter().cloned())
                .await?
                .try_for_each(|prefix_path| async move {
                    if prefix_path.is_file().await?
                        && path_changes.get(prefix_path.path().as_mpath())
                            != Some(&CreateChangeType::Deletion)
                    {
                        Err(MononokeError::InvalidRequest(format!(
                            "Creating files inside '{}' requires deleting the file at that path",
                            prefix_path.path()
                        )))
                    } else {
                        Ok(())
                    }
                })
                .await
        })
        .await
}

async fn check_addless_union_conflicts(
    ctx: &CoreContext,
    repo_blobstore: RepoBlobstore,
    changesets: &[ChangesetContext],
    fix_paths: &PathTree<CreateChangeType>,
) -> Result<(), MononokeError> {
    if changesets.len() < 2 {
        return Ok(());
    }

    let root_fsnodes: Vec<_> = stream::iter(changesets.iter().map(|cs_ctx| async move {
        Ok::<_, MononokeError>(cs_ctx.root_fsnode_id().await?.into_fsnode_id())
    }))
    .boxed()
    .buffered(10)
    .try_collect()
    .await?;

    let store = &repo_blobstore;

    let conflict_paths = bounded_traversal::bounded_traversal_stream(
        256,
        Some((root_fsnodes, MononokePath::new(None))),
        move |(fsnodes_to_check, current_path)| {
            Box::pin(async move {
                let mut leaf_content: BTreeMap<MPathElement, HashSet<_>> = BTreeMap::new();
                let mut trees: BTreeMap<MPathElement, BTreeSet<_>> = BTreeMap::new();

                for fsnode in fsnodes_to_check {
                    let fsnode = fsnode.load(ctx, store).await?;
                    for (path_element, entry) in fsnode.list() {
                        match entry {
                            FsnodeEntry::Directory(directory) => trees
                                .entry(path_element.clone())
                                .or_default()
                                .insert(*directory.id()),
                            FsnodeEntry::File(file) => leaf_content
                                .entry(path_element.clone())
                                .or_default()
                                .insert(*file),
                        };
                    }
                }

                // Conflict rules only apply to leaves. A path in `fix_paths` means no conflict
                //
                // Two rules:
                // 1. If there are multiple choices for content, then there's a conflict
                // 2. If there's a tree and a leaf for this path, then there's a conflict
                let conflicts: Vec<_> = leaf_content
                    .into_iter()
                    .filter_map(|(path_element, contents)| {
                        let path = current_path.append(&path_element);
                        let fix_exists = fix_paths
                            .get(path.as_mpath())
                            .map_or(false, CreateChangeType::is_modification);
                        let conflict_exists =
                            contents.len() > 1 || trees.contains_key(&path_element);
                        if !fix_exists && conflict_exists {
                            Some(path)
                        } else {
                            None
                        }
                    })
                    .collect();
                // Recurse into trees that might reveal more conflicts.
                // If we already have new content for a path, then we don't recurse into it
                let recurse: Vec<_> = trees
                    .into_iter()
                    .filter_map(|(path_element, fsnodes)| {
                        let path = current_path.append(&path_element);
                        let fix_exists = fix_paths
                            .get(path.as_mpath())
                            .map_or(false, CreateChangeType::is_modification);

                        if !fix_exists && fsnodes.len() > 1 {
                            Some((fsnodes.into_iter().collect(), path))
                        } else {
                            None
                        }
                    })
                    .collect();
                anyhow::Ok((conflicts, recurse))
            })
        },
    )
    .try_concat()
    .await?;

    if conflict_paths.is_empty() {
        Ok(())
    } else {
        Err(MononokeError::MergeConflicts { conflict_paths })
    }
}

impl RepoContext {
    pub(crate) async fn save_changesets(
        &self,
        changesets: Vec<BonsaiChangeset>,
        repo: &(impl ChangesetsRef + RepoBlobstoreRef + RepoIdentityRef + RepoConfigRef),
        bubble: Option<&Bubble>,
    ) -> Result<(), MononokeError> {
        let bubble_id = bubble.map(|x| x.bubble_id());
        let commit_infos = changesets
            .iter()
            .map(|changeset| CommitInfo::new(changeset, bubble_id))
            .collect();
        changesets_creation::save_changesets(self.ctx(), repo, changesets).await?;

        log_new_commits(self.ctx(), repo, None, commit_infos).await;

        Ok(())
    }

    /// Create a new changeset in the repository.
    ///
    /// The new changeset is created with the given metadata by unioning the
    /// contents of all parent changesets and then applying the provided
    /// changes on top.
    ///
    /// Note that:
    ///   - The changes must be internally consistent (there must be no path
    ///     conflicts between changed files).
    ///   - If a file in any parent changeset is being replaced by a directory
    ///     then that file must be deleted in the set of changes.
    ///   - If a directory in any parent changeset is being replaced by a file,
    ///     then the contents of the parent directory do not need to be deleted.
    ///     If deletions for the contents of the directory are included they will
    ///     be checked for correctness (the files must exist), but they will
    ///     otherwise be ignored.
    ///   - Any merge conflicts introduced by merging the parent changesets
    ///     must be resolved by a corresponding change in the set of changes.
    pub async fn create_changeset(
        &self,
        parents: Vec<ChangesetId>,
        info: CreateInfo,
        changes: BTreeMap<MononokePath, CreateChange>,
        // If some, this changeset is a snapshot. Currently unsupported to upload a
        // normal commit to a bubble, though can be easily added.
        bubble: Option<&Bubble>,
    ) -> Result<ChangesetContext, MononokeError> {
        let changesets = self
            .create_changeset_stack(parents, vec![info], vec![changes], bubble)
            .await?;
        changesets
            .into_iter()
            .exactly_one()
            .map_err(|e| anyhow!("Expected 1 changeset, but created {}", e.len()).into())
    }

    /// Create a new stack of changesets in the repository.
    ///
    /// The first new changeset is created with the given metadata by unioning the
    /// contents of all parent changesets and then applying the provided
    /// changes on top.  The subsequent changesets are then stacked on top of
    /// the first changeset.
    ///
    /// The requirements for `create_changeset` must be met for each changeset
    /// in the stack.
    pub async fn create_changeset_stack(
        &self,
        stack_parents: Vec<ChangesetId>,
        info_stack: Vec<CreateInfo>,
        changes_stack: Vec<BTreeMap<MononokePath, CreateChange>>,
        // If some, this changeset is a snapshot. Currently unsupported to upload a
        // normal commit to a bubble, though can be easily added.
        bubble: Option<&Bubble>,
    ) -> Result<Vec<ChangesetContext>, MononokeError> {
        self.start_write()?;
        self.authorization_context()
            .require_repo_write(
                self.ctx(),
                self.inner_repo(),
                RepoWriteOperation::CreateChangeset,
            )
            .await?;

        let allowed_no_parents = self
            .config()
            .source_control_service
            .permit_commits_without_parents;
        if !allowed_no_parents && stack_parents.is_empty() {
            return Err(MononokeError::InvalidRequest(String::from(
                "Changesets with no parents cannot be created",
            )));
        }

        // Obtain contexts for each of the parents (which should exist).
        let stack_parent_ctxs: Vec<_> =
            stream::iter(stack_parents.iter().map(|parent_id| async move {
                let parent_ctx = self
                    .changeset(ChangesetSpecifier::Bonsai(parent_id.clone()))
                    .await?
                    .ok_or_else(|| {
                        MononokeError::InvalidRequest(format!(
                            "Parent {} does not exist",
                            parent_id
                        ))
                    })?;
                Ok::<_, MononokeError>(parent_ctx)
            }))
            .boxed()
            .buffered(10)
            .try_collect()
            .await?;
        borrowed!(stack_parent_ctxs);

        // Collect together information about each new commit.

        // Extract the set of deleted files.
        let tracked_deletion_files_stack: Vec<BTreeSet<_>> = changes_stack
            .iter()
            .map(|changes| {
                changes
                    .iter()
                    .filter(|(_path, change)| matches!(change, CreateChange::Deletion))
                    .map(|(path, _change)| path.clone())
                    .collect()
            })
            .collect();

        // Build a path tree recording each path that has been created or deleted.
        let path_changes_stack: Vec<_> = changes_stack
            .iter()
            .map(|changes| {
                PathTree::from_iter(
                    changes
                        .iter()
                        .map(|(path, change)| (path.as_mpath().cloned(), change.change_type())),
                )
            })
            .collect();
        let path_changes_stack = path_changes_stack.as_slice();

        // Determine the prefixes of all changed files.
        let prefix_paths_stack: Vec<BTreeSet<_>> = changes_stack
            .iter()
            .map(|changes| {
                changes
                    .iter()
                    .filter(|(_path, change)| change.change_type() == CreateChangeType::Change)
                    .flat_map(|(path, _change)| path.clone().prefixes())
                    .collect()
            })
            .collect();

        // Convert change paths into the form needed for the bonsai changeset.
        let changes_stack: Vec<Vec<(MPath, CreateChange)>> = changes_stack
            .into_iter()
            .zip(path_changes_stack.iter())
            .map(|(changes, path_changes)| {
                changes
                    .into_iter()
                    // Filter deletions that have a change at a path prefix. The
                    // deletion is implicit from the change. (2)
                    .filter(|(path, change)| {
                        change.change_type() != CreateChangeType::Deletion
                            || !is_prefix_changed(path, path_changes)
                    })
                    // Then convert the paths to MPaths. Do this before we start
                    // resolving any changes, so that we don't start storing data
                    // until we're happy that the changes are valid.
                    .map(|(path, change)| {
                        path.into_mpath()
                            .ok_or_else(|| {
                                MononokeError::InvalidRequest(String::from(
                                    "Cannot create a file with an empty path",
                                ))
                            })
                            .map(move |mpath| (mpath, change))
                    })
                    .collect::<Result<_, _>>()
            })
            .collect::<Result<_, _>>()?;

        // Track the changes already made so far at each step in the stack.
        let stack_changes_stack = {
            let mut stack_changes_stack = vec![None];
            let mut stack_changes = PathTree::default();
            for (index, path_changes) in path_changes_stack.iter().enumerate() {
                if index < path_changes_stack.len() - 1 {
                    for (path, change) in path_changes.clone().into_iter() {
                        match change {
                            CreateChangeType::Change => {
                                stack_changes.insert_and_prune(path, change);
                            }
                            CreateChangeType::Deletion => {
                                stack_changes.insert(path, change);
                            }
                            CreateChangeType::None => {}
                        }
                    }
                    stack_changes_stack.push(Some(stack_changes.clone()))
                }
            }
            stack_changes_stack
        };
        let stack_changes_stack = stack_changes_stack.as_slice();

        // Check that changes are valid according to bonsai rules:
        // (1) deletions and copy-from info must reference a real path in a
        //     valid parent.
        // (2) deletions for paths where a prefix directory has been replaced
        //     by a file should be dropped, as the deletion is implicit from the
        //     file change for the prefix path.
        // (3) conversely, when a file has been replaced by a directory, there
        //     must be a delete for the file.

        // Check deleted files existed in a parent. (1)
        let verify_deleted_files_existed_fut = async move {
            stream::iter(
                tracked_deletion_files_stack
                    .into_iter()
                    .zip(stack_changes_stack.iter())
                    .map(Ok),
            )
            .try_for_each_concurrent(10, |(tracked_deletion_files, stack_changes)| async move {
                // This does NOT consider "missing" (untracked deletion) files as it is NOT
                // necessary for them to exist in a parent. If they don't exist on a parent,
                // this means the file was "hg added" and then manually deleted.
                let (stats, result) = verify_deleted_files_existed_in_a_parent(
                    stack_parent_ctxs,
                    stack_changes.as_ref(),
                    tracked_deletion_files,
                )
                .timed()
                .await;
                let mut scuba = self.ctx().scuba().clone();
                scuba.add_future_stats(&stats);
                scuba.log_with_msg("Verify deleted files existed in a parent", None);
                result
            })
            .await
        };

        // Check changes that replace a file with a directory also delete
        // this replaced file. (3)
        let verify_prefix_files_deleted_fut = async move {
            stream::iter(
                prefix_paths_stack
                    .into_iter()
                    .zip(path_changes_stack.iter())
                    .zip(stack_changes_stack.iter())
                    .map(Ok),
            )
            .try_for_each_concurrent(
                10,
                |((prefix_paths, path_changes), stack_changes)| async move {
                    let (stats, result) = verify_prefix_files_deleted(
                        stack_parent_ctxs,
                        stack_changes.as_ref(),
                        prefix_paths,
                        path_changes,
                    )
                    .timed()
                    .await;
                    let mut scuba = self.ctx().scuba().clone();
                    scuba.add_future_stats(&stats);
                    scuba.log_with_msg("Verify prefix files in parents have been deleted", None);
                    result
                },
            )
            .await
        };

        // Check for merge conflicts.  This only applies to the first commit
        // in a stack.
        let verify_no_merge_conflicts_fut = async {
            let (stats, result) = check_addless_union_conflicts(
                self.ctx(),
                match &bubble {
                    Some(bubble) => {
                        bubble.wrap_repo_blobstore(self.blob_repo().repo_blobstore().clone())
                    }
                    None => self.blob_repo().repo_blobstore().clone(),
                },
                stack_parent_ctxs,
                path_changes_stack
                    .first()
                    .ok_or_else(|| anyhow!("Should be at least one commit"))?,
            )
            .timed()
            .await;

            let mut scuba = self.ctx().scuba().clone();
            scuba.add_future_stats(&stats);
            scuba.log_with_msg("Verify all merge conflicts are resolved", None);
            result
        };

        // Resolve the changes so that they are ready to be converted into
        // bonsai changes. This also checks (1) for copy-from info.
        let blobstore = match &bubble {
            Some(bubble) => bubble.wrap_repo_blobstore(self.blob_repo().repo_blobstore().clone()),
            None => self.blob_repo().repo_blobstore().clone(),
        };
        borrowed!(blobstore);
        let resolve_file_changes_fut = async move {
            stream::iter(
                changes_stack
                    .into_iter()
                    .zip(stack_changes_stack.iter())
                    .map(|(changes, stack_changes)| async move {
                        let stack_changes = stack_changes.as_ref();
                        let (stats, result) = stream::iter(changes.into_iter().map(
                            |(path, mut change)| async move {
                                change
                                    .resolve(
                                        self.ctx(),
                                        *self.blob_repo().filestore_config(),
                                        blobstore.clone(),
                                        stack_changes,
                                        stack_parent_ctxs,
                                    )
                                    .await?;
                                Ok::<_, MononokeError>((path, change))
                            },
                        ))
                        .buffered(1000)
                        .try_collect::<SortedVectorMap<MPath, CreateChange>>()
                        .timed()
                        .await;
                        let mut scuba = self.ctx().scuba().clone();
                        scuba.add_future_stats(&stats);
                        scuba.log_with_msg(
                            "Convert create changeset parameters to bonsai changes",
                            None,
                        );
                        result
                    }),
            )
            .boxed()
            .buffered(10)
            .try_collect::<Vec<_>>()
            .await
        };

        let ((), (), (), file_changes_stack) = try_join!(
            verify_deleted_files_existed_fut,
            verify_prefix_files_deleted_fut,
            verify_no_merge_conflicts_fut,
            resolve_file_changes_fut,
        )?;

        let mut new_changesets = Vec::new();
        let mut new_changeset_ids = Vec::new();
        let mut parents = stack_parents;
        for (info, file_changes) in info_stack.into_iter().zip(file_changes_stack.into_iter()) {
            let author_date = MononokeDateTime::new(info.author_date);
            let committer_date = info.committer_date.map(MononokeDateTime::new);
            let hg_extra = info.extra.into();
            let git_extra_headers = info.git_extra_headers.map(SortedVectorMap::from);
            let file_changes = file_changes
                .into_iter()
                .map(|(path, change)| Ok((path, change.into_file_change(&parents)?)))
                .collect::<Result<SortedVectorMap<MPath, FileChange>, MononokeError>>()?;

            // Create the new Bonsai Changeset. The `freeze` method validates
            // that the bonsai changeset is internally consistent.

            let new_changeset = BonsaiChangesetMut {
                parents,
                author: info.author,
                author_date,
                committer: info.committer,
                committer_date,
                message: info.message,
                hg_extra,
                git_extra_headers,
                git_tree_hash: None,
                file_changes,
                is_snapshot: bubble.is_some(),
                git_annotated_tag: None,
            }
            .freeze()
            .map_err(|e| {
                MononokeError::InvalidRequest(format!(
                    "Changes create invalid bonsai changeset: {}",
                    e
                ))
            })?;

            let new_changeset_id = new_changeset.get_changeset_id();
            parents = vec![new_changeset_id];
            new_changesets.push(new_changeset);
            new_changeset_ids.push(new_changeset_id);
        }

        if let Some(bubble) = &bubble {
            self.save_changesets(
                new_changesets,
                &bubble.repo_view(self.inner_repo()),
                Some(bubble),
            )
            .await?;
        } else {
            self.save_changesets(new_changesets, self.inner_repo(), None)
                .await?;
        }

        Ok(new_changeset_ids
            .into_iter()
            .map(|new_changeset_id| ChangesetContext::new(self.clone(), new_changeset_id))
            .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::HashMap;

use anyhow::Context;
use bookmarks::BookmarkKey;
use bookmarks::BookmarkUpdateReason;
use bookmarks::BookmarksRef;
use bookmarks_movement::DeleteBookmarkOp;
use bytes::Bytes;
use mononoke_types::ChangesetId;

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

impl RepoContext {
    /// Delete a bookmark.
    pub async fn delete_bookmark(
        &self,
        bookmark: &BookmarkKey,
        old_target: Option<ChangesetId>,
        pushvars: Option<&HashMap<String, Bytes>>,
    ) -> Result<(), MononokeError> {
        self.start_write()?;

        // We need to find out where the bookmark currently points to in order
        // to delete it.  Make sure to bypass any out-of-date caches.
        let old_target = match old_target {
            Some(old_target) => old_target,
            None => self
                .blob_repo()
                .bookmarks()
                .get(self.ctx().clone(), bookmark)
                .await
                .context("Failed to fetch old bookmark target")?
                .ok_or_else(|| {
                    MononokeError::InvalidRequest(format!("bookmark '{}' does not exist", bookmark))
                })?,
        };

        fn make_delete_op<'a>(
            bookmark: &'a BookmarkKey,
            old_target: ChangesetId,
            pushvars: Option<&'a HashMap<String, Bytes>>,
        ) -> DeleteBookmarkOp<'a> {
            DeleteBookmarkOp::new(bookmark, old_target, BookmarkUpdateReason::ApiRequest)
                .with_pushvars(pushvars)
        }
        if let Some(redirector) = self.push_redirector.as_ref() {
            let large_bookmark = redirector.small_to_large_bookmark(bookmark).await?;
            if &large_bookmark == bookmark {
                return Err(MononokeError::InvalidRequest(format!(
                    "Cannot delete shared bookmark '{}' from small repo",
                    bookmark
                )));
            }
            let ctx = self.ctx();
            let old_target = redirector
                .get_small_to_large_commit_equivalent(ctx, old_target)
                .await?;
            make_delete_op(&large_bookmark, old_target, pushvars)
                .run(
                    self.ctx(),
                    self.authorization_context(),
                    redirector.repo.inner_repo(),
                )
                .await?;
            // Wait for bookmark to catch up on small repo
            redirector.backsync_latest(ctx).await?;
        } else {
            make_delete_op(bookmark, old_target, pushvars)
                .run(self.ctx(), self.authorization_context(), self.inner_repo())
                .await?;
        }

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

use std::collections::HashMap;

use anyhow::Context;
use blobstore::Blobstore;
use bonsai_git_mapping::BonsaiGitMappingEntry;
use bonsai_git_mapping::BonsaiGitMappingRef;
use bonsai_tag_mapping::BonsaiTagMappingEntry;
use bonsai_tag_mapping::BonsaiTagMappingRef;
use chrono::DateTime;
use chrono::FixedOffset;
use context::CoreContext;
use filestore::hash_bytes;
use filestore::Sha1IncrementalHasher;
use megarepo_error::cloneable_error;
use mononoke_types::bonsai_changeset::BonsaiAnnotatedTag;
use mononoke_types::hash::GitSha1;
use mononoke_types::BlobstoreBytes;
use mononoke_types::BonsaiChangesetMut;
use mononoke_types::DateTime as MononokeDateTime;
use thiserror::Error;

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

const HGGIT_MARKER_EXTRA: &str = "hg-git-rename-source";
const HGGIT_MARKER_VALUE: &[u8] = b"git";
const HGGIT_COMMIT_ID_EXTRA: &str = "convert_revision";
const GIT_OBJECT_PREFIX: &str = "git_object";
const SEPARATOR: &str = ".";

#[derive(Clone, Debug, Error)]
pub enum GitError {
    /// The provided hash and the derived hash do not match for the given content.
    #[error("Input hash {0} does not match the SHA1 hash {1} of the content")]
    HashMismatch(String, String),

    /// The input hash is not a valid SHA1 hash.
    #[error("Input hash {0} is not a valid SHA1 git hash")]
    InvalidHash(String),

    /// The raw object content provided do not correspond to a valid git object.
    #[error("Invalid git object content provided for object ID {0}. Cause: {1}")]
    InvalidContent(String, GitInternalError),

    /// The requested bubble does not exist.  Either it was never created or has expired.
    #[error(
        "The object corresponding to object ID {0} is a git blob. Cannot upload raw blob content"
    )]
    DisallowedBlobObject(String),

    /// Failed to get or store the git object in Mononoke store.
    #[error("Failed to get or store the git object (ID: {0}) in blobstore. Cause: {1}")]
    StorageFailure(String, GitInternalError),

    /// The git object doesn't exist in the Mononoke store.
    #[error("The object corresponding to object ID {0} does not exist in the data store")]
    NonExistentObject(String),

    /// The provided git object could not be converted to a valid bonsai changeset.
    #[error(
        "Validation failure while persisting git object (ID: {0}) as a bonsai changeset. Cause: {1}"
    )]
    InvalidBonsai(String, GitInternalError),
}

cloneable_error!(GitInternalError);

impl RepoContext {
    /// Set the bonsai to git mapping based on the changeset
    /// If the user is trusted, this will use the hggit extra
    /// Otherwise, it will only work if we can derive a git commit ID, and that ID matches the hggit extra
    /// or the hggit extra is missing from the changeset completely.
    pub async fn set_git_mapping_from_changeset(
        &self,
        changeset_ctx: &ChangesetContext,
    ) -> Result<(), MononokeError> {
        let mut extras: HashMap<_, _> = changeset_ctx.hg_extras().await?.into_iter().collect();

        //TODO(simonfar): Once we support deriving git commits, do derivation here
        // If there's no hggit extras, then give back the derived hash.
        // If there's a hggit extra, and it matches the derived commit, accept even if you
        // don't have permission

        if extras.get(HGGIT_MARKER_EXTRA).map(Vec::as_slice) == Some(HGGIT_MARKER_VALUE) {
            if let Some(hggit_sha1) = extras.remove(HGGIT_COMMIT_ID_EXTRA) {
                // We can't derive right now, so always do the permission check for
                // overriding in the case of mismatch.
                self.authorization_context()
                    .require_override_git_mapping(self.ctx(), self.inner_repo())
                    .await?;

                let hggit_sha1 = String::from_utf8_lossy(&hggit_sha1).parse()?;
                let entry = BonsaiGitMappingEntry::new(hggit_sha1, changeset_ctx.id());
                let mapping = self.inner_repo().bonsai_git_mapping();
                mapping
                    .bulk_add(self.ctx(), &[entry])
                    .await
                    .with_context(|| {
                        format!(
                            "Failed to set git mapping from changeset {}",
                            changeset_ctx.id()
                        )
                    })?;
            }
        }
        Ok(())
    }

    /// Upload serialized git objects
    pub async fn upload_git_object(
        &self,
        git_hash: &git_hash::oid,
        raw_content: Vec<u8>,
    ) -> anyhow::Result<(), GitError> {
        upload_git_object(&self.ctx, self.inner_repo(), git_hash, raw_content).await
    }

    /// Create Mononoke counterpart of Git tree object
    pub async fn create_git_tree(
        &self,
        git_tree_hash: &git_hash::oid,
    ) -> anyhow::Result<(), GitError> {
        create_git_tree(&self.ctx, self.inner_repo(), git_tree_hash).await
    }

    /// Create a new annotated tag in the repository.
    pub async fn create_annotated_tag(
        &self,
        name: String,
        author: Option<String>,
        author_date: Option<DateTime<FixedOffset>>,
        annotation: String,
        annotated_tag: BonsaiAnnotatedTag,
    ) -> Result<ChangesetContext, GitError> {
        let new_changeset_id = create_annotated_tag(
            self.ctx(),
            self.inner_repo(),
            name,
            author,
            author_date,
            annotation,
            annotated_tag,
        )
        .await?;

        Ok(ChangesetContext::new(self.clone(), new_changeset_id))
    }
}

/// Free function for uploading serialized git objects
pub async fn upload_git_object(
    ctx: &CoreContext,
    repo: &impl repo_blobstore::RepoBlobstoreRef,
    git_hash: &git_hash::oid,
    raw_content: Vec<u8>,
) -> anyhow::Result<(), GitError> {
    // Check if the provided Sha1 hash (i.e. ObjectId) of the bytes actually corresponds to the hash of the bytes
    let bytes = bytes::Bytes::from(raw_content);
    let sha1_hash = hash_bytes(Sha1IncrementalHasher::new(), &bytes);
    if sha1_hash.as_ref() != git_hash.as_bytes() {
        return Err(GitError::HashMismatch(
            git_hash.to_hex().to_string(),
            sha1_hash.to_hex().to_string(),
        ));
    };
    // Check if the bytes actually correspond to a valid Git object
    let blobstore_bytes = BlobstoreBytes::from_bytes(bytes.clone());
    let git_obj = git_object::ObjectRef::from_loose(bytes.as_ref()).map_err(|e| {
        GitError::InvalidContent(
            git_hash.to_hex().to_string(),
            anyhow::anyhow!(e.to_string()).into(),
        )
    })?;
    // Check if the git object is not a raw content blob. Raw content blobs are uploaded directly through
    // LFS. This method supports git commits, trees, tags, notes and similar pointer objects.
    if let git_object::ObjectRef::Blob(_) = git_obj {
        return Err(GitError::DisallowedBlobObject(
            git_hash.to_hex().to_string(),
        ));
    }
    // The bytes are valid, upload to blobstore with the key:
    // git_object_{hex-value-of-hash}
    let blobstore_key = format!("{}{}{}", GIT_OBJECT_PREFIX, SEPARATOR, git_hash.to_hex());
    repo.repo_blobstore()
        .put(ctx, blobstore_key, blobstore_bytes)
        .await
        .map_err(|e| GitError::StorageFailure(git_hash.to_hex().to_string(), e.into()))
}

/// Free function for creating Mononoke counterpart of Git tree object
pub async fn create_git_tree(
    ctx: &CoreContext,
    repo: &(impl changesets::ChangesetsRef + repo_blobstore::RepoBlobstoreRef),
    git_tree_hash: &git_hash::oid,
) -> anyhow::Result<(), GitError> {
    let blobstore_key = format!(
        "{}{}{}",
        GIT_OBJECT_PREFIX,
        SEPARATOR,
        git_tree_hash.to_hex()
    );
    // Before creating the Mononoke version of the git tree, validate if the raw git
    // tree is stored in the blobstore
    let get_result = repo
        .repo_blobstore()
        .get(ctx, &blobstore_key)
        .await
        .map_err(|e| GitError::StorageFailure(git_tree_hash.to_hex().to_string(), e.into()))?;
    if get_result.is_none() {
        return Err(GitError::NonExistentObject(
            git_tree_hash.to_hex().to_string(),
        ));
    }
    let mut changeset = BonsaiChangesetMut::default();
    // Get git hash from tree object ID
    let git_hash = GitSha1::from_bytes(git_tree_hash.as_bytes())
        .map_err(|_| GitError::InvalidHash(git_tree_hash.to_hex().to_string()))?;
    // Store hash in the changeset
    changeset.git_tree_hash = Some(git_hash);
    // Freeze the changeset to determine if there are any errors
    let changeset = changeset
        .freeze()
        .map_err(|e| GitError::InvalidBonsai(git_tree_hash.to_hex().to_string(), e.into()))?;

    // Store the created changeset
    changesets_creation::save_changesets(ctx, repo, vec![changeset])
        .await
        .map_err(|e| GitError::StorageFailure(git_tree_hash.to_hex().to_string(), e.into()))
}

/// Free function for creating a new annotated tag in the repository.
///
/// Annotated tags are bookmarks of category `Tag` or `Note` which point to one of these
/// annotated tag changesets.
/// Bookmarks of category `Tag` can also represent lightweight tags, pointing directly to
/// a changeset representing a commit.
/// Bookmarks of category `Note` can only represent annotated tags.
/// Bookmarks of category `Branch` are never annotated.
pub async fn create_annotated_tag(
    ctx: &CoreContext,
    repo: &(impl changesets::ChangesetsRef + repo_blobstore::RepoBlobstoreRef + BonsaiTagMappingRef),
    name: String,
    author: Option<String>,
    author_date: Option<DateTime<FixedOffset>>,
    annotation: String,
    annotated_tag: BonsaiAnnotatedTag,
) -> Result<mononoke_types::ChangesetId, GitError> {
    let tag_id = format!("{:?}", annotated_tag);

    // Create the new Bonsai Changeset. The `freeze` method validates
    // that the bonsai changeset is internally consistent.
    let mut changeset = BonsaiChangesetMut {
        message: annotation,
        git_annotated_tag: Some(annotated_tag),
        ..Default::default()
    };
    if let Some(author) = author {
        changeset.author = author;
    }
    if let Some(author_date) = author_date {
        changeset.author_date = MononokeDateTime::new(author_date);
    }

    let changeset = changeset
        .freeze()
        .map_err(|e| GitError::InvalidBonsai(tag_id.clone(), e.into()))?;

    let changeset_id = changeset.get_changeset_id();
    // Store the created changeset
    changesets_creation::save_changesets(ctx, repo, vec![changeset])
        .await
        .map_err(|e| GitError::StorageFailure(tag_id.clone(), e.into()))?;
    // Create a mapping between the tag name and the metadata changeset
    let mapping_entry = BonsaiTagMappingEntry {
        changeset_id,
        tag_name: name,
    };
    repo.bonsai_tag_mapping()
        .add_mappings(vec![mapping_entry])
        .await
        .map_err(|e| GitError::StorageFailure(tag_id, e.into()))?;
    Ok(changeset_id)
}


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

use std::collections::HashMap;
use std::collections::HashSet;

use blobstore::Loadable;
use bookmarks::BookmarkKey;
use bookmarks_movement::BookmarkKindRestrictions;
pub use bookmarks_movement::PushrebaseOutcome;
use bytes::Bytes;
use cloned::cloned;
use commit_graph::CommitGraphRef;
use cross_repo_sync::types::Large;
use cross_repo_sync::types::Small;
use cross_repo_sync::CommitSyncOutcome;
use futures::future;
use futures::future::TryFutureExt;
use futures::stream::StreamExt;
use futures::stream::TryStreamExt;
use hooks::CrossRepoPushSource;
use hooks::HookManagerRef;
use hooks::PushAuthoredBy;
use mononoke_types::ChangesetId;
use pushrebase_client::LocalPushrebaseClient;
use pushrebase_client::PushrebaseClient;
use repo_blobstore::RepoBlobstoreRef;
use unbundle::PushRedirector;

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

impl RepoContext {
    async fn convert_old_bookmark_value(
        &self,
        redirector: &PushRedirector<Repo>,
        bookmark_value: Large<Option<ChangesetId>>,
    ) -> anyhow::Result<Small<Option<ChangesetId>>> {
        let large_cs_id = match bookmark_value {
            Large(Some(cs_id)) => cs_id,
            Large(None) => return Ok(Small(None)),
        };
        let syncer = &redirector.large_to_small_commit_syncer;
        match syncer
            .get_commit_sync_outcome(self.ctx(), large_cs_id)
            .await?
        {
            None => anyhow::bail!(
                "Unexpected absence of CommitSyncOutcome for {} in {:?}",
                large_cs_id,
                syncer
            ),
            // EquivalentWorkingCopyAncestor is fine because the bookmark commit in the
            // large repo might not have come from the small repo
            Some(CommitSyncOutcome::RewrittenAs(small_cs_id, _))
            | Some(CommitSyncOutcome::EquivalentWorkingCopyAncestor(small_cs_id, _)) => {
                Ok(Small(Some(small_cs_id)))
            }
            Some(outcome) => anyhow::bail!(
                "Unexpected CommitSyncOutcome for {} in {:?}: {:?}",
                large_cs_id,
                syncer,
                outcome
            ),
        }
    }
    async fn convert_outcome(
        &self,
        redirector: &PushRedirector<Repo>,
        outcome: Large<PushrebaseOutcome>,
        _bookmark: BookmarkKey,
    ) -> Result<Small<PushrebaseOutcome>, MononokeError> {
        let ctx = self.ctx();
        let Large(PushrebaseOutcome {
            old_bookmark_value,
            head,
            retry_num,
            rebased_changesets,
            pushrebase_distance,
        }) = outcome;
        redirector.backsync_latest(ctx).await?;

        // Convert all fields from large to small repo
        let (Small(old_bookmark_value), head, rebased_changesets) = futures::try_join!(
            self.convert_old_bookmark_value(redirector, Large(old_bookmark_value)),
            redirector.get_large_to_small_commit_equivalent(ctx, head),
            redirector.convert_pushrebased_changesets(ctx, rebased_changesets)
        )?;

        Ok(Small(PushrebaseOutcome {
            old_bookmark_value,
            head,
            retry_num,
            rebased_changesets,
            pushrebase_distance,
        }))
    }

    /// Land a stack of commits to a bookmark via pushrebase.
    pub async fn land_stack(
        &self,
        bookmark: impl AsRef<str>,
        head: ChangesetId,
        base: ChangesetId,
        pushvars: Option<&HashMap<String, Bytes>>,
        bookmark_restrictions: BookmarkKindRestrictions,
        push_authored_by: PushAuthoredBy,
    ) -> Result<PushrebaseOutcome, MononokeError> {
        self.start_write()?;

        let bookmark = bookmark.as_ref();
        let bookmark = BookmarkKey::new(bookmark)?;

        // Check that base is an ancestor of the head commit, and fail with an
        // appropriate error message if that's not the case.
        if !self
            .repo()
            .commit_graph()
            .is_ancestor(self.ctx(), base, head)
            .await?
        {
            return Err(MononokeError::InvalidRequest(format!(
                "Not a stack: base commit {} is not an ancestor of head commit {}",
                base, head,
            )));
        }

        // Find the commits we are interested in, and load their bonsai
        // changesets.   These are the commits that are ancestors of the head
        // commit and descendants of the base commit.
        let ctx = self.ctx();
        let blobstore = self.blob_repo().repo_blobstore();
        let changesets: HashSet<_> = self
            .repo()
            .commit_graph()
            .range_stream(ctx, base, head)
            .await?
            .filter(|cs_id| future::ready(*cs_id != base))
            .map(|cs_id| {
                cloned!(ctx);
                async move {
                    cs_id
                        .load(&ctx, blobstore)
                        .map_err(MononokeError::from)
                        .await
                }
            })
            .buffer_unordered(100)
            .try_collect()
            .await?;

        // We CANNOT do remote pushrebase here otherwise it would result in an infinite
        // loop, as this code is used for remote pushrebase. Let's use local pushrebase.

        let outcome = if let Some(redirector) = self.push_redirector.as_ref() {
            // run hooks on small repo
            bookmarks_movement::run_hooks(
                ctx,
                self.hook_manager().as_ref(),
                &bookmark,
                changesets.iter(),
                pushvars,
                CrossRepoPushSource::NativeToThisRepo,
                push_authored_by,
            )
            .await?;
            // Convert changesets to large repo
            let large_bookmark = redirector.small_to_large_bookmark(&bookmark).await?;
            let small_to_large = redirector
                .sync_uploaded_changesets(ctx, changesets, Some(&large_bookmark))
                .await?;
            // Land the mapped changesets on the large repo
            let outcome = LocalPushrebaseClient {
                ctx: self.ctx(),
                authz: self.authorization_context(),
                repo: &redirector.repo.inner,
                hook_manager: redirector.repo.hook_manager(),
            }
            .pushrebase(
                &large_bookmark,
                small_to_large.into_values().collect(),
                pushvars,
                CrossRepoPushSource::PushRedirected,
                bookmark_restrictions,
                true, // log_new_public_commits_to_scribe
            )
            .await?;
            // Convert response back, finishing the land on the small repo
            self.convert_outcome(redirector, Large(outcome), bookmark)
                .await?
                .0
        } else {
            LocalPushrebaseClient {
                ctx: self.ctx(),
                authz: self.authorization_context(),
                repo: self.inner_repo(),
                hook_manager: self.hook_manager().as_ref(),
            }
            .pushrebase(
                &bookmark,
                changesets,
                pushvars,
                CrossRepoPushSource::NativeToThisRepo,
                bookmark_restrictions,
                true, // log_new_public_commits_to_scribe
            )
            .await?
        };

        Ok(outcome)
    }
}


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

use std::collections::HashMap;

use anyhow::Context;
use bookmarks::BookmarkKey;
use bookmarks::BookmarkUpdateReason;
use bookmarks::BookmarksRef;
use bookmarks_movement::BookmarkUpdatePolicy;
use bookmarks_movement::BookmarkUpdateTargets;
use bookmarks_movement::UpdateBookmarkOp;
use bytes::Bytes;
use hooks::HookManagerRef;
use mononoke_types::ChangesetId;
use tunables::tunables;

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

impl RepoContext {
    /// Move a bookmark.
    pub async fn move_bookmark(
        &self,
        bookmark: &BookmarkKey,
        target: ChangesetId,
        old_target: Option<ChangesetId>,
        allow_non_fast_forward: bool,
        pushvars: Option<&HashMap<String, Bytes>>,
    ) -> Result<(), MononokeError> {
        self.start_write()?;

        // We need to find out where the bookmark currently points to in order
        // to move it.  Make sure to bypass any out-of-date caches.
        let old_target = match old_target {
            Some(old_target) => old_target,
            None => self
                .blob_repo()
                .bookmarks()
                .get(self.ctx().clone(), bookmark)
                .await
                .context("Failed to fetch old bookmark target")?
                .ok_or_else(|| {
                    MononokeError::InvalidRequest(format!("bookmark '{}' does not exist", bookmark))
                })?,
        };

        fn make_move_op<'a>(
            bookmark: &'a BookmarkKey,
            target: ChangesetId,
            old_target: ChangesetId,
            allow_non_fast_forward: bool,
            pushvars: Option<&'a HashMap<String, Bytes>>,
        ) -> UpdateBookmarkOp<'a> {
            let mut op = UpdateBookmarkOp::new(
                bookmark,
                BookmarkUpdateTargets {
                    old: old_target,
                    new: target,
                },
                if allow_non_fast_forward {
                    BookmarkUpdatePolicy::AnyPermittedByConfig
                } else {
                    BookmarkUpdatePolicy::FastForwardOnly
                },
                BookmarkUpdateReason::ApiRequest,
            )
            .with_pushvars(pushvars);
            if !tunables()
                .disable_commit_scribe_logging_scs()
                .unwrap_or_default()
            {
                op = op.log_new_public_commits_to_scribe();
            }
            op
        }
        if let Some(redirector) = self.push_redirector.as_ref() {
            let large_bookmark = redirector.small_to_large_bookmark(bookmark).await?;
            if &large_bookmark == bookmark {
                return Err(MononokeError::InvalidRequest(format!(
                    "Cannot move shared bookmark '{}' from small repo",
                    bookmark
                )));
            }
            let ctx = self.ctx();
            let (target, old_target) = futures::try_join!(
                redirector.get_small_to_large_commit_equivalent(ctx, target),
                redirector.get_small_to_large_commit_equivalent(ctx, old_target),
            )?;
            make_move_op(
                &large_bookmark,
                target,
                old_target,
                allow_non_fast_forward,
                pushvars,
            )
            .run(
                self.ctx(),
                self.authorization_context(),
                redirector.repo.inner_repo(),
                redirector.repo.hook_manager(),
            )
            .await?;
            // Wait for bookmark to catch up on small repo
            redirector.backsync_latest(ctx).await?;
        } else {
            make_move_op(
                bookmark,
                target,
                old_target,
                allow_non_fast_forward,
                pushvars,
            )
            .run(
                self.ctx(),
                self.authorization_context(),
                self.inner_repo(),
                self.hook_manager().as_ref(),
            )
            .await?;
        }

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

use std::collections::HashMap;

use anyhow::Context;
use bonsai_git_mapping::BonsaiGitMappingEntry;
use bonsai_git_mapping::BonsaiGitMappingRef;
use itertools::Itertools;

use crate::changeset::ChangesetContext;
use crate::errors::MononokeError;
use crate::permissions::WritePermissionsModel;
use crate::repo_draft::RepoDraftContext;

const HGGIT_MARKER_EXTRA: &str = "hg-git-rename-source";
const HGGIT_MARKER_VALUE: &[u8] = b"git";
const HGGIT_COMMIT_ID_EXTRA: &str = "convert_revision";

impl RepoDraftContext {
    /// Set the bonsai to git mapping based on the changeset
    /// If the user is trusted, this will use the hggit extra
    /// Otherwise, it will only work if we can derive a git commit ID, and that ID matches the hggit extra
    /// or the hggit extra is missing from the changeset completely.
    pub async fn set_git_mapping_from_changeset(
        &self,
        changeset_ctx: &ChangesetContext,
    ) -> Result<(), MononokeError> {
        let mut extras: HashMap<_, _> = changeset_ctx.extras().await?.into_iter().collect();

        //TODO(simonfar): Once we support deriving git commits, do derivation here
        // If there's no hggit extras, then give back the derived hash.
        // If there's a hggit extra, and it matches the derived commit, accept even if you
        // don't have permission

        if extras.get(HGGIT_MARKER_EXTRA).map(Vec::as_slice) == Some(HGGIT_MARKER_VALUE) {
            if let Some(hggit_sha1) = extras.remove(HGGIT_COMMIT_ID_EXTRA) {
                // We can't derive right now, so always do the permission check
                self.check_method_permitted("set_git_mapping_from_changeset")?;
                // TODO(simonfar): Relax this to a hipster group check when @mbthomas lands repo_authorization rework
                if let WritePermissionsModel::AllowAnyWrite = self.permissions_model {
                    let identities = self.repo.ctx().metadata().identities();
                    let identities = if identities.is_empty() {
                        "<none>".to_string()
                    } else {
                        identities.iter().join(",")
                    };
                    return Err(MononokeError::PermissionDenied {
                        mode: "write git mapping".to_string(),
                        identities,
                        reponame: self.repo.name().to_string(),
                    });
                }

                let hggit_sha1 = String::from_utf8_lossy(&hggit_sha1).parse()?;
                let entry = BonsaiGitMappingEntry::new(hggit_sha1, changeset_ctx.id());
                let mapping = self.repo.inner_repo().bonsai_git_mapping();
                mapping
                    .bulk_add(self.repo.ctx(), &[entry])
                    .await
                    .context("set_git_mapping_from_changeset")?;
            }
        }

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

mod test_blame;
mod test_changeset_diff;
mod test_file_diff;
mod test_git;
mod test_history;
mod test_repo;
mod test_repo_bookmarks;
mod test_repo_create_changeset;
mod test_repo_create_changeset_stack;
mod test_repo_land_stack;
mod test_repo_modify_bookmarks;
mod test_sparse_profile;


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

use std::collections::HashMap;
use std::sync::Arc;

use anyhow::Result;
use context::CoreContext;
use fbinit::FacebookInit;
use mononoke_types::DateTime;
use mononoke_types::MPath;
use mutable_renames::MutableRenameEntry;
use pretty_assertions::assert_eq;
use tests_utils::CreateCommitContext;

use crate::changeset_path::ChangesetPathHistoryContext;
use crate::ChangesetId;
use crate::RepoContext;

// Generates this commit graph:
//

// o "c2"
// |
// o "c1"
// |
// o   "m1"
// |\
// o | "b3"
// | |
// o | "b2"
// | |
// o | "b1"
//   |
//   o "a3"
//   |
//   o "a2"
//   |
//   o "a1"
//
// a commits have as many lines in a as the number
// b commits have as many lines in b as the number
// m commits are pure merges without any changes
// c change the number of lines in a and b.
// There are no subdirectories here.
async fn init_repo(ctx: &CoreContext) -> Result<(RepoContext, HashMap<&'static str, ChangesetId>)> {
    let repo = test_repo_factory::build_empty(ctx.fb).await?;
    let mut changesets = HashMap::new();

    changesets.insert(
        "a1",
        CreateCommitContext::new_root(ctx, &repo)
            .add_file("a", "1\n")
            .set_author_date(DateTime::from_timestamp(1000, 0)?)
            .commit()
            .await?,
    );
    changesets.insert(
        "a2",
        CreateCommitContext::new(ctx, &repo, vec![changesets["a1"]])
            .add_file("a", "2\n1\n")
            .set_author_date(DateTime::from_timestamp(2000, 0)?)
            .commit()
            .await?,
    );
    changesets.insert(
        "a3",
        CreateCommitContext::new(ctx, &repo, vec![changesets["a2"]])
            .add_file("a", "2\n1\n3\n")
            .set_author_date(DateTime::from_timestamp(3000, 0)?)
            .commit()
            .await?,
    );
    changesets.insert(
        "b1",
        CreateCommitContext::new_root(ctx, &repo)
            .add_file("b", "1\n")
            .set_author_date(DateTime::from_timestamp(1500, 0)?)
            .commit()
            .await?,
    );
    changesets.insert(
        "b2",
        CreateCommitContext::new(ctx, &repo, vec![changesets["b1"]])
            .add_file("b", "1\n2\n")
            .set_author_date(DateTime::from_timestamp(2500, 0)?)
            .commit()
            .await?,
    );
    changesets.insert(
        "b3",
        CreateCommitContext::new(ctx, &repo, vec![changesets["b2"]])
            .add_file("b", "1\n2\n3\n")
            .set_author_date(DateTime::from_timestamp(3500, 0)?)
            .commit()
            .await?,
    );
    changesets.insert(
        "m1",
        CreateCommitContext::new(ctx, &repo, vec![changesets["b3"], changesets["a3"]])
            .set_author_date(DateTime::from_timestamp(4000, 0)?)
            .commit()
            .await?,
    );
    changesets.insert(
        "c1",
        CreateCommitContext::new(ctx, &repo, vec![changesets["m1"]])
            .add_file("a", "2\n1\n3\n4\n")
            .set_author_date(DateTime::from_timestamp(6000, 0)?)
            .commit()
            .await?,
    );
    changesets.insert(
        "c2",
        CreateCommitContext::new(ctx, &repo, vec![changesets["c1"]])
            .add_file("b", "4\n1\n2\n3\n")
            .set_author_date(DateTime::from_timestamp(10000, 0)?)
            .commit()
            .await?,
    );

    let repo_ctx = RepoContext::new_test(ctx.clone(), Arc::new(repo)).await?;
    Ok((repo_ctx, changesets))
}

#[fbinit::test]
async fn test_immutable_blame(fb: FacebookInit) -> Result<()> {
    let ctx = CoreContext::test_mock(fb);
    let (repo, changesets) = init_repo(&ctx).await?;

    {
        let cs_m1 = repo
            .changeset(changesets["m1"])
            .await?
            .expect("changeset exists");
        let b_m1_with_history = cs_m1.path_with_history("b").await?;
        let b_m1_blame = b_m1_with_history.blame(false).await?;
        let b_m1_blame_by_lines: Vec<_> = b_m1_blame
            .lines()?
            .map(|line| (*line.changeset_id, line.path.clone(), line.origin_offset))
            .collect();

        assert_eq!(
            b_m1_blame_by_lines,
            vec![
                (changesets["b1"], MPath::new(b"b")?, 0),
                (changesets["b2"], MPath::new(b"b")?, 1),
                (changesets["b3"], MPath::new(b"b")?, 2)
            ]
        );
    }

    {
        let cs_m1 = repo
            .changeset(changesets["m1"])
            .await?
            .expect("changeset exists");
        let a_m1_with_history = cs_m1.path_with_history("a").await?;
        let a_m1_blame = a_m1_with_history.blame(true).await?;
        let a_m1_blame_by_lines: Vec<_> = a_m1_blame
            .lines()?
            .map(|line| (*line.changeset_id, line.path.clone(), line.origin_offset))
            .collect();

        assert_eq!(
            a_m1_blame_by_lines,
            vec![
                (changesets["a2"], MPath::new(b"a")?, 0),
                (changesets["a1"], MPath::new(b"a")?, 0),
                (changesets["a3"], MPath::new(b"a")?, 2)
            ]
        );
    }
    Ok(())
}

async fn add_mutable_rename(
    src: &ChangesetPathHistoryContext,
    dst: &ChangesetPathHistoryContext,
) -> Result<()> {
    let repo = src.repo();
    let mutable_renames = repo.mutable_renames();

    let src_unode = src.unode_id().await?.expect("No source unode");

    let rename_entry = MutableRenameEntry::new(
        dst.changeset().id(),
        dst.path().as_mpath().cloned(),
        src.changeset().id(),
        src.path().as_mpath().cloned(),
        src_unode,
    )?;

    mutable_renames
        .add_or_overwrite_renames(
            repo.ctx(),
            repo.changesets(None).await?.as_ref(),
            vec![rename_entry],
        )
        .await
}

#[fbinit::test]
async fn test_linear_mutable_blame(fb: FacebookInit) -> Result<()> {
    let ctx = CoreContext::test_mock(fb);
    let (repo, changesets) = init_repo(&ctx).await?;

    // Add a mutable rename cutting out b2, so b3 goes straight to b1
    {
        let cs_b3 = repo
            .changeset(changesets["b3"])
            .await?
            .expect("changeset exists");
        let b_b3_with_history = cs_b3.path_with_history("b").await?;

        let cs_b1 = repo
            .changeset(changesets["b1"])
            .await?
            .expect("changeset exists");
        let b_b1_with_history = cs_b1.path_with_history("b").await?;
        add_mutable_rename(&b_b1_with_history, &b_b3_with_history).await?;
    }

    // Check that immutable blame isn't changed
    {
        let cs_m1 = repo
            .changeset(changesets["m1"])
            .await?
            .expect("changeset exists");
        let b_m1_with_history = cs_m1.path_with_history("b").await?;
        let b_m1_blame = b_m1_with_history.blame(false).await?;
        let b_m1_blame_by_lines: Vec<_> = b_m1_blame
            .lines()?
            .map(|line| (*line.changeset_id, line.path.clone(), line.origin_offset))
            .collect();

        assert_eq!(
            b_m1_blame_by_lines,
            vec![
                (changesets["b1"], MPath::new(b"b")?, 0),
                (changesets["b2"], MPath::new(b"b")?, 1),
                (changesets["b3"], MPath::new(b"b")?, 2)
            ]
        );
    }

    // Check it works direct
    {
        let mut cs_b3 = repo
            .changeset(changesets["b3"])
            .await?
            .expect("changeset exists");
        cs_b3
            .add_mutable_renames(vec![MPath::new(b"b")?.into()].into_iter())
            .await?;
        let b_b3_with_history = cs_b3.path_with_history("b").await?;
        let b_b3_blame = b_b3_with_history.blame(true).await?;
        let b_b3_blame_by_lines: Vec<_> = b_b3_blame
            .lines()?
            .map(|line| (*line.changeset_id, line.path.clone(), line.origin_offset))
            .collect();

        assert_eq!(
            b_b3_blame_by_lines,
            vec![
                (changesets["b1"], MPath::new(b"b")?, 0),
                (changesets["b3"], MPath::new(b"b")?, 1),
                (changesets["b3"], MPath::new(b"b")?, 2)
            ]
        );
    }

    // And from a descendant
    {
        let mut cs_m1 = repo
            .changeset(changesets["m1"])
            .await?
            .expect("changeset exists");
        cs_m1
            .add_mutable_renames(vec![MPath::new(b"b")?.into()].into_iter())
            .await?;
        let b_m1_with_history = cs_m1.path_with_history("b").await?;
        let b_m1_blame = b_m1_with_history.blame(true).await?;
        let b_m1_blame_by_lines: Vec<_> = b_m1_blame
            .lines()?
            .map(|line| (*line.changeset_id, line.path.clone(), line.origin_offset))
            .collect();

        assert_eq!(
            b_m1_blame_by_lines,
            vec![
                (changesets["b1"], MPath::new(b"b")?, 0),
                (changesets["b3"], MPath::new(b"b")?, 1),
                (changesets["b3"], MPath::new(b"b")?, 2)
            ]
        );
    }

    Ok(())
}

#[fbinit::test]
async fn test_merge_commit_mutable_blame(fb: FacebookInit) -> Result<()> {
    let ctx = CoreContext::test_mock(fb);
    let (repo, changesets) = init_repo(&ctx).await?;

    // Add a mutable rename sending b3 to a2, and one sending a3 to b2
    // This makes the commit tree (with : for immutable changes overriden by
    // mutable renames) look like:
    //     m1
    //    /\
    //   /  \
    //  a3   b3
    //  : \/ :
    //  : /\ :
    //  a2   b2
    //  |    |
    //  a1   b1
    // Note the cross in the middle - if you only look at mutable renames,
    // you get:
    //     m1
    //    /\
    //   /  \
    //  a3   b3
    //  |    |
    //  |    |
    //  b2   a2
    //  |    |
    //  b1   a1
    // swapping chains over
    {
        let cs_b3 = repo
            .changeset(changesets["b3"])
            .await?
            .expect("changeset exists");
        let b_b3_with_history = cs_b3.path_with_history("b").await?;

        let cs_a2 = repo
            .changeset(changesets["a2"])
            .await?
            .expect("changeset exists");
        let a_a2_with_history = cs_a2.path_with_history("a").await?;
        add_mutable_rename(&a_a2_with_history, &b_b3_with_history).await?;
    }
    {
        let cs_a3 = repo
            .changeset(changesets["a3"])
            .await?
            .expect("changeset exists");
        let a_a3_with_history = cs_a3.path_with_history("a").await?;
        let cs_b2 = repo
            .changeset(changesets["b2"])
            .await?
            .expect("changeset exists");
        let b_b2_with_history = cs_b2.path_with_history("b").await?;
        add_mutable_rename(&b_b2_with_history, &a_a3_with_history).await?;
    }

    // Check that immutable blame isn't changed
    {
        let cs_m1 = repo
            .changeset(changesets["m1"])
            .await?
            .expect("changeset exists");
        let b_m1_with_history = cs_m1.path_with_history("b").await?;
        let b_m1_blame = b_m1_with_history.blame(false).await?;
        let b_m1_blame_by_lines: Vec<_> = b_m1_blame
            .lines()?
            .map(|line| (*line.changeset_id, line.path.clone(), line.origin_offset))
            .collect();

        assert_eq!(
            b_m1_blame_by_lines,
            vec![
                (changesets["b1"], MPath::new(b"b")?, 0),
                (changesets["b2"], MPath::new(b"b")?, 1),
                (changesets["b3"], MPath::new(b"b")?, 2)
            ]
        );
    }
    {
        let cs_m1 = repo
            .changeset(changesets["m1"])
            .await?
            .expect("changeset exists");
        let a_m1_with_history = cs_m1.path_with_history("a").await?;
        let a_m1_blame = a_m1_with_history.blame(false).await?;
        let a_m1_blame_by_lines: Vec<_> = a_m1_blame
            .lines()?
            .map(|line| (*line.changeset_id, line.path.clone(), line.origin_offset))
            .collect();

        assert_eq!(
            a_m1_blame_by_lines,
            vec![
                (changesets["a2"], MPath::new(b"a")?, 0),
                (changesets["a1"], MPath::new(b"a")?, 0),
                (changesets["a3"], MPath::new(b"a")?, 2)
            ]
        );
    }

    // Check it works direct
    {
        let mut cs_b3 = repo
            .changeset(changesets["b3"])
            .await?
            .expect("changeset exists");
        cs_b3
            .add_mutable_renames(vec![MPath::new(b"b")?.into()].into_iter())
            .await?;
        let b_b3_with_history = cs_b3.path_with_history("b").await?;
        let b_b3_blame = b_b3_with_history.blame(true).await?;
        let b_b3_blame_by_lines: Vec<_> = b_b3_blame
            .lines()?
            .map(|line| (*line.changeset_id, line.path.clone(), line.origin_offset))
            .collect();

        assert_eq!(
            b_b3_blame_by_lines,
            vec![
                (changesets["a1"], MPath::new(b"a")?, 0),
                (changesets["b3"], MPath::new(b"b")?, 1),
                (changesets["b3"], MPath::new(b"b")?, 2)
            ]
        );
    }

    // And from a descendant
    {
        let mut cs_m1 = repo
            .changeset(changesets["m1"])
            .await?
            .expect("changeset exists");
        cs_m1
            .add_mutable_renames(vec![MPath::new(b"b")?.into()].into_iter())
            .await?;
        let b_m1_with_history = cs_m1.path_with_history("b").await?;
        let b_m1_blame = b_m1_with_history.blame(true).await?;
        let b_m1_blame_by_lines: Vec<_> = b_m1_blame
            .lines()?
            .map(|line| (*line.changeset_id, line.path.clone(), line.origin_offset))
            .collect();

        assert_eq!(
            b_m1_blame_by_lines,
            vec![
                (changesets["a1"], MPath::new(b"a")?, 0),
                (changesets["b3"], MPath::new(b"b")?, 1),
                (changesets["b3"], MPath::new(b"b")?, 2)
            ]
        );
    }

    Ok(())
}

#[fbinit::test]
async fn test_two_entry_mutable_blame(fb: FacebookInit) -> Result<()> {
    let ctx = CoreContext::test_mock(fb);
    let (repo, changesets) = init_repo(&ctx).await?;

    // Add a mutable rename cutting out b2, so b3 goes straight to b1,
    // and a second one taking b2 to a1 that should be inert
    {
        let cs_b3 = repo
            .changeset(changesets["b3"])
            .await?
            .expect("changeset exists");
        let b_b3_with_history = cs_b3.path_with_history("b").await?;

        let cs_b1 = repo
            .changeset(changesets["b1"])
            .await?
            .expect("changeset exists");
        let b_b1_with_history = cs_b1.path_with_history("b").await?;
        add_mutable_rename(&b_b1_with_history, &b_b3_with_history).await?;

        let cs_b2 = repo
            .changeset(changesets["b2"])
            .await?
            .expect("changeset exists");
        let b_b2_with_history = cs_b2.path_with_history("b").await?;
        let cs_a1 = repo
            .changeset(changesets["a1"])
            .await?
            .expect("changeset exists");
        let a_a1_with_history = cs_a1.path_with_history("a").await?;
        add_mutable_rename(&a_a1_with_history, &b_b2_with_history).await?;
    }

    // Check that immutable blame isn't changed
    {
        let cs_m1 = repo
            .changeset(changesets["m1"])
            .await?
            .expect("changeset exists");
        let b_m1_with_history = cs_m1.path_with_history("b").await?;
        let b_m1_blame = b_m1_with_history.blame(false).await?;
        let b_m1_blame_by_lines: Vec<_> = b_m1_blame
            .lines()?
            .map(|line| (*line.changeset_id, line.path.clone(), line.origin_offset))
            .collect();

        assert_eq!(
            b_m1_blame_by_lines,
            vec![
                (changesets["b1"], MPath::new(b"b")?, 0),
                (changesets["b2"], MPath::new(b"b")?, 1),
                (changesets["b3"], MPath::new(b"b")?, 2)
            ]
        );
    }

    // Check it works direct
    {
        let mut cs_b3 = repo
            .changeset(changesets["b3"])
            .await?
            .expect("changeset exists");
        cs_b3
            .add_mutable_renames(vec![MPath::new(b"b")?.into()].into_iter())
            .await?;
        let b_b3_with_history = cs_b3.path_with_history("b").await?;
        let b_b3_blame = b_b3_with_history.blame(true).await?;
        let b_b3_blame_by_lines: Vec<_> = b_b3_blame
            .lines()?
            .map(|line| (*line.changeset_id, line.path.clone(), line.origin_offset))
            .collect();

        assert_eq!(
            b_b3_blame_by_lines,
            vec![
                (changesets["b1"], MPath::new(b"b")?, 0),
                (changesets["b3"], MPath::new(b"b")?, 1),
                (changesets["b3"], MPath::new(b"b")?, 2)
            ]
        );
    }

    // And from a descendant
    {
        let mut cs_m1 = repo
            .changeset(changesets["m1"])
            .await?
            .expect("changeset exists");
        cs_m1
            .add_mutable_renames(vec![MPath::new(b"b")?.into()].into_iter())
            .await?;
        let b_m1_with_history = cs_m1.path_with_history("b").await?;
        let b_m1_blame = b_m1_with_history.blame(true).await?;
        let b_m1_blame_by_lines: Vec<_> = b_m1_blame
            .lines()?
            .map(|line| (*line.changeset_id, line.path.clone(), line.origin_offset))
            .collect();

        assert_eq!(
            b_m1_blame_by_lines,
            vec![
                (changesets["b1"], MPath::new(b"b")?, 0),
                (changesets["b3"], MPath::new(b"b")?, 1),
                (changesets["b3"], MPath::new(b"b")?, 2)
            ]
        );
    }

    Ok(())
}