/*
 * 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 anyhow::anyhow;
use anyhow::Result;
use blobstore::Blobstore;
use changesets::ChangesetInsert;
use changesets::ChangesetsRef;
use cloned::cloned;
use context::CoreContext;
use futures::future::try_join;
use futures::stream::FuturesUnordered;
use futures::stream::TryStreamExt;
use mononoke_types::BlobstoreKey;
use mononoke_types::BlobstoreValue;
use mononoke_types::BonsaiChangeset;
use mononoke_types::ChangesetId;
use repo_blobstore::RepoBlobstoreRef;
use topo_sort::sort_topological;

/// Upload bonsai changesets to the blobstore in parallel, and then store them
/// in the changesets table.
///
/// Parents of the changesets should already by saved in the repository.
pub async fn save_changesets(
    ctx: &CoreContext,
    repo: &(impl ChangesetsRef + RepoBlobstoreRef),
    bonsai_changesets: Vec<BonsaiChangeset>,
) -> Result<()> {
    let complete_changesets = repo.changesets();
    let blobstore = repo.repo_blobstore();

    let mut parents_to_check: HashSet<ChangesetId> = HashSet::new();
    for bcs in &bonsai_changesets {
        parents_to_check.extend(bcs.parents());
    }
    // Remove commits that we are uploading in this batch
    for bcs in &bonsai_changesets {
        parents_to_check.remove(&bcs.get_changeset_id());
    }

    let parents_to_check = parents_to_check
        .into_iter()
        .map({
            |p| {
                cloned!(complete_changesets);
                async move {
                    let exists = complete_changesets.exists(ctx, p).await?;
                    if exists {
                        Ok(())
                    } else {
                        Err(anyhow!("Commit {} does not exist in the repo", p))
                    }
                }
            }
        })
        .collect::<FuturesUnordered<_>>()
        .try_collect::<Vec<_>>();

    let bonsai_changesets: HashMap<_, _> = bonsai_changesets
        .into_iter()
        .map(|bcs| (bcs.get_changeset_id(), bcs))
        .collect();

    // Order of inserting entries in changeset table matters though, so we first need to
    // topologically sort commits.
    let mut bcs_parents = HashMap::new();
    for bcs in bonsai_changesets.values() {
        let parents: Vec<_> = bcs.parents().collect();
        bcs_parents.insert(bcs.get_changeset_id(), parents);
    }

    let topo_sorted_commits = sort_topological(&bcs_parents).expect("loop in commit chain!");
    let mut bonsai_complete_futs = vec![];
    for bcs_id in topo_sorted_commits {
        if let Some(bcs) = bonsai_changesets.get(&bcs_id) {
            let bcs_id = bcs.get_changeset_id();
            let completion_record = ChangesetInsert {
                cs_id: bcs_id,
                parents: bcs.parents().collect(),
            };
            bonsai_complete_futs.push(complete_changesets.add(ctx, completion_record));
        }
    }

    // Order of inserting bonsai changesets objects doesn't matter, so we can join them
    let bonsai_objects = bonsai_changesets
        .into_iter()
        .map({
            |(_, bcs)| {
                cloned!(blobstore);
                async move {
                    let bonsai_blob = bcs.into_blob();
                    let bcs_id = bonsai_blob.id().clone();
                    let blobstore_key = bcs_id.blobstore_key();
                    blobstore
                        .put(ctx, blobstore_key, bonsai_blob.into())
                        .await?;
                    Ok(())
                }
            }
        })
        .collect::<FuturesUnordered<_>>()
        .try_collect::<Vec<_>>();

    try_join(bonsai_objects, parents_to_check).await?;

    for bonsai_complete in bonsai_complete_futs {
        bonsai_complete.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 std::collections::HashSet;
use std::sync::Arc;

use abomonation_derive::Abomonation;
use anyhow::Error;
use async_trait::async_trait;
use bytes::Bytes;
use caching_ext::get_or_fill;
use caching_ext::get_or_fill_chunked;
use caching_ext::CacheDisposition;
use caching_ext::CacheHandlerFactory;
use caching_ext::CacheTtl;
use caching_ext::CachelibHandler;
use caching_ext::EntityStore;
use caching_ext::KeyedEntityStore;
use caching_ext::McErrorKind;
use caching_ext::McResult;
use caching_ext::MemcacheEntity;
use caching_ext::MemcacheHandler;
#[cfg(test)]
use caching_ext::MockStoreStats;
use changeset_entry_thrift as thrift;
use changesets::ChangesetEntry;
use changesets::ChangesetInsert;
use changesets::Changesets;
use changesets::SortOrder;
use context::CoreContext;
use fbthrift::compact_protocol;
use futures::stream::BoxStream;
use maplit::hashset;
use memcache::KeyGen;
use mononoke_types::ChangesetId;
use mononoke_types::ChangesetIdPrefix;
use mononoke_types::ChangesetIdsResolvedFromPrefix;
use mononoke_types::Generation;
use mononoke_types::RepositoryId;
use ref_cast::RefCast;
use stats::prelude::*;
use vec1::Vec1;

define_stats! {
    prefix = "mononoke.changesets.caching";
    gets: timeseries(Rate, Sum),
    get_many_by_prefix: timeseries(Rate, Sum),
    adds: timeseries(Rate, Sum),
}

pub fn get_cache_key(repo_id: RepositoryId, cs_id: &ChangesetId) -> String {
    format!("{}.{}", repo_id.prefix(), cs_id)
}

#[derive(Clone, Debug, Abomonation, RefCast)]
#[repr(transparent)]
pub struct ChangesetEntryWrapper(ChangesetEntry);

#[derive(Clone)]
pub struct CachingChangesets {
    changesets: Arc<dyn Changesets>,
    cachelib: CachelibHandler<ChangesetEntryWrapper>,
    memcache: MemcacheHandler,
    keygen: KeyGen,
    repo_id: RepositoryId,
}

fn get_keygen() -> KeyGen {
    let key_prefix = "scm.mononoke.changesets";

    KeyGen::new(
        key_prefix,
        thrift::MC_CODEVER as u32,
        thrift::MC_SITEVER as u32,
    )
}

impl CachingChangesets {
    pub fn new(
        changesets: Arc<dyn Changesets>,
        cache_handler_factory: CacheHandlerFactory,
    ) -> Self {
        Self {
            repo_id: changesets.repo_id(),
            changesets,
            cachelib: cache_handler_factory.cachelib(),
            memcache: cache_handler_factory.memcache(),
            keygen: get_keygen(),
        }
    }

    #[cfg(test)]
    pub fn mocked(changesets: Arc<dyn Changesets>) -> Self {
        Self::new(changesets, CacheHandlerFactory::Mocked)
    }

    #[cfg(test)]
    pub fn fork_cachelib(&self) -> Self {
        Self {
            repo_id: self.repo_id,
            changesets: self.changesets.clone(),
            cachelib: CachelibHandler::create_mock(),
            memcache: self.memcache.clone(),
            keygen: self.keygen.clone(),
        }
    }

    #[cfg(test)]
    pub fn cachelib_stats(&self) -> MockStoreStats {
        match self.cachelib {
            CachelibHandler::Real(_) | CachelibHandler::Noop => unimplemented!(),
            CachelibHandler::Mock(ref mock) => mock.stats(),
        }
    }

    #[cfg(test)]
    pub fn memcache_stats(&self) -> MockStoreStats {
        match self.memcache {
            MemcacheHandler::Real(_) | MemcacheHandler::Noop => unimplemented!(),
            MemcacheHandler::Mock(ref mock) => mock.stats(),
        }
    }
}

#[async_trait]
impl Changesets for CachingChangesets {
    fn repo_id(&self) -> RepositoryId {
        self.repo_id
    }

    async fn add(&self, ctx: &CoreContext, cs: ChangesetInsert) -> Result<bool, Error> {
        self.changesets.add(ctx, cs).await
    }

    async fn add_many(
        &self,
        ctx: &CoreContext,
        css: Vec1<(ChangesetInsert, Generation)>,
    ) -> Result<(), Error> {
        self.changesets.add_many(ctx, css).await
    }

    async fn get(
        &self,
        ctx: &CoreContext,
        cs_id: ChangesetId,
    ) -> Result<Option<ChangesetEntry>, Error> {
        STATS::gets.add_value(1);
        let ctx = (ctx, self);
        let mut map = get_or_fill(&ctx, hashset![cs_id]).await?;
        Ok(map.remove(&cs_id).map(|entry| entry.0))
    }

    async fn get_many(
        &self,
        ctx: &CoreContext,
        cs_ids: Vec<ChangesetId>,
    ) -> Result<Vec<ChangesetEntry>, Error> {
        STATS::gets.add_value(1);
        let ctx = (ctx, self);
        let res = get_or_fill_chunked(&ctx, cs_ids.into_iter().collect(), 1000, 2)
            .await?
            .into_values()
            .map(|val| val.0)
            .collect();
        Ok(res)
    }

    /// Use caching for the full changeset ids and slower path otherwise.
    async fn get_many_by_prefix(
        &self,
        ctx: &CoreContext,
        cs_prefix: ChangesetIdPrefix,
        limit: usize,
    ) -> Result<ChangesetIdsResolvedFromPrefix, Error> {
        STATS::get_many_by_prefix.add_value(1);
        if let Some(id) = cs_prefix.into_changeset_id() {
            let res = self.get(ctx, id).await?;
            return match res {
                Some(_) if limit > 0 => Ok(ChangesetIdsResolvedFromPrefix::Single(id)),
                _ => Ok(ChangesetIdsResolvedFromPrefix::NoMatch),
            };
        }
        self.changesets
            .get_many_by_prefix(ctx, cs_prefix, limit)
            .await
    }

    fn prime_cache(&self, _ctx: &CoreContext, changesets: &[ChangesetEntry]) {
        for cs in changesets {
            assert_eq!(cs.repo_id, self.repo_id);
            let key = get_cache_key(self.repo_id, &cs.cs_id);
            let _ = self
                .cachelib
                .set_cached(&key, ChangesetEntryWrapper::ref_cast(cs), None);
        }
    }

    async fn enumeration_bounds(
        &self,
        ctx: &CoreContext,
        read_from_master: bool,
        known_heads: Vec<ChangesetId>,
    ) -> Result<Option<(u64, u64)>, Error> {
        self.changesets
            .enumeration_bounds(ctx, read_from_master, known_heads)
            .await
    }

    fn list_enumeration_range(
        &self,
        ctx: &CoreContext,
        min_id: u64,
        max_id: u64,
        sort_and_limit: Option<(SortOrder, u64)>,
        read_from_master: bool,
    ) -> BoxStream<'_, Result<(ChangesetId, u64), Error>> {
        self.changesets.list_enumeration_range(
            ctx,
            min_id,
            max_id,
            sort_and_limit,
            read_from_master,
        )
    }
}

impl MemcacheEntity for ChangesetEntryWrapper {
    fn serialize(&self) -> Bytes {
        compact_protocol::serialize(&self.0.clone().into_thrift())
    }

    fn deserialize(bytes: Bytes) -> McResult<Self> {
        compact_protocol::deserialize(bytes)
            .and_then(ChangesetEntry::from_thrift)
            .map(ChangesetEntryWrapper)
            .map_err(|_| McErrorKind::Deserialization)
    }
}

type CacheRequest<'a> = (&'a CoreContext, &'a CachingChangesets);

impl EntityStore<ChangesetEntryWrapper> for CacheRequest<'_> {
    fn cachelib(&self) -> &CachelibHandler<ChangesetEntryWrapper> {
        let (_, mapping) = self;
        &mapping.cachelib
    }

    fn keygen(&self) -> &KeyGen {
        let (_, mapping) = self;
        &mapping.keygen
    }

    fn memcache(&self) -> &MemcacheHandler {
        let (_, mapping) = self;
        &mapping.memcache
    }

    fn cache_determinator(&self, _: &ChangesetEntryWrapper) -> CacheDisposition {
        CacheDisposition::Cache(CacheTtl::NoTtl)
    }

    caching_ext::impl_singleton_stats!("changesets");
}

#[async_trait]
impl KeyedEntityStore<ChangesetId, ChangesetEntryWrapper> for CacheRequest<'_> {
    fn get_cache_key(&self, cs_id: &ChangesetId) -> String {
        let (_, mapping) = self;
        get_cache_key(mapping.repo_id, cs_id)
    }

    async fn get_from_db(
        &self,
        keys: HashSet<ChangesetId>,
    ) -> Result<HashMap<ChangesetId, ChangesetEntryWrapper>, Error> {
        let (ctx, mapping) = self;

        let res = mapping
            .changesets
            .get_many(ctx, keys.into_iter().collect())
            .await?;

        Result::<_, Error>::Ok(
            res.into_iter()
                .map(|e| (e.cs_id, ChangesetEntryWrapper(e)))
                .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.
 */

