Compare commits

...

2 commits

Author SHA1 Message Date
c37bd2c64d WIP: add log graph to notifications
All checks were successful
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
2024-08-06 20:49:28 +01:00
8c19680056 refactor: macros use a more common syntax
Some checks failed
Rust / build (push) Successful in 1m15s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Failing after 10m22s
ci/woodpecker/cron/cron-docker-builder Pipeline was successful
ci/woodpecker/cron/push-next Pipeline was successful
ci/woodpecker/cron/tag-created Pipeline was successful
Parameters were separated by ':', but are now separated by ','.
2024-08-06 20:06:39 +01:00
25 changed files with 291 additions and 57 deletions

7
Cargo.lock generated
View file

@ -993,6 +993,7 @@ dependencies = [
"secrecy", "secrecy",
"serde", "serde",
"serde_json", "serde_json",
"take-until",
"test-log", "test-log",
"thiserror", "thiserror",
"time", "time",
@ -3635,6 +3636,12 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "take-until"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8bdb6fa0dfa67b38c1e66b7041ba9dcf23b99d8121907cd31c807a332f7a0bbb"
[[package]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.10.1" version = "3.10.1"

View file

@ -84,6 +84,9 @@ anyhow = "1.0"
thiserror = "1.0" thiserror = "1.0"
pike = "0.1" pike = "0.1"
# iters
take-until = "0.2"
# file watcher # file watcher
notify = "6.1" notify = "6.1"

View file

@ -1,6 +1,10 @@
// //
use git_next_core::{git::UserNotification, message, server::Shout}; use git_next_core::{git::UserNotification, message, server::Shout};
message!(UpdateShout: Shout: "Updated Shout configuration"); message!(UpdateShout, Shout, "Updated Shout configuration");
message!(NotifyUser: UserNotification: "Request to send the message payload to the notification webhook"); message!(
NotifyUser,
UserNotification,
"Request to send the message payload to the notification webhook"
);

View file

@ -6,22 +6,37 @@ use git_next_core::{
message, newtype, ForgeNotification, RegisteredWebhook, RepoConfig, WebhookAuth, WebhookId, message, newtype, ForgeNotification, RegisteredWebhook, RepoConfig, WebhookAuth, WebhookId,
}; };
message!(LoadConfigFromRepo: "Request to load the `git-next.toml` from the git repo."); message!(
message!(CloneRepo: "Request to clone (or open) the git repo."); LoadConfigFromRepo,
message!(ReceiveRepoConfig: RepoConfig: r#"Notification that the `git-next.toml` file has been loaded from the repo and parsed. "Request to load the `git-next.toml` from the git repo."
);
message!(CloneRepo, "Request to clone (or open) the git repo.");
message!(
ReceiveRepoConfig,
RepoConfig,
r#"Notification that the `git-next.toml` file has been loaded from the repo and parsed.
Contains the parsed contents of the `git-next.toml` file."#); Contains the parsed contents of the `git-next.toml` file."#
message!(ValidateRepo: MessageToken: r#"Request that the state of the branches in the git repo be assessed and generate any followup actions. );
message!(
ValidateRepo,
MessageToken,
r#"Request that the state of the branches in the git repo be assessed and generate any followup actions.
This is the main function of `git-next` where decisions are made on what branches need to be updated and when. This is the main function of `git-next` where decisions are made on what branches need to be updated and when.
Contains a [MessageToken] to reduce duplicate messages being sent. Only messages with the latest [MessageToken] are handled, Contains a [MessageToken] to reduce duplicate messages being sent. Only messages with the latest [MessageToken] are handled,
all others are dropped."#); all others are dropped."#
);
message!(WebhookRegistered: (WebhookId, WebhookAuth): r#"Notification that a webhook has been registered with a forge. message!(
WebhookRegistered,
(WebhookId, WebhookAuth),
r#"Notification that a webhook has been registered with a forge.
Contains a tuple of the ID for the webhook returned from the forge, and the unique authorisation token that Contains a tuple of the ID for the webhook returned from the forge, and the unique authorisation token that
incoming messages from the forge must provide."#); incoming messages from the forge must provide."#
);
impl WebhookRegistered { impl WebhookRegistered {
pub const fn webhook_id(&self) -> &WebhookId { pub const fn webhook_id(&self) -> &WebhookId {
&self.0 .0 &self.0 .0
@ -38,28 +53,51 @@ impl From<RegisteredWebhook> for WebhookRegistered {
} }
} }
message!(UnRegisterWebhook: "Request that the webhook be removed from the forge, so they will stop notifying us."); message!(
UnRegisterWebhook,
"Request that the webhook be removed from the forge, so they will stop notifying us."
);
newtype!(MessageToken: u32, Copy, Default, Display, PartialOrd, Ord: r#"An incremental token used to identify the current set of messages. newtype!(
MessageToken,
u32,
Copy,
Default,
Display,
PartialOrd,
Ord,
r#"An incremental token used to identify the current set of messages.
Primarily used by [ValidateRepo] to reduce duplicate messages. The token is incremented when a new Webhook message is Primarily used by [ValidateRepo] to reduce duplicate messages. The token is incremented when a new Webhook message is
received, marking that message the latest, and causing any previous messages still being processed to be dropped when received, marking that message the latest, and causing any previous messages still being processed to be dropped when
they next send a [ValidateRepo] message."#); they next send a [ValidateRepo] message."#
);
impl MessageToken { impl MessageToken {
pub const fn next(self) -> Self { pub const fn next(self) -> Self {
Self(self.0 + 1) Self(self.0 + 1)
} }
} }
message!(RegisterWebhook: "Requests that a Webhook be registered with the forge."); message!(
message!(CheckCIStatus: Commit: r#"Requests that the CI status for the commit be checked. RegisterWebhook,
"Requests that a Webhook be registered with the forge."
);
message!(
CheckCIStatus,
Commit,
r#"Requests that the CI status for the commit be checked.
Once the CI Status has been received it will be sent via a [ReceiveCIStatus] message. Once the CI Status has been received it will be sent via a [ReceiveCIStatus] message.
Contains the commit from the tip of the `next` branch."#); // next commit Contains the commit from the tip of the `next` branch."#
message!(ReceiveCIStatus: (Commit, Status): r#"Notification of the status of the CI checks for the commit. ); // next commit
message!(
ReceiveCIStatus,
(Commit, Status),
r#"Notification of the status of the CI checks for the commit.
Contains a tuple of the commit that was checked (the tip of the `next` branch) and the status."#); // commit and it's status Contains a tuple of the commit that was checked (the tip of the `next` branch) and the status."#
); // commit and it's status
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct AdvanceNextPayload { pub struct AdvanceNextPayload {
@ -67,7 +105,23 @@ pub struct AdvanceNextPayload {
pub main: Commit, pub main: Commit,
pub dev_commit_history: Vec<Commit>, pub dev_commit_history: Vec<Commit>,
} }
message!(AdvanceNext: AdvanceNextPayload: "Request to advance the `next` branch on to the next commit on the `dev branch."); // next commit and the dev commit history message!(
message!(AdvanceMain: Commit: "Request to advance the `main` branch on to same commit as the `next` branch."); // next commit AdvanceNext,
message!(WebhookNotification: ForgeNotification: "Notification of a webhook message from the forge."); AdvanceNextPayload,
message!(NotifyUser: UserNotification: "Request to send the message payload to the notification webhook"); "Request to advance the `next` branch on to the next commit on the `dev branch."
); // next commit and the dev commit history
message!(
AdvanceMain,
Commit,
"Request to advance the `main` branch on to same commit as the `next` branch."
); // next commit
message!(
WebhookNotification,
ForgeNotification,
"Notification of a webhook message from the forge."
);
message!(
NotifyUser,
UserNotification,
"Request to send the message payload to the notification webhook"
);

View file

@ -78,7 +78,7 @@ 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 Handler<ExamineActor> for RepoActor {
type Result = RepoActorView; type Result = RepoActorView;

View file

@ -9,11 +9,15 @@ use git_next_core::{
use std::net::SocketAddr; use std::net::SocketAddr;
// receive server config // receive server config
message!(ReceiveAppConfig: AppConfig: "Notification of newly loaded server configuration. message!(
ReceiveAppConfig,
AppConfig,
"Notification of newly loaded server configuration.
This message will prompt the `git-next` server to stop and restart all repo-actors. This message will prompt the `git-next` server to stop and restart all repo-actors.
Contains the new server configuration."); Contains the new server configuration."
);
// receive valid server config // receive valid server config
#[derive(Clone, Debug, PartialEq, Eq, Constructor)] #[derive(Clone, Debug, PartialEq, Eq, Constructor)]
@ -22,6 +26,10 @@ pub struct ValidAppConfig {
pub socket_address: SocketAddr, pub socket_address: SocketAddr,
pub storage: Storage, pub storage: Storage,
} }
message!(ReceiveValidAppConfig: ValidAppConfig: "Notification of validated server configuration."); message!(
ReceiveValidAppConfig,
ValidAppConfig,
"Notification of validated server configuration."
);
message!(Shutdown: "Notification to shutdown the server actor"); message!(Shutdown, "Notification to shutdown the server actor");

View file

@ -1,4 +1,4 @@
// //
use git_next_core::message; use git_next_core::message;
message!(ShutdownWebhook: "Request to shutdown the Webhook actor"); message!(ShutdownWebhook, "Request to shutdown the Webhook actor");

View file

@ -57,6 +57,9 @@ serde_json = { workspace = true }
mockall = { workspace = true } mockall = { workspace = true }
#iters
take-until = { workspace = true }
[dev-dependencies] [dev-dependencies]
# Testing # Testing
assert2 = { workspace = true } assert2 = { workspace = true }

View file

@ -3,4 +3,12 @@ use serde::Serialize;
use crate::newtype; use crate::newtype;
newtype!(BranchName: String, Display, Default, Hash, Serialize: "The name of a Git branch"); newtype!(
BranchName,
String,
Display,
Default,
Hash,
Serialize,
"The name of a Git branch"
);

View file

@ -1,6 +1,18 @@
use serde::Serialize; use serde::Serialize;
crate::newtype!(ForgeAlias: String, Hash, PartialOrd, Ord, derive_more::Display, Default, Serialize: "The name of a Forge to connect to"); use crate::newtype;
newtype!(
ForgeAlias,
String,
Hash,
PartialOrd,
Ord,
derive_more::Display,
Default,
Serialize,
"The name of a Forge to connect to"
);
impl From<&ForgeAlias> for std::path::PathBuf { impl From<&ForgeAlias> for std::path::PathBuf {
fn from(value: &ForgeAlias) -> Self { fn from(value: &ForgeAlias) -> Self {
Self::from(&value.0) Self::from(&value.0)

View file

@ -0,0 +1,3 @@
use crate::newtype;
newtype!(LogGraph, Vec<String>, "Git log showing branch positions");

View file

@ -1,2 +1,10 @@
// The hostname of a forge //
crate::newtype!(Hostname: String, derive_more::Display, Default: "The hostname for the forge"); use crate::newtype;
newtype!(
Hostname,
String,
derive_more::Display,
Default,
"The hostname for the forge"
);

View file

@ -7,6 +7,7 @@ mod forge_config;
mod forge_details; mod forge_details;
mod forge_type; mod forge_type;
pub mod git_dir; pub mod git_dir;
mod graphs;
mod host_name; mod host_name;
mod registered_webhook; mod registered_webhook;
mod remote_url; mod remote_url;

View file

@ -1,6 +1,14 @@
//
use std::borrow::ToOwned; use std::borrow::ToOwned;
crate::newtype!(RemoteUrl: git_url_parse::GitUrl, derive_more::Display: "The URL of a remote repository"); use crate::newtype;
newtype!(
RemoteUrl,
git_url_parse::GitUrl,
derive_more::Display,
"The URL of a remote repository"
);
impl RemoteUrl { impl RemoteUrl {
pub fn parse(url: impl Into<String>) -> Option<Self> { pub fn parse(url: impl Into<String>) -> Option<Self> {
Some(Self::new(::git_url_parse::GitUrl::parse(&url.into()).ok()?)) Some(Self::new(::git_url_parse::GitUrl::parse(&url.into()).ok()?))

View file

@ -3,6 +3,16 @@ use serde::Serialize;
use crate::newtype; use crate::newtype;
newtype!(RepoAlias: String, Display, Default, Hash, PartialOrd, Ord, Serialize: r#"The alias of a repo. newtype!(
RepoAlias,
String,
Display,
Default,
Hash,
PartialOrd,
Ord,
Serialize,
r#"The alias of a repo.
This is the alias for the repo within `git-next-server.toml`."#); This is the alias for the repo within `git-next-server.toml`."#
);

View file

@ -122,8 +122,13 @@ impl Listen {
} }
newtype!( newtype!(
ListenUrl: ListenUrl,
String, Serialize, Deserialize, PartialOrd, Ord, Display: String,
Serialize,
Deserialize,
PartialOrd,
Ord,
Display,
"The base url for receiving all webhooks from all forges" "The base url for receiving all webhooks from all forges"
); );
impl ListenUrl { impl ListenUrl {
@ -133,9 +138,13 @@ impl ListenUrl {
} }
} }
newtype!(ForgeWebhookUrl: String: "Raw URL from a forge Webhook"); newtype!(ForgeWebhookUrl, String, "Raw URL from a forge Webhook");
newtype!(RepoListenUrl: (ListenUrl, ForgeAlias, RepoAlias): "URL to listen for webhook from forges"); newtype!(
RepoListenUrl,
(ListenUrl, ForgeAlias, RepoAlias),
"URL to listen for webhook from forges"
);
impl Display for RepoListenUrl { impl Display for RepoListenUrl {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!( write!(

View file

@ -1,9 +1,14 @@
// //
use crate::newtype; use crate::newtype;
newtype!(WebhookAuth: ulid::Ulid, derive_more::Display: r#"The unique token authorisation for the webhook. newtype!(
WebhookAuth,
ulid::Ulid,
derive_more::Display,
r#"The unique token authorisation for the webhook.
Each monitored repository has it's own unique token, and it is different each time `git-next` runs."#); Each monitored repository has it's own unique token, and it is different each time `git-next` runs."#
);
impl WebhookAuth { impl WebhookAuth {
/// Parses the authorisation string as a `Ulid` to create a `WebhookAuth`. /// Parses the authorisation string as a `Ulid` to create a `WebhookAuth`.
/// ///

View file

@ -1,2 +1,9 @@
// //
crate::newtype!(WebhookId: String, derive_more::Display: "The ID of the webhook, as returned by the forge when it is registered."); use crate::newtype;
newtype!(
WebhookId,
String,
derive_more::Display,
"The ID of the webhook, as returned by the forge when it is registered."
);

View file

@ -41,8 +41,26 @@ impl From<webhook::Push> for Commit {
} }
} }
newtype!(Sha: String, Display, Hash,PartialOrd, Ord, Serialize: "The unique SHA for a git commit."); newtype!(
newtype!(Message: String, Display, Hash, PartialOrd, Ord, Serialize: "The commit message for a git commit."); Sha,
String,
Display,
Hash,
PartialOrd,
Ord,
Serialize,
"The unique SHA for a git commit."
);
newtype!(
Message,
String,
Display,
Hash,
PartialOrd,
Ord,
Serialize,
"The commit message for a git commit."
);
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Histories { pub struct Histories {

View file

@ -2,9 +2,16 @@ use derive_more::Display;
use crate::newtype; use crate::newtype;
newtype!(Generation: u32, Display, Default, Copy: r#"A counter for the server generation. newtype!(
Generation,
u32,
Display,
Default,
Copy,
r#"A counter for the server generation.
This counter is increased by one each time the server restarts itself when the configuration file is updated."#); This counter is increased by one each time the server restarts itself when the configuration file is updated."#
);
impl Generation { impl Generation {
pub fn inc(&mut self) { pub fn inc(&mut self) {
self.0 += 1; self.0 += 1;

View file

@ -5,7 +5,7 @@ use derive_more::Display;
use crate::git::{commit::Sha, Commit}; use crate::git::{commit::Sha, Commit};
newtype!(GitRef: String, Display: "A git reference to a git commit."); newtype!(GitRef, String, Display, "A git reference to a git commit.");
impl From<Commit> for GitRef { impl From<Commit> for GitRef {
fn from(value: Commit) -> Self { fn from(value: Commit) -> Self {
Self(value.sha().to_string()) Self(value.sha().to_string())

View file

@ -0,0 +1,58 @@
//
use std::borrow::ToOwned;
use take_until::TakeUntilExt;
use crate::{GitDir, RepoBranches};
// create a graph to log relative positions
//
// ANCESTOR=$(git merge-base --octopus origin/main origin/next origin/dev)
// SHORT_ANCESTOR=$(echo $ANCESTOR | cut -b -7)
fn ancesstor(gitdir: &GitDir, branches: &RepoBranches) -> Option<String> {
let result = std::process::Command::new("/usr/bin/git")
.current_dir(gitdir.to_path_buf())
.args([
"merge-base",
"--octopus",
format!("origin/{}", branches.main()).as_str(),
format!("origin/{}", branches.next()).as_str(),
format!("origin/{}", branches.dev()).as_str(),
])
.output();
if let Ok(output) = result {
return String::from_utf8_lossy(output.stdout.as_slice())
.split('\n')
.take(1)
.map(|line| line.chars().take(7).collect::<String>())
.collect::<Vec<_>>()
.first()
.cloned();
}
None
}
// git log --oneline --graph --decorate origin/main origin/dev origin/next | awk "1;/$SHORT_ANCESTOR/{exit}"
fn log(gitdir: &GitDir, branches: &RepoBranches, ancestor: &str) -> Vec<String> {
let result = std::process::Command::new("/usr/bin/git")
.current_dir(gitdir.to_path_buf())
.args([
"log",
"--oneline",
"--graph",
"--decorate",
format!("origin/{}", branches.main()).as_str(),
format!("origin/{}", branches.next()).as_str(),
format!("origin/{}", branches.dev()).as_str(),
])
.output();
if let Ok(output) = result {
return String::from_utf8_lossy(output.stdout.as_slice())
.split('\n')
.take_until(|line| line.starts_with(ancestor))
.map(ToOwned::to_owned)
.collect();
}
vec![]
}

View file

@ -6,6 +6,7 @@ pub mod forge;
mod generation; mod generation;
mod git_ref; mod git_ref;
mod git_remote; mod git_remote;
pub mod graph;
pub mod push; pub mod push;
mod repo_details; mod repo_details;
pub mod repository; pub mod repository;

View file

@ -1,25 +1,25 @@
#[macro_export] #[macro_export]
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 { impl actix::prelude::Message for $name {
type Result = (); 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 { impl actix::prelude::Message for $name {
type Result = (); type Result = ();
} }
}; };
($name:ident: $value:ty => $result:ty: $docs:literal) => { ($name:ident, $value:ty => $result:ty, $docs:literal) => {
git_next_core::newtype!($name is a $value: $docs); git_next_core::newtype!($name, $value, $docs);
impl actix::prelude::Message for $name { impl actix::prelude::Message for $name {
type Result = $result; 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 { impl actix::prelude::Message for $name {
type Result = $result; type Result = $result;
} }

View file

@ -1,7 +1,7 @@
// //
#[macro_export] #[macro_export]
macro_rules! newtype { macro_rules! newtype {
($name:ident: $docs:literal) => { ($name:ident, $docs:literal) => {
#[doc = $docs] #[doc = $docs]
#[derive( #[derive(
Clone, Clone,
@ -20,7 +20,7 @@ macro_rules! newtype {
)] )]
pub struct $name; pub struct $name;
}; };
($name:ident: $type:ty $(, $derive:ty)*: $docs:literal) => { ($name:ident, $type:ty $(, $derive:ty)*, $docs:literal) => {
#[doc = $docs] #[doc = $docs]
#[derive( #[derive(
Clone, Clone,