use std::io::Write as _;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use rand::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, thiserror::Error)]
pub enum WorktreeError {
#[error("not a git repository")]
NotGitRepo,
#[error("branch '{0}' already exists")]
BranchExists(String),
#[error("worktree has uncommitted changes")]
DirtyWorkingTree,
#[error("worktree has untracked files")]
UntrackedFiles,
#[error("cannot land from the main worktree")]
IsMainWorktree,
#[error("detached HEAD state")]
DetachedHead,
#[error("rebase conflict in: {0:?}")]
RebaseConflict(Vec<String>),
#[error("fast-forward failed — main has diverged")]
FastForwardFailed,
#[error("git command failed: {0}")]
GitCommand(String),
}
pub struct SpawnOptions<'a> {
pub repo_path: &'a Path,
pub branch: Option<&'a str>,
pub base_path: &'a Path,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SpawnResult {
pub worktree_path: PathBuf,
pub branch: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct LandResult {
pub branch: String,
pub main_branch: String,
}
const ADJECTIVES: &[&str] = &[
"swift", "quick", "bright", "calm", "clever", "cool", "crisp", "eager", "fast", "fresh",
"keen", "light", "neat", "prime", "sharp", "silent", "smooth", "steady", "warm", "bold",
"brave", "clear", "fleet", "golden", "agile", "nimble", "rapid", "blazing", "cosmic",
];
const NOUNS: &[&str] = &[
"fox", "wolf", "bear", "hawk", "lion", "tiger", "raven", "eagle", "falcon", "otter", "cedar",
"maple", "oak", "pine", "willow", "river", "stream", "brook", "delta", "canyon", "spark",
"flame", "ember", "comet", "meteor", "nova", "pulse", "wave", "drift", "glow",
];
fn path_str(path: &Path) -> Result<&str, WorktreeError> {
path.to_str()
.ok_or_else(|| WorktreeError::GitCommand("path is not valid UTF-8".into()))
}
fn git(dir: &Path, args: &[&str]) -> Result<String, WorktreeError> {
let output = Command::new("git")
.arg("-C")
.arg(dir)
.args(args)
.output()
.map_err(|e| WorktreeError::GitCommand(format!("failed to run git: {e}")))?;
if output.status.success() {
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
Err(WorktreeError::GitCommand(format!(
"git {} failed: {}",
args.join(" "),
stderr.trim()
)))
}
}
fn git_status(dir: &Path, args: &[&str]) -> Result<bool, WorktreeError> {
let status = Command::new("git")
.arg("-C")
.arg(dir)
.args(args)
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map_err(|e| WorktreeError::GitCommand(format!("failed to run git: {e}")))?;
Ok(status.success())
}
fn find_main_worktree(repo: &Path) -> Result<(PathBuf, String), WorktreeError> {
let output = git(repo, &["worktree", "list", "--porcelain"])?;
let mut path = None;
let mut branch = None;
for line in output.lines() {
if path.is_none()
&& let Some(p) = line.strip_prefix("worktree ")
{
path = Some(PathBuf::from(p));
}
if branch.is_none()
&& let Some(b) = line.strip_prefix("branch refs/heads/")
{
branch = Some(b.to_string());
}
if line.is_empty() {
break; }
}
match (path, branch) {
(Some(p), Some(b)) => Ok((p, b)),
_ => Err(WorktreeError::GitCommand(
"could not parse worktree list output".into(),
)),
}
}
fn generate_branch_name() -> String {
let mut rng = rand::rng();
let adj = ADJECTIVES.choose(&mut rng).copied().unwrap_or("swift");
let noun = NOUNS.choose(&mut rng).copied().unwrap_or("fox");
let num: u32 = rng.random_range(0..100);
format!("{adj}-{noun}-{num}")
}
pub struct WorktreeEntry {
pub path: PathBuf,
pub branch: Option<String>,
pub is_main: bool,
}
pub fn list_worktrees(repo_path: &Path) -> Result<Vec<WorktreeEntry>, WorktreeError> {
let output = git(repo_path, &["worktree", "list", "--porcelain"])?;
let mut entries = Vec::new();
let mut current_path = None;
let mut current_branch = None;
for line in output.lines() {
if let Some(p) = line.strip_prefix("worktree ") {
current_path = Some(PathBuf::from(p));
} else if let Some(b) = line.strip_prefix("branch refs/heads/") {
current_branch = Some(b.to_string());
} else if line.is_empty() {
if let Some(path) = current_path.take() {
let is_main = entries.is_empty();
entries.push(WorktreeEntry {
path,
branch: current_branch.take(),
is_main,
});
}
current_branch = None;
}
}
if let Some(path) = current_path {
let is_main = entries.is_empty();
entries.push(WorktreeEntry {
path,
branch: current_branch,
is_main,
});
}
Ok(entries)
}
pub fn spawn(options: &SpawnOptions<'_>) -> Result<SpawnResult, WorktreeError> {
if !git_status(options.repo_path, &["rev-parse", "--git-dir"])? {
return Err(WorktreeError::NotGitRepo);
}
let branch = match options.branch {
Some(b) => b.to_string(),
None => generate_branch_name(),
};
if git_status(
options.repo_path,
&[
"show-ref",
"--verify",
"--quiet",
&format!("refs/heads/{branch}"),
],
)? {
return Err(WorktreeError::BranchExists(branch));
}
let (main_path, _) = find_main_worktree(options.repo_path)?;
let project = main_path
.file_name()
.and_then(|n| n.to_str())
.ok_or_else(|| WorktreeError::GitCommand("could not determine project name".into()))?;
let worktree_path = options.base_path.join(project).join(&branch);
std::fs::create_dir_all(options.base_path.join(project))
.map_err(|e| WorktreeError::GitCommand(format!("failed to create directory: {e}")))?;
let wt_str = path_str(&worktree_path)?;
git(&main_path, &["worktree", "add", "-b", &branch, wt_str])?;
rsync_ignored(&main_path, &worktree_path)?;
Ok(SpawnResult {
worktree_path,
branch,
})
}
pub fn land(worktree_path: &Path) -> Result<LandResult, WorktreeError> {
let (main_path, main_branch) = find_main_worktree(worktree_path)?;
let toplevel = git(worktree_path, &["rev-parse", "--show-toplevel"])?;
if main_path == Path::new(toplevel.trim()) {
return Err(WorktreeError::IsMainWorktree);
}
let current_branch = git(worktree_path, &["rev-parse", "--abbrev-ref", "HEAD"])?;
let current_branch = current_branch.trim().to_string();
if current_branch == "HEAD" {
return Err(WorktreeError::DetachedHead);
}
if !git_status(worktree_path, &["diff", "--quiet"])? {
return Err(WorktreeError::DirtyWorkingTree);
}
if !git_status(worktree_path, &["diff", "--cached", "--quiet"])? {
return Err(WorktreeError::DirtyWorkingTree);
}
let untracked = git(
worktree_path,
&["ls-files", "--others", "--exclude-standard"],
)?;
if !untracked.trim().is_empty() {
return Err(WorktreeError::UntrackedFiles);
}
let rebase_output = Command::new("git")
.arg("-C")
.arg(worktree_path)
.args(["rebase", &main_branch])
.output()
.map_err(|e| WorktreeError::GitCommand(format!("failed to run git: {e}")))?;
if !rebase_output.status.success() {
let conflicts =
git(worktree_path, &["diff", "--name-only", "--diff-filter=U"]).unwrap_or_default();
let conflict_files: Vec<String> = conflicts
.lines()
.filter(|l| !l.is_empty())
.map(String::from)
.collect();
if conflict_files.is_empty() {
let stderr = String::from_utf8_lossy(&rebase_output.stderr);
return Err(WorktreeError::GitCommand(format!(
"rebase failed: {}",
stderr.trim()
)));
}
return Err(WorktreeError::RebaseConflict(conflict_files));
}
if !git_status(&main_path, &["merge", "--ff-only", ¤t_branch])? {
return Err(WorktreeError::FastForwardFailed);
}
Ok(LandResult {
branch: current_branch,
main_branch,
})
}
pub fn remove(worktree_path: &Path) -> Result<(), WorktreeError> {
let branch = git(worktree_path, &["rev-parse", "--abbrev-ref", "HEAD"])?;
let branch = branch.trim().to_string();
let (main_path, _) = find_main_worktree(worktree_path)?;
let wt_str = path_str(worktree_path)?;
git(&main_path, &["worktree", "remove", wt_str])?;
let _ = git(&main_path, &["branch", "-d", &branch]);
Ok(())
}
pub fn sync_to_main(worktree_path: &Path) -> Result<(), WorktreeError> {
let (_, main_branch) = find_main_worktree(worktree_path)?;
git(worktree_path, &["rebase", &main_branch])?;
Ok(())
}
pub fn reset_to_main(worktree_path: &Path) -> Result<(), WorktreeError> {
let (_, main_branch) = find_main_worktree(worktree_path)?;
git(worktree_path, &["reset", "--hard", &main_branch])?;
Ok(())
}
pub fn abort_rebase(worktree_path: &Path) -> Result<(), WorktreeError> {
git(worktree_path, &["rebase", "--abort"])?;
Ok(())
}
pub fn clean(worktree_path: &Path) -> Result<(), WorktreeError> {
git(worktree_path, &["clean", "-fd"])?;
Ok(())
}
pub fn has_unique_commits(worktree_path: &Path) -> Result<bool, WorktreeError> {
let (_, main_branch) = find_main_worktree(worktree_path)?;
let output = git(
worktree_path,
&["rev-list", "--count", &format!("{main_branch}..HEAD")],
)?;
let count: u64 = output
.trim()
.parse()
.map_err(|e| WorktreeError::GitCommand(format!("failed to parse rev-list count: {e}")))?;
Ok(count > 0)
}
pub fn is_rebase_in_progress(worktree_path: &Path) -> Result<bool, WorktreeError> {
let git_dir_output = git(worktree_path, &["rev-parse", "--git-dir"])?;
let git_dir = PathBuf::from(git_dir_output.trim());
Ok(git_dir.join("rebase-merge").exists() || git_dir.join("rebase-apply").exists())
}
pub fn ff_merge_main(worktree_path: &Path) -> Result<LandResult, WorktreeError> {
let (main_path, main_branch) = find_main_worktree(worktree_path)?;
let current_branch = git(worktree_path, &["rev-parse", "--abbrev-ref", "HEAD"])?;
let current_branch = current_branch.trim().to_string();
if !git_status(&main_path, &["merge", "--ff-only", ¤t_branch])? {
return Err(WorktreeError::FastForwardFailed);
}
Ok(LandResult {
branch: current_branch,
main_branch,
})
}
fn rsync_ignored(main_path: &Path, worktree_path: &Path) -> Result<(), WorktreeError> {
let ignored = git(
main_path,
&[
"ls-files",
"--others",
"--ignored",
"--exclude-standard",
"--directory",
],
)?;
if ignored.trim().is_empty() {
return Ok(());
}
let mut child = Command::new("rsync")
.arg("-a")
.arg("--files-from=-")
.arg(format!("{}/", main_path.display()))
.arg(format!("{}/", worktree_path.display()))
.stdin(Stdio::piped())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.map_err(|e| WorktreeError::GitCommand(format!("failed to run rsync: {e}")))?;
if let Some(mut stdin) = child.stdin.take() {
let _ = stdin.write_all(ignored.as_bytes());
}
let _ = child.wait();
Ok(())
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
fn init_repo(dir: &Path) {
git(dir, &["init"]).unwrap();
git(dir, &["config", "user.email", "test@test.com"]).unwrap();
git(dir, &["config", "user.name", "Test"]).unwrap();
fs::write(dir.join("README.md"), "# test repo\n").unwrap();
git(dir, &["add", "."]).unwrap();
git(dir, &["commit", "-m", "initial commit"]).unwrap();
}
fn commit_file(dir: &Path, name: &str, content: &str, message: &str) {
let file_path = dir.join(name);
if let Some(parent) = file_path.parent() {
fs::create_dir_all(parent).unwrap();
}
fs::write(&file_path, content).unwrap();
git(dir, &["add", name]).unwrap();
git(dir, &["commit", "-m", message]).unwrap();
}
fn spawn_opts<'a>(repo: &'a Path, base: &'a Path, branch: Option<&'a str>) -> SpawnOptions<'a> {
SpawnOptions {
repo_path: repo,
branch,
base_path: base,
}
}
#[test]
fn spawn_creates_worktree() {
let repo_dir = TempDir::new().unwrap();
let base_dir = TempDir::new().unwrap();
init_repo(repo_dir.path());
let result = spawn(&spawn_opts(
repo_dir.path(),
base_dir.path(),
Some("test-branch"),
));
let result = result.unwrap();
assert_eq!(result.branch, "test-branch");
assert!(result.worktree_path.exists());
assert!(result.worktree_path.join("README.md").exists());
}
#[test]
fn spawn_copies_gitignored_files() {
let repo_dir = TempDir::new().unwrap();
let base_dir = TempDir::new().unwrap();
init_repo(repo_dir.path());
fs::write(repo_dir.path().join(".gitignore"), "build/\n").unwrap();
git(repo_dir.path(), &["add", ".gitignore"]).unwrap();
git(repo_dir.path(), &["commit", "-m", "add gitignore"]).unwrap();
fs::create_dir_all(repo_dir.path().join("build")).unwrap();
fs::write(repo_dir.path().join("build/output.txt"), "compiled stuff\n").unwrap();
let result = spawn(&spawn_opts(
repo_dir.path(),
base_dir.path(),
Some("wt-ignored"),
));
let result = result.unwrap();
assert!(result.worktree_path.join("build/output.txt").exists());
}
#[test]
fn spawn_custom_branch_name() {
let repo_dir = TempDir::new().unwrap();
let base_dir = TempDir::new().unwrap();
init_repo(repo_dir.path());
let result = spawn(&spawn_opts(
repo_dir.path(),
base_dir.path(),
Some("my-feature"),
));
let result = result.unwrap();
assert_eq!(result.branch, "my-feature");
assert!(result.worktree_path.ends_with("my-feature"));
}
#[test]
fn spawn_duplicate_branch_errors() {
let repo_dir = TempDir::new().unwrap();
let base_dir = TempDir::new().unwrap();
init_repo(repo_dir.path());
git(repo_dir.path(), &["branch", "existing-branch"]).unwrap();
let result = spawn(&spawn_opts(
repo_dir.path(),
base_dir.path(),
Some("existing-branch"),
));
assert!(
matches!(result, Err(WorktreeError::BranchExists(ref b)) if b == "existing-branch")
);
}
#[test]
fn land_clean_rebase() {
let repo_dir = TempDir::new().unwrap();
let base_dir = TempDir::new().unwrap();
init_repo(repo_dir.path());
let spawned = spawn(&spawn_opts(
repo_dir.path(),
base_dir.path(),
Some("feature"),
));
let spawned = spawned.unwrap();
commit_file(&spawned.worktree_path, "new.txt", "hello\n", "add new file");
let landed = land(&spawned.worktree_path).unwrap();
assert_eq!(landed.branch, "feature");
let log = git(repo_dir.path(), &["log", "--oneline"]).unwrap();
assert!(log.contains("add new file"));
assert!(spawned.worktree_path.exists());
}
#[test]
fn land_with_conflict() {
let repo_dir = TempDir::new().unwrap();
let base_dir = TempDir::new().unwrap();
init_repo(repo_dir.path());
let spawned = spawn(&spawn_opts(
repo_dir.path(),
base_dir.path(),
Some("conflict-branch"),
));
let spawned = spawned.unwrap();
commit_file(repo_dir.path(), "file.txt", "main content\n", "main change");
commit_file(
&spawned.worktree_path,
"file.txt",
"worktree content\n",
"worktree change",
);
let result = land(&spawned.worktree_path);
assert!(
matches!(result, Err(WorktreeError::RebaseConflict(ref files)) if files.contains(&"file.txt".to_string()))
);
abort_rebase(&spawned.worktree_path).unwrap();
}
#[test]
fn land_dirty_worktree_errors() {
let repo_dir = TempDir::new().unwrap();
let base_dir = TempDir::new().unwrap();
init_repo(repo_dir.path());
let spawned = spawn(&spawn_opts(
repo_dir.path(),
base_dir.path(),
Some("dirty-branch"),
));
let spawned = spawned.unwrap();
fs::write(spawned.worktree_path.join("README.md"), "modified\n").unwrap();
let result = land(&spawned.worktree_path);
assert!(matches!(result, Err(WorktreeError::DirtyWorkingTree)));
}
#[test]
fn abort_rebase_restores_clean_state() {
let repo_dir = TempDir::new().unwrap();
let base_dir = TempDir::new().unwrap();
init_repo(repo_dir.path());
let spawned = spawn(&spawn_opts(
repo_dir.path(),
base_dir.path(),
Some("abort-branch"),
));
let spawned = spawned.unwrap();
commit_file(repo_dir.path(), "conflict.txt", "main\n", "main side");
commit_file(
&spawned.worktree_path,
"conflict.txt",
"worktree\n",
"wt side",
);
let result = land(&spawned.worktree_path);
assert!(matches!(result, Err(WorktreeError::RebaseConflict(_))));
abort_rebase(&spawned.worktree_path).unwrap();
assert!(git_status(&spawned.worktree_path, &["diff", "--quiet"]).unwrap());
}
#[test]
fn sync_to_main_picks_up_new_commits() {
let repo_dir = TempDir::new().unwrap();
let base_dir = TempDir::new().unwrap();
init_repo(repo_dir.path());
let spawned = spawn(&spawn_opts(
repo_dir.path(),
base_dir.path(),
Some("sync-branch"),
))
.unwrap();
commit_file(
repo_dir.path(),
"new-on-main.txt",
"from main\n",
"main commit",
);
assert!(!spawned.worktree_path.join("new-on-main.txt").exists());
sync_to_main(&spawned.worktree_path).unwrap();
assert!(spawned.worktree_path.join("new-on-main.txt").exists());
}
#[test]
fn sync_to_main_noop_when_up_to_date() {
let repo_dir = TempDir::new().unwrap();
let base_dir = TempDir::new().unwrap();
init_repo(repo_dir.path());
let spawned = spawn(&spawn_opts(
repo_dir.path(),
base_dir.path(),
Some("sync-noop"),
))
.unwrap();
sync_to_main(&spawned.worktree_path).unwrap();
}
#[test]
fn reset_to_main_discards_local_commits() {
let repo_dir = TempDir::new().unwrap();
let base_dir = TempDir::new().unwrap();
init_repo(repo_dir.path());
let spawned = spawn(&spawn_opts(
repo_dir.path(),
base_dir.path(),
Some("reset-branch"),
))
.unwrap();
commit_file(
&spawned.worktree_path,
"local.txt",
"local\n",
"local commit",
);
assert!(spawned.worktree_path.join("local.txt").exists());
reset_to_main(&spawned.worktree_path).unwrap();
assert!(!spawned.worktree_path.join("local.txt").exists());
}
#[test]
fn reset_to_main_after_conflict_abort() {
let repo_dir = TempDir::new().unwrap();
let base_dir = TempDir::new().unwrap();
init_repo(repo_dir.path());
let spawned = spawn(&spawn_opts(
repo_dir.path(),
base_dir.path(),
Some("reset-conflict"),
))
.unwrap();
commit_file(repo_dir.path(), "file.txt", "main\n", "main side");
commit_file(&spawned.worktree_path, "file.txt", "worktree\n", "wt side");
let result = land(&spawned.worktree_path);
assert!(matches!(result, Err(WorktreeError::RebaseConflict(_))));
abort_rebase(&spawned.worktree_path).unwrap();
reset_to_main(&spawned.worktree_path).unwrap();
let content = fs::read_to_string(spawned.worktree_path.join("file.txt")).unwrap();
assert_eq!(content, "main\n");
}
#[test]
fn clean_removes_untracked_files() {
let repo_dir = TempDir::new().unwrap();
let base_dir = TempDir::new().unwrap();
init_repo(repo_dir.path());
let spawned = spawn(&spawn_opts(
repo_dir.path(),
base_dir.path(),
Some("clean-branch"),
))
.unwrap();
fs::write(spawned.worktree_path.join("stray.txt"), "leftover\n").unwrap();
fs::create_dir_all(spawned.worktree_path.join("stray-dir")).unwrap();
fs::write(
spawned.worktree_path.join("stray-dir/nested.txt"),
"nested\n",
)
.unwrap();
assert!(spawned.worktree_path.join("stray.txt").exists());
assert!(spawned.worktree_path.join("stray-dir/nested.txt").exists());
clean(&spawned.worktree_path).unwrap();
assert!(!spawned.worktree_path.join("stray.txt").exists());
assert!(!spawned.worktree_path.join("stray-dir").exists());
assert!(spawned.worktree_path.join("README.md").exists());
}
#[test]
fn clean_preserves_gitignored_files() {
let repo_dir = TempDir::new().unwrap();
let base_dir = TempDir::new().unwrap();
init_repo(repo_dir.path());
fs::write(repo_dir.path().join(".gitignore"), "build/\n").unwrap();
git(repo_dir.path(), &["add", ".gitignore"]).unwrap();
git(repo_dir.path(), &["commit", "-m", "add gitignore"]).unwrap();
let spawned = spawn(&spawn_opts(
repo_dir.path(),
base_dir.path(),
Some("clean-ignore"),
))
.unwrap();
fs::create_dir_all(spawned.worktree_path.join("build")).unwrap();
fs::write(spawned.worktree_path.join("build/output.bin"), "binary\n").unwrap();
fs::write(spawned.worktree_path.join("stray.txt"), "leftover\n").unwrap();
clean(&spawned.worktree_path).unwrap();
assert!(!spawned.worktree_path.join("stray.txt").exists());
assert!(spawned.worktree_path.join("build/output.bin").exists());
}
#[test]
fn ff_merge_after_manual_conflict_resolution() {
let repo_dir = TempDir::new().unwrap();
let base_dir = TempDir::new().unwrap();
init_repo(repo_dir.path());
let spawned = spawn(&spawn_opts(
repo_dir.path(),
base_dir.path(),
Some("ff-resolve"),
))
.unwrap();
commit_file(repo_dir.path(), "file.txt", "main\n", "main side");
commit_file(&spawned.worktree_path, "file.txt", "worktree\n", "wt side");
let result = land(&spawned.worktree_path);
assert!(matches!(result, Err(WorktreeError::RebaseConflict(_))));
assert!(is_rebase_in_progress(&spawned.worktree_path).unwrap());
fs::write(spawned.worktree_path.join("file.txt"), "resolved\n").unwrap();
git(&spawned.worktree_path, &["add", "file.txt"]).unwrap();
git(&spawned.worktree_path, &["rebase", "--continue"]).unwrap();
assert!(!is_rebase_in_progress(&spawned.worktree_path).unwrap());
let landed = ff_merge_main(&spawned.worktree_path).unwrap();
assert_eq!(landed.branch, "ff-resolve");
let content = fs::read_to_string(repo_dir.path().join("file.txt")).unwrap();
assert_eq!(content, "resolved\n");
}
#[test]
fn is_rebase_in_progress_false_normally() {
let repo_dir = TempDir::new().unwrap();
let base_dir = TempDir::new().unwrap();
init_repo(repo_dir.path());
let spawned = spawn(&spawn_opts(
repo_dir.path(),
base_dir.path(),
Some("no-rebase"),
))
.unwrap();
assert!(!is_rebase_in_progress(&spawned.worktree_path).unwrap());
}
#[test]
fn has_unique_commits_true_when_ahead() {
let repo_dir = TempDir::new().unwrap();
let base_dir = TempDir::new().unwrap();
init_repo(repo_dir.path());
let spawned = spawn(&spawn_opts(
repo_dir.path(),
base_dir.path(),
Some("unique-commits"),
))
.unwrap();
assert!(!has_unique_commits(&spawned.worktree_path).unwrap());
commit_file(&spawned.worktree_path, "new.txt", "hello\n", "add file");
assert!(has_unique_commits(&spawned.worktree_path).unwrap());
}
#[test]
fn has_unique_commits_false_after_land() {
let repo_dir = TempDir::new().unwrap();
let base_dir = TempDir::new().unwrap();
init_repo(repo_dir.path());
let spawned = spawn(&spawn_opts(
repo_dir.path(),
base_dir.path(),
Some("unique-land"),
))
.unwrap();
commit_file(&spawned.worktree_path, "new.txt", "hello\n", "add file");
assert!(has_unique_commits(&spawned.worktree_path).unwrap());
land(&spawned.worktree_path).unwrap();
assert!(!has_unique_commits(&spawned.worktree_path).unwrap());
}
#[test]
fn remove_worktree() {
let repo_dir = TempDir::new().unwrap();
let base_dir = TempDir::new().unwrap();
init_repo(repo_dir.path());
let spawned = spawn(&spawn_opts(
repo_dir.path(),
base_dir.path(),
Some("rm-branch"),
));
let spawned = spawned.unwrap();
assert!(spawned.worktree_path.exists());
remove(&spawned.worktree_path).unwrap();
assert!(!spawned.worktree_path.exists());
let branch_check = git_status(
repo_dir.path(),
&["show-ref", "--verify", "--quiet", "refs/heads/rm-branch"],
)
.unwrap();
assert!(!branch_check);
}
}