mod caching;
mod sql;
#[cfg(test)]
mod test;

pub use crate::caching::get_cache_key;
pub use crate::caching::CachingChangesets;
pub use crate::sql::SqlChangesets;
pub use crate::sql::SqlChangesetsBuilder;


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

use anyhow::Context;
use anyhow::Error;
use anyhow::Result;
use async_trait::async_trait;
use changesets::ChangesetEntry;
use changesets::ChangesetInsert;
use changesets::Changesets;
use changesets::SortOrder;
use context::CoreContext;
use context::PerfCounterType;
use fbinit::FacebookInit;
use futures::stream;
use futures::stream::BoxStream;
use futures::stream::StreamExt;
use futures::TryFutureExt;
use mononoke_types::ChangesetId;
use mononoke_types::ChangesetIdPrefix;
use mononoke_types::ChangesetIdsResolvedFromPrefix;
use mononoke_types::Generation;
use mononoke_types::RepositoryId;
use rand::Rng;
use rendezvous::RendezVous;
use rendezvous::RendezVousOptions;
use rendezvous::RendezVousStats;
use rendezvous::TunablesRendezVousController;
use sql::Connection;
use sql::Transaction;
use sql_construct::SqlConstruct;
use sql_construct::SqlConstructFromMetadataDatabaseConfig;
use sql_ext::mononoke_queries;
use sql_ext::SqlConnections;
use stats::prelude::*;
use thiserror::Error;
use vec1::Vec1;

define_stats! {
    prefix = "mononoke.changesets.sql";
    gets: timeseries(Rate, Sum),
    gets_master: timeseries(Rate, Sum),
    get_many_by_prefix: timeseries(Rate, Sum),
    adds: timeseries(Rate, Sum),
}

#[derive(Debug, Eq, Error, PartialEq)]
pub enum SqlChangesetsError {
    #[error("Duplicate changeset {0} has different parents: {1:?} vs {2:?}")]
    DuplicateInsertionInconsistency(ChangesetId, Vec<ChangesetId>, Vec<ChangesetId>),
    #[error("Missing parents")]
    MissingParents(Vec<ChangesetId>),
}

#[derive(Clone)]
struct RendezVousConnection {
    rdv: RendezVous<ChangesetId, ChangesetEntry>,
    conn: Connection,
}

impl RendezVousConnection {
    fn new(conn: Connection, name: &str, opts: RendezVousOptions) -> Self {
        Self {
            conn,
            rdv: RendezVous::new(
                TunablesRendezVousController::new(opts),
                Arc::new(RendezVousStats::new(format!("changesets.{}", name,))),
            ),
        }
    }
}

#[derive(Clone)]
pub struct SqlChangesets {
    repo_id: RepositoryId,
    write_connection: Connection,
    read_connection: RendezVousConnection,
    read_master_connection: RendezVousConnection,
}

mononoke_queries! {
    write InsertChangeset(values: (repo_id: RepositoryId, cs_id: ChangesetId, gen: u64)) {
        insert_or_ignore,
        "{insert_or_ignore} INTO changesets (repo_id, cs_id, gen) VALUES {values}"
    }

    write InsertParents(values: (cs_id: u64, parent_id: u64, seq: i32)) {
        insert_or_ignore,
        "{insert_or_ignore} INTO csparents (cs_id, parent_id, seq) VALUES {values}"
    }

    read SelectChangeset(repo_id: RepositoryId, cs_id: ChangesetId, tok: i32) -> (u64, Option<ChangesetId>, Option<u64>, i32) {
        // NOTE: This selects seq even though we don't need it in order to sort by it.
        "
        SELECT cs0.gen AS gen, cs1.cs_id AS parent_id, csparents.seq AS seq, {tok}
        FROM csparents
        INNER JOIN changesets cs0 ON cs0.id = csparents.cs_id
        INNER JOIN changesets cs1 ON cs1.id = csparents.parent_id
        WHERE cs0.repo_id = {repo_id} AND cs0.cs_id = {cs_id} AND cs1.repo_id = {repo_id}

        UNION

        SELECT cs0.gen AS gen, NULL AS parent_id, NULL as seq, {tok}
        FROM changesets cs0
        WHERE cs0.repo_id = {repo_id} and cs0.cs_id = {cs_id}

        ORDER BY seq ASC
        "
    }

    read SelectManyChangesets(repo_id: RepositoryId, tok: i32, >list cs_id: ChangesetId) -> (ChangesetId, u64, Option<ChangesetId>, Option<u64>, i32) {
        "
        SELECT cs0.cs_id AS cs_id, cs0.gen AS gen, cs1.cs_id AS parent_id, csparents.seq AS seq, {tok}
        FROM csparents
        INNER JOIN changesets cs0 ON cs0.id = csparents.cs_id
        INNER JOIN changesets cs1 ON cs1.id = csparents.parent_id
        WHERE cs0.repo_id = {repo_id} AND cs0.cs_id IN {cs_id} AND cs1.repo_id = {repo_id}

        UNION

        SELECT cs0.cs_id AS cs_id, cs0.gen AS gen, NULL AS parent_id, NULL as seq, {tok}
        FROM changesets cs0
        WHERE cs0.repo_id = {repo_id} and cs0.cs_id IN {cs_id}

        ORDER BY seq ASC
        "
    }

    read SelectChangesets(repo_id: RepositoryId, >list cs_id: ChangesetId) -> (u64, ChangesetId, u64) {
        "SELECT id, cs_id, gen
         FROM changesets
         WHERE repo_id = {repo_id}
           AND cs_id IN {cs_id}"
    }

    read SelectChangesetsRange(repo_id: RepositoryId, min: &[u8], max: &[u8], limit: usize) -> (ChangesetId) {
        "SELECT cs_id
         FROM changesets
         WHERE repo_id = {repo_id}
           AND cs_id >= {min} AND cs_id <= {max}
           LIMIT {limit}
        "
    }

    read SelectAllChangesetsIdsInRange(repo_id: RepositoryId, min_id: u64, max_id: u64) -> (ChangesetId, u64) {
        mysql(
            "SELECT cs_id, id
            FROM changesets FORCE INDEX(repo_id_id)
            WHERE repo_id = {repo_id}
            AND id BETWEEN {min_id} AND {max_id}
            ORDER BY id"
        )
        sqlite(
            "SELECT cs_id, id
            FROM changesets
            WHERE repo_id = {repo_id}
            AND id BETWEEN {min_id} AND {max_id}
            ORDER BY id"
        )
    }

    read SelectAllChangesetsIdsInRangeLimitAsc(repo_id: RepositoryId, min_id: u64, max_id: u64, limit: u64) -> (ChangesetId, u64) {
        mysql(
            "SELECT cs_id, id
            FROM changesets FORCE INDEX(repo_id_id)
            WHERE repo_id = {repo_id}
            AND id BETWEEN {min_id} AND {max_id}
            ORDER BY id
            LIMIT {limit}"
        )
        sqlite(
            "SELECT cs_id, id
            FROM changesets
            WHERE repo_id = {repo_id}
            AND id BETWEEN {min_id} AND {max_id}
            ORDER BY id
            LIMIT {limit}"
        )
    }

    read SelectAllChangesetsIdsInRangeLimitDesc(repo_id: RepositoryId, min_id: u64, max_id: u64, limit: u64) -> (ChangesetId, u64) {
        mysql(
            "SELECT cs_id, id
            FROM changesets FORCE INDEX(repo_id_id)
            WHERE repo_id = {repo_id}
              AND id BETWEEN {min_id} AND {max_id}
            ORDER BY id DESC
            LIMIT {limit}"
        )
        sqlite(
            "SELECT cs_id, id
            FROM changesets
            WHERE repo_id = {repo_id}
              AND id BETWEEN {min_id} AND {max_id}
            ORDER BY id DESC
            LIMIT {limit}"
        )
    }

    read SelectChangesetsIdsBounds(repo_id: RepositoryId) -> (u64, u64) {
        "SELECT min(id), max(id)
         FROM changesets
         WHERE repo_id = {repo_id}"
    }

}

#[derive(Clone)]
pub struct SqlChangesetsBuilder {
    connections: SqlConnections,
}

impl SqlConstruct for SqlChangesetsBuilder {
    const LABEL: &'static str = "changesets";

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

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

impl SqlConstructFromMetadataDatabaseConfig for SqlChangesetsBuilder {}

impl SqlChangesetsBuilder {
    pub fn build(self, opts: RendezVousOptions, repo_id: RepositoryId) -> SqlChangesets {
        let SqlConnections {
            read_connection,
            read_master_connection,
            write_connection,
        } = self.connections;

        SqlChangesets {
            repo_id,
            read_connection: RendezVousConnection::new(read_connection, "read", opts),
            read_master_connection: RendezVousConnection::new(
                read_master_connection,
                "read_master",
                opts,
            ),
            write_connection,
        }
    }
}

#[async_trait]
impl Changesets for SqlChangesets {
    fn repo_id(&self) -> RepositoryId {
        self.repo_id
    }

    async fn add(&self, ctx: &CoreContext, cs: ChangesetInsert) -> Result<bool, Error> {
        STATS::adds.add_value(1);
        ctx.perf_counters()
            .increment_counter(PerfCounterType::SqlWrites);

        let parent_rows = {
            if cs.parents.is_empty() {
                Vec::new()
            } else {
                SelectChangesets::query(&self.write_connection, &self.repo_id, &cs.parents[..])
                    .await?
            }
        };
        check_missing_rows(&cs.parents, &parent_rows)?;
        let gen = parent_rows.iter().map(|row| row.2).max().unwrap_or(0) + 1;
        let transaction = self.write_connection.start_transaction().await?;
        let (transaction, result) = InsertChangeset::query_with_transaction(
            transaction,
            &[(&self.repo_id, &cs.cs_id, &gen)],
        )
        .await?;

        if result.affected_rows() == 1 && result.last_insert_id().is_some() {
            insert_parents(
                transaction,
                result.last_insert_id().unwrap(),
                cs,
                parent_rows,
            )
            .await?;
            Ok(true)
        } else {
            transaction.rollback().await?;
            check_changeset_matches(&self.write_connection, self.repo_id, cs).await?;
            Ok(false)
        }
    }

    async fn add_many(
        &self,
        ctx: &CoreContext,
        css: Vec1<(ChangesetInsert, Generation)>,
    ) -> Result<()> {
        STATS::adds.add_value(css.len() as i64);
        ctx.perf_counters()
            .increment_counter(PerfCounterType::SqlWrites);
        let transaction = self.write_connection.start_transaction().await?;
        // Part 1 - Add all changesets to the SQL table.
        let (transaction, result) = InsertChangeset::query_with_transaction(
            transaction,
            css.iter()
                .map(|(insert, gen)| (&self.repo_id, &insert.cs_id, gen.as_ref()))
                .collect::<Vec<_>>()
                .as_slice(),
        )
        .await
        .with_context(|| "during the InsertChangeset query")?;
        if result.affected_rows() == 0 {
            return Ok(());
        }
        // Part 2 - Query parent ids and cs ids from just inserted commits (the transaction is important!)
        let all_cs_ids_to_query = css
            .iter()
            .flat_map(|(insert, _)| {
                insert
                    .parents
                    .iter()
                    .copied()
                    .chain(std::iter::once(insert.cs_id))
            })
            // remove duplicates
            .collect::<HashSet<_>>()
            .into_iter()
            .collect::<Vec<_>>();
        let (transaction, changesets_info) = SelectChangesets::query_with_transaction(
            transaction,
            &self.repo_id,
            all_cs_ids_to_query.as_slice(),
        )
        .await
        .with_context(|| "during the SelectChangesets query")?;
        let cs_id_to_sql_id: HashMap<ChangesetId, u64> = changesets_info
            .into_iter()
            .map(|(sql_id, cs_id, _gen)| (cs_id, sql_id))
            .collect();
        let to_sql_id = |cs_id| {
            cs_id_to_sql_id
                .get(&cs_id)
                .with_context(|| format!("Missing {} from SQL table", cs_id))
        };
        // Part 3 - Insert parents
        let maybe_entries = css
            .into_iter()
            .flat_map(|(entry, _gen)| {
                entry
                    .parents
                    .into_iter()
                    .enumerate()
                    .map(move |(idx, p_cs_id)| (entry.cs_id, p_cs_id, idx))
            })
            .map(|(cs_id, p_cs_id, idx)| Ok((to_sql_id(cs_id)?, to_sql_id(p_cs_id)?, idx as i32)))
            .collect::<Result<Vec<_>>>();
        let parent_insert_entries = match maybe_entries {
            Ok(parent_insert_entries) => parent_insert_entries,
            Err(err) => {
                let _ = transaction.rollback().await;
                return Err(err);
            }
        };
        let (transaction, _result) = InsertParents::query_with_transaction(
            transaction,
            parent_insert_entries
                .iter()
                // Fixing references
                .map(|(a, b, c)| (*a, *b, c))
                .collect::<Vec<_>>()
                .as_slice(),
        )
        .await
        .with_context(|| "during the InsertParents query")?;
        transaction
            .commit()
            .await
            .with_context(|| "during the transaction commmit")?;

        Ok(())
    }

