Add a centralized ui module with Arc's visual identity: colored commit IDs (magenta), bookmarks (cyan), tags (yellow), status symbols, and diff highlighting. Update all command output and tests accordingly.
390 lines
11 KiB
Rust
390 lines
11 KiB
Rust
use std::process::Command;
|
|
use tempfile::TempDir;
|
|
|
|
fn arc_cmd() -> Command {
|
|
let mut cmd = Command::new(env!("CARGO_BIN_EXE_arc"));
|
|
cmd.env("NO_COLOR", "1");
|
|
cmd
|
|
}
|
|
|
|
fn init_repo() -> TempDir {
|
|
let dir = TempDir::new().unwrap();
|
|
arc_cmd()
|
|
.arg("init")
|
|
.current_dir(dir.path())
|
|
.output()
|
|
.expect("failed to init");
|
|
dir
|
|
}
|
|
|
|
fn commit_file(dir: &TempDir, name: &str, content: &str, msg: &str) {
|
|
std::fs::write(dir.path().join(name), content).unwrap();
|
|
let output = arc_cmd()
|
|
.args(["commit", msg])
|
|
.current_dir(dir.path())
|
|
.output()
|
|
.expect("failed to commit");
|
|
assert!(
|
|
output.status.success(),
|
|
"commit failed: {}",
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
}
|
|
|
|
fn run_ok(dir: &TempDir, args: &[&str]) -> String {
|
|
let output = arc_cmd()
|
|
.args(args)
|
|
.current_dir(dir.path())
|
|
.output()
|
|
.expect("failed to run");
|
|
assert!(
|
|
output.status.success(),
|
|
"command {:?} failed: {}",
|
|
args,
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
String::from_utf8_lossy(&output.stdout).trim().to_string()
|
|
}
|
|
|
|
fn run_fail(dir: &TempDir, args: &[&str]) -> String {
|
|
let output = arc_cmd()
|
|
.args(args)
|
|
.current_dir(dir.path())
|
|
.output()
|
|
.expect("failed to run");
|
|
assert!(
|
|
!output.status.success(),
|
|
"command {:?} should have failed but succeeded: {}",
|
|
args,
|
|
String::from_utf8_lossy(&output.stdout)
|
|
);
|
|
String::from_utf8_lossy(&output.stderr).trim().to_string()
|
|
}
|
|
|
|
#[test]
|
|
fn stash_create_creates_stash() {
|
|
let dir = init_repo();
|
|
let stdout = run_ok(&dir, &["stash", "create", "wip"]);
|
|
assert!(stdout.contains("stash 'wip' created"));
|
|
|
|
let stash_path = dir.path().join(".arc/stashes/named/wip.yml");
|
|
assert!(stash_path.exists());
|
|
}
|
|
|
|
#[test]
|
|
fn stash_create_sets_active() {
|
|
let dir = init_repo();
|
|
run_ok(&dir, &["stash", "create", "wip"]);
|
|
|
|
let state = std::fs::read_to_string(dir.path().join(".arc/stashes/state.yml")).unwrap();
|
|
assert!(state.contains("wip"));
|
|
}
|
|
|
|
#[test]
|
|
fn stash_create_fails_if_exists() {
|
|
let dir = init_repo();
|
|
run_ok(&dir, &["stash", "create", "wip"]);
|
|
let stderr = run_fail(&dir, &["stash", "create", "wip"]);
|
|
assert!(stderr.contains("stash already exists"));
|
|
}
|
|
|
|
#[test]
|
|
fn stash_create_fails_invalid_name() {
|
|
let dir = init_repo();
|
|
let stderr = run_fail(&dir, &["stash", "create", "../escape"]);
|
|
assert!(stderr.contains("invalid ref name"));
|
|
}
|
|
|
|
#[test]
|
|
fn stash_use_switches_active() {
|
|
let dir = init_repo();
|
|
run_ok(&dir, &["stash", "create", "first"]);
|
|
run_ok(&dir, &["stash", "create", "second"]);
|
|
run_ok(&dir, &["stash", "use", "first"]);
|
|
|
|
let state = std::fs::read_to_string(dir.path().join(".arc/stashes/state.yml")).unwrap();
|
|
assert!(state.contains("first"));
|
|
}
|
|
|
|
#[test]
|
|
fn stash_use_fails_nonexistent() {
|
|
let dir = init_repo();
|
|
let stderr = run_fail(&dir, &["stash", "use", "nope"]);
|
|
assert!(stderr.contains("stash not found"));
|
|
}
|
|
|
|
#[test]
|
|
fn stash_push_saves_and_resets() {
|
|
let dir = init_repo();
|
|
commit_file(&dir, "a.txt", "hello\n", "initial");
|
|
run_ok(&dir, &["stash", "create", "wip"]);
|
|
|
|
std::fs::write(dir.path().join("a.txt"), "modified\n").unwrap();
|
|
|
|
let stdout = run_ok(&dir, &["stash", "push"]);
|
|
assert!(stdout.contains("pushed"));
|
|
assert!(stdout.contains("change(s)"));
|
|
|
|
let content = std::fs::read_to_string(dir.path().join("a.txt")).unwrap();
|
|
assert_eq!(content, "hello\n");
|
|
}
|
|
|
|
#[test]
|
|
fn stash_push_handles_added_files() {
|
|
let dir = init_repo();
|
|
commit_file(&dir, "a.txt", "hello\n", "initial");
|
|
run_ok(&dir, &["stash", "create", "wip"]);
|
|
|
|
std::fs::write(dir.path().join("new.txt"), "added\n").unwrap();
|
|
|
|
run_ok(&dir, &["stash", "push"]);
|
|
|
|
assert!(!dir.path().join("new.txt").exists());
|
|
}
|
|
|
|
#[test]
|
|
fn stash_push_handles_deleted_files() {
|
|
let dir = init_repo();
|
|
commit_file(&dir, "a.txt", "hello\n", "initial");
|
|
run_ok(&dir, &["stash", "create", "wip"]);
|
|
|
|
std::fs::remove_file(dir.path().join("a.txt")).unwrap();
|
|
|
|
run_ok(&dir, &["stash", "push"]);
|
|
|
|
let content = std::fs::read_to_string(dir.path().join("a.txt")).unwrap();
|
|
assert_eq!(content, "hello\n");
|
|
}
|
|
|
|
#[test]
|
|
fn stash_push_fails_no_active() {
|
|
let dir = init_repo();
|
|
commit_file(&dir, "a.txt", "hello\n", "initial");
|
|
std::fs::write(dir.path().join("a.txt"), "changed\n").unwrap();
|
|
|
|
let stderr = run_fail(&dir, &["stash", "push"]);
|
|
assert!(stderr.contains("no active stash"));
|
|
}
|
|
|
|
#[test]
|
|
fn stash_push_fails_clean_worktree() {
|
|
let dir = init_repo();
|
|
commit_file(&dir, "a.txt", "hello\n", "initial");
|
|
run_ok(&dir, &["stash", "create", "wip"]);
|
|
|
|
let stderr = run_fail(&dir, &["stash", "push"]);
|
|
assert!(stderr.contains("nothing to stash"));
|
|
}
|
|
|
|
#[test]
|
|
fn stash_pop_restores_changes() {
|
|
let dir = init_repo();
|
|
commit_file(&dir, "a.txt", "hello\n", "initial");
|
|
run_ok(&dir, &["stash", "create", "wip"]);
|
|
|
|
std::fs::write(dir.path().join("a.txt"), "modified\n").unwrap();
|
|
run_ok(&dir, &["stash", "push"]);
|
|
|
|
let stdout = run_ok(&dir, &["stash", "pop"]);
|
|
assert!(stdout.contains("popped"));
|
|
|
|
let content = std::fs::read_to_string(dir.path().join("a.txt")).unwrap();
|
|
assert_eq!(content, "modified\n");
|
|
}
|
|
|
|
#[test]
|
|
fn stash_pop_restores_added_files() {
|
|
let dir = init_repo();
|
|
commit_file(&dir, "a.txt", "hello\n", "initial");
|
|
run_ok(&dir, &["stash", "create", "wip"]);
|
|
|
|
std::fs::write(dir.path().join("new.txt"), "added\n").unwrap();
|
|
run_ok(&dir, &["stash", "push"]);
|
|
assert!(!dir.path().join("new.txt").exists());
|
|
|
|
run_ok(&dir, &["stash", "pop"]);
|
|
let content = std::fs::read_to_string(dir.path().join("new.txt")).unwrap();
|
|
assert_eq!(content, "added\n");
|
|
}
|
|
|
|
#[test]
|
|
fn stash_pop_restores_deleted_files() {
|
|
let dir = init_repo();
|
|
commit_file(&dir, "a.txt", "hello\n", "initial");
|
|
run_ok(&dir, &["stash", "create", "wip"]);
|
|
|
|
std::fs::remove_file(dir.path().join("a.txt")).unwrap();
|
|
run_ok(&dir, &["stash", "push"]);
|
|
|
|
run_ok(&dir, &["stash", "pop"]);
|
|
assert!(!dir.path().join("a.txt").exists());
|
|
}
|
|
|
|
#[test]
|
|
fn stash_pop_fails_no_active() {
|
|
let dir = init_repo();
|
|
let stderr = run_fail(&dir, &["stash", "pop"]);
|
|
assert!(stderr.contains("no active stash"));
|
|
}
|
|
|
|
#[test]
|
|
fn stash_pop_fails_empty_stash() {
|
|
let dir = init_repo();
|
|
run_ok(&dir, &["stash", "create", "wip"]);
|
|
let stderr = run_fail(&dir, &["stash", "pop"]);
|
|
assert!(stderr.contains("stash is empty"));
|
|
}
|
|
|
|
#[test]
|
|
fn stash_pop_fails_dirty_worktree() {
|
|
let dir = init_repo();
|
|
commit_file(&dir, "a.txt", "hello\n", "initial");
|
|
run_ok(&dir, &["stash", "create", "wip"]);
|
|
|
|
std::fs::write(dir.path().join("a.txt"), "modified\n").unwrap();
|
|
run_ok(&dir, &["stash", "push"]);
|
|
|
|
std::fs::write(dir.path().join("a.txt"), "dirty\n").unwrap();
|
|
|
|
let stderr = run_fail(&dir, &["stash", "pop"]);
|
|
assert!(stderr.contains("uncommitted changes"));
|
|
}
|
|
|
|
#[test]
|
|
fn stash_pop_fails_base_mismatch() {
|
|
let dir = init_repo();
|
|
commit_file(&dir, "a.txt", "hello\n", "initial");
|
|
run_ok(&dir, &["stash", "create", "wip"]);
|
|
|
|
std::fs::write(dir.path().join("a.txt"), "modified\n").unwrap();
|
|
run_ok(&dir, &["stash", "push"]);
|
|
|
|
commit_file(&dir, "b.txt", "new file\n", "second commit");
|
|
|
|
let stderr = run_fail(&dir, &["stash", "pop"]);
|
|
assert!(stderr.contains("stash base does not match"));
|
|
}
|
|
|
|
#[test]
|
|
fn stash_rm_removes_stash() {
|
|
let dir = init_repo();
|
|
run_ok(&dir, &["stash", "create", "wip"]);
|
|
|
|
let stdout = run_ok(&dir, &["stash", "rm", "wip"]);
|
|
assert!(stdout.contains("stash 'wip' removed"));
|
|
|
|
let stash_path = dir.path().join(".arc/stashes/named/wip.yml");
|
|
assert!(!stash_path.exists());
|
|
}
|
|
|
|
#[test]
|
|
fn stash_rm_clears_active_if_removed() {
|
|
let dir = init_repo();
|
|
run_ok(&dir, &["stash", "create", "wip"]);
|
|
run_ok(&dir, &["stash", "rm", "wip"]);
|
|
|
|
let state = std::fs::read_to_string(dir.path().join(".arc/stashes/state.yml")).unwrap();
|
|
assert!(state.contains("null") || !state.contains("wip"));
|
|
}
|
|
|
|
#[test]
|
|
fn stash_rm_fails_nonexistent() {
|
|
let dir = init_repo();
|
|
let stderr = run_fail(&dir, &["stash", "rm", "nope"]);
|
|
assert!(stderr.contains("stash not found"));
|
|
}
|
|
|
|
#[test]
|
|
fn stash_list_shows_stashes() {
|
|
let dir = init_repo();
|
|
run_ok(&dir, &["stash", "create", "alpha"]);
|
|
run_ok(&dir, &["stash", "create", "beta"]);
|
|
|
|
let stdout = run_ok(&dir, &["stash", "list"]);
|
|
assert!(stdout.contains("alpha"));
|
|
assert!(stdout.contains("beta"));
|
|
}
|
|
|
|
#[test]
|
|
fn stash_list_marks_active() {
|
|
let dir = init_repo();
|
|
run_ok(&dir, &["stash", "create", "alpha"]);
|
|
run_ok(&dir, &["stash", "create", "beta"]);
|
|
|
|
let stdout = run_ok(&dir, &["stash", "list"]);
|
|
assert!(stdout.contains("★ beta"));
|
|
let has_inactive_alpha = stdout.lines().any(|l| l.trim_start().starts_with("alpha"));
|
|
assert!(has_inactive_alpha);
|
|
assert!(!stdout.contains("★ alpha"));
|
|
}
|
|
|
|
#[test]
|
|
fn stash_list_sorted() {
|
|
let dir = init_repo();
|
|
run_ok(&dir, &["stash", "create", "zebra"]);
|
|
run_ok(&dir, &["stash", "create", "alpha"]);
|
|
|
|
let stdout = run_ok(&dir, &["stash", "list"]);
|
|
let alpha_pos = stdout.find("alpha").unwrap();
|
|
let zebra_pos = stdout.find("zebra").unwrap();
|
|
assert!(alpha_pos < zebra_pos);
|
|
}
|
|
|
|
#[test]
|
|
fn stash_list_shows_entry_count() {
|
|
let dir = init_repo();
|
|
commit_file(&dir, "a.txt", "hello\n", "initial");
|
|
run_ok(&dir, &["stash", "create", "wip"]);
|
|
|
|
let stdout = run_ok(&dir, &["stash", "list"]);
|
|
assert!(stdout.contains("0 entries"));
|
|
|
|
std::fs::write(dir.path().join("a.txt"), "modified\n").unwrap();
|
|
run_ok(&dir, &["stash", "push"]);
|
|
|
|
let stdout = run_ok(&dir, &["stash", "list"]);
|
|
assert!(stdout.contains("1 entries"));
|
|
}
|
|
|
|
#[test]
|
|
fn stash_list_empty() {
|
|
let dir = init_repo();
|
|
let stdout = run_ok(&dir, &["stash", "list"]);
|
|
assert!(stdout.contains("no stashes"));
|
|
}
|
|
|
|
#[test]
|
|
fn stash_push_pop_multiple() {
|
|
let dir = init_repo();
|
|
commit_file(&dir, "a.txt", "v1\n", "initial");
|
|
run_ok(&dir, &["stash", "create", "wip"]);
|
|
|
|
std::fs::write(dir.path().join("a.txt"), "v2\n").unwrap();
|
|
run_ok(&dir, &["stash", "push"]);
|
|
|
|
std::fs::write(dir.path().join("a.txt"), "v3\n").unwrap();
|
|
run_ok(&dir, &["stash", "push"]);
|
|
|
|
run_ok(&dir, &["stash", "pop"]);
|
|
let content = std::fs::read_to_string(dir.path().join("a.txt")).unwrap();
|
|
assert_eq!(content, "v3\n");
|
|
|
|
run_ok(&dir, &["reset"]);
|
|
run_ok(&dir, &["stash", "pop"]);
|
|
let content = std::fs::read_to_string(dir.path().join("a.txt")).unwrap();
|
|
assert_eq!(content, "v2\n");
|
|
}
|
|
|
|
#[test]
|
|
fn stash_push_on_unborn() {
|
|
let dir = init_repo();
|
|
run_ok(&dir, &["stash", "create", "wip"]);
|
|
|
|
std::fs::write(dir.path().join("new.txt"), "content\n").unwrap();
|
|
run_ok(&dir, &["stash", "push"]);
|
|
assert!(!dir.path().join("new.txt").exists());
|
|
|
|
run_ok(&dir, &["stash", "pop"]);
|
|
let content = std::fs::read_to_string(dir.path().join("new.txt")).unwrap();
|
|
assert_eq!(content, "content\n");
|
|
}
|