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

use std::collections::BTreeMap;
use std::sync::Arc;

use anyhow::Result;
use bookmarks::BookmarkKey;
use bookmarks::BookmarkUpdateReason;
use bookmarks::BookmarksRef;
use context::CoreContext;
use fbinit::FacebookInit;
use futures::stream::TryStreamExt;
use mononoke_types::ChangesetId;
use tests_utils::drawdag::create_from_dag;

use crate::repo::BookmarkFreshness;
use crate::repo::Repo;
use crate::repo::RepoContext;

async fn init_repo(ctx: &CoreContext) -> Result<(RepoContext, BTreeMap<String, ChangesetId>)> {
    let repo: Repo = test_repo_factory::build_empty(ctx.fb).await?;
    let changesets = create_from_dag(
        ctx,
        &repo,
        r##"
            A-B-C-D-E
               \
                F-G
        "##,
    )
    .await?;
    let mut txn = repo.bookmarks().create_transaction(ctx.clone());
    txn.force_set(
        &BookmarkKey::new("trunk")?,
        changesets["E"],
        BookmarkUpdateReason::TestMove,
    )?;
    txn.create_scratch(&BookmarkKey::new("scratch/branch")?, changesets["G"])?;
    txn.create_scratch(&BookmarkKey::new("scratch/branchpoint")?, changesets["B"])?;
    txn.commit().await?;

    repo.warm_bookmarks_cache().sync(ctx).await;

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

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

    // Test that normal bookmarks are visible both in and through the cache.
    assert_eq!(
        repo.resolve_bookmark(&BookmarkKey::new("trunk")?, BookmarkFreshness::MostRecent)
            .await?
            .unwrap()
            .id(),
        changesets["E"],
    );

    assert_eq!(
        repo.resolve_bookmark(&BookmarkKey::new("trunk")?, BookmarkFreshness::MaybeStale)
            .await?
            .unwrap()
            .id(),
        changesets["E"],
    );

    // Test that scratch bookmarks are visible through the cache.
    assert_eq!(
        repo.resolve_bookmark(
            &BookmarkKey::new("scratch/branch")?,
            BookmarkFreshness::MaybeStale
        )
        .await?
        .unwrap()
        .id(),
        changesets["G"],
    );

    assert_eq!(
        repo.resolve_bookmark(
            &BookmarkKey::new("scratch/branchpoint")?,
            BookmarkFreshness::MaybeStale
        )
        .await?
        .unwrap()
        .id(),
        changesets["B"],
    );

    // Test that non-existent bookmarks don't exist either way.
    assert!(
        repo.resolve_bookmark(
            &BookmarkKey::new("scratch/nonexistent")?,
            BookmarkFreshness::MaybeStale
        )
        .await?
        .is_none()
    );

    assert!(
        repo.resolve_bookmark(
            &BookmarkKey::new("nonexistent")?,
            BookmarkFreshness::MostRecent
        )
        .await?
        .is_none()
    );

    Ok(())
}

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

    assert_eq!(
        repo.list_bookmarks(false, None, None, None)
            .await?
            .try_collect::<Vec<_>>()
            .await?,
        vec![(String::from("trunk"), changesets["E"])]
    );

    assert_eq!(
        repo.list_bookmarks(true, Some("scratch/"), None, Some(3))
            .await?
            .try_collect::<Vec<_>>()
            .await?,
        vec![
            (String::from("scratch/branch"), changesets["G"]),
            (String::from("scratch/branchpoint"), changesets["B"])
        ]
    );

    assert_eq!(
        repo.list_bookmarks(true, Some("scratch/"), Some("scratch/branch"), Some(3))
            .await?
            .try_collect::<Vec<_>>()
            .await?,
        vec![(String::from("scratch/branchpoint"), changesets["B"])]
    );
    Ok(())
}


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

use std::collections::BTreeMap;
use std::str::FromStr;

use anyhow::Error;
use assert_matches::assert_matches;
use bytes::Bytes;
use chrono::FixedOffset;
use chrono::TimeZone;
use derived_data_utils::derived_data_utils;
use fbinit::FacebookInit;
use fixtures::Linear;
use fixtures::ManyFilesDirs;
use fixtures::TestRepoFixture;
use futures::try_join;
use mononoke_types::hash::Sha256;
use repo_derived_data::RepoDerivedDataArc;
use smallvec::SmallVec;

use crate::ChangesetContext;
use crate::ChangesetId;
use crate::CoreContext;
use crate::CreateChange;
use crate::CreateChangeFile;
use crate::CreateInfo;
use crate::FileType;
use crate::Mononoke;
use crate::MononokeError;
use crate::MononokePath;
use crate::RepoContext;
use crate::StoreRequest;

#[fbinit::test]
async fn test_create_commit(fb: FacebookInit) -> Result<(), Error> {
    create_commit(fb, "skeleton_manifests").await?;

    Ok(())
}

// Check that commits were created correctly, and also check that only a single
// derived data type was derived (i.e. check that we don't derive something that we aren't supposed
// to).
async fn create_commit(fb: FacebookInit, derived_data_to_derive: &str) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let mononoke = Mononoke::new_test(vec![(
        "test".to_string(),
        Linear::get_custom_test_repo(fb).await,
    )])
    .await?;
    let repo = mononoke
        .repo(ctx.clone(), "test")
        .await?
        .expect("repo exists")
        .build()
        .await?;
    let expected_hash = "68c9120f387cf1c3b7e4c2e30cdbd5b953f27a732cfe9f42f335f0091ece3c6c";
    let parent_hash = "7785606eb1f26ff5722c831de402350cf97052dc44bc175da6ac0d715a3dbbf6";
    let parents = vec![ChangesetId::from_str(parent_hash)?];
    let author = String::from("Test Author <test@example.com>");
    let author_date = FixedOffset::east_opt(0)
        .unwrap()
        .with_ymd_and_hms(2000, 2, 1, 12, 0, 0)
        .unwrap();
    let committer = None;
    let committer_date = None;
    let message = String::from("Test Created Commit");
    let extra = BTreeMap::new();
    let bubble = None;
    let mut changes: BTreeMap<MononokePath, CreateChange> = BTreeMap::new();
    changes.insert(
        MononokePath::try_from("TEST_CREATE")?,
        CreateChange::Tracked(
            CreateChangeFile::New {
                bytes: Bytes::from("TEST CREATE\n"),
                file_type: FileType::Regular,
            },
            None,
        ),
    );

    // Pre-upload the file content for the second commit, and check its hash
    // on the way.
    let file_id = repo
        .upload_file_content(
            Bytes::from("TEST CREATE2\n"),
            &StoreRequest::with_sha256(
                13,
                Sha256::from_str(
                    "877f6bb6e0aeebc78c9b784ed633ef87019110bd61f867f0a4bf747085fec645",
                )?,
            ),
        )
        .await?;

    let cs = repo
        .create_changeset(
            parents,
            CreateInfo {
                author: author.clone(),
                author_date,
                committer: committer.clone(),
                committer_date,
                message: message.clone(),
                extra: extra.clone(),
                git_extra_headers: None,
            },
            changes.clone(),
            bubble,
        )
        .await?;

    changes.insert(
        MononokePath::try_from("TEST_CREATE")?,
        CreateChange::Tracked(
            CreateChangeFile::Existing {
                file_id,
                file_type: FileType::Regular,
                maybe_size: None,
            },
            None,
        ),
    );
    let second_cs = repo
        .create_changeset(
            vec![cs.id()],
            CreateInfo {
                author,
                author_date,
                committer,
                committer_date,
                message,
                extra,
                git_extra_headers: None,
            },
            changes,
            bubble,
        )
        .await?;

    validate_unnecessary_derived_data_is_not_derived(
        &ctx,
        &repo,
        cs.id(),
        second_cs.id(),
        derived_data_to_derive,
    )
    .await?;

    assert_eq!(cs.message().await?, "Test Created Commit");
    assert_eq!(cs.id(), ChangesetId::from_str(expected_hash)?);

    let content = cs
        .path_with_content("TEST_CREATE")
        .await?
        .file()
        .await?
        .expect("file should exist")
        .content_concat()
        .await?;
    assert_eq!(content, Bytes::from("TEST CREATE\n"));

    let content = second_cs
        .path_with_content("TEST_CREATE")
        .await?
        .file()
        .await?
        .expect("file should exist")
        .content_concat()
        .await?;
    assert_eq!(content, Bytes::from("TEST CREATE2\n"));

    Ok(())
}

// We expect that after creating a commit only derived a single specific derived data
// type is derived for a parent changeset, and none derived for the newly created changeset.
// This function validates it's actualy the case
async fn validate_unnecessary_derived_data_is_not_derived(
    ctx: &CoreContext,
    repo: &RepoContext,
    parent_cs_id: ChangesetId,
    cs_id: ChangesetId,
    derived_data_to_derive: &str,
) -> Result<(), Error> {
    for ty in &repo
        .blob_repo()
        .repo_derived_data_arc()
        .active_config()
        .types
    {
        if ty == "git_trees" {
            // Derived data utils doesn't support git_trees, so we have to skip it
            continue;
        }
        let utils = derived_data_utils(ctx.fb, repo.blob_repo(), ty)?;
        let not_derived = utils
            .pending(
                ctx.clone(),
                repo.blob_repo().repo_derived_data_arc(),
                vec![parent_cs_id, cs_id],
            )
            .await?;
        // It's expected to derive skeleton manifests for the parent commit
        if ty == derived_data_to_derive {
            assert_eq!(not_derived, vec![cs_id]);
        } else {
            assert_eq!(not_derived, vec![parent_cs_id, cs_id]);
        }
    }

    Ok(())
}

