Compare commits

..

1 commit

Author SHA1 Message Date
Renovate Bot
5ffe37707a chore(deps): update rust crate bon to v3
All checks were successful
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
ci/woodpecker/pr/cron-docker-builder Pipeline was successful
ci/woodpecker/pr/push-next Pipeline was successful
ci/woodpecker/pr/tag-created Pipeline was successful
2024-11-15 18:17:21 +00:00
44 changed files with 1082 additions and 1278 deletions

1153
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 = "3.0"
kxio = { version = "1.2" }
# 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::net::Net,
net: kxio::network::Network,
}
impl Actor for AlertsActor {

View file

@ -1,12 +1,13 @@
//
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::net::Net,
net: &kxio::network::Network,
) {
let Ok(webhook) =
Webhook::from_bytes(webhook_config.secret().expose_secret().as_bytes().into())
@ -21,7 +22,7 @@ async fn do_send_webhook(
user_notification: &UserNotification,
webhook: Webhook,
webhook_config: &OutboundWebhook,
net: &kxio::net::Net,
net: &kxio::network::Network,
) {
let message_id = format!("msg_{}", ulid::Ulid::new());
let timestamp = time::OffsetDateTime::now_utc();
@ -34,17 +35,20 @@ async fn do_send_webhook(
.sign(&message_id, timestamp, payload.to_string().as_ref())
.expect("signature");
tracing::info!(?signature, "");
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");
},
|_| (),
);
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");
},
|_| (),
);
}

View file

