#![cfg_attr(not(test), deny(clippy::unwrap_used))]
use serde::Serialize;
use std::fs;
use std::path::Path;
#[derive(Debug, Clone, Serialize, Default)]
pub struct RepoShapeInfo {
pub repo_shape: String,
pub has_workspace: bool,
pub workspace_member_count: u32,
pub buildable_roots: u32,
pub language_count: u32,
pub primary_language_ratio: f64,
}
pub fn detect_repo_shape(path: &Path) -> RepoShapeInfo {
let has_workspace = check_workspace_markers(path);
let buildable_roots = count_buildable_roots(path);
let repo_shape = classify(has_workspace, buildable_roots);
RepoShapeInfo {
repo_shape,
has_workspace,
workspace_member_count: 0, buildable_roots,
language_count: 0,
primary_language_ratio: 0.0,
}
}
fn check_workspace_markers(root: &Path) -> bool {
if has_cargo_workspace(root) {
return true;
}
if root.join("pnpm-workspace.yaml").is_file() {
return true;
}
if root.join("lerna.json").is_file() {
return true;
}
if root.join("go.work").is_file() {
return true;
}
false
}
fn has_cargo_workspace(dir: &Path) -> bool {
let cargo_toml = dir.join("Cargo.toml");
if !cargo_toml.is_file() {
return false;
}
fs::read_to_string(&cargo_toml)
.map(|contents| contents.contains("[workspace]"))
.unwrap_or(false)
}
fn count_buildable_roots(root: &Path) -> u32 {
let mut count = 0u32;
if is_buildable_root(root) {
count += 1;
}
let read_dir = match fs::read_dir(root) {
Ok(rd) => rd,
Err(_) => return count,
};
for entry in read_dir.flatten() {
let child = entry.path();
if !child.is_dir() {
continue;
}
if is_buildable_root(&child) {
count += 1;
}
}
count
}
fn is_buildable_root(dir: &Path) -> bool {
let cargo = dir.join("Cargo.toml");
if cargo.is_file() {
if let Ok(contents) = fs::read_to_string(&cargo) {
if contents.contains("[package]") {
return true;
}
}
}
let pkg_json = dir.join("package.json");
if pkg_json.is_file() {
if let Ok(contents) = fs::read_to_string(&pkg_json) {
if contents.contains("\"scripts\"") {
return true;
}
}
}
if dir.join("go.mod").is_file() {
return true;
}
let pyproject = dir.join("pyproject.toml");
if pyproject.is_file() {
if let Ok(contents) = fs::read_to_string(&pyproject) {
if contents.contains("[build-system]") || contents.contains("[project]") {
return true;
}
}
}
false
}
fn classify(has_workspace: bool, buildable_roots: u32) -> String {
if buildable_roots >= 3 {
"monorepo".to_string()
} else if has_workspace && buildable_roots < 3 {
"workspace".to_string()
} else if buildable_roots >= 2 && !has_workspace {
"multi-package".to_string()
} else {
"single-package".to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_single_package_default() {
let dir = TempDir::new().expect("create temp dir");
fs::write(dir.path().join("Cargo.toml"), "[package]\nname = \"foo\"").expect("write");
let shape = detect_repo_shape(dir.path());
assert_eq!(shape.repo_shape, "single-package");
assert!(!shape.has_workspace);
assert_eq!(shape.buildable_roots, 1);
}
#[test]
fn test_workspace_detected() {
let dir = TempDir::new().expect("create temp dir");
fs::write(
dir.path().join("Cargo.toml"),
"[workspace]\nmembers = [\"a\", \"b\"]",
)
.expect("write");
fs::create_dir_all(dir.path().join("a")).expect("mkdir");
fs::write(dir.path().join("a/Cargo.toml"), "[package]\nname = \"a\"").expect("write");
let shape = detect_repo_shape(dir.path());
assert_eq!(shape.repo_shape, "workspace");
assert!(shape.has_workspace);
}
#[test]
fn test_monorepo_by_buildable_roots() {
let dir = TempDir::new().expect("create temp dir");
for name in ["svc-a", "svc-b", "svc-c"] {
let p = dir.path().join(name);
fs::create_dir_all(&p).expect("mkdir");
fs::write(p.join("package.json"), r#"{"scripts":{"build":"tsc"}}"#).expect("write");
}
let shape = detect_repo_shape(dir.path());
assert_eq!(shape.repo_shape, "monorepo");
assert_eq!(shape.buildable_roots, 3);
}
#[test]
fn test_empty_dir() {
let dir = TempDir::new().expect("create temp dir");
let shape = detect_repo_shape(dir.path());
assert_eq!(shape.repo_shape, "single-package");
assert_eq!(shape.buildable_roots, 0);
}
#[test]
fn test_multi_package_no_workspace() {
let dir = TempDir::new().expect("create temp dir");
for name in ["svc-a", "svc-b"] {
let p = dir.path().join(name);
fs::create_dir_all(&p).expect("mkdir");
fs::write(p.join("go.mod"), "module example.com/svc").expect("write");
}
let shape = detect_repo_shape(dir.path());
assert_eq!(shape.repo_shape, "multi-package");
assert!(!shape.has_workspace);
assert_eq!(shape.buildable_roots, 2);
}
#[test]
fn test_pnpm_workspace_marker() {
let dir = TempDir::new().expect("create temp dir");
fs::write(dir.path().join("pnpm-workspace.yaml"), "packages:\n - 'packages/*'")
.expect("write");
let shape = detect_repo_shape(dir.path());
assert!(shape.has_workspace);
}
#[test]
fn test_lerna_workspace_marker() {
let dir = TempDir::new().expect("create temp dir");
fs::write(dir.path().join("lerna.json"), r#"{"version":"1.0.0"}"#).expect("write");
let shape = detect_repo_shape(dir.path());
assert!(shape.has_workspace);
}
#[test]
fn test_go_work_marker() {
let dir = TempDir::new().expect("create temp dir");
fs::write(dir.path().join("go.work"), "go 1.21\n").expect("write");
let shape = detect_repo_shape(dir.path());
assert!(shape.has_workspace);
}
#[test]
fn test_pyproject_buildable_root() {
let dir = TempDir::new().expect("create temp dir");
fs::write(
dir.path().join("pyproject.toml"),
"[build-system]\nrequires = [\"setuptools\"]",
)
.expect("write");
let shape = detect_repo_shape(dir.path());
assert_eq!(shape.buildable_roots, 1);
}
#[test]
fn test_defaults_language_fields() {
let dir = TempDir::new().expect("create temp dir");
let shape = detect_repo_shape(dir.path());
assert_eq!(shape.language_count, 0);
assert_eq!(shape.primary_language_ratio, 0.0);
}
#[test]
fn test_classify_workspace_takes_priority_over_multi_package() {
let dir = TempDir::new().expect("create temp dir");
fs::write(dir.path().join("go.work"), "go 1.21\n").expect("write");
for name in ["svc-a", "svc-b"] {
let p = dir.path().join(name);
fs::create_dir_all(&p).expect("mkdir");
fs::write(p.join("go.mod"), "module example.com/svc").expect("write");
}
let shape = detect_repo_shape(dir.path());
assert_eq!(shape.repo_shape, "workspace");
assert!(shape.has_workspace);
}
}