#[fbinit::test]
async fn create_commit_bad_changes(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let mononoke = Mononoke::new_test(vec![(
        "test".to_string(),
        ManyFilesDirs::get_custom_test_repo(fb).await,
    )])
    .await?;
    let repo = mononoke
        .repo(ctx, "test")
        .await?
        .expect("repo exists")
        .build()
        .await?;

    async fn create_changeset(
        repo: &RepoContext,
        changes: BTreeMap<MononokePath, CreateChange>,
    ) -> Result<ChangesetContext, MononokeError> {
        let parent_hash = "b0d1bf77898839595ee0f0cba673dd6e3be9dadaaa78bc6dd2dea97ca6bee77e";
        let parents = vec![ChangesetId::from_str(parent_hash)?];
        let author = String::from("Test Author <test@example.com>");
        let author_date = FixedOffset::east_opt(0)
            .unwrap()
            .with_ymd_and_hms(2000, 2, 1, 12, 0, 0)
            .unwrap();
        let committer = None;
        let committer_date = None;
        let message = String::from("Test Created Commit");
        let extra = BTreeMap::new();
        let bubble = None;
        let git_extra_headers =
            Some(maplit::btreemap! {SmallVec::new() => Bytes::from_static(b"world")});
        repo.create_changeset(
            parents,
            CreateInfo {
                author,
                author_date,
                committer,
                committer_date,
                message,
                extra,
                git_extra_headers,
            },
            changes,
            bubble,
        )
        .await
    }

    // Cannot delete a file that is not there
    let mut changes: BTreeMap<MononokePath, CreateChange> = BTreeMap::new();
    changes.insert(
        MononokePath::try_from("TEST_CREATE")?,
        CreateChange::Deletion,
    );
    assert_matches!(
        create_changeset(&repo, changes).await,
        Err(MononokeError::InvalidRequest(_))
    );

    // Cannot replace a file with a directory without deleting the file
    let mut changes: BTreeMap<MononokePath, CreateChange> = BTreeMap::new();
    changes.insert(
        MononokePath::try_from("1/TEST_CREATE")?,
        CreateChange::Tracked(
            CreateChangeFile::New {
                bytes: Bytes::from("test"),
                file_type: FileType::Regular,
            },
            None,
        ),
    );
    assert_matches!(
        create_changeset(&repo, changes.clone()).await,
        Err(MononokeError::InvalidRequest(_))
    );

    // Deleting the file means we can now replace it with a directory.
    changes.insert(MononokePath::try_from("1")?, CreateChange::Deletion);
    assert!(create_changeset(&repo, changes).await.is_ok());

    // Changes cannot introduce path conflicts
    let mut changes: BTreeMap<MononokePath, CreateChange> = BTreeMap::new();
    changes.insert(
        MononokePath::try_from("TEST_CREATE")?,
        CreateChange::Tracked(
            CreateChangeFile::New {
                bytes: Bytes::from("test"),
                file_type: FileType::Regular,
            },
            None,
        ),
    );
    changes.insert(
        MononokePath::try_from("TEST_CREATE/TEST_CREATE")?,
        CreateChange::Tracked(
            CreateChangeFile::New {
                bytes: Bytes::from("test"),
                file_type: FileType::Regular,
            },
            None,
        ),
    );
    assert_matches!(
        create_changeset(&repo, changes).await,
        Err(MononokeError::InvalidRequest(_))
    );

    // Superfluous changes when a directory is replaced by a file are dropped
    let mut changes: BTreeMap<MononokePath, CreateChange> = BTreeMap::new();
    changes.insert(
        MononokePath::try_from("dir1")?,
        CreateChange::Tracked(
            CreateChangeFile::New {
                bytes: Bytes::from("test"),
                file_type: FileType::Regular,
            },
            None,
        ),
    );
    let cs1 = create_changeset(&repo, changes.clone()).await?;

    changes.insert(
        MononokePath::try_from("dir1/file_1_in_dir1")?,
        CreateChange::Deletion,
    );
    changes.insert(
        MononokePath::try_from("dir1/subdir1/file_1")?,
        CreateChange::Deletion,
    );
    let cs2 = create_changeset(&repo, changes).await?;

    // Since the superfluous changes were dropped, the two commits
    // have the same bonsai hash.
    assert_eq!(cs1.id(), cs2.id());

    Ok(())
}

#[fbinit::test]
async fn test_create_merge_commit(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let mononoke = Mononoke::new_test(vec![(
        "test".to_string(),
        Linear::get_custom_test_repo(fb).await,
    )])
    .await?;
    let repo = mononoke
        .repo(ctx.clone(), "test")
        .await?
        .expect("repo exists")
        .build()
        .await?;

    async fn create_changeset(
        repo: &RepoContext,
        changes: BTreeMap<MononokePath, CreateChange>,
        parents: Vec<ChangesetId>,
    ) -> Result<ChangesetContext, MononokeError> {
        let author = String::from("Test Author <test@example.com>");
        let author_date = FixedOffset::east_opt(0)
            .unwrap()
            .with_ymd_and_hms(2000, 2, 1, 12, 0, 0)
            .unwrap();
        let committer = None;
        let committer_date = None;
        let message = String::from("Test Created Commit");
        let extra = BTreeMap::new();
        let bubble = None;
        let git_extra_headers = None;
        repo.create_changeset(
            parents,
            CreateInfo {
                author: author.clone(),
                author_date,
                committer: committer.clone(),
                committer_date,
                message: message.clone(),
                extra: extra.clone(),
                git_extra_headers,
            },
            changes.clone(),
            bubble,
        )
        .await
    }

    let initial_hash = "7785606eb1f26ff5722c831de402350cf97052dc44bc175da6ac0d715a3dbbf6";
    let initial_parents = vec![ChangesetId::from_str(initial_hash)?];
    let mut p1_changes: BTreeMap<MononokePath, CreateChange> = BTreeMap::new();
    p1_changes.insert(
        MononokePath::try_from("TEST_CREATE_p1")?,
        CreateChange::Tracked(
            CreateChangeFile::New {
                bytes: Bytes::from("TEST CREATE\n"),
                file_type: FileType::Regular,
            },
            None,
        ),
    );
    let mut p2_changes: BTreeMap<MononokePath, CreateChange> = BTreeMap::new();
    p2_changes.insert(
        MononokePath::try_from("TEST_CREATE_p2")?,
        CreateChange::Tracked(
            CreateChangeFile::New {
                bytes: Bytes::from("TEST CREATE\n"),
                file_type: FileType::Regular,
            },
            None,
        ),
    );

    let (p1, p2) = try_join!(
        create_changeset(&repo, p1_changes, initial_parents.clone(),),
        create_changeset(&repo, p2_changes, initial_parents)
    )?;

    let mut merge_changes: BTreeMap<MononokePath, CreateChange> = BTreeMap::new();
    merge_changes.insert(
        MononokePath::try_from("TEST_MERGE")?,
        CreateChange::Tracked(
            CreateChangeFile::New {
                bytes: Bytes::from("TEST CREATE\n"),
                file_type: FileType::Regular,
            },
            None,
        ),
    );
    create_changeset(&repo, merge_changes, vec![p1.id(), p2.id()]).await?;

    let mut merge_changes: BTreeMap<MononokePath, CreateChange> = BTreeMap::new();
    merge_changes.insert(
        MononokePath::try_from("TEST_CREATE_p1")?,
        CreateChange::Tracked(
            CreateChangeFile::New {
                bytes: Bytes::from("TEST MERGE OVERRIDE\n"),
                file_type: FileType::Regular,
            },
            None,
        ),
    );
    create_changeset(&repo, merge_changes, vec![p1.id(), p2.id()]).await?;

    Ok(())
}

#[fbinit::test]
async fn test_merge_commit_parent_file_conflict(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let mononoke = Mononoke::new_test(vec![(
        "test".to_string(),
        Linear::get_custom_test_repo(fb).await,
    )])
    .await?;
    let repo = mononoke
        .repo(ctx.clone(), "test")
        .await?
        .expect("repo exists")
        .build()
        .await?;

    async fn create_changeset(
        repo: &RepoContext,
        changes: BTreeMap<MononokePath, CreateChange>,
        parents: Vec<ChangesetId>,
    ) -> Result<ChangesetContext, MononokeError> {
        let author = String::from("Test Author <test@example.com>");
        let author_date = FixedOffset::east_opt(0)
            .unwrap()
            .with_ymd_and_hms(2000, 2, 1, 12, 0, 0)
            .unwrap();
        let committer = None;
        let committer_date = None;
        let message = String::from("Test Created Commit");
        let extra = BTreeMap::new();
        let bubble = None;
        let git_extra_headers =
            Some(maplit::btreemap! {SmallVec::new() => Bytes::from_static(b"world")});
        repo.create_changeset(
            parents,
            CreateInfo {
                author: author.clone(),
                author_date,
                committer: committer.clone(),
                committer_date,
                message: message.clone(),
                extra: extra.clone(),
                git_extra_headers,
            },
            changes.clone(),
            bubble,
        )
        .await
    }

    let initial_hash = "7785606eb1f26ff5722c831de402350cf97052dc44bc175da6ac0d715a3dbbf6";
    let initial_parents = vec![ChangesetId::from_str(initial_hash)?];
    let mut p1_changes: BTreeMap<MononokePath, CreateChange> = BTreeMap::new();
    p1_changes.insert(
        MononokePath::try_from("TEST_FILE")?,
        CreateChange::Tracked(
            CreateChangeFile::New {
                bytes: Bytes::from("TEST CREATE_p1\n"),
                file_type: FileType::Regular,
            },
            None,
        ),
    );
    let mut p2_changes: BTreeMap<MononokePath, CreateChange> = BTreeMap::new();
    p2_changes.insert(
        MononokePath::try_from("TEST_CREATE")?,
        CreateChange::Tracked(
            CreateChangeFile::New {
                bytes: Bytes::from("TEST CREATE_p2\n"),
                file_type: FileType::Regular,
            },
            None,
        ),
    );

    let mut p3_changes: BTreeMap<MononokePath, CreateChange> = BTreeMap::new();
    p3_changes.insert(
        MononokePath::try_from("TEST_FILE")?,
        CreateChange::Tracked(
            CreateChangeFile::New {
                bytes: Bytes::from("TEST CREATE_p3\n"),
                file_type: FileType::Regular,
            },
            None,
        ),
    );

    let (p1, p2, p3) = try_join!(
        create_changeset(&repo, p1_changes, initial_parents.clone()),
        create_changeset(&repo, p2_changes, initial_parents.clone()),
        create_changeset(&repo, p3_changes, initial_parents.clone())
    )?;

    // p1 and p2 do not conflict
    create_changeset(&repo, BTreeMap::new(), vec![p1.id(), p2.id()]).await?;

    // p1 and p3 do conflict
    assert!(
        create_changeset(&repo, BTreeMap::new(), vec![p1.id(), p3.id()])
            .await
            .is_err()
    );

    // Can't hide it by making p3 third parent, or by moving p1 to second parent and p3 as third
    assert!(
        create_changeset(&repo, BTreeMap::new(), vec![p1.id(), p2.id(), p3.id()])
            .await
            .is_err()
    );
    assert!(
        create_changeset(&repo, BTreeMap::new(), vec![p2.id(), p1.id(), p3.id()])
            .await
            .is_err()
    );

    Ok(())
}

#[fbinit::test]
async fn test_merge_commit_parent_tree_file_conflict(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let mononoke = Mononoke::new_test(vec![(
        "test".to_string(),
        Linear::get_custom_test_repo(fb).await,
    )])
    .await?;
    let repo = mononoke
        .repo(ctx.clone(), "test")
        .await?
        .expect("repo exists")
        .build()
        .await?;

    async fn create_changeset(
        repo: &RepoContext,
        changes: BTreeMap<MononokePath, CreateChange>,
        parents: Vec<ChangesetId>,
    ) -> Result<ChangesetContext, MononokeError> {
        let author = String::from("Test Author <test@example.com>");
        let author_date = FixedOffset::east_opt(0)
            .unwrap()
            .with_ymd_and_hms(2000, 2, 1, 12, 0, 0)
            .unwrap();
        let committer = None;
        let committer_date = None;
        let message = String::from("Test Created Commit");
        let extra = BTreeMap::new();
        let bubble = None;
        let git_extra_headers =
            Some(maplit::btreemap! {SmallVec::new() => Bytes::from_static(b"world")});
        repo.create_changeset(
            parents,
            CreateInfo {
                author: author.clone(),
                author_date,
                committer: committer.clone(),
                committer_date,
                message: message.clone(),
                extra: extra.clone(),
                git_extra_headers,
            },
            changes.clone(),
            bubble,
        )
        .await
    }

    let initial_hash = "7785606eb1f26ff5722c831de402350cf97052dc44bc175da6ac0d715a3dbbf6";
    let initial_parents = vec![ChangesetId::from_str(initial_hash)?];
    let mut p1_changes: BTreeMap<MononokePath, CreateChange> = BTreeMap::new();
    p1_changes.insert(
        MononokePath::try_from("TEST_FILE/REALLY_A_DIR")?,
        CreateChange::Tracked(
            CreateChangeFile::New {
                bytes: Bytes::from("TEST CREATE_p1\n"),
                file_type: FileType::Regular,
            },
            None,
        ),
    );
    let mut p2_changes: BTreeMap<MononokePath, CreateChange> = BTreeMap::new();
    p2_changes.insert(
        MononokePath::try_from("TEST_CREATE")?,
        CreateChange::Tracked(
            CreateChangeFile::New {
                bytes: Bytes::from("TEST CREATE_p2\n"),
                file_type: FileType::Regular,
            },
            None,
        ),
    );

    let mut p3_changes: BTreeMap<MononokePath, CreateChange> = BTreeMap::new();
    p3_changes.insert(
        MononokePath::try_from("TEST_FILE")?,
        CreateChange::Tracked(
            CreateChangeFile::New {
                bytes: Bytes::from("TEST CREATE_p3\n"),
                file_type: FileType::Regular,
            },
            None,
        ),
    );

    let (p1, p2, p3) = try_join!(
        create_changeset(&repo, p1_changes, initial_parents.clone()),
        create_changeset(&repo, p2_changes, initial_parents.clone()),
        create_changeset(&repo, p3_changes, initial_parents.clone())
    )?;

    // p1 and p2 do not conflict
    create_changeset(&repo, BTreeMap::new(), vec![p1.id(), p2.id()]).await?;

    // p1 and p3 do conflict
    assert!(
        create_changeset(&repo, BTreeMap::new(), vec![p1.id(), p3.id()])
            .await
            .is_err()
    );

    // Can't hide it by making p3 third parent, or by moving p1 to second parent and p3 as third
    assert!(
        create_changeset(&repo, BTreeMap::new(), vec![p1.id(), p2.id(), p3.id()])
            .await
            .is_err()
    );
    assert!(
        create_changeset(&repo, BTreeMap::new(), vec![p2.id(), p1.id(), p3.id()])
            .await
            .is_err()
    );

    Ok(())
}


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

