use crate::core::graph::Theme;
use crate::core::test_helpers::TestRepo;
fn setup_with_woven_branch() -> TestRepo {
let test_repo = TestRepo::new_with_remote();
let base_oid = test_repo.find_remote_branch_target("origin/main");
test_repo.create_branch_at("feature-a", &base_oid.to_string());
test_repo.switch_branch("feature-a");
test_repo.commit("Add feature file", "feature.txt");
test_repo.switch_branch("integration");
test_repo.merge_no_ff("feature-a");
test_repo
}
fn is_staged(porcelain: &str, filename: &str) -> bool {
porcelain.lines().any(|line| {
let x = line.chars().next().unwrap_or(' ');
(x == 'A' || x == 'M') && line.ends_with(filename)
})
}
fn run_add(files: Vec<String>) -> anyhow::Result<()> {
let theme = Theme::dark();
super::run(files, false, &theme)
}
#[test]
fn add_single_file_by_name() {
let test_repo = TestRepo::new();
test_repo.write_file("hello.txt", "hello world");
let result = test_repo.in_dir(|| run_add(vec!["hello.txt".to_string()]));
assert!(result.is_ok(), "add failed: {:?}", result);
let status = test_repo.status_porcelain();
assert!(
is_staged(&status, "hello.txt"),
"hello.txt should be staged; status: {}",
status
);
}
#[test]
fn add_single_file_by_shortid() {
let test_repo = setup_with_woven_branch();
test_repo.write_file("feature.txt", "modified content");
let short_id = test_repo.in_dir(|| {
let info = crate::core::repo::gather_repo_info(&test_repo.repo, false, 1).unwrap();
let entities = info.collect_entities();
let allocator = crate::core::shortid::IdAllocator::new(entities);
allocator.get_file("feature.txt").to_string()
});
let result = test_repo.in_dir(|| run_add(vec![short_id.clone()]));
assert!(
result.is_ok(),
"add by short ID '{}' failed: {:?}",
short_id,
result
);
let status = test_repo.status_porcelain();
assert!(
is_staged(&status, "feature.txt"),
"feature.txt should be staged; status: {}",
status
);
}
#[test]
fn add_multiple_files() {
let test_repo = TestRepo::new();
test_repo.write_file("a.txt", "aaa");
test_repo.write_file("b.txt", "bbb");
let result = test_repo.in_dir(|| run_add(vec!["a.txt".to_string(), "b.txt".to_string()]));
assert!(result.is_ok(), "add multiple failed: {:?}", result);
let status = test_repo.status_porcelain();
assert!(
is_staged(&status, "a.txt"),
"a.txt should be staged; status: {}",
status
);
assert!(
is_staged(&status, "b.txt"),
"b.txt should be staged; status: {}",
status
);
}
#[test]
fn add_zz_stages_all() {
let test_repo = TestRepo::new();
test_repo.write_file("one.txt", "1");
test_repo.write_file("two.txt", "2");
test_repo.write_file("three.txt", "3");
let result = test_repo.in_dir(|| run_add(vec!["zz".to_string()]));
assert!(result.is_ok(), "add zz failed: {:?}", result);
let status = test_repo.status_porcelain();
assert!(
is_staged(&status, "one.txt"),
"one.txt should be staged; status: {}",
status
);
assert!(
is_staged(&status, "two.txt"),
"two.txt should be staged; status: {}",
status
);
assert!(
is_staged(&status, "three.txt"),
"three.txt should be staged; status: {}",
status
);
}
#[test]
fn add_nonexistent_file_errors() {
let test_repo = TestRepo::new();
let result = test_repo.in_dir(|| run_add(vec!["nope.txt".to_string()]));
assert!(result.is_err(), "expected error for nonexistent file");
}
#[test]
fn add_invalid_shortid_errors() {
let test_repo = TestRepo::new();
let result = test_repo.in_dir(|| run_add(vec!["zq".to_string()]));
assert!(result.is_err(), "expected error for invalid short ID");
}
#[test]
fn add_patch_flag_placeholder() {
let test_repo = TestRepo::new();
let theme = Theme::dark();
let result = test_repo.in_dir(|| super::run(vec![], true, &theme));
match &result {
Ok(()) => {}
Err(e) if e.to_string() == "Cancelled" => {}
Err(e) => panic!("add -p should not crash: {e:?}"),
}
}
#[test]
fn collect_entries_includes_untracked_subdirs() {
let test_repo = TestRepo::new();
let subdir = test_repo.workdir().join("subdir");
std::fs::create_dir_all(&subdir).unwrap();
std::fs::write(subdir.join("a.txt"), "aaa\n").unwrap();
std::fs::write(subdir.join("b.txt"), "bbb\n").unwrap();
test_repo.write_file("root.txt", "root\n");
let workdir = test_repo.repo.workdir().expect("not bare").to_path_buf();
let entries = test_repo
.in_dir(|| crate::core::staging::collect_file_entries(&test_repo.repo, &workdir, &[]));
let entries = entries.unwrap();
let paths: Vec<&str> = entries.iter().map(|e| e.path.as_str()).collect();
assert!(
paths.contains(&"root.txt"),
"expected root.txt, got: {:?}",
paths
);
assert!(
paths.contains(&"subdir/a.txt"),
"expected subdir/a.txt, got: {:?}",
paths
);
assert!(
paths.contains(&"subdir/b.txt"),
"expected subdir/b.txt, got: {:?}",
paths
);
for entry in &entries {
assert!(
!entry.hunks.is_empty(),
"file '{}' should have at least one hunk",
entry.path
);
}
}
#[test]
fn collect_entries_includes_empty_untracked_files() {
let test_repo = TestRepo::new();
test_repo.write_file("empty.txt", "");
let subdir = test_repo.workdir().join("newdir");
std::fs::create_dir_all(&subdir).unwrap();
std::fs::write(subdir.join("also_empty.txt"), "").unwrap();
let workdir = test_repo.repo.workdir().expect("not bare").to_path_buf();
let entries = test_repo
.in_dir(|| crate::core::staging::collect_file_entries(&test_repo.repo, &workdir, &[]));
let entries = entries.unwrap();
let paths: Vec<&str> = entries.iter().map(|e| e.path.as_str()).collect();
assert!(
paths.contains(&"empty.txt"),
"expected empty.txt, got: {:?}",
paths
);
assert!(
paths.contains(&"newdir/also_empty.txt"),
"expected newdir/also_empty.txt, got: {:?}",
paths
);
}