use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use tempfile::TempDir;
#[test]
fn config_commands_show_path_init_and_contents() {
let harness = TestHarness::new();
let path = harness.bra(harness.temp.path(), ["config", "path"]);
assert!(
path.status.success(),
"{}",
String::from_utf8_lossy(&path.stderr)
);
assert_eq!(
stdout_trimmed(&path),
harness.config_path.display().to_string()
);
let init = harness.bra(harness.temp.path(), ["config", "init"]);
assert!(
init.status.success(),
"{}",
String::from_utf8_lossy(&init.stderr)
);
assert!(harness.config_path.exists());
assert!(stdout_trimmed(&init).contains("created config at"));
let init_again = harness.bra(harness.temp.path(), ["config", "init"]);
assert!(
init_again.status.success(),
"{}",
String::from_utf8_lossy(&init_again.stderr)
);
assert!(stdout_trimmed(&init_again).contains("config file already exists at"));
let show_missing = harness.bra(harness.temp.path(), ["config", "show"]);
assert!(
show_missing.status.success(),
"{}",
String::from_utf8_lossy(&show_missing.stderr)
);
let missing_stdout = String::from_utf8_lossy(&show_missing.stdout);
assert!(missing_stdout.contains("# bra configuration"));
let script = harness.temp.path().join("custom.sh");
write_script(&script, "#!/bin/sh\nexit 0\n");
let add = harness.bra(
harness.temp.path(),
[
"--project",
"demo",
"script",
"add",
"custom",
script.to_str().unwrap(),
],
);
assert!(
add.status.success(),
"{}",
String::from_utf8_lossy(&add.stderr)
);
assert!(harness.config_path.exists());
let show = harness.bra(harness.temp.path(), ["config", "show"]);
assert!(
show.status.success(),
"{}",
String::from_utf8_lossy(&show.stderr)
);
let show_stdout = String::from_utf8_lossy(&show.stdout);
assert!(show_stdout.contains("custom"));
assert!(show_stdout.contains("scripts.demo"));
}
#[test]
fn go_returns_parent_and_worktree_paths() {
let harness = TestHarness::new();
harness.write_config(&format!(
"worktree_destination = \"{}\"\nbranch_separator = \"-\"\n",
escape_toml_string(&harness.worktrees_dir.display().to_string())
));
let master = harness.bra_in_repo(["go", "master"]);
assert!(
master.status.success(),
"{}",
String::from_utf8_lossy(&master.stderr)
);
assert_eq!(
stdout_trimmed(&master),
harness
.repo_dir
.canonicalize()
.unwrap()
.display()
.to_string()
);
let feature = harness.bra_in_repo(["go", "feature/test"]);
assert!(
feature.status.success(),
"{}",
String::from_utf8_lossy(&feature.stderr)
);
assert_eq!(
stdout_trimmed(&feature),
harness
.worktrees_dir
.join("feature-test")
.display()
.to_string()
);
}
#[test]
fn go_uses_existing_worktree_without_configured_destination() {
let harness = TestHarness::new();
let target = harness.worktrees_dir.join("feature-test");
fs::create_dir_all(target.parent().unwrap()).unwrap();
git(
&harness.repo_dir,
["worktree", "add", target.to_str().unwrap(), "feature/test"],
);
harness.write_config("");
let output = harness.bra_in_repo(["go", "feature/test"]);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(stdout_trimmed(&output), target.display().to_string());
}
#[test]
fn open_creates_worktree_runs_script_and_list_shows_it() {
let harness = TestHarness::new();
let marker = harness.temp.path().join("switch-marker.txt");
let script = harness.temp.path().join("bootstrap.sh");
write_script(
&script,
&format!("#!/bin/sh\npwd > \"{}\"\n", marker.display()),
);
harness.write_config(&format!(
"worktree_destination = \"{}\"\nbranch_separator = \"-\"\n\n[[scripts.demo]]\nname = \"bootstrap\"\npath = \"{}\"\n",
escape_toml_string(&harness.worktrees_dir.display().to_string()),
escape_toml_string(&script.display().to_string())
));
let output = harness.bra_in_repo(["open", "feature/test"]);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
let target = harness.worktrees_dir.join("feature-test");
assert_eq!(stdout_trimmed(&output), target.display().to_string());
assert!(target.exists());
let branch = git_capture(&target, ["branch", "--show-current"]);
assert_eq!(branch.trim(), "feature/test");
let upstream = git_capture(&target, ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"]);
assert_eq!(upstream.trim(), "origin/feature/test");
let marker_contents = fs::read_to_string(&marker).unwrap();
assert_eq!(marker_contents.trim(), target.display().to_string());
let list = harness.bra_in_repo(["list"]);
assert!(
list.status.success(),
"{}",
String::from_utf8_lossy(&list.stderr)
);
let list_stdout = String::from_utf8_lossy(&list.stdout);
assert!(list_stdout.contains("master"));
assert!(list_stdout.contains("feature/test"));
assert!(list_stdout.contains(&target.display().to_string()));
}
#[test]
fn open_from_creates_new_branch_from_base_branch() {
let harness = TestHarness::new();
harness.write_config(&format!(
"worktree_destination = \"{}\"\nbranch_separator = \"-\"\n",
escape_toml_string(&harness.worktrees_dir.display().to_string())
));
git(&harness.repo_dir, ["checkout", "-b", "develop"]);
fs::write(harness.repo_dir.join("develop.txt"), "develop\n").unwrap();
git(&harness.repo_dir, ["add", "develop.txt"]);
git(&harness.repo_dir, ["commit", "-m", "Add develop file"]);
git(&harness.repo_dir, ["checkout", "master"]);
let output = harness.bra_in_repo(["open", "--from", "develop", "feature/from-develop"]);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
let target = harness.worktrees_dir.join("feature-from-develop");
assert_eq!(stdout_trimmed(&output), target.display().to_string());
assert!(target.join("develop.txt").exists());
let branch = git_capture(&target, ["branch", "--show-current"]);
assert_eq!(branch.trim(), "feature/from-develop");
assert!(!git_success(
&target,
["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"]
));
}
#[test]
fn open_from_reports_missing_base_branch() {
let harness = TestHarness::new();
harness.write_config(&format!(
"worktree_destination = \"{}\"\nbranch_separator = \"-\"\n",
escape_toml_string(&harness.worktrees_dir.display().to_string())
));
let output = harness.bra_in_repo(["open", "--from", "missing-base", "feature/missing-base"]);
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("base branch 'missing-base' not found"));
}
#[test]
fn close_removes_worktree_and_can_delete_branch() {
let harness = TestHarness::new();
harness.write_config(&format!(
"worktree_destination = \"{}\"\nbranch_separator = \"-\"\n",
escape_toml_string(&harness.worktrees_dir.display().to_string())
));
let open = harness.bra_in_repo(["open", "feature/close"]);
assert!(
open.status.success(),
"{}",
String::from_utf8_lossy(&open.stderr)
);
let target = harness.worktrees_dir.join("feature-close");
assert!(target.exists());
let close = harness.bra_in_repo(["close", "feature/close", "--delete-branch"]);
assert!(
close.status.success(),
"{}",
String::from_utf8_lossy(&close.stderr)
);
assert!(!target.exists());
assert!(!git_success(
&harness.repo_dir,
["show-ref", "--verify", "--quiet", "refs/heads/feature/close"]
));
}
#[test]
fn close_without_branch_uses_current_worktree_branch() {
let harness = TestHarness::new();
harness.write_config(&format!(
"worktree_destination = \"{}\"\nbranch_separator = \"-\"\n",
escape_toml_string(&harness.worktrees_dir.display().to_string())
));
let open = harness.bra_in_repo(["open", "feature/current-close"]);
assert!(
open.status.success(),
"{}",
String::from_utf8_lossy(&open.stderr)
);
let target = harness.worktrees_dir.join("feature-current-close");
assert!(target.exists());
let close = harness.bra(&target, ["close"]);
assert!(
close.status.success(),
"{}",
String::from_utf8_lossy(&close.stderr)
);
assert!(!target.exists());
assert!(harness.repo_dir.exists());
}
#[test]
fn close_with_delete_branch_from_inside_worktree() {
let harness = TestHarness::new();
harness.write_config(&format!(
"worktree_destination = \"{}\"\nbranch_separator = \"-\"\n",
escape_toml_string(&harness.worktrees_dir.display().to_string())
));
let open = harness.bra_in_repo(["open", "feature/close-delete"]);
assert!(
open.status.success(),
"{}",
String::from_utf8_lossy(&open.stderr)
);
let target = harness.worktrees_dir.join("feature-close-delete");
assert!(target.exists());
let close = harness.bra(&target, ["close", "--delete-branch"]);
assert!(
close.status.success(),
"{}",
String::from_utf8_lossy(&close.stderr)
);
assert!(!target.exists());
assert!(!git_success(
&harness.repo_dir,
["show-ref", "--verify", "--quiet", "refs/heads/feature/close-delete"]
));
}
#[test]
fn prune_removes_stale_worktree_metadata_and_merged_branches() {
let harness = TestHarness::new();
harness.write_config(&format!(
"worktree_destination = \"{}\"\nbranch_separator = \"-\"\n",
escape_toml_string(&harness.worktrees_dir.display().to_string())
));
let stale_worktree = harness.worktrees_dir.join("stale-worktree");
fs::create_dir_all(stale_worktree.parent().unwrap()).unwrap();
git(
&harness.repo_dir,
[
"worktree",
"add",
stale_worktree.to_str().unwrap(),
"-b",
"stale-worktree",
],
);
fs::remove_dir_all(&stale_worktree).unwrap();
let initial_commit = git_capture(&harness.repo_dir, ["rev-parse", "HEAD"]).trim().to_owned();
git(&harness.repo_dir, ["branch", "merged-branch", &initial_commit]);
git(&harness.repo_dir, ["checkout", "-b", "unmerged-branch"]);
fs::write(harness.repo_dir.join("unmerged.txt"), "unmerged\n").unwrap();
git(&harness.repo_dir, ["add", "unmerged.txt"]);
git(&harness.repo_dir, ["commit", "-m", "Add unmerged file"]);
git(&harness.repo_dir, ["checkout", "master"]);
let worktrees_before = git_capture(&harness.repo_dir, ["worktree", "list", "--porcelain"]);
assert!(worktrees_before.contains("stale-worktree"));
let output = harness.bra_in_repo(["prune", "--merged"]);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
let worktrees = git_capture(&harness.repo_dir, ["worktree", "list", "--porcelain"]);
assert!(!worktrees.contains("stale-worktree"));
assert!(!git_success(
&harness.repo_dir,
[
"show-ref",
"--verify",
"--quiet",
"refs/heads/merged-branch"
]
));
assert!(!git_success(
&harness.repo_dir,
[
"show-ref",
"--verify",
"--quiet",
"refs/heads/stale-worktree"
]
));
assert!(git_success(
&harness.repo_dir,
[
"show-ref",
"--verify",
"--quiet",
"refs/heads/unmerged-branch"
]
));
}
#[test]
fn prune_without_merged_keeps_merged_branches() {
let harness = TestHarness::new();
harness.write_config(&format!(
"worktree_destination = \"{}\"\nbranch_separator = \"-\"\n",
escape_toml_string(&harness.worktrees_dir.display().to_string())
));
git(&harness.repo_dir, ["branch", "merged-kept"]);
let output = harness.bra_in_repo(["prune"]);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert!(git_success(
&harness.repo_dir,
[
"show-ref",
"--verify",
"--quiet",
"refs/heads/merged-kept"
]
));
}
#[test]
fn prune_merged_skips_branches_checked_out_in_worktrees() {
let harness = TestHarness::new();
harness.write_config(&format!(
"worktree_destination = \"{}\"\nbranch_separator = \"-\"\n",
escape_toml_string(&harness.worktrees_dir.display().to_string())
));
let open = harness.bra_in_repo(["open", "feature/current-prune"]);
assert!(
open.status.success(),
"{}",
String::from_utf8_lossy(&open.stderr)
);
let target = harness.worktrees_dir.join("feature-current-prune");
git(&harness.repo_dir, ["branch", "merged-prune"]);
let output = harness.bra(&target, ["prune", "--merged"]);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert!(target.exists());
assert!(git_success(
&harness.repo_dir,
[
"show-ref",
"--verify",
"--quiet",
"refs/heads/feature/current-prune"
]
));
assert!(!git_success(
&harness.repo_dir,
[
"show-ref",
"--verify",
"--quiet",
"refs/heads/merged-prune"
]
));
}
#[test]
fn open_can_prefix_worktree_with_project_when_enabled() {
let harness = TestHarness::new();
harness.write_config(&format!(
"worktree_destination = \"{}\"\nproject_prefix = true\nbranch_separator = \"-\"\n",
escape_toml_string(&harness.worktrees_dir.display().to_string())
));
let output = harness.bra_in_repo(["open", "feature/test"]);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
let target = harness.worktrees_dir.join("demo").join("feature-test");
assert_eq!(stdout_trimmed(&output), target.display().to_string());
assert!(target.exists());
}
#[test]
fn open_without_branch_creates_timestamped_branch_from_current_branch() {
let harness = TestHarness::new();
harness.write_config(&format!(
"worktree_destination = \"{}\"\nbranch_separator = \"-\"\n",
escape_toml_string(&harness.worktrees_dir.display().to_string())
));
let output = harness.bra_in_repo(["open"]);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
let target = PathBuf::from(stdout_trimmed(&output));
assert!(target.exists());
let branch = git_capture(&target, ["branch", "--show-current"]);
let branch = branch.trim();
assert_timestamped_branch(branch, "master");
assert_eq!(target.file_name().unwrap().to_str().unwrap(), branch);
}
#[test]
fn open_without_branch_flattens_generated_branch_path() {
let harness = TestHarness::new();
harness.write_config(&format!(
"worktree_destination = \"{}\"\nbranch_separator = \"-\"\n",
escape_toml_string(&harness.worktrees_dir.display().to_string())
));
git(&harness.repo_dir, ["checkout", "feature/test"]);
let output = harness.bra_in_repo(["open"]);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
let target = PathBuf::from(stdout_trimmed(&output));
assert!(target.exists());
let branch = git_capture(&target, ["branch", "--show-current"]);
let branch = branch.trim();
assert_timestamped_branch(branch, "feature/test");
assert_eq!(
target.file_name().unwrap().to_str().unwrap(),
branch.replace('/', "-")
);
}
#[test]
fn open_without_branch_trims_trailing_dash_before_timestamp() {
let harness = TestHarness::new();
harness.write_config(&format!(
"worktree_destination = \"{}\"\nbranch_separator = \"-\"\n",
escape_toml_string(&harness.worktrees_dir.display().to_string())
));
git(&harness.repo_dir, ["checkout", "-b", "foo-"]);
let output = harness.bra_in_repo(["open"]);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
let target = PathBuf::from(stdout_trimmed(&output));
assert!(target.exists());
let branch = git_capture(&target, ["branch", "--show-current"]);
let branch = branch.trim();
assert_timestamped_branch(branch, "foo");
assert!(!branch.starts_with("foo--"), "branch was {branch}");
}
#[test]
fn open_without_branch_can_run_twice_from_same_branch() {
let harness = TestHarness::new();
harness.write_config(&format!(
"worktree_destination = \"{}\"\nbranch_separator = \"-\"\n",
escape_toml_string(&harness.worktrees_dir.display().to_string())
));
let first = harness.bra_in_repo(["open"]);
assert!(
first.status.success(),
"{}",
String::from_utf8_lossy(&first.stderr)
);
let second = harness.bra_in_repo(["open"]);
assert!(
second.status.success(),
"{}",
String::from_utf8_lossy(&second.stderr)
);
let first_target = PathBuf::from(stdout_trimmed(&first));
let second_target = PathBuf::from(stdout_trimmed(&second));
assert_ne!(first_target, second_target);
let first_branch = git_capture(&first_target, ["branch", "--show-current"]);
let second_branch = git_capture(&second_target, ["branch", "--show-current"]);
let first_branch = first_branch.trim();
let second_branch = second_branch.trim();
assert_timestamped_branch(first_branch, "master");
assert_timestamped_branch(second_branch, "master");
assert_ne!(first_branch, second_branch);
}
#[test]
fn open_without_branch_reports_missing_worktree_destination() {
let harness = TestHarness::new();
harness.write_config("");
let output = harness.bra_in_repo(["open"]);
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("worktree_destination is not configured"));
assert!(!stderr.contains("failed to generate a unique timestamped branch name"));
}
#[test]
fn script_commands_manage_config_and_run_script() {
let harness = TestHarness::new();
harness.write_config(&format!(
"worktree_destination = \"{}\"\n",
escape_toml_string(&harness.worktrees_dir.display().to_string())
));
let marker = harness.temp.path().join("script-run.txt");
let script = harness.temp.path().join("custom.sh");
write_script(
&script,
&format!("#!/bin/sh\npwd > \"{}\"\n", marker.display()),
);
let add = harness.bra(
harness.temp.path(),
[
"--project",
"demo",
"script",
"add",
"custom",
script.to_str().unwrap(),
],
);
assert!(
add.status.success(),
"{}",
String::from_utf8_lossy(&add.stderr)
);
let list = harness.bra(harness.temp.path(), ["--project", "demo", "script", "list"]);
assert!(
list.status.success(),
"{}",
String::from_utf8_lossy(&list.stderr)
);
assert!(String::from_utf8_lossy(&list.stdout).contains("custom"));
let all = harness.bra(harness.temp.path(), ["script", "all"]);
assert!(
all.status.success(),
"{}",
String::from_utf8_lossy(&all.stderr)
);
let all_stdout = String::from_utf8_lossy(&all.stdout);
assert!(all_stdout.contains("[demo]"));
assert!(all_stdout.contains("custom"));
let run = harness.bra_in_repo(["script", "run", "custom"]);
assert!(
run.status.success(),
"{}",
String::from_utf8_lossy(&run.stderr)
);
assert_eq!(
fs::read_to_string(&marker).unwrap().trim(),
harness.repo_dir.display().to_string()
);
let remove = harness.bra(
harness.temp.path(),
["--project", "demo", "script", "remove", "custom"],
);
assert!(
remove.status.success(),
"{}",
String::from_utf8_lossy(&remove.stderr)
);
let list_after = harness.bra(harness.temp.path(), ["--project", "demo", "script", "list"]);
assert!(
list_after.status.success(),
"{}",
String::from_utf8_lossy(&list_after.stderr)
);
assert!(
String::from_utf8_lossy(&list_after.stdout).trim()
== "no scripts configured for project 'demo'"
);
}
#[test]
fn init_refuses_dirty_repository() {
let harness = TestHarness::new();
harness.write_config(&format!(
"worktree_destination = \"{}\"\n",
escape_toml_string(&harness.worktrees_dir.display().to_string())
));
fs::write(harness.repo_dir.join("README.md"), "dirty\n").unwrap();
let output = harness.bra_in_repo(["init"]);
assert!(!output.status.success());
assert!(String::from_utf8_lossy(&output.stderr).contains("uncommitted changes"));
}
#[test]
fn missing_origin_error_is_clean() {
let harness = TestHarness::new();
let repo = harness.temp.path().join("no-origin");
git(harness.temp.path(), ["init", repo.to_str().unwrap()]);
git(&repo, ["config", "user.email", "test@example.com"]);
git(&repo, ["config", "user.name", "Test User"]);
let output = harness.bra(&repo, ["list"]);
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("error: failed to resolve project alias from"));
assert!(stderr.contains("caused by: No such remote 'origin'"));
assert!(!stderr.contains("error: error:"));
}
#[test]
fn script_commands_inline_text() {
let harness = TestHarness::new();
harness.write_config(&format!(
"worktree_destination = \"{}\"\n",
escape_toml_string(&harness.worktrees_dir.display().to_string())
));
let marker = harness.temp.path().join("inline-marker.txt");
let add = harness.bra(
harness.temp.path(),
[
"--project",
"demo",
"script",
"add",
"inline",
"--text",
&format!("echo hello > \"{}\"", marker.display()),
],
);
assert!(
add.status.success(),
"{}",
String::from_utf8_lossy(&add.stderr)
);
let list = harness.bra(harness.temp.path(), ["--project", "demo", "script", "list"]);
assert!(
list.status.success(),
"{}",
String::from_utf8_lossy(&list.stderr)
);
let list_stdout = String::from_utf8_lossy(&list.stdout);
assert!(list_stdout.contains("inline"));
assert!(list_stdout.contains("(inline)"));
let run = harness.bra_in_repo(["script", "run", "inline"]);
assert!(
run.status.success(),
"{}",
String::from_utf8_lossy(&run.stderr)
);
assert_eq!(fs::read_to_string(&marker).unwrap().trim(), "hello");
let remove = harness.bra(
harness.temp.path(),
["--project", "demo", "script", "remove", "inline"],
);
assert!(
remove.status.success(),
"{}",
String::from_utf8_lossy(&remove.stderr)
);
}
#[test]
fn script_commands_inline_text_from_stdin() {
let harness = TestHarness::new();
harness.write_config(&format!(
"worktree_destination = \"{}\"\n",
escape_toml_string(&harness.worktrees_dir.display().to_string())
));
let marker = harness.temp.path().join("stdin-marker.txt");
let mut command = std::process::Command::new(env!("CARGO_BIN_EXE_bra"));
command
.current_dir(&harness.temp)
.env("BRA_CONFIG", &harness.config_path)
.args(["--project", "demo", "script", "add", "from-stdin", "--text"]);
command.stdin(std::process::Stdio::piped());
let mut child = command.spawn().unwrap();
{
let stdin = child.stdin.take().unwrap();
use std::io::Write;
let mut writer = std::io::BufWriter::new(stdin);
writeln!(writer, "echo world > \"{}\"", marker.display()).unwrap();
}
let add = child.wait_with_output().unwrap();
assert!(
add.status.success(),
"{}",
String::from_utf8_lossy(&add.stderr)
);
let run = harness.bra_in_repo(["script", "run", "from-stdin"]);
assert!(
run.status.success(),
"{}",
String::from_utf8_lossy(&run.stderr)
);
assert_eq!(fs::read_to_string(&marker).unwrap().trim(), "world");
}
#[test]
fn open_creates_worktree_runs_inline_script() {
let harness = TestHarness::new();
let marker = harness.temp.path().join("init-inline-marker.txt");
harness.write_config(&format!(
"worktree_destination = \"{}\"\nbranch_separator = \"-\"\n\n[[scripts.demo]]\nname = \"init\"\ntext = \"pwd > \\\"{}\\\"\"\n",
escape_toml_string(&harness.worktrees_dir.display().to_string()),
escape_toml_string(&marker.display().to_string())
));
let output = harness.bra_in_repo(["open", "feature/test"]);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
let target = harness.worktrees_dir.join("feature-test");
assert_eq!(stdout_trimmed(&output), target.display().to_string());
assert!(target.exists());
let marker_contents = fs::read_to_string(&marker).unwrap();
assert_eq!(marker_contents.trim(), target.display().to_string());
}
struct TestHarness {
temp: TempDir,
repo_dir: PathBuf,
worktrees_dir: PathBuf,
config_path: PathBuf,
}
impl TestHarness {
fn new() -> Self {
let temp = TempDir::new().unwrap();
let remote_dir = temp.path().join("demo.git");
let seed_dir = temp.path().join("seed");
let repo_dir = temp.path().join("repo");
let worktrees_dir = temp.path().join("worktrees");
let config_path = temp.path().join("config.toml");
git(
temp.path(),
["init", "--bare", remote_dir.to_str().unwrap()],
);
git(
temp.path(),
[
"clone",
remote_dir.to_str().unwrap(),
seed_dir.to_str().unwrap(),
],
);
git(&seed_dir, ["config", "user.email", "test@example.com"]);
git(&seed_dir, ["config", "user.name", "Test User"]);
fs::write(seed_dir.join("README.md"), "hello\n").unwrap();
git(&seed_dir, ["add", "README.md"]);
git(&seed_dir, ["commit", "-m", "Initial commit"]);
git(&seed_dir, ["branch", "-M", "master"]);
git(&remote_dir, ["symbolic-ref", "HEAD", "refs/heads/master"]);
git(&seed_dir, ["push", "-u", "origin", "master"]);
git(&seed_dir, ["checkout", "-b", "feature/test"]);
fs::write(seed_dir.join("feature.txt"), "feature\n").unwrap();
git(&seed_dir, ["add", "feature.txt"]);
git(&seed_dir, ["commit", "-m", "Add feature"]);
git(&seed_dir, ["push", "-u", "origin", "feature/test"]);
git(&seed_dir, ["checkout", "master"]);
git(
temp.path(),
[
"clone",
remote_dir.to_str().unwrap(),
repo_dir.to_str().unwrap(),
],
);
git(&repo_dir, ["config", "user.email", "test@example.com"]);
git(&repo_dir, ["config", "user.name", "Test User"]);
Self {
temp,
repo_dir,
worktrees_dir,
config_path,
}
}
fn write_config(&self, contents: &str) {
fs::write(&self.config_path, contents).unwrap();
}
fn bra_in_repo<I, S>(&self, args: I) -> Output
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
self.bra(&self.repo_dir, args)
}
fn bra<I, S>(&self, cwd: &Path, args: I) -> Output
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let mut command = Command::new(env!("CARGO_BIN_EXE_bra"));
for arg in args {
command.arg(arg.as_ref());
}
command
.current_dir(cwd)
.env("BRA_CONFIG", &self.config_path);
command.output().unwrap()
}
fn git_bra<I, S>(&self, cwd: &Path, args: I) -> Output
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let mut command = Command::new(env!("CARGO_BIN_EXE_git-bra"));
for arg in args {
command.arg(arg.as_ref());
}
command
.current_dir(cwd)
.env("BRA_CONFIG", &self.config_path);
command.output().unwrap()
}
}
fn git<I, S>(cwd: &Path, args: I)
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let output = Command::new("git")
.args(
args.into_iter()
.map(|arg| arg.as_ref().to_owned())
.collect::<Vec<_>>(),
)
.current_dir(cwd)
.output()
.unwrap();
assert!(
output.status.success(),
"git failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
fn git_capture<I, S>(cwd: &Path, args: I) -> String
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let output = Command::new("git")
.args(
args.into_iter()
.map(|arg| arg.as_ref().to_owned())
.collect::<Vec<_>>(),
)
.current_dir(cwd)
.output()
.unwrap();
assert!(
output.status.success(),
"git failed: {}",
String::from_utf8_lossy(&output.stderr)
);
String::from_utf8(output.stdout).unwrap()
}
fn git_success<I, S>(cwd: &Path, args: I) -> bool
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
Command::new("git")
.args(
args.into_iter()
.map(|arg| arg.as_ref().to_owned())
.collect::<Vec<_>>(),
)
.current_dir(cwd)
.output()
.unwrap()
.status
.success()
}
fn write_script(path: &Path, contents: &str) {
fs::write(path, contents).unwrap();
let mut permissions = fs::metadata(path).unwrap().permissions();
permissions.set_mode(0o755);
fs::set_permissions(path, permissions).unwrap();
}
fn stdout_trimmed(output: &Output) -> String {
String::from_utf8(output.stdout.clone())
.unwrap()
.trim()
.to_owned()
}
fn assert_timestamped_branch(branch: &str, prefix: &str) {
let (head, nanos) = branch
.rsplit_once('-')
.unwrap_or_else(|| panic!("branch was {branch}"));
let (head, time) = head
.rsplit_once('-')
.unwrap_or_else(|| panic!("branch was {branch}"));
let (actual_prefix, date) = head
.rsplit_once('-')
.unwrap_or_else(|| panic!("branch was {branch}"));
assert_eq!(actual_prefix, prefix, "branch was {branch}");
assert_eq!(date.len(), 6, "branch was {branch}");
assert_eq!(time.len(), 6, "branch was {branch}");
assert_eq!(nanos.len(), 9, "branch was {branch}");
assert!(
[date, time, nanos]
.iter()
.all(|part| part.chars().all(|ch| ch.is_ascii_digit())),
"branch was {branch}"
);
}
fn escape_toml_string(value: &str) -> String {
value.replace('\\', "\\\\").replace('"', "\\\"")
}
#[test]
fn git_bra_binary_matches_direct_bra_binary() {
let harness = TestHarness::new();
let direct = harness.bra_in_repo(["list"]);
assert!(
direct.status.success(),
"{}",
String::from_utf8_lossy(&direct.stderr)
);
let git_style = harness.git_bra(&harness.repo_dir, ["list"]);
assert!(
git_style.status.success(),
"{}",
String::from_utf8_lossy(&git_style.stderr)
);
assert_eq!(stdout_trimmed(&git_style), stdout_trimmed(&direct));
}