WIP RepositoryFactory trait
All checks were successful
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 134049de57
15 changed files with 135 additions and 75 deletions

View file

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

View file

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

View file

@ -56,6 +56,7 @@ actix = { workspace = true }
assert2 = { workspace = true }
rand = { workspace = true }
pretty_assertions = { workspace = true }
mockall = { workspace = true }
[lints.clippy]
nursery = { level = "warn", priority = -1 }

View file

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

View file

@ -1,6 +1,10 @@
//
#[cfg(test)]
mod mock;
use std::sync::{atomic::AtomicBool, Arc, Mutex};
use derive_more::Deref as _;
#[cfg(test)]
pub use mock::MockRepository;
#[cfg(test)]
@ -26,11 +30,11 @@ pub use real::RealRepository;
use tracing::info;
use crate::repository::test::TestRepository;
use crate::validation::repo::validate_repo;
use super::RepoDetails;
// TODO: #[deprecated]
#[derive(Clone, Debug)]
#[allow(clippy::large_enum_variant)]
pub enum Repository {
@ -62,11 +66,11 @@ pub const fn test(fs: kxio::fs::FileSystem) -> TestRepository {
#[tracing::instrument(skip_all)]
#[cfg(not(tarpaulin_include))] // requires network access to either clone new and/or fetch.
pub fn open(
repository: &Repository,
repository: &dyn RepositoryFactory,
repo_details: &RepoDetails,
gitdir: config::GitDir,
) -> Result<OpenRepository> {
let repository = if !gitdir.exists() {
) -> Result<Box<dyn OpenRepositoryLike>> {
let open_repository = if !gitdir.exists() {
info!("Local copy not found - cloning...");
repository.git_clone(repo_details)?
} else {
@ -74,8 +78,44 @@ pub fn open(
repository.open(&gitdir)?
};
info!("Validating...");
validate_repo(&repository, repo_details).map_err(|e| Error::Validation(e.to_string()))?;
Ok(repository)
validate_repo(&*open_repository, repo_details).map_err(|e| Error::Validation(e.to_string()))?;
Ok(open_repository)
}
#[cfg_attr(test, mockall::automock)]
pub trait RepositoryFactory: std::fmt::Debug {
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)
}
#[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 {

View file

@ -23,6 +23,7 @@ pub use omock::MockOpenRepository;
pub use oreal::RealOpenRepository;
pub use otest::TestOpenRepository;
// TODO: #[deprecated]
#[derive(Clone, Debug)]
pub enum OpenRepository {
/// A real git repository.
@ -74,7 +75,8 @@ pub fn test_bare(
OpenRepository::Test(TestOpenRepository::new_bare(gitdir, fs, on_fetch, on_push))
}
pub trait OpenRepositoryLike {
pub trait OpenRepositoryLike: std::fmt::Debug {
fn duplicate(&self) -> Box<dyn OpenRepositoryLike>;
fn remote_branches(&self) -> git::push::Result<Vec<config::BranchName>>;
fn find_default_remote(&self, direction: Direction) -> Option<git::GitRemote>;
fn fetch(&self) -> Result<(), git::fetch::Error>;
@ -102,16 +104,16 @@ pub trait OpenRepositoryLike {
file_name: &Path,
) -> git::file::Result<String>;
}
impl std::ops::Deref for OpenRepository {
type Target = dyn OpenRepositoryLike;
fn deref(&self) -> &Self::Target {
match self {
Self::Real(real) => real,
Self::Test(test) => test,
#[cfg(test)]
Self::Mock(mock) => mock,
}
}
}
// impl std::ops::Deref for OpenRepository {
// type Target = dyn OpenRepositoryLike;
//
// fn deref(&self) -> &Self::Target {
// match self {
// Self::Real(real) => real,
// Self::Test(test) => test,
//
// #[cfg(test)]
// Self::Mock(mock) => mock,
// }
// }
// }

View file

@ -1,5 +1,5 @@
//
use crate as git;
use crate::{self as git, repository::OpenRepositoryLike};
use config::BranchName;
use derive_more::Constructor;
use git_next_config as config;
@ -201,6 +201,10 @@ impl super::OpenRepositoryLike for RealOpenRepository {
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 {

View file

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

View file

@ -16,7 +16,7 @@ pub struct Positions {
#[allow(clippy::cognitive_complexity)] // TODO: (#83) reduce complexity
pub fn validate_positions(
repository: &git::OpenRepository,
open_repository: &dyn git::repository::OpenRepositoryLike,
repo_details: &git::RepoDetails,
repo_config: config::RepoConfig,
) -> Result<Positions> {
@ -24,9 +24,9 @@ pub fn validate_positions(
let next_branch = repo_config.branches().next();
let dev_branch = repo_config.branches().dev();
// Collect Commit Histories for `main`, `next` and `dev` branches
repository.fetch()?;
open_repository.fetch()?;
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
let main = commit_histories
.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
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",);
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
if is_not_based_on(&commit_histories.dev, &next) {
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 {
@ -72,7 +72,7 @@ pub fn validate_positions(
}
fn reset_next_to_main(
repository: &crate::OpenRepository,
repository: &dyn crate::repository::OpenRepositoryLike,
repo_details: &crate::RepoDetails,
main: &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(
repository: &git::repository::OpenRepository,
repository: &dyn git::repository::OpenRepositoryLike,
repo_config: &config::RepoConfig,
) -> git::commit::log::Result<git::commit::Histories> {
let main = (repository.commit_log(&repo_config.branches().main(), &[]))?;

View file

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

View file

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

View file

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

View file

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

View file

@ -7,7 +7,7 @@ use config::server::{ServerConfig, ServerStorage, Webhook};
use git_next_config::{
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 kxio::{fs::FileSystem, network::Network};
use tracing::{error, info, warn};
@ -38,7 +38,7 @@ pub struct Server {
webhook: Option<Addr<WebhookActor>>,
fs: FileSystem,
net: Network,
repo: Repository,
repository_factory: Box<dyn RepositoryFactory>,
}
impl Actor for Server {
type Context = Context<Self>;
@ -114,14 +114,14 @@ impl Handler<ServerConfig> for 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();
Self {
generation,
webhook: None,
fs,
net,
repo,
repository_factory: repo,
}
}
fn create_forge_data_directories(
@ -180,7 +180,7 @@ impl Server {
let server_storage = server_storage.clone();
let webhook = webhook.clone();
let net = self.net.clone();
let repo = self.repo.clone();
let repository_factory = self.repository_factory.duplicate();
let generation = self.generation;
move |(repo_alias, 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(),
generation,
net.clone(),
repo.clone(),
repository_factory.duplicate(),
);
(forge_name.clone(), repo_alias, actor)
}

View file

@ -3,7 +3,7 @@ mod config;
//
use actix::prelude::*;
use git_next_git::Repository;
use git_next_git::repository::RepositoryFactory;
use kxio::{fs::FileSystem, network::Network};
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();
info!("Starting Server...");