use std::collections::BTreeMap;
use std::str::FromStr;

use anyhow::Context;
use anyhow::Error;
use bytes::Bytes;
use chrono::FixedOffset;
use chrono::TimeZone;
use fbinit::FacebookInit;
use fixtures::Linear;
use fixtures::TestRepoFixture;
use maplit::btreemap;

use crate::ChangesetContext;
use crate::ChangesetId;
use crate::CoreContext;
use crate::CreateChange;
use crate::CreateChangeFile;
use crate::CreateCopyInfo;
use crate::CreateInfo;
use crate::FileType;
use crate::Mononoke;
use crate::MononokeError;
use crate::MononokePath;
use crate::RepoContext;

async fn create_changeset_stack(
    repo: &RepoContext,
    changes_stack: Vec<BTreeMap<MononokePath, CreateChange>>,
    stack_parents: Vec<ChangesetId>,
) -> Result<Vec<ChangesetContext>, MononokeError> {
    let author = String::from("Test Author <test@example.com>");
    let author_date = FixedOffset::east_opt(0)
        .unwrap()
        .with_ymd_and_hms(2000, 2, 1, 12, 0, 0)
        .unwrap();
    let committer = None;
    let committer_date = None;
    let extra = BTreeMap::new();
    let bubble = None;
    let git_extra_headers = None;
    let info_stack = (1..=changes_stack.len())
        .map(|n| CreateInfo {
            author: author.clone(),
            author_date,
            committer: committer.clone(),
            committer_date,
            message: format!("Test Created Commit {n}"),
            extra: extra.clone(),
            git_extra_headers: git_extra_headers.clone(),
        })
        .collect::<Vec<_>>();
    repo.create_changeset_stack(stack_parents, info_stack, changes_stack, bubble)
        .await
}

async fn create_changesets_sequentially(
    repo: &RepoContext,
    changes_stack: Vec<BTreeMap<MononokePath, CreateChange>>,
    stack_parents: Vec<ChangesetId>,
) -> Result<Vec<ChangesetContext>, MononokeError> {
    let author = String::from("Test Author <test@example.com>");
    let author_date = FixedOffset::east_opt(0)
        .unwrap()
        .with_ymd_and_hms(2000, 2, 1, 12, 0, 0)
        .unwrap();
    let committer = None;
    let committer_date = None;
    let extra = BTreeMap::new();
    let bubble = None;
    let git_extra_headers = None;
    let mut parents = stack_parents;
    let mut change_num = 1;
    let mut result = Vec::new();
    for changes in changes_stack {
        let info = CreateInfo {
            author: author.clone(),
            author_date,
            committer: committer.clone(),
            committer_date,
            message: format!("Test Created Commit {change_num}"),
            extra: extra.clone(),
            git_extra_headers: git_extra_headers.clone(),
        };
        let commit = repo
            .create_changeset(parents, info, changes, bubble)
            .await?;
        parents = vec![commit.id()];
        result.push(commit);
        change_num += 1;
    }
    Ok(result)
}

async fn compare_create_stack(
    stack_repo: &RepoContext,
    seq_repo: &RepoContext,
    changes_stack: Vec<BTreeMap<MononokePath, CreateChange>>,
    stack_parents: Vec<ChangesetId>,
) -> Result<Option<Vec<ChangesetContext>>, Error> {
    let stack =
        create_changeset_stack(stack_repo, changes_stack.clone(), stack_parents.clone()).await;
    let seq = create_changesets_sequentially(seq_repo, changes_stack, stack_parents).await;

    match (stack, seq) {
        (Err(_), Err(_)) => Ok(None),
        (_, Err(e)) => Err(e).context("Create failed only on sequential create"),
        (Err(e), _) => Err(e).context("Create failed only on stacked create"),
        (Ok(stack), Ok(seq)) => {
            let stack_ids: Vec<_> = stack.iter().map(|c| c.id()).collect();
            let seq_ids: Vec<_> = seq.iter().map(|c| c.id()).collect();
            assert_eq!(
                stack_ids, seq_ids,
                "stack creation and sequential creation gave different commits"
            );
            Ok(Some(stack))
        }
    }
}

#[fbinit::test]
async fn test_create_commit_stack(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let mononoke = Mononoke::new_test(vec![
        (
            "test_stack".to_string(),
            Linear::get_custom_test_repo(fb).await,
        ),
        (
            "test_seq".to_string(),
            Linear::get_custom_test_repo(fb).await,
        ),
    ])
    .await?;
    let stack_repo = mononoke
        .repo(ctx.clone(), "test_stack")
        .await?
        .expect("repo exists")
        .build()
        .await?;
    let seq_repo = mononoke
        .repo(ctx.clone(), "test_seq")
        .await?
        .expect("repo exists")
        .build()
        .await?;

    let initial_hash = "7785606eb1f26ff5722c831de402350cf97052dc44bc175da6ac0d715a3dbbf6";
    let initial_parents = vec![ChangesetId::from_str(initial_hash)?];

    let changes = vec![
        btreemap! {
            MononokePath::try_from("TEST_FILE")? =>
            CreateChange::Tracked(
                CreateChangeFile::New {
                    bytes: Bytes::from("TEST CREATE 1\n"),
                    file_type: FileType::Regular,
                },
                None,
            ),
        },
        btreemap! {
            MononokePath::try_from("TEST_FILE")? =>
            CreateChange::Tracked(
                CreateChangeFile::New {
                    bytes: Bytes::from("TEST CHANGE 1\n"),
                    file_type: FileType::Regular,
                },
                None,
            ),
            MononokePath::try_from("TEST_DIR/TEST_FILE")? =>
            CreateChange::Tracked(
                CreateChangeFile::New {
                    bytes: Bytes::from("TEST CREATE 2\n"),
                    file_type: FileType::Regular,
                },
                None,
            ),
        },
        btreemap! {
            MononokePath::try_from("TEST_DIR/TEST_FILE")? =>
            CreateChange::Tracked(
                CreateChangeFile::New {
                    bytes: Bytes::from("TEST CHANGE 2\n"),
                    file_type: FileType::Regular,
                },
                None,
            ),
        },
    ];

    let stack = compare_create_stack(&stack_repo, &seq_repo, changes, initial_parents.clone())
        .await?
        .expect("stack should have been created");

    for (commit, path, content) in [
        (0, "TEST_FILE", "TEST CREATE 1\n"),
        (1, "TEST_FILE", "TEST CHANGE 1\n"),
        (2, "TEST_FILE", "TEST CHANGE 1\n"),
        (1, "TEST_DIR/TEST_FILE", "TEST CREATE 2\n"),
        (2, "TEST_DIR/TEST_FILE", "TEST CHANGE 2\n"),
    ] {
        let actual_content = stack[commit]
            .path_with_content(path)
            .await?
            .file()
            .await?
            .expect("file should exist")
            .content_concat()
            .await?;
        assert_eq!(actual_content, Bytes::from(content));
    }
    assert!(
        stack[0]
            .path_with_content("TEST_DIR/TEST_FILE")
            .await?
            .file()
            .await?
            .is_none(),
        "file should not exist yet"
    );
    Ok(())
}

#[fbinit::test]
async fn test_create_commit_stack_delete_files(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let mononoke = Mononoke::new_test(vec![
        (
            "test_stack".to_string(),
            Linear::get_custom_test_repo(fb).await,
        ),
        (
            "test_seq".to_string(),
            Linear::get_custom_test_repo(fb).await,
        ),
    ])
    .await?;
    let stack_repo = mononoke
        .repo(ctx.clone(), "test_stack")
        .await?
        .expect("repo exists")
        .build()
        .await?;
    let seq_repo = mononoke
        .repo(ctx.clone(), "test_seq")
        .await?
        .expect("repo exists")
        .build()
        .await?;

    let initial_hash = "7785606eb1f26ff5722c831de402350cf97052dc44bc175da6ac0d715a3dbbf6";
    let initial_parents = vec![ChangesetId::from_str(initial_hash)?];

    // Deleting a file that doesn't exist should fail.
    let changes = vec![
        btreemap! {
            MononokePath::try_from("TEST_FILE")? =>
            CreateChange::Tracked(
                CreateChangeFile::New {
                    bytes: Bytes::from("TEST CREATE 1\n"),
                    file_type: FileType::Regular,
                },
                None,
            ),
        },
        btreemap! {
            MononokePath::try_from("TEST_OTHER_FILE")? =>
            CreateChange::Deletion,
        },
    ];
    assert!(
        compare_create_stack(&stack_repo, &seq_repo, changes, initial_parents.clone())
            .await?
            .is_none()
    );

    // But succeed if the file was created in the stack.
    let changes = vec![
        btreemap! {
            MononokePath::try_from("TEST_NEW_FILE")? =>
            CreateChange::Tracked(
                CreateChangeFile::New {
                    bytes: Bytes::from("TEST CREATE 1\n"),
                    file_type: FileType::Regular,
                },
                None,
            ),
        },
        btreemap! {
            MononokePath::try_from("TEST_NEW_FILE")? =>
            CreateChange::Deletion,
        },
    ];
    let _stack = compare_create_stack(&stack_repo, &seq_repo, changes, initial_parents.clone())
        .await?
        .expect("stack should have been created");

    // Deleting a file twice in the stack should also fail.
    let changes = vec![
        btreemap! {
            MononokePath::try_from("TEST_FILE")? =>
            CreateChange::Tracked(
                CreateChangeFile::New {
                    bytes: Bytes::from("TEST CREATE 2\n"),
                    file_type: FileType::Regular,
                },
                None,
            ),
        },
        btreemap! {
            MononokePath::try_from("TEST_FILE")? =>
            CreateChange::Deletion,
        },
        btreemap! {
            MononokePath::try_from("TEST_FILE")? =>
            CreateChange::Deletion,
        },
    ];
    assert!(
        compare_create_stack(&stack_repo, &seq_repo, changes, initial_parents.clone())
            .await?
            .is_none()
    );

    // This should also be true if the first deletion was implicit.
    let changes = vec![
        btreemap! {
            MononokePath::try_from("TEST_PATH/SUBDIR/TEST_FILE")? =>
            CreateChange::Tracked(
                CreateChangeFile::New {
                    bytes: Bytes::from("TEST CREATE 3\n"),
                    file_type: FileType::Regular,
                },
                None,
            ),
        },
        btreemap! {
            MononokePath::try_from("TEST_PATH")? =>
            CreateChange::Tracked(
                CreateChangeFile::New {
                    bytes: Bytes::from("TEST CREATE 3\n"),
                    file_type: FileType::Regular,
                },
                None,
            ),
        },
        btreemap! {
            MononokePath::try_from("TEST_PATH/SUBDIR/TEST_FILE")? =>
            CreateChange::Deletion,
        },
    ];
    assert!(
        compare_create_stack(&stack_repo, &seq_repo, changes, initial_parents.clone())
            .await?
            .is_none()
    );

    Ok(())
}

