// file: crates/knowledge-git-lib/tests/git_write_push.rs // version: 1 //! Integration tests for guarded real Git pushes using local bare remotes. fn test_root(label: &str) -> std::path::PathBuf { let nanos_result = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH); let nanos = match nanos_result { Ok(value) => value.as_nanos(), Err(_) => 0, }; let mut path = std::env::temp_dir(); path.push(format!("sasedev-knowledge-git-push-{label}-{}-{nanos}", std::process::id())); let _ = std::fs::remove_dir_all(&path); return path; } fn run_git(repository: &std::path::Path, arguments: &[&str]) -> bool { let mut command = std::process::Command::new("git"); command.current_dir(repository); command.args(arguments); command.env("GIT_TERMINAL_PROMPT", "0"); let status_result = command.status(); match status_result { Ok(status) => return status.success(), Err(_) => return false, } } fn run_git_with_path(repository: &std::path::Path, arguments: &[&str], path: &std::path::Path) -> bool { let mut command = std::process::Command::new("git"); command.current_dir(repository); command.args(arguments); command.arg(path); command.env("GIT_TERMINAL_PROMPT", "0"); let status_result = command.status(); match status_result { Ok(status) => return status.success(), Err(_) => return false, } } fn run_git_dir_output(repository: &std::path::Path, reference: &str) -> Option { let mut command = std::process::Command::new("git"); command.arg("--git-dir"); command.arg(repository); command.arg("rev-parse"); command.arg("--verify"); command.arg(reference); command.env("GIT_TERMINAL_PROMPT", "0"); let output_result = command.output(); let output = match output_result { Ok(value) => value, Err(_) => return None, }; if !output.status.success() { return None; } let text_result = std::str::from_utf8(&output.stdout); match text_result { Ok(value) => return Some(value.trim().to_owned()), Err(_) => return None, } } fn prepare_remote(root: &std::path::Path) -> Option<(std::path::PathBuf, std::path::PathBuf)> { let remote = root.join("remote.git"); let seed = root.join("seed"); if std::fs::create_dir_all(root).is_err() { return None; } if !run_git_with_path(root, &["init", "--bare"], &remote) { return None; } if !run_git_with_path(root, &["init", "-b", "main"], &seed) { return None; } if !run_git(&seed, &["config", "user.name", "Remote Writer"]) { return None; } if !run_git(&seed, &["config", "user.email", "remote@example.invalid"]) { return None; } if !run_git(&seed, &["config", "commit.gpgsign", "false"]) { return None; } if std::fs::write(seed.join("content.txt"), "first\n").is_err() { return None; } if !run_git(&seed, &["add", "content.txt"]) { return None; } if !run_git(&seed, &["commit", "-m", "first"]) { return None; } if !run_git_with_path(&seed, &["remote", "add", "origin"], &remote) { return None; } if !run_git(&seed, &["push", "origin", "main"]) { return None; } return Some((remote, seed)); } fn configured_backend(root: &std::path::Path, remote: &std::path::Path) -> Option<(knowledge_git_lib::GitBackend, knowledge_domain_lib::SourceId)> { let source_id_result = knowledge_domain_lib::SourceId::new("integration-push-source"); let source_id = match source_id_result { Ok(value) => value, Err(_) => return None, }; let config_result = knowledge_git_lib::GitBackendConfig::new(root.join("state")); let config = match config_result { Ok(value) => value, Err(_) => return None, }; let source_result = knowledge_git_lib::GitSourceConfig::local(source_id.clone(), remote.to_path_buf()); let source_config = match source_result { Ok(value) => value, Err(_) => return None, }; let mut backend = knowledge_git_lib::GitBackend::new(config); if backend.register_source(source_config).is_err() { return None; } return Some((backend, source_id)); } fn resolved_main(backend: &mut knowledge_git_lib::GitBackend, source_id: &knowledge_domain_lib::SourceId) -> Option { if knowledge_git_api::GitRepository::refresh(backend, source_id).is_err() { return None; } let reference = knowledge_git_api::GitReference::new("refs/heads/main"); let revision_result = knowledge_git_api::GitRepository::resolve_revision(backend, source_id, &reference); match revision_result { Ok(value) => return Some(value), Err(_) => return None, } } fn mutation_path(value: &str) -> Option { let result = knowledge_git_api::GitMutationPath::new(value); match result { Ok(value) => return Some(value), Err(_) => return None, } } fn commit_identity() -> Option { let result = knowledge_git_api::GitCommitIdentity::new("Knowledge Writer", "writer@example.invalid"); match result { Ok(value) => return Some(value), Err(_) => return None, } } fn branch(value: &str) -> Option { let result = knowledge_git_api::GitBranch::new(value); match result { Ok(value) => return Some(value), Err(_) => return None, } } fn candidate_for( backend: &mut knowledge_git_lib::GitBackend, source_id: &knowledge_domain_lib::SourceId, base: knowledge_git_api::GitRevision, path: &str, content: &[u8], message: &str, ) -> Option { let mutation = match mutation_path(path) { Some(value) => knowledge_git_api::GitFileMutation::Write { path: value, content: content.to_vec() }, None => return None, }; let identity = match commit_identity() { Some(value) => value, None => return None, }; let request_result = knowledge_git_api::GitWriteRequest::new(base, vec![mutation], identity, message); let request = match request_result { Ok(value) => value, Err(_) => return None, }; let candidate_result = knowledge_git_api::GitRepositoryWriter::create_candidate(backend, source_id, &request); match candidate_result { Ok(value) => return Some(value), Err(_) => return None, } } #[test] fn guarded_push_updates_expected_branch_and_creates_only_expected_absent_branch() { let root = test_root("success"); let remote_result = prepare_remote(&root); assert!(remote_result.is_some()); let (remote, _) = match remote_result { Some(value) => value, None => return, }; let backend_result = configured_backend(&root, &remote); assert!(backend_result.is_some()); let (mut backend, source_id) = match backend_result { Some(value) => value, None => return, }; let base = match resolved_main(&mut backend, &source_id) { Some(value) => value, None => return, }; let candidate = match candidate_for(&mut backend, &source_id, base.clone(), "content.txt", b"second\n", "second") { Some(value) => value, None => return, }; let main = match branch("refs/heads/main") { Some(value) => value, None => return, }; let push = knowledge_git_api::GitPushRequest::new(main, knowledge_git_api::GitRemoteState::Revision(base), candidate.clone()); let outcome = knowledge_git_api::GitRepositoryWriter::push_candidate(&mut backend, &source_id, &push); assert_eq!(outcome, Ok(knowledge_git_api::GitPushOutcome::Updated(candidate.clone()))); assert_eq!(run_git_dir_output(&remote, "refs/heads/main"), Some(candidate.as_str().to_owned())); let proposal_candidate = match candidate_for(&mut backend, &source_id, candidate, "proposal.txt", b"proposal\n", "proposal") { Some(value) => value, None => return, }; let proposal = match branch("refs/heads/proposal") { Some(value) => value, None => return, }; let create = knowledge_git_api::GitPushRequest::new(proposal, knowledge_git_api::GitRemoteState::Absent, proposal_candidate.clone()); let create_outcome = knowledge_git_api::GitRepositoryWriter::push_candidate(&mut backend, &source_id, &create); assert_eq!(create_outcome, Ok(knowledge_git_api::GitPushOutcome::Updated(proposal_candidate.clone()))); assert_eq!(run_git_dir_output(&remote, "refs/heads/proposal"), Some(proposal_candidate.as_str().to_owned())); let _ = std::fs::remove_dir_all(&root); return; } #[test] fn guarded_push_returns_conflict_after_concurrent_remote_change_without_overwrite() { let root = test_root("conflict"); let remote_result = prepare_remote(&root); assert!(remote_result.is_some()); let (remote, seed) = match remote_result { Some(value) => value, None => return, }; let backend_result = configured_backend(&root, &remote); assert!(backend_result.is_some()); let (mut backend, source_id) = match backend_result { Some(value) => value, None => return, }; let base = match resolved_main(&mut backend, &source_id) { Some(value) => value, None => return, }; let candidate = match candidate_for(&mut backend, &source_id, base.clone(), "content.txt", b"candidate\n", "candidate") { Some(value) => value, None => return, }; assert!(std::fs::write(seed.join("content.txt"), "concurrent\n").is_ok()); assert!(run_git(&seed, &["add", "content.txt"])); assert!(run_git(&seed, &["commit", "-m", "concurrent"])); assert!(run_git(&seed, &["push", "origin", "main"])); let concurrent_text = match run_git_dir_output(&remote, "refs/heads/main") { Some(value) => value, None => return, }; let concurrent = knowledge_git_api::GitRevision::new(concurrent_text.as_str()); assert_ne!(concurrent, base); let main = match branch("refs/heads/main") { Some(value) => value, None => return, }; let request = knowledge_git_api::GitPushRequest::new(main, knowledge_git_api::GitRemoteState::Revision(base), candidate); let outcome = knowledge_git_api::GitRepositoryWriter::push_candidate(&mut backend, &source_id, &request); assert_eq!(outcome, Ok(knowledge_git_api::GitPushOutcome::Conflict { actual: knowledge_git_api::GitRemoteState::Revision(concurrent.clone()) })); assert_eq!(run_git_dir_output(&remote, "refs/heads/main"), Some(concurrent.as_str().to_owned())); let _ = std::fs::remove_dir_all(&root); return; }