Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for subtree-conditional prefix filter #961

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions josh-proxy/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -303,11 +303,9 @@ pub fn process_repo_update(repo_update: RepoUpdate) -> josh::JoshResult<String>

resp.push(text.to_string());

let mut warnings = josh::filter::compute_warnings(
&transaction,
filterobj,
transaction.repo().find_commit(oid)?.tree()?,
);
let commit = transaction.repo().find_commit(oid)?;
let mut warnings =
josh::filter::compute_warnings(&transaction, &commit, filterobj, commit.tree()?);

if !warnings.is_empty() {
resp.push("warnings:".to_string());
Expand Down
119 changes: 96 additions & 23 deletions src/filter/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use super::*;
use pest::Parser;
use std::iter::FromIterator;
use std::path::Path;
mod opt;
mod parse;
Expand All @@ -12,6 +13,8 @@ pub use parse::parse;
lazy_static! {
static ref FILTERS: std::sync::Mutex<std::collections::HashMap<Filter, Op>> =
std::sync::Mutex::new(std::collections::HashMap::new());
static ref ANCESTORS: std::sync::Mutex<std::collections::HashMap<git2::Oid, std::collections::HashSet<git2::Oid>>> =
std::sync::Mutex::new(std::collections::HashMap::new());
}

/// Filters are represented as `git2::Oid`, however they are not ever stored
Expand Down Expand Up @@ -105,6 +108,15 @@ enum Op {
Chain(Filter, Filter),
Subtract(Filter, Filter),
Exclude(Filter),

/// Acts like `Prefix(prefix)` but only for `subtree_tip` and its transitive parents.
SubtreePrefix {
// FIXME(RalfJung): I tried using the josh `Oid` type but converting that to/from strings
// did not work very well (probably because of the `Into` instances; as the docs say, one
// should always implement `From). So `git2::Oid` it is.
subtree_tip: git2::Oid,
path: std::path::PathBuf,
},
}

/// Pretty print the filter on multiple lines with initial indentation level.
Expand Down Expand Up @@ -241,6 +253,11 @@ fn spec2(op: &Op) -> String {
Op::Subdir(path) => format!(":/{}", parse::quote(&path.to_string_lossy())),
Op::File(path) => format!("::{}", parse::quote(&path.to_string_lossy())),
Op::Prefix(path) => format!(":prefix={}", parse::quote(&path.to_string_lossy())),
Op::SubtreePrefix { subtree_tip, path } => format!(
":subtree_prefix={},{}",
parse::quote(&subtree_tip.to_string()),
parse::quote(&path.to_string_lossy())
),
Op::Glob(pattern) => format!("::{}", parse::quote(pattern)),
}
}
Expand Down Expand Up @@ -405,7 +422,7 @@ fn apply_to_commit2(
.map(|(f, id)| Ok((f, repo.find_commit(id)?.tree()?)))
.collect::<JoshResult<Vec<_>>>()?;

tree::compose(transaction, filtered)?
tree::compose(transaction, commit, filtered)?
}
}
Op::Workspace(ws_path) => {
Expand Down Expand Up @@ -444,7 +461,7 @@ fn apply_to_commit2(
.chain(extra_parents.into_iter())
.collect();

let filtered_tree = apply(transaction, filter, commit.tree()?)?;
let filtered_tree = apply(transaction, commit, filter, commit.tree()?)?;

return Some(history::create_filtered_commit(
commit,
Expand Down Expand Up @@ -498,8 +515,8 @@ fn apply_to_commit2(
.unwrap_or_else(|_| tree::empty_id())
};
let bf = repo.find_tree(bf)?;
let bu = apply(transaction, invert(*b)?, bf)?;
let ba = apply(transaction, *a, bu)?.id();
let bu = apply(transaction, commit, invert(*b)?, bf)?;
let ba = apply(transaction, commit, *a, bu)?.id();
repo.find_tree(tree::subtract(transaction, af, ba)?)?
}
Op::Exclude(b) => {
Expand All @@ -515,7 +532,7 @@ fn apply_to_commit2(
};
repo.find_tree(tree::subtract(transaction, commit.tree_id(), bf)?)?
}
_ => apply(transaction, filter, commit.tree()?)?,
_ => apply(transaction, commit, filter, commit.tree()?)?,
};

let filtered_parent_ids = {
Expand All @@ -538,17 +555,20 @@ fn apply_to_commit2(
.transpose()
}

/// Filter a single tree. This does not involve walking history and is thus fast in most cases.
/// Filter a single tree, sourced from the given original commit.
/// This does not involve walking history and is thus fast in most cases.
pub fn apply<'a>(
transaction: &'a cache::Transaction,
commit: &git2::Commit<'a>,
filter: Filter,
tree: git2::Tree<'a>,
) -> JoshResult<git2::Tree<'a>> {
apply2(transaction, &to_op(filter), tree)
apply2(transaction, commit, &to_op(filter), tree)
}

fn apply2<'a>(
transaction: &'a cache::Transaction,
commit: &git2::Commit<'a>,
op: &Op,
tree: git2::Tree<'a>,
) -> JoshResult<git2::Tree<'a>> {
Expand Down Expand Up @@ -598,16 +618,24 @@ fn apply2<'a>(
.unwrap_or_else(|_| tree::empty(repo)));
}
Op::Prefix(path) => tree::insert(repo, &tree::empty(repo), path, tree.id(), 0o0040000),
Op::SubtreePrefix { subtree_tip, path } => {
if is_ancestor_of(repo, commit.id(), *subtree_tip)? {
tree::insert(repo, &tree::empty(repo), path, tree.id(), 0o0040000)
} else {
// Just leave it unchanged.
Ok(tree)
}
}

