use std::io::{Error, 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, remote: &str) -> bool { match Command::new("git") .args(["ls-remote", remote]) .stdout(Stdio::null()) .stderr(Stdio::null()) .status() { Ok(status) => status.success(), _ => false, } } fn sync_up(&self, remote: &str, directory: &Path) -> Result<()> { let timestamp = SystemTime::now() .duration_since(UNIX_EPOCH) .map_err(|_| Error::other("Invalid time"))? .as_millis(); let commit_name = format!("blog-sync-up-{timestamp}"); let command_arguments = vec![ vec!["init", "-b", "main"], vec!["add", "."], vec!["commit", "--allow-empty", "-m", &commit_name], vec!["push", &remote, "main", "--force"], ]; for arguments in command_arguments { let status = Command::new("git") .current_dir(directory) .args(&arguments) .stdout(Stdio::null()) .stderr(Stdio::null()) .status() .map_err(|_| Error::other("Failed while performing sync up with git"))?; if !status.success() { return Err(Error::other("Failed while performing sync up with git")); } } Ok(()) } fn sync_down(&self, remote: &str, directory: &Path) -> Result<()> { let command_arguments = vec![ vec!["init", "-b", "main"], vec!["reset", "--hard"], vec!["clean", ".", "-f"], vec!["pull", &remote, "main"], ]; for arguments in command_arguments { let status = Command::new("git") .current_dir(directory) .args(&arguments) .stdout(Stdio::null()) .stderr(Stdio::null()) .status() .map_err(|_| Error::other("Failed while performing sync down with git"))?; if !status.success() { return Err(Error::other("Failed while performing sync down with git")); } } Ok(()) } } #[cfg(test)] mod tests { use std::fs::create_dir_all; use super::*; use crate::remote::Remote; use test_utilities::*; #[test] fn test_can_handle_git_remotes() { let git = Git::new(); let test_dir = setup_test_dir(); let remote_dir = test_dir.join("remote"); let remote_dir_as_string = remote_dir.display().to_string(); create_dir_all(&remote_dir).expect("Could not create remote test directory"); create_remote(&remote_dir); assert!(git.can_handle(&remote_dir_as_string)); cleanup_test_dir(&test_dir); } #[test] fn test_can_not_handle_not_git_remotes() { let git = Git::new(); let test_dir = setup_test_dir(); let remote_dir = test_dir.join("remote"); let remote_dir_as_string = remote_dir.display().to_string(); create_dir_all(&remote_dir).expect("Could not create remote test directory"); assert!(!git.can_handle(&remote_dir_as_string)); cleanup_test_dir(&test_dir); } #[test] fn test_can_not_handle_invalid_paths() { let git = Git::new(); let invalid_remote = String::from("test\0"); assert!(!git.can_handle(&invalid_remote)); } #[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_in_repo_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", ); cleanup_test_dir(&test_dir); } #[test] fn test_remote_up_force_pushes() { let git = Git::new(); let test_dir = setup_test_dir(); let local_dir = test_dir.join("gitlocal"); let remote_dir = test_dir.join("gitremote"); 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_in_repo_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_in_repo_does_not_exist(&remote_dir_as_string, "boo.txt"); cleanup_test_dir(&test_dir); } #[test] fn test_sync_up_fails_with_an_invalid_string() { 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()); cleanup_test_dir(&test_dir); } #[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("my guy does not exist"); let result = git.sync_up(&invalid_remote, &local_dir); assert!(result.is_err()); cleanup_test_dir(&test_dir); } #[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"); cleanup_test_dir(&test_dir); } #[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()); cleanup_test_dir(&test_dir); } #[test] fn test_sync_down_fails_with_an_invalid_string() { 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()); cleanup_test_dir(&test_dir); } #[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("thisremotedoesnotexist"); let result = git.sync_down(&invalid_remote, &local_dir); assert!(result.is_err()); cleanup_test_dir(&test_dir); } }