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

This commit is contained in:
Renovate Bot 2024-11-20 22:46:33 +00:00 committed by Paul Campbell
parent ea264aaf12
commit d3dfedc95b
44 changed files with 1299 additions and 1082 deletions

1174
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"] }

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(|_| ())
} }