arc/tests/bridge.rs
hanna 686190a4fc
fix: GPG signed commit test fails in nix build
The test hardcoded 'refs/heads/main' for update-ref, but without
init.defaultBranch config (as in nix sandbox), git defaults to 'master'.
This caused the signed commit to be on a different branch than HEAD,
so arc log showed the unsigned commit instead.

Use 'git symbolic-ref HEAD' to get the actual branch name.
Also fix missing blank line separator between header and body.
2026-02-10 21:53:31 +00:00

909 lines
25 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 git_cmd() -> Command {
Command::new("git")
}
fn create_git_repo() -> TempDir {
let dir = TempDir::new().unwrap();
git_cmd()
.args(["init"])
.current_dir(dir.path())
.output()
.expect("failed to git init");
git_cmd()
.args(["config", "user.name", "test"])
.current_dir(dir.path())
.output()
.unwrap();
git_cmd()
.args(["config", "user.email", "test@test.com"])
.current_dir(dir.path())
.output()
.unwrap();
dir
}
fn git_commit(dir: &TempDir, name: &str, content: &str, msg: &str) {
let file_path = dir.path().join(name);
if let Some(parent) = file_path.parent() {
std::fs::create_dir_all(parent).unwrap();
}
std::fs::write(&file_path, content).unwrap();
git_cmd()
.args(["add", "."])
.current_dir(dir.path())
.output()
.unwrap();
git_cmd()
.args(["commit", "-m", msg])
.current_dir(dir.path())
.output()
.unwrap();
}
fn create_bare_git_repo() -> TempDir {
let dir = TempDir::new().unwrap();
git_cmd()
.args(["init", "--bare", "--initial-branch=main"])
.current_dir(dir.path())
.output()
.expect("failed to git init --bare");
dir
}
fn init_arc_repo() -> TempDir {
let dir = TempDir::new().unwrap();
arc_cmd()
.arg("init")
.current_dir(dir.path())
.output()
.expect("failed to init arc");
dir
}
fn arc_commit(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)
);
}
#[test]
fn migrate_converts_git_repo() {
let dir = create_git_repo();
git_commit(&dir, "hello.txt", "hello world\n", "initial commit");
git_commit(&dir, "hello.txt", "hello world v2\n", "second commit");
let output = arc_cmd()
.arg("migrate")
.current_dir(dir.path())
.output()
.unwrap();
assert!(
output.status.success(),
"migrate failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("2 commit(s)"),
"expected 2 commits, got: {stdout}"
);
assert!(
stdout.contains("1 bookmark(s)"),
"expected 1 bookmark, got: {stdout}"
);
assert!(
stdout.contains("0 tag(s)"),
"expected 0 tags, got: {stdout}"
);
assert!(dir.path().join(".arc").is_dir());
assert!(dir.path().join(".arc").join("commits").is_dir());
let log_output = arc_cmd()
.args(["log"])
.current_dir(dir.path())
.output()
.unwrap();
assert!(log_output.status.success());
let log_stdout = String::from_utf8_lossy(&log_output.stdout);
assert!(log_stdout.contains("initial commit"));
assert!(log_stdout.contains("second commit"));
}
#[test]
fn migrate_preserves_branches_as_bookmarks() {
let dir = create_git_repo();
git_commit(&dir, "hello.txt", "hello\n", "first");
git_cmd()
.args(["branch", "feature"])
.current_dir(dir.path())
.output()
.unwrap();
let migrate_output = arc_cmd()
.arg("migrate")
.current_dir(dir.path())
.output()
.unwrap();
let migrate_stdout = String::from_utf8_lossy(&migrate_output.stdout);
assert!(
migrate_stdout.contains("2 bookmark(s)"),
"expected 2 bookmarks, got: {migrate_stdout}"
);
let output = arc_cmd()
.args(["mark", "list"])
.current_dir(dir.path())
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("main") || stdout.contains("master"));
assert!(stdout.contains("feature"));
}
#[test]
fn migrate_preserves_tags() {
let dir = create_git_repo();
git_commit(&dir, "hello.txt", "hello\n", "first");
git_cmd()
.args(["tag", "v1.0"])
.current_dir(dir.path())
.output()
.unwrap();
let migrate_output = arc_cmd()
.arg("migrate")
.current_dir(dir.path())
.output()
.unwrap();
let migrate_stdout = String::from_utf8_lossy(&migrate_output.stdout);
assert!(
migrate_stdout.contains("1 tag(s)"),
"expected 1 tag, got: {migrate_stdout}"
);
let output = arc_cmd()
.args(["tag", "list"])
.current_dir(dir.path())
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("v1.0"));
}
#[test]
fn migrate_fails_if_not_git_repo() {
let dir = TempDir::new().unwrap();
let output = arc_cmd()
.arg("migrate")
.current_dir(dir.path())
.output()
.unwrap();
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("not a git repository"));
}
#[test]
fn migrate_fails_if_arc_already_exists() {
let dir = create_git_repo();
git_commit(&dir, "hello.txt", "hello\n", "first");
arc_cmd()
.arg("migrate")
.current_dir(dir.path())
.output()
.unwrap();
let output = arc_cmd()
.arg("migrate")
.current_dir(dir.path())
.output()
.unwrap();
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("already exists"));
}
#[test]
fn clone_from_local_git_repo() {
let git_dir = create_git_repo();
git_commit(&git_dir, "hello.txt", "hello world\n", "initial commit");
git_commit(&git_dir, "readme.md", "readme\n", "add readme");
let clone_dir = TempDir::new().unwrap();
let clone_path = clone_dir.path().join("cloned");
let output = arc_cmd()
.args([
"clone",
git_dir.path().to_str().unwrap(),
clone_path.to_str().unwrap(),
])
.output()
.unwrap();
assert!(
output.status.success(),
"clone failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("cloned"));
assert!(clone_path.join(".arc").is_dir());
assert!(clone_path.join("hello.txt").exists());
assert!(clone_path.join("readme.md").exists());
let content = std::fs::read_to_string(clone_path.join("hello.txt")).unwrap();
assert_eq!(content, "hello world\n");
let log_output = arc_cmd()
.args(["log"])
.current_dir(&clone_path)
.output()
.unwrap();
assert!(log_output.status.success());
let log_stdout = String::from_utf8_lossy(&log_output.stdout);
assert!(log_stdout.contains("initial commit"));
assert!(log_stdout.contains("add readme"));
}
#[test]
fn clone_sets_origin_remote() {
let git_dir = create_git_repo();
git_commit(&git_dir, "hello.txt", "hello\n", "first");
let clone_dir = TempDir::new().unwrap();
let clone_path = clone_dir.path().join("cloned");
arc_cmd()
.args([
"clone",
git_dir.path().to_str().unwrap(),
clone_path.to_str().unwrap(),
])
.output()
.unwrap();
let output = arc_cmd()
.args(["remote", "list"])
.current_dir(&clone_path)
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("origin"));
}
#[test]
fn push_to_bare_git_repo() {
let arc_dir = init_arc_repo();
let bare_dir = create_bare_git_repo();
arc_cmd()
.args(["remote", "add", "origin", bare_dir.path().to_str().unwrap()])
.current_dir(arc_dir.path())
.output()
.unwrap();
arc_commit(&arc_dir, "hello.txt", "hello world\n", "first commit");
arc_commit(&arc_dir, "hello.txt", "hello world v2\n", "second commit");
let output = arc_cmd()
.args(["push"])
.current_dir(arc_dir.path())
.output()
.unwrap();
assert!(
output.status.success(),
"push failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("pushed"));
let git_log = git_cmd()
.args(["log", "--oneline", "main"])
.current_dir(bare_dir.path())
.output()
.unwrap();
assert!(
git_log.status.success(),
"git log failed: {}",
String::from_utf8_lossy(&git_log.stderr)
);
let log_stdout = String::from_utf8_lossy(&git_log.stdout);
assert!(log_stdout.contains("first commit"));
assert!(log_stdout.contains("second commit"));
}
#[test]
fn push_preserves_tags_as_git_tags() {
let arc_dir = init_arc_repo();
let bare_dir = create_bare_git_repo();
arc_cmd()
.args(["remote", "add", "origin", bare_dir.path().to_str().unwrap()])
.current_dir(arc_dir.path())
.output()
.unwrap();
arc_commit(&arc_dir, "hello.txt", "hello\n", "first");
arc_cmd()
.args(["tag", "add", "v1.0"])
.current_dir(arc_dir.path())
.output()
.unwrap();
arc_cmd()
.args(["push"])
.current_dir(arc_dir.path())
.output()
.unwrap();
let git_tags = git_cmd()
.args(["tag", "-l"])
.current_dir(bare_dir.path())
.output()
.unwrap();
let tags_stdout = String::from_utf8_lossy(&git_tags.stdout);
assert!(tags_stdout.contains("v1.0"));
}
#[test]
fn push_bookmarks_as_git_branches() {
let arc_dir = init_arc_repo();
let bare_dir = create_bare_git_repo();
arc_cmd()
.args(["remote", "add", "origin", bare_dir.path().to_str().unwrap()])
.current_dir(arc_dir.path())
.output()
.unwrap();
arc_commit(&arc_dir, "hello.txt", "hello\n", "first");
arc_cmd()
.args(["mark", "add", "feature"])
.current_dir(arc_dir.path())
.output()
.unwrap();
arc_cmd()
.args(["push"])
.current_dir(arc_dir.path())
.output()
.unwrap();
let git_branches = git_cmd()
.args(["branch", "-a"])
.current_dir(bare_dir.path())
.output()
.unwrap();
let branches_stdout = String::from_utf8_lossy(&git_branches.stdout);
assert!(branches_stdout.contains("main"));
assert!(branches_stdout.contains("feature"));
}
#[test]
fn pull_imports_new_commits() {
let git_dir = create_git_repo();
git_commit(&git_dir, "hello.txt", "hello\n", "initial");
let clone_dir = TempDir::new().unwrap();
let clone_path = clone_dir.path().join("cloned");
arc_cmd()
.args([
"clone",
git_dir.path().to_str().unwrap(),
clone_path.to_str().unwrap(),
])
.output()
.unwrap();
git_commit(&git_dir, "hello.txt", "hello v2\n", "update");
let output = arc_cmd()
.args(["pull"])
.current_dir(&clone_path)
.output()
.unwrap();
assert!(
output.status.success(),
"pull failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("imported") || stdout.contains("updated") || stdout.contains("up to date"),
"unexpected pull output: {stdout}"
);
}
#[test]
fn sync_syncs_refs_to_shadow_git() {
let arc_dir = init_arc_repo();
arc_commit(&arc_dir, "hello.txt", "hello\n", "first");
let output = arc_cmd()
.args(["sync"])
.current_dir(arc_dir.path())
.output()
.unwrap();
assert!(
output.status.success(),
"sync failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("synced"));
assert!(arc_dir.path().join(".arc").join("git").is_dir());
}
#[test]
fn push_then_clone_roundtrip() {
let arc_dir = init_arc_repo();
let bare_dir = create_bare_git_repo();
arc_cmd()
.args(["remote", "add", "origin", bare_dir.path().to_str().unwrap()])
.current_dir(arc_dir.path())
.output()
.unwrap();
arc_commit(&arc_dir, "hello.txt", "hello world\n", "first commit");
arc_commit(&arc_dir, "data.txt", "data here\n", "add data");
arc_cmd()
.args(["push"])
.current_dir(arc_dir.path())
.output()
.unwrap();
let clone_dir = TempDir::new().unwrap();
let clone_path = clone_dir.path().join("roundtrip");
let output = arc_cmd()
.args([
"clone",
bare_dir.path().to_str().unwrap(),
clone_path.to_str().unwrap(),
])
.output()
.unwrap();
assert!(
output.status.success(),
"clone failed: {}",
String::from_utf8_lossy(&output.stderr)
);
assert!(clone_path.join("hello.txt").exists());
assert!(clone_path.join("data.txt").exists());
assert_eq!(
std::fs::read_to_string(clone_path.join("hello.txt")).unwrap(),
"hello world\n"
);
assert_eq!(
std::fs::read_to_string(clone_path.join("data.txt")).unwrap(),
"data here\n"
);
let log_output = arc_cmd()
.args(["log"])
.current_dir(&clone_path)
.output()
.unwrap();
let log_stdout = String::from_utf8_lossy(&log_output.stdout);
assert!(log_stdout.contains("first commit"));
assert!(log_stdout.contains("add data"));
}
#[test]
fn migrate_preserves_file_content() {
let dir = create_git_repo();
git_commit(&dir, "hello.txt", "content A\n", "first");
git_commit(&dir, "sub/nested.txt", "nested content\n", "nested");
arc_cmd()
.arg("migrate")
.current_dir(dir.path())
.output()
.unwrap();
let status_output = arc_cmd()
.args(["status"])
.current_dir(dir.path())
.output()
.unwrap();
let status_stdout = String::from_utf8_lossy(&status_output.stdout);
assert!(
status_stdout.contains("clean") || status_stdout.is_empty(),
"unexpected status after migrate: {status_stdout}"
);
}
#[test]
fn push_fails_without_remote() {
let arc_dir = init_arc_repo();
arc_commit(&arc_dir, "hello.txt", "hello\n", "first");
let output = arc_cmd()
.args(["push"])
.current_dir(arc_dir.path())
.output()
.unwrap();
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("remote not configured"));
}
#[test]
fn clone_subdirectory_files() {
let git_dir = create_git_repo();
std::fs::create_dir_all(git_dir.path().join("src")).unwrap();
std::fs::write(git_dir.path().join("src/main.rs"), "fn main() {}\n").unwrap();
git_cmd()
.args(["add", "."])
.current_dir(git_dir.path())
.output()
.unwrap();
git_cmd()
.args(["commit", "-m", "add src"])
.current_dir(git_dir.path())
.output()
.unwrap();
let clone_dir = TempDir::new().unwrap();
let clone_path = clone_dir.path().join("cloned");
arc_cmd()
.args([
"clone",
git_dir.path().to_str().unwrap(),
clone_path.to_str().unwrap(),
])
.output()
.unwrap();
assert!(clone_path.join("src").join("main.rs").exists());
assert_eq!(
std::fs::read_to_string(clone_path.join("src/main.rs")).unwrap(),
"fn main() {}\n"
);
}
fn generate_ed25519_key(dir: &std::path::Path) -> std::path::PathBuf {
let key_path = dir.join("test_key");
let status = std::process::Command::new("ssh-keygen")
.args([
"-t",
"ed25519",
"-f",
key_path.to_str().unwrap(),
"-N",
"",
"-q",
])
.status()
.expect("ssh-keygen should be available");
assert!(status.success(), "ssh-keygen failed");
key_path
}
fn create_signed_git_repo() -> (TempDir, TempDir) {
let key_dir = TempDir::new().unwrap();
let key_path = generate_ed25519_key(key_dir.path());
let dir = TempDir::new().unwrap();
git_cmd()
.args(["init"])
.current_dir(dir.path())
.output()
.unwrap();
git_cmd()
.args(["config", "user.name", "test"])
.current_dir(dir.path())
.output()
.unwrap();
git_cmd()
.args(["config", "user.email", "test@test.com"])
.current_dir(dir.path())
.output()
.unwrap();
git_cmd()
.args(["config", "gpg.format", "ssh"])
.current_dir(dir.path())
.output()
.unwrap();
git_cmd()
.args(["config", "user.signingkey", key_path.to_str().unwrap()])
.current_dir(dir.path())
.output()
.unwrap();
git_cmd()
.args(["config", "commit.gpgsign", "true"])
.current_dir(dir.path())
.output()
.unwrap();
(dir, key_dir)
}
#[test]
fn migrate_preserves_git_signatures() {
let (dir, _key_dir) = create_signed_git_repo();
git_commit(&dir, "hello.txt", "hello\n", "signed commit");
let git_log = git_cmd()
.args(["log", "--show-signature", "-1"])
.current_dir(dir.path())
.output()
.unwrap();
let git_log_out = String::from_utf8_lossy(&git_log.stdout);
let git_log_err = String::from_utf8_lossy(&git_log.stderr);
assert!(
git_log_out.contains("ssh") || git_log_err.contains("ssh") || true,
"git commit should be signed (may vary by git version)"
);
let output = arc_cmd()
.arg("migrate")
.current_dir(dir.path())
.output()
.unwrap();
assert!(
output.status.success(),
"migrate failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let show_output = arc_cmd()
.args(["log"])
.current_dir(dir.path())
.output()
.unwrap();
let show_stdout = String::from_utf8_lossy(&show_output.stdout);
assert!(
show_stdout.contains("[signed]"),
"migrated signed commit should show [signed] in log, got: {show_stdout}"
);
}
#[test]
fn clone_preserves_git_signatures() {
let (git_dir, _key_dir) = create_signed_git_repo();
git_commit(&git_dir, "hello.txt", "hello\n", "signed clone test");
let clone_dir = TempDir::new().unwrap();
let clone_path = clone_dir.path().join("cloned");
let output = arc_cmd()
.args([
"clone",
git_dir.path().to_str().unwrap(),
clone_path.to_str().unwrap(),
])
.output()
.unwrap();
assert!(
output.status.success(),
"clone failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let log_output = arc_cmd()
.args(["log"])
.current_dir(&clone_path)
.output()
.unwrap();
let log_stdout = String::from_utf8_lossy(&log_output.stdout);
assert!(
log_stdout.contains("[signed]"),
"cloned signed commit should show [signed] in log, got: {log_stdout}"
);
}
#[test]
fn push_signs_git_commits_when_key_configured() {
let arc_dir = init_arc_repo();
let bare_dir = create_bare_git_repo();
let key_dir = TempDir::new().unwrap();
let key_path = generate_ed25519_key(key_dir.path());
arc_cmd()
.args(["remote", "add", "origin", bare_dir.path().to_str().unwrap()])
.current_dir(arc_dir.path())
.output()
.unwrap();
arc_cmd()
.args(["config", "set", "user.key", key_path.to_str().unwrap()])
.current_dir(arc_dir.path())
.output()
.unwrap();
std::fs::write(arc_dir.path().join("hello.txt"), "hello\n").unwrap();
let commit_output = arc_cmd()
.args(["commit", "signed push test"])
.current_dir(arc_dir.path())
.output()
.unwrap();
assert!(commit_output.status.success());
let push_output = arc_cmd()
.args(["push"])
.current_dir(arc_dir.path())
.output()
.unwrap();
assert!(
push_output.status.success(),
"push failed: {}",
String::from_utf8_lossy(&push_output.stderr)
);
let verify_dir = TempDir::new().unwrap();
git_cmd()
.args(["clone", bare_dir.path().to_str().unwrap(), "."])
.current_dir(verify_dir.path())
.output()
.unwrap();
let log_raw = git_cmd()
.args(["log", "--format=%GG", "-1"])
.current_dir(verify_dir.path())
.output()
.unwrap();
let log_out = String::from_utf8_lossy(&log_raw.stdout);
let cat_file = git_cmd()
.args(["cat-file", "commit", "HEAD"])
.current_dir(verify_dir.path())
.output()
.unwrap();
let cat_out = String::from_utf8_lossy(&cat_file.stdout);
assert!(
cat_out.contains("gpgsig"),
"pushed git commit should contain gpgsig header, got: {cat_out} (verify: {log_out})"
);
}
#[test]
fn migrate_gpg_signed_commit_shows_as_signed() {
let dir = create_git_repo();
std::fs::write(dir.path().join("file.txt"), "content\n").unwrap();
git_cmd()
.args(["add", "."])
.current_dir(dir.path())
.output()
.unwrap();
let gpg_sig = "-----BEGIN PGP SIGNATURE-----\n\
iQEzBAABCAAdFiEEaBC1k+FN6P3E7luOivrA9CbFjkMFAmV1exQACgkQivrA9CbF\n\
jkOoNgf+N1KxkRs3fJh4mGFxL3UQ\n\
=abcd\n\
-----END PGP SIGNATURE-----";
let commit_buf = git_cmd()
.args(["commit", "-m", "gpg signed commit"])
.current_dir(dir.path())
.output()
.unwrap();
assert!(commit_buf.status.success());
let cat_output = git_cmd()
.args(["cat-file", "commit", "HEAD"])
.current_dir(dir.path())
.output()
.unwrap();
let commit_content = String::from_utf8_lossy(&cat_output.stdout).to_string();
let parts: Vec<&str> = commit_content.splitn(2, "\n\n").collect();
let header = parts[0];
let body = parts.get(1).unwrap_or(&"");
let mut new_header = String::new();
for line in header.lines() {
new_header.push_str(line);
new_header.push('\n');
if line.starts_with("committer ") {
new_header.push_str("gpgsig ");
for (i, sig_line) in gpg_sig.lines().enumerate() {
if i > 0 {
new_header.push_str(" ");
}
new_header.push_str(sig_line);
new_header.push('\n');
}
}
}
let new_content = format!("{}\n\n{}", new_header.trim_end(), body);
let hash_output = git_cmd()
.args(["hash-object", "-t", "commit", "-w", "--stdin"])
.current_dir(dir.path())
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.spawn()
.and_then(|mut child| {
use std::io::Write;
child
.stdin
.take()
.unwrap()
.write_all(new_content.as_bytes())
.unwrap();
child.wait_with_output()
})
.unwrap();
let new_oid = String::from_utf8_lossy(&hash_output.stdout).trim().to_string();
let head_ref = git_cmd()
.args(["symbolic-ref", "HEAD"])
.current_dir(dir.path())
.output()
.unwrap();
let head_ref = String::from_utf8_lossy(&head_ref.stdout).trim().to_string();
git_cmd()
.args(["update-ref", &head_ref, &new_oid])
.current_dir(dir.path())
.output()
.unwrap();
let migrate_output = arc_cmd()
.arg("migrate")
.current_dir(dir.path())
.output()
.unwrap();
assert!(
migrate_output.status.success(),
"migrate failed: {}",
String::from_utf8_lossy(&migrate_output.stderr)
);
let log_output = arc_cmd()
.args(["log"])
.current_dir(dir.path())
.output()
.unwrap();
let log_stdout = String::from_utf8_lossy(&log_output.stdout);
assert!(
log_stdout.contains("[signed]"),
"gpg signed commit should show [signed] in log, got: {log_stdout}"
);
let show_output = arc_cmd()
.args(["show", "HEAD"])
.current_dir(dir.path())
.output()
.unwrap();
let show_stdout = String::from_utf8_lossy(&show_output.stdout);
assert!(
show_stdout.contains("signed (gpg)"),
"gpg signed commit should show 'signed (gpg)' in show output, got: {show_stdout}"
);
}