Op::Subtract(a, b) => {
let af = apply(transaction, *a, tree.clone())?;
let bf = apply(transaction, *b, tree.clone())?;
let bu = apply(transaction, invert(*b)?, bf)?;
let ba = apply(transaction, *a, bu)?.id();
let af = apply(transaction, commit, *a, tree.clone())?;
let bf = apply(transaction, commit, *b, tree.clone())?;
let bu = apply(transaction, commit, invert(*b)?, bf)?;
let ba = apply(transaction, commit, *a, bu)?.id();
Ok(repo.find_tree(tree::subtract(transaction, af.id(), ba)?)?)
}
Op::Exclude(b) => {
let bf = apply(transaction, *b, tree.clone())?.id();
let bf = apply(transaction, commit, *b, tree.clone())?.id();
Ok(repo.find_tree(tree::subtract(transaction, tree.id(), bf)?)?)
}

Expand All @@ -623,6 +651,7 @@ fn apply2<'a>(
let wsj_file = chain(base, wsj_file);
apply(
transaction,
commit,
compose(wsj_file, compose(get_workspace(repo, &tree, path), base)),
tree,
)
Expand All @@ -631,30 +660,68 @@ fn apply2<'a>(
Op::Compose(filters) => {
let filtered: Vec<_> = filters
.iter()
.map(|f| apply(transaction, *f, tree.clone()))
.map(|f| apply(transaction, commit, *f, tree.clone()))
.collect::<JoshResult<_>>()?;
let filtered: Vec<_> = filters.iter().zip(filtered.into_iter()).collect();
tree::compose(transaction, filtered)
tree::compose(transaction, commit, filtered)
}

Op::Chain(a, b) => {
return apply(transaction, *b, apply(transaction, *a, tree)?);
return apply(
transaction,
commit,
*b,
apply(transaction, commit, *a, tree)?,
);
}
}
}

/// Check if `commit` is an ancestor of `tip`.
///
/// Creates a cache for a given `tip` so repeated queries with the same `tip` are more efficient.
fn is_ancestor_of(repo: &git2::Repository, commit: git2::Oid, tip: git2::Oid) -> JoshResult<bool> {
let mut ancestor_cache = ANCESTORS.lock().unwrap();
let ancestors = match ancestor_cache.entry(tip) {
std::collections::hash_map::Entry::Occupied(entry) => entry.into_mut(),
std::collections::hash_map::Entry::Vacant(entry) => {
tracing::trace!("is_ancestor_of tip={tip}");
// Recursively compute all ancestors of `tip`.
// Invariant: Everything in `todo` is also in `ancestors`.
let mut todo = vec![tip];
let mut ancestors = std::collections::HashSet::from_iter(todo.iter().copied());
while let Some(commit) = todo.pop() {
for parent in repo.find_commit(commit)?.parent_ids() {
if ancestors.insert(parent) {
// Newly inserted! Also handle its parents.
todo.push(parent);
}
}
}
entry.insert(ancestors)
}
};
Ok(ancestors.contains(&commit))
}

