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"
# fs/network
kxio = { version = "1.2" }
kxio = "3.0"
# TOML parsing
serde = { version = "1.0", features = ["derive"] }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -59,7 +59,7 @@ async fn should_open() -> TestResult {
let _ = opened_ref.write().map(|mut l| l.push(()));
Ok(Box::new(open_repository))
});
fs.dir_create(&repo_details.gitdir)?;
fs.dir(&repo_details.gitdir).create()?;
//when
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();
expect::open_repository(&mut repository_factory, open_repository);
fs.dir_create(&repo_details.gitdir)?;
fs.dir(&repo_details.gitdir).create()?;
//when
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();
expect::open_repository(&mut repository_factory, open_repository);
fs.dir_create(&repo_details.gitdir)?;
fs.dir(&repo_details.gitdir).create()?;
//when
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();
expect::open_repository(&mut repository_factory, open_repository);
fs.dir_create(&repo_details.gitdir)?;
fs.dir(&repo_details.gitdir).create()?;
//when
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));
let mut repository_factory = MockRepositoryFactory::new();
expect::open_repository(&mut repository_factory, open_repository);
fs.dir_create(&repo_details.gitdir)?;
fs.dir(&repo_details.gitdir).create()?;
//when
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
.expect_commit_status()
.with(mockall::predicate::eq(commit))
.return_once(|_| status);
.return_once(|_| Ok(status));
forge
.expect_duplicate()
.return_once(move || Box::new(commit_status_forge));

View file

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

View file

