/*
 * 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::ops::Range;

use anyhow::anyhow;
use anyhow::Context;
use anyhow::Error;
use bytes::Bytes;
use futures::try_join;
use lazy_static::lazy_static;
use mononoke_types::ContentMetadataV2;
use regex::Regex;
pub use xdiff::CopyInfo;

use crate::changeset_path::ChangesetPathContentContext;
use crate::errors::MononokeError;
use crate::file::FileType;
use crate::FileContext;

lazy_static! {
    static ref BEGIN_MANUAL_SECTION_REGEX: Regex =
        Regex::new(r"^(\s|[[:punct:]])*BEGIN MANUAL SECTION").unwrap();
    static ref END_MANUAL_SECTION_REGEX: Regex =
        Regex::new(r"^(\s|[[:punct:]])*END MANUAL SECTION").unwrap();
}

/// A path difference between two commits.
///
/// A ChangesetPathDiffContext shows the difference between two corresponding
/// files in the commits.
///
/// The changed, copied and moved variants contain the items in the same
/// order as the commits that were compared, i.e. in `a.diff(b)`, they
/// will contain `(a, b)`.  This usually means the destination is first.
#[derive(Clone, Debug)]
pub enum ChangesetPathDiffContext {
    Added(ChangesetPathContentContext),
    Removed(ChangesetPathContentContext),
    Changed(ChangesetPathContentContext, ChangesetPathContentContext),
    Copied(ChangesetPathContentContext, ChangesetPathContentContext),
    Moved(ChangesetPathContentContext, ChangesetPathContentContext),
}

/// A diff between two files in extended unified diff format
pub struct UnifiedDiff {
    /// Raw diff as bytes.
    pub raw_diff: Vec<u8>,
    /// One of the diffed files is binary, raw diff contains just a placeholder.
    pub is_binary: bool,
}

#[derive(Clone, Copy, Eq, PartialEq)]
pub enum UnifiedDiffMode {
    /// Unified diff is generated inline as normal.
    Inline,
    /// Content is not fetched - instead a placeholder diff like
    ///
    /// diff --git a/file.txt b/file.txt
    /// Binary file file.txt has changed
    ///
    /// is generated
    OmitContent,
}

/// Metadata about the differences between two files that is useful to
/// Phabricator.
pub struct MetadataDiff {
    /// Information about the file before the change.
    pub old_file_info: MetadataDiffFileInfo,

    /// Information about the file after the change.
    pub new_file_info: MetadataDiffFileInfo,

    /// Lines count in the diff between the two files.
    pub lines_count: Option<MetadataDiffLinesCount>,
}

/// File information that concerns the metadata diff.
pub struct MetadataDiffFileInfo {
    /// File type (file, exec, or link)
    pub file_type: Option<FileType>,

    /// File content type (text, non-utf8, or binary)
    pub file_content_type: Option<FileContentType>,

    /// File generated status (fully, partially, or not generated)
    pub file_generated_status: Option<FileGeneratedStatus>,
}

impl MetadataDiffFileInfo {
    fn new(file_type: Option<FileType>, parsed_file_content: Option<&ParsedFileContent>) -> Self {
        let file_generated_status = match parsed_file_content {
            Some(ParsedFileContent::Text(text_file)) => Some((&text_file.generated_span).into()),
            _ => None,
        };

        MetadataDiffFileInfo {
            file_type,
            file_content_type: parsed_file_content.map(FileContentType::from),
            file_generated_status,
        }
    }
}

/// Lines count in a diff for the metadata diff.
#[derive(Default)]
pub struct MetadataDiffLinesCount {
    /// Number of added lines.
    pub added_lines_count: usize,

    /// Number of deleted lines.
    pub deleted_lines_count: usize,

    /// Number of significant (not generated) added lines.
    pub significant_added_lines_count: usize,

    /// Number of significant (not generated) deleted lines.
    pub significant_deleted_lines_count: usize,

    /// Line number of the first added line (1-based).
    pub first_added_line_number: Option<usize>,
}

impl MetadataDiffLinesCount {
    fn new(
        old_parsed_file_content: Option<&ParsedFileContent>,
        new_parsed_file_content: Option<&ParsedFileContent>,
    ) -> Option<Self> {
        match (old_parsed_file_content, new_parsed_file_content) {
            (
                Some(ParsedFileContent::Text(old_text_file)),
                Some(ParsedFileContent::Text(new_text_file)),
            ) => Some(Self::diff_files(old_text_file, new_text_file)),
            (Some(ParsedFileContent::Text(old_text_file)), _) => {
                Some(Self::file_deleted(old_text_file))
            }
            (_, Some(ParsedFileContent::Text(new_text_file))) => {
                Some(Self::file_created(new_text_file))
            }
            _ => None,
        }
    }

    fn diff_files(old_text_file: &TextFile, new_text_file: &TextFile) -> Self {
        xdiff::diff_hunks(
            old_text_file.file_content.clone(),
            new_text_file.file_content.clone(),
        )
        .into_iter()
        .fold(
            Default::default(),
            |mut acc: MetadataDiffLinesCount, hunk| {
                acc.add_to_added_lines_count(hunk.add.len());
                acc.add_to_deleted_lines_count(hunk.remove.len());
                acc.add_to_significant_added_lines_count(
                    new_text_file.significant_lines_count_in_a_range(&hunk.add),
                );
                acc.add_to_significant_deleted_lines_count(
                    old_text_file.significant_lines_count_in_a_range(&hunk.remove),
                );
                if !hunk.add.is_empty() {
                    acc.first_added_line_number
                        .get_or_insert(hunk.add.start.saturating_add(1)); // +1 because hunk boundaries are 0-based.
                }

                acc
            },
        )
    }

    fn file_created(new_text_file: &TextFile) -> Self {
        Self {
            added_lines_count: new_text_file.lines(),
            significant_added_lines_count: new_text_file.significant_lines_count(),
            first_added_line_number: Some(1),
            ..Default::default()
        }
    }

    fn file_deleted(old_text_file: &TextFile) -> Self {
        Self {
            deleted_lines_count: old_text_file.lines(),
            significant_deleted_lines_count: old_text_file.significant_lines_count(),
            ..Default::default()
        }
    }

    fn add_to_added_lines_count(&mut self, count: usize) {
        self.added_lines_count = self.added_lines_count.saturating_add(count);
    }

    fn add_to_deleted_lines_count(&mut self, count: usize) {
        self.deleted_lines_count = self.deleted_lines_count.saturating_add(count);
    }

    fn add_to_significant_added_lines_count(&mut self, count: usize) {
        self.significant_added_lines_count =
            self.significant_added_lines_count.saturating_add(count);
    }

    fn add_to_significant_deleted_lines_count(&mut self, count: usize) {
        self.significant_deleted_lines_count =
            self.significant_deleted_lines_count.saturating_add(count);
    }
}

pub enum FileContentType {
    Text,
    NonUtf8,
    Binary,
}

pub enum FileGeneratedStatus {
    /// File is fully generated.
    FullyGenerated,
    /// File is partially generated.
    PartiallyGenerated,
    /// File is not generated.
    NotGenerated,
}

enum ParsedFileContent {
    Text(TextFile),
    NonUtf8,
    Binary,
}

impl From<&ParsedFileContent> for FileContentType {
    fn from(parsed_file_content: &ParsedFileContent) -> Self {
        match parsed_file_content {
            ParsedFileContent::Text(_) => FileContentType::Text,
            ParsedFileContent::NonUtf8 => FileContentType::NonUtf8,
            ParsedFileContent::Binary => FileContentType::Binary,
        }
    }
}

impl From<&FileGeneratedSpan> for FileGeneratedStatus {
    fn from(file_generated_span: &FileGeneratedSpan) -> Self {
        match file_generated_span {
            FileGeneratedSpan::FullyGenerated => FileGeneratedStatus::FullyGenerated,
            FileGeneratedSpan::PartiallyGenerated(_) => FileGeneratedStatus::PartiallyGenerated,
            FileGeneratedSpan::NotGenerated => FileGeneratedStatus::NotGenerated,
        }
    }
}

impl ParsedFileContent {
    async fn new(file: FileContext) -> Result<Self, MononokeError> {
        let metadata = file.metadata().await?;
        let parsed_content = if metadata.is_binary {
            ParsedFileContent::Binary
        } else if metadata.is_utf8 {
            let file_content = file.content_concat().await?;
            ParsedFileContent::Text(TextFile::new(file_content, metadata)?)
        } else {
            ParsedFileContent::NonUtf8
        };
        Ok(parsed_content)
    }
}

struct TextFile {
    file_content: Bytes,
    metadata: ContentMetadataV2,
    generated_span: FileGeneratedSpan,
}

impl TextFile {
    fn new(file_content: Bytes, metadata: ContentMetadataV2) -> Result<Self, MononokeError> {
        Ok(TextFile {
            generated_span: FileGeneratedSpan::new(file_content.clone(), &metadata)?,
            file_content,
            metadata,
        })
    }

    /// This method replaces text.lines().count() and use the metadata to get
    /// that information. The behavior should be identical in most cases except
    /// when the text ends in a newline. The lines().count() method does not consider
    /// the last newline in that case but it would still be counted in metadata.newline_count.
    fn lines(&self) -> usize {
        if self.metadata.ends_in_newline {
            (self.metadata.newline_count - 1) as usize
        } else {
            self.metadata.newline_count as usize
        }
    }

    fn significant_lines_count(&self) -> usize {
        match &self.generated_span {
            FileGeneratedSpan::FullyGenerated => 0usize,
            FileGeneratedSpan::PartiallyGenerated(manual_sections) => manual_sections
                .iter()
                .fold(0usize, |acc, section| acc.saturating_add(section.len())),
            FileGeneratedSpan::NotGenerated => self.lines(),
        }
    }

    fn significant_lines_count_in_a_range(&self, range: &Range<usize>) -> usize {
        match &self.generated_span {
            FileGeneratedSpan::FullyGenerated => 0usize,
            FileGeneratedSpan::PartiallyGenerated(manual_sections) => {
                manual_sections.iter().fold(0usize, |acc, section| {
                    acc.saturating_add(
                        section
                            .end
                            .min(range.end)
                            .saturating_sub(section.start.max(range.start)),
                    )
                })
            }
            FileGeneratedSpan::NotGenerated => range.len(),
        }
    }
}

enum FileGeneratedSpan {
    FullyGenerated,
    PartiallyGenerated(Vec<Range<usize>>),
    NotGenerated,
}

impl FileGeneratedSpan {
    fn new(content: Bytes, metadata: &ContentMetadataV2) -> Result<Self, MononokeError> {
        if !metadata.is_generated && !metadata.is_partially_generated {
            return Ok(FileGeneratedSpan::NotGenerated);
        }
        let content = std::str::from_utf8(&content)
            .context("Failed to parse valid UTF8 bytes for determining generated status")?;
        let mut found_generated_annotation = false;
        let mut manual_sections_ranges = Vec::new();
        let mut manual_section_start = None;

        for (line_number, line) in content.lines().enumerate() {
            if line.contains(concat!("@", "generated"))
                || line.contains(concat!("@", "partially-generated"))
            // The redundant concat is used to avoid marking this file as generated.
            {
                found_generated_annotation = true;
            }

            if END_MANUAL_SECTION_REGEX.is_match(line) {
                if let Some(manual_section_start) = manual_section_start {
                    manual_sections_ranges.push(manual_section_start..line_number);
                }
                manual_section_start = None;
            }

            if BEGIN_MANUAL_SECTION_REGEX.is_match(line) {
                manual_section_start = Some(line_number + 1);
            }
        }

        Ok(
            match (
                found_generated_annotation,
                manual_sections_ranges.is_empty(),
            ) {
                (true, true) => FileGeneratedSpan::FullyGenerated,
                (true, false) => FileGeneratedSpan::PartiallyGenerated(manual_sections_ranges),
                (false, _) => FileGeneratedSpan::NotGenerated,
            },
        )
    }
}

impl ChangesetPathDiffContext {
    /// Create a new path diff context that compares the contents of two
    /// changeset paths.
    ///
    /// Copy information must be provided if the file has been copied or
    /// moved.
    pub fn new(
        base: Option<ChangesetPathContentContext>,
        other: Option<ChangesetPathContentContext>,
        copy_info: CopyInfo,
    ) -> Result<Self, MononokeError> {
        match (base, other, copy_info) {
            (Some(base), None, CopyInfo::None) => Ok(Self::Added(base)),
            (None, Some(other), CopyInfo::None) => Ok(Self::Removed(other)),
            (Some(base), Some(other), CopyInfo::None) => Ok(Self::Changed(base, other)),
            (Some(base), Some(other), CopyInfo::Copy) => Ok(Self::Copied(base, other)),
            (Some(base), Some(other), CopyInfo::Move) => Ok(Self::Moved(base, other)),
            invalid_args => Err(anyhow!(
                "Invalid changeset path diff context parameters: {:?}",
                invalid_args
            )
            .into()),
        }
    }

    /// Return the base path that is being compared.  This is the
    /// contents after modification.
    pub fn base(&self) -> Option<&ChangesetPathContentContext> {
        match self {
            Self::Added(base)
            | Self::Changed(base, _)
            | Self::Copied(base, _)
            | Self::Moved(base, _) => Some(base),
            Self::Removed(_) => None,
        }
    }

    /// Return the other path that is being compared against.  This
    /// is the contents before modification.
    pub fn other(&self) -> Option<&ChangesetPathContentContext> {
        match self {
            Self::Removed(other)
            | Self::Changed(_, other)
            | Self::Copied(_, other)
            | Self::Moved(_, other) => Some(other),
            Self::Added(_) => None,
        }
    }

    /// Return the main path for this difference.  This is the added or
    /// removed path, or the base (destination) in the case of modifications,
    /// copies, or moves.
    pub fn path(&self) -> &ChangesetPathContentContext {
        match self {
            Self::Added(base)
            | Self::Changed(base, _)
            | Self::Copied(base, _)
            | Self::Moved(base, _) => base,
            Self::Removed(other) => other,
        }
    }

    /// Return the copy information for this difference.
    pub fn copy_info(&self) -> CopyInfo {
        match self {
            Self::Added(_) | Self::Removed(_) | Self::Changed(_, _) => CopyInfo::None,
            Self::Copied(_, _) => CopyInfo::Copy,
            Self::Moved(_, _) => CopyInfo::Move,
        }
    }

    // Helper for getting file information.
    async fn get_file_data(
        path: Option<&ChangesetPathContentContext>,
        mode: UnifiedDiffMode,
    ) -> Result<Option<xdiff::DiffFile<String, Bytes>>, MononokeError> {
        match path {
            Some(path) => {
                if let Some(file_type) = path.file_type().await? {
                    let file = path.file().await?.ok_or_else(|| {
                        MononokeError::from(Error::msg("assertion error: file should exist"))
                    })?;
                    let file_type = match file_type {
                        FileType::Regular => xdiff::FileType::Regular,
                        FileType::Executable => xdiff::FileType::Executable,
                        FileType::Symlink => xdiff::FileType::Symlink,
                    };
                    let contents = match mode {
                        UnifiedDiffMode::Inline => {
                            let contents = file.content_concat().await?;
                            xdiff::FileContent::Inline(contents)
                        }
                        UnifiedDiffMode::OmitContent => {
                            let content_id = file.metadata().await?.content_id;
                            xdiff::FileContent::Omitted {
                                content_hash: format!("{}", content_id),
                            }
                        }
                    };
                    Ok(Some(xdiff::DiffFile {
                        path: path.path().to_string(),
                        contents,
                        file_type,
                    }))
                } else {
                    Ok(None)
                }
            }
            None => Ok(None),
        }
    }

    /// Renders the diff (in the git diff format).
    ///
    /// If `mode` is `Placeholder` then `unified_diff(...)` doesn't fetch content,
    /// but just generates a placeholder diff that says that the files differ.
    pub async fn unified_diff(
        &self,
        context_lines: usize,
        mode: UnifiedDiffMode,
    ) -> Result<UnifiedDiff, MononokeError> {
        let (base_file, other_file) = try_join!(
            Self::get_file_data(self.base(), mode),
            Self::get_file_data(self.other(), mode)
        )?;
        let is_binary = xdiff::file_is_binary(&base_file) || xdiff::file_is_binary(&other_file);
        let copy_info = self.copy_info();
        let opts = xdiff::DiffOpts {
            context: context_lines,
            copy_info,
        };
        // The base is the target, so we diff in the opposite direction.
        let raw_diff = xdiff::diff_unified(other_file, base_file, opts);
        Ok(UnifiedDiff {
            raw_diff,
            is_binary,
        })
    }

    pub async fn metadata_diff(&self) -> Result<MetadataDiff, MononokeError> {
        let (new_file_type, mut new_file) = match self.base() {
            Some(path) => try_join!(path.file_type(), path.file())?,
            None => (None, None),
        };
        let new_parsed_file_content = match new_file.take() {
            Some(file) => Some(ParsedFileContent::new(file).await?),
            _ => None,
        };

        let (old_file_type, mut old_file) = match self.other() {
            Some(path) => try_join!(path.file_type(), path.file())?,
            None => (None, None),
        };
        let old_parsed_file_content = match old_file.take() {
            Some(file) => Some(ParsedFileContent::new(file).await?),
            _ => None,
        };

        Ok(MetadataDiff {
            old_file_info: MetadataDiffFileInfo::new(
                old_file_type,
                old_parsed_file_content.as_ref(),
            ),
            new_file_info: MetadataDiffFileInfo::new(
                new_file_type,
                new_parsed_file_content.as_ref(),
            ),
            lines_count: MetadataDiffLinesCount::new(
                old_parsed_file_content.as_ref(),
                new_parsed_file_content.as_ref(),
            ),
        })
    }
}


/*
 * 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::any::Demand;
use std::backtrace::Backtrace;
use std::convert::Infallible;
use std::error::Error as StdError;
use std::fmt;
use std::sync::Arc;

use anyhow::Error;
use blame::BlameError;
use blobstore::LoadableError;
use bookmarks_movement::describe_hook_rejections;
use bookmarks_movement::BookmarkMovementError;
use bookmarks_movement::HookRejection;
use derived_data::DeriveError;
use itertools::Itertools;
use megarepo_error::MegarepoError;
use pushrebase::PushrebaseError;
use repo_authorization::AuthorizationError;
use thiserror::Error;

use crate::path::MononokePath;

#[derive(Clone, Debug)]
pub struct InternalError(Arc<Error>);

impl InternalError {
    pub fn backtrace(&self) -> &Backtrace {
        self.0.backtrace()
    }
}

impl fmt::Display for InternalError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.0.fmt(f)
    }
}

impl From<Error> for InternalError {
    fn from(error: Error) -> Self {
        Self(Arc::new(error))
    }
}

impl StdError for InternalError {
    fn source(&self) -> Option<&(dyn StdError + 'static)> {
        Some(&**self.0)
    }

    fn provide<'a>(&'a self, demand: &mut Demand<'a>) {
        demand.provide_ref::<Backtrace>(self.backtrace());
    }
}

#[derive(Clone, Debug, Error)]
pub enum MononokeError {
    #[error("invalid request: {0}")]
    InvalidRequest(String),
    #[error("unresolved path conflicts in merge:\n {}", .conflict_paths.iter().join("\n"))]
    MergeConflicts { conflict_paths: Vec<MononokePath> },
    #[error("Conflicts while pushrebasing: {0:?}")]
    PushrebaseConflicts(Vec<pushrebase::PushrebaseConflict>),
    #[error(
        "permission denied: access to repo {reponame} on behalf of {service_identity} not permitted for {identities}"
    )]
    ServicePermissionDenied {
        identities: String,
        reponame: String,
        service_identity: String,
    },
    #[error("hooks failed:\n{}", describe_hook_rejections(.0.as_slice()))]
    HookFailure(Vec<HookRejection>),
    #[error("not available: {0}")]
    NotAvailable(String),
    #[error("permission denied: {0}")]
    AuthorizationError(String),
    #[error("internal error: {0}")]
    InternalError(#[source] InternalError),
}

impl From<Error> for MononokeError {
    fn from(e: Error) -> Self {
        MononokeError::InternalError(InternalError(Arc::new(e)))
    }
}

impl From<Infallible> for MononokeError {
    fn from(_i: Infallible) -> Self {
        unreachable!()
    }
}

impl From<LoadableError> for MononokeError {
    fn from(e: LoadableError) -> Self {
        MononokeError::InternalError(InternalError(Arc::new(e.into())))
    }
}

impl From<DeriveError> for MononokeError {
    fn from(e: DeriveError) -> Self {
        match e {
            e @ DeriveError::Disabled(..) => MononokeError::NotAvailable(e.to_string()),
            DeriveError::Error(e) => MononokeError::from(e),
        }
    }
}

impl From<BookmarkMovementError> for MononokeError {
    fn from(e: BookmarkMovementError) -> Self {
        match e {
            BookmarkMovementError::AuthorizationError(e) => {
                MononokeError::AuthorizationError(e.to_string())
            }
            BookmarkMovementError::HookFailure(rejections) => {
                MononokeError::HookFailure(rejections)
            }
            BookmarkMovementError::PushrebaseError(PushrebaseError::Conflicts(conflicts)) => {
                MononokeError::PushrebaseConflicts(conflicts)
            }
            BookmarkMovementError::Error(e) => MononokeError::InternalError(InternalError::from(e)),
            _ => MononokeError::InvalidRequest(e.to_string()),
        }
    }
}

impl From<AuthorizationError> for MononokeError {
    fn from(e: AuthorizationError) -> Self {
        match e {
            AuthorizationError::PermissionDenied(e) => {
                MononokeError::AuthorizationError(e.to_string())
            }
            AuthorizationError::Error(e) => MononokeError::InternalError(InternalError::from(e)),
        }
    }
}

impl From<BlameError> for MononokeError {
    fn from(e: BlameError) -> Self {
        use BlameError::*;
        match e {
            NoSuchPath(_) | IsDirectory(_) | Rejected(_) => {
                MononokeError::InvalidRequest(e.to_string())
            }
            DeriveError(e) => MononokeError::from(e),
            _ => MononokeError::from(anyhow::Error::from(e)),
        }
    }
}

impl From<MononokeError> for edenapi_types::ServerError {
    fn from(e: MononokeError) -> Self {
        edenapi_types::ServerError::from(&e)
    }
}

impl From<&MononokeError> for edenapi_types::ServerError {
    fn from(e: &MononokeError) -> Self {
        let message = format!("{:?}", e);
        let code = match e {
            MononokeError::InternalError(e)
                if e.0.is::<segmented_changelog::MismatchedHeadsError>() =>
            {
                1
            }
            _ => 0,
        };
        Self::new(message, code)
    }
}

impl From<MononokeError> for MegarepoError {
    fn from(e: MononokeError) -> Self {
        MegarepoError::internal(e)
    }
}


/*
 * 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 anyhow::format_err;
use bytes::Bytes;
use bytes::BytesMut;
use cloned::cloned;
use context::CoreContext;
use filestore::get_metadata;
use filestore::FetchKey;
use futures::stream::TryStreamExt;
use futures::try_join;
use futures_lazy_shared::LazyShared;
/// A file's ID is its content id.
pub use mononoke_types::ContentId as FileId;
/// Metadata about a file.
pub use mononoke_types::ContentMetadataV2 as FileMetadata;
/// The type of a file.
pub use mononoke_types::FileType;
use repo_blobstore::RepoBlobstoreRef;

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

#[derive(Clone)]
pub struct FileContext {
    repo: RepoContext,
    fetch_key: FetchKey,
    metadata: LazyShared<Result<FileMetadata, MononokeError>>,
}

impl fmt::Debug for FileContext {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(
            f,
            "FileContext(repo={:?} fetch_key={:?})",
            self.repo().name(),
            self.fetch_key
        )
    }
}

/// Context for accessing a file in a repository.
///
/// Files are content-addressed, so if the same file occurs in multiple
/// places in the repository, this context represents all of them. As such,
/// it's not possible to go back to the commit or path from a `FileContext`.
///
/// See `ChangesetPathContentContext` if you need to refer to a specific file in a
/// specific commit.
impl FileContext {
    /// Create a new FileContext.  The file must exist in the repository,
    /// and the user must be known to have permission to access the path
    /// the file exists at.
    ///
    /// To construct a `FileContext` for a file that might not exist, use
    /// `new_check_exists`.
    pub(crate) fn new_authorized(repo: RepoContext, fetch_key: FetchKey) -> Self {
        Self {
            repo,
            fetch_key,
            metadata: LazyShared::new_empty(),
        }
    }

    /// Create a new  FileContext using an ID that might not exist. Returns
    /// `None` if the file doesn't exist.
    pub(crate) async fn new_check_exists(
        repo: RepoContext,
        fetch_key: FetchKey,
    ) -> Result<Option<Self>, MononokeError> {
        // Access to an arbitrary file requires full access to the repo,
        // as we do not know which path it corresponds to.
        repo.authorization_context()
            .require_full_repo_read(repo.ctx(), repo.inner_repo())
            .await?;
        // Try to get the file metadata immediately to see if it exists.
        let file = get_metadata(repo.blob_repo().repo_blobstore(), repo.ctx(), &fetch_key)
            .await?
            .map(|metadata| Self {
                repo,
                fetch_key,
                metadata: LazyShared::new_ready(Ok(metadata)),
            });
        Ok(file)
    }

    /// The context for this query.
    pub(crate) fn ctx(&self) -> &CoreContext {
        self.repo.ctx()
    }

    /// The `RepoContext` for this query.
    pub(crate) fn repo(&self) -> &RepoContext {
        &self.repo
    }

    /// Return the ID of the file.
    pub async fn id(&self) -> Result<FileId, MononokeError> {
        let meta = self.metadata().await?;
        Ok(meta.content_id)
    }

    /// Return the metadata for a file.
    pub async fn metadata(&self) -> Result<FileMetadata, MononokeError> {
        self.metadata
            .get_or_init(|| {
                cloned!(self.repo, self.fetch_key);
                async move {
                    get_metadata(repo.blob_repo().repo_blobstore(), repo.ctx(), &fetch_key)
                        .await
                        .map_err(MononokeError::from)
                        .and_then(|metadata| {
                            metadata.ok_or_else(|| content_not_found_error(&fetch_key))
                        })
                }
            })
            .await
    }

    /// Return the content for the file.
    ///
    /// This method buffers the full file content in memory, which may
    /// be expensive in the case of large files.
    pub async fn content_concat(&self) -> Result<Bytes, MononokeError> {
        let bytes = filestore::fetch_concat_opt(
            self.repo().blob_repo().repo_blobstore(),
            self.ctx(),
            &self.fetch_key,
        )
        .await;

        match bytes {
            Ok(Some(bytes)) => Ok(bytes),
            Ok(None) => Err(content_not_found_error(&self.fetch_key)),
            Err(e) => Err(MononokeError::from(e)),
        }
    }

    /// Return the content for a range within the file.
    ///
    /// If the range goes past the end of the file, then content up to
    /// the end of the file is returned.  If the range starts past the
    /// end of the file, then an empty buffer is returned.
    pub async fn content_range_concat(
        &self,
        start: u64,
        size: u64,
    ) -> Result<Bytes, MononokeError> {
        let ret = filestore::fetch_range_with_size(
            self.repo().blob_repo().repo_blobstore(),
            self.ctx(),
            &self.fetch_key,
            filestore::Range::sized(start, size),
        )
        .await;

        match ret {
            Ok(Some((stream, size))) => {
                let size = size.try_into().map_err(|_| {
                    MononokeError::from(format_err!("content too large: {:?}", self.fetch_key))
                })?;

                let bytes = stream
                    .map_err(MononokeError::from)
                    .try_fold(
                        BytesMut::with_capacity(size),
                        |mut buff, chunk| async move {
                            buff.extend_from_slice(&chunk);
                            Ok(buff)
                        },
                    )
                    .await?
                    .freeze();

                Ok(bytes)
            }
            Ok(None) => Err(content_not_found_error(&self.fetch_key)),
            Err(e) => Err(MononokeError::from(e)),
        }
    }
}

/// A diff between two files in headerless unified diff format
pub struct HeaderlessUnifiedDiff {
    /// Raw diff as bytes.
    pub raw_diff: Vec<u8>,
    /// One of the diffed files is binary, raw diff contains just a placeholder.
    pub is_binary: bool,
}

pub async fn headerless_unified_diff(
    old_file: &FileContext,
    new_file: &FileContext,
    context_lines: usize,
) -> Result<HeaderlessUnifiedDiff, MononokeError> {
    let (old_diff_file, new_diff_file) =
        try_join!(old_file.content_concat(), new_file.content_concat(),)?;
    let is_binary = old_diff_file.contains(&0) || new_diff_file.contains(&0);
    let raw_diff = if is_binary {
        b"Binary files differ".to_vec()
    } else {
        let opts = xdiff::HeaderlessDiffOpts {
            context: context_lines,
        };
        xdiff::diff_unified_headerless(&old_diff_file, &new_diff_file, opts)
    };
    Ok(HeaderlessUnifiedDiff {
        raw_diff,
        is_binary,
    })
}

/// File contexts should only exist for files that are known to be in the
/// blobstore. If attempting to access the content results in an error, this
/// error is returned. This is an internal error, as it means either the data
/// has been lost from the blobstore, or the file context was erroneously
/// constructed.
fn content_not_found_error(fetch_key: &FetchKey) -> MononokeError {
    MononokeError::from(format_err!("content not found: {:?}", fetch_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.
 */