/// Calculate a tree with minimal differences from `parent_tree`
/// such that `apply(unapply(tree, parent_tree)) == tree`
pub fn unapply<'a>(
transaction: &'a cache::Transaction,
commit: &git2::Commit<'a>,
filter: Filter,
tree: git2::Tree<'a>,
parent_tree: git2::Tree<'a>,
) -> JoshResult<git2::Tree<'a>> {
if let Ok(inverted) = invert(filter) {
let matching = apply(transaction, chain(filter, inverted), parent_tree.clone())?;
let matching = apply(
transaction,
commit,
chain(filter, inverted),
parent_tree.clone(),
)?;
let stripped = tree::subtract(transaction, parent_tree.id(), matching.id())?;
let new_tree = apply(transaction, inverted, tree)?;
let new_tree = apply(transaction, commit, inverted, tree)?;

return Ok(transaction.repo().find_tree(tree::overlay(
transaction.repo(),
Expand All @@ -665,6 +732,7 @@ pub fn unapply<'a>(

if let Some(ws) = unapply_workspace(
transaction,
commit,
&to_op(filter),
tree.clone(),
parent_tree.clone(),
Expand All @@ -673,11 +741,12 @@ pub fn unapply<'a>(
}

if let Op::Chain(a, b) = to_op(filter) {
let p = apply(transaction, a, parent_tree.clone())?;
let p = apply(transaction, commit, a, parent_tree.clone())?;
return unapply(
transaction,
commit,
a,
unapply(transaction, b, tree, p)?,
unapply(transaction, commit, b, tree, p)?,
parent_tree,
);
}
Expand All @@ -687,6 +756,7 @@ pub fn unapply<'a>(

fn unapply_workspace<'a>(
transaction: &'a cache::Transaction,
commit: &git2::Commit<'a>,
op: &Op,
tree: git2::Tree<'a>,
parent_tree: git2::Tree<'a>,
Expand All @@ -704,11 +774,12 @@ fn unapply_workspace<'a>(
let original_filter = compose(wsj_file, compose(original_workspace, root));
let matching = apply(
transaction,
commit,
chain(original_filter, invert(original_filter)?),
parent_tree.clone(),
)?;
let stripped = tree::subtract(transaction, parent_tree.id(), matching.id())?;
let new_tree = apply(transaction, invert(filter)?, tree)?;
let new_tree = apply(transaction, commit, invert(filter)?, tree)?;

let result = transaction.repo().find_tree(tree::overlay(
transaction.repo(),
Expand Down Expand Up @@ -766,6 +837,7 @@ pub fn compose(first: Filter, second: Filter) -> Filter {
/// Compute the warnings (filters not matching anything) for the filter applied to the tree
pub fn compute_warnings<'a>(
transaction: &'a cache::Transaction,
commit: &git2::Commit<'a>,
filter: Filter,
tree: git2::Tree<'a>,
) -> Vec<String> {
Expand All @@ -791,23 +863,24 @@ pub fn compute_warnings<'a>(
for f in filters {
let tree = transaction.repo().find_tree(tree.id());
if let Ok(tree) = tree {
warnings.append(&mut compute_warnings2(transaction, f, tree));
warnings.append(&mut compute_warnings2(transaction, commit, f, tree));
}
}
} else {
warnings.append(&mut compute_warnings2(transaction, filter, tree));
warnings.append(&mut compute_warnings2(transaction, commit, filter, tree));
}
warnings
}

fn compute_warnings2<'a>(
transaction: &'a cache::Transaction,
commit: &git2::Commit<'a>,
filter: Filter,
tree: git2::Tree<'a>,
) -> Vec<String> {
let mut warnings = Vec::new();

let tree = apply(transaction, filter, tree);
let tree = apply(transaction, commit, filter, tree);
if let Ok(tree) = tree {
if tree.is_empty() {
warnings.push(format!("No match for \"{}\"", pretty(filter, 2)));
Expand Down
8 changes: 8 additions & 0 deletions src/filter/opt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,14 @@ pub fn invert(filter: Filter) -> JoshResult<Filter> {
Op::Subdir(path) => Some(Op::Prefix(path)),
Op::File(path) => Some(Op::File(path)),
Op::Prefix(path) => Some(Op::Subdir(path)),
Op::SubtreePrefix {
subtree_tip: _,
path: _,
} => {
// We assume that new commits being added don't need the prefix fixup any more.
// FIXME(RalfJung): does that make sense?
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The alternative would be to do the ancestor test as well here. Not sure if that makes sense though, it would only fire if the commits line up perfectly -- I don't quite know what even the commit ID that gets passed down here. (Or rather, currently it doesn't get passed down yet, the callers would have to propagate that.)

Some(Op::Nop)
}
Op::Glob(pattern) => Some(Op::Glob(pattern)),
_ => None,
};
Expand Down
19 changes: 17 additions & 2 deletions src/filter/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ fn make_op(args: &[&str]) -> JoshResult<Op> {
["nop"] => Ok(Op::Nop),
["empty"] => Ok(Op::Empty),
["prefix", arg] => Ok(Op::Prefix(Path::new(arg).to_owned())),
["subtree_prefix", subtree_tip, path] => Ok(Op::SubtreePrefix {
subtree_tip: git2::Oid::from_str(subtree_tip)?,
path: Path::new(path).to_owned(),
}),
["replace", regex, replacement] => Ok(Op::RegexReplace(
regex::Regex::new(regex)?,
replacement.to_string(),
Expand All @@ -19,7 +23,18 @@ fn make_op(args: &[&str]) -> JoshResult<Op> {

:prefix=path

Where `path` is path to be used as a prefix
Where `path` is the path to be used as a prefix
"#
))),
["subtree_prefix"] | ["subtree_prefix", _] => Err(josh_error(indoc!(
r#"
Filter ":prefix" requires two arguments.

Note: use "=" to provide the argument values and "," to separate the arguments:

:subtree_prefix=commit,path

Where `commit` is the tip of the subtree and `path` is the path to be used as a prefix
"#
))),
["workspace"] => Err(josh_error(indoc!(
Expand All @@ -30,7 +45,7 @@ fn make_op(args: &[&str]) -> JoshResult<Op> {

:workspace=path

Where `path` is path to the directory where workspace.josh file is located
Where `path` is the path to the directory where workspace.josh file is located
"#
))),
["SQUASH"] => Ok(Op::Squash),
Expand Down
Loading