#[fbinit::test]
async fn test_create_commit_stack_path_conflicts(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let mononoke = Mononoke::new_test(vec![
        (
            "test_stack".to_string(),
            Linear::get_custom_test_repo(fb).await,
        ),
        (
            "test_seq".to_string(),
            Linear::get_custom_test_repo(fb).await,
        ),
    ])
    .await?;
    let stack_repo = mononoke
        .repo(ctx.clone(), "test_stack")
        .await?
        .expect("repo exists")
        .build()
        .await?;
    let seq_repo = mononoke
        .repo(ctx.clone(), "test_seq")
        .await?
        .expect("repo exists")
        .build()
        .await?;

    let initial_hash = "7785606eb1f26ff5722c831de402350cf97052dc44bc175da6ac0d715a3dbbf6";
    let initial_parents = vec![ChangesetId::from_str(initial_hash)?];

    // Attempting to create path conflicts in a stack should fail
    let changes = vec![
        btreemap! {
            MononokePath::try_from("TEST_PATH")? =>
            CreateChange::Tracked(
                CreateChangeFile::New {
                    bytes: Bytes::from("TEST CREATE 1\n"),
                    file_type: FileType::Regular,
                },
                None,
            ),
        },
        btreemap! {
            MononokePath::try_from("TEST_PATH/SUBDIR/TEST_FILE")? =>
            CreateChange::Tracked(
                CreateChangeFile::New {
                    bytes: Bytes::from("TEST CREATE 1\n"),
                    file_type: FileType::Regular,
                },
                None,
            ),
        },
    ];
    assert!(
        compare_create_stack(&stack_repo, &seq_repo, changes, initial_parents.clone())
            .await?
            .is_none()
    );

    // But succeeds if you resolve the path conflict
    let changes = vec![
        btreemap! {
            MononokePath::try_from("TEST_PATH")? =>
            CreateChange::Tracked(
                CreateChangeFile::New {
                    bytes: Bytes::from("TEST CREATE 1\n"),
                    file_type: FileType::Regular,
                },
                None,
            ),
        },
        btreemap! {
            MononokePath::try_from("TEST_PATH")? =>
            CreateChange::Deletion,
            MononokePath::try_from("TEST_PATH/SUBDIR/TEST_FILE")? =>
            CreateChange::Tracked(
                CreateChangeFile::New {
                    bytes: Bytes::from("TEST CREATE 1\n"),
                    file_type: FileType::Regular,
                },
                None,
            ),
        },
    ];
    let _stack = compare_create_stack(&stack_repo, &seq_repo, changes, initial_parents.clone())
        .await?
        .expect("stack should have been created");

    Ok(())
}

#[fbinit::test]
async fn test_create_commit_stack_copy_from(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let mononoke = Mononoke::new_test(vec![
        (
            "test_stack".to_string(),
            Linear::get_custom_test_repo(fb).await,
        ),
        (
            "test_seq".to_string(),
            Linear::get_custom_test_repo(fb).await,
        ),
    ])
    .await?;
    let stack_repo = mononoke
        .repo(ctx.clone(), "test_stack")
        .await?
        .expect("repo exists")
        .build()
        .await?;
    let seq_repo = mononoke
        .repo(ctx.clone(), "test_seq")
        .await?
        .expect("repo exists")
        .build()
        .await?;

    let initial_hash = "7785606eb1f26ff5722c831de402350cf97052dc44bc175da6ac0d715a3dbbf6";
    let initial_parents = vec![ChangesetId::from_str(initial_hash)?];

    // Copy from source must exist in the parent
    let mut changes = vec![
        btreemap! {
            MononokePath::try_from("TEST_FILE")? =>
            CreateChange::Tracked(
                CreateChangeFile::New {
                    bytes: Bytes::from("TEST CREATE 1\n"),
                    file_type: FileType::Regular,
                },
                None,
            ),
        },
        btreemap! {
            MononokePath::try_from("TEST_FILE2")? =>
            CreateChange::Tracked(
                CreateChangeFile::New {
                    bytes: Bytes::from("TEST CREATE 1\n"),
                    file_type: FileType::Regular,
                },
                Some(CreateCopyInfo::new(MononokePath::try_from("OTHER_FILE")?, 0)),
            ),
        },
    ];
    assert!(
        compare_create_stack(
            &stack_repo,
            &seq_repo,
            changes.clone(),
            initial_parents.clone()
        )
        .await?
        .is_none()
    );

    // It's ok if it was created earlier in the stack
    changes.insert(
        0,
        btreemap! {
            MononokePath::try_from("OTHER_FILE")? =>
            CreateChange::Tracked(
                CreateChangeFile::New {
                    bytes: Bytes::from("TEST CREATE 2\n"),
                    file_type: FileType::Regular,
                },
                None,
            ),
        },
    );
    let _stack = compare_create_stack(&stack_repo, &seq_repo, changes, initial_parents.clone())
        .await?
        .expect("stack should have been created");

    Ok(())
}


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

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

use anyhow::Result;
use bookmarks::BookmarkKey;
use bookmarks::BookmarkUpdateLogRef;
use bookmarks::BookmarkUpdateReason;
use bookmarks::BookmarksRef;
use bookmarks::Freshness;
use bookmarks_movement::BookmarkKindRestrictions::AnyKind;
use context::CoreContext;
use fbinit::FacebookInit;
use futures::stream::TryStreamExt;
use hooks::PushAuthoredBy::User;
use maplit::hashset;
use mononoke_types::ChangesetId;
use tests_utils::drawdag::create_from_dag;

use crate::repo::BookmarkFreshness;
use crate::repo::Repo;
use crate::repo::RepoContext;

async fn init_repo(ctx: &CoreContext) -> Result<(RepoContext, BTreeMap<String, ChangesetId>)> {
    let repo: Repo = test_repo_factory::build_empty(ctx.fb).await?;
    let changesets = create_from_dag(
        ctx,
        &repo,
        r##"
            A-B-C-G
             \ \
              \ F
               D-E
        "##,
    )
    .await?;
    let mut txn = repo.bookmarks().create_transaction(ctx.clone());
    txn.force_set(
        &BookmarkKey::new("trunk")?,
        changesets["C"],
        BookmarkUpdateReason::TestMove,
    )?;
    txn.commit().await?;

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

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

    // Land G - it should be rewritten even though its parent is C.
    let outcome = repo
        .land_stack(
            "trunk",
            changesets["G"],
            changesets["C"],
            None,
            AnyKind,
            User,
        )
        .await?;
    let key = BookmarkKey::new("trunk")?;
    let trunk_g = repo
        .resolve_bookmark(&key, BookmarkFreshness::MostRecent)
        .await?
        .expect("trunk should be set");
    assert_eq!(trunk_g.id(), outcome.head);
    assert_ne!(trunk_g.id(), changesets["G"]);
    assert_eq!(outcome.rebased_changesets[0].id_old, changesets["G"]);
    assert_eq!(outcome.rebased_changesets[0].id_new, trunk_g.id());

    // Land D and E, both commits should get mapped
    let outcome = repo
        .land_stack(
            "trunk",
            changesets["E"],
            changesets["A"],
            None,
            AnyKind,
            User,
        )
        .await?;
    let trunk_e = repo
        .resolve_bookmark(&key, BookmarkFreshness::MostRecent)
        .await?
        .expect("trunk should be set");
    assert_eq!(trunk_e.id(), outcome.head);
    let mapping: HashMap<_, _> = outcome
        .rebased_changesets
        .iter()
        .map(|pair| (pair.id_old, pair.id_new))
        .collect();
    assert_eq!(mapping.len(), 2);
    assert_ne!(mapping[&changesets["D"]], changesets["D"]);
    assert_ne!(mapping[&changesets["E"]], changesets["D"]);
    assert_ne!(mapping[&changesets["D"]], trunk_e.id());
    assert_eq!(mapping[&changesets["E"]], trunk_e.id());

    // Land F, its parent should be the landed version of E
    let outcome = repo
        .land_stack(
            "trunk",
            changesets["F"],
            changesets["B"],
            None,
            AnyKind,
            User,
        )
        .await?;
    let trunk_f = repo
        .resolve_bookmark(&key, BookmarkFreshness::MostRecent)
        .await?
        .expect("trunk should be set");
    assert_eq!(trunk_f.id(), outcome.head);
    assert_eq!(trunk_f.parents().await?, vec![trunk_e.id()]);

    // With everything landed, all files should be present
    let files: HashSet<_> = trunk_f
        .path_with_content("")
        .await?
        .tree()
        .await?
        .expect("root must be a tree")
        .list()
        .await?
        .map(|(name, _entry)| name)
        .collect();
    let expected_files: HashSet<_> = hashset! {"A", "B", "C", "D", "E", "F", "G"}
        .into_iter()
        .map(String::from)
        .collect();
    assert_eq!(files, expected_files);

    // Check the bookmark moves created BookmarkLogUpdate entries
    let entries = repo
        .blob_repo()
        .bookmark_update_log()
        .list_bookmark_log_entries(
            ctx.clone(),
            BookmarkKey::new("trunk")?,
            4,
            None,
            Freshness::MostRecent,
        )
        .map_ok(|(_id, cs, rs, _ts)| (cs, rs))
        .try_collect::<Vec<_>>()
        .await?;
    assert_eq!(
        entries,
        vec![
            (Some(trunk_f.id()), BookmarkUpdateReason::Pushrebase),
            (Some(trunk_e.id()), BookmarkUpdateReason::Pushrebase),
            (Some(trunk_g.id()), BookmarkUpdateReason::Pushrebase),
            (Some(changesets["C"]), BookmarkUpdateReason::TestMove),
        ]
    );

    Ok(())
}


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

use std::collections::BTreeMap;
use std::sync::Arc;

use anyhow::Result;
use bookmarks::BookmarkCategory;
use bookmarks::BookmarkKey;
use bookmarks::BookmarkName;
use bookmarks::BookmarkUpdateLogRef;
use bookmarks::BookmarkUpdateReason;
use bookmarks::BookmarksRef;
use bookmarks::Freshness;
use context::CoreContext;
use fbinit::FacebookInit;
use futures::stream::TryStreamExt;
use mononoke_types::ChangesetId;
use tests_utils::drawdag::create_from_dag;

use crate::repo::BookmarkFreshness;
use crate::repo::Repo;
use crate::repo::RepoContext;