    async fn get(
        &self,
        ctx: &CoreContext,
        cs_id: ChangesetId,
    ) -> Result<Option<ChangesetEntry>, Error> {
        let res = self.get_many(ctx, vec![cs_id]).await?.into_iter().next();
        Ok(res)
    }

    async fn get_many(
        &self,
        ctx: &CoreContext,
        cs_ids: Vec<ChangesetId>,
    ) -> Result<Vec<ChangesetEntry>, Error> {
        if cs_ids.is_empty() {
            return Ok(vec![]);
        }
        STATS::gets.add_value(1);
        ctx.perf_counters()
            .increment_counter(PerfCounterType::SqlReadsReplica);

        let fetched_cs =
            select_many_changesets(ctx.fb, &self.read_connection, self.repo_id, &cs_ids).await?;
        let fetched_set: HashSet<_> = fetched_cs
            .clone()
            .into_iter()
            .map(|cs_entry| cs_entry.cs_id)
            .collect();

        let notfetched_cs_ids: Vec<_> = cs_ids
            .into_iter()
            .filter(|cs_id| !fetched_set.contains(cs_id))
            .collect();
        if notfetched_cs_ids.is_empty() {
            Ok(fetched_cs)
        } else {
            STATS::gets_master.add_value(1);
            ctx.perf_counters()
                .increment_counter(PerfCounterType::SqlReadsMaster);
            let mut master_fetched_cs = select_many_changesets(
                ctx.fb,
                &self.read_master_connection,
                self.repo_id,
                &notfetched_cs_ids,
            )
            .await?;
            master_fetched_cs.extend(fetched_cs);
            Ok(master_fetched_cs)
        }
    }

    async fn get_many_by_prefix(
        &self,
        ctx: &CoreContext,
        cs_prefix: ChangesetIdPrefix,
        limit: usize,
    ) -> Result<ChangesetIdsResolvedFromPrefix, Error> {
        STATS::get_many_by_prefix.add_value(1);
        ctx.perf_counters()
            .increment_counter(PerfCounterType::SqlReadsReplica);
        let resolved_cs =
            fetch_many_by_prefix(&self.read_connection.conn, self.repo_id, &cs_prefix, limit)
                .await?;
        match resolved_cs {
            ChangesetIdsResolvedFromPrefix::NoMatch => {
                ctx.perf_counters()
                    .increment_counter(PerfCounterType::SqlReadsMaster);
                fetch_many_by_prefix(
                    &self.read_master_connection.conn,
                    self.repo_id,
                    &cs_prefix,
                    limit,
                )
                .await
            }
            _ => Ok(resolved_cs),
        }
    }

    fn prime_cache(&self, _ctx: &CoreContext, _changesets: &[ChangesetEntry]) {
        // No-op
    }

    async fn enumeration_bounds(
        &self,
        _ctx: &CoreContext,
        read_from_master: bool,
        known_heads: Vec<ChangesetId>,
    ) -> Result<Option<(u64, u64)>, Error> {
        let conn = self.read_conn(read_from_master);
        let rows = SelectChangesetsIdsBounds::query(conn, &self.repo_id).await?;
        if rows.is_empty() {
            Ok(None)
        } else {
            let (mut lo, hi) = (rows[0].0, rows[0].1);
            if !known_heads.is_empty() {
                let rows = SelectChangesets::query(conn, &self.repo_id, &known_heads).await?;
                let max_id = rows
                    .into_iter()
                    .map(|(id, _cs, _gen)| id)
                    .max()
                    // We want to skip the commits we've been given
                    .map_or(lo, |i| i + 1);
                lo = lo.max(max_id);
            }
            Ok(Some((lo, hi)))
        }
    }

    fn list_enumeration_range(
        &self,
        _ctx: &CoreContext,
        min_id: u64,
        max_id: u64,
        sort_and_limit: Option<(SortOrder, u64)>,
        read_from_master: bool,
    ) -> BoxStream<'_, Result<(ChangesetId, u64), Error>> {
        // We expect the range [min_id, max_id), so subtract 1 from max_id as
        // SQL request is BETWEEN, which means both bounds are inclusive.
        let max_id = max_id - 1;
        let conn = self.read_conn(read_from_master);

        async move {
            match sort_and_limit {
                None => {
                    SelectAllChangesetsIdsInRange::query(conn, &self.repo_id, &min_id, &max_id)
                        .await
                }
                Some((SortOrder::Ascending, limit)) => {
                    SelectAllChangesetsIdsInRangeLimitAsc::query(
                        conn,
                        &self.repo_id,
                        &min_id,
                        &max_id,
                        &limit,
                    )
                    .await
                }
                Some((SortOrder::Descending, limit)) => {
                    SelectAllChangesetsIdsInRangeLimitDesc::query(
                        conn,
                        &self.repo_id,
                        &min_id,
                        &max_id,
                        &limit,
                    )
                    .await
                }
            }
        }
        .map_ok(|rows| {
            let changesets_ids = rows.into_iter().map(|row| Ok((row.0, row.1)));
            stream::iter(changesets_ids)
        })
        .try_flatten_stream()
        .boxed()
    }
}

async fn fetch_many_by_prefix(
    connection: &Connection,
    repo_id: RepositoryId,
    cs_prefix: &ChangesetIdPrefix,
    limit: usize,
) -> Result<ChangesetIdsResolvedFromPrefix, Error> {
    let rows = SelectChangesetsRange::query(
        connection,
        &repo_id,
        &cs_prefix.min_as_ref(),
        &cs_prefix.max_as_ref(),
        &(limit + 1),
    )
    .await?;
    let mut fetched_cs: Vec<ChangesetId> = rows.into_iter().map(|row| row.0).collect();
    let result = match fetched_cs.len() {
        0 => ChangesetIdsResolvedFromPrefix::NoMatch,
        1 => ChangesetIdsResolvedFromPrefix::Single(fetched_cs[0].clone()),
        l if l <= limit => ChangesetIdsResolvedFromPrefix::Multiple(fetched_cs),
        _ => ChangesetIdsResolvedFromPrefix::TooMany({
            fetched_cs.pop();
            fetched_cs
        }),
    };
    Ok(result)
}

impl SqlChangesets {
    fn read_conn(&self, read_from_master: bool) -> &Connection {
        if read_from_master {
            &self.read_master_connection.conn
        } else {
            &self.read_connection.conn
        }
    }
}

fn check_missing_rows(
    expected: &[ChangesetId],
    actual: &[(u64, ChangesetId, u64)],
) -> Result<(), SqlChangesetsError> {
    // Could just count the number here and report an error if any are missing, but the reporting
    // wouldn't be as nice.
    let expected_set: HashSet<_> = expected.iter().collect();
    let actual_set: HashSet<_> = actual.iter().map(|row| &row.1).collect();
    let diff = &expected_set - &actual_set;
    if diff.is_empty() {
        Ok(())
    } else {
        Err(SqlChangesetsError::MissingParents(
            diff.into_iter().copied().collect(),
        ))
    }
}

async fn insert_parents(
    transaction: Transaction,
    new_cs_id: u64,
    cs: ChangesetInsert,
    parent_rows: Vec<(u64, ChangesetId, u64)>,
) -> Result<(), Error> {
    // parent_rows might not be in the same order as cs.parents.
    let parent_map: HashMap<_, _> = parent_rows.into_iter().map(|row| (row.1, row.0)).collect();

    // enumerate() would be OK here too, but involve conversions from usize
    // to i32 within the map function.
    let parent_inserts: Vec<_> = (0..(cs.parents.len() as i32))
        .zip(cs.parents.iter())
        .map(|(seq, parent)| {
            // check_missing_rows should have ensured that all the IDs are
            // present.
            let parent_id = parent_map
                .get(parent)
                .expect("check_missing_rows check failed");

            (new_cs_id, *parent_id, seq)
        })
        .collect();

    let ref_parent_inserts: Vec<_> = parent_inserts
        .iter()
        .map(|row| (&row.0, &row.1, &row.2))
        .collect();

    let (transaction, _) =
        InsertParents::query_with_transaction(transaction, &ref_parent_inserts[..]).await?;
    transaction.commit().await?;
    Ok(())
}

async fn check_changeset_matches(
    connection: &Connection,
    repo_id: RepositoryId,
    cs: ChangesetInsert,
) -> Result<(), Error> {
    let stored_parents = select_changeset(connection, repo_id, cs.cs_id)
        .await?
        .map(|cs| cs.parents);
    if Some(&cs.parents) == stored_parents.as_ref() {
        Ok(())
    } else {
        Err(SqlChangesetsError::DuplicateInsertionInconsistency(
            cs.cs_id,
            stored_parents.unwrap_or_default(),
            cs.parents,
        )
        .into())
    }
}

async fn select_changeset(
    connection: &Connection,
    repo_id: RepositoryId,
    cs_id: ChangesetId,
) -> Result<Option<ChangesetEntry>, Error> {
    let tok: i32 = rand::thread_rng().gen();
    let rows = SelectChangeset::query(connection, &repo_id, &cs_id, &tok).await?;
    let result = if rows.is_empty() {
        None
    } else {
        let gen = rows[0].0;
        Some(ChangesetEntry {
            repo_id,
            cs_id,
            parents: rows.into_iter().filter_map(|row| row.1).collect(),
            gen,
        })
    };
    Ok(result)
}

