259 lines
7.8 KiB
Rust
259 lines
7.8 KiB
Rust
//! Worktree-manager integration tests against a real `git` binary and a
|
|
//! tmpdir-backed source repo.
|
|
|
|
mod common;
|
|
|
|
use std::path::PathBuf;
|
|
use std::process::Command;
|
|
|
|
use common::init_repo;
|
|
use maestro_daemon::worktree::WorktreeManager;
|
|
use tempfile::TempDir;
|
|
|
|
fn worktree_branches(repo: &PathBuf) -> Vec<String> {
|
|
let out = Command::new("git")
|
|
.arg("-C")
|
|
.arg(repo)
|
|
.args(["worktree", "list", "--porcelain"])
|
|
.output()
|
|
.expect("git worktree list");
|
|
assert!(out.status.success());
|
|
let s = String::from_utf8_lossy(&out.stdout);
|
|
s.lines()
|
|
.filter_map(|l| l.strip_prefix("branch refs/heads/").map(String::from))
|
|
.collect()
|
|
}
|
|
|
|
#[test]
|
|
fn add_creates_worktree_on_new_branch() {
|
|
let repo_dir = TempDir::new().unwrap();
|
|
let repo = repo_dir.path().to_path_buf();
|
|
init_repo(&repo);
|
|
|
|
let wt_root = TempDir::new().unwrap();
|
|
let mgr = WorktreeManager::new(Some(wt_root.path().to_path_buf()));
|
|
|
|
let path = mgr.add_new(&repo, "feat/example").expect("add worktree");
|
|
assert!(path.exists());
|
|
assert!(path.join("README").exists());
|
|
assert!(path.starts_with(wt_root.path()));
|
|
|
|
let branches = worktree_branches(&repo);
|
|
assert!(branches.iter().any(|b| b == "feat/example"), "{branches:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn add_places_worktree_inside_the_base_repo_by_default() {
|
|
let repo_dir = TempDir::new().unwrap();
|
|
let repo = repo_dir.path().to_path_buf();
|
|
init_repo(&repo);
|
|
|
|
// No override: the worktree must land where the repository keeps its
|
|
// own worktrees (#56), not under some daemon state dir.
|
|
let mgr = WorktreeManager::new(None);
|
|
|
|
let path = mgr.add_new(&repo, "feat/example").expect("add worktree");
|
|
assert_eq!(path, repo.join("worktrees").join("feat-example"));
|
|
assert!(path.exists());
|
|
assert!(path.join("README").exists());
|
|
|
|
let branches = worktree_branches(&repo);
|
|
assert!(branches.iter().any(|b| b == "feat/example"), "{branches:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn worktree_root_override_redirects_placement() {
|
|
let repo_dir = TempDir::new().unwrap();
|
|
let repo = repo_dir.path().to_path_buf();
|
|
init_repo(&repo);
|
|
|
|
let wt_root = TempDir::new().unwrap();
|
|
let mgr = WorktreeManager::new(Some(wt_root.path().to_path_buf()));
|
|
|
|
let path = mgr.add_new(&repo, "feat/example").expect("add worktree");
|
|
assert_eq!(path, wt_root.path().join("feat-example"));
|
|
assert!(path.exists());
|
|
}
|
|
|
|
#[test]
|
|
fn existing_directory_at_target_path_is_an_error_carrying_the_path() {
|
|
let repo_dir = TempDir::new().unwrap();
|
|
let repo = repo_dir.path().to_path_buf();
|
|
init_repo(&repo);
|
|
|
|
// A predictable path means collisions are possible; they must be
|
|
// reported, never silently relocated.
|
|
std::fs::create_dir_all(repo.join("worktrees").join("feat-collide")).unwrap();
|
|
|
|
let mgr = WorktreeManager::new(None);
|
|
let err = mgr.add_new(&repo, "feat/collide").unwrap_err();
|
|
let msg = format!("{err}");
|
|
assert!(
|
|
msg.contains(
|
|
repo.join("worktrees")
|
|
.join("feat-collide")
|
|
.to_str()
|
|
.unwrap()
|
|
),
|
|
"{msg}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn add_twice_generates_distinct_paths() {
|
|
let repo_dir = TempDir::new().unwrap();
|
|
let repo = repo_dir.path().to_path_buf();
|
|
init_repo(&repo);
|
|
|
|
let wt_root = TempDir::new().unwrap();
|
|
let mgr = WorktreeManager::new(Some(wt_root.path().to_path_buf()));
|
|
|
|
let p1 = mgr.add_new(&repo, "feat/a").expect("add a");
|
|
let p2 = mgr.add_new(&repo, "feat/b").expect("add b");
|
|
assert_ne!(p1, p2);
|
|
assert!(p1.exists());
|
|
assert!(p2.exists());
|
|
}
|
|
|
|
#[test]
|
|
fn remove_prunes_worktree() {
|
|
let repo_dir = TempDir::new().unwrap();
|
|
let repo = repo_dir.path().to_path_buf();
|
|
init_repo(&repo);
|
|
|
|
let wt_root = TempDir::new().unwrap();
|
|
let mgr = WorktreeManager::new(Some(wt_root.path().to_path_buf()));
|
|
|
|
let path = mgr.add_new(&repo, "feat/gone").expect("add");
|
|
assert!(path.exists());
|
|
|
|
mgr.remove(&repo, &path).expect("remove");
|
|
assert!(!path.exists());
|
|
|
|
let branches = worktree_branches(&repo);
|
|
// The branch itself still exists, but the worktree should be gone — only
|
|
// the base repo's main branch should show in `worktree list`.
|
|
assert_eq!(branches, vec!["main".to_string()]);
|
|
}
|
|
|
|
#[test]
|
|
fn add_existing_checks_out_branch_without_creating_it() {
|
|
let repo_dir = TempDir::new().unwrap();
|
|
let repo = repo_dir.path().to_path_buf();
|
|
init_repo(&repo);
|
|
|
|
// Pre-create the branch with its own commit so we can tell a checkout
|
|
// from an accidental `-b`.
|
|
let created = Command::new("git")
|
|
.arg("-C")
|
|
.arg(&repo)
|
|
.args(["checkout", "-q", "-b", "feat/dup"])
|
|
.status()
|
|
.unwrap();
|
|
assert!(created.success());
|
|
std::fs::write(repo.join("FEATURE"), b"dup\n").unwrap();
|
|
let staged = Command::new("git")
|
|
.arg("-C")
|
|
.arg(&repo)
|
|
.args(["add", "."])
|
|
.status()
|
|
.unwrap();
|
|
assert!(staged.success());
|
|
let committed = Command::new("git")
|
|
.arg("-C")
|
|
.arg(&repo)
|
|
.args(["commit", "-q", "-am", "wip"])
|
|
.status()
|
|
.unwrap();
|
|
assert!(committed.success());
|
|
let back = Command::new("git")
|
|
.arg("-C")
|
|
.arg(&repo)
|
|
.args(["checkout", "-q", "main"])
|
|
.status()
|
|
.unwrap();
|
|
assert!(back.success());
|
|
|
|
let wt_root = TempDir::new().unwrap();
|
|
let mgr = WorktreeManager::new(Some(wt_root.path().to_path_buf()));
|
|
|
|
let path = mgr.add_existing(&repo, "feat/dup").expect("add existing");
|
|
assert!(path.exists());
|
|
assert!(path.join("README").exists());
|
|
|
|
// The branch set is unchanged: `feat/dup` was checked out, not recreated.
|
|
let branches = worktree_branches(&repo);
|
|
let mut sorted = branches.clone();
|
|
sorted.sort();
|
|
assert_eq!(
|
|
sorted,
|
|
vec!["feat/dup".to_string(), "main".to_string()],
|
|
"{branches:?}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn remove_refuses_worktree_with_uncommitted_changes() {
|
|
let repo_dir = TempDir::new().unwrap();
|
|
let repo = repo_dir.path().to_path_buf();
|
|
init_repo(&repo);
|
|
|
|
let mgr = WorktreeManager::new(None);
|
|
let path = mgr.add_new(&repo, "feat/dirty").expect("add");
|
|
std::fs::write(path.join("README"), b"unsaved edits\n").unwrap();
|
|
|
|
mgr.remove(&repo, &path)
|
|
.expect_err("dirty worktree must be refused");
|
|
assert!(path.exists(), "guarded worktree must stay on disk");
|
|
|
|
// Committing the change doesn't help while it is the only copy…
|
|
run_git(&path, &["add", "."]);
|
|
run_git(&path, &["commit", "-q", "-m", "unsaved"]);
|
|
|
|
mgr.remove(&repo, &path)
|
|
.expect_err("unique commit must be refused");
|
|
assert!(path.exists());
|
|
|
|
// …but once the commit is reachable from main, removal is safe.
|
|
run_git(&repo, &["merge", "-q", "--no-edit", "feat/dirty"]);
|
|
mgr.remove(&repo, &path).expect("merged worktree can go");
|
|
assert!(!path.exists());
|
|
}
|
|
|
|
fn run_git(dir: &std::path::Path, args: &[&str]) {
|
|
let out = Command::new("git")
|
|
.arg("-C")
|
|
.arg(dir)
|
|
.args(args)
|
|
.output()
|
|
.expect("git spawn");
|
|
assert!(
|
|
out.status.success(),
|
|
"git {args:?} failed: {}",
|
|
String::from_utf8_lossy(&out.stderr)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn add_fails_when_branch_already_exists() {
|
|
let repo_dir = TempDir::new().unwrap();
|
|
let repo = repo_dir.path().to_path_buf();
|
|
init_repo(&repo);
|
|
|
|
// pre-create the branch
|
|
let status = Command::new("git")
|
|
.arg("-C")
|
|
.arg(&repo)
|
|
.args(["branch", "feat/dup"])
|
|
.status()
|
|
.unwrap();
|
|
assert!(status.success());
|
|
|
|
let wt_root = TempDir::new().unwrap();
|
|
let mgr = WorktreeManager::new(Some(wt_root.path().to_path_buf()));
|
|
|
|
let err = mgr.add_new(&repo, "feat/dup").unwrap_err();
|
|
let msg = format!("{err}");
|
|
assert!(msg.contains("git exited"), "{msg}");
|
|
}
|