#![feature(error_generic_member_access)]
#![feature(provide_any)]

use std::sync::Arc;

use anyhow::Error;
pub use bookmarks::BookmarkCategory;
pub use bookmarks::BookmarkKey;
use mononoke_repos::MononokeRepos;
use mononoke_types::RepositoryId;

use crate::repo::RepoContextBuilder;

pub mod changeset;
pub mod changeset_path;
pub mod changeset_path_diff;
pub mod errors;
pub mod file;
pub mod path;
pub mod repo;
pub mod sparse_profile;
pub mod specifiers;
pub mod tree;
mod xrepo;

#[cfg(test)]
mod test;

// Re-export types that are useful for clients.
pub use context::CoreContext;
pub use context::LoggingContainer;
pub use context::SessionContainer;

pub use crate::changeset::ChangesetContext;
pub use crate::changeset::ChangesetDiffItem;
pub use crate::changeset::ChangesetFileOrdering;
pub use crate::changeset::ChangesetHistoryOptions;
pub use crate::changeset::Generation;
pub use crate::changeset_path::ChangesetPathContentContext;
pub use crate::changeset_path::ChangesetPathHistoryOptions;
pub use crate::changeset_path::PathEntry;
pub use crate::changeset_path_diff::ChangesetPathDiffContext;
pub use crate::changeset_path_diff::CopyInfo;
pub use crate::changeset_path_diff::FileContentType;
pub use crate::changeset_path_diff::FileGeneratedStatus;
pub use crate::changeset_path_diff::MetadataDiff;
pub use crate::changeset_path_diff::MetadataDiffFileInfo;
pub use crate::changeset_path_diff::MetadataDiffLinesCount;
pub use crate::changeset_path_diff::UnifiedDiff;
pub use crate::changeset_path_diff::UnifiedDiffMode;
pub use crate::errors::MononokeError;
pub use crate::file::headerless_unified_diff;
pub use crate::file::FileContext;
pub use crate::file::FileId;
pub use crate::file::FileMetadata;
pub use crate::file::FileType;
pub use crate::file::HeaderlessUnifiedDiff;
pub use crate::path::MononokePath;
pub use crate::repo::create_changeset::CreateChange;
pub use crate::repo::create_changeset::CreateChangeFile;
pub use crate::repo::create_changeset::CreateCopyInfo;
pub use crate::repo::create_changeset::CreateInfo;
pub use crate::repo::land_stack::PushrebaseOutcome;
pub use crate::repo::BookmarkFreshness;
pub use crate::repo::BookmarkInfo;
pub use crate::repo::Repo;
pub use crate::repo::RepoContext;
pub use crate::repo::StoreRequest;
pub use crate::specifiers::ChangesetId;
pub use crate::specifiers::ChangesetIdPrefix;
pub use crate::specifiers::ChangesetPrefixSpecifier;
pub use crate::specifiers::ChangesetSpecifier;
pub use crate::specifiers::ChangesetSpecifierPrefixResolution;
pub use crate::specifiers::Globalrev;
pub use crate::specifiers::HgChangesetId;
pub use crate::specifiers::HgChangesetIdPrefix;
pub use crate::tree::TreeContext;
pub use crate::tree::TreeEntry;
pub use crate::tree::TreeId;
pub use crate::tree::TreeSummary;
pub use crate::xrepo::CandidateSelectionHintArgs;