async fn select_many_changesets(
    fb: FacebookInit,
    connection: &RendezVousConnection,
    repo_id: RepositoryId,
    cs_ids: &[ChangesetId],
) -> Result<Vec<ChangesetEntry>, Error> {
    if cs_ids.is_empty() {
        return Ok(vec![]);
    }

    let ret = connection
        .rdv
        .dispatch(fb, cs_ids.iter().copied().collect(), || {
            let conn = connection.conn.clone();
            move |cs_ids| async move {
                let cs_ids = cs_ids.into_iter().collect::<Vec<_>>();

                let tok: i32 = rand::thread_rng().gen();

                let fetched_changesets =
                    SelectManyChangesets::query(&conn, &repo_id, &tok, &cs_ids[..]).await?;

                let mut cs_id_to_cs_entry = HashMap::new();
                for (cs_id, gen, maybe_parent, _, _) in fetched_changesets {
                    cs_id_to_cs_entry
                        .entry(cs_id)
                        .or_insert(ChangesetEntry {
                            repo_id,
                            cs_id,
                            parents: vec![],
                            gen,
                        })
                        .parents
                        .extend(maybe_parent.into_iter());
                }

                Ok(cs_id_to_cs_entry)
            }
        })
        .await?;

    Ok(ret.into_iter().filter_map(|(_, v)| v).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.
 */

//! Tests for the Changesets store.
use std::collections::HashSet;
use std::str::FromStr;
use std::sync::Arc;

use anyhow::Error;
use assert_matches::assert_matches;
use caching_ext::MockStoreStats;
use changesets::ChangesetEntry;
use changesets::ChangesetInsert;
use changesets::Changesets;
use changesets::ChangesetsRef;
use changesets::SortOrder;
use context::CoreContext;
use fbinit::FacebookInit;
use futures::Future;
use futures::TryStreamExt;
use maplit::hashset;
use mononoke_types::ChangesetIdPrefix;
use mononoke_types::ChangesetIdsResolvedFromPrefix;
use mononoke_types::Generation;
use mononoke_types_mocks::changesetid::*;
use mononoke_types_mocks::repo::*;
use pretty_assertions::assert_eq;
use rendezvous::RendezVousOptions;
use sql_construct::SqlConstruct;
use vec1::Vec1;

use super::CachingChangesets;
use super::SqlChangesets;
use super::SqlChangesetsBuilder;
use crate::sql::SqlChangesetsError;

async fn run_test<F, FO>(fb: FacebookInit, test_fn: F) -> Result<(), Error>
where
    F: FnOnce(FacebookInit, SqlChangesets) -> FO,
    FO: Future<Output = Result<(), Error>>,
{
    test_fn(
        fb,
        SqlChangesetsBuilder::with_sqlite_in_memory()
            .unwrap()
            .build(RendezVousOptions::for_test(), REPO_ZERO),
    )
    .await?;
    Ok(())
}

async fn run_caching_test<F, FO>(fb: FacebookInit, test_fn: F) -> Result<(), Error>
where
    F: FnOnce(FacebookInit, CachingChangesets) -> FO,
    FO: Future<Output = Result<(), Error>>,
{
    let real_changesets = Arc::new(
        SqlChangesetsBuilder::with_sqlite_in_memory()
            .unwrap()
            .build(RendezVousOptions::for_test(), REPO_ZERO),
    );
    let changesets = CachingChangesets::mocked(real_changesets);
    test_fn(fb, changesets).await?;
    Ok(())
}

async fn add_and_get<C: Changesets + 'static>(
    fb: FacebookInit,
    changesets: C,
) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let ctx = &ctx;

    let row = ChangesetInsert {
        cs_id: ONES_CSID,
        parents: vec![],
    };

    changesets.add(ctx, row).await?;
    let result = changesets.get(ctx, ONES_CSID).await?;
    assert_eq!(
        result,
        Some(ChangesetEntry {
            repo_id: REPO_ZERO,
            cs_id: ONES_CSID,
            parents: vec![],
            gen: 1,
        }),
    );
    Ok(())
}

async fn add_missing_parents<C: Changesets>(fb: FacebookInit, changesets: C) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let ctx = &ctx;

    let row = ChangesetInsert {
        cs_id: ONES_CSID,
        parents: vec![TWOS_CSID],
    };

    let result = changesets
        .add(ctx, row)
        .await
        .expect_err("Adding entry with missing parents failed (should have succeeded)");
    assert_matches!(
        result.downcast::<SqlChangesetsError>(),
        Ok(SqlChangesetsError::MissingParents(ref x)) if x == &vec![TWOS_CSID]
    );
    Ok(())
}

async fn missing<C: Changesets + 'static>(fb: FacebookInit, changesets: C) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let ctx = &ctx;
    let result = changesets
        .get(ctx, ONES_CSID)
        .await
        .expect("Failed to fetch missing changeset (should succeed with None instead)");
    assert_eq!(result, None);
    Ok(())
}

async fn duplicate<C: Changesets + 'static>(fb: FacebookInit, changesets: C) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let ctx = &ctx;
    let row = ChangesetInsert {
        cs_id: ONES_CSID,
        parents: vec![],
    };

    assert!(
        changesets.add(ctx, row.clone()).await?,
        "inserting unique changeset must return true"
    );

    assert!(
        !changesets.add(ctx, row.clone()).await?,
        "inserting the same changeset must return false"
    );
    Ok(())
}

async fn broken_duplicate<C: Changesets + 'static>(
    fb: FacebookInit,
    changesets: C,
) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let ctx = &ctx;
    let row = ChangesetInsert {
        cs_id: ONES_CSID,
        parents: vec![],
    };
    assert!(
        changesets.add(ctx, row).await?,
        "inserting unique changeset must return true"
    );

    let row = ChangesetInsert {
        cs_id: TWOS_CSID,
        parents: vec![],
    };
    assert!(
        changesets.add(ctx, row).await?,
        "inserting unique changeset must return true"
    );

    let row = ChangesetInsert {
        cs_id: ONES_CSID,
        parents: vec![TWOS_CSID],
    };
    let result = changesets
        .add(ctx, row)
        .await
        .expect_err("Adding changeset with the same hash but differen parents should fail");
    match result.downcast::<SqlChangesetsError>() {
        Ok(SqlChangesetsError::DuplicateInsertionInconsistency(..)) => {}
        err => panic!("unexpected error: {:?}", err),
    };

    Ok(())
}

async fn complex<C: Changesets>(fb: FacebookInit, changesets: C) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let ctx = &ctx;

    let row1 = ChangesetInsert {
        cs_id: ONES_CSID,
        parents: vec![],
    };
    changesets.add(ctx, row1).await?;

    let row2 = ChangesetInsert {
        cs_id: TWOS_CSID,
        parents: vec![],
    };
    changesets.add(ctx, row2).await?;

    let row3 = ChangesetInsert {
        cs_id: THREES_CSID,
        parents: vec![TWOS_CSID],
    };
    changesets.add(ctx, row3).await?;

    let row4 = ChangesetInsert {
        cs_id: FOURS_CSID,
        parents: vec![ONES_CSID, THREES_CSID],
    };
    changesets.add(ctx, row4).await?;

    let row5 = ChangesetInsert {
        cs_id: FIVES_CSID,
        parents: vec![ONES_CSID, TWOS_CSID, FOURS_CSID],
    };
    changesets.add(ctx, row5).await?;

    assert_eq!(
        changesets.get(ctx, ONES_CSID).await?,
        Some(ChangesetEntry {
            repo_id: REPO_ZERO,
            cs_id: ONES_CSID,
            parents: vec![],
            gen: 1,
        }),
    );

    assert_eq!(
        changesets.get(ctx, TWOS_CSID).await?,
        Some(ChangesetEntry {
            repo_id: REPO_ZERO,
            cs_id: TWOS_CSID,
            parents: vec![],
            gen: 1,
        }),
    );

    assert_eq!(
        changesets.get(ctx, THREES_CSID).await?,
        Some(ChangesetEntry {
            repo_id: REPO_ZERO,
            cs_id: THREES_CSID,
            parents: vec![TWOS_CSID],
            gen: 2,
        }),
    );

    assert_eq!(
        changesets.get(ctx, FOURS_CSID).await?,
        Some(ChangesetEntry {
            repo_id: REPO_ZERO,
            cs_id: FOURS_CSID,
            parents: vec![ONES_CSID, THREES_CSID],
            gen: 3,
        }),
    );

    assert_eq!(
        changesets.get(ctx, FIVES_CSID).await?,
        Some(ChangesetEntry {
            repo_id: REPO_ZERO,
            cs_id: FIVES_CSID,
            parents: vec![ONES_CSID, TWOS_CSID, FOURS_CSID],
            gen: 4,
        }),
    );

    Ok(())
}

async fn get_many<C: Changesets>(fb: FacebookInit, changesets: C) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let ctx = &ctx;

    let row1 = ChangesetInsert {
        cs_id: ONES_CSID,
        parents: vec![],
    };
    changesets.add(ctx, row1).await?;

    let row2 = ChangesetInsert {
        cs_id: TWOS_CSID,
        parents: vec![],
    };
    changesets.add(ctx, row2).await?;

    let row3 = ChangesetInsert {
        cs_id: THREES_CSID,
        parents: vec![TWOS_CSID],
    };
    changesets.add(ctx, row3).await?;

    let row4 = ChangesetInsert {
        cs_id: FOURS_CSID,
        parents: vec![ONES_CSID, THREES_CSID],
    };
    changesets.add(ctx, row4).await?;

    let row5 = ChangesetInsert {
        cs_id: FIVES_CSID,
        parents: vec![THREES_CSID, ONES_CSID, TWOS_CSID, FOURS_CSID],
    };
    changesets.add(ctx, row5).await?;

    let actual = changesets.get_many(ctx, vec![ONES_CSID, TWOS_CSID]).await?;

    assert_eq!(
        HashSet::from_iter(actual),
        hashset![
            ChangesetEntry {
                repo_id: REPO_ZERO,
                cs_id: ONES_CSID,
                parents: vec![],
                gen: 1,
            },
            ChangesetEntry {
                repo_id: REPO_ZERO,
                cs_id: TWOS_CSID,
                parents: vec![],
                gen: 1,
            },
        ]
    );

    let actual = changesets
        .get_many(ctx, vec![ONES_CSID, TWOS_CSID, THREES_CSID])
        .await?;
    assert_eq!(
        HashSet::from_iter(actual),
        hashset![
            ChangesetEntry {
                repo_id: REPO_ZERO,
                cs_id: ONES_CSID,
                parents: vec![],
                gen: 1,
            },
            ChangesetEntry {
                repo_id: REPO_ZERO,
                cs_id: TWOS_CSID,
                parents: vec![],
                gen: 1,
            },
            ChangesetEntry {
                repo_id: REPO_ZERO,
                cs_id: THREES_CSID,
                parents: vec![TWOS_CSID],
                gen: 2,
            },
        ]
    );

    let actual = changesets.get_many(ctx, vec![]).await?;
    assert_eq!(HashSet::from_iter(actual), hashset![]);

    let actual = changesets
        .get_many(ctx, vec![ONES_CSID, FOURS_CSID])
        .await?;
    assert_eq!(
        HashSet::from_iter(actual),
        hashset![
            ChangesetEntry {
                repo_id: REPO_ZERO,
                cs_id: ONES_CSID,
                parents: vec![],
                gen: 1,
            },
            ChangesetEntry {
                repo_id: REPO_ZERO,
                cs_id: FOURS_CSID,
                parents: vec![ONES_CSID, THREES_CSID],
                gen: 3,
            },
        ]
    );

    let actual = changesets
        .get_many(ctx, vec![ONES_CSID, FOURS_CSID, FIVES_CSID])
        .await?;
    assert_eq!(
        HashSet::from_iter(actual),
        hashset![
            ChangesetEntry {
                repo_id: REPO_ZERO,
                cs_id: ONES_CSID,
                parents: vec![],
                gen: 1,
            },
            ChangesetEntry {
                repo_id: REPO_ZERO,
                cs_id: FOURS_CSID,
                parents: vec![ONES_CSID, THREES_CSID],
                gen: 3,
            },
            ChangesetEntry {
                repo_id: REPO_ZERO,
                cs_id: FIVES_CSID,
                parents: vec![THREES_CSID, ONES_CSID, TWOS_CSID, FOURS_CSID],
                gen: 4,
            },
        ]
    );

    Ok(())
}

async fn get_many_missing<C: Changesets>(fb: FacebookInit, changesets: C) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let ctx = &ctx;

    let row1 = ChangesetInsert {
        cs_id: ONES_CSID,
        parents: vec![],
    };
    changesets.add(ctx, row1).await?;

    let row2 = ChangesetInsert {
        cs_id: TWOS_CSID,
        parents: vec![],
    };
    changesets.add(ctx, row2).await?;

    let actual = changesets
        .get_many(ctx, vec![ONES_CSID, TWOS_CSID, THREES_CSID])
        .await?;
    assert_eq!(
        HashSet::from_iter(actual),
        hashset![
            ChangesetEntry {
                repo_id: REPO_ZERO,
                cs_id: ONES_CSID,
                parents: vec![],
                gen: 1,
            },
            ChangesetEntry {
                repo_id: REPO_ZERO,
                cs_id: TWOS_CSID,
                parents: vec![],
                gen: 1,
            },
        ]
    );

    Ok(())
}