async fn init_repo(ctx: &CoreContext) -> Result<(RepoContext, BTreeMap<String, ChangesetId>)> {
    let repo: Repo = test_repo_factory::build_empty(ctx.fb).await?;
    let changesets = create_from_dag(
        ctx,
        &repo,
        r##"
            A-B-C-D-E
               \
                F-G
        "##,
    )
    .await?;

    let mut txn = repo.bookmarks().create_transaction(ctx.clone());
    txn.force_set(
        &BookmarkKey::new("trunk")?,
        changesets["C"],
        BookmarkUpdateReason::TestMove,
    )?;
    txn.commit().await?;

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

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

    // Can create public bookmarks on existing changesets (ancestors of trunk).
    let key = BookmarkKey::new("bookmark1")?;
    repo.create_bookmark(&key, changesets["A"], None).await?;
    let bookmark1 = repo
        .resolve_bookmark(&key, BookmarkFreshness::MostRecent)
        .await?
        .expect("bookmark should be set");
    assert_eq!(bookmark1.id(), changesets["A"]);

    // Can create public bookmarks on other changesets (not ancestors of trunk).
    let key = BookmarkKey::new("bookmark2")?;
    repo.create_bookmark(&key, changesets["F"], None).await?;
    let bookmark2 = repo
        .resolve_bookmark(&key, BookmarkFreshness::MostRecent)
        .await?
        .expect("bookmark should be set");
    assert_eq!(bookmark2.id(), changesets["F"]);

    // Can create scratch bookmarks.
    let key = BookmarkKey::new("scratch/bookmark3")?;
    repo.create_bookmark(&key, changesets["G"], None).await?;
    let bookmark3 = repo
        .resolve_bookmark(&key, BookmarkFreshness::MostRecent)
        .await?
        .expect("bookmark should be set");
    assert_eq!(bookmark3.id(), changesets["G"]);

    // Can create tag bookmark
    let key =
        BookmarkKey::with_name_and_category(BookmarkName::new("tag1")?, BookmarkCategory::Tag);
    repo.create_bookmark(&key, changesets["B"], None).await?;
    let tag = repo
        .resolve_bookmark(&key, BookmarkFreshness::MostRecent)
        .await?
        .expect("bookmark should be set");
    assert_eq!(tag.id(), changesets["B"]);

    // Can create note bookmark
    let key =
        BookmarkKey::with_name_and_category(BookmarkName::new("note1")?, BookmarkCategory::Note);
    repo.create_bookmark(&key, changesets["D"], None).await?;
    let note = repo
        .resolve_bookmark(&key, BookmarkFreshness::MostRecent)
        .await?
        .expect("bookmark should be set");
    assert_eq!(note.id(), changesets["D"]);

    // F is now public.  G is not.
    let stack = repo.stack(vec![changesets["G"]], 10).await?;
    assert_eq!(stack.draft, vec![changesets["G"]]);
    assert_eq!(stack.public, vec![changesets["F"]]);

    Ok(())
}

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

    let key = BookmarkKey::new("trunk")?;
    repo.move_bookmark(&key, changesets["E"], None, false, None)
        .await?;
    let trunk = repo
        .resolve_bookmark(&key, BookmarkFreshness::MostRecent)
        .await?
        .expect("bookmark should be set");
    assert_eq!(trunk.id(), changesets["E"]);

    // Attempt to move to a non-descendant commit without allowing
    // non-fast-forward moves should fail.
    assert!(
        repo.move_bookmark(&key, changesets["G"], None, false, None)
            .await
            .is_err()
    );
    repo.move_bookmark(&key, changesets["G"], None, true, None)
        .await?;
    let trunk = repo
        .resolve_bookmark(&key, BookmarkFreshness::MostRecent)
        .await?
        .expect("bookmark should be set");
    assert_eq!(trunk.id(), changesets["G"]);

    // Check the bookmark moves created BookmarkLogUpdate entries
    let entries = repo
        .blob_repo()
        .bookmark_update_log()
        .list_bookmark_log_entries(ctx.clone(), key, 3, None, Freshness::MostRecent)
        .map_ok(|(_id, cs, rs, _ts)| (cs, rs))
        .try_collect::<Vec<_>>()
        .await?;
    assert_eq!(
        entries,
        vec![
            (Some(changesets["G"]), BookmarkUpdateReason::ApiRequest),
            (Some(changesets["E"]), BookmarkUpdateReason::ApiRequest),
            (Some(changesets["C"]), BookmarkUpdateReason::TestMove),
        ]
    );

    // Tags can also be moved
    let key =
        BookmarkKey::with_name_and_category(BookmarkName::new("tag1")?, BookmarkCategory::Tag);
    repo.create_bookmark(&key, changesets["C"], None).await?;
    repo.move_bookmark(&key, changesets["D"], None, false, None)
        .await?;
    let entries = repo
        .blob_repo()
        .bookmark_update_log()
        .list_bookmark_log_entries(ctx.clone(), key, 1, None, Freshness::MostRecent)
        .map_ok(|(_id, cs, rs, _ts)| (cs, rs))
        .try_collect::<Vec<_>>()
        .await?;
    assert_eq!(
        entries,
        vec![(Some(changesets["D"]), BookmarkUpdateReason::ApiRequest),]
    );

    Ok(())
}

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

    let bookmark1_key = BookmarkKey::new("bookmark1")?;
    repo.create_bookmark(&bookmark1_key, changesets["A"], None)
        .await?;
    let bookmark2_key = BookmarkKey::new("bookmark2")?;
    repo.create_bookmark(&bookmark2_key, changesets["F"], None)
        .await?;
    let bookmark3_key = BookmarkKey::new("scratch/bookmark3")?;
    repo.create_bookmark(&bookmark3_key, changesets["G"], None)
        .await?;
    let tag_key =
        BookmarkKey::with_name_and_category(BookmarkName::new("tag1")?, BookmarkCategory::Tag);
    repo.create_bookmark(&tag_key, changesets["B"], None)
        .await?;
    let note_key =
        BookmarkKey::with_name_and_category(BookmarkName::new("note1")?, BookmarkCategory::Note);
    repo.create_bookmark(&note_key, changesets["D"], None)
        .await?;

    // Can delete public bookmarks (of any category).
    repo.delete_bookmark(&bookmark1_key, None, None).await?;
    assert!(
        repo.resolve_bookmark(&bookmark1_key, BookmarkFreshness::MostRecent)
            .await?
            .is_none()
    );
    repo.delete_bookmark(&tag_key, None, None).await?;
    assert!(
        repo.resolve_bookmark(&tag_key, BookmarkFreshness::MostRecent)
            .await?
            .is_none()
    );
    repo.delete_bookmark(&note_key, None, None).await?;
    assert!(
        repo.resolve_bookmark(&note_key, BookmarkFreshness::MostRecent)
            .await?
            .is_none()
    );

    // Deleting a bookmark with the wrong old-target fails.
    assert!(
        repo.delete_bookmark(&bookmark2_key, Some(changesets["E"]), None)
            .await
            .is_err()
    );
    let bookmark2 = repo
        .resolve_bookmark(&bookmark2_key, BookmarkFreshness::MostRecent)
        .await?
        .expect("bookmark should be set");
    assert_eq!(bookmark2.id(), changesets["F"]);

    // But with the right old-target succeeds.
    repo.delete_bookmark(&bookmark2_key, Some(changesets["F"]), None)
        .await?;
    assert!(
        repo.resolve_bookmark(&bookmark1_key, BookmarkFreshness::MostRecent)
            .await?
            .is_none()
    );

    // Deleting a scratch bookmark with the wrong old-target fails.
    assert!(
        repo.delete_bookmark(&bookmark3_key, Some(changesets["E"]), None)
            .await
            .is_err()
    );

    // But with the right old-target succeeds.
    repo.delete_bookmark(&bookmark3_key, Some(changesets["G"]), None)
        .await?;
    assert!(
        repo.resolve_bookmark(&bookmark3_key, BookmarkFreshness::MostRecent)
            .await?
            .is_none()
    );

    Ok(())
}


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

use std::collections::BTreeMap;
use std::collections::HashSet;
use std::sync::Arc;

use anyhow::Context;
use anyhow::Result;
use context::CoreContext;
use fbinit::FacebookInit;
use fixtures::ManyFilesDirs;
use fixtures::TestRepoFixture;
use maplit::btreemap;
use maplit::hashmap;
use maplit::hashset;
use mercurial_types::HgChangesetId;
use metaconfig_types::SparseProfilesConfig;
use mononoke_types::ChangesetId;
use mononoke_types::MPath;
use pathmatcher::Matcher;
use repo_sparse_profiles::RepoSparseProfiles;
use tests_utils::store_files;
use tests_utils::CreateCommitContext;
use types::RepoPath;

use crate::sparse_profile::fetch;
use crate::sparse_profile::get_profile_delta_size;
use crate::sparse_profile::MonitoringProfiles;
use crate::sparse_profile::ProfileSizeChange;
use crate::sparse_profile::SparseProfileMonitoring;
use crate::ChangesetContext;
use crate::Mononoke;
use crate::RepoContext;

async fn init_sparse_profile(
    ctx: &CoreContext,
    repo: &RepoContext,
    cs_id: HgChangesetId,
) -> Result<ChangesetId> {
    let base_profile_content = r#"
        [metadata]
        title: test sparse profile
        description: For test only
        # this is a comment
        ; this is a comment as well

        [include]
        path:dir2
    "#;
    let include_test_profile_content = r#"
        %include sparse/base
        [include]
        path:dir1/subdir1
    "#;
    let other_profile_content = r#"
        [include]
        path:dir1
    "#;
    let top_level_files_profile_content = r#"
        [include]
        glob:{1,2}
    "#;
    let empty_profile_content = r#""#;
    let ignored = r#"[[[["#;

    CreateCommitContext::new(ctx, repo.blob_repo(), vec![cs_id])
        .add_file("sparse/base", base_profile_content)
        .add_file("sparse/include", include_test_profile_content)
        .add_file("sparse/other", other_profile_content)
        .add_file("sparse/top_level_files", top_level_files_profile_content)
        .add_file("sparse/empty", empty_profile_content)
        .add_file("sparse/validation/lib/buck.py", ignored)
        .commit()
        .await
}

async fn commit_changes<T: AsRef<str>>(
    ctx: &CoreContext,
    repo: &RepoContext,
    cs_id: ChangesetId,
    changes: BTreeMap<&str, Option<T>>,
) -> Result<ChangesetId> {
    let changes = store_files(ctx, changes, repo.blob_repo()).await;
    let commit = CreateCommitContext::new(ctx, repo.blob_repo(), vec![cs_id]);
    changes
        .into_iter()
        .fold(commit, |commit, (path, change)| {
            commit.add_file_change(path, change)
        })
        .commit()
        .await
}

fn mock_sparse_monitoring(
    excluded_paths: Vec<String>,
    monitored_profiles: Vec<String>,
    requested_profiles: MonitoringProfiles,
) -> Result<SparseProfileMonitoring> {
    SparseProfileMonitoring::new(
        "test",
        Arc::new(RepoSparseProfiles::new(None)),
        Some(SparseProfilesConfig {
            sparse_profiles_location: "sparse".to_string(),
            excluded_paths,
            monitored_profiles,
        }),
        requested_profiles,
    )
    .context("mock fail")
}

fn mock_default_sparse_monitoring() -> Result<SparseProfileMonitoring> {
    mock_sparse_monitoring(
        vec!["validation/**".to_string()],
        vec![],
        MonitoringProfiles::All,
    )
}