/// An instance of Mononoke, which may manage multiple repositories.
pub struct Mononoke {
    // Collection of instantiated repos currently being served.
    pub repos: Arc<MononokeRepos<Repo>>,
    // The collective list of all enabled repos that exist
    // in the current tier (e.g. prod, backup, etc.)
    pub repo_names_in_tier: Vec<String>,
}

impl Mononoke {
    /// Create a MononokeAPI instance for MononokeRepos
    ///
    /// Takes extra argument containing list of all aviailable repos
    /// (used to power APIs listing repos; TODO: change that arg to MonononokeConfigs)
    pub fn new(
        repos: Arc<MononokeRepos<Repo>>,
        repo_names_in_tier: Vec<String>,
    ) -> Result<Self, Error> {
        Ok(Self {
            repos,
            repo_names_in_tier,
        })
    }

    /// Start a request on a repository by name.
    // Method is async and fallible as in the future this may involve
    // instantiating the repo lazily.
    pub async fn repo(
        &self,
        ctx: CoreContext,
        name: impl AsRef<str>,
    ) -> Result<Option<RepoContextBuilder>, MononokeError> {
        match self.repos.get_by_name(name.as_ref()) {
            None => Ok(None),
            Some(repo) => Ok(Some(
                RepoContextBuilder::new(ctx, repo, self.repos.as_ref()).await?,
            )),
        }
    }

