Compare commits

...

2 commits

Author SHA1 Message Date
Renovate Bot
c11a6137d9 chore(deps): update rust crate bon to v3
All checks were successful
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
ci/woodpecker/pr/cron-docker-builder Pipeline was successful
ci/woodpecker/pr/push-next Pipeline was successful
ci/woodpecker/pr/tag-created Pipeline was successful
ci/woodpecker/pull_request_closed/cron-docker-builder Pipeline was successful
ci/woodpecker/pull_request_closed/push-next Pipeline was successful
ci/woodpecker/pull_request_closed/tag-created Pipeline was successful
2024-11-21 10:46:26 +00:00
Renovate Bot
d3dfedc95b chore(deps): update rust crate kxio to v3
All checks were successful
Rust / build (map[name:nightly]) (push) Successful in 12m43s
Rust / build (map[name:stable]) (push) Successful in 18m7s
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 7m43s
2024-11-21 10:30:38 +00:00
44 changed files with 1312 additions and 1083 deletions

1186
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -60,7 +60,7 @@ async-trait = "0.1"
git-url-parse = "0.4" git-url-parse = "0.4"
# fs/network # fs/network
kxio = { version = "1.2" } kxio = "3.0"
# TOML parsing # TOML parsing
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
@ -81,7 +81,7 @@ time = "0.3"
standardwebhooks = "1.0" standardwebhooks = "1.0"
# boilerplate # boilerplate
bon = "2.0" bon = "3.0"
derive_more = { version = "1.0.0-beta", features = [ derive_more = { version = "1.0.0-beta", features = [
"as_ref", "as_ref",
"constructor", "constructor",

View file

@ -22,7 +22,7 @@ mod tests;
pub struct AlertsActor { pub struct AlertsActor {
shout: Option<Shout>, // config for sending alerts to users shout: Option<Shout>, // config for sending alerts to users
history: History, // record of alerts sent recently (e.g. 24 hours) history: History, // record of alerts sent recently (e.g. 24 hours)
net: kxio::network::Network, net: kxio::net::Net,
} }
impl Actor for AlertsActor { impl Actor for AlertsActor {

View file

@ -1,13 +1,12 @@
// //
use git_next_core::{git::UserNotification, server::OutboundWebhook}; use git_next_core::{git::UserNotification, server::OutboundWebhook};
use kxio::network::{NetRequest, NetUrl, RequestBody, ResponseType};
use secrecy::ExposeSecret as _; use secrecy::ExposeSecret as _;
use standardwebhooks::Webhook; use standardwebhooks::Webhook;
pub(super) async fn send_webhook( pub(super) async fn send_webhook(
user_notification: &UserNotification, user_notification: &UserNotification,
webhook_config: &OutboundWebhook, webhook_config: &OutboundWebhook,
net: &kxio::network::Network, net: &kxio::net::Net,
) { ) {
let Ok(webhook) = let Ok(webhook) =
Webhook::from_bytes(webhook_config.secret().expose_secret().as_bytes().into()) Webhook::from_bytes(webhook_config.secret().expose_secret().as_bytes().into())
@ -22,7 +21,7 @@ async fn do_send_webhook(
user_notification: &UserNotification, user_notification: &UserNotification,
webhook: Webhook, webhook: Webhook,
webhook_config: &OutboundWebhook, webhook_config: &OutboundWebhook,
net: &kxio::network::Network, net: &kxio::net::Net,
) { ) {
let message_id = format!("msg_{}", ulid::Ulid::new()); let message_id = format!("msg_{}", ulid::Ulid::new());
let timestamp = time::OffsetDateTime::now_utc(); let timestamp = time::OffsetDateTime::now_utc();
@ -35,20 +34,17 @@ async fn do_send_webhook(
.sign(&message_id, timestamp, payload.to_string().as_ref()) .sign(&message_id, timestamp, payload.to_string().as_ref())
.expect("signature"); .expect("signature");
tracing::info!(?signature, ""); tracing::info!(?signature, "");
let url = webhook_config.url(); net.post(webhook_config.url())
.body(payload.to_string())
let net_url = NetUrl::new(url.to_string()); .header("webhook-id", message_id)
let request = NetRequest::post(net_url) .header("webhook-timestamp", timestamp.to_string())
.body(RequestBody::Json(payload)) .header("webhook-signature", signature)
.header("webhook-id", &message_id) .send()
.header("webhook-timestamp", &timestamp.to_string()) .await
.header("webhook-signature", &signature) .map_or_else(
.response_type(ResponseType::None) |err| {
.build(); tracing::warn!(?err, "sending webhook");
net.post_json::<()>(request).await.map_or_else( },
|err| { |_| (),
tracing::warn!(?err, "sending webhook"); );
},
|_| (),
);
} }

View file

@ -7,13 +7,13 @@ use git_next_forge_forgejo::ForgeJo;
#[cfg(feature = "github")] #[cfg(feature = "github")]
use git_next_forge_github::Github; use git_next_forge_github::Github;
use kxio::network::Network; use kxio::net::Net;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Forge; pub struct Forge;
impl Forge { impl Forge {
pub fn create(repo_details: RepoDetails, net: Network) -> Box<dyn ForgeLike> { pub fn create(repo_details: RepoDetails, net: Net) -> Box<dyn ForgeLike> {
match repo_details.forge.forge_type() { match repo_details.forge.forge_type() {
#[cfg(feature = "forgejo")] #[cfg(feature = "forgejo")]
git_next_core::ForgeType::ForgeJo => Box::new(ForgeJo::new(repo_details, net)), git_next_core::ForgeType::ForgeJo => Box::new(ForgeJo::new(repo_details, net)),

View file

@ -11,18 +11,18 @@ use git_next_core::{
#[cfg(feature = "forgejo")] #[cfg(feature = "forgejo")]
#[test] #[test]
fn test_forgejo_name() { fn test_forgejo_name() {
let net = Network::new_mock(); let net = kxio::net::mock();
let repo_details = given_repo_details(git_next_core::ForgeType::ForgeJo); let repo_details = given_repo_details(git_next_core::ForgeType::ForgeJo);
let forge = Forge::create(repo_details, net); let forge = Forge::create(repo_details, net.into());
assert_eq!(forge.name(), "forgejo"); assert_eq!(forge.name(), "forgejo");
} }
#[cfg(feature = "github")] #[cfg(feature = "github")]
#[test] #[test]
fn test_github_name() { fn test_github_name() {
let net = Network::new_mock(); let net = kxio::net::mock();
let repo_details = given_repo_details(git_next_core::ForgeType::GitHub); let repo_details = given_repo_details(git_next_core::ForgeType::GitHub);
let forge = Forge::create(repo_details, net); let forge = Forge::create(repo_details, net.into());
assert_eq!(forge.name(), "github"); assert_eq!(forge.name(), "github");
} }

View file

@ -5,12 +5,14 @@ use kxio::fs::FileSystem;
pub fn run(fs: &FileSystem) -> Result<()> { pub fn run(fs: &FileSystem) -> Result<()> {
let pathbuf = fs.base().join(".git-next.toml"); let pathbuf = fs.base().join(".git-next.toml");
if fs if fs
.path_exists(&pathbuf) .path(&pathbuf)
.exists()
.with_context(|| format!("Checking for existing file: {pathbuf:?}"))? .with_context(|| format!("Checking for existing file: {pathbuf:?}"))?
{ {
eprintln!("The configuration file already exists at {pathbuf:?} - not overwritting it.",); eprintln!("The configuration file already exists at {pathbuf:?} - not overwritting it.",);
} else { } else {
fs.file_write(&pathbuf, include_str!("../default.toml")) fs.file(&pathbuf)
.write(include_str!("../default.toml"))
.with_context(|| format!("Writing file: {pathbuf:?}"))?; .with_context(|| format!("Writing file: {pathbuf:?}"))?;
println!("Created a default configuration file at {pathbuf:?}"); println!("Created a default configuration file at {pathbuf:?}");
} }

View file

@ -21,7 +21,7 @@ use std::path::PathBuf;
use clap::Parser; use clap::Parser;
use color_eyre::Result; use color_eyre::Result;
use kxio::{fs, network::Network}; use kxio::{fs, net};
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[clap(version = clap::crate_version!(), author = clap::crate_authors!(), about = clap::crate_description!())] #[clap(version = clap::crate_version!(), author = clap::crate_authors!(), about = clap::crate_description!())]
@ -48,7 +48,7 @@ enum Server {
fn main() -> Result<()> { fn main() -> Result<()> {
let fs = fs::new(PathBuf::default()); let fs = fs::new(PathBuf::default());
let net = Network::new_real(); let net = net::new();
let repository_factory = git::repository::factory::real(); let repository_factory = git::repository::factory::real();
let commands = Commands::parse(); let commands = Commands::parse();

View file

@ -1,7 +1,7 @@
// //
use actix::prelude::*; use actix::prelude::*;
use tracing::{debug, Instrument as _}; use tracing::{debug, warn, Instrument as _};
use crate::{ use crate::{
repo::{ repo::{
@ -26,9 +26,13 @@ impl Handler<CheckCIStatus> for RepoActor {
self.update_tui(RepoUpdate::CheckingCI); self.update_tui(RepoUpdate::CheckingCI);
// get the status - pass, fail, pending (all others map to fail, e.g. error) // get the status - pass, fail, pending (all others map to fail, e.g. error)
async move { async move {
let status = forge.commit_status(&next).await; match forge.commit_status(&next).await {
debug!("got status: {status:?}"); Ok(status) => {
do_send(&addr, ReceiveCIStatus::new((next, status)), log.as_ref()); debug!("got status: {status:?}");
do_send(&addr, ReceiveCIStatus::new((next, status)), log.as_ref());
}
Err(err) => warn!(?err, "fetching commit status"),
}
} }
.in_current_span() .in_current_span()
.into_actor(self) .into_actor(self)

View file

@ -70,6 +70,7 @@ impl Handler<ReceiveCIStatus> for RepoActor {
.into_actor(self) .into_actor(self)
.wait(ctx); .wait(ctx);
} }
Status::Error(_) => todo!(),
} }
} }
} }

View file

@ -6,7 +6,7 @@ use crate::{
server::{actor::messages::RepoUpdate, ServerActor}, server::{actor::messages::RepoUpdate, ServerActor},
}; };
use derive_more::Deref; use derive_more::Deref;
use kxio::network::Network; use kxio::net::Net;
use tracing::{info, instrument, warn, Instrument}; use tracing::{info, instrument, warn, Instrument};
use git_next_core::{ use git_next_core::{
@ -57,7 +57,7 @@ pub struct RepoActor {
last_dev_commit: Option<git::Commit>, last_dev_commit: Option<git::Commit>,
repository_factory: Box<dyn RepositoryFactory>, repository_factory: Box<dyn RepositoryFactory>,
open_repository: Option<Box<dyn OpenRepositoryLike>>, open_repository: Option<Box<dyn OpenRepositoryLike>>,
net: Network, net: Net,
forge: Box<dyn git::ForgeLike>, forge: Box<dyn git::ForgeLike>,
log: Option<ActorLog>, log: Option<ActorLog>,
notify_user_recipient: Option<Recipient<NotifyUser>>, notify_user_recipient: Option<Recipient<NotifyUser>>,
@ -70,7 +70,7 @@ impl RepoActor {
forge: Box<dyn git::ForgeLike>, forge: Box<dyn git::ForgeLike>,
listen_url: ListenUrl, listen_url: ListenUrl,
generation: git::Generation, generation: git::Generation,
net: Network, net: Net,
repository_factory: Box<dyn RepositoryFactory>, repository_factory: Box<dyn RepositoryFactory>,
sleep_duration: std::time::Duration, sleep_duration: std::time::Duration,
notify_user_recipient: Option<Recipient<NotifyUser>>, notify_user_recipient: Option<Recipient<NotifyUser>>,

View file

@ -1,8 +1,8 @@
use git_next_core::server::ListenUrl;
// //
use super::*; use super::*;
use git_next_core::server::ListenUrl;
pub fn has_all_valid_remote_defaults( pub fn has_all_valid_remote_defaults(
open_repository: &mut MockOpenRepositoryLike, open_repository: &mut MockOpenRepositoryLike,
repo_details: &RepoDetails, repo_details: &RepoDetails,
@ -48,8 +48,8 @@ pub fn a_repo_alias() -> RepoAlias {
RepoAlias::new(a_name()) RepoAlias::new(a_name())
} }
pub fn a_network() -> kxio::network::MockNetwork { pub fn a_network() -> kxio::net::MockNet {
kxio::network::MockNetwork::new() kxio::net::mock()
} }
pub fn a_listen_url() -> ListenUrl { pub fn a_listen_url() -> ListenUrl {
@ -152,8 +152,8 @@ pub fn a_commit_sha() -> Sha {
Sha::new(a_name()) Sha::new(a_name())
} }
pub fn a_filesystem() -> kxio::fs::FileSystem { pub fn a_filesystem() -> kxio::fs::TempFileSystem {
kxio::fs::temp().unwrap_or_else(|e| panic!("{}", e)) kxio::fs::temp().expect("temp fs")
} }
pub fn repo_details(fs: &kxio::fs::FileSystem) -> RepoDetails { pub fn repo_details(fs: &kxio::fs::FileSystem) -> RepoDetails {
@ -196,7 +196,7 @@ pub fn a_repo_actor(
repo_details: RepoDetails, repo_details: RepoDetails,
repository_factory: Box<dyn RepositoryFactory>, repository_factory: Box<dyn RepositoryFactory>,
forge: Box<dyn ForgeLike>, forge: Box<dyn ForgeLike>,
net: kxio::network::Network, net: kxio::net::Net,
) -> (RepoActor, ActorLog) { ) -> (RepoActor, ActorLog) {
let listen_url = given::a_listen_url(); let listen_url = given::a_listen_url();
let generation = Generation::default(); let generation = Generation::default();

View file

@ -59,7 +59,7 @@ async fn should_open() -> TestResult {
let _ = opened_ref.write().map(|mut l| l.push(())); let _ = opened_ref.write().map(|mut l| l.push(()));
Ok(Box::new(open_repository)) Ok(Box::new(open_repository))
}); });
fs.dir_create(&repo_details.gitdir)?; fs.dir(&repo_details.gitdir).create()?;
//when //when
let (addr, _log) = when::start_actor(repository_factory, repo_details, given::a_forge()); let (addr, _log) = when::start_actor(repository_factory, repo_details, given::a_forge());
@ -94,7 +94,7 @@ async fn when_server_has_no_repo_config_should_send_load_from_repo() -> TestResu
let mut repository_factory = MockRepositoryFactory::new(); let mut repository_factory = MockRepositoryFactory::new();
expect::open_repository(&mut repository_factory, open_repository); expect::open_repository(&mut repository_factory, open_repository);
fs.dir_create(&repo_details.gitdir)?; fs.dir(&repo_details.gitdir).create()?;
//when //when
let (addr, log) = when::start_actor(repository_factory, repo_details, given::a_forge()); let (addr, log) = when::start_actor(repository_factory, repo_details, given::a_forge());
@ -123,7 +123,7 @@ async fn when_server_has_repo_config_should_send_register_webhook() -> TestResul
let mut repository_factory = MockRepositoryFactory::new(); let mut repository_factory = MockRepositoryFactory::new();
expect::open_repository(&mut repository_factory, open_repository); expect::open_repository(&mut repository_factory, open_repository);
fs.dir_create(&repo_details.gitdir)?; fs.dir(&repo_details.gitdir).create()?;
//when //when
let (addr, log) = when::start_actor(repository_factory, repo_details, given::a_forge()); let (addr, log) = when::start_actor(repository_factory, repo_details, given::a_forge());
@ -156,7 +156,7 @@ async fn opened_repo_with_no_default_push_should_not_proceed() -> TestResult {
let mut repository_factory = MockRepositoryFactory::new(); let mut repository_factory = MockRepositoryFactory::new();
expect::open_repository(&mut repository_factory, open_repository); expect::open_repository(&mut repository_factory, open_repository);
fs.dir_create(&repo_details.gitdir)?; fs.dir(&repo_details.gitdir).create()?;
//when //when
let (addr, log) = when::start_actor(repository_factory, repo_details, given::a_forge()); let (addr, log) = when::start_actor(repository_factory, repo_details, given::a_forge());
@ -180,7 +180,7 @@ async fn opened_repo_with_no_default_fetch_should_not_proceed() -> TestResult {
.return_once(|| Err(git::fetch::Error::NoFetchRemoteFound)); .return_once(|| Err(git::fetch::Error::NoFetchRemoteFound));
let mut repository_factory = MockRepositoryFactory::new(); let mut repository_factory = MockRepositoryFactory::new();
expect::open_repository(&mut repository_factory, open_repository); expect::open_repository(&mut repository_factory, open_repository);
fs.dir_create(&repo_details.gitdir)?; fs.dir(&repo_details.gitdir).create()?;
//when //when
let (addr, log) = when::start_actor(repository_factory, repo_details, given::a_forge()); let (addr, log) = when::start_actor(repository_factory, repo_details, given::a_forge());

View file

@ -30,7 +30,7 @@ pub fn commit_status(forge: &mut MockForgeLike, commit: Commit, status: Status)
commit_status_forge commit_status_forge
.expect_commit_status() .expect_commit_status()
.with(mockall::predicate::eq(commit)) .with(mockall::predicate::eq(commit))
.return_once(|_| status); .return_once(|_| Ok(status));
forge forge
.expect_duplicate() .expect_duplicate()
.return_once(move || Box::new(commit_status_forge)); .return_once(move || Box::new(commit_status_forge));

View file

@ -20,7 +20,7 @@ use git_next_core::{
ForgeAlias, ForgeConfig, GitDir, RepoAlias, ServerRepoConfig, StoragePathType, ForgeAlias, ForgeConfig, GitDir, RepoAlias, ServerRepoConfig, StoragePathType,
}; };
use kxio::{fs::FileSystem, network::Network}; use kxio::{fs::FileSystem, net::Net};
use std::{ use std::{
collections::BTreeMap, collections::BTreeMap,
@ -52,7 +52,7 @@ pub struct ServerActor {
generation: Generation, generation: Generation,
webhook_actor_addr: Option<Addr<WebhookActor>>, webhook_actor_addr: Option<Addr<WebhookActor>>,
fs: FileSystem, fs: FileSystem,
net: Network, net: Net,
alerts: Addr<AlertsActor>, alerts: Addr<AlertsActor>,
repository_factory: Box<dyn RepositoryFactory>, repository_factory: Box<dyn RepositoryFactory>,
sleep_duration: std::time::Duration, sleep_duration: std::time::Duration,
@ -71,7 +71,7 @@ impl Actor for ServerActor {
impl ServerActor { impl ServerActor {
pub fn new( pub fn new(
fs: FileSystem, fs: FileSystem,
net: Network, net: Net,
alerts: Addr<AlertsActor>, alerts: Addr<AlertsActor>,
repo: Box<dyn RepositoryFactory>, repo: Box<dyn RepositoryFactory>,
sleep_duration: std::time::Duration, sleep_duration: std::time::Duration,
@ -100,13 +100,14 @@ impl ServerActor {
for (forge_name, _forge_config) in app_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)? { let path_handle = self.fs.path(&path);
if !self.fs.path_is_dir(&path)? { if path_handle.exists()? {
if !path_handle.is_dir()? {
return Err(Error::ForgeDirIsNotDirectory { path }); return Err(Error::ForgeDirIsNotDirectory { path });
} }
} else { } else {
tracing::info!(%forge_name, ?path, "creating storage"); tracing::info!(%forge_name, ?path_handle, "creating storage");
self.fs.dir_create_all(&path)?; self.fs.dir(&path).create_all()?;
} }
} }
@ -213,7 +214,7 @@ impl ServerActor {
let server_storage = app_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(dir).create() {
error!(?err, ?dir, "Failed to create server storage"); error!(?err, ?dir, "Failed to create server storage");
return None; return None;
} }

View file

@ -5,14 +5,14 @@ use actix::prelude::*;
use crate::alerts::{AlertsActor, History}; use crate::alerts::{AlertsActor, History};
// //
pub fn a_filesystem() -> kxio::fs::FileSystem { pub fn a_filesystem() -> kxio::fs::TempFileSystem {
kxio::fs::temp().unwrap_or_else(|e| panic!("{}", e)) kxio::fs::temp().expect("temp fs")
} }
pub fn a_network() -> kxio::network::MockNetwork { pub fn a_network() -> kxio::net::MockNet {
kxio::network::MockNetwork::new() kxio::net::mock()
} }
pub fn an_alerts_actor(net: kxio::network::Network) -> Addr<AlertsActor> { pub fn an_alerts_actor(net: kxio::net::Net) -> Addr<AlertsActor> {
AlertsActor::new(None, History::new(Duration::from_millis(1)), net).start() AlertsActor::new(None, History::new(Duration::from_millis(1)), net).start()
} }

View file

@ -23,7 +23,7 @@ async fn when_webhook_url_has_trailing_slash_should_not_send() {
let duration = std::time::Duration::from_millis(1); let duration = std::time::Duration::from_millis(1);
// sut // sut
let server = ServerActor::new(fs.clone(), net.into(), alerts, repo, duration); let server = ServerActor::new(fs.as_real(), net.into(), alerts, repo, duration);
// collaborators // collaborators
let listen = Listen::new( let listen = Listen::new(

View file

@ -19,7 +19,7 @@ pub use actor::ServerActor;
use git_next_core::git::RepositoryFactory; use git_next_core::git::RepositoryFactory;
use color_eyre::{eyre::Context, Result}; use color_eyre::{eyre::Context, Result};
use kxio::{fs::FileSystem, network::Network}; use kxio::{fs::FileSystem, net::Net};
use tracing::info; use tracing::info;
use std::{ use std::{
@ -34,12 +34,14 @@ pub fn init(fs: &FileSystem) -> Result<()> {
let file_name = "git-next-server.toml"; let file_name = "git-next-server.toml";
let pathbuf = PathBuf::from(file_name); let pathbuf = PathBuf::from(file_name);
if fs if fs
.path_exists(&pathbuf) .path(&pathbuf)
.exists()
.with_context(|| format!("Checking for existing file: {pathbuf:?}"))? .with_context(|| format!("Checking for existing file: {pathbuf:?}"))?
{ {
eprintln!("The configuration file already exists at {pathbuf:?} - not overwritting it.",); eprintln!("The configuration file already exists at {pathbuf:?} - not overwritting it.",);
} else { } else {
fs.file_write(&pathbuf, include_str!("server-default.toml")) fs.file(&pathbuf)
.write(include_str!("server-default.toml"))
.with_context(|| format!("Writing file: {pathbuf:?}"))?; .with_context(|| format!("Writing file: {pathbuf:?}"))?;
println!("Created a default configuration file at {pathbuf:?}",); println!("Created a default configuration file at {pathbuf:?}",);
} }
@ -50,7 +52,7 @@ pub fn init(fs: &FileSystem) -> Result<()> {
pub fn start( pub fn start(
ui: bool, ui: bool,
fs: FileSystem, fs: FileSystem,
net: Network, net: Net,
repo: Box<dyn RepositoryFactory>, repo: Box<dyn RepositoryFactory>,
sleep_duration: std::time::Duration, sleep_duration: std::time::Duration,
) -> Result<()> { ) -> Result<()> {

View file

@ -6,12 +6,12 @@ mod init {
fn should_not_update_file_if_it_exists() -> TestResult { fn should_not_update_file_if_it_exists() -> TestResult {
let fs = kxio::fs::temp()?; let fs = kxio::fs::temp()?;
let file = fs.base().join(".git-next.toml"); let file = fs.base().join(".git-next.toml");
fs.file_write(&file, "contents")?; fs.file(&file).write("contents")?;
crate::init::run(&fs)?; crate::init::run(&fs)?;
assert_eq!( assert_eq!(
fs.file_read_to_string(&file)?, fs.file(&file).reader()?.to_string(),
"contents", "contents",
"The file has been changed" "The file has been changed"
); );
@ -27,10 +27,10 @@ mod init {
let file = fs.base().join(".git-next.toml"); let file = fs.base().join(".git-next.toml");
assert!(fs.path_exists(&file)?, "The file has not been created"); assert!(fs.path(&file).exists()?, "The file has not been created");
assert_eq!( assert_eq!(
fs.file_read_to_string(&file)?, fs.file(&file).reader()?.to_string(),
include_str!("../default.toml"), include_str!("../default.toml"),
"The file does not match the default template" "The file does not match the default template"
); );
@ -54,7 +54,7 @@ mod file_watcher {
async fn should_not_block_calling_thread() -> TestResult { async fn should_not_block_calling_thread() -> TestResult {
let fs = kxio::fs::temp()?; let fs = kxio::fs::temp()?;
let path = fs.base().join("file"); let path = fs.base().join("file");
fs.file_write(&path, "foo")?; fs.file(&path).write("foo")?;
let listener = Listener; let listener = Listener;
let l_addr = listener.start(); let l_addr = listener.start();

View file

@ -57,7 +57,7 @@ impl AppConfig {
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");
info!(?file, ""); info!(?file, "");
let str = fs.file_read_to_string(&file)?; let str = fs.file(&file).reader()?.to_string();
Ok(toml::from_str(&str)?) Ok(toml::from_str(&str)?)
} }

View file

@ -624,10 +624,8 @@ token = "{forge_token}"
"# "#
); );
println!("{file_contents}"); println!("{file_contents}");
fs.file_write( fs.file(&fs.base().join("git-next-server.toml"))
&fs.base().join("git-next-server.toml"), .write(file_contents.as_str())?;
file_contents.as_str(),
)?;
Ok(()) Ok(())
} }
} }

View file

@ -3,4 +3,5 @@ pub enum Status {
Pass, Pass,
Fail, Fail,
Pending, Pending,
Error(String),
} }

View file

@ -32,7 +32,10 @@ pub trait ForgeLike: std::fmt::Debug + Send + Sync {
) -> git::forge::webhook::Result<webhook::push::Push>; ) -> git::forge::webhook::Result<webhook::push::Push>;
/// Checks the results of any (e.g. CI) status checks for the commit. /// Checks the results of any (e.g. CI) status checks for the commit.
async fn commit_status(&self, commit: &git::Commit) -> git::forge::commit::Status; async fn commit_status(
&self,
commit: &git::Commit,
) -> git::forge::webhook::Result<git::forge::commit::Status>;
// Lists all the webhooks // Lists all the webhooks
async fn list_webhooks( async fn list_webhooks(

View file

@ -4,8 +4,16 @@ pub type Result<T> = core::result::Result<T, Error>;
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum Error { pub enum Error {
#[error("network")] #[error("network")]
Network(#[from] kxio::network::NetworkError), Network(#[from] kxio::net::Error),
#[error("reqwest")]
Reqwest(#[from] kxio::net::RequestError),
// #[error("header")]
// Header(#[from] http::header::InvalidHeaderValue),
// #[error("parse url")]
// UrlParse(#[from] url::ParseError),
#[error("failed to register: {0}")] #[error("failed to register: {0}")]
FailedToRegister(String), FailedToRegister(String),

View file

@ -23,7 +23,7 @@ pub enum Error {
Io(#[from] std::io::Error), Io(#[from] std::io::Error),
#[error("network: {0}")] #[error("network: {0}")]
Network(#[from] kxio::network::NetworkError), Network(#[from] kxio::net::Error),
#[error("fetch: {0}")] #[error("fetch: {0}")]
Fetch(#[from] git::fetch::Error), Fetch(#[from] git::fetch::Error),

View file

@ -142,7 +142,8 @@ impl RepoDetails {
let fs = self.gitdir.as_fs(); let fs = self.gitdir.as_fs();
// load config file // load config file
let config_filename = &self.gitdir.join("config"); let config_filename = &self.gitdir.join("config");
let config_file = fs.file_read_to_string(config_filename)?; let file = fs.file(config_filename);
let config_file = file.reader()?.to_string();
let mut config_lines = config_file let mut config_lines = config_file
.lines() .lines()
.map(ToOwned::to_owned) .map(ToOwned::to_owned)
@ -165,7 +166,7 @@ impl RepoDetails {
} }
tracing::debug!(?config_lines, "updated file"); tracing::debug!(?config_lines, "updated file");
// write config file back out // write config file back out
fs.file_write(config_filename, config_lines.join("\n").as_str())?; file.write(config_lines.join("\n").as_str())?;
Ok(()) Ok(())
} }
} }

View file

@ -172,10 +172,12 @@ impl TestOpenRepository {
fn write_origin(gitdir: &GitDir, fs: &kxio::fs::FileSystem) { fn write_origin(gitdir: &GitDir, fs: &kxio::fs::FileSystem) {
let config_file = fs.base().join(gitdir.to_path_buf()).join(".git/config"); let config_file = fs.base().join(gitdir.to_path_buf()).join(".git/config");
let file = fs.file(&config_file);
#[allow(clippy::expect_used)] #[allow(clippy::expect_used)]
let contents = fs let contents = file
.file_read_to_string(&config_file) .reader()
.expect("read original .git/config"); .expect("read original .git/config")
.to_string();
let updated_contents = format!( let updated_contents = format!(
r#"{contents} r#"{contents}
[remote "origin"] [remote "origin"]
@ -184,7 +186,7 @@ impl TestOpenRepository {
"# "#
); );
#[allow(clippy::expect_used)] #[allow(clippy::expect_used)]
fs.file_write(&config_file, &updated_contents) file.write(&updated_contents)
.expect("write updated .git/config"); .expect("write updated .git/config");
} }
} }

View file

@ -1,3 +1,5 @@
use std::ops::Deref as _;
use crate::CommitCount; use crate::CommitCount;
// //
@ -9,7 +11,7 @@ fn should_return_single_item_in_commit_log_when_not_searching() -> TestResult {
let_assert!(Ok(fs) = kxio::fs::temp()); let_assert!(Ok(fs) = kxio::fs::temp());
let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal); let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal);
let forge_details = given::forge_details(); let forge_details = given::forge_details();
let test_repository = git::repository::test(fs.clone(), forge_details); let test_repository = git::repository::test(fs.deref().clone(), forge_details);
let_assert!(Ok(open_repository) = test_repository.open(&gitdir)); let_assert!(Ok(open_repository) = test_repository.open(&gitdir));
let repo_config = &given::a_repo_config(); let repo_config = &given::a_repo_config();
let branches = repo_config.branches(); let branches = repo_config.branches();
@ -29,7 +31,7 @@ fn should_return_capacity_25_in_commit_log_when_searching_for_garbage() -> TestR
let forge_details = given::forge_details().with_max_dev_commits(Some(CommitCount::from(25))); let forge_details = given::forge_details().with_max_dev_commits(Some(CommitCount::from(25)));
let_assert!(Some(max_dev_commits) = forge_details.max_dev_commits()); let_assert!(Some(max_dev_commits) = forge_details.max_dev_commits());
assert!(**max_dev_commits >= 25); assert!(**max_dev_commits >= 25);
let test_repository = git::repository::test(fs.clone(), forge_details); let test_repository = git::repository::test(fs.deref().clone(), forge_details);
let_assert!(Ok(open_repository) = test_repository.open(&gitdir)); let_assert!(Ok(open_repository) = test_repository.open(&gitdir));
for _ in [0; 25] { for _ in [0; 25] {
then::create_a_commit_on_branch(&fs, &gitdir, &branch_name)?; then::create_a_commit_on_branch(&fs, &gitdir, &branch_name)?;
@ -48,7 +50,7 @@ fn should_return_5_in_commit_log_when_searching_for_5th_item() -> TestResult {
let forge_details = given::forge_details().with_max_dev_commits(Some(CommitCount::from(10))); let forge_details = given::forge_details().with_max_dev_commits(Some(CommitCount::from(10)));
let_assert!(Some(max_dev_commits) = forge_details.max_dev_commits()); let_assert!(Some(max_dev_commits) = forge_details.max_dev_commits());
assert!(**max_dev_commits > 5); assert!(**max_dev_commits > 5);
let test_repository = git::repository::test(fs.clone(), forge_details); let test_repository = git::repository::test(fs.deref().clone(), forge_details);
let_assert!( let_assert!(
Ok(open_repository) = test_repository.open(&gitdir), Ok(open_repository) = test_repository.open(&gitdir),
"open repository" "open repository"

View file

@ -1,3 +1,5 @@
use std::ops::Deref as _;
// //
use super::*; use super::*;
@ -11,7 +13,7 @@ fn should_return_file() -> TestResult {
let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal); let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal);
let forge_details = given::forge_details(); let forge_details = given::forge_details();
let test_repository = git::repository::test(fs.clone(), forge_details); let test_repository = git::repository::test(fs.deref().clone(), forge_details);
let_assert!(Ok(open_repository) = test_repository.open(&gitdir)); let_assert!(Ok(open_repository) = test_repository.open(&gitdir));
then::commit_named_file_to_branch( then::commit_named_file_to_branch(
&file_name, &file_name,
@ -35,7 +37,7 @@ fn should_error_on_missing_file() -> TestResult {
let_assert!(Ok(fs) = kxio::fs::temp()); let_assert!(Ok(fs) = kxio::fs::temp());
let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal); let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal);
let forge_details = given::forge_details(); let forge_details = given::forge_details();
let test_repository = git::repository::test(fs.clone(), forge_details); let test_repository = git::repository::test(fs.deref().clone(), forge_details);
let_assert!(Ok(open_repository) = test_repository.open(&gitdir)); let_assert!(Ok(open_repository) = test_repository.open(&gitdir));
let repo_config = &given::a_repo_config(); let repo_config = &given::a_repo_config();
let branches = repo_config.branches(); let branches = repo_config.branches();

View file

@ -23,7 +23,7 @@ fn open_where_storage_external_auth_matches() -> TestResult {
tracing::debug!(?result, "open"); tracing::debug!(?result, "open");
assert!(result.is_ok()); assert!(result.is_ok());
// verify origin in .git/config matches url // verify origin in .git/config matches url
let config = fs.file_read_to_string(&fs.base().join("config"))?; let config = fs.file(&fs.base().join("config")).reader()?.to_string();
tracing::debug!(config=?config.lines().collect::<Vec<_>>(), "auth"); tracing::debug!(config=?config.lines().collect::<Vec<_>>(), "auth");
assert!(config.lines().any(|line| line.contains(url))); assert!(config.lines().any(|line| line.contains(url)));
Ok(()) Ok(())
@ -60,7 +60,7 @@ fn open_where_storage_external_auth_differs_error() -> TestResult {
)); ));
// verify origin in .git/config is unchanged // verify origin in .git/config is unchanged
let config = fs.file_read_to_string(&fs.base().join("config"))?; let config = fs.file(&fs.base().join("config")).reader()?.to_string();
tracing::debug!(config=?config.lines().collect::<Vec<_>>(), "auth"); tracing::debug!(config=?config.lines().collect::<Vec<_>>(), "auth");
assert!(config.lines().any(|line| line.contains(original_url))); // the original urk assert!(config.lines().any(|line| line.contains(original_url))); // the original urk
Ok(()) Ok(())
@ -86,7 +86,7 @@ fn open_where_storage_internal_auth_matches() -> TestResult {
tracing::debug!(?result, "open"); tracing::debug!(?result, "open");
assert!(result.is_ok()); assert!(result.is_ok());
// verify origin in .git/config matches url // verify origin in .git/config matches url
let config = fs.file_read_to_string(&fs.base().join("config"))?; let config = fs.file(&fs.base().join("config")).reader()?.to_string();
tracing::debug!(config=?config.lines().collect::<Vec<_>>(), "auth"); tracing::debug!(config=?config.lines().collect::<Vec<_>>(), "auth");
assert!( assert!(
config.lines().any(|line| line.contains(url)), config.lines().any(|line| line.contains(url)),
@ -121,7 +121,7 @@ fn open_where_storage_internal_auth_differs_update_config() -> TestResult {
assert!(result.is_ok()); assert!(result.is_ok());
// verify origin in .git/config is unchanged // verify origin in .git/config is unchanged
let config = fs.file_read_to_string(&fs.base().join("config"))?; let config = fs.file(&fs.base().join("config")).reader()?.to_string();
tracing::debug!(config=?config.lines().collect::<Vec<_>>(), "auth"); tracing::debug!(config=?config.lines().collect::<Vec<_>>(), "auth");
assert!( assert!(
config config

View file

@ -306,7 +306,7 @@ pub mod given {
webhook::Push::new(branch, sha.to_string(), message.to_string()) webhook::Push::new(branch, sha.to_string(), message.to_string())
} }
pub fn a_filesystem() -> kxio::fs::FileSystem { pub fn a_filesystem() -> kxio::fs::TempFileSystem {
kxio::fs::temp().unwrap_or_else(|e| panic!("{}", e)) kxio::fs::temp().unwrap_or_else(|e| panic!("{}", e))
} }
@ -337,7 +337,8 @@ pub mod given {
let repo = gix::prepare_clone_bare(url, fs.base()).unwrap(); let repo = gix::prepare_clone_bare(url, fs.base()).unwrap();
repo.persist(); repo.persist();
// load config file // load config file
let config_file = fs.file_read_to_string(&path.join("config")).unwrap(); let file = fs.file(&path.join("config"));
let config_file = file.reader().unwrap().to_string();
// 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"]"#);
@ -345,8 +346,7 @@ pub mod given {
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
fs.file_write(&path.join("config"), config_lines.join("\n").as_str()) file.write(config_lines.join("\n").as_str()).unwrap();
.unwrap();
} }
#[allow(clippy::unwrap_used)] #[allow(clippy::unwrap_used)]
@ -374,7 +374,7 @@ pub mod then {
let pathbuf = PathBuf::from(gitdir); let pathbuf = PathBuf::from(gitdir);
let file = fs.base().join(pathbuf).join(file_name); let file = fs.base().join(pathbuf).join(file_name);
#[allow(clippy::expect_used)] #[allow(clippy::expect_used)]
fs.file_write(&file, contents)?; fs.file(&file).write(contents)?;
// git add ${file} // git add ${file}
git_add_file(gitdir, &file)?; git_add_file(gitdir, &file)?;
// git commit -m"Added ${file}" // git commit -m"Added ${file}"
@ -396,7 +396,7 @@ pub mod then {
let word = given::a_name(); let word = given::a_name();
let pathbuf = PathBuf::from(gitdir); let pathbuf = PathBuf::from(gitdir);
let file = fs.base().join(pathbuf).join(&word); let file = fs.base().join(pathbuf).join(&word);
fs.file_write(&file, &word)?; fs.file(&file).write(&word)?;
// git add ${file} // git add ${file}
git_add_file(gitdir, &file)?; git_add_file(gitdir, &file)?;
// git commit -m"Added ${file}" // git commit -m"Added ${file}"
@ -420,9 +420,9 @@ pub mod then {
let local_branch = gitrefs.join("heads").join(branch_name.to_string().as_str()); let local_branch = gitrefs.join("heads").join(branch_name.to_string().as_str());
let origin_heads = gitrefs.join("remotes").join("origin"); let origin_heads = gitrefs.join("remotes").join("origin");
let remote_branch = origin_heads.join(branch_name.to_string().as_str()); let remote_branch = origin_heads.join(branch_name.to_string().as_str());
let contents = fs.file_read_to_string(&local_branch)?; let contents = fs.file(&local_branch).reader()?.to_string();
fs.dir_create_all(&origin_heads)?; fs.dir(&origin_heads).create_all()?;
fs.file_write(&remote_branch, &contents)?; fs.file(&remote_branch).write(&contents)?;
Ok(()) Ok(())
} }
@ -514,7 +514,7 @@ pub mod then {
.join("refs") .join("refs")
.join("heads") .join("heads")
.join(branch_name.to_string().as_str()); .join(branch_name.to_string().as_str());
let sha = fs.file_read_to_string(&main_ref)?; let sha = fs.file(&main_ref).reader()?.to_string();
Ok(git::commit::Sha::new(sha.trim().to_string())) Ok(git::commit::Sha::new(sha.trim().to_string()))
} }
} }

View file

@ -15,6 +15,8 @@ use crate::{
use assert2::let_assert; use assert2::let_assert;
use std::ops::Deref as _;
mod repos { mod repos {
use super::*; use super::*;
@ -208,12 +210,12 @@ mod positions {
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 forge_details = given::forge_details(); let forge_details = given::forge_details();
let mut test_repository = git::repository::test(fs.clone(), forge_details); let mut test_repository = git::repository::test(fs.deref().clone(), forge_details);
let repo_config = given::a_repo_config(); let repo_config = given::a_repo_config();
test_repository.on_fetch(OnFetch::new( test_repository.on_fetch(OnFetch::new(
repo_config.branches().clone(), repo_config.branches().clone(),
gitdir.clone(), gitdir.clone(),
fs.clone(), fs.deref().clone(),
|branches, gitdir, fs| { |branches, gitdir, fs| {
// /--- 4 next // /--- 4 next
// 0 --- 1 --- 3 main // 0 --- 1 --- 3 main
@ -259,12 +261,12 @@ mod positions {
let_assert!(Ok(fs) = kxio::fs::temp(), "temp fs"); let_assert!(Ok(fs) = kxio::fs::temp(), "temp fs");
let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal); let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal);
let forge_details = given::forge_details(); let forge_details = given::forge_details();
let mut test_repository = git::repository::test(fs.clone(), forge_details); let mut test_repository = git::repository::test(fs.deref().clone(), forge_details);
let repo_config = given::a_repo_config(); let repo_config = given::a_repo_config();
test_repository.on_fetch(OnFetch::new( test_repository.on_fetch(OnFetch::new(
repo_config.branches().clone(), repo_config.branches().clone(),
gitdir.clone(), gitdir.clone(),
fs.clone(), fs.deref().clone(),
|branches, gitdir, fs| { |branches, gitdir, fs| {
// /--- 4 dev // /--- 4 dev
// 0 --- 1 --- 3 main // 0 --- 1 --- 3 main
@ -288,7 +290,7 @@ mod positions {
test_repository.on_fetch(OnFetch::new( test_repository.on_fetch(OnFetch::new(
repo_config.branches().clone(), repo_config.branches().clone(),
gitdir.clone(), gitdir.clone(),
fs.clone(), fs.deref().clone(),
|_branches, _gitdir, _fs| { |_branches, _gitdir, _fs| {
// don't change anything // don't change anything
git::fetch::Result::Ok(()) git::fetch::Result::Ok(())
@ -297,7 +299,7 @@ mod positions {
test_repository.on_push(OnPush::new( test_repository.on_push(OnPush::new(
repo_config.branches().clone(), repo_config.branches().clone(),
gitdir.clone(), gitdir.clone(),
fs.clone(), fs.deref().clone(),
|_repo_details, branch_name, gitref, force, repo_branches, gitdir, fs| { |_repo_details, branch_name, gitref, force, repo_branches, gitdir, fs| {
assert_eq!( assert_eq!(
branch_name, branch_name,
@ -346,12 +348,12 @@ mod positions {
let_assert!(Ok(fs) = kxio::fs::temp(), "temp fs"); let_assert!(Ok(fs) = kxio::fs::temp(), "temp fs");
let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal); let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal);
let forge_details = given::forge_details(); let forge_details = given::forge_details();
let mut test_repository = git::repository::test(fs.clone(), forge_details); let mut test_repository = git::repository::test(fs.deref().clone(), forge_details);
let repo_config = given::a_repo_config(); let repo_config = given::a_repo_config();
test_repository.on_fetch(OnFetch::new( test_repository.on_fetch(OnFetch::new(
repo_config.branches().clone(), repo_config.branches().clone(),
gitdir.clone(), gitdir.clone(),
fs.clone(), fs.deref().clone(),
|branches, gitdir, fs| { |branches, gitdir, fs| {
// /--- 4 dev // /--- 4 dev
// 0 --- 1 --- 3 main // 0 --- 1 --- 3 main
@ -375,7 +377,7 @@ mod positions {
test_repository.on_fetch(OnFetch::new( test_repository.on_fetch(OnFetch::new(
repo_config.branches().clone(), repo_config.branches().clone(),
gitdir.clone(), gitdir.clone(),
fs.clone(), fs.deref().clone(),
|_branches, _gitdir, _fs| { |_branches, _gitdir, _fs| {
// don't change anything // don't change anything
git::fetch::Result::Ok(()) git::fetch::Result::Ok(())
@ -384,7 +386,7 @@ mod positions {
test_repository.on_push(OnPush::new( test_repository.on_push(OnPush::new(
repo_config.branches().clone(), repo_config.branches().clone(),
gitdir.clone(), gitdir.clone(),
fs.clone(), fs.deref().clone(),
|_repo_details, _branch_name, _gitref, _force, _repo_branches, _gitdir, _fs| { |_repo_details, _branch_name, _gitref, _force, _repo_branches, _gitdir, _fs| {
git::push::Result::Err(git::push::Error::Lock) git::push::Result::Err(git::push::Error::Lock)
}, },
@ -420,12 +422,12 @@ mod positions {
let_assert!(Ok(fs) = kxio::fs::temp(), "temp fs"); let_assert!(Ok(fs) = kxio::fs::temp(), "temp fs");
let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal); let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal);
let forge_details = given::forge_details(); let forge_details = given::forge_details();
let mut test_repository = git::repository::test(fs.clone(), forge_details); let mut test_repository = git::repository::test(fs.deref().clone(), forge_details);
let repo_config = given::a_repo_config(); let repo_config = given::a_repo_config();
test_repository.on_fetch(OnFetch::new( test_repository.on_fetch(OnFetch::new(
repo_config.branches().clone(), repo_config.branches().clone(),
gitdir.clone(), gitdir.clone(),
fs.clone(), fs.deref().clone(),
|branches, gitdir, fs| { |branches, gitdir, fs| {
// /--- 3 next // /--- 3 next
// 0 --- 1 main & dev // 0 --- 1 main & dev
@ -446,7 +448,7 @@ mod positions {
test_repository.on_fetch(OnFetch::new( test_repository.on_fetch(OnFetch::new(
repo_config.branches().clone(), repo_config.branches().clone(),
gitdir.clone(), gitdir.clone(),
fs.clone(), fs.deref().clone(),
|_branches, _gitdir, _fs| { |_branches, _gitdir, _fs| {
// don't change anything // don't change anything
git::fetch::Result::Ok(()) git::fetch::Result::Ok(())
@ -455,7 +457,7 @@ mod positions {
test_repository.on_push(OnPush::new( test_repository.on_push(OnPush::new(
repo_config.branches().clone(), repo_config.branches().clone(),
gitdir.clone(), gitdir.clone(),
fs.clone(), fs.deref().clone(),
|_repo_details, branch_name, gitref, force, repo_branches, gitdir, fs| { |_repo_details, branch_name, gitref, force, repo_branches, gitdir, fs| {
assert_eq!( assert_eq!(
branch_name, branch_name,
@ -506,12 +508,12 @@ mod positions {
let_assert!(Ok(fs) = kxio::fs::temp(), "temp fs"); let_assert!(Ok(fs) = kxio::fs::temp(), "temp fs");
let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal); let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal);
let forge_details = given::forge_details(); let forge_details = given::forge_details();
let mut test_repository = git::repository::test(fs.clone(), forge_details); let mut test_repository = git::repository::test(fs.deref().clone(), forge_details);
let repo_config = given::a_repo_config(); let repo_config = given::a_repo_config();
test_repository.on_fetch(OnFetch::new( test_repository.on_fetch(OnFetch::new(
repo_config.branches().clone(), repo_config.branches().clone(),
gitdir.clone(), gitdir.clone(),
fs.clone(), fs.deref().clone(),
|branches, gitdir, fs| { |branches, gitdir, fs| {
// /--- 3 next // /--- 3 next
// 0 --- 1 main // 0 --- 1 main
@ -533,7 +535,7 @@ mod positions {
test_repository.on_fetch(OnFetch::new( test_repository.on_fetch(OnFetch::new(
repo_config.branches().clone(), repo_config.branches().clone(),
gitdir.clone(), gitdir.clone(),
fs.clone(), fs.deref().clone(),
|_branches, _gitdir, _fs| { |_branches, _gitdir, _fs| {
// don't change anything // don't change anything
git::fetch::Result::Ok(()) git::fetch::Result::Ok(())
@ -542,7 +544,7 @@ mod positions {
test_repository.on_push(OnPush::new( test_repository.on_push(OnPush::new(
repo_config.branches().clone(), repo_config.branches().clone(),
gitdir.clone(), gitdir.clone(),
fs.clone(), fs.deref().clone(),
|_repo_details, branch_name, gitref, force, repo_branches, gitdir, fs| { |_repo_details, branch_name, gitref, force, repo_branches, gitdir, fs| {
assert_eq!( assert_eq!(
branch_name, branch_name,
@ -604,12 +606,12 @@ mod positions {
let_assert!(Ok(fs) = kxio::fs::temp(), "temp fs"); let_assert!(Ok(fs) = kxio::fs::temp(), "temp fs");
let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal); let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal);
let forge_details = given::forge_details(); let forge_details = given::forge_details();
let mut test_repository = git::repository::test(fs.clone(), forge_details); let mut test_repository = git::repository::test(fs.deref().clone(), forge_details);
let repo_config = given::a_repo_config(); let repo_config = given::a_repo_config();
test_repository.on_fetch(OnFetch::new( test_repository.on_fetch(OnFetch::new(
repo_config.branches().clone(), repo_config.branches().clone(),
gitdir.clone(), gitdir.clone(),
fs.clone(), fs.deref().clone(),
|branches, gitdir, fs| { |branches, gitdir, fs| {
// 0 --- 1 main // 0 --- 1 main
// \--- 2 next // \--- 2 next

View file

@ -13,17 +13,16 @@ use git_next_core::{
ForgeNotification, RegisteredWebhook, WebhookAuth, WebhookId, ForgeNotification, RegisteredWebhook, WebhookAuth, WebhookId,
}; };
use kxio::network::{self, Network}; use kxio::net::Net;
use tracing::warn;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct ForgeJo { pub struct ForgeJo {
repo_details: git::RepoDetails, repo_details: git::RepoDetails,
net: Network, net: Net,
} }
impl ForgeJo { impl ForgeJo {
#[must_use] #[must_use]
pub const fn new(repo_details: git::RepoDetails, net: Network) -> Self { pub const fn new(repo_details: git::RepoDetails, net: Net) -> Self {
Self { repo_details, net } Self { repo_details, net }
} }
} }
@ -52,45 +51,28 @@ impl git::ForgeLike for ForgeJo {
webhook::parse_body(body) webhook::parse_body(body)
} }
async fn commit_status(&self, commit: &git::Commit) -> Status { async fn commit_status(&self, commit: &git::Commit) -> git::forge::webhook::Result<Status> {
let repo_details = &self.repo_details; let repo_details = &self.repo_details;
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 api_token = &repo_details.forge.token(); let api_token = &repo_details.forge.token();
use secrecy::ExposeSecret; use secrecy::ExposeSecret;
let token = api_token.expose_secret(); let token = api_token.expose_secret();
let url = network::NetUrl::new(format!( let url = format!(
"https://{hostname}/api/v1/repos/{repo_path}/commits/{commit}/status?token={token}" "https://{hostname}/api/v1/repos/{repo_path}/commits/{commit}/status?token={token}"
));
let request = network::NetRequest::new(
network::RequestMethod::Get,
url,
network::NetRequestHeaders::new(),
network::RequestBody::None,
network::ResponseType::Json,
None,
network::NetRequestLogging::None,
); );
let result = self.net.get::<CombinedStatus>(request).await;
match result { let Ok(response) = self.net.get(url).send().await else {
Ok(response) => match response.response_body() { return Ok(Status::Pending);
Some(status) => match status.state { };
ForgejoState::Success => Status::Pass, let combined_status = response.json::<CombinedStatus>().await.unwrap_or_default();
ForgejoState::Pending | ForgejoState::Blank => Status::Pending, eprintln!("combined_status: {:?}", combined_status);
ForgejoState::Failure | ForgejoState::Error => Status::Fail, let status = match combined_status.state {
}, ForgejoState::Success => Status::Pass,
None => { ForgejoState::Pending | ForgejoState::Blank => Status::Pending,
#[cfg(not(tarpaulin_include))] ForgejoState::Failure | ForgejoState::Error => Status::Fail,
unreachable!(); // response.response_body() is always Some when };
// request responseType::Json Ok(status)
}
},
Err(e) => {
warn!(?e, "Failed to get commit status");
Status::Pending // assume issue is transient and allow retry
}
}
} }
async fn list_webhooks( async fn list_webhooks(
@ -112,16 +94,17 @@ impl git::ForgeLike for ForgeJo {
} }
} }
#[derive(Debug, serde::Deserialize)] #[derive(Debug, Default, serde::Deserialize)]
struct CombinedStatus { struct CombinedStatus {
pub state: ForgejoState, pub state: ForgejoState,
} }
#[derive(Debug, serde::Deserialize)] #[derive(Debug, Default, serde::Deserialize)]
enum ForgejoState { enum ForgejoState {
#[serde(rename = "success")] #[serde(rename = "success")]
Success, Success,
#[serde(rename = "pending")] #[serde(rename = "pending")]
#[default]
Pending, Pending,
#[serde(rename = "failure")] #[serde(rename = "failure")]
Failure, Failure,

View file

@ -1,14 +1,16 @@
// //
#![allow(clippy::expect_used)] // used with mock net
use crate::ForgeJo; use crate::ForgeJo;
use git_next_core::{ use git_next_core::{
git::{self, forge::commit::Status, ForgeLike as _}, git::{self, forge::commit::Status, ForgeLike as _},
server::ListenUrl, server::{ListenUrl, RepoListenUrl},
BranchName, ForgeAlias, ForgeConfig, ForgeNotification, ForgeType, GitDir, Hostname, RepoAlias, BranchName, ForgeAlias, ForgeConfig, ForgeNotification, ForgeType, GitDir, Hostname, RepoAlias,
RepoBranches, RepoPath, ServerRepoConfig, StoragePathType, WebhookAuth, WebhookId, RepoBranches, RepoPath, ServerRepoConfig, StoragePathType, WebhookAuth, WebhookId,
}; };
use assert2::let_assert; use assert2::let_assert;
use kxio::network::{self, MockNetwork, StatusCode}; use kxio::net::{MockNet, StatusCode};
use secrecy::ExposeSecret; use secrecy::ExposeSecret;
use serde::Serialize; use serde::Serialize;
use serde_json::json; use serde_json::json;
@ -116,77 +118,105 @@ mod forgejo {
let fs = given::a_filesystem(); let fs = given::a_filesystem();
let repo_details = given::repo_details(&fs); let repo_details = given::repo_details(&fs);
let commit = given::a_commit(); let commit = given::a_commit();
let mut net = given::a_network(); let net = given::a_network();
given::a_commit_state("success", &mut net, &repo_details, &commit); given::a_commit_state("success", &net, &repo_details, &commit);
let forge = given::a_forgejo_forge(&repo_details, net); let forge = given::a_forgejo_forge(&repo_details, net);
assert_eq!(forge.commit_status(&commit).await, Status::Pass); assert_eq!(
forge.commit_status(&commit).await.expect("status"),
Status::Pass
);
} }
#[tokio::test] #[tokio::test]
async fn should_return_pending_for_pending() { async fn should_return_pending_for_pending() {
let fs = given::a_filesystem(); let fs = given::a_filesystem();
let repo_details = given::repo_details(&fs); let repo_details = given::repo_details(&fs);
let commit = given::a_commit(); let commit = given::a_commit();
let mut net = given::a_network(); let net = given::a_network();
given::a_commit_state("pending", &mut net, &repo_details, &commit); given::a_commit_state("pending", &net, &repo_details, &commit);
let forge = given::a_forgejo_forge(&repo_details, net); let forge = given::a_forgejo_forge(&repo_details, net);
assert_eq!(forge.commit_status(&commit).await, Status::Pending); assert_eq!(
forge.commit_status(&commit).await.expect("status"),
Status::Pending
);
} }
#[tokio::test] #[tokio::test]
async fn should_return_fail_for_failure() { async fn should_return_fail_for_failure() {
let fs = given::a_filesystem(); let fs = given::a_filesystem();
let repo_details = given::repo_details(&fs); let repo_details = given::repo_details(&fs);
let commit = given::a_commit(); let commit = given::a_commit();
let mut net = given::a_network(); let net = given::a_network();
given::a_commit_state("failure", &mut net, &repo_details, &commit); given::a_commit_state("failure", &net, &repo_details, &commit);
let forge = given::a_forgejo_forge(&repo_details, net); let forge = given::a_forgejo_forge(&repo_details, net);
assert_eq!(forge.commit_status(&commit).await, Status::Fail); assert_eq!(
forge.commit_status(&commit).await.expect("status"),
Status::Fail
);
} }
#[tokio::test] #[tokio::test]
async fn should_return_fail_for_error() { async fn should_return_fail_for_error() {
let fs = given::a_filesystem(); let fs = given::a_filesystem();
let repo_details = given::repo_details(&fs); let repo_details = given::repo_details(&fs);
let commit = given::a_commit(); let commit = given::a_commit();
let mut net = given::a_network(); let net = given::a_network();
given::a_commit_state("error", &mut net, &repo_details, &commit); given::a_commit_state("error", &net, &repo_details, &commit);
let forge = given::a_forgejo_forge(&repo_details, net); let forge = given::a_forgejo_forge(&repo_details, net);
assert_eq!(forge.commit_status(&commit).await, Status::Fail); assert_eq!(
forge.commit_status(&commit).await.expect("status"),
Status::Fail
);
} }
#[tokio::test] #[tokio::test]
async fn should_return_pending_for_blank() { async fn should_return_pending_for_blank() {
let fs = given::a_filesystem(); let fs = given::a_filesystem();
let repo_details = given::repo_details(&fs); let repo_details = given::repo_details(&fs);
let commit = given::a_commit(); let commit = given::a_commit();
let mut net = given::a_network(); let net = given::a_network();
given::a_commit_state("", &mut net, &repo_details, &commit); given::a_commit_state("", &net, &repo_details, &commit);
let forge = given::a_forgejo_forge(&repo_details, net); let forge = given::a_forgejo_forge(&repo_details, net);
assert_eq!(forge.commit_status(&commit).await, Status::Pending); assert_eq!(
forge.commit_status(&commit).await.expect("status"),
Status::Pending
);
} }
#[tokio::test] #[tokio::test]
async fn should_return_pending_for_no_statuses() { async fn should_return_pending_for_no_statuses() {
let fs = given::a_filesystem(); let fs = given::a_filesystem();
let repo_details = given::repo_details(&fs); let repo_details = given::repo_details(&fs);
let commit = given::a_commit(); let commit = given::a_commit();
let mut net = given::a_network(); let net = given::a_network();
net.add_get_response( net.on()
&given::a_commit_status_url(&repo_details, &commit), .get(given::a_commit_status_url(&repo_details, &commit))
StatusCode::OK, .respond(StatusCode::OK)
"", .body(
); json!({
"state": "" // blank => Pending
})
.to_string(),
)
.expect("mock");
let forge = given::a_forgejo_forge(&repo_details, net); let forge = given::a_forgejo_forge(&repo_details, net);
assert_eq!(forge.commit_status(&commit).await, Status::Pending); assert_eq!(
forge.commit_status(&commit).await.expect("status"),
Status::Pending
);
} }
#[tokio::test] #[tokio::test]
async fn should_return_pending_for_network_error() { async fn should_return_pending_for_network_error() {
let fs = given::a_filesystem(); let fs = given::a_filesystem();
let repo_details = given::repo_details(&fs); let repo_details = given::repo_details(&fs);
let commit = given::a_commit(); let commit = given::a_commit();
let mut net = given::a_network(); let mock_net = given::a_network();
net.add_get_error( mock_net
&given::a_commit_status_url(&repo_details, &commit), .on()
"boom today", .get(given::a_commit_status_url(&repo_details, &commit))
.respond(StatusCode::INTERNAL_SERVER_ERROR)
.body("book today")
.expect("mock");
let forge = given::a_forgejo_forge(&repo_details, mock_net);
assert_eq!(
forge.commit_status(&commit).await.expect("status"),
Status::Pending
); );
let forge = given::a_forgejo_forge(&repo_details, net);
assert_eq!(forge.commit_status(&commit).await, Status::Pending);
} }
} }
@ -246,13 +276,15 @@ mod forgejo {
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 token = repo_details.forge.token().expose_secret(); let token = repo_details.forge.token().expose_secret();
let mut net = given::a_network(); let net = given::a_network();
net.add_get_error( net.on()
format!("https://{hostname}/api/v1/repos/{repo_path}/hooks?page=1&token={token}") .get(format!(
.as_str(), "https://{hostname}/api/v1/repos/{repo_path}/hooks?page=1&token={token}"
"error_message", ))
); .respond(StatusCode::INTERNAL_SERVER_ERROR)
.body("error_message")
.expect("mock");
let forge = given::a_forgejo_forge(&repo_details, net); let forge = given::a_forgejo_forge(&repo_details, net);
@ -271,16 +303,15 @@ mod forgejo {
let repo_path = &repo_details.repo_path; let repo_path = &repo_details.repo_path;
let token = repo_details.forge.token().expose_secret(); let token = repo_details.forge.token().expose_secret();
let webhook_id = given::a_webhook_id(); let webhook_id = given::a_webhook_id();
let mut net = given::a_network(); let net = given::a_network();
net.add_delete_response( net.on()
format!( .delete(format!(
"https://{hostname}/api/v1/repos/{repo_path}/hooks/{webhook_id}?token={token}" "https://{hostname}/api/v1/repos/{repo_path}/hooks/{webhook_id}?token={token}"
) ))
.as_str(), .respond(StatusCode::OK)
StatusCode::OK, .body("")
"", .expect("mock");
);
let forge = given::a_forgejo_forge(&repo_details, net); let forge = given::a_forgejo_forge(&repo_details, net);
@ -295,15 +326,15 @@ mod forgejo {
let repo_path = &repo_details.repo_path; let repo_path = &repo_details.repo_path;
let token = repo_details.forge.token().expose_secret(); let token = repo_details.forge.token().expose_secret();
let webhook_id = given::a_webhook_id(); let webhook_id = given::a_webhook_id();
let mut net = given::a_network(); let net = given::a_network();
net.add_delete_error( net.on()
format!( .delete(format!(
"https://{hostname}/api/v1/repos/{repo_path}/hooks/{webhook_id}?token={token}" "https://{hostname}/api/v1/repos/{repo_path}/hooks/{webhook_id}?token={token}"
) ))
.as_str(), .respond(StatusCode::INTERNAL_SERVER_ERROR)
"error", .body("error")
); .expect("mock");
let forge = given::a_forgejo_forge(&repo_details, net); let forge = given::a_forgejo_forge(&repo_details, net);
@ -343,11 +374,13 @@ mod forgejo {
with::get_webhooks_by_page(1, &[], &mut args); with::get_webhooks_by_page(1, &[], &mut args);
// register the webhook will succeed // register the webhook will succeed
let webhook_id = given::a_forgejo_webhook_id(); let webhook_id = given::a_forgejo_webhook_id();
net.add_post_response( net.on()
format!("https://{hostname}/api/v1/repos/{repo_path}/hooks?token={token}").as_str(), .post(format!(
StatusCode::OK, "https://{hostname}/api/v1/repos/{repo_path}/hooks?token={token}"
json!({"id": webhook_id, "config":{}}).to_string().as_str(), ))
); .respond(StatusCode::OK)
.body(json!({"id": webhook_id, "config":{}}).to_string())
.expect("mock");
let forge = given::a_forgejo_forge(&repo_details, net); let forge = given::a_forgejo_forge(&repo_details, net);
@ -415,11 +448,13 @@ mod forgejo {
with::unregister_webhook(&hooks[1], &mut args); with::unregister_webhook(&hooks[1], &mut args);
// register the webhook will succeed // register the webhook will succeed
let webhook_id = given::a_forgejo_webhook_id(); let webhook_id = given::a_forgejo_webhook_id();
net.add_post_response( net.on()
format!("https://{hostname}/api/v1/repos/{repo_path}/hooks?token={token}").as_str(), .post(format!(
StatusCode::OK, "https://{hostname}/api/v1/repos/{repo_path}/hooks?token={token}"
json!({"id": webhook_id, "config":{}}).to_string().as_str(), ))
); .respond(StatusCode::OK)
.body(json!({"id": webhook_id, "config":{}}).to_string())
.expect("mock");
let forge = given::a_forgejo_forge(&repo_details, net); let forge = given::a_forgejo_forge(&repo_details, net);
@ -454,17 +489,19 @@ mod forgejo {
// there are no existing matching webhooks // there are no existing matching webhooks
with::get_webhooks_by_page(1, &[], &mut args); with::get_webhooks_by_page(1, &[], &mut args);
// register the webhook will return empty response // register the webhook will return empty response
net.add_post_response( net.on()
format!("https://{hostname}/api/v1/repos/{repo_path}/hooks?token={token}").as_str(), .post(format!(
StatusCode::OK, "https://{hostname}/api/v1/repos/{repo_path}/hooks?token={token}"
json!({}).to_string().as_str(), // empty response ))
); .respond(StatusCode::OK)
.body(json!({}).to_string()) // empty response)
.expect("mock");
let forge = given::a_forgejo_forge(&repo_details, net); let forge = given::a_forgejo_forge(&repo_details, net);
let_assert!(Err(err) = forge.register_webhook(&repo_listen_url).await); let_assert!(Err(err) = forge.register_webhook(&repo_listen_url).await);
assert!( assert!(
matches!(err, git::forge::webhook::Error::FailedToRegister(_)), matches!(err, git::forge::webhook::Error::NetworkResponseEmpty),
"{err:?}" "{err:?}"
); );
} }
@ -493,10 +530,13 @@ mod forgejo {
// there are no existing matching webhooks // there are no existing matching webhooks
with::get_webhooks_by_page(1, &[], &mut args); with::get_webhooks_by_page(1, &[], &mut args);
// register the webhook will return empty response // register the webhook will return empty response
net.add_post_error( net.on()
format!("https://{hostname}/api/v1/repos/{repo_path}/hooks?token={token}").as_str(), .post(format!(
"error", "https://{hostname}/api/v1/repos/{repo_path}/hooks?token={token}"
); ))
.respond(StatusCode::INTERNAL_SERVER_ERROR)
.body("error")
.expect("mock");
let forge = given::a_forgejo_forge(&repo_details, net); let forge = given::a_forgejo_forge(&repo_details, net);
@ -508,7 +548,6 @@ mod forgejo {
} }
} }
mod with { mod with {
use git_next_core::server::RepoListenUrl;
use super::*; use super::*;
@ -520,31 +559,31 @@ mod forgejo {
let hostname = args.hostname; let hostname = args.hostname;
let repo_path = args.repo_path; let repo_path = args.repo_path;
let token = args.token; let token = args.token;
args.net.add_get_response( args.net
format!( .on()
.get(format!(
"https://{hostname}/api/v1/repos/{repo_path}/hooks?page={page}&token={token}" "https://{hostname}/api/v1/repos/{repo_path}/hooks?page={page}&token={token}"
) ))
.as_str(), .respond(StatusCode::OK)
StatusCode::OK, .body(json!(response).to_string())
json!(response).to_string().as_str(), .expect("mock");
);
} }
pub fn unregister_webhook(hook1: &ReturnedWebhook, args: &mut WebhookArgs) { pub fn unregister_webhook(hook1: &ReturnedWebhook, args: &mut WebhookArgs) {
let webhook_id = hook1.id; let webhook_id = hook1.id;
let hostname = args.hostname; let hostname = args.hostname;
let repo_path = args.repo_path; let repo_path = args.repo_path;
let token = args.token; let token = args.token;
args.net.add_delete_response( args.net
format!( .on()
.delete(format!(
"https://{hostname}/api/v1/repos/{repo_path}/hooks/{webhook_id}?token={token}" "https://{hostname}/api/v1/repos/{repo_path}/hooks/{webhook_id}?token={token}"
) ))
.as_str(), .respond(StatusCode::OK)
StatusCode::OK, .body("")
"", .expect("mock");
);
} }
pub struct WebhookArgs<'a> { pub struct WebhookArgs<'a> {
pub net: &'a mut network::MockNetwork, pub net: &'a MockNet,
pub hostname: &'a Hostname, pub hostname: &'a Hostname,
pub repo_path: &'a RepoPath, pub repo_path: &'a RepoPath,
pub token: &'a str, pub token: &'a str,
@ -573,21 +612,21 @@ mod forgejo {
use git::RepoDetails; use git::RepoDetails;
use git_next_core::server::RepoListenUrl; use git_next_core::server::RepoListenUrl;
use kxio::net::{MockNet, StatusCode};
use super::*; use super::*;
pub fn a_commit_state( pub fn a_commit_state(
state: impl AsRef<str>, state: impl AsRef<str>,
net: &mut MockNetwork, net: &MockNet,
repo_details: &git::RepoDetails, repo_details: &git::RepoDetails,
commit: &git::Commit, commit: &git::Commit,
) { ) {
let response = json!({"state":state.as_ref()}); net.on()
net.add_get_response( .get(a_commit_status_url(repo_details, commit))
a_commit_status_url(repo_details, commit).as_str(), .respond(StatusCode::OK)
StatusCode::OK, .body((json!({"state":state.as_ref()})).to_string())
response.to_string().as_str(), .expect("mock");
);
} }
pub fn a_commit_status_url( pub fn a_commit_status_url(
@ -644,9 +683,10 @@ mod forgejo {
pub fn a_forgejo_forge( pub fn a_forgejo_forge(
repo_details: &git::RepoDetails, repo_details: &git::RepoDetails,
net: impl Into<kxio::network::Network>, net: impl Into<kxio::net::Net>,
) -> ForgeJo { ) -> ForgeJo {
ForgeJo::new(repo_details.clone(), net.into()) let net: kxio::net::Net = net.into();
ForgeJo::new(repo_details.clone(), net)
} }
pub fn a_forgejo_webhook_id() -> i64 { pub fn a_forgejo_webhook_id() -> i64 {
@ -674,8 +714,8 @@ mod forgejo {
RepoAlias::new(a_name()) RepoAlias::new(a_name())
} }
pub fn a_network() -> kxio::network::MockNetwork { pub fn a_network() -> kxio::net::MockNet {
kxio::network::MockNetwork::new() kxio::net::mock()
} }
pub fn a_name() -> String { pub fn a_name() -> String {
@ -758,8 +798,8 @@ mod forgejo {
git::commit::Sha::new(a_name()) git::commit::Sha::new(a_name())
} }
pub fn a_filesystem() -> kxio::fs::FileSystem { pub fn a_filesystem() -> kxio::fs::TempFileSystem {
kxio::fs::temp().unwrap_or_else(|e| panic!("{}", e)) kxio::fs::temp().expect("temp fs")
} }
pub fn repo_details(fs: &kxio::fs::FileSystem) -> git::RepoDetails { pub fn repo_details(fs: &kxio::fs::FileSystem) -> git::RepoDetails {

View file

@ -1,14 +1,13 @@
// //
use git_next_core::{git, server::RepoListenUrl, WebhookId}; use git_next_core::{git, server::RepoListenUrl, WebhookId};
use kxio::net::Net;
use kxio::network;
use crate::webhook::Hook; use crate::webhook::Hook;
pub async fn list( pub async fn list(
repo_details: &git::RepoDetails, repo_details: &git::RepoDetails,
repo_listen_url: &RepoListenUrl, repo_listen_url: &RepoListenUrl,
net: &network::Network, net: &Net,
) -> git::forge::webhook::Result<Vec<WebhookId>> { ) -> git::forge::webhook::Result<Vec<WebhookId>> {
let mut ids: Vec<WebhookId> = vec![]; let mut ids: Vec<WebhookId> = vec![];
let hostname = &repo_details.forge.hostname(); let hostname = &repo_details.forge.hostname();
@ -17,22 +16,15 @@ pub async fn list(
loop { loop {
use secrecy::ExposeSecret; use secrecy::ExposeSecret;
let token = &repo_details.forge.token().expose_secret(); let token = &repo_details.forge.token().expose_secret();
let url = match net
format!("https://{hostname}/api/v1/repos/{repo_path}/hooks?page={page}&token={token}"); .get(format!(
let net_url = network::NetUrl::new(url); "https://{hostname}/api/v1/repos/{repo_path}/hooks?page={page}&token={token}"
let request = network::NetRequest::new( ))
network::RequestMethod::Get, .send()
net_url, .await
network::NetRequestHeaders::new(), {
network::RequestBody::None,
network::ResponseType::Json,
None,
network::NetRequestLogging::None,
);
let result = net.get::<Vec<Hook>>(request).await;
match result {
Ok(response) => { Ok(response) => {
if let Some(list) = response.response_body() { if let Ok(list) = response.json::<Vec<Hook>>().await {
if list.is_empty() { if list.is_empty() {
return Ok(ids); return Ok(ids);
} }

View file

@ -1,8 +1,9 @@
// //
use git_next_core::{git, server::RepoListenUrl, RegisteredWebhook, WebhookAuth, WebhookId}; use git_next_core::{git, server::RepoListenUrl, RegisteredWebhook, WebhookAuth, WebhookId};
use kxio::network; use kxio::net::Net;
use secrecy::ExposeSecret as _; use secrecy::ExposeSecret as _;
use serde_json::json;
use tracing::{info, instrument, warn}; use tracing::{info, instrument, warn};
use crate::webhook; use crate::webhook;
@ -12,7 +13,7 @@ use crate::webhook::Hook;
pub async fn register( pub async fn register(
repo_details: &git::RepoDetails, repo_details: &git::RepoDetails,
repo_listen_url: &RepoListenUrl, repo_listen_url: &RepoListenUrl,
net: &network::Network, net: &Net,
) -> git::forge::webhook::Result<RegisteredWebhook> { ) -> git::forge::webhook::Result<RegisteredWebhook> {
let Some(repo_config) = repo_details.repo_config.clone() else { let Some(repo_config) = repo_details.repo_config.clone() else {
return Err(git::forge::webhook::Error::NoRepoConfig); return Err(git::forge::webhook::Error::NoRepoConfig);
@ -27,35 +28,27 @@ pub async fn register(
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 token = repo_details.forge.token().expose_secret(); let token = repo_details.forge.token().expose_secret();
let url = network::NetUrl::new(format!( let url = format!("https://{hostname}/api/v1/repos/{repo_path}/hooks?token={token}");
"https://{hostname}/api/v1/repos/{repo_path}/hooks?token={token}"
));
let headers = network::NetRequestHeaders::new().with("Content-Type", "application/json");
let authorisation = WebhookAuth::generate(); let authorisation = WebhookAuth::generate();
let body = network::json!({ match net
"active": true, .post(url)
"authorization_header": authorisation.header_value(), .header("Content-Type", "application/json")
"branch_filter": format!("{{{},{},{}}}", repo_config.branches().main(), repo_config.branches().next(), repo_config.branches().dev()), .body(json!({
"config": { "active": true,
"content_type": "json", "authorization_header": authorisation.header_value(),
"url": repo_listen_url.to_string(), "branch_filter": format!("{{{},{},{}}}", repo_config.branches().main(), repo_config.branches().next(), repo_config.branches().dev()),
}, "config": {
"events": [ "push" ], "content_type": "json",
"type": "forgejo" "url": repo_listen_url.to_string(),
}); },
let request = network::NetRequest::new( "events": [ "push" ],
network::RequestMethod::Post, "type": "forgejo"
url, }).to_string())
headers, .send()
network::RequestBody::Json(body), .await
network::ResponseType::Json, {
None,
network::NetRequestLogging::None,
);
let result = net.post_json::<Hook>(request).await;
match result {
Ok(response) => { Ok(response) => {
let Some(hook) = response.response_body() else { let Ok(hook) = response.json::<Hook>().await else {
#[cfg(not(tarpaulin_include))] #[cfg(not(tarpaulin_include))]
// request response is Json so response_body never returns None // request response is Json so response_body never returns None
return Err(git::forge::webhook::Error::NetworkResponseEmpty); return Err(git::forge::webhook::Error::NetworkResponseEmpty);
@ -71,4 +64,5 @@ pub async fn register(
Err(git::forge::webhook::Error::FailedToRegister(e.to_string())) Err(git::forge::webhook::Error::FailedToRegister(e.to_string()))
} }
} }
// Ok(())
} }

View file

@ -1,30 +1,24 @@
// //
use git_next_core::{git, WebhookId}; use git_next_core::{git, WebhookId};
use kxio::network; use kxio::net::Net;
use secrecy::ExposeSecret as _; use secrecy::ExposeSecret as _;
pub async fn unregister( pub async fn unregister(
webhook_id: &WebhookId, webhook_id: &WebhookId,
repo_details: &git::RepoDetails, repo_details: &git::RepoDetails,
net: &network::Network, net: &Net,
) -> git::forge::webhook::Result<()> { ) -> git::forge::webhook::Result<()> {
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 token = repo_details.forge.token().expose_secret(); let token = repo_details.forge.token().expose_secret();
let url = network::NetUrl::new(format!( match net
"https://{hostname}/api/v1/repos/{repo_path}/hooks/{webhook_id}?token={token}" .delete(format!(
)); "https://{hostname}/api/v1/repos/{repo_path}/hooks/{webhook_id}?token={token}"
let request = network::NetRequest::new( ))
network::RequestMethod::Delete, .send()
url, .await
network::NetRequestHeaders::new(), {
network::RequestBody::None,
network::ResponseType::None,
None,
network::NetRequestLogging::None,
);
match net.delete(request).await {
Err(e) => { Err(e) => {
tracing::warn!("Failed to unregister webhook"); tracing::warn!("Failed to unregister webhook");
Err(git::forge::webhook::Error::FailedToUnregister( Err(git::forge::webhook::Error::FailedToUnregister(

View file

@ -2,53 +2,46 @@
use crate::{self as github, GithubState}; use crate::{self as github, GithubState};
use git_next_core::git::{self, forge::commit::Status}; use git_next_core::git::{self, forge::commit::Status};
use github::GithubStatus; use github::GithubStatus;
use kxio::network;
/// Checks the results of any (e.g. CI) status checks for the commit. /// Checks the results of any (e.g. CI) status checks for the commit.
/// ///
/// GitHub: <https://docs.github.com/en/rest/commits/statuses?apiVersion=2022-11-28#list-commit-statuses-for-a-reference> /// GitHub: <https://docs.github.com/en/rest/commits/statuses?apiVersion=2022-11-28#list-commit-statuses-for-a-reference>
pub async fn status(github: &github::Github, commit: &git::Commit) -> git::forge::commit::Status { pub async fn status(
github: &github::Github,
commit: &git::Commit,
) -> git::forge::webhook::Result<git::forge::commit::Status> {
let repo_details = &github.repo_details; let repo_details = &github.repo_details;
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 url = network::NetUrl::new(format!( let url = format!("https://api.{hostname}/repos/{repo_path}/commits/{commit}/statuses");
"https://api.{hostname}/repos/{repo_path}/commits/{commit}/statuses" let Ok(response) = github
)); .net
let headers = github::webhook::headers(repo_details.forge.token()); .get(url)
let request = network::NetRequest::new( .headers(github::webhook::headers(repo_details.forge.token()))
network::RequestMethod::Get, .body("")
url, .send()
headers, .await
network::RequestBody::None, else {
network::ResponseType::Json, return Ok(Status::Pending);
None, };
network::NetRequestLogging::None, let statuses = response.json::<Vec<GithubStatus>>().await?;
); let result = statuses
let result = github.net.get::<Vec<GithubStatus>>(request).await; .into_iter()
match result { .map(|status| match status.state {
Ok(response) => response GithubState::Success => Status::Pass,
.response_body() GithubState::Pending | GithubState::Blank => Status::Pending,
.and_then(|statuses| { GithubState::Failure | GithubState::Error => Status::Fail,
statuses })
.into_iter() .reduce(|l, r| match (l, r) {
.map(|status| match status.state { (Status::Pass, Status::Pass) => Status::Pass,
GithubState::Success => Status::Pass, (_, Status::Fail) | (Status::Fail, _) => Status::Fail,
GithubState::Pending | GithubState::Blank => Status::Pending, (_, Status::Pending) | (Status::Pending, _) => Status::Pending,
GithubState::Failure | GithubState::Error => Status::Fail, (Status::Error(e1), Status::Error(e2)) => Status::Error(format!("{e1} / {e2}")),
}) (_, Status::Error(err)) | (Status::Error(err), _) => Status::Error(err),
.reduce(|l, r| match (l, r) { })
(Status::Pass, Status::Pass) => Status::Pass, .unwrap_or_else(|| {
(_, Status::Fail) | (Status::Fail, _) => Status::Fail, tracing::warn!("No status checks configured for 'next' branch",);
(_, Status::Pending) | (Status::Pending, _) => Status::Pending, Status::Pass
}) });
}) Ok(result)
.unwrap_or_else(|| {
tracing::warn!("No status checks configured for 'next' branch",);
Status::Pass
}),
Err(e) => {
tracing::warn!(?e, "Failed to get commit status");
Status::Pending // assume issue is transient and allow retry
}
}
} }

View file

@ -7,8 +7,7 @@ mod webhook;
use crate as github; use crate as github;
use git_next_core::{ use git_next_core::{
self as core, self as core, git,
git::{self, forge::commit::Status},
server::{self, RepoListenUrl}, server::{self, RepoListenUrl},
ForgeNotification, RegisteredWebhook, WebhookAuth, WebhookId, ForgeNotification, RegisteredWebhook, WebhookAuth, WebhookId,
}; };
@ -18,7 +17,7 @@ use derive_more::Constructor;
#[derive(Clone, Debug, Constructor)] #[derive(Clone, Debug, Constructor)]
pub struct Github { pub struct Github {
repo_details: git::RepoDetails, repo_details: git::RepoDetails,
net: kxio::network::Network, net: kxio::net::Net,
} }
#[async_trait::async_trait] #[async_trait::async_trait]
impl git::ForgeLike for Github { impl git::ForgeLike for Github {
@ -52,7 +51,10 @@ impl git::ForgeLike for Github {
github::webhook::parse_body(body) github::webhook::parse_body(body)
} }
async fn commit_status(&self, commit: &git::Commit) -> Status { async fn commit_status(
&self,
commit: &git::Commit,
) -> git::forge::webhook::Result<git::forge::commit::Status> {
github::commit::status(self, commit).await github::commit::status(self, commit).await
} }

View file

@ -1,4 +1,6 @@
// //
#![allow(clippy::expect_used)]
use crate::{Github, GithubState, GithubStatus}; use crate::{Github, GithubState, GithubStatus};
use git_next_core::{ use git_next_core::{
git::{self, forge::commit::Status, ForgeLike}, git::{self, forge::commit::Status, ForgeLike},
@ -9,7 +11,7 @@ use git_next_core::{
}; };
use assert2::let_assert; use assert2::let_assert;
use kxio::network::{self, MockNetwork, StatusCode}; use kxio::net::{MockNet, StatusCode};
use rand::RngCore; use rand::RngCore;
use serde::Serialize; use serde::Serialize;
use serde_json::json; use serde_json::json;
@ -99,10 +101,10 @@ mod github {
let (states, expected) = $value; let (states, expected) = $value;
let repo_details = given::repo_details(); let repo_details = given::repo_details();
let commit = given::a_commit(); let commit = given::a_commit();
let mut net = given::a_network(); let net = given::a_network();
given::commit_states(&states, &mut net, &repo_details, &commit); given::commit_states(&states, &net, &repo_details, &commit);
let forge = given::a_github_forge(&repo_details, net); let forge = given::a_github_forge(&repo_details, net);
assert_eq!(forge.commit_status(&commit).await, expected); assert_eq!(forge.commit_status(&commit).await.expect("status"), expected);
} }
)* )*
} }
@ -125,29 +127,37 @@ mod github {
); );
#[tokio::test] #[tokio::test]
async fn should_return_pending_for_no_statuses() { async fn should_return_pass_for_no_statuses() {
let repo_details = given::repo_details(); let repo_details = given::repo_details();
let commit = given::a_commit(); let commit = given::a_commit();
let mut net = given::a_network(); let net = given::a_network();
net.add_get_response( net.on()
&given::a_commit_status_url(&repo_details, &commit), .get(given::a_commit_status_url(&repo_details, &commit))
StatusCode::OK, .respond(StatusCode::OK)
"", .body(json!([]).to_string())
); .expect("mock");
let forge = given::a_github_forge(&repo_details, net); let forge = given::a_github_forge(&repo_details, net);
assert_eq!(forge.commit_status(&commit).await, Status::Pending); assert_eq!(
forge.commit_status(&commit).await.expect("status"),
Status::Pass // no CI checks configured
);
} }
#[tokio::test] #[tokio::test]
async fn should_return_pending_for_network_error() { async fn should_return_pending_for_network_error() {
let repo_details = given::repo_details(); let repo_details = given::repo_details();
let commit = given::a_commit(); let commit = given::a_commit();
let mut net = given::a_network(); let net = given::a_network();
net.add_get_error( net.on()
&given::a_commit_status_url(&repo_details, &commit), .get(given::a_commit_status_url(&repo_details, &commit))
"boom today", .respond(StatusCode::INTERNAL_SERVER_ERROR)
); .body("boom today")
.expect("mock");
let forge = given::a_github_forge(&repo_details, net); let forge = given::a_github_forge(&repo_details, net);
assert_eq!(forge.commit_status(&commit).await, Status::Pending); assert_eq!(
forge.commit_status(&commit).await.expect("status"),
Status::Pending
);
} }
} }
@ -164,9 +174,9 @@ mod github {
let hook_id_1 = given::a_github_webhook_id(); let hook_id_1 = given::a_github_webhook_id();
let hook_id_2 = given::a_github_webhook_id(); let hook_id_2 = given::a_github_webhook_id();
let hook_id_3 = given::a_github_webhook_id(); let hook_id_3 = given::a_github_webhook_id();
let mut net = given::a_network(); let net = given::a_network();
let mut args = with::WebhookArgs { let args = with::WebhookArgs {
net: &mut net, net: &net,
hostname, hostname,
repo_path, repo_path,
}; };
@ -178,10 +188,10 @@ mod github {
with::ReturnedWebhook::new(hook_id_2, &repo_listen_url), with::ReturnedWebhook::new(hook_id_2, &repo_listen_url),
with::ReturnedWebhook::new(hook_id_3, &given::any_webhook_url()), with::ReturnedWebhook::new(hook_id_3, &given::any_webhook_url()),
], ],
&mut args, &args,
); );
// page 2 with no items - stops pagination // page 2 with no items - stops pagination
with::get_webhooks_by_page(2, &[], &mut args); with::get_webhooks_by_page(2, &[], &args);
let forge = given::a_github_forge(&repo_details, net); let forge = given::a_github_forge(&repo_details, net);
@ -201,11 +211,14 @@ mod github {
let repo_listen_url = given::a_repo_listen_url(&repo_details); let repo_listen_url = given::a_repo_listen_url(&repo_details);
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 mut net = given::a_network(); let net = given::a_network();
net.add_get_error( net.on()
format!("https://api.{hostname}/repos/{repo_path}/hooks?page=1").as_str(), .get(format!(
"error_message", "https://api.{hostname}/repos/{repo_path}/hooks?page=1"
); ))
.respond(StatusCode::INTERNAL_SERVER_ERROR)
.body("error_message")
.expect("mock");
let forge = given::a_github_forge(&repo_details, net); let forge = given::a_github_forge(&repo_details, net);
@ -222,12 +235,14 @@ mod github {
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 webhook_id = given::a_webhook_id(); let webhook_id = given::a_webhook_id();
let mut net = given::a_network(); let net = given::a_network();
net.add_delete_response( net.on()
format!("https://api.{hostname}/repos/{repo_path}/hooks/{webhook_id}").as_str(), .delete(format!(
StatusCode::OK, "https://api.{hostname}/repos/{repo_path}/hooks/{webhook_id}"
"", ))
); .respond(StatusCode::OK)
.body("")
.expect("mock");
let forge = given::a_github_forge(&repo_details, net); let forge = given::a_github_forge(&repo_details, net);
@ -242,16 +257,19 @@ mod github {
); );
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 mut net = given::a_network(); let net = given::a_network();
// unregister the webhook will return empty response // unregister the webhook will return empty response
net.add_delete_error( let webhook_id = given::a_webhook_id();
format!("https://api.{hostname}/repos/{repo_path}/hooks").as_str(), net.on()
"error", .delete(format!(
); "https://api.{hostname}/repos/{repo_path}/hooks/{webhook_id}"
))
.respond(StatusCode::INTERNAL_SERVER_ERROR)
.mock()
.expect("mock");
let forge = given::a_github_forge(&repo_details, net); let forge = given::a_github_forge(&repo_details, net);
let webhook_id = given::a_webhook_id();
let_assert!(Err(err) = forge.unregister_webhook(&webhook_id).await); let_assert!(Err(err) = forge.unregister_webhook(&webhook_id).await);
assert!( assert!(
matches!(err, git::forge::webhook::Error::FailedToRegister(_)), matches!(err, git::forge::webhook::Error::FailedToRegister(_)),
@ -274,23 +292,24 @@ mod github {
let repo_listen_url = given::a_repo_listen_url(&repo_details); let repo_listen_url = given::a_repo_listen_url(&repo_details);
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 mut net = given::a_network(); let net = given::a_network();
let mut args = with::WebhookArgs { let args = with::WebhookArgs {
net: &mut net, net: &net,
hostname, hostname,
repo_path, repo_path,
}; };
// there are no existing matching webhooks // there are no existing matching webhooks
with::get_webhooks_by_page(1, &[], &mut args); with::get_webhooks_by_page(1, &[], &args);
// register the webhook will succeed // register the webhook will succeed
let webhook_id = given::a_github_webhook_id(); let webhook_id = given::a_github_webhook_id();
net.add_post_response( net.on()
format!("https://api.{hostname}/repos/{repo_path}/hooks").as_str(), .post(format!("https://api.{hostname}/repos/{repo_path}/hooks"))
StatusCode::OK, .respond(StatusCode::OK)
json!({"id": webhook_id, "config":{"url": repo_listen_url.to_string()}}) .body(
.to_string() json!({"id": webhook_id, "config":{"url": repo_listen_url.to_string()}})
.as_str(), .to_string(),
); )
.expect("mock");
let forge = given::a_github_forge(&repo_details, net); let forge = given::a_github_forge(&repo_details, net);
@ -329,9 +348,9 @@ mod github {
let repo_listen_url = given::a_repo_listen_url(&repo_details); let repo_listen_url = given::a_repo_listen_url(&repo_details);
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 mut net = given::a_network(); let net = given::a_network();
let mut args = with::WebhookArgs { let args = with::WebhookArgs {
net: &mut net, net: &net,
hostname, hostname,
repo_path, repo_path,
}; };
@ -341,20 +360,21 @@ mod github {
with::ReturnedWebhook::new(given::a_github_webhook_id(), &given::any_webhook_url()); with::ReturnedWebhook::new(given::a_github_webhook_id(), &given::any_webhook_url());
let hooks = [hook_1, hook_2, hook_3]; let hooks = [hook_1, hook_2, hook_3];
// there are three existing webhooks, two are matching webhooks // there are three existing webhooks, two are matching webhooks
with::get_webhooks_by_page(1, &hooks, &mut args); with::get_webhooks_by_page(1, &hooks, &args);
with::get_webhooks_by_page(2, &[], &mut args); with::get_webhooks_by_page(2, &[], &args);
// should unregister 1 and 2, but not 3 // should unregister 1 and 2, but not 3
with::unregister_webhook(&hooks[0], &mut args); with::unregister_webhook(&hooks[0], &args);
with::unregister_webhook(&hooks[1], &mut args); with::unregister_webhook(&hooks[1], &args);
// register the webhook will succeed // register the webhook will succeed
let webhook_id = given::a_github_webhook_id(); let webhook_id = given::a_github_webhook_id();
net.add_post_response( net.on()
format!("https://api.{hostname}/repos/{repo_path}/hooks").as_str(), .post(format!("https://api.{hostname}/repos/{repo_path}/hooks"))
StatusCode::OK, .respond(StatusCode::OK)
json!({"id": webhook_id, "config":{"url": repo_listen_url.to_string()}}) .body(
.to_string() json!({"id": webhook_id, "config":{"url": repo_listen_url.to_string()}})
.as_str(), .to_string(),
); )
.expect("mock");
let forge = given::a_github_forge(&repo_details, net); let forge = given::a_github_forge(&repo_details, net);
@ -375,26 +395,26 @@ mod github {
let repo_listen_url = given::a_repo_listen_url(&repo_details); let repo_listen_url = given::a_repo_listen_url(&repo_details);
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 mut net = given::a_network(); let net = given::a_network();
let mut args = with::WebhookArgs { let args = with::WebhookArgs {
net: &mut net, net: &net,
hostname, hostname,
repo_path, repo_path,
}; };
// there are no existing matching webhooks // there are no existing matching webhooks
with::get_webhooks_by_page(1, &[], &mut args); with::get_webhooks_by_page(1, &[], &args);
// register the webhook will return empty response // register the webhook will return empty response
net.add_post_response( net.on()
format!("https://api.{hostname}/repos/{repo_path}/hooks").as_str(), .post(format!("https://api.{hostname}/repos/{repo_path}/hooks"))
StatusCode::OK, .respond(StatusCode::OK)
json!({}).to_string().as_str(), // empty response .body(json!({}).to_string())
); .expect("mock");
let forge = given::a_github_forge(&repo_details, net); let forge = given::a_github_forge(&repo_details, net);
let_assert!(Err(err) = forge.register_webhook(&repo_listen_url).await); let_assert!(Err(err) = forge.register_webhook(&repo_listen_url).await);
assert!( assert!(
matches!(err, git::forge::webhook::Error::FailedToRegister(_)), matches!(err, git::forge::webhook::Error::NetworkResponseEmpty),
"{err:?}" "{err:?}"
); );
} }
@ -409,19 +429,20 @@ mod github {
let repo_listen_url = given::a_repo_listen_url(&repo_details); let repo_listen_url = given::a_repo_listen_url(&repo_details);
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 mut net = given::a_network(); let net = given::a_network();
let mut args = with::WebhookArgs { let args = with::WebhookArgs {
net: &mut net, net: &net,
hostname, hostname,
repo_path, repo_path,
}; };
// there are no existing matching webhooks // there are no existing matching webhooks
with::get_webhooks_by_page(1, &[], &mut args); with::get_webhooks_by_page(1, &[], &args);
// register the webhook will return empty response // register the webhook will return empty response
net.add_post_error( net.on()
format!("https://api.{hostname}/repos/{repo_path}/hooks").as_str(), .post(format!("https://api.{hostname}/repos/{repo_path}/hooks"))
"error", .respond(StatusCode::INTERNAL_SERVER_ERROR)
); .body("error")
.expect("mock");
let forge = given::a_github_forge(&repo_details, net); let forge = given::a_github_forge(&repo_details, net);
@ -438,32 +459,34 @@ mod github {
use super::*; use super::*;
pub fn get_webhooks_by_page( pub fn get_webhooks_by_page(page: u8, response: &[ReturnedWebhook], args: &WebhookArgs) {
page: u8,
response: &[ReturnedWebhook],
args: &mut WebhookArgs,
) {
let hostname = args.hostname; let hostname = args.hostname;
let repo_path = args.repo_path; let repo_path = args.repo_path;
args.net.add_get_response( args.net
format!("https://api.{hostname}/repos/{repo_path}/hooks?page={page}").as_str(), .on()
StatusCode::OK, .get(format!(
json!(response).to_string().as_str(), "https://api.{hostname}/repos/{repo_path}/hooks?page={page}"
); ))
.respond(StatusCode::OK)
.body(json!(response).to_string())
.expect("mock");
} }
pub fn unregister_webhook(hook1: &ReturnedWebhook, args: &mut WebhookArgs) { pub fn unregister_webhook(hook1: &ReturnedWebhook, args: &WebhookArgs) {
let webhook_id = hook1.id; let webhook_id = hook1.id;
let hostname = args.hostname; let hostname = args.hostname;
let repo_path = args.repo_path; let repo_path = args.repo_path;
args.net.add_delete_response( args.net
format!("https://api.{hostname}/repos/{repo_path}/hooks/{webhook_id}").as_str(), .on()
StatusCode::OK, .delete(format!(
"", "https://api.{hostname}/repos/{repo_path}/hooks/{webhook_id}"
); ))
.respond(StatusCode::OK)
.body("")
.expect("mock");
} }
pub struct WebhookArgs<'a> { pub struct WebhookArgs<'a> {
pub net: &'a mut network::MockNetwork, pub net: &'a kxio::net::MockNet,
pub hostname: &'a Hostname, pub hostname: &'a Hostname,
pub repo_path: &'a RepoPath, pub repo_path: &'a RepoPath,
} }
@ -497,21 +520,23 @@ mod github {
pub fn commit_states( pub fn commit_states(
states: &[GithubState], states: &[GithubState],
net: &mut MockNetwork, net: &MockNet,
repo_details: &git::RepoDetails, repo_details: &git::RepoDetails,
commit: &git::Commit, commit: &git::Commit,
) { ) {
let response = json!(states net.on()
.iter() .get(a_commit_status_url(repo_details, commit))
.map(|state| GithubStatus { .respond(StatusCode::OK)
state: state.to_owned() .body(
}) json!(states
.collect::<Vec<_>>()); .iter()
net.add_get_response( .map(|state| GithubStatus {
a_commit_status_url(repo_details, commit).as_str(), state: state.to_owned()
StatusCode::OK, })
response.to_string().as_str(), .collect::<Vec<_>>())
); .to_string(),
)
.expect("mock");
} }
pub fn a_commit_status_url( pub fn a_commit_status_url(
@ -578,10 +603,11 @@ mod github {
pub fn a_github_forge( pub fn a_github_forge(
repo_details: &git::RepoDetails, repo_details: &git::RepoDetails,
net: impl Into<kxio::network::Network>, net: impl Into<kxio::net::Net>,
) -> Github { ) -> Github {
Github::new(repo_details.clone(), net.into()) Github::new(repo_details.clone(), net.into())
} }
pub fn repo_details() -> git::RepoDetails { pub fn repo_details() -> git::RepoDetails {
git::RepoDetails::new( git::RepoDetails::new(
git::Generation::default(), git::Generation::default(),
@ -614,8 +640,8 @@ mod github {
pub fn a_repo_alias() -> RepoAlias { pub fn a_repo_alias() -> RepoAlias {
RepoAlias::new(a_name()) RepoAlias::new(a_name())
} }
pub fn a_network() -> kxio::network::MockNetwork { pub fn a_network() -> kxio::net::MockNet {
kxio::network::MockNetwork::new() kxio::net::mock()
} }
pub fn a_repo_listen_url(repo_details: &RepoDetails) -> RepoListenUrl { pub fn a_repo_listen_url(repo_details: &RepoDetails) -> RepoListenUrl {

View file

@ -2,8 +2,6 @@
use crate as github; use crate as github;
use git_next_core::{git, server::RepoListenUrl, WebhookId}; use git_next_core::{git, server::RepoListenUrl, WebhookId};
use kxio::network;
// https://docs.github.com/en/rest/repos/webhooks?apiVersion=2022-11-28#list-repository-webhooks // https://docs.github.com/en/rest/repos/webhooks?apiVersion=2022-11-28#list-repository-webhooks
pub async fn list( pub async fn list(
github: &github::Github, github: &github::Github,
@ -15,40 +13,36 @@ pub async fn list(
let net = &github.net; let net = &github.net;
let mut page = 1; let mut page = 1;
loop { loop {
let request = network::NetRequest::new( let result = net
network::RequestMethod::Get, .get(format!(
network::NetUrl::new(format!(
"https://api.{hostname}/repos/{}/hooks?page={page}", "https://api.{hostname}/repos/{}/hooks?page={page}",
repo_details.repo_path, repo_details.repo_path,
)), ))
github::webhook::headers(repo_details.forge.token()), .headers(github::webhook::headers(repo_details.forge.token()))
network::RequestBody::None, .send()
network::ResponseType::Json, .await;
None,
network::NetRequestLogging::None,
);
let result = net.get::<Vec<github::GithubHook>>(request).await;
match result { match result {
Ok(response) => { Ok(response) => match response.json::<Vec<github::GithubHook>>().await {
let Some(list) = response.response_body() else { Err(err) => {
#[cfg(not(tarpaulin_include))] tracing::warn!(?err, "failed");
// request response is Json so response_body never returns None
return Err(git::forge::webhook::Error::NetworkResponseEmpty); return Err(git::forge::webhook::Error::NetworkResponseEmpty);
};
if list.is_empty() {
return Ok(ids);
} }
for hook in list { Ok(list) => {
if hook if list.is_empty() {
.url() return Ok(ids);
.as_ref()
.starts_with(&repo_listen_url.to_string())
{
ids.push(hook.id());
} }
for hook in list {
if hook
.url()
.as_ref()
.starts_with(&repo_listen_url.to_string())
{
ids.push(hook.id());
}
}
page += 1;
} }
page += 1; },
}
Err(e) => { Err(e) => {
return Err(git::forge::webhook::Error::Network(e)); return Err(git::forge::webhook::Error::Network(e));
} }

View file

@ -1,3 +1,5 @@
use std::collections::HashMap;
// //
use git_next_core::{git, webhook, ApiToken, BranchName}; use git_next_core::{git, webhook, ApiToken, BranchName};
@ -16,19 +18,24 @@ pub use unregister::unregister;
#[cfg(test)] #[cfg(test)]
pub use authorisation::sign_body; pub use authorisation::sign_body;
pub fn headers(token: &ApiToken) -> kxio::network::NetRequestHeaders { pub fn headers(token: &ApiToken) -> HashMap<String, String> {
use secrecy::ExposeSecret; use secrecy::ExposeSecret;
kxio::network::NetRequestHeaders::default()
.with("Accept", "application/vnd.github+json") HashMap::from([
.with( (
"User-Agent", "Accept".to_string(),
format!("git-next/server/{}", clap::crate_version!()).as_str(), "application/vnd.github+json".to_string(),
) ),
.with( (
"Authorization", "User-Agent".to_string(),
format!("Bearer {}", token.expose_secret()).as_str(), format!("git-next/server/{}", clap::crate_version!()),
) ),
.with("X-GitHub-Api-Version", "2022-11-28") (
"Authorization".to_string(),
format!("Bearer {}", token.expose_secret()),
),
("X-GitHub-Api-Version".to_string(), "2022-11-28".to_string()),
])
} }
#[derive(Debug, serde::Deserialize)] #[derive(Debug, serde::Deserialize)]

View file

@ -1,8 +1,7 @@
// //
use crate::{self as github, webhook}; use crate::{self as github, webhook};
use git_next_core::{git, server::RepoListenUrl, RegisteredWebhook, WebhookAuth, WebhookId}; use git_next_core::{git, server::RepoListenUrl, RegisteredWebhook, WebhookAuth, WebhookId};
use serde_json::json;
use kxio::network;
// https://docs.github.com/en/rest/repos/webhooks?apiVersion=2022-11-28#create-a-repository-webhook // https://docs.github.com/en/rest/repos/webhooks?apiVersion=2022-11-28#create-a-repository-webhook
pub async fn register( pub async fn register(
@ -23,45 +22,48 @@ pub async fn register(
let net = &github.net; let net = &github.net;
let hostname = repo_details.forge.hostname(); let hostname = repo_details.forge.hostname();
let authorisation = WebhookAuth::generate(); let authorisation = WebhookAuth::generate();
let request = network::NetRequest::new( match net
network::RequestMethod::Post, .post(format!(
network::NetUrl::new(format!(
"https://api.{hostname}/repos/{}/hooks", "https://api.{hostname}/repos/{}/hooks",
repo_details.repo_path repo_details.repo_path
)), ))
github::webhook::headers(repo_details.forge.token()), .headers(github::webhook::headers(repo_details.forge.token()))
network::RequestBody::Json(network::json!({ .body(
"name": "web", json!({
"active": true, "name": "web",
"events": ["push"], "active": true,
"config": { "events": ["push"],
"url": repo_listen_url.to_string(), "config": {
"content_type": "json", "url": repo_listen_url.to_string(),
"secret": authorisation.to_string(), "content_type": "json",
"insecure_ssl": "0", "secret": authorisation.to_string(),
} "insecure_ssl": "0",
})), }
network::ResponseType::Json, })
None, .to_string(),
network::NetRequestLogging::None, )
); .send()
let result = net.post_json::<github::GithubHook>(request).await; .await
match result { {
Ok(response) => {
let Some(hook) = response.response_body() else {
#[cfg(not(tarpaulin_include))]
// request response is Json so response_body never returns None
return Err(git::forge::webhook::Error::NetworkResponseEmpty);
};
tracing::info!(webhook_id = %hook.id, "Webhook registered");
Ok(RegisteredWebhook::new(
WebhookId::new(format!("{}", hook.id)),
authorisation,
))
}
Err(e) => { Err(e) => {
tracing::warn!("Failed to register webhook"); tracing::warn!("Failed to register webhook");
Err(git::forge::webhook::Error::FailedToRegister(e.to_string())) Err(git::forge::webhook::Error::FailedToRegister(e.to_string()))
} }
Ok(response) => {
match response.json::<github::GithubHook>().await {
Err(_) => {
#[cfg(not(tarpaulin_include))]
// request response is Json so response_body never returns None
return Err(git::forge::webhook::Error::NetworkResponseEmpty);
}
Ok(hook) => {
tracing::info!(webhook_id = %hook.id, "Webhook registered");
Ok(RegisteredWebhook::new(
WebhookId::new(format!("{}", hook.id)),
authorisation,
))
}
}
}
} }
} }

View file

@ -2,8 +2,6 @@
use crate as github; use crate as github;
use git_next_core::{git, WebhookId}; use git_next_core::{git, WebhookId};
use kxio::network;
// https://docs.github.com/en/rest/repos/webhooks?apiVersion=2022-11-28#delete-a-repository-webhook // https://docs.github.com/en/rest/repos/webhooks?apiVersion=2022-11-28#delete-a-repository-webhook
pub async fn unregister( pub async fn unregister(
github: &github::Github, github: &github::Github,
@ -12,20 +10,13 @@ pub async fn unregister(
let net = &github.net; let net = &github.net;
let repo_details = &github.repo_details; let repo_details = &github.repo_details;
let hostname = repo_details.forge.hostname(); let hostname = repo_details.forge.hostname();
let request = network::NetRequest::new( net.delete(format!(
network::RequestMethod::Delete, "https://api.{hostname}/repos/{}/hooks/{}",
network::NetUrl::new(format!( repo_details.repo_path, webhook_id
"https://api.{hostname}/repos/{}/hooks/{}", ))
repo_details.repo_path, webhook_id .headers(github::webhook::headers(repo_details.forge.token()))
)), .send()
github::webhook::headers(repo_details.forge.token()), .await
network::RequestBody::None, .map_err(|e| git::forge::webhook::Error::FailedToRegister(e.to_string()))
network::ResponseType::None, .map(|_| ())
None,
network::NetRequestLogging::None,
);
net.delete(request)
.await
.map_err(|e| git::forge::webhook::Error::FailedToRegister(e.to_string()))
.map(|_| ())
} }