#[fbinit::test]
async fn test_sparse_monitoring_config(fb: FacebookInit) -> Result<()> {
    let ctx = CoreContext::test_mock(fb);
    let mononoke = Mononoke::new_test(vec![(
        "test".to_string(),
        ManyFilesDirs::get_custom_test_repo(fb).await,
    )])
    .await?;
    let repo = mononoke
        .repo(ctx.clone(), "test")
        .await?
        .expect("repo exists")
        .build()
        .await?;
    let hg_cs_id = "d261bc7900818dea7c86935b3fb17a33b2e3a6b4".parse::<HgChangesetId>()?;

    let a = init_sparse_profile(&ctx, &repo, hg_cs_id).await?;
    let changeset_a = ChangesetContext::new(repo.clone(), a);

    // Default should list all files in sparse location
    let monitor = mock_sparse_monitoring(vec![], vec![], MonitoringProfiles::All)?;
    let paths: HashSet<_> = monitor
        .get_monitoring_profiles(&changeset_a)
        .await?
        .into_iter()
        .collect();

    assert_eq!(
        hashset![
            MPath::new("sparse/base")?,
            MPath::new("sparse/include")?,
            MPath::new("sparse/other")?,
            MPath::new("sparse/top_level_files")?,
            MPath::new("sparse/empty")?,
            MPath::new("sparse/validation/lib/buck.py")?,
        ],
        paths
    );

    // Test exclude path
    let monitor = mock_sparse_monitoring(
        vec!["validation/**".to_string()],
        vec![],
        MonitoringProfiles::All,
    )?;
    let paths: HashSet<_> = monitor
        .get_monitoring_profiles(&changeset_a)
        .await?
        .into_iter()
        .collect();

    assert_eq!(
        hashset![
            MPath::new("sparse/base")?,
            MPath::new("sparse/include")?,
            MPath::new("sparse/other")?,
            MPath::new("sparse/top_level_files")?,
            MPath::new("sparse/empty")?,
        ],
        paths
    );

    // Should respect exclude and include
    let monitor = mock_sparse_monitoring(
        vec!["validation/**".to_string()],
        vec!["include".to_string()],
        MonitoringProfiles::All,
    )?;
    let paths = monitor.get_monitoring_profiles(&changeset_a).await?;

    assert_eq!(vec![MPath::new("sparse/include")?], paths);

    // If Exact profiles provided, should ignore monitoring and excludes
    let monitor = mock_sparse_monitoring(
        vec!["validation/**".to_string()],
        vec!["include".to_string()],
        MonitoringProfiles::Exact {
            profiles: vec![
                "sparse/other".to_string(),
                "dir1/file_1_in_dir1".to_string(),
            ],
        },
    )?;
    let paths: HashSet<_> = monitor
        .get_monitoring_profiles(&changeset_a)
        .await?
        .into_iter()
        .collect();

    assert_eq!(
        hashset![
            MPath::new("sparse/other")?,
            MPath::new("dir1/file_1_in_dir1")?
        ],
        paths
    );

    Ok(())
}

#[fbinit::test]
async fn sparse_profile_parsing(fb: FacebookInit) -> Result<()> {
    let ctx = CoreContext::test_mock(fb);
    let mononoke = Mononoke::new_test(vec![(
        "test".to_string(),
        ManyFilesDirs::get_custom_test_repo(fb).await,
    )])
    .await?;
    let repo = mononoke
        .repo(ctx.clone(), "test")
        .await?
        .expect("repo exists")
        .build()
        .await?;
    let hg_cs_id = "d261bc7900818dea7c86935b3fb17a33b2e3a6b4".parse::<HgChangesetId>()?;

    let a = init_sparse_profile(&ctx, &repo, hg_cs_id).await?;

    let changeset = ChangesetContext::new(repo, a);

    let path = "sparse/include".to_string();
    let content = fetch(path.clone(), &changeset).await?.unwrap();
    let profile = sparse::Root::from_bytes(content, path)?;
    let matcher = profile.matcher(|path| fetch(path, &changeset)).await?;

    assert!(!matcher.matches_file(RepoPath::from_str("1")?)?);
    assert!(!matcher.matches_file(RepoPath::from_str("dir1/file1")?)?);
    assert!(matcher.matches_file(RepoPath::from_str("dir1/subdir1/file1")?)?);
    assert!(matcher.matches_file(RepoPath::from_str("dir1/subdir1/subsubdir1/file1")?)?);
    assert!(matcher.matches_file(RepoPath::from_str("dir2/file1")?)?);
    Ok(())
}

#[fbinit::test]
async fn sparse_profile_size(fb: FacebookInit) -> Result<()> {
    let ctx = CoreContext::test_mock(fb);
    let mononoke = Mononoke::new_test(vec![(
        "test".to_string(),
        ManyFilesDirs::get_custom_test_repo(fb).await,
    )])
    .await?;
    let repo = mononoke
        .repo(ctx.clone(), "test")
        .await?
        .expect("repo exists")
        .build()
        .await?;
    let hg_cs_id = "d261bc7900818dea7c86935b3fb17a33b2e3a6b4".parse::<HgChangesetId>()?;

    let a = init_sparse_profile(&ctx, &repo, hg_cs_id).await?;
    let changeset_a = ChangesetContext::new(repo.clone(), a);
    let monitor = mock_default_sparse_monitoring()?;
    let size = monitor
        .get_profile_size(&ctx, &changeset_a, vec![MPath::new("sparse/include")?])
        .await?;

    assert_eq!(size, hashmap! {"sparse/include".to_string() => 45});

    // change size of a file which is included in sparse profile
    // profile size should change.
    let content = "1";
    let changes = btreemap! {
        "dir1/subdir1/file_1" => Some(content),
    };
    let b = commit_changes(&ctx, &repo, a, changes).await?;

    let changeset_b = ChangesetContext::new(repo.clone(), b);
    let size = monitor
        .get_profile_size(&ctx, &changeset_b, vec![MPath::new("sparse/include")?])
        .await?;
    assert_eq!(size, hashmap! {"sparse/include".to_string() => 37});

    // change size of file which is NOT included in sparse profile
    // profile size should not change.
    let content = "1";
    let changes = btreemap! {
        "dir1/file_1_in_dir1" => Some(content),
    };
    let c = commit_changes(&ctx, &repo, b, changes).await?;

    let changeset_c = ChangesetContext::new(repo, c);
    let size = monitor
        .get_profile_size(&ctx, &changeset_c, vec![MPath::new("sparse/include")?])
        .await?;
    assert_eq!(size, hashmap! {"sparse/include".to_string() => 37});

    Ok(())
}

#[fbinit::test]
async fn multiple_sparse_profile_sizes(fb: FacebookInit) -> Result<()> {
    let ctx = CoreContext::test_mock(fb);
    let mononoke = Mononoke::new_test(vec![(
        "test".to_string(),
        ManyFilesDirs::get_custom_test_repo(fb).await,
    )])
    .await?;
    let repo = mononoke
        .repo(ctx.clone(), "test")
        .await?
        .expect("repo exists")
        .build()
        .await?;
    let hg_cs_id = "d261bc7900818dea7c86935b3fb17a33b2e3a6b4".parse::<HgChangesetId>()?;

    let a = init_sparse_profile(&ctx, &repo, hg_cs_id).await?;
    let changeset_a = ChangesetContext::new(repo.clone(), a);
    let profiles_map = hashmap! {
        "sparse/base".to_string() => 9,
        "sparse/include".to_string() => 45,
        "sparse/other".to_string() => 54,
        "sparse/top_level_files".to_string() => 4,
        "sparse/empty".to_string() => 427,
    };
    let profiles_names: Result<Vec<MPath>> = profiles_map.keys().map(MPath::new).collect();
    let monitor = mock_default_sparse_monitoring()?;
    let sizes = monitor
        .get_profile_size(&ctx, &changeset_a, profiles_names?)
        .await?;

    assert_eq!(sizes, profiles_map);

    Ok(())
}

#[fbinit::test]
async fn sparse_profile_delta(fb: FacebookInit) -> Result<()> {
    let ctx = CoreContext::test_mock(fb);
    let mononoke = Mononoke::new_test(vec![(
        "test".to_string(),
        ManyFilesDirs::get_custom_test_repo(fb).await,
    )])
    .await?;
    let repo = mononoke
        .repo(ctx.clone(), "test")
        .await?
        .expect("repo exists")
        .build()
        .await?;
    let hg_cs_id = "d261bc7900818dea7c86935b3fb17a33b2e3a6b4".parse::<HgChangesetId>()?;

    let a = init_sparse_profile(&ctx, &repo, hg_cs_id).await?;
    let changeset_a = ChangesetContext::new(repo.clone(), a);

    let monitor = mock_default_sparse_monitoring()?;
    let profiles_names = monitor.get_monitoring_profiles(&changeset_a).await?;

    // replace the file of size 9 with the file of size 17
    let b = CreateCommitContext::new(&ctx, repo.blob_repo(), vec![a])
        .add_file("dir1/subdir1/file_2", "added new file_2\n")
        .delete_file("dir1/subdir1/file_1")
        .commit()
        .await?;
    let changeset_b = ChangesetContext::new(repo.clone(), b);
    let sizes = get_profile_delta_size(
        &ctx,
        &monitor,
        &changeset_b,
        &changeset_a,
        profiles_names.clone(),
    )
    .await?;
    // should affect 3 profiles
    let expected = hashmap! {
        "sparse/include".to_string() => ProfileSizeChange::Changed(8),
        "sparse/other".to_string() => ProfileSizeChange::Changed(8),
        "sparse/empty".to_string() => ProfileSizeChange::Changed(8),
    };
    assert_eq!(sizes, expected);

    // move file and change content at the same time
    let b1 = CreateCommitContext::new(&ctx, repo.blob_repo(), vec![a])
        .delete_file("dir1/subdir1/file_1")
        .add_file_with_copy_info(
            "dir2/file_3",
            "added new file_2\n",
            (a, "dir1/subdir1/file_1"),
        )
        .commit()
        .await?;
    let changeset_b1 = ChangesetContext::new(repo.clone(), b1);
    let sizes = get_profile_delta_size(
        &ctx,
        &monitor,
        &changeset_b1,
        &changeset_a,
        profiles_names.clone(),
    )
    .await?;

    let expected = hashmap! {
        "sparse/base".to_string() => ProfileSizeChange::Changed(17),
        "sparse/other".to_string() => ProfileSizeChange::Changed(-9),
        "sparse/include".to_string() => ProfileSizeChange::Changed(8),
        "sparse/empty".to_string() => ProfileSizeChange::Changed(8),
    };
    assert_eq!(sizes, expected);

    // move file from one sparse profile to another
    let c = CreateCommitContext::new(&ctx, repo.blob_repo(), vec![b])
        .delete_file("dir1/subdir1/file_2")
        .add_file("dir2/file_2", "added new file_2\n")
        .add_file_with_copy_info(
            "dir2/file_3",
            "added new file_2\n",
            (b, "dir1/subdir1/file_2"),
        )
        .commit()
        .await?;
    let changeset_c = ChangesetContext::new(repo.clone(), c);
    let sizes = get_profile_delta_size(
        &ctx,
        &monitor,
        &changeset_c,
        &changeset_b,
        profiles_names.clone(),
    )
    .await?;

    let expected = hashmap! {
        "sparse/base".to_string() => ProfileSizeChange::Changed(34),
        "sparse/empty".to_string() => ProfileSizeChange::Changed(17),
        "sparse/include".to_string() => ProfileSizeChange::Changed(17),
        "sparse/other".to_string() => ProfileSizeChange::Changed(-17),
    };
    assert_eq!(sizes, expected);

    // replace directory with file
    let c1 = CreateCommitContext::new(&ctx, repo.blob_repo(), vec![c])
        .add_file("dir1/subdir1", "len4")
        .commit()
        .await?;
    let changeset_c1 = ChangesetContext::new(repo.clone(), c1);
    let sizes = get_profile_delta_size(
        &ctx,
        &monitor,
        &changeset_c1,
        &changeset_c,
        profiles_names.clone(),
    )
    .await?;
    let expected = hashmap! {
        "sparse/other".to_string() => ProfileSizeChange::Changed(-23),
        "sparse/include".to_string() => ProfileSizeChange::Changed(-23),
        "sparse/empty".to_string() => ProfileSizeChange::Changed(-23),
    };
    assert_eq!(sizes, expected);

    let d = CreateCommitContext::new(&ctx, repo.blob_repo(), vec![c])
        // this essentially deletes 'dir1' directory
        .delete_file("dir1/file_1_in_dir1")
        .delete_file("dir1/file_2_in_dir1")
        .delete_file("dir1/subdir1/file_2")
        .delete_file("dir1/subdir1/subsubdir1/file_1")
        .delete_file("dir1/subdir1/subsubdir2/file_1")
        .delete_file("dir1/subdir1/subsubdir2/file_2")
        .commit()
        .await?;
    let changeset_d = ChangesetContext::new(repo.clone(), d);
    let sizes = get_profile_delta_size(
        &ctx,
        &monitor,
        &changeset_d,
        &changeset_c,
        profiles_names.clone(),
    )
    .await?;

    let expected = hashmap! {
        "sparse/include".to_string() => ProfileSizeChange::Changed(-27),
        "sparse/other".to_string() => ProfileSizeChange::Changed(-45),
        "sparse/empty".to_string() => ProfileSizeChange::Changed(-45),
    };
    assert_eq!(sizes, expected);

    // change file content from 17 to 1 -> sparse should change -16
    let content = "1";
    let changes = btreemap! {
        "dir2/file_2" => Some(content),
    };
    let e = commit_changes(&ctx, &repo, d, changes).await?;

    let changeset_e = ChangesetContext::new(repo.clone(), e);
    let sizes =
        get_profile_delta_size(&ctx, &monitor, &changeset_e, &changeset_d, profiles_names).await?;
    let expected = hashmap! {
        "sparse/base".to_string() => ProfileSizeChange::Changed(-16),
        "sparse/include".to_string() => ProfileSizeChange::Changed(-16),
        "sparse/empty".to_string() => ProfileSizeChange::Changed(-16),
    };
    assert_eq!(sizes, expected);

    Ok(())
}