async fn get_many_by_prefix<C: Changesets>(fb: FacebookInit, changesets: C) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let ctx = &ctx;

    let row1 = ChangesetInsert {
        cs_id: ONES_CSID,
        parents: vec![],
    };
    let row2 = ChangesetInsert {
        cs_id: TWOS_CSID,
        parents: vec![],
    };
    let row3 = ChangesetInsert {
        cs_id: FS_ES_CSID,
        parents: vec![],
    };
    let row4 = ChangesetInsert {
        cs_id: FS_CSID,
        parents: vec![],
    };

    changesets.add(ctx, row1).await?;
    changesets.add(ctx, row2).await?;
    changesets.add(ctx, row3).await?;
    changesets.add(ctx, row4).await?;

    // found a single changeset
    let actual = changesets
        .get_many_by_prefix(
            ctx,
            ChangesetIdPrefix::from_bytes(&ONES_CSID.as_ref()[0..8]).unwrap(),
            10,
        )
        .await?;
    assert_eq!(actual, ChangesetIdsResolvedFromPrefix::Single(ONES_CSID));

    // found a single changeset
    let actual = changesets
        .get_many_by_prefix(
            ctx,
            ChangesetIdPrefix::from_bytes(&TWOS_CSID.as_ref()[0..12]).unwrap(),
            1,
        )
        .await?;
    assert_eq!(actual, ChangesetIdsResolvedFromPrefix::Single(TWOS_CSID));

    // found several changesets within the limit
    let actual = changesets
        .get_many_by_prefix(
            ctx,
            ChangesetIdPrefix::from_bytes(&FS_CSID.as_ref()[0..8]).unwrap(),
            10,
        )
        .await?;
    assert_eq!(
        actual,
        ChangesetIdsResolvedFromPrefix::Multiple(vec![FS_ES_CSID, FS_CSID]),
    );

    // found several changesets within the limit by hex string prefix
    let actual = changesets
        .get_many_by_prefix(ctx, ChangesetIdPrefix::from_str("fff").unwrap(), 10)
        .await?;
    assert_eq!(
        actual,
        ChangesetIdsResolvedFromPrefix::Multiple(vec![FS_ES_CSID, FS_CSID]),
    );

    // found several changesets exceeding the limit
    let actual = changesets
        .get_many_by_prefix(
            ctx,
            ChangesetIdPrefix::from_bytes(&FS_CSID.as_ref()[0..8]).unwrap(),
            1,
        )
        .await?;
    assert_eq!(
        actual,
        ChangesetIdsResolvedFromPrefix::TooMany(vec![FS_ES_CSID]),
    );

    // not found
    let actual = changesets
        .get_many_by_prefix(
            ctx,
            ChangesetIdPrefix::from_bytes(&THREES_CSID.as_ref()[0..16]).unwrap(),
            10,
        )
        .await?;
    assert_eq!(actual, ChangesetIdsResolvedFromPrefix::NoMatch);

    Ok(())
}

async fn caching_fill<C: Changesets + 'static>(
    fb: FacebookInit,
    changesets: C,
) -> Result<(), Error> {
    let changesets = Arc::new(changesets);
    let mut cc = CachingChangesets::mocked(changesets.clone());
    let ctx = CoreContext::test_mock(fb);
    let ctx = &ctx;

    let row1 = ChangesetInsert {
        cs_id: ONES_CSID,
        parents: vec![],
    };
    let row2 = ChangesetInsert {
        cs_id: TWOS_CSID,
        parents: vec![],
    };
    let row3 = ChangesetInsert {
        cs_id: THREES_CSID,
        parents: vec![],
    };

    changesets.add(ctx, row1).await?;
    changesets.add(ctx, row2).await?;
    changesets.add(ctx, row3).await?;

    // First read should miss everyhwere.
    let _ = cc.get_many(ctx, vec![ONES_CSID, TWOS_CSID]).await?;
    assert_eq!(
        cc.cachelib_stats(),
        MockStoreStats {
            gets: 2,
            sets: 2,
            misses: 2,
            hits: 0
        },
        "read 1, cachelib"
    );
    assert_eq!(
        cc.memcache_stats(),
        MockStoreStats {
            gets: 2,
            sets: 2,
            misses: 2,
            hits: 0
        },
        "read 1, memcache"
    );

    // Another read with the same pool should hit in local cache.
    let _ = cc.get_many(ctx, vec![ONES_CSID, TWOS_CSID]).await?;
    assert_eq!(
        cc.cachelib_stats(),
        MockStoreStats {
            gets: 4,
            sets: 2,
            misses: 2,
            hits: 2
        },
        "read 2, cachelib"
    );
    assert_eq!(
        cc.memcache_stats(),
        MockStoreStats {
            gets: 2,
            sets: 2,
            misses: 2,
            hits: 0
        },
        "read 2, memcache"
    );

    cc = cc.fork_cachelib();

    // Read with a separate pool should hit in Memcache.
    let _ = cc.get_many(ctx, vec![ONES_CSID, TWOS_CSID]).await?;
    assert_eq!(
        cc.cachelib_stats(),
        MockStoreStats {
            gets: 2,
            sets: 2,
            misses: 2,
            hits: 0
        },
        "read 3, cachelib"
    );
    assert_eq!(
        cc.memcache_stats(),
        MockStoreStats {
            gets: 4,
            sets: 2,
            misses: 2,
            hits: 2
        },
        "read 3, memcache"
    );

    // Reading again from the separate pool should now hit in local cache.
    let _ = cc.get_many(ctx, vec![ONES_CSID, TWOS_CSID]).await?;
    assert_eq!(
        cc.cachelib_stats(),
        MockStoreStats {
            gets: 4,
            sets: 2,
            misses: 2,
            hits: 2
        },
        "read 4, cachelib"
    );
    assert_eq!(
        cc.memcache_stats(),
        MockStoreStats {
            gets: 4,
            sets: 2,
            misses: 2,
            hits: 2
        },
        "read 4, memcache"
    );

    // Partial read should partially hit
    let _ = cc
        .get_many(ctx, vec![ONES_CSID, TWOS_CSID, THREES_CSID])
        .await?;
    assert_eq!(
        cc.cachelib_stats(),
        MockStoreStats {
            gets: 7,
            sets: 3,
            misses: 3,
            hits: 4
        },
        "read 5, cachelib"
    );
    assert_eq!(
        cc.memcache_stats(),
        MockStoreStats {
            gets: 5,
            sets: 3,
            misses: 3,
            hits: 2
        },
        "read 5, memcache"
    );

    // Partial read should have filled local cache.
    let _ = cc
        .get_many(ctx, vec![ONES_CSID, TWOS_CSID, THREES_CSID])
        .await?;
    assert_eq!(
        cc.cachelib_stats(),
        MockStoreStats {
            gets: 10,
            sets: 3,
            misses: 3,
            hits: 7
        },
        "read 6, cachelib"
    );
    assert_eq!(
        cc.memcache_stats(),
        MockStoreStats {
            gets: 5,
            sets: 3,
            misses: 3,
            hits: 2
        },
        "read 6, memcache"
    );

    Ok(())
}

async fn caching_shared<C: Changesets + 'static>(
    fb: FacebookInit,
    changesets: C,
) -> Result<(), Error> {
    let changesets = Arc::new(changesets);
    let cc = CachingChangesets::mocked(changesets.clone());
    let ctx = CoreContext::test_mock(fb);
    let ctx = &ctx;

    let row1 = ChangesetInsert {
        cs_id: ONES_CSID,
        parents: vec![],
    };
    let row2 = ChangesetInsert {
        cs_id: TWOS_CSID,
        parents: vec![],
    };
    let row3 = ChangesetInsert {
        cs_id: THREES_CSID,
        parents: vec![],
    };

    changesets.add(ctx, row1).await?;
    changesets.add(ctx, row2).await?;
    changesets.add(ctx, row3).await?;

    // get should miss
    let _ = cc.get(ctx, ONES_CSID).await?;
    assert_eq!(
        cc.cachelib_stats(),
        MockStoreStats {
            gets: 1,
            sets: 1,
            misses: 1,
            hits: 0
        },
        "read 1, cachelib"
    );
    assert_eq!(
        cc.memcache_stats(),
        MockStoreStats {
            gets: 1,
            sets: 1,
            misses: 1,
            hits: 0
        },
        "read 1, memcache"
    );

    // get_many should hit for what was filled by get
    let _ = cc
        .get_many(ctx, vec![ONES_CSID, TWOS_CSID, THREES_CSID])
        .await?;
    assert_eq!(
        cc.cachelib_stats(),
        MockStoreStats {
            gets: 4,
            sets: 3,
            misses: 3,
            hits: 1
        },
        "read 2, cachelib"
    );
    assert_eq!(
        cc.memcache_stats(),
        MockStoreStats {
            gets: 3,
            sets: 3,
            misses: 3,
            hits: 0
        },
        "read 2, memcache"
    );

    // get should hit for what was filled by get_many
    let _ = cc.get(ctx, THREES_CSID).await?;
    assert_eq!(
        cc.cachelib_stats(),
        MockStoreStats {
            gets: 5,
            sets: 3,
            misses: 3,
            hits: 2
        },
        "read 3, cachelib"
    );
    assert_eq!(
        cc.memcache_stats(),
        MockStoreStats {
            gets: 3,
            sets: 3,
            misses: 3,
            hits: 0
        },
        "read 3, memcache"
    );

    Ok(())
}

async fn test_add_many_fixture<F: fixtures::TestRepoFixture + Send, C: Changesets>(
    fb: FacebookInit,
    changesets: &C,
) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let repo = F::get_test_repo(fb).await;
    let all_changeset_ids: Vec<_> = repo
        .changesets()
        .list_enumeration_range(&ctx, 0, 10000, Some((SortOrder::Ascending, 10000)), false)
        .map_ok(|(cs_id, _)| cs_id)
        .try_collect()
        .await?;
    let mut all_changesets = repo
        .changesets()
        .get_many(&ctx, all_changeset_ids.clone())
        .await?;
    all_changesets.sort_by_key(|entry| (entry.gen, entry.cs_id));
    {
        // Let's insert in two parts to make sure it works fine with
        // pre-existing parents
        let insert_data: Vec<_> = all_changesets
            .clone()
            .into_iter()
            .map(|entry| {
                (
                    ChangesetInsert {
                        cs_id: entry.cs_id,
                        parents: entry.parents,
                    },
                    Generation::new(entry.gen),
                )
            })
            .collect();
        let mid = insert_data.len() / 2;
        let left_part = &insert_data[..mid];
        // Have some overlap to test we can reinsert changesets without issue
        let right_part = &insert_data[(mid.max(2) - 2)..];
        if let Ok(data) = Vec1::try_from(left_part) {
            changesets.add_many(&ctx, data).await?;
        }
        if let Ok(data) = Vec1::try_from(right_part) {
            changesets.add_many(&ctx, data).await?;
        }
    }
    let mut new_changesets = changesets.get_many(&ctx, all_changeset_ids).await?;
    new_changesets.sort_by_key(|entry| (entry.gen, entry.cs_id));
    assert_eq!(all_changesets, new_changesets);

    Ok(())
}

// NOTE: Use this wrapper macro to make sure tests are executed both with Changesets and
// CachingChangesets. Define tests using #[test] if you need to only execute them for Changesets or
// CachingChangesets.
macro_rules! testify {
    ($test: ident) => {
        paste::item! {
            #[fbinit::test]
            async fn [<test_ $test>](fb: FacebookInit) -> Result<(), Error> {
                run_test(fb, $test).await
            }

            #[fbinit::test]
            async fn [<test_caching_ $test>](fb: FacebookInit) -> Result<(), Error> {
                run_caching_test(fb, $test).await
            }
        }
    };
}

testify!(add_and_get);
testify!(add_missing_parents);
testify!(missing);
testify!(duplicate);
testify!(broken_duplicate);
testify!(complex);
testify!(get_many);
testify!(get_many_by_prefix);
testify!(get_many_missing);

#[fbinit::test]
async fn test_caching_fill(fb: FacebookInit) -> Result<(), Error> {
    run_test(fb, caching_fill).await
}

#[fbinit::test]
async fn test_caching_shared(fb: FacebookInit) -> Result<(), Error> {
    run_test(fb, caching_shared).await
}

macro_rules! add_many_tests {
    ($fixture: ident) => {
        paste::item! {
            async fn [<add_many_ $fixture:snake>]<C: Changesets>(fb: FacebookInit, changesets: C) -> Result<(), Error> {
                test_add_many_fixture::<fixtures::$fixture, C>(fb, &changesets).await
            }
            testify!([<add_many_ $fixture:snake>]);
        }
    };
    ($( $fixture:ident ),+ ) => {
        $(
            add_many_tests!($fixture);
        )+
    };
}