@ -5,14 +5,14 @@ use actix::prelude::*;
use crate::alerts::{AlertsActor, History};
//
pub fn a_filesystem() -> kxio::fs::FileSystem {
kxio::fs::temp().unwrap_or_else(|e| panic!("{}", e))
pub fn a_filesystem() -> kxio::fs::TempFileSystem {
kxio::fs::temp().expect("temp fs")
}
pub fn a_network() -> kxio::network::MockNetwork {
kxio::network::MockNetwork::new()
pub fn a_network() -> kxio::net::MockNet {
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()
}

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);
// 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
let listen = Listen::new(

View file

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

View file

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

View file

@ -57,7 +57,7 @@ impl AppConfig {
pub fn load(fs: &FileSystem) -> Result<Self> {
let file = fs.base().join("git-next-server.toml");
info!(?file, "");
let str = fs.file_read_to_string(&file)?;
let str = fs.file(&file).reader()?.to_string();
Ok(toml::from_str(&str)?)
}

View file

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

View file

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

View file

@ -32,7 +32,10 @@ pub trait ForgeLike: std::fmt::Debug + Send + Sync {
) -> git::forge::webhook::Result<webhook::push::Push>;
/// 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
async fn list_webhooks(

View file

@ -4,8 +4,16 @@ pub type Result<T> = core::result::Result<T, Error>;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[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}")]
FailedToRegister(String),

View file

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

View file

@ -142,7 +142,8 @@ impl RepoDetails {
let fs = self.gitdir.as_fs();
// load config file
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
.lines()
.map(ToOwned::to_owned)
@ -165,7 +166,7 @@ impl RepoDetails {
}
tracing::debug!(?config_lines, "updated file");
// 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(())
}
}

View file

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

View file

@ -1,3 +1,5 @@
use std::ops::Deref as _;
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 gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal);
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 repo_config = &given::a_repo_config();
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_assert!(Some(max_dev_commits) = forge_details.max_dev_commits());
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));
for _ in [0; 25] {
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_assert!(Some(max_dev_commits) = forge_details.max_dev_commits());
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!(
Ok(open_repository) = test_repository.open(&gitdir),
"open repository"

View file

@ -1,3 +1,5 @@
use std::ops::Deref as _;
//
use super::*;
@ -11,7 +13,7 @@ fn should_return_file() -> TestResult {
let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal);
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));
then::commit_named_file_to_branch(
&file_name,
@ -35,7 +37,7 @@ fn should_error_on_missing_file() -> TestResult {
let_assert!(Ok(fs) = kxio::fs::temp());
let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal);
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 repo_config = &given::a_repo_config();
let branches = repo_config.branches();

View file

@ -23,7 +23,7 @@ fn open_where_storage_external_auth_matches() -> TestResult {
tracing::debug!(?result, "open");
assert!(result.is_ok());
// 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");
assert!(config.lines().any(|line| line.contains(url)));
Ok(())
@ -60,7 +60,7 @@ fn open_where_storage_external_auth_differs_error() -> TestResult {
));
// 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");
assert!(config.lines().any(|line| line.contains(original_url))); // the original urk
Ok(())
@ -86,7 +86,7 @@ fn open_where_storage_internal_auth_matches() -> TestResult {
tracing::debug!(?result, "open");
assert!(result.is_ok());
// 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");
assert!(
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());
// 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");
assert!(
config

View file

@ -306,7 +306,7 @@ pub mod given {
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))
}
@ -337,7 +337,8 @@ pub mod given {
let repo = gix::prepare_clone_bare(url, fs.base()).unwrap();
repo.persist();
// 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
let mut config_lines = config_file.lines().collect::<Vec<_>>();
config_lines.push(r#"[remote "origin"]"#);
@ -345,8 +346,7 @@ pub mod given {
tracing::info!(?url, %url_line, "writing");
config_lines.push(&url_line);
// write config file back out
fs.file_write(&path.join("config"), config_lines.join("\n").as_str())
.unwrap();
file.write(config_lines.join("\n").as_str()).unwrap();
}
#[allow(clippy::unwrap_used)]
@ -374,7 +374,7 @@ pub mod then {
let pathbuf = PathBuf::from(gitdir);
let file = fs.base().join(pathbuf).join(file_name);
#[allow(clippy::expect_used)]
fs.file_write(&file, contents)?;
fs.file(&file).write(contents)?;
// git add ${file}
git_add_file(gitdir, &file)?;
// git commit -m"Added ${file}"
@ -396,7 +396,7 @@ pub mod then {
let word = given::a_name();
let pathbuf = PathBuf::from(gitdir);
let file = fs.base().join(pathbuf).join(&word);
fs.file_write(&file, &word)?;
fs.file(&file).write(&word)?;
// git add ${file}
git_add_file(gitdir, &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 origin_heads = gitrefs.join("remotes").join("origin");
let remote_branch = origin_heads.join(branch_name.to_string().as_str());
let contents = fs.file_read_to_string(&local_branch)?;
fs.dir_create_all(&origin_heads)?;
fs.file_write(&remote_branch, &contents)?;
let contents = fs.file(&local_branch).reader()?.to_string();
fs.dir(&origin_heads).create_all()?;
fs.file(&remote_branch).write(&contents)?;
Ok(())
}
@ -514,7 +514,7 @@ pub mod then {
.join("refs")
.join("heads")
.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()))
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,53 +2,46 @@
use crate::{self as github, GithubState};
use git_next_core::git::{self, forge::commit::Status};
use github::GithubStatus;
use kxio::network;
/// 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>
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 hostname = repo_details.forge.hostname();
let repo_path = &repo_details.repo_path;
let url = network::NetUrl::new(format!(
"https://api.{hostname}/repos/{repo_path}/commits/{commit}/statuses"
));
let headers = github::webhook::headers(repo_details.forge.token());
let request = network::NetRequest::new(
network::RequestMethod::Get,
url,
headers,
network::RequestBody::None,
network::ResponseType::Json,
None,
network::NetRequestLogging::None,
);
let result = github.net.get::<Vec<GithubStatus>>(request).await;
match result {
Ok(response) => response
.response_body()
.and_then(|statuses| {
statuses
.into_iter()
.map(|status| match status.state {
GithubState::Success => Status::Pass,
GithubState::Pending | GithubState::Blank => Status::Pending,
GithubState::Failure | GithubState::Error => Status::Fail,
})
.reduce(|l, r| match (l, r) {
(Status::Pass, Status::Pass) => Status::Pass,
(_, Status::Fail) | (Status::Fail, _) => Status::Fail,
(_, Status::Pending) | (Status::Pending, _) => Status::Pending,
})
})
.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
}
}
let url = format!("https://api.{hostname}/repos/{repo_path}/commits/{commit}/statuses");
let Ok(response) = github
.net
.get(url)
.headers(github::webhook::headers(repo_details.forge.token()))
.body("")
.send()
.await
else {
return Ok(Status::Pending);
};
let statuses = response.json::<Vec<GithubStatus>>().await?;
let result = statuses
.into_iter()
.map(|status| match status.state {
GithubState::Success => Status::Pass,
GithubState::Pending | GithubState::Blank => Status::Pending,
GithubState::Failure | GithubState::Error => Status::Fail,
})
.reduce(|l, r| match (l, r) {
(Status::Pass, Status::Pass) => Status::Pass,
(_, Status::Fail) | (Status::Fail, _) => Status::Fail,
(_, Status::Pending) | (Status::Pending, _) => Status::Pending,
(Status::Error(e1), Status::Error(e2)) => Status::Error(format!("{e1} / {e2}")),
(_, Status::Error(err)) | (Status::Error(err), _) => Status::Error(err),
})
.unwrap_or_else(|| {
tracing::warn!("No status checks configured for 'next' branch",);
Status::Pass
});
Ok(result)
}

View file

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

View file

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

View file

@ -2,8 +2,6 @@
use crate as github;
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
pub async fn list(
github: &github::Github,
@ -15,40 +13,36 @@ pub async fn list(
let net = &github.net;
let mut page = 1;
loop {
let request = network::NetRequest::new(
network::RequestMethod::Get,
network::NetUrl::new(format!(
let result = net
.get(format!(
"https://api.{hostname}/repos/{}/hooks?page={page}",
repo_details.repo_path,
)),
github::webhook::headers(repo_details.forge.token()),
network::RequestBody::None,
network::ResponseType::Json,
None,
network::NetRequestLogging::None,
);
let result = net.get::<Vec<github::GithubHook>>(request).await;
))
.headers(github::webhook::headers(repo_details.forge.token()))
.send()
.await;
match result {
Ok(response) => {
let Some(list) = response.response_body() else {
#[cfg(not(tarpaulin_include))]
// request response is Json so response_body never returns None
Ok(response) => match response.json::<Vec<github::GithubHook>>().await {
Err(err) => {
tracing::warn!(?err, "failed");
return Err(git::forge::webhook::Error::NetworkResponseEmpty);
};
if list.is_empty() {
return Ok(ids);
}
for hook in list {
if hook
.url()
.as_ref()
.starts_with(&repo_listen_url.to_string())
{
ids.push(hook.id());
Ok(list) => {
if list.is_empty() {
return Ok(ids);
}
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) => {
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};
@ -16,19 +18,24 @@ pub use unregister::unregister;
#[cfg(test)]
pub use authorisation::sign_body;
pub fn headers(token: &ApiToken) -> kxio::network::NetRequestHeaders {
pub fn headers(token: &ApiToken) -> HashMap<String, String> {
use secrecy::ExposeSecret;
kxio::network::NetRequestHeaders::default()
.with("Accept", "application/vnd.github+json")
.with(
"User-Agent",
format!("git-next/server/{}", clap::crate_version!()).as_str(),
)
.with(
"Authorization",
format!("Bearer {}", token.expose_secret()).as_str(),
)
.with("X-GitHub-Api-Version", "2022-11-28")
HashMap::from([
(
"Accept".to_string(),
"application/vnd.github+json".to_string(),
),
(
"User-Agent".to_string(),
format!("git-next/server/{}", clap::crate_version!()),
),
(
"Authorization".to_string(),
format!("Bearer {}", token.expose_secret()),
),
("X-GitHub-Api-Version".to_string(), "2022-11-28".to_string()),
])
}
#[derive(Debug, serde::Deserialize)]

View file

@ -1,8 +1,7 @@
//
use crate::{self as github, webhook};
use git_next_core::{git, server::RepoListenUrl, RegisteredWebhook, WebhookAuth, WebhookId};
use kxio::network;
use serde_json::json;
// https://docs.github.com/en/rest/repos/webhooks?apiVersion=2022-11-28#create-a-repository-webhook
pub async fn register(
@ -23,45 +22,48 @@ pub async fn register(
let net = &github.net;
let hostname = repo_details.forge.hostname();
let authorisation = WebhookAuth::generate();
let request = network::NetRequest::new(
network::RequestMethod::Post,
network::NetUrl::new(format!(
match net
.post(format!(
"https://api.{hostname}/repos/{}/hooks",
repo_details.repo_path
)),
github::webhook::headers(repo_details.forge.token()),
network::RequestBody::Json(network::json!({
"name": "web",
"active": true,
"events": ["push"],
"config": {
"url": repo_listen_url.to_string(),
"content_type": "json",
"secret": authorisation.to_string(),
"insecure_ssl": "0",
}
})),
network::ResponseType::Json,
None,
network::NetRequestLogging::None,
);
let result = net.post_json::<github::GithubHook>(request).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,
))
}
))
.headers(github::webhook::headers(repo_details.forge.token()))
.body(
json!({
"name": "web",
"active": true,
"events": ["push"],
"config": {
"url": repo_listen_url.to_string(),
"content_type": "json",
"secret": authorisation.to_string(),
"insecure_ssl": "0",
}
})
.to_string(),
)
.send()
.await
{
Err(e) => {
tracing::warn!("Failed to register webhook");
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 git_next_core::{git, WebhookId};
use kxio::network;
// https://docs.github.com/en/rest/repos/webhooks?apiVersion=2022-11-28#delete-a-repository-webhook
pub async fn unregister(
github: &github::Github,
@ -12,20 +10,13 @@ pub async fn unregister(
let net = &github.net;
let repo_details = &github.repo_details;
let hostname = repo_details.forge.hostname();
let request = network::NetRequest::new(
network::RequestMethod::Delete,
network::NetUrl::new(format!(
"https://api.{hostname}/repos/{}/hooks/{}",
repo_details.repo_path, webhook_id
)),
github::webhook::headers(repo_details.forge.token()),
network::RequestBody::None,
network::ResponseType::None,
None,
network::NetRequestLogging::None,
);
net.delete(request)
.await
.map_err(|e| git::forge::webhook::Error::FailedToRegister(e.to_string()))
.map(|_| ())
net.delete(format!(
"https://api.{hostname}/repos/{}/hooks/{}",
repo_details.repo_path, webhook_id
))
.headers(github::webhook::headers(repo_details.forge.token()))
.send()
.await
.map_err(|e| git::forge::webhook::Error::FailedToRegister(e.to_string()))
.map(|_| ())
}