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

#![feature(trait_alias)]

use ::repo_lock::RepoLockRef;
use blobrepo::AsBlobRepo;
use bonsai_git_mapping::BonsaiGitMappingArc;
use bonsai_globalrev_mapping::BonsaiGlobalrevMappingArc;
use bonsai_hg_mapping::BonsaiHgMappingRef;
use bookmarks::BookmarksRef;
use bookmarks_types::BookmarkKey;
use changeset_fetcher::ChangesetFetcherArc;
use changesets::ChangesetsRef;
use commit_graph::CommitGraphRef;
use itertools::Itertools;
use metaconfig_types::RepoConfigRef;
use mononoke_types::ChangesetId;
use mononoke_types::MPath;
use phases::PhasesRef;
use pushrebase::PushrebaseError;
use pushrebase_mutation_mapping::PushrebaseMutationMappingRef;
use repo_authorization::AuthorizationError;
use repo_blobstore::RepoBlobstoreArc;
use repo_blobstore::RepoBlobstoreRef;
use repo_bookmark_attrs::RepoBookmarkAttrsRef;
use repo_cross_repo::RepoCrossRepoRef;
use repo_derived_data::RepoDerivedDataRef;
use repo_identity::RepoIdentityRef;
use repo_permission_checker::RepoPermissionCheckerRef;
use thiserror::Error;

pub mod affected_changesets;
mod create;
mod delete;
mod git_mapping;
mod hook_running;
mod pushrebase_onto;
mod repo_lock;
mod restrictions;
mod update;

pub use bookmarks_types::BookmarkKind;
pub use hooks::CrossRepoPushSource;
pub use hooks::HookRejection;
pub use pushrebase::PushrebaseOutcome;

pub use crate::create::CreateBookmarkOp;
pub use crate::delete::DeleteBookmarkOp;
pub use crate::hook_running::run_hooks;
pub use crate::pushrebase_onto::get_pushrebase_hooks;
pub use crate::pushrebase_onto::PushrebaseOntoBookmarkOp;
pub use crate::restrictions::check_bookmark_sync_config;
pub use crate::restrictions::BookmarkKindRestrictions;
pub use crate::update::BookmarkUpdatePolicy;
pub use crate::update::BookmarkUpdateTargets;
pub use crate::update::UpdateBookmarkOp;

/// Trait alias for bookmarks movement repositories.
///
/// These are the repo attributes that are necessary to call most functions in
/// bookmarks movement.
pub trait Repo = AsBlobRepo
    + BonsaiHgMappingRef
    + BonsaiGitMappingArc
    + BonsaiGlobalrevMappingArc
    + BookmarksRef
    + ChangesetFetcherArc
    + ChangesetsRef
    + PhasesRef
    + PushrebaseMutationMappingRef
    + RepoBookmarkAttrsRef
    + RepoConfigRef
    + RepoDerivedDataRef
    + RepoBlobstoreArc
    + RepoBlobstoreRef
    + RepoCrossRepoRef
    + RepoIdentityRef
    + RepoPermissionCheckerRef
    + RepoLockRef
    + CommitGraphRef
    + Send
    + Sync;

/// An error encountered during an attempt to move a bookmark.
#[derive(Debug, Error)]
pub enum BookmarkMovementError {
    #[error("Non fast-forward bookmark move of '{bookmark}' from {from} to {to}")]
    NonFastForwardMove {
        bookmark: BookmarkKey,
        from: ChangesetId,
        to: ChangesetId,
    },

    #[error("Deletion of '{bookmark}' is prohibited")]
    DeletionProhibited { bookmark: BookmarkKey },

    #[error(transparent)]
    AuthorizationError(#[from] AuthorizationError),

    #[error(
        "Invalid scratch bookmark: {bookmark} (scratch bookmarks must match pattern {pattern})"
    )]
    InvalidScratchBookmark {
        bookmark: BookmarkKey,
        pattern: String,
    },

    #[error(
        "Invalid publishing bookmark: {bookmark} (only scratch bookmarks may match pattern {pattern})"
    )]
    InvalidPublishingBookmark {
        bookmark: BookmarkKey,
        pattern: String,
    },

    #[error(
        "Invalid scratch bookmark: {bookmark} (scratch bookmarks are not enabled for this repo)"
    )]
    ScratchBookmarksDisabled { bookmark: BookmarkKey },

    #[error("Bookmark transaction failed")]
    TransactionFailed,

    #[error("Hooks failed:\n{}", describe_hook_rejections(.0.as_slice()))]
    HookFailure(Vec<HookRejection>),

    #[error("Pushrebase failed: {0}")]
    PushrebaseError(#[source] PushrebaseError),

    #[error("Repo is locked: {0}")]
    RepoLocked(String),

    #[error("Case conflict found in {changeset_id}: {path1} conflicts with {path2}")]
    CaseConflict {
        changeset_id: ChangesetId,
        path1: MPath,
        path2: MPath,
    },

    #[error(
        "This repository uses Globalrevs. Pushrebase is only allowed onto the bookmark '{}', this push was for '{}'",
        .globalrevs_publishing_bookmark,
        .bookmark
    )]
    PushrebaseInvalidGlobalrevsBookmark {
        bookmark: BookmarkKey,
        globalrevs_publishing_bookmark: BookmarkKey,
    },

    #[error(
        "Pushrebase is not allowed onto the bookmark '{}', because this bookmark is required to be an ancestor of '{}'",
        .bookmark,
        .descendant_bookmark,
    )]
    PushrebaseNotAllowedRequiresAncestorsOf {
        bookmark: BookmarkKey,
        descendant_bookmark: BookmarkKey,
    },

    #[error("Bookmark '{bookmark}' can only be moved to ancestors of '{descendant_bookmark}'")]
    RequiresAncestorOf {
        bookmark: BookmarkKey,
        descendant_bookmark: BookmarkKey,
    },

    #[error(
        "Bookmark '{bookmark}' cannot be moved because publishing bookmarks are being redirected"
    )]
    PushRedirectorEnabledForPublishing { bookmark: BookmarkKey },

    #[error(
        "Bookmark '{bookmark}' cannot be moved because scratch bookmarks are being redirected"
    )]
    PushRedirectorEnabledForScratch { bookmark: BookmarkKey },

    #[error(transparent)]
    Error(#[from] anyhow::Error),
}

pub fn describe_hook_rejections(rejections: &[HookRejection]) -> String {
    rejections
        .iter()
        .map(|rejection| {
            format!(
                "  {} for {}: {}",
                rejection.hook_name, rejection.cs_id, rejection.reason.long_description
            )
        })
        .join("\n")
}


