refactor: cleanup pedantic clippy in core crate
All checks were successful
Rust / build (push) Successful in 1m19s
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
Release Please / Release-plz (push) Successful in 47s

This commit is contained in:
Paul Campbell 2024-08-06 07:43:28 +01:00 committed by Paul Campbell
parent 24251f0c9c
commit 6acefda5d3
52 changed files with 433 additions and 296 deletions

View file

@ -9,7 +9,7 @@ use crate::repo::{
notify_user, RepoActor, notify_user, RepoActor,
}; };
use git_next_core::git::validation::positions::{validate_positions, Error, Positions}; use git_next_core::git::validation::positions::{validate, Error, Positions};
impl Handler<ValidateRepo> for RepoActor { impl Handler<ValidateRepo> for RepoActor {
type Result = (); type Result = ();
@ -53,7 +53,7 @@ impl Handler<ValidateRepo> for RepoActor {
}; };
logger(self.log.as_ref(), "have repo config"); logger(self.log.as_ref(), "have repo config");
match validate_positions(&**open_repository, &self.repo_details, repo_config) { match validate(&**open_repository, &self.repo_details, &repo_config) {
Ok(Positions { Ok(Positions {
main, main,
next, next,

View file

@ -47,7 +47,7 @@ async fn when_repo_config_should_fetch_then_push_then_revalidate() -> TestResult
} }
#[actix::test] #[actix::test]
async fn when_server_config_should_fetch_then_push_then_revalidate() -> TestResult { async fn when_app_config_should_fetch_then_push_then_revalidate() -> TestResult {
//given //given
let fs = given::a_filesystem(); let fs = given::a_filesystem();
let (mut open_repository, mut repo_details) = given::an_open_repository(&fs); let (mut open_repository, mut repo_details) = given::an_open_repository(&fs);

View file

@ -1,19 +1,19 @@
// //
use actix::prelude::*; use actix::prelude::*;
use git_next_core::server::ServerConfig; use git_next_core::server::AppConfig;
use crate::{ use crate::{
file_watcher::FileUpdated, file_watcher::FileUpdated,
server::actor::{messages::ReceiveServerConfig, ServerActor}, server::actor::{messages::ReceiveAppConfig, ServerActor},
}; };
impl Handler<FileUpdated> for ServerActor { impl Handler<FileUpdated> for ServerActor {
type Result = (); type Result = ();
fn handle(&mut self, _msg: FileUpdated, ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, _msg: FileUpdated, ctx: &mut Self::Context) -> Self::Result {
match ServerConfig::load(&self.fs) { match AppConfig::load(&self.fs) {
Ok(server_config) => self.do_send(ReceiveServerConfig::new(server_config), ctx), Ok(app_config) => self.do_send(ReceiveAppConfig::new(app_config), ctx),
Err(err) => self.abort(ctx, format!("Failed to load config file. Error: {err}")), Err(err) => self.abort(ctx, format!("Failed to load config file. Error: {err}")),
}; };
} }

View file

@ -1,4 +1,4 @@
mod file_updated; mod file_updated;
mod receive_server_config; mod receive_app_config;
mod receive_valid_server_config; mod receive_valid_app_config;
mod shutdown; mod shutdown;

View file

@ -1,15 +1,15 @@
use actix::prelude::*; use actix::prelude::*;
use crate::server::actor::{ use crate::server::actor::{
messages::{ReceiveServerConfig, ReceiveValidServerConfig, ValidServerConfig}, messages::{ReceiveAppConfig, ReceiveValidAppConfig, ValidAppConfig},
ServerActor, ServerActor,
}; };
impl Handler<ReceiveServerConfig> for ServerActor { impl Handler<ReceiveAppConfig> for ServerActor {
type Result = (); type Result = ();
#[allow(clippy::cognitive_complexity)] #[allow(clippy::cognitive_complexity)]
fn handle(&mut self, msg: ReceiveServerConfig, ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: ReceiveAppConfig, ctx: &mut Self::Context) -> Self::Result {
tracing::info!("recieved server config"); tracing::info!("recieved server config");
let Ok(socket_addr) = msg.listen_socket_addr() else { let Ok(socket_addr) = msg.listen_socket_addr() else {
return self.abort(ctx, "Unable to parse http.addr"); return self.abort(ctx, "Unable to parse http.addr");
@ -24,7 +24,7 @@ impl Handler<ReceiveServerConfig> for ServerActor {
} }
self.do_send( self.do_send(
ReceiveValidServerConfig::new(ValidServerConfig::new( ReceiveValidAppConfig::new(ValidAppConfig::new(
msg.unwrap(), msg.unwrap(),
socket_addr, socket_addr,
server_storage, server_storage,

View file

@ -8,7 +8,7 @@ use crate::{
alerts::messages::UpdateShout, alerts::messages::UpdateShout,
repo::{messages::CloneRepo, RepoActor}, repo::{messages::CloneRepo, RepoActor},
server::actor::{ server::actor::{
messages::{ReceiveValidServerConfig, ValidServerConfig}, messages::{ReceiveValidAppConfig, ValidAppConfig},
ServerActor, ServerActor,
}, },
webhook::{ webhook::{
@ -18,14 +18,14 @@ use crate::{
}, },
}; };
impl Handler<ReceiveValidServerConfig> for ServerActor { impl Handler<ReceiveValidAppConfig> for ServerActor {
type Result = (); type Result = ();
fn handle(&mut self, msg: ReceiveValidServerConfig, _ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: ReceiveValidAppConfig, _ctx: &mut Self::Context) -> Self::Result {
let ValidServerConfig { let ValidAppConfig {
server_config, app_config,
socket_address, socket_address,
server_storage, storage: server_storage,
} = msg.unwrap(); } = msg.unwrap();
// shutdown any existing webhook actor // shutdown any existing webhook actor
if let Some(webhook_actor_addr) = self.webhook_actor_addr.take() { if let Some(webhook_actor_addr) = self.webhook_actor_addr.take() {
@ -35,10 +35,10 @@ impl Handler<ReceiveValidServerConfig> for ServerActor {
// Webhook Server // Webhook Server
info!("Starting Webhook Server..."); info!("Starting Webhook Server...");
let webhook_router = WebhookRouterActor::default().start(); let webhook_router = WebhookRouterActor::default().start();
let listen_url = server_config.listen().url(); let listen_url = app_config.listen().url();
let notify_user_recipient = self.alerts.clone().recipient(); let notify_user_recipient = self.alerts.clone().recipient();
// Forge Actors // Forge Actors
for (forge_alias, forge_config) in server_config.forges() { for (forge_alias, forge_config) in app_config.forges() {
let repo_actors = self let repo_actors = self
.create_forge_repos( .create_forge_repos(
forge_config, forge_config,
@ -68,8 +68,8 @@ impl Handler<ReceiveValidServerConfig> for ServerActor {
let webhook_actor_addr = let webhook_actor_addr =
WebhookActor::new(socket_address, webhook_router.recipient()).start(); WebhookActor::new(socket_address, webhook_router.recipient()).start();
self.webhook_actor_addr.replace(webhook_actor_addr); self.webhook_actor_addr.replace(webhook_actor_addr);
let shout = server_config.shout().clone(); let shout = app_config.shout().clone();
self.server_config.replace(server_config); self.app_config.replace(app_config);
self.alerts.do_send(UpdateShout::new(shout)); self.alerts.do_send(UpdateShout::new(shout));
} }
} }

View file

@ -3,13 +3,13 @@ use derive_more::Constructor;
use git_next_core::{ use git_next_core::{
message, message,
server::{ServerConfig, ServerStorage}, server::{AppConfig, Storage},
}; };
use std::net::SocketAddr; use std::net::SocketAddr;
// receive server config // receive server config
message!(ReceiveServerConfig: ServerConfig: "Notification of newly loaded server configuration. message!(ReceiveAppConfig: AppConfig: "Notification of newly loaded server configuration.
This message will prompt the `git-next` server to stop and restart all repo-actors. This message will prompt the `git-next` server to stop and restart all repo-actors.
@ -17,11 +17,11 @@ Contains the new server configuration.");
// receive valid server config // receive valid server config
#[derive(Clone, Debug, PartialEq, Eq, Constructor)] #[derive(Clone, Debug, PartialEq, Eq, Constructor)]
pub struct ValidServerConfig { pub struct ValidAppConfig {
pub server_config: ServerConfig, pub app_config: AppConfig,
pub socket_address: SocketAddr, pub socket_address: SocketAddr,
pub server_storage: ServerStorage, pub storage: Storage,
} }
message!(ReceiveValidServerConfig: ValidServerConfig: "Notification of validated server configuration."); message!(ReceiveValidAppConfig: ValidAppConfig: "Notification of validated server configuration.");
message!(Shutdown: "Notification to shutdown the server actor"); message!(Shutdown: "Notification to shutdown the server actor");

View file

@ -1,6 +1,6 @@
// //
use actix::prelude::*; use actix::prelude::*;
use messages::ReceiveServerConfig; use messages::ReceiveAppConfig;
use tracing::error; use tracing::error;
#[cfg(test)] #[cfg(test)]
@ -16,7 +16,7 @@ use crate::{
use git_next_core::{ use git_next_core::{
git::{repository::factory::RepositoryFactory, Generation, RepoDetails}, git::{repository::factory::RepositoryFactory, Generation, RepoDetails},
server::{self, ListenUrl, ServerConfig, ServerStorage}, server::{self, AppConfig, ListenUrl, Storage},
ForgeAlias, ForgeConfig, GitDir, RepoAlias, ServerRepoConfig, StoragePathType, ForgeAlias, ForgeConfig, GitDir, RepoAlias, ServerRepoConfig, StoragePathType,
}; };
@ -48,7 +48,7 @@ type Result<T> = core::result::Result<T, Error>;
#[derive(derive_with::With)] #[derive(derive_with::With)]
#[with(message_log)] #[with(message_log)]
pub struct ServerActor { pub struct ServerActor {
server_config: Option<ServerConfig>, app_config: Option<AppConfig>,
generation: Generation, generation: Generation,
webhook_actor_addr: Option<Addr<WebhookActor>>, webhook_actor_addr: Option<Addr<WebhookActor>>,
fs: FileSystem, fs: FileSystem,
@ -75,7 +75,7 @@ impl ServerActor {
) -> Self { ) -> Self {
let generation = Generation::default(); let generation = Generation::default();
Self { Self {
server_config: None, app_config: None,
generation, generation,
webhook_actor_addr: None, webhook_actor_addr: None,
fs, fs,
@ -89,10 +89,10 @@ impl ServerActor {
} }
fn create_forge_data_directories( fn create_forge_data_directories(
&self, &self,
server_config: &ServerConfig, app_config: &AppConfig,
server_dir: &std::path::Path, server_dir: &std::path::Path,
) -> Result<()> { ) -> Result<()> {
for (forge_name, _forge_config) in server_config.forges() { for (forge_name, _forge_config) in app_config.forges() {
let forge_dir: PathBuf = (&forge_name).into(); let forge_dir: PathBuf = (&forge_name).into();
let path = server_dir.join(&forge_dir); let path = server_dir.join(&forge_dir);
if self.fs.path_exists(&path)? { if self.fs.path_exists(&path)? {
@ -112,7 +112,7 @@ impl ServerActor {
&self, &self,
forge_config: &ForgeConfig, forge_config: &ForgeConfig,
forge_name: ForgeAlias, forge_name: ForgeAlias,
server_storage: &ServerStorage, server_storage: &Storage,
listen_url: &ListenUrl, listen_url: &ListenUrl,
notify_user_recipient: &Recipient<NotifyUser>, notify_user_recipient: &Recipient<NotifyUser>,
) -> Vec<(ForgeAlias, RepoAlias, RepoActor)> { ) -> Vec<(ForgeAlias, RepoAlias, RepoActor)> {
@ -143,7 +143,7 @@ impl ServerActor {
&self, &self,
forge_name: ForgeAlias, forge_name: ForgeAlias,
forge_config: ForgeConfig, forge_config: ForgeConfig,
server_storage: &ServerStorage, server_storage: &Storage,
listen_url: &ListenUrl, listen_url: &ListenUrl,
) -> impl Fn( ) -> impl Fn(
(RepoAlias, &ServerRepoConfig, Recipient<NotifyUser>), (RepoAlias, &ServerRepoConfig, Recipient<NotifyUser>),
@ -196,8 +196,8 @@ impl ServerActor {
} }
} }
fn server_storage(&self, server_config: &ReceiveServerConfig) -> Option<ServerStorage> { fn server_storage(&self, app_config: &ReceiveAppConfig) -> Option<Storage> {
let server_storage = server_config.storage().clone(); let server_storage = app_config.storage().clone();
let dir = server_storage.path(); let dir = server_storage.path();
if !dir.exists() { if !dir.exists() {
if let Err(err) = self.fs.dir_create(dir) { if let Err(err) = self.fs.dir_create(dir) {
@ -209,7 +209,7 @@ impl ServerActor {
error!(?dir, "Failed to confirm server storage"); error!(?dir, "Failed to confirm server storage");
return None; return None;
}; };
if let Err(err) = self.create_forge_data_directories(server_config, &canon) { if let Err(err) = self.create_forge_data_directories(app_config, &canon) {
error!(?err, "Failure creating forge storage"); error!(?err, "Failure creating forge storage");
return None; return None;
} }

View file

@ -1,3 +1,3 @@
mod receive_server_config; mod receive_app_config;
mod given; mod given;

View file

@ -1,10 +1,10 @@
// //
use actix::prelude::*; use actix::prelude::*;
use crate::server::actor::{tests::given, ReceiveServerConfig, ServerActor}; use crate::server::actor::{tests::given, ReceiveAppConfig, ServerActor};
use git_next_core::{ use git_next_core::{
git, git,
server::{Http, Listen, ListenUrl, ServerConfig, ServerStorage, Shout}, server::{AppConfig, Http, Listen, ListenUrl, Shout, Storage},
}; };
use std::{ use std::{
@ -31,7 +31,7 @@ async fn when_webhook_url_has_trailing_slash_should_not_send() {
ListenUrl::new("http://localhost/".to_string()), // with trailing slash ListenUrl::new("http://localhost/".to_string()), // with trailing slash
); );
let shout = Shout::default(); let shout = Shout::default();
let server_storage = ServerStorage::new((fs.base()).to_path_buf()); let server_storage = Storage::new((fs.base()).to_path_buf());
let repos = BTreeMap::default(); let repos = BTreeMap::default();
// debugging // debugging
@ -39,9 +39,7 @@ async fn when_webhook_url_has_trailing_slash_should_not_send() {
let server = server.with_message_log(Some(message_log.clone())); let server = server.with_message_log(Some(message_log.clone()));
//when //when
server server.start().do_send(ReceiveAppConfig::new(AppConfig::new(
.start()
.do_send(ReceiveServerConfig::new(ServerConfig::new(
listen, listen,
shout, shout,
server_storage, server_storage,

View file

@ -6,6 +6,15 @@ license = { workspace = true }
repository = { workspace = true } repository = { workspace = true }
description = "core for git-next, the trunk-based development manager" description = "core for git-next, the trunk-based development manager"
[lints.clippy]
nursery = { level = "warn", priority = -1 }
pedantic = { level = "warn", priority = -1 }
unwrap_used = "warn"
expect_used = "warn"
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin_include)'] }
[features] [features]
default = ["forgejo", "github"] default = ["forgejo", "github"]
forgejo = [] forgejo = []
@ -54,12 +63,3 @@ assert2 = { workspace = true }
rand = { workspace = true } rand = { workspace = true }
test-log = { workspace = true } test-log = { workspace = true }
pretty_assertions = { workspace = true } pretty_assertions = { workspace = true }
[lints.clippy]
nursery = { level = "warn", priority = -1 }
# pedantic = "warn"
unwrap_used = "warn"
expect_used = "warn"
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin_include)'] }

View file

@ -1,6 +1,6 @@
/// The API Token for the [user] /// The API Token for the [user]
/// ForgeJo: https://{hostname}/user/settings/applications /// `ForgeJo`: <https://{hostname}/user/settings/applications>
/// Github: https://github.com/settings/tokens /// `Github`: <https://github.com/settings/tokens>
#[derive(Clone, Debug, derive_more::Constructor)] #[derive(Clone, Debug, derive_more::Constructor)]
pub struct ApiToken(secrecy::Secret<String>); pub struct ApiToken(secrecy::Secret<String>);
/// The API Token is in effect a password, so it must be explicitly exposed to access its value /// The API Token is in effect a password, so it must be explicitly exposed to access its value
@ -11,6 +11,6 @@ impl secrecy::ExposeSecret<String> for ApiToken {
} }
impl Default for ApiToken { impl Default for ApiToken {
fn default() -> Self { fn default() -> Self {
Self("".to_string().into()) Self(String::new().into())
} }
} }

View file

@ -1,4 +1,6 @@
use derive_more::derive::Display; use derive_more::derive::Display;
use serde::Serialize; use serde::Serialize;
crate::newtype!(BranchName: String, Display, Default, Hash, Serialize: "The name of a Git branch"); use crate::newtype;
newtype!(BranchName: String, Display, Default, Hash, Serialize: "The name of a Git branch");

View file

@ -3,6 +3,7 @@ use crate::config::{
RepoConfig, RepoConfigSource, RepoPath, User, RepoConfig, RepoConfigSource, RepoPath, User,
}; };
#[must_use]
pub fn forge_details(n: u32, forge_type: ForgeType) -> ForgeDetails { pub fn forge_details(n: u32, forge_type: ForgeType) -> ForgeDetails {
ForgeDetails::new( ForgeDetails::new(
forge_name(n), forge_name(n),
@ -13,34 +14,35 @@ pub fn forge_details(n: u32, forge_type: ForgeType) -> ForgeDetails {
) )
} }
pub fn api_token(n: u32) -> ApiToken { pub(crate) fn api_token(n: u32) -> ApiToken {
ApiToken::new(format!("api-{}", n).into()) ApiToken::new(format!("api-{n}").into())
} }
pub fn user(n: u32) -> User { pub(crate) fn user(n: u32) -> User {
User::new(format!("user-{}", n)) User::new(format!("user-{n}"))
} }
pub fn hostname(n: u32) -> Hostname { pub(crate) fn hostname(n: u32) -> Hostname {
Hostname::new(format!("hostname-{}", n)) Hostname::new(format!("hostname-{n}"))
} }
pub fn forge_name(n: u32) -> ForgeAlias { pub(crate) fn forge_name(n: u32) -> ForgeAlias {
ForgeAlias::new(format!("forge-name-{}", n)) ForgeAlias::new(format!("forge-name-{n}"))
} }
pub fn branch_name(n: u32) -> BranchName { pub(crate) fn branch_name(n: u32) -> BranchName {
BranchName::new(format!("branch-name-{}", n)) BranchName::new(format!("branch-name-{n}"))
} }
pub fn repo_path(n: u32) -> RepoPath { pub(crate) fn repo_path(n: u32) -> RepoPath {
RepoPath::new(format!("repo-path-{}", n)) RepoPath::new(format!("repo-path-{n}"))
} }
pub fn repo_alias(n: u32) -> RepoAlias { pub(crate) fn repo_alias(n: u32) -> RepoAlias {
RepoAlias::new(format!("repo-alias-{}", n)) RepoAlias::new(format!("repo-alias-{n}"))
} }
#[must_use]
pub fn repo_config(n: u32, source: RepoConfigSource) -> RepoConfig { pub fn repo_config(n: u32, source: RepoConfigSource) -> RepoConfig {
RepoConfig::new( RepoConfig::new(
RepoBranches::new(format!("main-{n}"), format!("next-{n}"), format!("dev-{n}")), RepoBranches::new(format!("main-{n}"), format!("next-{n}"), format!("dev-{n}")),

View file

@ -25,19 +25,19 @@ pub struct ForgeConfig {
repos: BTreeMap<String, ServerRepoConfig>, repos: BTreeMap<String, ServerRepoConfig>,
} }
impl ForgeConfig { impl ForgeConfig {
pub const fn forge_type(&self) -> ForgeType { pub(crate) const fn forge_type(&self) -> ForgeType {
self.forge_type self.forge_type
} }
pub fn hostname(&self) -> Hostname { pub(crate) fn hostname(&self) -> Hostname {
Hostname::new(&self.hostname) Hostname::new(&self.hostname)
} }
pub fn user(&self) -> User { pub(crate) fn user(&self) -> User {
User::new(self.user.clone()) User::new(self.user.clone())
} }
pub fn token(&self) -> ApiToken { pub(crate) fn token(&self) -> ApiToken {
ApiToken::new(self.token.clone().into()) ApiToken::new(self.token.clone().into())
} }
@ -47,6 +47,8 @@ impl ForgeConfig {
.map(|(name, repo)| (RepoAlias::new(name), repo)) .map(|(name, repo)| (RepoAlias::new(name), repo))
} }
#[cfg(test)]
#[must_use]
pub fn get_repo(&self, arg: &str) -> Option<&ServerRepoConfig> { pub fn get_repo(&self, arg: &str) -> Option<&ServerRepoConfig> {
self.repos.get(arg) self.repos.get(arg)
} }

View file

@ -12,18 +12,26 @@ pub struct ForgeDetails {
// Private SSH Key Path // Private SSH Key Path
} }
impl ForgeDetails { impl ForgeDetails {
#[must_use]
pub const fn forge_alias(&self) -> &ForgeAlias { pub const fn forge_alias(&self) -> &ForgeAlias {
&self.forge_alias &self.forge_alias
} }
#[must_use]
pub const fn forge_type(&self) -> ForgeType { pub const fn forge_type(&self) -> ForgeType {
self.forge_type self.forge_type
} }
#[must_use]
pub const fn hostname(&self) -> &Hostname { pub const fn hostname(&self) -> &Hostname {
&self.hostname &self.hostname
} }
pub const fn user(&self) -> &User {
pub(crate) const fn user(&self) -> &User {
&self.user &self.user
} }
#[must_use]
pub const fn token(&self) -> &ApiToken { pub const fn token(&self) -> &ApiToken {
&self.token &self.token
} }

View file

@ -24,6 +24,6 @@ pub enum ForgeType {
} }
impl std::fmt::Display for ForgeType { impl std::fmt::Display for ForgeType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self) write!(f, "{self:?}")
} }
} }

View file

@ -22,13 +22,15 @@ pub struct GitDir {
storage_path_type: StoragePathType, storage_path_type: StoragePathType,
} }
impl GitDir { impl GitDir {
pub const fn pathbuf(&self) -> &PathBuf { pub(crate) const fn pathbuf(&self) -> &PathBuf {
&self.pathbuf &self.pathbuf
} }
pub const fn storage_path_type(&self) -> StoragePathType {
pub(crate) const fn storage_path_type(&self) -> StoragePathType {
self.storage_path_type self.storage_path_type
} }
pub fn as_fs(&self) -> kxio::fs::FileSystem {
pub(crate) fn as_fs(&self) -> kxio::fs::FileSystem {
pike! { pike! {
self self
|> Self::pathbuf |> Self::pathbuf

View file

@ -26,6 +26,7 @@ mod tests;
pub use api_token::ApiToken; pub use api_token::ApiToken;
pub use branch_name::BranchName; pub use branch_name::BranchName;
pub use forge_alias::ForgeAlias; pub use forge_alias::ForgeAlias;
#[allow(clippy::module_name_repetitions)]
pub use forge_config::ForgeConfig; pub use forge_config::ForgeConfig;
pub use forge_details::ForgeDetails; pub use forge_details::ForgeDetails;
pub use forge_type::ForgeType; pub use forge_type::ForgeType;
@ -36,10 +37,13 @@ pub use registered_webhook::RegisteredWebhook;
pub use remote_url::RemoteUrl; pub use remote_url::RemoteUrl;
pub use repo_alias::RepoAlias; pub use repo_alias::RepoAlias;
pub use repo_branches::RepoBranches; pub use repo_branches::RepoBranches;
#[allow(clippy::module_name_repetitions)]
pub use repo_config::RepoConfig; pub use repo_config::RepoConfig;
pub use repo_config_source::RepoConfigSource; pub use repo_config_source::RepoConfigSource;
pub use repo_path::RepoPath; pub use repo_path::RepoPath;
#[allow(clippy::module_name_repetitions)]
pub use server_repo_config::ServerRepoConfig; pub use server_repo_config::ServerRepoConfig;
#[allow(clippy::module_name_repetitions)]
pub use user::User; pub use user::User;
pub use webhook::auth::WebhookAuth; pub use webhook::auth::WebhookAuth;
pub use webhook::forge_notification::ForgeNotification; pub use webhook::forge_notification::ForgeNotification;

View file

@ -7,9 +7,12 @@ pub struct RegisteredWebhook {
auth: WebhookAuth, auth: WebhookAuth,
} }
impl RegisteredWebhook { impl RegisteredWebhook {
#[must_use]
pub const fn id(&self) -> &WebhookId { pub const fn id(&self) -> &WebhookId {
&self.id &self.id
} }
#[must_use]
pub const fn auth(&self) -> &WebhookAuth { pub const fn auth(&self) -> &WebhookAuth {
&self.auth &self.auth
} }

View file

@ -1,3 +1,5 @@
use std::borrow::ToOwned;
crate::newtype!(RemoteUrl: git_url_parse::GitUrl, derive_more::Display: "The URL of a remote repository"); crate::newtype!(RemoteUrl: git_url_parse::GitUrl, derive_more::Display: "The URL of a remote repository");
impl RemoteUrl { impl RemoteUrl {
pub fn parse(url: impl Into<String>) -> Option<Self> { pub fn parse(url: impl Into<String>) -> Option<Self> {
@ -8,7 +10,7 @@ impl TryFrom<gix::Url> for RemoteUrl {
type Error = (); type Error = ();
fn try_from(url: gix::Url) -> Result<Self, Self::Error> { fn try_from(url: gix::Url) -> Result<Self, Self::Error> {
let pass = url.password().map(|p| p.to_owned()); let pass = url.password().map(ToOwned::to_owned);
let mut parsed = ::git_url_parse::GitUrl::parse(&url.to_string()).map_err(|_| ())?; let mut parsed = ::git_url_parse::GitUrl::parse(&url.to_string()).map_err(|_| ())?;
parsed.token = pass; parsed.token = pass;
Ok(Self(parsed)) Ok(Self(parsed))

View file

@ -21,14 +21,17 @@ pub struct RepoBranches {
dev: String, dev: String,
} }
impl RepoBranches { impl RepoBranches {
#[must_use]
pub fn main(&self) -> BranchName { pub fn main(&self) -> BranchName {
BranchName::new(&self.main) BranchName::new(&self.main)
} }
#[must_use]
pub fn next(&self) -> BranchName { pub fn next(&self) -> BranchName {
BranchName::new(&self.next) BranchName::new(&self.next)
} }
#[must_use]
pub fn dev(&self) -> BranchName { pub fn dev(&self) -> BranchName {
BranchName::new(&self.dev) BranchName::new(&self.dev)
} }

View file

@ -23,14 +23,22 @@ pub struct RepoConfig {
source: RepoConfigSource, source: RepoConfigSource,
} }
impl RepoConfig { impl RepoConfig {
/// Parses the TOML document into a `RepoConfig`.
///
/// # Errors
///
/// Will return `Err` if the TOML file is invalid or otherwise doesn't
/// match a `RepoConfig`.
pub fn parse(toml: &str) -> Result<Self, toml::de::Error> { pub fn parse(toml: &str) -> Result<Self, toml::de::Error> {
toml::from_str(format!("source = \"Repo\"\n{}", toml).as_str()) toml::from_str(format!("source = \"Repo\"\n{toml}").as_str())
} }
#[must_use]
pub const fn branches(&self) -> &RepoBranches { pub const fn branches(&self) -> &RepoBranches {
&self.branches &self.branches
} }
#[must_use]
pub const fn source(&self) -> RepoConfigSource { pub const fn source(&self) -> RepoConfigSource {
self.source self.source
} }

View file

@ -46,13 +46,13 @@ type Result<T> = core::result::Result<T, Error>;
serde::Deserialize, serde::Deserialize,
derive_more::Constructor, derive_more::Constructor,
)] )]
pub struct ServerConfig { pub struct AppConfig {
listen: Listen, listen: Listen,
shout: Shout, shout: Shout,
storage: ServerStorage, storage: Storage,
pub forge: BTreeMap<String, ForgeConfig>, pub forge: BTreeMap<String, ForgeConfig>,
} }
impl ServerConfig { impl AppConfig {
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub fn load(fs: &FileSystem) -> Result<Self> { pub fn load(fs: &FileSystem) -> Result<Self> {
let file = fs.base().join("git-next-server.toml"); let file = fs.base().join("git-next-server.toml");
@ -67,18 +67,26 @@ impl ServerConfig {
.map(|(alias, forge)| (ForgeAlias::new(alias.clone()), forge)) .map(|(alias, forge)| (ForgeAlias::new(alias.clone()), forge))
} }
pub const fn storage(&self) -> &ServerStorage { #[must_use]
pub const fn storage(&self) -> &Storage {
&self.storage &self.storage
} }
#[must_use]
pub const fn shout(&self) -> &Shout { pub const fn shout(&self) -> &Shout {
&self.shout &self.shout
} }
#[must_use]
pub const fn listen(&self) -> &Listen { pub const fn listen(&self) -> &Listen {
&self.listen &self.listen
} }
/// Returns the `SocketAddr` to listen to for incoming webhooks.
///
/// # Errors
///
/// Will return an `Err` if the IP address or port from the config file are invalid.
pub fn listen_socket_addr(&self) -> Result<SocketAddr> { pub fn listen_socket_addr(&self) -> Result<SocketAddr> {
self.listen.http.socket_addr() self.listen.http.socket_addr()
} }
@ -102,11 +110,12 @@ pub struct Listen {
url: ListenUrl, url: ListenUrl,
} }
impl Listen { impl Listen {
/// Returns the URL a Repo will listen to for updates from the Forge // /// Returns the URL a Repo will listen to for updates from the Forge
pub fn repo_url(&self, forge_alias: ForgeAlias, repo_alias: RepoAlias) -> RepoListenUrl { // pub fn repo_url(&self, forge_alias: ForgeAlias, repo_alias: RepoAlias) -> RepoListenUrl {
self.url.repo_url(forge_alias, repo_alias) // self.url.repo_url(forge_alias, repo_alias)
} // }
#[must_use]
pub const fn url(&self) -> &ListenUrl { pub const fn url(&self) -> &ListenUrl {
&self.url &self.url
} }
@ -118,6 +127,7 @@ newtype!(
"The base url for receiving all webhooks from all forges" "The base url for receiving all webhooks from all forges"
); );
impl ListenUrl { impl ListenUrl {
#[must_use]
pub fn repo_url(&self, forge_alias: ForgeAlias, repo_alias: RepoAlias) -> RepoListenUrl { pub fn repo_url(&self, forge_alias: ForgeAlias, repo_alias: RepoAlias) -> RepoListenUrl {
RepoListenUrl::new((self.clone(), forge_alias, repo_alias)) RepoListenUrl::new((self.clone(), forge_alias, repo_alias))
} }
@ -182,10 +192,11 @@ impl Http {
serde::Deserialize, serde::Deserialize,
derive_more::Constructor, derive_more::Constructor,
)] )]
pub struct ServerStorage { pub struct Storage {
path: PathBuf, path: PathBuf,
} }
impl ServerStorage { impl Storage {
#[must_use]
pub fn path(&self) -> &Path { pub fn path(&self) -> &Path {
self.path.as_path() self.path.as_path()
} }
@ -212,11 +223,13 @@ pub struct Shout {
desktop: Option<bool>, desktop: Option<bool>,
} }
impl Shout { impl Shout {
#[must_use]
pub const fn webhook(&self) -> Option<&OutboundWebhook> { pub const fn webhook(&self) -> Option<&OutboundWebhook> {
self.webhook.as_ref() self.webhook.as_ref()
} }
pub fn webhook_url(&self) -> Option<String> { #[cfg(test)]
pub(crate) fn webhook_url(&self) -> Option<String> {
self.webhook.clone().map(|x| x.url) self.webhook.clone().map(|x| x.url)
} }
@ -224,10 +237,12 @@ impl Shout {
self.webhook.clone().map(|x| x.secret).map(Secret::new) self.webhook.clone().map(|x| x.secret).map(Secret::new)
} }
#[must_use]
pub const fn email(&self) -> Option<&EmailConfig> { pub const fn email(&self) -> Option<&EmailConfig> {
self.email.as_ref() self.email.as_ref()
} }
#[must_use]
pub const fn desktop(&self) -> Option<bool> { pub const fn desktop(&self) -> Option<bool> {
self.desktop self.desktop
} }
@ -249,9 +264,11 @@ pub struct OutboundWebhook {
secret: String, secret: String,
} }
impl OutboundWebhook { impl OutboundWebhook {
#[must_use]
pub fn url(&self) -> &str { pub fn url(&self) -> &str {
self.url.as_ref() self.url.as_ref()
} }
#[must_use]
pub fn secret(&self) -> Secret<String> { pub fn secret(&self) -> Secret<String> {
Secret::new(self.secret.clone()) Secret::new(self.secret.clone())
} }
@ -275,14 +292,17 @@ pub struct EmailConfig {
smtp: Option<SmtpConfig>, smtp: Option<SmtpConfig>,
} }
impl EmailConfig { impl EmailConfig {
#[must_use]
pub fn from(&self) -> &str { pub fn from(&self) -> &str {
&self.from &self.from
} }
#[must_use]
pub fn to(&self) -> &str { pub fn to(&self) -> &str {
&self.to &self.to
} }
#[must_use]
pub const fn smtp(&self) -> Option<&SmtpConfig> { pub const fn smtp(&self) -> Option<&SmtpConfig> {
self.smtp.as_ref() self.smtp.as_ref()
} }
@ -305,14 +325,17 @@ pub struct SmtpConfig {
password: String, password: String,
} }
impl SmtpConfig { impl SmtpConfig {
#[must_use]
pub fn username(&self) -> &str { pub fn username(&self) -> &str {
&self.username &self.username
} }
#[must_use]
pub fn password(&self) -> &str { pub fn password(&self) -> &str {
&self.password &self.password
} }
#[must_use]
pub fn hostname(&self) -> &str { pub fn hostname(&self) -> &str {
&self.hostname &self.hostname
} }

View file

@ -6,7 +6,7 @@ use crate::config::{
RepoPath, RepoPath,
}; };
/// Defines a Repo within a ForgeConfig to be monitored by the server /// Defines a Repo within a `ForgeConfig` to be monitored by the server
/// Maps from `git-next-server.toml` at `forge.{forge}.repos.{name}` /// Maps from `git-next-server.toml` at `forge.{forge}.repos.{name}`
#[derive( #[derive(
Clone, Clone,
@ -30,14 +30,15 @@ pub struct ServerRepoConfig {
dev: Option<String>, dev: Option<String>,
} }
impl ServerRepoConfig { impl ServerRepoConfig {
pub fn repo(&self) -> RepoPath { pub(crate) fn repo(&self) -> RepoPath {
RepoPath::new(self.repo.clone()) RepoPath::new(self.repo.clone())
} }
pub fn branch(&self) -> BranchName { pub(crate) fn branch(&self) -> BranchName {
BranchName::new(&self.branch) BranchName::new(&self.branch)
} }
#[must_use]
pub fn gitdir(&self) -> Option<GitDir> { pub fn gitdir(&self) -> Option<GitDir> {
self.gitdir self.gitdir
.clone() .clone()
@ -45,8 +46,8 @@ impl ServerRepoConfig {
.map(|dir| GitDir::new(dir, StoragePathType::External)) .map(|dir| GitDir::new(dir, StoragePathType::External))
} }
/// Returns a RepoConfig from the server configuration if ALL THREE branches were provided /// Returns a `RepoConfig` from the server configuration if ALL THREE branches were provided
pub fn repo_config(&self) -> Option<RepoConfig> { pub(crate) fn repo_config(&self) -> Option<RepoConfig> {
match (&self.main, &self.next, &self.dev) { match (&self.main, &self.next, &self.dev) {
(Some(main), Some(next), Some(dev)) => Some(RepoConfig::new( (Some(main), Some(next), Some(dev)) => Some(RepoConfig::new(
RepoBranches::new(main.to_string(), next.to_string(), dev.to_string()), RepoBranches::new(main.to_string(), next.to_string(), dev.to_string()),

View file

@ -6,9 +6,9 @@ use secrecy::ExposeSecret;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::path::PathBuf; use std::path::PathBuf;
use crate::server::AppConfig;
use crate::server::Http; use crate::server::Http;
use crate::server::ServerConfig; use crate::server::Storage;
use crate::server::ServerStorage;
use crate::webhook::push::Branch; use crate::webhook::push::Branch;
mod url; mod url;
@ -450,26 +450,23 @@ mod server {
use super::*; use super::*;
#[test] #[test]
fn load_should_parse_server_config() -> TestResult { fn load_should_parse_app_config() -> TestResult {
let server_config = given::a_server_config(); let app_config = given::an_app_config();
let fs = kxio::fs::temp()?; let fs = kxio::fs::temp()?;
let_assert!(Ok(_) = write_server_config(&server_config, &fs), "write"); let_assert!(Ok(()) = write_app_config(&app_config, &fs), "write");
let_assert!(Ok(config) = ServerConfig::load(&fs), "load"); let_assert!(Ok(config) = AppConfig::load(&fs), "load");
assert_eq!(config, server_config, "compare"); assert_eq!(config, app_config, "compare");
Ok(()) Ok(())
} }
fn write_server_config( fn write_app_config(app_config: &AppConfig, fs: &kxio::fs::FileSystem) -> TestResult {
server_config: &ServerConfig, let http = &app_config.listen_socket_addr()?;
fs: &kxio::fs::FileSystem,
) -> TestResult {
let http = &server_config.listen_socket_addr()?;
let http_addr = http.ip(); let http_addr = http.ip();
let http_port = server_config.listen_socket_addr()?.port(); let http_port = app_config.listen_socket_addr()?.port();
let listen_url = server_config.listen().url(); let listen_url = app_config.listen().url();
let storage_path = server_config.storage().path(); let storage_path = app_config.storage().path();
let shout = server_config.shout(); let shout = app_config.shout();
let shout_webhook_url = shout.webhook_url().unwrap_or_default(); let shout_webhook_url = shout.webhook_url().unwrap_or_default();
let shout_webhook_secret = shout let shout_webhook_secret = shout
.webhook_secret() .webhook_secret()
@ -482,19 +479,18 @@ mod server {
let shout_email_smtp_hostname = shout_email_smtp.hostname(); let shout_email_smtp_hostname = shout_email_smtp.hostname();
let shout_email_smtp_username = shout_email_smtp.username(); let shout_email_smtp_username = shout_email_smtp.username();
let shout_email_smtp_password = shout_email_smtp.password(); let shout_email_smtp_password = shout_email_smtp.password();
let forge_alias = server_config let forge_alias = app_config
.forges() .forges()
.next() .next()
.map(|(fa, _)| fa) .map(|(fa, _)| fa)
.ok_or("forge missing")?; .ok_or("forge missing")?;
let forge_default = server_config let forge_default = app_config
.forge .forge
.get(forge_alias.as_ref()) .get(forge_alias.as_ref())
.ok_or("forge missing")?; .ok_or("forge missing")?;
let forge_type = forge_default.forge_type(); let forge_type = forge_default.forge_type();
let forge_hostname = forge_default.hostname(); let forge_hostname = forge_default.hostname();
let forge_user = forge_default.user(); let forge_user = forge_default.user();
use secrecy::ExposeSecret;
let forge_token = forge_default.token().expose_secret().to_string(); let forge_token = forge_default.token().expose_secret().to_string();
let mut repos: Vec<String> = vec![]; let mut repos: Vec<String> = vec![];
for (repo_alias, server_repo_config) in forge_default.repos() { for (repo_alias, server_repo_config) in forge_default.repos() {
@ -591,7 +587,7 @@ mod webhook {
let message = ForgeNotification::new( let message = ForgeNotification::new(
forge_alias.clone(), forge_alias.clone(),
given::a_repo_alias(), given::a_repo_alias(),
Default::default(), BTreeMap::default(),
given::a_webhook_message_body(), given::a_webhook_message_body(),
); );
assert_eq!(message.forge_alias(), &forge_alias); assert_eq!(message.forge_alias(), &forge_alias);
@ -602,7 +598,7 @@ mod webhook {
let message = ForgeNotification::new( let message = ForgeNotification::new(
given::a_forge_alias(), given::a_forge_alias(),
repo_alias.clone(), repo_alias.clone(),
Default::default(), BTreeMap::default(),
given::a_webhook_message_body(), given::a_webhook_message_body(),
); );
assert_eq!(message.repo_alias(), &repo_alias); assert_eq!(message.repo_alias(), &repo_alias);
@ -613,7 +609,7 @@ mod webhook {
let message = ForgeNotification::new( let message = ForgeNotification::new(
given::a_forge_alias(), given::a_forge_alias(),
given::a_repo_alias(), given::a_repo_alias(),
Default::default(), BTreeMap::default(),
body.clone(), body.clone(),
); );
assert_eq!(message.body().as_bytes(), body.as_bytes()); assert_eq!(message.body().as_bytes(), body.as_bytes());
@ -622,7 +618,7 @@ mod webhook {
fn should_return_header() { fn should_return_header() {
let key = given::a_name(); let key = given::a_name();
let value = given::a_name(); let value = given::a_name();
let mut headers: BTreeMap<String, String> = Default::default(); let mut headers = BTreeMap::default();
headers.insert(key.clone(), value.clone()); headers.insert(key.clone(), value.clone());
let message = ForgeNotification::new( let message = ForgeNotification::new(
given::a_forge_alias(), given::a_forge_alias(),
@ -730,8 +726,8 @@ mod given {
} }
generate(5) generate(5)
} }
pub fn a_server_config() -> ServerConfig { pub fn an_app_config() -> AppConfig {
ServerConfig::new( AppConfig::new(
a_listen(), a_listen(),
a_shout(), a_shout(),
a_server_storage(), a_server_storage(),
@ -770,8 +766,8 @@ mod given {
Some(SmtpConfig::new(a_name(), a_name(), a_name())), Some(SmtpConfig::new(a_name(), a_name(), a_name())),
) )
} }
pub fn a_server_storage() -> ServerStorage { pub fn a_server_storage() -> Storage {
ServerStorage::new(a_name().into()) Storage::new(a_name().into())
} }
pub fn a_shout() -> Shout { pub fn a_shout() -> Shout {
Shout::new( Shout::new(

View file

@ -1,25 +1,21 @@
use super::*; use super::*;
#[test_log::test] #[test_log::test]
fn url_parse_https_url() -> TestResult { fn url_parse_https_url() {
let url = "https://user:pass@git.host/user/repo.git"; let url = "https://user:pass@git.host/user/repo.git";
let result = RemoteUrl::parse(url); let result = RemoteUrl::parse(url);
tracing::debug!(?result); tracing::debug!(?result);
assert!(result.is_some()); assert!(result.is_some());
Ok(())
} }
#[test_log::test] #[test_log::test]
fn url_parse_ssh_url() -> TestResult { fn url_parse_ssh_url() {
let url = "git@git.host:user/repo.git"; let url = "git@git.host:user/repo.git";
let result = RemoteUrl::parse(url); let result = RemoteUrl::parse(url);
tracing::debug!(?result); tracing::debug!(?result);
assert!(result.is_some()); assert!(result.is_some());
Ok(())
} }

View file

@ -5,6 +5,11 @@ newtype!(WebhookAuth: ulid::Ulid, derive_more::Display: r#"The unique token auth
Each monitored repository has it's own unique token, and it is different each time `git-next` runs."#); Each monitored repository has it's own unique token, and it is different each time `git-next` runs."#);
impl WebhookAuth { impl WebhookAuth {
/// Parses the authorisation string as a `Ulid` to create a `WebhookAuth`.
///
/// # Errors
///
/// Will return an `Err` if the authorisation string is not a valid `Ulid`.
pub fn try_new(authorisation: &str) -> Result<Self, ulid::DecodeError> { pub fn try_new(authorisation: &str) -> Result<Self, ulid::DecodeError> {
use std::str::FromStr as _; use std::str::FromStr as _;
let id = ulid::Ulid::from_str(authorisation)?; let id = ulid::Ulid::from_str(authorisation)?;
@ -12,15 +17,18 @@ impl WebhookAuth {
Ok(Self(id)) Ok(Self(id))
} }
#[must_use]
pub fn generate() -> Self { pub fn generate() -> Self {
Self(ulid::Ulid::new()) Self(ulid::Ulid::new())
} }
#[must_use]
pub fn header_value(&self) -> String { pub fn header_value(&self) -> String {
format!("Basic {}", self) format!("Basic {self}")
} }
pub const fn to_bytes(&self) -> [u8; 16] { #[cfg(test)]
pub(crate) const fn to_bytes(&self) -> [u8; 16] {
self.0.to_bytes() self.0.to_bytes()
} }
} }

View file

@ -3,7 +3,7 @@ use crate::config::{ForgeAlias, RepoAlias};
use derive_more::Constructor; use derive_more::Constructor;
use std::collections::BTreeMap; use std::{collections::BTreeMap, string::ToString};
/// A notification receive from a Forge, typically via a Webhook. /// A notification receive from a Forge, typically via a Webhook.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, derive_more::Constructor)] #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, derive_more::Constructor)]
@ -14,28 +14,37 @@ pub struct ForgeNotification {
body: Body, body: Body,
} }
impl ForgeNotification { impl ForgeNotification {
#[must_use]
pub const fn forge_alias(&self) -> &ForgeAlias { pub const fn forge_alias(&self) -> &ForgeAlias {
&self.forge_alias &self.forge_alias
} }
#[must_use]
pub const fn repo_alias(&self) -> &RepoAlias { pub const fn repo_alias(&self) -> &RepoAlias {
&self.repo_alias &self.repo_alias
} }
#[must_use]
pub const fn body(&self) -> &Body { pub const fn body(&self) -> &Body {
&self.body &self.body
} }
#[must_use]
pub fn header(&self, header: &str) -> Option<String> { pub fn header(&self, header: &str) -> Option<String> {
self.headers.get(header).map(|value| value.to_string()) self.headers.get(header).map(ToString::to_string)
} }
} }
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Constructor)] #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Constructor)]
pub struct Body(String); pub struct Body(String);
impl Body { impl Body {
#[must_use]
pub fn as_str(&self) -> &str { pub fn as_str(&self) -> &str {
self.0.as_str() self.0.as_str()
} }
pub fn as_bytes(&self) -> &[u8] { #[cfg(test)]
pub(crate) fn as_bytes(&self) -> &[u8] {
self.0.as_bytes() self.0.as_bytes()
} }
} }

View file

@ -22,13 +22,18 @@ impl Push {
tracing::warn!(branch = %self.branch, "Unexpected branch"); tracing::warn!(branch = %self.branch, "Unexpected branch");
None None
} }
#[must_use]
pub fn sha(&self) -> &str { pub fn sha(&self) -> &str {
&self.sha &self.sha
} }
#[must_use]
pub fn message(&self) -> &str { pub fn message(&self) -> &str {
&self.message &self.message
} }
} }
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub enum Branch { pub enum Branch {
Main, Main,

View file

@ -22,9 +22,11 @@ pub struct Commit {
message: Message, message: Message,
} }
impl Commit { impl Commit {
#[must_use]
pub const fn sha(&self) -> &Sha { pub const fn sha(&self) -> &Sha {
&self.sha &self.sha
} }
#[must_use]
pub const fn message(&self) -> &Message { pub const fn message(&self) -> &Message {
&self.message &self.message
} }

View file

@ -1,3 +1,7 @@
pub mod commit; pub mod commit;
pub(super) mod like; pub(super) mod r#trait;
pub mod webhook; pub mod webhook;
#[allow(clippy::module_name_repetitions)]
pub use r#trait::ForgeLike;
pub use r#trait::MockForgeLike;

View file

@ -16,11 +16,16 @@ pub trait ForgeLike: std::fmt::Debug + Send + Sync {
/// Checks if the message should be ignored. /// Checks if the message should be ignored.
/// ///
/// Default implementation says that no messages should be ignored. /// Default implementation says that no messages should be ignored.
fn should_ignore_message(&self, _message: &ForgeNotification) -> bool { #[allow(unused_variables)]
fn should_ignore_message(&self, message: &ForgeNotification) -> bool {
false false
} }
/// Parses the webhook body into Some(Push) struct if appropriate, or None if not. /// Parses the webhook body into Some(Push) struct if appropriate, or None if not.
///
/// # Errors
///
/// Will return an `Err` if the body is not a message in the expected format.
fn parse_webhook_body( fn parse_webhook_body(
&self, &self,
body: &webhook::forge_notification::Body, body: &webhook::forge_notification::Body,

View file

@ -7,6 +7,6 @@ newtype!(Generation: u32, Display, Default, Copy: r#"A counter for the server ge
This counter is increased by one each time the server restarts itself when the configuration file is updated."#); This counter is increased by one each time the server restarts itself when the configuration file is updated."#);
impl Generation { impl Generation {
pub fn inc(&mut self) { pub fn inc(&mut self) {
self.0 += 1 self.0 += 1;
} }
} }

View file

@ -9,11 +9,13 @@ pub struct GitRemote {
host: Hostname, host: Hostname,
repo_path: RepoPath, repo_path: RepoPath,
} }
#[cfg(test)]
impl GitRemote { impl GitRemote {
pub const fn host(&self) -> &Hostname { pub(crate) const fn host(&self) -> &Hostname {
&self.host &self.host
} }
pub const fn repo_path(&self) -> &RepoPath { pub(crate) const fn repo_path(&self) -> &RepoPath {
&self.repo_path &self.repo_path
} }
} }

View file

@ -16,10 +16,12 @@ pub mod validation;
mod tests; mod tests;
pub use commit::Commit; pub use commit::Commit;
pub use forge::like::ForgeLike; pub use forge::ForgeLike;
pub use forge::like::MockForgeLike; pub use forge::MockForgeLike;
pub use generation::Generation; pub use generation::Generation;
#[allow(clippy::module_name_repetitions)]
pub use git_ref::GitRef; pub use git_ref::GitRef;
#[allow(clippy::module_name_repetitions)]
pub use git_remote::GitRemote; pub use git_remote::GitRemote;
pub use repo_details::RepoDetails; pub use repo_details::RepoDetails;
pub use repository::Repository; pub use repository::Repository;
@ -33,6 +35,7 @@ use crate::ForgeDetails;
use crate::GitDir; use crate::GitDir;
use crate::RepoConfig; use crate::RepoConfig;
#[must_use]
pub fn repo_details( pub fn repo_details(
n: u32, n: u32,
generation: Generation, generation: Generation,

View file

@ -10,7 +10,7 @@ impl std::fmt::Display for Force {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
Self::No => write!(f, "fast-forward"), Self::No => write!(f, "fast-forward"),
Self::From(from) => write!(f, "force-if-from:{}", from), Self::From(from) => write!(f, "force-if-from:{from}"),
} }
} }
} }
@ -45,6 +45,15 @@ pub enum Error {
TestResult(#[from] Box<dyn std::error::Error>), TestResult(#[from] Box<dyn std::error::Error>),
} }
/// Resets the position of a branch in the remote repo
///
/// Performs a 'git fetch' first to ensure we have up-to-date branch positions before
/// performing `git push`.
///
/// # Errors
///
/// Will return an `Err` if their is no remote fetch defined in .git/config, or
/// if there are any network connectivity issues with the remote server.
pub fn reset( pub fn reset(
open_repository: &dyn OpenRepositoryLike, open_repository: &dyn OpenRepositoryLike,
repo_details: &git::RepoDetails, repo_details: &git::RepoDetails,

View file

@ -3,7 +3,7 @@ use crate::{
git::{ git::{
self, self,
repository::open::{oreal::RealOpenRepository, OpenRepositoryLike}, repository::open::{oreal::RealOpenRepository, OpenRepositoryLike},
Generation, GitRemote, Generation,
}, },
pike, BranchName, ForgeAlias, ForgeConfig, ForgeDetails, GitDir, Hostname, RemoteUrl, pike, BranchName, ForgeAlias, ForgeConfig, ForgeDetails, GitDir, Hostname, RemoteUrl,
RepoAlias, RepoConfig, RepoPath, ServerRepoConfig, StoragePathType, RepoAlias, RepoConfig, RepoPath, ServerRepoConfig, StoragePathType,
@ -11,7 +11,8 @@ use crate::{
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
use secrecy::Secret; use secrecy::{ExposeSecret, Secret};
use tracing::instrument;
/// The derived information about a repo, used to interact with it /// The derived information about a repo, used to interact with it
#[derive(Clone, Debug, derive_more::Display, derive_with::With)] #[derive(Clone, Debug, derive_more::Display, derive_with::With)]
@ -26,6 +27,7 @@ pub struct RepoDetails {
pub gitdir: GitDir, pub gitdir: GitDir,
} }
impl RepoDetails { impl RepoDetails {
#[must_use]
pub fn new( pub fn new(
generation: Generation, generation: Generation,
repo_alias: &RepoAlias, repo_alias: &RepoAlias,
@ -50,26 +52,24 @@ impl RepoDetails {
), ),
} }
} }
pub fn origin(&self) -> secrecy::Secret<String> { pub(crate) fn origin(&self) -> secrecy::Secret<String> {
let repo_details = self; let repo_details = self;
let user = &repo_details.forge.user(); let user = &repo_details.forge.user();
let hostname = &repo_details.forge.hostname(); let hostname = &repo_details.forge.hostname();
let repo_path = &repo_details.repo_path; let repo_path = &repo_details.repo_path;
let expose_secret = repo_details.forge.token(); let expose_secret = repo_details.forge.token();
use secrecy::ExposeSecret;
let token = expose_secret.expose_secret(); let token = expose_secret.expose_secret();
let origin = format!("https://{user}:{token}@{hostname}/{repo_path}.git"); let origin = format!("https://{user}:{token}@{hostname}/{repo_path}.git");
origin.into() origin.into()
} }
pub const fn gitdir(&self) -> &GitDir { pub(crate) const fn gitdir(&self) -> &GitDir {
&self.gitdir &self.gitdir
} }
pub fn git_remote(&self) -> GitRemote { #[must_use]
GitRemote::new(self.forge.hostname().clone(), self.repo_path.clone())
}
pub fn with_hostname(mut self, hostname: Hostname) -> Self { pub fn with_hostname(mut self, hostname: Hostname) -> Self {
let forge = self.forge; let forge = self.forge;
self.forge = forge.with_hostname(hostname); self.forge = forge.with_hostname(hostname);
@ -77,21 +77,17 @@ impl RepoDetails {
} }
// url is a secret as it contains auth token // url is a secret as it contains auth token
pub fn url(&self) -> Secret<String> { pub(crate) fn url(&self) -> Secret<String> {
let user = self.forge.user(); let user = self.forge.user();
use secrecy::ExposeSecret;
let token = self.forge.token().expose_secret(); let token = self.forge.token().expose_secret();
let auth_delim = match token.is_empty() { let auth_delim = if token.is_empty() { "" } else { ":" };
true => "",
false => ":",
};
let hostname = self.forge.hostname(); let hostname = self.forge.hostname();
let repo_path = &self.repo_path; let repo_path = &self.repo_path;
format!("https://{user}{auth_delim}{token}@{hostname}/{repo_path}.git").into() format!("https://{user}{auth_delim}{token}@{hostname}/{repo_path}.git").into()
} }
#[allow(clippy::result_large_err)] #[allow(clippy::result_large_err)]
pub fn open(&self) -> Result<impl OpenRepositoryLike, git::validation::remotes::Error> { pub(crate) fn open(&self) -> Result<impl OpenRepositoryLike, git::validation::remotes::Error> {
let gix_repo = pike! { let gix_repo = pike! {
self self
|> Self::gitdir |> Self::gitdir
@ -107,12 +103,13 @@ impl RepoDetails {
Ok(repo) Ok(repo)
} }
#[must_use]
pub fn remote_url(&self) -> Option<RemoteUrl> { pub fn remote_url(&self) -> Option<RemoteUrl> {
use secrecy::ExposeSecret; use secrecy::ExposeSecret;
RemoteUrl::parse(self.url().expose_secret()) RemoteUrl::parse(self.url().expose_secret())
} }
#[tracing::instrument] #[instrument]
pub fn assert_remote_url(&self, found: Option<RemoteUrl>) -> git::repository::Result<()> { pub fn assert_remote_url(&self, found: Option<RemoteUrl>) -> git::repository::Result<()> {
let Some(found) = found else { let Some(found) = found else {
tracing::debug!("No remote url found to assert"); tracing::debug!("No remote url found to assert");
@ -152,10 +149,10 @@ impl RepoDetails {
let config_file = fs.file_read_to_string(config_filename)?; let config_file = fs.file_read_to_string(config_filename)?;
let mut config_lines = config_file let mut config_lines = config_file
.lines() .lines()
.map(|l| l.to_owned()) .map(ToOwned::to_owned)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
tracing::debug!(?config_lines, "original file"); tracing::debug!(?config_lines, "original file");
let url_line = format!(r#" url = "{}""#, url); let url_line = format!(r#" url = "{url}""#);
if config_lines if config_lines
.iter() .iter()
.any(|line| *line == r#"[remote "origin"]"#) .any(|line| *line == r#"[remote "origin"]"#)
@ -163,7 +160,7 @@ impl RepoDetails {
tracing::debug!("has an 'origin' remote"); tracing::debug!("has an 'origin' remote");
config_lines config_lines
.iter_mut() .iter_mut()
.filter(|line| line.starts_with(r#" url = "#)) .filter(|line| line.starts_with(r" url = "))
.for_each(|line| line.clone_from(&url_line)); .for_each(|line| line.clone_from(&url_line));
} else { } else {
tracing::debug!("has no 'origin' remote"); tracing::debug!("has no 'origin' remote");

View file

@ -8,21 +8,37 @@ use crate::git::{
RepoDetails, RepoDetails,
}; };
use derive_more::Deref as _; use secrecy::ExposeSecret as _;
use std::sync::{atomic::AtomicBool, Arc, RwLock}; use std::sync::{atomic::AtomicBool, Arc, RwLock};
#[allow(clippy::module_name_repetitions)]
#[mockall::automock] #[mockall::automock]
pub trait RepositoryFactory: std::fmt::Debug + Sync + Send { pub trait RepositoryFactory: std::fmt::Debug + Sync + Send {
fn duplicate(&self) -> Box<dyn RepositoryFactory>; fn duplicate(&self) -> Box<dyn RepositoryFactory>;
/// Opens the repository.
///
/// # Errors
///
/// Will return an `Err` if the repository can't be opened.
fn open(&self, repo_details: &RepoDetails) -> Result<Box<dyn OpenRepositoryLike>>; fn open(&self, repo_details: &RepoDetails) -> Result<Box<dyn OpenRepositoryLike>>;
/// Clones the git repository from the remote server.
///
/// # Errors
///
/// Will return an `Err` if there are any network connectivity issues
/// connecting with the server.
fn git_clone(&self, repo_details: &RepoDetails) -> Result<Box<dyn OpenRepositoryLike>>; fn git_clone(&self, repo_details: &RepoDetails) -> Result<Box<dyn OpenRepositoryLike>>;
} }
#[must_use]
pub fn real() -> Box<dyn RepositoryFactory> { pub fn real() -> Box<dyn RepositoryFactory> {
Box::new(RealRepositoryFactory) Box::new(RealRepositoryFactory)
} }
#[must_use]
pub fn mock() -> Box<MockRepositoryFactory> { pub fn mock() -> Box<MockRepositoryFactory> {
Box::new(MockRepositoryFactory::new()) Box::new(MockRepositoryFactory::new())
} }
@ -44,10 +60,9 @@ impl RepositoryFactory for RealRepositoryFactory {
fn git_clone(&self, repo_details: &RepoDetails) -> Result<Box<dyn OpenRepositoryLike>> { fn git_clone(&self, repo_details: &RepoDetails) -> Result<Box<dyn OpenRepositoryLike>> {
tracing::info!("creating"); tracing::info!("creating");
use secrecy::ExposeSecret;
let (gix_repo, _outcome) = gix::prepare_clone_bare( let (gix_repo, _outcome) = gix::prepare_clone_bare(
repo_details.origin().expose_secret().as_str(), repo_details.origin().expose_secret().as_str(),
repo_details.gitdir.deref(), &*repo_details.gitdir,
)? )?
.fetch_only(gix::progress::Discard, &AtomicBool::new(false))?; .fetch_only(gix::progress::Discard, &AtomicBool::new(false))?;
tracing::info!("created"); tracing::info!("created");

View file

@ -17,6 +17,7 @@ pub mod factory;
pub mod open; pub mod open;
mod test; mod test;
#[allow(clippy::module_name_repetitions)]
pub use factory::RepositoryFactory; pub use factory::RepositoryFactory;
#[cfg(test)] #[cfg(test)]
@ -29,7 +30,8 @@ pub enum Repository {
Test(TestRepository), Test(TestRepository),
} }
pub const fn test(fs: kxio::fs::FileSystem) -> TestRepository { #[cfg(test)]
pub(crate) const fn test(fs: kxio::fs::FileSystem) -> TestRepository {
TestRepository::new(fs, vec![], vec![]) TestRepository::new(fs, vec![], vec![])
} }
@ -40,12 +42,12 @@ pub fn open(
repository_factory: &dyn factory::RepositoryFactory, repository_factory: &dyn factory::RepositoryFactory,
repo_details: &RepoDetails, repo_details: &RepoDetails,
) -> Result<Box<dyn OpenRepositoryLike>> { ) -> Result<Box<dyn OpenRepositoryLike>> {
let open_repository = if !repo_details.gitdir.exists() { let open_repository = if repo_details.gitdir.exists() {
info!("Local copy not found - cloning...");
repository_factory.git_clone(repo_details)?
} else {
info!("Local copy found - opening..."); info!("Local copy found - opening...");
repository_factory.open(repo_details)? repository_factory.open(repo_details)?
} else {
info!("Local copy not found - cloning...");
repository_factory.git_clone(repo_details)?
}; };
info!("Validating..."); info!("Validating...");
validate_default_remotes(&*open_repository, repo_details) validate_default_remotes(&*open_repository, repo_details)
@ -53,20 +55,23 @@ pub fn open(
Ok(open_repository) Ok(open_repository)
} }
#[allow(clippy::module_name_repetitions)]
pub trait RepositoryLike { pub trait RepositoryLike {
/// Opens the repository.
///
/// # Errors
///
/// Will return an `Err` if the repository can't be opened.
fn open(&self, gitdir: &GitDir) -> Result<OpenRepository>; fn open(&self, gitdir: &GitDir) -> Result<OpenRepository>;
/// Clones the git repository from the remote server.
///
/// # Errors
///
/// Will return an `Err` if there are any network connectivity issues
/// connecting with the server.
fn git_clone(&self, repo_details: &RepoDetails) -> Result<OpenRepository>; fn git_clone(&self, repo_details: &RepoDetails) -> Result<OpenRepository>;
} }
// impl std::ops::Deref for Repository {
// type Target = dyn RepositoryLike;
//
// fn deref(&self) -> &Self::Target {
// match self {
// Self::Real => &real::RealRepository,
// Self::Test(test_repository) => test_repository,
// }
// }
// }
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] #[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
pub enum Direction { pub enum Direction {

View file

@ -19,6 +19,7 @@ use std::{
sync::{Arc, RwLock}, sync::{Arc, RwLock},
}; };
#[allow(clippy::module_name_repetitions)]
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum OpenRepository { pub enum OpenRepository {
/// A real git repository. /// A real git repository.
@ -43,21 +44,44 @@ pub fn real(gix_repo: gix::Repository) -> OpenRepository {
} }
#[cfg(not(tarpaulin_include))] // don't test mocks #[cfg(not(tarpaulin_include))] // don't test mocks
pub fn test( pub(crate) fn test(
gitdir: &GitDir, gitdir: &GitDir,
fs: kxio::fs::FileSystem, fs: &kxio::fs::FileSystem,
on_fetch: Vec<otest::OnFetch>, on_fetch: Vec<otest::OnFetch>,
on_push: Vec<otest::OnPush>, on_push: Vec<otest::OnPush>,
) -> OpenRepository { ) -> OpenRepository {
OpenRepository::Test(TestOpenRepository::new(gitdir, fs, on_fetch, on_push)) OpenRepository::Test(TestOpenRepository::new(gitdir, fs, on_fetch, on_push))
} }
#[allow(clippy::module_name_repetitions)]
#[mockall::automock] #[mockall::automock]
pub trait OpenRepositoryLike: std::fmt::Debug + Sync { pub trait OpenRepositoryLike: std::fmt::Debug + Sync {
/// Creates a clone of the `OpenRepositoryLike`.
fn duplicate(&self) -> Box<dyn OpenRepositoryLike>; fn duplicate(&self) -> Box<dyn OpenRepositoryLike>;
/// Returns a `Vec` of all the branches in the remote repo.
///
/// # Errors
///
/// Will return `Err` if there are any network connectivity issues with
/// the remote server.
fn remote_branches(&self) -> git::push::Result<Vec<BranchName>>; fn remote_branches(&self) -> git::push::Result<Vec<BranchName>>;
fn find_default_remote(&self, direction: Direction) -> Option<RemoteUrl>; fn find_default_remote(&self, direction: Direction) -> Option<RemoteUrl>;
/// Performs a `git fetch`
///
/// # Errors
///
/// Will return an `Err` if their is no remote fetch defined in .git/config, or
/// if there are any network connectivity issues with the remote server.
fn fetch(&self) -> Result<(), git::fetch::Error>; fn fetch(&self) -> Result<(), git::fetch::Error>;
/// Performs a `git push`
///
/// # Errors
///
/// Will return an `Err` if their is no remote push defined in .git/config, or
/// if there are any network connectivity issues with the remote server.
fn push( fn push(
&self, &self,
repo_details: &git::RepoDetails, repo_details: &git::RepoDetails,
@ -67,6 +91,10 @@ pub trait OpenRepositoryLike: std::fmt::Debug + Sync {
) -> git::push::Result<()>; ) -> git::push::Result<()>;
/// List of commits in a branch, optionally up-to any specified commit. /// List of commits in a branch, optionally up-to any specified commit.
///
/// # Errors
///
/// Will return `Err` if there are any network connectivity issues with the remote server.
fn commit_log( fn commit_log(
&self, &self,
branch_name: &BranchName, branch_name: &BranchName,
@ -76,10 +104,15 @@ pub trait OpenRepositoryLike: std::fmt::Debug + Sync {
/// Read the contents of a file as a string. /// Read the contents of a file as a string.
/// ///
/// Only handles files in the root of the repo. /// Only handles files in the root of the repo.
///
/// # Errors
///
/// Will return `Err` if the file does not exists on the specified branch.
fn read_file(&self, branch_name: &BranchName, file_name: &Path) -> git::file::Result<String>; fn read_file(&self, branch_name: &BranchName, file_name: &Path) -> git::file::Result<String>;
} }
pub fn mock() -> Box<MockOpenRepositoryLike> { #[cfg(test)]
pub(crate) fn mock() -> Box<MockOpenRepositoryLike> {
Box::new(MockOpenRepositoryLike::new()) Box::new(MockOpenRepositoryLike::new())
} }

View file

@ -9,7 +9,9 @@ use gix::bstr::BStr;
use tracing::{info, warn}; use tracing::{info, warn};
use std::{ use std::{
borrow::ToOwned,
path::Path, path::Path,
result::Result,
sync::{Arc, RwLock}, sync::{Arc, RwLock},
}; };
@ -24,11 +26,12 @@ impl super::OpenRepositoryLike for RealOpenRepository {
.and_then(|repo| { .and_then(|repo| {
Ok(repo.to_thread_local().references()?).and_then(|refs| { Ok(repo.to_thread_local().references()?).and_then(|refs| {
Ok(refs.remote_branches().map(|rb| { Ok(refs.remote_branches().map(|rb| {
rb.filter_map(|rbi| rbi.ok()) rb.filter_map(Result::ok)
.map(|r| r.name().to_owned()) .map(|r| r.name().to_owned())
.map(|n| n.to_string()) .map(|n| n.to_string())
.filter_map(|p| { .filter_map(|p| {
p.strip_prefix("refs/remotes/origin/").map(|v| v.to_owned()) p.strip_prefix("refs/remotes/origin/")
.map(ToOwned::to_owned)
}) })
.filter(|b| b.as_str() != "HEAD") .filter(|b| b.as_str() != "HEAD")
.map(BranchName::new) .map(BranchName::new)
@ -61,6 +64,8 @@ impl super::OpenRepositoryLike for RealOpenRepository {
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
#[cfg(not(tarpaulin_include))] // would require writing to external service #[cfg(not(tarpaulin_include))] // would require writing to external service
fn fetch(&self) -> Result<(), git::fetch::Error> { fn fetch(&self) -> Result<(), git::fetch::Error> {
use std::sync::atomic::AtomicBool;
let Ok(repository) = self.0.read() else { let Ok(repository) = self.0.read() else {
#[cfg(not(tarpaulin_include))] // don't test mutex lock failure #[cfg(not(tarpaulin_include))] // don't test mutex lock failure
return Err(git::fetch::Error::Lock); return Err(git::fetch::Error::Lock);
@ -75,9 +80,12 @@ impl super::OpenRepositoryLike for RealOpenRepository {
remote remote
.connect(gix::remote::Direction::Fetch) .connect(gix::remote::Direction::Fetch)
.map_err(|gix| git::fetch::Error::Connect(gix.to_string()))? .map_err(|gix| git::fetch::Error::Connect(gix.to_string()))?
.prepare_fetch(gix::progress::Discard, Default::default()) .prepare_fetch(
gix::progress::Discard,
gix::remote::ref_map::Options::default(),
)
.map_err(|gix| git::fetch::Error::Prepare(gix.to_string()))? .map_err(|gix| git::fetch::Error::Prepare(gix.to_string()))?
.receive(gix::progress::Discard, &Default::default()) .receive(gix::progress::Discard, &AtomicBool::default())
.map_err(|gix| git::fetch::Error::Receive(gix.to_string()))?; .map_err(|gix| git::fetch::Error::Receive(gix.to_string()))?;
info!("Fetch okay"); info!("Fetch okay");
Ok(()) Ok(())
@ -93,15 +101,16 @@ impl super::OpenRepositoryLike for RealOpenRepository {
to_commit: &git::GitRef, to_commit: &git::GitRef,
force: &git::push::Force, force: &git::push::Force,
) -> Result<(), git::push::Error> { ) -> Result<(), git::push::Error> {
use secrecy::ExposeSecret as _;
let origin = repo_details.origin(); let origin = repo_details.origin();
let force = match force { let force = match force {
git::push::Force::No => "".to_string(), git::push::Force::No => String::new(),
git::push::Force::From(old_ref) => { git::push::Force::From(old_ref) => {
format!("--force-with-lease={branch_name}:{old_ref}") format!("--force-with-lease={branch_name}:{old_ref}")
} }
}; };
// INFO: never log the command as it contains the API token within the 'origin' // INFO: never log the command as it contains the API token within the 'origin'
use secrecy::ExposeSecret;
let command: secrecy::Secret<String> = format!( let command: secrecy::Secret<String> = format!(
"/usr/bin/git push {} {to_commit}:{branch_name} {force}", "/usr/bin/git push {} {to_commit}:{branch_name} {force}",
origin.expose_secret() origin.expose_secret()
@ -131,10 +140,7 @@ impl super::OpenRepositoryLike for RealOpenRepository {
branch_name: &BranchName, branch_name: &BranchName,
find_commits: &[git::Commit], find_commits: &[git::Commit],
) -> Result<Vec<git::Commit>, git::commit::log::Error> { ) -> Result<Vec<git::Commit>, git::commit::log::Error> {
let limit = match find_commits.is_empty() { let limit = if find_commits.is_empty() { 1 } else { 25 };
true => 1,
false => 25,
};
self.0 self.0
.read() .read()
.map_err(|_| git::commit::log::Error::Lock) .map_err(|_| git::commit::log::Error::Lock)
@ -195,8 +201,7 @@ impl super::OpenRepositoryLike for RealOpenRepository {
.map_err(|_| git::file::Error::Lock) .map_err(|_| git::file::Error::Lock)
.and_then(|repo| { .and_then(|repo| {
let thread_local = repo.to_thread_local(); let thread_local = repo.to_thread_local();
let fref = let fref = thread_local.find_reference(format!("origin/{branch_name}").as_str())?;
thread_local.find_reference(format!("origin/{}", branch_name).as_str())?;
let id = fref.try_id().ok_or(git::file::Error::TryId)?; let id = fref.try_id().ok_or(git::file::Error::TryId)?;
let oid = id.detach(); let oid = id.detach();
let obj = thread_local.find_object(oid)?; let obj = thread_local.find_object(oid)?;

View file

@ -7,7 +7,7 @@ use crate::{
BranchName, GitDir, RemoteUrl, RepoBranches, BranchName, GitDir, RemoteUrl, RepoBranches,
}; };
use derive_more::{Constructor, Deref}; use derive_more::Constructor;
use std::{ use std::{
path::Path, path::Path,
@ -23,6 +23,12 @@ pub struct OnFetch {
action: OnFetchFn, action: OnFetchFn,
} }
impl OnFetch { impl OnFetch {
/// Invokes the action function.
///
/// # Errors
///
/// Will return any `Err` if there is no fetch remote defined in .git/config
/// of if there are any network connectivity issues with the remote server.
pub fn invoke(&self) -> git::fetch::Result<()> { pub fn invoke(&self) -> git::fetch::Result<()> {
(self.action)(&self.repo_branches, &self.gitdir, &self.fs) (self.action)(&self.repo_branches, &self.gitdir, &self.fs)
} }
@ -45,6 +51,12 @@ pub struct OnPush {
action: OnPushFn, action: OnPushFn,
} }
impl OnPush { impl OnPush {
/// Invokes the action function.
///
/// # Errors
///
/// Will return any `Err` if there is no push remote defined in .git/config
/// of if there are any network connectivity issues with the remote server.
pub fn invoke( pub fn invoke(
&self, &self,
repo_details: &git::RepoDetails, repo_details: &git::RepoDetails,
@ -86,16 +98,17 @@ impl git::repository::OpenRepositoryLike for TestOpenRepository {
let i: usize = *self let i: usize = *self
.fetch_counter .fetch_counter
.read() .read()
.map_err(|_| git::fetch::Error::Lock)? .map_err(|_| git::fetch::Error::Lock)?;
.deref();
println!("Fetch: {i}"); println!("Fetch: {i}");
self.fetch_counter self.fetch_counter
.write() .write()
.map_err(|_| git::fetch::Error::Lock) .map_err(|_| git::fetch::Error::Lock)
.map(|mut c| *c += 1)?; .map(|mut c| *c += 1)?;
self.on_fetch.get(i).map(|f| f.invoke()).unwrap_or_else(|| { #[allow(clippy::expect_used)]
unimplemented!("Unexpected fetch"); self.on_fetch
}) .get(i)
.map(OnFetch::invoke)
.expect("unexpected fetch")
} }
fn push( fn push(
@ -108,19 +121,17 @@ impl git::repository::OpenRepositoryLike for TestOpenRepository {
let i: usize = *self let i: usize = *self
.push_counter .push_counter
.read() .read()
.map_err(|_| git::fetch::Error::Lock)? .map_err(|_| git::fetch::Error::Lock)?;
.deref();
println!("Push: {i}"); println!("Push: {i}");
self.push_counter self.push_counter
.write() .write()
.map_err(|_| git::fetch::Error::Lock) .map_err(|_| git::fetch::Error::Lock)
.map(|mut c| *c += 1)?; .map(|mut c| *c += 1)?;
#[allow(clippy::expect_used)]
self.on_push self.on_push
.get(i) .get(i)
.map(|f| f.invoke(repo_details, branch_name, to_commit, force)) .map(|f| f.invoke(repo_details, branch_name, to_commit, force))
.unwrap_or_else(|| { .expect("unexpected push")
unimplemented!("Unexpected push");
})
} }
fn commit_log( fn commit_log(
@ -140,16 +151,16 @@ impl git::repository::OpenRepositoryLike for TestOpenRepository {
} }
} }
impl TestOpenRepository { impl TestOpenRepository {
pub fn new( pub(crate) fn new(
gitdir: &GitDir, gitdir: &GitDir,
fs: kxio::fs::FileSystem, fs: &kxio::fs::FileSystem,
on_fetch: Vec<OnFetch>, on_fetch: Vec<OnFetch>,
on_push: Vec<OnPush>, on_push: Vec<OnPush>,
) -> Self { ) -> Self {
let pathbuf = fs.base().join(gitdir.to_path_buf()); let pathbuf = fs.base().join(gitdir.to_path_buf());
#[allow(clippy::expect_used)] #[allow(clippy::expect_used)]
let gix = gix::init(pathbuf).expect("git init"); let gix = gix::init(pathbuf).expect("git init");
Self::write_origin(gitdir, &fs); Self::write_origin(gitdir, fs);
Self { Self {
on_fetch, on_fetch,
fetch_counter: Arc::new(RwLock::new(0)), fetch_counter: Arc::new(RwLock::new(0)),
@ -174,6 +185,6 @@ impl TestOpenRepository {
); );
#[allow(clippy::expect_used)] #[allow(clippy::expect_used)]
fs.file_write(&config_file, &updated_contents) fs.file_write(&config_file, &updated_contents)
.expect("write updated .git/config") .expect("write updated .git/config");
} }
} }

View file

@ -9,5 +9,5 @@ fn should_fetch_from_repo() {
let gitdir = GitDir::new(cwd.join("../.."), StoragePathType::External); let gitdir = GitDir::new(cwd.join("../.."), StoragePathType::External);
let repo_details = given::repo_details(&given::a_filesystem()).with_gitdir(gitdir); let repo_details = given::repo_details(&given::a_filesystem()).with_gitdir(gitdir);
let_assert!(Ok(repo) = crate::git::repository::factory::real().open(&repo_details)); let_assert!(Ok(repo) = crate::git::repository::factory::real().open(&repo_details));
let_assert!(Ok(_) = repo.fetch()); let_assert!(Ok(()) = repo.fetch());
} }

View file

@ -16,6 +16,7 @@ use crate::{
GitDir, GitDir,
}; };
#[allow(clippy::module_name_repetitions)]
#[derive(Clone, Debug, Constructor)] #[derive(Clone, Debug, Constructor)]
pub struct TestRepository { pub struct TestRepository {
fs: kxio::fs::FileSystem, fs: kxio::fs::FileSystem,
@ -35,7 +36,7 @@ impl RepositoryLike for TestRepository {
fn open(&self, gitdir: &GitDir) -> Result<OpenRepository> { fn open(&self, gitdir: &GitDir) -> Result<OpenRepository> {
Ok(git::repository::open::test( Ok(git::repository::open::test(
gitdir, gitdir,
self.fs.clone(), &self.fs,
self.on_fetch.clone(), self.on_fetch.clone(),
self.on_push.clone(), self.on_push.clone(),
)) ))

View file

@ -11,7 +11,6 @@ fn open_where_storage_external_auth_matches() -> TestResult {
let fs = given::a_filesystem(); let fs = given::a_filesystem();
let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::External); let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::External);
let repo_details = given::repo_details(&fs).with_gitdir(gitdir); let repo_details = given::repo_details(&fs).with_gitdir(gitdir);
use secrecy::ExposeSecret;
let url = repo_details.url(); let url = repo_details.url();
let url = url.expose_secret(); let url = url.expose_secret();
given::a_bare_repo_with_url(fs.base(), url, &fs); given::a_bare_repo_with_url(fs.base(), url, &fs);
@ -37,7 +36,6 @@ fn open_where_storage_external_auth_differs_error() -> TestResult {
//given //given
let fs = given::a_filesystem(); let fs = given::a_filesystem();
let repo_details = given::repo_details(&fs); let repo_details = given::repo_details(&fs);
use secrecy::ExposeSecret;
let original_url = repo_details.url(); let original_url = repo_details.url();
let original_url = original_url.expose_secret(); let original_url = original_url.expose_secret();
given::a_bare_repo_with_url(fs.base(), original_url, &fs); given::a_bare_repo_with_url(fs.base(), original_url, &fs);
@ -75,7 +73,6 @@ fn open_where_storage_internal_auth_matches() -> TestResult {
let fs = given::a_filesystem(); let fs = given::a_filesystem();
let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal); let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal);
let repo_details = given::repo_details(&fs).with_gitdir(gitdir); let repo_details = given::repo_details(&fs).with_gitdir(gitdir);
use secrecy::ExposeSecret;
let url = repo_details.url(); let url = repo_details.url();
let url = url.expose_secret(); let url = url.expose_secret();
// create a bare repg with the auth from the forge_config // create a bare repg with the auth from the forge_config
@ -105,7 +102,6 @@ fn open_where_storage_internal_auth_differs_update_config() -> TestResult {
//given //given
let fs = given::a_filesystem(); let fs = given::a_filesystem();
let repo_details = given::repo_details(&fs); let repo_details = given::repo_details(&fs);
use secrecy::ExposeSecret;
let original_url = repo_details.url(); let original_url = repo_details.url();
let original_url = original_url.expose_secret(); let original_url = original_url.expose_secret();
given::a_bare_repo_with_url(fs.base(), original_url, &fs); given::a_bare_repo_with_url(fs.base(), original_url, &fs);

View file

@ -5,6 +5,7 @@ use crate::{
}; };
use assert2::let_assert; use assert2::let_assert;
use secrecy::ExposeSecret as _;
mod factory; mod factory;
mod validate; mod validate;

View file

@ -101,7 +101,7 @@ mod push {
#[test] #[test]
fn force_no_should_display() { fn force_no_should_display() {
assert_eq!(git::push::Force::No.to_string(), "fast-forward") assert_eq!(git::push::Force::No.to_string(), "fast-forward");
} }
#[test] #[test]
@ -111,7 +111,7 @@ mod push {
assert_eq!( assert_eq!(
git::push::Force::From(GitRef::from(commit)).to_string(), git::push::Force::From(GitRef::from(commit)).to_string(),
format!("force-if-from:{sha}") format!("force-if-from:{sha}")
) );
} }
mod reset { mod reset {
@ -138,7 +138,7 @@ mod push {
let commit = given::a_commit(); let commit = given::a_commit();
let gitref = GitRef::from(commit); let gitref = GitRef::from(commit);
let_assert!( let_assert!(
Ok(_) = git::push::reset( Ok(()) = git::push::reset(
&*open_repository, &*open_repository,
&repo_details, &repo_details,
branch_name, branch_name,
@ -182,38 +182,6 @@ mod repo_details {
"https://user:token@host/repo.git" "https://user:token@host/repo.git"
); );
} }
#[test]
fn should_return_git_remote() {
let rd = RepoDetails::new(
Generation::default(),
&RepoAlias::new("foo"),
&ServerRepoConfig::new(
"user/repo".to_string(),
"branch".to_string(),
None,
None,
None,
None,
),
&ForgeAlias::new("default".to_string()),
&ForgeConfig::new(
ForgeType::MockForge,
"host".to_string(),
"user".to_string(),
"token".to_string(),
BTreeMap::new(),
),
GitDir::new(PathBuf::default().join("foo"), StoragePathType::Internal),
);
assert_eq!(
rd.git_remote(),
GitRemote::new(
Hostname::new("host".to_string()),
RepoPath::new("user/repo".to_string())
)
);
}
} }
pub mod given { pub mod given {
use super::*; use super::*;
@ -267,7 +235,7 @@ pub mod given {
format!("hostname-{}", a_name()), format!("hostname-{}", a_name()),
format!("user-{}", a_name()), format!("user-{}", a_name()),
format!("token-{}", a_name()), format!("token-{}", a_name()),
Default::default(), // no repos BTreeMap::default(), // no repos
) )
} }
@ -349,7 +317,7 @@ pub mod given {
// add use are origin url // add use are origin url
let mut config_lines = config_file.lines().collect::<Vec<_>>(); let mut config_lines = config_file.lines().collect::<Vec<_>>();
config_lines.push(r#"[remote "origin"]"#); config_lines.push(r#"[remote "origin"]"#);
let url_line = format!(r#" url = "{}""#, url); let url_line = format!(r#" url = "{url}""#);
tracing::info!(?url, %url_line, "writing"); tracing::info!(?url, %url_line, "writing");
config_lines.push(&url_line); config_lines.push(&url_line);
// write config file back out // write config file back out
@ -436,7 +404,7 @@ pub mod then {
pub fn git_checkout_new_branch(branch_name: &BranchName, gitdir: &GitDir) -> TestResult { pub fn git_checkout_new_branch(branch_name: &BranchName, gitdir: &GitDir) -> TestResult {
exec( exec(
format!("git checkout -b {}", branch_name), &format!("git checkout -b {branch_name}"),
std::process::Command::new("/usr/bin/git") std::process::Command::new("/usr/bin/git")
.current_dir(gitdir.to_path_buf()) .current_dir(gitdir.to_path_buf())
.args(["checkout", "-b", branch_name.to_string().as_str()]) .args(["checkout", "-b", branch_name.to_string().as_str()])
@ -447,7 +415,7 @@ pub mod then {
pub fn git_switch(branch_name: &BranchName, gitdir: &GitDir) -> TestResult { pub fn git_switch(branch_name: &BranchName, gitdir: &GitDir) -> TestResult {
exec( exec(
format!("git switch {}", branch_name), &format!("git switch {branch_name}"),
std::process::Command::new("/usr/bin/git") std::process::Command::new("/usr/bin/git")
.current_dir(gitdir.to_path_buf()) .current_dir(gitdir.to_path_buf())
.args(["switch", branch_name.to_string().as_str()]) .args(["switch", branch_name.to_string().as_str()])
@ -455,7 +423,7 @@ pub mod then {
) )
} }
fn exec(label: String, output: Result<std::process::Output, std::io::Error>) -> TestResult { fn exec(label: &str, output: Result<std::process::Output, std::io::Error>) -> TestResult {
println!("== {label}"); println!("== {label}");
match output { match output {
Ok(output) => { Ok(output) => {
@ -479,7 +447,7 @@ pub mod then {
fn git_add_file(gitdir: &GitDir, file: &Path) -> TestResult { fn git_add_file(gitdir: &GitDir, file: &Path) -> TestResult {
exec( exec(
format!("git add {file:?}"), &format!("git add {file:?}"),
std::process::Command::new("/usr/bin/git") std::process::Command::new("/usr/bin/git")
.current_dir(gitdir.to_path_buf()) .current_dir(gitdir.to_path_buf())
.args(["add", file.display().to_string().as_str()]) .args(["add", file.display().to_string().as_str()])
@ -489,7 +457,7 @@ pub mod then {
fn git_commit(gitdir: &GitDir, file: &Path) -> TestResult { fn git_commit(gitdir: &GitDir, file: &Path) -> TestResult {
exec( exec(
format!(r#"git commit -m"Added {file:?}""#), &format!(r#"git commit -m"Added {file:?}""#),
std::process::Command::new("/usr/bin/git") std::process::Command::new("/usr/bin/git")
.current_dir(gitdir.to_path_buf()) .current_dir(gitdir.to_path_buf())
.args([ .args([
@ -502,7 +470,7 @@ pub mod then {
pub fn git_log_all(gitdir: &GitDir) -> TestResult { pub fn git_log_all(gitdir: &GitDir) -> TestResult {
exec( exec(
"git log --all --oneline --decorate --graph".to_string(), "git log --all --oneline --decorate --graph",
std::process::Command::new("/usr/bin/git") std::process::Command::new("/usr/bin/git")
.current_dir(gitdir.to_path_buf()) .current_dir(gitdir.to_path_buf())
.args(["log", "--all", "--oneline", "--decorate", "--graph"]) .args(["log", "--all", "--oneline", "--decorate", "--graph"])

View file

@ -29,6 +29,7 @@ pub enum UserNotification {
}, },
} }
impl UserNotification { impl UserNotification {
#[must_use]
pub fn as_json(&self, timestamp: time::OffsetDateTime) -> serde_json::Value { pub fn as_json(&self, timestamp: time::OffsetDateTime) -> serde_json::Value {
let timestamp = timestamp.unix_timestamp().to_string(); let timestamp = timestamp.unix_timestamp().to_string();
match self { match self {

View file

@ -15,32 +15,41 @@ pub struct Positions {
pub next_is_valid: bool, pub next_is_valid: bool,
} }
/// Validates the relative positions of the three branches, resetting next back to main if
/// it has gone astry.
///
/// # Errors
///
/// Will return an `Err` if any of the branches has no commits, or if user intervention is
/// required, or if there is an error resetting the next branch back to main.
#[allow(clippy::result_large_err)] #[allow(clippy::result_large_err)]
pub fn validate_positions( pub fn validate(
open_repository: &dyn OpenRepositoryLike, open_repository: &dyn OpenRepositoryLike,
repo_details: &git::RepoDetails, repo_details: &git::RepoDetails,
repo_config: RepoConfig, repo_config: &RepoConfig,
) -> Result<Positions> { ) -> Result<Positions> {
let main_branch = repo_config.branches().main(); let main_branch = repo_config.branches().main();
let next_branch = repo_config.branches().next(); let next_branch = repo_config.branches().next();
let dev_branch = repo_config.branches().dev(); let dev_branch = repo_config.branches().dev();
// Collect Commit Histories for `main`, `next` and `dev` branches // Collect Commit Histories for `main`, `next` and `dev` branches
open_repository.fetch()?; open_repository.fetch()?;
let commit_histories = get_commit_histories(open_repository, &repo_config)?; let commit_histories = get_commit_histories(open_repository, repo_config)?;
// branch tips // branch tips
let main = let main = commit_histories
commit_histories.main.first().cloned().ok_or_else(|| { .main
Error::NonRetryable(format!("Branch has no commits: {}", main_branch)) .first()
})?; .cloned()
let next = .ok_or_else(|| Error::NonRetryable(format!("Branch has no commits: {main_branch}")))?;
commit_histories.next.first().cloned().ok_or_else(|| { let next = commit_histories
Error::NonRetryable(format!("Branch has no commits: {}", next_branch)) .next
})?; .first()
.cloned()
.ok_or_else(|| Error::NonRetryable(format!("Branch has no commits: {next_branch}")))?;
let dev = commit_histories let dev = commit_histories
.dev .dev
.first() .first()
.cloned() .cloned()
.ok_or_else(|| Error::NonRetryable(format!("Branch has no commits: {}", dev_branch)))?; .ok_or_else(|| Error::NonRetryable(format!("Branch has no commits: {dev_branch}")))?;
// Validations: // Validations:
// Dev must be on main branch, else the USER must rebase it // Dev must be on main branch, else the USER must rebase it
if is_not_based_on(&commit_histories.dev, &main) { if is_not_based_on(&commit_histories.dev, &main) {

View file

@ -8,7 +8,7 @@ use crate::{
Direction, RepositoryLike as _, Direction, RepositoryLike as _,
}, },
tests::{given, then}, tests::{given, then},
validation::positions::validate_positions, validation::positions::validate,
}, },
GitDir, StoragePathType, GitDir, StoragePathType,
}; };
@ -52,7 +52,7 @@ mod repos {
mod positions { mod positions {
use super::*; use super::*;
mod validate_positions { mod validate {
use super::*; use super::*;
@ -75,7 +75,7 @@ mod positions {
); );
let repo_config = given::a_repo_config(); let repo_config = given::a_repo_config();
let result = validate_positions(&*repository, &repo_details, repo_config); let result = validate(&*repository, &repo_details, &repo_config);
println!("{result:?}"); println!("{result:?}");
let_assert!(Err(err) = result, "validate"); let_assert!(Err(err) = result, "validate");
@ -115,7 +115,7 @@ mod positions {
"open repo" "open repo"
); );
let result = validate_positions(&*open_repository, &repo_details, repo_config); let result = validate(&*open_repository, &repo_details, &repo_config);
println!("{result:?}"); println!("{result:?}");
assert!(matches!( assert!(matches!(
@ -154,7 +154,7 @@ mod positions {
"open repo" "open repo"
); );
let result = validate_positions(&*open_repository, &repo_details, repo_config); let result = validate(&*open_repository, &repo_details, &repo_config);
println!("{result:?}"); println!("{result:?}");
assert!(matches!( assert!(matches!(
@ -193,7 +193,7 @@ mod positions {
"open repo" "open repo"
); );
let result = validate_positions(&*open_repository, &repo_details, repo_config); let result = validate(&*open_repository, &repo_details, &repo_config);
println!("{result:?}"); println!("{result:?}");
assert!(matches!( assert!(matches!(
@ -240,7 +240,7 @@ mod positions {
//when //when
let_assert!( let_assert!(
Err(err) = validate_positions(&*open_repository, &repo_details, repo_config), Err(err) = validate(&*open_repository, &repo_details, &repo_config),
"validate" "validate"
); );
@ -325,7 +325,7 @@ mod positions {
//when //when
let_assert!( let_assert!(
Err(err) = validate_positions(&*open_repository, &repo_details, repo_config), Err(err) = validate(&*open_repository, &repo_details, &repo_config),
"validate" "validate"
); );
@ -394,8 +394,7 @@ mod positions {
//when //when
let_assert!( let_assert!(
Err(err) = Err(err) = validate(&*open_repository, &repo_details, &repo_config),
validate_positions(&*open_repository, &repo_details, repo_config.clone()),
"validate" "validate"
); );
@ -482,7 +481,7 @@ mod positions {
//when //when
let_assert!( let_assert!(
Err(err) = validate_positions(&*open_repository, &repo_details, repo_config), Err(err) = validate(&*open_repository, &repo_details, &repo_config),
"validate" "validate"
); );
@ -568,8 +567,7 @@ mod positions {
//when //when
let_assert!( let_assert!(
Ok(positions) = Ok(positions) = validate(&*open_repository, &repo_details, &repo_config),
validate_positions(&*open_repository, &repo_details, repo_config.clone()),
"validate" "validate"
); );
@ -628,8 +626,7 @@ mod positions {
//when //when
let_assert!( let_assert!(
Ok(positions) = Ok(positions) = validate(&*open_repository, &repo_details, &repo_config),
validate_positions(&*open_repository, &repo_details, repo_config.clone()),
"validate" "validate"
); );

View file

@ -38,6 +38,7 @@ macro_rules! newtype {
Self(value.into()) Self(value.into())
} }
#[allow(clippy::missing_const_for_fn)] #[allow(clippy::missing_const_for_fn)]
#[must_use]
pub fn unwrap(self) -> $type { pub fn unwrap(self) -> $type {
self.0 self.0
} }