extern crate git_workarea;
use self::git_workarea::CommitId;
use super::super::*;
#[derive(Debug)]
pub struct ThirdParty {
pub name: String,
pub path: String,
pub root: String,
pub script: String,
}
impl ThirdParty {
pub fn new<N, P, R, S>(name: N, path: P, root: R, script: S) -> Self
where N: ToString,
P: ToString,
R: ToString,
S: ToString,
{
ThirdParty {
name: name.to_string(),
path: path.to_string(),
root: root.to_string(),
script: script.to_string(),
}
}
}
enum CheckRefResult {
IsImport,
Rejected(String),
}
impl CheckRefResult {
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> {
let mut result = CheckResult::new();
let mut check_tree = false;
for diff in &commit.diffs {
let name_path = diff.name.as_path();
if !name_path.starts_with(&self.path) {
continue;
}
let check_ref = |sha1: &CommitId| {
let rev_list = try!(ctx.git()
.arg("rev-list")
.arg("--first-parent")
.arg("--max-parents=0")
.arg(sha1.as_str())
.output()
.chain_err(|| "failed to construct rev-list command"));
if !rev_list.status.success() {
bail!(ErrorKind::Git(format!("failed to get list the root commit for a {} \
third-party change: {}",
self.name,
String::from_utf8_lossy(&rev_list.stderr))));
}
let refs = String::from_utf8_lossy(&rev_list.stdout);
let is_import = self.root == refs.trim();
Ok(if !is_import {
let msg = format!("commit {} not allowed; the `{}` file is maintained by the \
third party scripts; please use `{}` to update this file.",
commit.sha1_short,
diff.name,
self.script);
CheckRefResult::Rejected(msg)
} else {
CheckRefResult::IsImport
})
};
if commit.parents.len() == 2 {
let is_import_res: Result<_> = check_ref(&commit.parents[1]);
let is_import = try!(is_import_res).is_import();
if is_import {
check_tree = true;
} else {
try!(check_ref(&commit.sha1)).add_result(&mut result);
}
} else {
try!(check_ref(&commit.sha1)).add_result(&mut result);
}
}
if check_tree {
let rev_parse = try!(ctx.git()
.arg("rev-parse")
.arg(format!("{}^{{tree}}", commit.parents[1]))
.output()
.chain_err(|| "failed to construct rev-parse command"));
if !rev_parse.status.success() {
bail!(ErrorKind::Git(format!("failed to parse the expected tree object for the \
{} third-party directory: {}",
self.name,
String::from_utf8_lossy(&rev_parse.stderr))));
}
let expected_tree = String::from_utf8_lossy(&rev_parse.stdout);
let ls_tree = try!(ctx.git()
.arg("ls-tree")
.arg(commit.sha1.as_str())
.arg(&self.path)
.output()
.chain_err(|| "failed to construct ls-tree command"));
if !ls_tree.status.success() {
bail!(ErrorKind::Git(format!("failed to parse the actual tree object for the \
{} third-party directory: {}",
self.name,
String::from_utf8_lossy(&ls_tree.stderr))));
}
let ls_tree_output = String::from_utf8_lossy(&ls_tree.stdout);
let actual_tree = ls_tree_output.split_whitespace()
.nth(2)
.unwrap();
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 script.",
commit.sha1_short,
self.path);
result.add_error(msg);
}
}
Ok(result)
}
}
#[cfg(test)]
mod tests {
use super::ThirdParty;
use super::super::test::*;
static BASE_COMMIT: &'static str = "26576e49345a141eca310af92737e489c9baac24";
static VALID_UPDATE_TOPIC: &'static str = "0bd161c8187d4f727a7acc17020711dcc139b166";
static INVALID_UPDATE_TOPIC: &'static str = "af154fdff05c871125f2db03eccbdde8571d484e";
static EVIL_UPDATE_TOPIC: &'static str = "add18e5ab9a67303337cb2754c675fb2e0a45a79";
fn make_third_party_check() -> ThirdParty {
ThirdParty::new("check_size",
"check_size",
"d50197ebd7167b0941d34405686164068db0b77b",
"./update.sh")
}
#[test]
fn test_third_party_valid_update() {
let check = make_third_party_check();
let mut conf = GitCheckConfiguration::new();
conf.add_check(&check);
let result = test_check_base("test_third_party_valid_update",
VALID_UPDATE_TOPIC,
BASE_COMMIT,
&conf);
assert_eq!(result.warnings().len(), 0);
assert_eq!(result.alerts().len(), 0);
assert_eq!(result.errors().len(), 0);
assert_eq!(result.allowed(), false);
assert_eq!(result.pass(), true);
}
#[test]
fn test_third_party_invalid_update() {
let check = make_third_party_check();
let mut conf = GitCheckConfiguration::new();
conf.add_check(&check);
let result = test_check_base("test_third_party_invalid_update",
INVALID_UPDATE_TOPIC,
BASE_COMMIT,
&conf);
assert_eq!(result.warnings().len(), 0);
assert_eq!(result.alerts().len(), 0);
assert_eq!(result.errors().len(), 1);
assert_eq!(result.errors()[0],
"commit af154fd not allowed; the `check_size/increased-limit` file is \
maintained by the third party scripts; please use `./update.sh` to \
update this file.");
assert_eq!(result.allowed(), false);
assert_eq!(result.pass(), false);
}
#[test]
fn test_third_party_invalid_update_evil() {
let check = make_third_party_check();
let mut conf = GitCheckConfiguration::new();
conf.add_check(&check);
let result = test_check_base("test_third_party_invalid_update_evil",
EVIL_UPDATE_TOPIC,
BASE_COMMIT,
&conf);
assert_eq!(result.warnings().len(), 0);
assert_eq!(result.alerts().len(), 0);
assert_eq!(result.errors().len(), 1);
assert_eq!(result.errors()[0],
"commit add18e5 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 script.");
assert_eq!(result.allowed(), false);
assert_eq!(result.pass(), false);
}
}