/*
 * 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 bonsai_git_mapping::BonsaiGitMappingArc;
use bonsai_globalrev_mapping::BonsaiGlobalrevMappingArc;
use bookmarks::BookmarkUpdateReason;
use bookmarks_types::BookmarkKey;
use bytes::Bytes;
use context::CoreContext;
use futures_stats::TimedFutureExt;
use git_mapping_pushrebase_hook::GitMappingPushrebaseHook;
use globalrev_pushrebase_hook::GlobalrevPushrebaseHook;
use hooks::CrossRepoPushSource;
use hooks::HookManager;
use metaconfig_types::PushrebaseParams;
use mononoke_types::BonsaiChangeset;
use pushrebase_hook::PushrebaseHook;
use pushrebase_mutation_mapping::PushrebaseMutationMappingRef;
use repo_authorization::AuthorizationContext;
use repo_authorization::RepoWriteOperation;
use repo_bookmark_attrs::RepoBookmarkAttrsRef;
use repo_cross_repo::RepoCrossRepoRef;
use repo_identity::RepoIdentityRef;
use repo_update_logger::log_bookmark_operation;
use repo_update_logger::log_new_commits;
use repo_update_logger::BookmarkInfo;
use repo_update_logger::BookmarkOperation;
use repo_update_logger::CommitInfo;

use crate::affected_changesets::AdditionalChangesets;
use crate::affected_changesets::AffectedChangesets;
use crate::repo_lock::check_repo_lock;
use crate::repo_lock::RepoLockPushrebaseHook;
use crate::restrictions::check_bookmark_sync_config;
use crate::restrictions::BookmarkKindRestrictions;
use crate::BookmarkMovementError;
use crate::Repo;

#[must_use = "PushrebaseOntoBookmarkOp must be run to have an effect"]
pub struct PushrebaseOntoBookmarkOp<'op> {
    bookmark: &'op BookmarkKey,
    affected_changesets: AffectedChangesets,
    bookmark_restrictions: BookmarkKindRestrictions,
    cross_repo_push_source: CrossRepoPushSource,
    pushvars: Option<&'op HashMap<String, Bytes>>,
    log_new_public_commits_to_scribe: bool,
    only_log_acl_checks: bool,
}

impl<'op> PushrebaseOntoBookmarkOp<'op> {
    pub fn new(
        bookmark: &'op BookmarkKey,
        changesets: HashSet<BonsaiChangeset>,
    ) -> PushrebaseOntoBookmarkOp<'op> {
        PushrebaseOntoBookmarkOp {
            bookmark,
            affected_changesets: AffectedChangesets::with_source_changesets(changesets),
            bookmark_restrictions: BookmarkKindRestrictions::AnyKind,
            cross_repo_push_source: CrossRepoPushSource::NativeToThisRepo,
            pushvars: None,
            log_new_public_commits_to_scribe: false,
            only_log_acl_checks: false,
        }
    }

    pub fn only_if_scratch(mut self) -> Self {
        self.bookmark_restrictions = BookmarkKindRestrictions::OnlyScratch;
        self
    }

    pub fn only_if_public(mut self) -> Self {
        self.bookmark_restrictions = BookmarkKindRestrictions::OnlyPublishing;
        self
    }

    pub fn with_bookmark_restrictions(
        mut self,
        bookmark_restrictions: BookmarkKindRestrictions,
    ) -> Self {
        self.bookmark_restrictions = bookmark_restrictions;
        self
    }

    pub fn with_pushvars(mut self, pushvars: Option<&'op HashMap<String, Bytes>>) -> Self {
        self.pushvars = pushvars;
        self
    }

    pub fn with_push_source(mut self, cross_repo_push_source: CrossRepoPushSource) -> Self {
        self.cross_repo_push_source = cross_repo_push_source;
        self
    }

    pub fn log_new_public_commits_to_scribe(mut self) -> Self {
        self.log_new_public_commits_to_scribe = true;
        self
    }

    pub fn only_log_acl_checks(mut self, only_log: bool) -> Self {
        self.only_log_acl_checks = only_log;
        self
    }

    pub async fn run(
        mut self,
        ctx: &'op CoreContext,
        authz: &'op AuthorizationContext,
        repo: &'op impl Repo,
        hook_manager: &'op HookManager,
    ) -> Result<pushrebase::PushrebaseOutcome, BookmarkMovementError> {
        let kind = self.bookmark_restrictions.check_kind(repo, self.bookmark)?;

        if self.only_log_acl_checks {
            if authz
                .check_repo_write(ctx, repo, RepoWriteOperation::LandStack(kind))
                .await
                .is_denied()
            {
                ctx.scuba().clone().log_with_msg(
                    "Repo write ACL check would fail for bookmark pushrebase",
                    None,
                );
            }
        } else {
            authz
                .require_repo_write(ctx, repo, RepoWriteOperation::LandStack(kind))
                .await?;
        }
        authz
            .require_bookmark_modify(ctx, repo, self.bookmark)
            .await?;

        check_bookmark_sync_config(repo, self.bookmark, kind)?;

        if repo.repo_config().pushrebase.block_merges {
            let any_merges = self
                .affected_changesets
                .source_changesets()
                .iter()
                .any(BonsaiChangeset::is_merge);
            if any_merges {
                return Err(anyhow!(
                    "Pushrebase blocked because it contains a merge commit.\n\
                    If you need this for a specific use case please contact\n\
                    the Source Control team at https://fburl.com/27qnuyl2"
                )
                .into());
            }
        }
        let reason = BookmarkUpdateReason::Pushrebase;

        self.affected_changesets
            .check_restrictions(
                ctx,
                authz,
                repo,
                hook_manager,
                self.bookmark,
                self.pushvars,
                reason,
                kind,
                AdditionalChangesets::None,
                self.cross_repo_push_source,
            )
            .await?;

        let mut pushrebase_hooks =
            get_pushrebase_hooks(ctx, repo, self.bookmark, &repo.repo_config().pushrebase)?;

        // For pushrebase, we check the repo lock once at the beginning of the
        // pushrebase operation, and then once more as part of the pushrebase
        // bookmark update transaction, to check if the repo got locked while
        // we were peforming the pushrebase.
        check_repo_lock(repo, kind, self.pushvars, ctx.metadata().identities()).await?;

        if let Some(hook) = RepoLockPushrebaseHook::new(
            repo.repo_identity().id(),
            kind,
            self.pushvars,
            repo.repo_permission_checker(),
            ctx.metadata().identities(),
        )
        .await
        {
            pushrebase_hooks.push(hook);
        }

        let mut flags = repo.repo_config().pushrebase.flags.clone();
        if let Some(rewritedates) = repo
            .repo_bookmark_attrs()
            .should_rewrite_dates(self.bookmark)
        {
            // Bookmark config overrides repo flags.rewritedates config
            flags.rewritedates = rewritedates;
        }

        ctx.scuba()
            .clone()
            .add("bookmark", self.bookmark.to_string())
            .log_with_msg("Pushrebase started", None);
        let (stats, result) = pushrebase::do_pushrebase_bonsai(
            ctx,
            repo,
            &flags,
            self.bookmark,
            self.affected_changesets.source_changesets(),
            pushrebase_hooks.as_slice(),
        )
        .timed()
        .await;

        let mut scuba_logger = ctx.scuba().clone();
        scuba_logger.add_future_stats(&stats);
        match &result {
            Ok(outcome) => {
                scuba_logger
                    .add("pushrebase_retry_num", outcome.retry_num.0)
                    .add("pushrebase_distance", outcome.pushrebase_distance.0)
                    .add("bookmark", self.bookmark.to_string())
                    .add("changeset_id", format!("{}", outcome.head))
                    .log_with_msg("Pushrebase finished", None);

                if self.log_new_public_commits_to_scribe {
                    let mut changesets_to_log: HashMap<_, _> = self
                        .affected_changesets
                        .source_changesets()
                        .iter()
                        .map(|bcs| (bcs.get_changeset_id(), CommitInfo::new(bcs, None)))
                        .collect();

                    for pair in outcome.rebased_changesets.iter() {
                        let info = changesets_to_log
                            .get_mut(&pair.id_old)
                            .ok_or_else(|| anyhow!("Missing commit info for {}", pair.id_old))?;
                        info.update_changeset_id(pair.id_old, pair.id_new)?;
                    }

                    log_new_commits(
                        ctx,
                        repo,
                        Some((self.bookmark, kind)),
                        changesets_to_log.into_values().collect(),
                    )
                    .await;
                }

                let info = BookmarkInfo {
                    bookmark_name: self.bookmark.clone(),
                    bookmark_kind: kind,
                    operation: BookmarkOperation::Pushrebase(
                        outcome.old_bookmark_value,
                        outcome.head,
                    ),
                    reason,
                };
                log_bookmark_operation(ctx, repo, &info).await;
            }
            Err(err) => scuba_logger.log_with_msg("Pushrebase failed", Some(format!("{:#?}", err))),
        }

        result.map_err(BookmarkMovementError::PushrebaseError)
    }
}

/// Get a Vec of the relevant pushrebase hooks for PushrebaseParams, using this repo when
/// required by those hooks.
pub fn get_pushrebase_hooks(
    ctx: &CoreContext,
    repo: &(
         impl BonsaiGitMappingArc
         + BonsaiGlobalrevMappingArc
         + PushrebaseMutationMappingRef
         + RepoBookmarkAttrsRef
         + RepoCrossRepoRef
         + RepoIdentityRef
     ),
    bookmark: &BookmarkKey,
    pushrebase_params: &PushrebaseParams,
) -> Result<Vec<Box<dyn PushrebaseHook>>, BookmarkMovementError> {
    let mut pushrebase_hooks = Vec::new();

    match pushrebase_params.globalrev_config.as_ref() {
        Some(config) if config.publishing_bookmark == *bookmark => {
            let add_hook = if let Some(small_repo_id) = config.small_repo_id {
                // Only add hook if pushes are being redirected
                repo.repo_cross_repo()
                    .live_commit_sync_config()
                    .push_redirector_enabled_for_public(small_repo_id)
            } else {
                true
            };
            if add_hook {
                let hook = GlobalrevPushrebaseHook::new(
                    ctx.clone(),
                    repo.bonsai_globalrev_mapping_arc().clone(),
                    repo.repo_identity().id(),
                    config.small_repo_id,
                );
                pushrebase_hooks.push(hook);
            }
        }
        Some(config) if config.small_repo_id.is_none() => {
            return Err(BookmarkMovementError::PushrebaseInvalidGlobalrevsBookmark {
                bookmark: bookmark.clone(),
                globalrevs_publishing_bookmark: config.publishing_bookmark.clone(),
            });
        }
        _ => {
            // No hook necessary
        }
    };

    for attr in repo.repo_bookmark_attrs().select(bookmark) {
        if let Some(descendant_bookmark) = &attr.params().ensure_ancestor_of {
            return Err(
                BookmarkMovementError::PushrebaseNotAllowedRequiresAncestorsOf {
                    bookmark: bookmark.clone(),
                    descendant_bookmark: descendant_bookmark.clone(),
                },
            );
        }
    }

    if pushrebase_params.populate_git_mapping {
        let hook = GitMappingPushrebaseHook::new(repo.bonsai_git_mapping_arc().clone());
        pushrebase_hooks.push(hook);
    }

    match repo.pushrebase_mutation_mapping().get_hook() {
        Some(hook) => pushrebase_hooks.push(hook),
        None => {}
    }
    Ok(pushrebase_hooks)
}


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

use std::collections::HashMap;

use anyhow::anyhow;
use anyhow::Context;
use anyhow::Result;
use async_trait::async_trait;
use bookmarks::BookmarkTransactionError;
use bookmarks_types::BookmarkKind;
use bytes::Bytes;
use context::CoreContext;
use mononoke_types::BonsaiChangesetMut;
use mononoke_types::ChangesetId;
use mononoke_types::RepositoryId;
use permission_checker::MononokeIdentitySet;
use pushrebase_hook::PushrebaseCommitHook;
use pushrebase_hook::PushrebaseHook;
use pushrebase_hook::PushrebaseTransactionHook;
use pushrebase_hook::RebasedChangesets;
use repo_lock::RepoLockRef;
use repo_lock::RepoLockState;
use repo_lock::TransactionRepoLock;
use repo_permission_checker::RepoPermissionChecker;
use repo_permission_checker::RepoPermissionCheckerRef;
use sql::Transaction;
use tunables::tunables;

use crate::BookmarkMovementError;

async fn should_check_repo_lock(
    kind: BookmarkKind,
    pushvars: Option<&HashMap<String, Bytes>>,
    repo_perm_checker: &dyn RepoPermissionChecker,
    idents: &MononokeIdentitySet,
) -> bool {
    match kind {
        BookmarkKind::Scratch => false,
        BookmarkKind::Publishing | BookmarkKind::PullDefaultPublishing => {
            if let Some(pushvars) = pushvars {
                if let Some(value) = pushvars.get("BYPASS_READONLY") {
                    let bypass_allowed = repo_perm_checker
                        .check_if_read_only_bypass_allowed(idents)
                        .await;

                    let enforce_acl_check =
                        tunables().enforce_bypass_readonly_acl().unwrap_or_default();

                    if !bypass_allowed && enforce_acl_check {
                        return true;
                    }

                    if value.to_ascii_lowercase() == b"true" {
                        return false;
                    }
                }
            }
            true
        }
    }
}

pub(crate) async fn check_repo_lock(
    repo: &(impl RepoLockRef + RepoPermissionCheckerRef),
    kind: BookmarkKind,
    pushvars: Option<&HashMap<String, Bytes>>,
    idents: &MononokeIdentitySet,
) -> Result<(), BookmarkMovementError> {
    if should_check_repo_lock(kind, pushvars, repo.repo_permission_checker(), idents).await {
        let state = repo
            .repo_lock()
            .check_repo_lock()
            .await
            .context("Failed to fetch repo lock state")?;

        if let RepoLockState::Locked(reason) = state {
            return Err(BookmarkMovementError::RepoLocked(reason));
        }
    }

    Ok(())
}

pub(crate) struct RepoLockPushrebaseHook {
    transaction_repo_lock: TransactionRepoLock,
}

impl RepoLockPushrebaseHook {
    pub(crate) async fn new(
        repo_id: RepositoryId,
        kind: BookmarkKind,
        pushvars: Option<&HashMap<String, Bytes>>,
        repo_perm_checker: &dyn RepoPermissionChecker,
        idents: &MononokeIdentitySet,
    ) -> Option<Box<dyn PushrebaseHook>> {
        if should_check_repo_lock(kind, pushvars, repo_perm_checker, idents).await {
            let hook = Box::new(RepoLockPushrebaseHook {
                transaction_repo_lock: TransactionRepoLock::new(repo_id),
            });
            Some(hook as Box<dyn PushrebaseHook>)
        } else {
            None
        }
    }
}

#[async_trait]
impl PushrebaseHook for RepoLockPushrebaseHook {
    async fn in_critical_section(&self) -> Result<Box<dyn PushrebaseCommitHook>> {
        let hook = Box::new(RepoLockCommitTransactionHook {
            transaction_repo_lock: self.transaction_repo_lock,
        });
        Ok(hook as Box<dyn PushrebaseCommitHook>)
    }
}

struct RepoLockCommitTransactionHook {
    transaction_repo_lock: TransactionRepoLock,
}

#[async_trait]
impl PushrebaseCommitHook for RepoLockCommitTransactionHook {
    fn post_rebase_changeset(
        &mut self,
        _bcs_old: ChangesetId,
        _bcs_new: &mut BonsaiChangesetMut,
    ) -> Result<()> {
        Ok(())
    }

    async fn into_transaction_hook(
        self: Box<Self>,
        _ctx: &CoreContext,
        _rebased: &RebasedChangesets,
    ) -> Result<Box<dyn PushrebaseTransactionHook>> {
        Ok(self as Box<dyn PushrebaseTransactionHook>)
    }
}

#[async_trait]
impl PushrebaseTransactionHook for RepoLockCommitTransactionHook {
    async fn populate_transaction(
        &self,
        _ctx: &CoreContext,
        txn: Transaction,
    ) -> Result<Transaction, BookmarkTransactionError> {
        let (txn, state) = self
            .transaction_repo_lock
            .check_repo_lock_with_transaction(txn)
            .await
            .context("Failed to fetch repo lock state")?;
        if let RepoLockState::Locked(reason) = state {
            return Err(BookmarkTransactionError::Other(anyhow!(
                "Repo is locked: {}",
                reason
            )));
        }

        Ok(txn)
    }
}


/*
 * 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 bookmarks::BookmarkKey;
use bookmarks::BookmarkKind;
use bookmarks::BookmarkUpdateReason;
use context::CoreContext;
use futures::stream;
use futures::StreamExt;
use futures::TryStreamExt;
use metaconfig_types::RepoConfigRef;
use mononoke_types::ChangesetId;
use repo_authorization::AuthorizationContext;
use repo_cross_repo::RepoCrossRepoRef;
use repo_identity::RepoIdentityRef;
use tunables::tunables;

use crate::BookmarkMovementError;
use crate::Repo;

pub(crate) fn should_run_hooks(authz: &AuthorizationContext, reason: BookmarkUpdateReason) -> bool {
    if authz.is_service() {
        reason == BookmarkUpdateReason::Pushrebase
            && tunables()
                .enable_hooks_on_service_pushrebase()
                .unwrap_or_default()
    } else {
        true
    }
}

#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum BookmarkKindRestrictions {
    AnyKind,
    OnlyScratch,
    OnlyPublishing,
}

impl BookmarkKindRestrictions {
    pub(crate) fn check_kind(
        &self,
        repo: &impl RepoConfigRef,
        name: &BookmarkKey,
    ) -> Result<BookmarkKind, BookmarkMovementError> {
        let infinitepush_params = &repo.repo_config().infinitepush;
        match (self, &infinitepush_params.namespace) {
            (Self::OnlyScratch, None) => Err(BookmarkMovementError::ScratchBookmarksDisabled {
                bookmark: name.clone(),
            }),
            (Self::OnlyScratch, Some(namespace)) if !namespace.matches_bookmark(name) => {
                Err(BookmarkMovementError::InvalidScratchBookmark {
                    bookmark: name.clone(),
                    pattern: namespace.as_str().to_string(),
                })
            }
            (Self::OnlyPublishing, Some(namespace)) if namespace.matches_bookmark(name) => {
                Err(BookmarkMovementError::InvalidPublishingBookmark {
                    bookmark: name.clone(),
                    pattern: namespace.as_str().to_string(),
                })
            }
            (_, Some(namespace)) if namespace.matches_bookmark(name) => Ok(BookmarkKind::Scratch),
            (_, _) => Ok(BookmarkKind::Publishing),
        }
    }
}

pub(crate) async fn check_restriction_ensure_ancestor_of(
    ctx: &CoreContext,
    repo: &impl Repo,
    bookmark_to_move: &BookmarkKey,
    target: ChangesetId,
) -> Result<(), BookmarkMovementError> {
    // NOTE: Obviously this is a little racy, but the bookmark could move after we check, so it
    // doesn't matter.

    let mut descendant_bookmarks = vec![];
    for attr in repo.repo_bookmark_attrs().select(bookmark_to_move) {
        if let Some(descendant_bookmark) = &attr.params().ensure_ancestor_of {
            descendant_bookmarks.push(descendant_bookmark);
        }
    }

    if let Some(config) = &repo.repo_config().pushrebase.globalrev_config {
        if config.small_repo_id.is_none() {
            // On large repo, it's fine not to be descendant of the bookmark.
            descendant_bookmarks.push(&config.publishing_bookmark);
        }
    }

    stream::iter(descendant_bookmarks)
        .map(Ok)
        .try_for_each_concurrent(10, |descendant_bookmark| async move {
            let is_ancestor =
                ensure_ancestor_of(ctx, repo, bookmark_to_move, descendant_bookmark, target)
                    .await?;
            if !is_ancestor {
                let e = BookmarkMovementError::RequiresAncestorOf {
                    bookmark: bookmark_to_move.clone(),
                    descendant_bookmark: descendant_bookmark.clone(),
                };
                return Err(e);
            }
            Ok(())
        })
        .await?;

    Ok(())
}

pub(crate) async fn ensure_ancestor_of(
    ctx: &CoreContext,
    repo: &impl Repo,
    bookmark_to_move: &BookmarkKey,
    descendant_bookmark: &BookmarkKey,
    target: ChangesetId,
) -> Result<bool, BookmarkMovementError> {
    let descendant_cs_id = repo
        .bookmarks()
        .get(ctx.clone(), descendant_bookmark)
        .await?
        .ok_or_else(|| {
            anyhow::anyhow!(
                "Bookmark '{}' does not exist, but it should be a descendant of '{}'!",
                descendant_bookmark,
                bookmark_to_move
            )
        })?;

    Ok(repo
        .commit_graph()
        .is_ancestor(ctx, target, descendant_cs_id)
        .await?)
}

pub fn check_bookmark_sync_config(
    repo: &(impl RepoIdentityRef + RepoCrossRepoRef),
    bookmark: &BookmarkKey,
    kind: BookmarkKind,
) -> Result<(), BookmarkMovementError> {
    match kind {
        BookmarkKind::Publishing | BookmarkKind::PullDefaultPublishing => {
            if repo
                .repo_cross_repo()
                .live_commit_sync_config()
                .push_redirector_enabled_for_public(repo.repo_identity().id())
            {
                return Err(BookmarkMovementError::PushRedirectorEnabledForPublishing {
                    bookmark: bookmark.clone(),
                });
            }
        }
        BookmarkKind::Scratch => {
            if repo
                .repo_cross_repo()
                .live_commit_sync_config()
                .push_redirector_enabled_for_draft(repo.repo_identity().id())
            {
                return Err(BookmarkMovementError::PushRedirectorEnabledForScratch {
                    bookmark: bookmark.clone(),
                });
            }
        }
    }
    Ok(())
}


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

use std::collections::HashMap;

use anyhow::Result;
use bookmarks::BookmarkUpdateReason;
use bookmarks_types::BookmarkKey;
use bookmarks_types::BookmarkKind;
use bytes::Bytes;
use context::CoreContext;
use hooks::CrossRepoPushSource;
use hooks::HookManager;
use mononoke_types::BonsaiChangeset;
use mononoke_types::ChangesetId;
use repo_authorization::AuthorizationContext;
use repo_authorization::RepoWriteOperation;
use repo_update_logger::find_draft_ancestors;
use repo_update_logger::log_bookmark_operation;
use repo_update_logger::log_new_bonsai_changesets;
use repo_update_logger::BookmarkInfo;
use repo_update_logger::BookmarkOperation;

use crate::affected_changesets::AdditionalChangesets;
use crate::affected_changesets::AffectedChangesets;
use crate::repo_lock::check_repo_lock;
use crate::restrictions::check_bookmark_sync_config;
use crate::restrictions::BookmarkKindRestrictions;
use crate::BookmarkMovementError;
use crate::Repo;

/// The old and new changeset during a bookmark update.
///
/// This is a struct to make sure it is clear which is the old target and which is the new.
pub struct BookmarkUpdateTargets {
    pub old: ChangesetId,
    pub new: ChangesetId,
}

/// Which kinds of bookmark updates are allowed for a request.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum BookmarkUpdatePolicy {
    /// Only allow fast-forward moves (updates where the new target is a descendant
    /// of the old target).
    FastForwardOnly,

    /// Allow any update that is permitted for the bookmark by repo config.
    AnyPermittedByConfig,
}

impl BookmarkUpdatePolicy {
    async fn check_update_permitted(
        &self,
        ctx: &CoreContext,
        repo: &impl Repo,
        bookmark: &BookmarkKey,
        targets: &BookmarkUpdateTargets,
    ) -> Result<(), BookmarkMovementError> {
        let fast_forward_only = match self {
            Self::FastForwardOnly => true,
            Self::AnyPermittedByConfig => repo.repo_bookmark_attrs().is_fast_forward_only(bookmark),
        };
        if fast_forward_only && targets.old != targets.new {
            // Check that this move is a fast-forward move.
            if !repo
                .commit_graph()
                .is_ancestor(ctx, targets.old, targets.new)
                .await?
            {
                return Err(BookmarkMovementError::NonFastForwardMove {
                    bookmark: bookmark.clone(),
                    from: targets.old,
                    to: targets.new,
                });
            }
        }
        Ok(())
    }
}

#[must_use = "UpdateBookmarkOp must be run to have an effect"]
pub struct UpdateBookmarkOp<'op> {
    bookmark: &'op BookmarkKey,
    targets: BookmarkUpdateTargets,
    update_policy: BookmarkUpdatePolicy,
    reason: BookmarkUpdateReason,
    kind_restrictions: BookmarkKindRestrictions,
    cross_repo_push_source: CrossRepoPushSource,
    affected_changesets: AffectedChangesets,
    pushvars: Option<&'op HashMap<String, Bytes>>,
    log_new_public_commits_to_scribe: bool,
    only_log_acl_checks: bool,
}

impl<'op> UpdateBookmarkOp<'op> {
    pub fn new(
        bookmark: &'op BookmarkKey,
        targets: BookmarkUpdateTargets,
        update_policy: BookmarkUpdatePolicy,
        reason: BookmarkUpdateReason,
    ) -> UpdateBookmarkOp<'op> {
        UpdateBookmarkOp {
            bookmark,
            targets,
            update_policy,
            reason,
            kind_restrictions: BookmarkKindRestrictions::AnyKind,
            cross_repo_push_source: CrossRepoPushSource::NativeToThisRepo,
            affected_changesets: AffectedChangesets::new(),
            pushvars: None,
            log_new_public_commits_to_scribe: false,
            only_log_acl_checks: false,
        }
    }

    pub fn only_if_scratch(mut self) -> Self {
        self.kind_restrictions = BookmarkKindRestrictions::OnlyScratch;
        self
    }

    pub fn only_if_public(mut self) -> Self {
        self.kind_restrictions = BookmarkKindRestrictions::OnlyPublishing;
        self
    }

    pub fn with_pushvars(mut self, pushvars: Option<&'op HashMap<String, Bytes>>) -> Self {
        self.pushvars = pushvars;
        self
    }

    /// Include bonsai changesets for changesets that have just been added to
    /// the repository.
    pub fn with_new_changesets(
        mut self,
        changesets: HashMap<ChangesetId, BonsaiChangeset>,
    ) -> Self {
        self.affected_changesets.add_new_changesets(changesets);
        self
    }

    pub fn with_push_source(mut self, cross_repo_push_source: CrossRepoPushSource) -> Self {
        self.cross_repo_push_source = cross_repo_push_source;
        self
    }

    pub fn log_new_public_commits_to_scribe(mut self) -> Self {
        self.log_new_public_commits_to_scribe = true;
        self
    }

    pub fn only_log_acl_checks(mut self, only_log: bool) -> Self {
        self.only_log_acl_checks = only_log;
        self
    }

    pub async fn run(
        mut self,
        ctx: &'op CoreContext,
        authz: &'op AuthorizationContext,
        repo: &'op impl Repo,
        hook_manager: &'op HookManager,
    ) -> Result<(), BookmarkMovementError> {
        let kind = self.kind_restrictions.check_kind(repo, self.bookmark)?;

        if self.only_log_acl_checks {
            if authz
                .check_repo_write(ctx, repo, RepoWriteOperation::UpdateBookmark(kind))
                .await
                .is_denied()
            {
                ctx.scuba()
                    .clone()
                    .log_with_msg("Repo write ACL check would fail for bookmark update", None);
            }
        } else {
            authz
                .require_repo_write(ctx, repo, RepoWriteOperation::UpdateBookmark(kind))
                .await?;
        }
        authz
            .require_bookmark_modify(ctx, repo, self.bookmark)
            .await?;

        check_bookmark_sync_config(repo, self.bookmark, kind)?;

        self.update_policy
            .check_update_permitted(ctx, repo, self.bookmark, &self.targets)
            .await?;

        self.affected_changesets
            .check_restrictions(
                ctx,
                authz,
                repo,
                hook_manager,
                self.bookmark,
                self.pushvars,
                self.reason,
                kind,
                AdditionalChangesets::Range {
                    head: self.targets.new,
                    base: self.targets.old,
                },
                self.cross_repo_push_source,
            )
            .await?;

        check_repo_lock(repo, kind, self.pushvars, ctx.metadata().identities()).await?;

        let mut txn = repo.bookmarks().create_transaction(ctx.clone());
        let txn_hook;

        let commits_to_log = match kind {
            BookmarkKind::Scratch => {
                txn_hook = None;

                ctx.scuba()
                    .clone()
                    .add("bookmark", self.bookmark.to_string())
                    .log_with_msg("Updating scratch bookmark", None);
                txn.update_scratch(self.bookmark, self.targets.new, self.targets.old)?;

                vec![]
            }
            BookmarkKind::Publishing | BookmarkKind::PullDefaultPublishing => {
                crate::restrictions::check_restriction_ensure_ancestor_of(
                    ctx,
                    repo,
                    self.bookmark,
                    self.targets.new,
                )
                .await?;

                let txn_hook_fut = crate::git_mapping::populate_git_mapping_txn_hook(
                    ctx,
                    repo,
                    self.targets.new,
                    self.affected_changesets.new_changesets(),
                );

                let to_log = async {
                    if self.log_new_public_commits_to_scribe {
                        let res = find_draft_ancestors(ctx, repo, self.targets.new).await;
                        match res {
                            Ok(bcss) => bcss,
                            Err(err) => {
                                ctx.scuba().clone().log_with_msg(
                                    "Failed to find draft ancestors",
                                    Some(format!("{}", err)),
                                );
                                vec![]
                            }
                        }
                    } else {
                        vec![]
                    }
                };

                let (txn_hook_res, to_log) = futures::join!(txn_hook_fut, to_log);
                txn_hook = txn_hook_res?;

                ctx.scuba()
                    .clone()
                    .add("bookmark", self.bookmark.to_string())
                    .log_with_msg("Updating public bookmark", None);

                txn.update(
                    self.bookmark,
                    self.targets.new,
                    self.targets.old,
                    self.reason,
                )?;
                to_log
            }
        };

        let ok = match txn_hook {
            Some(txn_hook) => txn.commit_with_hook(txn_hook).await?,
            None => txn.commit().await?,
        };
        if !ok {
            return Err(BookmarkMovementError::TransactionFailed);
        }

        if self.log_new_public_commits_to_scribe {
            log_new_bonsai_changesets(ctx, repo, self.bookmark, kind, commits_to_log.clone()).await;
        }
        let info = BookmarkInfo {
            bookmark_name: self.bookmark.clone(),
            bookmark_kind: kind,
            operation: BookmarkOperation::Update(self.targets.old, self.targets.new),
            reason: self.reason,
        };
        log_bookmark_operation(ctx, repo, &info).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::fmt;
use std::ops::Bound;
use std::ops::Range;
use std::ops::RangeBounds;
use std::ops::RangeFrom;
use std::ops::RangeFull;
use std::str::FromStr;

use anyhow::format_err;
use anyhow::Error;
use ascii::AsciiChar;
use ascii::AsciiString;
use quickcheck::Arbitrary;
use quickcheck::Gen;
use quickcheck_arbitrary_derive::Arbitrary;
use sql::mysql;
use sql::mysql_async::prelude::ConvIr;
use sql::mysql_async::prelude::FromValue;
use sql::mysql_async::FromValueError;
use sql::mysql_async::Value;

/// This enum represents how fresh you want results to be. MostRecent will go to the master, so you
/// normally don't want to issue queries using MostRecent unless you have a very good reason.
/// MaybeStale will go to a replica, which might lag behind the master (there is no SLA on
/// replication lag). MaybeStale reads might also be served from a local cache.

#[derive(Arbitrary, Debug, Eq, PartialEq, Clone, Copy)]
pub enum Freshness {
    MostRecent,
    MaybeStale,
}

#[derive(Arbitrary, Clone, Debug, Eq, Hash, PartialEq)]
pub struct Bookmark {
    pub key: BookmarkKey,
    pub kind: BookmarkKind,
}

impl Bookmark {
    pub fn new(key: BookmarkKey, kind: BookmarkKind) -> Self {
        Bookmark { key, kind }
    }

    pub fn into_key(self) -> BookmarkKey {
        self.key
    }

    pub fn key(&self) -> &BookmarkKey {
        &self.key
    }

    pub fn name(&self) -> &BookmarkName {
        &self.key.name
    }

    pub fn kind(&self) -> &BookmarkKind {
        &self.kind
    }

    pub fn publishing(&self) -> bool {
        use BookmarkKind::*;

        match self.kind {
            Scratch => false,
            Publishing => true,
            PullDefaultPublishing => true,
        }
    }

    pub fn pull_default(&self) -> bool {
        use BookmarkKind::*;

        match self.kind {
            Scratch => false,
            Publishing => false,
            PullDefaultPublishing => true,
        }
    }
}

#[derive(Arbitrary, Clone, Debug, Eq, Hash, PartialEq, Ord, PartialOrd)]
pub struct BookmarkKey {
    name: BookmarkName,
    category: BookmarkCategory,
}

impl FromStr for BookmarkKey {
    type Err = Error;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        BookmarkKey::new(s)
    }
}

impl fmt::Display for BookmarkKey {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}", self.name)
    }
}

impl BookmarkKey {
    pub fn with_name_and_category(name: BookmarkName, category: BookmarkCategory) -> Self {
        Self { name, category }
    }

    pub fn with_name(name: BookmarkName) -> Self {
        Self::with_name_and_category(name, Default::default())
    }

    /// First bookmark key with a given name.  Used for constructing ranges
    /// which include all categories.
    pub fn with_first_name(name: BookmarkName) -> Self {
        Self::with_name_and_category(name, BookmarkCategory::ALL.first().copied().unwrap())
    }

    /// Last bookmark key with a given name.  Used for constructing ranges
    /// which include all categories.
    pub fn with_last_name(name: BookmarkName) -> Self {
        Self::with_name_and_category(name, BookmarkCategory::ALL.last().copied().unwrap())
    }

    pub fn new<B: AsRef<str>>(bookmark: B) -> Result<Self, Error> {
        Ok(Self {
            name: BookmarkName::new(bookmark)?,
            category: Default::default(),
        })
    }

    pub fn new_ascii(bookmark: AsciiString) -> Self {
        Self {
            name: BookmarkName::new_ascii(bookmark),
            category: Default::default(),
        }
    }

    pub fn as_ascii(&self) -> &AsciiString {
        self.name.as_ascii()
    }

    pub fn into_string(self) -> String {
        self.name.into_string()
    }

    pub fn into_byte_vec(self) -> Vec<u8> {
        self.name.into_byte_vec()
    }

    pub fn as_str(&self) -> &str {
        self.name.as_str()
    }

    pub fn name(&self) -> &BookmarkName {
        &self.name
    }

    pub fn category(&self) -> &BookmarkCategory {
        &self.category
    }

    pub fn into_name(self) -> BookmarkName {
        self.name
    }
}

#[derive(Clone, Debug, Eq, Hash, PartialEq, Ord, PartialOrd)]
#[derive(mysql::OptTryFromRowField)]
pub struct BookmarkName {
    bookmark: AsciiString,
}

impl FromStr for BookmarkName {
    type Err = Error;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        BookmarkName::new(s)
    }
}

impl fmt::Display for BookmarkName {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}", self.bookmark)
    }
}

impl Arbitrary for BookmarkName {
    fn arbitrary(g: &mut Gen) -> Self {
        // NOTE: We use a specific large size here because our tests exercise DB Bookmarks, which
        // require unique names in the DB.
        let size = 128;
        let mut bookmark = AsciiString::with_capacity(size);
        for _ in 0..size {
            bookmark.push(ascii_ext::AsciiChar::arbitrary(g).0);
        }
        Self { bookmark }
    }
}

impl BookmarkName {
    pub fn new<B: AsRef<str>>(bookmark: B) -> Result<Self, Error> {
        Ok(Self {
            bookmark: AsciiString::from_ascii(bookmark.as_ref())
                .map_err(|bytes| format_err!("non-ascii bookmark name: {:?}", bytes))?,
        })
    }

    pub fn new_ascii(bookmark: AsciiString) -> Self {
        Self { bookmark }
    }

    pub fn as_ascii(&self) -> &AsciiString {
        &self.bookmark
    }

    pub fn into_string(self) -> String {
        self.bookmark.into()
    }

    pub fn into_byte_vec(self) -> Vec<u8> {
        self.bookmark.into()
    }

    pub fn as_str(&self) -> &str {
        self.bookmark.as_str()
    }
}

impl TryFrom<&str> for BookmarkName {
    type Error = Error;

    fn try_from(s: &str) -> Result<Self, Error> {
        Self::new(s)
    }
}

impl From<BookmarkName> for Value {
    fn from(bookmark: BookmarkName) -> Self {
        Value::Bytes(bookmark.bookmark.into())
    }
}

impl ConvIr<BookmarkName> for BookmarkName {
    fn new(v: Value) -> Result<Self, FromValueError> {
        match v {
            Value::Bytes(bytes) => AsciiString::from_ascii(bytes)
                .map_err(|err| FromValueError(Value::Bytes(err.into_source())))
                .map(BookmarkName::new_ascii),
            v => Err(FromValueError(v)),
        }
    }

    fn commit(self) -> BookmarkName {
        self
    }

    fn rollback(self) -> Value {
        self.into()
    }
}

impl FromValue for BookmarkName {
    type Intermediate = BookmarkName;
}

impl From<BookmarkPrefix> for Value {
    fn from(bookmark_prefix: BookmarkPrefix) -> Self {
        Value::Bytes(bookmark_prefix.bookmark_prefix.into())
    }
}

/// Describes the behavior of a Bookmark in Mercurial operations.
#[derive(
    Arbitrary,
    Clone,
    Debug,
    Eq,
    Hash,
    PartialEq,
    Copy,
    clap::ValueEnum,
    mysql::OptTryFromRowField
)]
pub enum BookmarkKind {
    Scratch,
    Publishing,
    PullDefaultPublishing,
}

impl BookmarkKind {
    pub const ALL: &'static [BookmarkKind] = &[
        BookmarkKind::Scratch,
        BookmarkKind::Publishing,
        BookmarkKind::PullDefaultPublishing,
    ];
    pub const ALL_PUBLISHING: &'static [BookmarkKind] = &[
        BookmarkKind::Publishing,
        BookmarkKind::PullDefaultPublishing,
    ];

    pub fn is_public(&self) -> bool {
        match self {
            BookmarkKind::Scratch => false,
            BookmarkKind::Publishing | BookmarkKind::PullDefaultPublishing => true,
        }
    }
}

impl std::fmt::Display for BookmarkKind {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        use BookmarkKind::*;

        let s = match self {
            Scratch => "scratch",
            Publishing => "publishing",
            PullDefaultPublishing => "pull_default",
        };

        write!(f, "{}", s)
    }
}

const SCRATCH_KIND: &[u8] = b"scratch";
const PUBLISHING_KIND: &[u8] = b"publishing";
const PULL_DEFAULT_KIND: &[u8] = b"pull_default";

impl ConvIr<BookmarkKind> for BookmarkKind {
    fn new(v: Value) -> Result<Self, FromValueError> {
        use BookmarkKind::*;

        match v {
            Value::Bytes(ref b) if b == SCRATCH_KIND => Ok(Scratch),
            Value::Bytes(ref b) if b == PUBLISHING_KIND => Ok(Publishing),
            Value::Bytes(ref b) if b == PULL_DEFAULT_KIND => Ok(PullDefaultPublishing),
            v => Err(FromValueError(v)),
        }
    }

    fn commit(self) -> BookmarkKind {
        self
    }

    fn rollback(self) -> Value {
        self.into()
    }
}

impl FromValue for BookmarkKind {
    type Intermediate = BookmarkKind;
}

impl From<BookmarkKind> for Value {
    fn from(bookmark_update_reason: BookmarkKind) -> Self {
        use BookmarkKind::*;

        match bookmark_update_reason {
            Scratch => Value::Bytes(SCRATCH_KIND.to_vec()),
            Publishing => Value::Bytes(PUBLISHING_KIND.to_vec()),
            PullDefaultPublishing => Value::Bytes(PULL_DEFAULT_KIND.to_vec()),
        }
    }
}

/// Historically, in Mononoke, bookmark names have been unique.
/// Since we import repositories from Git, this invariant is not true anymore.
/// A tag, a branch or a note can have the same name as they are namespaced in Git.
/// BookmarkCategory allows to differentiate between e.g. a tag called foo and a branch called
/// foo.
/// See https://docs.rs/git-ref/0.23.1/git_ref/enum.Category.html for an exhaustive
/// list of what git supports.
/// Here, we explicitly don't support worktrees or the distinction between remote and local
/// branches as these concepts don't fit in the mononoke data model. We also don't support pseudo
/// refs such as HEAD as the server doesn't need to know about this.
#[derive(
    Arbitrary,
    Clone,
    Debug,
    Eq,
    Hash,
    PartialEq,
    Copy,
    Ord,
    PartialOrd,
    clap::ValueEnum,
    mysql::OptTryFromRowField
)]
pub enum BookmarkCategory {
    /// A bookmark created in Mononoke or imported from a Git branch (ref living under
    /// `refs/heads` or `refs/remotes/<remote_name>`)
    Branch,
    /// A bookmark created from importing a Git tag (under `refs/tags`)
    Tag,
    /// A bookmark created from importing a Git note (under `refs/notes`)
    Note,
}

impl BookmarkCategory {
    pub const ALL: &'static [BookmarkCategory] = &[Self::Branch, Self::Tag, Self::Note];
}

const BRANCH_CATEGORY: &[u8] = b"branch";
const TAG_CATEGORY: &[u8] = b"tag";
const NOTE_CATEGORY: &[u8] = b"note";

impl Default for BookmarkCategory {
    fn default() -> Self {
        Self::Branch
    }
}

impl From<BookmarkCategory> for Value {
    fn from(category: BookmarkCategory) -> Self {
        use BookmarkCategory::*;

        match category {
            Branch => Value::Bytes(BRANCH_CATEGORY.to_vec()),
            Tag => Value::Bytes(TAG_CATEGORY.to_vec()),
            Note => Value::Bytes(NOTE_CATEGORY.to_vec()),
        }
    }
}

impl ConvIr<BookmarkCategory> for BookmarkCategory {
    fn new(v: Value) -> Result<Self, FromValueError> {
        use BookmarkCategory::*;

        match v {
            Value::Bytes(ref b) if b == BRANCH_CATEGORY => Ok(Branch),
            Value::Bytes(ref b) if b == TAG_CATEGORY => Ok(Tag),
            Value::Bytes(ref b) if b == NOTE_CATEGORY => Ok(Note),
            v => Err(FromValueError(v)),
        }
    }

    fn commit(self) -> BookmarkCategory {
        self
    }

    fn rollback(self) -> Value {
        self.into()
    }
}

impl FromValue for BookmarkCategory {
    type Intermediate = BookmarkCategory;
}

impl std::fmt::Display for BookmarkCategory {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        use BookmarkCategory::*;

        let s = match self {
            Branch => "branch",
            Tag => "tag",
            Note => "note",
        };

        write!(f, "{}", s)
    }
}

/// Bookmark name filter for pagination.
///
/// If set to `BookmarkPagination::After(name)`, Filters bookmarks to those
/// starting after the given start point (exclusive).
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub enum BookmarkPagination {
    FromStart,
    After(BookmarkName),
}

/// Bookmark name filter for prefixes.
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct BookmarkPrefix {
    bookmark_prefix: AsciiString,
}

impl fmt::Display for BookmarkPrefix {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}", self.bookmark_prefix)
    }
}

impl FromStr for BookmarkPrefix {
    type Err = Error;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        BookmarkPrefix::new(s)
    }
}

pub enum BookmarkPrefixRange {
    /// All bookmarks in the given half-open range.
    Range(Range<BookmarkKey>),

    /// All bookmarks in the given range from an inclusive start.
    RangeFrom(RangeFrom<BookmarkKey>),

    /// All bookmarks.
    RangeFull(RangeFull),

    /// All bookmarks after the given name (exclusive).
    After(BookmarkKey),

    /// All bookmarks between the given names (exclusive on both sides).
    Between(BookmarkKey, BookmarkKey),

    /// No bookmarks.
    ///
    /// The `RangeBounds` methods must still return a value that
    /// includes a reference to a valid bookmark name, and must
    /// provide a valid range.  To do this, we use an arbitrary
    /// name owned by this `BookmarkPrefixRange`, and return
    /// the half-open range `[name, name)`, which is empty.
    Empty(BookmarkKey),
}

impl BookmarkPrefixRange {
    /// Modify a `BookmarkPrefixRange` to only include bookmarks
    /// after a given bookmark page start (exclusively).
    pub fn with_pagination(self, pagination: BookmarkPagination) -> BookmarkPrefixRange {
        use BookmarkPrefixRange::*;
        let last = BookmarkKey::with_last_name;
        // For `Empty` we only need an arbitrary value, so the last one will do.
        match pagination {
            BookmarkPagination::FromStart => self,
            BookmarkPagination::After(name) => match self {
                Range(r) if name >= *r.end.name() => Empty(last(name)),
                Range(r) if name >= *r.start.name() => Between(last(name), r.end),
                RangeFrom(r) if name >= *r.start.name() => After(last(name)),
                RangeFull(_) => After(last(name)),
                Between(_, e) if name >= *e.name() => Empty(last(name)),
                Between(s, e) if name >= *s.name() => Between(last(name), e),
                After(a) if name >= *a.name() => After(last(name)),
                range => range,
            },
        }
    }
}

impl RangeBounds<BookmarkKey> for BookmarkPrefixRange {
    fn start_bound(&self) -> Bound<&BookmarkKey> {
        use BookmarkPrefixRange::*;
        match self {
            Range(r) => r.start_bound(),
            RangeFrom(r) => r.start_bound(),
            RangeFull(r) => r.start_bound(),
            After(a) => Bound::Excluded(a),
            Between(s, _) => Bound::Excluded(s),
            Empty(n) => Bound::Included(n),
        }
    }

    fn end_bound(&self) -> Bound<&BookmarkKey> {
        use BookmarkPrefixRange::*;
        match self {
            Range(r) => r.end_bound(),
            RangeFrom(r) => r.end_bound(),
            RangeFull(r) => r.end_bound(),
            After(_) => Bound::Unbounded,
            Between(_, e) => Bound::Excluded(e),
            Empty(n) => Bound::Excluded(n),
        }
    }
}

impl BookmarkPrefix {
    pub fn new<B: AsRef<str>>(bookmark_prefix: B) -> Result<Self, Error> {
        Ok(Self {
            bookmark_prefix: AsciiString::from_ascii(bookmark_prefix.as_ref())
                .map_err(|bytes| format_err!("non-ascii bookmark prefix: {:?}", bytes))?,
        })
    }

    pub fn new_ascii(bookmark_prefix: AsciiString) -> Self {
        Self { bookmark_prefix }
    }

    pub fn empty() -> Self {
        Self {
            bookmark_prefix: AsciiString::default(),
        }
    }

    pub fn is_empty(&self) -> bool {
        self.bookmark_prefix.is_empty()
    }

    pub fn to_range(&self) -> BookmarkPrefixRange {
        match prefix_to_range_end(self.bookmark_prefix.clone()) {
            Some(range_end) => {
                let range = Range {
                    start: BookmarkKey::with_first_name(BookmarkName::new_ascii(
                        self.bookmark_prefix.clone(),
                    )),
                    end: BookmarkKey::with_first_name(BookmarkName::new_ascii(range_end)),
                };
                BookmarkPrefixRange::Range(range)
            }
            None => match self.bookmark_prefix.len() {
                0 => BookmarkPrefixRange::RangeFull(RangeFull),
                _ => {
                    let range = RangeFrom {
                        start: BookmarkKey::with_first_name(BookmarkName::new_ascii(
                            self.bookmark_prefix.clone(),
                        )),
                    };
                    BookmarkPrefixRange::RangeFrom(range)
                }
            },
        }
    }

    /// Convert the bookmark prefix to an escaped SQL pattern suitable for use
    /// in a LIKE expression.
    ///
    /// For example, `my_prefix` is converted to `my\_prefix%`.
    pub fn to_escaped_sql_like_pattern(&self) -> String {
        let mut like_pattern = String::with_capacity(self.bookmark_prefix.len());
        for ch in self.bookmark_prefix.chars() {
            if ch == '\\' || ch == '%' || ch == '_' {
                like_pattern.push('\\');
            }
            like_pattern.push(ch.into());
        }
        like_pattern.push('%');
        like_pattern
    }

    pub fn is_prefix_of(&self, bookmark: &BookmarkName) -> bool {
        bookmark
            .bookmark
            .as_bytes()
            .starts_with(self.bookmark_prefix.as_bytes())
    }
}

fn prefix_to_range_end(mut prefix: AsciiString) -> Option<AsciiString> {
    // If we have a prefix, then we need to take the last character of the prefix, increment it by
    // 1, then take that as an ASCII char. So, if you prefix is foobarA, then the range will be
    // from foobarA (inclusive) to foobarB (exclusive). Basically, what we're trying to implement
    // here is a little bit like Ruby's str#next.
    loop {
        match prefix.pop() {
            Some(chr) => match AsciiChar::from_ascii(chr.as_byte() + 1) {
                Ok(next_chr) => {
                    // Happy path, we found the next character, so just put that in and move on.
                    prefix.push(next_chr);
                    return Some(prefix);
                }
                Err(_) => {
                    // The last character doesn't fit in ASCII (i.e. it's DEL). This means we have
                    // something like foobaA[DEL]. In this case, we need to set the bound to be the
                    // character after the one before the DEL, so we want foobaB
                    continue;
                }
            },
            None => {
                // We exhausted the entire string. This will only happen if the string is 0 or more
                // DEL characters. In this case, return the fact that no next string can be
                // produced.
                return None;
            }
        }
    }
}

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

    use super::*;

    #[test]
    fn bookmark_kind_all_contains_all_kinds() {
        // Test that `BookmarkKind::ALL` does indeed contain all bookmark
        // kinds.  If you need to add a variant here, make sure you add it
        // to `BookmarkKind::ALL` or this test will fail.
        let mut count = 0;
        for kind in BookmarkKind::ALL.iter() {
            match kind {
                BookmarkKind::Scratch
                | BookmarkKind::Publishing
                | BookmarkKind::PullDefaultPublishing => count += 1,
            }
        }
        assert_eq!(count, BookmarkKind::ALL.len());
    }

    quickcheck! {
        fn test_prefix_range_contains_self(bookmark: Bookmark) -> bool {
            let prefix = BookmarkPrefix::new_ascii(bookmark.name().as_ascii().clone());
            prefix.to_range().contains(bookmark.key())
        }

        fn test_prefix_range_contains_its_suffixes(bookmark: Bookmark, more: ascii_ext::AsciiString) -> bool {
            let prefix = BookmarkPrefix::new_ascii(bookmark.name().as_ascii().clone());
            let mut name = bookmark.name().as_ascii().clone();
            name.push_str(&more.0);
            prefix.to_range().contains(&BookmarkKey::new_ascii(name))
        }

        fn test_prefix_range_does_not_contains_its_prefixes(bookmark: Bookmark, chr: ascii_ext::AsciiChar) -> bool {
            let mut prefix = bookmark.name().as_ascii().clone();
            prefix.push(chr.0);
            let prefix = BookmarkPrefix::new_ascii(prefix);

            !prefix.to_range().contains(bookmark.key())
        }

        fn test_empty_range_contains_any(bookmark: Bookmark) -> bool {
            let prefix = BookmarkPrefix::empty();
            prefix.to_range().contains(bookmark.key())
        }

        fn test_pagination_excludes_start(prefix_char: Option<ascii_ext::AsciiChar>, after: BookmarkName) -> bool {
            let prefix = match prefix_char {
                Some(ch) => BookmarkPrefix::new_ascii(AsciiString::from(&[ch.0][..])),
                None => BookmarkPrefix::empty(),
            };
            let pagination = BookmarkPagination::After(after.clone());
            let after_key = BookmarkKey::with_last_name(after);
            !prefix.to_range().with_pagination(pagination).contains(&after_key)
        }
    }
}


/*
 * 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 mononoke_types::RepositoryId;
use sql_construct::SqlConstruct;
use sql_construct::SqlConstructFromMetadataDatabaseConfig;
use sql_ext::SqlConnections;

use crate::store::SqlBookmarks;

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

impl SqlConstruct for SqlBookmarksBuilder {
    const LABEL: &'static str = "bookmarks";

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

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

impl SqlConstructFromMetadataDatabaseConfig for SqlBookmarksBuilder {}

impl SqlBookmarksBuilder {
    pub fn with_repo_id(self, repo_id: RepositoryId) -> SqlBookmarks {
        SqlBookmarks::new(repo_id, self.connections)
    }
}


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

pub mod builder;
pub mod store;
mod subscription;
pub mod transaction;

pub use crate::builder::SqlBookmarksBuilder;
pub use crate::store::ArcSqlBookmarks;

#[cfg(test)]
mod test {
    use std::collections::BTreeMap;
    use std::collections::HashSet;

    use anyhow::Result;
    use ascii::AsciiString;
    use bookmarks::Bookmark;
    use bookmarks::BookmarkCategory;
    use bookmarks::BookmarkKey;
    use bookmarks::BookmarkKind;
    use bookmarks::BookmarkPagination;
    use bookmarks::BookmarkPrefix;
    use bookmarks::BookmarkUpdateReason;
    use bookmarks::Bookmarks;
    use bookmarks::Freshness;
    use context::CoreContext;
    use fbinit::FacebookInit;
    use futures::stream::TryStreamExt;
    use mononoke_types::ChangesetId;
    use mononoke_types::RepositoryId;
    use mononoke_types_mocks::changesetid::ONES_CSID;
    use mononoke_types_mocks::changesetid::TWOS_CSID;
    use mononoke_types_mocks::repo::REPO_ZERO;
    use quickcheck::quickcheck;
    use sql_construct::SqlConstruct;
    use tokio::runtime::Runtime;

    use super::*;

    #[fbinit::test]
    async fn test_update_kind_compatibility(fb: FacebookInit) -> Result<()> {
        let ctx = CoreContext::test_mock(fb);
        let store = SqlBookmarksBuilder::with_sqlite_in_memory()
            .unwrap()
            .with_repo_id(REPO_ZERO);
        let scratch_name = BookmarkKey::new("book1").unwrap();
        let publishing_name = BookmarkKey::new("book2").unwrap();
        let pull_default_name = BookmarkKey::new("book3").unwrap();

        let conn = store.connections.write_connection.clone();

        let rows = vec![
            (
                &REPO_ZERO,
                &scratch_name,
                &ONES_CSID,
                &BookmarkKind::Scratch,
            ),
            (
                &REPO_ZERO,
                &publishing_name,
                &ONES_CSID,
                &BookmarkKind::Publishing,
            ),
            (
                &REPO_ZERO,
                &pull_default_name,
                &ONES_CSID,
                &BookmarkKind::PullDefaultPublishing,
            ),
        ];

        crate::transaction::insert_bookmarks(&conn, rows).await?;

        // Using 'create_scratch' to replace a non-scratch bookmark should fail.
        let mut txn = store.create_transaction(ctx.clone());
        txn.create_scratch(&publishing_name, ONES_CSID)?;
        assert!(!txn.commit().await?);

        // Using 'create' to replace a scratch bookmark should fail.
        let mut txn = store.create_transaction(ctx.clone());
        txn.create(&scratch_name, ONES_CSID, BookmarkUpdateReason::TestMove)?;
        assert!(!txn.commit().await?);

        // Using 'update_scratch' to update a publishing bookmark should fail.
        let mut txn = store.create_transaction(ctx.clone());
        txn.update_scratch(&publishing_name, TWOS_CSID, ONES_CSID)?;
        assert!(!txn.commit().await?);

        // Using 'update_scratch' to update a pull-default bookmark should fail.
        let mut txn = store.create_transaction(ctx.clone());
        txn.update_scratch(&pull_default_name, TWOS_CSID, ONES_CSID)?;
        assert!(!txn.commit().await?);

        // Using 'update' to update a publishing bookmark should succeed.
        let mut txn = store.create_transaction(ctx.clone());
        txn.update(
            &publishing_name,
            TWOS_CSID,
            ONES_CSID,
            BookmarkUpdateReason::TestMove,
        )?;
        assert!(txn.commit().await?);

        // Using 'update' to update a pull-default bookmark should succeed.
        let mut txn = store.create_transaction(ctx.clone());
        txn.update(
            &pull_default_name,
            TWOS_CSID,
            ONES_CSID,
            BookmarkUpdateReason::TestMove,
        )?;
        assert!(txn.commit().await?);

        // Using 'update' to update a scratch bookmark should fail.
        let mut txn = store.create_transaction(ctx.clone());
        txn.update(
            &scratch_name,
            TWOS_CSID,
            ONES_CSID,
            BookmarkUpdateReason::TestMove,
        )?;
        assert!(!txn.commit().await?);

        // Using 'update_scratch' to update a scratch bookmark should succeed.
        let mut txn = store.create_transaction(ctx.clone());
        txn.update_scratch(&scratch_name, TWOS_CSID, ONES_CSID)?;
        assert!(txn.commit().await?);

        Ok(())
    }

    fn mock_bookmarks_response(
        bookmarks: &BTreeMap<BookmarkKey, (BookmarkKind, ChangesetId)>,
        prefix: &BookmarkPrefix,
        categories: &[BookmarkCategory],
        kinds: &[BookmarkKind],
        pagination: &BookmarkPagination,
        limit: u64,
    ) -> Vec<(Bookmark, ChangesetId)> {
        let range = prefix.to_range().with_pagination(pagination.clone());
        bookmarks
            .range(range)
            .filter_map(|(key, (kind, csid))| {
                let category = key.category();
                if kinds.iter().any(|k| kind == k) && categories.iter().any(|c| category == c) {
                    let bookmark = Bookmark {
                        key: key.clone(),
                        kind: *kind,
                    };
                    Some((bookmark, *csid))
                } else {
                    None
                }
            })
            .take(limit as usize)
            .collect()
    }

    fn insert_then_query(
        fb: FacebookInit,
        bookmarks: &BTreeMap<BookmarkKey, (BookmarkKind, ChangesetId)>,
        query_freshness: Freshness,
        query_prefix: &BookmarkPrefix,
        query_categories: &[BookmarkCategory],
        query_kinds: &[BookmarkKind],
        query_pagination: &BookmarkPagination,
        query_limit: u64,
    ) -> Vec<(Bookmark, ChangesetId)> {
        let rt = Runtime::new().unwrap();

        let ctx = CoreContext::test_mock(fb);
        let repo_id = RepositoryId::new(123);

        let store = SqlBookmarksBuilder::with_sqlite_in_memory()
            .unwrap()
            .with_repo_id(repo_id);

        let rows = bookmarks
            .iter()
            .map(|(bookmark, (kind, changeset_id))| (&repo_id, bookmark, changeset_id, kind));

        rt.block_on(crate::transaction::insert_bookmarks(
            &store.connections.write_connection,
            rows,
        ))
        .expect("insert failed");

        let response = store
            .list(
                ctx,
                query_freshness,
                query_prefix,
                query_categories,
                query_kinds,
                query_pagination,
                query_limit,
            )
            .try_collect::<Vec<_>>();

        rt.block_on(response).expect("query failed")
    }

    quickcheck! {
        fn responses_match(
            fb: FacebookInit,
            bookmarks: BTreeMap<BookmarkKey, (BookmarkKind, ChangesetId)>,
            freshness: Freshness,
            categories: HashSet<BookmarkCategory>,
            kinds: HashSet<BookmarkKind>,
            prefix_char: Option<ascii_ext::AsciiChar>,
            after: Option<BookmarkKey>,
            limit: u64
        ) -> bool {
            // Test that requests return what is expected.
            let categories: Vec<_> = categories.into_iter().collect();
            let kinds: Vec<_> = kinds.into_iter().collect();
            let prefix = match prefix_char {
                Some(ch) => BookmarkPrefix::new_ascii(AsciiString::from(&[ch.0][..])),
                None => BookmarkPrefix::empty(),
            };
            let pagination = match after {
                Some(key) => BookmarkPagination::After(key.into_name()),
                None => BookmarkPagination::FromStart,
            };
            let mut have = insert_then_query(
                fb,
                &bookmarks,
                freshness,
                &prefix,
                categories.as_slice(),
                kinds.as_slice(),
                &pagination,
                limit,
            );
            let mut want = mock_bookmarks_response(
                &bookmarks,
                &prefix,
                categories.as_slice(),
                kinds.as_slice(),
                &pagination,
                limit,
            );
            have.sort_by_key(|(_, csid)| *csid);
            want.sort_by_key(|(_, csid)| *csid);
            have == want
        }
    }
}