    /// Start a request on a repository by id.
    // Method is async and fallible as in the future this may involve
    // instantiating the repo lazily.
    pub async fn repo_by_id(
        &self,
        ctx: CoreContext,
        repo_id: RepositoryId,
    ) -> Result<Option<RepoContextBuilder>, MononokeError> {
        match self.repos.get_by_id(repo_id.id()) {
            None => Ok(None),
            Some(repo) => Ok(Some(
                RepoContextBuilder::new(ctx, repo, self.repos.as_ref()).await?,
            )),
        }
    }

    /// Return the raw underlying repo corresponding to the provided
    /// repo name.
    pub fn raw_repo(&self, name: impl AsRef<str>) -> Option<Arc<Repo>> {
        self.repos.get_by_name(name.as_ref())
    }

    /// Return the raw underlying repo corresponding to the provided
    /// repo id.
    pub fn raw_repo_by_id(&self, id: i32) -> Option<Arc<Repo>> {
        self.repos.get_by_id(id)
    }

    /// Get all known repository ids
    pub fn known_repo_ids(&self) -> Vec<RepositoryId> {
        self.repos.iter_ids().map(RepositoryId::new).collect()
    }

    /// Returns an `Iterator` over all repo names.
    pub fn repo_names(&self) -> impl Iterator<Item = String> {
        self.repos.iter_names()
    }

