use std::fs; use bds_core::engine::git::{FileStatusKind, GitEngine}; fn git(dir: &std::path::Path, args: &[&str]) { let output = std::process::Command::new("git") .args(args) .current_dir(dir) .output() .unwrap(); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); } fn git_text(dir: &std::path::Path, args: &[&str]) -> String { let output = std::process::Command::new("git") .args(args) .current_dir(dir) .output() .unwrap(); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); String::from_utf8(output.stdout).unwrap().trim().to_string() } #[test] fn status_diff_history_and_missing_diff_sides_follow_the_spec() { let dir = tempfile::tempdir().unwrap(); git(dir.path(), &["init", "-b", "master"]); git(dir.path(), &["config", "user.name", "RuDS Test"]); git(dir.path(), &["config", "user.email", "test@example.com"]); fs::write(dir.path().join("kept.txt"), "before\n").unwrap(); fs::write(dir.path().join("deleted.txt"), "gone\n").unwrap(); git(dir.path(), &["add", "-A"]); git(dir.path(), &["commit", "-m", "initial"]); fs::write(dir.path().join("kept.txt"), "after\n").unwrap(); fs::remove_file(dir.path().join("deleted.txt")).unwrap(); fs::write(dir.path().join("added.txt"), "new\n").unwrap(); let engine = GitEngine::new(dir.path()); let status = engine.status().unwrap(); assert!( status .iter() .any(|file| { file.path == "kept.txt" && file.kind == FileStatusKind::Modified }) ); assert!( status .iter() .any(|file| { file.path == "deleted.txt" && file.kind == FileStatusKind::Deleted }) ); assert!( status .iter() .any(|file| { file.path == "added.txt" && file.kind == FileStatusKind::Untracked }) ); let diff = engine.diff().unwrap(); assert!(diff.unstaged.contains("-before")); assert!(diff.unstaged.contains("+after")); let added = engine.file_diff("added.txt").unwrap(); assert_eq!(added.original, ""); assert_eq!(added.modified, "new\n"); let deleted = engine.file_diff("deleted.txt").unwrap(); assert_eq!(deleted.original, "gone\n"); assert_eq!(deleted.modified, ""); assert_eq!(engine.file_history("kept.txt").unwrap().len(), 1); git(dir.path(), &["add", "-A"]); git(dir.path(), &["commit", "-m", "working tree changes"]); let hash = git_text(dir.path(), &["rev-parse", "HEAD"]); let changes = engine.commit_files(&hash).unwrap(); let added_change = changes .iter() .find(|change| change.path == "added.txt") .unwrap(); let deleted_change = changes .iter() .find(|change| change.path == "deleted.txt") .unwrap(); let added = engine.commit_file_diff(&hash, added_change).unwrap(); let deleted = engine.commit_file_diff(&hash, deleted_change).unwrap(); assert_eq!( (added.original.as_str(), added.modified.as_str()), ("", "new\n") ); assert_eq!( (deleted.original.as_str(), deleted.modified.as_str()), ("gone\n", "") ); } #[test] fn commit_rejects_blank_messages_before_staging() { let dir = tempfile::tempdir().unwrap(); git(dir.path(), &["init", "-b", "master"]); fs::write(dir.path().join("untracked.txt"), "content\n").unwrap(); let error = GitEngine::new(dir.path()).commit_all(" \n\t ").unwrap_err(); assert!(error.to_string().contains("commit message")); let output = std::process::Command::new("git") .args(["diff", "--cached", "--name-only"]) .current_dir(dir.path()) .output() .unwrap(); assert!(output.stdout.is_empty()); } #[test] fn remote_state_history_fetch_and_fast_forward_pull_use_real_refs() { let root = tempfile::tempdir().unwrap(); let remote = root.path().join("remote.git"); let local = root.path().join("local"); let peer = root.path().join("peer"); fs::create_dir(&local).unwrap(); git( root.path(), &["init", "--bare", "-b", "master", remote.to_str().unwrap()], ); git(&local, &["init", "-b", "master"]); git(&local, &["config", "user.name", "Local"]); git(&local, &["config", "user.email", "local@example.com"]); fs::write(local.join("post.md"), "one\n").unwrap(); git(&local, &["add", "-A"]); git(&local, &["commit", "-m", "local one"]); let engine = GitEngine::new(&local); git( &local, &["remote", "add", "origin", remote.to_str().unwrap()], ); git(&local, &["push", "-u", "origin", "master"]); git( root.path(), &["clone", remote.to_str().unwrap(), peer.to_str().unwrap()], ); git(&peer, &["config", "user.name", "Peer"]); git(&peer, &["config", "user.email", "peer@example.com"]); fs::write(peer.join("post.md"), "two\n").unwrap(); git(&peer, &["add", "-A"]); git(&peer, &["commit", "-m", "remote two"]); git(&peer, &["push"]); let mut streamed = String::new(); engine .fetch(|| false, |chunk| streamed.push_str(&chunk.text)) .unwrap(); let state = engine.remote_state().unwrap(); assert_eq!(state.behind, 1); assert_eq!(state.ahead, 0); assert!( engine .history("master") .unwrap() .iter() .any(|commit| commit.subject.as_deref() == Some("remote two") && commit.sync_status == bds_core::engine::git::SyncStatus::RemoteOnly) ); engine.pull(|| false, |_| {}).unwrap(); let state = engine.remote_state().unwrap(); assert_eq!((state.ahead, state.behind), (0, 0)); assert_eq!(fs::read_to_string(local.join("post.md")).unwrap(), "two\n"); } #[test] fn file_history_follows_renames_and_is_limited_to_fifty_commits() { let dir = tempfile::tempdir().unwrap(); git(dir.path(), &["init", "-b", "master"]); git(dir.path(), &["config", "user.name", "RuDS Test"]); git(dir.path(), &["config", "user.email", "test@example.com"]); fs::write(dir.path().join("old.txt"), "0\n").unwrap(); git(dir.path(), &["add", "-A"]); git(dir.path(), &["commit", "-m", "old name"]); git(dir.path(), &["mv", "old.txt", "new.txt"]); git(dir.path(), &["commit", "-m", "renamed"]); for index in 1..=49 { fs::write(dir.path().join("new.txt"), format!("{index}\n")).unwrap(); git(dir.path(), &["add", "new.txt"]); git(dir.path(), &["commit", "-m", &format!("change {index}")]); } let history = GitEngine::new(dir.path()).file_history("new.txt").unwrap(); assert_eq!(history.len(), 50); assert_eq!(history[0].subject.as_deref(), Some("change 49")); assert!( history .iter() .any(|commit| commit.subject.as_deref() == Some("renamed")) ); }