add_many_tests!(
    Linear,
    BranchEven,
    BranchUneven,
    BranchWide,
    MergeEven,
    MergeUneven,
    UnsharedMergeEven,
    UnsharedMergeUneven,
    ManyDiamonds
);


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

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

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

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

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

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

        conf.base_path(base_path);

        let options = "types_crate=changeset-entry-thrift__types";
        if !options.is_empty() {
            conf.options(options);
        }

        let include_srcs = vec![

        ];
        conf.include_srcs(include_srcs);

        conf
    };

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


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


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

use abomonation_derive::Abomonation;
use anyhow::Result;
use bytes::Bytes;
use fbthrift::compact_protocol;
use mononoke_types::ChangesetId;
use mononoke_types::RepositoryId;

#[derive(Abomonation, Clone, Debug, Eq, Hash, PartialEq)]
pub struct ChangesetEntry {
    pub repo_id: RepositoryId,
    pub cs_id: ChangesetId,
    pub parents: Vec<ChangesetId>,
    pub gen: u64,
}

impl ChangesetEntry {
    pub fn from_thrift(thrift_entry: changeset_entry_thrift::ChangesetEntry) -> Result<Self> {
        let parents: Result<Vec<_>> = thrift_entry
            .parents
            .into_iter()
            .map(ChangesetId::from_thrift)
            .collect();

        Ok(Self {
            repo_id: RepositoryId::new(thrift_entry.repo_id.0),
            cs_id: ChangesetId::from_thrift(thrift_entry.cs_id)?,
            parents: parents?,
            gen: thrift_entry.gen.0 as u64,
        })
    }

    pub fn into_thrift(self) -> changeset_entry_thrift::ChangesetEntry {
        changeset_entry_thrift::ChangesetEntry {
            repo_id: changeset_entry_thrift::RepoId(self.repo_id.id()),
            cs_id: self.cs_id.into_thrift(),
            parents: self.parents.into_iter().map(|p| p.into_thrift()).collect(),
            gen: changeset_entry_thrift::GenerationNum(self.gen as i64),
        }
    }
}

pub fn serialize_cs_entries(cs_entries: Vec<ChangesetEntry>) -> Bytes {
    let mut thrift_entries = vec![];
    for entry in cs_entries {
        let thrift_entry = changeset_entry_thrift::ChangesetEntry {
            repo_id: changeset_entry_thrift::RepoId(entry.repo_id.id()),
            cs_id: entry.cs_id.into_thrift(),
            parents: entry.parents.into_iter().map(|p| p.into_thrift()).collect(),
            gen: changeset_entry_thrift::GenerationNum(entry.gen as i64),
        };
        thrift_entries.push(thrift_entry);
    }

    compact_protocol::serialize(&thrift_entries)
}

pub fn deserialize_cs_entries(blob: &Bytes) -> Result<Vec<ChangesetEntry>> {
    let thrift_entries: Vec<changeset_entry_thrift::ChangesetEntry> =
        compact_protocol::deserialize(blob)?;
    let mut entries = vec![];
    for thrift_entry in thrift_entries {
        let parents = thrift_entry
            .parents
            .into_iter()
            .map(ChangesetId::from_thrift)
            .collect::<Result<Vec<_>>>()?;
        let entry = ChangesetEntry {
            repo_id: RepositoryId::new(thrift_entry.repo_id.0),
            cs_id: ChangesetId::from_thrift(thrift_entry.cs_id)?,
            parents,
            gen: thrift_entry.gen.0 as u64,
        };
        entries.push(entry);
    }
    Ok(entries)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn serialize_deserialize() {
        let entry = ChangesetEntry {
            repo_id: RepositoryId::new(0),
            cs_id: mononoke_types_mocks::changesetid::ONES_CSID,
            parents: vec![mononoke_types_mocks::changesetid::TWOS_CSID],
            gen: 2,
        };

        let res = deserialize_cs_entries(&serialize_cs_entries(vec![entry.clone(), entry.clone()]))
            .unwrap();
        assert_eq!(vec![entry.clone(), entry], res);
    }
}


