use self::repository::GitRepository;
use super::{Check, CheckError};
use std::fmt::Debug;
use thiserror::Error;
mod credentials;
mod repository;
pub struct GitCheck(pub GitRepository);
#[derive(Debug, Error)]
pub enum GitError {
#[error("{0} does not exist or not a git repository")]
NotAGitRepository(String),
#[error("repository is not on a branch")]
NotOnABranch,
#[error("branch {0} doesn't have a remote")]
NoRemoteForBranch(String),
#[error("there are uncommited changes in the directory")]
DirtyWorkingTree,
#[error("cannot load git config")]
ConfigLoadingFailed,
#[error("cannot fetch, might be a network error")]
FetchFailed,
#[error("cannot update branch, this is likely a merge conflict")]
MergeConflict,
#[error("could not set HEAD to fetch commit {0}")]
FailedSettingHead(String),
}
impl From<GitError> for CheckError {
fn from(value: GitError) -> Self {
match value {
GitError::NotAGitRepository(_)
| GitError::NotOnABranch
| GitError::NoRemoteForBranch(_) => CheckError::Misconfigured(value.to_string()),
GitError::ConfigLoadingFailed => CheckError::PermissionDenied(value.to_string()),
GitError::DirtyWorkingTree | GitError::MergeConflict => {
CheckError::Conflict(value.to_string())
}
GitError::FetchFailed | GitError::FailedSettingHead(_) => {
CheckError::FailedUpdate(value.to_string())
}
}
}
}
impl GitCheck {
pub fn open(directory: &str) -> Result<Self, CheckError> {
let repo = GitRepository::open(directory)?;
Ok(GitCheck(repo))
}
fn check_inner(&mut self) -> Result<bool, GitError> {
let GitCheck(repo) = self;
let fetch_commit = repo.fetch()?;
if repo.check_if_updatable(&fetch_commit)? && repo.pull(&fetch_commit)? {
Ok(true)
} else {
Ok(false)
}
}
}
impl Check for GitCheck {
fn check(&mut self) -> Result<bool, CheckError> {
let update_successful = self.check_inner()?;
Ok(update_successful)
}
}
#[cfg(test)]
mod tests {
use super::*;
use duct::cmd;
use rand::distributions::{Alphanumeric, DistString};
use std::{error::Error, fs, path::Path};
fn get_random_id() -> String {
Alphanumeric.sample_string(&mut rand::thread_rng(), 16)
}
fn create_empty_repository(local: &str) -> Result<(), Box<dyn Error>> {
let remote = format!("{local}-remote");
fs::create_dir(&remote)?;
cmd!("git", "init", "--bare").dir(&remote).read()?;
cmd!("git", "clone", &remote, &local).read()?;
fs::write(format!("{local}/1"), "1")?;
cmd!("git", "add", "-A").dir(local).read()?;
cmd!("git", "commit", "-m1").dir(local).read()?;
cmd!("git", "push", "origin", "master").dir(local).read()?;
Ok(())
}
fn create_other_repository(local: &str) -> Result<(), Box<dyn Error>> {
let remote = format!("{local}-remote");
let other = format!("{local}-other");
cmd!("git", "clone", &remote, &other).read()?;
fs::write(format!("{other}/2"), "2")?;
cmd!("git", "add", "-A").dir(&other).read()?;
cmd!("git", "commit", "-m1").dir(&other).read()?;
cmd!("git", "push", "origin", "master").dir(other).read()?;
Ok(())
}
fn create_tag(path: &str, tag: &str) -> Result<(), Box<dyn Error>> {
cmd!("git", "tag", tag).dir(path).read()?;
cmd!("git", "push", "--tags").dir(path).read()?;
Ok(())
}
fn get_tags(path: &str) -> Result<String, Box<dyn Error>> {
let tags = cmd!("git", "tag", "-l").dir(path).read()?;
Ok(tags)
}
fn cleanup_repository(local: &str) -> Result<(), Box<dyn Error>> {
let remote = format!("{local}-remote");
let other = format!("{local}-other");
fs::remove_dir_all(local)?;
if Path::new(&remote).exists() {
fs::remove_dir_all(remote)?;
}
if Path::new(&other).exists() {
fs::remove_dir_all(other)?;
}
Ok(())
}
fn create_failing_repository(local: &str, create_commit: bool) -> Result<(), Box<dyn Error>> {
fs::create_dir(local)?;
cmd!("git", "init").dir(local).read()?;
if create_commit {
fs::write(format!("{local}/1"), "1")?;
cmd!("git", "add", "-A").dir(local).read()?;
cmd!("git", "commit", "-m1").dir(local).read()?;
}
Ok(())
}
fn create_merge_conflict(local: &str) -> Result<(), Box<dyn Error>> {
let other = format!("{local}-other");
fs::write(format!("{local}/1"), "11")?;
cmd!("git", "add", "-A").dir(local).read()?;
cmd!("git", "commit", "-m1").dir(local).read()?;
fs::write(format!("{other}/1"), "12")?;
cmd!("git", "add", "-A").dir(&other).read()?;
cmd!("git", "commit", "-m2").dir(other).read()?;
Ok(())
}
#[test]
fn it_should_open_a_repository() -> Result<(), Box<dyn Error>> {
let id = get_random_id();
let local = format!("test_directories/{id}");
create_empty_repository(&local)?;
let _ = GitCheck::open(&local)?;
cleanup_repository(&local)?;
Ok(())
}
#[test]
fn it_should_fail_if_path_is_invalid() -> Result<(), Box<dyn Error>> {
let error = GitCheck::open("/path/to/nowhere").err().unwrap();
assert!(
matches!(error, CheckError::Misconfigured(_)),
"{error:?} should be Misconfigured"
);
Ok(())
}
#[test]
fn it_should_fail_if_we_are_not_on_a_branch() -> Result<(), Box<dyn Error>> {
let id = get_random_id();
let local = format!("test_directories/{id}");
create_failing_repository(&local, false)?;
let mut check: GitCheck = GitCheck::open(&local)?;
let error = check.check_inner().err().unwrap();
assert!(
matches!(error, GitError::NotOnABranch),
"{error:?} should be NotOnABranch"
);
cleanup_repository(&local)?;
Ok(())
}
#[test]
fn it_should_fail_if_there_is_no_remote() -> Result<(), Box<dyn Error>> {
let id = get_random_id();
let local = format!("test_directories/{id}");
create_failing_repository(&local, true)?;
let mut check: GitCheck = GitCheck::open(&local)?;
let error = check.check_inner().err().unwrap();
assert!(
matches!(error, GitError::NoRemoteForBranch(_)),
"{error:?} should be NoRemoteForBranch"
);
cleanup_repository(&local)?;
Ok(())
}
#[test]
fn it_should_return_false_if_the_remote_didnt_change() -> Result<(), Box<dyn Error>> {
let id = get_random_id();
let local = format!("test_directories/{id}");
create_empty_repository(&local)?;
let mut check = GitCheck::open(&local)?;
let is_pulled = check.check_inner()?;
assert!(!is_pulled);
cleanup_repository(&local)?;
Ok(())
}
#[test]
fn it_should_return_true_if_the_remote_changes() -> Result<(), Box<dyn Error>> {
let id = get_random_id();
let local = format!("test_directories/{id}");
create_empty_repository(&local)?;
create_other_repository(&local)?;
let mut check = GitCheck::open(&local)?;
let is_pulled = check.check_inner()?;
assert!(is_pulled);
assert!(Path::new(&format!("{local}/2")).exists());
cleanup_repository(&local)?;
Ok(())
}
#[test]
fn it_should_return_true_if_the_remote_changes_even_with_tags() -> Result<(), Box<dyn Error>> {
let id = get_random_id();
let local = format!("test_directories/{id}");
create_empty_repository(&local)?;
create_other_repository(&local)?;
create_tag(&format!("{local}-other"), "v0.1.0")?;
let mut check = GitCheck::open(&local)?;
let is_pulled = check.check_inner()?;
assert!(is_pulled);
assert!(Path::new(&format!("{local}/2")).exists());
let tags = get_tags(&local)?;
assert_eq!(tags, "v0.1.0");
cleanup_repository(&local)?;
Ok(())
}
#[test]
fn it_should_fail_if_the_working_tree_is_dirty() -> Result<(), Box<dyn Error>> {
let id = get_random_id();
let local = format!("test_directories/{id}");
create_empty_repository(&local)?;
create_other_repository(&local)?;
fs::write(format!("{local}/1"), "22")?;
let mut check = GitCheck::open(&local)?;
let error = check.check_inner().err().unwrap();
assert!(
matches!(error, GitError::DirtyWorkingTree),
"{error:?} should be DirtyWorkingTree"
);
assert!(!Path::new(&format!("{local}/2")).exists());
cleanup_repository(&local)?;
Ok(())
}
#[test]
fn it_should_fail_if_there_is_a_merge_conflict() -> Result<(), Box<dyn Error>> {
let id = get_random_id();
let local = format!("test_directories/{id}");
create_empty_repository(&local)?;
create_other_repository(&local)?;
create_merge_conflict(&local)?;
let mut check = GitCheck::open(&local)?;
let error = check.check_inner().err().unwrap();
assert!(
matches!(error, GitError::MergeConflict),
"{error:?} should be MergeConflict"
);
cleanup_repository(&local)?;
Ok(())
}
#[test]
fn it_should_fail_if_repository_is_not_accessible() -> Result<(), Box<dyn Error>> {
let id = get_random_id();
let local = format!("test_directories/{id}");
create_empty_repository(&local)?;
create_other_repository(&local)?;
let mut perms = fs::metadata(&local)?.permissions();
perms.set_readonly(true);
fs::set_permissions(&local, perms)?;
let mut check: GitCheck = GitCheck::open(&local)?;
let error = check.check_inner().err().unwrap();
assert!(
matches!(error, GitError::FailedSettingHead(_)),
"{error:?} should be FailedSettingHead"
);
let mut perms = fs::metadata(&local)?.permissions();
#[allow(clippy::permissions_set_readonly_false)]
perms.set_readonly(false);
fs::set_permissions(&local, perms)?;
cleanup_repository(&local)?;
Ok(())
}
}