#[fbinit::test]
async fn sparse_profile_config_change(fb: FacebookInit) -> Result<()> {
    let ctx = CoreContext::test_mock(fb);
    let mononoke = Mononoke::new_test(vec![(
        "test".to_string(),
        ManyFilesDirs::get_custom_test_repo(fb).await,
    )])
    .await?;
    let repo = mononoke
        .repo(ctx.clone(), "test")
        .await?
        .expect("repo exists")
        .build()
        .await?;
    let hg_cs_id = "d261bc7900818dea7c86935b3fb17a33b2e3a6b4".parse::<HgChangesetId>()?;

    let a = init_sparse_profile(&ctx, &repo, hg_cs_id).await?;
    let changeset_a = ChangesetContext::new(repo.clone(), a);

    let monitor = mock_default_sparse_monitoring()?;
    let profiles_names = monitor.get_monitoring_profiles(&changeset_a).await?;

    // modify base profile config
    let content = r#"
        [metadata]
        title: test sparse profile
        description: For test only
        # this is a comment
        ; this is a comment as well

        [include]
        path:dir1
    "#;
    let changes = btreemap! {
        "sparse/base" => Some(content),
    };
    let e = commit_changes(&ctx, &repo, a, changes).await?;

    let changeset_e = ChangesetContext::new(repo.clone(), e);
    let sizes = get_profile_delta_size(
        &ctx,
        &monitor,
        &changeset_e,
        &changeset_a,
        profiles_names.clone(),
    )
    .await?;

    let expected = hashmap! {
        "sparse/base".to_string() => ProfileSizeChange::Changed(45),
    };
    assert_eq!(sizes, expected);

    // add new profile
    let content = r#"
        [include]
        path:dir2
    "#;
    let f = CreateCommitContext::new(&ctx, repo.blob_repo(), vec![e])
        .add_file("sparse/another", content)
        .commit()
        .await?;

    let changeset_f = ChangesetContext::new(repo.clone(), f);
    let sizes =
        get_profile_delta_size(&ctx, &monitor, &changeset_f, &changeset_e, profiles_names).await?;

    let expected = hashmap! {
        "sparse/another".to_string() => ProfileSizeChange::Added(9),
    };
    assert_eq!(sizes, expected);

    // remove profile
    let g = CreateCommitContext::new(&ctx, repo.blob_repo(), vec![f])
        .delete_file("sparse/top_level_files")
        .commit()
        .await?;

    let changeset_g = ChangesetContext::new(repo.clone(), g);
    let profiles_names = monitor.get_monitoring_profiles(&changeset_g).await?;
    let sizes =
        get_profile_delta_size(&ctx, &monitor, &changeset_g, &changeset_f, profiles_names).await?;

    let expected = hashmap! {
        "sparse/top_level_files".to_string() => ProfileSizeChange::Removed(4),
    };
    assert_eq!(sizes, expected);

    // copy profile
    let other_profile_content = r#"
        [include]
        path:dir1
    "#;
    let h = CreateCommitContext::new(&ctx, repo.blob_repo(), vec![g])
        .add_file_with_copy_info(
            "sparse/other_copy",
            other_profile_content,
            (g, "sparse/other"),
        )
        .commit()
        .await?;
    let changeset_h = ChangesetContext::new(repo.clone(), h);
    let profiles_names = monitor.get_monitoring_profiles(&changeset_h).await?;
    let sizes =
        get_profile_delta_size(&ctx, &monitor, &changeset_h, &changeset_g, profiles_names).await?;

    let expected = hashmap! {
        "sparse/other_copy".to_string() => ProfileSizeChange::Added(54),
    };
    assert_eq!(sizes, expected);

    Ok(())
}

// add test to analyse external profile config and it's change.
#[fbinit::test]
async fn test_sparse_external_config(fb: FacebookInit) -> Result<()> {
    let ctx = CoreContext::test_mock(fb);
    let mononoke = Mononoke::new_test(vec![(
        "test".to_string(),
        ManyFilesDirs::get_custom_test_repo(fb).await,
    )])
    .await?;
    let repo = mononoke
        .repo(ctx.clone(), "test")
        .await?
        .expect("repo exists")
        .build()
        .await?;
    let hg_cs_id = "d261bc7900818dea7c86935b3fb17a33b2e3a6b4".parse::<HgChangesetId>()?;

    let a = init_sparse_profile(&ctx, &repo, hg_cs_id).await?;
    let changeset_a = ChangesetContext::new(repo.clone(), a);
    let content = r#"
        [include]
        path:dir1/subdir1/subsubdir2
    "#;
    // add new profile
    let b = CreateCommitContext::new(&ctx, repo.blob_repo(), vec![a])
        .add_file("dir1/external_profile", content)
        .commit()
        .await?;
    let changeset_b = ChangesetContext::new(repo.clone(), b);

    // Default should list all files in sparse location
    let monitor = mock_sparse_monitoring(
        vec!["validation/**".to_string()],
        vec!["include".to_string()],
        MonitoringProfiles::Exact {
            profiles: vec![
                "sparse/other".to_string(),
                "dir1/external_profile".to_string(),
            ],
        },
    )?;
    let paths = monitor.get_monitoring_profiles(&changeset_b).await?;
    let sizes = get_profile_delta_size(&ctx, &monitor, &changeset_b, &changeset_a, paths).await?;

    let expected = hashmap! {
        "dir1/external_profile".to_string() => ProfileSizeChange::Added(18),
    };
    assert_eq!(sizes, expected);
    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 acl_regions::AclRegions;
use blobrepo::AsBlobRepo;
use blobrepo::BlobRepo;
use bonsai_git_mapping::BonsaiGitMapping;
use bonsai_globalrev_mapping::BonsaiGlobalrevMapping;
use bonsai_hg_mapping::BonsaiHgMapping;
use bonsai_tag_mapping::BonsaiTagMapping;
use bookmarks::BookmarkUpdateLog;
use bookmarks::Bookmarks;
use changeset_fetcher::ChangesetFetcher;
use changesets::Changesets;
use commit_graph::CommitGraph;
use ephemeral_blobstore::RepoEphemeralStore;
use filenodes::Filenodes;
use filestore::FilestoreConfig;
use mercurial_mutation::HgMutationStore;
use metaconfig_types::RepoConfig;
use mutable_counters::MutableCounters;
use mutable_renames::MutableRenames;
use phases::Phases;
use pushrebase_mutation_mapping::PushrebaseMutationMapping;
use repo_blobstore::RepoBlobstore;
use repo_bookmark_attrs::RepoBookmarkAttrs;
use repo_cross_repo::RepoCrossRepo;
use repo_derived_data::RepoDerivedData;
use repo_identity::RepoIdentity;
use repo_lock::RepoLock;
use repo_permission_checker::RepoPermissionChecker;
use repo_sparse_profiles::RepoSparseProfiles;
use segmented_changelog_types::SegmentedChangelog;
use streaming_clone::StreamingClone;

// Eventually everything inside Repo should really be here
// The fields of BlobRepo that are not used in e.g. LFS server should also be moved here
// Each binary will then be able to only build what they use of the "repo attributes".
#[facet::container]
#[derive(Clone)]
pub struct InnerRepo {
    #[delegate(
        FilestoreConfig,
        RepoBlobstore,
        RepoBookmarkAttrs,
        RepoDerivedData,
        RepoIdentity,
        dyn BonsaiGitMapping,
        dyn BonsaiTagMapping,
        dyn BonsaiGlobalrevMapping,
        dyn BonsaiHgMapping,
        dyn BookmarkUpdateLog,
        dyn Bookmarks,
        dyn ChangesetFetcher,
        dyn Changesets,
        dyn Filenodes,
        dyn Phases,
        dyn PushrebaseMutationMapping,
        dyn HgMutationStore,
        dyn MutableCounters,
        dyn RepoPermissionChecker,
        dyn RepoLock,
        CommitGraph,
    )]
    pub blob_repo: BlobRepo,

    #[facet]
    pub repo_config: RepoConfig,

    #[facet]
    pub segmented_changelog: dyn SegmentedChangelog,

    #[facet]
    pub ephemeral_store: RepoEphemeralStore,

    #[facet]
    pub mutable_renames: MutableRenames,

    #[facet]
    pub repo_cross_repo: RepoCrossRepo,

    #[facet]
    pub acl_regions: dyn AclRegions,

    #[facet]
    pub sparse_profiles: RepoSparseProfiles,

    #[facet]
    pub streaming_clone: StreamingClone,
}

impl AsBlobRepo for InnerRepo {
    fn as_blob_repo(&self) -> &BlobRepo {
        &self.blob_repo
    }
}


