use std::io::{Error, ErrorKind::Other, Result}; use std::path::Path; use std::process::{Command, Stdio}; use std::time::{SystemTime, UNIX_EPOCH}; pub struct Git; impl Git { pub fn new() -> Self { Git } } impl super::Remote for Git { fn can_handle(&self, _: &str) -> bool { // If we ever implement another remote we will want a check strategy true } fn sync_up(&self, remote: &str, directory: &Path) -> Result<()> { let timestamp = SystemTime::now() .duration_since(UNIX_EPOCH) .map_err(|_| Error::new(Other, "Invalid time"))? .as_millis(); let commands = vec![ format!("cd {} && git init -b main", directory.display()), format!("cd {} && git add .", directory.display()), format!( "cd {} && git commit --allow-empty -m blog-sync-up-{}", directory.display(), timestamp ), format!( "cd {} && git push {} main --force", directory.display(), remote ), ]; for command in commands { Command::new("sh") .arg("-c") .arg(&command) .stdout(Stdio::null()) .stderr(Stdio::null()) .status() .map_err(|_| Error::new(Other, "Failed while performing sync up with git"))?; } Ok(()) } fn sync_down(&self, remote: &str, directory: &Path) -> Result<()> { let commands = vec![ format!("cd {} && git init -b main", directory.display()), format!("cd {} && git checkout .", directory.display()), format!("cd {} && git clean . -f", directory.display()), format!("cd {} && git pull {} main", directory.display(), remote), ]; for command in commands { Command::new("sh") .arg("-c") .arg(&command) .stdout(Stdio::null()) .stderr(Stdio::null()) .status() .map_err(|_| Error::new(Other, "Failed while performing sync down with git"))?; } Ok(()) } } #[cfg(test)] mod tests { use std::fs::create_dir_all; use std::path::Path; use std::process::{Command, Stdio}; use super::*; use crate::remote::Remote; use test_utilities::*; fn create_remote(path: &Path) { let remote_path = path.display(); Command::new("sh") .arg("-c") .arg(format!("cd {remote_path} && git init --bare")) .stdout(Stdio::null()) .stderr(Stdio::null()) .status() .expect("Failed to initialize bare repository"); } fn assert_file_does_not_exist(repo_path: &str, file_path: &str) { let output = Command::new("sh") .arg("-c") .arg(format!("cd {repo_path} && git show main:{file_path}")) .stderr(Stdio::null()) .output() .expect("Failed to read from repo"); assert!(output.status.code().unwrap() != 0); } fn assert_file_in_repo_with_contents(repo_path: &str, file_path: &str, contents: &str) { let output = Command::new("sh") .arg("-c") .arg(format!("cd {repo_path} && git show main:{file_path}")) .stderr(Stdio::null()) .output() .expect("Failed to read from repo"); assert!(String::from_utf8(output.stdout) .map(|file| file.trim() == contents) .unwrap_or(false)); } fn commit_file_to_remote(repo_path: &str, file_path: &str, contents: &str) { let test_dir = setup_test_dir(); let test_dir_display = test_dir.display(); Command::new("sh") .arg("-c") .arg(format!( "cd {test_dir_display} && \ git clone {repo_path} . && \ echo '{contents}' > {file_path} && \ git add {file_path} && \ git -c user.name='test' -c user.email='test@example.com' commit -m 'test commit' && \ git push origin main", )) .stdout(Stdio::null()) .stderr(Stdio::null()) .status() .expect("Failed to commit file"); cleanup_test_dir(&test_dir); } #[test] fn test_dummy_handle() { let git = Git::new(); let can_handle = git.can_handle("anything"); assert!(can_handle); } #[test] fn test_syncs_remote_up() { let git = Git::new(); let test_dir = setup_test_dir(); let local_dir = test_dir.join("local"); let remote_dir = test_dir.join("remote"); create_dir_all(&local_dir).expect("Could not create local test directory"); create_dir_all(&remote_dir).expect("Could not create remote test directory"); create_remote(&remote_dir); let remote_dir_as_string = remote_dir.display().to_string(); create_test_file(&local_dir.join("file1.txt"), "I exist"); git.sync_up(&remote_dir_as_string, &local_dir) .expect("Could not sync up to remote"); assert_file_in_repo_with_contents(&remote_dir_as_string, "file1.txt", "I exist"); assert_file_does_not_exist(&remote_dir_as_string, "file2.txt"); create_test_file(&local_dir.join("file2.txt"), "I also exist now, btw"); git.sync_up(&remote_dir_as_string, &local_dir) .expect("Could not sync up to remote"); assert_file_in_repo_with_contents(&remote_dir_as_string, "file1.txt", "I exist"); assert_file_in_repo_with_contents( &remote_dir_as_string, "file2.txt", "I also exist now, btw", ); } #[test] fn test_remote_up_force_pushes() { let git = Git::new(); let test_dir = setup_test_dir(); let local_dir = test_dir.join("local"); let remote_dir = test_dir.join("remote"); create_dir_all(&local_dir).expect("Could not create local test directory"); create_dir_all(&remote_dir).expect("Could not create remote test directory"); create_remote(&remote_dir); let remote_dir_as_string = remote_dir.display().to_string(); commit_file_to_remote(&remote_dir_as_string, "boo.txt", "A ghost"); assert_file_does_not_exist(&remote_dir_as_string, "file1.txt"); assert_file_in_repo_with_contents(&remote_dir_as_string, "boo.txt", "A ghost"); create_test_file(&local_dir.join("file1.txt"), "I exist"); git.sync_up(&remote_dir_as_string, &local_dir) .expect("Could not sync up to remote"); assert_file_in_repo_with_contents(&remote_dir_as_string, "file1.txt", "I exist"); assert_file_does_not_exist(&remote_dir_as_string, "boo.txt"); } #[test] fn test_sync_up_fails_with_an_invalid_remote() { let git = Git::new(); let test_dir = setup_test_dir(); let local_dir = test_dir.join("local"); create_dir_all(&local_dir).expect("Could not create local test directory"); let invalid_remote = String::from("test\0"); let result = git.sync_up(&invalid_remote, &local_dir); assert!(result.is_err()); } #[test] fn test_syncs_remote_down() { let git = Git::new(); let test_dir = setup_test_dir(); let local_dir = test_dir.join("local"); let remote_dir = test_dir.join("remote"); create_dir_all(&local_dir).expect("Could not create local test directory"); create_dir_all(&remote_dir).expect("Could not create remote test directory"); create_remote(&remote_dir); let remote_dir_as_string = remote_dir.display().to_string(); commit_file_to_remote(&remote_dir_as_string, "file1.txt", "I exist, but remotely"); assert!(!&local_dir.join("file1.txt").exists()); assert!(!&local_dir.join("file2.txt").exists()); git.sync_down(&remote_dir_as_string, &local_dir) .expect("Could not sync down from remote"); assert_file_contents(&local_dir.join("file1.txt"), "I exist, but remotely"); assert!(!&local_dir.join("file2.txt").exists()); commit_file_to_remote( &remote_dir_as_string, "file2.txt", "I also exist, but remotely", ); assert!(!&local_dir.join("file2.txt").exists()); git.sync_down(&remote_dir_as_string, &local_dir) .expect("Could not sync down from remote"); assert_file_contents(&local_dir.join("file1.txt"), "I exist, but remotely"); assert_file_contents(&local_dir.join("file2.txt"), "I also exist, but remotely"); } #[test] fn test_sync_down_overwrites_local() { let git = Git::new(); let test_dir = setup_test_dir(); let local_dir = test_dir.join("local"); let remote_dir = test_dir.join("remote"); create_dir_all(&local_dir).expect("Could not create local test directory"); create_dir_all(&remote_dir).expect("Could not create remote test directory"); create_remote(&remote_dir); let remote_dir_as_string = remote_dir.display().to_string(); create_test_file(&local_dir.join("nooo.txt"), "I will no longer exist"); commit_file_to_remote(&remote_dir_as_string, "file1.txt", "I exist, but remotely"); assert!(&local_dir.join("nooo.txt").exists()); assert!(!&local_dir.join("file1.txt").exists()); git.sync_down(&remote_dir_as_string, &local_dir) .expect("Could not sync down from remote"); assert_file_contents(&local_dir.join("file1.txt"), "I exist, but remotely"); assert!(!&local_dir.join("nooo.txt").exists()); } #[test] fn test_sync_down_fails_with_an_invalid_remote() { let git = Git::new(); let test_dir = setup_test_dir(); let local_dir = test_dir.join("local"); create_dir_all(&local_dir).expect("Could not create local test directory"); let invalid_remote = String::from("test\0"); let result = git.sync_down(&invalid_remote, &local_dir); assert!(result.is_err()); } }