use std::cmp::Ordering;
use std::cmp::Reverse;
use std::collections::HashMap;
use std::collections::HashSet;
use std::path::Component;
use std::path::Path;
use std::path::PathBuf;
use itertools::Itertools;
use toml::Table;
use toml::Value;
use walkdir::WalkDir;
use crate::constants::CARGO_TOML;
use crate::project::AbsolutePath;
use crate::project::CargoParseResult;
use crate::project::MemberGroup;
use crate::project::Package;
use crate::project::ProjectFields;
use crate::project::RootItem;
use crate::project::RustInfo;
use crate::project::RustProject;
use crate::project::VendoredPackage;
use crate::project::WorktreeGroup;
mod build;
mod dependencies;
mod vendored;
mod workspace;
mod worktrees;
pub(crate) use build::build_tree;
pub(crate) use build::cargo_project_to_item;
pub(crate) use build::dir_size;
use dependencies::package_path_dependencies;
use dependencies::workspace_path_dependencies;
use vendored::extract_vendored_new;
pub(crate) use workspace::normalize_workspace_path;
use workspace::workspace_member_paths_new;
pub(super) fn merge_worktrees_new(items: &mut Vec<RootItem>) {
worktrees::merge_worktrees_new(items);
}
#[cfg(test)]
#[allow(
clippy::expect_used,
clippy::unreachable,
reason = "tests should panic on unexpected values"
)]
mod tests {
use super::*;
use crate::project::Workspace;
use crate::project::WorktreeStatus;
fn status_for(is_linked_worktree: bool, primary_abs: Option<&str>) -> WorktreeStatus {
match (is_linked_worktree, primary_abs) {
(_, None) => WorktreeStatus::NotGit,
(true, Some(p)) => WorktreeStatus::Linked {
primary: AbsolutePath::from(p.to_string()),
},
(false, Some(p)) => WorktreeStatus::Primary {
root: AbsolutePath::from(p.to_string()),
},
}
}
fn make_workspace(
name: Option<&str>,
abs_path: &str,
is_linked_worktree: bool,
primary_abs: Option<&str>,
) -> RootItem {
RootItem::Rust(RustProject::Workspace(Workspace {
path: AbsolutePath::from(abs_path),
name: name.map(String::from),
worktree_status: status_for(is_linked_worktree, primary_abs),
..Workspace::default()
}))
}
fn make_package(
name: Option<&str>,
abs_path: &str,
is_linked_worktree: bool,
primary_abs: Option<&str>,
) -> RootItem {
RootItem::Rust(RustProject::Package(Package {
path: AbsolutePath::from(abs_path),
name: name.map(String::from),
worktree_status: status_for(is_linked_worktree, primary_abs),
..Package::default()
}))
}
#[test]
fn merge_virtual_workspace() {
let primary = make_workspace(None, "/home/ws", false, Some("/home/ws"));
let worktree = make_workspace(None, "/home/ws_feat", true, Some("/home/ws"));
let mut items = vec![primary, worktree];
merge_worktrees_new(&mut items);
assert_eq!(items.len(), 1, "worktree should be merged into primary");
let RootItem::Worktrees(group) = &items[0] else {
unreachable!("merged workspace should produce a worktree group")
};
assert!(
matches!(&group.primary, RustProject::Workspace(_)),
"primary should be a workspace"
);
assert_eq!(group.linked.len(), 1, "should have one linked worktree");
}
#[test]
fn merge_named_workspace() {
let primary = make_workspace(Some("my-ws"), "/home/ws", false, Some("/home/ws"));
let worktree = make_workspace(Some("my-ws"), "/home/ws_feat", true, Some("/home/ws"));
let mut items = vec![primary, worktree];
merge_worktrees_new(&mut items);
assert_eq!(items.len(), 1);
let RootItem::Worktrees(group) = &items[0] else {
unreachable!("merged named workspace should produce a worktree group")
};
assert!(
matches!(&group.primary, RustProject::Workspace(_)),
"primary should be a workspace"
);
assert_eq!(group.linked.len(), 1);
}
#[test]
fn build_tree_only_nests_manifest_members() {
let tmp = tempfile::tempdir().expect("create manifest-member test tempdir");
let workspace_dir = tmp.path().join("hana");
let included_dir = workspace_dir.join("crates").join("hana");
let vendored_dir = workspace_dir.join("crates").join("clay-layout");
std::fs::create_dir_all(&included_dir).expect("create included workspace member directory");
std::fs::create_dir_all(&vendored_dir).expect("create vendored crate directory");
std::fs::write(
workspace_dir.join("Cargo.toml"),
"[workspace]\nmembers = [\"crates/hana\"]\n",
)
.expect("write workspace manifest fixture");
let workspace = make_workspace(Some("hana"), &workspace_dir.to_string_lossy(), false, None);
let included = make_package(
Some("hana-node-api"),
&included_dir.to_string_lossy(),
false,
None,
);
let vendored = make_package(
Some("clay-layout"),
&vendored_dir.to_string_lossy(),
false,
None,
);
let items = build_tree(&[workspace, included, vendored], &["crates".to_string()]);
let ws_item = items
.iter()
.find(|item| item.path() == workspace_dir.as_path())
.expect("find workspace root item");
let RootItem::Rust(RustProject::Workspace(ws)) = ws_item else {
unreachable!("workspace root item should be a workspace")
};
assert_eq!(ws.groups().len(), 1);
assert_eq!(ws.groups()[0].members().len(), 1);
assert_eq!(ws.groups()[0].members()[0].path(), included_dir.as_path());
assert!(
ws.groups()
.iter()
.flat_map(|group| group.members().iter())
.all(|member| member.path() != vendored_dir.as_path()),
"non-member crate should not be grouped as a workspace member"
);
assert_eq!(ws.vendored().len(), 1);
assert_eq!(ws.vendored()[0].path(), vendored_dir.as_path());
}
#[test]
fn build_tree_assigns_workspace_path_dependency_to_member() {
let tmp = tempfile::tempdir().expect("create path-dependency test tempdir");
let workspace_dir = tmp.path().join("bevy_hana");
let member_dir = workspace_dir.join("crates").join("bevy_diegetic");
let sibling_dir = workspace_dir.join("crates").join("bevy_lagrange");
let vendored_dir = workspace_dir.join("vendor").join("clay-layout");
std::fs::create_dir_all(&member_dir).expect("create workspace member directory");
std::fs::create_dir_all(&sibling_dir).expect("create sibling workspace member directory");
std::fs::create_dir_all(&vendored_dir).expect("create vendored dependency directory");
std::fs::write(
workspace_dir.join("Cargo.toml"),
"[workspace]\n\
members = [\"crates/*\"]\n\
exclude = [\"vendor/clay-layout\"]\n\
\n\
[workspace.dependencies]\n\
clay-layout = { path = \"vendor/clay-layout\" }\n",
)
.expect("write workspace dependency manifest fixture");
std::fs::write(
member_dir.join("Cargo.toml"),
"[package]\n\
name = \"bevy_diegetic\"\n\
version = \"0.1.0\"\n\
\n\
[dev-dependencies]\n\
clay-layout = { workspace = true }\n",
)
.expect("write member manifest fixture");
std::fs::write(
sibling_dir.join("Cargo.toml"),
"[package]\nname = \"bevy_lagrange\"\nversion = \"0.1.0\"\n",
)
.expect("write sibling manifest fixture");
let workspace = make_workspace(
Some("bevy_hana"),
&workspace_dir.to_string_lossy(),
false,
None,
);
let member = make_package(
Some("bevy_diegetic"),
&member_dir.to_string_lossy(),
false,
None,
);
let sibling = make_package(
Some("bevy_lagrange"),
&sibling_dir.to_string_lossy(),
false,
None,
);
let vendored = make_package(
Some("clay-layout"),
&vendored_dir.to_string_lossy(),
false,
None,
);
let items = build_tree(
&[workspace, member, sibling, vendored],
&["crates".to_string()],
);
let RootItem::Rust(RustProject::Workspace(ws)) = &items[0] else {
unreachable!("tree root should be a workspace")
};
assert!(ws.vendored().is_empty());
let member = ws.groups()[0]
.members()
.iter()
.find(|member| member.path() == member_dir.as_path())
.expect("find workspace member with path dependency");
assert_eq!(member.vendored().len(), 1);
assert_eq!(member.vendored()[0].path(), vendored_dir.as_path());
}
#[test]
fn merge_standalone_project() {
let primary = make_package(Some("app"), "/home/app", false, Some("/home/app"));
let worktree = make_package(Some("app"), "/home/app_feat", true, Some("/home/app"));
let mut items = vec![primary, worktree];
merge_worktrees_new(&mut items);
assert_eq!(items.len(), 1);
let RootItem::Worktrees(group) = &items[0] else {
unreachable!("merged package should produce a worktree group")
};
assert!(
matches!(&group.primary, RustProject::Package(_)),
"primary should be a package"
);
assert_eq!(group.linked.len(), 1);
}
#[test]
fn no_merge_different_repos() {
let a = make_package(Some("a"), "/home/a", false, Some("/home/a"));
let b = make_package(Some("b"), "/home/b", true, Some("/home/b"));
let mut items = vec![a, b];
merge_worktrees_new(&mut items);
assert_eq!(items.len(), 2, "different repos should remain separate");
}
#[test]
fn no_merge_none_identity() {
let a = make_package(Some("x"), "/home/x", false, None);
let b = make_package(Some("x"), "/home/x2", true, None);
let mut items = vec![a, b];
merge_worktrees_new(&mut items);
assert_eq!(
items.len(),
2,
"nodes without identity should not be merged"
);
}
}