use git2::{Repository, Signature};
use gwm::workspace;
use std::fs;
use std::path::Path;
use tempfile::TempDir;
fn init_repo_at(path: &Path) -> Repository {
fs::create_dir_all(path).unwrap();
let repo = Repository::init(path).unwrap();
repo.set_head("refs/heads/main").ok();
let sig = Signature::now("gwm-test", "gwm@test").unwrap();
let tree_id = {
let mut index = repo.index().unwrap();
index.write_tree().unwrap()
};
let tree = repo.find_tree(tree_id).unwrap();
repo.commit(Some("HEAD"), &sig, &sig, "init", &tree, &[]).unwrap();
Repository::open(path).unwrap()
}
#[test]
fn discover_finds_child_repos_and_ignores_non_repos() {
let root = TempDir::new().unwrap();
init_repo_at(&root.path().join("alpha"));
init_repo_at(&root.path().join("beta"));
fs::create_dir_all(root.path().join("notes")).unwrap();
fs::write(root.path().join("README.md"), "hi").unwrap();
let ws = workspace::discover(root.path()).unwrap();
let names: Vec<&str> = ws.repos.iter().map(|r| r.name.as_str()).collect();
assert_eq!(names, vec!["alpha", "beta"], "only the two git repos, got {names:?}");
}
#[test]
fn discover_sorts_repos_alphabetically() {
let root = TempDir::new().unwrap();
init_repo_at(&root.path().join("zulu"));
init_repo_at(&root.path().join("mike"));
init_repo_at(&root.path().join("alfa"));
let ws = workspace::discover(root.path()).unwrap();
let names: Vec<&str> = ws.repos.iter().map(|r| r.name.as_str()).collect();
assert_eq!(names, vec!["alfa", "mike", "zulu"], "alphabetical, got {names:?}");
}
#[test]
fn discover_does_not_recurse_below_one_level() {
let root = TempDir::new().unwrap();
init_repo_at(&root.path().join("alpha"));
init_repo_at(&root.path().join("group").join("nested"));
let ws = workspace::discover(root.path()).unwrap();
let names: Vec<&str> = ws.repos.iter().map(|r| r.name.as_str()).collect();
assert_eq!(names, vec!["alpha"], "nested repo excluded, got {names:?}");
}
#[test]
fn discover_skips_a_linked_worktree_sibling() {
let root = TempDir::new().unwrap();
let main = init_repo_at(&root.path().join("main"));
main
.worktree("wt", &root.path().join("wt"), None)
.expect("create linked worktree");
let ws = workspace::discover(root.path()).unwrap();
let names: Vec<&str> = ws.repos.iter().map(|r| r.name.as_str()).collect();
assert_eq!(
names,
vec!["main"],
"the linked worktree sibling is skipped, got {names:?}"
);
}
#[test]
fn discover_includes_an_orphan_linked_worktree_via_its_owner() {
let owner_dir = TempDir::new().unwrap();
let owner = init_repo_at(owner_dir.path());
let root = TempDir::new().unwrap();
owner
.worktree("wt", &root.path().join("wt"), None)
.expect("create linked worktree under the root");
let ws = workspace::discover(root.path()).unwrap();
assert_eq!(
ws.repos.len(),
1,
"the orphan worktree resolves to one owner repo, got {:?}",
ws.repos
);
let got = ws.repos[0]
.path
.canonicalize()
.unwrap_or_else(|_| ws.repos[0].path.clone());
let want = owner_dir
.path()
.canonicalize()
.unwrap_or_else(|_| owner_dir.path().to_path_buf());
assert_eq!(got, want, "the workspace repo is the owner checkout");
}
#[test]
fn discover_disambiguates_duplicate_repo_names() {
let owner_a = TempDir::new().unwrap();
let owner_b = TempDir::new().unwrap();
let a = init_repo_at(&owner_a.path().join("main"));
let b = init_repo_at(&owner_b.path().join("main"));
let root = TempDir::new().unwrap();
a.worktree("wt-a", &root.path().join("wt-a"), None).unwrap();
b.worktree("wt-b", &root.path().join("wt-b"), None).unwrap();
let ws = workspace::discover(root.path()).unwrap();
assert_eq!(ws.repos.len(), 2, "two distinct owners, got {:?}", ws.repos);
assert!(ws.repos[0].path < ws.repos[1].path, "stable path tie-break order");
assert_eq!(ws.repos[0].name, "main", "smaller-path owner keeps the bare name");
assert_eq!(
ws.repos[1].name, "main-2",
"the collision is suffixed deterministically"
);
}
#[test]
fn discover_skips_the_root_repos_own_git_dir() {
let root = TempDir::new().unwrap();
init_repo_at(root.path()); init_repo_at(&root.path().join("kid"));
let ws = workspace::discover(root.path()).unwrap();
let names: Vec<&str> = ws.repos.iter().map(|r| r.name.as_str()).collect();
assert_eq!(names, vec!["kid"], "only the real child repo, no `.git`, got {names:?}");
}
#[test]
fn discover_empty_root_yields_no_repos() {
let root = TempDir::new().unwrap();
fs::create_dir_all(root.path().join("plain")).unwrap();
let ws = workspace::discover(root.path()).unwrap();
assert!(ws.repos.is_empty(), "no git repos under the root");
assert!(ws.is_empty(), "is_empty() reflects an empty repo set");
}
#[test]
fn discover_missing_root_is_an_error() {
let root = TempDir::new().unwrap();
let missing = root.path().join("does-not-exist");
assert!(workspace::discover(&missing).is_err(), "a missing root must error");
}
#[test]
fn autodetect_fires_when_cwd_is_not_a_repo_but_holds_child_repos() {
let root = TempDir::new().unwrap();
init_repo_at(&root.path().join("alpha"));
init_repo_at(&root.path().join("beta"));
let ws = workspace::autodetect(root.path()).expect("a repo-free dir with child repos triggers");
let names: Vec<&str> = ws.repos.iter().map(|r| r.name.as_str()).collect();
assert_eq!(
names,
vec!["alpha", "beta"],
"auto-detected workspace lists the children"
);
}
#[test]
fn autodetect_declines_when_cwd_is_itself_a_repo() {
let root = TempDir::new().unwrap();
init_repo_at(root.path());
init_repo_at(&root.path().join("vendored"));
assert!(
workspace::autodetect(root.path()).is_none(),
"a directory that is itself a repo must not auto-open as a workspace"
);
}
#[test]
fn autodetect_declines_when_no_child_repos() {
let root = TempDir::new().unwrap();
fs::create_dir_all(root.path().join("plain")).unwrap();
assert!(
workspace::autodetect(root.path()).is_none(),
"no child repos → nothing to open as a workspace"
);
}
#[test]
fn merge_worktrees_tags_each_row_with_its_repo() {
let root = TempDir::new().unwrap();
init_repo_at(&root.path().join("alpha"));
init_repo_at(&root.path().join("beta"));
let ws = workspace::discover(root.path()).unwrap();
let rows = workspace::merge_worktrees(&ws).unwrap();
let repo_names: Vec<&str> = rows.iter().map(|r| r.repo_name.as_str()).collect();
assert!(repo_names.contains(&"alpha"), "alpha rows present: {repo_names:?}");
assert!(repo_names.contains(&"beta"), "beta rows present: {repo_names:?}");
let alpha_main = rows
.iter()
.find(|r| r.repo_name == "alpha" && r.info.is_main)
.expect("alpha main worktree row");
assert!(
alpha_main.info.path.ends_with("alpha"),
"main worktree path points at the repo dir, got {:?}",
alpha_main.info.path
);
let first_beta = repo_names.iter().position(|n| *n == "beta").unwrap();
let last_alpha = repo_names.iter().rposition(|n| *n == "alpha").unwrap();
assert!(last_alpha < first_beta, "alpha rows precede beta rows: {repo_names:?}");
}