tests: add more tests to git crate
Some checks failed
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
Rust / build (push) Failing after 1m46s

This commit is contained in:
Paul Campbell 2024-06-09 10:21:09 +01:00
parent 926851db19
commit 7b11dc929d
24 changed files with 1931 additions and 326 deletions

View file

@ -1,5 +1,7 @@
use derive_more::Display;
/// The name of a Branch /// The name of a Branch
#[derive(Clone, Default, Debug, Hash, PartialEq, Eq, derive_more::Display)] #[derive(Clone, Default, Debug, Hash, PartialEq, Eq, Display, PartialOrd, Ord)]
pub struct BranchName(String); pub struct BranchName(String);
impl BranchName { impl BranchName {
pub fn new(str: impl Into<String>) -> Self { pub fn new(str: impl Into<String>) -> Self {

View file

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

View file

@ -40,21 +40,19 @@ pub struct Histories {
pub mod log { pub mod log {
use git_next_config as config;
pub type Result<T> = core::result::Result<T, Error>; pub type Result<T> = core::result::Result<T, Error>;
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum Error { pub enum Error {
#[error("gix: {0}")] #[error("branch: {branch}, error: {error}")]
Gix(String), Gix {
branch: config::BranchName,
error: String,
},
#[error("lock")] #[error("lock")]
Lock, Lock,
} }
impl From<String> for Error {
#[cfg(not(tarpaulin_include))]
fn from(e: String) -> Self {
Self::Gix(e)
}
}
} }

View file

@ -1,3 +1,6 @@
//
pub type Result<T> = core::result::Result<T, Error>;
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum Error { pub enum Error {
#[error("unable to open repo: {0}")] #[error("unable to open repo: {0}")]
@ -17,4 +20,12 @@ pub enum Error {
#[error("lock")] #[error("lock")]
Lock, Lock,
#[cfg(test)]
#[error("expected failure in test")]
TestFailureExpected,
#[cfg(test)]
#[error("test")]
TestResult(#[from] Box<dyn std::error::Error>),
} }

View file

@ -37,7 +37,7 @@ pub enum Error {
NoTreeInCommit(String), NoTreeInCommit(String),
#[error("no .git-next.toml file found in repo")] #[error("no .git-next.toml file found in repo")]
NoGitNextToml, FileNotFound,
#[error("find reference: {0}")] #[error("find reference: {0}")]
FindReference(String), FindReference(String),

View file

@ -1,9 +1,18 @@
//
use crate as git;
use derive_more::Constructor;
use crate::Commit; use crate::Commit;
#[derive(Clone, Debug, Hash, PartialEq, Eq, derive_more::Display)] #[derive(Clone, Constructor, Debug, Hash, PartialEq, Eq, derive_more::Display)]
pub struct GitRef(String); pub struct GitRef(String);
impl From<Commit> for GitRef { impl From<Commit> for GitRef {
fn from(value: Commit) -> Self { fn from(value: Commit) -> Self {
Self(value.sha().to_string()) Self(value.sha().to_string())
} }
} }
impl From<git::commit::Sha> for GitRef {
fn from(value: git::commit::Sha) -> Self {
Self(value.to_string())
}
}

View file

@ -2,7 +2,7 @@ use super::GitRef;
use crate as git; use crate as git;
use git_next_config as config; use git_next_config as config;
#[derive(Debug)] #[derive(Debug, PartialEq, Eq)]
pub enum Force { pub enum Force {
No, No,
From(GitRef), From(GitRef),
@ -40,6 +40,10 @@ pub enum Error {
#[error("gix iter init: {0}")] #[error("gix iter init: {0}")]
GixIterInit(#[from] gix::reference::iter::init::Error), GixIterInit(#[from] gix::reference::iter::init::Error),
#[cfg(test)]
#[error("test")]
TestResult(#[from] Box<dyn std::error::Error>),
} }
pub fn reset( pub fn reset(

View file

@ -7,9 +7,7 @@ use super::{Generation, GitRemote};
/// The derived information about a repo, used to interact with it /// The derived information about a repo, used to interact with it
#[derive(Clone, Default, Debug, derive_more::Display, derive_with::With)] #[derive(Clone, Default, Debug, derive_more::Display, derive_with::With)]
#[display("gen-{}:{}:{}/{}:{}@{}/{}@{}", generation, forge.forge_type(), #[display("gen-{}:{}:{}/{}", generation, forge.forge_type(), forge.forge_alias(), repo_alias )]
forge.forge_alias(), repo_alias, forge.user(), forge.hostname(), repo_path,
branch)]
pub struct RepoDetails { pub struct RepoDetails {
pub generation: Generation, pub generation: Generation,
pub repo_alias: RepoAlias, pub repo_alias: RepoAlias,

View file

@ -6,19 +6,12 @@ use std::{
sync::{Arc, Mutex}, sync::{Arc, Mutex},
}; };
use crate::{self as git, Repository}; use crate as git;
use crate::{
repository::{
open::{OpenRepository, OpenRepositoryLike},
Direction, RepositoryLike,
},
GitRemote, RepoDetails,
};
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 MockRepository {
open_repos: Arc<Mutex<HashMap<config::GitDir, MockOpenRepository>>>, open_repos: Arc<Mutex<HashMap<config::GitDir, git::repository::MockOpenRepository>>>,
} }
impl MockRepository { impl MockRepository {
pub fn new() -> Self { pub fn new() -> Self {
@ -27,8 +20,11 @@ impl MockRepository {
} }
} }
pub fn given_can_be_opened(&mut self, gitdir: &config::GitDir) -> MockOpenRepository { pub fn given_can_be_opened(
let open_repo = MockOpenRepository::new(); &mut self,
gitdir: &config::GitDir,
) -> git::repository::MockOpenRepository {
let open_repo = git::repository::MockOpenRepository::new();
#[allow(clippy::unwrap_used)] #[allow(clippy::unwrap_used)]
self.open_repos self.open_repos
.lock() .lock()
@ -37,19 +33,25 @@ impl MockRepository {
open_repo open_repo
} }
pub fn seal(self) -> (Repository, Self) { pub fn seal(self) -> (git::Repository, Self) {
(Repository::Mock(self.clone()), self) (git::Repository::Mock(self.clone()), self)
} }
pub fn unseal(self, _repository: 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> {
self.open_repos
.lock()
.map(|or| or.get(gitdir).cloned())
.unwrap_or(None)
}
} }
impl RepositoryLike for MockRepository { impl git::repository::RepositoryLike for MockRepository {
fn open( fn open(
&self, &self,
gitdir: &config::GitDir, gitdir: &config::GitDir,
) -> std::result::Result<OpenRepository, crate::repository::Error> { ) -> std::result::Result<git::repository::OpenRepository, crate::repository::Error> {
#[allow(clippy::unwrap_used)] #[allow(clippy::unwrap_used)]
self.open_repos self.open_repos
.lock() .lock()
@ -62,96 +64,8 @@ impl RepositoryLike for MockRepository {
fn git_clone( fn git_clone(
&self, &self,
_repo_details: &RepoDetails, _repo_details: &git::RepoDetails,
) -> std::result::Result<OpenRepository, crate::repository::Error> { ) -> std::result::Result<git::repository::OpenRepository, crate::repository::Error> {
todo!("MockRepository::git_clone") todo!("MockRepository::git_clone")
} }
} }
#[derive(Clone, Debug, Default)]
pub struct MockOpenRepository {
default_push_remote: Arc<Mutex<Option<GitRemote>>>,
default_fetch_remote: Arc<Mutex<Option<GitRemote>>>,
}
impl MockOpenRepository {
pub fn new() -> Self {
Self::default()
}
pub fn given_has_default_remote(&mut self, direction: Direction, remote: Option<GitRemote>) {
#[allow(clippy::unwrap_used)]
match direction {
Direction::Push => self
.default_push_remote
.lock()
.map(|mut o| match remote {
Some(gr) => o.replace(gr),
None => o.take(),
})
.unwrap(),
Direction::Fetch => self
.default_fetch_remote
.lock()
.map(|mut o| match remote {
Some(gr) => o.replace(gr),
None => o.take(),
})
.unwrap(),
};
}
}
impl From<MockOpenRepository> for OpenRepository {
fn from(value: MockOpenRepository) -> Self {
Self::Mock(value)
}
}
#[allow(clippy::unwrap_used)]
impl OpenRepositoryLike for MockOpenRepository {
fn remote_branches(&self) -> git::push::Result<Vec<config::BranchName>> {
todo!("MockOpenRepository::remote_branched")
}
fn find_default_remote(&self, direction: Direction) -> Option<GitRemote> {
match direction {
Direction::Push => self
.default_push_remote
.lock()
.map(|r| r.clone())
.unwrap_or(None),
Direction::Fetch => self
.default_fetch_remote
.lock()
.map(|r| r.clone())
.unwrap_or(None),
}
}
fn fetch(&self) -> core::result::Result<(), crate::fetch::Error> {
todo!("MockOpenRepository::fetch")
}
fn push(
&self,
_repo_details: &RepoDetails,
_branch_name: &git_next_config::BranchName,
_to_commit: &crate::GitRef,
_force: &crate::push::Force,
) -> core::result::Result<(), crate::push::Error> {
todo!("MockOpenRepository::push")
}
fn commit_log(
&self,
_branch_name: &git_next_config::BranchName,
_find_commits: &[git::Commit],
) -> core::result::Result<Vec<crate::Commit>, git::commit::log::Error> {
todo!("MockOpenRepository::commit_log")
}
fn read_file(
&self,
_branch_name: &git_next_config::BranchName,
_file_name: &str,
) -> git::file::Result<String> {
todo!("MockOpenRepository::read_file")
}
}

View file

@ -1,7 +1,10 @@
// //
#[cfg(test)]
mod mock; mod mock;
mod open; mod open;
mod real; mod real;
#[cfg(test)]
mod test;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
@ -9,29 +12,59 @@ mod tests;
use git_next_config as config; use git_next_config as config;
use git_next_config::GitDir; use git_next_config::GitDir;
#[cfg(test)]
pub use mock::MockRepository; pub use mock::MockRepository;
#[cfg(test)]
pub use open::otest::OnFetch;
#[cfg(test)]
pub use open::otest::OnPush;
#[cfg(test)]
pub use open::MockOpenRepository;
pub use open::OpenRepository; pub use open::OpenRepository;
pub use open::OpenRepositoryLike;
pub use open::RealOpenRepository;
pub use real::RealRepository;
use tracing::info; use tracing::info;
#[cfg(test)]
use crate::repository::test::TestRepository;
use crate::validation::repo::validate_repo; use crate::validation::repo::validate_repo;
use super::RepoDetails; use super::RepoDetails;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
#[allow(clippy::large_enum_variant)]
pub enum Repository { pub enum Repository {
Real, Real,
#[cfg(test)]
Mock(MockRepository), Mock(MockRepository),
#[cfg(test)]
Test(TestRepository),
} }
pub const fn new() -> Repository { pub const fn new() -> Repository {
Repository::Real Repository::Real
} }
#[cfg(test)]
pub fn mock() -> MockRepository { pub fn mock() -> MockRepository {
MockRepository::new() MockRepository::new()
// (Repository::Mock(mock_repository.clone()), mock_repository)
} }
#[cfg(test)]
pub const fn test(fs: kxio::fs::FileSystem) -> TestRepository {
TestRepository::new(false, fs, vec![], vec![])
}
// #[cfg(test)]
// pub const fn test_bare(fs: kxio::fs::FileSystem) -> TestRepository {
// TestRepository::new(true, fs, vec![], vec![])
// }
/// Opens a repository, cloning if necessary /// Opens a repository, cloning if necessary
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
#[cfg(not(tarpaulin_include))] // requires network access to either clone new and/or fetch.
pub fn open( pub fn open(
repository: &Repository, repository: &Repository,
repo_details: &RepoDetails, repo_details: &RepoDetails,
@ -59,7 +92,10 @@ impl std::ops::Deref for Repository {
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
match self { match self {
Self::Real => &real::RealRepository, Self::Real => &real::RealRepository,
#[cfg(test)]
Self::Mock(mock_repository) => mock_repository, Self::Mock(mock_repository) => mock_repository,
#[cfg(test)]
Self::Test(test_repository) => test_repository,
} }
} }
} }

View file

@ -1,32 +1,89 @@
// //
#![allow(dead_code)]
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
pub mod oreal; pub mod oreal;
use std::sync::{Arc, Mutex}; #[cfg(test)]
pub mod otest;
#[cfg(test)]
pub mod omock;
use std::{
path::Path,
sync::{Arc, Mutex},
};
use crate as git; use crate as git;
use git::repository::open::oreal::RealOpenRepository;
use git::repository::Direction; use git::repository::Direction;
use git_next_config as config; use git_next_config as config;
#[cfg(test)]
pub use omock::MockOpenRepository;
pub use oreal::RealOpenRepository;
#[cfg(test)]
pub use otest::TestOpenRepository;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum OpenRepository { pub enum OpenRepository {
/// A real git repository.
///
/// This variant is the normal implementation for use in production code.
Real(RealOpenRepository), Real(RealOpenRepository),
Mock(git::repository::mock::MockOpenRepository), // TODO: (#38) contain a mock model of a repo /// A fake git repository.
///
/// This variant has no on-disk presense, and only fakes some of
/// the behaviour of a git repository. Once the [Self::LocalOnly]
/// variant is ready for use, tests should be converted to using
/// that instead.
#[cfg(test)]
Mock(git::repository::MockOpenRepository), // TODO: (#38) contain a mock model of a repo
/// A real git repository, but with preprogrammed responses to network access.
///
/// This variant is for use only in testing. Requests to methods
/// that would require network access, such as to the git remote
/// server will result in an error, unless a predefined change
/// has been scheduled for that request.
#[cfg(test)]
Test(TestOpenRepository),
} }
impl OpenRepository {
pub fn real(gix_repo: gix::Repository) -> Self { pub fn real(gix_repo: gix::Repository) -> OpenRepository {
Self::Real(oreal::RealOpenRepository::new(Arc::new(Mutex::new( OpenRepository::Real(oreal::RealOpenRepository::new(Arc::new(Mutex::new(
gix_repo, gix_repo,
)))) ))))
}
#[cfg(not(tarpaulin_include))] // don't test mocks
pub const fn mock(mock: git::repository::mock::MockOpenRepository) -> Self {
Self::Mock(mock)
}
} }
#[cfg(not(tarpaulin_include))] // don't test mocks
#[cfg(test)]
pub const fn mock(mock: git::repository::MockOpenRepository) -> OpenRepository {
OpenRepository::Mock(mock)
}
#[cfg(not(tarpaulin_include))] // don't test mocks
#[cfg(test)]
pub fn test(
gitdir: &config::GitDir,
fs: kxio::fs::FileSystem,
on_fetch: Vec<otest::OnFetch>,
on_push: Vec<otest::OnPush>,
) -> OpenRepository {
OpenRepository::Test(TestOpenRepository::new(gitdir, fs, on_fetch, on_push))
}
#[cfg(not(tarpaulin_include))] // don't test mocks
#[cfg(test)]
pub fn test_bare(
gitdir: &config::GitDir,
fs: kxio::fs::FileSystem,
on_fetch: Vec<otest::OnFetch>,
on_push: Vec<otest::OnPush>,
) -> OpenRepository {
OpenRepository::Test(TestOpenRepository::new_bare(gitdir, fs, on_fetch, on_push))
}
pub trait OpenRepositoryLike { pub trait 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>;
@ -52,7 +109,7 @@ pub trait OpenRepositoryLike {
fn read_file( fn read_file(
&self, &self,
branch_name: &config::BranchName, branch_name: &config::BranchName,
file_name: &str, file_name: &Path,
) -> git::file::Result<String>; ) -> git::file::Result<String>;
} }
impl std::ops::Deref for OpenRepository { impl std::ops::Deref for OpenRepository {
@ -61,7 +118,10 @@ impl std::ops::Deref for OpenRepository {
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
match self { match self {
Self::Real(real) => real, Self::Real(real) => real,
#[cfg(test)]
Self::Mock(mock) => mock, Self::Mock(mock) => mock,
#[cfg(test)]
Self::Test(test) => test,
} }
} }
} }

View file

@ -0,0 +1,121 @@
//
use crate as git;
use git_next_config as config;
use std::{
path::Path,
sync::{Arc, Mutex},
};
#[derive(Clone, Debug, Default)]
pub struct MockOpenRepository {
default_push_remote: Arc<Mutex<Option<git::GitRemote>>>,
default_fetch_remote: Arc<Mutex<Option<git::GitRemote>>>,
operations: Arc<Mutex<Vec<String>>>,
}
impl MockOpenRepository {
pub fn new() -> Self {
Self::default()
}
pub fn given_has_default_remote(
&mut self,
direction: git::repository::Direction,
remote: Option<git::GitRemote>,
) {
#[allow(clippy::unwrap_used)]
match direction {
git::repository::Direction::Push => self
.default_push_remote
.lock()
.map(|mut o| match remote {
Some(gr) => o.replace(gr),
None => o.take(),
})
.unwrap(),
git::repository::Direction::Fetch => self
.default_fetch_remote
.lock()
.map(|mut o| match remote {
Some(gr) => o.replace(gr),
None => o.take(),
})
.unwrap(),
};
}
pub fn operations(&self) -> Vec<String> {
self.operations
.lock()
.map(|operations| operations.clone())
.unwrap_or_default()
}
}
impl From<MockOpenRepository> for git::OpenRepository {
fn from(value: MockOpenRepository) -> Self {
Self::Mock(value)
}
}
#[allow(clippy::unwrap_used)]
impl git::repository::OpenRepositoryLike for MockOpenRepository {
fn remote_branches(&self) -> git::push::Result<Vec<config::BranchName>> {
todo!("MockOpenRepository::remote_branched")
}
fn find_default_remote(&self, direction: git::repository::Direction) -> Option<git::GitRemote> {
match direction {
git::repository::Direction::Push => self
.default_push_remote
.lock()
.map(|r| r.clone())
.unwrap_or(None),
git::repository::Direction::Fetch => self
.default_fetch_remote
.lock()
.map(|r| r.clone())
.unwrap_or(None),
}
}
fn fetch(&self) -> core::result::Result<(), crate::fetch::Error> {
self.operations
.lock()
.map_err(|_| crate::fetch::Error::Lock)
.map(|mut operations| operations.push("fetch".to_string()))?;
Ok(())
}
fn push(
&self,
repo_details: &git::RepoDetails,
branch_name: &git_next_config::BranchName,
to_commit: &crate::GitRef,
force: &crate::push::Force,
) -> core::result::Result<(), crate::push::Error> {
let forge_alias = repo_details.forge.forge_alias();
let repo_alias = &repo_details.repo_alias;
self.operations
.lock()
.map_err(|_| crate::fetch::Error::Lock)
.map(|mut operations| {
operations.push(format!(
"push fa:{forge_alias} ra:{repo_alias} bn:{branch_name} tc:{to_commit} f:{force}"
))
})?;
Ok(())
}
fn commit_log(
&self,
_branch_name: &git_next_config::BranchName,
_find_commits: &[git::Commit],
) -> core::result::Result<Vec<crate::Commit>, git::commit::log::Error> {
todo!("MockOpenRepository::commit_log")
}
fn read_file(
&self,
_branch_name: &git_next_config::BranchName,
_file_name: &Path,
) -> git::file::Result<String> {
todo!("MockOpenRepository::read_file")
}
}

View file

@ -1,13 +1,17 @@
// //
use crate as git; use crate as git;
use config::BranchName; use config::BranchName;
use derive_more::Constructor;
use git_next_config as config; use git_next_config as config;
use gix::bstr::BStr; use gix::bstr::BStr;
use std::sync::{Arc, Mutex}; use std::{
path::Path,
sync::{Arc, Mutex},
};
use tracing::{info, warn}; use tracing::{info, warn};
#[derive(Clone, Debug, derive_more::Constructor)] #[derive(Clone, Debug, Constructor)]
pub struct RealOpenRepository(Arc<Mutex<gix::Repository>>); pub struct RealOpenRepository(Arc<Mutex<gix::Repository>>);
impl super::OpenRepositoryLike for RealOpenRepository { impl super::OpenRepositoryLike for RealOpenRepository {
fn remote_branches(&self) -> git::push::Result<Vec<config::BranchName>> { fn remote_branches(&self) -> git::push::Result<Vec<config::BranchName>> {
@ -45,6 +49,7 @@ impl super::OpenRepositoryLike for RealOpenRepository {
} }
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
#[cfg(not(tarpaulin_include))] // would require writing to external service
fn fetch(&self) -> Result<(), git::fetch::Error> { fn fetch(&self) -> Result<(), git::fetch::Error> {
let Ok(repository) = self.0.lock() else { let Ok(repository) = self.0.lock() else {
#[cfg(not(tarpaulin_include))] // don't test mutex lock failure #[cfg(not(tarpaulin_include))] // don't test mutex lock failure
@ -123,23 +128,40 @@ impl super::OpenRepositoryLike for RealOpenRepository {
.lock() .lock()
.map_err(|_| git::commit::log::Error::Lock) .map_err(|_| git::commit::log::Error::Lock)
.map(|repo| { .map(|repo| {
let branch_name = format!("remotes/origin/{branch_name}"); let branch = format!("remotes/origin/{branch_name}");
let branch_name = BStr::new(&branch_name); let branch = BStr::new(&branch);
let branch_head = repo let branch_head = repo
.rev_parse_single(branch_name) .rev_parse_single(branch)
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())
let object = branch_head.object().map_err(|e| e.to_string())?; .map_err(as_gix_error(branch_name.clone()))?;
let commit = object.try_into_commit().map_err(|e| e.to_string())?; let object = branch_head
.object()
.map_err(|e| e.to_string())
.map_err(as_gix_error(branch_name.clone()))?;
let commit = object
.try_into_commit()
.map_err(|e| e.to_string())
.map_err(as_gix_error(branch_name.clone()))?;
let walk = repo let walk = repo
.rev_walk([commit.id]) .rev_walk([commit.id])
.all() .all()
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())
.map_err(as_gix_error(branch_name.clone()))?;
let mut commits = vec![]; let mut commits = vec![];
for item in walk.take(limit) { for item in walk.take(limit) {
let item = item.map_err(|e| e.to_string())?; let item = item
let commit = item.object().map_err(|e| e.to_string())?; .map_err(|e| e.to_string())
.map_err(as_gix_error(branch_name.clone()))?;
let commit = item
.object()
.map_err(|e| e.to_string())
.map_err(as_gix_error(branch_name.clone()))?;
let id = commit.id().to_string(); let id = commit.id().to_string();
let message = commit.message_raw().map_err(|e| e.to_string())?.to_string(); let message = commit
.message_raw()
.map_err(|e| e.to_string())
.map_err(as_gix_error(branch_name.clone()))?
.to_string();
let commit = git::Commit::new( let commit = git::Commit::new(
git::commit::Sha::new(id), git::commit::Sha::new(id),
git::commit::Message::new(message), git::commit::Message::new(message),
@ -154,11 +176,11 @@ impl super::OpenRepositoryLike for RealOpenRepository {
})? })?
} }
#[tracing::instrument(skip_all, fields(%branch_name, %file_name))] #[tracing::instrument(skip_all, fields(%branch_name, ?file_name))]
fn read_file( fn read_file(
&self, &self,
branch_name: &config::BranchName, branch_name: &config::BranchName,
file_name: &str, file_name: &Path,
) -> git::file::Result<String> { ) -> git::file::Result<String> {
self.0 self.0
.lock() .lock()
@ -171,8 +193,8 @@ impl super::OpenRepositoryLike for RealOpenRepository {
let commit = obj.into_commit(); let commit = obj.into_commit();
let tree = commit.tree()?; let tree = commit.tree()?;
let ent = tree let ent = tree
.find_entry(".git-next.toml") .find_entry(file_name.to_string_lossy().to_string())
.ok_or(git::file::Error::NoGitNextToml)?; .ok_or(git::file::Error::FileNotFound)?;
let fobj = ent.object()?; let fobj = ent.object()?;
let blob = fobj.into_blob().take_data(); let blob = fobj.into_blob().take_data();
let content = String::from_utf8(blob)?; let content = String::from_utf8(blob)?;
@ -181,6 +203,10 @@ impl super::OpenRepositoryLike for RealOpenRepository {
} }
} }
fn as_gix_error(branch: BranchName) -> impl FnOnce(String) -> git::commit::log::Error {
|error| git::commit::log::Error::Gix { branch, error }
}
impl From<&gix::Url> for git::GitRemote { impl From<&gix::Url> for git::GitRemote {
fn from(url: &gix::Url) -> Self { fn from(url: &gix::Url) -> Self {
let host = url.host().unwrap_or_default(); let host = url.host().unwrap_or_default();

View file

@ -0,0 +1,184 @@
//
use crate as git;
use derive_more::Constructor;
use git_next_config as config;
use std::{
cell::Cell,
path::Path,
sync::{Arc, Mutex},
};
use assert2::let_assert;
pub type OnFetchFn =
fn(&config::RepoBranches, &config::GitDir, &kxio::fs::FileSystem) -> git::fetch::Result<()>;
#[derive(Clone, Debug, Constructor)]
pub struct OnFetch {
repo_branches: config::RepoBranches,
gitdir: config::GitDir,
fs: kxio::fs::FileSystem,
action: OnFetchFn,
}
impl OnFetch {
pub fn invoke(&self) -> git::fetch::Result<()> {
(self.action)(&self.repo_branches, &self.gitdir, &self.fs)
}
}
pub type OnPushFn = fn(
&git::RepoDetails,
&config::BranchName,
&git::GitRef,
&git::push::Force,
&config::RepoBranches,
&config::GitDir,
&kxio::fs::FileSystem,
) -> git::push::Result<()>;
#[derive(Clone, Debug, Constructor)]
pub struct OnPush {
repo_branches: config::RepoBranches,
gitdir: config::GitDir,
fs: kxio::fs::FileSystem,
action: OnPushFn,
}
impl OnPush {
pub fn invoke(
&self,
repo_details: &git::RepoDetails,
branch_name: &config::BranchName,
to_commit: &git::GitRef,
force: &git::push::Force,
) -> git::push::Result<()> {
(self.action)(
repo_details,
branch_name,
to_commit,
force,
&self.repo_branches,
&self.gitdir,
&self.fs,
)
}
}
#[derive(Clone, Debug)]
pub struct TestOpenRepository {
gitdir: config::GitDir,
fs: kxio::fs::FileSystem,
on_fetch: Vec<OnFetch>,
fetch_counter: Cell<usize>,
on_push: Vec<OnPush>,
push_counter: Cell<usize>,
real: git::repository::RealOpenRepository,
}
impl git::repository::OpenRepositoryLike for TestOpenRepository {
fn remote_branches(&self) -> git::push::Result<Vec<config::BranchName>> {
self.real.remote_branches()
}
fn find_default_remote(&self, direction: git::repository::Direction) -> Option<git::GitRemote> {
self.real.find_default_remote(direction)
}
fn fetch(&self) -> Result<(), git::fetch::Error> {
let i = self.fetch_counter.get();
eprintln!("Fetch: {i}");
self.fetch_counter.set(i + 1);
self.on_fetch.get(i).map(|f| f.invoke()).unwrap_or_else(|| {
unimplemented!("Unexpected fetch");
})
}
fn push(
&self,
repo_details: &git::RepoDetails,
branch_name: &config::BranchName,
to_commit: &git::GitRef,
force: &git::push::Force,
) -> git::push::Result<()> {
let i = self.push_counter.get();
self.push_counter.set(i + 1);
self.on_push
.get(i)
.map(|f| f.invoke(repo_details, branch_name, to_commit, force))
.unwrap_or_else(|| {
unimplemented!("Unexpected push");
})
}
fn commit_log(
&self,
branch_name: &config::BranchName,
find_commits: &[git::Commit],
) -> git::commit::log::Result<Vec<crate::Commit>> {
self.real.commit_log(branch_name, find_commits)
}
fn read_file(
&self,
branch_name: &config::BranchName,
file_name: &Path,
) -> git::file::Result<String> {
self.real.read_file(branch_name, file_name)
}
}
impl TestOpenRepository {
pub fn new(
gitdir: &config::GitDir,
fs: kxio::fs::FileSystem,
on_fetch: Vec<OnFetch>,
on_push: Vec<OnPush>,
) -> Self {
let pathbuf = fs.base().join(gitdir.to_path_buf());
let_assert!(Ok(gix) = gix::init(pathbuf), "git init bare");
Self::write_origin(gitdir, &fs);
Self {
gitdir: gitdir.clone(),
fs,
on_fetch,
fetch_counter: Cell::new(0),
on_push,
push_counter: Cell::new(0),
real: git::repository::RealOpenRepository::new(Arc::new(Mutex::new(gix))),
}
}
pub fn new_bare(
gitdir: &config::GitDir,
fs: kxio::fs::FileSystem,
on_fetch: Vec<OnFetch>,
on_push: Vec<OnPush>,
) -> Self {
let pathbuf = fs.base().join(gitdir.to_path_buf());
let_assert!(Ok(gix) = gix::init_bare(pathbuf), "git init bare");
Self::write_origin(gitdir, &fs);
Self {
gitdir: gitdir.clone(),
fs,
on_fetch,
fetch_counter: Cell::new(0),
on_push,
push_counter: Cell::new(0),
real: git::repository::RealOpenRepository::new(Arc::new(Mutex::new(gix))),
}
}
fn write_origin(gitdir: &config::GitDir, fs: &kxio::fs::FileSystem) {
let config_file = fs.base().join(gitdir.to_path_buf()).join(".git/config");
#[allow(clippy::expect_used)]
let contents = fs
.file_read_to_string(&config_file)
.expect("read original .git/config");
let updated_contents = format!(
r#"{contents}
[remote "origin"]
url = git@foo.example,net
fetch = +refs/heads/*:refs/remotes/origin/*
"#
);
eprintln!("{config_file:?}:\n{updated_contents}");
#[allow(clippy::expect_used)]
fs.file_write(&config_file, &updated_contents)
.expect("write updated .git/config");
}
}

View file

@ -1,22 +1,299 @@
// //
use crate as git;
use crate::repository::RepositoryLike as _;
use git::tests::given;
use git::tests::then;
use git_next_config as config;
use assert2::let_assert;
type TestResult = Result<(), Box<dyn std::error::Error>>;
mod server_repo_config {
use super::*;
use std::path::PathBuf;
use assert2::let_assert;
use crate::tests::given;
#[test]
fn should_not_return_repo_config_when_no_branches() {
let main = None;
let next = None;
let dev = None;
let src =
config::ServerRepoConfig::new(given::a_name(), given::a_name(), None, main, next, dev);
let_assert!(None = src.repo_config());
}
#[test]
fn should_return_repo_config_when_branches() {
let main = given::a_name();
let next = given::a_name();
let dev = given::a_name();
let src = config::ServerRepoConfig::new(
given::a_name(),
given::a_name(),
None,
Some(main.clone()),
Some(next.clone()),
Some(dev.clone()),
);
let_assert!(Some(rc) = src.repo_config());
assert_eq!(
rc,
config::RepoConfig::new(
config::RepoBranches::new(main, next, dev),
config::RepoConfigSource::Server
)
);
}
#[test]
fn should_return_repo() {
let repo_path = given::a_name();
let src = config::ServerRepoConfig::new(
repo_path.clone(),
given::a_name(),
None,
Some(given::a_name()),
Some(given::a_name()),
Some(given::a_name()),
);
assert_eq!(src.repo(), config::RepoPath::new(repo_path));
}
#[test]
fn should_return_branch() {
let branch = given::a_name();
let src = config::ServerRepoConfig::new(
given::a_name(),
branch.clone(),
None,
Some(given::a_name()),
Some(given::a_name()),
Some(given::a_name()),
);
assert_eq!(src.branch(), config::BranchName::new(branch));
}
#[test]
fn should_return_gitdir() {
let gitdir = given::a_name();
let src = config::ServerRepoConfig::new(
given::a_name(),
given::a_name(),
Some(gitdir.clone().into()),
Some(given::a_name()),
Some(given::a_name()),
Some(given::a_name()),
);
assert_eq!(
src.gitdir(),
Some(config::GitDir::new(&PathBuf::default().join(gitdir)))
);
}
}
mod repo_config {
use super::*;
#[test]
fn should_parse_toml() -> TestResult {
let main = given::a_name();
let next = given::a_name();
let dev = given::a_name();
let toml = format!(
r#"
[branches]
main = "{main}"
next = "{next}"
dev = "{dev}"
"#
);
let rc = config::RepoConfig::load(toml.as_str())?;
assert_eq!(
rc,
config::RepoConfig::new(
config::RepoBranches::new(main, next, dev),
config::RepoConfigSource::Repo // reading from repo is the default
)
);
Ok(())
}
#[test]
fn should_return_branches() {
let main = given::a_name();
let next = given::a_name();
let dev = given::a_name();
let branches = config::RepoBranches::new(main, next, dev);
let repo_config = config::RepoConfig::new(branches.clone(), config::RepoConfigSource::Repo);
assert_eq!(repo_config.branches(), &branches);
}
#[test]
fn should_return_source() {
let main = given::a_name();
let next = given::a_name();
let dev = given::a_name();
let repo_config = config::RepoConfig::new(
config::RepoBranches::new(main, next, dev),
config::RepoConfigSource::Repo,
);
assert_eq!(repo_config.source(), config::RepoConfigSource::Repo);
}
}
mod forge_config {
use super::*;
use std::collections::BTreeMap;
use secrecy::ExposeSecret;
use crate::tests::given;
#[test]
fn should_return_repos() {
let forge_type = config::ForgeType::MockForge;
let hostname = given::a_name();
let user = given::a_name();
let token = given::a_name();
// alphabetical order by key
let red_name = format!("a-{}", given::a_name());
let blue_name = format!("b-{}", given::a_name());
let red = config::ServerRepoConfig::new(
red_name.clone(),
given::a_name(),
None,
None,
None,
None,
);
let blue = config::ServerRepoConfig::new(
blue_name.clone(),
given::a_name(),
None,
None,
None,
None,
);
let mut repos = BTreeMap::new();
repos.insert(red_name.clone(), red.clone());
repos.insert(blue_name.clone(), blue.clone());
let fc = config::ForgeConfig::new(forge_type, hostname, user, token, repos);
let returned_repos = fc.repos().collect::<Vec<_>>();
assert_eq!(
returned_repos,
vec![
// alphabetical order by key
(config::RepoAlias::new(red_name.as_str()), &red),
(config::RepoAlias::new(blue_name.as_str()), &blue),
]
);
}
#[test]
fn should_return_forge_type() {
let forge_type = config::ForgeType::MockForge;
let hostname = given::a_name();
let user = given::a_name();
let token = given::a_name();
let repos = BTreeMap::new();
let fc = config::ForgeConfig::new(forge_type, hostname, user, token, repos);
assert_eq!(fc.forge_type(), config::ForgeType::MockForge);
}
#[test]
fn should_return_hostname() {
let forge_type = config::ForgeType::MockForge;
let hostname = given::a_name();
let user = given::a_name();
let token = given::a_name();
let repos = BTreeMap::new();
let fc = config::ForgeConfig::new(forge_type, hostname.clone(), user, token, repos);
assert_eq!(fc.hostname(), config::Hostname::new(hostname));
}
#[test]
fn should_return_user() {
let forge_type = config::ForgeType::MockForge;
let hostname = given::a_name();
let user = given::a_name();
let token = given::a_name();
let repos = BTreeMap::new();
let fc = config::ForgeConfig::new(forge_type, hostname, user.clone(), token, repos);
assert_eq!(fc.user(), config::User::new(user));
}
#[test]
fn should_return_token() {
let forge_type = config::ForgeType::MockForge;
let hostname = given::a_name();
let user = given::a_name();
let token = given::a_name();
let repos = BTreeMap::new();
let fc = config::ForgeConfig::new(forge_type, hostname, user, token.clone(), repos);
assert_eq!(fc.token().expose_secret(), token.as_str());
}
#[test]
fn should_return_repo() {
let forge_type = config::ForgeType::MockForge;
let hostname = given::a_name();
let user = given::a_name();
let token = given::a_name();
let red_name = given::a_name();
let blue_name = given::a_name();
let red = config::ServerRepoConfig::new(
red_name.clone(),
given::a_name(),
None,
None,
None,
None,
);
let blue = config::ServerRepoConfig::new(
blue_name.clone(),
given::a_name(),
None,
None,
None,
None,
);
let mut repos = BTreeMap::new();
repos.insert(red_name.clone(), red.clone());
repos.insert(blue_name, blue);
let fc = config::ForgeConfig::new(forge_type, hostname, user, token, repos);
let returned_repo = fc.get_repo(red_name.as_str());
assert_eq!(returned_repo, Some(&red),);
}
}
mod find_default_remote { mod find_default_remote {
use assert2::let_assert; use super::*;
use git_next_config::{GitDir, Hostname, RepoPath};
use assert2::let_assert;
use crate::GitRemote;
#[test] #[test]
fn should_find_default_push_remote() { fn should_find_default_push_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 = 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::new().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,
GitRemote::new( git::GitRemote::new(
Hostname::new("git.kemitix.net"), config::Hostname::new("git.kemitix.net"),
RepoPath::new("kemitix/git-next".to_string()) config::RepoPath::new("kemitix/git-next".to_string())
) )
) )
} }
@ -36,3 +313,146 @@ mod fetch {
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 {
use git::tests::given;
use crate::tests::then;
use super::*;
#[test]
// assumes running in the git-next repo which should have main, next and dev as remote branches
fn should_return_single_item_in_commit_log_when_not_searching() -> 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.main())?;
let_assert!(Ok(result) = open_repository.commit_log(&branches.main(), &[]));
assert_eq!(result.len(), 1);
Ok(())
}
#[test]
// assumes running in the git-next repo which should have main, next and dev as remote branches
fn should_return_capacity_50_in_commit_log_when_searching_for_garbage() -> TestResult {
let_assert!(Ok(fs) = kxio::fs::temp());
let branch_name = given::a_branch_name();
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));
for _ in [0; 60] {
// create 60 commits
then::create_a_commit_on_branch(&fs, &gitdir, &branch_name)?;
}
let_assert!(Ok(result) = open_repository.commit_log(&branch_name, &[given::a_commit()]));
assert_eq!(result.len(), 50);
Ok(())
}
#[test]
// assumes running in the git-next repo which should have main, next and dev as remote branches
fn should_return_5_in_commit_log_when_searching_for_5th_item() -> TestResult {
let_assert!(Ok(fs) = kxio::fs::temp());
let branch_name = given::a_branch_name();
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));
for _ in [0; 10] {
then::create_a_commit_on_branch(&fs, &gitdir, &branch_name)?;
}
// search to garbage to get all 10 items
let_assert!(
Ok(long_list) = open_repository.commit_log(&branch_name, &[given::a_commit()]),
"get commit_log"
);
// pick the 5th item
let search = &long_list[4]; // zero-based
// search for the 25th item
let_assert!(Ok(result) = open_repository.commit_log(&branch_name, &[search.clone()]));
// returns
assert_eq!(result.len(), 5);
Ok(())
}
}
mod read_file {
use git::tests::given;
use git::tests::then;
type TestResult = Result<(), Box<dyn std::error::Error>>;
use super::*;
#[test]
// assumes running in the git-next repo which should have main, next and dev as remote branches
fn should_return_file() -> TestResult {
let_assert!(Ok(fs) = kxio::fs::temp());
let repo_config = given::a_repo_config();
let file_name = given::a_pathbuf();
let contents = given::a_name();
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));
then::commit_named_file_to_branch(
&file_name,
&contents,
&fs,
&gitdir,
&repo_config.branches().main(),
)?;
// then::create_a_commit_on_branch(&fs, &gitdir, &repo_config.branches().main())?;
let_assert!(
Ok(result) = open_repository.read_file(&repo_config.branches().main(), &file_name),
"read file"
);
assert_eq!(result, contents);
Ok(())
}
#[test]
// assumes running in the git-next repo which should have main, next and dev as remote branches
fn should_error_on_missing_file() -> 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.dev())?;
let_assert!(
Err(err) = open_repository.read_file(&branches.dev(), &given::a_pathbuf()),
"read file"
);
eprintln!("err: {err:#?}");
assert!(matches!(err, git::file::Error::FileNotFound));
Ok(())
}
}

View file

@ -1,22 +1,23 @@
//
use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicBool;
use crate as git;
use derive_more::Deref as _; use derive_more::Deref as _;
use git_next_config::GitDir; use git_next_config::GitDir;
use crate::{
repository::{open::OpenRepository, Error, RepositoryLike},
RepoDetails,
};
pub struct RealRepository; pub struct RealRepository;
impl RepositoryLike for RealRepository { impl git::repository::RepositoryLike for RealRepository {
fn open(&self, gitdir: &GitDir) -> Result<OpenRepository, Error> { fn open(&self, gitdir: &GitDir) -> Result<git::OpenRepository, git::repository::Error> {
let gix_repo = gix::ThreadSafeRepository::open(gitdir.to_path_buf())?.to_thread_local(); let gix_repo = gix::ThreadSafeRepository::open(gitdir.to_path_buf())?.to_thread_local();
Ok(OpenRepository::real(gix_repo)) Ok(git::repository::open::real(gix_repo))
} }
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
fn git_clone(&self, repo_details: &RepoDetails) -> Result<OpenRepository, Error> { #[cfg(not(tarpaulin_include))] // requires external server
fn git_clone(
&self,
repo_details: &git::RepoDetails,
) -> Result<git::OpenRepository, git::repository::Error> {
tracing::info!("creating"); tracing::info!("creating");
use secrecy::ExposeSecret; use secrecy::ExposeSecret;
let (gix_repo, _outcome) = gix::prepare_clone_bare( let (gix_repo, _outcome) = gix::prepare_clone_bare(
@ -26,6 +27,6 @@ impl RepositoryLike for RealRepository {
.fetch_only(gix::progress::Discard, &AtomicBool::new(false))?; .fetch_only(gix::progress::Discard, &AtomicBool::new(false))?;
tracing::info!("created"); tracing::info!("created");
Ok(OpenRepository::real(gix_repo)) Ok(git::repository::open::real(gix_repo))
} }
} }

View file

@ -0,0 +1,53 @@
//
use derive_more::Constructor;
use crate as git;
use git::repository::RepositoryLike;
use git_next_config as config;
#[derive(Clone, Debug, Constructor)]
pub struct TestRepository {
is_bare: bool,
fs: kxio::fs::FileSystem,
on_fetch: Vec<git::repository::open::otest::OnFetch>,
on_push: Vec<git::repository::open::otest::OnPush>,
}
impl TestRepository {
pub fn on_fetch(&mut self, on_fetch: git::repository::OnFetch) {
self.on_fetch.push(on_fetch);
}
pub fn on_push(&mut self, on_push: git::repository::OnPush) {
self.on_push.push(on_push);
}
pub const fn fs(&self) -> &kxio::fs::FileSystem {
&self.fs
}
}
impl RepositoryLike for TestRepository {
fn open(&self, gitdir: &config::GitDir) -> super::Result<crate::OpenRepository> {
if self.is_bare {
Ok(git::repository::open::test_bare(
gitdir,
self.fs.clone(),
self.on_fetch.clone(),
self.on_push.clone(),
))
} else {
Ok(git::repository::open::test(
gitdir,
self.fs.clone(),
self.on_fetch.clone(),
self.on_push.clone(),
))
}
}
fn git_clone(
&self,
_repo_details: &crate::RepoDetails,
) -> super::Result<crate::OpenRepository> {
todo!()
}
}

View file

@ -1,4 +1,5 @@
use crate as git; use crate as git;
use git_next_config as config;
mod commit { mod commit {
use super::*; use super::*;
@ -81,23 +82,59 @@ mod gitremote {
} }
} }
mod push { mod push {
use crate::{commit, push::Force, Commit, GitRef}; use super::*;
use crate::GitRef;
#[test] #[test]
fn force_no_should_display() { fn force_no_should_display() {
assert_eq!(Force::No.to_string(), "fast-forward") assert_eq!(git::push::Force::No.to_string(), "fast-forward")
} }
#[test] #[test]
fn force_from_should_display() { fn force_from_should_display() {
let commit = Commit::new( let sha = given::a_name();
commit::Sha::new("sha".to_string()), let commit = given::a_commit_with_sha(&git::commit::Sha::new(sha.clone()));
commit::Message::new("message".to_string()),
);
assert_eq!( assert_eq!(
Force::From(GitRef::from(commit)).to_string(), git::push::Force::From(GitRef::from(commit)).to_string(),
"force-if-from:sha" format!("force-if-from:{sha}")
) )
} }
mod reset {
use super::*;
use crate::{tests::given, OpenRepository};
use assert2::let_assert;
#[test]
fn should_perform_a_fetch_then_push() {
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 branch_name = &repo_details.branch;
let commit = given::a_commit();
let gitref = GitRef::from(commit);
let_assert!(
Ok(_) = git::push::reset(
&open_repository,
&repo_details,
branch_name,
&gitref,
&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}")]
);
}
}
} }
mod repo_details { mod repo_details {
@ -172,13 +209,19 @@ mod repo_details {
); );
} }
} }
mod given { pub mod given {
#![allow(dead_code)] #![allow(dead_code)]
use std::path::PathBuf;
// //
use crate as git; use crate::{
self as git,
repository::{MockOpenRepository, MockRepository},
tests::given,
};
use config::{ use config::{
BranchName, ForgeAlias, ForgeConfig, ForgeType, GitDir, RepoAlias, RepoBranches, BranchName, ForgeAlias, ForgeConfig, ForgeType, GitDir, RepoAlias, RepoBranches,
ServerRepoConfig, WebhookAuth, WebhookId, RepoConfig, ServerRepoConfig, WebhookAuth, WebhookId,
}; };
use git_next_config as config; use git_next_config as config;
@ -201,7 +244,11 @@ mod given {
} }
pub fn repo_branches() -> RepoBranches { pub fn repo_branches() -> RepoBranches {
RepoBranches::new(a_name(), a_name(), a_name()) RepoBranches::new(
format!("main-{}", a_name()),
format!("next-{}", a_name()),
format!("dev-{}", a_name()),
)
} }
pub fn a_forge_alias() -> ForgeAlias { pub fn a_forge_alias() -> ForgeAlias {
@ -227,6 +274,10 @@ mod given {
a_webhook_url(&a_forge_alias(), &a_repo_alias()) a_webhook_url(&a_forge_alias(), &a_repo_alias())
} }
pub fn a_pathbuf() -> PathBuf {
PathBuf::from(given::a_name())
}
pub fn a_name() -> String { pub fn a_name() -> String {
use rand::Rng; use rand::Rng;
use std::iter; use std::iter;
@ -282,6 +333,10 @@ mod given {
) )
} }
pub fn a_repo_config() -> RepoConfig {
RepoConfig::new(given::repo_branches(), config::RepoConfigSource::Repo)
}
pub fn a_commit() -> git::Commit { pub fn a_commit() -> git::Commit {
git::Commit::new(a_commit_sha(), a_commit_message()) git::Commit::new(a_commit_sha(), a_commit_message())
} }
@ -335,11 +390,185 @@ mod given {
) )
} }
// pub fn an_open_repository(fs: &kxio::fs::FileSystem) -> (OpenRepository, MockRepository) { pub fn an_open_repository(
// let (repo, mock) = git::repository::mock(); fs: &kxio::fs::FileSystem,
// ) -> (MockOpenRepository, GitDir, MockRepository) {
// let gitdir = a_git_dir(fs); let mut mock = git::repository::mock();
// let op = repo.open(&gitdir).unwrap(); let gitdir = a_git_dir(fs);
// (op, mock) let or = mock.given_can_be_opened(&gitdir);
// } (or, gitdir, mock)
}
}
pub mod then {
use std::path::{Path, PathBuf};
type TestResult = Result<(), Box<dyn std::error::Error>>;
use super::*;
pub fn commit_named_file_to_branch(
file_name: &Path,
contents: &str,
fs: &kxio::fs::FileSystem,
gitdir: &config::GitDir,
branch_name: &config::BranchName,
) -> TestResult {
// git checkout ${branch_name}
git_checkout(branch_name, gitdir)?;
// echo ${word} > file-${word}
let pathbuf = PathBuf::from(gitdir);
let file = fs.base().join(pathbuf).join(file_name);
#[allow(clippy::expect_used)]
fs.file_write(&file, contents)?;
// git add ${file}
git_add_file(gitdir, &file)?;
// git commit -m"Added ${file}"
git_commit(gitdir, &file)?;
then::push_branch(fs, gitdir, branch_name)?;
Ok(())
}
pub fn create_a_commit_on_branch(
fs: &kxio::fs::FileSystem,
gitdir: &config::GitDir,
branch_name: &config::BranchName,
) -> TestResult {
// git checkout ${branch_name}
git_checkout(branch_name, gitdir)?;
// echo ${word} > file-${word}
let word = given::a_name();
let pathbuf = PathBuf::from(gitdir);
let file = fs.base().join(pathbuf).join(&word);
#[allow(clippy::expect_used)]
fs.file_write(&file, &word)?;
// git add ${file}
git_add_file(gitdir, &file)?;
// git commit -m"Added ${file}"
git_commit(gitdir, &file)?;
then::push_branch(fs, gitdir, branch_name)?;
Ok(())
}
fn push_branch(
fs: &kxio::fs::FileSystem,
gitdir: &config::GitDir,
branch_name: &config::BranchName,
) -> TestResult {
let gitrefs = fs
.base()
.join(gitdir.to_path_buf())
.join(".git")
.join("refs");
let local_branch = gitrefs.join("heads").join(branch_name.to_string().as_str());
let origin_heads = gitrefs.join("remotes").join("origin");
let remote_branch = origin_heads.join(branch_name.to_string().as_str());
let contents = fs.file_read_to_string(&local_branch)?;
fs.dir_create_all(&origin_heads)?;
fs.file_write(&remote_branch, &contents)?;
Ok(())
}
pub fn git_checkout(
branch_name: &git_next_config::BranchName,
gitdir: &git_next_config::GitDir,
) -> TestResult {
exec(
format!("git checkout -b {}", branch_name),
std::process::Command::new("git")
.current_dir(gitdir.to_path_buf())
.args(["checkout", "-b", branch_name.to_string().as_str()])
.output()?,
)
}
pub fn git_switch(
branch_name: &git_next_config::BranchName,
gitdir: &git_next_config::GitDir,
) -> TestResult {
exec(
format!("git switch {}", branch_name),
std::process::Command::new("git")
.current_dir(gitdir.to_path_buf())
.args(["switch", branch_name.to_string().as_str()])
.output()?,
)
}
fn exec(label: String, output: std::process::Output) -> TestResult {
eprintln!("== {label}");
eprintln!(
"\nstdout:\n{}",
String::from_utf8_lossy(output.stdout.as_slice())
);
eprintln!(
"\nstderr:\n{}",
String::from_utf8_lossy(output.stderr.as_slice())
);
eprintln!("=============================");
Ok(())
}
fn git_add_file(gitdir: &git_next_config::GitDir, file: &Path) -> TestResult {
exec(
format!("git add {file:?}"),
std::process::Command::new("git")
.current_dir(gitdir.to_path_buf())
.args(["add", file.display().to_string().as_str()])
.output()?,
)
}
fn git_commit(gitdir: &git_next_config::GitDir, file: &Path) -> TestResult {
exec(
format!(r#"git commit -m"Added {file:?}""#),
std::process::Command::new("git")
.current_dir(gitdir.to_path_buf())
.args([
"commit",
format!(r#"-m"Added {}"#, file.display().to_string().as_str()).as_str(),
])
.output()?,
)
}
pub fn git_log_all(gitdir: &config::GitDir) -> TestResult {
exec(
"git log --all --oneline --decorate --graph".to_string(),
std::process::Command::new("git")
.current_dir(gitdir.to_path_buf())
.args(["log", "--all", "--oneline", "--decorate", "--graph"])
.output()?,
)
}
#[allow(dead_code)]
pub fn ls_la(gitdir: &git_next_config::GitDir) -> TestResult {
exec(
"ls -la".to_string(),
std::process::Command::new("ls")
.current_dir(gitdir.to_path_buf())
.args(["-la"])
.output()?,
)
}
pub fn get_sha_for_branch(
fs: &kxio::fs::FileSystem,
gitdir: &git_next_config::GitDir,
branch_name: &git_next_config::BranchName,
) -> Result<git::commit::Sha, Box<dyn std::error::Error>> {
let main_ref = fs
.base()
.join(gitdir.to_path_buf())
.join(".git")
.join("refs")
.join("heads")
.join(branch_name.to_string().as_str());
let sha = fs.file_read_to_string(&main_ref)?;
Ok(git::commit::Sha::new(sha.trim().to_string()))
}
} }

View file

@ -1,2 +1,5 @@
pub mod positions; pub mod positions;
pub mod repo; pub mod repo;
#[cfg(test)]
mod tests;

View file

@ -2,10 +2,11 @@
use crate as git; use crate as git;
use git_next_config as config; use git_next_config as config;
use tracing::{debug, error, info, warn}; use tracing::{error, info, warn};
pub type Result<T> = core::result::Result<T, Error>; pub type Result<T> = core::result::Result<T, Error>;
#[derive(Debug)]
pub struct Positions { pub struct Positions {
pub main: git::Commit, pub main: git::Commit,
pub next: git::Commit, pub next: git::Commit,
@ -19,133 +20,49 @@ pub fn validate_positions(
repo_details: &git::RepoDetails, repo_details: &git::RepoDetails,
repo_config: config::RepoConfig, repo_config: config::RepoConfig,
) -> Result<Positions> { ) -> Result<Positions> {
let main_branch = repo_config.branches().main();
let next_branch = repo_config.branches().next();
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()?; repository.fetch()?;
let commit_histories = get_commit_histories(repository, &repo_config); let commit_histories =
let commit_histories = match commit_histories { get_commit_histories(repository, &repo_config).map_err(Error::CommitLog)?;
Ok(commit_histories) => commit_histories, // branch tips
Err(err) => { let main = commit_histories
error!(?err, "Failed to get commit histories"); .main
return Err(git::validation::positions::Error::CommitLog(err)); .first()
} .cloned()
}; .ok_or_else(|| Error::BranchHasNoCommits(main_branch.clone()))?;
// Validations let next = commit_histories
let Some(main) = commit_histories.main.first().cloned() else {
warn!(
"No commits on main branch '{}'",
repo_config.branches().main()
);
return Err(git::validation::positions::Error::BranchHasNoCommits(
repo_config.branches().main(),
));
};
// Dev must be on main branch, or user must rebase it
let dev_has_main = commit_histories.dev.iter().any(|commit| commit == &main);
if !dev_has_main {
warn!(
"Dev branch '{}' is not based on main branch '{}' - user should rebase onto main branch '{}'",
repo_config.branches().dev(),
repo_config.branches().main(),
repo_config.branches().main(),
);
return Err(git::validation::positions::Error::DevBranchNotBasedOn {
dev: repo_config.branches().dev(),
other: repo_config.branches().main(),
});
}
// verify that next is an ancestor of dev, and force update to it main if it isn't
let Some(next) = commit_histories.next.first().cloned() else {
warn!(
"No commits on next branch '{}",
repo_config.branches().next()
);
return Err(git::validation::positions::Error::BranchHasNoCommits(
repo_config.branches().next(),
));
};
let next_is_ancestor_of_dev = commit_histories.dev.iter().any(|dev| dev == &next);
if !next_is_ancestor_of_dev {
info!("Next is not an ancestor of dev - resetting next to main");
if let Err(err) = git::push::reset(
repository,
repo_details,
&repo_config.branches().next(),
&main.into(),
&git::push::Force::From(next.clone().into()),
) {
warn!(?err, "Failed to reset next to main");
return Err(git::validation::positions::Error::FailedToResetBranch {
branch: repo_config.branches().next(),
commit: next,
});
}
return Err(git::validation::positions::Error::NextBranchResetRequired(
repo_config.branches().next(),
));
}
let next_commits = commit_histories
.next .next
.into_iter() .first()
.take(2) // next should never be more than one commit ahead of main, so this should be .cloned()
// either be next and main on two adjacent commits, or next and main on the same commit, .ok_or_else(|| Error::BranchHasNoCommits(next_branch.clone()))?;
// plus the parent of main. let dev = commit_histories
.collect::<Vec<_>>(); .dev
if !next_commits.contains(&main) { .first()
warn!( .cloned()
"Main branch '{}' is not on the same commit as next branch '{}', or it's parent - resetting next to main", .ok_or_else(|| Error::BranchHasNoCommits(dev_branch.clone()))?;
repo_config.branches().main(), // Validations:
repo_config.branches().next() // Dev must be on main branch, else the USER must rebase it
); if is_not_based_on(&commit_histories.dev, &main) {
if let Err(err) = git::push::reset( warn!("Dev '{dev_branch}' not based on main '{main_branch}' - user must rebase",);
repository, return Err(Error::DevBranchNotBasedOn {
repo_details, dev: dev_branch,
&repo_config.branches().next(), other: main_branch,
&main.into(),
&git::push::Force::From(next.clone().into()),
) {
warn!(?err, "Failed to reset next to main");
return Err(git::validation::positions::Error::FailedToResetBranch {
branch: repo_config.branches().next(),
commit: next,
}); });
} }
return Err(git::validation::positions::Error::NextBranchResetRequired( // verify that next is on main or at most one commit on top of main, else reset it back to main
repo_config.branches().next(), 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);
} }
let Some(next) = next_commits.first().cloned() else { // verify that next is an ancestor of dev, else reset it back to main
warn!( if is_not_based_on(&commit_histories.dev, &next) {
"No commits on next branch '{}'", info!("Next is not an ancestor of dev - resetting next to main");
repo_config.branches().next() return reset_next_to_main(repository, repo_details, &main, &next, &next_branch);
);
return Err(git::validation::positions::Error::BranchHasNoCommits(
repo_config.branches().next(),
));
};
let dev_has_next = commit_histories
.dev
.iter()
.any(|commit| commit == &next_commits[0]);
if !dev_has_next {
warn!(
"Dev branch '{}' is not based on next branch '{}' - next branch will be updated shortly",
repo_config.branches().dev(),
repo_config.branches().next()
);
return Err(git::validation::positions::Error::DevBranchNotBasedOn {
dev: repo_config.branches().dev(),
other: repo_config.branches().next(),
}); // dev is not based on next
} }
let Some(dev) = commit_histories.dev.first().cloned() else {
warn!(
"No commits on dev branch '{}'",
repo_config.branches().dev()
);
return Err(git::validation::positions::Error::BranchHasNoCommits(
repo_config.branches().dev(),
));
};
Ok(git::validation::positions::Positions { Ok(git::validation::positions::Positions {
main, main,
next, next,
@ -154,6 +71,34 @@ pub fn validate_positions(
}) })
} }
fn reset_next_to_main(
repository: &crate::OpenRepository,
repo_details: &crate::RepoDetails,
main: &crate::Commit,
next: &crate::Commit,
next_branch: &config::BranchName,
) -> Result<Positions> {
git::push::reset(
repository,
repo_details,
next_branch,
&main.clone().into(),
&git::push::Force::From(next.clone().into()),
)
.map_err(|err| {
warn!(?err, "Failed to reset next to main");
Error::FailedToResetBranch {
branch: next_branch.clone(),
commit: next.clone(),
}
})?;
Err(Error::NextBranchResetRequired(next_branch.clone()))
}
fn is_not_based_on(commits: &[crate::commit::Commit], needle: &crate::Commit) -> bool {
commits.iter().filter(|commit| *commit == needle).count() == 0
}
fn get_commit_histories( fn get_commit_histories(
repository: &git::repository::OpenRepository, repository: &git::repository::OpenRepository,
repo_config: &config::RepoConfig, repo_config: &config::RepoConfig,
@ -162,15 +107,10 @@ fn get_commit_histories(
let main_head = [main[0].clone()]; let main_head = [main[0].clone()];
let next = repository.commit_log(&repo_config.branches().next(), &main_head)?; let next = repository.commit_log(&repo_config.branches().next(), &main_head)?;
let dev = repository.commit_log(&repo_config.branches().dev(), &main_head)?; let dev = repository.commit_log(&repo_config.branches().dev(), &main_head)?;
debug!(
main = main.len(),
next = next.len(),
dev = dev.len(),
"Commit histories"
);
let histories = git::commit::Histories { main, next, dev }; let histories = git::commit::Histories { main, next, dev };
Ok(histories) Ok(histories)
} }
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum Error { pub enum Error {
#[error("fetch: {0}")] #[error("fetch: {0}")]

View file

@ -7,13 +7,12 @@ pub fn validate_repo(
repository: &git::OpenRepository, repository: &git::OpenRepository,
repo_details: &git::RepoDetails, repo_details: &git::RepoDetails,
) -> Result<()> { ) -> Result<()> {
let Some(push_remote) = repository.find_default_remote(git::repository::Direction::Push) else { let push_remote = repository
return Err(Error::NoDefaultPushRemote); .find_default_remote(git::repository::Direction::Push)
}; .ok_or_else(|| Error::NoDefaultPushRemote)?;
let Some(fetch_remote) = repository.find_default_remote(git::repository::Direction::Fetch) let fetch_remote = repository
else { .find_default_remote(git::repository::Direction::Fetch)
return Err(Error::NoDefaultFetchRemote); .ok_or_else(|| Error::NoDefaultFetchRemote)?;
};
let git_remote = repo_details.git_remote(); let git_remote = repo_details.git_remote();
info!(config = %git_remote, push = %push_remote, fetch = %fetch_remote, "Check remotes match"); info!(config = %git_remote, push = %push_remote, fetch = %fetch_remote, "Check remotes match");
if git_remote != push_remote { if git_remote != push_remote {

View file

@ -0,0 +1,594 @@
//
use crate as git;
use git::tests::given;
use git_next_config as config;
use assert2::let_assert;
mod repos {
use crate::repository::RepositoryLike as _;
use super::*;
#[test]
fn where_repo_has_no_push_remote() {
let_assert!(Ok(fs) = kxio::fs::temp(), "temp fs");
let gitdir: config::GitDir = fs.base().to_path_buf().into();
let test_repository = git::repository::test(fs.clone());
// default has no push or fetch remotes
let repo_details = given::repo_details(&fs);
let_assert!(Ok(repository) = test_repository.open(&gitdir), "open repo");
let_assert!(Err(err) = git::validation::repo::validate_repo(&repository, &repo_details));
assert!(matches!(
err,
git::validation::repo::Error::NoDefaultPushRemote
));
}
}
mod positions {
use super::*;
use git::repository::RepositoryLike as _;
mod validate_positions {
use git::validation::positions::validate_positions;
use git::tests::then;
use super::*;
#[test]
fn where_fetch_fails_should_error() {
let_assert!(Ok(fs) = kxio::fs::temp(), "temp fs");
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::Err(git::fetch::Error::TestFailureExpected),
));
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_assert!(
Err(err) = validate_positions(&repository, &repo_details, repo_config),
"validate"
);
assert!(matches!(
err,
git::validation::positions::Error::Fetch(git::fetch::Error::TestFailureExpected)
));
}
#[test]
fn where_main_branch_is_missing_or_commit_log_is_empty_should_error() {
let_assert!(Ok(fs) = kxio::fs::temp(), "temp fs");
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_assert!(
Err(err) = validate_positions(&repository, &repo_details, repo_config.clone()),
"validate"
);
let branch_name = repo_config.branches().main();
let error_message = format!(
r#"The ref partially named "remotes/origin/{branch_name}" could not be found"#
);
assert!(matches!(
err,
git::validation::positions::Error::CommitLog(git::commit::log::Error::Gix {
branch,
error
}) if branch == branch_name && error == error_message
));
}
#[test]
fn where_next_branch_is_missing_or_commit_log_is_empty_should_error() {
//given
let_assert!(Ok(fs) = kxio::fs::temp(), "temp fs");
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();
test_repository.on_fetch(git::repository::OnFetch::new(
repo_config.branches().clone(),
gitdir.clone(),
fs.clone(),
|branches, gitdir, fs| {
// add a commit to main
then::create_a_commit_on_branch(fs, gitdir, &branches.main())?;
git::fetch::Result::Ok(())
},
));
let_assert!(Ok(repository) = test_repository.open(&gitdir), "open repo");
let repo_details = given::repo_details(&fs).with_gitdir(gitdir);
//when
let_assert!(
Err(err) = validate_positions(&repository, &repo_details, repo_config.clone()),
"validate"
);
//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!(
err,
git::validation::positions::Error::CommitLog(git::commit::log::Error::Gix {
branch,
error
}) if branch == branch_name && error == error_message
));
}
#[test]
fn where_dev_branch_is_missing_or_commit_log_is_empty_should_error() {
//given
let_assert!(Ok(fs) = kxio::fs::temp(), "temp fs");
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();
test_repository.on_fetch(git::repository::OnFetch::new(
repo_config.branches().clone(),
gitdir.clone(),
fs.clone(),
|branches, gitdir, fs| {
// add a commit to main
then::create_a_commit_on_branch(fs, gitdir, &branches.main())?;
// add a commit to next
then::create_a_commit_on_branch(fs, gitdir, &branches.next())?;
git::fetch::Result::Ok(())
},
));
let_assert!(Ok(repository) = test_repository.open(&gitdir), "open repo");
let repo_details = given::repo_details(&fs).with_gitdir(gitdir);
//when
let_assert!(
Err(err) = validate_positions(&repository, &repo_details, repo_config.clone()),
"validate"
);
//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!(
err,
git::validation::positions::Error::CommitLog(git::commit::log::Error::Gix {
branch,
error
}) if branch == branch_name && error == error_message
));
}
#[test]
fn where_dev_branch_is_not_based_on_main_should_error() {
//given
let_assert!(Ok(fs) = kxio::fs::temp(), "temp fs");
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();
test_repository.on_fetch(git::repository::OnFetch::new(
repo_config.branches().clone(),
gitdir.clone(),
fs.clone(),
|branches, gitdir, fs| {
// /--- 4 next
// 0 --- 1 --- 3 main
// \--- 2 dev
// add a commit to main (0 -> 1)
then::create_a_commit_on_branch(fs, gitdir, &branches.main())?;
// add a commit to dev ( 1 -> 2)
then::create_a_commit_on_branch(fs, gitdir, &branches.dev())?;
// switch back to main
then::git_switch(&branches.main(), gitdir)?;
// add a second commit to main (1 -> 3)
then::git_log_all(gitdir)?;
then::create_a_commit_on_branch(fs, gitdir, &branches.main())?;
// add a commit to next 1 -> 4
then::create_a_commit_on_branch(fs, gitdir, &branches.next())?;
then::git_log_all(gitdir)?;
git::fetch::Result::Ok(())
},
));
let_assert!(Ok(repository) = test_repository.open(&gitdir), "open repo");
let repo_details = given::repo_details(&fs).with_gitdir(gitdir);
//when
let_assert!(
Err(err) = validate_positions(&repository, &repo_details, repo_config.clone()),
"validate"
);
//then
assert!(matches!(
err,
git::validation::positions::Error::DevBranchNotBasedOn { dev, other }
if dev == repo_config.branches().dev() && other == repo_config.branches().main()
));
}
#[test]
fn where_next_branch_is_not_based_on_main_should_reset_next_branch_to_main_and_then_error()
{
//given
let_assert!(Ok(fs) = kxio::fs::temp(), "temp fs");
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();
test_repository.on_fetch(git::repository::OnFetch::new(
repo_config.branches().clone(),
gitdir.clone(),
fs.clone(),
|branches, gitdir, fs| {
// /--- 4 dev
// 0 --- 1 --- 3 main
// \--- 2 next
// add a commit to main (0 -> 1)
then::create_a_commit_on_branch(fs, gitdir, &branches.main())?;
// add a commit to next (1 -> 2)
then::create_a_commit_on_branch(fs, gitdir, &branches.next())?;
// switch back to main (1)
then::git_switch(&branches.main(), gitdir)?;
// add a second commit to main (1 -> 3)
then::create_a_commit_on_branch(fs, gitdir, &branches.main())?;
// add a commit to dev (3 -> 4)
then::create_a_commit_on_branch(fs, gitdir, &branches.dev())?;
then::git_log_all(gitdir)?;
git::fetch::Result::Ok(())
},
));
// second fetch as prep to push
test_repository.on_fetch(git::repository::OnFetch::new(
repo_config.branches().clone(),
gitdir.clone(),
fs.clone(),
|_branches, _gitdir, _fs| {
// don't change anything
git::fetch::Result::Ok(())
},
));
test_repository.on_push(git::repository::OnPush::new(
repo_config.branches().clone(),
gitdir.clone(),
fs.clone(),
|_repo_details, branch_name, gitref, force, repo_branches, gitdir, fs| {
assert_eq!(
branch_name,
&repo_branches.next(),
"branch to reset should be 'next'"
);
let sha_main = then::get_sha_for_branch(fs, gitdir, &repo_branches.main())?;
assert_eq!(
gitref,
&git::GitRef::from(sha_main),
"should reset to the sha of the 'main' branch"
);
let sha_next = then::get_sha_for_branch(fs, gitdir, &repo_branches.next())?;
assert_eq!(
force,
&git::push::Force::From(sha_next.into()),
"should force push only if next is on expected sha"
);
git::push::Result::Ok(())
},
));
let_assert!(Ok(repository) = test_repository.open(&gitdir), "open repo");
let repo_details = given::repo_details(&fs).with_gitdir(gitdir);
//when
let_assert!(
Err(err) = validate_positions(&repository, &repo_details, repo_config.clone()),
"validate"
);
//then
eprintln!("{err:?}");
// NOTE: assertions for correct push are in on_push above
assert!(matches!(
err,
git::validation::positions::Error::NextBranchResetRequired(branch)
if branch == repo_config.branches().next()
));
}
#[test]
fn where_next_branch_is_not_based_on_main_and_reset_of_next_fails_should_error() {
//given
let_assert!(Ok(fs) = kxio::fs::temp(), "temp fs");
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();
test_repository.on_fetch(git::repository::OnFetch::new(
repo_config.branches().clone(),
gitdir.clone(),
fs.clone(),
|branches, gitdir, fs| {
// /--- 4 dev
// 0 --- 1 --- 3 main
// \--- 2 next
// add a commit to main (0 -> 1)
then::create_a_commit_on_branch(fs, gitdir, &branches.main())?;
// add a commit to next (1 -> 2)
then::create_a_commit_on_branch(fs, gitdir, &branches.next())?;
// switch back to main (1)
then::git_switch(&branches.main(), gitdir)?;
// add a second commit to main (1 -> 3)
then::create_a_commit_on_branch(fs, gitdir, &branches.main())?;
// add a commit to dev (3 -> 4)
then::create_a_commit_on_branch(fs, gitdir, &branches.dev())?;
then::git_log_all(gitdir)?;
git::fetch::Result::Ok(())
},
));
// second fetch as prep to push
test_repository.on_fetch(git::repository::OnFetch::new(
repo_config.branches().clone(),
gitdir.clone(),
fs.clone(),
|_branches, _gitdir, _fs| {
// don't change anything
git::fetch::Result::Ok(())
},
));
test_repository.on_push(git::repository::OnPush::new(
repo_config.branches().clone(),
gitdir.clone(),
fs.clone(),
|_repo_details, _branch_name, _gitref, _force, _repo_branches, _gitdir, _fs| {
git::push::Result::Err(git::push::Error::Lock)
},
));
let_assert!(Ok(repository) = test_repository.open(&gitdir), "open repo");
let repo_details = given::repo_details(&fs).with_gitdir(gitdir.clone());
//when
let_assert!(
Err(err) = validate_positions(&repository, &repo_details, repo_config.clone()),
"validate"
);
//then
eprintln!("{err:?}");
let_assert!(
Ok(sha_next) =
then::get_sha_for_branch(&fs, &gitdir, &repo_config.branches().next()),
"load next branch sha"
);
assert!(matches!(
err,
git::validation::positions::Error::FailedToResetBranch{branch, commit}
if branch == repo_config.branches().next() && commit.sha() == &sha_next
));
}
#[test]
fn where_dev_branch_is_not_based_on_next_should_reset_next_branch_to_main_and_then_error() {
//given
let_assert!(Ok(fs) = kxio::fs::temp(), "temp fs");
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();
test_repository.on_fetch(git::repository::OnFetch::new(
repo_config.branches().clone(),
gitdir.clone(),
fs.clone(),
|branches, gitdir, fs| {
// /--- 3 next
// 0 --- 1 main
// \--- 2 dev
// add a commit to main (0 -> 1)
then::create_a_commit_on_branch(fs, gitdir, &branches.main())?;
// add a commit to dev (1 -> 2)
then::create_a_commit_on_branch(fs, gitdir, &branches.dev())?;
// switch back to main (1)
then::git_switch(&branches.main(), gitdir)?;
// add a commit to next (1 -> 3)
then::create_a_commit_on_branch(fs, gitdir, &branches.next())?;
then::git_log_all(gitdir)?;
git::fetch::Result::Ok(())
},
));
// second fetch as prep to push
test_repository.on_fetch(git::repository::OnFetch::new(
repo_config.branches().clone(),
gitdir.clone(),
fs.clone(),
|_branches, _gitdir, _fs| {
// don't change anything
git::fetch::Result::Ok(())
},
));
test_repository.on_push(git::repository::OnPush::new(
repo_config.branches().clone(),
gitdir.clone(),
fs.clone(),
|_repo_details, branch_name, gitref, force, repo_branches, gitdir, fs| {
assert_eq!(
branch_name,
&repo_branches.next(),
"branch to reset should be 'next'"
);
let sha_main = then::get_sha_for_branch(fs, gitdir, &repo_branches.main())?;
assert_eq!(
gitref,
&git::GitRef::from(sha_main),
"should reset to the sha of the 'main' branch"
);
let sha_next = then::get_sha_for_branch(fs, gitdir, &repo_branches.next())?;
assert_eq!(
force,
&git::push::Force::From(sha_next.into()),
"should force push only if next is on expected sha"
);
git::push::Result::Ok(())
},
));
let_assert!(Ok(repository) = test_repository.open(&gitdir), "open repo");
let repo_details = given::repo_details(&fs).with_gitdir(gitdir);
//when
let_assert!(
Err(err) = validate_positions(&repository, &repo_details, repo_config.clone()),
"validate"
);
//then
eprintln!("{err:?}");
// NOTE: assertions for correct push are in on_push above
assert!(matches!(
err,
git::validation::positions::Error::NextBranchResetRequired(branch)
if branch == repo_config.branches().next()
));
}
#[test]
fn where_dev_branch_is_not_based_on_next_and_reset_of_next_fails_should_error() {
//given
let_assert!(Ok(fs) = kxio::fs::temp(), "temp fs");
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();
test_repository.on_fetch(git::repository::OnFetch::new(
repo_config.branches().clone(),
gitdir.clone(),
fs.clone(),
|branches, gitdir, fs| {
// /--- 3 next
// 0 --- 1 main
// \--- 2 dev
// add a commit to main (0 -> 1)
then::create_a_commit_on_branch(fs, gitdir, &branches.main())?;
// add a commit to dev (1 -> 2)
then::create_a_commit_on_branch(fs, gitdir, &branches.dev())?;
// switch back to main (1)
then::git_switch(&branches.main(), gitdir)?;
// add a commit to next (1 -> 3)
then::create_a_commit_on_branch(fs, gitdir, &branches.next())?;
then::git_log_all(gitdir)?;
git::fetch::Result::Ok(())
},
));
// second fetch as prep to push
test_repository.on_fetch(git::repository::OnFetch::new(
repo_config.branches().clone(),
gitdir.clone(),
fs.clone(),
|_branches, _gitdir, _fs| {
// don't change anything
git::fetch::Result::Ok(())
},
));
test_repository.on_push(git::repository::OnPush::new(
repo_config.branches().clone(),
gitdir.clone(),
fs.clone(),
|_repo_details, _branch_name, _gitref, _force, _repo_branches, _gitdir, _fs| {
git::push::Result::Err(git::push::Error::Lock)
},
));
let_assert!(Ok(repository) = test_repository.open(&gitdir), "open repo");
let repo_details = given::repo_details(&fs).with_gitdir(gitdir.clone());
//when
let_assert!(
Err(err) = validate_positions(&repository, &repo_details, repo_config.clone()),
"validate"
);
//then
eprintln!("{err:?}");
let_assert!(
Ok(sha_next) =
then::get_sha_for_branch(&fs, &gitdir, &repo_config.branches().next()),
"load next branch sha"
);
assert!(matches!(
err,
git::validation::positions::Error::FailedToResetBranch{branch, commit}
if branch == repo_config.branches().next() && commit.sha() == &sha_next
));
}
#[test]
fn where_branches_are_all_valid_should_return_positions() {
//given
let_assert!(Ok(fs) = kxio::fs::temp(), "temp fs");
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();
test_repository.on_fetch(git::repository::OnFetch::new(
repo_config.branches().clone(),
gitdir.clone(),
fs.clone(),
|branches, gitdir, fs| {
// 0 --- 1 main
// \--- 2 next
// \--- dev
// add a commit to main (0 -> 1)
then::create_a_commit_on_branch(fs, gitdir, &branches.main())?;
// add a commit to next (1 -> 2)
then::create_a_commit_on_branch(fs, gitdir, &branches.next())?;
// add a commit to dev (2 -> 3)
then::create_a_commit_on_branch(fs, gitdir, &branches.dev())?;
then::git_log_all(gitdir)?;
git::fetch::Result::Ok(())
},
));
let_assert!(Ok(repository) = test_repository.open(&gitdir), "open repo");
let repo_details = given::repo_details(&fs).with_gitdir(gitdir.clone());
//when
let_assert!(
Ok(positions) = validate_positions(&repository, &repo_details, repo_config.clone()),
"validate"
);
//then
eprintln!("{positions:#?}");
let_assert!(
Ok(main_sha) =
then::get_sha_for_branch(&fs, &gitdir, &repo_config.branches().main())
);
assert_eq!(positions.main.sha(), &main_sha);
let_assert!(
Ok(next_sha) =
then::get_sha_for_branch(&fs, &gitdir, &repo_config.branches().next())
);
assert_eq!(positions.next.sha(), &next_sha);
let_assert!(
Ok(dev_sha) = then::get_sha_for_branch(&fs, &gitdir, &repo_config.branches().dev())
);
assert_eq!(positions.dev.sha(), &dev_sha);
}
}
}

View file

@ -93,7 +93,7 @@ impl Actor for RepoActor {
pub struct CloneRepo; pub struct CloneRepo;
impl Handler<CloneRepo> for RepoActor { impl Handler<CloneRepo> for RepoActor {
type Result = (); type Result = ();
#[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) {

View file

@ -1,3 +1,5 @@
use std::path::PathBuf;
// //
use actix::prelude::*; use actix::prelude::*;
@ -31,7 +33,7 @@ async fn load(
details: &git::RepoDetails, details: &git::RepoDetails,
open_repository: &git::OpenRepository, open_repository: &git::OpenRepository,
) -> Result<config::RepoConfig, Error> { ) -> Result<config::RepoConfig, Error> {
let contents = open_repository.read_file(&details.branch, ".git-next.toml")?; let contents = open_repository.read_file(&details.branch, &PathBuf::from(".git-next.toml"))?;
let config = config::RepoConfig::load(&contents)?; let config = config::RepoConfig::load(&contents)?;
let config = validate(config, open_repository).await?; let config = validate(config, open_repository).await?;
Ok(config) Ok(config)