feat: add short git log graph to notifications

Closes kemitix/git-next#133
This commit is contained in:
Paul Campbell 2024-08-06 20:48:53 +01:00
parent 8c19680056
commit ef24cb583c
13 changed files with 140 additions and 28 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

@ -297,7 +297,13 @@ Sample payload:
"main": "main" "main": "main"
}, },
"forge_alias": "jo", "forge_alias": "jo",
"repo_alias": "kxio" "repo_alias": "kxio",
"log": [
"* 9bfce91 (origin/dev) fix: add log graph to notifications",
"| * c37bd2c (origin/next, origin/main) feat: add log graph to notifications",
"|/",
"* 8c19680 refactor: macros use a more common syntax"
]
}, },
"timestamp": "1721760933", "timestamp": "1721760933",
"type": "branch.dev.not-on-main" "type": "branch.dev.not-on-main"
@ -318,11 +324,16 @@ Sample payload:
{ {
"data": { "data": {
"commit": { "commit": {
"sha": "98abef1af6825f9770d725a681e5cfc09d7fd4f2", "sha": "c37bd2caf6825f9770d725a681e5cfc09d7fd4f2",
"message": "feat: add foo to bar template" "message": "feat: add log graph to notifications (1 of 2)"
}, },
"forge_alias": "jo", "forge_alias": "jo",
"repo_alias": "kxio" "repo_alias": "kxio",
"log": [
"* 9bfce91 (origin/dev) feat: add log graph to notifications (2 of 2)",
"* c37bd2c (origin/next) feat: add log graph to notifications (1 of 2)",
"* 8c19680 (origin/main) refactor: macros use a more common syntax"
]
}, },
"timestamp": "1721760933", "timestamp": "1721760933",
"type": "cicheck.failed" "type": "cicheck.failed"

View file

@ -35,6 +35,7 @@ fn short_message(user_notification: &UserNotification) -> String {
forge_alias, forge_alias,
repo_alias, repo_alias,
commit, commit,
log: _,
} => format!("CI Check Failed: {forge_alias}/{repo_alias}: {commit}"), } => format!("CI Check Failed: {forge_alias}/{repo_alias}: {commit}"),
UserNotification::RepoConfigLoadFailure { UserNotification::RepoConfigLoadFailure {
forge_alias, forge_alias,
@ -53,6 +54,7 @@ fn short_message(user_notification: &UserNotification) -> String {
main_branch: _, main_branch: _,
dev_commit: _, dev_commit: _,
main_commit: _, main_commit: _,
log: _,
} => format!("Dev not based on Main: {forge_alias}/{repo_alias}"), } => format!("Dev not based on Main: {forge_alias}/{repo_alias}"),
}; };
format!("[git-next] {tail}") format!("[git-next] {tail}")
@ -64,6 +66,7 @@ fn full_message(user_notification: &UserNotification) -> String {
forge_alias, forge_alias,
repo_alias, repo_alias,
commit, commit,
log,
} => { } => {
let sha = commit.sha(); let sha = commit.sha();
let message = commit.message(); let message = commit.message();
@ -71,6 +74,8 @@ fn full_message(user_notification: &UserNotification) -> String {
"CI Checks had Failed".to_string(), "CI Checks had Failed".to_string(),
format!("Forge: {forge_alias}\nRepo : {repo_alias}"), format!("Forge: {forge_alias}\nRepo : {repo_alias}"),
format!("Commit:\n - {sha}\n - {message}"), format!("Commit:\n - {sha}\n - {message}"),
"Log:".to_string(),
log.join("\n"),
] ]
.join("\n\n") .join("\n\n")
} }
@ -99,21 +104,16 @@ fn full_message(user_notification: &UserNotification) -> String {
repo_alias, repo_alias,
dev_branch, dev_branch,
main_branch, main_branch,
dev_commit, dev_commit: _,
main_commit, main_commit: _,
} => { log,
let dev_sha = dev_commit.sha(); } => [
let dev_message = dev_commit.message(); format!("The branch '{dev_branch}' is not based on the branch '{main_branch}'."),
let main_sha = main_commit.sha(); format!("TODO: Rebase '{dev_branch}' onto '{main_branch}'."),
let main_message = main_commit.message(); format!("Forge: {forge_alias}\nRepo : {repo_alias}"),
[ "Log:".to_string(),
format!("The branch '{dev_branch}' is not based on the branch '{main_branch}'."), log.join("\n"),
format!("TODO: Rebase '{dev_branch}' onto '{main_branch}'."), ]
format!("Forge: {forge_alias}\nRepo : {repo_alias}"), .join("\n\n"),
format!("{dev_branch}:\n - {dev_sha}\n - {dev_message}"),
format!("{main_branch}:\n - {main_sha}\n - {main_message}"),
]
.join("\n\n")
}
} }
} }

View file

@ -1,7 +1,7 @@
// //
use actix::prelude::*; use actix::prelude::*;
use git_next_core::git::{forge::commit::Status, UserNotification}; use git_next_core::git::{forge::commit::Status, graph, UserNotification};
use tracing::debug; use tracing::debug;
use crate::repo::{ use crate::repo::{
@ -40,6 +40,7 @@ impl Handler<ReceiveCIStatus> for RepoActor {
forge_alias, forge_alias,
repo_alias, repo_alias,
commit: next, commit: next,
log: graph::log(&self.repo_details),
}, },
log.as_ref(), log.as_ref(),
); );

View file

