use std::collections::HashSet;
use crate::package_manager::Project;
use crate::path::AbsolutePath;
pub fn match_files_to_projects(
projects: &[Project],
git_root: &AbsolutePath,
changed_files: &HashSet<String>,
) -> Vec<bool> {
let rel_paths: Vec<Option<String>> = projects
.iter()
.map(|p| {
p.path()
.strip_prefix(git_root.as_path())
.ok()
.map(|r| r.to_string_lossy().into_owned())
})
.collect();
let mut matched = vec![false; projects.len()];
for file in changed_files {
let candidates: Vec<(usize, usize)> = rel_paths
.iter()
.enumerate()
.filter_map(|(i, rel_opt)| {
let rel = rel_opt.as_deref()?;
if rel.is_empty() {
Some((i, 0usize))
} else if file.starts_with(rel)
&& (file.len() == rel.len() || file.as_bytes().get(rel.len()) == Some(&b'/'))
{
Some((i, rel.len()))
} else {
None
}
})
.collect();
if let Some(&(_, best_len)) = candidates.iter().max_by_key(|(_, len)| *len) {
candidates
.iter()
.filter(|(_, len)| *len == best_len)
.for_each(|(i, _)| matched[*i] = true);
}
}
matched
}
pub fn match_files_to_projects_in_scope(
projects: &[Project],
attribution_scope: &[Project],
git_root: &AbsolutePath,
changed_files: &HashSet<String>,
) -> Vec<bool> {
let scope_matched = match_files_to_projects(attribution_scope, git_root, changed_files);
let matched_paths: HashSet<&AbsolutePath> = attribution_scope
.iter()
.zip(scope_matched.iter())
.filter_map(|(p, &m)| m.then_some(p.path()))
.collect();
projects
.iter()
.map(|p| matched_paths.contains(p.path()))
.collect()
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use crate::package_manager::Project;
use crate::path::AbsolutePath;
use super::*;
#[test]
fn basic_prefix_match() {
let path = AbsolutePath::new("/repo").unwrap();
let projects = vec![
Project::new_test("a", "/repo/packages/a"),
Project::new_test("b", "/repo/packages/b"),
];
let mut files = HashSet::new();
files.insert("packages/a/src/lib.rs".to_string());
assert_eq!(
match_files_to_projects(&projects, &path, &files),
vec![true, false]
);
}
#[test]
fn no_match_for_different_project() {
let path = AbsolutePath::new("/repo").unwrap();
let projects = vec![
Project::new_test("a", "/repo/packages/a"),
Project::new_test("b", "/repo/packages/b"),
];
let mut files = HashSet::new();
files.insert("packages/b/src/lib.rs".to_string());
assert_eq!(
match_files_to_projects(&projects, &path, &files),
vec![false, true]
);
}
#[test]
fn no_prefix_match_without_separator() {
let path = AbsolutePath::new("/repo").unwrap();
let projects = vec![
Project::new_test("a", "/repo/packages/a"),
Project::new_test("a-extra", "/repo/packages/a-extra"),
];
let mut files = HashSet::new();
files.insert("packages/a-extra/lib.rs".to_string());
assert_eq!(
match_files_to_projects(&projects, &path, &files),
vec![false, true]
);
}
#[test]
fn nested_file_goes_to_child() {
let path = AbsolutePath::new("/repo").unwrap();
let projects = vec![
Project::new_test("parent", "/repo/packages/a"),
Project::new_test("child", "/repo/packages/a/sub"),
];
let mut files = HashSet::new();
files.insert("packages/a/sub/src/lib.rs".to_string());
assert_eq!(
match_files_to_projects(&projects, &path, &files),
vec![false, true]
);
}
#[test]
fn nested_parent_file_goes_to_parent() {
let path = AbsolutePath::new("/repo").unwrap();
let projects = vec![
Project::new_test("parent", "/repo/packages/a"),
Project::new_test("child", "/repo/packages/a/sub"),
];
let mut files = HashSet::new();
files.insert("packages/a/README.md".to_string());
assert_eq!(
match_files_to_projects(&projects, &path, &files),
vec![true, false]
);
}
#[test]
fn root_project_matches_unowned_file() {
let path = AbsolutePath::new("/repo").unwrap();
let projects = vec![
Project::new_test("root", "/repo"),
Project::new_test("a", "/repo/packages/a"),
];
let mut files = HashSet::new();
files.insert("src/main.rs".to_string());
assert_eq!(
match_files_to_projects(&projects, &path, &files),
vec![true, false]
);
}
#[test]
fn root_does_not_steal_from_subproject() {
let path = AbsolutePath::new("/repo").unwrap();
let projects = vec![
Project::new_test("root", "/repo"),
Project::new_test("a", "/repo/packages/a"),
];
let mut files = HashSet::new();
files.insert("packages/a/src/lib.rs".to_string());
assert_eq!(
match_files_to_projects(&projects, &path, &files),
vec![false, true]
);
}
#[test]
fn empty_files() {
let path = AbsolutePath::new("/repo").unwrap();
let projects = vec![Project::new_test("root", "/repo")];
let files = HashSet::new();
assert_eq!(
match_files_to_projects(&projects, &path, &files),
vec![false]
);
}
#[test]
fn outside_git_root_always_unchanged() {
let path = AbsolutePath::new("/repo").unwrap();
let projects = vec![Project::new_test("outside", "/other/path")];
let files = HashSet::new();
assert_eq!(
match_files_to_projects(&projects, &path, &files),
vec![false]
);
}
#[test]
fn outside_git_root_unchanged_even_with_files() {
let path = AbsolutePath::new("/repo").unwrap();
let projects = vec![
Project::new_test("outside", "/other/path"),
Project::new_test("a", "/repo/packages/a"),
];
let mut files = HashSet::new();
files.insert("packages/a/src/lib.rs".to_string());
assert_eq!(
match_files_to_projects(&projects, &path, &files),
vec![false, true]
);
}
#[test]
fn unowned_file_with_no_root() {
let path = AbsolutePath::new("/repo").unwrap();
let projects = vec![
Project::new_test("a", "/repo/packages/a"),
Project::new_test("b", "/repo/packages/b"),
];
let mut files = HashSet::new();
files.insert("other/random.txt".to_string());
assert_eq!(
match_files_to_projects(&projects, &path, &files),
vec![false, false]
);
}
#[test]
fn multiple_at_same_path_all_marked() {
let path = AbsolutePath::new("/repo").unwrap();
let projects = vec![
Project::new_test("npm-root", "/repo"),
Project::new_test("cargo-root", "/repo"),
Project::new_test("sub", "/repo/packages/sub"),
];
let mut files = HashSet::new();
files.insert("README.md".to_string());
assert_eq!(
match_files_to_projects(&projects, &path, &files),
vec![true, true, false]
);
}
#[test]
fn exact_path_length_match() {
let path = AbsolutePath::new("/repo").unwrap();
let projects = vec![Project::new_test("my-pkg", "/repo/my-pkg")];
let mut files = HashSet::new();
files.insert("my-pkg".to_string()); assert_eq!(
match_files_to_projects(&projects, &path, &files),
vec![true]
);
}
#[test]
fn in_scope_ignored_subproject_prevents_parent_attribution() {
let path = AbsolutePath::new("/repo").unwrap();
let releasable = vec![
Project::new_test("root", "/repo"),
Project::new_test("foo", "/repo/foo"),
];
let all = vec![
Project::new_test("root", "/repo"),
Project::new_test("foo", "/repo/foo"),
Project::new_test("foo-tests", "/repo/foo/tests"),
];
let mut files = HashSet::new();
files.insert("foo/tests/README.md".to_string());
assert_eq!(
match_files_to_projects_in_scope(&releasable, &all, &path, &files),
vec![false, false]
);
}
#[test]
fn in_scope_file_outside_ignored_subproject_still_attributes_parent() {
let path = AbsolutePath::new("/repo").unwrap();
let releasable = vec![
Project::new_test("root", "/repo"),
Project::new_test("foo", "/repo/foo"),
];
let all = vec![
Project::new_test("root", "/repo"),
Project::new_test("foo", "/repo/foo"),
Project::new_test("foo-tests", "/repo/foo/tests"),
];
let mut files = HashSet::new();
files.insert("foo/src/lib.rs".to_string());
assert_eq!(
match_files_to_projects_in_scope(&releasable, &all, &path, &files),
vec![false, true]
);
}
#[test]
fn in_scope_same_scope_as_projects_matches_identically() {
let path = AbsolutePath::new("/repo").unwrap();
let projects = vec![
Project::new_test("a", "/repo/packages/a"),
Project::new_test("b", "/repo/packages/b"),
];
let mut files = HashSet::new();
files.insert("packages/a/src/lib.rs".to_string());
let direct = match_files_to_projects(&projects, &path, &files);
let scoped = match_files_to_projects_in_scope(&projects, &projects, &path, &files);
assert_eq!(direct, scoped);
}
#[test]
fn multiple_at_same_path_subproject_wins() {
let path = AbsolutePath::new("/repo").unwrap();
let projects = vec![
Project::new_test("npm-root", "/repo"),
Project::new_test("cargo-root", "/repo"),
Project::new_test("sub", "/repo/packages/sub"),
];
let mut files = HashSet::new();
files.insert("packages/sub/src/lib.rs".to_string());
assert_eq!(
match_files_to_projects(&projects, &path, &files),
vec![false, false, true]
);
}
}