    pub fn repos(&self) -> impl Iterator<Item = Arc<Repo>> {
        self.repos.iter()
    }

    pub fn repo_name_from_id(&self, repo_id: RepositoryId) -> Option<String> {
        self.repos
            .get_by_id(repo_id.id())
            .map(|repo| repo.name().to_string())
    }

    pub fn repo_id_from_name(&self, name: impl AsRef<str>) -> Option<RepositoryId> {
        self.repos
            .get_by_name(name.as_ref())
            .map(|repo| repo.repoid())
    }

    /// Report configured monitoring stats
    pub async fn report_monitoring_stats(&self, ctx: &CoreContext) -> Result<(), MononokeError> {
        for repo in self.repos.iter() {
            repo.report_monitoring_stats(ctx).await?;
        }

        Ok(())
    }
}

pub mod test_impl {
    use blobrepo::BlobRepo;
    use cloned::cloned;
    use live_commit_sync_config::LiveCommitSyncConfig;
    use metaconfig_types::CommitSyncConfig;
    use repo_identity::RepoIdentityRef;
    use synced_commit_mapping::ArcSyncedCommitMapping;

    use super::*;

    impl Mononoke {
        /// Create a Mononoke instance for testing.
        pub async fn new_test(
            repos: impl IntoIterator<Item = (String, Repo)>,
        ) -> Result<Self, Error> {
            let repos = repos
                .into_iter()
                .map(|(name, repo)| (repo.blob_repo().repo_identity().id().id(), name, repo))
                .collect::<Vec<_>>();
            let repo_names_in_tier = repos
                .iter()
                .map(|(_, name, _)| name.to_string())
                .collect::<Vec<_>>();
            let mononoke_repos = MononokeRepos::new();
            mononoke_repos.populate(repos);
            Ok(Self {
                repos: Arc::new(mononoke_repos),
                repo_names_in_tier,
            })
        }