@ -14,6 +14,7 @@ impl NotifyUser {
forge_alias, forge_alias,
repo_alias, repo_alias,
commit, commit,
log,
} => json!({ } => json!({
"type": "cicheck.failed", "type": "cicheck.failed",
"timestamp": timestamp, "timestamp": timestamp,
@ -23,7 +24,8 @@ impl NotifyUser {
"commit": { "commit": {
"sha": commit.sha(), "sha": commit.sha(),
"message": commit.message() "message": commit.message()
} },
"log": **log
} }
}), }),
UserNotification::RepoConfigLoadFailure { UserNotification::RepoConfigLoadFailure {
@ -59,6 +61,7 @@ impl NotifyUser {
main_branch, main_branch,
dev_commit, dev_commit,
main_commit, main_commit,
log,
} => json!({ } => json!({
"type": "branch.dev.not-on-main", "type": "branch.dev.not-on-main",
"timestamp": timestamp, "timestamp": timestamp,
@ -78,7 +81,8 @@ impl NotifyUser {
"sha": main_commit.sha(), "sha": main_commit.sha(),
"message": main_commit.message() "message": main_commit.message()
} }
} },
"log": **log
} }
}), }),
} }

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

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

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

@ -0,0 +1,69 @@
//
use std::borrow::ToOwned;
use take_until::TakeUntilExt;
use crate::{newtype, GitDir, RepoBranches};
use super::RepoDetails;
newtype!(Log, Vec<String>, Default, Hash, "Git log of branches");
// 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}"
pub fn log(repo_details: &RepoDetails) -> Log {
if let Some(repo_config) = &repo_details.repo_config {
let branches = repo_config.branches();
let result = std::process::Command::new("/usr/bin/git")
.current_dir(repo_details.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 {
if let Some(ancestor) = ancesstor(repo_details.gitdir(), branches) {
return String::from_utf8_lossy(output.stdout.as_slice())
.split('\n')
.take_until(|line| line.contains(&ancestor))
.map(str::trim)
.map(ToOwned::to_owned)
.collect::<Vec<_>>()
.into();
}
}
}
Log::default()
}

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

@ -2,12 +2,15 @@
use crate::{git::Commit, BranchName, ForgeAlias, RepoAlias}; use crate::{git::Commit, BranchName, ForgeAlias, RepoAlias};
use serde_json::json; use serde_json::json;
use super::graph::Log;
#[derive(Clone, Debug, Hash, PartialEq, Eq)] #[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub enum UserNotification { pub enum UserNotification {
CICheckFailed { CICheckFailed {
forge_alias: ForgeAlias, forge_alias: ForgeAlias,
repo_alias: RepoAlias, repo_alias: RepoAlias,
commit: Commit, commit: Commit,
log: Log,
}, },
RepoConfigLoadFailure { RepoConfigLoadFailure {
forge_alias: ForgeAlias, forge_alias: ForgeAlias,
@ -26,6 +29,7 @@ pub enum UserNotification {
main_branch: BranchName, main_branch: BranchName,
dev_commit: Commit, dev_commit: Commit,
main_commit: Commit, main_commit: Commit,
log: Log,
}, },
} }
impl UserNotification { impl UserNotification {
@ -37,6 +41,7 @@ impl UserNotification {
forge_alias, forge_alias,
repo_alias, repo_alias,
commit, commit,
log,
} => json!({ } => json!({
"type": "cicheck.failed", "type": "cicheck.failed",
"timestamp": timestamp, "timestamp": timestamp,
@ -46,7 +51,8 @@ impl UserNotification {
"commit": { "commit": {
"sha": commit.sha(), "sha": commit.sha(),
"message": commit.message() "message": commit.message()
} },
"log": **log
} }
}), }),
Self::RepoConfigLoadFailure { Self::RepoConfigLoadFailure {
@ -59,7 +65,7 @@ impl UserNotification {
"data": { "data": {
"forge_alias": forge_alias, "forge_alias": forge_alias,
"repo_alias": repo_alias, "repo_alias": repo_alias,
"reason": reason "reason": reason,
} }
}), }),
Self::WebhookRegistration { Self::WebhookRegistration {
@ -72,7 +78,7 @@ impl UserNotification {
"data": { "data": {
"forge_alias": forge_alias, "forge_alias": forge_alias,
"repo_alias": repo_alias, "repo_alias": repo_alias,
"reason": reason "reason": reason,
} }
}), }),
Self::DevNotBasedOnMain { Self::DevNotBasedOnMain {
@ -82,6 +88,7 @@ impl UserNotification {
main_branch, main_branch,
dev_commit, dev_commit,
main_commit, main_commit,
log,
} => json!({ } => json!({
"type": "branch.dev.not-on-main", "type": "branch.dev.not-on-main",
"timestamp": timestamp, "timestamp": timestamp,
@ -101,7 +108,8 @@ impl UserNotification {
"sha": main_commit.sha(), "sha": main_commit.sha(),
"message": main_commit.message() "message": main_commit.message()
} }
} },
"log": **log
} }
}), }),
} }

View file

@ -1,6 +1,6 @@
// //
use crate::{ use crate::{
git::{self, repository::open::OpenRepositoryLike, RepoDetails, UserNotification}, git::{self, graph::log, repository::open::OpenRepositoryLike, RepoDetails, UserNotification},
BranchName, RepoConfig, BranchName, RepoConfig,
}; };
@ -61,6 +61,7 @@ pub fn validate(
main_branch, main_branch,
dev_commit: dev, dev_commit: dev,
main_commit: main, main_commit: main,
log: log(repo_details),
}, },
)); ));
} }