2018-05-30 12:01:15 +03:00
|
|
|
// Copyright (c) 2004-present, Facebook, Inc.
|
|
|
|
// All Rights Reserved.
|
|
|
|
//
|
|
|
|
// This software may be used and distributed according to the terms of the
|
|
|
|
// GNU General Public License version 2 or any later version.
|
|
|
|
|
|
|
|
//! This implements a command line utility called 'runhook' which runs a Mononoke hook
|
|
|
|
//! against a specified changeset.
|
|
|
|
//! It's main purpose is to allow easy testing of hooks without having to run them as part of
|
|
|
|
//! a push in a Mononoke server
|
|
|
|
//! It currently supports hooks written in Lua only
|
2018-05-31 17:18:23 +03:00
|
|
|
|
|
|
|
#![deny(warnings)]
|
2018-06-08 13:48:21 +03:00
|
|
|
#![feature(try_from)]
|
2018-05-31 17:18:23 +03:00
|
|
|
|
2018-06-18 18:16:40 +03:00
|
|
|
extern crate blobrepo;
|
|
|
|
extern crate blobstore;
|
2018-08-07 21:35:18 +03:00
|
|
|
extern crate cachelib;
|
2018-05-30 12:01:15 +03:00
|
|
|
extern crate clap;
|
2018-08-07 21:35:20 +03:00
|
|
|
extern crate cmdlib;
|
2018-12-04 22:28:47 +03:00
|
|
|
extern crate context;
|
2018-05-30 12:01:15 +03:00
|
|
|
extern crate failure_ext as failure;
|
|
|
|
extern crate futures;
|
2018-07-04 17:03:43 +03:00
|
|
|
#[macro_use]
|
2018-05-30 12:01:15 +03:00
|
|
|
extern crate futures_ext;
|
2018-06-18 18:16:40 +03:00
|
|
|
extern crate hooks;
|
2018-05-30 12:01:15 +03:00
|
|
|
extern crate manifoldblob;
|
|
|
|
extern crate mercurial_types;
|
|
|
|
extern crate mononoke_types;
|
|
|
|
#[macro_use]
|
|
|
|
extern crate slog;
|
|
|
|
extern crate slog_glog_fmt;
|
2018-07-17 14:37:14 +03:00
|
|
|
extern crate tokio;
|
2018-05-30 12:01:15 +03:00
|
|
|
|
2018-06-18 18:16:40 +03:00
|
|
|
#[cfg(test)]
|
|
|
|
extern crate async_unit;
|
2018-07-05 13:06:38 +03:00
|
|
|
extern crate bookmarks;
|
2018-06-18 18:16:40 +03:00
|
|
|
#[cfg(test)]
|
2018-08-06 20:31:41 +03:00
|
|
|
extern crate fixtures;
|
2018-06-07 18:21:14 +03:00
|
|
|
#[cfg(test)]
|
|
|
|
extern crate tempdir;
|
|
|
|
|
2018-11-05 21:26:48 +03:00
|
|
|
use blobrepo::BlobRepo;
|
2018-07-05 13:06:38 +03:00
|
|
|
use bookmarks::Bookmark;
|
2018-05-30 12:01:15 +03:00
|
|
|
use clap::{App, ArgMatches};
|
2018-12-04 22:28:47 +03:00
|
|
|
use context::CoreContext;
|
2018-05-30 12:01:15 +03:00
|
|
|
use failure::{Error, Result};
|
2018-07-04 17:03:43 +03:00
|
|
|
use futures::Future;
|
2018-05-30 12:01:15 +03:00
|
|
|
use futures_ext::{BoxFuture, FutureExt};
|
2018-10-02 17:01:31 +03:00
|
|
|
use hooks::{HookExecution, HookManager};
|
2018-06-18 18:16:40 +03:00
|
|
|
use hooks::lua_hook::LuaHook;
|
2018-06-08 13:48:21 +03:00
|
|
|
use mercurial_types::{HgChangesetId, RepositoryId};
|
2018-05-30 12:01:15 +03:00
|
|
|
use slog::{Drain, Level, Logger};
|
|
|
|
use slog_glog_fmt::default_drain as glog_drain;
|
2018-06-07 18:21:14 +03:00
|
|
|
use std::env::args;
|
2018-05-30 12:01:15 +03:00
|
|
|
use std::fs::File;
|
|
|
|
use std::io::prelude::*;
|
2018-06-18 18:16:40 +03:00
|
|
|
use std::str::FromStr;
|
|
|
|
use std::sync::Arc;
|
2018-05-30 12:01:15 +03:00
|
|
|
|
2018-06-18 18:16:40 +03:00
|
|
|
fn run_hook(
|
|
|
|
args: Vec<String>,
|
|
|
|
repo_creator: fn(&Logger, &ArgMatches) -> BlobRepo,
|
|
|
|
) -> BoxFuture<HookExecution, Error> {
|
2018-05-30 12:01:15 +03:00
|
|
|
// Define command line args and parse command line
|
2018-08-07 21:35:20 +03:00
|
|
|
let matches = cmdlib::args::add_cachelib_args(
|
|
|
|
App::new("runhook")
|
|
|
|
.version("0.0.0")
|
|
|
|
.about("run a hook")
|
|
|
|
.args_from_usage(concat!(
|
|
|
|
"<REPO_NAME> 'name of repository\n",
|
|
|
|
"<HOOK_FILE> 'file containing hook code\n",
|
|
|
|
"<HOOK_TYPE> 'the type of the hook (perfile, percs)\n",
|
|
|
|
"<REV> 'revision hash'\n",
|
2018-10-09 20:11:29 +03:00
|
|
|
"-p, --myrouter-port=[PORT] 'port for local myrouter instance'\n",
|
2018-08-07 21:35:20 +03:00
|
|
|
)),
|
2018-09-27 17:54:20 +03:00
|
|
|
false, /* hide_advanced_args */
|
2018-08-07 21:35:20 +03:00
|
|
|
).get_matches_from(args);
|
|
|
|
|
|
|
|
cmdlib::args::init_cachelib(&matches);
|
2018-05-30 12:01:15 +03:00
|
|
|
|
2018-12-04 22:28:47 +03:00
|
|
|
let ctx = CoreContext::test_mock();
|
2018-05-30 12:01:15 +03:00
|
|
|
let logger = {
|
|
|
|
let level = if matches.is_present("debug") {
|
|
|
|
Level::Debug
|
|
|
|
} else {
|
|
|
|
Level::Info
|
|
|
|
};
|
|
|
|
|
|
|
|
let drain = glog_drain().filter_level(level).fuse();
|
|
|
|
slog::Logger::root(drain, o![])
|
|
|
|
};
|
|
|
|
|
2018-06-18 18:16:40 +03:00
|
|
|
let repo_name = String::from(matches.value_of("REPO_NAME").unwrap());
|
2018-05-30 12:01:15 +03:00
|
|
|
let hook_file = matches.value_of("HOOK_FILE").unwrap();
|
2018-07-04 17:03:43 +03:00
|
|
|
let hook_type = matches.value_of("HOOK_TYPE").unwrap();
|
|
|
|
println!("hook type is {}", hook_type);
|
|
|
|
let file_hook = match hook_type.as_ref() {
|
|
|
|
"perfile" => true,
|
|
|
|
"percs" => false,
|
|
|
|
_ => panic!("Invalid hook type"),
|
|
|
|
};
|
2018-05-30 12:01:15 +03:00
|
|
|
let revstr = matches.value_of("REV").unwrap();
|
2018-06-18 18:16:40 +03:00
|
|
|
let repo = repo_creator(&logger, &matches);
|
2018-05-30 12:01:15 +03:00
|
|
|
|
|
|
|
let mut file = File::open(hook_file).expect("Unable to open the hook file");
|
|
|
|
let mut code = String::new();
|
|
|
|
file.read_to_string(&mut code)
|
|
|
|
.expect("Unable to read the file");
|
2018-06-07 18:21:12 +03:00
|
|
|
println!("======= Running hook =========");
|
2018-06-18 18:16:36 +03:00
|
|
|
println!("Repository name is {}", repo_name);
|
2018-06-07 18:21:12 +03:00
|
|
|
println!("Hook file is {} revision is {:?}", hook_file, revstr);
|
|
|
|
println!("Hook code is {}", code);
|
|
|
|
println!("==============================");
|
2018-12-04 22:28:47 +03:00
|
|
|
let mut hook_manager =
|
|
|
|
HookManager::new_with_blobrepo(ctx.clone(), Default::default(), repo.clone(), logger);
|
2018-05-31 17:18:23 +03:00
|
|
|
let hook = LuaHook {
|
|
|
|
name: String::from("testhook"),
|
|
|
|
code,
|
|
|
|
};
|
2018-07-04 17:03:43 +03:00
|
|
|
if file_hook {
|
2018-10-11 15:41:02 +03:00
|
|
|
hook_manager.register_file_hook("testhook", Arc::new(hook), None);
|
2018-07-04 17:03:43 +03:00
|
|
|
} else {
|
2018-10-11 15:41:02 +03:00
|
|
|
hook_manager.register_changeset_hook("testhook", Arc::new(hook), None);
|
2018-07-04 17:03:43 +03:00
|
|
|
}
|
2018-07-05 13:06:38 +03:00
|
|
|
let bookmark = Bookmark::new("testbm").unwrap();
|
|
|
|
hook_manager.set_hooks_for_bookmark(bookmark.clone(), vec!["testhook".to_string()]);
|
2018-07-04 17:03:43 +03:00
|
|
|
let id = try_boxfuture!(HgChangesetId::from_str(revstr));
|
|
|
|
if file_hook {
|
|
|
|
hook_manager
|
2018-12-04 22:28:47 +03:00
|
|
|
.run_file_hooks_for_bookmark(ctx, id, &bookmark, None)
|
2018-10-11 15:41:13 +03:00
|
|
|
.map(|executions| {
|
|
|
|
for execution in executions.iter() {
|
|
|
|
if let (_, HookExecution::Rejected(_)) = execution {
|
|
|
|
println!("Returning the first failed hook");
|
|
|
|
return execution.1.clone();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
executions.get(0).unwrap().1.clone()
|
|
|
|
})
|
2018-07-04 17:03:43 +03:00
|
|
|
.boxify()
|
|
|
|
} else {
|
|
|
|
hook_manager
|
2018-12-04 22:28:47 +03:00
|
|
|
.run_changeset_hooks_for_bookmark(ctx, id, &bookmark, None)
|
2018-07-04 17:03:43 +03:00
|
|
|
.map(|executions| executions.get(0).unwrap().1.clone())
|
|
|
|
.boxify()
|
2018-06-18 23:49:18 +03:00
|
|
|
}
|
2018-05-30 12:01:15 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
fn create_blobrepo(logger: &Logger, matches: &ArgMatches) -> BlobRepo {
|
2018-11-05 21:26:48 +03:00
|
|
|
let manifold_args = cmdlib::args::parse_manifold_args(matches);
|
2018-10-11 20:47:48 +03:00
|
|
|
let myrouter_port = matches
|
|
|
|
.value_of("myrouter-port")
|
|
|
|
.expect("missing myrouter port")
|
|
|
|
.parse::<u16>()
|
|
|
|
.expect("myrouter port is not a valid u16");
|
2018-09-27 14:17:08 +03:00
|
|
|
BlobRepo::new_manifold_no_postcommit(
|
2018-05-30 12:01:15 +03:00
|
|
|
logger.clone(),
|
2018-11-05 21:26:48 +03:00
|
|
|
&manifold_args,
|
2018-05-30 12:01:15 +03:00
|
|
|
RepositoryId::new(0),
|
2018-10-11 20:47:48 +03:00
|
|
|
myrouter_port,
|
2018-05-30 12:01:15 +03:00
|
|
|
).expect("failed to create blobrepo instance")
|
|
|
|
}
|
|
|
|
|
|
|
|
// It all starts here
|
|
|
|
fn main() -> Result<()> {
|
2018-06-07 18:21:14 +03:00
|
|
|
let args_vec = args().collect();
|
2018-07-17 14:37:14 +03:00
|
|
|
tokio::run(run_hook(args_vec, create_blobrepo).then(|res| {
|
|
|
|
match res {
|
|
|
|
Ok(HookExecution::Accepted) => println!("Hook accepted the changeset"),
|
|
|
|
Ok(HookExecution::Rejected(rejection_info)) => {
|
|
|
|
println!("Hook rejected the changeset {}", rejection_info.description)
|
|
|
|
}
|
|
|
|
Err(e) => println!("Failed to run hook {:?}", e),
|
2018-06-07 18:21:14 +03:00
|
|
|
}
|
2018-07-17 14:37:14 +03:00
|
|
|
Ok(())
|
|
|
|
}));
|
2018-06-07 18:21:14 +03:00
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod test {
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
use std::fs::File;
|
|
|
|
use tempdir::TempDir;
|
|
|
|
|
|
|
|
#[test]
|
2018-07-05 13:06:34 +03:00
|
|
|
fn test_file_hook_accepted() {
|
|
|
|
test_hook_accepted(true);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_cs_hook_accepted() {
|
|
|
|
test_hook_accepted(false);
|
|
|
|
}
|
|
|
|
|
2018-10-08 18:56:09 +03:00
|
|
|
#[test]
|
|
|
|
fn test_cs_hook_initial_commit() {
|
|
|
|
// Runs hook on a commit that has no parents
|
|
|
|
async_unit::tokio_unit_test(move || {
|
|
|
|
let code = String::from(
|
|
|
|
"hook = function (ctx)\n\
|
|
|
|
return true\n\
|
|
|
|
end",
|
|
|
|
);
|
|
|
|
let changeset_id = String::from("2d7d4ba9ce0a6ffd222de7785b249ead9c51c536");
|
|
|
|
match test_hook(code, changeset_id, true /* file hook */) {
|
|
|
|
Ok(HookExecution::Accepted) => (),
|
|
|
|
Ok(HookExecution::Rejected(_)) => assert!(false, "Hook should be accepted"),
|
|
|
|
Err(e) => assert!(false, format!("Unexpected error {:?}", e)),
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-10-08 18:56:14 +03:00
|
|
|
#[test]
|
|
|
|
fn test_cs_hook_check_comments() {
|
|
|
|
// Runs hook on a commit that has no parents
|
|
|
|
async_unit::tokio_unit_test(move || {
|
|
|
|
let code = String::from(
|
|
|
|
"hook = function (ctx)\n\
|
|
|
|
return ctx.info.comments == \"modified 10\"\n\
|
|
|
|
end",
|
|
|
|
);
|
|
|
|
let changeset_id = String::from("79a13814c5ce7330173ec04d279bf95ab3f652fb");
|
|
|
|
match test_hook(code, changeset_id, false) {
|
|
|
|
Ok(HookExecution::Accepted) => (),
|
|
|
|
Ok(HookExecution::Rejected(_)) => assert!(false, "Hook should be accepted"),
|
|
|
|
Err(e) => assert!(false, format!("Unexpected error {:?}", e)),
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-07-05 13:06:34 +03:00
|
|
|
fn test_hook_accepted(file: bool) {
|
2018-07-04 17:03:43 +03:00
|
|
|
async_unit::tokio_unit_test(move || {
|
2018-06-18 18:16:40 +03:00
|
|
|
let code = String::from(
|
2018-07-05 13:06:34 +03:00
|
|
|
"hook = function (ctx)\n\
|
|
|
|
return true\n\
|
2018-06-18 18:16:40 +03:00
|
|
|
end",
|
|
|
|
);
|
|
|
|
let changeset_id = String::from("a5ffa77602a066db7d5cfb9fb5823a0895717c5a");
|
2018-07-05 13:06:34 +03:00
|
|
|
match test_hook(code, changeset_id, file) {
|
2018-06-18 18:16:40 +03:00
|
|
|
Ok(HookExecution::Accepted) => (),
|
|
|
|
Ok(HookExecution::Rejected(_)) => assert!(false, "Hook should be accepted"),
|
|
|
|
Err(e) => assert!(false, format!("Unexpected error {:?}", e)),
|
2018-07-05 13:06:34 +03:00
|
|
|
}
|
2018-06-18 18:16:40 +03:00
|
|
|
});
|
2018-06-07 18:21:14 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
2018-07-05 13:06:34 +03:00
|
|
|
fn test_file_hook_rejected() {
|
|
|
|
test_hook_rejected(true)
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_cs_hook_rejected() {
|
|
|
|
test_hook_rejected(false)
|
|
|
|
}
|
|
|
|
|
2018-10-11 15:41:13 +03:00
|
|
|
#[test]
|
|
|
|
fn test_another_repo_file_hook_rejected() {
|
|
|
|
async_unit::tokio_unit_test(move || {
|
|
|
|
let code = String::from(
|
|
|
|
r#"hook = function (ctx)
|
|
|
|
if ctx.file.path == "dir1/file_1_in_dir1" then
|
|
|
|
return false, "sausages"
|
|
|
|
end
|
|
|
|
return true
|
|
|
|
end"#,
|
|
|
|
);
|
|
|
|
|
|
|
|
let changeset_id = String::from("2f866e7e549760934e31bf0420a873f65100ad63");
|
|
|
|
let res = test_hook_with_repo(code, changeset_id, true, test_many_files_dirs);
|
|
|
|
match res {
|
|
|
|
Ok(HookExecution::Accepted) => assert!(false, "Hook should be rejected"),
|
|
|
|
Ok(HookExecution::Rejected(rejection_info)) => {
|
|
|
|
assert!(rejection_info.description.starts_with("sausages"))
|
|
|
|
}
|
|
|
|
Err(e) => assert!(false, format!("Unexpected error {:?}", e)),
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-07-05 13:06:34 +03:00
|
|
|
fn test_hook_rejected(file: bool) {
|
2018-07-04 17:03:43 +03:00
|
|
|
async_unit::tokio_unit_test(move || {
|
2018-06-18 18:16:40 +03:00
|
|
|
let code = String::from(
|
2018-07-05 13:06:34 +03:00
|
|
|
"hook = function (ctx)\n\
|
|
|
|
return false, \"sausages\"\n\
|
2018-06-18 18:16:40 +03:00
|
|
|
end",
|
|
|
|
);
|
|
|
|
let changeset_id = String::from("a5ffa77602a066db7d5cfb9fb5823a0895717c5a");
|
2018-07-05 13:06:34 +03:00
|
|
|
match test_hook(code, changeset_id, file) {
|
2018-06-18 18:16:40 +03:00
|
|
|
Ok(HookExecution::Accepted) => assert!(false, "Hook should be rejected"),
|
|
|
|
Ok(HookExecution::Rejected(rejection_info)) => {
|
2018-07-05 13:06:34 +03:00
|
|
|
assert!(rejection_info.description.starts_with("sausages"))
|
2018-06-18 18:16:40 +03:00
|
|
|
}
|
|
|
|
Err(e) => assert!(false, format!("Unexpected error {:?}", e)),
|
2018-07-05 13:06:34 +03:00
|
|
|
}
|
2018-06-18 18:16:40 +03:00
|
|
|
});
|
2018-06-07 18:21:14 +03:00
|
|
|
}
|
|
|
|
|
2018-07-05 13:06:34 +03:00
|
|
|
fn test_hook(code: String, changeset_id: String, run_file: bool) -> Result<HookExecution> {
|
2018-10-11 15:41:13 +03:00
|
|
|
test_hook_with_repo(code, changeset_id, run_file, test_blobrepo)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn test_hook_with_repo(
|
|
|
|
code: String,
|
|
|
|
changeset_id: String,
|
|
|
|
run_file: bool,
|
|
|
|
repo_creator: fn(&Logger, &ArgMatches) -> BlobRepo,
|
|
|
|
) -> Result<HookExecution> {
|
2018-06-07 18:21:14 +03:00
|
|
|
let dir = TempDir::new("runhook").unwrap();
|
|
|
|
let file_path = dir.path().join("testhook.lua");
|
|
|
|
let mut file = File::create(file_path.clone()).unwrap();
|
|
|
|
file.write(code.as_bytes()).unwrap();
|
|
|
|
let args = vec![
|
|
|
|
String::from("runhook"),
|
2018-07-04 17:03:43 +03:00
|
|
|
String::from("test_repo"),
|
2018-06-07 18:21:14 +03:00
|
|
|
file_path.to_str().unwrap().into(),
|
2018-07-05 13:06:34 +03:00
|
|
|
if run_file {
|
|
|
|
String::from("perfile")
|
|
|
|
} else {
|
|
|
|
String::from("percs")
|
|
|
|
},
|
2018-06-07 18:21:14 +03:00
|
|
|
changeset_id,
|
|
|
|
];
|
2018-10-11 15:41:13 +03:00
|
|
|
run_hook(args, repo_creator).wait()
|
2018-06-18 18:16:40 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
fn test_blobrepo(_logger: &Logger, _matches: &ArgMatches) -> BlobRepo {
|
2018-08-06 20:31:41 +03:00
|
|
|
fixtures::linear::getrepo(None)
|
2018-06-07 18:21:14 +03:00
|
|
|
}
|
|
|
|
|
2018-10-11 15:41:13 +03:00
|
|
|
fn test_many_files_dirs(_logger: &Logger, _matches: &ArgMatches) -> BlobRepo {
|
|
|
|
fixtures::many_files_dirs::getrepo(None)
|
|
|
|
}
|
|
|
|
|
2018-05-30 12:01:15 +03:00
|
|
|
}
|