@ -7,13 +7,13 @@ use git_next_forge_forgejo::ForgeJo;
#[cfg(feature = "github")]
use git_next_forge_github::Github;
use kxio::net::Net;
use kxio::network::Network;
#[derive(Clone, Debug)]
pub struct Forge;
impl Forge {
pub fn create(repo_details: RepoDetails, net: Net) -> Box<dyn ForgeLike> {
pub fn create(repo_details: RepoDetails, net: Network) -> 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 = kxio::net::mock();
let net = Network::new_mock();
let repo_details = given_repo_details(git_next_core::ForgeType::ForgeJo);
let forge = Forge::create(repo_details, net.into());
let forge = Forge::create(repo_details, net);
assert_eq!(forge.name(), "forgejo");
}
#[cfg(feature = "github")]
#[test]
fn test_github_name() {
let net = kxio::net::mock();
let net = Network::new_mock();
let repo_details = given_repo_details(git_next_core::ForgeType::GitHub);
let forge = Forge::create(repo_details, net.into());
let forge = Forge::create(repo_details, net);
assert_eq!(forge.name(), "github");
}

View file

@ -5,14 +5,12 @@ use kxio::fs::FileSystem;
pub fn run(fs: &FileSystem) -> Result<()> {
let pathbuf = fs.base().join(".git-next.toml");
if fs
.path(&pathbuf)
.exists()
.path_exists(&pathbuf)
.with_context(|| format!("Checking for existing file: {pathbuf:?}"))?
{
eprintln!("The configuration file already exists at {pathbuf:?} - not overwritting it.",);
} else {
fs.file(&pathbuf)
.write(include_str!("../default.toml"))
fs.file_write(&pathbuf, 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, net};
use kxio::{fs, network::Network};
#[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 = net::new();
let net = Network::new_real();
let repository_factory = git::repository::factory::real();
let commands = Commands::parse();

View file

@ -1,7 +1,7 @@
//
use actix::prelude::*;
use tracing::{debug, warn, Instrument as _};
use tracing::{debug, Instrument as _};
use crate::{
repo::{
@ -26,13 +26,9 @@ 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 {
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"),
}
let status = forge.commit_status(&next).await;
debug!("got status: {status:?}");
do_send(&addr, ReceiveCIStatus::new((next, status)), log.as_ref());
}
.in_current_span()
.into_actor(self)

View file

@ -70,7 +70,6 @@ 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::net::Net;
use kxio::network::Network;
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: Net,
net: Network,
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: Net,
net: Network,
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::net::MockNet {
kxio::net::mock()
pub fn a_network() -> kxio::network::MockNetwork {
kxio::network::MockNetwork::new()
}
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::TempFileSystem {
kxio::fs::temp().expect("temp fs")
pub fn a_filesystem() -> kxio::fs::FileSystem {
kxio::fs::temp().unwrap_or_else(|e| panic!("{}", e))
}
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::net::Net,
net: kxio::network::Network,
) -> (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(&repo_details.gitdir).create()?;
fs.dir_create(&repo_details.gitdir)?;
//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(&repo_details.gitdir).create()?;
fs.dir_create(&repo_details.gitdir)?;
//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(&repo_details.gitdir).create()?;
fs.dir_create(&repo_details.gitdir)?;
//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(&repo_details.gitdir).create()?;
fs.dir_create(&repo_details.gitdir)?;
//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(&repo_details.gitdir).create()?;
fs.dir_create(&repo_details.gitdir)?;
//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(|_| Ok(status));
.return_once(|_| 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, net::Net};
use kxio::{fs::FileSystem, network::Network};
use std::{
collections::BTreeMap,
@ -52,7 +52,7 @@ pub struct ServerActor {
generation: Generation,
webhook_actor_addr: Option<Addr<WebhookActor>>,
fs: FileSystem,
net: Net,
net: Network,
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: Net,
net: Network,
alerts: Addr<AlertsActor>,
repo: Box<dyn RepositoryFactory>,
sleep_duration: std::time::Duration,
@ -100,14 +100,13 @@ 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);
let path_handle = self.fs.path(&path);
if path_handle.exists()? {
if !path_handle.is_dir()? {
if self.fs.path_exists(&path)? {
if !self.fs.path_is_dir(&path)? {
return Err(Error::ForgeDirIsNotDirectory { path });
}
} else {
tracing::info!(%forge_name, ?path_handle, "creating storage");
self.fs.dir(&path).create_all()?;
tracing::info!(%forge_name, ?path, "creating storage");
self.fs.dir_create_all(&path)?;
}
}
@ -214,7 +213,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(dir).create() {
if let Err(err) = self.fs.dir_create(dir) {
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::TempFileSystem {
kxio::fs::temp().expect("temp fs")
pub fn a_filesystem() -> kxio::fs::FileSystem {
kxio::fs::temp().unwrap_or_else(|e| panic!("{}", e))
}
pub fn a_network() -> kxio::net::MockNet {
kxio::net::mock()
pub fn a_network() -> kxio::network::MockNetwork {
kxio::network::MockNetwork::new()
}
pub fn an_alerts_actor(net: kxio::net::Net) -> Addr<AlertsActor> {
pub fn an_alerts_actor(net: kxio::network::Network) -> 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.as_real(), net.into(), alerts, repo, duration);
let server = ServerActor::new(fs.clone(), 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, net::Net};
use kxio::{fs::FileSystem, network::Network};
use tracing::info;
use std::{
@ -34,14 +34,12 @@ pub fn init(fs: &FileSystem) -> Result<()> {
let file_name = "git-next-server.toml";
let pathbuf = PathBuf::from(file_name);
if fs
.path(&pathbuf)
.exists()
.path_exists(&pathbuf)
.with_context(|| format!("Checking for existing file: {pathbuf:?}"))?
{
eprintln!("The configuration file already exists at {pathbuf:?} - not overwritting it.",);
} else {
fs.file(&pathbuf)
.write(include_str!("server-default.toml"))
fs.file_write(&pathbuf, include_str!("server-default.toml"))
.with_context(|| format!("Writing file: {pathbuf:?}"))?;
println!("Created a default configuration file at {pathbuf:?}",);
}
@ -52,7 +50,7 @@ pub fn init(fs: &FileSystem) -> Result<()> {
pub fn start(
ui: bool,
fs: FileSystem,
net: Net,
net: Network,
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(&file).write("contents")?;
fs.file_write(&file, "contents")?;
crate::init::run(&fs)?;
assert_eq!(
fs.file(&file).reader()?.to_string(),
fs.file_read_to_string(&file)?,
"contents",
"The file has been changed"
);
@ -27,10 +27,10 @@ mod init {
let file = fs.base().join(".git-next.toml");
assert!(fs.path(&file).exists()?, "The file has not been created");
assert!(fs.path_exists(&file)?, "The file has not been created");
assert_eq!(
fs.file(&file).reader()?.to_string(),
fs.file_read_to_string(&file)?,
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(&path).write("foo")?;
fs.file_write(&path, "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(&file).reader()?.to_string();
let str = fs.file_read_to_string(&file)?;
Ok(toml::from_str(&str)?)
}

View file

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

View file

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

View file

@ -32,10 +32,7 @@ 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::webhook::Result<git::forge::commit::Status>;
async fn commit_status(&self, commit: &git::Commit) -> git::forge::commit::Status;
// Lists all the webhooks
async fn list_webhooks(

View file

@ -4,16 +4,8 @@ pub type Result<T> = core::result::Result<T, Error>;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("network")]
Network(#[from] kxio::net::Error),
Network(#[from] kxio::network::NetworkError),
#[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::net::Error),
Network(#[from] kxio::network::NetworkError),
#[error("fetch: {0}")]
Fetch(#[from] git::fetch::Error),

View file

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

View file

@ -172,12 +172,10 @@ 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 = file
.reader()
.expect("read original .git/config")
.to_string();
let contents = fs
.file_read_to_string(&config_file)
.expect("read original .git/config");
let updated_contents = format!(
r#"{contents}
[remote "origin"]
@ -186,7 +184,7 @@ impl TestOpenRepository {
"#
);
#[allow(clippy::expect_used)]
file.write(&updated_contents)
fs.file_write(&config_file, &updated_contents)
.expect("write updated .git/config");
}
}

View file

@ -1,5 +1,3 @@
use std::ops::Deref as _;
use crate::CommitCount;
//
@ -11,7 +9,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.deref().clone(), forge_details);
let test_repository = git::repository::test(fs.clone(), forge_details);
let_assert!(Ok(open_repository) = test_repository.open(&gitdir));
let repo_config = &given::a_repo_config();
let branches = repo_config.branches();
@ -31,7 +29,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.deref().clone(), forge_details);
let test_repository = git::repository::test(fs.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)?;
@ -50,7 +48,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.deref().clone(), forge_details);
let test_repository = git::repository::test(fs.clone(), forge_details);
let_assert!(
Ok(open_repository) = test_repository.open(&gitdir),
"open repository"

View file

@ -1,5 +1,3 @@
use std::ops::Deref as _;
//
use super::*;
@ -13,7 +11,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.deref().clone(), forge_details);
let test_repository = git::repository::test(fs.clone(), forge_details);
let_assert!(Ok(open_repository) = test_repository.open(&gitdir));
then::commit_named_file_to_branch(
&file_name,
@ -37,7 +35,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.deref().clone(), forge_details);
let test_repository = git::repository::test(fs.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(&fs.base().join("config")).reader()?.to_string();
let config = fs.file_read_to_string(&fs.base().join("config"))?;
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(&fs.base().join("config")).reader()?.to_string();
let config = fs.file_read_to_string(&fs.base().join("config"))?;
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(&fs.base().join("config")).reader()?.to_string();
let config = fs.file_read_to_string(&fs.base().join("config"))?;
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(&fs.base().join("config")).reader()?.to_string();
let config = fs.file_read_to_string(&fs.base().join("config"))?;
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::TempFileSystem {
pub fn a_filesystem() -> kxio::fs::FileSystem {
kxio::fs::temp().unwrap_or_else(|e| panic!("{}", e))
}
@ -337,8 +337,7 @@ pub mod given {
let repo = gix::prepare_clone_bare(url, fs.base()).unwrap();
repo.persist();
// load config file
let file = fs.file(&path.join("config"));
let config_file = file.reader().unwrap().to_string();
let config_file = fs.file_read_to_string(&path.join("config")).unwrap();
// add use are origin url
let mut config_lines = config_file.lines().collect::<Vec<_>>();
config_lines.push(r#"[remote "origin"]"#);
@ -346,7 +345,8 @@ pub mod given {
tracing::info!(?url, %url_line, "writing");
config_lines.push(&url_line);
// write config file back out
file.write(config_lines.join("\n").as_str()).unwrap();
fs.file_write(&path.join("config"), 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(&file).write(contents)?;
fs.file_write(&file, 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(&file).write(&word)?;
fs.file_write(&file, &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(&local_branch).reader()?.to_string();
fs.dir(&origin_heads).create_all()?;
fs.file(&remote_branch).write(&contents)?;
let contents = fs.file_read_to_string(&local_branch)?;
fs.dir_create_all(&origin_heads)?;
fs.file_write(&remote_branch, &contents)?;
Ok(())
}
@ -514,7 +514,7 @@ pub mod then {
.join("refs")
.join("heads")
.join(branch_name.to_string().as_str());
let sha = fs.file(&main_ref).reader()?.to_string();
let sha = fs.file_read_to_string(&main_ref)?;
Ok(git::commit::Sha::new(sha.trim().to_string()))
}
}

View file

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

View file

@ -13,16 +13,17 @@ use git_next_core::{
ForgeNotification, RegisteredWebhook, WebhookAuth, WebhookId,
};
use kxio::net::Net;
use kxio::network::{self, Network};
use tracing::warn;
#[derive(Clone, Debug)]
pub struct ForgeJo {
repo_details: git::RepoDetails,
net: Net,
net: Network,
}
impl ForgeJo {
#[must_use]
pub const fn new(repo_details: git::RepoDetails, net: Net) -> Self {
pub const fn new(repo_details: git::RepoDetails, net: Network) -> Self {
Self { repo_details, net }
}
}
@ -51,28 +52,45 @@ impl git::ForgeLike for ForgeJo {
webhook::parse_body(body)
}
async fn commit_status(&self, commit: &git::Commit) -> git::forge::webhook::Result<Status> {
async fn commit_status(&self, commit: &git::Commit) -> 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 = format!(
let url = network::NetUrl::new(format!(
"https://{hostname}/api/v1/repos/{repo_path}/commits/{commit}/status?token={token}"
);
));
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)
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
}
}
}
async fn list_webhooks(
@ -94,17 +112,16 @@ impl git::ForgeLike for ForgeJo {
}
}
#[derive(Debug, Default, serde::Deserialize)]
#[derive(Debug, serde::Deserialize)]
struct CombinedStatus {
pub state: ForgejoState,
}
#[derive(Debug, Default, serde::Deserialize)]
#[derive(Debug, serde::Deserialize)]
enum ForgejoState {
#[serde(rename = "success")]
Success,
#[serde(rename = "pending")]
#[default]
Pending,
#[serde(rename = "failure")]
Failure,

View file

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

View file

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

View file

@ -1,9 +1,8 @@
//
use git_next_core::{git, server::RepoListenUrl, RegisteredWebhook, WebhookAuth, WebhookId};
use kxio::net::Net;
use kxio::network;
use secrecy::ExposeSecret as _;
use serde_json::json;
use tracing::{info, instrument, warn};
use crate::webhook;
@ -13,7 +12,7 @@ use crate::webhook::Hook;
pub async fn register(
repo_details: &git::RepoDetails,
repo_listen_url: &RepoListenUrl,
net: &Net,
net: &network::Network,
) -> git::forge::webhook::Result<RegisteredWebhook> {
let Some(repo_config) = repo_details.repo_config.clone() else {
return Err(git::forge::webhook::Error::NoRepoConfig);
@ -28,27 +27,35 @@ 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 = format!("https://{hostname}/api/v1/repos/{repo_path}/hooks?token={token}");
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 authorisation = WebhookAuth::generate();
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
{
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 {
Ok(response) => {
let Ok(hook) = response.json::<Hook>().await else {
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);
@ -64,5 +71,4 @@ pub async fn register(
Err(git::forge::webhook::Error::FailedToRegister(e.to_string()))
}
}
// Ok(())
}

View file

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

View file

@ -2,46 +2,53 @@
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::webhook::Result<git::forge::commit::Status> {
pub async fn status(github: &github::Github, commit: &git::Commit) -> 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 = 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)
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
}
}
}

View file

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

View file

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

View file

@ -2,6 +2,8 @@
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,
@ -13,36 +15,40 @@ pub async fn list(
let net = &github.net;
let mut page = 1;
loop {
let result = net
.get(format!(
let request = network::NetRequest::new(
network::RequestMethod::Get,
network::NetUrl::new(format!(
"https://api.{hostname}/repos/{}/hooks?page={page}",
repo_details.repo_path,
))
.headers(github::webhook::headers(repo_details.forge.token()))
.send()
.await;
)),
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;
match result {
Ok(response) => match response.json::<Vec<github::GithubHook>>().await {
Err(err) => {
tracing::warn!(?err, "failed");
Ok(response) => {
let Some(list) = 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);
};
if list.is_empty() {
return Ok(ids);
}
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());
}
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,5 +1,3 @@
use std::collections::HashMap;
//
use git_next_core::{git, webhook, ApiToken, BranchName};
@ -18,24 +16,19 @@ pub use unregister::unregister;
#[cfg(test)]
pub use authorisation::sign_body;
pub fn headers(token: &ApiToken) -> HashMap<String, String> {
pub fn headers(token: &ApiToken) -> kxio::network::NetRequestHeaders {
use secrecy::ExposeSecret;
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()),
])
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")
}
#[derive(Debug, serde::Deserialize)]

View file

@ -1,7 +1,8 @@
//
use crate::{self as github, webhook};
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
pub async fn register(
@ -22,48 +23,45 @@ pub async fn register(
let net = &github.net;
let hostname = repo_details.forge.hostname();
let authorisation = WebhookAuth::generate();
match net
.post(format!(
let request = network::NetRequest::new(
network::RequestMethod::Post,
network::NetUrl::new(format!(
"https://api.{hostname}/repos/{}/hooks",
repo_details.repo_path
))
.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
{
)),
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,
))
}
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,6 +2,8 @@
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,
@ -10,13 +12,20 @@ pub async fn unregister(
let net = &github.net;
let repo_details = &github.repo_details;
let hostname = repo_details.forge.hostname();
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(|_| ())
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(|_| ())
}