Add hook method to get any file content

Summary: Add a new method for changeset hooks to allow the content of any file to be retrieved.

Reviewed By: StanislavGlebik

Differential Revision: D10255914

fbshipit-source-id: 4ec89369835a2807675b1eda41b4399cf0c66b32
This commit is contained in:
Tim Fox 2018-10-10 03:26:56 -07:00 committed by Facebook Github Bot
parent 1d69b1f884
commit 370c9f9bd9
2 changed files with 169 additions and 37 deletions

View File

@ -373,12 +373,14 @@ impl HookManager {
.collect();
let comments = str::from_utf8(changeset.comments())?.into();
let parents = HookChangesetParents::from(changeset.parents());
Ok(HookChangeset {
Ok(HookChangeset::new(
author,
files,
comments,
parents,
})
changeset_id,
content_store,
))
},
))
}
@ -393,12 +395,30 @@ where
/// Represents a changeset - more user friendly than the blob changeset
/// as this uses String not Vec[u8]
#[derive(Clone, Debug, PartialEq)]
#[derive(Clone)]
pub struct HookChangeset {
pub author: String,
pub files: Vec<HookFile>,
pub comments: String,
pub parents: HookChangesetParents,
content_store: Arc<FileContentStore>,
changeset_id: HgChangesetId,
}
impl fmt::Debug for HookChangeset {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"HookChangeset changeset_id: {:?} files: {:?}, comments: {:?}",
self.changeset_id, self.files, self.comments
)
}
}
impl PartialEq for HookChangeset {
fn eq(&self, other: &HookChangeset) -> bool {
self.changeset_id == other.changeset_id
}
}
#[derive(Clone)]
@ -490,8 +510,12 @@ impl HookFile {
pub fn file_content(&self) -> BoxFuture<Bytes, Error> {
let path = try_boxfuture!(MPath::new(self.path.as_bytes()));
let changeset_id = self.changeset_id.clone();
self.content_store
.get_file_content_for_changeset(self.changeset_id, path)
.get_file_content_for_changeset(self.changeset_id, path.clone())
.and_then(move |opt| {
opt.ok_or(ErrorKind::NoFileContent(changeset_id, path.into()).into())
})
.boxify()
}
}
@ -502,14 +526,25 @@ impl HookChangeset {
files: Vec<HookFile>,
comments: String,
parents: HookChangesetParents,
changeset_id: HgChangesetId,
content_store: Arc<FileContentStore>,
) -> HookChangeset {
HookChangeset {
author,
files,
comments,
parents,
content_store,
changeset_id,
}
}
pub fn file_content(&self, path: String) -> BoxFuture<Option<Bytes>, Error> {
let path = try_boxfuture!(MPath::new(path.as_bytes()));
self.content_store
.get_file_content_for_changeset(self.changeset_id, path.clone())
.boxify()
}
}
#[derive(Clone, Debug, PartialEq)]
@ -671,7 +706,7 @@ pub trait FileContentStore: Send + Sync {
&self,
changesetid: HgChangesetId,
path: MPath,
) -> BoxFuture<Bytes, Error>;
) -> BoxFuture<Option<Bytes>, Error>;
}
#[derive(Clone)]
@ -684,12 +719,11 @@ impl FileContentStore for InMemoryFileContentStore {
&self,
changesetid: HgChangesetId,
path: MPath,
) -> BoxFuture<Bytes, Error> {
let fut = match self.map.get(&(changesetid, path.clone())) {
Some(bytes) => finished(bytes.clone()),
None => failed(ErrorKind::NoFileContent(changesetid, path.into()).into()),
};
fut.boxify()
) -> BoxFuture<Option<Bytes>, Error> {
let opt = self.map
.get(&(changesetid, path.clone()))
.map(|bytes| bytes.clone());
finished(opt).boxify()
}
}
@ -717,21 +751,26 @@ impl FileContentStore for BlobRepoFileContentStore {
&self,
changesetid: HgChangesetId,
path: MPath,
) -> BoxFuture<Bytes, Error> {
) -> BoxFuture<Option<Bytes>, Error> {
let repo = self.repo.clone();
let repo2 = repo.clone();
let path2 = path.clone();
repo.get_changeset_by_changesetid(&changesetid)
.and_then(move |changeset| {
repo.find_file_in_manifest(&path, changeset.manifestid().clone())
})
.and_then(move |opt| {
opt.ok_or(ErrorKind::NoFileContent(changesetid, path2.into()).into())
.and_then(move |opt| match opt {
Some(hash) => repo2
.get_file_content(&hash.into_nodehash())
.map(|content| Some(content))
.boxify(),
None => finished(None).boxify(),
})
.and_then(move |hash| repo2.get_file_content(&hash.into_nodehash()))
.and_then(|content| {
let FileContents::Bytes(bytes) = content;
Ok(bytes)
.and_then(|opt| match opt {
Some(content) => {
let FileContents::Bytes(bytes) = content;
Ok(Some(bytes))
}
None => Ok(None),
})
.boxify()
}
@ -1006,6 +1045,40 @@ mod test {
Box::new(LengthMatchingChangesetHook { expected_lengths })
}
#[derive(Clone, Debug)]
struct OtherFileMatchingChangesetHook {
file_path: String,
expected_content: Option<String>,
}
impl Hook<HookChangeset> for OtherFileMatchingChangesetHook {
fn run(&self, context: HookContext<HookChangeset>) -> BoxFuture<HookExecution, Error> {
let expected_content = self.expected_content.clone();
context
.data
.file_content(self.file_path.clone())
.map(|opt| opt.map(|content| str::from_utf8(&*content).unwrap().to_string()))
.map(move |opt| {
if opt == expected_content {
HookExecution::Accepted
} else {
default_rejection()
}
})
.boxify()
}
}
fn other_file_matching_changeset_hook(
file_path: String,
expected_content: Option<String>,
) -> Box<Hook<HookChangeset>> {
Box::new(OtherFileMatchingChangesetHook {
file_path,
expected_content,
})
}
#[derive(Clone, Debug)]
struct FnFileHook {
f: fn(HookContext<HookFile>) -> HookExecution,
@ -1211,6 +1284,8 @@ mod test {
hook_files,
"3".into(),
parents,
cs_id,
content_store,
);
let expected_context = HookContext {
hook_name: "hook1".into(),
@ -1265,6 +1340,30 @@ mod test {
});
}
#[test]
fn test_changeset_hook_other_file_content() {
async_unit::tokio_unit_test(|| {
let hooks: HashMap<String, Box<Hook<HookChangeset>>> = hashmap! {
"hook1".to_string() => other_file_matching_changeset_hook("dir1/subdir1/subsubdir1/file_1".to_string(), Some("elephants".to_string())),
"hook2".to_string() => other_file_matching_changeset_hook("dir1/subdir1/subsubdir1/file_1".to_string(), Some("giraffes".to_string())),
"hook3".to_string() => other_file_matching_changeset_hook("dir1/subdir1/subsubdir2/file_2".to_string(), Some("aardvarks".to_string())),
"hook4".to_string() => other_file_matching_changeset_hook("no/such/path".to_string(), None),
"hook5".to_string() => other_file_matching_changeset_hook("no/such/path".to_string(), Some("whateva".to_string())),
};
let bookmarks = hashmap! {
"bm1".to_string() => vec!["hook1".to_string(), "hook2".to_string(), "hook3".to_string(), "hook4".to_string(), "hook5".to_string()]
};
let expected = hashmap! {
"hook1".to_string() => HookExecution::Accepted,
"hook2".to_string() => default_rejection(),
"hook3".to_string() => default_rejection(),
"hook4".to_string() => HookExecution::Accepted,
"hook5".to_string() => default_rejection(),
};
run_changeset_hooks("bm1", hooks, bookmarks, expected);
});
}
#[test]
fn test_changeset_hook_file_content() {
async_unit::tokio_unit_test(|| {

View File

@ -39,6 +39,7 @@ __hook_start = function(info, arg)
end
ctx.files = files
ctx.file_content = function(path) return coroutine.yield(__file_content(path)) end
end)
end
";
@ -93,7 +94,6 @@ impl Hook<HookChangeset> for LuaHook {
.map(|file| (file.path.clone(), file.clone()))
.collect();
let files_map2 = files_map.clone();
let files_map3 = files_map.clone();
let contains_string = {
move |path: String, string: String| -> Result<AnyFuture, Error> {
@ -115,29 +115,25 @@ impl Hook<HookChangeset> for LuaHook {
};
let contains_string = function2(contains_string);
let file_content = {
let context2 = context.clone();
move |path: String| -> Result<AnyFuture, Error> {
match files_map2.get(&path) {
Some(file) => {
let future = file.file_content()
.map_err(|err| {
LuaError::ExecutionError(format!(
"failed to get file content: {}",
err
))
})
.map(|content| {
AnyLuaValue::LuaAnyString(AnyLuaString(content.to_vec()))
});
Ok(AnyFuture::new(future))
}
None => Ok(AnyFuture::new(ok(AnyLuaValue::LuaBoolean(false)))),
}
let future = context2
.data
.file_content(path)
.map_err(|err| {
LuaError::ExecutionError(format!("failed to get file content: {}", err))
})
.map(|opt| match opt {
Some(content) => AnyLuaValue::LuaAnyString(AnyLuaString(content.to_vec())),
None => AnyLuaValue::LuaNil,
});
Ok(AnyFuture::new(future))
}
};
let file_content = function1(file_content);
let file_len = {
move |path: String| -> Result<AnyFuture, Error> {
match files_map3.get(&path) {
match files_map2.get(&path) {
Some(file) => {
let future = file.len()
.map_err(|err| {
@ -433,6 +429,40 @@ mod test {
});
}
#[test]
fn test_cs_hook_other_file_content_match() {
async_unit::tokio_unit_test(|| {
let changeset = default_changeset();
let code = String::from(
"hook = function (ctx)\n\
return ctx.file_content(\"file1\") == \"file1sausages\" and\n
ctx.file_content(\"file2\") == \"file2sausages\" and\n
ctx.file_content(\"file3\") == \"file3sausages\"\n
end",
);
assert_matches!(
run_changeset_hook(code, changeset),
Ok(HookExecution::Accepted)
);
});
}
#[test]
fn test_file_content_not_found_returns_nil() {
async_unit::tokio_unit_test(|| {
let changeset = default_changeset();
let code = String::from(
"hook = function (ctx)\n\
return ctx.file_content(\"no/such/path\") == nil\n
end",
);
assert_matches!(
run_changeset_hook(code, changeset),
Ok(HookExecution::Accepted)
);
});
}
#[test]
fn test_cs_hook_check_type() {
async_unit::tokio_unit_test(|| {
@ -1007,6 +1037,7 @@ mod test {
content_store.insert((cs_id.clone(), to_mpath(&path)), content_bytes.into());
}
let content_store = Arc::new(content_store);
let content_store2 = content_store.clone();
let create_hook_files = move |files: Vec<String>, ty: ChangedFileType| -> Vec<HookFile> {
files
@ -1025,6 +1056,8 @@ mod test {
hook_files,
"some-comments".into(),
HookChangesetParents::One("p1-hash".into()),
cs_id,
content_store2,
)
}