diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs deleted file mode 100644 index cd94931..0000000 --- a/crates/git/src/repository.rs +++ /dev/null @@ -1,249 +0,0 @@ -// -#![cfg(not(tarpaulin_include))] -use std::{ - ops::Deref as _, - sync::{atomic::AtomicBool, Arc, Mutex}, -}; - -use git_next_config::{BranchName, GitDir, Hostname, RepoPath}; -use tracing::{info, warn}; - -use crate::{fetch, push, GitRef, GitRemote}; - -use super::RepoDetails; - -#[derive(Clone, Copy, Debug)] -pub enum Repository { - Real, - Mock, -} -pub const fn new() -> Repository { - Repository::Real -} -pub const fn mock() -> Repository { - Repository::Mock -} -mod mock { - use super::*; - pub struct MockRepository; - impl RepositoryLike for MockRepository { - fn open(&self, _gitdir: &GitDir) -> Result { - Ok(open::OpenRepository::Mock) - } - - fn git_clone(&self, _repo_details: &RepoDetails) -> Result { - Ok(open::OpenRepository::Mock) - } - } -} - -pub trait RepositoryLike { - fn open(&self, gitdir: &GitDir) -> Result; - fn git_clone(&self, repo_details: &RepoDetails) -> Result; -} -impl std::ops::Deref for Repository { - type Target = dyn RepositoryLike; - - fn deref(&self) -> &Self::Target { - match self { - Self::Real => &real::RealRepository, - Self::Mock => &mock::MockRepository, - } - } -} -mod real { - use super::*; - pub struct RealRepository; - impl RepositoryLike for RealRepository { - fn open(&self, gitdir: &GitDir) -> Result { - Ok(open::OpenRepository::Real(open::RealOpenRepository::new( - Arc::new(Mutex::new( - gix::ThreadSafeRepository::open(gitdir.to_path_buf())?.to_thread_local(), - )), - ))) - } - - fn git_clone(&self, repo_details: &RepoDetails) -> Result { - use secrecy::ExposeSecret; - let origin = repo_details.origin(); - let (repository, _outcome) = gix::prepare_clone_bare( - origin.expose_secret().as_str(), - repo_details.gitdir.deref(), - )? - .fetch_only(gix::progress::Discard, &AtomicBool::new(false))?; - - Ok(open::OpenRepository::Real(open::RealOpenRepository::new( - Arc::new(Mutex::new(repository)), - ))) - } - } -} - -pub mod open { - use super::*; - #[derive(Clone, Debug)] - pub enum OpenRepository { - Real(RealOpenRepository), - Mock, // TODO: (#38) contain a mock model of a repo - } - pub trait OpenRepositoryLike { - fn find_default_remote(&self, direction: Direction) -> Option; - fn fetch(&self) -> Result<(), fetch::Error>; - fn push( - &self, - repo_details: &RepoDetails, - branch_name: BranchName, - to_commit: GitRef, - force: push::Force, - ) -> Result<(), push::Error>; - } - impl std::ops::Deref for OpenRepository { - type Target = dyn OpenRepositoryLike; - - fn deref(&self) -> &Self::Target { - match self { - Self::Real(real) => real, - Self::Mock => todo!(), - } - } - } - - #[derive(Clone, Debug, derive_more::Constructor)] - pub struct RealOpenRepository(Arc>); - impl OpenRepositoryLike for RealOpenRepository { - fn find_default_remote(&self, direction: Direction) -> Option { - let Ok(repository) = self.0.lock() else { - return None; - }; - let Some(Ok(remote)) = repository.find_default_remote(direction.into()) else { - return None; - }; - let url = remote.url(direction.into())?; - let host = url.host()?; - let path = url.path.to_string(); - let path = path.strip_prefix('/').map_or(path.as_str(), |path| path); - let path = path.strip_suffix(".git").map_or(path, |path| path); - Some(GitRemote::new( - Hostname::new(host), - RepoPath::new(path.to_string()), - )) - } - - fn fetch(&self) -> Result<(), fetch::Error> { - let Ok(repository) = self.0.lock() else { - return Err(fetch::Error::Lock); - }; - let Some(Ok(remote)) = repository.find_default_remote(Direction::Fetch.into()) else { - return Err(fetch::Error::NoFetchRemoteFound); - }; - remote - .connect(gix::remote::Direction::Fetch) - .map_err(|e| fetch::Error::Connect(e.to_string()))? - .prepare_fetch(gix::progress::Discard, Default::default()) - .map_err(|e| fetch::Error::Fetch(e.to_string()))? - .receive(gix::progress::Discard, &Default::default()) - .map_err(|e| fetch::Error::Fetch(e.to_string()))?; - - Ok(()) - } - - // TODO: (#72) reimplement using `gix` - fn push( - &self, - repo_details: &RepoDetails, - branch_name: BranchName, - to_commit: GitRef, - force: push::Force, - ) -> Result<(), push::Error> { - let origin = repo_details.origin(); - let force = match force { - push::Force::No => "".to_string(), - push::Force::From(old_ref) => format!("--force-with-lease={branch_name}:{old_ref}"), - }; - // INFO: never log the command as it contains the API token within the 'origin' - use secrecy::ExposeSecret; - let command: secrecy::Secret = format!( - "/usr/bin/git push {} {to_commit}:{branch_name} {force}", - origin.expose_secret() - ) - .into(); - - let git_dir = self - .0 - .lock() - .map_err(|_| push::Error::Lock) - .map(|r| r.git_dir().to_path_buf())?; - - let ctx = gix::diff::command::Context { - git_dir: Some(git_dir.clone()), - ..Default::default() - }; - match gix::command::prepare(command.expose_secret()) - .with_context(ctx) - .with_shell_allow_argument_splitting() - .stdout(std::process::Stdio::null()) - .stderr(std::process::Stdio::null()) - .spawn() - { - Ok(mut child) => match child.wait() { - Ok(_) => { - info!("Branch updated"); - Ok(()) - } - Err(err) => { - warn!(?err, ?git_dir, "Failed (wait)"); - Err(push::Error::Push) - } - }, - Err(err) => { - warn!(?err, ?git_dir, "Failed (spawn)"); - Err(push::Error::Push) - } - } - } - } -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum Direction { - /// Push local changes to the remote. - Push, - /// Fetch changes from the remote to the local repository. - Fetch, -} -impl From for gix::remote::Direction { - fn from(value: Direction) -> Self { - match value { - Direction::Push => Self::Push, - Direction::Fetch => Self::Fetch, - } - } -} - -#[derive(Debug, derive_more::Display)] -pub enum Error { - InvalidGitDir(git_next_config::GitDir), - Io(std::io::Error), - Wait(std::io::Error), - Spawn(std::io::Error), - Validation(String), - GixClone(Box), - GixOpen(Box), - GixFetch(Box), -} -impl std::error::Error for Error {} -impl From for Error { - fn from(value: gix::clone::Error) -> Self { - Self::GixClone(Box::new(value)) - } -} -impl From for Error { - fn from(value: gix::open::Error) -> Self { - Self::GixOpen(Box::new(value)) - } -} -impl From for Error { - fn from(value: gix::clone::fetch::Error) -> Self { - Self::GixFetch(Box::new(value)) - } -} diff --git a/crates/git/src/repository/mock.rs b/crates/git/src/repository/mock.rs new file mode 100644 index 0000000..0617744 --- /dev/null +++ b/crates/git/src/repository/mock.rs @@ -0,0 +1,11 @@ +use super::*; +pub struct MockRepository; +impl RepositoryLike for MockRepository { + fn open(&self, _gitdir: &GitDir) -> Result { + Ok(open::OpenRepository::Mock) + } + + fn git_clone(&self, _repo_details: &RepoDetails) -> Result { + Ok(open::OpenRepository::Mock) + } +} diff --git a/crates/git/src/repository/mod.rs b/crates/git/src/repository/mod.rs new file mode 100644 index 0000000..d6dc5ec --- /dev/null +++ b/crates/git/src/repository/mod.rs @@ -0,0 +1,89 @@ +// +#![cfg(not(tarpaulin_include))] + +mod mock; +pub mod open; +mod real; + +use std::{ + ops::Deref as _, + sync::{atomic::AtomicBool, Arc, Mutex}, +}; + +use git_next_config::{BranchName, GitDir, Hostname, RepoPath}; +use tracing::{info, warn}; + +use crate::{fetch, push, GitRef, GitRemote}; + +use super::RepoDetails; + +#[derive(Clone, Copy, Debug)] +pub enum Repository { + Real, + Mock, +} +pub const fn new() -> Repository { + Repository::Real +} +pub const fn mock() -> Repository { + Repository::Mock +} + +pub trait RepositoryLike { + fn open(&self, gitdir: &GitDir) -> Result; + fn git_clone(&self, repo_details: &RepoDetails) -> Result; +} +impl std::ops::Deref for Repository { + type Target = dyn RepositoryLike; + + fn deref(&self) -> &Self::Target { + match self { + Self::Real => &real::RealRepository, + Self::Mock => &mock::MockRepository, + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum Direction { + /// Push local changes to the remote. + Push, + /// Fetch changes from the remote to the local repository. + Fetch, +} +impl From for gix::remote::Direction { + fn from(value: Direction) -> Self { + match value { + Direction::Push => Self::Push, + Direction::Fetch => Self::Fetch, + } + } +} + +#[derive(Debug, derive_more::Display)] +pub enum Error { + InvalidGitDir(git_next_config::GitDir), + Io(std::io::Error), + Wait(std::io::Error), + Spawn(std::io::Error), + Validation(String), + GixClone(Box), + GixOpen(Box), + GixFetch(Box), +} +impl std::error::Error for Error {} +impl From for Error { + fn from(value: gix::clone::Error) -> Self { + Self::GixClone(Box::new(value)) + } +} +impl From for Error { + fn from(value: gix::open::Error) -> Self { + Self::GixOpen(Box::new(value)) + } +} +impl From for Error { + fn from(value: gix::clone::fetch::Error) -> Self { + Self::GixFetch(Box::new(value)) + } +} diff --git a/crates/git/src/repository/open.rs b/crates/git/src/repository/open.rs new file mode 100644 index 0000000..6184e7f --- /dev/null +++ b/crates/git/src/repository/open.rs @@ -0,0 +1,122 @@ +use super::*; +#[derive(Clone, Debug)] +pub enum OpenRepository { + Real(RealOpenRepository), + Mock, // TODO: (#38) contain a mock model of a repo +} +pub trait OpenRepositoryLike { + fn find_default_remote(&self, direction: Direction) -> Option; + fn fetch(&self) -> Result<(), fetch::Error>; + fn push( + &self, + repo_details: &RepoDetails, + branch_name: BranchName, + to_commit: GitRef, + force: push::Force, + ) -> Result<(), push::Error>; +} +impl std::ops::Deref for OpenRepository { + type Target = dyn OpenRepositoryLike; + + fn deref(&self) -> &Self::Target { + match self { + Self::Real(real) => real, + Self::Mock => todo!(), + } + } +} + +#[derive(Clone, Debug, derive_more::Constructor)] +pub struct RealOpenRepository(Arc>); +impl OpenRepositoryLike for RealOpenRepository { + fn find_default_remote(&self, direction: Direction) -> Option { + let Ok(repository) = self.0.lock() else { + return None; + }; + let Some(Ok(remote)) = repository.find_default_remote(direction.into()) else { + return None; + }; + let url = remote.url(direction.into())?; + let host = url.host()?; + let path = url.path.to_string(); + let path = path.strip_prefix('/').map_or(path.as_str(), |path| path); + let path = path.strip_suffix(".git").map_or(path, |path| path); + Some(GitRemote::new( + Hostname::new(host), + RepoPath::new(path.to_string()), + )) + } + + fn fetch(&self) -> Result<(), fetch::Error> { + let Ok(repository) = self.0.lock() else { + return Err(fetch::Error::Lock); + }; + let Some(Ok(remote)) = repository.find_default_remote(Direction::Fetch.into()) else { + return Err(fetch::Error::NoFetchRemoteFound); + }; + remote + .connect(gix::remote::Direction::Fetch) + .map_err(|e| fetch::Error::Connect(e.to_string()))? + .prepare_fetch(gix::progress::Discard, Default::default()) + .map_err(|e| fetch::Error::Fetch(e.to_string()))? + .receive(gix::progress::Discard, &Default::default()) + .map_err(|e| fetch::Error::Fetch(e.to_string()))?; + + Ok(()) + } + + // TODO: (#72) reimplement using `gix` + fn push( + &self, + repo_details: &RepoDetails, + branch_name: BranchName, + to_commit: GitRef, + force: push::Force, + ) -> Result<(), push::Error> { + let origin = repo_details.origin(); + let force = match force { + push::Force::No => "".to_string(), + push::Force::From(old_ref) => format!("--force-with-lease={branch_name}:{old_ref}"), + }; + // INFO: never log the command as it contains the API token within the 'origin' + use secrecy::ExposeSecret; + let command: secrecy::Secret = format!( + "/usr/bin/git push {} {to_commit}:{branch_name} {force}", + origin.expose_secret() + ) + .into(); + + let git_dir = self + .0 + .lock() + .map_err(|_| push::Error::Lock) + .map(|r| r.git_dir().to_path_buf())?; + + let ctx = gix::diff::command::Context { + git_dir: Some(git_dir.clone()), + ..Default::default() + }; + match gix::command::prepare(command.expose_secret()) + .with_context(ctx) + .with_shell_allow_argument_splitting() + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .spawn() + { + Ok(mut child) => match child.wait() { + Ok(_) => { + info!("Branch updated"); + Ok(()) + } + Err(err) => { + warn!(?err, ?git_dir, "Failed (wait)"); + Err(push::Error::Push) + } + }, + Err(err) => { + warn!(?err, ?git_dir, "Failed (spawn)"); + Err(push::Error::Push) + } + } + } +} diff --git a/crates/git/src/repository/real.rs b/crates/git/src/repository/real.rs new file mode 100644 index 0000000..96df79c --- /dev/null +++ b/crates/git/src/repository/real.rs @@ -0,0 +1,23 @@ +use super::*; +pub struct RealRepository; +impl RepositoryLike for RealRepository { + fn open(&self, gitdir: &GitDir) -> Result { + Ok(open::OpenRepository::Real(open::RealOpenRepository::new( + Arc::new(Mutex::new( + gix::ThreadSafeRepository::open(gitdir.to_path_buf())?.to_thread_local(), + )), + ))) + } + + fn git_clone(&self, repo_details: &RepoDetails) -> Result { + use secrecy::ExposeSecret; + let origin = repo_details.origin(); + let (repository, _outcome) = + gix::prepare_clone_bare(origin.expose_secret().as_str(), repo_details.gitdir.deref())? + .fetch_only(gix::progress::Discard, &AtomicBool::new(false))?; + + Ok(open::OpenRepository::Real(open::RealOpenRepository::new( + Arc::new(Mutex::new(repository)), + ))) + } +}