feat: switch to kameo actor system (dropping actix)
Some checks failed
ci/woodpecker/push/tag-created Pipeline is pending
ci/woodpecker/push/cron-docker-builder Pipeline was successful
Rust / build (map[name:stable]) (push) Failing after 45s
Rust / build (map[name:nightly]) (push) Failing after 52s
ci/woodpecker/push/push-next Pipeline failed
Some checks failed
ci/woodpecker/push/tag-created Pipeline is pending
ci/woodpecker/push/cron-docker-builder Pipeline was successful
Rust / build (map[name:stable]) (push) Failing after 45s
Rust / build (map[name:nightly]) (push) Failing after 52s
ci/woodpecker/push/push-next Pipeline failed
This commit is contained in:
parent
d8c2e9a23f
commit
1bc29c164a
79 changed files with 2358 additions and 1698 deletions
119
Cargo.lock
generated
119
Cargo.lock
generated
|
@ -2,63 +2,6 @@
|
||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 3
|
version = 3
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "actix"
|
|
||||||
version = "0.13.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "de7fa236829ba0841304542f7614c42b80fca007455315c45c785ccfa873a85b"
|
|
||||||
dependencies = [
|
|
||||||
"actix-macros",
|
|
||||||
"actix-rt",
|
|
||||||
"actix_derive",
|
|
||||||
"bitflags 2.6.0",
|
|
||||||
"bytes",
|
|
||||||
"crossbeam-channel",
|
|
||||||
"futures-core",
|
|
||||||
"futures-sink",
|
|
||||||
"futures-task",
|
|
||||||
"futures-util",
|
|
||||||
"log",
|
|
||||||
"once_cell",
|
|
||||||
"parking_lot",
|
|
||||||
"pin-project-lite",
|
|
||||||
"smallvec",
|
|
||||||
"tokio",
|
|
||||||
"tokio-util",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "actix-macros"
|
|
||||||
version = "0.2.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb"
|
|
||||||
dependencies = [
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "actix-rt"
|
|
||||||
version = "2.10.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "24eda4e2a6e042aa4e55ac438a2ae052d3b5da0ecf83d7411e1a368946925208"
|
|
||||||
dependencies = [
|
|
||||||
"actix-macros",
|
|
||||||
"futures-core",
|
|
||||||
"tokio",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "actix_derive"
|
|
||||||
version = "0.6.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b6ac1e58cded18cb28ddc17143c4dea5345b3ad575e14f32f66e4054a56eb271"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "addr2line"
|
name = "addr2line"
|
||||||
version = "0.21.0"
|
version = "0.21.0"
|
||||||
|
@ -807,6 +750,12 @@ version = "1.0.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
|
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dyn-clone"
|
||||||
|
version = "1.0.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "either"
|
name = "either"
|
||||||
version = "1.13.0"
|
version = "1.13.0"
|
||||||
|
@ -1123,8 +1072,6 @@ dependencies = [
|
||||||
name = "git-next"
|
name = "git-next"
|
||||||
version = "0.13.11"
|
version = "0.13.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"actix",
|
|
||||||
"actix-rt",
|
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"assert2",
|
"assert2",
|
||||||
"bon",
|
"bon",
|
||||||
|
@ -1139,6 +1086,7 @@ dependencies = [
|
||||||
"git-next-core",
|
"git-next-core",
|
||||||
"git-next-forge-forgejo",
|
"git-next-forge-forgejo",
|
||||||
"git-next-forge-github",
|
"git-next-forge-github",
|
||||||
|
"kameo",
|
||||||
"kxio",
|
"kxio",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"lettre",
|
"lettre",
|
||||||
|
@ -1171,13 +1119,13 @@ dependencies = [
|
||||||
name = "git-next-core"
|
name = "git-next-core"
|
||||||
version = "0.13.11"
|
version = "0.13.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"actix",
|
|
||||||
"assert2",
|
"assert2",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"derive-with",
|
"derive-with",
|
||||||
"derive_more",
|
"derive_more",
|
||||||
"git-url-parse",
|
"git-url-parse",
|
||||||
"gix",
|
"gix",
|
||||||
|
"kameo",
|
||||||
"kxio",
|
"kxio",
|
||||||
"mockall",
|
"mockall",
|
||||||
"pike",
|
"pike",
|
||||||
|
@ -2747,6 +2695,36 @@ dependencies = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "kameo"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "62237a96597618543798a36ec723eb75c5ac301e2690243fd600be1f5eb3dd2d"
|
||||||
|
dependencies = [
|
||||||
|
"dyn-clone",
|
||||||
|
"futures",
|
||||||
|
"itertools",
|
||||||
|
"kameo_macros",
|
||||||
|
"once_cell",
|
||||||
|
"serde",
|
||||||
|
"tokio",
|
||||||
|
"tokio-stream",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "kameo_macros"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5bbbd8e8d7b02bc67eae0dcbdb82c0a71cc7cc61734059ee3e7439a1ee1e0e85"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kqueue"
|
name = "kqueue"
|
||||||
version = "1.0.8"
|
version = "1.0.8"
|
||||||
|
@ -4354,6 +4332,7 @@ dependencies = [
|
||||||
"signal-hook-registry",
|
"signal-hook-registry",
|
||||||
"socket2",
|
"socket2",
|
||||||
"tokio-macros",
|
"tokio-macros",
|
||||||
|
"tracing",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -4389,6 +4368,17 @@ dependencies = [
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-stream"
|
||||||
|
version = "0.1.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-tungstenite"
|
name = "tokio-tungstenite"
|
||||||
version = "0.21.0"
|
version = "0.21.0"
|
||||||
|
@ -4691,6 +4681,15 @@ version = "0.2.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uuid"
|
||||||
|
version = "1.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.2.15",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "valuable"
|
name = "valuable"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
|
@ -101,9 +101,8 @@ take-until = "0.2"
|
||||||
notify = "7.0"
|
notify = "7.0"
|
||||||
|
|
||||||
# Actors
|
# Actors
|
||||||
actix = "0.13"
|
kameo = "0.13"
|
||||||
actix-rt = "2.9"
|
tokio = { version = "1.37", features = ["full"] }
|
||||||
tokio = { version = "1.37", features = ["rt", "macros"] }
|
|
||||||
|
|
||||||
# email
|
# email
|
||||||
lettre = "0.11"
|
lettre = "0.11"
|
||||||
|
|
|
@ -50,8 +50,7 @@ git-conventional = { workspace = true }
|
||||||
toml = { workspace = true }
|
toml = { workspace = true }
|
||||||
|
|
||||||
# Actors
|
# Actors
|
||||||
actix = { workspace = true }
|
kameo = { workspace = true }
|
||||||
actix-rt = { workspace = true }
|
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
|
|
||||||
# boilerplate
|
# boilerplate
|
||||||
|
|
|
@ -655,6 +655,18 @@ stateDiagram-v2
|
||||||
forge_github --> core
|
forge_github --> core
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Actor Supervision Tree
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
mindmap
|
||||||
|
Root
|
||||||
|
Alerts
|
||||||
|
FileWatcher
|
||||||
|
Server
|
||||||
|
Repo 1
|
||||||
|
Repo 2
|
||||||
|
```
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
`git-next` is released under the [MIT License](./LICENSE).
|
`git-next` is released under the [MIT License](./LICENSE).
|
||||||
|
|
|
@ -1,19 +1,22 @@
|
||||||
//
|
//
|
||||||
use actix::prelude::*;
|
use kameo::message::{Context, Message};
|
||||||
|
use tracing::debug;
|
||||||
use tracing::{info, Instrument as _};
|
|
||||||
|
|
||||||
use crate::alerts::{
|
use crate::alerts::{
|
||||||
desktop::send_desktop_notification, email::send_email, messages::NotifyUser,
|
desktop::send_desktop_notification, email::send_email, messages::NotifyUser,
|
||||||
webhook::send_webhook, AlertsActor,
|
webhook::send_webhook, AlertsActor,
|
||||||
};
|
};
|
||||||
|
|
||||||
impl Handler<NotifyUser> for AlertsActor {
|
impl Message<NotifyUser> for AlertsActor {
|
||||||
type Result = ();
|
type Reply = ();
|
||||||
|
|
||||||
fn handle(&mut self, msg: NotifyUser, ctx: &mut Self::Context) -> Self::Result {
|
async fn handle(
|
||||||
|
&mut self,
|
||||||
|
msg: NotifyUser,
|
||||||
|
_ctx: Context<'_, Self, Self::Reply>,
|
||||||
|
) -> Self::Reply {
|
||||||
let Some(shout) = &self.shout else {
|
let Some(shout) = &self.shout else {
|
||||||
info!("No shout config available");
|
debug!("No shout config available");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let net = self.net.clone();
|
let net = self.net.clone();
|
||||||
|
@ -21,7 +24,6 @@ impl Handler<NotifyUser> for AlertsActor {
|
||||||
let Some(user_notification) = self.history.sendable(msg.peel()) else {
|
let Some(user_notification) = self.history.sendable(msg.peel()) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
async move {
|
|
||||||
if let Some(webhook_config) = shout.webhook() {
|
if let Some(webhook_config) = shout.webhook() {
|
||||||
send_webhook(&user_notification, webhook_config, &net).await;
|
send_webhook(&user_notification, webhook_config, &net).await;
|
||||||
}
|
}
|
||||||
|
@ -34,8 +36,4 @@ impl Handler<NotifyUser> for AlertsActor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.in_current_span()
|
|
||||||
.into_actor(self)
|
|
||||||
.wait(ctx);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
//
|
//
|
||||||
use actix::prelude::*;
|
use kameo::message::{Context, Message};
|
||||||
|
|
||||||
use crate::alerts::{messages::UpdateShout, AlertsActor};
|
use crate::alerts::{messages::UpdateShout, AlertsActor};
|
||||||
|
|
||||||
impl Handler<UpdateShout> for AlertsActor {
|
impl Message<UpdateShout> for AlertsActor {
|
||||||
type Result = ();
|
type Reply = ();
|
||||||
|
|
||||||
fn handle(&mut self, msg: UpdateShout, _ctx: &mut Self::Context) -> Self::Result {
|
async fn handle(
|
||||||
|
&mut self,
|
||||||
|
msg: UpdateShout,
|
||||||
|
_ctx: Context<'_, Self, Self::Reply>,
|
||||||
|
) -> Self::Reply {
|
||||||
self.shout.replace(msg.peel());
|
self.shout.replace(msg.peel());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
//
|
//
|
||||||
use actix::prelude::*;
|
|
||||||
|
|
||||||
use derive_more::derive::Constructor;
|
use derive_more::derive::Constructor;
|
||||||
|
|
||||||
use git_next_core::{git::UserNotification, server::Shout};
|
use git_next_core::{git::UserNotification, server::Shout};
|
||||||
|
|
||||||
pub use history::History;
|
pub use history::History;
|
||||||
|
use kameo::{mailbox::unbounded::UnboundedMailbox, Actor};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
default_on_actor_link_died, default_on_actor_panic, default_on_actor_start,
|
||||||
|
default_on_actor_stop,
|
||||||
|
};
|
||||||
|
|
||||||
mod desktop;
|
mod desktop;
|
||||||
mod email;
|
mod email;
|
||||||
|
@ -24,9 +28,12 @@ pub struct AlertsActor {
|
||||||
history: History, // record of alerts sent recently (e.g. 24 hours)
|
history: History, // record of alerts sent recently (e.g. 24 hours)
|
||||||
net: kxio::net::Net,
|
net: kxio::net::Net,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Actor for AlertsActor {
|
impl Actor for AlertsActor {
|
||||||
type Context = Context<Self>;
|
type Mailbox = UnboundedMailbox<Self>;
|
||||||
|
default_on_actor_start!(this, actor_ref);
|
||||||
|
default_on_actor_panic!(this, actor_ref, err);
|
||||||
|
default_on_actor_link_died!(this, actor_ref, id, reason);
|
||||||
|
default_on_actor_stop!(this, actor_ref, reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn short_message(user_notification: &UserNotification) -> String {
|
fn short_message(user_notification: &UserNotification) -> String {
|
||||||
|
|
19
crates/cli/src/base_actor.rs
Normal file
19
crates/cli/src/base_actor.rs
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
//
|
||||||
|
use kameo::{actor::ActorRef, Actor};
|
||||||
|
|
||||||
|
use crate::{spawn, spawn_in_thread};
|
||||||
|
|
||||||
|
/// Adds a spawn mathod to the actor.
|
||||||
|
pub trait BaseActor: Actor {
|
||||||
|
async fn spawn<Parent: Actor>(self, parent_actor_ref: ActorRef<Parent>) -> ActorRef<Self> {
|
||||||
|
spawn!(parent_actor_ref, self)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn spawn_in_thread<Parent: Actor>(
|
||||||
|
self,
|
||||||
|
parent_actor_ref: ActorRef<Parent>,
|
||||||
|
) -> ActorRef<Self> {
|
||||||
|
spawn_in_thread!(parent_actor_ref, self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<T: Actor> BaseActor for T {}
|
|
@ -1,50 +1,83 @@
|
||||||
//
|
//
|
||||||
use actix::prelude::*;
|
use std::{path::PathBuf, sync::mpsc::Receiver};
|
||||||
|
|
||||||
use actix::Recipient;
|
use anyhow::Context;
|
||||||
use anyhow::{Context, Result};
|
use kameo::{mailbox::unbounded::UnboundedMailbox, message::Message, Actor};
|
||||||
use notify::{event::ModifyKind, Watcher};
|
use notify::{event::ModifyKind, FsEventWatcher, Watcher};
|
||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
|
|
||||||
use std::{
|
use git_next_core::message;
|
||||||
path::PathBuf,
|
|
||||||
sync::{
|
use crate::{
|
||||||
atomic::{AtomicBool, Ordering},
|
default_on_actor_link_died, default_on_actor_panic, default_on_actor_stop, on_actor_start,
|
||||||
Arc,
|
publish, tell, MessageBus,
|
||||||
},
|
|
||||||
time::Duration,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Message)]
|
message!(
|
||||||
#[rtype(result = "()")]
|
FileUpdated,
|
||||||
pub struct FileUpdated;
|
"Notification that watched file has been updated"
|
||||||
|
);
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
message!(Watch, "Watch for the next event on the file");
|
||||||
pub enum Error {
|
|
||||||
#[error("io")]
|
pub struct FileWatcherActor {
|
||||||
Io(#[from] std::io::Error),
|
file_updates_bus: MessageBus<FileUpdated>,
|
||||||
|
file: PathBuf,
|
||||||
|
event_receiver: Option<Receiver<Result<notify::Event, notify::Error>>>,
|
||||||
|
watcher: Option<FsEventWatcher>,
|
||||||
}
|
}
|
||||||
pub fn watch_file(path: PathBuf, recipient: Recipient<FileUpdated>) -> Result<Arc<AtomicBool>> {
|
|
||||||
let (tx, rx) = std::sync::mpsc::channel();
|
|
||||||
let shutdown = Arc::new(AtomicBool::default());
|
|
||||||
|
|
||||||
let mut handler = notify::recommended_watcher(tx).context("file watcher")?;
|
impl FileWatcherActor {
|
||||||
handler
|
pub const fn new(file_updates_bus: MessageBus<FileUpdated>, file: PathBuf) -> Self {
|
||||||
.watch(&path, notify::RecursiveMode::NonRecursive)
|
Self {
|
||||||
.with_context(|| format!("Watching: {path:?}"))?;
|
file_updates_bus,
|
||||||
let thread_shutdown = shutdown.clone();
|
file,
|
||||||
actix_rt::task::spawn_blocking(move || {
|
event_receiver: None,
|
||||||
loop {
|
watcher: None,
|
||||||
if thread_shutdown.load(Ordering::Relaxed) {
|
|
||||||
drop(handler);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
for result in rx.try_iter() {
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Actor for FileWatcherActor {
|
||||||
|
type Mailbox = UnboundedMailbox<Self>;
|
||||||
|
|
||||||
|
on_actor_start!(this, actor_ref, {
|
||||||
|
//
|
||||||
|
let (tx, rx) = std::sync::mpsc::channel();
|
||||||
|
this.event_receiver.replace(rx);
|
||||||
|
|
||||||
|
let mut watcher = notify::recommended_watcher(tx).context("file watcher")?;
|
||||||
|
watcher
|
||||||
|
.watch(&this.file, notify::RecursiveMode::NonRecursive)
|
||||||
|
.with_context(|| format!("Watching: {:?}", this.file))?;
|
||||||
|
this.watcher.replace(watcher);
|
||||||
|
|
||||||
|
tell!("file_watcher", actor_ref, Watch)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
|
||||||
|
default_on_actor_panic!(this, actor_ref, err);
|
||||||
|
default_on_actor_link_died!(this, actor_ref, id, reason);
|
||||||
|
default_on_actor_stop!(this, actor_ref, reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Message<Watch> for FileWatcherActor {
|
||||||
|
type Reply = color_eyre::Result<()>;
|
||||||
|
|
||||||
|
async fn handle(
|
||||||
|
&mut self,
|
||||||
|
msg: Watch,
|
||||||
|
ctx: kameo::message::Context<'_, Self, Self::Reply>,
|
||||||
|
) -> Self::Reply {
|
||||||
|
if let Some(rx) = &self.event_receiver {
|
||||||
|
while let Ok(result) = rx.recv() {
|
||||||
match result {
|
match result {
|
||||||
Ok(event) => match event.kind {
|
Ok(event) => match event.kind {
|
||||||
notify::EventKind::Modify(ModifyKind::Data(_)) => {
|
notify::EventKind::Modify(ModifyKind::Data(_)) => {
|
||||||
info!("File modified");
|
info!("===================================================================================");
|
||||||
recipient.do_send(FileUpdated);
|
info!(?event, "File modified");
|
||||||
|
publish!("file_updates_bus", self.file_updates_bus, FileUpdated)?;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
notify::EventKind::Modify(_)
|
notify::EventKind::Modify(_)
|
||||||
|
@ -55,12 +88,12 @@ pub fn watch_file(path: PathBuf, recipient: Recipient<FileUpdated>) -> Result<Ar
|
||||||
| notify::EventKind::Other => { /* do nothing */ }
|
| notify::EventKind::Other => { /* do nothing */ }
|
||||||
},
|
},
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!(?err, "Watching file: {path:?}");
|
error!(?err, "Watching file: {:?}", self.file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
std::thread::sleep(Duration::from_millis(1000));
|
|
||||||
}
|
}
|
||||||
});
|
tell!("file_watcher", ctx.actor_ref(), msg)?;
|
||||||
Ok(shutdown)
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
201
crates/cli/src/macros/actor.rs
Normal file
201
crates/cli/src/macros/actor.rs
Normal file
|
@ -0,0 +1,201 @@
|
||||||
|
//
|
||||||
|
|
||||||
|
/// Called when the actor starts, before it processes any messages.
|
||||||
|
///
|
||||||
|
/// Messages sent internally by the actor during `on_start` are prioritized and processed
|
||||||
|
/// before any externally sent messages, even if external messages are received first.
|
||||||
|
///
|
||||||
|
/// This ensures that the actor can properly initialize before handling external messages.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// impl Actor for ServerActor {
|
||||||
|
/// type Mailbox = UnboundedMailbox<Self>;
|
||||||
|
///
|
||||||
|
/// on_actor_start!(this, actor_ref, {
|
||||||
|
/// // handle start here
|
||||||
|
/// Ok(())
|
||||||
|
/// });
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! on_actor_start {
|
||||||
|
($this:ident, $actor_ref:ident, $body:expr) => {
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
#[tracing::instrument(skip_all)]
|
||||||
|
async fn on_start(
|
||||||
|
&mut self,
|
||||||
|
actor_ref: kameo::actor::ActorRef<Self>,
|
||||||
|
) -> std::result::Result<(), kameo::error::BoxError> {
|
||||||
|
tracing::debug!(?actor_ref, "{}", <Self as Actor>::name());
|
||||||
|
let $this = self;
|
||||||
|
let $actor_ref = actor_ref;
|
||||||
|
$body
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! default_on_actor_start {
|
||||||
|
($this:ident, $actor_ref:ident) => {
|
||||||
|
$crate::on_actor_start!($this, $actor_ref, { Ok(()) });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called swhen the actor encounters a panic or an error during "tell" message handling.
|
||||||
|
///
|
||||||
|
/// This method gives the actor an opportunity to clean up or reset its state and determine
|
||||||
|
/// whether it should be stopped or continue processing messages.
|
||||||
|
///
|
||||||
|
/// # Parameters
|
||||||
|
/// - `err`: The panic or error that occurred.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// - `Some(ActorStopReason)`: Stops the actor.
|
||||||
|
/// - `None`: Allows the actor to continue processing messages.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// impl Actor for ServerActor {
|
||||||
|
/// type Mailbox = UnboundedMailbox<Self>;
|
||||||
|
///
|
||||||
|
/// on_actor_panic!(this, actor_ref, err, {
|
||||||
|
/// // handle panic here
|
||||||
|
/// Ok(())
|
||||||
|
/// });
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! on_actor_panic {
|
||||||
|
($this:ident, $actor_ref:ident, $err:ident, $body:expr) => {
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
#[tracing::instrument(skip_all)]
|
||||||
|
async fn on_panic(
|
||||||
|
&mut self,
|
||||||
|
actor_ref: kameo::actor::WeakActorRef<Self>,
|
||||||
|
err: kameo::error::PanicError,
|
||||||
|
) -> std::result::Result<Option<kameo::error::ActorStopReason>, kameo::error::BoxError> {
|
||||||
|
tracing::debug!(?actor_ref, %err, "{}", <Self as Actor>::name());
|
||||||
|
let $this = self;
|
||||||
|
let $actor_ref = actor_ref;
|
||||||
|
let $err = err;
|
||||||
|
$body
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! default_on_actor_panic {
|
||||||
|
($this:ident, $actor_ref:ident, $err:ident) => {
|
||||||
|
$crate::on_actor_panic!($this, $actor_ref, $err, {
|
||||||
|
Ok(Some(kameo::error::ActorStopReason::Panicked($err)))
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called when a linked actor dies.
|
||||||
|
///
|
||||||
|
/// By default, the actor will stop if the reason for the linked actor's death is anything other
|
||||||
|
/// than `Normal`. You can customize this behavior in the implementation.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// Whether the actor should stop or continue processing messages.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// impl Actor for ServerActor {
|
||||||
|
/// type Mailbox = UnboundedMailbox<Self>;
|
||||||
|
///
|
||||||
|
/// on_actor_link_died!(this, actor_ref, id, reason, {
|
||||||
|
/// // handle link death here
|
||||||
|
/// Ok(())
|
||||||
|
/// });
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! on_actor_link_died {
|
||||||
|
($this:ident, $actor_ref:ident, $id:ident, $reason:ident, $body:expr) => {
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
#[tracing::instrument(skip_all)]
|
||||||
|
async fn on_link_died(
|
||||||
|
&mut self,
|
||||||
|
actor_ref: kameo::actor::WeakActorRef<Self>,
|
||||||
|
id: kameo::actor::ActorID,
|
||||||
|
reason: kameo::error::ActorStopReason,
|
||||||
|
) -> std::result::Result<Option<kameo::error::ActorStopReason>, kameo::error::BoxError> {
|
||||||
|
tracing::debug!(?actor_ref, %id, %reason, "{}", <Self as Actor>::name());
|
||||||
|
let $this = self;
|
||||||
|
let $actor_ref = actor_ref;
|
||||||
|
let $id = id;
|
||||||
|
let $reason = reason;
|
||||||
|
$body
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! default_on_actor_link_died {
|
||||||
|
($this:ident, $actor_ref:ident, $id:ident, $reason:ident) => {
|
||||||
|
$crate::on_actor_link_died!($this, $actor_ref, $id, $reason, {
|
||||||
|
match &$reason {
|
||||||
|
kameo::error::ActorStopReason::Normal => Ok(None),
|
||||||
|
kameo::error::ActorStopReason::Killed
|
||||||
|
| kameo::error::ActorStopReason::Panicked(_)
|
||||||
|
| kameo::error::ActorStopReason::LinkDied { .. } => {
|
||||||
|
Ok(Some(kameo::error::ActorStopReason::LinkDied {
|
||||||
|
id: $id,
|
||||||
|
reason: Box::new($reason),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called before the actor stops.
|
||||||
|
///
|
||||||
|
/// This allows the actor to perform any necessary cleanup or release resources before being fully stopped.
|
||||||
|
///
|
||||||
|
/// # Parameters
|
||||||
|
/// - `reason`: The reason why the actor is being stopped.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// impl Actor for ServerActor {
|
||||||
|
/// type Mailbox = UnboundedMailbox<Self>;
|
||||||
|
///
|
||||||
|
/// on_actor_stop!(this, actor_ref, id, reason, {
|
||||||
|
/// // handle stop here
|
||||||
|
/// Ok(())
|
||||||
|
/// });
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! on_actor_stop {
|
||||||
|
($this:ident, $actor_ref:ident, $reason:ident, $body:expr) => {
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
#[tracing::instrument(skip_all)]
|
||||||
|
async fn on_stop(
|
||||||
|
&mut self,
|
||||||
|
actor_ref: kameo::actor::WeakActorRef<Self>,
|
||||||
|
reason: kameo::error::ActorStopReason,
|
||||||
|
) -> std::result::Result<(), kameo::error::BoxError> {
|
||||||
|
tracing::debug!(?actor_ref, %reason, "{}", <Self as Actor>::name());
|
||||||
|
let $this = self;
|
||||||
|
let $actor_ref = actor_ref;
|
||||||
|
let $reason = reason;
|
||||||
|
$body
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! default_on_actor_stop {
|
||||||
|
($this:ident, $actor_ref:ident, $reason:ident) => {
|
||||||
|
$crate::on_actor_stop!($this, $actor_ref, $reason, { Ok(()) });
|
||||||
|
};
|
||||||
|
}
|
3
crates/cli/src/macros/mod.rs
Normal file
3
crates/cli/src/macros/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
mod actor;
|
||||||
|
mod send;
|
||||||
|
mod spawn;
|
53
crates/cli/src/macros/send.rs
Normal file
53
crates/cli/src/macros/send.rs
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
//
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! tell {
|
||||||
|
($actor_ref:expr, $message:expr) => {
|
||||||
|
tell!(stringify!($actor_ref), $actor_ref, $message)
|
||||||
|
};
|
||||||
|
($actor_name:expr, $actor_ref:expr, $message:expr) => {{
|
||||||
|
tracing::debug!(actor = $actor_name, msg = stringify!($message), "send");
|
||||||
|
$actor_ref.tell($message).await
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! subscribe {
|
||||||
|
($message_bus:expr, $actor_ref:expr) => {
|
||||||
|
subscribe!(
|
||||||
|
stringify!($message_bus),
|
||||||
|
$message_bus,
|
||||||
|
stringify!($actor_ref),
|
||||||
|
$actor_ref
|
||||||
|
)
|
||||||
|
};
|
||||||
|
($message_bus:expr, $actor_name:expr, $actor_ref:expr) => {
|
||||||
|
subscribe!(
|
||||||
|
stringify!($message_bus),
|
||||||
|
$message_bus,
|
||||||
|
$actor_name,
|
||||||
|
$actor_ref
|
||||||
|
)
|
||||||
|
};
|
||||||
|
($bus_name:expr, $message_bus:expr, $actor_ref:expr) => {
|
||||||
|
subscribe!($bus_name, $message_bus, stringify!($actor_ref), $actor_ref)
|
||||||
|
};
|
||||||
|
($bus_name:expr, $message_bus:expr, $actor_name:expr, $actor_ref:expr) => {{
|
||||||
|
tracing::debug!(msg_bus = $bus_name, actor = $actor_name, "subscribe");
|
||||||
|
$message_bus
|
||||||
|
.tell(kameo::actor::pubsub::Subscribe($actor_ref))
|
||||||
|
.await
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! publish {
|
||||||
|
($message_bus:expr, $message:expr) => {
|
||||||
|
publish!(stringify!($message_bus), $message_bus, $message)
|
||||||
|
};
|
||||||
|
($bus_name:expr, $message_bus:expr, $message:expr) => {{
|
||||||
|
tracing::debug!(bus = $bus_name, msg = stringify!($message), "publish");
|
||||||
|
$message_bus
|
||||||
|
.tell(kameo::actor::pubsub::Publish($message))
|
||||||
|
.await
|
||||||
|
}};
|
||||||
|
}
|
25
crates/cli/src/macros/spawn.rs
Normal file
25
crates/cli/src/macros/spawn.rs
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
//
|
||||||
|
/// spawns a new actor and sets up bi-directional links
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! spawn {
|
||||||
|
($parent:expr, $actor:expr) => {{
|
||||||
|
tracing::debug!("spawning : {}", stringify!($actor));
|
||||||
|
let new_actor_ref = kameo::spawn($actor);
|
||||||
|
new_actor_ref.link(&$parent).await;
|
||||||
|
$parent.link(&new_actor_ref).await;
|
||||||
|
tracing::debug!("spawned : {}", stringify!($actor));
|
||||||
|
new_actor_ref
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! spawn_in_thread {
|
||||||
|
($parent:expr, $actor:expr) => {{
|
||||||
|
tracing::debug!("spawning in thread : {}", stringify!($actor));
|
||||||
|
let new_actor_ref = kameo::actor::spawn_in_thread($actor);
|
||||||
|
new_actor_ref.link(&$parent).await;
|
||||||
|
$parent.link(&new_actor_ref).await;
|
||||||
|
tracing::debug!("spawned in thread : {}", stringify!($actor));
|
||||||
|
new_actor_ref
|
||||||
|
}};
|
||||||
|
}
|
|
@ -2,10 +2,13 @@
|
||||||
#![allow(clippy::module_name_repetitions)]
|
#![allow(clippy::module_name_repetitions)]
|
||||||
|
|
||||||
mod alerts;
|
mod alerts;
|
||||||
|
mod base_actor;
|
||||||
mod file_watcher;
|
mod file_watcher;
|
||||||
mod forge;
|
mod forge;
|
||||||
mod init;
|
mod init;
|
||||||
|
mod macros;
|
||||||
mod repo;
|
mod repo;
|
||||||
|
mod root;
|
||||||
mod server;
|
mod server;
|
||||||
|
|
||||||
#[cfg(feature = "tui")]
|
#[cfg(feature = "tui")]
|
||||||
|
@ -13,9 +16,11 @@ mod tui;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
mod webhook;
|
mod webhook;
|
||||||
|
|
||||||
use git_next_core::git;
|
use git_next_core::git;
|
||||||
|
use kameo::actor::{pubsub::PubSub, ActorRef};
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
@ -23,6 +28,8 @@ use clap::Parser;
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
use kxio::{fs, net};
|
use kxio::{fs, net};
|
||||||
|
|
||||||
|
pub type MessageBus<T> = ActorRef<PubSub<T>>;
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[clap(version = clap::crate_version!(), author = clap::crate_authors!(), about = clap::crate_description!())]
|
#[clap(version = clap::crate_version!(), author = clap::crate_authors!(), about = clap::crate_description!())]
|
||||||
struct Commands {
|
struct Commands {
|
||||||
|
@ -47,6 +54,8 @@ enum Server {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
|
color_eyre::install()?;
|
||||||
|
|
||||||
let fs = fs::new(PathBuf::default());
|
let fs = fs::new(PathBuf::default());
|
||||||
let net = net::new();
|
let net = net::new();
|
||||||
let repository_factory = git::repository::factory::real();
|
let repository_factory = git::repository::factory::real();
|
||||||
|
@ -59,16 +68,16 @@ fn main() -> Result<()> {
|
||||||
#[cfg(not(feature = "tui"))]
|
#[cfg(not(feature = "tui"))]
|
||||||
Server::Start {} => server::start(
|
Server::Start {} => server::start(
|
||||||
false,
|
false,
|
||||||
fs,
|
&fs,
|
||||||
net,
|
&net,
|
||||||
repository_factory,
|
repository_factory,
|
||||||
std::time::Duration::from_secs(10),
|
std::time::Duration::from_secs(10),
|
||||||
),
|
),
|
||||||
#[cfg(feature = "tui")]
|
#[cfg(feature = "tui")]
|
||||||
Server::Start { ui } => server::start(
|
Server::Start { ui } => server::start(
|
||||||
ui,
|
ui,
|
||||||
fs,
|
&fs,
|
||||||
net,
|
&net,
|
||||||
repository_factory,
|
repository_factory,
|
||||||
std::time::Duration::from_secs(10),
|
std::time::Duration::from_secs(10),
|
||||||
),
|
),
|
||||||
|
|
|
@ -20,7 +20,7 @@ pub fn advance_next(
|
||||||
commit: Option<Commit>,
|
commit: Option<Commit>,
|
||||||
force: git_next_core::git::push::Force,
|
force: git_next_core::git::push::Force,
|
||||||
repo_details: &RepoDetails,
|
repo_details: &RepoDetails,
|
||||||
repo_config: RepoConfig,
|
repo_config: &RepoConfig,
|
||||||
open_repository: &dyn OpenRepositoryLike,
|
open_repository: &dyn OpenRepositoryLike,
|
||||||
message_token: MessageToken,
|
message_token: MessageToken,
|
||||||
) -> Result<MessageToken> {
|
) -> Result<MessageToken> {
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
//
|
//
|
||||||
use actix::prelude::*;
|
|
||||||
|
|
||||||
use git_next_core::{git, RepoConfigSource};
|
use git_next_core::{git, RepoConfigSource};
|
||||||
|
|
||||||
|
use color_eyre::Result;
|
||||||
|
use kameo::message::{Context, Message};
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -15,45 +15,55 @@ use crate::{
|
||||||
server::actor::messages::RepoUpdate,
|
server::actor::messages::RepoUpdate,
|
||||||
};
|
};
|
||||||
|
|
||||||
impl Handler<AdvanceMain> for RepoActor {
|
impl Message<AdvanceMain> for RepoActor {
|
||||||
type Result = ();
|
type Reply = Result<()>;
|
||||||
#[tracing::instrument(name = "RepoActor::AdvanceMainTo", skip_all, fields(repo = %self.repo_details, commit = ?msg))]
|
#[tracing::instrument(name = "RepoActor::AdvanceMainTo", skip_all, fields(repo = %self.repo_details, commit = ?msg))]
|
||||||
fn handle(&mut self, msg: AdvanceMain, ctx: &mut Self::Context) -> Self::Result {
|
async fn handle(
|
||||||
|
&mut self,
|
||||||
|
msg: AdvanceMain,
|
||||||
|
ctx: Context<'_, Self, Self::Reply>,
|
||||||
|
) -> Self::Reply {
|
||||||
let Some(repo_config) = self.repo_details.repo_config.clone() else {
|
let Some(repo_config) = self.repo_details.repo_config.clone() else {
|
||||||
warn!("No config loaded");
|
warn!("No config loaded");
|
||||||
return;
|
return Ok(());
|
||||||
};
|
};
|
||||||
let Some(open_repository) = &self.open_repository else {
|
let Some(open_repository) = &self.open_repository else {
|
||||||
return;
|
return Ok(());
|
||||||
};
|
};
|
||||||
let repo_details = self.repo_details.clone();
|
|
||||||
let addr = ctx.address();
|
|
||||||
let message_token = self.message_token;
|
|
||||||
let commit = msg.peel();
|
let commit = msg.peel();
|
||||||
|
|
||||||
self.update_tui(RepoUpdate::AdvancingMain {
|
self.update_tui(RepoUpdate::AdvancingMain {
|
||||||
commit: commit.clone(),
|
commit: commit.clone(),
|
||||||
});
|
})
|
||||||
|
.await?;
|
||||||
if let Err(err) = advance_main(commit, &repo_details, &repo_config, &**open_repository) {
|
if let Err(err) = advance_main(commit, &self.repo_details, &repo_config, &**open_repository)
|
||||||
|
{
|
||||||
warn!("advance main: {err}");
|
warn!("advance main: {err}");
|
||||||
self.alert_tui(format!("advance main: {err}"));
|
self.alert_tui(format!("advance main: {err}")).await?;
|
||||||
} else {
|
} else {
|
||||||
self.update_tui(RepoUpdate::MainUpdated);
|
self.update_tui(RepoUpdate::MainUpdated).await?;
|
||||||
if let Some(open_repository) = &self.open_repository {
|
if let Some(open_repository) = &self.open_repository {
|
||||||
match open_repository.fetch() {
|
match open_repository.fetch() {
|
||||||
Ok(()) => self.update_tui_log(git::graph::log(&self.repo_details)),
|
Ok(()) => {
|
||||||
Err(err) => self.alert_tui(format!("fetching: {err}")),
|
self.update_tui_log(git::graph::log(&self.repo_details))
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
Err(err) => self.alert_tui(format!("fetching: {err}")).await?,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
match repo_config.source() {
|
match repo_config.source() {
|
||||||
RepoConfigSource::Repo => {
|
RepoConfigSource::Repo => {
|
||||||
do_send(&addr, LoadConfigFromRepo, self.log.as_ref());
|
do_send(&ctx.actor_ref(), LoadConfigFromRepo, self.log.as_ref()).await?;
|
||||||
}
|
}
|
||||||
RepoConfigSource::Server => {
|
RepoConfigSource::Server => {
|
||||||
do_send(&addr, ValidateRepo::new(message_token), self.log.as_ref());
|
do_send(
|
||||||
|
&ctx.actor_ref(),
|
||||||
|
ValidateRepo::new(self.message_token),
|
||||||
|
self.log.as_ref(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
//
|
//
|
||||||
use actix::prelude::*;
|
use color_eyre::Result;
|
||||||
|
use kameo::message::{Context, Message};
|
||||||
|
use tracing::warn;
|
||||||
|
|
||||||
use git_next_core::git;
|
use git_next_core::git;
|
||||||
use tracing::{warn, Instrument};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
repo::{
|
repo::{
|
||||||
|
@ -14,15 +15,19 @@ use crate::{
|
||||||
server::actor::messages::RepoUpdate,
|
server::actor::messages::RepoUpdate,
|
||||||
};
|
};
|
||||||
|
|
||||||
impl Handler<AdvanceNext> for RepoActor {
|
impl Message<AdvanceNext> for RepoActor {
|
||||||
type Result = ();
|
type Reply = Result<()>;
|
||||||
|
|
||||||
fn handle(&mut self, msg: AdvanceNext, ctx: &mut Self::Context) -> Self::Result {
|
async fn handle(
|
||||||
|
&mut self,
|
||||||
|
msg: AdvanceNext,
|
||||||
|
ctx: Context<'_, Self, Self::Reply>,
|
||||||
|
) -> Self::Reply {
|
||||||
let Some(repo_config) = &self.repo_details.repo_config else {
|
let Some(repo_config) = &self.repo_details.repo_config else {
|
||||||
return;
|
return Ok(());
|
||||||
};
|
};
|
||||||
let Some(open_repository) = &self.open_repository else {
|
let Some(open_repository) = &self.open_repository else {
|
||||||
return;
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
|
||||||
let AdvanceNextPayload {
|
let AdvanceNextPayload {
|
||||||
|
@ -30,45 +35,44 @@ impl Handler<AdvanceNext> for RepoActor {
|
||||||
main,
|
main,
|
||||||
dev_commit_history,
|
dev_commit_history,
|
||||||
} = msg.peel();
|
} = msg.peel();
|
||||||
let repo_details = self.repo_details.clone();
|
|
||||||
let repo_config = repo_config.clone();
|
|
||||||
let addr = ctx.address();
|
|
||||||
|
|
||||||
let (commit, force) = find_next_commit_on_dev(&next, &main, &dev_commit_history);
|
let (commit, force) = find_next_commit_on_dev(&next, &main, &dev_commit_history);
|
||||||
if let Some(commit) = &commit {
|
if let Some(commit) = &commit {
|
||||||
self.update_tui(RepoUpdate::AdvancingNext {
|
self.update_tui(RepoUpdate::AdvancingNext {
|
||||||
commit: commit.clone(),
|
commit: commit.clone(),
|
||||||
force: force.clone(),
|
force: force.clone(),
|
||||||
});
|
})
|
||||||
|
.await?;
|
||||||
};
|
};
|
||||||
match advance_next(
|
match advance_next(
|
||||||
commit,
|
commit,
|
||||||
force,
|
force,
|
||||||
&repo_details,
|
&self.repo_details,
|
||||||
repo_config,
|
repo_config,
|
||||||
&**open_repository,
|
&**open_repository,
|
||||||
self.message_token,
|
self.message_token,
|
||||||
) {
|
) {
|
||||||
Ok(message_token) => {
|
Ok(message_token) => {
|
||||||
self.update_tui(RepoUpdate::NextUpdated);
|
self.update_tui(RepoUpdate::NextUpdated).await?;
|
||||||
match open_repository.fetch() {
|
match open_repository.fetch() {
|
||||||
Ok(()) => self.update_tui_log(git::graph::log(&self.repo_details)),
|
Ok(()) => {
|
||||||
Err(err) => self.alert_tui(format!("fetching: {err}")),
|
self.update_tui_log(git::graph::log(&self.repo_details))
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
Err(err) => self.alert_tui(format!("fetching: {err}")).await?,
|
||||||
}
|
}
|
||||||
// INFO: pause to allow any CI checks to be started
|
// INFO: pause to allow any CI checks to be started
|
||||||
let sleep_duration = self.sleep_duration;
|
tokio::time::sleep(self.sleep_duration).await;
|
||||||
let log = self.log.clone();
|
Ok(do_send(
|
||||||
async move {
|
&ctx.actor_ref(),
|
||||||
actix_rt::time::sleep(sleep_duration).await;
|
ValidateRepo::new(message_token),
|
||||||
do_send(&addr, ValidateRepo::new(message_token), log.as_ref());
|
self.log.as_ref(),
|
||||||
}
|
)
|
||||||
.in_current_span()
|
.await?)
|
||||||
.into_actor(self)
|
|
||||||
.wait(ctx);
|
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
warn!("advance next: {err}");
|
warn!("advance next: {err}");
|
||||||
self.alert_tui(err.to_string());
|
self.alert_tui(err.to_string()).await?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
//
|
//
|
||||||
use actix::prelude::*;
|
use color_eyre::Result;
|
||||||
|
use kameo::message::{Context, Message};
|
||||||
use tracing::{debug, warn, Instrument as _};
|
use tracing::{debug, warn};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
repo::{
|
repo::{
|
||||||
|
@ -12,30 +12,30 @@ use crate::{
|
||||||
server::actor::messages::RepoUpdate,
|
server::actor::messages::RepoUpdate,
|
||||||
};
|
};
|
||||||
|
|
||||||
impl Handler<CheckCIStatus> for RepoActor {
|
impl Message<CheckCIStatus> for RepoActor {
|
||||||
type Result = ();
|
type Reply = Result<()>;
|
||||||
|
|
||||||
fn handle(&mut self, msg: CheckCIStatus, ctx: &mut Self::Context) -> Self::Result {
|
async fn handle(
|
||||||
crate::repo::logger(self.log.as_ref(), "start: CheckCIStatus");
|
&mut self,
|
||||||
|
msg: CheckCIStatus,
|
||||||
let addr = ctx.address();
|
ctx: Context<'_, Self, Self::Reply>,
|
||||||
let forge = self.forge.duplicate();
|
) -> Self::Reply {
|
||||||
|
crate::repo::logger(self.log.as_ref(), "start: CheckCIStatus").await;
|
||||||
let next = msg.peel();
|
let next = msg.peel();
|
||||||
let log = self.log.clone();
|
self.update_tui(RepoUpdate::CheckingCI).await?;
|
||||||
|
|
||||||
self.update_tui(RepoUpdate::CheckingCI);
|
|
||||||
// get the status - pass, fail, pending (all others map to fail, e.g. error)
|
// get the status - pass, fail, pending (all others map to fail, e.g. error)
|
||||||
async move {
|
match self.forge.commit_status(&next).await {
|
||||||
match forge.commit_status(&next).await {
|
|
||||||
Ok(status) => {
|
Ok(status) => {
|
||||||
debug!("got status: {status:?}");
|
debug!("got status: {status:?}");
|
||||||
do_send(&addr, ReceiveCIStatus::new((next, status)), log.as_ref());
|
do_send(
|
||||||
|
&ctx.actor_ref(),
|
||||||
|
ReceiveCIStatus::new((next, status)),
|
||||||
|
self.log.as_ref(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
Err(err) => warn!(?err, "fetching commit status"),
|
Err(err) => warn!(?err, "fetching commit status"),
|
||||||
}
|
}
|
||||||
}
|
Ok(())
|
||||||
.in_current_span()
|
|
||||||
.into_actor(self)
|
|
||||||
.wait(ctx);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
//
|
//
|
||||||
use actix::prelude::*;
|
use color_eyre::Result;
|
||||||
|
use kameo::message::{Context, Message};
|
||||||
|
use tracing::{debug, instrument, warn};
|
||||||
|
|
||||||
use git_next_core::git;
|
use git_next_core::git;
|
||||||
use tracing::{debug, instrument, warn};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
repo::{
|
repo::{
|
||||||
|
@ -13,31 +14,38 @@ use crate::{
|
||||||
server::actor::messages::RepoUpdate,
|
server::actor::messages::RepoUpdate,
|
||||||
};
|
};
|
||||||
|
|
||||||
impl Handler<CloneRepo> for RepoActor {
|
impl Message<CloneRepo> for RepoActor {
|
||||||
type Result = ();
|
type Reply = Result<()>;
|
||||||
#[instrument(name = "RepoActor::CloneRepo", skip_all, fields(repo = %self.repo_details))]
|
#[instrument(name = "RepoActor::CloneRepo", skip_all, fields(repo = %self.repo_details))]
|
||||||
fn handle(&mut self, _msg: CloneRepo, ctx: &mut Self::Context) -> Self::Result {
|
async fn handle(
|
||||||
logger(self.log.as_ref(), "Handler: CloneRepo: start");
|
&mut self,
|
||||||
self.update_tui(RepoUpdate::Opening);
|
_msg: CloneRepo,
|
||||||
|
ctx: Context<'_, Self, Self::Reply>,
|
||||||
|
) -> Self::Reply {
|
||||||
|
logger(self.log.as_ref(), "Handler: CloneRepo: start").await;
|
||||||
|
self.update_tui(RepoUpdate::Opening).await?;
|
||||||
debug!("Handler: CloneRepo: start");
|
debug!("Handler: CloneRepo: start");
|
||||||
match git::repository::open(&*self.repository_factory, &self.repo_details) {
|
match git::repository::open(&*self.repository_factory, &self.repo_details) {
|
||||||
Ok(repository) => {
|
Ok(repository) => {
|
||||||
logger(self.log.as_ref(), "open okay");
|
logger(self.log.as_ref(), "open okay").await;
|
||||||
debug!("open okay");
|
debug!("open okay");
|
||||||
self.update_tui(RepoUpdate::Opened);
|
self.update_tui(RepoUpdate::Opened).await?;
|
||||||
self.open_repository.replace(repository);
|
self.open_repository.replace(repository);
|
||||||
if self.repo_details.repo_config.is_none() {
|
if self.repo_details.repo_config.is_none() {
|
||||||
do_send(&ctx.address(), LoadConfigFromRepo, self.log.as_ref());
|
debug!("no repo config, need to load from repo");
|
||||||
|
do_send(&ctx.actor_ref(), LoadConfigFromRepo, self.log.as_ref()).await?;
|
||||||
} else {
|
} else {
|
||||||
do_send(&ctx.address(), RegisterWebhook::new(), self.log.as_ref());
|
debug!("have repo config");
|
||||||
|
do_send(&ctx.actor_ref(), RegisterWebhook::new(), self.log.as_ref()).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
logger(self.log.as_ref(), "open failed");
|
logger(self.log.as_ref(), "open failed").await;
|
||||||
warn!("Could not open repo: {err:?}");
|
warn!("Could not open repo: {err:?}");
|
||||||
self.alert_tui(err.to_string());
|
self.alert_tui(err.to_string()).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
debug!("Handler: CloneRepo: finish");
|
debug!("Handler: CloneRepo: finish");
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
//
|
//
|
||||||
use actix::prelude::*;
|
use color_eyre::Result;
|
||||||
|
use kameo::message::{Context, Message};
|
||||||
|
use tracing::{debug, instrument};
|
||||||
|
|
||||||
use git_next_core::git::UserNotification;
|
use git_next_core::git::UserNotification;
|
||||||
|
|
||||||
use tracing::{debug, instrument, Instrument as _};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
repo::{
|
repo::{
|
||||||
do_send, load,
|
do_send, load,
|
||||||
|
@ -14,41 +14,43 @@ use crate::{
|
||||||
server::actor::messages::RepoUpdate,
|
server::actor::messages::RepoUpdate,
|
||||||
};
|
};
|
||||||
|
|
||||||
impl Handler<LoadConfigFromRepo> for RepoActor {
|
impl Message<LoadConfigFromRepo> for RepoActor {
|
||||||
type Result = ();
|
type Reply = Result<()>;
|
||||||
|
|
||||||
#[instrument(name = "Repocrate::repo::LoadConfigFromRepo", skip_all, fields(repo = %self.repo_details))]
|
#[instrument(name = "Repocrate::repo::LoadConfigFromRepo", skip_all, fields(repo = %self.repo_details))]
|
||||||
fn handle(&mut self, _msg: LoadConfigFromRepo, ctx: &mut Self::Context) -> Self::Result {
|
async fn handle(
|
||||||
|
&mut self,
|
||||||
|
_msg: LoadConfigFromRepo,
|
||||||
|
ctx: Context<'_, Self, Self::Reply>,
|
||||||
|
) -> Self::Reply {
|
||||||
debug!("Handler: LoadConfigFromRepo: start");
|
debug!("Handler: LoadConfigFromRepo: start");
|
||||||
self.update_tui(RepoUpdate::LoadingConfigFromRepo);
|
self.update_tui(RepoUpdate::LoadingConfigFromRepo).await?;
|
||||||
let Some(open_repository) = &self.open_repository else {
|
let Some(open_repository) = &self.open_repository else {
|
||||||
return;
|
return Ok(());
|
||||||
};
|
};
|
||||||
let open_repository = open_repository.duplicate();
|
match load::config_from_repository(&self.repo_details, &**open_repository).await {
|
||||||
let repo_details = self.repo_details.clone();
|
|
||||||
let forge_alias = repo_details.forge.forge_alias().clone();
|
|
||||||
let repo_alias = repo_details.repo_alias.clone();
|
|
||||||
let addr = ctx.address();
|
|
||||||
let notify_user_recipient = self.notify_user_recipient.clone();
|
|
||||||
let log = self.log.clone();
|
|
||||||
async move {
|
|
||||||
match load::config_from_repository(repo_details, &*open_repository).await {
|
|
||||||
Ok(repo_config) => {
|
Ok(repo_config) => {
|
||||||
do_send(&addr, ReceiveRepoConfig::new(repo_config), log.as_ref());
|
do_send(
|
||||||
|
&ctx.actor_ref(),
|
||||||
|
ReceiveRepoConfig::new(repo_config),
|
||||||
|
self.log.as_ref(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
Err(err) => notify_user(
|
Err(err) => {
|
||||||
notify_user_recipient.as_ref(),
|
notify_user(
|
||||||
|
self.notify_user_recipient.as_ref(),
|
||||||
UserNotification::RepoConfigLoadFailure {
|
UserNotification::RepoConfigLoadFailure {
|
||||||
forge_alias,
|
forge_alias: self.repo_details.forge.forge_alias().clone(),
|
||||||
repo_alias,
|
repo_alias: self.repo_details.repo_alias.clone(),
|
||||||
reason: err.to_string(),
|
reason: err.to_string(),
|
||||||
},
|
},
|
||||||
log.as_ref(),
|
self.log.as_ref(),
|
||||||
),
|
)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.in_current_span()
|
|
||||||
.into_actor(self)
|
|
||||||
.wait(ctx);
|
|
||||||
debug!("Handler: LoadConfigFromRepo: finish");
|
debug!("Handler: LoadConfigFromRepo: finish");
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
//
|
//
|
||||||
use actix::prelude::*;
|
use color_eyre::Result;
|
||||||
|
use kameo::message::{Context, Message};
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
use git_next_core::git::{forge::commit::Status, graph, UserNotification};
|
use git_next_core::git::{forge::commit::Status, graph, UserNotification};
|
||||||
use tracing::{debug, Instrument};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
repo::{
|
repo::{
|
||||||
|
@ -13,64 +14,64 @@ use crate::{
|
||||||
server::actor::messages::RepoUpdate,
|
server::actor::messages::RepoUpdate,
|
||||||
};
|
};
|
||||||
|
|
||||||
impl Handler<ReceiveCIStatus> for RepoActor {
|
impl Message<ReceiveCIStatus> for RepoActor {
|
||||||
type Result = ();
|
type Reply = Result<()>;
|
||||||
|
|
||||||
fn handle(&mut self, msg: ReceiveCIStatus, ctx: &mut Self::Context) -> Self::Result {
|
async fn handle(
|
||||||
logger(self.log.as_ref(), "start: ReceiveCIStatus");
|
&mut self,
|
||||||
|
msg: ReceiveCIStatus,
|
||||||
|
ctx: Context<'_, Self, Self::Reply>,
|
||||||
|
) -> Self::Reply {
|
||||||
|
logger(self.log.as_ref(), "start: ReceiveCIStatus").await;
|
||||||
let (next, status) = msg.peel();
|
let (next, status) = msg.peel();
|
||||||
self.update_tui(RepoUpdate::ReceiveCIStatus {
|
self.update_tui(RepoUpdate::ReceiveCIStatus {
|
||||||
status: status.clone(),
|
status: status.clone(),
|
||||||
});
|
})
|
||||||
|
.await?;
|
||||||
debug!(?status, "");
|
debug!(?status, "");
|
||||||
let graph_log = graph::log(&self.repo_details);
|
let graph_log = graph::log(&self.repo_details);
|
||||||
self.update_tui_log(graph_log.clone());
|
self.update_tui_log(graph_log.clone()).await?;
|
||||||
|
|
||||||
let addr = ctx.address();
|
|
||||||
let forge_alias = self.repo_details.forge.forge_alias().clone();
|
|
||||||
let repo_alias = self.repo_details.repo_alias.clone();
|
|
||||||
let message_token = self.message_token;
|
|
||||||
let sleep_duration = self.sleep_duration;
|
|
||||||
match status {
|
match status {
|
||||||
Status::Pass => {
|
Status::Pass => {
|
||||||
do_send(&addr, AdvanceMain::new(next), self.log.as_ref());
|
do_send(&ctx.actor_ref(), AdvanceMain::new(next), self.log.as_ref()).await?;
|
||||||
}
|
}
|
||||||
Status::Pending => {
|
Status::Pending => {
|
||||||
let log = self.log.clone();
|
tokio::time::sleep(self.sleep_duration).await;
|
||||||
async move {
|
do_send(
|
||||||
actix_rt::time::sleep(sleep_duration).await;
|
&ctx.actor_ref(),
|
||||||
do_send(&addr, ValidateRepo::new(message_token), log.as_ref());
|
ValidateRepo::new(self.message_token),
|
||||||
}
|
self.log.as_ref(),
|
||||||
.in_current_span()
|
)
|
||||||
.into_actor(self)
|
.await?;
|
||||||
.wait(ctx);
|
|
||||||
}
|
}
|
||||||
Status::Fail => {
|
Status::Fail => {
|
||||||
tracing::warn!("Checks have failed");
|
tracing::warn!("Checks have failed");
|
||||||
|
|
||||||
notify_user(
|
notify_user(
|
||||||
self.notify_user_recipient.as_ref(),
|
self.notify_user_recipient.as_ref(),
|
||||||
UserNotification::CICheckFailed {
|
UserNotification::CICheckFailed {
|
||||||
forge_alias,
|
forge_alias: self.repo_details.forge.forge_alias().clone(),
|
||||||
repo_alias,
|
repo_alias: self.repo_details.repo_alias.clone(),
|
||||||
commit: next,
|
commit: next,
|
||||||
log: graph_log,
|
log: graph_log,
|
||||||
},
|
},
|
||||||
self.log.as_ref(),
|
self.log.as_ref(),
|
||||||
);
|
)
|
||||||
let log = self.log.clone();
|
.await?;
|
||||||
async move {
|
|
||||||
debug!("sleeping before retrying...");
|
debug!("sleeping before retrying...");
|
||||||
logger(log.as_ref(), "before sleep");
|
logger(self.log.clone().as_ref(), "before sleep").await;
|
||||||
actix_rt::time::sleep(sleep_duration).await;
|
tokio::time::sleep(self.sleep_duration).await;
|
||||||
logger(log.as_ref(), "after sleep");
|
logger(self.log.clone().as_ref(), "after sleep").await;
|
||||||
do_send(&addr, ValidateRepo::new(message_token), log.as_ref());
|
do_send(
|
||||||
|
&ctx.actor_ref(),
|
||||||
|
ValidateRepo::new(self.message_token),
|
||||||
|
self.log.as_ref(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
.in_current_span()
|
Status::Error(err) => {
|
||||||
.into_actor(self)
|
tracing::warn!(?err, "Check CI Status");
|
||||||
.wait(ctx);
|
|
||||||
}
|
}
|
||||||
Status::Error(_) => todo!(),
|
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
//
|
//
|
||||||
use actix::prelude::*;
|
use color_eyre::Result;
|
||||||
|
use kameo::message::{Context, Message};
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -11,16 +12,22 @@ use crate::{
|
||||||
server::actor::messages::RepoUpdate,
|
server::actor::messages::RepoUpdate,
|
||||||
};
|
};
|
||||||
|
|
||||||
impl Handler<ReceiveRepoConfig> for RepoActor {
|
impl Message<ReceiveRepoConfig> for RepoActor {
|
||||||
type Result = ();
|
type Reply = Result<()>;
|
||||||
#[instrument(name = "RepoActor::ReceiveRepoConfig", skip_all, fields(repo = %self.repo_details, branches = ?msg))]
|
#[instrument(name = "RepoActor::ReceiveRepoConfig", skip_all, fields(repo = %self.repo_details, branches = ?msg))]
|
||||||
fn handle(&mut self, msg: ReceiveRepoConfig, ctx: &mut Self::Context) -> Self::Result {
|
async fn handle(
|
||||||
|
&mut self,
|
||||||
|
msg: ReceiveRepoConfig,
|
||||||
|
ctx: Context<'_, Self, Self::Reply>,
|
||||||
|
) -> Self::Reply {
|
||||||
let repo_config = msg.peel();
|
let repo_config = msg.peel();
|
||||||
self.update_tui(RepoUpdate::ReceiveRepoConfig {
|
self.update_tui(RepoUpdate::ReceiveRepoConfig {
|
||||||
repo_config: repo_config.clone(),
|
repo_config: repo_config.clone(),
|
||||||
});
|
})
|
||||||
|
.await?;
|
||||||
self.repo_details.repo_config.replace(repo_config);
|
self.repo_details.repo_config.replace(repo_config);
|
||||||
self.update_tui_branches();
|
self.update_tui_branches().await?;
|
||||||
do_send(&ctx.address(), RegisterWebhook::new(), self.log.as_ref());
|
do_send(&ctx.actor_ref(), RegisterWebhook::new(), self.log.as_ref()).await?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
//
|
//
|
||||||
use actix::prelude::*;
|
use color_eyre::Result;
|
||||||
|
use kameo::message::{Context, Message};
|
||||||
use tracing::{debug, error, Instrument as _};
|
use tracing::{debug, error};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
repo::{
|
repo::{
|
||||||
|
@ -14,51 +14,52 @@ use crate::{
|
||||||
|
|
||||||
use git_next_core::git::UserNotification;
|
use git_next_core::git::UserNotification;
|
||||||
|
|
||||||
impl Handler<RegisterWebhook> for RepoActor {
|
impl Message<RegisterWebhook> for RepoActor {
|
||||||
type Result = ();
|
type Reply = Result<()>;
|
||||||
|
|
||||||
fn handle(&mut self, _msg: RegisterWebhook, ctx: &mut Self::Context) -> Self::Result {
|
async fn handle(
|
||||||
|
&mut self,
|
||||||
|
_msg: RegisterWebhook,
|
||||||
|
ctx: Context<'_, Self, Self::Reply>,
|
||||||
|
) -> Self::Reply {
|
||||||
if self.webhook_id.is_none() {
|
if self.webhook_id.is_none() {
|
||||||
let forge_alias = self.repo_details.forge.forge_alias().clone();
|
self.update_tui(RepoUpdate::RegisteringWebhook).await?;
|
||||||
let repo_alias = self.repo_details.repo_alias.clone();
|
|
||||||
let repo_listen_url = self
|
|
||||||
.listen_url
|
|
||||||
.repo_url(forge_alias.clone(), repo_alias.clone());
|
|
||||||
let forge = self.forge.duplicate();
|
|
||||||
let addr = ctx.address();
|
|
||||||
let notify_user_recipient = self.notify_user_recipient.clone();
|
|
||||||
let log = self.log.clone();
|
|
||||||
self.update_tui(RepoUpdate::RegisteringWebhook);
|
|
||||||
debug!("registering webhook");
|
debug!("registering webhook");
|
||||||
async move {
|
match self
|
||||||
match forge.register_webhook(&repo_listen_url).await {
|
.forge
|
||||||
|
.register_webhook(&self.listen_url.repo_url(
|
||||||
|
self.repo_details.forge.forge_alias().clone(),
|
||||||
|
self.repo_details.repo_alias.clone(),
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(registered_webhook) => {
|
Ok(registered_webhook) => {
|
||||||
debug!(?registered_webhook, "webhook registered");
|
debug!(?registered_webhook, "webhook registered");
|
||||||
do_send(
|
do_send(
|
||||||
&addr,
|
&ctx.actor_ref(),
|
||||||
WebhookRegistered::from(registered_webhook),
|
WebhookRegistered::from(registered_webhook),
|
||||||
log.as_ref(),
|
self.log.as_ref(),
|
||||||
);
|
)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!(?err, "failed to register webhook");
|
error!(?err, "failed to register webhook");
|
||||||
notify_user(
|
notify_user(
|
||||||
notify_user_recipient.as_ref(),
|
self.notify_user_recipient.clone().as_ref(),
|
||||||
UserNotification::WebhookRegistration {
|
UserNotification::WebhookRegistration {
|
||||||
forge_alias,
|
forge_alias: self.repo_details.forge.forge_alias().clone(),
|
||||||
repo_alias,
|
repo_alias: self.repo_details.repo_alias.clone(),
|
||||||
reason: err.to_string(),
|
reason: err.to_string(),
|
||||||
},
|
},
|
||||||
log.as_ref(),
|
self.log.as_ref(),
|
||||||
);
|
)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.in_current_span()
|
|
||||||
.into_actor(self)
|
|
||||||
.wait(ctx);
|
|
||||||
} else {
|
} else {
|
||||||
self.alert_tui("already have a webhook id - cant register webhook");
|
self.alert_tui("already have a webhook id - cant register webhook")
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,32 +1,31 @@
|
||||||
//
|
//
|
||||||
use actix::prelude::*;
|
use color_eyre::Result;
|
||||||
|
use kameo::message::{Context, Message};
|
||||||
use tracing::{debug, warn, Instrument as _};
|
use tracing::{debug, warn};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
repo::{messages::UnRegisterWebhook, RepoActor},
|
repo::{messages::UnRegisterWebhook, RepoActor},
|
||||||
server::actor::messages::RepoUpdate,
|
server::actor::messages::RepoUpdate,
|
||||||
};
|
};
|
||||||
|
|
||||||
impl Handler<UnRegisterWebhook> for RepoActor {
|
impl Message<UnRegisterWebhook> for RepoActor {
|
||||||
type Result = ();
|
type Reply = Result<()>;
|
||||||
|
|
||||||
fn handle(&mut self, _msg: UnRegisterWebhook, ctx: &mut Self::Context) -> Self::Result {
|
async fn handle(
|
||||||
|
&mut self,
|
||||||
|
_msg: UnRegisterWebhook,
|
||||||
|
_ctx: Context<'_, Self, Self::Reply>,
|
||||||
|
) -> Self::Reply {
|
||||||
let Some(webhook_id) = self.webhook_id.take() else {
|
let Some(webhook_id) = self.webhook_id.take() else {
|
||||||
return;
|
return Ok(());
|
||||||
};
|
};
|
||||||
self.update_tui(RepoUpdate::UnregisteringWebhook);
|
self.update_tui(RepoUpdate::UnregisteringWebhook).await?;
|
||||||
let forge = self.forge.duplicate();
|
|
||||||
debug!("unregistering webhook");
|
debug!("unregistering webhook");
|
||||||
async move {
|
match self.forge.unregister_webhook(&webhook_id).await {
|
||||||
match forge.unregister_webhook(&webhook_id).await {
|
|
||||||
Ok(()) => debug!("unregistered webhook"),
|
Ok(()) => debug!("unregistered webhook"),
|
||||||
Err(err) => warn!(?err, "unregistering webhook"),
|
Err(err) => warn!(?err, "unregistering webhook"),
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.in_current_span()
|
|
||||||
.into_actor(self)
|
|
||||||
.wait(ctx);
|
|
||||||
debug!("unregistering webhook done");
|
debug!("unregistering webhook done");
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
//
|
//
|
||||||
use actix::prelude::*;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use tracing::{info, instrument, Instrument as _};
|
use color_eyre::Result;
|
||||||
|
use kameo::message::{Context, Message};
|
||||||
|
use tracing::{debug, info, instrument, warn};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
repo::{
|
repo::{
|
||||||
|
@ -11,20 +13,23 @@ use crate::{
|
||||||
},
|
},
|
||||||
server::actor::messages::RepoUpdate,
|
server::actor::messages::RepoUpdate,
|
||||||
};
|
};
|
||||||
|
|
||||||
use git_next_core::git::{
|
use git_next_core::git::{
|
||||||
push::Force,
|
push::Force,
|
||||||
validation::positions::{validate, Error, Positions},
|
validation::positions::{validate, Error, Positions},
|
||||||
UserNotification,
|
UserNotification,
|
||||||
};
|
};
|
||||||
|
use git_next_core::s;
|
||||||
|
|
||||||
impl Handler<ValidateRepo> for RepoActor {
|
impl Message<ValidateRepo> for RepoActor {
|
||||||
type Result = ();
|
type Reply = Result<()>;
|
||||||
|
|
||||||
#[instrument(name = "RepoActor::ValidateRepo", skip_all, fields(repo = %self.repo_details, token = %&*msg))]
|
#[instrument(name = "RepoActor::ValidateRepo", skip_all, fields(repo = %self.repo_details, token = %&*msg))]
|
||||||
fn handle(&mut self, msg: ValidateRepo, ctx: &mut Self::Context) -> Self::Result {
|
async fn handle(
|
||||||
logger(self.log.as_ref(), "start: ValidateRepo");
|
&mut self,
|
||||||
|
msg: ValidateRepo,
|
||||||
|
ctx: Context<'_, Self, Self::Reply>,
|
||||||
|
) -> Self::Reply {
|
||||||
|
logger(self.log.as_ref(), "start: ValidateRepo").await;
|
||||||
// Message Token - make sure we are only triggered for the latest/current token
|
// Message Token - make sure we are only triggered for the latest/current token
|
||||||
match self.token_status(msg.peel()) {
|
match self.token_status(msg.peel()) {
|
||||||
TokenStatus::Current => {} // do nothing
|
TokenStatus::Current => {} // do nothing
|
||||||
|
@ -32,38 +37,38 @@ impl Handler<ValidateRepo> for RepoActor {
|
||||||
logger(
|
logger(
|
||||||
self.log.as_ref(),
|
self.log.as_ref(),
|
||||||
format!("discarded: old message token: {}", self.message_token),
|
format!("discarded: old message token: {}", self.message_token),
|
||||||
);
|
)
|
||||||
return; // message is expired
|
.await;
|
||||||
|
return Ok(()); // message is expired
|
||||||
}
|
}
|
||||||
TokenStatus::New(message_token) => {
|
TokenStatus::New(message_token) => {
|
||||||
self.message_token = message_token;
|
self.message_token = message_token;
|
||||||
logger(
|
logger(
|
||||||
self.log.as_ref(),
|
self.log.as_ref(),
|
||||||
format!("new message token: {}", self.message_token),
|
format!("new message token: {}", self.message_token),
|
||||||
);
|
)
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logger(
|
logger(
|
||||||
self.log.as_ref(),
|
self.log.as_ref(),
|
||||||
format!("accepted token: {}", self.message_token),
|
format!("accepted token: {}", self.message_token),
|
||||||
);
|
)
|
||||||
|
.await;
|
||||||
self.update_tui(RepoUpdate::ValidateRepo);
|
self.update_tui(RepoUpdate::ValidateRepo).await?;
|
||||||
|
|
||||||
// Repository positions
|
// Repository positions
|
||||||
let Some(ref open_repository) = self.open_repository else {
|
let Some(ref open_repository) = self.open_repository else {
|
||||||
logger(self.log.as_ref(), "no open repository");
|
logger(self.log.as_ref(), "no open repository").await;
|
||||||
self.alert_tui("repo not open");
|
self.alert_tui("repo not open").await?;
|
||||||
return;
|
return Ok(());
|
||||||
};
|
};
|
||||||
logger(self.log.as_ref(), "have open repository");
|
logger(self.log.as_ref(), "have open repository").await;
|
||||||
let Some(repo_config) = self.repo_details.repo_config.clone() else {
|
let Some(repo_config) = self.repo_details.repo_config.clone() else {
|
||||||
logger(self.log.as_ref(), "no repo config");
|
logger(self.log.as_ref(), "no repo config").await;
|
||||||
self.alert_tui("no repo config");
|
self.alert_tui("no repo config").await?;
|
||||||
return;
|
return Ok(());
|
||||||
};
|
};
|
||||||
logger(self.log.as_ref(), "have repo config");
|
logger(self.log.as_ref(), "have repo config").await;
|
||||||
|
|
||||||
match validate(&**open_repository, &self.repo_details, &repo_config) {
|
match validate(&**open_repository, &self.repo_details, &repo_config) {
|
||||||
Ok((
|
Ok((
|
||||||
Positions {
|
Positions {
|
||||||
|
@ -75,70 +80,88 @@ impl Handler<ValidateRepo> for RepoActor {
|
||||||
},
|
},
|
||||||
git_log,
|
git_log,
|
||||||
)) => {
|
)) => {
|
||||||
info!(%main, %next, %dev, "positions");
|
let mut positions = HashMap::new();
|
||||||
self.update_tui_log(git_log);
|
positions
|
||||||
|
.entry(s!(s!(main.sha())[0..7]))
|
||||||
|
.or_insert_with(Vec::new)
|
||||||
|
.extend(["main"]);
|
||||||
|
positions
|
||||||
|
.entry(s!(s!(next.sha())[0..7]))
|
||||||
|
.or_insert_with(Vec::new)
|
||||||
|
.extend(["next"]);
|
||||||
|
positions
|
||||||
|
.entry(s!(s!(dev.sha())[0..7]))
|
||||||
|
.or_insert_with(Vec::new)
|
||||||
|
.extend(["dev"]);
|
||||||
|
info!(?positions, "");
|
||||||
|
self.update_tui_log(git_log).await?;
|
||||||
if next_is_valid && next != main {
|
if next_is_valid && next != main {
|
||||||
info!("Checking CI");
|
info!("Checking CI");
|
||||||
do_send(&ctx.address(), CheckCIStatus::new(next), self.log.as_ref());
|
do_send(
|
||||||
|
&ctx.actor_ref(),
|
||||||
|
CheckCIStatus::new(next),
|
||||||
|
self.log.as_ref(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
} else if next != dev {
|
} else if next != dev {
|
||||||
info!("Advance next");
|
info!("Advance next");
|
||||||
self.update_tui(RepoUpdate::AdvancingNext {
|
self.update_tui(RepoUpdate::AdvancingNext {
|
||||||
commit: next.clone(),
|
commit: next.clone(),
|
||||||
force: Force::No,
|
force: Force::No,
|
||||||
});
|
})
|
||||||
|
.await?;
|
||||||
do_send(
|
do_send(
|
||||||
&ctx.address(),
|
&ctx.actor_ref(),
|
||||||
AdvanceNext::new(AdvanceNextPayload {
|
AdvanceNext::new(AdvanceNextPayload {
|
||||||
next,
|
next,
|
||||||
main,
|
main,
|
||||||
dev_commit_history,
|
dev_commit_history,
|
||||||
}),
|
}),
|
||||||
self.log.as_ref(),
|
self.log.as_ref(),
|
||||||
);
|
)
|
||||||
|
.await?;
|
||||||
} else {
|
} else {
|
||||||
info!("do nothing");
|
info!("do nothing");
|
||||||
self.update_tui(RepoUpdate::Okay { main, next, dev });
|
self.update_tui(RepoUpdate::Okay { main, next, dev })
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(Error::Retryable(message)) => {
|
Err(Error::Retryable(message)) => {
|
||||||
info!(?message, "Retryable");
|
warn!(?message, "Retryable");
|
||||||
self.alert_tui(format!("retryable: {message}"));
|
self.alert_tui(format!("retryable: {message}")).await?;
|
||||||
logger(self.log.as_ref(), message);
|
logger(self.log.as_ref(), message).await;
|
||||||
let addr = ctx.address();
|
debug!("sleeping before retrying...");
|
||||||
let message_token = self.message_token;
|
tokio::time::sleep(self.sleep_duration).await;
|
||||||
let sleep_duration = self.sleep_duration;
|
do_send(
|
||||||
let log = self.log.clone();
|
&ctx.actor_ref(),
|
||||||
async move {
|
ValidateRepo::new(self.message_token),
|
||||||
info!("sleeping before retrying...");
|
self.log.as_ref(),
|
||||||
logger(log.as_ref(), "before sleep");
|
)
|
||||||
actix_rt::time::sleep(sleep_duration).await;
|
.await?;
|
||||||
logger(log.as_ref(), "after sleep");
|
|
||||||
do_send(&addr, ValidateRepo::new(message_token), log.as_ref());
|
|
||||||
}
|
|
||||||
.in_current_span()
|
|
||||||
.into_actor(self)
|
|
||||||
.wait(ctx);
|
|
||||||
}
|
}
|
||||||
Err(Error::UserIntervention(user_notification)) => {
|
Err(Error::UserIntervention(user_notification)) => {
|
||||||
info!(?user_notification, "User Intervention");
|
warn!(?user_notification, "User Intervention");
|
||||||
self.alert_tui(format!("USER INTERVENTION: {user_notification}"));
|
self.alert_tui(format!("USER INTERVENTION: {user_notification}"))
|
||||||
|
.await?;
|
||||||
if let UserNotification::CICheckFailed { log, .. }
|
if let UserNotification::CICheckFailed { log, .. }
|
||||||
| UserNotification::DevNotBasedOnMain { log, .. } = &user_notification
|
| UserNotification::DevNotBasedOnMain { log, .. } = &user_notification
|
||||||
{
|
{
|
||||||
self.update_tui_log(log.clone());
|
self.update_tui_log(log.clone()).await?;
|
||||||
}
|
}
|
||||||
notify_user(
|
notify_user(
|
||||||
self.notify_user_recipient.as_ref(),
|
self.notify_user_recipient.as_ref(),
|
||||||
user_notification,
|
user_notification,
|
||||||
self.log.as_ref(),
|
self.log.as_ref(),
|
||||||
);
|
)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
Err(Error::NonRetryable(message)) => {
|
Err(Error::NonRetryable(message)) => {
|
||||||
info!(?message, "NonRetryable");
|
warn!(?message, "NonRetryable");
|
||||||
self.alert_tui(format!("Error: {message}"));
|
self.alert_tui(format!("Error: {message}")).await?;
|
||||||
logger(self.log.as_ref(), message);
|
logger(self.log.as_ref(), message).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,14 @@
|
||||||
//
|
//
|
||||||
use actix::prelude::*;
|
use color_eyre::Result;
|
||||||
|
use kameo::message::{Context, Message};
|
||||||
use tracing::{info, instrument, warn};
|
use tracing::{info, instrument, warn};
|
||||||
|
|
||||||
|
use git_next_core::{
|
||||||
|
git::{Commit, ForgeLike},
|
||||||
|
webhook::{push::Branch, Push},
|
||||||
|
BranchName, WebhookAuth,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
repo::{
|
repo::{
|
||||||
do_send, logger,
|
do_send, logger,
|
||||||
|
@ -12,21 +18,19 @@ use crate::{
|
||||||
server::actor::messages::RepoUpdate,
|
server::actor::messages::RepoUpdate,
|
||||||
};
|
};
|
||||||
|
|
||||||
use git_next_core::{
|
impl Message<WebhookNotification> for RepoActor {
|
||||||
git::{Commit, ForgeLike},
|
type Reply = Result<()>;
|
||||||
webhook::{push::Branch, Push},
|
|
||||||
BranchName, WebhookAuth,
|
|
||||||
};
|
|
||||||
|
|
||||||
impl Handler<WebhookNotification> for RepoActor {
|
|
||||||
type Result = ();
|
|
||||||
|
|
||||||
#[instrument(name = "RepoActor::WebhookMessage", skip_all, fields(token = %self.message_token, repo = %self.repo_details))]
|
#[instrument(name = "RepoActor::WebhookMessage", skip_all, fields(token = %self.message_token, repo = %self.repo_details))]
|
||||||
fn handle(&mut self, msg: WebhookNotification, ctx: &mut Self::Context) -> Self::Result {
|
async fn handle(
|
||||||
|
&mut self,
|
||||||
|
msg: WebhookNotification,
|
||||||
|
ctx: Context<'_, Self, Self::Reply>,
|
||||||
|
) -> Self::Reply {
|
||||||
let Some(config) = &self.repo_details.repo_config else {
|
let Some(config) = &self.repo_details.repo_config else {
|
||||||
logger(self.log.as_ref(), "server has no repo config");
|
logger(self.log.as_ref(), "server has no repo config").await;
|
||||||
warn!("No repo config");
|
warn!("No repo config");
|
||||||
return;
|
return Ok(());
|
||||||
};
|
};
|
||||||
if validate_notification(
|
if validate_notification(
|
||||||
&msg,
|
&msg,
|
||||||
|
@ -34,72 +38,78 @@ impl Handler<WebhookNotification> for RepoActor {
|
||||||
&*self.forge,
|
&*self.forge,
|
||||||
self.log.as_ref(),
|
self.log.as_ref(),
|
||||||
)
|
)
|
||||||
|
.await
|
||||||
.is_err()
|
.is_err()
|
||||||
{
|
{
|
||||||
return;
|
return Ok(());
|
||||||
}
|
}
|
||||||
let body = msg.body();
|
match self.forge.parse_webhook_body(msg.body()) {
|
||||||
match self.forge.parse_webhook_body(body) {
|
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
logger(self.log.as_ref(), "message parse error - not a push");
|
logger(self.log.as_ref(), "message parse error - not a push").await;
|
||||||
warn!(?err, "Not a 'push'");
|
warn!(?err, "Not a 'push'");
|
||||||
return;
|
return Ok(());
|
||||||
}
|
}
|
||||||
Ok(push) => match push.branch(config.branches()) {
|
Ok(push) => match push.branch(config.branches()) {
|
||||||
None => {
|
None => {
|
||||||
logger(self.log.as_ref(), "unknown branch");
|
logger(self.log.as_ref(), "unknown branch").await;
|
||||||
warn!(
|
warn!(
|
||||||
?push,
|
?push,
|
||||||
"Unrecognised branch, we should be filtering to only the ones we want"
|
"Unrecognised branch, we should be filtering to only the ones we want"
|
||||||
);
|
);
|
||||||
return;
|
return Ok(());
|
||||||
}
|
}
|
||||||
Some(Branch::Main) => {
|
Some(Branch::Main) => {
|
||||||
self.update_tui(RepoUpdate::WebhookReceived {
|
self.update_tui(RepoUpdate::WebhookReceived {
|
||||||
branch: Branch::Main,
|
branch: Branch::Main,
|
||||||
push: push.clone(),
|
push: push.clone(),
|
||||||
});
|
})
|
||||||
|
.await?;
|
||||||
if handle_push(
|
if handle_push(
|
||||||
push,
|
push,
|
||||||
&config.branches().main(),
|
&config.branches().main(),
|
||||||
&mut self.last_main_commit,
|
&mut self.last_main_commit,
|
||||||
self.log.as_ref(),
|
self.log.as_ref(),
|
||||||
)
|
)
|
||||||
|
.await
|
||||||
.is_err()
|
.is_err()
|
||||||
{
|
{
|
||||||
return;
|
return Ok(());
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
Some(Branch::Next) => {
|
Some(Branch::Next) => {
|
||||||
self.update_tui(RepoUpdate::WebhookReceived {
|
self.update_tui(RepoUpdate::WebhookReceived {
|
||||||
branch: Branch::Next,
|
branch: Branch::Next,
|
||||||
push: push.clone(),
|
push: push.clone(),
|
||||||
});
|
})
|
||||||
|
.await?;
|
||||||
if handle_push(
|
if handle_push(
|
||||||
push,
|
push,
|
||||||
&config.branches().next(),
|
&config.branches().next(),
|
||||||
&mut self.last_next_commit,
|
&mut self.last_next_commit,
|
||||||
self.log.as_ref(),
|
self.log.as_ref(),
|
||||||
)
|
)
|
||||||
|
.await
|
||||||
.is_err()
|
.is_err()
|
||||||
{
|
{
|
||||||
return;
|
return Ok(());
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
Some(Branch::Dev) => {
|
Some(Branch::Dev) => {
|
||||||
self.update_tui(RepoUpdate::WebhookReceived {
|
self.update_tui(RepoUpdate::WebhookReceived {
|
||||||
branch: Branch::Dev,
|
branch: Branch::Dev,
|
||||||
push: push.clone(),
|
push: push.clone(),
|
||||||
});
|
})
|
||||||
|
.await?;
|
||||||
if handle_push(
|
if handle_push(
|
||||||
push,
|
push,
|
||||||
&config.branches().dev(),
|
&config.branches().dev(),
|
||||||
&mut self.last_dev_commit,
|
&mut self.last_dev_commit,
|
||||||
self.log.as_ref(),
|
self.log.as_ref(),
|
||||||
)
|
)
|
||||||
|
.await
|
||||||
.is_err()
|
.is_err()
|
||||||
{
|
{
|
||||||
return;
|
return Ok(());
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -110,27 +120,29 @@ impl Handler<WebhookNotification> for RepoActor {
|
||||||
"New commit"
|
"New commit"
|
||||||
);
|
);
|
||||||
do_send(
|
do_send(
|
||||||
&ctx.address(),
|
&ctx.actor_ref(),
|
||||||
ValidateRepo::new(message_token),
|
ValidateRepo::new(message_token),
|
||||||
self.log.as_ref(),
|
self.log.as_ref(),
|
||||||
);
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_notification(
|
async fn validate_notification(
|
||||||
msg: &WebhookNotification,
|
msg: &WebhookNotification,
|
||||||
webhook_auth: Option<&WebhookAuth>,
|
webhook_auth: Option<&WebhookAuth>,
|
||||||
forge: &dyn ForgeLike,
|
forge: &dyn ForgeLike,
|
||||||
log: Option<&ActorLog>,
|
log: Option<&ActorLog>,
|
||||||
) -> Result<(), ()> {
|
) -> Result<(), ()> {
|
||||||
let Some(expected_authorization) = webhook_auth else {
|
let Some(expected_authorization) = webhook_auth else {
|
||||||
logger(log, "server has no auth token");
|
logger(log, "server has no auth token").await;
|
||||||
warn!("Don't know what authorization to expect");
|
warn!("Don't know what authorization to expect");
|
||||||
return Err(());
|
return Err(());
|
||||||
};
|
};
|
||||||
|
|
||||||
if !forge.is_message_authorised(msg, expected_authorization) {
|
if !forge.is_message_authorised(msg, expected_authorization) {
|
||||||
logger(log, "message authorisation is invalid");
|
logger(log, "message authorisation is invalid").await;
|
||||||
warn!(
|
warn!(
|
||||||
"Invalid authorization - expected {}",
|
"Invalid authorization - expected {}",
|
||||||
expected_authorization
|
expected_authorization
|
||||||
|
@ -138,22 +150,22 @@ fn validate_notification(
|
||||||
return Err(());
|
return Err(());
|
||||||
}
|
}
|
||||||
if forge.should_ignore_message(msg) {
|
if forge.should_ignore_message(msg) {
|
||||||
logger(log, "forge sent ignorable message");
|
logger(log, "forge sent ignorable message").await;
|
||||||
return Err(());
|
return Err(());
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_push(
|
async fn handle_push(
|
||||||
push: Push,
|
push: Push,
|
||||||
branch: &BranchName,
|
branch: &BranchName,
|
||||||
last_commit: &mut Option<Commit>,
|
last_commit: &mut Option<Commit>,
|
||||||
log: Option<&ActorLog>,
|
log: Option<&ActorLog>,
|
||||||
) -> Result<(), ()> {
|
) -> Result<(), ()> {
|
||||||
logger(log, format!("message is for {branch} branch"));
|
logger(log, format!("message is for {branch} branch")).await;
|
||||||
let commit = Commit::from(push);
|
let commit = Commit::from(push);
|
||||||
if last_commit.as_ref() == Some(&commit) {
|
if last_commit.as_ref() == Some(&commit) {
|
||||||
logger(log, format!("not a new commit on {branch}"));
|
logger(log, format!("not a new commit on {branch}")).await;
|
||||||
info!(
|
info!(
|
||||||
%branch ,
|
%branch ,
|
||||||
%commit,
|
%commit,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
//
|
//
|
||||||
use actix::prelude::*;
|
use color_eyre::Result;
|
||||||
|
use kameo::message::{Context, Message};
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -11,17 +12,24 @@ use crate::{
|
||||||
server::actor::messages::RepoUpdate,
|
server::actor::messages::RepoUpdate,
|
||||||
};
|
};
|
||||||
|
|
||||||
impl Handler<WebhookRegistered> for RepoActor {
|
impl Message<WebhookRegistered> for RepoActor {
|
||||||
type Result = ();
|
type Reply = Result<()>;
|
||||||
|
|
||||||
#[instrument(name = "RepoActor::WebhookRegistered", skip_all, fields(repo = %self.repo_details, webhook_id = %msg.webhook_id()))]
|
#[instrument(name = "RepoActor::WebhookRegistered", skip_all, fields(repo = %self.repo_details, webhook_id = %msg.webhook_id()))]
|
||||||
fn handle(&mut self, msg: WebhookRegistered, ctx: &mut Self::Context) -> Self::Result {
|
async fn handle(
|
||||||
self.update_tui(RepoUpdate::RegisteredWebhook);
|
&mut self,
|
||||||
|
msg: WebhookRegistered,
|
||||||
|
ctx: Context<'_, Self, Self::Reply>,
|
||||||
|
) -> Self::Reply {
|
||||||
|
self.update_tui(RepoUpdate::RegisteredWebhook).await?;
|
||||||
self.webhook_id.replace(msg.webhook_id().clone());
|
self.webhook_id.replace(msg.webhook_id().clone());
|
||||||
self.webhook_auth.replace(msg.webhook_auth().clone());
|
self.webhook_auth.replace(msg.webhook_auth().clone());
|
||||||
do_send(
|
do_send(
|
||||||
&ctx.address(),
|
&ctx.actor_ref(),
|
||||||
ValidateRepo::new(self.message_token),
|
ValidateRepo::new(self.message_token),
|
||||||
self.log.as_ref(),
|
self.log.as_ref(),
|
||||||
);
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ use tracing::{info, instrument};
|
||||||
/// Loads the [RepoConfig] from the `.git-next.toml` file in the repository
|
/// Loads the [RepoConfig] from the `.git-next.toml` file in the repository
|
||||||
#[instrument(skip_all, fields(branch = %repo_details.branch))]
|
#[instrument(skip_all, fields(branch = %repo_details.branch))]
|
||||||
pub async fn config_from_repository(
|
pub async fn config_from_repository(
|
||||||
repo_details: RepoDetails,
|
repo_details: &RepoDetails,
|
||||||
open_repository: &dyn OpenRepositoryLike,
|
open_repository: &dyn OpenRepositoryLike,
|
||||||
) -> Result<RepoConfig> {
|
) -> Result<RepoConfig> {
|
||||||
info!("Loading .git-next.toml from repo");
|
info!("Loading .git-next.toml from repo");
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
//
|
//
|
||||||
use actix::prelude::*;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::{
|
use color_eyre::{eyre::eyre, Result};
|
||||||
alerts::messages::NotifyUser,
|
|
||||||
server::{actor::messages::RepoUpdate, ServerActor},
|
|
||||||
};
|
|
||||||
use derive_more::Deref;
|
use derive_more::Deref;
|
||||||
|
use kameo::{actor::ActorRef, mailbox::unbounded::UnboundedMailbox, Actor};
|
||||||
use kxio::net::Net;
|
use kxio::net::Net;
|
||||||
use tracing::{info, instrument, warn, Instrument};
|
use tokio::sync::RwLock;
|
||||||
|
use tracing::{debug, info, instrument, warn};
|
||||||
|
|
||||||
use git_next_core::{
|
use git_next_core::{
|
||||||
git::{
|
git::{
|
||||||
|
@ -19,6 +18,16 @@ use git_next_core::{
|
||||||
WebhookAuth, WebhookId,
|
WebhookAuth, WebhookId,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
alerts::{messages::NotifyUser, AlertsActor},
|
||||||
|
default_on_actor_link_died, default_on_actor_panic, default_on_actor_start, on_actor_stop,
|
||||||
|
server::{
|
||||||
|
actor::messages::{RepoUpdate, ServerUpdate},
|
||||||
|
ServerActor,
|
||||||
|
},
|
||||||
|
tell,
|
||||||
|
};
|
||||||
|
|
||||||
mod branch;
|
mod branch;
|
||||||
pub mod handlers;
|
pub mod handlers;
|
||||||
mod load;
|
mod load;
|
||||||
|
@ -29,14 +38,21 @@ mod notifications;
|
||||||
pub mod tests;
|
pub mod tests;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub struct ActorLog(std::sync::Arc<std::sync::RwLock<Vec<String>>>);
|
pub struct ActorLog(Arc<RwLock<Vec<String>>>);
|
||||||
impl Deref for ActorLog {
|
impl Deref for ActorLog {
|
||||||
type Target = std::sync::Arc<std::sync::RwLock<Vec<String>>>;
|
type Target = Arc<RwLock<Vec<String>>>;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
&self.0
|
&self.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
impl ActorLog {
|
||||||
|
pub async fn log(&self, message: impl Into<String> + Send) {
|
||||||
|
let message = message.into();
|
||||||
|
debug!(%message, "log");
|
||||||
|
self.write().await.push(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// An actor that represents a Git Repository.
|
/// An actor that represents a Git Repository.
|
||||||
///
|
///
|
||||||
|
@ -45,6 +61,7 @@ impl Deref for ActorLog {
|
||||||
#[derive(Debug, derive_more::Display, derive_with::With)]
|
#[derive(Debug, derive_more::Display, derive_with::With)]
|
||||||
#[display("{}:{}:{}", generation, repo_details.forge.forge_alias(), repo_details.repo_alias)]
|
#[display("{}:{}:{}", generation, repo_details.forge.forge_alias(), repo_details.repo_alias)]
|
||||||
pub struct RepoActor {
|
pub struct RepoActor {
|
||||||
|
ui: bool,
|
||||||
sleep_duration: std::time::Duration,
|
sleep_duration: std::time::Duration,
|
||||||
generation: git::Generation,
|
generation: git::Generation,
|
||||||
message_token: messages::MessageToken,
|
message_token: messages::MessageToken,
|
||||||
|
@ -60,12 +77,13 @@ pub struct RepoActor {
|
||||||
net: Net,
|
net: Net,
|
||||||
forge: Box<dyn git::ForgeLike>,
|
forge: Box<dyn git::ForgeLike>,
|
||||||
log: Option<ActorLog>,
|
log: Option<ActorLog>,
|
||||||
notify_user_recipient: Option<Recipient<NotifyUser>>,
|
notify_user_recipient: Option<ActorRef<AlertsActor>>,
|
||||||
server_addr: Option<Addr<ServerActor>>,
|
server_actor_ref: ActorRef<ServerActor>,
|
||||||
}
|
}
|
||||||
impl RepoActor {
|
impl RepoActor {
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn new(
|
pub fn new(
|
||||||
|
ui: bool,
|
||||||
repo_details: git::RepoDetails,
|
repo_details: git::RepoDetails,
|
||||||
forge: Box<dyn git::ForgeLike>,
|
forge: Box<dyn git::ForgeLike>,
|
||||||
listen_url: ListenUrl,
|
listen_url: ListenUrl,
|
||||||
|
@ -73,11 +91,12 @@ impl RepoActor {
|
||||||
net: Net,
|
net: Net,
|
||||||
repository_factory: Box<dyn RepositoryFactory>,
|
repository_factory: Box<dyn RepositoryFactory>,
|
||||||
sleep_duration: std::time::Duration,
|
sleep_duration: std::time::Duration,
|
||||||
notify_user_recipient: Option<Recipient<NotifyUser>>,
|
notify_user_recipient: Option<ActorRef<AlertsActor>>,
|
||||||
server_addr: Option<Addr<ServerActor>>,
|
server_actor_ref: ActorRef<ServerActor>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let message_token = messages::MessageToken::default();
|
let message_token = messages::MessageToken::default();
|
||||||
Self {
|
Self {
|
||||||
|
ui,
|
||||||
generation,
|
generation,
|
||||||
message_token,
|
message_token,
|
||||||
repo_details,
|
repo_details,
|
||||||
|
@ -94,111 +113,114 @@ impl RepoActor {
|
||||||
sleep_duration,
|
sleep_duration,
|
||||||
log: None,
|
log: None,
|
||||||
notify_user_recipient,
|
notify_user_recipient,
|
||||||
server_addr,
|
server_actor_ref,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_tui_branches(&self) {
|
async fn update_tui_branches(&self) -> Result<()> {
|
||||||
if cfg!(feature = "tui") {
|
if cfg!(feature = "tui") {
|
||||||
use crate::server::actor::messages::RepoUpdate;
|
use crate::server::actor::messages::RepoUpdate;
|
||||||
let Some(repo_config) = &self.repo_details.repo_config else {
|
let Some(repo_config) = &self.repo_details.repo_config else {
|
||||||
return;
|
return Ok(());
|
||||||
};
|
};
|
||||||
let branches = repo_config.branches().clone();
|
let branches = repo_config.branches().clone();
|
||||||
self.update_tui(RepoUpdate::Branches { branches });
|
self.update_tui(RepoUpdate::Branches { branches }).await?;
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
fn update_tui_log(&self, log: git::graph::Log) {
|
async fn update_tui_log(&self, log: git::graph::Log) -> Result<()> {
|
||||||
if cfg!(feature = "tui") {
|
if cfg!(feature = "tui") {
|
||||||
self.update_tui(RepoUpdate::Log { log });
|
self.update_tui(RepoUpdate::Log { log }).await?;
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
fn alert_tui(&self, alert: impl Into<String>) {
|
async fn alert_tui(&self, alert: impl Into<String> + Send) -> Result<()> {
|
||||||
if cfg!(feature = "tui") {
|
if cfg!(feature = "tui") {
|
||||||
self.update_tui(RepoUpdate::Alert {
|
self.update_tui(RepoUpdate::Alert {
|
||||||
alert: alert.into(),
|
alert: alert.into(),
|
||||||
});
|
})
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
fn update_tui(&self, repo_update: RepoUpdate) {
|
#[instrument(skip_all)]
|
||||||
if cfg!(feature = "tui") {
|
async fn update_tui(&self, repo_update: RepoUpdate) -> Result<()> {
|
||||||
let Some(server_addr) = &self.server_addr else {
|
if cfg!(feature = "tui") && self.ui {
|
||||||
return;
|
tell!(
|
||||||
};
|
"server",
|
||||||
|
self.server_actor_ref,
|
||||||
let update = crate::server::actor::messages::ServerUpdate::RepoUpdate {
|
ServerUpdate::RepoUpdate {
|
||||||
forge_alias: self.repo_details.forge.forge_alias().clone(),
|
forge_alias: self.repo_details.forge.forge_alias().clone(),
|
||||||
repo_alias: self.repo_details.repo_alias.clone(),
|
repo_alias: self.repo_details.repo_alias.clone(),
|
||||||
repo_update,
|
repo_update,
|
||||||
};
|
|
||||||
server_addr.do_send(update);
|
|
||||||
}
|
}
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl Actor for RepoActor {
|
impl Actor for RepoActor {
|
||||||
type Context = Context<Self>;
|
type Mailbox = UnboundedMailbox<Self>;
|
||||||
#[instrument(name = "RepoActor::stopping", skip_all, fields(repo = %self.repo_details))]
|
|
||||||
fn stopping(&mut self, ctx: &mut Self::Context) -> Running {
|
default_on_actor_start!(this, actor_ref);
|
||||||
tracing::debug!("stopping");
|
default_on_actor_panic!(this, actor_ref, err);
|
||||||
|
default_on_actor_link_died!(this, actor_ref, id, reason);
|
||||||
|
|
||||||
|
on_actor_stop!(this, actor_ref, reason, {
|
||||||
info!("Checking webhook");
|
info!("Checking webhook");
|
||||||
match self.webhook_id.take() {
|
if let Some(webhook_id) = this.webhook_id.take() {
|
||||||
Some(webhook_id) => {
|
|
||||||
tracing::warn!("stopping - unregistering webhook");
|
tracing::warn!("stopping - unregistering webhook");
|
||||||
info!(%webhook_id, "Unregistring webhook");
|
info!(%webhook_id, "Unregistring webhook");
|
||||||
let forge = self.forge.duplicate();
|
let forge = this.forge.duplicate();
|
||||||
async move {
|
|
||||||
if let Err(err) = forge.unregister_webhook(&webhook_id).await {
|
if let Err(err) = forge.unregister_webhook(&webhook_id).await {
|
||||||
warn!("unregistering webhook: {err}");
|
warn!("unregistering webhook: {err}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.in_current_span()
|
Ok(())
|
||||||
.into_actor(self)
|
});
|
||||||
.wait(ctx);
|
|
||||||
Running::Continue
|
|
||||||
}
|
|
||||||
None => Running::Stop,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn do_send<M>(addr: &Addr<RepoActor>, msg: M, log: Option<&ActorLog>)
|
pub async fn do_send<M>(
|
||||||
|
repo_actor_ref: &ActorRef<RepoActor>,
|
||||||
|
msg: M,
|
||||||
|
log: Option<&ActorLog>,
|
||||||
|
) -> Result<()>
|
||||||
where
|
where
|
||||||
M: actix::Message + Send + 'static + std::fmt::Debug,
|
M: Send + Sync + 'static + std::fmt::Debug,
|
||||||
RepoActor: actix::Handler<M>,
|
RepoActor: kameo::message::Message<M>,
|
||||||
<M as actix::Message>::Result: Send,
|
<RepoActor as kameo::message::Message<M>>::Reply: Send + Sync + 'static + std::fmt::Debug,
|
||||||
|
<<RepoActor as kameo::message::Message<M>>::Reply as kameo::Reply>::Error: std::fmt::Debug,
|
||||||
{
|
{
|
||||||
let log_message = format!("send: {msg:?}");
|
let log_message = format!("send: {msg:?}");
|
||||||
info!(log_message);
|
logger(log, log_message.clone()).await;
|
||||||
logger(log, log_message);
|
|
||||||
if cfg!(not(test)) {
|
if cfg!(not(test)) {
|
||||||
// #[cfg(not(test))]
|
tell!(repo_actor_ref, msg).map_err(|e| eyre!(format!("error: {log_message}: {e:?}")))?;
|
||||||
addr.do_send(msg);
|
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn logger(log: Option<&ActorLog>, message: impl Into<String>) {
|
pub async fn logger(log: Option<&ActorLog>, message: impl Into<String> + Send) {
|
||||||
if let Some(log) = log {
|
if let Some(log) = log {
|
||||||
let message: String = message.into();
|
log.log(message).await;
|
||||||
tracing::debug!(message);
|
|
||||||
let _ = log.write().map(|mut l| l.push(message));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn notify_user(
|
pub async fn notify_user(
|
||||||
recipient: Option<&Recipient<NotifyUser>>,
|
recipient: Option<&ActorRef<AlertsActor>>,
|
||||||
user_notification: UserNotification,
|
user_notification: UserNotification,
|
||||||
log: Option<&ActorLog>,
|
log: Option<&ActorLog>,
|
||||||
) {
|
) -> Result<()> {
|
||||||
let msg = NotifyUser::from(user_notification);
|
let msg = NotifyUser::from(user_notification);
|
||||||
let log_message = format!("send: {msg:?}");
|
let log_message = format!("send: {msg:?}");
|
||||||
tracing::debug!(log_message);
|
debug!(log_message);
|
||||||
logger(log, log_message);
|
logger(log, log_message).await;
|
||||||
if let Some(recipient) = &recipient {
|
if let Some(recipient) = &recipient {
|
||||||
recipient.do_send(msg);
|
tell!("alerts", recipient, msg)?;
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ fn advance_next_sut(
|
||||||
main: &Commit,
|
main: &Commit,
|
||||||
dev_commit_history: &[Commit],
|
dev_commit_history: &[Commit],
|
||||||
repo_details: &RepoDetails,
|
repo_details: &RepoDetails,
|
||||||
repo_config: RepoConfig,
|
repo_config: &RepoConfig,
|
||||||
open_repository: &dyn OpenRepositoryLike,
|
open_repository: &dyn OpenRepositoryLike,
|
||||||
message_token: MessageToken,
|
message_token: MessageToken,
|
||||||
) -> branch::Result<MessageToken> {
|
) -> branch::Result<MessageToken> {
|
||||||
|
@ -43,7 +43,7 @@ mod when_at_dev {
|
||||||
main,
|
main,
|
||||||
dev_commit_history,
|
dev_commit_history,
|
||||||
&repo_details,
|
&repo_details,
|
||||||
repo_config,
|
&repo_config,
|
||||||
&open_repository,
|
&open_repository,
|
||||||
message_token,
|
message_token,
|
||||||
)
|
)
|
||||||
|
@ -78,7 +78,7 @@ mod can_advance {
|
||||||
main,
|
main,
|
||||||
dev_commit_history,
|
dev_commit_history,
|
||||||
&repo_details,
|
&repo_details,
|
||||||
repo_config,
|
&repo_config,
|
||||||
&open_repository,
|
&open_repository,
|
||||||
message_token,
|
message_token,
|
||||||
)
|
)
|
||||||
|
@ -109,7 +109,7 @@ mod can_advance {
|
||||||
main,
|
main,
|
||||||
dev_commit_history,
|
dev_commit_history,
|
||||||
&repo_details,
|
&repo_details,
|
||||||
repo_config,
|
&repo_config,
|
||||||
&open_repository,
|
&open_repository,
|
||||||
message_token,
|
message_token,
|
||||||
)
|
)
|
||||||
|
@ -149,7 +149,7 @@ mod can_advance {
|
||||||
main,
|
main,
|
||||||
dev_commit_history,
|
dev_commit_history,
|
||||||
&repo_details,
|
&repo_details,
|
||||||
repo_config,
|
&repo_config,
|
||||||
&open_repository,
|
&open_repository,
|
||||||
message_token,
|
message_token,
|
||||||
)
|
)
|
||||||
|
@ -181,7 +181,7 @@ mod can_advance {
|
||||||
main,
|
main,
|
||||||
dev_commit_history,
|
dev_commit_history,
|
||||||
&repo_details,
|
&repo_details,
|
||||||
repo_config,
|
&repo_config,
|
||||||
&open_repository,
|
&open_repository,
|
||||||
message_token,
|
message_token,
|
||||||
)
|
)
|
||||||
|
|
|
@ -10,7 +10,7 @@ mod advance_next;
|
||||||
use crate::git;
|
use crate::git;
|
||||||
use crate::repo::branch;
|
use crate::repo::branch;
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[tokio::test]
|
||||||
async fn test_find_next_commit_on_dev_when_next_is_at_main() {
|
async fn test_find_next_commit_on_dev_when_next_is_at_main() {
|
||||||
let next = given::a_commit(); // and main
|
let next = given::a_commit(); // and main
|
||||||
let expected = given::a_commit();
|
let expected = given::a_commit();
|
||||||
|
@ -27,7 +27,7 @@ async fn test_find_next_commit_on_dev_when_next_is_at_main() {
|
||||||
assert_eq!(force, Force::No, "should not try to force");
|
assert_eq!(force, Force::No, "should not try to force");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[tokio::test]
|
||||||
async fn test_find_next_commit_on_dev_when_next_is_not_on_dev() {
|
async fn test_find_next_commit_on_dev_when_next_is_not_on_dev() {
|
||||||
let next = given::a_commit();
|
let next = given::a_commit();
|
||||||
let main = given::a_commit();
|
let main = given::a_commit();
|
||||||
|
|
|
@ -1,7 +1,14 @@
|
||||||
|
use crate::{
|
||||||
|
alerts::{AlertsActor, History},
|
||||||
|
server::{actor::messages::ServerUpdate, ServerActor},
|
||||||
|
};
|
||||||
|
|
||||||
//
|
//
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
use git_next_core::server::ListenUrl;
|
use git_next_core::server::ListenUrl;
|
||||||
|
use kameo::actor::{pubsub::PubSub, ActorRef};
|
||||||
|
use kxio::{fs::FileSystem, net::Net};
|
||||||
|
|
||||||
pub fn has_all_valid_remote_defaults(
|
pub fn has_all_valid_remote_defaults(
|
||||||
open_repository: &mut MockOpenRepositoryLike,
|
open_repository: &mut MockOpenRepositoryLike,
|
||||||
|
@ -66,7 +73,7 @@ pub fn a_name() -> String {
|
||||||
let one_char = || CHARSET[rng.gen_range(0..CHARSET.len())] as char;
|
let one_char = || CHARSET[rng.gen_range(0..CHARSET.len())] as char;
|
||||||
iter::repeat_with(one_char).take(len).collect()
|
iter::repeat_with(one_char).take(len).collect()
|
||||||
}
|
}
|
||||||
generate(5)
|
generate(7)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn maybe_a_number() -> Option<u32> {
|
pub fn maybe_a_number() -> Option<u32> {
|
||||||
|
@ -197,6 +204,7 @@ pub fn a_repo_actor(
|
||||||
repo_details: RepoDetails,
|
repo_details: RepoDetails,
|
||||||
repository_factory: Box<dyn RepositoryFactory>,
|
repository_factory: Box<dyn RepositoryFactory>,
|
||||||
forge: Box<dyn ForgeLike>,
|
forge: Box<dyn ForgeLike>,
|
||||||
|
server_actor: ActorRef<ServerActor>,
|
||||||
net: kxio::net::Net,
|
net: kxio::net::Net,
|
||||||
) -> (RepoActor, ActorLog) {
|
) -> (RepoActor, ActorLog) {
|
||||||
let listen_url = given::a_listen_url();
|
let listen_url = given::a_listen_url();
|
||||||
|
@ -205,6 +213,7 @@ pub fn a_repo_actor(
|
||||||
let actors_log = log.clone();
|
let actors_log = log.clone();
|
||||||
(
|
(
|
||||||
RepoActor::new(
|
RepoActor::new(
|
||||||
|
false,
|
||||||
repo_details,
|
repo_details,
|
||||||
forge,
|
forge,
|
||||||
listen_url,
|
listen_url,
|
||||||
|
@ -213,13 +222,34 @@ pub fn a_repo_actor(
|
||||||
repository_factory,
|
repository_factory,
|
||||||
std::time::Duration::from_nanos(1),
|
std::time::Duration::from_nanos(1),
|
||||||
None,
|
None,
|
||||||
None,
|
server_actor,
|
||||||
)
|
)
|
||||||
.with_log(actors_log),
|
.with_log(actors_log),
|
||||||
log,
|
log,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn a_server_actor(fs: FileSystem, net: Net) -> ActorRef<ServerActor> {
|
||||||
|
let alerts_actor_ref = kameo::spawn(AlertsActor::new(
|
||||||
|
None,
|
||||||
|
History::new(Duration::from_secs(0)),
|
||||||
|
net.clone(),
|
||||||
|
));
|
||||||
|
let server_updates_actor_ref = kameo::spawn(PubSub::<ServerUpdate>::new());
|
||||||
|
let repo_factor = Box::new(MockRepositoryFactory::new());
|
||||||
|
let duration = Duration::from_secs(0);
|
||||||
|
let server_actor = ServerActor::new(
|
||||||
|
false, // ui
|
||||||
|
fs,
|
||||||
|
net,
|
||||||
|
alerts_actor_ref,
|
||||||
|
server_updates_actor_ref,
|
||||||
|
repo_factor,
|
||||||
|
duration,
|
||||||
|
);
|
||||||
|
kameo::spawn(server_actor)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn a_hostname() -> Hostname {
|
pub fn a_hostname() -> Hostname {
|
||||||
Hostname::new(given::a_name())
|
Hostname::new(given::a_name())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
|
use crate::{repo::messages::AdvanceMain, tell};
|
||||||
|
|
||||||
//
|
//
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[actix::test]
|
#[tokio::test]
|
||||||
async fn when_repo_config_should_fetch_then_push_then_revalidate() -> TestResult {
|
async fn when_repo_config_should_fetch_then_push_then_revalidate() -> TestResult {
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
|
@ -37,21 +39,16 @@ async fn when_repo_config_should_fetch_then_push_then_revalidate() -> TestResult
|
||||||
repo_details,
|
repo_details,
|
||||||
given::a_forge(),
|
given::a_forge(),
|
||||||
);
|
);
|
||||||
addr.send(crate::repo::messages::AdvanceMain::new(next_commit.clone()))
|
tell!(addr, AdvanceMain::new(next_commit.clone()))?;
|
||||||
.await?;
|
|
||||||
System::current().stop();
|
|
||||||
|
|
||||||
//then
|
//then
|
||||||
tracing::debug!(?log, "");
|
tracing::debug!(?log, "");
|
||||||
log.read().map_err(|e| e.to_string()).map(|l| {
|
log.require_message_containing("send: LoadConfigFromRepo")
|
||||||
assert!(l
|
.await?;
|
||||||
.iter()
|
|
||||||
.any(|message| message.contains("send: LoadConfigFromRepo")));
|
|
||||||
})?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix::test]
|
#[test_log::test(tokio::test)]
|
||||||
async fn when_app_config_should_fetch_then_push_then_revalidate() -> TestResult {
|
async fn when_app_config_should_fetch_then_push_then_revalidate() -> TestResult {
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
|
@ -87,12 +84,10 @@ async fn when_app_config_should_fetch_then_push_then_revalidate() -> TestResult
|
||||||
repo_details,
|
repo_details,
|
||||||
given::a_forge(),
|
given::a_forge(),
|
||||||
);
|
);
|
||||||
addr.send(crate::repo::messages::AdvanceMain::new(next_commit.clone()))
|
tell!(addr, AdvanceMain::new(next_commit.clone()))?;
|
||||||
.await?;
|
|
||||||
System::current().stop();
|
|
||||||
|
|
||||||
//then
|
//then
|
||||||
tracing::debug!(?log, "");
|
tracing::debug!(?log, "log");
|
||||||
log.require_message_containing("send: ValidateRepo")?;
|
log.require_message_containing("send: ValidateRepo").await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use crate::repo::messages::AdvanceNextPayload;
|
use crate::{
|
||||||
|
repo::messages::{AdvanceNext, AdvanceNextPayload},
|
||||||
|
tell,
|
||||||
|
};
|
||||||
|
|
||||||
//
|
//
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test_log::test(actix::test)]
|
#[test_log::test(tokio::test)]
|
||||||
async fn should_fetch_then_push_then_revalidate() -> TestResult {
|
async fn should_fetch_then_push_then_revalidate() -> TestResult {
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
|
@ -40,19 +43,18 @@ async fn should_fetch_then_push_then_revalidate() -> TestResult {
|
||||||
repo_details,
|
repo_details,
|
||||||
given::a_forge(),
|
given::a_forge(),
|
||||||
);
|
);
|
||||||
addr.send(crate::repo::messages::AdvanceNext::new(
|
tell!(
|
||||||
AdvanceNextPayload {
|
addr,
|
||||||
|
AdvanceNext::new(AdvanceNextPayload {
|
||||||
next: next_commit.clone(),
|
next: next_commit.clone(),
|
||||||
main: next_commit.clone(),
|
main: next_commit.clone(),
|
||||||
dev_commit_history,
|
dev_commit_history,
|
||||||
},
|
})
|
||||||
))
|
)?;
|
||||||
.await?;
|
tokio::time::sleep(Duration::from_millis(9)).await;
|
||||||
actix_rt::time::sleep(Duration::from_millis(9)).await;
|
|
||||||
System::current().stop();
|
|
||||||
|
|
||||||
//then
|
//then
|
||||||
tracing::debug!(?log, "");
|
tracing::debug!(?log, "");
|
||||||
log.require_message_containing("send: ValidateRepo")?;
|
log.require_message_containing("send: ValidateRepo").await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,19 @@
|
||||||
|
use crate::{repo::messages::CheckCIStatus, tell};
|
||||||
|
|
||||||
//
|
//
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[actix::test]
|
#[tokio::test]
|
||||||
async fn should_passthrough_to_receive_ci_status() -> TestResult {
|
async fn should_passthrough_to_receive_ci_status() -> TestResult {
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
let (open_repository, repo_details) = given::an_open_repository(&fs);
|
let (open_repository, repo_details) = given::an_open_repository(&fs);
|
||||||
let next_commit = given::a_named_commit("next");
|
let next_commit = given::a_named_commit("next");
|
||||||
let mut forge = git::MockForgeLike::new();
|
let mut forge = git::MockForgeLike::new();
|
||||||
when::commit_status(
|
forge
|
||||||
&mut forge,
|
.expect_commit_status()
|
||||||
next_commit.clone(),
|
.with(mockall::predicate::eq(next_commit.clone()))
|
||||||
git::forge::commit::Status::Pass,
|
.return_once(|_| Ok(git::forge::commit::Status::Pass));
|
||||||
);
|
|
||||||
|
|
||||||
//when
|
//when
|
||||||
let (addr, log) = when::start_actor_with_open_repository(
|
let (addr, log) = when::start_actor_with_open_repository(
|
||||||
|
@ -20,18 +21,11 @@ async fn should_passthrough_to_receive_ci_status() -> TestResult {
|
||||||
repo_details,
|
repo_details,
|
||||||
Box::new(forge),
|
Box::new(forge),
|
||||||
);
|
);
|
||||||
addr.send(crate::repo::messages::CheckCIStatus::new(
|
tell!(addr, CheckCIStatus::new(next_commit.clone()))?;
|
||||||
next_commit.clone(),
|
|
||||||
))
|
|
||||||
.await?;
|
|
||||||
System::current().stop();
|
|
||||||
|
|
||||||
//then
|
//then
|
||||||
tracing::debug!(?log, "");
|
tracing::debug!(?log, "");
|
||||||
log.read().map_err(|e| e.to_string()).map(|l| {
|
log.require_message_containing("send: ReceiveCIStatus")
|
||||||
assert!(l
|
.await?;
|
||||||
.iter()
|
|
||||||
.any(|message| message.contains("send: ReceiveCIStatus")));
|
|
||||||
})?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
|
use kxio::net::Net;
|
||||||
|
|
||||||
|
use crate::tell;
|
||||||
|
|
||||||
//
|
//
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[actix::test]
|
#[test_log::test(tokio::test)]
|
||||||
async fn should_clone() -> TestResult {
|
async fn should_clone() -> TestResult {
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
|
@ -23,13 +27,19 @@ async fn should_clone() -> TestResult {
|
||||||
let _ = cloned_ref.write().map(|mut l| l.push(()));
|
let _ = cloned_ref.write().map(|mut l| l.push(()));
|
||||||
Ok(Box::new(open_repository))
|
Ok(Box::new(open_repository))
|
||||||
});
|
});
|
||||||
|
let net: Net = given::a_network().into();
|
||||||
|
|
||||||
//when
|
//when
|
||||||
let (addr, _log) = when::start_actor(repository_factory, repo_details, given::a_forge());
|
let (addr, _log) = when::start_actor(
|
||||||
addr.send(CloneRepo::new()).await?;
|
repository_factory,
|
||||||
System::current().stop();
|
repo_details,
|
||||||
|
given::a_forge(),
|
||||||
|
fs.as_real(),
|
||||||
|
net,
|
||||||
|
);
|
||||||
|
tell!(addr, CloneRepo::new())?;
|
||||||
//then
|
//then
|
||||||
|
tick(1).await;
|
||||||
cloned
|
cloned
|
||||||
.read()
|
.read()
|
||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
|
@ -38,7 +48,7 @@ async fn should_clone() -> TestResult {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix::test]
|
#[tokio::test]
|
||||||
async fn should_open() -> TestResult {
|
async fn should_open() -> TestResult {
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
|
@ -60,13 +70,20 @@ async fn should_open() -> TestResult {
|
||||||
Ok(Box::new(open_repository))
|
Ok(Box::new(open_repository))
|
||||||
});
|
});
|
||||||
fs.dir(&repo_details.gitdir).create()?;
|
fs.dir(&repo_details.gitdir).create()?;
|
||||||
|
let net: Net = given::a_network().into();
|
||||||
|
|
||||||
//when
|
//when
|
||||||
let (addr, _log) = when::start_actor(repository_factory, repo_details, given::a_forge());
|
let (addr, _log) = when::start_actor(
|
||||||
addr.send(CloneRepo::new()).await?;
|
repository_factory,
|
||||||
System::current().stop();
|
repo_details,
|
||||||
|
given::a_forge(),
|
||||||
|
fs.as_real(),
|
||||||
|
net,
|
||||||
|
);
|
||||||
|
tell!(addr, CloneRepo::new())?;
|
||||||
|
|
||||||
//then
|
//then
|
||||||
|
tick(1).await;
|
||||||
opened
|
opened
|
||||||
.read()
|
.read()
|
||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
|
@ -78,7 +95,7 @@ async fn should_open() -> TestResult {
|
||||||
/// The server config can optionally include the names of the main, next and dev
|
/// The server config can optionally include the names of the main, next and dev
|
||||||
/// branches. When it doesn't we should load the `.git-next.yaml` from from the
|
/// branches. When it doesn't we should load the `.git-next.yaml` from from the
|
||||||
/// repo and get the branch names from there by sending a [LoadConfigFromRepo] message.
|
/// repo and get the branch names from there by sending a [LoadConfigFromRepo] message.
|
||||||
#[actix::test]
|
#[tokio::test]
|
||||||
async fn when_server_has_no_repo_config_should_send_load_from_repo() -> TestResult {
|
async fn when_server_has_no_repo_config_should_send_load_from_repo() -> TestResult {
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
|
@ -95,21 +112,28 @@ async fn when_server_has_no_repo_config_should_send_load_from_repo() -> TestResu
|
||||||
let mut repository_factory = MockRepositoryFactory::new();
|
let mut repository_factory = MockRepositoryFactory::new();
|
||||||
expect::open_repository(&mut repository_factory, open_repository);
|
expect::open_repository(&mut repository_factory, open_repository);
|
||||||
fs.dir(&repo_details.gitdir).create()?;
|
fs.dir(&repo_details.gitdir).create()?;
|
||||||
|
let net: Net = given::a_network().into();
|
||||||
|
|
||||||
//when
|
//when
|
||||||
let (addr, log) = when::start_actor(repository_factory, repo_details, given::a_forge());
|
let (addr, log) = when::start_actor(
|
||||||
addr.send(CloneRepo::new()).await?;
|
repository_factory,
|
||||||
System::current().stop();
|
repo_details,
|
||||||
|
given::a_forge(),
|
||||||
|
fs.as_real(),
|
||||||
|
net,
|
||||||
|
);
|
||||||
|
tell!(addr, CloneRepo::new())?;
|
||||||
|
|
||||||
//then
|
//then
|
||||||
log.require_message_containing("send: LoadConfigFromRepo")?;
|
log.require_message_containing("send: LoadConfigFromRepo")
|
||||||
|
.await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The server config can optionally include the names of the main, next and dev
|
/// The server config can optionally include the names of the main, next and dev
|
||||||
/// branches. When it does we should register the webhook by sending [RegisterWebhook] message.
|
/// branches. When it does we should register the webhook by sending [RegisterWebhook] message.
|
||||||
#[actix::test]
|
#[test_log::test(tokio::test)]
|
||||||
async fn when_server_has_repo_config_should_send_register_webhook() -> TestResult {
|
async fn when_server_has_repo_config_should_send_register_webhook() -> TestResult {
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
|
@ -124,71 +148,23 @@ async fn when_server_has_repo_config_should_send_register_webhook() -> TestResul
|
||||||
let mut repository_factory = MockRepositoryFactory::new();
|
let mut repository_factory = MockRepositoryFactory::new();
|
||||||
expect::open_repository(&mut repository_factory, open_repository);
|
expect::open_repository(&mut repository_factory, open_repository);
|
||||||
fs.dir(&repo_details.gitdir).create()?;
|
fs.dir(&repo_details.gitdir).create()?;
|
||||||
|
let net: Net = given::a_network().into();
|
||||||
|
|
||||||
//when
|
//when
|
||||||
let (addr, log) = when::start_actor(repository_factory, repo_details, given::a_forge());
|
let (addr, log) = when::start_actor(
|
||||||
addr.send(CloneRepo::new()).await?;
|
repository_factory,
|
||||||
System::current().stop();
|
repo_details,
|
||||||
|
given::a_forge(),
|
||||||
//then
|
fs.as_real(),
|
||||||
log.require_message_containing("send: RegisterWebhook")?;
|
net,
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test_log::test(actix::test)]
|
|
||||||
async fn opened_repo_with_no_default_push_should_not_proceed() -> TestResult {
|
|
||||||
//given
|
|
||||||
let fs = given::a_filesystem();
|
|
||||||
let (mut open_repository, repo_details) = given::an_open_repository(&fs);
|
|
||||||
open_repository
|
|
||||||
.expect_fetch()
|
|
||||||
.times(1)
|
|
||||||
.return_once(|| Ok(()));
|
|
||||||
|
|
||||||
given::has_remote_defaults(
|
|
||||||
&mut open_repository,
|
|
||||||
HashMap::from([
|
|
||||||
(Direction::Push, None),
|
|
||||||
(Direction::Fetch, repo_details.remote_url()),
|
|
||||||
]),
|
|
||||||
);
|
);
|
||||||
|
tell!(addr, CloneRepo::new())?;
|
||||||
let mut repository_factory = MockRepositoryFactory::new();
|
|
||||||
expect::open_repository(&mut repository_factory, open_repository);
|
|
||||||
fs.dir(&repo_details.gitdir).create()?;
|
|
||||||
|
|
||||||
//when
|
|
||||||
let (addr, log) = when::start_actor(repository_factory, repo_details, given::a_forge());
|
|
||||||
addr.send(CloneRepo::new()).await?;
|
|
||||||
System::current().stop();
|
|
||||||
|
|
||||||
//then
|
//then
|
||||||
log.require_message_containing("open failed")?;
|
tick(1).await;
|
||||||
|
debug!(?log, "");
|
||||||
Ok(())
|
log.require_message_containing("send: RegisterWebhook")
|
||||||
}
|
.await?;
|
||||||
|
|
||||||
#[test_log::test(actix::test)]
|
|
||||||
async fn opened_repo_with_no_default_fetch_should_not_proceed() -> TestResult {
|
|
||||||
//given
|
|
||||||
let fs = given::a_filesystem();
|
|
||||||
let (mut open_repository, repo_details) = given::an_open_repository(&fs);
|
|
||||||
open_repository
|
|
||||||
.expect_fetch()
|
|
||||||
.times(1)
|
|
||||||
.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()?;
|
|
||||||
|
|
||||||
//when
|
|
||||||
let (addr, log) = when::start_actor(repository_factory, repo_details, given::a_forge());
|
|
||||||
addr.send(CloneRepo::new()).await?;
|
|
||||||
System::current().stop();
|
|
||||||
|
|
||||||
//then
|
|
||||||
log.require_message_containing("open failed")?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
|
use crate::{repo::messages::LoadConfigFromRepo, tell};
|
||||||
|
|
||||||
//
|
//
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[actix::test]
|
#[tokio::test]
|
||||||
async fn when_read_file_ok_should_send_config_loaded() -> TestResult {
|
async fn when_read_file_ok_should_send_config_loaded() -> TestResult {
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
let (mut open_repository, repo_details) = given::an_open_repository(&fs);
|
let (mut open_repository, repo_details) = given::an_open_repository(&fs);
|
||||||
let mut load_config_open_repo = MockOpenRepositoryLike::new();
|
// let mut load_config_open_repo = MockOpenRepositoryLike::new();
|
||||||
let branches = given::repo_branches();
|
let branches = given::repo_branches();
|
||||||
let remote_branches = vec![branches.main(), branches.next(), branches.dev()];
|
let remote_branches = vec![branches.main(), branches.next(), branches.dev()];
|
||||||
load_config_open_repo
|
open_repository.expect_read_file().return_once(move |_, _| {
|
||||||
.expect_read_file()
|
|
||||||
.return_once(move |_, _| {
|
|
||||||
Ok(format!(
|
Ok(format!(
|
||||||
r#"
|
r#"
|
||||||
[branches]
|
[branches]
|
||||||
|
@ -25,58 +25,46 @@ async fn when_read_file_ok_should_send_config_loaded() -> TestResult {
|
||||||
))
|
))
|
||||||
});
|
});
|
||||||
|
|
||||||
load_config_open_repo
|
open_repository
|
||||||
.expect_remote_branches()
|
.expect_remote_branches()
|
||||||
.return_once(|| Ok(remote_branches));
|
.return_once(|| Ok(remote_branches));
|
||||||
|
|
||||||
open_repository
|
|
||||||
.expect_duplicate()
|
|
||||||
.return_once(|| Box::new(load_config_open_repo));
|
|
||||||
|
|
||||||
//when
|
//when
|
||||||
let (addr, log) = when::start_actor_with_open_repository(
|
let (addr, log) = when::start_actor_with_open_repository(
|
||||||
Box::new(open_repository),
|
Box::new(open_repository),
|
||||||
repo_details,
|
repo_details,
|
||||||
given::a_forge(),
|
given::a_forge(),
|
||||||
);
|
);
|
||||||
addr.send(crate::repo::messages::LoadConfigFromRepo::new())
|
tell!(addr, LoadConfigFromRepo::new())?;
|
||||||
.await?;
|
|
||||||
System::current().stop();
|
|
||||||
|
|
||||||
//then
|
//then
|
||||||
tracing::debug!(?log, "");
|
tracing::debug!(?log, "");
|
||||||
log.require_message_containing("send: ReceiveRepoConfig")?;
|
log.require_message_containing("send: ReceiveRepoConfig")
|
||||||
log.no_message_contains("send: NotifyUsers")?;
|
.await?;
|
||||||
|
log.no_message_contains("send: NotifyUsers").await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix::test]
|
#[tokio::test]
|
||||||
async fn when_read_file_err_should_notify_user() -> TestResult {
|
async fn when_read_file_err_should_notify_user() -> TestResult {
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
let (mut open_repository, repo_details) = given::an_open_repository(&fs);
|
let (mut open_repository, repo_details) = given::an_open_repository(&fs);
|
||||||
let mut load_config_open_repo = MockOpenRepositoryLike::new();
|
open_repository
|
||||||
load_config_open_repo
|
|
||||||
.expect_read_file()
|
.expect_read_file()
|
||||||
.return_once(move |_, _| Err(git::file::Error::FileNotFound));
|
.return_once(move |_, _| Err(git::file::Error::FileNotFound));
|
||||||
|
|
||||||
open_repository
|
|
||||||
.expect_duplicate()
|
|
||||||
.return_once(|| Box::new(load_config_open_repo));
|
|
||||||
|
|
||||||
//when
|
//when
|
||||||
let (addr, log) = when::start_actor_with_open_repository(
|
let (addr, log) = when::start_actor_with_open_repository(
|
||||||
Box::new(open_repository),
|
Box::new(open_repository),
|
||||||
repo_details,
|
repo_details,
|
||||||
given::a_forge(),
|
given::a_forge(),
|
||||||
);
|
);
|
||||||
addr.send(crate::repo::messages::LoadConfigFromRepo::new())
|
tell!(addr, LoadConfigFromRepo::new())?;
|
||||||
.await?;
|
|
||||||
System::current().stop();
|
|
||||||
|
|
||||||
//then
|
//then
|
||||||
tracing::debug!(?log, "");
|
tracing::debug!(?log, "");
|
||||||
log.require_message_containing("send: NotifyUser")?;
|
log.require_message_containing("send: NotifyUser").await?;
|
||||||
log.no_message_contains("send: ReceiveRepoConfig")?;
|
log.no_message_contains("send: ReceiveRepoConfig").await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
//
|
//
|
||||||
|
use crate::{repo::messages::ReceiveRepoConfig, tell};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[actix::test]
|
#[tokio::test]
|
||||||
async fn should_store_repo_config_in_actor() -> TestResult {
|
async fn should_store_repo_config_in_actor() -> TestResult {
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
|
@ -15,16 +17,15 @@ async fn should_store_repo_config_in_actor() -> TestResult {
|
||||||
repo_details,
|
repo_details,
|
||||||
given::a_forge(),
|
given::a_forge(),
|
||||||
);
|
);
|
||||||
addr.send(crate::repo::messages::ReceiveRepoConfig::new(
|
tell!(addr, ReceiveRepoConfig::new(new_repo_config.clone()))?;
|
||||||
new_repo_config.clone(),
|
|
||||||
))
|
|
||||||
.await?;
|
|
||||||
System::current().stop();
|
|
||||||
|
|
||||||
//then
|
//then
|
||||||
tracing::debug!(?log, "");
|
tracing::debug!(?log, "");
|
||||||
|
|
||||||
let reo_actor_view = addr.send(ExamineActor).await?;
|
let reo_actor_view = addr
|
||||||
|
.ask(ExamineActor)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("examine actor: {e:?}"))?;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
reo_actor_view.repo_details.repo_config,
|
reo_actor_view.repo_details.repo_config,
|
||||||
Some(new_repo_config)
|
Some(new_repo_config)
|
||||||
|
@ -32,7 +33,7 @@ async fn should_store_repo_config_in_actor() -> TestResult {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test_log::test(actix::test)]
|
#[test_log::test(tokio::test)]
|
||||||
async fn should_register_webhook() -> TestResult {
|
async fn should_register_webhook() -> TestResult {
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
|
@ -46,14 +47,11 @@ async fn should_register_webhook() -> TestResult {
|
||||||
repo_details,
|
repo_details,
|
||||||
given::a_forge(),
|
given::a_forge(),
|
||||||
);
|
);
|
||||||
addr.send(crate::repo::messages::ReceiveRepoConfig::new(
|
tell!(addr, ReceiveRepoConfig::new(new_repo_config.clone()))?;
|
||||||
new_repo_config.clone(),
|
|
||||||
))
|
|
||||||
.await?;
|
|
||||||
System::current().stop();
|
|
||||||
|
|
||||||
//then
|
//then
|
||||||
tracing::debug!(?log, "");
|
tracing::debug!(?log, "");
|
||||||
log.require_message_containing("send: RegisterWebhook")?;
|
log.require_message_containing("send: RegisterWebhook")
|
||||||
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use git::forge::commit::Status;
|
||||||
|
|
||||||
|
use crate::{repo::messages::ReceiveCIStatus, tell};
|
||||||
|
|
||||||
//
|
//
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test_log::test(actix::test)]
|
#[test_log::test(tokio::test)]
|
||||||
async fn when_pass_should_advance_main_to_next() -> TestResult {
|
async fn when_pass_should_advance_main_to_next() -> TestResult {
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
|
@ -16,24 +20,19 @@ async fn when_pass_should_advance_main_to_next() -> TestResult {
|
||||||
repo_details,
|
repo_details,
|
||||||
given::a_forge(),
|
given::a_forge(),
|
||||||
);
|
);
|
||||||
addr.send(crate::repo::messages::ReceiveCIStatus::new((
|
tell!(
|
||||||
next_commit.clone(),
|
addr,
|
||||||
git::forge::commit::Status::Pass,
|
ReceiveCIStatus::new((next_commit.clone(), Status::Pass))
|
||||||
)))
|
)?;
|
||||||
.await?;
|
|
||||||
System::current().stop();
|
|
||||||
|
|
||||||
//then
|
//then
|
||||||
tracing::debug!(?log, "");
|
tracing::debug!(?log, "");
|
||||||
log.read().map_err(|e| e.to_string()).map(|l| {
|
log.require_message_containing(format!("send: AdvanceMain({next_commit:?})"))
|
||||||
let expected = format!("send: AdvanceMain({next_commit:?})");
|
.await?;
|
||||||
tracing::debug!(%expected,"");
|
|
||||||
assert!(l.iter().any(|message| message.contains(&expected)));
|
|
||||||
})?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test_log::test(actix::test)]
|
#[test_log::test(tokio::test)]
|
||||||
async fn when_pending_should_recheck_ci_status() -> TestResult {
|
async fn when_pending_should_recheck_ci_status() -> TestResult {
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
|
@ -46,21 +45,19 @@ async fn when_pending_should_recheck_ci_status() -> TestResult {
|
||||||
repo_details,
|
repo_details,
|
||||||
given::a_forge(),
|
given::a_forge(),
|
||||||
);
|
);
|
||||||
addr.send(crate::repo::messages::ReceiveCIStatus::new((
|
tell!(
|
||||||
next_commit.clone(),
|
addr,
|
||||||
git::forge::commit::Status::Pending,
|
ReceiveCIStatus::new((next_commit.clone(), Status::Pending))
|
||||||
)))
|
)?;
|
||||||
.await?;
|
tokio::time::sleep(Duration::from_millis(9)).await;
|
||||||
actix_rt::time::sleep(Duration::from_millis(9)).await;
|
|
||||||
System::current().stop();
|
|
||||||
|
|
||||||
//then
|
//then
|
||||||
tracing::debug!(?log, "");
|
tracing::debug!(?log, "");
|
||||||
log.require_message_containing("send: ValidateRepo")?;
|
log.require_message_containing("send: ValidateRepo").await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test_log::test(actix::test)]
|
#[test_log::test(tokio::test)]
|
||||||
async fn when_fail_should_recheck_after_delay() -> TestResult {
|
async fn when_fail_should_recheck_after_delay() -> TestResult {
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
|
@ -73,20 +70,18 @@ async fn when_fail_should_recheck_after_delay() -> TestResult {
|
||||||
repo_details,
|
repo_details,
|
||||||
given::a_forge(),
|
given::a_forge(),
|
||||||
);
|
);
|
||||||
addr.send(crate::repo::messages::ReceiveCIStatus::new((
|
tell!(
|
||||||
next_commit.clone(),
|
addr,
|
||||||
git::forge::commit::Status::Fail,
|
ReceiveCIStatus::new((next_commit.clone(), Status::Fail))
|
||||||
)))
|
)?;
|
||||||
.await?;
|
tokio::time::sleep(Duration::from_millis(9)).await;
|
||||||
actix_rt::time::sleep(Duration::from_millis(9)).await;
|
|
||||||
System::current().stop();
|
|
||||||
|
|
||||||
//then
|
//then
|
||||||
log.require_message_containing("send: ValidateRepo")?;
|
log.require_message_containing("send: ValidateRepo").await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test_log::test(actix::test)]
|
#[test_log::test(tokio::test)]
|
||||||
async fn when_fail_should_notify_user() -> TestResult {
|
async fn when_fail_should_notify_user() -> TestResult {
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
|
@ -99,14 +94,12 @@ async fn when_fail_should_notify_user() -> TestResult {
|
||||||
repo_details,
|
repo_details,
|
||||||
given::a_forge(),
|
given::a_forge(),
|
||||||
);
|
);
|
||||||
addr.send(crate::repo::messages::ReceiveCIStatus::new((
|
tell!(
|
||||||
next_commit.clone(),
|
addr,
|
||||||
git::forge::commit::Status::Fail,
|
ReceiveCIStatus::new((next_commit.clone(), Status::Fail))
|
||||||
)))
|
)?;
|
||||||
.await?;
|
|
||||||
System::current().stop();
|
|
||||||
|
|
||||||
//then
|
//then
|
||||||
log.require_message_containing("send: NotifyUser")?;
|
log.require_message_containing("send: NotifyUser").await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,71 +1,57 @@
|
||||||
|
use crate::{repo::messages::RegisterWebhook, tell};
|
||||||
|
|
||||||
//
|
//
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[actix::test]
|
#[tokio::test]
|
||||||
async fn when_registered_ok_should_send_webhook_registered() -> TestResult {
|
async fn when_registered_ok_should_send_webhook_registered() -> TestResult {
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
let (open_repository, repo_details) = given::an_open_repository(&fs);
|
let (open_repository, repo_details) = given::an_open_repository(&fs);
|
||||||
|
|
||||||
let registered_webhook = given::a_registered_webhook();
|
let registered_webhook = given::a_registered_webhook();
|
||||||
let mut my_forge = git::MockForgeLike::new();
|
let mut forge = git::MockForgeLike::new();
|
||||||
my_forge
|
forge
|
||||||
.expect_register_webhook()
|
.expect_register_webhook()
|
||||||
.return_once(move |_| Ok(registered_webhook));
|
.return_once(move |_| Ok(registered_webhook));
|
||||||
|
|
||||||
let mut forge = git::MockForgeLike::new();
|
|
||||||
forge.expect_duplicate().return_once(|| Box::new(my_forge));
|
|
||||||
|
|
||||||
//when
|
//when
|
||||||
let (addr, log) = when::start_actor_with_open_repository(
|
let (addr, log) = when::start_actor_with_open_repository(
|
||||||
Box::new(open_repository),
|
Box::new(open_repository),
|
||||||
repo_details,
|
repo_details,
|
||||||
Box::new(forge),
|
Box::new(forge),
|
||||||
);
|
);
|
||||||
addr.send(crate::repo::messages::RegisterWebhook::new())
|
tell!(addr, RegisterWebhook::new())?;
|
||||||
.await?;
|
|
||||||
System::current().stop();
|
|
||||||
|
|
||||||
//then
|
//then
|
||||||
tracing::debug!(?log, "");
|
tracing::debug!(?log, "");
|
||||||
log.read().map_err(|e| e.to_string()).map(|l| {
|
log.require_message_containing("send: WebhookRegistered")
|
||||||
assert!(l
|
.await?;
|
||||||
.iter()
|
|
||||||
.any(|message| message.contains("send: WebhookRegistered")));
|
|
||||||
})?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix::test]
|
#[tokio::test]
|
||||||
async fn when_registered_error_should_send_notify_user() -> TestResult {
|
async fn when_registered_error_should_send_notify_user() -> TestResult {
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
let (open_repository, repo_details) = given::an_open_repository(&fs);
|
let (open_repository, repo_details) = given::an_open_repository(&fs);
|
||||||
|
|
||||||
let mut my_forge = git::MockForgeLike::new();
|
let mut forge = git::MockForgeLike::new();
|
||||||
my_forge.expect_register_webhook().return_once(move |_| {
|
forge.expect_register_webhook().return_once(move |_| {
|
||||||
Err(git::forge::webhook::Error::FailedToRegister(
|
Err(git::forge::webhook::Error::FailedToRegister(
|
||||||
"foo".to_string(),
|
"foo".to_string(),
|
||||||
))
|
))
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut forge = git::MockForgeLike::new();
|
|
||||||
forge.expect_duplicate().return_once(|| Box::new(my_forge));
|
|
||||||
|
|
||||||
//when
|
//when
|
||||||
let (addr, log) = when::start_actor_with_open_repository(
|
let (addr, log) = when::start_actor_with_open_repository(
|
||||||
Box::new(open_repository),
|
Box::new(open_repository),
|
||||||
repo_details,
|
repo_details,
|
||||||
Box::new(forge),
|
Box::new(forge),
|
||||||
);
|
);
|
||||||
addr.send(crate::repo::messages::RegisterWebhook::new())
|
tell!(addr, crate::repo::messages::RegisterWebhook::new())?;
|
||||||
.await?;
|
|
||||||
System::current().stop();
|
|
||||||
|
|
||||||
//then
|
//then
|
||||||
tracing::debug!(?log, "");
|
tracing::debug!(?log, "");
|
||||||
log.read()
|
log.require_message_containing("send: NotifyUser").await?;
|
||||||
.map_err(|e| e.to_string())
|
|
||||||
.map(|l| assert!(l.iter().any(|message| message.contains("send: NotifyUser"))))?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,14 @@
|
||||||
use crate::repo::messages::{AdvanceNext, AdvanceNextPayload};
|
|
||||||
|
|
||||||
//
|
//
|
||||||
|
use kxio::net::Net;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
repo::messages::{AdvanceNext, AdvanceNextPayload, ValidateRepo},
|
||||||
|
tell,
|
||||||
|
};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test_log::test(actix::test)]
|
#[test_log::test(tokio::test)]
|
||||||
async fn repo_with_next_not_an_ancestor_of_dev_and_dev_on_main_should_be_reset_to_main(
|
async fn repo_with_next_not_an_ancestor_of_dev_and_dev_on_main_should_be_reset_to_main(
|
||||||
) -> TestResult {
|
) -> TestResult {
|
||||||
//given
|
//given
|
||||||
|
@ -41,18 +46,15 @@ async fn repo_with_next_not_an_ancestor_of_dev_and_dev_on_main_should_be_reset_t
|
||||||
repo_details,
|
repo_details,
|
||||||
given::a_forge(),
|
given::a_forge(),
|
||||||
);
|
);
|
||||||
addr.send(crate::repo::messages::ValidateRepo::new(
|
tell!(addr, ValidateRepo::new(MessageToken::default()))?;
|
||||||
MessageToken::default(),
|
|
||||||
))
|
|
||||||
.await?;
|
|
||||||
System::current().stop();
|
|
||||||
|
|
||||||
//then
|
//then
|
||||||
log.require_message_containing(format!("Branch {} has been reset", branches.next()))?;
|
log.require_message_containing(format!("Branch {} has been reset", branches.next()))
|
||||||
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test_log::test(actix::test)]
|
#[test_log::test(tokio::test)]
|
||||||
async fn repo_with_next_not_an_ancestor_of_dev_and_dev_ahead_of_main_should_be_reset_to_dev(
|
async fn repo_with_next_not_an_ancestor_of_dev_and_dev_ahead_of_main_should_be_reset_to_dev(
|
||||||
) -> TestResult {
|
) -> TestResult {
|
||||||
//given
|
//given
|
||||||
|
@ -92,11 +94,10 @@ async fn repo_with_next_not_an_ancestor_of_dev_and_dev_ahead_of_main_should_be_r
|
||||||
repo_details,
|
repo_details,
|
||||||
given::a_forge(),
|
given::a_forge(),
|
||||||
);
|
);
|
||||||
addr.send(crate::repo::messages::ValidateRepo::new(
|
tell!(
|
||||||
MessageToken::default(),
|
addr,
|
||||||
))
|
crate::repo::messages::ValidateRepo::new(MessageToken::default())
|
||||||
.await?;
|
)?;
|
||||||
System::current().stop();
|
|
||||||
|
|
||||||
//then
|
//then
|
||||||
let expected = AdvanceNext::new(AdvanceNextPayload {
|
let expected = AdvanceNext::new(AdvanceNextPayload {
|
||||||
|
@ -104,11 +105,12 @@ async fn repo_with_next_not_an_ancestor_of_dev_and_dev_ahead_of_main_should_be_r
|
||||||
main: main_commit,
|
main: main_commit,
|
||||||
dev_commit_history: dev_branch_log,
|
dev_commit_history: dev_branch_log,
|
||||||
});
|
});
|
||||||
log.require_message_containing(format!("send: {expected:?}",))?;
|
log.require_message_containing(format!("send: {expected:?}",))
|
||||||
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test_log::test(actix::test)]
|
#[test_log::test(tokio::test)]
|
||||||
async fn repo_with_next_not_on_or_near_main_should_be_reset() -> TestResult {
|
async fn repo_with_next_not_on_or_near_main_should_be_reset() -> TestResult {
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
|
@ -145,18 +147,15 @@ async fn repo_with_next_not_on_or_near_main_should_be_reset() -> TestResult {
|
||||||
repo_details,
|
repo_details,
|
||||||
given::a_forge(),
|
given::a_forge(),
|
||||||
);
|
);
|
||||||
addr.send(crate::repo::messages::ValidateRepo::new(
|
tell!(addr, ValidateRepo::new(MessageToken::default()))?;
|
||||||
MessageToken::default(),
|
|
||||||
))
|
|
||||||
.await?;
|
|
||||||
System::current().stop();
|
|
||||||
|
|
||||||
//then
|
//then
|
||||||
log.require_message_containing(format!("Branch {} has been reset", branches.next()))?;
|
log.require_message_containing(format!("Branch {} has been reset", branches.next()))
|
||||||
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test_log::test(actix::test)]
|
#[test_log::test(tokio::test)]
|
||||||
async fn repo_with_next_not_based_on_main_should_be_reset() -> TestResult {
|
async fn repo_with_next_not_based_on_main_should_be_reset() -> TestResult {
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
|
@ -193,18 +192,15 @@ async fn repo_with_next_not_based_on_main_should_be_reset() -> TestResult {
|
||||||
repo_details,
|
repo_details,
|
||||||
given::a_forge(),
|
given::a_forge(),
|
||||||
);
|
);
|
||||||
addr.send(crate::repo::messages::ValidateRepo::new(
|
tell!(addr, ValidateRepo::new(MessageToken::default()))?;
|
||||||
MessageToken::default(),
|
|
||||||
))
|
|
||||||
.await?;
|
|
||||||
System::current().stop();
|
|
||||||
|
|
||||||
//then
|
//then
|
||||||
log.require_message_containing(format!("Branch {} has been reset", branches.next()))?;
|
log.require_message_containing(format!("Branch {} has been reset", branches.next()))
|
||||||
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test_log::test(actix::test)]
|
#[test_log::test(tokio::test)]
|
||||||
async fn repo_with_next_ahead_of_main_should_check_ci_status() -> TestResult {
|
async fn repo_with_next_ahead_of_main_should_check_ci_status() -> TestResult {
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
|
@ -240,18 +236,15 @@ async fn repo_with_next_ahead_of_main_should_check_ci_status() -> TestResult {
|
||||||
repo_details,
|
repo_details,
|
||||||
given::a_forge(),
|
given::a_forge(),
|
||||||
);
|
);
|
||||||
addr.send(crate::repo::messages::ValidateRepo::new(
|
tell!(addr, ValidateRepo::new(MessageToken::default()))?;
|
||||||
MessageToken::default(),
|
|
||||||
))
|
|
||||||
.await?;
|
|
||||||
System::current().stop();
|
|
||||||
|
|
||||||
//then
|
//then
|
||||||
log.require_message_containing(format!("send: CheckCIStatus({next_commit:?})"))?;
|
log.require_message_containing(format!("send: CheckCIStatus({next_commit:?})"))
|
||||||
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test_log::test(actix::test)]
|
#[test_log::test(tokio::test)]
|
||||||
async fn repo_with_dev_and_next_on_main_should_do_nothing() -> TestResult {
|
async fn repo_with_dev_and_next_on_main_should_do_nothing() -> TestResult {
|
||||||
// Do nothing, when the situation changes we will hear about it via a webhook
|
// Do nothing, when the situation changes we will hear about it via a webhook
|
||||||
//given
|
//given
|
||||||
|
@ -288,18 +281,14 @@ async fn repo_with_dev_and_next_on_main_should_do_nothing() -> TestResult {
|
||||||
repo_details,
|
repo_details,
|
||||||
given::a_forge(),
|
given::a_forge(),
|
||||||
);
|
);
|
||||||
addr.send(crate::repo::messages::ValidateRepo::new(
|
tell!(addr, ValidateRepo::new(MessageToken::default()))?;
|
||||||
MessageToken::default(),
|
|
||||||
))
|
|
||||||
.await?;
|
|
||||||
System::current().stop();
|
|
||||||
|
|
||||||
//then
|
//then
|
||||||
log.no_message_contains("send:")?;
|
log.no_message_contains("send:").await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test_log::test(actix::test)]
|
#[test_log::test(tokio::test)]
|
||||||
async fn repo_with_dev_ahead_of_next_should_advance_next() -> TestResult {
|
async fn repo_with_dev_ahead_of_next_should_advance_next() -> TestResult {
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
|
@ -337,11 +326,7 @@ async fn repo_with_dev_ahead_of_next_should_advance_next() -> TestResult {
|
||||||
repo_details,
|
repo_details,
|
||||||
given::a_forge(),
|
given::a_forge(),
|
||||||
);
|
);
|
||||||
addr.send(crate::repo::messages::ValidateRepo::new(
|
tell!(addr, ValidateRepo::new(MessageToken::default()))?;
|
||||||
MessageToken::default(),
|
|
||||||
))
|
|
||||||
.await?;
|
|
||||||
System::current().stop();
|
|
||||||
|
|
||||||
//then
|
//then
|
||||||
let expected = AdvanceNext::new(AdvanceNextPayload {
|
let expected = AdvanceNext::new(AdvanceNextPayload {
|
||||||
|
@ -349,11 +334,12 @@ async fn repo_with_dev_ahead_of_next_should_advance_next() -> TestResult {
|
||||||
main: main_commit,
|
main: main_commit,
|
||||||
dev_commit_history: dev_branch_log,
|
dev_commit_history: dev_branch_log,
|
||||||
});
|
});
|
||||||
log.require_message_containing(format!("send: {expected:?}"))?;
|
log.require_message_containing(format!("send: {expected:?}"))
|
||||||
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test_log::test(actix::test)]
|
#[test_log::test(tokio::test)]
|
||||||
async fn repo_with_dev_not_ahead_of_main_should_notify_user() -> TestResult {
|
async fn repo_with_dev_not_ahead_of_main_should_notify_user() -> TestResult {
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
|
@ -391,96 +377,87 @@ async fn repo_with_dev_not_ahead_of_main_should_notify_user() -> TestResult {
|
||||||
repo_details,
|
repo_details,
|
||||||
given::a_forge(),
|
given::a_forge(),
|
||||||
);
|
);
|
||||||
addr.send(crate::repo::messages::ValidateRepo::new(
|
tell!(addr, ValidateRepo::new(MessageToken::default()))?;
|
||||||
MessageToken::default(),
|
|
||||||
))
|
|
||||||
.await?;
|
|
||||||
System::current().stop();
|
|
||||||
|
|
||||||
//then
|
//then
|
||||||
log.require_message_containing("send: NotifyUser")?;
|
log.require_message_containing("send: NotifyUser").await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test_log::test(actix::test)]
|
#[test_log::test(tokio::test)]
|
||||||
async fn should_accept_message_with_current_token() -> TestResult {
|
async fn should_accept_message_with_current_token() -> TestResult {
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
let repo_details = given::repo_details(&fs);
|
let repo_details = given::repo_details(&fs);
|
||||||
|
let net: Net = given::a_network().into();
|
||||||
//when
|
let server_actor_ref = given::a_server_actor(fs.as_real(), net.clone());
|
||||||
let (actor, log) = given::a_repo_actor(
|
let (actor, log) = given::a_repo_actor(
|
||||||
repo_details,
|
repo_details,
|
||||||
git::repository::factory::mock(),
|
git::repository::factory::mock(),
|
||||||
given::a_forge(),
|
given::a_forge(),
|
||||||
given::a_network().into(),
|
server_actor_ref,
|
||||||
|
net,
|
||||||
);
|
);
|
||||||
let actor = actor.with_message_token(MessageToken::new(2_u32));
|
let actor = actor.with_message_token(MessageToken::new(2_u32));
|
||||||
let addr = actor.start();
|
let addr = kameo::spawn(actor);
|
||||||
addr.send(crate::repo::messages::ValidateRepo::new(MessageToken::new(
|
//
|
||||||
2_u32,
|
//when
|
||||||
)))
|
tell!(addr, ValidateRepo::new(MessageToken::new(2_u32)))?;
|
||||||
.await?;
|
|
||||||
System::current().stop();
|
|
||||||
|
|
||||||
//then
|
//then
|
||||||
log.require_message_containing("accepted token: 2")?;
|
log.require_message_containing("accepted token: 2").await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test_log::test(actix::test)]
|
#[test_log::test(tokio::test)]
|
||||||
async fn should_accept_message_with_new_token() -> TestResult {
|
async fn should_accept_message_with_new_token() -> TestResult {
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
let repo_details = given::repo_details(&fs);
|
let repo_details = given::repo_details(&fs);
|
||||||
|
let net: Net = given::a_network().into();
|
||||||
|
|
||||||
//when
|
//when
|
||||||
let (actor, log) = given::a_repo_actor(
|
let (actor, log) = given::a_repo_actor(
|
||||||
repo_details,
|
repo_details,
|
||||||
git::repository::factory::mock(),
|
git::repository::factory::mock(),
|
||||||
given::a_forge(),
|
given::a_forge(),
|
||||||
given::a_network().into(),
|
given::a_server_actor(fs.as_real(), net.clone()),
|
||||||
|
net,
|
||||||
);
|
);
|
||||||
let actor = actor.with_message_token(MessageToken::new(2_u32));
|
let actor = actor.with_message_token(MessageToken::new(2_u32));
|
||||||
let addr = actor.start();
|
let addr = kameo::spawn(actor);
|
||||||
addr.send(crate::repo::messages::ValidateRepo::new(MessageToken::new(
|
tell!(addr, ValidateRepo::new(MessageToken::new(3_u32)))?;
|
||||||
3_u32,
|
|
||||||
)))
|
|
||||||
.await?;
|
|
||||||
System::current().stop();
|
|
||||||
|
|
||||||
//then
|
//then
|
||||||
log.require_message_containing("accepted token: 3")?;
|
log.require_message_containing("accepted token: 3").await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test_log::test(actix::test)]
|
#[test_log::test(tokio::test)]
|
||||||
async fn should_reject_message_with_expired_token() -> TestResult {
|
async fn should_reject_message_with_expired_token() -> TestResult {
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
let repo_details = given::repo_details(&fs);
|
let repo_details = given::repo_details(&fs);
|
||||||
|
let net: Net = given::a_network().into();
|
||||||
|
|
||||||
//when
|
//when
|
||||||
let (actor, log) = given::a_repo_actor(
|
let (actor, log) = given::a_repo_actor(
|
||||||
repo_details,
|
repo_details,
|
||||||
git::repository::factory::mock(),
|
git::repository::factory::mock(),
|
||||||
given::a_forge(),
|
given::a_forge(),
|
||||||
given::a_network().into(),
|
given::a_server_actor(fs.as_real(), net.clone()),
|
||||||
|
net,
|
||||||
);
|
);
|
||||||
let actor = actor.with_message_token(MessageToken::new(4_u32));
|
let actor = actor.with_message_token(MessageToken::new(4_u32));
|
||||||
let addr = actor.start();
|
let addr = kameo::spawn(actor);
|
||||||
addr.send(crate::repo::messages::ValidateRepo::new(MessageToken::new(
|
tell!(addr, ValidateRepo::new(MessageToken::new(3_u32)))?;
|
||||||
3_u32,
|
|
||||||
)))
|
|
||||||
.await?;
|
|
||||||
System::current().stop();
|
|
||||||
|
|
||||||
//then
|
//then
|
||||||
log.no_message_contains("accepted token")?;
|
log.no_message_contains("accepted token").await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test_log::test(actix::test)]
|
#[test_log::test(tokio::test)]
|
||||||
// NOTE: failed then passed on retry: count = 6
|
// NOTE: failed then passed on retry: count = 6
|
||||||
async fn should_send_validate_repo_when_retryable_error() -> TestResult {
|
async fn should_send_validate_repo_when_retryable_error() -> TestResult {
|
||||||
//given
|
//given
|
||||||
|
@ -497,20 +474,16 @@ async fn should_send_validate_repo_when_retryable_error() -> TestResult {
|
||||||
repo_details,
|
repo_details,
|
||||||
given::a_forge(),
|
given::a_forge(),
|
||||||
);
|
);
|
||||||
addr.send(crate::repo::messages::ValidateRepo::new(
|
tell!(addr, ValidateRepo::new(MessageToken::default()))?;
|
||||||
MessageToken::default(),
|
tokio::time::sleep(std::time::Duration::from_millis(2)).await;
|
||||||
))
|
|
||||||
.await?;
|
|
||||||
actix_rt::time::sleep(std::time::Duration::from_millis(2)).await;
|
|
||||||
System::current().stop();
|
|
||||||
|
|
||||||
//then
|
//then
|
||||||
log.require_message_containing("accepted token: 0")?;
|
log.require_message_containing("accepted token: 0").await?;
|
||||||
log.require_message_containing("send: ValidateRepo")?;
|
log.require_message_containing("send: ValidateRepo").await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test_log::test(actix::test)]
|
#[test_log::test(tokio::test)]
|
||||||
async fn should_send_notify_user_when_non_retryable_error() -> TestResult {
|
async fn should_send_notify_user_when_non_retryable_error() -> TestResult {
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
|
@ -546,15 +519,11 @@ async fn should_send_notify_user_when_non_retryable_error() -> TestResult {
|
||||||
repo_details,
|
repo_details,
|
||||||
given::a_forge(),
|
given::a_forge(),
|
||||||
);
|
);
|
||||||
addr.send(crate::repo::messages::ValidateRepo::new(
|
tell!(addr, ValidateRepo::new(MessageToken::default()))?;
|
||||||
MessageToken::default(),
|
tokio::time::sleep(std::time::Duration::from_millis(1)).await;
|
||||||
))
|
|
||||||
.await?;
|
|
||||||
actix_rt::time::sleep(std::time::Duration::from_millis(1)).await;
|
|
||||||
System::current().stop();
|
|
||||||
|
|
||||||
//then
|
//then
|
||||||
log.require_message_containing("accepted token")?;
|
log.require_message_containing("accepted token").await?;
|
||||||
log.require_message_containing("send: NotifyUser")?;
|
log.require_message_containing("send: NotifyUser").await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
|
use kxio::net::Net;
|
||||||
|
|
||||||
|
use crate::{repo::messages::WebhookNotification, tell};
|
||||||
|
|
||||||
//
|
//
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[actix::test]
|
#[tokio::test]
|
||||||
async fn when_no_expected_auth_token_drop_notification() -> TestResult {
|
async fn when_no_expected_auth_token_drop_notification() -> TestResult {
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
|
@ -12,30 +16,30 @@ async fn when_no_expected_auth_token_drop_notification() -> TestResult {
|
||||||
let body = Body::new(String::new());
|
let body = Body::new(String::new());
|
||||||
let forge_notification = ForgeNotification::new(forge_alias, repo_alias, headers, body);
|
let forge_notification = ForgeNotification::new(forge_alias, repo_alias, headers, body);
|
||||||
let repository_factory = MockRepositoryFactory::new();
|
let repository_factory = MockRepositoryFactory::new();
|
||||||
|
let net: Net = given::a_network().into();
|
||||||
let (actor, log) = given::a_repo_actor(
|
let (actor, log) = given::a_repo_actor(
|
||||||
repo_details,
|
repo_details,
|
||||||
Box::new(repository_factory),
|
Box::new(repository_factory),
|
||||||
given::a_forge(),
|
given::a_forge(),
|
||||||
given::a_network().into(),
|
given::a_server_actor(fs.as_real(), net.clone()),
|
||||||
|
net,
|
||||||
);
|
);
|
||||||
let actor = actor.with_webhook_auth(None);
|
let actor = actor.with_webhook_auth(None);
|
||||||
|
|
||||||
//when
|
//when
|
||||||
actor
|
tell!(
|
||||||
.start()
|
kameo::spawn(actor),
|
||||||
.send(crate::repo::messages::WebhookNotification::new(
|
WebhookNotification::new(forge_notification)
|
||||||
forge_notification,
|
)?;
|
||||||
))
|
|
||||||
.await?;
|
|
||||||
System::current().stop();
|
|
||||||
|
|
||||||
//then
|
//then
|
||||||
log.no_message_contains("send")?;
|
log.no_message_contains("send").await?;
|
||||||
log.require_message_containing("server has no auth token")?;
|
log.require_message_containing("server has no auth token")
|
||||||
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix::test]
|
#[tokio::test]
|
||||||
async fn when_no_repo_config_drop_notification() -> TestResult {
|
async fn when_no_repo_config_drop_notification() -> TestResult {
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
|
@ -46,30 +50,30 @@ async fn when_no_repo_config_drop_notification() -> TestResult {
|
||||||
let body = Body::new(String::new());
|
let body = Body::new(String::new());
|
||||||
let forge_notification = ForgeNotification::new(forge_alias, repo_alias, headers, body);
|
let forge_notification = ForgeNotification::new(forge_alias, repo_alias, headers, body);
|
||||||
let repository_factory = MockRepositoryFactory::new();
|
let repository_factory = MockRepositoryFactory::new();
|
||||||
|
let net: Net = given::a_network().into();
|
||||||
let (actor, log) = given::a_repo_actor(
|
let (actor, log) = given::a_repo_actor(
|
||||||
repo_details,
|
repo_details,
|
||||||
Box::new(repository_factory),
|
Box::new(repository_factory),
|
||||||
given::a_forge(),
|
given::a_forge(),
|
||||||
given::a_network().into(),
|
given::a_server_actor(fs.as_real(), net.clone()),
|
||||||
|
net,
|
||||||
);
|
);
|
||||||
let actor = actor.with_webhook_auth(Some(given::a_webhook_auth()));
|
let actor = actor.with_webhook_auth(Some(given::a_webhook_auth()));
|
||||||
|
|
||||||
//when
|
//when
|
||||||
actor
|
tell!(
|
||||||
.start()
|
kameo::spawn(actor),
|
||||||
.send(crate::repo::messages::WebhookNotification::new(
|
WebhookNotification::new(forge_notification)
|
||||||
forge_notification,
|
)?;
|
||||||
))
|
|
||||||
.await?;
|
|
||||||
System::current().stop();
|
|
||||||
|
|
||||||
//then
|
//then
|
||||||
log.no_message_contains("send")?;
|
log.no_message_contains("send").await?;
|
||||||
log.require_message_containing("server has no repo config")?;
|
log.require_message_containing("server has no repo config")
|
||||||
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix::test]
|
#[tokio::test]
|
||||||
async fn when_message_auth_is_invalid_drop_notification() -> TestResult {
|
async fn when_message_auth_is_invalid_drop_notification() -> TestResult {
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
|
@ -84,30 +88,30 @@ async fn when_message_auth_is_invalid_drop_notification() -> TestResult {
|
||||||
forge
|
forge
|
||||||
.expect_is_message_authorised()
|
.expect_is_message_authorised()
|
||||||
.return_once(|_, _| false); // is not valid
|
.return_once(|_, _| false); // is not valid
|
||||||
|
let net: Net = given::a_network().into();
|
||||||
let (actor, log) = given::a_repo_actor(
|
let (actor, log) = given::a_repo_actor(
|
||||||
repo_details,
|
repo_details,
|
||||||
Box::new(repository_factory),
|
Box::new(repository_factory),
|
||||||
forge,
|
forge,
|
||||||
given::a_network().into(),
|
given::a_server_actor(fs.as_real(), net.clone()),
|
||||||
|
net,
|
||||||
);
|
);
|
||||||
let actor = actor.with_webhook_auth(Some(given::a_webhook_auth()));
|
let actor = actor.with_webhook_auth(Some(given::a_webhook_auth()));
|
||||||
|
|
||||||
//when
|
//when
|
||||||
actor
|
tell!(
|
||||||
.start()
|
kameo::spawn(actor),
|
||||||
.send(crate::repo::messages::WebhookNotification::new(
|
WebhookNotification::new(forge_notification)
|
||||||
forge_notification,
|
)?;
|
||||||
))
|
|
||||||
.await?;
|
|
||||||
System::current().stop();
|
|
||||||
|
|
||||||
//then
|
//then
|
||||||
log.no_message_contains("send")?;
|
log.no_message_contains("send").await?;
|
||||||
log.require_message_containing("message authorisation is invalid")?;
|
log.require_message_containing("message authorisation is invalid")
|
||||||
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix::test]
|
#[tokio::test]
|
||||||
async fn when_message_is_ignorable_drop_notification() -> TestResult {
|
async fn when_message_is_ignorable_drop_notification() -> TestResult {
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
|
@ -126,30 +130,30 @@ async fn when_message_is_ignorable_drop_notification() -> TestResult {
|
||||||
forge
|
forge
|
||||||
.expect_parse_webhook_body()
|
.expect_parse_webhook_body()
|
||||||
.return_once(|_| Err(git::forge::webhook::Error::NetworkResponseEmpty));
|
.return_once(|_| Err(git::forge::webhook::Error::NetworkResponseEmpty));
|
||||||
|
let net: Net = given::a_network().into();
|
||||||
let (actor, log) = given::a_repo_actor(
|
let (actor, log) = given::a_repo_actor(
|
||||||
repo_details,
|
repo_details,
|
||||||
Box::new(repository_factory),
|
Box::new(repository_factory),
|
||||||
forge,
|
forge,
|
||||||
given::a_network().into(),
|
given::a_server_actor(fs.as_real(), net.clone()),
|
||||||
|
net,
|
||||||
);
|
);
|
||||||
let actor = actor.with_webhook_auth(Some(given::a_webhook_auth()));
|
let actor = actor.with_webhook_auth(Some(given::a_webhook_auth()));
|
||||||
|
|
||||||
//when
|
//when
|
||||||
actor
|
tell!(
|
||||||
.start()
|
kameo::spawn(actor),
|
||||||
.send(crate::repo::messages::WebhookNotification::new(
|
WebhookNotification::new(forge_notification)
|
||||||
forge_notification,
|
)?;
|
||||||
))
|
|
||||||
.await?;
|
|
||||||
System::current().stop();
|
|
||||||
|
|
||||||
//then
|
//then
|
||||||
log.no_message_contains("send")?;
|
log.no_message_contains("send").await?;
|
||||||
log.require_message_containing("forge sent ignorable message")?;
|
log.require_message_containing("forge sent ignorable message")
|
||||||
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix::test]
|
#[tokio::test]
|
||||||
async fn when_message_is_not_a_push_drop_notification() -> TestResult {
|
async fn when_message_is_not_a_push_drop_notification() -> TestResult {
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
|
@ -168,30 +172,30 @@ async fn when_message_is_not_a_push_drop_notification() -> TestResult {
|
||||||
forge
|
forge
|
||||||
.expect_parse_webhook_body()
|
.expect_parse_webhook_body()
|
||||||
.return_once(|_| Err(git::forge::webhook::Error::NetworkResponseEmpty));
|
.return_once(|_| Err(git::forge::webhook::Error::NetworkResponseEmpty));
|
||||||
|
let net: Net = given::a_network().into();
|
||||||
let (actor, log) = given::a_repo_actor(
|
let (actor, log) = given::a_repo_actor(
|
||||||
repo_details,
|
repo_details,
|
||||||
Box::new(repository_factory),
|
Box::new(repository_factory),
|
||||||
forge,
|
forge,
|
||||||
given::a_network().into(),
|
given::a_server_actor(fs.as_real(), net.clone()),
|
||||||
|
net,
|
||||||
);
|
);
|
||||||
let actor = actor.with_webhook_auth(Some(given::a_webhook_auth()));
|
let actor = actor.with_webhook_auth(Some(given::a_webhook_auth()));
|
||||||
|
|
||||||
//when
|
//when
|
||||||
actor
|
tell!(
|
||||||
.start()
|
kameo::spawn(actor),
|
||||||
.send(crate::repo::messages::WebhookNotification::new(
|
WebhookNotification::new(forge_notification)
|
||||||
forge_notification,
|
)?;
|
||||||
))
|
|
||||||
.await?;
|
|
||||||
System::current().stop();
|
|
||||||
|
|
||||||
//then
|
//then
|
||||||
log.no_message_contains("send")?;
|
log.no_message_contains("send").await?;
|
||||||
log.require_message_containing("message parse error - not a push")?;
|
log.require_message_containing("message parse error - not a push")
|
||||||
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix::test]
|
#[tokio::test]
|
||||||
async fn when_message_is_push_on_unknown_branch_drop_notification() -> TestResult {
|
async fn when_message_is_push_on_unknown_branch_drop_notification() -> TestResult {
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
|
@ -214,32 +218,31 @@ async fn when_message_is_push_on_unknown_branch_drop_notification() -> TestResul
|
||||||
.return_once(|_, _| true); // is valid
|
.return_once(|_, _| true); // is valid
|
||||||
forge.expect_should_ignore_message().returning(|_| false);
|
forge.expect_should_ignore_message().returning(|_| false);
|
||||||
forge.expect_parse_webhook_body().return_once(|_| Ok(push));
|
forge.expect_parse_webhook_body().return_once(|_| Ok(push));
|
||||||
|
let net: Net = given::a_network().into();
|
||||||
let (actor, log) = given::a_repo_actor(
|
let (actor, log) = given::a_repo_actor(
|
||||||
repo_details,
|
repo_details,
|
||||||
Box::new(repository_factory),
|
Box::new(repository_factory),
|
||||||
forge,
|
forge,
|
||||||
given::a_network().into(),
|
given::a_server_actor(fs.as_real(), net.clone()),
|
||||||
|
net,
|
||||||
);
|
);
|
||||||
let actor = actor
|
let actor = actor
|
||||||
.with_webhook_auth(Some(given::a_webhook_auth()))
|
.with_webhook_auth(Some(given::a_webhook_auth()))
|
||||||
.with_last_main_commit(commit);
|
.with_last_main_commit(commit);
|
||||||
|
|
||||||
//when
|
//when
|
||||||
actor
|
tell!(
|
||||||
.start()
|
kameo::spawn(actor),
|
||||||
.send(crate::repo::messages::WebhookNotification::new(
|
WebhookNotification::new(forge_notification)
|
||||||
forge_notification,
|
)?;
|
||||||
))
|
|
||||||
.await?;
|
|
||||||
System::current().stop();
|
|
||||||
|
|
||||||
//then
|
//then
|
||||||
log.no_message_contains("send")?;
|
log.no_message_contains("send").await?;
|
||||||
log.require_message_containing("unknown branch")?;
|
log.require_message_containing("unknown branch").await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix::test]
|
#[tokio::test]
|
||||||
async fn when_message_is_push_already_seen_commit_to_main() -> TestResult {
|
async fn when_message_is_push_already_seen_commit_to_main() -> TestResult {
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
|
@ -263,32 +266,32 @@ async fn when_message_is_push_already_seen_commit_to_main() -> TestResult {
|
||||||
.return_once(|_, _| true); // is valid
|
.return_once(|_, _| true); // is valid
|
||||||
forge.expect_should_ignore_message().returning(|_| false);
|
forge.expect_should_ignore_message().returning(|_| false);
|
||||||
forge.expect_parse_webhook_body().return_once(|_| Ok(push));
|
forge.expect_parse_webhook_body().return_once(|_| Ok(push));
|
||||||
|
let net: Net = given::a_network().into();
|
||||||
let (actor, log) = given::a_repo_actor(
|
let (actor, log) = given::a_repo_actor(
|
||||||
repo_details,
|
repo_details,
|
||||||
Box::new(repository_factory),
|
Box::new(repository_factory),
|
||||||
forge,
|
forge,
|
||||||
given::a_network().into(),
|
given::a_server_actor(fs.as_real(), net.clone()),
|
||||||
|
net,
|
||||||
);
|
);
|
||||||
let actor = actor
|
let actor = actor
|
||||||
.with_webhook_auth(Some(given::a_webhook_auth()))
|
.with_webhook_auth(Some(given::a_webhook_auth()))
|
||||||
.with_last_main_commit(commit);
|
.with_last_main_commit(commit);
|
||||||
|
|
||||||
//when
|
//when
|
||||||
actor
|
tell!(
|
||||||
.start()
|
kameo::spawn(actor),
|
||||||
.send(crate::repo::messages::WebhookNotification::new(
|
WebhookNotification::new(forge_notification)
|
||||||
forge_notification,
|
)?;
|
||||||
))
|
|
||||||
.await?;
|
|
||||||
System::current().stop();
|
|
||||||
|
|
||||||
//then
|
//then
|
||||||
log.no_message_contains("send")?;
|
log.no_message_contains("send").await?;
|
||||||
log.require_message_containing(format!("not a new commit on {main}"))?;
|
log.require_message_containing(format!("not a new commit on {main}"))
|
||||||
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix::test]
|
#[tokio::test]
|
||||||
async fn when_message_is_push_already_seen_commit_to_next() -> TestResult {
|
async fn when_message_is_push_already_seen_commit_to_next() -> TestResult {
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
|
@ -301,9 +304,9 @@ async fn when_message_is_push_already_seen_commit_to_next() -> TestResult {
|
||||||
let forge_notification = ForgeNotification::new(forge_alias, repo_alias, headers, body);
|
let forge_notification = ForgeNotification::new(forge_alias, repo_alias, headers, body);
|
||||||
let repository_factory = MockRepositoryFactory::new();
|
let repository_factory = MockRepositoryFactory::new();
|
||||||
let commit = given::a_commit();
|
let commit = given::a_commit();
|
||||||
let next = repo_config.branches().next();
|
let next_branch = repo_config.branches().next();
|
||||||
let push = given::a_push()
|
let push = given::a_push()
|
||||||
.with_branch(next.clone())
|
.with_branch(next_branch.clone())
|
||||||
.with_sha(commit.sha().to_string())
|
.with_sha(commit.sha().to_string())
|
||||||
.with_message(commit.message().to_string());
|
.with_message(commit.message().to_string());
|
||||||
let mut forge = given::a_forge();
|
let mut forge = given::a_forge();
|
||||||
|
@ -312,32 +315,32 @@ async fn when_message_is_push_already_seen_commit_to_next() -> TestResult {
|
||||||
.return_once(|_, _| true); // is valid
|
.return_once(|_, _| true); // is valid
|
||||||
forge.expect_should_ignore_message().returning(|_| false);
|
forge.expect_should_ignore_message().returning(|_| false);
|
||||||
forge.expect_parse_webhook_body().return_once(|_| Ok(push));
|
forge.expect_parse_webhook_body().return_once(|_| Ok(push));
|
||||||
|
let net: Net = given::a_network().into();
|
||||||
let (actor, log) = given::a_repo_actor(
|
let (actor, log) = given::a_repo_actor(
|
||||||
repo_details,
|
repo_details,
|
||||||
Box::new(repository_factory),
|
Box::new(repository_factory),
|
||||||
forge,
|
forge,
|
||||||
given::a_network().into(),
|
given::a_server_actor(fs.as_real(), net.clone()),
|
||||||
|
net,
|
||||||
);
|
);
|
||||||
let actor = actor
|
let actor = actor
|
||||||
.with_webhook_auth(Some(given::a_webhook_auth()))
|
.with_webhook_auth(Some(given::a_webhook_auth()))
|
||||||
.with_last_next_commit(commit);
|
.with_last_next_commit(commit);
|
||||||
|
|
||||||
//when
|
//when
|
||||||
actor
|
tell!(
|
||||||
.start()
|
kameo::spawn(actor),
|
||||||
.send(crate::repo::messages::WebhookNotification::new(
|
WebhookNotification::new(forge_notification)
|
||||||
forge_notification,
|
)?;
|
||||||
))
|
|
||||||
.await?;
|
|
||||||
System::current().stop();
|
|
||||||
|
|
||||||
//then
|
//then
|
||||||
log.no_message_contains("send")?;
|
log.no_message_contains("send").await?;
|
||||||
log.require_message_containing(format!("not a new commit on {next}"))?;
|
log.require_message_containing(format!("not a new commit on {next_branch}"))
|
||||||
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix::test]
|
#[tokio::test]
|
||||||
async fn when_message_is_push_already_seen_commit_to_dev() -> TestResult {
|
async fn when_message_is_push_already_seen_commit_to_dev() -> TestResult {
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
|
@ -361,32 +364,32 @@ async fn when_message_is_push_already_seen_commit_to_dev() -> TestResult {
|
||||||
.return_once(|_, _| true); // is valid
|
.return_once(|_, _| true); // is valid
|
||||||
forge.expect_should_ignore_message().returning(|_| false);
|
forge.expect_should_ignore_message().returning(|_| false);
|
||||||
forge.expect_parse_webhook_body().return_once(|_| Ok(push));
|
forge.expect_parse_webhook_body().return_once(|_| Ok(push));
|
||||||
|
let net: Net = given::a_network().into();
|
||||||
let (actor, log) = given::a_repo_actor(
|
let (actor, log) = given::a_repo_actor(
|
||||||
repo_details,
|
repo_details,
|
||||||
Box::new(repository_factory),
|
Box::new(repository_factory),
|
||||||
forge,
|
forge,
|
||||||
given::a_network().into(),
|
given::a_server_actor(fs.as_real(), net.clone()),
|
||||||
|
net,
|
||||||
);
|
);
|
||||||
let actor = actor
|
let actor = actor
|
||||||
.with_webhook_auth(Some(given::a_webhook_auth()))
|
.with_webhook_auth(Some(given::a_webhook_auth()))
|
||||||
.with_last_dev_commit(commit);
|
.with_last_dev_commit(commit);
|
||||||
|
|
||||||
//when
|
//when
|
||||||
actor
|
tell!(
|
||||||
.start()
|
kameo::spawn(actor),
|
||||||
.send(crate::repo::messages::WebhookNotification::new(
|
WebhookNotification::new(forge_notification)
|
||||||
forge_notification,
|
)?;
|
||||||
))
|
|
||||||
.await?;
|
|
||||||
System::current().stop();
|
|
||||||
|
|
||||||
//then
|
//then
|
||||||
log.no_message_contains("send")?;
|
log.no_message_contains("send").await?;
|
||||||
log.require_message_containing(format!("not a new commit on {dev}"))?;
|
log.require_message_containing(format!("not a new commit on {dev}"))
|
||||||
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix::test]
|
#[tokio::test]
|
||||||
async fn when_message_is_push_new_commit_to_main_should_stash_and_validate_repo() -> TestResult {
|
async fn when_message_is_push_new_commit_to_main_should_stash_and_validate_repo() -> TestResult {
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
|
@ -409,32 +412,33 @@ async fn when_message_is_push_new_commit_to_main_should_stash_and_validate_repo(
|
||||||
.return_once(|_, _| true); // is valid
|
.return_once(|_, _| true); // is valid
|
||||||
forge.expect_should_ignore_message().returning(|_| false);
|
forge.expect_should_ignore_message().returning(|_| false);
|
||||||
forge.expect_parse_webhook_body().return_once(|_| Ok(push));
|
forge.expect_parse_webhook_body().return_once(|_| Ok(push));
|
||||||
|
let net: Net = given::a_network().into();
|
||||||
let (actor, log) = given::a_repo_actor(
|
let (actor, log) = given::a_repo_actor(
|
||||||
repo_details,
|
repo_details,
|
||||||
Box::new(repository_factory),
|
Box::new(repository_factory),
|
||||||
forge,
|
forge,
|
||||||
given::a_network().into(),
|
given::a_server_actor(fs.as_real(), net.clone()),
|
||||||
|
net,
|
||||||
);
|
);
|
||||||
let actor = actor
|
let actor = actor
|
||||||
.with_webhook_auth(Some(given::a_webhook_auth()))
|
.with_webhook_auth(Some(given::a_webhook_auth()))
|
||||||
.with_last_main_commit(given::a_commit());
|
.with_last_main_commit(given::a_commit());
|
||||||
|
|
||||||
//when
|
//when
|
||||||
let addr = actor.start();
|
let addr = kameo::spawn(actor);
|
||||||
addr.send(crate::repo::messages::WebhookNotification::new(
|
tell!(addr, WebhookNotification::new(forge_notification))?;
|
||||||
forge_notification,
|
|
||||||
))
|
|
||||||
.await?;
|
|
||||||
System::current().stop();
|
|
||||||
|
|
||||||
//then
|
//then
|
||||||
let view = addr.send(ExamineActor).await?;
|
let view = addr
|
||||||
|
.ask(ExamineActor)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("examine actor: {e:?}"))?;
|
||||||
assert_eq!(view.last_main_commit, Some(push_commit));
|
assert_eq!(view.last_main_commit, Some(push_commit));
|
||||||
log.require_message_containing("send: ValidateRepo")?;
|
log.require_message_containing("send: ValidateRepo").await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix::test]
|
#[tokio::test]
|
||||||
async fn when_message_is_push_new_commit_to_next_should_stash_and_validate_repo() -> TestResult {
|
async fn when_message_is_push_new_commit_to_next_should_stash_and_validate_repo() -> TestResult {
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
|
@ -457,32 +461,33 @@ async fn when_message_is_push_new_commit_to_next_should_stash_and_validate_repo(
|
||||||
.return_once(|_, _| true); // is valid
|
.return_once(|_, _| true); // is valid
|
||||||
forge.expect_should_ignore_message().returning(|_| false);
|
forge.expect_should_ignore_message().returning(|_| false);
|
||||||
forge.expect_parse_webhook_body().return_once(|_| Ok(push));
|
forge.expect_parse_webhook_body().return_once(|_| Ok(push));
|
||||||
|
let net: Net = given::a_network().into();
|
||||||
let (actor, log) = given::a_repo_actor(
|
let (actor, log) = given::a_repo_actor(
|
||||||
repo_details,
|
repo_details,
|
||||||
Box::new(repository_factory),
|
Box::new(repository_factory),
|
||||||
forge,
|
forge,
|
||||||
given::a_network().into(),
|
given::a_server_actor(fs.as_real(), net.clone()),
|
||||||
|
net,
|
||||||
);
|
);
|
||||||
let actor = actor
|
let actor = actor
|
||||||
.with_webhook_auth(Some(given::a_webhook_auth()))
|
.with_webhook_auth(Some(given::a_webhook_auth()))
|
||||||
.with_last_next_commit(given::a_commit());
|
.with_last_next_commit(given::a_commit());
|
||||||
|
|
||||||
//when
|
//when
|
||||||
let addr = actor.start();
|
let addr = kameo::spawn(actor);
|
||||||
addr.send(crate::repo::messages::WebhookNotification::new(
|
tell!(addr, WebhookNotification::new(forge_notification))?;
|
||||||
forge_notification,
|
|
||||||
))
|
|
||||||
.await?;
|
|
||||||
System::current().stop();
|
|
||||||
|
|
||||||
//then
|
//then
|
||||||
let view = addr.send(ExamineActor).await?;
|
let view = addr
|
||||||
|
.ask(ExamineActor)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("examine actor: {e:?}"))?;
|
||||||
assert_eq!(view.last_next_commit, Some(push_commit));
|
assert_eq!(view.last_next_commit, Some(push_commit));
|
||||||
log.require_message_containing("send: ValidateRepo")?;
|
log.require_message_containing("send: ValidateRepo").await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix::test]
|
#[tokio::test]
|
||||||
async fn when_message_is_push_new_commit_to_dev_should_stash_and_validate_repo() -> TestResult {
|
async fn when_message_is_push_new_commit_to_dev_should_stash_and_validate_repo() -> TestResult {
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
|
@ -505,27 +510,28 @@ async fn when_message_is_push_new_commit_to_dev_should_stash_and_validate_repo()
|
||||||
.return_once(|_, _| true); // is valid
|
.return_once(|_, _| true); // is valid
|
||||||
forge.expect_should_ignore_message().returning(|_| false);
|
forge.expect_should_ignore_message().returning(|_| false);
|
||||||
forge.expect_parse_webhook_body().return_once(|_| Ok(push));
|
forge.expect_parse_webhook_body().return_once(|_| Ok(push));
|
||||||
|
let net: Net = given::a_network().into();
|
||||||
let (actor, log) = given::a_repo_actor(
|
let (actor, log) = given::a_repo_actor(
|
||||||
repo_details,
|
repo_details,
|
||||||
Box::new(repository_factory),
|
Box::new(repository_factory),
|
||||||
forge,
|
forge,
|
||||||
given::a_network().into(),
|
given::a_server_actor(fs.as_real(), net.clone()),
|
||||||
|
net,
|
||||||
);
|
);
|
||||||
let actor = actor
|
let actor = actor
|
||||||
.with_webhook_auth(Some(given::a_webhook_auth()))
|
.with_webhook_auth(Some(given::a_webhook_auth()))
|
||||||
.with_last_dev_commit(given::a_commit());
|
.with_last_dev_commit(given::a_commit());
|
||||||
|
|
||||||
//when
|
//when
|
||||||
let addr = actor.start();
|
let addr = kameo::spawn(actor);
|
||||||
addr.send(crate::repo::messages::WebhookNotification::new(
|
tell!(addr, WebhookNotification::new(forge_notification))?;
|
||||||
forge_notification,
|
|
||||||
))
|
|
||||||
.await?;
|
|
||||||
System::current().stop();
|
|
||||||
|
|
||||||
//then
|
//then
|
||||||
let view = addr.send(ExamineActor).await?;
|
let view = addr
|
||||||
|
.ask(ExamineActor)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("examine actor: {e:?}"))?;
|
||||||
assert_eq!(view.last_dev_commit, Some(push_commit));
|
assert_eq!(view.last_dev_commit, Some(push_commit));
|
||||||
log.require_message_containing("send: ValidateRepo")?;
|
log.require_message_containing("send: ValidateRepo").await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
|
use crate::{repo::messages::WebhookRegistered, tell};
|
||||||
|
|
||||||
//
|
//
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[actix::test]
|
#[tokio::test]
|
||||||
async fn should_store_webhook_details() -> TestResult {
|
async fn should_store_webhook_details() -> TestResult {
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
|
@ -15,21 +17,23 @@ async fn should_store_webhook_details() -> TestResult {
|
||||||
repo_details,
|
repo_details,
|
||||||
given::a_forge(),
|
given::a_forge(),
|
||||||
);
|
);
|
||||||
addr.send(crate::repo::messages::WebhookRegistered::new((
|
tell!(
|
||||||
webhook_id.clone(),
|
addr,
|
||||||
webhook_auth.clone(),
|
WebhookRegistered::new((webhook_id.clone(), webhook_auth.clone()))
|
||||||
)))
|
)?;
|
||||||
.await?;
|
|
||||||
System::current().stop();
|
|
||||||
|
|
||||||
//then
|
//then
|
||||||
let view = addr.send(ExamineActor).await?;
|
// let view = addr.ask(ExamineActor).await.expect("examine actor");
|
||||||
|
let view = addr
|
||||||
|
.ask(ExamineActor)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("examine actor: {e:?}"))?;
|
||||||
assert_eq!(view.webhook_id, Some(webhook_id));
|
assert_eq!(view.webhook_id, Some(webhook_id));
|
||||||
assert_eq!(view.webhook_auth, Some(webhook_auth));
|
assert_eq!(view.webhook_auth, Some(webhook_auth));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix::test]
|
#[tokio::test]
|
||||||
async fn should_send_validate_repo_message() -> TestResult {
|
async fn should_send_validate_repo_message() -> TestResult {
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
|
@ -43,14 +47,12 @@ async fn should_send_validate_repo_message() -> TestResult {
|
||||||
repo_details,
|
repo_details,
|
||||||
given::a_forge(),
|
given::a_forge(),
|
||||||
);
|
);
|
||||||
addr.send(crate::repo::messages::WebhookRegistered::new((
|
tell!(
|
||||||
webhook_id.clone(),
|
addr,
|
||||||
webhook_auth.clone(),
|
WebhookRegistered::new((webhook_id.clone(), webhook_auth.clone()))
|
||||||
)))
|
)?;
|
||||||
.await?;
|
|
||||||
System::current().stop();
|
|
||||||
|
|
||||||
//then
|
//then
|
||||||
log.require_message_containing("send: ValidateRepo")?;
|
log.require_message_containing("send: ValidateRepo").await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ use super::*;
|
||||||
use crate::git::file;
|
use crate::git::file;
|
||||||
use crate::repo::load;
|
use crate::repo::load;
|
||||||
|
|
||||||
#[actix::test]
|
#[tokio::test]
|
||||||
async fn when_file_not_found_should_error() -> TestResult {
|
async fn when_file_not_found_should_error() -> TestResult {
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
|
@ -13,7 +13,7 @@ async fn when_file_not_found_should_error() -> TestResult {
|
||||||
.expect_read_file()
|
.expect_read_file()
|
||||||
.returning(|_, _| Err(file::Error::FileNotFound));
|
.returning(|_, _| Err(file::Error::FileNotFound));
|
||||||
//when
|
//when
|
||||||
let_assert!(Err(err) = load::config_from_repository(repo_details, &open_repository).await);
|
let_assert!(Err(err) = load::config_from_repository(&repo_details, &open_repository).await);
|
||||||
//then
|
//then
|
||||||
debug!("Got: {err:?}");
|
debug!("Got: {err:?}");
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
|
@ -22,7 +22,7 @@ async fn when_file_not_found_should_error() -> TestResult {
|
||||||
));
|
));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
#[actix::test]
|
#[tokio::test]
|
||||||
async fn when_file_format_invalid_should_error() -> TestResult {
|
async fn when_file_format_invalid_should_error() -> TestResult {
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
|
@ -32,14 +32,14 @@ async fn when_file_format_invalid_should_error() -> TestResult {
|
||||||
.expect_read_file()
|
.expect_read_file()
|
||||||
.return_once(move |_, _| Ok(contents));
|
.return_once(move |_, _| Ok(contents));
|
||||||
//when
|
//when
|
||||||
let_assert!(Err(err) = load::config_from_repository(repo_details, &open_repository).await);
|
let_assert!(Err(err) = load::config_from_repository(&repo_details, &open_repository).await);
|
||||||
//then
|
//then
|
||||||
debug!("Got: {err:?}");
|
debug!("Got: {err:?}");
|
||||||
assert!(matches!(err, load::Error::Toml(_)));
|
assert!(matches!(err, load::Error::Toml(_)));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix::test]
|
#[tokio::test]
|
||||||
async fn when_main_branch_is_missing_should_error() -> TestResult {
|
async fn when_main_branch_is_missing_should_error() -> TestResult {
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
|
@ -64,14 +64,14 @@ async fn when_main_branch_is_missing_should_error() -> TestResult {
|
||||||
.expect_remote_branches()
|
.expect_remote_branches()
|
||||||
.return_once(move || Ok(branches));
|
.return_once(move || Ok(branches));
|
||||||
//when
|
//when
|
||||||
let_assert!(Err(err) = load::config_from_repository(repo_details, &open_repository).await);
|
let_assert!(Err(err) = load::config_from_repository(&repo_details, &open_repository).await);
|
||||||
//then
|
//then
|
||||||
debug!("Got: {err:?}");
|
debug!("Got: {err:?}");
|
||||||
assert!(matches!(err, load::Error::BranchNotFound(branch) if branch == main));
|
assert!(matches!(err, load::Error::BranchNotFound(branch) if branch == main));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix::test]
|
#[tokio::test]
|
||||||
async fn when_next_branch_is_missing_should_error() -> TestResult {
|
async fn when_next_branch_is_missing_should_error() -> TestResult {
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
|
@ -96,14 +96,14 @@ async fn when_next_branch_is_missing_should_error() -> TestResult {
|
||||||
.expect_remote_branches()
|
.expect_remote_branches()
|
||||||
.return_once(move || Ok(branches));
|
.return_once(move || Ok(branches));
|
||||||
//when
|
//when
|
||||||
let_assert!(Err(err) = load::config_from_repository(repo_details, &open_repository).await);
|
let_assert!(Err(err) = load::config_from_repository(&repo_details, &open_repository).await);
|
||||||
//then
|
//then
|
||||||
debug!("Got: {err:?}");
|
debug!("Got: {err:?}");
|
||||||
assert!(matches!(err, load::Error::BranchNotFound(branch) if branch == next));
|
assert!(matches!(err, load::Error::BranchNotFound(branch) if branch == next));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix::test]
|
#[tokio::test]
|
||||||
async fn when_dev_branch_is_missing_should_error() -> TestResult {
|
async fn when_dev_branch_is_missing_should_error() -> TestResult {
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
|
@ -128,14 +128,14 @@ async fn when_dev_branch_is_missing_should_error() -> TestResult {
|
||||||
.expect_remote_branches()
|
.expect_remote_branches()
|
||||||
.return_once(move || Ok(branches));
|
.return_once(move || Ok(branches));
|
||||||
//when
|
//when
|
||||||
let_assert!(Err(err) = load::config_from_repository(repo_details, &open_repository).await);
|
let_assert!(Err(err) = load::config_from_repository(&repo_details, &open_repository).await);
|
||||||
//then
|
//then
|
||||||
debug!("Got: {err:?}");
|
debug!("Got: {err:?}");
|
||||||
assert!(matches!(err, load::Error::BranchNotFound(branch) if branch == dev));
|
assert!(matches!(err, load::Error::BranchNotFound(branch) if branch == dev));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix::test]
|
#[tokio::test]
|
||||||
async fn when_valid_file_should_return_repo_config() -> TestResult {
|
async fn when_valid_file_should_return_repo_config() -> TestResult {
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
|
@ -161,7 +161,7 @@ async fn when_valid_file_should_return_repo_config() -> TestResult {
|
||||||
.expect_remote_branches()
|
.expect_remote_branches()
|
||||||
.return_once(move || Ok(branches));
|
.return_once(move || Ok(branches));
|
||||||
//when
|
//when
|
||||||
let_assert!(Ok(result) = load::config_from_repository(repo_details, &open_repository).await);
|
let_assert!(Ok(result) = load::config_from_repository(&repo_details, &open_repository).await);
|
||||||
//then
|
//then
|
||||||
debug!("Got: {result:?}");
|
debug!("Got: {result:?}");
|
||||||
assert_eq!(result, repo_config);
|
assert_eq!(result, repo_config);
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
//
|
//
|
||||||
use actix::prelude::*;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
git,
|
git,
|
||||||
repo::{
|
repo::{
|
||||||
|
@ -12,7 +10,6 @@ use crate::{
|
||||||
use git_next_core::{
|
use git_next_core::{
|
||||||
git::{
|
git::{
|
||||||
commit::Sha,
|
commit::Sha,
|
||||||
forge::commit::Status,
|
|
||||||
repository::{
|
repository::{
|
||||||
factory::{mock, MockRepositoryFactory, RepositoryFactory},
|
factory::{mock, MockRepositoryFactory, RepositoryFactory},
|
||||||
open::{MockOpenRepositoryLike, OpenRepositoryLike},
|
open::{MockOpenRepositoryLike, OpenRepositoryLike},
|
||||||
|
@ -28,12 +25,17 @@ use git_next_core::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use assert2::let_assert;
|
use assert2::let_assert;
|
||||||
|
use kameo::{
|
||||||
|
message::{Context, Message},
|
||||||
|
Reply,
|
||||||
|
};
|
||||||
use mockall::predicate::eq;
|
use mockall::predicate::eq;
|
||||||
use tracing::{debug, error};
|
use tracing::{debug, error};
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::{BTreeMap, HashMap},
|
collections::{BTreeMap, HashMap},
|
||||||
sync::{Arc, RwLock},
|
sync::{Arc, RwLock},
|
||||||
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
type TestResult = Result<(), Box<dyn std::error::Error>>;
|
type TestResult = Result<(), Box<dyn std::error::Error>>;
|
||||||
|
@ -45,33 +47,43 @@ mod handlers;
|
||||||
mod load;
|
mod load;
|
||||||
mod when;
|
mod when;
|
||||||
|
|
||||||
|
pub async fn tick(millis: u64) {
|
||||||
|
tokio::time::sleep(Duration::from_millis(millis)).await;
|
||||||
|
}
|
||||||
|
|
||||||
impl ActorLog {
|
impl ActorLog {
|
||||||
pub fn no_message_contains(&self, needle: impl AsRef<str> + std::fmt::Display) -> TestResult {
|
pub async fn no_message_contains(
|
||||||
if self.find_in_messages(needle.as_ref())? {
|
&self,
|
||||||
|
needle: impl AsRef<str> + Send + std::fmt::Display,
|
||||||
|
) -> TestResult {
|
||||||
|
if self.find_in_messages(needle.as_ref()).await? {
|
||||||
error!(?self, "");
|
error!(?self, "");
|
||||||
panic!("found unexpected message: {needle}");
|
panic!("found unexpected message: {needle}");
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn require_message_containing(
|
pub async fn require_message_containing(
|
||||||
&self,
|
&self,
|
||||||
needle: impl AsRef<str> + std::fmt::Display,
|
needle: impl AsRef<str> + Send + std::fmt::Display,
|
||||||
) -> TestResult {
|
) -> TestResult {
|
||||||
if !self.find_in_messages(needle.as_ref())? {
|
if !self.find_in_messages(needle.as_ref()).await? {
|
||||||
error!(?self, "");
|
error!(?self, "");
|
||||||
panic!("expected message not found: {needle}");
|
panic!("expected message not found: {needle}");
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_in_messages(
|
async fn find_in_messages(
|
||||||
&self,
|
&self,
|
||||||
needle: impl AsRef<str>,
|
needle: impl AsRef<str> + Send,
|
||||||
) -> Result<bool, Box<dyn std::error::Error>> {
|
) -> Result<bool, Box<dyn std::error::Error>> {
|
||||||
|
// Very short sleep to allow tests to get a chance to tick
|
||||||
|
// This should be enough for most tests.
|
||||||
|
tokio::time::sleep(Duration::from_millis(1)).await;
|
||||||
let found = self
|
let found = self
|
||||||
.read()
|
.read()
|
||||||
.map_err(|e| e.to_string())?
|
.await
|
||||||
.iter()
|
.iter()
|
||||||
.any(|message| message.contains(needle.as_ref()));
|
.any(|message| message.contains(needle.as_ref()));
|
||||||
Ok(found)
|
Ok(found)
|
||||||
|
@ -79,15 +91,19 @@ impl ActorLog {
|
||||||
}
|
}
|
||||||
|
|
||||||
message!(ExamineActor => RepoActorView, "Request a view of the current state of the [RepoActor].");
|
message!(ExamineActor => RepoActorView, "Request a view of the current state of the [RepoActor].");
|
||||||
impl Handler<ExamineActor> for RepoActor {
|
impl Message<ExamineActor> for RepoActor {
|
||||||
type Result = RepoActorView;
|
type Reply = RepoActorView;
|
||||||
|
|
||||||
fn handle(&mut self, _msg: ExamineActor, _ctx: &mut Self::Context) -> Self::Result {
|
async fn handle(
|
||||||
|
&mut self,
|
||||||
|
_msg: ExamineActor,
|
||||||
|
_ctx: Context<'_, Self, Self::Reply>,
|
||||||
|
) -> Self::Reply {
|
||||||
let repo_actor: &Self = self;
|
let repo_actor: &Self = self;
|
||||||
Self::Result::from(repo_actor)
|
Self::Reply::from(repo_actor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[derive(Debug, MessageResponse)]
|
#[derive(Debug, Reply)]
|
||||||
pub struct RepoActorView {
|
pub struct RepoActorView {
|
||||||
pub repo_details: RepoDetails,
|
pub repo_details: RepoDetails,
|
||||||
pub webhook_id: Option<WebhookId>, // INFO: if [None] then no webhook is configured
|
pub webhook_id: Option<WebhookId>, // INFO: if [None] then no webhook is configured
|
||||||
|
|
|
@ -1,37 +1,37 @@
|
||||||
//
|
//
|
||||||
|
use kameo::actor::ActorRef;
|
||||||
|
use kxio::{fs::FileSystem, net::Net};
|
||||||
|
|
||||||
|
use crate::server::ServerActor;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
pub fn start_actor(
|
pub fn start_actor(
|
||||||
repository_factory: MockRepositoryFactory,
|
repository_factory: MockRepositoryFactory,
|
||||||
repo_details: RepoDetails,
|
repo_details: RepoDetails,
|
||||||
forge: Box<dyn ForgeLike>,
|
forge: Box<dyn ForgeLike>,
|
||||||
) -> (actix::Addr<RepoActor>, ActorLog) {
|
fs: FileSystem,
|
||||||
|
net: Net,
|
||||||
|
) -> (ActorRef<RepoActor>, ActorLog) {
|
||||||
let (actor, log) = given::a_repo_actor(
|
let (actor, log) = given::a_repo_actor(
|
||||||
repo_details,
|
repo_details,
|
||||||
Box::new(repository_factory),
|
Box::new(repository_factory),
|
||||||
forge,
|
forge,
|
||||||
given::a_network().into(),
|
given::a_server_actor(fs, net.clone()),
|
||||||
|
net,
|
||||||
);
|
);
|
||||||
(actor.start(), log)
|
(kameo::spawn(actor), log)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn start_actor_with_open_repository(
|
pub fn start_actor_with_open_repository(
|
||||||
open_repository: Box<dyn OpenRepositoryLike>,
|
open_repository: Box<dyn OpenRepositoryLike>,
|
||||||
repo_details: RepoDetails,
|
repo_details: RepoDetails,
|
||||||
forge: Box<dyn ForgeLike>,
|
forge: Box<dyn ForgeLike>,
|
||||||
) -> (actix::Addr<RepoActor>, ActorLog) {
|
) -> (ActorRef<RepoActor>, ActorLog) {
|
||||||
let (actor, log) = given::a_repo_actor(repo_details, mock(), forge, given::a_network().into());
|
let fs = given::a_filesystem();
|
||||||
|
let net: Net = given::a_network().into();
|
||||||
|
let server_actor_ref: ActorRef<ServerActor> = given::a_server_actor(fs.as_real(), net.clone());
|
||||||
|
let (actor, log) = given::a_repo_actor(repo_details, mock(), forge, server_actor_ref, net);
|
||||||
let actor = actor.with_open_repository(Some(open_repository));
|
let actor = actor.with_open_repository(Some(open_repository));
|
||||||
(actor.start(), log)
|
(kameo::spawn(actor), log)
|
||||||
}
|
|
||||||
|
|
||||||
pub fn commit_status(forge: &mut MockForgeLike, commit: Commit, status: Status) {
|
|
||||||
let mut commit_status_forge = MockForgeLike::new();
|
|
||||||
commit_status_forge
|
|
||||||
.expect_commit_status()
|
|
||||||
.with(mockall::predicate::eq(commit))
|
|
||||||
.return_once(|_| Ok(status));
|
|
||||||
forge
|
|
||||||
.expect_duplicate()
|
|
||||||
.return_once(move || Box::new(commit_status_forge));
|
|
||||||
}
|
}
|
||||||
|
|
221
crates/cli/src/root.rs
Normal file
221
crates/cli/src/root.rs
Normal file
|
@ -0,0 +1,221 @@
|
||||||
|
//
|
||||||
|
|
||||||
|
// Root actor for all other actors - supervises them all
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use color_eyre::Result;
|
||||||
|
use derive_more::derive::Constructor;
|
||||||
|
use git_next_core::{git::RepositoryFactory, s};
|
||||||
|
use kameo::{
|
||||||
|
actor::{pubsub::PubSub, ActorRef},
|
||||||
|
mailbox::unbounded::UnboundedMailbox,
|
||||||
|
message::{Context, Message},
|
||||||
|
Actor,
|
||||||
|
};
|
||||||
|
use kxio::{fs::FileSystem, net::Net};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
alerts::{AlertsActor, History},
|
||||||
|
base_actor::BaseActor,
|
||||||
|
default_on_actor_panic, default_on_actor_start,
|
||||||
|
file_watcher::{FileUpdated, FileWatcherActor},
|
||||||
|
on_actor_link_died, on_actor_stop, publish,
|
||||||
|
server::{actor::messages::ServerUpdate, ServerActor},
|
||||||
|
subscribe,
|
||||||
|
tui::Tui,
|
||||||
|
MessageBus,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct RootActor {
|
||||||
|
ui: bool,
|
||||||
|
fs: FileSystem,
|
||||||
|
net: Net,
|
||||||
|
sleep_duration: std::time::Duration,
|
||||||
|
tx_shutdown: tokio::sync::mpsc::Sender<String>,
|
||||||
|
|
||||||
|
alerts_actor_ref: Option<ActorRef<AlertsActor>>,
|
||||||
|
server_updates_bus: Option<MessageBus<ServerUpdate>>,
|
||||||
|
server_actor_ref: Option<ActorRef<ServerActor>>,
|
||||||
|
file_updates_bus: Option<MessageBus<FileUpdated>>,
|
||||||
|
file_watcher_actor_ref: Option<ActorRef<FileWatcherActor>>,
|
||||||
|
|
||||||
|
#[cfg(feature = "tui")]
|
||||||
|
tui_actor_ref: Option<ActorRef<Tui>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RootActor {
|
||||||
|
pub const fn new(
|
||||||
|
ui: bool,
|
||||||
|
fs: FileSystem,
|
||||||
|
net: Net,
|
||||||
|
sleep_duration: std::time::Duration,
|
||||||
|
tx_shutdown: tokio::sync::mpsc::Sender<String>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
ui,
|
||||||
|
fs,
|
||||||
|
net,
|
||||||
|
sleep_duration,
|
||||||
|
tx_shutdown,
|
||||||
|
|
||||||
|
alerts_actor_ref: None,
|
||||||
|
server_updates_bus: None,
|
||||||
|
server_actor_ref: None,
|
||||||
|
file_updates_bus: None,
|
||||||
|
file_watcher_actor_ref: None,
|
||||||
|
|
||||||
|
#[cfg(feature = "tui")]
|
||||||
|
tui_actor_ref: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const A_DAY: Duration = Duration::from_secs(24 * 60 * 60);
|
||||||
|
|
||||||
|
type RootContext<'a> = Context<'a, RootActor, Result<()>>;
|
||||||
|
|
||||||
|
#[derive(Constructor, Debug)]
|
||||||
|
pub struct Start {
|
||||||
|
repo: Box<dyn RepositoryFactory>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Message<Start> for RootActor {
|
||||||
|
type Reply = Result<()>;
|
||||||
|
|
||||||
|
async fn handle(&mut self, msg: Start, ctx: RootContext<'_>) -> Self::Reply {
|
||||||
|
let alerts_actor_ref = self.start_alerts_actor(&ctx).await?;
|
||||||
|
|
||||||
|
let server_updates_bus = self.start_server_updates_bus(&ctx).await;
|
||||||
|
let file_updates_bus = self.start_file_updates_bus(&ctx).await;
|
||||||
|
|
||||||
|
self.start_server_actor(
|
||||||
|
&ctx,
|
||||||
|
alerts_actor_ref,
|
||||||
|
server_updates_bus.clone(),
|
||||||
|
file_updates_bus.clone(),
|
||||||
|
msg.repo,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
self.start_file_watcher_actor(&ctx, file_updates_bus.clone())
|
||||||
|
.await;
|
||||||
|
|
||||||
|
#[cfg(feature = "tui")]
|
||||||
|
if self.ui {
|
||||||
|
self.start_tui_actor(&ctx, server_updates_bus).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// trigger initial config file to load
|
||||||
|
publish!(file_updates_bus, FileUpdated)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl RootActor {
|
||||||
|
async fn start_alerts_actor(&mut self, ctx: &RootContext<'_>) -> Result<ActorRef<AlertsActor>> {
|
||||||
|
let actor_ref = AlertsActor::new(None, History::new(A_DAY), self.net.clone())
|
||||||
|
.spawn(ctx.actor_ref())
|
||||||
|
.await;
|
||||||
|
self.alerts_actor_ref.replace(actor_ref.clone());
|
||||||
|
Ok(actor_ref)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn start_file_watcher_actor(
|
||||||
|
&mut self,
|
||||||
|
ctx: &RootContext<'_>,
|
||||||
|
file_updates_bus: MessageBus<FileUpdated>,
|
||||||
|
) -> ActorRef<FileWatcherActor> {
|
||||||
|
let actor_ref = FileWatcherActor::new(
|
||||||
|
file_updates_bus,
|
||||||
|
self.fs.base().join("git-next-server.toml"),
|
||||||
|
)
|
||||||
|
.spawn_in_thread(ctx.actor_ref())
|
||||||
|
.await;
|
||||||
|
self.file_watcher_actor_ref.replace(actor_ref.clone());
|
||||||
|
actor_ref
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn start_server_updates_bus(
|
||||||
|
&mut self,
|
||||||
|
ctx: &RootContext<'_>,
|
||||||
|
) -> MessageBus<ServerUpdate> {
|
||||||
|
let actor_ref = PubSub::<ServerUpdate>::new().spawn(ctx.actor_ref()).await;
|
||||||
|
self.server_updates_bus.replace(actor_ref.clone());
|
||||||
|
actor_ref
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn start_file_updates_bus(&mut self, ctx: &RootContext<'_>) -> MessageBus<FileUpdated> {
|
||||||
|
let actor_ref = PubSub::<FileUpdated>::new().spawn(ctx.actor_ref()).await;
|
||||||
|
self.file_updates_bus.replace(actor_ref.clone());
|
||||||
|
actor_ref
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn start_server_actor(
|
||||||
|
&mut self,
|
||||||
|
ctx: &RootContext<'_>,
|
||||||
|
alerts_actor_ref: ActorRef<AlertsActor>,
|
||||||
|
server_updates_bus: MessageBus<ServerUpdate>,
|
||||||
|
file_updates_bus: MessageBus<FileUpdated>,
|
||||||
|
repo: Box<dyn RepositoryFactory>,
|
||||||
|
) -> Result<ActorRef<ServerActor>> {
|
||||||
|
let actor_ref = ServerActor::new(
|
||||||
|
self.ui,
|
||||||
|
self.fs.clone(),
|
||||||
|
self.net.clone(),
|
||||||
|
alerts_actor_ref,
|
||||||
|
server_updates_bus,
|
||||||
|
repo,
|
||||||
|
self.sleep_duration,
|
||||||
|
)
|
||||||
|
.spawn(ctx.actor_ref())
|
||||||
|
.await;
|
||||||
|
subscribe!(file_updates_bus, "server", actor_ref.clone())?;
|
||||||
|
self.server_actor_ref.replace(actor_ref.clone());
|
||||||
|
Ok(actor_ref)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "tui")]
|
||||||
|
async fn start_tui_actor(
|
||||||
|
&mut self,
|
||||||
|
ctx: &RootContext<'_>,
|
||||||
|
server_updates_bus: MessageBus<ServerUpdate>,
|
||||||
|
) -> Result<ActorRef<Tui>> {
|
||||||
|
let actor_ref = Tui::new().spawn_in_thread(ctx.actor_ref()).await;
|
||||||
|
self.tui_actor_ref.replace(actor_ref.clone());
|
||||||
|
subscribe!(server_updates_bus, actor_ref.clone())?;
|
||||||
|
Ok(actor_ref)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Actor for RootActor {
|
||||||
|
type Mailbox = UnboundedMailbox<Self>;
|
||||||
|
|
||||||
|
default_on_actor_start!(this, actor_ref);
|
||||||
|
default_on_actor_panic!(this, actor_ref, err);
|
||||||
|
|
||||||
|
on_actor_link_died!(this, actor_ref, id, reason, {
|
||||||
|
this.tx_shutdown.send(s!("link died")).await?;
|
||||||
|
match &reason {
|
||||||
|
kameo::error::ActorStopReason::Normal => Ok(None),
|
||||||
|
kameo::error::ActorStopReason::Killed
|
||||||
|
| kameo::error::ActorStopReason::Panicked(_)
|
||||||
|
| kameo::error::ActorStopReason::LinkDied { .. } => {
|
||||||
|
Ok(Some(kameo::error::ActorStopReason::LinkDied {
|
||||||
|
id,
|
||||||
|
reason: Box::new(reason),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
on_actor_stop!(this, actor_ref, reason, {
|
||||||
|
#[allow(clippy::expect_used)]
|
||||||
|
this.tx_shutdown
|
||||||
|
.send(s!("stopping"))
|
||||||
|
.await
|
||||||
|
.expect("send shutdown");
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,20 +1,36 @@
|
||||||
//
|
//
|
||||||
use actix::prelude::*;
|
use color_eyre::Result;
|
||||||
|
use kameo::message::{Context, Message};
|
||||||
|
|
||||||
use git_next_core::server::AppConfig;
|
use git_next_core::server::AppConfig;
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
file_watcher::FileUpdated,
|
file_watcher::FileUpdated,
|
||||||
server::actor::{messages::ReceiveAppConfig, ServerActor},
|
server::actor::{messages::ReceiveAppConfig, ServerActor},
|
||||||
|
tell,
|
||||||
};
|
};
|
||||||
|
|
||||||
impl Handler<FileUpdated> for ServerActor {
|
impl Message<FileUpdated> for ServerActor {
|
||||||
type Result = ();
|
type Reply = Result<()>;
|
||||||
|
|
||||||
fn handle(&mut self, _msg: FileUpdated, ctx: &mut Self::Context) -> Self::Result {
|
async fn handle(
|
||||||
|
&mut self,
|
||||||
|
_msg: FileUpdated,
|
||||||
|
ctx: Context<'_, Self, Self::Reply>,
|
||||||
|
) -> Self::Reply {
|
||||||
|
debug!("FileUpdated");
|
||||||
match AppConfig::load(&self.fs) {
|
match AppConfig::load(&self.fs) {
|
||||||
Ok(app_config) => self.do_send(ReceiveAppConfig::new(app_config), ctx),
|
Ok(app_config) => Ok(tell!(
|
||||||
Err(err) => self.abort(ctx, format!("Failed to load config file. Error: {err}")),
|
"server",
|
||||||
};
|
ctx.actor_ref(),
|
||||||
|
ReceiveAppConfig::new(app_config)
|
||||||
|
)?),
|
||||||
|
Err(err) => {
|
||||||
|
tracing::error!("Failed to load config file. Error: {err}");
|
||||||
|
ctx.actor_ref().kill();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,4 +4,3 @@ mod receive_valid_app_config;
|
||||||
mod server_update;
|
mod server_update;
|
||||||
mod shutdown;
|
mod shutdown;
|
||||||
mod shutdown_trigger;
|
mod shutdown_trigger;
|
||||||
mod subscribe_updates;
|
|
||||||
|
|
|
@ -1,35 +1,46 @@
|
||||||
use actix::prelude::*;
|
//
|
||||||
|
use color_eyre::Result;
|
||||||
|
use kameo::message::{Context, Message};
|
||||||
|
|
||||||
use crate::server::actor::{
|
use crate::{
|
||||||
|
server::actor::{
|
||||||
messages::{ReceiveAppConfig, ReceiveValidAppConfig, ValidAppConfig},
|
messages::{ReceiveAppConfig, ReceiveValidAppConfig, ValidAppConfig},
|
||||||
ServerActor,
|
ServerActor,
|
||||||
|
},
|
||||||
|
tell,
|
||||||
};
|
};
|
||||||
|
|
||||||
impl Handler<ReceiveAppConfig> for ServerActor {
|
impl Message<ReceiveAppConfig> for ServerActor {
|
||||||
type Result = ();
|
type Reply = Result<()>;
|
||||||
|
|
||||||
#[allow(clippy::cognitive_complexity)]
|
#[allow(clippy::cognitive_complexity)]
|
||||||
fn handle(&mut self, msg: ReceiveAppConfig, ctx: &mut Self::Context) -> Self::Result {
|
async fn handle(
|
||||||
tracing::info!("recieved server config");
|
&mut self,
|
||||||
|
msg: ReceiveAppConfig,
|
||||||
|
ctx: Context<'_, Self, Self::Reply>,
|
||||||
|
) -> Self::Reply {
|
||||||
let Ok(socket_addr) = msg.listen_socket_addr() else {
|
let Ok(socket_addr) = msg.listen_socket_addr() else {
|
||||||
return self.abort(ctx, "Unable to parse http.addr");
|
self.abort(&ctx, "Unable to parse http.addr").await?;
|
||||||
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(server_storage) = self.server_storage(&msg) else {
|
let Some(server_storage) = self.server_storage(&msg) else {
|
||||||
return self.abort(ctx, "Server storage not available");
|
self.abort(&ctx, "Server storage not available").await?;
|
||||||
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
|
||||||
if msg.listen().url().ends_with('/') {
|
if msg.listen().url().ends_with('/') {
|
||||||
return self.abort(ctx, "webhook.url must not end with a '/'");
|
self.abort(&ctx, "webhook.url must not end with a '/'")
|
||||||
|
.await?;
|
||||||
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
tell!(
|
||||||
self.do_send(
|
"server",
|
||||||
|
ctx.actor_ref(),
|
||||||
ReceiveValidAppConfig::new(ValidAppConfig::new(
|
ReceiveValidAppConfig::new(ValidAppConfig::new(
|
||||||
msg.peel(),
|
msg.peel(),
|
||||||
socket_addr,
|
socket_addr,
|
||||||
server_storage,
|
server_storage,
|
||||||
)),
|
))
|
||||||
ctx,
|
)?;
|
||||||
);
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,43 +1,47 @@
|
||||||
//
|
//
|
||||||
use actix::prelude::*;
|
use color_eyre::Result;
|
||||||
|
use kameo::{
|
||||||
|
actor::ActorRef,
|
||||||
|
message::{Context, Message},
|
||||||
|
};
|
||||||
|
|
||||||
use git_next_core::{ForgeAlias, RepoAlias};
|
use git_next_core::{ForgeAlias, RepoAlias};
|
||||||
use tracing::info;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
alerts::messages::UpdateShout,
|
alerts::messages::UpdateShout,
|
||||||
|
base_actor::BaseActor as _,
|
||||||
repo::{messages::CloneRepo, RepoActor},
|
repo::{messages::CloneRepo, RepoActor},
|
||||||
server::actor::{
|
server::actor::{
|
||||||
messages::{ReceiveValidAppConfig, ServerUpdate, ValidAppConfig},
|
messages::{ReceiveValidAppConfig, ServerUpdate, ValidAppConfig},
|
||||||
ServerActor,
|
ServerActor,
|
||||||
},
|
},
|
||||||
webhook::{
|
spawn, tell,
|
||||||
messages::ShutdownWebhook,
|
webhook::{self, router::AddWebhookRecipient, WebhookActor},
|
||||||
router::{AddWebhookRecipient, WebhookRouterActor},
|
|
||||||
WebhookActor,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
impl Handler<ReceiveValidAppConfig> for ServerActor {
|
impl Message<ReceiveValidAppConfig> for ServerActor {
|
||||||
type Result = ();
|
type Reply = Result<()>;
|
||||||
|
|
||||||
fn handle(&mut self, msg: ReceiveValidAppConfig, ctx: &mut Self::Context) -> Self::Result {
|
async fn handle(
|
||||||
|
&mut self,
|
||||||
|
msg: ReceiveValidAppConfig,
|
||||||
|
ctx: Context<'_, Self, Self::Reply>,
|
||||||
|
) -> Self::Reply {
|
||||||
let ValidAppConfig {
|
let ValidAppConfig {
|
||||||
app_config,
|
app_config,
|
||||||
socket_address,
|
socket_address,
|
||||||
storage: server_storage,
|
storage: server_storage,
|
||||||
} = msg.peel();
|
} = msg.peel();
|
||||||
// shutdown any existing webhook actor
|
// shutdown any existing webhook actor
|
||||||
if let Some(webhook_actor_addr) = self.webhook_actor_addr.take() {
|
if let Some(webhook_actor_ref) = self.webhook_actor_ref.take() {
|
||||||
webhook_actor_addr.do_send(ShutdownWebhook);
|
webhook_actor_ref.kill();
|
||||||
}
|
}
|
||||||
self.generation.inc();
|
self.generation.inc();
|
||||||
|
|
||||||
// Webhook Server
|
// Webhook Server
|
||||||
info!("Starting Webhook Server...");
|
if let Some(webhook_router_actor_ref) = &self.webhook_router_actor_ref {
|
||||||
let webhook_router = WebhookRouterActor::default().start();
|
|
||||||
let listen_url = app_config.listen().url();
|
let listen_url = app_config.listen().url();
|
||||||
let notify_user_recipient = self.alerts.clone().recipient();
|
let server_actor_ref = ctx.actor_ref();
|
||||||
let server_addr = Some(ctx.address());
|
|
||||||
// Forge Actors
|
// Forge Actors
|
||||||
for (forge_alias, forge_config) in app_config.forges() {
|
for (forge_alias, forge_config) in app_config.forges() {
|
||||||
let repo_actors = self
|
let repo_actors = self
|
||||||
|
@ -46,52 +50,62 @@ impl Handler<ReceiveValidAppConfig> for ServerActor {
|
||||||
forge_alias.clone(),
|
forge_alias.clone(),
|
||||||
&server_storage,
|
&server_storage,
|
||||||
listen_url,
|
listen_url,
|
||||||
¬ify_user_recipient,
|
&self.alerts,
|
||||||
server_addr.clone(),
|
server_actor_ref.clone(),
|
||||||
)
|
)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(start_repo_actor)
|
.map(|repo_actor_tuple| {
|
||||||
|
start_repo_actor(repo_actor_tuple, server_actor_ref.clone())
|
||||||
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
repo_actors
|
for repo_actor in repo_actors {
|
||||||
.iter()
|
let (repo_alias, repo_actor_ref) = repo_actor.await?;
|
||||||
.map(|(repo_alias, addr)| {
|
tell!(
|
||||||
|
webhook_router_actor_ref,
|
||||||
AddWebhookRecipient::new(
|
AddWebhookRecipient::new(
|
||||||
forge_alias.clone(),
|
forge_alias.clone(),
|
||||||
repo_alias.clone(),
|
repo_alias.clone(),
|
||||||
addr.clone().recipient(),
|
repo_actor_ref.clone(),
|
||||||
)
|
)
|
||||||
})
|
)?;
|
||||||
.for_each(|msg| webhook_router.do_send(msg));
|
|
||||||
for (repo_alias, addr) in repo_actors {
|
|
||||||
self.repo_actors
|
self.repo_actors
|
||||||
.insert((forge_alias.clone(), repo_alias), addr);
|
.insert((forge_alias.clone(), repo_alias), repo_actor_ref);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let webhook_actor_addr =
|
let webhook_actor_ref = spawn!(
|
||||||
WebhookActor::new(socket_address, webhook_router.recipient()).start();
|
ctx.actor_ref(),
|
||||||
self.webhook_actor_addr.replace(webhook_actor_addr);
|
WebhookActor::new(socket_address, webhook_router_actor_ref.clone())
|
||||||
|
);
|
||||||
|
tell!(webhook_actor_ref, webhook::Start)?;
|
||||||
|
self.webhook_actor_ref.replace(webhook_actor_ref);
|
||||||
|
}
|
||||||
let shout = app_config.shout().clone();
|
let shout = app_config.shout().clone();
|
||||||
self.app_config.replace(app_config.clone());
|
self.app_config.replace(app_config.clone());
|
||||||
self.do_send(
|
tell!(
|
||||||
|
"server",
|
||||||
|
ctx.actor_ref(),
|
||||||
ServerUpdate::AppConfigLoaded {
|
ServerUpdate::AppConfigLoaded {
|
||||||
app_config: ValidAppConfig {
|
app_config: ValidAppConfig {
|
||||||
app_config,
|
app_config,
|
||||||
socket_address,
|
socket_address,
|
||||||
storage: server_storage,
|
storage: server_storage,
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
ctx,
|
)?;
|
||||||
);
|
tell!("alert", self.alerts, UpdateShout::new(shout))?;
|
||||||
self.alerts.do_send(UpdateShout::new(shout));
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn start_repo_actor(actor: (ForgeAlias, RepoAlias, RepoActor)) -> (RepoAlias, Addr<RepoActor>) {
|
async fn start_repo_actor(
|
||||||
let (forge_name, repo_alias, actor) = actor;
|
actor: (ForgeAlias, RepoAlias, RepoActor),
|
||||||
|
server_actor_ref: ActorRef<ServerActor>,
|
||||||
|
) -> Result<(RepoAlias, ActorRef<RepoActor>)> {
|
||||||
|
let (forge_name, repo_alias, repo_actor) = actor;
|
||||||
let span = tracing::info_span!("start_repo_actor", forge = %forge_name, repo = %repo_alias);
|
let span = tracing::info_span!("start_repo_actor", forge = %forge_name, repo = %repo_alias);
|
||||||
let _guard = span.enter();
|
let _guard = span.enter();
|
||||||
let addr = actor.start();
|
let repo_actor_ref = repo_actor.spawn(server_actor_ref).await;
|
||||||
addr.do_send(CloneRepo);
|
tell!(repo_actor_ref, CloneRepo)?;
|
||||||
tracing::info!("Started");
|
Ok((repo_alias, repo_actor_ref))
|
||||||
(repo_alias, addr)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,20 @@
|
||||||
use actix::Handler;
|
|
||||||
|
|
||||||
//
|
//
|
||||||
use crate::server::{actor::messages::ServerUpdate, ServerActor};
|
use kameo::message::{Context, Message};
|
||||||
|
|
||||||
impl Handler<ServerUpdate> for ServerActor {
|
use crate::{
|
||||||
type Result = ();
|
publish,
|
||||||
|
server::{actor::messages::ServerUpdate, ServerActor},
|
||||||
|
};
|
||||||
|
|
||||||
fn handle(&mut self, msg: ServerUpdate, _ctx: &mut Self::Context) -> Self::Result {
|
impl Message<ServerUpdate> for ServerActor {
|
||||||
self.subscribers.iter().for_each(move |subscriber| {
|
type Reply = color_eyre::Result<()>;
|
||||||
subscriber.do_send(msg.clone());
|
|
||||||
});
|
async fn handle(
|
||||||
|
&mut self,
|
||||||
|
msg: ServerUpdate,
|
||||||
|
_ctx: Context<'_, Self, Self::Reply>,
|
||||||
|
) -> Self::Reply {
|
||||||
|
publish!("server_updates_bus", self.server_updates_bus, msg)?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,30 +1,35 @@
|
||||||
//-
|
//
|
||||||
|
use color_eyre::Result;
|
||||||
use actix::prelude::*;
|
use kameo::message::{Context, Message};
|
||||||
use tracing::debug;
|
use tracing::{debug, info};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
repo::messages::UnRegisterWebhook,
|
repo::messages::UnRegisterWebhook,
|
||||||
server::actor::{messages::Shutdown, ServerActor},
|
server::actor::{messages::Shutdown, ServerActor},
|
||||||
webhook::messages::ShutdownWebhook,
|
tell,
|
||||||
};
|
};
|
||||||
|
|
||||||
impl Handler<Shutdown> for ServerActor {
|
impl Message<Shutdown> for ServerActor {
|
||||||
type Result = ();
|
type Reply = Result<()>;
|
||||||
|
|
||||||
fn handle(&mut self, _msg: Shutdown, _ctx: &mut Self::Context) -> Self::Result {
|
async fn handle(
|
||||||
self.repo_actors
|
&mut self,
|
||||||
.iter()
|
_msg: Shutdown,
|
||||||
.for_each(|((forge_alias, repo_alias), addr)| {
|
_ctx: Context<'_, Self, Self::Reply>,
|
||||||
|
) -> Self::Reply {
|
||||||
|
for ((forge_alias, repo_alias), repo_actor_ref) in &self.repo_actors {
|
||||||
debug!(%forge_alias, %repo_alias, "removing webhook");
|
debug!(%forge_alias, %repo_alias, "removing webhook");
|
||||||
addr.do_send(UnRegisterWebhook::new());
|
tell!(repo_actor_ref, UnRegisterWebhook::new())?;
|
||||||
debug!(%forge_alias, %repo_alias, "removed webhook");
|
debug!(%forge_alias, %repo_alias, "removed webhook");
|
||||||
});
|
repo_actor_ref.kill();
|
||||||
|
info!(%forge_alias, %repo_alias, "killed repo actor");
|
||||||
|
}
|
||||||
debug!("server shutdown");
|
debug!("server shutdown");
|
||||||
if let Some(webhook) = self.webhook_actor_addr.take() {
|
if let Some(webhook_actor_ref) = self.webhook_actor_ref.take() {
|
||||||
debug!("shutting down webhook");
|
debug!("shutting down webhook");
|
||||||
webhook.do_send(ShutdownWebhook);
|
webhook_actor_ref.kill();
|
||||||
debug!("webhook shutdown");
|
debug!("webhook shutdown");
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
//
|
//
|
||||||
use actix::Handler;
|
use kameo::message::{Context, Message};
|
||||||
|
|
||||||
use crate::server::{actor::messages::ShutdownTrigger, ServerActor};
|
use crate::server::{actor::messages::ShutdownTrigger, ServerActor};
|
||||||
|
|
||||||
impl Handler<ShutdownTrigger> for ServerActor {
|
impl Message<ShutdownTrigger> for ServerActor {
|
||||||
type Result = ();
|
type Reply = ();
|
||||||
|
|
||||||
fn handle(&mut self, msg: ShutdownTrigger, _ctx: &mut Self::Context) -> Self::Result {
|
async fn handle(
|
||||||
|
&mut self,
|
||||||
|
msg: ShutdownTrigger,
|
||||||
|
_ctx: Context<'_, Self, Self::Reply>,
|
||||||
|
) -> Self::Reply {
|
||||||
self.shutdown_trigger.replace(msg.peel());
|
self.shutdown_trigger.replace(msg.peel());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
use crate::server::actor::{messages::SubscribeToUpdates, ServerActor};
|
|
||||||
|
|
||||||
//
|
|
||||||
impl actix::Handler<SubscribeToUpdates> for ServerActor {
|
|
||||||
type Result = ();
|
|
||||||
|
|
||||||
fn handle(&mut self, msg: SubscribeToUpdates, _ctx: &mut Self::Context) -> Self::Result {
|
|
||||||
self.subscribers.push(msg.peel());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +1,8 @@
|
||||||
//
|
//
|
||||||
use actix::{Message, Recipient};
|
use std::net::SocketAddr;
|
||||||
|
|
||||||
use derive_more::Constructor;
|
use derive_more::Constructor;
|
||||||
|
use tokio::sync::mpsc::Sender;
|
||||||
|
|
||||||
use git_next_core::{
|
use git_next_core::{
|
||||||
git::{self, forge::commit::Status, graph::Log, Commit},
|
git::{self, forge::commit::Status, graph::Log, Commit},
|
||||||
|
@ -10,8 +12,6 @@ use git_next_core::{
|
||||||
ForgeAlias, RepoAlias, RepoBranches, RepoConfig,
|
ForgeAlias, RepoAlias, RepoBranches, RepoConfig,
|
||||||
};
|
};
|
||||||
|
|
||||||
use std::net::SocketAddr;
|
|
||||||
|
|
||||||
// receive server config
|
// receive server config
|
||||||
message!(
|
message!(
|
||||||
ReceiveAppConfig,
|
ReceiveAppConfig,
|
||||||
|
@ -38,8 +38,7 @@ message!(
|
||||||
|
|
||||||
message!(Shutdown, "Notification to shutdown the server actor");
|
message!(Shutdown, "Notification to shutdown the server actor");
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Message)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
#[rtype(result = "()")]
|
|
||||||
pub enum ServerUpdate {
|
pub enum ServerUpdate {
|
||||||
/// List of all configured forges and aliases
|
/// List of all configured forges and aliases
|
||||||
AppConfigLoaded { app_config: ValidAppConfig },
|
AppConfigLoaded { app_config: ValidAppConfig },
|
||||||
|
@ -96,18 +95,11 @@ pub enum RepoUpdate {
|
||||||
MainUpdated,
|
MainUpdated,
|
||||||
}
|
}
|
||||||
|
|
||||||
message!(
|
|
||||||
SubscribeToUpdates,
|
|
||||||
Recipient<ServerUpdate>,
|
|
||||||
"Subscribe to receive updates from the server"
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Sends a channel to be used to shutdown the server
|
/// Sends a channel to be used to shutdown the server
|
||||||
#[derive(Message, Constructor)]
|
#[derive(Constructor)]
|
||||||
#[rtype(result = "()")]
|
pub struct ShutdownTrigger(Sender<String>);
|
||||||
pub struct ShutdownTrigger(std::sync::mpsc::Sender<String>);
|
|
||||||
impl ShutdownTrigger {
|
impl ShutdownTrigger {
|
||||||
pub fn peel(self) -> std::sync::mpsc::Sender<String> {
|
pub fn peel(self) -> Sender<String> {
|
||||||
self.0
|
self.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,33 @@
|
||||||
//
|
//
|
||||||
use actix::prelude::*;
|
use std::{
|
||||||
|
collections::BTreeMap,
|
||||||
|
path::PathBuf,
|
||||||
|
sync::{Arc, RwLock},
|
||||||
|
};
|
||||||
|
|
||||||
|
use kameo::{actor::ActorRef, mailbox::unbounded::UnboundedMailbox, message::Context, Actor};
|
||||||
|
use kxio::{fs::FileSystem, net::Net};
|
||||||
|
use tokio::sync::mpsc::Sender;
|
||||||
|
use tracing::{error, instrument, warn};
|
||||||
|
|
||||||
|
use git_next_core::{
|
||||||
|
git::{repository::factory::RepositoryFactory, Generation, RepoDetails},
|
||||||
|
s,
|
||||||
|
server::{self, AppConfig, ListenUrl, Storage},
|
||||||
|
ForgeAlias, ForgeConfig, GitDir, RepoAlias, ServerRepoConfig, StoragePathType,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
alerts::AlertsActor,
|
||||||
|
forge::Forge,
|
||||||
|
on_actor_link_died, on_actor_panic, on_actor_start, on_actor_stop,
|
||||||
|
repo::RepoActor,
|
||||||
|
spawn, tell,
|
||||||
|
webhook::{router::WebhookRouterActor, WebhookActor},
|
||||||
|
MessageBus,
|
||||||
|
};
|
||||||
|
|
||||||
use messages::{ReceiveAppConfig, ServerUpdate, Shutdown};
|
use messages::{ReceiveAppConfig, ServerUpdate, Shutdown};
|
||||||
use tracing::error;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
@ -9,25 +35,6 @@ mod tests;
|
||||||
mod handlers;
|
mod handlers;
|
||||||
pub mod messages;
|
pub mod messages;
|
||||||
|
|
||||||
use crate::{
|
|
||||||
alerts::messages::NotifyUser, alerts::AlertsActor, forge::Forge, repo::RepoActor,
|
|
||||||
webhook::WebhookActor,
|
|
||||||
};
|
|
||||||
|
|
||||||
use git_next_core::{
|
|
||||||
git::{repository::factory::RepositoryFactory, Generation, RepoDetails},
|
|
||||||
server::{self, AppConfig, ListenUrl, Storage},
|
|
||||||
ForgeAlias, ForgeConfig, GitDir, RepoAlias, ServerRepoConfig, StoragePathType,
|
|
||||||
};
|
|
||||||
|
|
||||||
use kxio::{fs::FileSystem, net::Net};
|
|
||||||
|
|
||||||
use std::{
|
|
||||||
collections::BTreeMap,
|
|
||||||
path::PathBuf,
|
|
||||||
sync::{Arc, RwLock},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, derive_more::Display, derive_more::From)]
|
#[derive(Debug, derive_more::Display, derive_more::From)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[display("Failed to create data directories")]
|
#[display("Failed to create data directories")]
|
||||||
|
@ -41,65 +48,112 @@ pub enum Error {
|
||||||
Config(server::Error),
|
Config(server::Error),
|
||||||
|
|
||||||
Io(std::io::Error),
|
Io(std::io::Error),
|
||||||
|
|
||||||
|
General(String),
|
||||||
}
|
}
|
||||||
|
impl std::error::Error for Error {}
|
||||||
|
|
||||||
type Result<T> = core::result::Result<T, Error>;
|
type Result<T> = core::result::Result<T, Error>;
|
||||||
|
|
||||||
#[allow(clippy::module_name_repetitions)]
|
#[allow(clippy::module_name_repetitions)]
|
||||||
#[derive(derive_with::With)]
|
#[derive(derive_with::With)]
|
||||||
#[with(message_log)]
|
#[with(message_log)]
|
||||||
pub struct ServerActor {
|
pub struct ServerActor {
|
||||||
|
ui: bool,
|
||||||
app_config: Option<AppConfig>,
|
app_config: Option<AppConfig>,
|
||||||
generation: Generation,
|
generation: Generation,
|
||||||
webhook_actor_addr: Option<Addr<WebhookActor>>,
|
webhook_actor_ref: Option<ActorRef<WebhookActor>>,
|
||||||
fs: FileSystem,
|
fs: FileSystem,
|
||||||
net: Net,
|
net: Net,
|
||||||
alerts: Addr<AlertsActor>,
|
alerts: ActorRef<AlertsActor>,
|
||||||
repository_factory: Box<dyn RepositoryFactory>,
|
repository_factory: Box<dyn RepositoryFactory>,
|
||||||
sleep_duration: std::time::Duration,
|
sleep_duration: std::time::Duration,
|
||||||
repo_actors: BTreeMap<(ForgeAlias, RepoAlias), Addr<RepoActor>>,
|
repo_actors: BTreeMap<(ForgeAlias, RepoAlias), ActorRef<RepoActor>>,
|
||||||
|
|
||||||
shutdown_trigger: Option<std::sync::mpsc::Sender<String>>,
|
shutdown_trigger: Option<Sender<String>>,
|
||||||
subscribers: Vec<Recipient<ServerUpdate>>,
|
server_updates_bus: MessageBus<ServerUpdate>,
|
||||||
|
webhook_router_actor_ref: Option<ActorRef<WebhookRouterActor>>,
|
||||||
|
|
||||||
// testing
|
// testing
|
||||||
message_log: Option<Arc<RwLock<Vec<String>>>>,
|
message_log: Option<Arc<RwLock<Vec<String>>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Actor for ServerActor {
|
impl Actor for ServerActor {
|
||||||
type Context = Context<Self>;
|
type Mailbox = UnboundedMailbox<Self>;
|
||||||
|
|
||||||
|
on_actor_start!(this, actor_ref, {
|
||||||
|
let webhook_router = spawn!(actor_ref, WebhookRouterActor::default());
|
||||||
|
this.webhook_router_actor_ref.replace(webhook_router);
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
|
||||||
|
on_actor_panic!(this, actor_ref, err, {
|
||||||
|
warn!(%err, "paniced: {}", <Self as Actor>::name());
|
||||||
|
Ok(Some(kameo::error::ActorStopReason::Panicked(err)))
|
||||||
|
});
|
||||||
|
|
||||||
|
on_actor_link_died!(this, actor_ref, id, reason, {
|
||||||
|
match &reason {
|
||||||
|
kameo::error::ActorStopReason::Killed | kameo::error::ActorStopReason::Normal => {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
kameo::error::ActorStopReason::Panicked(_)
|
||||||
|
| kameo::error::ActorStopReason::LinkDied { .. } => {
|
||||||
|
if let Some(actor_ref) = actor_ref.upgrade() {
|
||||||
|
tell!(actor_ref, Shutdown)?;
|
||||||
|
}
|
||||||
|
if let Some(trigger) = this.shutdown_trigger.take() {
|
||||||
|
trigger
|
||||||
|
.send(s!("link died"))
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("failed sending shutdown trigger: {e:?}"))?;
|
||||||
|
}
|
||||||
|
Ok(Some(kameo::error::ActorStopReason::LinkDied {
|
||||||
|
id,
|
||||||
|
reason: Box::new(reason),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
on_actor_stop!(this, actor_ref, reason, Ok(()));
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ServerActor {
|
impl ServerActor {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
|
ui: bool,
|
||||||
fs: FileSystem,
|
fs: FileSystem,
|
||||||
net: Net,
|
net: Net,
|
||||||
alerts: Addr<AlertsActor>,
|
alerts: ActorRef<AlertsActor>,
|
||||||
|
server_updates_bus: MessageBus<ServerUpdate>,
|
||||||
repo: Box<dyn RepositoryFactory>,
|
repo: Box<dyn RepositoryFactory>,
|
||||||
sleep_duration: std::time::Duration,
|
sleep_duration: std::time::Duration,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let generation = Generation::default();
|
|
||||||
Self {
|
Self {
|
||||||
|
ui,
|
||||||
app_config: None,
|
app_config: None,
|
||||||
generation,
|
generation: Generation::default(),
|
||||||
webhook_actor_addr: None,
|
webhook_actor_ref: None,
|
||||||
fs,
|
fs,
|
||||||
net,
|
net,
|
||||||
alerts,
|
alerts,
|
||||||
repository_factory: repo,
|
repository_factory: repo,
|
||||||
shutdown_trigger: None,
|
shutdown_trigger: None,
|
||||||
subscribers: Vec::default(),
|
server_updates_bus,
|
||||||
|
webhook_router_actor_ref: None,
|
||||||
sleep_duration,
|
sleep_duration,
|
||||||
repo_actors: BTreeMap::new(),
|
repo_actors: BTreeMap::new(),
|
||||||
message_log: None,
|
message_log: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_forge_data_directories(
|
fn create_forge_data_directories(
|
||||||
&self,
|
&self,
|
||||||
app_config: &AppConfig,
|
app_config: &AppConfig,
|
||||||
server_dir: &std::path::Path,
|
server_dir: &std::path::Path,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
for (forge_name, _forge_config) in app_config.forges() {
|
for (forge_name, _forge_config) in app_config.forges() {
|
||||||
let forge_dir: PathBuf = (&forge_name).into();
|
let path = server_dir.join(PathBuf::from(&forge_name));
|
||||||
let path = server_dir.join(&forge_dir);
|
|
||||||
let path_handle = self.fs.path(&path);
|
let path_handle = self.fs.path(&path);
|
||||||
if path_handle.exists()? {
|
if path_handle.exists()? {
|
||||||
if !path_handle.is_dir()? {
|
if !path_handle.is_dir()? {
|
||||||
|
@ -110,45 +164,37 @@ impl ServerActor {
|
||||||
self.fs.dir(&path).create_all()?;
|
self.fs.dir(&path).create_all()?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(skip_all)]
|
||||||
fn create_forge_repos(
|
fn create_forge_repos(
|
||||||
&self,
|
&self,
|
||||||
forge_config: &ForgeConfig,
|
forge_config: &ForgeConfig,
|
||||||
forge_name: ForgeAlias,
|
forge_name: ForgeAlias,
|
||||||
server_storage: &Storage,
|
server_storage: &Storage,
|
||||||
listen_url: &ListenUrl,
|
listen_url: &ListenUrl,
|
||||||
notify_user_recipient: &Recipient<NotifyUser>,
|
notify_user_recipient: &ActorRef<AlertsActor>,
|
||||||
server_addr: Option<Addr<Self>>,
|
server_actor_ref: ActorRef<Self>,
|
||||||
) -> Vec<(ForgeAlias, RepoAlias, RepoActor)> {
|
) -> Vec<(ForgeAlias, RepoAlias, RepoActor)> {
|
||||||
let span =
|
tracing::info!(%forge_name, %forge_config, "");
|
||||||
tracing::info_span!("create_forge_repos", name = %forge_name, config = %forge_config);
|
|
||||||
|
|
||||||
let _guard = span.enter();
|
|
||||||
tracing::info!("Creating Forge");
|
|
||||||
let mut repos = vec![];
|
|
||||||
let creator = self.create_actor(
|
let creator = self.create_actor(
|
||||||
forge_name,
|
forge_name,
|
||||||
forge_config.clone(),
|
forge_config.clone(),
|
||||||
server_storage,
|
server_storage,
|
||||||
listen_url,
|
listen_url,
|
||||||
server_addr,
|
server_actor_ref,
|
||||||
);
|
);
|
||||||
for (repo_alias, server_repo_config) in forge_config.repos() {
|
forge_config
|
||||||
let forge_repo = creator((
|
.repos()
|
||||||
|
.map(|(repo_alias, server_repo_config)| {
|
||||||
|
creator((
|
||||||
repo_alias,
|
repo_alias,
|
||||||
server_repo_config,
|
server_repo_config,
|
||||||
notify_user_recipient.clone(),
|
notify_user_recipient.clone(),
|
||||||
));
|
))
|
||||||
tracing::info!(
|
})
|
||||||
alias = %forge_repo.1,
|
.collect::<Vec<_>>()
|
||||||
"Created Repo"
|
|
||||||
);
|
|
||||||
repos.push(forge_repo);
|
|
||||||
}
|
|
||||||
repos
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_actor(
|
fn create_actor(
|
||||||
|
@ -157,9 +203,9 @@ impl ServerActor {
|
||||||
forge_config: ForgeConfig,
|
forge_config: ForgeConfig,
|
||||||
server_storage: &Storage,
|
server_storage: &Storage,
|
||||||
listen_url: &ListenUrl,
|
listen_url: &ListenUrl,
|
||||||
server_addr: Option<Addr<Self>>,
|
server_actor_ref: ActorRef<Self>,
|
||||||
) -> impl Fn(
|
) -> impl Fn(
|
||||||
(RepoAlias, &ServerRepoConfig, Recipient<NotifyUser>),
|
(RepoAlias, &ServerRepoConfig, ActorRef<AlertsActor>),
|
||||||
) -> (ForgeAlias, RepoAlias, RepoActor) {
|
) -> (ForgeAlias, RepoAlias, RepoActor) {
|
||||||
let server_storage = server_storage.clone();
|
let server_storage = server_storage.clone();
|
||||||
let listen_url = listen_url.clone();
|
let listen_url = listen_url.clone();
|
||||||
|
@ -167,12 +213,11 @@ impl ServerActor {
|
||||||
let repository_factory = self.repository_factory.duplicate();
|
let repository_factory = self.repository_factory.duplicate();
|
||||||
let generation = self.generation;
|
let generation = self.generation;
|
||||||
let sleep_duration = self.sleep_duration;
|
let sleep_duration = self.sleep_duration;
|
||||||
|
let ui = self.ui;
|
||||||
move |(repo_alias, server_repo_config, notify_user_recipient)| {
|
move |(repo_alias, server_repo_config, notify_user_recipient)| {
|
||||||
let span = tracing::info_span!("create_actor", alias = %repo_alias, config = %server_repo_config);
|
let span = tracing::info_span!("create_actor", alias = %repo_alias, config = %server_repo_config);
|
||||||
let _guard = span.enter();
|
let _guard = span.enter();
|
||||||
tracing::info!("Creating Repo");
|
let gitdir = server_repo_config.gitdir().unwrap_or_else(|| {
|
||||||
let gitdir = server_repo_config.gitdir().map_or_else(
|
|
||||||
|| {
|
|
||||||
GitDir::new(
|
GitDir::new(
|
||||||
server_storage
|
server_storage
|
||||||
.path()
|
.path()
|
||||||
|
@ -180,9 +225,7 @@ impl ServerActor {
|
||||||
.join(repo_alias.to_string()),
|
.join(repo_alias.to_string()),
|
||||||
StoragePathType::Internal,
|
StoragePathType::Internal,
|
||||||
)
|
)
|
||||||
},
|
});
|
||||||
|gitdir| gitdir,
|
|
||||||
);
|
|
||||||
// INFO: can't canonicalise gitdir as the path needs to exist to do that and we may not
|
// INFO: can't canonicalise gitdir as the path needs to exist to do that and we may not
|
||||||
// have cloned the repo yet
|
// have cloned the repo yet
|
||||||
let repo_details = RepoDetails::new(
|
let repo_details = RepoDetails::new(
|
||||||
|
@ -194,8 +237,8 @@ impl ServerActor {
|
||||||
gitdir,
|
gitdir,
|
||||||
);
|
);
|
||||||
let forge = Forge::create(repo_details.clone(), net.clone());
|
let forge = Forge::create(repo_details.clone(), net.clone());
|
||||||
tracing::info!("Starting Repo Actor");
|
|
||||||
let actor = RepoActor::new(
|
let actor = RepoActor::new(
|
||||||
|
ui,
|
||||||
repo_details,
|
repo_details,
|
||||||
forge,
|
forge,
|
||||||
listen_url.clone(),
|
listen_url.clone(),
|
||||||
|
@ -204,7 +247,7 @@ impl ServerActor {
|
||||||
repository_factory.duplicate(),
|
repository_factory.duplicate(),
|
||||||
sleep_duration,
|
sleep_duration,
|
||||||
Some(notify_user_recipient),
|
Some(notify_user_recipient),
|
||||||
server_addr.clone(),
|
server_actor_ref.clone(),
|
||||||
);
|
);
|
||||||
(forge_name.clone(), repo_alias, actor)
|
(forge_name.clone(), repo_alias, actor)
|
||||||
}
|
}
|
||||||
|
@ -231,31 +274,43 @@ impl ServerActor {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attempts to gracefully shutdown the server before stopping the system.
|
/// Attempts to gracefully shutdown the server before stopping the system.
|
||||||
fn abort(&mut self, ctx: &<Self as actix::Actor>::Context, message: impl Into<String>) {
|
async fn abort(
|
||||||
self.do_send(crate::server::actor::messages::Shutdown, ctx);
|
&mut self,
|
||||||
|
ctx: &Context<'_, Self, color_eyre::Result<()>>,
|
||||||
|
message: impl Into<String> + Send,
|
||||||
|
) -> Result<()> {
|
||||||
|
let message = message.into();
|
||||||
|
error!(%message, "aborting");
|
||||||
|
self.do_send(Shutdown, ctx).await?;
|
||||||
if let Some(t) = self.shutdown_trigger.take() {
|
if let Some(t) = self.shutdown_trigger.take() {
|
||||||
let _ = t.send(message.into());
|
t.send(message)
|
||||||
} else {
|
.await
|
||||||
error!("{}", message.into());
|
.map_err(|e| format!("failed sending shutdown trigger: {e:?}"))?;
|
||||||
self.do_send(Shutdown, ctx);
|
|
||||||
// System::current().stop_with_code(1);
|
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn do_send<M>(&self, msg: M, ctx: &<Self as actix::Actor>::Context)
|
async fn do_send<M>(
|
||||||
|
&self,
|
||||||
|
msg: M,
|
||||||
|
ctx: &Context<'_, Self, color_eyre::Result<()>>,
|
||||||
|
) -> Result<()>
|
||||||
where
|
where
|
||||||
M: actix::Message + Send + 'static + std::fmt::Debug,
|
M: Send + Sync + 'static + std::fmt::Debug,
|
||||||
Self: actix::Handler<M>,
|
Self: kameo::message::Message<M>,
|
||||||
<M as actix::Message>::Result: Send,
|
<Self as kameo::message::Message<M>>::Reply: Send + Sync + 'static + std::fmt::Debug,
|
||||||
|
<<Self as kameo::message::Message<M>>::Reply as kameo::Reply>::Error: std::fmt::Debug,
|
||||||
{
|
{
|
||||||
if let Some(message_log) = &self.message_log {
|
|
||||||
let log_message = format!("send: {msg:?}");
|
let log_message = format!("send: {msg:?}");
|
||||||
|
if let Some(message_log) = &self.message_log {
|
||||||
if let Ok(mut log) = message_log.write() {
|
if let Ok(mut log) = message_log.write() {
|
||||||
log.push(log_message);
|
log.push(log_message.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if cfg!(not(test)) {
|
if cfg!(not(test)) {
|
||||||
ctx.address().do_send(msg);
|
tell!(ctx.actor_ref(), msg)
|
||||||
|
.map_err(|e| format!("failed sending: {log_message}: {e:?}"))?;
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
|
//
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use actix::prelude::*;
|
use kameo::actor::ActorRef;
|
||||||
|
|
||||||
use crate::alerts::{AlertsActor, History};
|
use crate::alerts::{AlertsActor, History};
|
||||||
|
|
||||||
//
|
|
||||||
pub fn a_filesystem() -> kxio::fs::TempFileSystem {
|
pub fn a_filesystem() -> kxio::fs::TempFileSystem {
|
||||||
#[allow(clippy::expect_used)]
|
#[allow(clippy::expect_used)]
|
||||||
kxio::fs::temp().expect("temp fs")
|
kxio::fs::temp().expect("temp fs")
|
||||||
|
@ -14,6 +14,10 @@ pub fn a_network() -> kxio::net::MockNet {
|
||||||
kxio::net::mock()
|
kxio::net::mock()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn an_alerts_actor(net: kxio::net::Net) -> Addr<AlertsActor> {
|
pub fn an_alerts_actor(net: kxio::net::Net) -> ActorRef<AlertsActor> {
|
||||||
AlertsActor::new(None, History::new(Duration::from_millis(1)), net).start()
|
kameo::spawn(AlertsActor::new(
|
||||||
|
None,
|
||||||
|
History::new(Duration::from_millis(1)),
|
||||||
|
net,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,25 @@
|
||||||
//
|
//
|
||||||
use actix::prelude::*;
|
|
||||||
|
|
||||||
use crate::server::actor::{tests::given, ReceiveAppConfig, ServerActor};
|
|
||||||
use git_next_core::{
|
|
||||||
git,
|
|
||||||
server::{AppConfig, Http, Listen, ListenUrl, Shout, Storage},
|
|
||||||
};
|
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::BTreeMap,
|
collections::BTreeMap,
|
||||||
sync::{Arc, RwLock},
|
sync::{Arc, RwLock},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[test_log::test(actix::test)]
|
use kameo::actor::pubsub::PubSub;
|
||||||
async fn when_webhook_url_has_trailing_slash_should_not_send() {
|
|
||||||
|
use git_next_core::{
|
||||||
|
git,
|
||||||
|
server::{AppConfig, Http, Listen, ListenUrl, Shout, Storage},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
server::actor::{tests::given, ReceiveAppConfig, ServerActor},
|
||||||
|
tell,
|
||||||
|
};
|
||||||
|
|
||||||
|
type TestResult = Result<(), Box<dyn std::error::Error>>;
|
||||||
|
|
||||||
|
#[test_log::test(tokio::test)]
|
||||||
|
async fn when_webhook_url_has_trailing_slash_should_not_send() -> TestResult {
|
||||||
//given
|
//given
|
||||||
// parameters
|
// parameters
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
|
@ -22,8 +28,18 @@ async fn when_webhook_url_has_trailing_slash_should_not_send() {
|
||||||
let repo = git::repository::factory::mock();
|
let repo = git::repository::factory::mock();
|
||||||
let duration = std::time::Duration::from_millis(1);
|
let duration = std::time::Duration::from_millis(1);
|
||||||
|
|
||||||
|
let file_update_subs = kameo::spawn(PubSub::new());
|
||||||
|
|
||||||
// sut
|
// sut
|
||||||
let server = ServerActor::new(fs.as_real(), net.into(), alerts, repo, duration);
|
let server = ServerActor::new(
|
||||||
|
false, // ui
|
||||||
|
fs.as_real(),
|
||||||
|
net.into(),
|
||||||
|
alerts,
|
||||||
|
file_update_subs,
|
||||||
|
repo,
|
||||||
|
duration,
|
||||||
|
);
|
||||||
|
|
||||||
// collaborators
|
// collaborators
|
||||||
let listen = Listen::new(
|
let listen = Listen::new(
|
||||||
|
@ -39,13 +55,11 @@ async fn when_webhook_url_has_trailing_slash_should_not_send() {
|
||||||
let server = server.with_message_log(Some(message_log.clone()));
|
let server = server.with_message_log(Some(message_log.clone()));
|
||||||
|
|
||||||
//when
|
//when
|
||||||
server.start().do_send(ReceiveAppConfig::new(AppConfig::new(
|
tell!(
|
||||||
listen,
|
kameo::spawn(server),
|
||||||
shout,
|
ReceiveAppConfig::new(AppConfig::new(listen, shout, server_storage, repos,))
|
||||||
server_storage,
|
)?;
|
||||||
repos,
|
tokio::time::sleep(std::time::Duration::from_millis(1)).await;
|
||||||
)));
|
|
||||||
actix_rt::time::sleep(std::time::Duration::from_millis(1)).await;
|
|
||||||
|
|
||||||
//then
|
//then
|
||||||
// INFO: assert that ReceiveValidServerConfig is NOT sent
|
// INFO: assert that ReceiveValidServerConfig is NOT sent
|
||||||
|
@ -53,4 +67,6 @@ async fn when_webhook_url_has_trailing_slash_should_not_send() {
|
||||||
assert!(message_log.read().iter().any(|log| !log
|
assert!(message_log.read().iter().any(|log| !log
|
||||||
.iter()
|
.iter()
|
||||||
.any(|line| line == "send: ReceiveValidServerConfig")));
|
.any(|line| line == "send: ReceiveValidServerConfig")));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,35 +1,22 @@
|
||||||
//
|
//
|
||||||
|
use std::{path::PathBuf, sync::Arc};
|
||||||
|
|
||||||
|
use color_eyre::{eyre::Context, Result};
|
||||||
|
use kxio::{fs::FileSystem, net::Net};
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
use git_next_core::git::RepositoryFactory;
|
||||||
|
|
||||||
|
use crate::{root::RootActor, tell};
|
||||||
|
|
||||||
|
pub use actor::ServerActor;
|
||||||
|
|
||||||
pub mod actor;
|
pub mod actor;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
use actix::prelude::*;
|
|
||||||
use actix_rt::signal;
|
|
||||||
use actor::messages::ShutdownTrigger;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
alerts::{AlertsActor, History},
|
|
||||||
file_watcher::{watch_file, FileUpdated},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[allow(clippy::module_name_repetitions)]
|
|
||||||
pub use actor::ServerActor;
|
|
||||||
|
|
||||||
use git_next_core::git::RepositoryFactory;
|
|
||||||
|
|
||||||
use color_eyre::{eyre::Context, Result};
|
|
||||||
use kxio::{fs::FileSystem, net::Net};
|
|
||||||
use tracing::info;
|
|
||||||
|
|
||||||
use std::{
|
|
||||||
path::PathBuf,
|
|
||||||
sync::{atomic::Ordering, mpsc::channel, Arc, RwLock},
|
|
||||||
time::Duration,
|
|
||||||
};
|
|
||||||
|
|
||||||
const A_DAY: Duration = Duration::from_secs(24 * 60 * 60);
|
|
||||||
|
|
||||||
pub fn init(fs: &FileSystem) -> Result<()> {
|
pub fn init(fs: &FileSystem) -> Result<()> {
|
||||||
let file_name = "git-next-server.toml";
|
let file_name = "git-next-server.toml";
|
||||||
let pathbuf = PathBuf::from(file_name);
|
let pathbuf = PathBuf::from(file_name);
|
||||||
|
@ -51,8 +38,8 @@ pub fn init(fs: &FileSystem) -> Result<()> {
|
||||||
#[allow(clippy::too_many_lines)]
|
#[allow(clippy::too_many_lines)]
|
||||||
pub fn start(
|
pub fn start(
|
||||||
ui: bool,
|
ui: bool,
|
||||||
fs: FileSystem,
|
fs: &FileSystem,
|
||||||
net: Net,
|
net: &Net,
|
||||||
repo: Box<dyn RepositoryFactory>,
|
repo: Box<dyn RepositoryFactory>,
|
||||||
sleep_duration: std::time::Duration,
|
sleep_duration: std::time::Duration,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
@ -66,111 +53,48 @@ pub fn start(
|
||||||
}
|
}
|
||||||
|
|
||||||
let shutdown_message_holder: Arc<RwLock<Option<String>>> = Arc::new(RwLock::new(None));
|
let shutdown_message_holder: Arc<RwLock<Option<String>>> = Arc::new(RwLock::new(None));
|
||||||
let shutdown_message_holder_exec = shutdown_message_holder.clone();
|
let shutdown_message_holder_clone = shutdown_message_holder.clone();
|
||||||
let file_watcher_err_holder: Arc<RwLock<Option<anyhow::Error>>> = Arc::new(RwLock::new(None));
|
|
||||||
let file_watcher_err_holder_exec = file_watcher_err_holder.clone();
|
|
||||||
let execution = async move {
|
|
||||||
info!("Starting Alert Dispatcher...");
|
|
||||||
let alerts_addr = AlertsActor::new(None, History::new(A_DAY), net.clone()).start();
|
|
||||||
|
|
||||||
info!("Starting Server...");
|
#[allow(clippy::expect_used)]
|
||||||
let server =
|
tokio::runtime::Runtime::new()?.block_on(async {
|
||||||
ServerActor::new(fs.clone(), net.clone(), alerts_addr, repo, sleep_duration).start();
|
let (tx_shutdown, mut rx_shutdown) = tokio::sync::mpsc::channel::<String>(1);
|
||||||
|
|
||||||
info!("Starting File Watcher...");
|
let root_actor_ref = kameo::spawn(RootActor::new(
|
||||||
let watch_file = watch_file("git-next-server.toml".into(), server.clone().recipient());
|
ui,
|
||||||
let fw_shutdown = match watch_file {
|
fs.clone(),
|
||||||
Ok(fw_shutdown) => fw_shutdown,
|
net.clone(),
|
||||||
Err(err) => {
|
sleep_duration,
|
||||||
// shutdown now
|
tx_shutdown,
|
||||||
server.do_send(crate::server::actor::messages::Shutdown);
|
));
|
||||||
actix_rt::time::sleep(std::time::Duration::from_millis(10)).await;
|
tell!("root", root_actor_ref, crate::root::Start::new(repo)).expect("start root actor");
|
||||||
System::current().stop();
|
|
||||||
let _ = file_watcher_err_holder_exec
|
|
||||||
.write()
|
|
||||||
.map(|mut o| o.replace(err));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let (tx_shutdown, rx_shutdown) = channel::<String>();
|
|
||||||
if ui {
|
|
||||||
#[cfg(feature = "tui")]
|
|
||||||
{
|
|
||||||
use crate::server::actor::messages::SubscribeToUpdates;
|
|
||||||
use crate::tui;
|
|
||||||
|
|
||||||
let tui_addr = tui::Tui::new(tx_shutdown.clone()).start();
|
|
||||||
server.do_send(SubscribeToUpdates::new(tui_addr.clone().recipient()));
|
|
||||||
server.do_send(ShutdownTrigger::new(tx_shutdown));
|
|
||||||
server.do_send(FileUpdated); // update file after ui subscription in place
|
|
||||||
loop {
|
|
||||||
let _ = tui_addr.send(tui::Tick).await;
|
|
||||||
if let Ok(message) = rx_shutdown.try_recv() {
|
|
||||||
let _ = shutdown_message_holder_exec
|
|
||||||
.write()
|
|
||||||
.map(|mut o| o.replace(message));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
actix_rt::time::sleep(Duration::from_millis(16)).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
server.do_send(ShutdownTrigger::new(tx_shutdown.clone()));
|
|
||||||
server.do_send(FileUpdated);
|
|
||||||
|
|
||||||
info!("Server running - Press Ctrl-C to stop...");
|
info!("Server running - Press Ctrl-C to stop...");
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
_r = signal::ctrl_c() => {
|
_r = tokio::signal::ctrl_c() => {
|
||||||
info!("Ctrl-C received, shutting down...");
|
info!("Ctrl-C received, shutting down...");
|
||||||
}
|
}
|
||||||
_x = async move {
|
_x = async move {
|
||||||
loop{
|
loop{
|
||||||
if let Ok(message) = rx_shutdown.try_recv() {
|
if let Some(message) = rx_shutdown.recv().await {
|
||||||
let _ = shutdown_message_holder_exec
|
info!("rx shutdown received: {message}");
|
||||||
.write()
|
let _ = shutdown_message_holder_clone.write().await.replace(message);
|
||||||
.map(|mut o| o.replace(message));
|
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
actix_rt::task::yield_now().await;
|
|
||||||
}
|
}
|
||||||
} => {
|
} => {
|
||||||
info!("signaled shutdown");
|
info!("signaled shutdown");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
});
|
||||||
|
|
||||||
// shutdown
|
|
||||||
fw_shutdown.store(true, Ordering::Relaxed);
|
|
||||||
server.do_send(crate::server::actor::messages::Shutdown);
|
|
||||||
actix_rt::time::sleep(std::time::Duration::from_millis(10)).await;
|
|
||||||
System::current().stop();
|
|
||||||
};
|
|
||||||
|
|
||||||
let system = System::new();
|
|
||||||
Arbiter::current().spawn(execution);
|
|
||||||
system.run()?;
|
|
||||||
|
|
||||||
// check for error from server thread
|
|
||||||
#[allow(clippy::unwrap_used)]
|
|
||||||
if let Some(err) = &*shutdown_message_holder.read().unwrap() {
|
|
||||||
#[cfg(feature = "tui")]
|
#[cfg(feature = "tui")]
|
||||||
if ui {
|
if ui {
|
||||||
ratatui::restore();
|
ratatui::restore();
|
||||||
}
|
}
|
||||||
if !err.is_empty() {
|
|
||||||
return Err(color_eyre::eyre::eyre!(format!("{err}")));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// check for error from file watcher thread
|
if let Some(ref message) = *shutdown_message_holder.blocking_write() {
|
||||||
#[allow(clippy::unwrap_used)]
|
info!(%message, "shutdown");
|
||||||
if let Some(err) = &*file_watcher_err_holder.read().unwrap() {
|
|
||||||
#[cfg(feature = "tui")]
|
|
||||||
if ui {
|
|
||||||
ratatui::restore();
|
|
||||||
}
|
|
||||||
return Err(color_eyre::eyre::eyre!(format!("{err}")));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -38,44 +38,3 @@ mod init {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mod file_watcher {
|
|
||||||
use std::{sync::atomic::Ordering, time::Duration};
|
|
||||||
|
|
||||||
use actix::{Actor, Context, Handler};
|
|
||||||
use rstest::*;
|
|
||||||
|
|
||||||
use crate::file_watcher::{self, FileUpdated};
|
|
||||||
|
|
||||||
use super::TestResult;
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[actix::test]
|
|
||||||
#[timeout(Duration::from_millis(80))]
|
|
||||||
async fn should_not_block_calling_thread() -> TestResult {
|
|
||||||
let fs = kxio::fs::temp()?;
|
|
||||||
let path = fs.base().join("file");
|
|
||||||
fs.file(&path).write("foo")?;
|
|
||||||
|
|
||||||
let listener = Listener;
|
|
||||||
let l_addr = listener.start();
|
|
||||||
let recipient = l_addr.recipient();
|
|
||||||
|
|
||||||
let fw_shutdown = file_watcher::watch_file(path, recipient)?;
|
|
||||||
std::thread::sleep(Duration::from_millis(10));
|
|
||||||
fw_shutdown.store(true, Ordering::Relaxed);
|
|
||||||
|
|
||||||
Ok(()) // was not blocked
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Listener;
|
|
||||||
impl Actor for Listener {
|
|
||||||
type Context = Context<Self>;
|
|
||||||
}
|
|
||||||
impl Handler<FileUpdated> for Listener {
|
|
||||||
type Result = ();
|
|
||||||
|
|
||||||
fn handle(&mut self, _msg: FileUpdated, _ctx: &mut Self::Context) -> Self::Result {
|
|
||||||
// todo!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
//
|
//
|
||||||
use actix::Handler;
|
use kameo::message::{Context, Message};
|
||||||
use ratatui::style::Color;
|
use ratatui::style::Color;
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
server::actor::messages::{RepoUpdate, ServerUpdate},
|
server::actor::messages::{RepoUpdate, ServerUpdate},
|
||||||
|
@ -12,10 +13,14 @@ static PREP: Color = Color::Gray;
|
||||||
static ACTING: Color = Color::LightBlue;
|
static ACTING: Color = Color::LightBlue;
|
||||||
static WARN: Color = Color::Red;
|
static WARN: Color = Color::Red;
|
||||||
|
|
||||||
impl Handler<ServerUpdate> for Tui {
|
impl Message<ServerUpdate> for Tui {
|
||||||
type Result = ();
|
type Reply = ();
|
||||||
|
|
||||||
fn handle(&mut self, msg: ServerUpdate, _ctx: &mut Self::Context) -> Self::Result {
|
async fn handle(
|
||||||
|
&mut self,
|
||||||
|
msg: ServerUpdate,
|
||||||
|
_ctx: Context<'_, Self, Self::Reply>,
|
||||||
|
) -> Self::Reply {
|
||||||
self.state.tap();
|
self.state.tap();
|
||||||
match msg {
|
match msg {
|
||||||
ServerUpdate::AppConfigLoaded { app_config } => {
|
ServerUpdate::AppConfigLoaded { app_config } => {
|
||||||
|
@ -29,12 +34,23 @@ impl Handler<ServerUpdate> for Tui {
|
||||||
} => {
|
} => {
|
||||||
if let ServerState::Configured { forges } = &mut self.state.mode {
|
if let ServerState::Configured { forges } = &mut self.state.mode {
|
||||||
let Some(forge_state) = forges.get_mut(&forge_alias) else {
|
let Some(forge_state) = forges.get_mut(&forge_alias) else {
|
||||||
|
debug!("ServerState::Configured: no forge state available");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let Some(repo_state) = forge_state.repos.get_mut(&repo_alias) else {
|
let Some(repo_state) = forge_state.repos.get_mut(&repo_alias) else {
|
||||||
|
debug!("ServerState::Configured: no repo state available");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
repo_state.clear_alert();
|
repo_state.clear_alert();
|
||||||
|
debug!(?repo_update, "ServerState::Configured: RepoUpdate");
|
||||||
|
handle_repo_update(repo_update, repo_state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_repo_update(repo_update: RepoUpdate, repo_state: &mut crate::tui::actor::RepoState) {
|
||||||
match repo_update {
|
match repo_update {
|
||||||
RepoUpdate::Branches { branches } => {
|
RepoUpdate::Branches { branches } => {
|
||||||
repo_state.update_branches(branches);
|
repo_state.update_branches(branches);
|
||||||
|
@ -42,7 +58,9 @@ impl Handler<ServerUpdate> for Tui {
|
||||||
RepoUpdate::Log { log } => {
|
RepoUpdate::Log { log } => {
|
||||||
repo_state.update_log(log);
|
repo_state.update_log(log);
|
||||||
}
|
}
|
||||||
RepoUpdate::ValidateRepo => repo_state.update_message("polling...", ACTING),
|
RepoUpdate::ValidateRepo => {
|
||||||
|
repo_state.update_message("polling...", ACTING);
|
||||||
|
}
|
||||||
RepoUpdate::Okay { main, next, dev } => {
|
RepoUpdate::Okay { main, next, dev } => {
|
||||||
repo_state.clear_alert();
|
repo_state.clear_alert();
|
||||||
repo_state.update_message("okay", OKAY);
|
repo_state.update_message("okay", OKAY);
|
||||||
|
@ -55,15 +73,13 @@ impl Handler<ServerUpdate> for Tui {
|
||||||
repo_state.update_message("Checking CI status", ACTING);
|
repo_state.update_message("Checking CI status", ACTING);
|
||||||
}
|
}
|
||||||
RepoUpdate::AdvancingNext { commit, force: _ } => {
|
RepoUpdate::AdvancingNext { commit, force: _ } => {
|
||||||
repo_state
|
repo_state.update_message(format!("advancing next to {commit}"), ACTING);
|
||||||
.update_message(format!("advancing next to {commit}"), ACTING);
|
|
||||||
}
|
}
|
||||||
RepoUpdate::NextUpdated => {
|
RepoUpdate::NextUpdated => {
|
||||||
repo_state.update_message("next updated - pause while CI starts", OKAY);
|
repo_state.update_message("next updated - pause while CI starts", OKAY);
|
||||||
}
|
}
|
||||||
RepoUpdate::AdvancingMain { commit } => {
|
RepoUpdate::AdvancingMain { commit } => {
|
||||||
repo_state
|
repo_state.update_message(format!("advancing main to {commit}"), ACTING);
|
||||||
.update_message(format!("advancing main to {commit}"), ACTING);
|
|
||||||
}
|
}
|
||||||
RepoUpdate::MainUpdated => {
|
RepoUpdate::MainUpdated => {
|
||||||
repo_state.update_message("main updated", OKAY);
|
repo_state.update_message("main updated", OKAY);
|
||||||
|
@ -90,15 +106,10 @@ impl Handler<ServerUpdate> for Tui {
|
||||||
repo_state.update_message("unregistering webhook...", PREP);
|
repo_state.update_message("unregistering webhook...", PREP);
|
||||||
}
|
}
|
||||||
RepoUpdate::WebhookReceived { branch, push: _ } => {
|
RepoUpdate::WebhookReceived { branch, push: _ } => {
|
||||||
repo_state
|
repo_state.update_message(format!("webhook update: {branch:?}"), ACTING);
|
||||||
.update_message(format!("webhook update: {branch:?}"), ACTING);
|
|
||||||
}
|
}
|
||||||
RepoUpdate::RegisteredWebhook => {
|
RepoUpdate::RegisteredWebhook => {
|
||||||
repo_state.update_message("registered webhook", PREP);
|
repo_state.update_message("registered webhook", PREP);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,25 @@
|
||||||
//
|
//
|
||||||
use actix::Handler;
|
use std::time::Duration;
|
||||||
|
|
||||||
use crate::tui::actor::{messages::Tick, Tui};
|
use color_eyre::Result;
|
||||||
|
use kameo::message::{Context, Message};
|
||||||
|
|
||||||
impl Handler<Tick> for Tui {
|
use crate::{
|
||||||
type Result = std::io::Result<()>;
|
tell,
|
||||||
|
tui::actor::{messages::Tick, Tui},
|
||||||
|
};
|
||||||
|
|
||||||
fn handle(&mut self, _msg: Tick, ctx: &mut Self::Context) -> Self::Result {
|
impl Message<Tick> for Tui {
|
||||||
|
type Reply = Result<()>;
|
||||||
|
|
||||||
|
async fn handle(&mut self, _msg: Tick, ctx: Context<'_, Self, Self::Reply>) -> Self::Reply {
|
||||||
self.state.tap();
|
self.state.tap();
|
||||||
self.draw()?;
|
self.draw()?;
|
||||||
self.handle_input(ctx)?;
|
self.handle_input(&ctx.actor_ref())?;
|
||||||
|
|
||||||
|
tokio::time::sleep(Duration::from_millis(16)).await;
|
||||||
|
tell!(ctx.actor_ref(), Tick)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,61 +6,85 @@ mod model;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
use std::sync::mpsc::Sender;
|
use color_eyre::Result;
|
||||||
|
use kameo::{
|
||||||
use actix::{Actor, ActorContext as _, Context};
|
actor::{ActorRef, WeakActorRef},
|
||||||
|
error::{ActorStopReason, BoxError, PanicError},
|
||||||
pub use model::*;
|
mailbox::unbounded::UnboundedMailbox,
|
||||||
|
Actor,
|
||||||
|
};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
crossterm::event::{self, KeyCode, KeyEventKind},
|
crossterm::event::{self, KeyCode, KeyEventKind},
|
||||||
DefaultTerminal,
|
DefaultTerminal,
|
||||||
};
|
};
|
||||||
use tui_scrollview::ScrollViewState;
|
use tui_scrollview::ScrollViewState;
|
||||||
|
|
||||||
|
pub use model::*;
|
||||||
|
|
||||||
|
use crate::tell;
|
||||||
|
|
||||||
|
use super::Tick;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Tui {
|
pub struct Tui {
|
||||||
terminal: Option<DefaultTerminal>,
|
terminal: Option<DefaultTerminal>,
|
||||||
signal_shutdown: Sender<String>,
|
|
||||||
pub state: State,
|
pub state: State,
|
||||||
scroll_view_state: ScrollViewState,
|
scroll_view_state: ScrollViewState,
|
||||||
}
|
}
|
||||||
impl Actor for Tui {
|
impl Actor for Tui {
|
||||||
type Context = Context<Self>;
|
type Mailbox = UnboundedMailbox<Self>;
|
||||||
fn started(&mut self, _ctx: &mut Self::Context) {
|
|
||||||
|
async fn on_start(&mut self, actor_ref: ActorRef<Self>) -> Result<(), BoxError> {
|
||||||
self.terminal.replace(ratatui::init());
|
self.terminal.replace(ratatui::init());
|
||||||
|
tell!(actor_ref, Tick)?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
fn stopped(&mut self, _ctx: &mut Self::Context) {
|
|
||||||
|
async fn on_stop(
|
||||||
|
&mut self,
|
||||||
|
_actor_ref: WeakActorRef<Self>,
|
||||||
|
_reason: ActorStopReason,
|
||||||
|
) -> Result<(), BoxError> {
|
||||||
self.terminal.take();
|
self.terminal.take();
|
||||||
ratatui::restore();
|
ratatui::restore();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn on_panic(
|
||||||
|
&mut self,
|
||||||
|
_actor_ref: WeakActorRef<Self>,
|
||||||
|
err: PanicError,
|
||||||
|
) -> Result<Option<ActorStopReason>, BoxError> {
|
||||||
|
self.terminal.take();
|
||||||
|
ratatui::restore();
|
||||||
|
Ok(Some(ActorStopReason::Panicked(err)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl Tui {
|
impl Tui {
|
||||||
pub fn new(signal_shutdown: Sender<String>) -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
terminal: None,
|
terminal: None,
|
||||||
signal_shutdown,
|
|
||||||
state: State::initial(),
|
state: State::initial(),
|
||||||
scroll_view_state: ScrollViewState::default(),
|
scroll_view_state: ScrollViewState::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw(&mut self) -> std::io::Result<()> {
|
fn draw(&mut self) -> std::io::Result<()> {
|
||||||
let t = self.terminal.take();
|
if let Some(terminal) = &mut self.terminal {
|
||||||
let scroll_view_state = &mut self.scroll_view_state;
|
|
||||||
let state = &self.state;
|
|
||||||
if let Some(mut terminal) = t {
|
|
||||||
terminal.draw(|frame| {
|
terminal.draw(|frame| {
|
||||||
frame.render_stateful_widget(state, frame.area(), scroll_view_state);
|
frame.render_stateful_widget(
|
||||||
|
&self.state,
|
||||||
|
frame.area(),
|
||||||
|
&mut self.scroll_view_state,
|
||||||
|
);
|
||||||
})?;
|
})?;
|
||||||
self.terminal = Some(terminal);
|
|
||||||
} else {
|
} else {
|
||||||
eprintln!("No terminal setup");
|
eprintln!("No terminal setup");
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_input(&mut self, ctx: &mut <Self as actix::Actor>::Context) -> std::io::Result<()> {
|
fn handle_input(&mut self, actor_tui: &ActorRef<Self>) -> Result<()> {
|
||||||
if event::poll(std::time::Duration::from_millis(16))? {
|
if event::poll(std::time::Duration::from_millis(16))? {
|
||||||
let event::Event::Key(key) = event::read()? else {
|
let event::Event::Key(key) = event::read()? else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
@ -70,10 +94,7 @@ impl Tui {
|
||||||
}
|
}
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Char('q') => {
|
KeyCode::Char('q') => {
|
||||||
ctx.stop();
|
actor_tui.kill();
|
||||||
if let Err(err) = self.signal_shutdown.send(String::new()) {
|
|
||||||
tracing::error!(?err, "Failed to signal shutdown");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
KeyCode::Char('j') | KeyCode::Down => self.scroll_view_state.scroll_down(),
|
KeyCode::Char('j') | KeyCode::Down => self.scroll_view_state.scroll_down(),
|
||||||
KeyCode::Char('k') | KeyCode::Up => self.scroll_view_state.scroll_up(),
|
KeyCode::Char('k') | KeyCode::Up => self.scroll_view_state.scroll_up(),
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
//
|
//
|
||||||
|
use std::{collections::BTreeMap, fmt::Display, time::Instant};
|
||||||
|
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::Alignment,
|
layout::Alignment,
|
||||||
prelude::{Buffer, Rect},
|
prelude::{Buffer, Rect},
|
||||||
|
@ -7,15 +9,13 @@ use ratatui::{
|
||||||
text::{Line, Span},
|
text::{Line, Span},
|
||||||
widgets::{Block, Paragraph, StatefulWidget, Widget},
|
widgets::{Block, Paragraph, StatefulWidget, Widget},
|
||||||
};
|
};
|
||||||
|
use tracing::info;
|
||||||
|
use tui_scrollview::ScrollViewState;
|
||||||
|
|
||||||
use git_next_core::{
|
use git_next_core::{
|
||||||
git::{self, graph::Log, Commit},
|
git::{self, graph::Log, Commit},
|
||||||
ForgeAlias, RepoAlias, RepoBranches,
|
ForgeAlias, RepoAlias, RepoBranches,
|
||||||
};
|
};
|
||||||
use tracing::info;
|
|
||||||
use tui_scrollview::ScrollViewState;
|
|
||||||
|
|
||||||
use std::{collections::BTreeMap, fmt::Display, time::Instant};
|
|
||||||
|
|
||||||
use crate::{server::actor::messages::ValidAppConfig, tui::components::ConfiguredAppWidget};
|
use crate::{server::actor::messages::ValidAppConfig, tui::components::ConfiguredAppWidget};
|
||||||
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
mod shutdown_webhook;
|
|
|
@ -1,13 +0,0 @@
|
||||||
//
|
|
||||||
use actix::prelude::*;
|
|
||||||
|
|
||||||
use crate::webhook::{messages::ShutdownWebhook, WebhookActor};
|
|
||||||
|
|
||||||
impl Handler<ShutdownWebhook> for WebhookActor {
|
|
||||||
type Result = ();
|
|
||||||
|
|
||||||
fn handle(&mut self, _msg: ShutdownWebhook, ctx: &mut Self::Context) -> Self::Result {
|
|
||||||
self.spawn_handle.take();
|
|
||||||
ctx.stop();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
//
|
|
||||||
use git_next_core::message;
|
|
||||||
|
|
||||||
message!(ShutdownWebhook, "Request to shutdown the Webhook actor");
|
|
|
@ -1,43 +1,56 @@
|
||||||
//
|
//
|
||||||
use actix::prelude::*;
|
|
||||||
|
|
||||||
mod handlers;
|
|
||||||
pub mod messages;
|
|
||||||
pub mod router;
|
|
||||||
mod server;
|
|
||||||
|
|
||||||
use crate::repo::messages::WebhookNotification;
|
|
||||||
|
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
|
|
||||||
use tracing::Instrument;
|
use color_eyre::Result;
|
||||||
|
use kameo::{actor::ActorRef, mailbox::unbounded::UnboundedMailbox, message::Message, Actor};
|
||||||
|
use router::WebhookRouterActor;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
default_on_actor_link_died, default_on_actor_panic, default_on_actor_start,
|
||||||
|
default_on_actor_stop,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub mod router;
|
||||||
|
mod server;
|
||||||
|
|
||||||
#[allow(clippy::module_name_repetitions)]
|
#[allow(clippy::module_name_repetitions)]
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct WebhookActor {
|
pub struct WebhookActor {
|
||||||
socket_addr: SocketAddr,
|
socket_addr: SocketAddr,
|
||||||
span: tracing::Span,
|
message_receiver: ActorRef<WebhookRouterActor>,
|
||||||
spawn_handle: Option<actix::SpawnHandle>,
|
|
||||||
message_receiver: Recipient<WebhookNotification>,
|
|
||||||
}
|
}
|
||||||
impl WebhookActor {
|
impl WebhookActor {
|
||||||
pub fn new(socket_addr: SocketAddr, message_receiver: Recipient<WebhookNotification>) -> Self {
|
pub const fn new(
|
||||||
let span = tracing::info_span!("WebhookActor");
|
socket_addr: SocketAddr,
|
||||||
|
message_receiver: ActorRef<WebhookRouterActor>,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
socket_addr,
|
socket_addr,
|
||||||
span,
|
|
||||||
message_receiver,
|
message_receiver,
|
||||||
spawn_handle: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Actor for WebhookActor {
|
impl Actor for WebhookActor {
|
||||||
type Context = actix::Context<Self>;
|
type Mailbox = UnboundedMailbox<Self>;
|
||||||
fn started(&mut self, ctx: &mut Self::Context) {
|
|
||||||
let _gaurd = self.span.enter();
|
default_on_actor_start!(this, actor_ref);
|
||||||
let address: Recipient<WebhookNotification> = self.message_receiver.clone();
|
default_on_actor_panic!(this, actor_ref, err);
|
||||||
let server = server::start(self.socket_addr, address);
|
default_on_actor_link_died!(this, actor_ref, id, reason);
|
||||||
let spawn_handle = ctx.spawn(server.in_current_span().into_actor(self));
|
default_on_actor_stop!(this, actor_ref, reason);
|
||||||
self.spawn_handle.replace(spawn_handle);
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Start;
|
||||||
|
impl Message<Start> for WebhookActor {
|
||||||
|
type Reply = Result<()>;
|
||||||
|
|
||||||
|
async fn handle(
|
||||||
|
&mut self,
|
||||||
|
_msg: Start,
|
||||||
|
_ctx: kameo::message::Context<'_, Self, Self::Reply>,
|
||||||
|
) -> Self::Reply {
|
||||||
|
server::start(self.socket_addr, self.message_receiver.clone()).await?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,26 +1,28 @@
|
||||||
//
|
//
|
||||||
use actix::prelude::*;
|
|
||||||
|
|
||||||
use derive_more::Constructor;
|
|
||||||
use tracing::{debug, info, warn};
|
|
||||||
|
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use crate::repo::messages::WebhookNotification;
|
use derive_more::Constructor;
|
||||||
|
use kameo::{
|
||||||
|
actor::ActorRef,
|
||||||
|
message::{Context, Message},
|
||||||
|
Actor,
|
||||||
|
};
|
||||||
|
use tracing::{debug, warn};
|
||||||
|
|
||||||
use git_next_core::{ForgeAlias, RepoAlias};
|
use git_next_core::{ForgeAlias, RepoAlias};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
repo::{messages::WebhookNotification, RepoActor},
|
||||||
|
tell,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Actor)]
|
||||||
pub struct WebhookRouterActor {
|
pub struct WebhookRouterActor {
|
||||||
span: tracing::Span,
|
span: tracing::Span,
|
||||||
recipients: BTreeMap<ForgeAlias, BTreeMap<RepoAlias, Recipient<WebhookNotification>>>,
|
recipients: BTreeMap<ForgeAlias, BTreeMap<RepoAlias, ActorRef<RepoActor>>>,
|
||||||
}
|
}
|
||||||
impl Default for WebhookRouterActor {
|
impl Default for WebhookRouterActor {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl WebhookRouterActor {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
let span = tracing::info_span!("WebhookRouter");
|
let span = tracing::info_span!("WebhookRouter");
|
||||||
Self {
|
Self {
|
||||||
span,
|
span,
|
||||||
|
@ -28,43 +30,45 @@ impl WebhookRouterActor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl Actor for WebhookRouterActor {
|
impl Message<WebhookNotification> for WebhookRouterActor {
|
||||||
type Context = Context<Self>;
|
type Reply = color_eyre::Result<()>;
|
||||||
}
|
|
||||||
|
|
||||||
impl Handler<WebhookNotification> for WebhookRouterActor {
|
async fn handle(
|
||||||
type Result = ();
|
&mut self,
|
||||||
|
msg: WebhookNotification,
|
||||||
fn handle(&mut self, msg: WebhookNotification, _ctx: &mut Self::Context) -> Self::Result {
|
_ctx: Context<'_, Self, Self::Reply>,
|
||||||
|
) -> Self::Reply {
|
||||||
let _gaurd = self.span.enter();
|
let _gaurd = self.span.enter();
|
||||||
let forge_alias = msg.forge_alias();
|
let forge_alias = msg.forge_alias();
|
||||||
let repo_alias = msg.repo_alias();
|
let repo_alias = msg.repo_alias();
|
||||||
debug!(forge = %forge_alias, repo = %repo_alias, "Router...");
|
debug!(forge = %forge_alias, repo = %repo_alias, "Router...");
|
||||||
let Some(forge_repos) = self.recipients.get(forge_alias) else {
|
let Some(forge_repos) = self.recipients.get(forge_alias) else {
|
||||||
warn!(forge = %forge_alias, "No forge repos found");
|
warn!(forge = %forge_alias, "No forge repos found");
|
||||||
return;
|
return Ok(());
|
||||||
};
|
};
|
||||||
let Some(recipient) = forge_repos.get(repo_alias) else {
|
let Some(recipient) = forge_repos.get(repo_alias) else {
|
||||||
debug!(forge = %forge_alias, repo = %repo_alias, "No recipient found");
|
debug!(forge = %forge_alias, repo = %repo_alias, "No recipient found");
|
||||||
return;
|
return Ok(());
|
||||||
};
|
};
|
||||||
recipient.do_send(msg);
|
tell!(recipient, msg)?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Message, Constructor)]
|
#[derive(Constructor, Debug)]
|
||||||
#[rtype(result = "()")]
|
|
||||||
pub struct AddWebhookRecipient {
|
pub struct AddWebhookRecipient {
|
||||||
pub forge_alias: ForgeAlias,
|
pub forge_alias: ForgeAlias,
|
||||||
pub repo_alias: RepoAlias,
|
pub repo_alias: RepoAlias,
|
||||||
pub recipient: Recipient<WebhookNotification>,
|
pub recipient: ActorRef<RepoActor>,
|
||||||
}
|
}
|
||||||
impl Handler<AddWebhookRecipient> for WebhookRouterActor {
|
impl Message<AddWebhookRecipient> for WebhookRouterActor {
|
||||||
type Result = ();
|
type Reply = ();
|
||||||
|
|
||||||
fn handle(&mut self, msg: AddWebhookRecipient, _ctx: &mut Self::Context) -> Self::Result {
|
async fn handle(
|
||||||
let _gaurd = self.span.enter();
|
&mut self,
|
||||||
info!(forge = %msg.forge_alias, repo = %msg.repo_alias, "Register Recipient");
|
msg: AddWebhookRecipient,
|
||||||
|
_ctx: Context<'_, Self, Self::Reply>,
|
||||||
|
) -> Self::Reply {
|
||||||
if !self.recipients.contains_key(&msg.forge_alias) {
|
if !self.recipients.contains_key(&msg.forge_alias) {
|
||||||
self.recipients
|
self.recipients
|
||||||
.insert(msg.forge_alias.clone(), BTreeMap::new());
|
.insert(msg.forge_alias.clone(), BTreeMap::new());
|
||||||
|
|
|
@ -1,18 +1,17 @@
|
||||||
//
|
//
|
||||||
use actix::prelude::*;
|
|
||||||
|
|
||||||
use std::{collections::BTreeMap, net::SocketAddr};
|
use std::{collections::BTreeMap, net::SocketAddr};
|
||||||
|
|
||||||
|
use color_eyre::Result;
|
||||||
|
use kameo::actor::ActorRef;
|
||||||
use tracing::{info, warn};
|
use tracing::{info, warn};
|
||||||
|
|
||||||
use crate::repo::messages::WebhookNotification;
|
|
||||||
|
|
||||||
use git_next_core::{webhook, ForgeAlias, ForgeNotification, RepoAlias};
|
use git_next_core::{webhook, ForgeAlias, ForgeNotification, RepoAlias};
|
||||||
|
|
||||||
pub async fn start(
|
use crate::{repo::messages::WebhookNotification, tell};
|
||||||
socket_addr: SocketAddr,
|
|
||||||
address: actix::prelude::Recipient<WebhookNotification>,
|
use super::router::WebhookRouterActor;
|
||||||
) {
|
|
||||||
|
pub async fn start(socket_addr: SocketAddr, address: ActorRef<WebhookRouterActor>) -> Result<()> {
|
||||||
// start webhook server
|
// start webhook server
|
||||||
use warp::Filter;
|
use warp::Filter;
|
||||||
// Define the Warp route to handle incoming HTTP requests
|
// Define the Warp route to handle incoming HTTP requests
|
||||||
|
@ -23,37 +22,28 @@ pub async fn start(
|
||||||
.and(warp::header::headers_cloned())
|
.and(warp::header::headers_cloned())
|
||||||
.and(warp::body::bytes())
|
.and(warp::body::bytes())
|
||||||
.and_then(
|
.and_then(
|
||||||
|recipient: Recipient<WebhookNotification>,
|
|recipient: ActorRef<WebhookRouterActor>,
|
||||||
forge_alias: String,
|
forge_alias: String,
|
||||||
repo_alias: String,
|
repo_alias: String,
|
||||||
// query: String,
|
// query: String,
|
||||||
headers: warp::http::HeaderMap,
|
headers: warp::http::HeaderMap,
|
||||||
body: bytes::Bytes| async move {
|
body: bytes::Bytes| async move {
|
||||||
info!("POST received");
|
info!("POST received");
|
||||||
let forge_alias = ForgeAlias::new(forge_alias);
|
let msg = WebhookNotification::new(ForgeNotification::new(
|
||||||
let repo_alias = RepoAlias::new(repo_alias);
|
ForgeAlias::new(forge_alias),
|
||||||
let bytes = body.to_vec();
|
RepoAlias::new(repo_alias),
|
||||||
let body = webhook::forge_notification::Body::new(
|
headers
|
||||||
String::from_utf8_lossy(&bytes).to_string(),
|
|
||||||
);
|
|
||||||
let headers = headers
|
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|(k, v)| {
|
.filter_map(|(k, v)| {
|
||||||
k.map(|k| (k.to_string(), v.to_str().unwrap_or_default().to_string()))
|
k.map(|k| (k.to_string(), v.to_str().unwrap_or_default().to_string()))
|
||||||
})
|
})
|
||||||
.collect::<BTreeMap<String, String>>();
|
.collect::<BTreeMap<String, String>>(),
|
||||||
let message = WebhookNotification::new(ForgeNotification::new(
|
webhook::forge_notification::Body::new(
|
||||||
forge_alias,
|
String::from_utf8_lossy(&body).to_string(),
|
||||||
repo_alias,
|
),
|
||||||
headers,
|
|
||||||
body,
|
|
||||||
));
|
));
|
||||||
recipient
|
tell!(recipient, msg)
|
||||||
.try_send(message)
|
.map(|()| warp::reply::with_status("OK", warp::http::StatusCode::OK))
|
||||||
.map(|()| {
|
|
||||||
info!("Message sent ok");
|
|
||||||
warp::reply::with_status("OK", warp::http::StatusCode::OK)
|
|
||||||
})
|
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
warn!("Unknown error: {:?}", e);
|
warn!("Unknown error: {:?}", e);
|
||||||
warp::reject()
|
warp::reject()
|
||||||
|
@ -63,5 +53,9 @@ pub async fn start(
|
||||||
|
|
||||||
// Start the server
|
// Start the server
|
||||||
info!("Starting webhook server: {}", socket_addr);
|
info!("Starting webhook server: {}", socket_addr);
|
||||||
warp::serve(route).run(socket_addr).await;
|
let (socket, f) = warp::serve(route).try_bind_ephemeral(socket_addr)?;
|
||||||
|
info!("Server bound to {socket}");
|
||||||
|
f.await;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,7 @@ tracing = { workspace = true }
|
||||||
kxio = { workspace = true }
|
kxio = { workspace = true }
|
||||||
|
|
||||||
# Actors
|
# Actors
|
||||||
actix = { workspace = true }
|
kameo = { workspace = true }
|
||||||
|
|
||||||
# TOML parsing
|
# TOML parsing
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
|
|
|
@ -352,3 +352,13 @@ impl SmtpConfig {
|
||||||
&self.hostname
|
&self.hostname
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
const fn is_sendable<T: Send>() {}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
const fn normal() {
|
||||||
|
is_sendable::<super::AppConfig>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -50,14 +50,11 @@ pub fn open(
|
||||||
|
|
||||||
let open_repository = if repo_details.gitdir.exists() {
|
let open_repository = if repo_details.gitdir.exists() {
|
||||||
info!("Local copy found - opening...");
|
info!("Local copy found - opening...");
|
||||||
let repo = repository_factory.open(repo_details)?;
|
repository_factory.open(repo_details)?
|
||||||
repo.fetch()?;
|
|
||||||
repo
|
|
||||||
} else {
|
} else {
|
||||||
info!("Local copy not found - cloning...");
|
info!("Local copy not found - cloning...");
|
||||||
repository_factory.git_clone(repo_details)?
|
repository_factory.git_clone(repo_details)?
|
||||||
};
|
};
|
||||||
info!("Validating...");
|
|
||||||
validate_default_remotes(&*open_repository, repo_details)
|
validate_default_remotes(&*open_repository, repo_details)
|
||||||
.map_err(|e| Error::Validation(s!(e)))?;
|
.map_err(|e| Error::Validation(s!(e)))?;
|
||||||
Ok(open_repository)
|
Ok(open_repository)
|
||||||
|
|
|
@ -63,7 +63,7 @@ pub(crate) fn test(
|
||||||
|
|
||||||
#[allow(clippy::module_name_repetitions)]
|
#[allow(clippy::module_name_repetitions)]
|
||||||
#[mockall::automock]
|
#[mockall::automock]
|
||||||
pub trait OpenRepositoryLike: std::fmt::Debug + Sync {
|
pub trait OpenRepositoryLike: std::fmt::Debug + Sync + Send {
|
||||||
/// Creates a clone of the `OpenRepositoryLike`.
|
/// Creates a clone of the `OpenRepositoryLike`.
|
||||||
fn duplicate(&self) -> Box<dyn OpenRepositoryLike>;
|
fn duplicate(&self) -> Box<dyn OpenRepositoryLike>;
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,6 @@ use crate::{
|
||||||
s, RemoteUrl,
|
s, RemoteUrl,
|
||||||
};
|
};
|
||||||
|
|
||||||
use tracing::info;
|
|
||||||
|
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
pub fn validate_default_remotes(
|
pub fn validate_default_remotes(
|
||||||
open_repository: &dyn OpenRepositoryLike,
|
open_repository: &dyn OpenRepositoryLike,
|
||||||
|
@ -22,7 +20,6 @@ pub fn validate_default_remotes(
|
||||||
"Unable to build forge url"
|
"Unable to build forge url"
|
||||||
)));
|
)));
|
||||||
};
|
};
|
||||||
info!(config = %remote_url, push = %push_remote, fetch = %fetch_remote, "Check remotes match");
|
|
||||||
if !remote_url.matches(&push_remote) {
|
if !remote_url.matches(&push_remote) {
|
||||||
return Err(Error::MismatchDefaultPushRemote {
|
return Err(Error::MismatchDefaultPushRemote {
|
||||||
found: push_remote,
|
found: push_remote,
|
||||||
|
|
|
@ -2,26 +2,14 @@
|
||||||
macro_rules! message {
|
macro_rules! message {
|
||||||
($name:ident, $value:ty, $docs:literal) => {
|
($name:ident, $value:ty, $docs:literal) => {
|
||||||
git_next_core::newtype!($name, $value, $docs);
|
git_next_core::newtype!($name, $value, $docs);
|
||||||
impl actix::prelude::Message for $name {
|
|
||||||
type Result = ();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
($name:ident, $docs:literal) => {
|
($name:ident, $docs:literal) => {
|
||||||
git_next_core::newtype!($name, $docs);
|
git_next_core::newtype!($name, $docs);
|
||||||
impl actix::prelude::Message for $name {
|
|
||||||
type Result = ();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
($name:ident, $value:ty => $result:ty, $docs:literal) => {
|
($name:ident, $value:ty => $result:ty, $docs:literal) => {
|
||||||
git_next_core::newtype!($name, $value, $docs);
|
git_next_core::newtype!($name, $value, $docs);
|
||||||
impl actix::prelude::Message for $name {
|
|
||||||
type Result = $result;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
($name:ident => $result:ty, $docs:literal) => {
|
($name:ident => $result:ty, $docs:literal) => {
|
||||||
git_next_core::newtype!($name, $docs);
|
git_next_core::newtype!($name, $docs);
|
||||||
impl actix::prelude::Message for $name {
|
|
||||||
type Result = $result;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use git_next_core::git::forge::webhook::Error;
|
||||||
//
|
//
|
||||||
use git_next_core::{git, server::RepoListenUrl, RegisteredWebhook, WebhookAuth, WebhookId};
|
use git_next_core::{git, server::RepoListenUrl, RegisteredWebhook, WebhookAuth, WebhookId};
|
||||||
|
|
||||||
|
@ -9,14 +10,14 @@ use tracing::{info, instrument, warn};
|
||||||
use crate::webhook;
|
use crate::webhook;
|
||||||
use crate::webhook::Hook;
|
use crate::webhook::Hook;
|
||||||
|
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all, fields(forge = %repo_details.forge.forge_alias(), repo = %repo_details.repo_alias))]
|
||||||
pub async fn register(
|
pub async fn register(
|
||||||
repo_details: &git::RepoDetails,
|
repo_details: &git::RepoDetails,
|
||||||
repo_listen_url: &RepoListenUrl,
|
repo_listen_url: &RepoListenUrl,
|
||||||
net: &Net,
|
net: &Net,
|
||||||
) -> git::forge::webhook::Result<RegisteredWebhook> {
|
) -> git::forge::webhook::Result<RegisteredWebhook> {
|
||||||
let Some(repo_config) = repo_details.repo_config.clone() else {
|
let Some(repo_config) = repo_details.repo_config.clone() else {
|
||||||
return Err(git::forge::webhook::Error::NoRepoConfig);
|
return Err(Error::NoRepoConfig);
|
||||||
};
|
};
|
||||||
|
|
||||||
// remove any lingering webhooks for the same URL
|
// remove any lingering webhooks for the same URL
|
||||||
|
@ -51,17 +52,17 @@ pub async fn register(
|
||||||
let Ok(hook) = response.json::<Hook>().await else {
|
let Ok(hook) = response.json::<Hook>().await else {
|
||||||
#[cfg(not(tarpaulin_include))]
|
#[cfg(not(tarpaulin_include))]
|
||||||
// request response is Json so response_body never returns None
|
// request response is Json so response_body never returns None
|
||||||
return Err(git::forge::webhook::Error::NetworkResponseEmpty);
|
return Err(Error::NetworkResponseEmpty);
|
||||||
};
|
};
|
||||||
info!(webhook_id = %hook.id, "Webhook registered");
|
info!(webhook_id = %hook.id, "ok");
|
||||||
Ok(RegisteredWebhook::new(
|
Ok(RegisteredWebhook::new(
|
||||||
WebhookId::new(format!("{}", hook.id)),
|
WebhookId::new(format!("{}", hook.id)),
|
||||||
authorisation,
|
authorisation,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("Failed to register webhook");
|
warn!(?e, "failed");
|
||||||
Err(git::forge::webhook::Error::FailedToRegister(e.to_string()))
|
Err(Error::FailedToRegister(e.to_string()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Ok(())
|
// Ok(())
|
||||||
|
|
3
quickfix-564
Normal file
3
quickfix-564
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
crates/cli/src/server/actor/mod.rs ┃74┃pub fn new(
|
||||||
|
crates/cli/src/server/actor/tests/receive_app_config.rs ┃31┃let server = ServerActor::new(
|
||||||
|
crates/cli/src/server/mod.rs ┃83┃let server = kameo::spqawn(ServerActor::new(
|
Loading…
Reference in a new issue