use std::collections::BTreeSet;
use derive_builder::Builder;
use git_checks_core::impl_prelude::*;
use thiserror::Error;
#[derive(Debug, Error)]
enum ThirdPartyError {
#[error(
"failed to list revisions to find the root commit of {} for {}: {}",
commit,
import,
output
)]
FindRoot {
import: String,
commit: CommitId,
output: String,
},
#[error(
"failed to get the tree object for {} (expected) for {}: {}",
commit,
import,
output
)]
ExpectedTreeObject {
import: String,
commit: CommitId,
output: String,
},
#[error(
"failed to get the tree object for {} (actual) for {}: {}",
commit,
import,
output
)]
ActualTreeObject {
import: String,
commit: CommitId,
output: String,
},
#[error("unexpected output from `git ls-tree`: {}", output)]
LsTreeOutput { output: String },
}
impl ThirdPartyError {
fn find_root(import: String, commit: CommitId, output: &[u8]) -> Self {
ThirdPartyError::FindRoot {
import,
commit,
output: String::from_utf8_lossy(output).into(),
}
}
fn expected_tree_object(import: String, commit: CommitId, output: &[u8]) -> Self {
ThirdPartyError::ExpectedTreeObject {
import,
commit,
output: String::from_utf8_lossy(output).into(),
}
}
fn actual_tree_object(import: String, commit: CommitId, output: &[u8]) -> Self {
ThirdPartyError::ActualTreeObject {
import,
commit,
output: String::from_utf8_lossy(output).into(),
}
}
fn ls_tree_output(output: String) -> Self {
ThirdPartyError::LsTreeOutput {
output,
}
}
}
#[derive(Builder, Debug, Clone)]
#[builder(field(private))]
pub struct ThirdParty {
#[builder(setter(into))]
pub name: String,
#[builder(setter(into))]
pub path: String,
#[builder(setter(into))]
pub root: String,
#[builder(setter(into))]
pub utility: String,
}
impl ThirdParty {
pub fn builder() -> ThirdPartyBuilder {
ThirdPartyBuilder::default()
}
}
enum CheckRefResult {
IsImport,
Rejected(String),
}
impl CheckRefResult {
#[allow(clippy::match_like_matches_macro)]
fn is_import(&self) -> bool {
if let CheckRefResult::IsImport = *self {
true
} else {
false
}
}
fn add_result(self, result: &mut CheckResult) {
if let CheckRefResult::Rejected(err) = self {
result.add_error(err);
}
}
}
impl Check for ThirdParty {
fn name(&self) -> &str {
"third-party"
}
fn check(&self, ctx: &CheckGitContext, commit: &Commit) -> Result<CheckResult, Box<dyn Error>> {
let mut result = CheckResult::new();
let mut check_tree = false;
let mut names_checked = BTreeSet::new();
for diff in &commit.diffs {
let name_path = diff.name.as_path();
if !name_path.starts_with(&self.path) {
continue;
}
if !names_checked.insert(diff.name.as_bytes()) {
continue;
}
let check_ref = |sha1: &CommitId| -> Result<_, Box<dyn Error>> {
let rev_list = ctx
.git()
.arg("rev-list")
.arg("--first-parent")
.arg("--max-parents=0")
.arg(sha1.as_str())
.output()
.map_err(|err| GitError::subcommand("rev-list", err))?;
if !rev_list.status.success() {
return Err(ThirdPartyError::find_root(
self.name.clone(),
sha1.clone(),
&rev_list.stderr,
)
.into());
}
let refs = String::from_utf8_lossy(&rev_list.stdout);
let is_import = self.root == refs.trim();
Ok(if is_import {
CheckRefResult::IsImport
} else {
let msg = format!(
"commit {} not allowed; the `{}` file is maintained by the third party \
utilities; please use `{}` to update this file.",
commit.sha1, diff.name, self.utility,
);
CheckRefResult::Rejected(msg)
})
};
if commit.parents.len() == 2 {
let is_import = check_ref(&commit.parents[1])?.is_import();
if is_import {
check_tree = true;
} else {
check_ref(&commit.sha1)?.add_result(&mut result);
}
} else {
check_ref(&commit.sha1)?.add_result(&mut result);
}
}
if check_tree {
let rev_parse = ctx
.git()
.arg("rev-parse")
.arg(format!("{}^{{tree}}", commit.parents[1]))
.output()
.map_err(|err| GitError::subcommand("rev-parse", err))?;
if !rev_parse.status.success() {
return Err(ThirdPartyError::expected_tree_object(
self.name.clone(),
commit.parents[1].clone(),
&rev_parse.stderr,
)
.into());
}
let expected_tree = String::from_utf8_lossy(&rev_parse.stdout);
let ls_tree = ctx
.git()
.arg("ls-tree")
.arg(commit.sha1.as_str())
.arg(&self.path)
.output()
.map_err(|err| GitError::subcommand("ls-tree", err))?;
if !ls_tree.status.success() {
return Err(ThirdPartyError::actual_tree_object(
self.name.clone(),
commit.sha1.clone(),
&ls_tree.stderr,
)
.into());
}
let ls_tree_output = String::from_utf8_lossy(&ls_tree.stdout);
let actual_tree = ls_tree_output
.split_whitespace()
.nth(2)
.ok_or_else(|| ThirdPartyError::ls_tree_output(ls_tree_output.clone().into()))?;
if actual_tree != expected_tree.trim() {
let msg = format!(
"commit {} not allowed; the `{}` directory contains changes not on the import \
branch; merge conflicts should not happen and indicate that the import \
directory was manually edited at some point. Please find and revert the bad \
edit, apply it to the imported repository (if necessary), and then run the \
import utility.",
commit.sha1, self.path,
);
result.add_error(msg);
}
}
Ok(result)
}
}
#[cfg(feature = "config")]
pub(crate) mod config {
use git_checks_config::{register_checks, CommitCheckConfig, IntoCheck};
use serde::Deserialize;
#[cfg(test)]
use serde_json::json;
#[cfg(test)]
use crate::test;
use crate::ThirdParty;
#[derive(Deserialize, Debug)]
pub struct ThirdPartyConfig {
name: String,
path: String,
root: String,
utility: String,
}
impl IntoCheck for ThirdPartyConfig {
type Check = ThirdParty;
fn into_check(self) -> Self::Check {
ThirdParty::builder()
.name(self.name)
.path(self.path)
.root(self.root)
.utility(self.utility)
.build()
.expect("configuration mismatch for `ThirdParty`")
}
}
register_checks! {
ThirdPartyConfig {
"third_party" => CommitCheckConfig,
},
}
#[test]
fn test_third_party_config_empty() {
let json = json!({});
let err = serde_json::from_value::<ThirdPartyConfig>(json).unwrap_err();
test::check_missing_json_field(err, "name");
}
#[test]
fn test_third_party_config_name_is_required() {
let exp_path = "path/to/import/of/extlib";
let exp_root = "root commit";
let exp_utility = "path/to/update/utility";
let json = json!({
"path": exp_path,
"root": exp_root,
"utility": exp_utility,
});
let err = serde_json::from_value::<ThirdPartyConfig>(json).unwrap_err();
test::check_missing_json_field(err, "name");
}
#[test]
fn test_third_party_config_path_is_required() {
let exp_name = "extlib";
let exp_root = "root commit";
let exp_utility = "path/to/update/utility";
let json = json!({
"name": exp_name,
"root": exp_root,
"utility": exp_utility,
});
let err = serde_json::from_value::<ThirdPartyConfig>(json).unwrap_err();
test::check_missing_json_field(err, "path");
}
#[test]
fn test_third_party_config_root_is_required() {
let exp_name = "extlib";
let exp_path = "path/to/import/of/extlib";
let exp_utility = "path/to/update/utility";
let json = json!({
"name": exp_name,
"path": exp_path,
"utility": exp_utility,
});
let err = serde_json::from_value::<ThirdPartyConfig>(json).unwrap_err();
test::check_missing_json_field(err, "root");
}
#[test]
fn test_third_party_config_utility_is_required() {
let exp_name = "extlib";
let exp_path = "path/to/import/of/extlib";
let exp_root = "root commit";
let json = json!({
"name": exp_name,
"path": exp_path,
"root": exp_root,
});
let err = serde_json::from_value::<ThirdPartyConfig>(json).unwrap_err();
test::check_missing_json_field(err, "utility");
}
#[test]
fn test_third_party_config_minimum_fields() {
let exp_name = "extlib";
let exp_path = "path/to/import/of/extlib";
let exp_root = "root commit";
let exp_utility = "path/to/update/utility";
let json = json!({
"name": exp_name,
"path": exp_path,
"root": exp_root,
"utility": exp_utility,
});
let check: ThirdPartyConfig = serde_json::from_value(json).unwrap();
assert_eq!(check.name, exp_name);
assert_eq!(check.path, exp_path);
assert_eq!(check.root, exp_root);
assert_eq!(check.utility, exp_utility);
let check = check.into_check();
assert_eq!(check.name, exp_name);
assert_eq!(check.path, exp_path);
assert_eq!(check.root, exp_root);
assert_eq!(check.utility, exp_utility);
}
}
#[cfg(test)]
mod tests {
use git_checks_core::Check;
use crate::test::*;
use crate::ThirdParty;
const BASE_COMMIT: &str = "26576e49345a141eca310af92737e489c9baac24";
const VALID_UPDATE_TOPIC: &str = "0bd161c8187d4f727a7acc17020711dcc139b166";
const INVALID_UPDATE_TOPIC: &str = "af154fdff05c871125f2db03eccbdde8571d484e";
const EVIL_UPDATE_TOPIC: &str = "add18e5ab9a67303337cb2754c675fb2e0a45a79";
const EVIL_UPDATE_TOPIC_AND_PARENT: &str = "1c6a384e064b0fc5a80685216f24dd702bdfa5c7";
#[test]
fn test_third_party_builder_default() {
assert!(ThirdParty::builder().build().is_err());
}
#[test]
fn test_third_party_builder_name_is_required() {
assert!(ThirdParty::builder()
.path("check_size")
.root("d50197ebd7167b0941d34405686164068db0b77b")
.utility("./update.sh")
.build()
.is_err());
}
#[test]
fn test_third_party_builder_path_is_required() {
assert!(ThirdParty::builder()
.name("check_size")
.root("d50197ebd7167b0941d34405686164068db0b77b")
.utility("./update.sh")
.build()
.is_err());
}
#[test]
fn test_third_party_builder_root_is_required() {
assert!(ThirdParty::builder()
.name("check_size")
.path("check_size")
.utility("./update.sh")
.build()
.is_err());
}
#[test]
fn test_third_party_builder_utility_is_required() {
assert!(ThirdParty::builder()
.name("check_size")
.path("check_size")
.root("d50197ebd7167b0941d34405686164068db0b77b")
.build()
.is_err());
}
#[test]
fn test_third_party_builder_minimum_fields() {
assert!(ThirdParty::builder()
.name("check_size")
.path("check_size")
.root("d50197ebd7167b0941d34405686164068db0b77b")
.utility("./update.sh")
.build()
.is_ok());
}
#[test]
fn test_third_party_name_commit() {
let check = ThirdParty::builder()
.name("check_size")
.path("check_size")
.root("d50197ebd7167b0941d34405686164068db0b77b")
.utility("./update.sh")
.build()
.unwrap();
assert_eq!(Check::name(&check), "third-party");
}
fn make_third_party_check() -> ThirdParty {
ThirdParty::builder()
.name("check_size")
.path("check_size")
.root("d50197ebd7167b0941d34405686164068db0b77b")
.utility("./update.sh")
.build()
.unwrap()
}
#[test]
fn test_third_party_valid_update() {
let check = make_third_party_check();
let conf = make_check_conf(&check);
let result = test_check_base(
"test_third_party_valid_update",
VALID_UPDATE_TOPIC,
BASE_COMMIT,
&conf,
);
test_result_ok(result);
}
#[test]
fn test_third_party_invalid_update() {
let check = make_third_party_check();
let conf = make_check_conf(&check);
let result = test_check_base(
"test_third_party_invalid_update",
INVALID_UPDATE_TOPIC,
BASE_COMMIT,
&conf,
);
test_result_errors(result, &[
"commit af154fdff05c871125f2db03eccbdde8571d484e not allowed; the \
`check_size/increased-limit` file is maintained by the third party utilities; please \
use `./update.sh` to update this file.",
]);
}
#[test]
fn test_third_party_invalid_update_evil() {
let check = make_third_party_check();
let conf = make_check_conf(&check);
let result = test_check_base(
"test_third_party_invalid_update_evil",
EVIL_UPDATE_TOPIC,
BASE_COMMIT,
&conf,
);
test_result_errors(
result,
&[
"commit add18e5ab9a67303337cb2754c675fb2e0a45a79 not allowed; the `check_size` \
directory contains changes not on the import branch; merge conflicts should not \
happen and indicate that the import directory was manually edited at some point. \
Please find and revert the bad edit, apply it to the imported repository (if \
necessary), and then run the import utility.",
],
);
}
#[test]
fn test_third_party_invalid_update_evil_and_parent() {
let check = make_third_party_check();
let conf = make_check_conf(&check);
let result = test_check_base(
"test_third_party_invalid_update_evil_and_parent",
EVIL_UPDATE_TOPIC_AND_PARENT,
BASE_COMMIT,
&conf,
);
test_result_errors(result, &[
"commit af154fdff05c871125f2db03eccbdde8571d484e not allowed; the \
`check_size/increased-limit` file is maintained by the third party utilities; please \
use `./update.sh` to update this file.",
"commit 1c6a384e064b0fc5a80685216f24dd702bdfa5c7 not allowed; the \
`check_size/increased-limit` file is maintained by the third party utilities; please \
use `./update.sh` to update this file.",
]);
}
}