        pub async fn new_test_xrepo(
            ctx: CoreContext,
            small_repo: (String, BlobRepo),
            large_repo: (String, BlobRepo),
            _commit_sync_config: CommitSyncConfig,
            mapping: ArcSyncedCommitMapping,
            lv_cfg: Arc<dyn LiveCommitSyncConfig>,
        ) -> Result<Self, Error> {
            use futures::stream::FuturesOrdered;
            use futures::stream::TryStreamExt;

            let repos = vec![small_repo, large_repo]
                .into_iter()
                .map({
                    move |(name, repo)| {
                        cloned!(ctx, lv_cfg, mapping);
                        async move {
                            Repo::new_test_xrepo(ctx.clone(), repo, lv_cfg, mapping)
                                .await
                                .map(move |repo| {
                                    (repo.blob_repo().repo_identity().id().id(), name, repo)
                                })
                        }
                    }
                })
                .collect::<FuturesOrdered<_>>()
                .try_collect::<Vec<_>>()
                .await?;

            let repo_names_in_tier =
                Vec::from_iter(repos.iter().map(|(_, name, _)| name.to_string()));
            let mononoke_repos = MononokeRepos::new();
            mononoke_repos.populate(repos);
            Ok(Self {
                repos: Arc::new(mononoke_repos),
                repo_names_in_tier,
            })
        }
    }
}


