refactor: create a RepositoryFactory trait
All checks were successful
Rust / build (push) Successful in 1m14s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful

This commit is contained in:
Paul Campbell 2024-06-19 16:40:10 +01:00
parent ea20afee12
commit 94ad2c441c
22 changed files with 481 additions and 401 deletions

View file

@ -96,3 +96,4 @@ tokio = { version = "1.37", features = ["rt", "macros"] }
assert2 = "0.3" assert2 = "0.3"
pretty_assertions = "1.4" pretty_assertions = "1.4"
rand = "0.8" rand = "0.8"
mockall = "0.12"

View file

@ -30,7 +30,7 @@ enum Server {
async fn main() { async fn main() {
let fs = fs::new(PathBuf::default()); let fs = fs::new(PathBuf::default());
let net = Network::new_real(); let net = Network::new_real();
let repo = git_next_git::repository::new(); let repo = git_next_git::repository::real();
let commands = Commands::parse(); let commands = Commands::parse();
match commands.command { match commands.command {

View file

@ -51,12 +51,15 @@ actix = { workspace = true }
# actix-rt = { workspace = true } # actix-rt = { workspace = true }
# tokio = { workspace = true } # tokio = { workspace = true }
mockall = { workspace = true }
[dev-dependencies] [dev-dependencies]
# Testing # Testing
assert2 = { workspace = true } assert2 = { workspace = true }
rand = { workspace = true } rand = { workspace = true }
pretty_assertions = { workspace = true } pretty_assertions = { workspace = true }
[lints.clippy] [lints.clippy]
nursery = { level = "warn", priority = -1 } nursery = { level = "warn", priority = -1 }
# pedantic = "warn" # pedantic = "warn"

View file

@ -47,7 +47,7 @@ pub enum Error {
} }
pub fn reset( pub fn reset(
repository: &git::OpenRepository, repository: &dyn git::repository::OpenRepositoryLike,
repo_details: &git::RepoDetails, repo_details: &git::RepoDetails,
branch_name: &config::BranchName, branch_name: &config::BranchName,
to_commit: &git::GitRef, to_commit: &git::GitRef,

View file

@ -10,10 +10,10 @@ use crate as git;
use git_next_config as config; use git_next_config as config;
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]
pub struct MockRepository { pub struct FakeRepository {
open_repos: Arc<Mutex<HashMap<config::GitDir, git::repository::MockOpenRepository>>>, open_repos: Arc<Mutex<HashMap<config::GitDir, git::repository::FakeOpenRepository>>>,
} }
impl MockRepository { impl FakeRepository {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
open_repos: Default::default(), open_repos: Default::default(),
@ -23,8 +23,8 @@ impl MockRepository {
pub fn given_can_be_opened( pub fn given_can_be_opened(
&mut self, &mut self,
gitdir: &config::GitDir, gitdir: &config::GitDir,
) -> git::repository::MockOpenRepository { ) -> git::repository::FakeOpenRepository {
let open_repo = git::repository::MockOpenRepository::new(); let open_repo = git::repository::FakeOpenRepository::new();
#[allow(clippy::unwrap_used)] #[allow(clippy::unwrap_used)]
self.open_repos self.open_repos
.lock() .lock()
@ -34,20 +34,20 @@ impl MockRepository {
} }
pub fn seal(self) -> (git::Repository, Self) { pub fn seal(self) -> (git::Repository, Self) {
(git::Repository::Mock(self.clone()), self) (git::Repository::Fake(self.clone()), self)
} }
pub fn unseal(self, _repository: git::Repository) -> Self { pub fn unseal(self, _repository: git::Repository) -> Self {
// drop repository to allow same mutable access to mock repository // drop repository to allow same mutable access to mock repository
self self
} }
pub fn get(&self, gitdir: &config::GitDir) -> Option<git::repository::MockOpenRepository> { pub fn get(&self, gitdir: &config::GitDir) -> Option<git::repository::FakeOpenRepository> {
self.open_repos self.open_repos
.lock() .lock()
.map(|or| or.get(gitdir).cloned()) .map(|or| or.get(gitdir).cloned())
.unwrap_or(None) .unwrap_or(None)
} }
} }
impl git::repository::RepositoryLike for MockRepository { impl git::repository::RepositoryLike for FakeRepository {
fn open( fn open(
&self, &self,
gitdir: &config::GitDir, gitdir: &config::GitDir,
@ -55,7 +55,7 @@ impl git::repository::RepositoryLike for MockRepository {
#[allow(clippy::unwrap_used)] #[allow(clippy::unwrap_used)]
self.open_repos self.open_repos
.lock() .lock()
.map_err(|_| crate::repository::Error::MockLock) .map_err(|_| crate::repository::Error::FakeLock)
.map(|or| or.get(gitdir).cloned()) .map(|or| or.get(gitdir).cloned())
.transpose() .transpose()
.unwrap_or_else(|| Err(crate::repository::Error::InvalidGitDir(gitdir.clone()))) .unwrap_or_else(|| Err(crate::repository::Error::InvalidGitDir(gitdir.clone())))

View file

@ -1,12 +1,16 @@
// //
#[cfg(test)] #[cfg(test)]
mod mock; mod fake;
#[cfg(test)] use std::sync::{atomic::AtomicBool, Arc, Mutex};
pub use mock::MockRepository;
#[cfg(test)]
pub use open::MockOpenRepository;
mod open; use derive_more::Deref as _;
#[cfg(test)]
pub use fake::FakeRepository;
#[cfg(test)]
pub use open::FakeOpenRepository;
pub mod open;
mod real; mod real;
mod test; mod test;
@ -26,7 +30,6 @@ pub use real::RealRepository;
use tracing::info; use tracing::info;
use crate::repository::test::TestRepository; use crate::repository::test::TestRepository;
use crate::validation::repo::validate_repo; use crate::validation::repo::validate_repo;
use super::RepoDetails; use super::RepoDetails;
@ -36,17 +39,18 @@ use super::RepoDetails;
pub enum Repository { pub enum Repository {
Real, Real,
#[cfg(test)] #[cfg(test)]
Mock(MockRepository), Fake(FakeRepository),
Test(TestRepository), Test(TestRepository),
} }
#[deprecated(note = "use git::repository::real()")]
pub const fn new() -> Repository { pub const fn new() -> Repository {
Repository::Real Repository::Real
} }
#[cfg(test)] #[cfg(test)]
pub fn mock() -> MockRepository { pub fn fake() -> FakeRepository {
MockRepository::new() FakeRepository::new()
} }
pub const fn test(fs: kxio::fs::FileSystem) -> TestRepository { pub const fn test(fs: kxio::fs::FileSystem) -> TestRepository {
@ -62,11 +66,11 @@ pub const fn test(fs: kxio::fs::FileSystem) -> TestRepository {
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
#[cfg(not(tarpaulin_include))] // requires network access to either clone new and/or fetch. #[cfg(not(tarpaulin_include))] // requires network access to either clone new and/or fetch.
pub fn open( pub fn open(
repository: &Repository, repository: &dyn RepositoryFactory,
repo_details: &RepoDetails, repo_details: &RepoDetails,
gitdir: config::GitDir, gitdir: config::GitDir,
) -> Result<OpenRepository> { ) -> Result<Box<dyn OpenRepositoryLike>> {
let repository = if !gitdir.exists() { let open_repository = if !gitdir.exists() {
info!("Local copy not found - cloning..."); info!("Local copy not found - cloning...");
repository.git_clone(repo_details)? repository.git_clone(repo_details)?
} else { } else {
@ -74,8 +78,50 @@ pub fn open(
repository.open(&gitdir)? repository.open(&gitdir)?
}; };
info!("Validating..."); info!("Validating...");
validate_repo(&repository, repo_details).map_err(|e| Error::Validation(e.to_string()))?; validate_repo(&*open_repository, repo_details).map_err(|e| Error::Validation(e.to_string()))?;
Ok(repository) Ok(open_repository)
}
#[mockall::automock]
pub trait RepositoryFactory: std::fmt::Debug + Sync + Send {
fn duplicate(&self) -> Box<dyn RepositoryFactory>;
fn open(&self, gitdir: &GitDir) -> Result<Box<dyn OpenRepositoryLike>>;
fn git_clone(&self, repo_details: &RepoDetails) -> Result<Box<dyn OpenRepositoryLike>>;
}
pub fn real() -> Box<dyn RepositoryFactory> {
Box::new(RealRepositoryFactory)
}
pub fn mock() -> Box<MockRepositoryFactory> {
Box::new(MockRepositoryFactory::new())
}
#[derive(Debug, Clone)]
struct RealRepositoryFactory;
impl RepositoryFactory for RealRepositoryFactory {
fn open(&self, gitdir: &GitDir) -> Result<Box<dyn OpenRepositoryLike>> {
let gix_repo = gix::ThreadSafeRepository::open(gitdir.to_path_buf())?.to_thread_local();
let repo = RealOpenRepository::new(Arc::new(Mutex::new(gix_repo)));
Ok(Box::new(repo))
}
fn git_clone(&self, repo_details: &RepoDetails) -> Result<Box<dyn OpenRepositoryLike>> {
tracing::info!("creating");
use secrecy::ExposeSecret;
let (gix_repo, _outcome) = gix::prepare_clone_bare(
repo_details.origin().expose_secret().as_str(),
repo_details.gitdir.deref(),
)?
.fetch_only(gix::progress::Discard, &AtomicBool::new(false))?;
tracing::info!("created");
let repo = RealOpenRepository::new(Arc::new(Mutex::new(gix_repo)));
Ok(Box::new(repo))
}
fn duplicate(&self) -> Box<dyn RepositoryFactory> {
Box::new(self.clone())
}
} }
pub trait RepositoryLike { pub trait RepositoryLike {
@ -91,7 +137,7 @@ impl std::ops::Deref for Repository {
Self::Test(test_repository) => test_repository, Self::Test(test_repository) => test_repository,
#[cfg(test)] #[cfg(test)]
Self::Mock(mock_repository) => mock_repository, Self::Fake(mock_repository) => mock_repository,
} }
} }
} }
@ -140,8 +186,8 @@ pub enum Error {
#[error("git fetch: {0}")] #[error("git fetch: {0}")]
Fetch(String), Fetch(String),
#[error("mock lock")] #[error("fake repository lock")]
MockLock, FakeLock,
} }
mod gix_errors { mod gix_errors {

View file

@ -8,7 +8,7 @@ pub mod oreal;
pub mod otest; pub mod otest;
#[cfg(test)] #[cfg(test)]
pub mod omock; pub mod ofake;
use std::{ use std::{
path::Path, path::Path,
@ -19,7 +19,7 @@ use crate as git;
use git::repository::Direction; use git::repository::Direction;
use git_next_config as config; use git_next_config as config;
#[cfg(test)] #[cfg(test)]
pub use omock::MockOpenRepository; pub use ofake::FakeOpenRepository;
pub use oreal::RealOpenRepository; pub use oreal::RealOpenRepository;
pub use otest::TestOpenRepository; pub use otest::TestOpenRepository;
@ -45,7 +45,7 @@ pub enum OpenRepository {
/// variant is ready for use, tests should be converted to using /// variant is ready for use, tests should be converted to using
/// that instead. /// that instead.
#[cfg(test)] #[cfg(test)]
Mock(git::repository::MockOpenRepository), // TODO: (#38) contain a mock model of a repo Mock(git::repository::FakeOpenRepository), // TODO: (#38) contain a mock model of a repo
} }
pub fn real(gix_repo: gix::Repository) -> OpenRepository { pub fn real(gix_repo: gix::Repository) -> OpenRepository {
@ -74,7 +74,9 @@ pub fn test_bare(
OpenRepository::Test(TestOpenRepository::new_bare(gitdir, fs, on_fetch, on_push)) OpenRepository::Test(TestOpenRepository::new_bare(gitdir, fs, on_fetch, on_push))
} }
pub trait OpenRepositoryLike { #[mockall::automock]
pub trait OpenRepositoryLike: std::fmt::Debug + Sync {
fn duplicate(&self) -> Box<dyn OpenRepositoryLike>;
fn remote_branches(&self) -> git::push::Result<Vec<config::BranchName>>; fn remote_branches(&self) -> git::push::Result<Vec<config::BranchName>>;
fn find_default_remote(&self, direction: Direction) -> Option<git::GitRemote>; fn find_default_remote(&self, direction: Direction) -> Option<git::GitRemote>;
fn fetch(&self) -> Result<(), git::fetch::Error>; fn fetch(&self) -> Result<(), git::fetch::Error>;
@ -102,6 +104,11 @@ pub trait OpenRepositoryLike {
file_name: &Path, file_name: &Path,
) -> git::file::Result<String>; ) -> git::file::Result<String>;
} }
pub fn mock() -> Box<MockOpenRepositoryLike> {
Box::new(MockOpenRepositoryLike::new())
}
impl std::ops::Deref for OpenRepository { impl std::ops::Deref for OpenRepository {
type Target = dyn OpenRepositoryLike; type Target = dyn OpenRepositoryLike;

View file

@ -1,5 +1,5 @@
// //
use crate as git; use crate::{self as git, repository::OpenRepositoryLike};
use git_next_config as config; use git_next_config as config;
use std::{ use std::{
@ -8,12 +8,12 @@ use std::{
}; };
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct MockOpenRepository { pub struct FakeOpenRepository {
default_push_remote: Arc<Mutex<Option<git::GitRemote>>>, default_push_remote: Arc<Mutex<Option<git::GitRemote>>>,
default_fetch_remote: Arc<Mutex<Option<git::GitRemote>>>, default_fetch_remote: Arc<Mutex<Option<git::GitRemote>>>,
operations: Arc<Mutex<Vec<String>>>, operations: Arc<Mutex<Vec<String>>>,
} }
impl MockOpenRepository { impl FakeOpenRepository {
pub fn new() -> Self { pub fn new() -> Self {
Self::default() Self::default()
} }
@ -50,13 +50,13 @@ impl MockOpenRepository {
.unwrap_or_default() .unwrap_or_default()
} }
} }
impl From<MockOpenRepository> for git::OpenRepository { impl From<FakeOpenRepository> for git::OpenRepository {
fn from(value: MockOpenRepository) -> Self { fn from(value: FakeOpenRepository) -> Self {
Self::Mock(value) Self::Mock(value)
} }
} }
#[allow(clippy::unwrap_used)] #[allow(clippy::unwrap_used)]
impl git::repository::OpenRepositoryLike for MockOpenRepository { impl git::repository::OpenRepositoryLike for FakeOpenRepository {
fn remote_branches(&self) -> git::push::Result<Vec<config::BranchName>> { fn remote_branches(&self) -> git::push::Result<Vec<config::BranchName>> {
todo!("MockOpenRepository::remote_branched") todo!("MockOpenRepository::remote_branched")
} }
@ -118,4 +118,8 @@ impl git::repository::OpenRepositoryLike for MockOpenRepository {
) -> git::file::Result<String> { ) -> git::file::Result<String> {
todo!("MockOpenRepository::read_file") todo!("MockOpenRepository::read_file")
} }
fn duplicate(&self) -> Box<dyn OpenRepositoryLike> {
Box::new(self.clone())
}
} }

View file

@ -1,5 +1,5 @@
// //
use crate as git; use crate::{self as git, repository::OpenRepositoryLike};
use config::BranchName; use config::BranchName;
use derive_more::Constructor; use derive_more::Constructor;
use git_next_config as config; use git_next_config as config;
@ -201,6 +201,10 @@ impl super::OpenRepositoryLike for RealOpenRepository {
Ok(content) Ok(content)
}) })
} }
fn duplicate(&self) -> Box<dyn OpenRepositoryLike> {
Box::new(self.clone())
}
} }
fn as_gix_error(branch: BranchName) -> impl FnOnce(String) -> git::commit::log::Error { fn as_gix_error(branch: BranchName) -> impl FnOnce(String) -> git::commit::log::Error {

View file

@ -1,5 +1,5 @@
// //
use crate as git; use crate::{self as git, repository::OpenRepositoryLike};
use derive_more::{Constructor, Deref}; use derive_more::{Constructor, Deref};
use git_next_config as config; use git_next_config as config;
@ -132,6 +132,10 @@ impl git::repository::OpenRepositoryLike for TestOpenRepository {
) -> git::file::Result<String> { ) -> git::file::Result<String> {
self.real.read_file(branch_name, file_name) self.real.read_file(branch_name, file_name)
} }
fn duplicate(&self) -> Box<dyn OpenRepositoryLike> {
Box::new(self.clone())
}
} }
impl TestOpenRepository { impl TestOpenRepository {
pub fn new( pub fn new(

View file

@ -2,7 +2,6 @@
use crate as git; use crate as git;
use crate::repository::RepositoryLike as _; use crate::repository::RepositoryLike as _;
use git::tests::given; use git::tests::given;
use git::tests::then;
use git_next_config as config; use git_next_config as config;
use assert2::let_assert; use assert2::let_assert;
@ -50,6 +49,7 @@ mod server_repo_config {
) )
); );
} }
#[test] #[test]
fn should_return_repo() { fn should_return_repo() {
let repo_path = given::a_name(); let repo_path = given::a_name();
@ -64,6 +64,7 @@ mod server_repo_config {
assert_eq!(src.repo(), config::RepoPath::new(repo_path)); assert_eq!(src.repo(), config::RepoPath::new(repo_path));
} }
#[test] #[test]
fn should_return_branch() { fn should_return_branch() {
let branch = given::a_name(); let branch = given::a_name();
@ -78,6 +79,7 @@ mod server_repo_config {
assert_eq!(src.branch(), config::BranchName::new(branch)); assert_eq!(src.branch(), config::BranchName::new(branch));
} }
#[test] #[test]
fn should_return_gitdir() { fn should_return_gitdir() {
let gitdir = given::a_name(); let gitdir = given::a_name();
@ -96,6 +98,7 @@ mod server_repo_config {
); );
} }
} }
mod repo_config { mod repo_config {
use super::*; use super::*;
@ -149,6 +152,7 @@ mod repo_config {
assert_eq!(repo_config.source(), config::RepoConfigSource::Repo); assert_eq!(repo_config.source(), config::RepoConfigSource::Repo);
} }
} }
mod forge_config { mod forge_config {
use super::*; use super::*;
use std::collections::BTreeMap; use std::collections::BTreeMap;
@ -198,6 +202,7 @@ mod forge_config {
] ]
); );
} }
#[test] #[test]
fn should_return_forge_type() { fn should_return_forge_type() {
let forge_type = config::ForgeType::MockForge; let forge_type = config::ForgeType::MockForge;
@ -209,6 +214,7 @@ mod forge_config {
assert_eq!(fc.forge_type(), config::ForgeType::MockForge); assert_eq!(fc.forge_type(), config::ForgeType::MockForge);
} }
#[test] #[test]
fn should_return_hostname() { fn should_return_hostname() {
let forge_type = config::ForgeType::MockForge; let forge_type = config::ForgeType::MockForge;
@ -220,6 +226,7 @@ mod forge_config {
assert_eq!(fc.hostname(), config::Hostname::new(hostname)); assert_eq!(fc.hostname(), config::Hostname::new(hostname));
} }
#[test] #[test]
fn should_return_user() { fn should_return_user() {
let forge_type = config::ForgeType::MockForge; let forge_type = config::ForgeType::MockForge;
@ -231,6 +238,7 @@ mod forge_config {
assert_eq!(fc.user(), config::User::new(user)); assert_eq!(fc.user(), config::User::new(user));
} }
#[test] #[test]
fn should_return_token() { fn should_return_token() {
let forge_type = config::ForgeType::MockForge; let forge_type = config::ForgeType::MockForge;
@ -242,6 +250,7 @@ mod forge_config {
assert_eq!(fc.token().expose_secret(), token.as_str()); assert_eq!(fc.token().expose_secret(), token.as_str());
} }
#[test] #[test]
fn should_return_repo() { fn should_return_repo() {
let forge_type = config::ForgeType::MockForge; let forge_type = config::ForgeType::MockForge;
@ -266,6 +275,7 @@ mod forge_config {
None, None,
None, None,
); );
let mut repos = BTreeMap::new(); let mut repos = BTreeMap::new();
repos.insert(red_name.clone(), red.clone()); repos.insert(red_name.clone(), red.clone());
repos.insert(blue_name, blue); repos.insert(blue_name, blue);
@ -277,6 +287,27 @@ mod forge_config {
} }
} }
// mod remote_branches {
// use super::*;
// #[test]
// // assumes running in the git-next repo which should have main, next and dev as remote branches
// fn should_return_remote_branches() -> TestResult {
// let_assert!(Ok(fs) = kxio::fs::temp());
// let gitdir: config::GitDir = fs.base().to_path_buf().into();
// let test_repository = git::repository::test(fs.clone());
// let_assert!(Ok(open_repository) = test_repository.open(&gitdir));
// let repo_config = &given::a_repo_config();
// let branches = repo_config.branches();
// then::create_a_commit_on_branch(&fs, &gitdir, &branches.main())?;
// then::create_a_commit_on_branch(&fs, &gitdir, &branches.next())?;
// then::create_a_commit_on_branch(&fs, &gitdir, &branches.dev())?;
// let_assert!(Ok(remote_branches) = open_repository.remote_branches());
// assert!(remote_branches.contains(&branches.main()));
// assert!(remote_branches.contains(&branches.next()));
// assert!(remote_branches.contains(&branches.dev()));
// Ok(())
// }
// }
mod find_default_remote { mod find_default_remote {
use super::*; use super::*;
@ -287,7 +318,7 @@ mod find_default_remote {
// uses the current repo // uses the current repo
let_assert!(Ok(cwd) = std::env::current_dir()); let_assert!(Ok(cwd) = std::env::current_dir());
let gitdir = config::GitDir::from(cwd.join("../..")); // from ./crate/git directory to the project rook let gitdir = config::GitDir::from(cwd.join("../..")); // from ./crate/git directory to the project rook
let_assert!(Ok(repo) = crate::repository::new().open(&gitdir)); let_assert!(Ok(repo) = crate::repository::real().open(&gitdir));
let_assert!(Some(remote) = repo.find_default_remote(crate::repository::Direction::Push)); let_assert!(Some(remote) = repo.find_default_remote(crate::repository::Direction::Push));
assert_eq!( assert_eq!(
remote, remote,
@ -304,37 +335,16 @@ mod fetch {
use git_next_config::GitDir; use git_next_config::GitDir;
#[test] #[test]
#[ignore] // requires authentication to the server #[ignore] // requires authentication to the server - which the CI doesn't have
fn should_fetch_from_repo() { fn should_fetch_from_repo() {
// uses the current repo and fetches from the remote server // uses the current repo and fetches from the remote server
let_assert!(Ok(cwd) = std::env::current_dir()); let_assert!(Ok(cwd) = std::env::current_dir());
let gitdir = GitDir::from(cwd.join("../..")); let gitdir = GitDir::from(cwd.join("../.."));
let_assert!(Ok(repo) = crate::repository::new().open(&gitdir)); let_assert!(Ok(repo) = crate::repository::real().open(&gitdir));
let_assert!(Ok(_) = repo.fetch()); let_assert!(Ok(_) = repo.fetch());
} }
} }
mod remote_branches {
use super::*;
#[test]
// assumes running in the git-next repo which should have main, next and dev as remote branches
fn should_return_remote_branches() -> TestResult {
let_assert!(Ok(fs) = kxio::fs::temp());
let gitdir: config::GitDir = fs.base().to_path_buf().into();
let test_repository = git::repository::test(fs.clone());
let_assert!(Ok(open_repository) = test_repository.open(&gitdir));
let repo_config = &given::a_repo_config();
let branches = repo_config.branches();
then::create_a_commit_on_branch(&fs, &gitdir, &branches.main())?;
then::create_a_commit_on_branch(&fs, &gitdir, &branches.next())?;
then::create_a_commit_on_branch(&fs, &gitdir, &branches.dev())?;
let_assert!(Ok(remote_branches) = open_repository.remote_branches());
assert!(remote_branches.contains(&branches.main()));
assert!(remote_branches.contains(&branches.next()));
assert!(remote_branches.contains(&branches.dev()));
Ok(())
}
}
mod commit_log { mod commit_log {
use git::tests::given; use git::tests::given;
@ -407,6 +417,7 @@ mod commit_log {
Ok(()) Ok(())
} }
} }
mod read_file { mod read_file {
use git::tests::given; use git::tests::given;

View file

@ -1,173 +1,100 @@
use crate as git; use crate as git;
mod validate { mod validate {
use crate::{validation::repo::validate_repo, GitRemote, RepoDetails}; use crate::{tests::given, validation::repo::validate_repo};
use super::*; use super::*;
use assert2::let_assert; use git::repository::Direction;
use git_next_config::{ForgeDetails, GitDir, Hostname, RepoPath};
#[test] #[test]
fn should_ok_a_valid_repo() { fn should_ok_a_valid_repo() {
let repo_details = RepoDetails::default() let fs = given::a_filesystem();
.with_forge( let repo_details = given::repo_details(&fs);
ForgeDetails::default().with_hostname(Hostname::new("localhost".to_string())), let repo_details_mock = repo_details.clone();
)
.with_repo_path(RepoPath::new("kemitix/test".to_string()));
let gitdir = GitDir::from("foo");
let remote = GitRemote::new(
Hostname::new("localhost"),
RepoPath::new("kemitix/test".to_string()),
);
let mut mock_repository = git::repository::mock(); let mut open_repository = git::repository::open::mock();
{ open_repository
let mut mock_open_repo = mock_repository.given_can_be_opened(&gitdir); .expect_find_default_remote()
mock_open_repo .returning(move |_direction| Some(repo_details_mock.git_remote()));
.given_has_default_remote(git::repository::Direction::Push, Some(remote.clone()));
mock_open_repo let result = validate_repo(&*open_repository, &repo_details);
.given_has_default_remote(git::repository::Direction::Fetch, Some(remote)); println!("{result:?}");
} assert!(result.is_ok());
let (repository, _mock_repository) = mock_repository.seal();
let_assert!(Ok(open_repository) = repository.open(&gitdir));
let_assert!(Ok(_) = validate_repo(&open_repository, &repo_details));
} }
#[test] #[test]
fn should_fail_where_no_default_push_remote() { fn should_fail_where_no_default_push_remote() {
let repo_details = RepoDetails::default() let fs = given::a_filesystem();
.with_forge( let repo_details = given::repo_details(&fs);
ForgeDetails::default().with_hostname(Hostname::new("localhost".to_string())), let repo_details_mock = repo_details.clone();
)
.with_repo_path(RepoPath::new("kemitix/test".to_string()));
let gitdir = GitDir::from("foo");
let remote = GitRemote::new(
Hostname::new("localhost"),
RepoPath::new("kemitix/test".to_string()),
);
let mut mock_repository = git::repository::mock(); let mut open_repository = git::repository::open::mock();
{ open_repository
let mut mock_open_repo = mock_repository.given_can_be_opened(&gitdir); .expect_find_default_remote()
mock_open_repo.given_has_default_remote(git::repository::Direction::Push, None); .returning(move |direction| match direction {
mock_open_repo Direction::Push => None,
.given_has_default_remote(git::repository::Direction::Fetch, Some(remote)); Direction::Fetch => Some(repo_details_mock.git_remote()),
} });
let (repository, _mock_repository) = mock_repository.seal();
let_assert!(Ok(open_repository) = repository.open(&gitdir)); let result = validate_repo(&*open_repository, &repo_details);
let_assert!(Err(_) = validate_repo(&open_repository, &repo_details)); println!("{result:?}");
assert!(result.is_err());
} }
#[test] #[test]
fn should_fail_where_no_default_fetch_remote() { fn should_fail_where_no_default_fetch_remote() {
let repo_details = RepoDetails::default() let fs = given::a_filesystem();
.with_forge( let repo_details = given::repo_details(&fs);
ForgeDetails::default().with_hostname(Hostname::new("localhost".to_string())), let repo_details_mock = repo_details.clone();
)
.with_repo_path(RepoPath::new("kemitix/test".to_string()));
let gitdir = GitDir::from("foo");
let remote = GitRemote::new(
Hostname::new("localhost"),
RepoPath::new("kemitix/test".to_string()),
);
let mut mock_repository = git::repository::mock(); let mut open_repository = git::repository::open::mock();
{ open_repository
let mut mock_open_repo = mock_repository.given_can_be_opened(&gitdir); .expect_find_default_remote()
mock_open_repo.given_has_default_remote(git::repository::Direction::Push, None); .returning(move |direction| match direction {
mock_open_repo Direction::Push => Some(repo_details_mock.git_remote()),
.given_has_default_remote(git::repository::Direction::Fetch, Some(remote)); Direction::Fetch => None,
} });
let (repository, _mock_repository) = mock_repository.seal();
let_assert!(Ok(open_repository) = repository.open(&gitdir)); let result = validate_repo(&*open_repository, &repo_details);
let_assert!(Err(_) = validate_repo(&open_repository, &repo_details)); println!("{result:?}");
assert!(result.is_err());
} }
#[test] #[test]
fn should_fail_where_invalid_default_push_remote() { fn should_fail_where_invalid_default_push_remote() {
let repo_details = RepoDetails::default() let fs = given::a_filesystem();
.with_forge( let repo_details = given::repo_details(&fs);
ForgeDetails::default().with_hostname(Hostname::new("localhost".to_string())), let repo_details_mock = repo_details.clone();
)
.with_repo_path(RepoPath::new("kemitix/test".to_string()));
let gitdir = GitDir::from("foo");
let remote = GitRemote::new(
Hostname::new("localhost"),
RepoPath::new("kemitix/test".to_string()),
);
let other_remote = GitRemote::new(
Hostname::new("localhost"),
RepoPath::new("kemitix/other".to_string()),
);
let mut mock_repository = git::repository::mock(); let mut open_repository = git::repository::open::mock();
{ open_repository
let mut mock_open_repo = mock_repository.given_can_be_opened(&gitdir); .expect_find_default_remote()
mock_open_repo .returning(move |direction| match direction {
.given_has_default_remote(git::repository::Direction::Push, Some(other_remote)); Direction::Push => Some(given::a_git_remote()),
mock_open_repo Direction::Fetch => Some(repo_details_mock.git_remote()),
.given_has_default_remote(git::repository::Direction::Fetch, Some(remote)); });
}
let (repository, _mock_repository) = mock_repository.seal(); let result = validate_repo(&*open_repository, &repo_details);
let_assert!(Ok(open_repository) = repository.open(&gitdir)); println!("{result:?}");
let_assert!(Err(_) = validate_repo(&open_repository, &repo_details)); assert!(result.is_err());
} }
#[test] #[test]
fn should_fail_where_invalid_default_fetch_remote() { fn should_fail_where_invalid_default_fetch_remote() {
let repo_details = RepoDetails::default() let fs = given::a_filesystem();
.with_forge( let repo_details = given::repo_details(&fs);
ForgeDetails::default().with_hostname(Hostname::new("localhost".to_string())), let repo_details_mock = repo_details.clone();
)
.with_repo_path(RepoPath::new("kemitix/test".to_string()));
let gitdir = GitDir::from("foo");
let remote = GitRemote::new(
Hostname::new("localhost"),
RepoPath::new("kemitix/test".to_string()),
);
let other_remote = GitRemote::new(
Hostname::new("localhost"),
RepoPath::new("kemitix/other".to_string()),
);
let mut mock_repository = git::repository::mock(); let mut open_repository = git::repository::open::mock();
{ open_repository
let mut mock_open_repo = mock_repository.given_can_be_opened(&gitdir); .expect_find_default_remote()
mock_open_repo.given_has_default_remote(git::repository::Direction::Push, Some(remote)); .returning(move |direction| match direction {
mock_open_repo Direction::Push => Some(repo_details_mock.git_remote()),
.given_has_default_remote(git::repository::Direction::Fetch, Some(other_remote)); Direction::Fetch => Some(given::a_git_remote()),
} });
let (repository, _mock_repository) = mock_repository.seal();
let_assert!(Ok(open_repository) = repository.open(&gitdir)); let result = validate_repo(&*open_repository, &repo_details);
let_assert!(Err(_) = validate_repo(&open_repository, &repo_details)); println!("{result:?}");
} assert!(result.is_err());
}
mod git_clone {
use super::*;
use assert2::let_assert;
use git_next_config::{ForgeDetails, GitDir, Hostname, RepoPath};
use crate::{GitRemote, RepoDetails};
#[test]
#[ignore] // slow test ~1.5 seconds
fn should_clone_repo() {
let_assert!(Ok(fs) = kxio::fs::temp());
let r = crate::repository::new();
let repo_details = RepoDetails::default()
.with_forge(
ForgeDetails::default().with_hostname(Hostname::new("git.kemitix.net".to_string())),
)
.with_gitdir(GitDir::new(fs.base()))
.with_repo_path(RepoPath::new("kemitix/git-next".to_string()));
let_assert!(Ok(open_repo) = r.git_clone(&repo_details));
let_assert!(
Some(remote) = open_repo.find_default_remote(git::repository::Direction::Fetch)
);
assert_eq!(
remote,
GitRemote::new(
Hostname::new("git.kemitix.net"),
RepoPath::new("kemitix/git-next".to_string())
)
);
} }
} }

View file

@ -1,3 +1,5 @@
#![allow(dead_code)]
use crate as git; use crate as git;
use git_next_config as config; use git_next_config as config;
@ -102,37 +104,38 @@ mod push {
mod reset { mod reset {
use super::*; use super::*;
use crate::{tests::given, OpenRepository}; use crate::tests::given;
use assert2::let_assert; use assert2::let_assert;
#[test] #[test]
fn should_perform_a_fetch_then_push() { fn should_perform_a_fetch_then_push() {
let mut open_repository = git::repository::open::mock();
let mut seq = mockall::Sequence::new();
open_repository
.expect_fetch()
.times(1)
.in_sequence(&mut seq)
.returning(|| Ok(()));
open_repository
.expect_push()
.times(1)
.in_sequence(&mut seq)
.returning(|_repo_details, _branch_name, _gitref, _force| Ok(()));
let fs = given::a_filesystem(); let fs = given::a_filesystem();
let (mock_open_repository, gitdir, mock_repository) = given::an_open_repository(&fs);
let open_repository: OpenRepository = mock_open_repository.into();
let repo_details = given::repo_details(&fs); let repo_details = given::repo_details(&fs);
let branch_name = &repo_details.branch; let branch_name = &repo_details.branch;
let commit = given::a_commit(); let commit = given::a_commit();
let gitref = GitRef::from(commit); let gitref = GitRef::from(commit);
let_assert!( let_assert!(
Ok(_) = git::push::reset( Ok(_) = git::push::reset(
&open_repository, &*open_repository,
&repo_details, &repo_details,
branch_name, branch_name,
&gitref, &gitref,
&git::push::Force::No &git::push::Force::No
) )
); );
let_assert!(Some(mock_open_repository) = mock_repository.get(&gitdir));
let operations = mock_open_repository.operations();
let forge_alias = repo_details.forge.forge_alias();
let repo_alias = &repo_details.repo_alias;
let to_commit = gitref;
let force = "fast-forward";
assert_eq!(
operations,
vec![format!("fetch"), format!("push fa:{forge_alias} ra:{repo_alias} bn:{branch_name} tc:{to_commit} f:{force}")]
);
} }
} }
} }
@ -215,7 +218,7 @@ pub mod given {
// //
use crate::{ use crate::{
self as git, self as git,
repository::{MockOpenRepository, MockRepository}, repository::{FakeOpenRepository, FakeRepository},
tests::given, tests::given,
}; };
use config::{ use config::{
@ -348,12 +351,19 @@ pub mod given {
pub fn an_open_repository( pub fn an_open_repository(
fs: &kxio::fs::FileSystem, fs: &kxio::fs::FileSystem,
) -> (MockOpenRepository, GitDir, MockRepository) { ) -> (FakeOpenRepository, GitDir, FakeRepository) {
let mut mock = git::repository::mock(); let mut mock = git::repository::fake();
let gitdir = a_git_dir(fs); let gitdir = a_git_dir(fs);
let or = mock.given_can_be_opened(&gitdir); let or = mock.given_can_be_opened(&gitdir);
(or, gitdir, mock) (or, gitdir, mock)
} }
pub fn a_git_remote() -> git::GitRemote {
git::GitRemote::new(
config::Hostname::new(given::a_name()),
config::RepoPath::new(given::a_name()),
)
}
} }
pub mod then { pub mod then {
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};

View file

@ -16,7 +16,7 @@ pub struct Positions {
#[allow(clippy::cognitive_complexity)] // TODO: (#83) reduce complexity #[allow(clippy::cognitive_complexity)] // TODO: (#83) reduce complexity
pub fn validate_positions( pub fn validate_positions(
repository: &git::OpenRepository, open_repository: &dyn git::repository::OpenRepositoryLike,
repo_details: &git::RepoDetails, repo_details: &git::RepoDetails,
repo_config: config::RepoConfig, repo_config: config::RepoConfig,
) -> Result<Positions> { ) -> Result<Positions> {
@ -24,9 +24,9 @@ pub fn validate_positions(
let next_branch = repo_config.branches().next(); let next_branch = repo_config.branches().next();
let dev_branch = repo_config.branches().dev(); let dev_branch = repo_config.branches().dev();
// Collect Commit Histories for `main`, `next` and `dev` branches // Collect Commit Histories for `main`, `next` and `dev` branches
repository.fetch()?; open_repository.fetch()?;
let commit_histories = let commit_histories =
get_commit_histories(repository, &repo_config).map_err(Error::CommitLog)?; get_commit_histories(open_repository, &repo_config).map_err(Error::CommitLog)?;
// branch tips // branch tips
let main = commit_histories let main = commit_histories
.main .main
@ -55,12 +55,12 @@ pub fn validate_positions(
// verify that next is on main or at most one commit on top of main, else reset it back to main // verify that next is on main or at most one commit on top of main, else reset it back to main
if is_not_based_on(&commit_histories.next[0..=1], &main) { if is_not_based_on(&commit_histories.next[0..=1], &main) {
info!("Main not on same commit as next, or it's parent - resetting next to main",); info!("Main not on same commit as next, or it's parent - resetting next to main",);
return reset_next_to_main(repository, repo_details, &main, &next, &next_branch); return reset_next_to_main(open_repository, repo_details, &main, &next, &next_branch);
} }
// verify that next is an ancestor of dev, else reset it back to main // verify that next is an ancestor of dev, else reset it back to main
if is_not_based_on(&commit_histories.dev, &next) { if is_not_based_on(&commit_histories.dev, &next) {
info!("Next is not an ancestor of dev - resetting next to main"); info!("Next is not an ancestor of dev - resetting next to main");
return reset_next_to_main(repository, repo_details, &main, &next, &next_branch); return reset_next_to_main(open_repository, repo_details, &main, &next, &next_branch);
} }
Ok(git::validation::positions::Positions { Ok(git::validation::positions::Positions {
@ -72,7 +72,7 @@ pub fn validate_positions(
} }
fn reset_next_to_main( fn reset_next_to_main(
repository: &crate::OpenRepository, repository: &dyn crate::repository::OpenRepositoryLike,
repo_details: &crate::RepoDetails, repo_details: &crate::RepoDetails,
main: &crate::Commit, main: &crate::Commit,
next: &crate::Commit, next: &crate::Commit,
@ -100,7 +100,7 @@ fn is_not_based_on(commits: &[crate::commit::Commit], needle: &crate::Commit) ->
} }
fn get_commit_histories( fn get_commit_histories(
repository: &git::repository::OpenRepository, repository: &dyn git::repository::OpenRepositoryLike,
repo_config: &config::RepoConfig, repo_config: &config::RepoConfig,
) -> git::commit::log::Result<git::commit::Histories> { ) -> git::commit::log::Result<git::commit::Histories> {
let main = (repository.commit_log(&repo_config.branches().main(), &[]))?; let main = (repository.commit_log(&repo_config.branches().main(), &[]))?;

View file

@ -4,13 +4,13 @@ use crate as git;
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub fn validate_repo( pub fn validate_repo(
repository: &git::OpenRepository, open_repository: &dyn git::repository::OpenRepositoryLike,
repo_details: &git::RepoDetails, repo_details: &git::RepoDetails,
) -> Result<()> { ) -> Result<()> {
let push_remote = repository let push_remote = open_repository
.find_default_remote(git::repository::Direction::Push) .find_default_remote(git::repository::Direction::Push)
.ok_or_else(|| Error::NoDefaultPushRemote)?; .ok_or_else(|| Error::NoDefaultPushRemote)?;
let fetch_remote = repository let fetch_remote = open_repository
.find_default_remote(git::repository::Direction::Fetch) .find_default_remote(git::repository::Direction::Fetch)
.ok_or_else(|| Error::NoDefaultFetchRemote)?; .ok_or_else(|| Error::NoDefaultFetchRemote)?;
let git_remote = repo_details.git_remote(); let git_remote = repo_details.git_remote();

View file

@ -1,13 +1,16 @@
// //
use crate as git; use crate as git;
use git::repository::RepositoryFactory as _;
use git::repository::RepositoryLike as _;
use git::tests::given; use git::tests::given;
use git_next_config as config; use git_next_config as config;
use assert2::let_assert; use assert2::let_assert;
mod repos { mod repos {
use crate::repository::RepositoryLike as _;
use crate::repository::Direction;
use super::*; use super::*;
@ -15,23 +18,34 @@ mod repos {
fn where_repo_has_no_push_remote() { fn where_repo_has_no_push_remote() {
let_assert!(Ok(fs) = kxio::fs::temp(), "temp fs"); let_assert!(Ok(fs) = kxio::fs::temp(), "temp fs");
let gitdir: config::GitDir = fs.base().to_path_buf().into(); let gitdir: config::GitDir = fs.base().to_path_buf().into();
let test_repository = git::repository::test(fs.clone()); let mut mock_open_repository = git::repository::open::mock();
// default has no push or fetch remotes mock_open_repository
.expect_find_default_remote()
.with(mockall::predicate::eq(Direction::Push))
.return_once(|_| None);
let mut repository_factory = git::repository::mock();
repository_factory
.expect_open()
.return_once(move |_| Ok(mock_open_repository));
let repo_details = given::repo_details(&fs); let repo_details = given::repo_details(&fs);
let_assert!(Ok(repository) = test_repository.open(&gitdir), "open repo"); let_assert!(
Ok(open_repository) = repository_factory.open(&gitdir),
"open repo"
);
let_assert!(Err(err) = git::validation::repo::validate_repo(&repository, &repo_details)); let result = git::validation::repo::validate_repo(&*open_repository, &repo_details);
print!("{result:?}");
let_assert!(Err(err) = result);
assert!(matches!( assert!(matches!(
err, err,
git::validation::repo::Error::NoDefaultPushRemote git::validation::repo::Error::NoDefaultPushRemote
)); ));
} }
} }
mod positions { mod positions {
use super::*; use super::*;
use git::repository::RepositoryLike as _;
mod validate_positions { mod validate_positions {
use git::validation::positions::validate_positions; use git::validation::positions::validate_positions;
@ -42,23 +56,26 @@ mod positions {
#[test] #[test]
fn where_fetch_fails_should_error() { fn where_fetch_fails_should_error() {
let_assert!(Ok(fs) = kxio::fs::temp(), "temp fs"); let fs = given::a_filesystem();
let gitdir: config::GitDir = fs.base().to_path_buf().into(); let gitdir: config::GitDir = fs.base().to_path_buf().into();
let mut test_repository = git::repository::test(fs.clone()); let mut mock_open_repository = git::repository::open::mock();
test_repository.on_fetch(git::repository::OnFetch::new( mock_open_repository
given::repo_branches(), .expect_fetch()
gitdir.clone(), .return_once(|| Err(git::fetch::Error::TestFailureExpected));
fs.clone(), let mut repository_factory = git::repository::mock();
|_, _, _| git::fetch::Result::Err(git::fetch::Error::TestFailureExpected), repository_factory
)); .expect_open()
let_assert!(Ok(repository) = test_repository.open(&gitdir), "open repo"); .return_once(move |_| Ok(mock_open_repository));
let repo_details = given::repo_details(&fs).with_gitdir(gitdir); let_assert!(
Ok(repository) = repository_factory.open(&gitdir),
"open repo"
);
let repo_details = given::repo_details(&fs); //.with_gitdir(gitdir);
let repo_config = given::a_repo_config(); let repo_config = given::a_repo_config();
let_assert!( let result = validate_positions(&*repository, &repo_details, repo_config);
Err(err) = validate_positions(&repository, &repo_details, repo_config), println!("{result:?}");
"validate" let_assert!(Err(err) = result, "validate");
);
assert!(matches!( assert!(matches!(
err, err,
@ -68,123 +85,137 @@ mod positions {
#[test] #[test]
fn where_main_branch_is_missing_or_commit_log_is_empty_should_error() { fn where_main_branch_is_missing_or_commit_log_is_empty_should_error() {
let_assert!(Ok(fs) = kxio::fs::temp(), "temp fs"); let fs = given::a_filesystem();
let gitdir: config::GitDir = fs.base().to_path_buf().into(); let gitdir: config::GitDir = fs.base().to_path_buf().into();
let mut test_repository = git::repository::test(fs.clone());
test_repository.on_fetch(git::repository::OnFetch::new(
given::repo_branches(),
gitdir.clone(),
fs.clone(),
|_, _, _| git::fetch::Result::Ok(()),
));
// test repo is a new bare repo with no commits
let_assert!(Ok(repository) = test_repository.open(&gitdir), "open repo");
let repo_details = given::repo_details(&fs).with_gitdir(gitdir);
let repo_config = given::a_repo_config(); let repo_config = given::a_repo_config();
let main_branch = repo_config.branches().main();
let mut mock_open_repository = git::repository::open::mock();
mock_open_repository.expect_fetch().return_once(|| Ok(()));
mock_open_repository
.expect_commit_log()
.returning(move |branch_name, _| {
if branch_name == &main_branch {
Err(git::commit::log::Error::Gix {
branch: branch_name.clone(),
error: "foo".to_string(),
})
} else {
Ok(vec![])
}
});
let mut repository_factory = git::repository::mock();
repository_factory
.expect_open()
.return_once(move |_| Ok(mock_open_repository));
let_assert!( let_assert!(
Err(err) = validate_positions(&repository, &repo_details, repo_config.clone()), Ok(open_repository) = repository_factory.open(&gitdir),
"validate" "open repo"
);
let branch_name = repo_config.branches().main();
let error_message = format!(
r#"The ref partially named "remotes/origin/{branch_name}" could not be found"#
); );
let repo_details = given::repo_details(&fs);
let main_branch = repo_config.branches().main();
let result = validate_positions(&*open_repository, &repo_details, repo_config);
println!("{result:?}");
assert!(matches!( assert!(matches!(
err, result,
git::validation::positions::Error::CommitLog(git::commit::log::Error::Gix { Err(git::validation::positions::Error::CommitLog(git::commit::log::Error::Gix {
branch, branch,
error error
}) if branch == branch_name && error == error_message })) if branch == main_branch && error == "foo"
)); ));
} }
#[test] #[test]
fn where_next_branch_is_missing_or_commit_log_is_empty_should_error() { fn where_next_branch_is_missing_or_commit_log_is_empty_should_error() {
//given let fs = given::a_filesystem();
let_assert!(Ok(fs) = kxio::fs::temp(), "temp fs");
let gitdir: config::GitDir = fs.base().to_path_buf().into(); let gitdir: config::GitDir = fs.base().to_path_buf().into();
let mut test_repository = git::repository::test(fs.clone());
let repo_config = given::a_repo_config(); let repo_config = given::a_repo_config();
test_repository.on_fetch(git::repository::OnFetch::new( let next_branch = repo_config.branches().next();
repo_config.branches().clone(), let mut mock_open_repository = git::repository::open::mock();
gitdir.clone(), mock_open_repository.expect_fetch().return_once(|| Ok(()));
fs.clone(), mock_open_repository
|branches, gitdir, fs| { .expect_commit_log()
// add a commit to main .returning(move |branch_name, _| {
then::create_a_commit_on_branch(fs, gitdir, &branches.main())?; if branch_name == &next_branch {
git::fetch::Result::Ok(()) Err(git::commit::log::Error::Gix {
}, branch: branch_name.clone(),
)); error: "foo".to_string(),
let_assert!(Ok(repository) = test_repository.open(&gitdir), "open repo"); })
let repo_details = given::repo_details(&fs).with_gitdir(gitdir); } else {
Ok(vec![given::a_commit()])
//when }
});
let mut repository_factory = git::repository::mock();
repository_factory
.expect_open()
.return_once(move |_| Ok(mock_open_repository));
let_assert!( let_assert!(
Err(err) = validate_positions(&repository, &repo_details, repo_config.clone()), Ok(open_repository) = repository_factory.open(&gitdir),
"validate" "open repo"
); );
let repo_details = given::repo_details(&fs);
let next_branch = repo_config.branches().next();
let result = validate_positions(&*open_repository, &repo_details, repo_config);
println!("{result:?}");
//then
let branch_name = repo_config.branches().next();
let error_message = format!(
r#"The ref partially named "remotes/origin/{branch_name}" could not be found"#
);
assert!(matches!( assert!(matches!(
err, result,
git::validation::positions::Error::CommitLog(git::commit::log::Error::Gix { Err(git::validation::positions::Error::CommitLog(git::commit::log::Error::Gix {
branch, branch,
error error
}) if branch == branch_name && error == error_message })) if branch == next_branch && error == "foo"
)); ));
} }
#[test] #[test]
fn where_dev_branch_is_missing_or_commit_log_is_empty_should_error() { fn where_dev_branch_is_missing_or_commit_log_is_empty_should_error() {
//given let fs = given::a_filesystem();
let_assert!(Ok(fs) = kxio::fs::temp(), "temp fs");
let gitdir: config::GitDir = fs.base().to_path_buf().into(); let gitdir: config::GitDir = fs.base().to_path_buf().into();
let mut test_repository = git::repository::test(fs.clone());
let repo_config = given::a_repo_config(); let repo_config = given::a_repo_config();
test_repository.on_fetch(git::repository::OnFetch::new( let dev_branch = repo_config.branches().dev();
repo_config.branches().clone(), let mut mock_open_repository = git::repository::open::mock();
gitdir.clone(), mock_open_repository.expect_fetch().return_once(|| Ok(()));
fs.clone(), mock_open_repository
|branches, gitdir, fs| { .expect_commit_log()
// add a commit to main .returning(move |branch_name, _| {
then::create_a_commit_on_branch(fs, gitdir, &branches.main())?; if branch_name == &dev_branch {
// add a commit to next Err(git::commit::log::Error::Gix {
then::create_a_commit_on_branch(fs, gitdir, &branches.next())?; branch: branch_name.clone(),
git::fetch::Result::Ok(()) error: "foo".to_string(),
}, })
)); } else {
let_assert!(Ok(repository) = test_repository.open(&gitdir), "open repo"); Ok(vec![given::a_commit()])
let repo_details = given::repo_details(&fs).with_gitdir(gitdir); }
});
//when let mut repository_factory = git::repository::mock();
repository_factory
.expect_open()
.return_once(move |_| Ok(mock_open_repository));
let_assert!( let_assert!(
Err(err) = validate_positions(&repository, &repo_details, repo_config.clone()), Ok(open_repository) = repository_factory.open(&gitdir),
"validate" "open repo"
); );
let repo_details = given::repo_details(&fs);
let dev_branch = repo_config.branches().dev();
let result = validate_positions(&*open_repository, &repo_details, repo_config);
println!("{result:?}");
//then
let branch_name = repo_config.branches().dev();
let error_message = format!(
r#"The ref partially named "remotes/origin/{branch_name}" could not be found"#
);
assert!(matches!( assert!(matches!(
err, result,
git::validation::positions::Error::CommitLog(git::commit::log::Error::Gix { Err(git::validation::positions::Error::CommitLog(git::commit::log::Error::Gix {
branch, branch,
error error
}) if branch == branch_name && error == error_message })) if branch == dev_branch && error == "foo"
)); ));
} }
#[test] #[test]
fn where_dev_branch_is_not_based_on_main_should_error() { fn where_dev_branch_is_not_based_on_main_should_error() {
//given //given
let_assert!(Ok(fs) = kxio::fs::temp(), "temp fs"); let fs = given::a_filesystem();
let gitdir: config::GitDir = fs.base().to_path_buf().into(); let gitdir: config::GitDir = fs.base().to_path_buf().into();
let mut test_repository = git::repository::test(fs.clone()); let mut test_repository = git::repository::test(fs.clone());
let repo_config = given::a_repo_config(); let repo_config = given::a_repo_config();
@ -211,12 +242,16 @@ mod positions {
git::fetch::Result::Ok(()) git::fetch::Result::Ok(())
}, },
)); ));
let_assert!(Ok(repository) = test_repository.open(&gitdir), "open repo"); let_assert!(
Ok(open_repository) = test_repository.open(&gitdir),
"open repo"
);
let repo_details = given::repo_details(&fs).with_gitdir(gitdir); let repo_details = given::repo_details(&fs).with_gitdir(gitdir);
//when //when
let_assert!( let_assert!(
Err(err) = validate_positions(&repository, &repo_details, repo_config.clone()), Err(err) =
validate_positions(&*open_repository, &repo_details, repo_config.clone()),
"validate" "validate"
); );
@ -294,12 +329,16 @@ mod positions {
git::push::Result::Ok(()) git::push::Result::Ok(())
}, },
)); ));
let_assert!(Ok(repository) = test_repository.open(&gitdir), "open repo"); let_assert!(
Ok(open_repository) = test_repository.open(&gitdir),
"open repo"
);
let repo_details = given::repo_details(&fs).with_gitdir(gitdir); let repo_details = given::repo_details(&fs).with_gitdir(gitdir);
//when //when
let_assert!( let_assert!(
Err(err) = validate_positions(&repository, &repo_details, repo_config.clone()), Err(err) =
validate_positions(&*open_repository, &repo_details, repo_config.clone()),
"validate" "validate"
); );
@ -361,12 +400,16 @@ mod positions {
git::push::Result::Err(git::push::Error::Lock) git::push::Result::Err(git::push::Error::Lock)
}, },
)); ));
let_assert!(Ok(repository) = test_repository.open(&gitdir), "open repo"); let_assert!(
Ok(open_repository) = test_repository.open(&gitdir),
"open repo"
);
let repo_details = given::repo_details(&fs).with_gitdir(gitdir.clone()); let repo_details = given::repo_details(&fs).with_gitdir(gitdir.clone());
//when //when
let_assert!( let_assert!(
Err(err) = validate_positions(&repository, &repo_details, repo_config.clone()), Err(err) =
validate_positions(&*open_repository, &repo_details, repo_config.clone()),
"validate" "validate"
); );
@ -447,12 +490,16 @@ mod positions {
git::push::Result::Ok(()) git::push::Result::Ok(())
}, },
)); ));
let_assert!(Ok(repository) = test_repository.open(&gitdir), "open repo"); let_assert!(
Ok(open_repository) = test_repository.open(&gitdir),
"open repo"
);
let repo_details = given::repo_details(&fs).with_gitdir(gitdir); let repo_details = given::repo_details(&fs).with_gitdir(gitdir);
//when //when
let_assert!( let_assert!(
Err(err) = validate_positions(&repository, &repo_details, repo_config.clone()), Err(err) =
validate_positions(&*open_repository, &repo_details, repo_config.clone()),
"validate" "validate"
); );
@ -512,12 +559,16 @@ mod positions {
git::push::Result::Err(git::push::Error::Lock) git::push::Result::Err(git::push::Error::Lock)
}, },
)); ));
let_assert!(Ok(repository) = test_repository.open(&gitdir), "open repo"); let_assert!(
Ok(open_repository) = test_repository.open(&gitdir),
"open repo"
);
let repo_details = given::repo_details(&fs).with_gitdir(gitdir.clone()); let repo_details = given::repo_details(&fs).with_gitdir(gitdir.clone());
//when //when
let_assert!( let_assert!(
Err(err) = validate_positions(&repository, &repo_details, repo_config.clone()), Err(err) =
validate_positions(&*open_repository, &repo_details, repo_config.clone()),
"validate" "validate"
); );
@ -561,12 +612,16 @@ mod positions {
git::fetch::Result::Ok(()) git::fetch::Result::Ok(())
}, },
)); ));
let_assert!(Ok(repository) = test_repository.open(&gitdir), "open repo"); let_assert!(
Ok(open_repository) = test_repository.open(&gitdir),
"open repo"
);
let repo_details = given::repo_details(&fs).with_gitdir(gitdir.clone()); let repo_details = given::repo_details(&fs).with_gitdir(gitdir.clone());
//when //when
let_assert!( let_assert!(
Ok(positions) = validate_positions(&repository, &repo_details, repo_config.clone()), Ok(positions) =
validate_positions(&*open_repository, &repo_details, repo_config.clone()),
"validate" "validate"
); );

View file

@ -16,7 +16,7 @@ pub async fn advance_next(
dev_commit_history: Vec<git::Commit>, dev_commit_history: Vec<git::Commit>,
repo_details: git::RepoDetails, repo_details: git::RepoDetails,
repo_config: config::RepoConfig, repo_config: config::RepoConfig,
repository: git::OpenRepository, open_repository: &dyn git::repository::OpenRepositoryLike,
addr: Addr<super::RepoActor>, addr: Addr<super::RepoActor>,
message_token: MessageToken, message_token: MessageToken,
) { ) {
@ -31,7 +31,7 @@ pub async fn advance_next(
} }
info!("Advancing next to commit '{}'", commit); info!("Advancing next to commit '{}'", commit);
if let Err(err) = git::push::reset( if let Err(err) = git::push::reset(
&repository, open_repository,
&repo_details, &repo_details,
&repo_config.branches().next(), &repo_config.branches().next(),
&commit.into(), &commit.into(),
@ -81,11 +81,11 @@ pub async fn advance_main(
next: git::Commit, next: git::Commit,
repo_details: &git::RepoDetails, repo_details: &git::RepoDetails,
repo_config: &config::RepoConfig, repo_config: &config::RepoConfig,
repository: &git::OpenRepository, open_repository: &dyn git::repository::OpenRepositoryLike,
) { ) {
info!("Advancing main to next"); info!("Advancing main to next");
if let Err(err) = git::push::reset( if let Err(err) = git::push::reset(
repository, open_repository,
repo_details, repo_details,
&repo_config.branches().main(), &repo_config.branches().main(),
&next.into(), &next.into(),

View file

@ -32,8 +32,8 @@ pub struct RepoActor {
last_main_commit: Option<git::Commit>, last_main_commit: Option<git::Commit>,
last_next_commit: Option<git::Commit>, last_next_commit: Option<git::Commit>,
last_dev_commit: Option<git::Commit>, last_dev_commit: Option<git::Commit>,
repository: git::Repository, repository: Box<dyn git::repository::RepositoryFactory>,
open_repository: Option<git::OpenRepository>, open_repository: Option<Box<dyn git::repository::OpenRepositoryLike>>,
net: Network, net: Network,
forge: forge::Forge, forge: forge::Forge,
} }
@ -43,7 +43,7 @@ impl RepoActor {
webhook: config::server::Webhook, webhook: config::server::Webhook,
generation: git::Generation, generation: git::Generation,
net: Network, net: Network,
repo: git::Repository, repository: Box<dyn git::repository::RepositoryFactory>,
) -> Self { ) -> Self {
let forge = forge::Forge::new(details.clone(), net.clone()); let forge = forge::Forge::new(details.clone(), net.clone());
debug!(?forge, "new"); debug!(?forge, "new");
@ -57,7 +57,7 @@ impl RepoActor {
last_main_commit: None, last_main_commit: None,
last_next_commit: None, last_next_commit: None,
last_dev_commit: None, last_dev_commit: None,
repository: repo, repository,
open_repository: None, open_repository: None,
net, net,
forge, forge,
@ -96,7 +96,7 @@ impl Handler<CloneRepo> for RepoActor {
#[tracing::instrument(name = "RepoActor::CloneRepo", skip_all, fields(repo = %self.repo_details /*, gitdir = %self.repo_details.gitdir */))] #[tracing::instrument(name = "RepoActor::CloneRepo", skip_all, fields(repo = %self.repo_details /*, gitdir = %self.repo_details.gitdir */))]
fn handle(&mut self, _msg: CloneRepo, ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, _msg: CloneRepo, ctx: &mut Self::Context) -> Self::Result {
let gitdir = self.repo_details.gitdir.clone(); let gitdir = self.repo_details.gitdir.clone();
match git::repository::open(&self.repository, &self.repo_details, gitdir) { match git::repository::open(&*self.repository, &self.repo_details, gitdir) {
Ok(repository) => { Ok(repository) => {
self.open_repository.replace(repository); self.open_repository.replace(repository);
if self.repo_details.repo_config.is_none() { if self.repo_details.repo_config.is_none() {
@ -121,14 +121,15 @@ impl Handler<LoadConfigFromRepo> for RepoActor {
fn handle(&mut self, _msg: LoadConfigFromRepo, ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, _msg: LoadConfigFromRepo, ctx: &mut Self::Context) -> Self::Result {
let details = self.repo_details.clone(); let details = self.repo_details.clone();
let addr = ctx.address(); let addr = ctx.address();
let Some(open_repository) = self.open_repository.clone() else { let Some(open_repository) = &self.open_repository else {
warn!("missing open repository - can't load configuration"); warn!("missing open repository - can't load configuration");
return; return;
}; };
repo_actor::load::load_file(details, addr, open_repository) let open_repository = open_repository.duplicate();
async move { repo_actor::load::load_file(details, addr, &*open_repository).await }
.in_current_span() .in_current_span()
.into_actor(self) .into_actor(self)
.wait(ctx); .wait(ctx)
} }
} }
@ -195,15 +196,15 @@ impl Handler<ValidateRepo> for RepoActor {
.into_actor(self) .into_actor(self)
.wait(ctx); .wait(ctx);
} }
if let (Some(repository), Some(repo_config)) = ( if let (Some(open_repository), Some(repo_config)) =
self.open_repository.clone(), (&self.open_repository, self.repo_details.repo_config.clone())
self.repo_details.repo_config.clone(), {
) {
let repo_details = self.repo_details.clone(); let repo_details = self.repo_details.clone();
let addr = ctx.address(); let addr = ctx.address();
let message_token = self.message_token; let message_token = self.message_token;
let open_repository = open_repository.duplicate();
async move { async move {
match validate_positions(&repository, &repo_details, repo_config) { match validate_positions(&*open_repository, &repo_details, repo_config) {
Ok(Positions { Ok(Positions {
main, main,
next, next,
@ -258,16 +259,22 @@ impl Handler<StartMonitoring> for RepoActor {
.into_actor(self) .into_actor(self)
.wait(ctx); .wait(ctx);
} else if dev_ahead_of_next { } else if dev_ahead_of_next {
if let Some(repository) = self.open_repository.clone() { if let Some(open_repository) = &self.open_repository {
let open_repository = open_repository.duplicate();
let repo_details = self.repo_details.clone();
let message_token = self.message_token;
async move {
branch::advance_next( branch::advance_next(
msg.next, msg.next,
msg.dev_commit_history, msg.dev_commit_history,
self.repo_details.clone(), repo_details,
repo_config, repo_config,
repository, &*open_repository,
addr, addr,
self.message_token, message_token,
) )
.await
}
.in_current_span() .in_current_span()
.into_actor(self) .into_actor(self)
.wait(ctx); .wait(ctx);
@ -304,15 +311,16 @@ impl Handler<AdvanceMainTo> for RepoActor {
warn!("No config loaded"); warn!("No config loaded");
return; return;
}; };
let Some(repository) = self.open_repository.clone() else { let Some(open_repository) = &self.open_repository else {
warn!("No repository opened"); warn!("No repository opened");
return; return;
}; };
let repo_details = self.repo_details.clone(); let repo_details = self.repo_details.clone();
let addr = ctx.address(); let addr = ctx.address();
let message_token = self.message_token; let message_token = self.message_token;
let open_repository = open_repository.duplicate();
async move { async move {
branch::advance_main(msg.0, &repo_details, &repo_config, &repository).await; branch::advance_main(msg.0, &repo_details, &repo_config, &*open_repository).await;
match repo_config.source() { match repo_config.source() {
git_next_config::RepoConfigSource::Repo => addr.do_send(LoadConfigFromRepo), git_next_config::RepoConfigSource::Repo => addr.do_send(LoadConfigFromRepo),
git_next_config::RepoConfigSource::Server => { git_next_config::RepoConfigSource::Server => {

View file

@ -15,10 +15,10 @@ use super::{LoadedConfig, RepoActor};
pub async fn load_file( pub async fn load_file(
repo_details: git::RepoDetails, repo_details: git::RepoDetails,
addr: Addr<RepoActor>, addr: Addr<RepoActor>,
open_repository: git::OpenRepository, open_repository: &dyn git::repository::OpenRepositoryLike,
) { ) {
info!("Loading .git-next.toml from repo"); info!("Loading .git-next.toml from repo");
let repo_config = match load(&repo_details, &open_repository).await { let repo_config = match load(&repo_details, open_repository).await {
Ok(repo_config) => repo_config, Ok(repo_config) => repo_config,
Err(err) => { Err(err) => {
error!(?err, "Failed to load config"); error!(?err, "Failed to load config");
@ -31,7 +31,7 @@ pub async fn load_file(
async fn load( async fn load(
details: &git::RepoDetails, details: &git::RepoDetails,
open_repository: &git::OpenRepository, open_repository: &dyn git::repository::OpenRepositoryLike,
) -> Result<config::RepoConfig, Error> { ) -> Result<config::RepoConfig, Error> {
let contents = open_repository.read_file(&details.branch, &PathBuf::from(".git-next.toml"))?; let contents = open_repository.read_file(&details.branch, &PathBuf::from(".git-next.toml"))?;
let config = config::RepoConfig::parse(&contents)?; let config = config::RepoConfig::parse(&contents)?;
@ -50,7 +50,7 @@ pub enum Error {
pub async fn validate( pub async fn validate(
config: config::RepoConfig, config: config::RepoConfig,
open_repository: &git::OpenRepository, open_repository: &dyn git::repository::OpenRepositoryLike,
) -> Result<config::RepoConfig, Error> { ) -> Result<config::RepoConfig, Error> {
let branches = open_repository.remote_branches()?; let branches = open_repository.remote_branches()?;
if !branches if !branches

View file

@ -7,7 +7,7 @@ use config::server::{ServerConfig, ServerStorage, Webhook};
use git_next_config::{ use git_next_config::{
self as config, ForgeAlias, ForgeConfig, GitDir, RepoAlias, ServerRepoConfig, self as config, ForgeAlias, ForgeConfig, GitDir, RepoAlias, ServerRepoConfig,
}; };
use git_next_git::{Generation, RepoDetails, Repository}; use git_next_git::{repository::RepositoryFactory, Generation, RepoDetails};
use git_next_repo_actor::{CloneRepo, RepoActor}; use git_next_repo_actor::{CloneRepo, RepoActor};
use kxio::{fs::FileSystem, network::Network}; use kxio::{fs::FileSystem, network::Network};
use tracing::{error, info, warn}; use tracing::{error, info, warn};
@ -38,7 +38,7 @@ pub struct Server {
webhook: Option<Addr<WebhookActor>>, webhook: Option<Addr<WebhookActor>>,
fs: FileSystem, fs: FileSystem,
net: Network, net: Network,
repo: Repository, repository_factory: Box<dyn RepositoryFactory>,
} }
impl Actor for Server { impl Actor for Server {
type Context = Context<Self>; type Context = Context<Self>;
@ -114,14 +114,14 @@ impl Handler<ServerConfig> for Server {
} }
} }
impl Server { impl Server {
pub fn new(fs: FileSystem, net: Network, repo: Repository) -> Self { pub fn new(fs: FileSystem, net: Network, repo: Box<dyn RepositoryFactory>) -> Self {
let generation = Generation::new(); let generation = Generation::new();
Self { Self {
generation, generation,
webhook: None, webhook: None,
fs, fs,
net, net,
repo, repository_factory: repo,
} }
} }
fn create_forge_data_directories( fn create_forge_data_directories(
@ -180,7 +180,7 @@ impl Server {
let server_storage = server_storage.clone(); let server_storage = server_storage.clone();
let webhook = webhook.clone(); let webhook = webhook.clone();
let net = self.net.clone(); let net = self.net.clone();
let repo = self.repo.clone(); let repository_factory = self.repository_factory.duplicate();
let generation = self.generation; let generation = self.generation;
move |(repo_alias, server_repo_config)| { move |(repo_alias, server_repo_config)| {
let span = tracing::info_span!("create_actor", alias = %repo_alias, config = %server_repo_config); let span = tracing::info_span!("create_actor", alias = %repo_alias, config = %server_repo_config);
@ -213,7 +213,7 @@ impl Server {
webhook.clone(), webhook.clone(),
generation, generation,
net.clone(), net.clone(),
repo.clone(), repository_factory.duplicate(),
); );
(forge_name.clone(), repo_alias, actor) (forge_name.clone(), repo_alias, actor)
} }

View file

@ -60,9 +60,9 @@ fn repo_details_find_default_push_remote_finds_correct_remote() -> Result<()> {
.with_hostname(Hostname::new("git.kemitix.net")); .with_hostname(Hostname::new("git.kemitix.net"));
repo_details.repo_path = RepoPath::new("kemitix/git-next".to_string()); repo_details.repo_path = RepoPath::new("kemitix/git-next".to_string());
let gitdir = &repo_details.gitdir; let gitdir = &repo_details.gitdir;
let repository = git::repository::new().open(gitdir)?; let open_repository = git::repository::real().open(gitdir)?;
let_assert!( let_assert!(
Some(found_git_remote) = repository.find_default_remote(Direction::Push), Some(found_git_remote) = open_repository.find_default_remote(Direction::Push),
"Default Push Remote not found" "Default Push Remote not found"
); );
let config_git_remote = repo_details.git_remote(); let config_git_remote = repo_details.git_remote();
@ -85,14 +85,14 @@ fn gitdir_validate_should_pass_a_valid_git_repo() -> Result<()> {
config::common::forge_details(1, ForgeType::MockForge), config::common::forge_details(1, ForgeType::MockForge),
None, None,
GitDir::new(root), // Server GitDir - should be ignored GitDir::new(root), // Server GitDir - should be ignored
); )
.with_repo_path(RepoPath::new("kemitix/git-next".to_string()));
repo_details.forge = repo_details repo_details.forge = repo_details
.forge .forge
.with_hostname(Hostname::new("git.kemitix.net")); .with_hostname(Hostname::new("git.kemitix.net"));
repo_details.repo_path = RepoPath::new("kemitix/git-next".to_string());
let gitdir = &repo_details.gitdir; let gitdir = &repo_details.gitdir;
let repository = git::repository::new().open(gitdir)?; let repository = git::repository::real().open(gitdir)?;
validate_repo(&repository, &repo_details)?; validate_repo(&*repository, &repo_details)?;
Ok(()) Ok(())
} }
@ -103,17 +103,17 @@ fn gitdir_validate_should_fail_a_git_repo_with_wrong_remote() -> Result<()> {
Ok(cli_crate_dir) = std::env::current_dir().map_err(git::validation::repo::Error::Io) Ok(cli_crate_dir) = std::env::current_dir().map_err(git::validation::repo::Error::Io)
); );
let_assert!(Some(Some(root)) = cli_crate_dir.parent().map(|p| p.parent())); let_assert!(Some(Some(root)) = cli_crate_dir.parent().map(|p| p.parent()));
let mut repo_details = git::common::repo_details( let repo_details = git::common::repo_details(
1, 1,
git::Generation::new(), git::Generation::new(),
config::common::forge_details(1, ForgeType::MockForge), config::common::forge_details(1, ForgeType::MockForge),
None, None,
GitDir::new(root), // Server GitDir - should be ignored GitDir::new(root), // Server GitDir - should be ignored
); )
repo_details.repo_path = RepoPath::new("hello/world".to_string()); .with_repo_path(RepoPath::new("hello/world".to_string()));
let gitdir = &repo_details.gitdir; let gitdir = &repo_details.gitdir;
let repository = git::repository::new().open(gitdir)?; let repository = git::repository::real().open(gitdir)?;
let_assert!(Err(_) = validate_repo(&repository, &repo_details)); let_assert!(Err(_) = validate_repo(&*repository, &repo_details));
Ok(()) Ok(())
} }

View file

@ -3,7 +3,7 @@ mod config;
// //
use actix::prelude::*; use actix::prelude::*;
use git_next_git::Repository; use git_next_git::repository::RepositoryFactory;
use kxio::{fs::FileSystem, network::Network}; use kxio::{fs::FileSystem, network::Network};
use std::path::PathBuf; use std::path::PathBuf;
@ -37,7 +37,7 @@ pub fn init(fs: FileSystem) {
} }
} }
pub async fn start(fs: FileSystem, net: Network, repo: Repository) { pub async fn start(fs: FileSystem, net: Network, repo: Box<dyn RepositoryFactory>) {
init_logging(); init_logging();
info!("Starting Server..."); info!("Starting Server...");