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.
909 lines
25 KiB
Rust
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}"
|
|
);
|
|
}
|