/*
 * 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 mononoke_types::MPath;
use mononoke_types::MPathElement;

use crate::errors::MononokeError;

// Define a wrapper around `Option<MPath>` to make it more convenient to
// use in the API.
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct MononokePath(Option<MPath>);

/// Whether this path is a path prefix of the given path.
/// `foo` is a prefix of `foo/bar`, but not of `foo1`.
pub fn is_prefix_of(lhs: Option<&MPath>, rhs: Option<&MPath>) -> bool {
    match (lhs, rhs) {
        (None, _) => true,
        (_, None) => false,
        (Some(lhs_mpath), Some(rhs_mpath)) => lhs_mpath.is_prefix_of(rhs_mpath),
    }
}

pub fn is_related_to(lhs: Option<&MPath>, rhs: Option<&MPath>) -> bool {
    is_prefix_of(lhs, rhs) || is_prefix_of(rhs, lhs)
}

impl MononokePath {
    pub fn new(path: Option<MPath>) -> Self {
        Self(path)
    }

    pub fn as_mpath(&self) -> Option<&MPath> {
        self.0.as_ref()
    }

    pub fn into_mpath(self) -> Option<MPath> {
        self.0
    }

    pub fn prefixes(&self) -> MononokePathPrefixes {
        MononokePathPrefixes::new(self)
    }

    /// Whether self is prefix of other or the other way arround.
    pub fn is_related_to(&self, other: &Self) -> bool {
        is_related_to(self.as_mpath(), other.as_mpath())
    }

    pub fn append(&self, element: &MPathElement) -> Self {
        Self(Some(MPath::join_opt_element(self.0.as_ref(), element)))
    }
}

// Because of conflicting generic traits, we cannot implement this
// generically for `AsRef<str>`.  Instead, implement the most common
// variants.
impl TryFrom<&str> for MononokePath {
    type Error = MononokeError;

    fn try_from(path: &str) -> Result<MononokePath, MononokeError> {
        if path.is_empty() {
            Ok(MononokePath(None))
        } else {
            let mpath = MPath::try_from(path)
                .map_err(|error| MononokeError::InvalidRequest(error.to_string()))?;
            Ok(MononokePath(Some(mpath)))
        }
    }
}

impl TryFrom<&String> for MononokePath {
    type Error = MononokeError;

    fn try_from(path: &String) -> Result<MononokePath, MononokeError> {
        MononokePath::try_from(path.as_str())
    }
}

impl From<MononokePath> for Option<MPath> {
    fn from(path: MononokePath) -> Option<MPath> {
        path.0
    }
}

impl From<MPath> for MononokePath {
    fn from(mpath: MPath) -> MononokePath {
        MononokePath(Some(mpath))
    }
}

impl From<Option<MPath>> for MononokePath {
    fn from(mpath: Option<MPath>) -> MononokePath {
        MononokePath(mpath)
    }
}

impl fmt::Display for MononokePath {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        if let Some(ref mpath) = self.0 {
            return write!(f, "{}", mpath);
        }
        write!(f, "")
    }
}

impl fmt::Debug for MononokePath {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{:?}", self.0)
    }
}

/// An iterator over all the prefixes of a path.
pub struct MononokePathPrefixes {
    next_path: Option<MononokePath>,
}

impl MononokePathPrefixes {
    fn new(path: &MononokePath) -> Self {
        let next_path = path
            .as_mpath()
            .map(|mpath| MononokePath::new(mpath.split_dirname().0));
        MononokePathPrefixes { next_path }
    }
}

impl Iterator for MononokePathPrefixes {
    type Item = MononokePath;

    fn next(&mut self) -> Option<MononokePath> {
        match self.next_path.take() {
            None => None,
            Some(path) => {
                self.next_path = path
                    .as_mpath()
                    .map(|mpath| MononokePath::new(mpath.split_dirname().0));
                Some(path)
            }
        }
    }
}

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

    #[test]
    fn path_relations() -> Result<(), MononokeError> {
        let x = MononokePath::try_from("a/b/c")?;
        let y = MononokePath::try_from("a/b")?;
        let z = MononokePath::try_from("a/d")?;
        assert!(is_prefix_of(y.as_mpath(), x.as_mpath()));
        assert!(!is_prefix_of(z.as_mpath(), x.as_mpath()));
        assert!(is_prefix_of(x.as_mpath(), x.as_mpath()));
        assert!(!is_prefix_of(x.as_mpath(), y.as_mpath()));
        assert!(is_related_to(x.as_mpath(), y.as_mpath()));
        assert!(!is_related_to(x.as_mpath(), z.as_mpath()));
        Ok(())
    }
}