WIP: test(git): make repository more testable
All checks were 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-05-18 11:41:18 +01:00
parent c3a5e50ad5
commit 7e03121bcb
12 changed files with 155 additions and 63 deletions

View file

@ -2,7 +2,7 @@ use std::ops::Deref;
use tracing::{debug, info};
use super::{RepoDetails, Repository};
use super::{LegacyRepository, RepoDetails};
#[derive(Debug, derive_more::From, derive_more::Display)]
pub enum Error {
@ -14,7 +14,7 @@ pub enum Error {
impl std::error::Error for Error {}
#[tracing::instrument(skip_all, fields(repo = %repo_details))]
pub fn fetch(repository: &Repository, repo_details: &RepoDetails) -> Result<(), Error> {
pub fn fetch(repository: &LegacyRepository, repo_details: &RepoDetails) -> Result<(), Error> {
let repository = repository.deref().to_thread_local();
let Some(remote) = repository.find_default_remote(gix::remote::Direction::Fetch) else {
return Err(Error::NoFetchRemoteFound);

View file

@ -19,5 +19,6 @@ pub use git_ref::GitRef;
pub use git_remote::GitRemote;
pub use push::push;
pub use repo_details::RepoDetails;
pub use repository::LegacyRepository;
pub use repository::Repository;
pub use validate::validate;

View file

@ -4,7 +4,7 @@ use git_next_config::BranchName;
use secrecy::ExposeSecret;
use tracing::{info, warn};
use super::{GitRef, RepoDetails, Repository};
use super::{GitRef, LegacyRepository, RepoDetails};
#[derive(Debug)]
pub enum Force {
@ -23,7 +23,7 @@ impl std::fmt::Display for Force {
// TODO: (#72) reimplement using `gix`
#[tracing::instrument(skip_all, fields(branch = %branch_name, to = %to_commit, force = %force))]
pub fn push(
repository: &Repository,
repository: &LegacyRepository,
repo_details: &RepoDetails,
branch_name: BranchName,
to_commit: GitRef,

View file

@ -1,11 +1,123 @@
//
use std::{ops::Deref as _, path::PathBuf, sync::atomic::AtomicBool};
use git_next_config::{GitDir, Hostname, RepoPath};
use crate::GitRemote;
use super::RepoDetails;
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<OpenRepository, Error>;
fn clone(&self, repo_details: &RepoDetails) -> Result<OpenRepository, Error>;
}
impl std::ops::Deref for Repository {
type Target = dyn RepositoryLike;
fn deref(&self) -> &Self::Target {
match self {
Self::Real => &RealRepository,
Self::Mock => todo!(),
}
}
}
struct RealRepository;
impl RepositoryLike for RealRepository {
fn open(&self, gitdir: &GitDir) -> Result<OpenRepository, Error> {
Ok(OpenRepository::Real(RealOpenRepository(
gix::ThreadSafeRepository::open(gitdir.to_path_buf())?.to_thread_local(),
)))
}
fn clone(&self, repo_details: &RepoDetails) -> Result<OpenRepository, Error> {
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(OpenRepository::Real(RealOpenRepository(repository)))
}
}
struct MockRepository;
impl RepositoryLike for MockRepository {
fn open(&self, _gitdir: &GitDir) -> Result<OpenRepository, Error> {
Ok(OpenRepository::Mock)
}
fn clone(&self, _repo_details: &RepoDetails) -> Result<OpenRepository, Error> {
Ok(OpenRepository::Mock)
}
}
pub enum OpenRepository {
Real(RealOpenRepository),
Mock, // TODO: contain a mock model of a repo
}
pub trait OpenRepositoryLike {
fn find_default_remote(&self, direction: Direction) -> Option<GitRemote>;
}
impl OpenRepositoryLike for OpenRepository {
fn find_default_remote(&self, direction: Direction) -> Option<GitRemote> {
match self {
OpenRepository::Real(real) => real.find_default_remote(direction),
OpenRepository::Mock => todo!(),
}
}
}
struct RealOpenRepository(gix::Repository);
impl OpenRepositoryLike for RealOpenRepository {
fn find_default_remote(&self, direction: Direction) -> Option<GitRemote> {
let repository = &self.0;
let Some(Ok(remote)) = repository.find_default_remote(direction.into()) else {
return None;
};
let Some(url) = remote.url(direction.into()) else {
return None;
};
let Some(host) = url.host() else {
return None;
};
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()),
))
}
}
pub enum Direction {
/// Push local changes to the remote.
Push,
/// Fetch changes from the remote to the local repository.
Fetch,
}
impl From<Direction> for gix::remote::Direction {
fn from(value: Direction) -> Self {
match value {
Direction::Push => gix::remote::Direction::Push,
Direction::Fetch => gix::remote::Direction::Fetch,
}
}
}
#[derive(Debug, Clone, derive_more::From, derive_more::Deref)]
pub struct Repository(gix::ThreadSafeRepository);
impl Repository {
#[deprecated(note = "Use Repository::Real::open() or Repository::Real::clone()")]
pub struct LegacyRepository(gix::ThreadSafeRepository);
impl LegacyRepository {
pub fn open(gitdir: impl Into<PathBuf>) -> Result<Self, Error> {
Ok(Self(gix::ThreadSafeRepository::open(gitdir.into())?))
}

View file

@ -1,16 +1,14 @@
use std::ops::Deref as _;
use git_next_config::{Hostname, RepoPath};
use gix::remote::Direction;
use tracing::info;
use super::{GitRemote, RepoDetails, Repository};
use crate::repository::{Direction, OpenRepository, OpenRepositoryLike};
use super::{GitRemote, RepoDetails};
#[tracing::instrument(skip_all)]
pub fn validate(repository: &Repository, repo_details: &RepoDetails) -> Result<()> {
pub fn validate(repository: &OpenRepository, repo_details: &RepoDetails) -> Result<()> {
let git_remote = repo_details.git_remote();
let push_remote = find_default_remote(repository, Direction::Push)?;
let fetch_remote = find_default_remote(repository, Direction::Fetch)?;
let Some(push_remote) = repository.find_default_remote(Direction::Push);
let Some(fetch_remote) = repository.find_default_remote(Direction::Fetch);
info!(config = %git_remote, push = %push_remote, fetch = %fetch_remote, "Check remotes match");
if git_remote != push_remote {
return Err(Error::MismatchDefaultPushRemote {
@ -27,31 +25,6 @@ pub fn validate(repository: &Repository, repo_details: &RepoDetails) -> Result<(
Ok(())
}
#[tracing::instrument(skip_all, fields(?direction))]
pub fn find_default_remote(
repository: &Repository,
direction: gix::remote::Direction,
) -> Result<GitRemote> {
let repository = repository.deref().to_thread_local();
let Some(Ok(remote)) = repository.find_default_remote(gix::remote::Direction::Push) else {
return Err(Error::NoDefaultPushRemote);
};
let Some(url) = remote.url(direction) else {
return Err(Error::NoUrlForDefaultPushRemote);
};
let Some(host) = url.host() else {
return Err(Error::NoHostnameForDefaultPushRemote);
};
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);
info!(%host, %path, "found");
Ok(GitRemote::new(
Hostname::new(host),
RepoPath::new(path.to_string()),
))
}
type Result<T> = core::result::Result<T, Error>;
#[derive(Debug, derive_more::Display)]
pub enum Error {

View file

@ -18,7 +18,7 @@ pub async fn advance_next(
dev_commit_history: Vec<git::Commit>,
repo_config: RepoConfig,
forge: gitforge::Forge,
repository: git::Repository,
repository: git::LegacyRepository,
addr: Addr<super::RepoActor>,
message_token: super::MessageToken,
) {
@ -82,7 +82,7 @@ pub async fn advance_main(
next: git::Commit,
repo_config: RepoConfig,
forge: gitforge::Forge,
repository: git::Repository,
repository: git::LegacyRepository,
addr: Addr<RepoActor>,
message_token: super::MessageToken,
) {

View file

@ -28,7 +28,7 @@ pub struct RepoActor {
last_main_commit: Option<git::Commit>,
last_next_commit: Option<git::Commit>,
last_dev_commit: Option<git::Commit>,
repository: Option<git::Repository>,
repository: Option<git::LegacyRepository>,
net: Network,
forge: gitforge::Forge,
}

View file

@ -1,11 +1,12 @@
use std::collections::BTreeMap;
use assert2::let_assert;
use git::repository::Repository;
use git_next_config::{
ForgeType, GitDir, Hostname, RepoBranches, RepoConfig, RepoConfigSource, RepoPath,
ServerRepoConfig,
};
use git_next_git::{self as git, Generation, GitRemote, Repository};
use git_next_git::{self as git, Generation, GitRemote, LegacyRepository};
use gix::remote::Direction;
use kxio::fs;
use pretty_assertions::assert_eq;
@ -179,7 +180,7 @@ fn repo_details_find_default_push_remote_finds_correct_remote() -> Result<()> {
.with_hostname(Hostname::new("git.kemitix.net"));
repo_details.repo_path = RepoPath::new("kemitix/git-next".to_string());
let gitdir = &repo_details.gitdir;
let repository = Repository::open(gitdir)?;
let repository = git::repository::new().open(gitdir)?;
let found_git_remote = git::validate::find_default_remote(&repository, Direction::Push)?;
let config_git_remote = repo_details.git_remote();
@ -207,7 +208,7 @@ fn gitdir_validate_should_pass_a_valid_git_repo() -> Result<()> {
.with_hostname(Hostname::new("git.kemitix.net"));
repo_details.repo_path = RepoPath::new("kemitix/git-next".to_string());
let gitdir = &repo_details.gitdir;
let repository = Repository::open(gitdir)?;
let repository = LegacyRepository::open(gitdir)?;
git::validate(&repository, &repo_details)?;
Ok(())
@ -226,7 +227,7 @@ fn gitdir_validate_should_fail_a_git_repo_with_wrong_remote() -> Result<()> {
);
repo_details.repo_path = RepoPath::new("hello/world".to_string());
let gitdir = &repo_details.gitdir;
let repository = Repository::open(gitdir)?;
let repository = LegacyRepository::open(gitdir)?;
let_assert!(Err(_) = git::validate(&repository, &repo_details));
Ok(())

View file

@ -28,7 +28,7 @@ pub struct ValidatedPositions {
pub async fn validate_positions(
forge: &gitforge::forgejo::ForgeJoEnv,
repository: &git::Repository,
repository: &git::LegacyRepository,
repo_config: RepoConfig,
) -> Result<ValidatedPositions, Error> {
let repo_details = &forge.repo_details;

View file

@ -6,7 +6,7 @@ use std::time::Duration;
use actix::prelude::*;
use git_next_config::{BranchName, GitDir, RepoConfig};
use git_next_git::{self as git, GitRef, RepoDetails, Repository};
use git_next_git::{self as git, GitRef, LegacyRepository, RepoDetails, Repository};
use kxio::network::{self, Network};
use tracing::{error, info, warn};
@ -21,10 +21,15 @@ struct ForgeJo;
pub struct ForgeJoEnv {
repo_details: RepoDetails,
net: Network,
repo: Repository,
}
impl ForgeJoEnv {
pub(super) const fn new(repo_details: RepoDetails, net: Network) -> Self {
Self { repo_details, net }
pub(super) const fn new(repo_details: RepoDetails, net: Network, repo: Repository) -> Self {
Self {
repo_details,
net,
repo,
}
}
}
#[async_trait::async_trait]
@ -47,7 +52,7 @@ impl super::ForgeLike for ForgeJoEnv {
async fn branches_validate_positions(
&self,
repository: git::Repository,
repository: git::LegacyRepository,
repo_config: RepoConfig,
addr: Addr<RepoActor>,
message_token: MessageToken,
@ -71,7 +76,7 @@ impl super::ForgeLike for ForgeJoEnv {
fn branch_reset(
&self,
repository: &git::Repository,
repository: &git::LegacyRepository,
branch_name: BranchName,
to_commit: GitRef,
force: git::push::Force,
@ -130,12 +135,12 @@ impl super::ForgeLike for ForgeJoEnv {
}
}
fn repo_clone(&self, gitdir: GitDir) -> Result<Repository, git::repository::Error> {
fn repo_clone(&self, gitdir: GitDir) -> Result<LegacyRepository, git::repository::Error> {
let repository = if !gitdir.exists() {
info!("Local copy not found - cloning...");
Repository::clone(&self.repo_details)?
self.repo.clone(&self.repo_details)?
} else {
Repository::open(gitdir)?
self.repo.open(&gitdir)?
};
info!("Validating...");
git::validate(&repository, &self.repo_details)

View file

@ -1,5 +1,5 @@
use git_next_config::{BranchName, GitDir, RepoConfig};
use git_next_git::{self as git, GitRef, Repository};
use git_next_git::{self as git, GitRef, LegacyRepository};
use crate::{actors::repo::RepoActor, gitforge, types::MessageToken};
@ -31,7 +31,7 @@ impl super::ForgeLike for MockForgeEnv {
async fn branches_validate_positions(
&self,
_repository: Repository,
_repository: LegacyRepository,
_repo_config: RepoConfig,
_addr: actix::prelude::Addr<RepoActor>,
_message_token: MessageToken,
@ -41,7 +41,7 @@ impl super::ForgeLike for MockForgeEnv {
fn branch_reset(
&self,
_repository: &Repository,
_repository: &LegacyRepository,
_branch_name: BranchName,
_to_commit: GitRef,
_force: git::push::Force,
@ -53,7 +53,7 @@ impl super::ForgeLike for MockForgeEnv {
todo!()
}
fn repo_clone(&self, _gitdir: GitDir) -> Result<Repository, git::repository::Error> {
fn repo_clone(&self, _gitdir: GitDir) -> Result<LegacyRepository, git::repository::Error> {
todo!()
}
}

View file

@ -1,7 +1,7 @@
#![allow(dead_code)]
use git_next_config::{BranchName, GitDir, RepoConfig};
use git_next_git::{self as git, GitRef, RepoDetails, Repository};
use git_next_git::{self as git, GitRef, LegacyRepository, RepoDetails};
use kxio::network::Network;
#[cfg(feature = "forgejo")]
@ -38,7 +38,7 @@ pub trait ForgeLike {
/// positions as needed.
async fn branches_validate_positions(
&self,
repository: Repository,
repository: LegacyRepository,
repo_config: RepoConfig,
addr: actix::prelude::Addr<super::actors::repo::RepoActor>,
message_token: MessageToken,
@ -47,7 +47,7 @@ pub trait ForgeLike {
/// Moves a branch to a new commit.
fn branch_reset(
&self,
repository: &Repository,
repository: &LegacyRepository,
branch_name: BranchName,
to_commit: GitRef,
force: git::push::Force,
@ -57,7 +57,7 @@ pub trait ForgeLike {
async fn commit_status(&self, commit: &git::Commit) -> CommitStatus;
/// Clones a repo to disk.
fn repo_clone(&self, gitdir: GitDir) -> Result<Repository, git::repository::Error>;
fn repo_clone(&self, gitdir: GitDir) -> Result<LegacyRepository, git::repository::Error>;
}
#[derive(Clone, Debug)]