/*
 * 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::Error;
use anyhow::Result;
use async_trait::async_trait;
use auto_impl::auto_impl;
use context::CoreContext;
use futures::stream::BoxStream;
use mononoke_types::ChangesetId;
use mononoke_types::ChangesetIdPrefix;
use mononoke_types::ChangesetIdsResolvedFromPrefix;
use mononoke_types::Generation;
use mononoke_types::RepositoryId;
use vec1::Vec1;

mod entry;

pub use crate::entry::deserialize_cs_entries;
pub use crate::entry::serialize_cs_entries;
pub use crate::entry::ChangesetEntry;

#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct ChangesetInsert {
    pub cs_id: ChangesetId,
    pub parents: Vec<ChangesetId>,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SortOrder {
    Ascending,
    Descending,
}

/// Interface to storage of changesets that have been completely stored in Mononoke.
#[facet::facet]
#[async_trait]
#[auto_impl(&, Arc)]
pub trait Changesets: Send + Sync {
    /// The repository this `Changesets` is for.
    fn repo_id(&self) -> RepositoryId;

    /// Add a new entry to the changesets table. Returns true if new changeset was inserted,
    /// returns false if the same changeset has already existed.
    async fn add(&self, ctx: &CoreContext, cs: ChangesetInsert) -> Result<bool, Error>;

    /// Add new entries for multiple changesets, may be faster than adding an entry at a time.
    /// Should be sorted in topological order. Generation must be provided.
    async fn add_many(
        &self,
        ctx: &CoreContext,
        css: Vec1<(ChangesetInsert, Generation)>,
    ) -> Result<(), Error>;

    /// Retrieve the row specified by this commit, if available.
    async fn get(
        &self,
        ctx: &CoreContext,
        cs_id: ChangesetId,
    ) -> Result<Option<ChangesetEntry>, Error>;

    /// Return whether a changeset is stored in the backend
    async fn exists(&self, ctx: &CoreContext, cs_id: ChangesetId) -> Result<bool, Error> {
        Ok(self.get(ctx, cs_id).await?.is_some())
    }

    /// Retrieve the rows for all the commits if available
    async fn get_many(
        &self,
        ctx: &CoreContext,
        cs_ids: Vec<ChangesetId>,
    ) -> Result<Vec<ChangesetEntry>, Error>;

    /// Retrieve the rows for all the commits with the given prefix up to the given limit
    async fn get_many_by_prefix(
        &self,
        ctx: &CoreContext,
        cs_prefix: ChangesetIdPrefix,
        limit: usize,
    ) -> Result<ChangesetIdsResolvedFromPrefix, Error>;

    /// Prime any caches with known changeset entries.  The changeset entries
    /// must be for the repository associated with this `Changesets`.
    fn prime_cache(&self, ctx: &CoreContext, changesets: &[ChangesetEntry]);

    /// Enumerate public changesets that are after all commits in `known_heads`
    ///
    /// This returns a pair of unique integers that are the largest unique
    /// changeset ID for anything in `known_heads` in this repo, and the
    /// maximum so far assigned. If `known_heads` is empty, you will
    /// get the minimum ID in this repo.
    ///
    /// Note that this may result in your enumeration not covering all commits
    /// since any entry in `known_heads` if `known_heads` is not empty.
    /// You are expected to handle this case.
    ///
    /// This range can be used in subsequent calls to `list_enumeration_range`
    /// to enumerate the changesets.
    async fn enumeration_bounds(
        &self,
        ctx: &CoreContext,
        read_from_master: bool,
        known_heads: Vec<ChangesetId>,
    ) -> Result<Option<(u64, u64)>>;

    /// Enumerate a range of public changesets in the repository.
    ///
    /// This lists all changesets in the given range of unique integer ids
    /// that belong to this repositories, along with their unique integer ids.
    /// Unique ids are assigned for all changesets (public or draft) in all
    /// repositories, so a given range may not have any changesets for this
    /// repository.
    ///
    /// The results can optionally be sorted and limited so that enumeration
    /// can be performed in chunks for repositories with large numbers of
    /// commits.
    ///
    /// Use `enumeration_bounds` to find suitable starting values for
    /// `min_id` and `max_id`.
    fn list_enumeration_range(
        &self,
        ctx: &CoreContext,
        min_id: u64,
        max_id: u64,
        sort_and_limit: Option<(SortOrder, u64)>,
        read_from_master: bool,
    ) -> BoxStream<'_, Result<(ChangesetId, u64), Error>>;
}


/*
 * 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 clap::ArgMatches;

#[doc(hidden)]
pub mod macro_export {
    pub use anyhow;
    pub use clap;
    pub use heck;
    pub use static_assertions::assert_impl_all;
}

pub trait BaseApp {
    /// Returns the selected subcommand of the app (if this app
    /// has subcommands).
    fn subcommand(&self) -> Option<(&str, &ArgMatches)>;
}

/// Lower level version of mononoke_app::subcommands that allows changing the app
#[macro_export]
macro_rules! subcommands {
     ( type App = $app:ty; $( mod $command:ident $(if $env:literal)? ; )* ) => {
         $( mod $command; )*

         $crate::macro_export::assert_impl_all!($app: $crate::BaseApp);

         /// Add args for all commands.
         pub(crate) fn subcommands() -> Vec<$crate::macro_export::clap::Command> {
             use $crate::macro_export::clap::CommandFactory;
             use $crate::macro_export::heck::KebabCase;
             let mut apps = vec![];
            $(
                $( if std::env::var($env).is_ok() )?
                {
                    apps.push($command::CommandArgs::command()
                        .name(stringify!($command).to_kebab_case()));
                }
            )*

            apps
         }

         /// Dispatch a command invocation.
         pub(crate) async fn dispatch(app: $app) -> $crate::macro_export::anyhow::Result<()> {
             use $crate::macro_export::clap::FromArgMatches;
             use $crate::macro_export::heck::SnakeCase;
             use $crate::BaseApp;
             if let Some((name, matches)) = app.subcommand() {
                 match name.to_snake_case().as_str() {
                     $(
                         stringify!($command) => {
                             let args = $command::CommandArgs::from_arg_matches(matches)?;
                             $command::run(app, args).await
                         }
                     )*
                     _ => Err($crate::macro_export::anyhow::anyhow!("unrecognised subcommand: {}", name)),
                 }
             } else {
                 Err($crate::macro_export::anyhow::anyhow!("no subcommand specified"))
             }
         }

         /// Confirm whether the subcommand this app was passed is covered by the subcommands
         /// registered in this scope.
         pub(crate) fn subcommand_is_in_scope(app: &$app) -> bool {
             use $crate::macro_export::heck::SnakeCase;
             use $crate::BaseApp;
             if let Some((name, matches)) = app.subcommand() {
                 match name.to_snake_case().as_str() {
                     $(
                         stringify!($command) => true,
                     )*
                     _ => false,
                 }
             } else {
                 false
             }
         }
     }
 }


/*
 * 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 arg_extensions::ArgDefaults;
use clap::Args;
use clap::ValueEnum;

#[derive(ValueEnum, Clone, Copy, Debug, PartialEq, Eq)]
pub enum CacheMode {
    /// Caching is enabled, both local and shared caches.
    Enabled,

    /// Caching is enabled, but only the local cache is active.
    LocalOnly,

    /// Caching is disabled.
    Disabled,
}

impl ArgDefaults for CacheMode {
    fn arg_defaults(&self) -> Vec<(&'static str, String)> {
        vec![(
            "cache_mode",
            self.to_possible_value()
                .expect("default value must exist")
                .get_name()
                .to_string(),
        )]
    }
}

#[derive(Args, Debug)]
pub struct CachelibArgs {
    /// Mode to initialize caching in.
    #[clap(long, value_enum, default_value_t = CacheMode::Enabled)]
    pub cache_mode: CacheMode,

    #[clap(long)]
    /// Disable cacheadmin for cachelib
    pub cachelib_disable_cacheadmin: bool,

    /// Number of shards to control concurrent access to a blobstore
    /// behind cachelib.
    #[clap(long, default_value = "0")]
    pub cachelib_shards: usize,

    /// Size of the cachelib cache, in GiB
    #[clap(long, value_name = "SIZE")]
    pub cache_size_gb: f64,

    /// Process size at which cachelib will shrink, in GiB
    #[clap(long)]
    pub max_process_size: Option<u32>,

    /// Process size at which cachelib will grow back to the
    /// value of --cache-size-gb, in GiB
    #[clap(long)]
    pub min_process_size: Option<u32>,

    /// Use the Tupperware-aware cache shrinker to avoid OOM
    #[clap(long)]
    pub use_tupperware_shrinker: bool,

    /// Ensure that objects of all size enjoy a similar LRU policy
    #[clap(long)]
    pub cachelib_rebalancing_use_lru: bool,

    /// How often to rebalance across allocation classes, in secs
    #[clap(long)]
    pub cachelib_rebalancing_interval_secs: u64,

    /// Override size of the blob cache
    #[clap(long, value_name = "SIZE", hide = true)]
    pub blob_cache_size: Option<usize>,

    /// Override size of the blob presence cache
    #[clap(long, value_name = "SIZE", hide = true)]
    pub presence_cache_size: Option<usize>,

    /// Override size of the changesets cache
    #[clap(long, value_name = "SIZE", hide = true)]
    pub changesets_cache_size: Option<usize>,

    /// Override size of the commit graph cache
    #[clap(long, value_name = "SIZE", hide = true)]
    pub commit_graph_cache_size: Option<usize>,

    /// Override size of the filenodes cache (individual filenodes)
    #[clap(long, value_name = "SIZE", hide = true)]
    pub filenodes_cache_size: Option<usize>,

    /// Override size of the filenodes history cache (batches of history)
    #[clap(long, value_name = "SIZE", hide = true)]
    pub filenodes_history_cache_size: Option<usize>,

    /// Override size of the hg mutation store cache
    #[clap(long, value_name = "SIZE", hide = true)]
    pub hg_mutation_store_cache_size: Option<usize>,

    /// Override size of the bonsai-hg mapping cache
    #[clap(long, value_name = "SIZE", hide = true)]
    pub idmapping_cache_size: Option<usize>,

    /// Override size of the bonsai-globalrevs mapping cache
    #[clap(long, value_name = "SIZE", hide = true)]
    pub globalrevs_cache_size: Option<usize>,

    /// Override size of the bonsai-svnrevs mapping cache
    #[clap(long, value_name = "SIZE", hide = true)]
    pub svnrevs_cache_size: Option<usize>,

    /// Override size of the phases cache
    #[clap(long, value_name = "SIZE", hide = true)]
    pub phases_cache_size: Option<usize>,

    /// Override size of the segmented changelog cache
    #[clap(long, value_name = "SIZE", hide = true)]
    pub segmented_changelog_cache_size: Option<usize>,

    /// Override size of the mutable renames cache
    #[clap(long, value_name = "SIZE", hide = true)]
    pub mutable_renames_cache_size: Option<usize>,

    /// Override size of the sql cache
    #[clap(long, value_name = "SIZE", hide = true)]
    pub sql_cache_size: Option<usize>,

    /// Override the power for cachelib's hashtable buckets
    #[clap(long, value_name = "SIZE", hide = true)]
    pub buckets_power: Option<u32>,
}


/*
 * 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 args;
#[cfg(fbcode_build)]
pub mod facebook;
mod settings;

use environment::Caching;
use environment::LocalCacheConfig;
use fbinit::FacebookInit;

pub use crate::args::CacheMode;
pub use crate::args::CachelibArgs;
pub use crate::settings::CachelibSettings;

pub fn init_cachelib(
    fb: FacebookInit,
    settings: &CachelibSettings,
    args: &CachelibArgs,
) -> Caching {
    if args.cache_mode == CacheMode::Disabled {
        return Caching::Disabled;
    }

    let mut settings = settings.clone();
    settings.update_from_args(args);

    #[cfg(not(fbcode_build))]
    {
        let _ = fb;
        unimplemented!("Initialization of cachelib works only for fbcode builds")
    }
    #[cfg(fbcode_build)]
    {
        let enable_cacheadmin = !args.cachelib_disable_cacheadmin;
        facebook::init_cachelib_from_settings(fb, settings, enable_cacheadmin)
            .expect("cachelib initialize should always succeed");

        match args.cache_mode {
            CacheMode::Enabled => Caching::Enabled(LocalCacheConfig {
                blobstore_cache_shards: args.cachelib_shards,
            }),
            CacheMode::LocalOnly => Caching::LocalOnly(LocalCacheConfig {
                blobstore_cache_shards: args.cachelib_shards,
            }),
            CacheMode::Disabled => unreachable!(),
        }
    }
}


/*
 * 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::time::Duration;

use super::args::CachelibArgs;

const ONE_GIB: usize = 1 << 30; // 2 ^ 30 = 1 GiB

#[derive(Clone)]
pub struct CachelibSettings {
    pub cache_size: usize,
    pub max_process_size_gib: Option<u32>,
    pub min_process_size_gib: Option<u32>,
    pub buckets_power: Option<u32>,
    pub use_tupperware_shrinker: bool,
    pub presence_cache_size: Option<usize>,
    pub changesets_cache_size: Option<usize>,
    pub commit_graph_cache_size: Option<usize>,
    pub filenodes_cache_size: Option<usize>,
    pub filenodes_history_cache_size: Option<usize>,
    pub hg_mutation_store_cache_size: Option<usize>,
    pub idmapping_cache_size: Option<usize>,
    pub globalrev_cache_size: Option<usize>,
    pub svnrev_cache_size: Option<usize>,
    pub blob_cache_size: Option<usize>,
    pub phases_cache_size: Option<usize>,
    pub segmented_changelog_cache_size: Option<usize>,
    pub mutable_renames_cache_size: Option<usize>,
    pub sql_cache_size: Option<usize>,
    pub expected_item_size_bytes: Option<usize>,
    pub rebalancing_use_lru: bool,
    pub rebalancing_interval: Duration,
}

impl CachelibSettings {
    pub fn arg_defaults(&self) -> Vec<(&'static str, String)> {
        let mut defaults = vec![
            ("cache_size_gb", (self.cache_size / ONE_GIB).to_string()),
            (
                "cachelib_rebalancing_interval_secs",
                self.rebalancing_interval.as_secs().to_string(),
            ),
        ];

        fn set_default<T: ToString>(
            defaults: &mut Vec<(&'static str, String)>,
            name: &'static str,
            value: &Option<T>,
        ) {
            if let Some(value) = value.as_ref() {
                defaults.push((name, value.to_string()));
            }
        }

        set_default(
            &mut defaults,
            "max-process-size",
            &self.max_process_size_gib,
        );
        set_default(
            &mut defaults,
            "min-process-size",
            &self.min_process_size_gib,
        );
        set_default(&mut defaults, "buckets-power", &self.buckets_power);
        set_default(
            &mut defaults,
            "presence-cache-size",
            &self.presence_cache_size,
        );
        set_default(
            &mut defaults,
            "changesets-cache-size",
            &self.changesets_cache_size,
        );
        set_default(
            &mut defaults,
            "commit-graph-cache-size",
            &self.commit_graph_cache_size,
        );
        set_default(
            &mut defaults,
            "filenodes-cache-size",
            &self.filenodes_cache_size,
        );
        set_default(
            &mut defaults,
            "filenodes-history-cache-size",
            &self.filenodes_history_cache_size,
        );
        set_default(
            &mut defaults,
            "hg-mutation-store-cache-size",
            &self.hg_mutation_store_cache_size,
        );
        set_default(
            &mut defaults,
            "idmapping-cache-size",
            &self.idmapping_cache_size,
        );
        set_default(
            &mut defaults,
            "globalrevs-cache-size",
            &self.globalrev_cache_size,
        );
        set_default(&mut defaults, "svnrevs-cache-size", &self.svnrev_cache_size);
        set_default(&mut defaults, "blob-cache-size", &self.blob_cache_size);
        set_default(&mut defaults, "phases-cache-size", &self.phases_cache_size);
        set_default(
            &mut defaults,
            "segmented-changelog-cache-size",
            &self.segmented_changelog_cache_size,
        );
        set_default(
            &mut defaults,
            "mutable-renames-cache-size",
            &self.mutable_renames_cache_size,
        );
        set_default(&mut defaults, "sql-cache-size", &self.sql_cache_size);

        defaults
    }

    pub fn update_from_args(&mut self, args: &CachelibArgs) {
        self.cache_size = (args.cache_size_gb * ONE_GIB as f64) as usize;
        self.use_tupperware_shrinker = args.use_tupperware_shrinker;
        self.rebalancing_use_lru = args.cachelib_rebalancing_use_lru;
        self.rebalancing_interval = Duration::from_secs(args.cachelib_rebalancing_interval_secs);

        fn replace<T: Clone>(target: &mut Option<T>, value: &Option<T>) {
            if value.is_some() {
                *target = value.as_ref().cloned();
            }
        }

        replace(&mut self.max_process_size_gib, &args.max_process_size);
        replace(&mut self.min_process_size_gib, &args.min_process_size);
        replace(&mut self.buckets_power, &args.buckets_power);
        replace(&mut self.presence_cache_size, &args.presence_cache_size);
        replace(&mut self.changesets_cache_size, &args.changesets_cache_size);
        replace(
            &mut self.commit_graph_cache_size,
            &args.commit_graph_cache_size,
        );
        replace(&mut self.filenodes_cache_size, &args.filenodes_cache_size);
        replace(
            &mut self.filenodes_history_cache_size,
            &args.filenodes_history_cache_size,
        );
        replace(
            &mut self.hg_mutation_store_cache_size,
            &args.hg_mutation_store_cache_size,
        );
        replace(&mut self.idmapping_cache_size, &args.idmapping_cache_size);
        replace(&mut self.globalrev_cache_size, &args.globalrevs_cache_size);
        replace(&mut self.svnrev_cache_size, &args.svnrevs_cache_size);
        replace(&mut self.blob_cache_size, &args.blob_cache_size);
        replace(&mut self.phases_cache_size, &args.phases_cache_size);
        replace(
            &mut self.segmented_changelog_cache_size,
            &args.segmented_changelog_cache_size,
        );
        replace(
            &mut self.mutable_renames_cache_size,
            &args.mutable_renames_cache_size,
        );
        replace(&mut self.sql_cache_size, &args.sql_cache_size);
    }
}

impl Default for CachelibSettings {
    fn default() -> Self {
        Self {
            cache_size: 20 * ONE_GIB,
            max_process_size_gib: None,
            min_process_size_gib: None,
            buckets_power: None,
            use_tupperware_shrinker: false,
            presence_cache_size: None,
            changesets_cache_size: None,
            commit_graph_cache_size: None,
            filenodes_cache_size: None,
            filenodes_history_cache_size: None,
            hg_mutation_store_cache_size: None,
            idmapping_cache_size: None,
            globalrev_cache_size: None,
            svnrev_cache_size: None,
            blob_cache_size: None,
            phases_cache_size: None,
            segmented_changelog_cache_size: None,
            mutable_renames_cache_size: None,
            sql_cache_size: None,
            expected_item_size_bytes: None,
            rebalancing_use_lru: false,
            rebalancing_interval: Duration::from_secs(300),
        }
    }
}


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

//! Scaffolding that's generally useful to build CLI tools on top of Mononoke.

#![feature(trait_alias)]
use std::sync::Arc;

use anyhow::bail;
use anyhow::Error;
use cacheblob::LeaseOps;
use context::CoreContext;
use cross_repo_sync::create_commit_syncer_lease;
use cross_repo_sync::create_commit_syncers;
use cross_repo_sync::types::Source;
use cross_repo_sync::types::Target;
use cross_repo_sync::CommitSyncRepos;
use cross_repo_sync::CommitSyncer;
use cross_repo_sync::Syncers;
use futures::future;
use live_commit_sync_config::CfgrLiveCommitSyncConfig;
use live_commit_sync_config::LiveCommitSyncConfig;
use mononoke_app::args::AsRepoArg;
use mononoke_app::args::SourceAndTargetRepoArgs;
use mononoke_app::MononokeApp;
use sql_construct::SqlConstructFromMetadataDatabaseConfig;
use synced_commit_mapping::SqlSyncedCommitMapping;

pub trait CrossRepo =
    cross_repo_sync::Repo + for<'a> facet::AsyncBuildable<'a, repo_factory::RepoFactoryBuilder<'a>>;

/// Instantiate the `Syncers` struct by parsing `app`
pub async fn create_commit_syncers_from_app<R: CrossRepo>(
    ctx: &CoreContext,
    app: &MononokeApp,
    repo_args: &SourceAndTargetRepoArgs,
) -> Result<Syncers<SqlSyncedCommitMapping, R>, Error> {
    create_commit_syncers_from_app_impl(ctx, app, repo_args, false).await
}

pub async fn create_commit_syncers_from_app_unredacted<R: CrossRepo>(
    ctx: &CoreContext,
    app: &MononokeApp,
    repo_args: &SourceAndTargetRepoArgs,
) -> Result<Syncers<SqlSyncedCommitMapping, R>, Error> {
    create_commit_syncers_from_app_impl(ctx, app, repo_args, true).await
}

async fn create_commit_syncers_from_app_impl<R: CrossRepo>(
    ctx: &CoreContext,
    app: &MononokeApp,
    repo_args: &SourceAndTargetRepoArgs,
    unredacted: bool,
) -> Result<Syncers<SqlSyncedCommitMapping, R>, Error> {
    let (source_repo, target_repo, mapping, live_commit_sync_config) =
        get_things_from_app::<R>(ctx, app, repo_args, unredacted).await?;

    let common_config =
        live_commit_sync_config.get_common_config(source_repo.0.repo_identity().id())?;

    let caching = app.environment().caching;
    let x_repo_syncer_lease = create_commit_syncer_lease(app.fb, caching)?;

    let large_repo_id = common_config.large_repo_id;
    let source_repo_id = source_repo.0.repo_identity().id();
    let target_repo_id = target_repo.0.repo_identity().id();
    let (small_repo, large_repo) = if large_repo_id == source_repo_id {
        (target_repo.0, source_repo.0)
    } else if large_repo_id == target_repo_id {
        (source_repo.0, target_repo.0)
    } else {
        bail!(
            "Unexpectedly CommitSyncConfig {:?} has neither of {}, {} as a large repo",
            common_config,
            source_repo_id,
            target_repo_id
        );
    };

    create_commit_syncers(
        ctx,
        small_repo,
        large_repo,
        mapping,
        live_commit_sync_config,
        x_repo_syncer_lease,
    )
}

/// Instantiate the source-target `CommitSyncer` struct by parsing `app`
pub async fn create_commit_syncer_from_app<R: CrossRepo>(
    ctx: &CoreContext,
    app: &MononokeApp,
    repo_args: &SourceAndTargetRepoArgs,
) -> Result<CommitSyncer<SqlSyncedCommitMapping, R>, Error> {
    create_commit_syncer_from_app_impl(ctx, app, false /* reverse */, repo_args).await
}