/*
 * 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 async_trait::async_trait;
use bytes::Bytes;
use mercurial_types::HgNodeHash;
use mercurial_types::HgParents;
use mononoke_api::errors::MononokeError;
use revisionstore_types::Metadata;

use super::repo::HgRepoContext;

/// Trait describing the common interface for working with Mercurial's
/// blob-like types, such as files and trees.
#[async_trait]
pub trait HgDataContext: Send + Sync + 'static {
    type NodeId;

    /// Get the ID (node hash) of this blob.
    fn node_id(&self) -> Self::NodeId;

    /// Get the blob's parents as a strongly-typed tuple.
    fn parents(&self) -> (Option<Self::NodeId>, Option<Self::NodeId>);

    /// Get the parents as an HgParents enum.
    fn hg_parents(&self) -> HgParents;

    /// Fetch the blob content.
    async fn content(&self) -> Result<(Bytes, Metadata), MononokeError>;
}

/// Trait describing a typed identifier for a blob-like type.
/// Typical examples include manifest and filenode hashes.
/// This trait allows constructing `HgDataContext` from these
/// identifiers in a generic and type-safe way.
#[async_trait]
pub trait HgDataId: Send + Sync + 'static {
    type Context: HgDataContext;

    /// Convert a HgNodeHash (which could represent any kind
    /// of Mercurial ID) into this specific ID type.
    fn from_node_hash(hash: HgNodeHash) -> Self;

    /// Load a context for this blob from the repo.
    async fn context(self, repo: HgRepoContext) -> Result<Option<Self::Context>, MononokeError>;
}


/*
 * 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_api::RepoContext;

use super::HgRepoContext;

pub trait RepoContextHgExt {
    /// Get an HgRepoContext to access this repo's data in Mercurial-specific formats.
    fn hg(self) -> HgRepoContext;
}

impl RepoContextHgExt for RepoContext {
    fn hg(self) -> HgRepoContext {
        HgRepoContext::new(self)
    }
}


/*
 * 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 async_trait::async_trait;
use blobrepo_hg::file_history::get_file_history_maybe_incomplete;
use blobstore::Loadable;
use blobstore::LoadableError;
use bytes::Bytes;
use futures::TryStream;
use futures::TryStreamExt;
use getbundle_response::SessionLfsParams;
use mercurial_types::envelope::HgFileEnvelope;
use mercurial_types::FileType;
use mercurial_types::HgFileHistoryEntry;
use mercurial_types::HgFileNodeId;
use mercurial_types::HgNodeHash;
use mercurial_types::HgParents;
use mononoke_api::errors::MononokeError;
use mononoke_types::fsnode::FsnodeFile;
use mononoke_types::ContentMetadataV2;
use mononoke_types::MPath;
use remotefilelog::create_getpack_v2_blob;
use repo_blobstore::RepoBlobstoreRef;
use revisionstore_types::Metadata;

use super::HgDataContext;
use super::HgDataId;
use super::HgRepoContext;

/// An abstraction around a Mercurial filenode.
///
/// In Mercurial's data model, a filenode is addressed by its content along with
/// its history -- a filenode ID is a hash of the file content and its parents'
/// filenode hashes. Notably, filenodes are not addressed by the path of the file
/// within the repo; as such, perhaps counterintuitively, an HgFileContext is not
/// aware of the path to the file to which it refers.
#[derive(Clone)]
pub struct HgFileContext {
    repo: HgRepoContext,
    envelope: HgFileEnvelope,
}

impl HgFileContext {
    /// Create a new `HgFileContext`. The file must exist in the repository.
    ///
    /// To construct an `HgFileContext` for a file that may not exist, use
    /// `new_check_exists`.
    pub async fn new(
        repo: HgRepoContext,
        filenode_id: HgFileNodeId,
    ) -> Result<Self, MononokeError> {
        // Fetch and store Mononoke's internal representation of the metadata of this
        // file. The actual file contents are not fetched here.
        let ctx = repo.ctx();
        let blobstore = repo.blob_repo().repo_blobstore();
        let envelope = filenode_id.load(ctx, blobstore).await?;
        Ok(Self { repo, envelope })
    }

    pub async fn new_check_exists(
        repo: HgRepoContext,
        filenode_id: HgFileNodeId,
    ) -> Result<Option<Self>, MononokeError> {
        let ctx = repo.ctx();
        let blobstore = repo.blob_repo().repo_blobstore();
        match filenode_id.load(ctx, blobstore).await {
            Ok(envelope) => Ok(Some(Self { repo, envelope })),
            Err(LoadableError::Missing(_)) => Ok(None),
            Err(e) => Err(e.into()),
        }
    }

    /// Get the history of this file (at a particular path in the repo) as a stream of Mercurial
    /// file history entries.
    ///
    /// Note that since this context could theoretically represent a filenode that existed at
    /// multiple paths within the repo (for example, two files with identical content that were
    /// added at different locations), the caller is required to specify the exact path of the
    /// file to query.
    pub fn history(
        &self,
        path: MPath,
        max_length: Option<u32>,
    ) -> impl TryStream<Ok = HgFileHistoryEntry, Error = MononokeError> {
        let ctx = self.repo.ctx().clone();
        let blob_repo = self.repo.blob_repo().clone();
        let filenode_id = self.node_id();
        get_file_history_maybe_incomplete(
            ctx,
            blob_repo,
            filenode_id,
            path,
            max_length.map(|len| len as u64),
        )
        .map_err(MononokeError::from)
    }

    pub async fn content_metadata(&self) -> Result<ContentMetadataV2, MononokeError> {
        let content_id = self.envelope.content_id();
        let fetch_key = filestore::FetchKey::Canonical(content_id);
        let blobstore = self.repo.blob_repo().repo_blobstore();
        filestore::get_metadata(blobstore, self.repo.ctx(), &fetch_key)
            .await?
            .ok_or_else(|| {
                MononokeError::NotAvailable(format!(
                    "metadata not found for content id {}",
                    content_id
                ))
            })
    }

    /// Fetches the metadata that would be present in this file's corresponding FsNode, returning
    /// it with the FsNode type, but without actually fetching the FsNode.
    ///
    /// Instead, this method separately reads the `ContentId`, uses that to fetch the size, Sha1,
    /// and Sha256, and combines that with the FileType, which the user must be provide (available
    /// in the parent tree manifest).
    pub async fn fetch_fsnode_data(
        &self,
        file_type: FileType,
    ) -> Result<FsnodeFile, MononokeError> {
        let metadata = self.content_metadata().await?;
        Ok(FsnodeFile::new(
            metadata.content_id,
            file_type,
            metadata.total_size,
            metadata.sha1,
            metadata.sha256,
        ))
    }
}

#[async_trait]
impl HgDataContext for HgFileContext {
    type NodeId = HgFileNodeId;

    /// Get the filenode hash (HgFileNodeId) for this file version.
    ///
    /// This should be same as the HgFileNodeId specified when this context was created,
    /// but the value returned here comes from the data loaded from Mononoke.
    fn node_id(&self) -> HgFileNodeId {
        self.envelope.node_id()
    }

    /// Get the parents of this file version in a strongly typed way.
    ///
    /// Useful for implementing anything that needs to traverse the history
    /// of file nodes, or otherwise needs to use make further queries using
    /// the returned `HgFileNodeId`s.
    fn parents(&self) -> (Option<HgFileNodeId>, Option<HgFileNodeId>) {
        self.envelope.parents()
    }

    /// Get the parents of this file version in a format that can be easily
    /// sent to the Mercurial client as part of a serialized response.
    fn hg_parents(&self) -> HgParents {
        self.envelope.hg_parents()
    }

    /// Get the content and metadata for this file in the format expected by
    /// Mercurial's data storage layer. In particular, this returns the full
    /// content of the file, in some cases prefixed with a small header. Callers
    /// should not assume that the data returned by this function only contains
    /// file content.
    async fn content(&self) -> Result<(Bytes, Metadata), MononokeError> {
        let ctx = self.repo.ctx();
        let blob_repo = self.repo.blob_repo();
        let filenode_id = self.node_id();
        let lfs_params = SessionLfsParams {
            threshold: self.repo.config().lfs.threshold,
        };

        let (_size, content_fut) =
            create_getpack_v2_blob(ctx, blob_repo, filenode_id, lfs_params, false).await?;

        // TODO(kulshrax): Right now this buffers the entire file content in memory. It would
        // probably be better for this method to return a stream of the file content instead.
        let (_filenode, content, metadata) = content_fut.await?;

        Ok((content, metadata))
    }
}

#[async_trait]
impl HgDataId for HgFileNodeId {
    type Context = HgFileContext;

    fn from_node_hash(hash: HgNodeHash) -> Self {
        HgFileNodeId::new(hash)
    }

    async fn context(self, repo: HgRepoContext) -> Result<Option<HgFileContext>, MononokeError> {
        HgFileContext::new_check_exists(repo, self).await
    }
}

#[cfg(test)]
mod tests {
    use std::str::FromStr;
    use std::sync::Arc;

    use context::CoreContext;
    use fbinit::FacebookInit;
    use fixtures::ManyFilesDirs;
    use fixtures::TestRepoFixture;
    use futures::TryStreamExt;
    use mercurial_types::HgChangesetId;
    use mercurial_types::NULL_HASH;
    use mononoke_api::repo::Repo;
    use mononoke_api::repo::RepoContext;

    use super::*;
    use crate::RepoContextHgExt;

    #[fbinit::test]
    async fn test_hg_file_context(fb: FacebookInit) -> Result<(), MononokeError> {
        let ctx = CoreContext::test_mock(fb);
        let repo = Arc::new(ManyFilesDirs::get_custom_test_repo::<Repo>(fb).await);

        // The `ManyFilesDirs` test repo contains the following files (at tip):
        //   $ hg manifest --debug
        //   b8e02f6433738021a065f94175c7cd23db5f05be 644   1
        //   5d9299349fc01ddd25d0070d149b124d8f10411e 644   2
        //   e2ac7cbe1f85e0d8b416005e905aa2189434ce6c 644   dir1
        //   0eb86721b74ed44cf176ee48b5e95f0192dc2824 644   dir2/file_1_in_dir2

        let repo_ctx = RepoContext::new_test(ctx, repo).await?;
        let hg = repo_ctx.hg();

        // Test HgFileContext::new.
        let file_id = HgFileNodeId::from_str("b8e02f6433738021a065f94175c7cd23db5f05be").unwrap();
        let hg_file = HgFileContext::new(hg.clone(), file_id).await?;

        assert_eq!(file_id, hg_file.node_id());

        let content = hg_file.content().await?;
        assert_eq!(&content.0, &b"1\n"[..]);
        assert_eq!(&content.1, &Metadata::default());

        // Test HgFileContext::new_check_exists.
        let hg_file = HgFileContext::new_check_exists(hg.clone(), file_id).await?;
        assert!(hg_file.is_some());

        let null_id = HgFileNodeId::new(NULL_HASH);
        let null_file = HgFileContext::new(hg.clone(), null_id).await;
        assert!(null_file.is_err());

        let null_file = HgFileContext::new_check_exists(hg.clone(), null_id).await?;
        assert!(null_file.is_none());

        Ok(())
    }

    #[fbinit::test]
    async fn test_hg_file_history(fb: FacebookInit) -> Result<(), MononokeError> {
        let ctx = CoreContext::test_mock(fb);
        let repo = Arc::new(ManyFilesDirs::get_custom_test_repo::<Repo>(fb).await);

        // The `ManyFilesDirs` test repo contains the following files (at tip):
        //   $ hg manifest --debug
        //   b8e02f6433738021a065f94175c7cd23db5f05be 644   1
        //   5d9299349fc01ddd25d0070d149b124d8f10411e 644   2
        //   e2ac7cbe1f85e0d8b416005e905aa2189434ce6c 644   dir1
        //   0eb86721b74ed44cf176ee48b5e95f0192dc2824 644   dir2/file_1_in_dir2

        let repo_ctx = RepoContext::new_test(ctx, repo).await?;
        let hg = repo_ctx.hg();

        // Test HgFileContext::new.
        let file_id = HgFileNodeId::from_str("b8e02f6433738021a065f94175c7cd23db5f05be").unwrap();
        let hg_file = HgFileContext::new(hg.clone(), file_id).await?;

        let path = MPath::new("1")?;
        let history = hg_file.history(path, None).try_collect::<Vec<_>>().await?;

        let expected = vec![HgFileHistoryEntry::new(
            file_id,
            HgParents::None,
            HgChangesetId::new(NULL_HASH),
            None,
        )];
        assert_eq!(history, expected);

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

pub use mercurial_types::HgFileNodeId;
pub use mercurial_types::HgManifestId;
pub use mercurial_types::HgParents;

pub mod data;
pub mod ext;
pub mod file;
pub mod repo;
pub mod tree;

pub use data::HgDataContext;
pub use data::HgDataId;
pub use ext::RepoContextHgExt;
pub use file::HgFileContext;
pub use repo::HgRepoContext;
pub use tree::HgTreeContext;