mirror of
https://github.com/facebook/sapling.git
synced 2024-10-09 16:31:02 +03:00
1ab8052daf
Summary: It's nice to be able to expect it Differential Revision: D29366561 fbshipit-source-id: 12b3cb31e9c5821d942a0a10f97962e3ae1ddc41
260 lines
8.5 KiB
Rust
260 lines
8.5 KiB
Rust
/*
|
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
*
|
|
* This software may be used and distributed according to the terms of the
|
|
* GNU General Public License version 2.
|
|
*/
|
|
|
|
use crate::error::SubcommandError;
|
|
|
|
use anyhow::{format_err, Error};
|
|
use blobrepo::BlobRepo;
|
|
use blobrepo_hg::BlobRepoHg;
|
|
use blobstore::Loadable;
|
|
use clap::{App, Arg, ArgMatches, SubCommand};
|
|
use cloned::cloned;
|
|
use cmdlib::{
|
|
args::{self, MononokeMatches},
|
|
helpers,
|
|
};
|
|
use context::CoreContext;
|
|
use deleted_files_manifest::{find_entries, list_all_entries, RootDeletedManifestId};
|
|
use derived_data::BonsaiDerived;
|
|
use fbinit::FacebookInit;
|
|
use futures::{
|
|
compat::{Future01CompatExt, Stream01CompatExt},
|
|
future, StreamExt, TryStreamExt,
|
|
};
|
|
use manifest::{get_implicit_deletes, PathOrPrefix};
|
|
use mononoke_types::{ChangesetId, DeletedManifestId, MPath};
|
|
use revset::AncestorsNodeStream;
|
|
use slog::{debug, Logger};
|
|
use std::{collections::BTreeSet, str::FromStr};
|
|
|
|
pub const DELETED_MANIFEST: &str = "deleted-manifest";
|
|
const COMMAND_MANIFEST: &str = "manifest";
|
|
const COMMAND_VERIFY: &str = "verify";
|
|
const COMMAND_FETCH: &str = "fetch";
|
|
const ARG_CSID: &str = "csid";
|
|
const ARG_ID: &str = "id";
|
|
const ARG_LIMIT: &str = "limit";
|
|
const ARG_PATH: &str = "path";
|
|
|
|
pub fn build_subcommand<'a, 'b>() -> App<'a, 'b> {
|
|
let csid_arg = Arg::with_name(ARG_CSID)
|
|
.help("{hg|bonsai} changeset id or bookmark name")
|
|
.index(1)
|
|
.required(true);
|
|
|
|
let path_arg = Arg::with_name(ARG_PATH)
|
|
.help("path")
|
|
.index(2)
|
|
.default_value("");
|
|
|
|
SubCommand::with_name(DELETED_MANIFEST)
|
|
.about("derive, inspect and verify deleted files manifest")
|
|
.subcommand(
|
|
SubCommand::with_name(COMMAND_MANIFEST)
|
|
.about("recursively list all deleted files manifest entries under the given path")
|
|
.arg(csid_arg.clone())
|
|
.arg(path_arg.clone()),
|
|
)
|
|
.subcommand(
|
|
SubCommand::with_name(COMMAND_VERIFY)
|
|
.about("verify deleted manifest against actual paths deleted in commits")
|
|
.arg(csid_arg.clone())
|
|
.arg(
|
|
Arg::with_name(ARG_LIMIT)
|
|
.help("number of commits to be verified")
|
|
.takes_value(true)
|
|
.required(true),
|
|
),
|
|
)
|
|
.subcommand(
|
|
SubCommand::with_name(COMMAND_FETCH)
|
|
.about("fetch and print deleted manifest entry by id")
|
|
.arg(
|
|
Arg::with_name(ARG_ID)
|
|
.help("deleted file manifest id to fetch")
|
|
.takes_value(true)
|
|
.required(true),
|
|
),
|
|
)
|
|
}
|
|
|
|
pub async fn subcommand_deleted_manifest<'a>(
|
|
fb: FacebookInit,
|
|
logger: Logger,
|
|
matches: &'a MononokeMatches<'_>,
|
|
sub_matches: &'a ArgMatches<'_>,
|
|
) -> Result<(), SubcommandError> {
|
|
let repo: BlobRepo = args::open_repo(fb, &logger, &matches).await?;
|
|
let ctx = CoreContext::new_with_logger(fb, logger.clone());
|
|
|
|
match sub_matches.subcommand() {
|
|
(COMMAND_MANIFEST, Some(matches)) => {
|
|
let hash_or_bookmark = String::from(matches.value_of(ARG_CSID).unwrap());
|
|
let path = match matches.value_of(ARG_PATH).unwrap() {
|
|
"" => None,
|
|
p => MPath::new(p).map(Some)?,
|
|
};
|
|
let cs_id = helpers::csid_resolve(ctx.clone(), repo.clone(), hash_or_bookmark)
|
|
.compat()
|
|
.await?;
|
|
subcommand_manifest(ctx, repo, cs_id, path).await?;
|
|
Ok(())
|
|
}
|
|
(COMMAND_VERIFY, Some(matches)) => {
|
|
let hash_or_bookmark = String::from(matches.value_of(ARG_CSID).unwrap());
|
|
let limit = matches
|
|
.value_of(ARG_LIMIT)
|
|
.unwrap()
|
|
.parse::<u64>()
|
|
.expect("limit must be an integer");
|
|
let cs_id = helpers::csid_resolve(ctx.clone(), repo.clone(), hash_or_bookmark)
|
|
.compat()
|
|
.await?;
|
|
subcommand_verify(ctx, repo, cs_id, limit).await?;
|
|
Ok(())
|
|
}
|
|
(COMMAND_FETCH, Some(matches)) => {
|
|
let mf_id = DeletedManifestId::from_str(
|
|
matches
|
|
.value_of(ARG_ID)
|
|
.ok_or_else(|| format_err!("{} not set", ARG_ID))?,
|
|
)?;
|
|
let mf = mf_id
|
|
.load(&ctx, repo.blobstore())
|
|
.await
|
|
.map_err(Error::from)?;
|
|
println!("{:?}", mf);
|
|
Ok(())
|
|
}
|
|
_ => Err(SubcommandError::InvalidArgs),
|
|
}
|
|
}
|
|
|
|
async fn subcommand_manifest(
|
|
ctx: CoreContext,
|
|
repo: BlobRepo,
|
|
cs_id: ChangesetId,
|
|
prefix: Option<MPath>,
|
|
) -> Result<(), Error> {
|
|
let root_manifest = RootDeletedManifestId::derive(&ctx, &repo, cs_id).await?;
|
|
debug!(
|
|
ctx.logger(),
|
|
"ROOT Deleted Files Manifest {:?}", root_manifest,
|
|
);
|
|
let mf_id = root_manifest.deleted_manifest_id().clone();
|
|
let mut entries: Vec<_> = find_entries(
|
|
ctx.clone(),
|
|
repo.get_blobstore(),
|
|
mf_id,
|
|
Some(PathOrPrefix::Prefix(prefix)),
|
|
)
|
|
.try_collect()
|
|
.await?;
|
|
entries.sort_by_key(|(path, _)| path.clone());
|
|
for (path, mf_id) in entries {
|
|
println!("{}/ {:?}", MPath::display_opt(path.as_ref()), mf_id);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
async fn subcommand_verify(
|
|
ctx: CoreContext,
|
|
repo: BlobRepo,
|
|
cs_id: ChangesetId,
|
|
limit: u64,
|
|
) -> Result<(), Error> {
|
|
let mut csids = AncestorsNodeStream::new(ctx.clone(), &repo.get_changeset_fetcher(), cs_id)
|
|
.compat()
|
|
.take(limit as usize);
|
|
while let Some(cs_id) = csids.try_next().await? {
|
|
verify_single_commit(ctx.clone(), repo.clone(), cs_id).await?
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
async fn get_file_changes(
|
|
ctx: CoreContext,
|
|
repo: BlobRepo,
|
|
cs_id: ChangesetId,
|
|
) -> Result<(Vec<MPath>, Vec<MPath>), Error> {
|
|
let paths_added_fut = async {
|
|
let bonsai = cs_id.load(&ctx, repo.blobstore()).await?;
|
|
let paths = bonsai
|
|
.into_mut()
|
|
.file_changes
|
|
.into_iter()
|
|
.filter_map(
|
|
|(path, change)| {
|
|
if change.is_some() { Some(path) } else { None }
|
|
},
|
|
)
|
|
.collect::<Vec<_>>();
|
|
Ok::<_, Error>(paths)
|
|
};
|
|
|
|
let parent_manifests_fut = async {
|
|
let hg_cs_id = repo
|
|
.get_hg_from_bonsai_changeset(ctx.clone(), cs_id)
|
|
.await?;
|
|
let parents = repo.get_changeset_parents(ctx.clone(), hg_cs_id).await?;
|
|
let parents_futs = parents.into_iter().map(|csid| {
|
|
cloned!(ctx, repo);
|
|
async move {
|
|
let blob = csid.load(&ctx, repo.blobstore()).await?;
|
|
Ok(blob.manifestid())
|
|
}
|
|
});
|
|
future::try_join_all(parents_futs).await
|
|
};
|
|
|
|
let (paths_added, parent_manifests) =
|
|
futures::try_join!(paths_added_fut, parent_manifests_fut)?;
|
|
let paths_deleted = get_implicit_deletes(
|
|
&ctx,
|
|
repo.get_blobstore(),
|
|
paths_added.clone(),
|
|
parent_manifests,
|
|
)
|
|
.try_collect()
|
|
.await?;
|
|
Ok((paths_added, paths_deleted))
|
|
}
|
|
|
|
async fn verify_single_commit(
|
|
ctx: CoreContext,
|
|
repo: BlobRepo,
|
|
cs_id: ChangesetId,
|
|
) -> Result<(), Error> {
|
|
let file_changes = get_file_changes(ctx.clone(), repo.clone(), cs_id.clone());
|
|
let deleted_manifest_paths = async move {
|
|
let root_manifest = RootDeletedManifestId::derive(&ctx, &repo, cs_id).await?;
|
|
let mf_id = root_manifest.deleted_manifest_id().clone();
|
|
let entries: BTreeSet<_> = list_all_entries(ctx.clone(), repo.get_blobstore(), mf_id)
|
|
.try_filter_map(|(path_opt, ..)| async move { Ok(path_opt) })
|
|
.try_collect()
|
|
.await?;
|
|
Ok(entries)
|
|
};
|
|
let ((paths_added, paths_deleted), deleted_manifest_paths) =
|
|
futures::try_join!(file_changes, deleted_manifest_paths)?;
|
|
for path in paths_added {
|
|
// check that changed files are alive
|
|
if deleted_manifest_paths.contains(&path) {
|
|
println!("Path {} is alive in changeset {:?}", path, cs_id);
|
|
return Err(format_err!("Path {} is alive", path));
|
|
}
|
|
}
|
|
for path in paths_deleted {
|
|
// check that deleted files are in the manifest
|
|
if !deleted_manifest_paths.contains(&path) {
|
|
println!("Path {} was deleted in changeset {:?}", path, cs_id);
|
|
return Err(format_err!("Path {} is deleted", path));
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|