/// Instantiate the target-source `CommitSyncer` struct by parsing `app`
pub async fn create_reverse_commit_syncer_from_app<R: CrossRepo>(
    ctx: &CoreContext,
    app: &MononokeApp,
    repo_args: &SourceAndTargetRepoArgs,
) -> Result<CommitSyncer<SqlSyncedCommitMapping, R>, Error> {
    create_commit_syncer_from_app_impl(ctx, app, true /* reverse */, repo_args).await
}

/// Instantiate some auxiliary things from `app`
/// Naming is hard.
async fn get_things_from_app<R: CrossRepo>(
    ctx: &CoreContext,
    app: &MononokeApp,
    repo_args: &SourceAndTargetRepoArgs,
    unredacted: bool,
) -> Result<
    (
        Source<R>,
        Target<R>,
        SqlSyncedCommitMapping,
        Arc<dyn LiveCommitSyncConfig>,
    ),
    Error,
> {
    let fb = app.fb;

    let config_store = app.config_store();
    let (_, source_repo_config) = app.repo_config(repo_args.source_repo.as_repo_arg())?;
    let (_, target_repo_config) = app.repo_config(repo_args.target_repo.as_repo_arg())?;

    if source_repo_config.storage_config.metadata != target_repo_config.storage_config.metadata {
        return Err(Error::msg(
            "source repo and target repo have different metadata database configs!",
        ));
    }

    let mysql_options = app.mysql_options();
    let readonly_storage = app.readonly_storage();

    let mapping = SqlSyncedCommitMapping::with_metadata_database_config(
        fb,
        &source_repo_config.storage_config.metadata,
        mysql_options,
        readonly_storage.0,
    )?;

    let (source_repo, target_repo) = if !unredacted {
        app.open_source_and_target_repos(repo_args).await?
    } else {
        future::try_join(
            app.open_repo_unredacted(&repo_args.source_repo),
            app.open_repo_unredacted(&repo_args.target_repo),
        )
        .await?
    };

    let live_commit_sync_config: Arc<dyn LiveCommitSyncConfig> =
        Arc::new(CfgrLiveCommitSyncConfig::new(ctx.logger(), config_store)?);

    Ok((
        Source(source_repo),
        Target(target_repo),
        mapping,
        live_commit_sync_config,
    ))
}

fn flip_direction<T>(source_item: Source<T>, target_item: Target<T>) -> (Source<T>, Target<T>) {
    (Source(target_item.0), Target(source_item.0))
}

async fn create_commit_syncer_from_app_impl<R: CrossRepo>(
    ctx: &CoreContext,
    app: &MononokeApp,
    reverse: bool,
    repo_args: &SourceAndTargetRepoArgs,
) -> Result<CommitSyncer<SqlSyncedCommitMapping, R>, Error> {
    let (source_repo, target_repo, mapping, live_commit_sync_config) =
        get_things_from_app(ctx, app, repo_args, false).await?;

    let (source_repo, target_repo) = if reverse {
        flip_direction(source_repo, target_repo)
    } else {
        (source_repo, target_repo)
    };

    let caching = app.environment().caching;
    let x_repo_syncer_lease = create_commit_syncer_lease(app.fb, caching)?;

    create_commit_syncer(
        ctx,
        source_repo,
        target_repo,
        mapping,
        live_commit_sync_config,
        x_repo_syncer_lease,
    )
    .await
}

async fn create_commit_syncer<'a, R: CrossRepo>(
    ctx: &'a CoreContext,
    source_repo: Source<R>,
    target_repo: Target<R>,
    mapping: SqlSyncedCommitMapping,
    live_commit_sync_config: Arc<dyn LiveCommitSyncConfig>,
    x_repo_syncer_lease: Arc<dyn LeaseOps>,
) -> Result<CommitSyncer<SqlSyncedCommitMapping, R>, Error> {
    let common_config =
        live_commit_sync_config.get_common_config(source_repo.0.repo_identity().id())?;

    let repos = CommitSyncRepos::new(source_repo.0, target_repo.0, &common_config)?;
    let commit_syncer = CommitSyncer::new(
        ctx,
        mapping,
        repos,
        live_commit_sync_config,
        x_repo_syncer_lease,
    );
    Ok(commit_syncer)
}


/*
 * 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::fmt;
use std::fmt::Display;

use anyhow::Context;
use anyhow::Error;
use anyhow::Result;
use mononoke_types::BonsaiChangeset;
use mononoke_types::ChangesetId;
use mononoke_types::DateTime;
use mononoke_types::FileChange;
use serde::Serialize;

#[derive(Serialize)]
pub struct DisplayChangeset {
    pub changeset_id: ChangesetId,
    pub parents: Vec<ChangesetId>,
    pub author: String,
    pub author_date: DateTime,
    pub committer: Option<String>,
    pub committer_date: Option<DateTime>,
    pub message: String,
    pub hg_extra: BTreeMap<String, Vec<u8>>,
    pub file_changes: BTreeMap<String, FileChange>,
}

impl TryFrom<&BonsaiChangeset> for DisplayChangeset {
    type Error = Error;

    fn try_from(bonsai: &BonsaiChangeset) -> Result<Self> {
        Ok(DisplayChangeset {
            changeset_id: bonsai.get_changeset_id(),
            parents: bonsai.parents().collect(),
            author: bonsai.author().to_string(),
            author_date: bonsai.author_date().clone(),
            committer: bonsai.committer().map(ToString::to_string),
            committer_date: bonsai.committer_date().cloned(),
            message: bonsai.message().to_string(),
            hg_extra: bonsai
                .hg_extra()
                .map(|(k, v)| (k.to_string(), v.to_vec()))
                .collect(),
            file_changes: bonsai
                .file_changes_map()
                .iter()
                .map(|(k, v)| {
                    Ok((
                        String::from_utf8(k.to_vec())
                            .with_context(|| format!("Invalid extra name: {:?}", k))?,
                        v.clone(),
                    ))
                })
                .collect::<Result<_>>()?,
        })
    }
}

impl Display for DisplayChangeset {
    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
        writeln!(fmt, "BonsaiChangesetId: {}", self.changeset_id)?;
        writeln!(fmt, "Author: {}", self.author)?;
        writeln!(fmt, "Message: {}", self.message)?;
        writeln!(fmt, "FileChanges:")?;
        for (path, change) in self.file_changes.iter() {
            match change {
                FileChange::Change(change) => match change.copy_from() {
                    Some(_) => writeln!(fmt, "\t COPY/MOVE: {} {}", path, change.content_id())?,
                    None => writeln!(fmt, "\t ADDED/MODIFIED: {} {}", path, change.content_id())?,
                },
                FileChange::Deletion => writeln!(fmt, "\t REMOVED: {}", path)?,
                FileChange::UntrackedChange(change) => writeln!(
                    fmt,
                    "\t UNTRACKED ADD/MODIFY: {} {}",
                    path,
                    change.content_id()
                )?,
                FileChange::UntrackedDeletion => writeln!(fmt, "\t MISSING: {}", path)?,
            }
        }
        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::io::Write;

use anyhow::Result;

use crate::hexdump;

/// Displays content, handling the possibility of binary content.
///
/// Non-UTF-8 content is displayed as a hex-dump of the first 1024 bytes.
/// UTF-8 content is preceded by a newline, so it is distinct from any
/// hexdump.
pub fn display_content(mut w: impl Write, content: impl AsRef<[u8]>) -> Result<()> {
    let content = content.as_ref();
    if let Ok(utf8_content) = std::str::from_utf8(content) {
        writeln!(w, "\n{}", utf8_content)?;
    } else {
        writeln!(w, "Hexdump (first 1024 bytes):")?;
        hexdump(w, &content[..content.len().min(1024)])?;
    }
    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 changeset;
mod content;
mod mercurial;

use std::io::Write;

use anyhow::Result;
pub use changeset::DisplayChangeset;
pub use content::display_content;
pub use mercurial::display_hg_manifest;

/// Hexdump a block of data to the output stream.
pub fn hexdump(mut w: impl Write, data: impl AsRef<[u8]>) -> Result<()> {
    const CHUNK_SIZE: usize = 16;

    fn sanitize(slice: &[u8]) -> String {
        slice
            .iter()
            .map(|c| {
                if *c < b' ' || *c > b'~' {
                    '.'
                } else {
                    *c as char
                }
            })
            .collect::<String>()
    }

    for (i, slice) in data.as_ref().chunks(CHUNK_SIZE).enumerate() {
        writeln!(
            w,
            "{:08x}: {:<32}  {}",
            i * CHUNK_SIZE,
            hex::encode(slice),
            sanitize(slice),
        )?;
    }
    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::io::Write;

use anyhow::Result;
use manifest::Entry;
use manifest::Manifest;
use mercurial_types::blobs::HgBlobManifest;
use unicode_truncate::Alignment;
use unicode_truncate::UnicodeTruncateStr;
use unicode_width::UnicodeWidthStr;

/// Displays a Mercurial manifest, one entry per line.
pub fn display_hg_manifest(mut w: impl Write, manifest: &HgBlobManifest) -> Result<()> {
    let entries = manifest
        .list()
        .map(|(name, entry)| (String::from_utf8_lossy(name.as_ref()).into_owned(), entry))
        .collect::<Vec<_>>();
    let max_width = entries
        .iter()
        .map(|(name, _)| name.width())
        .max()
        .unwrap_or(0);
    for (name, entry) in entries {
        let (ty, id) = match entry {
            Entry::Leaf((ty, id)) => (ty.to_string(), id.to_string()),
            Entry::Tree(id) => ("tree".to_string(), id.to_string()),
        };
        writeln!(
            w,
            "{} {} {}",
            name.unicode_pad(max_width, Alignment::Left, false),
            id,
            ty,
        )?;
    }
    Ok(())
}