feat(tui): (experimental) show repo state, messages and git log
All checks were successful
Rust / build (push) Successful in 14m16s
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 1m3s

This commit is contained in:
Paul Campbell 2024-08-12 21:25:24 +01:00 committed by Paul Campbell
parent f504b62ff6
commit 5d9915bdbd
57 changed files with 2269 additions and 644 deletions

1004
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -28,13 +28,17 @@ git-next-forge-github = { path = "crates/forge-github", version = "0.13" }
# TUI # TUI
ratatui = "0.28" ratatui = "0.28"
directories = "5.0.1"
lazy_static = "1.5.0"
color-eyre = "0.6.3"
# CLI parsing # CLI parsing
clap = { version = "4.5", features = ["cargo", "derive"] } clap = { version = "4.5", features = ["cargo", "derive"] }
# logging # logging
tracing = "0.1" tracing = "0.1"
tracing-subscriber = "0.3" tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tracing-error = "0.2.0"
# base64 decoding # base64 decoding
base64 = "0.22" base64 = "0.22"

View file

@ -12,10 +12,11 @@ keywords = { workspace = true }
categories = { workspace = true } categories = { workspace = true }
[features] [features]
default = ["forgejo", "github"] # default = ["forgejo", "github"]
default = ["forgejo", "github", "tui"]
forgejo = ["git-next-forge-forgejo"] forgejo = ["git-next-forge-forgejo"]
github = ["git-next-forge-github"] github = ["git-next-forge-github"]
tui = ["ratatui"] tui = ["ratatui", "directories", "lazy_static"]
[dependencies] [dependencies]
git-next-core = { workspace = true } git-next-core = { workspace = true }
@ -24,6 +25,9 @@ git-next-forge-github = { workspace = true, optional = true }
# TUI # TUI
ratatui = { workspace = true, optional = true } ratatui = { workspace = true, optional = true }
directories = { workspace = true, optional = true }
lazy_static = { workspace = true, optional = true }
color-eyre = { workspace = true }
# CLI parsing # CLI parsing
clap = { workspace = true } clap = { workspace = true }
@ -34,6 +38,7 @@ kxio = { workspace = true }
# logging # logging
tracing = { workspace = true } tracing = { workspace = true }
tracing-subscriber = { workspace = true } tracing-subscriber = { workspace = true }
tracing-error.workspace = true
# Conventional Commit check # Conventional Commit check
git-conventional = { workspace = true } git-conventional = { workspace = true }

View file

@ -1,5 +1,5 @@
// //
use anyhow::{Context, Result}; use color_eyre::{eyre::Context, Result};
use kxio::fs::FileSystem; use kxio::fs::FileSystem;
pub fn run(fs: &FileSystem) -> Result<()> { pub fn run(fs: &FileSystem) -> Result<()> {

View file

@ -1,4 +1,6 @@
// //
#![allow(clippy::module_name_repetitions)]
mod alerts; mod alerts;
mod file_watcher; mod file_watcher;
mod forge; mod forge;
@ -17,8 +19,8 @@ use git_next_core::git;
use std::path::PathBuf; use std::path::PathBuf;
use anyhow::Result;
use clap::Parser; use clap::Parser;
use color_eyre::Result;
use kxio::{fs, network::Network}; use kxio::{fs, network::Network};
#[derive(Parser, Debug)] #[derive(Parser, Debug)]

View file

@ -17,15 +17,13 @@ use tracing::{info, instrument, warn};
// advance next to the next commit towards the head of the dev branch // advance next to the next commit towards the head of the dev branch
#[instrument(fields(next), skip_all)] #[instrument(fields(next), skip_all)]
pub fn advance_next( pub fn advance_next(
next: &Commit, commit: Option<Commit>,
main: &Commit, force: git_next_core::git::push::Force,
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,
) -> Result<MessageToken> { ) -> Result<MessageToken> {
let (commit, force) = find_next_commit_on_dev(next, main, dev_commit_history);
let commit = commit.ok_or_else(|| Error::NextAtDev)?; let commit = commit.ok_or_else(|| Error::NextAtDev)?;
validate_commit_message(commit.message())?; validate_commit_message(commit.message())?;
info!("Advancing next to commit '{}'", commit); info!("Advancing next to commit '{}'", commit);

View file

@ -5,11 +5,14 @@ use git_next_core::RepoConfigSource;
use tracing::warn; use tracing::warn;
use crate::repo::{ use crate::{
branch::advance_main, repo::{
do_send, branch::advance_main,
messages::{AdvanceMain, LoadConfigFromRepo, ValidateRepo}, do_send,
RepoActor, messages::{AdvanceMain, LoadConfigFromRepo, ValidateRepo},
RepoActor,
},
server::actor::messages::RepoUpdate,
}; };
impl Handler<AdvanceMain> for RepoActor { impl Handler<AdvanceMain> for RepoActor {
@ -26,13 +29,13 @@ impl Handler<AdvanceMain> for RepoActor {
let repo_details = self.repo_details.clone(); let repo_details = self.repo_details.clone();
let addr = ctx.address(); let addr = ctx.address();
let message_token = self.message_token; let message_token = self.message_token;
let commit = msg.unwrap();
match advance_main( self.update_tui(RepoUpdate::AdvancingMain {
msg.unwrap(), commit: commit.clone(),
&repo_details, });
&repo_config,
&**open_repository, match advance_main(commit, &repo_details, &repo_config, &**open_repository) {
) {
Err(err) => { Err(err) => {
warn!("advance main: {err}"); warn!("advance main: {err}");
} }

View file

@ -3,11 +3,14 @@ use actix::prelude::*;
use tracing::warn; use tracing::warn;
use crate::repo::{ use crate::{
branch::advance_next, repo::{
do_send, branch::{advance_next, find_next_commit_on_dev},
messages::{AdvanceNext, AdvanceNextPayload, ValidateRepo}, do_send,
RepoActor, messages::{AdvanceNext, AdvanceNextPayload, ValidateRepo},
RepoActor,
},
server::actor::messages::RepoUpdate,
}; };
impl Handler<AdvanceNext> for RepoActor { impl Handler<AdvanceNext> for RepoActor {
@ -20,6 +23,7 @@ impl Handler<AdvanceNext> for RepoActor {
let Some(open_repository) = &self.open_repository else { let Some(open_repository) = &self.open_repository else {
return; return;
}; };
let AdvanceNextPayload { let AdvanceNextPayload {
next, next,
main, main,
@ -29,10 +33,15 @@ impl Handler<AdvanceNext> for RepoActor {
let repo_config = repo_config.clone(); let repo_config = repo_config.clone();
let addr = ctx.address(); let addr = ctx.address();
let (commit, force) = find_next_commit_on_dev(&next, &main, &dev_commit_history);
self.update_tui(RepoUpdate::AdvancingNext {
commit: commit.clone(),
force: force.clone(),
});
match advance_next( match advance_next(
&next, commit,
&main, force,
&dev_commit_history,
repo_details, repo_details,
repo_config, repo_config,
&**open_repository, &**open_repository,

View file

@ -3,10 +3,13 @@ use actix::prelude::*;
use tracing::{debug, Instrument as _}; use tracing::{debug, Instrument as _};
use crate::repo::{ use crate::{
do_send, repo::{
messages::{CheckCIStatus, ReceiveCIStatus}, do_send,
RepoActor, messages::{CheckCIStatus, ReceiveCIStatus},
RepoActor,
},
server::actor::messages::RepoUpdate,
}; };
impl Handler<CheckCIStatus> for RepoActor { impl Handler<CheckCIStatus> for RepoActor {
@ -14,11 +17,13 @@ impl Handler<CheckCIStatus> for RepoActor {
fn handle(&mut self, msg: CheckCIStatus, ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: CheckCIStatus, ctx: &mut Self::Context) -> Self::Result {
crate::repo::logger(self.log.as_ref(), "start: CheckCIStatus"); crate::repo::logger(self.log.as_ref(), "start: CheckCIStatus");
let addr = ctx.address(); let addr = ctx.address();
let forge = self.forge.duplicate(); let forge = self.forge.duplicate();
let next = msg.unwrap(); let next = msg.unwrap();
let log = self.log.clone(); let log = self.log.clone();
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 { async move {
let status = forge.commit_status(&next).await; let status = forge.commit_status(&next).await;

View file

@ -4,10 +4,13 @@ use actix::prelude::*;
use git_next_core::git; use git_next_core::git;
use tracing::{debug, instrument, warn}; use tracing::{debug, instrument, warn};
use crate::repo::{ use crate::{
do_send, logger, repo::{
messages::{CloneRepo, LoadConfigFromRepo, RegisterWebhook}, do_send, logger,
RepoActor, messages::{CloneRepo, LoadConfigFromRepo, RegisterWebhook},
RepoActor,
},
server::actor::messages::RepoUpdate,
}; };
impl Handler<CloneRepo> for RepoActor { impl Handler<CloneRepo> for RepoActor {
@ -15,11 +18,13 @@ impl Handler<CloneRepo> for RepoActor {
#[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 { fn handle(&mut self, _msg: CloneRepo, ctx: &mut Self::Context) -> Self::Result {
logger(self.log.as_ref(), "Handler: CloneRepo: start"); logger(self.log.as_ref(), "Handler: CloneRepo: start");
self.update_tui(RepoUpdate::Opening);
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");
debug!("open okay"); debug!("open okay");
self.update_tui(RepoUpdate::Opened);
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()); do_send(&ctx.address(), LoadConfigFromRepo, self.log.as_ref());
@ -30,6 +35,7 @@ impl Handler<CloneRepo> for RepoActor {
Err(err) => { Err(err) => {
logger(self.log.as_ref(), "open failed"); logger(self.log.as_ref(), "open failed");
warn!("Could not open repo: {err:?}"); warn!("Could not open repo: {err:?}");
self.alert_tui(err.to_string());
} }
} }
debug!("Handler: CloneRepo: finish"); debug!("Handler: CloneRepo: finish");

View file

@ -5,10 +5,13 @@ use git_next_core::git::UserNotification;
use tracing::{debug, instrument, Instrument as _}; use tracing::{debug, instrument, Instrument as _};
use crate::repo::{ use crate::{
do_send, load, repo::{
messages::{LoadConfigFromRepo, ReceiveRepoConfig}, do_send, load,
notify_user, RepoActor, messages::{LoadConfigFromRepo, ReceiveRepoConfig},
notify_user, RepoActor,
},
server::actor::messages::RepoUpdate,
}; };
impl Handler<LoadConfigFromRepo> for RepoActor { impl Handler<LoadConfigFromRepo> for RepoActor {
@ -16,6 +19,7 @@ impl Handler<LoadConfigFromRepo> for RepoActor {
#[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 { fn handle(&mut self, _msg: LoadConfigFromRepo, ctx: &mut Self::Context) -> Self::Result {
debug!("Handler: LoadConfigFromRepo: start"); debug!("Handler: LoadConfigFromRepo: start");
self.update_tui(RepoUpdate::LoadingConfigFromRepo);
let Some(open_repository) = &self.open_repository else { let Some(open_repository) = &self.open_repository else {
return; return;
}; };

View file

@ -4,10 +4,13 @@ use actix::prelude::*;
use git_next_core::git::{forge::commit::Status, graph, UserNotification}; use git_next_core::git::{forge::commit::Status, graph, UserNotification};
use tracing::debug; use tracing::debug;
use crate::repo::{ use crate::{
delay_send, do_send, logger, repo::{
messages::{AdvanceMain, ReceiveCIStatus, ValidateRepo}, delay_send, do_send, logger,
notify_user, RepoActor, messages::{AdvanceMain, ReceiveCIStatus, ValidateRepo},
notify_user, RepoActor,
},
server::actor::messages::RepoUpdate,
}; };
impl Handler<ReceiveCIStatus> for RepoActor { impl Handler<ReceiveCIStatus> for RepoActor {
@ -22,6 +25,11 @@ impl Handler<ReceiveCIStatus> for RepoActor {
let repo_alias = self.repo_details.repo_alias.clone(); let repo_alias = self.repo_details.repo_alias.clone();
let message_token = self.message_token; let message_token = self.message_token;
let sleep_duration = self.sleep_duration; let sleep_duration = self.sleep_duration;
let graph_log = graph::log(&self.repo_details);
self.update_tui_log(graph_log.clone());
self.update_tui(RepoUpdate::ReceiveCIStatus {
status: status.clone(),
});
debug!(?status, ""); debug!(?status, "");
match status { match status {
@ -34,13 +42,14 @@ impl Handler<ReceiveCIStatus> for RepoActor {
} }
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,
repo_alias, repo_alias,
commit: next, commit: next,
log: graph::log(&self.repo_details), log: graph_log,
}, },
log.as_ref(), log.as_ref(),
); );

View file

@ -2,10 +2,13 @@
use actix::prelude::*; use actix::prelude::*;
use tracing::instrument; use tracing::instrument;
use crate::repo::{ use crate::{
do_send, repo::{
messages::{ReceiveRepoConfig, RegisterWebhook}, do_send,
RepoActor, messages::{ReceiveRepoConfig, RegisterWebhook},
RepoActor,
},
server::actor::messages::RepoUpdate,
}; };
impl Handler<ReceiveRepoConfig> for RepoActor { impl Handler<ReceiveRepoConfig> for RepoActor {
@ -13,8 +16,11 @@ impl Handler<ReceiveRepoConfig> for RepoActor {
#[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 { fn handle(&mut self, msg: ReceiveRepoConfig, ctx: &mut Self::Context) -> Self::Result {
let repo_config = msg.unwrap(); let repo_config = msg.unwrap();
self.update_tui(RepoUpdate::ReceiveRepoConfig {
repo_config: repo_config.clone(),
});
self.repo_details.repo_config.replace(repo_config); self.repo_details.repo_config.replace(repo_config);
self.update_tui_branches();
do_send(&ctx.address(), RegisterWebhook::new(), self.log.as_ref()); do_send(&ctx.address(), RegisterWebhook::new(), self.log.as_ref());
} }
} }

View file

@ -1,12 +1,15 @@
// //
use actix::prelude::*; use actix::prelude::*;
use tracing::{debug, Instrument as _}; use tracing::{debug, error, Instrument as _};
use crate::repo::{ use crate::{
do_send, repo::{
messages::{RegisterWebhook, WebhookRegistered}, do_send,
notify_user, RepoActor, messages::{RegisterWebhook, WebhookRegistered},
notify_user, RepoActor,
},
server::actor::messages::RepoUpdate,
}; };
use git_next_core::git::UserNotification; use git_next_core::git::UserNotification;
@ -25,11 +28,12 @@ impl Handler<RegisterWebhook> for RepoActor {
let addr = ctx.address(); let addr = ctx.address();
let notify_user_recipient = self.notify_user_recipient.clone(); let notify_user_recipient = self.notify_user_recipient.clone();
let log = self.log.clone(); let log = self.log.clone();
self.update_tui(RepoUpdate::RegisteringWebhook);
debug!("registering webhook"); debug!("registering webhook");
async move { async move {
match forge.register_webhook(&repo_listen_url).await { match forge.register_webhook(&repo_listen_url).await {
Ok(registered_webhook) => { Ok(registered_webhook) => {
debug!(?registered_webhook, ""); debug!(?registered_webhook, "webhook registered");
do_send( do_send(
&addr, &addr,
WebhookRegistered::from(registered_webhook), WebhookRegistered::from(registered_webhook),
@ -37,6 +41,7 @@ impl Handler<RegisterWebhook> for RepoActor {
); );
} }
Err(err) => { Err(err) => {
error!(?err, "failed to register webhook");
notify_user( notify_user(
notify_user_recipient.as_ref(), notify_user_recipient.as_ref(),
UserNotification::WebhookRegistration { UserNotification::WebhookRegistration {
@ -52,6 +57,8 @@ impl Handler<RegisterWebhook> for RepoActor {
.in_current_span() .in_current_span()
.into_actor(self) .into_actor(self)
.wait(ctx); .wait(ctx);
} else {
self.alert_tui("already have a webhook id - cant register webhook");
} }
} }
} }

View file

@ -3,13 +3,17 @@ use actix::prelude::*;
use tracing::{debug, warn, Instrument as _}; use tracing::{debug, warn, Instrument as _};
use crate::repo::{messages::UnRegisterWebhook, RepoActor}; use crate::{
repo::{messages::UnRegisterWebhook, RepoActor},
server::actor::messages::RepoUpdate,
};
impl Handler<UnRegisterWebhook> for RepoActor { impl Handler<UnRegisterWebhook> for RepoActor {
type Result = (); type Result = ();
fn handle(&mut self, _msg: UnRegisterWebhook, ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, _msg: UnRegisterWebhook, ctx: &mut Self::Context) -> Self::Result {
if let Some(webhook_id) = self.webhook_id.take() { if let Some(webhook_id) = self.webhook_id.take() {
self.update_tui(RepoUpdate::UnregisteringWebhook);
let forge = self.forge.duplicate(); let forge = self.forge.duplicate();
debug!("unregistering webhook"); debug!("unregistering webhook");
async move { async move {

View file

@ -1,15 +1,21 @@
// //
use actix::prelude::*; use actix::prelude::*;
use tracing::{debug, instrument, Instrument as _}; use tracing::{debug, info, instrument, Instrument as _};
use crate::repo::{ use crate::{
do_send, logger, repo::{
messages::{AdvanceNext, AdvanceNextPayload, CheckCIStatus, MessageToken, ValidateRepo}, do_send, logger,
notify_user, RepoActor, messages::{AdvanceNext, AdvanceNextPayload, CheckCIStatus, MessageToken, ValidateRepo},
notify_user, RepoActor,
},
server::actor::messages::RepoUpdate,
}; };
use git_next_core::git::validation::positions::{validate, Error, Positions}; use git_next_core::git::{
self,
validation::positions::{validate, Error, Positions},
};
impl Handler<ValidateRepo> for RepoActor { impl Handler<ValidateRepo> for RepoActor {
type Result = (); type Result = ();
@ -41,19 +47,34 @@ impl Handler<ValidateRepo> for RepoActor {
format!("accepted token: {}", self.message_token), format!("accepted token: {}", self.message_token),
); );
self.update_tui(RepoUpdate::ValidateRepo);
// 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");
self.alert_tui("repo not open");
return; return;
}; };
logger(self.log.as_ref(), "have open repository"); logger(self.log.as_ref(), "have open repository");
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");
self.alert_tui("no repo config");
return; return;
}; };
logger(self.log.as_ref(), "have repo config"); logger(self.log.as_ref(), "have repo config");
match validate(&**open_repository, &self.repo_details, &repo_config) { info!("collecting git graph log");
let git_log = git::graph::log(&self.repo_details);
info!(?git_log, "collected git graph log");
self.update_tui_log(git_log.clone());
info!("sent to ui git graph log");
match validate(
&**open_repository,
&self.repo_details,
&repo_config,
git_log,
) {
Ok(Positions { Ok(Positions {
main, main,
next, next,
@ -75,10 +96,11 @@ impl Handler<ValidateRepo> for RepoActor {
self.log.as_ref(), self.log.as_ref(),
); );
} else { } else {
// do nothing self.update_tui(RepoUpdate::Okay { main, next, dev });
} }
} }
Err(Error::Retryable(message)) => { Err(Error::Retryable(message)) => {
self.alert_tui(format!("[retryable: {message}]"));
logger(self.log.as_ref(), message); logger(self.log.as_ref(), message);
let addr = ctx.address(); let addr = ctx.address();
let message_token = self.message_token; let message_token = self.message_token;
@ -95,12 +117,16 @@ impl Handler<ValidateRepo> for RepoActor {
.into_actor(self) .into_actor(self)
.wait(ctx); .wait(ctx);
} }
Err(Error::UserIntervention(user_notification)) => notify_user( Err(Error::UserIntervention(user_notification)) => {
self.notify_user_recipient.as_ref(), self.alert_tui(format!("[USER INTERVENTION: {user_notification}]"));
user_notification, notify_user(
self.log.as_ref(), self.notify_user_recipient.as_ref(),
), user_notification,
self.log.as_ref(),
);
}
Err(Error::NonRetryable(message)) => { Err(Error::NonRetryable(message)) => {
self.alert_tui(format!("[Error: {message}]"));
logger(self.log.as_ref(), message); logger(self.log.as_ref(), message);
} }
} }

View file

@ -3,10 +3,13 @@ use actix::prelude::*;
use tracing::{info, instrument, warn}; use tracing::{info, instrument, warn};
use crate::repo::{ use crate::{
do_send, logger, repo::{
messages::{ValidateRepo, WebhookNotification}, do_send, logger,
ActorLog, RepoActor, messages::{ValidateRepo, WebhookNotification},
ActorLog, RepoActor,
},
server::actor::messages::RepoUpdate,
}; };
use git_next_core::{ use git_next_core::{
@ -52,6 +55,10 @@ impl Handler<WebhookNotification> for RepoActor {
return; return;
} }
Some(Branch::Main) => { Some(Branch::Main) => {
self.update_tui(RepoUpdate::WebhookReceived {
branch: Branch::Main,
push: push.clone(),
});
if handle_push( if handle_push(
push, push,
&config.branches().main(), &config.branches().main(),
@ -64,6 +71,10 @@ impl Handler<WebhookNotification> for RepoActor {
}; };
} }
Some(Branch::Next) => { Some(Branch::Next) => {
self.update_tui(RepoUpdate::WebhookReceived {
branch: Branch::Next,
push: push.clone(),
});
if handle_push( if handle_push(
push, push,
&config.branches().next(), &config.branches().next(),
@ -76,6 +87,10 @@ impl Handler<WebhookNotification> for RepoActor {
}; };
} }
Some(Branch::Dev) => { Some(Branch::Dev) => {
self.update_tui(RepoUpdate::WebhookReceived {
branch: Branch::Dev,
push: push.clone(),
});
if handle_push( if handle_push(
push, push,
&config.branches().dev(), &config.branches().dev(),
@ -135,7 +150,7 @@ fn handle_push(
last_commit: &mut Option<Commit>, last_commit: &mut Option<Commit>,
log: Option<&ActorLog>, log: Option<&ActorLog>,
) -> Result<(), ()> { ) -> Result<(), ()> {
logger(log, "message is for dev branch"); logger(log, format!("message is for {branch} branch"));
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}"));

View file

@ -2,16 +2,20 @@
use actix::prelude::*; use actix::prelude::*;
use tracing::instrument; use tracing::instrument;
use crate::repo::{ use crate::{
do_send, repo::{
messages::{ValidateRepo, WebhookRegistered}, do_send,
RepoActor, messages::{ValidateRepo, WebhookRegistered},
RepoActor,
},
server::actor::messages::RepoUpdate,
}; };
impl Handler<WebhookRegistered> for RepoActor { impl Handler<WebhookRegistered> for RepoActor {
type Result = (); type 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 { fn handle(&mut self, msg: WebhookRegistered, ctx: &mut Self::Context) -> Self::Result {
self.update_tui(RepoUpdate::RegisteredWebhook);
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(

View file

@ -1,11 +1,17 @@
// //
use actix::prelude::*; use actix::prelude::*;
use crate::alerts::messages::NotifyUser; use crate::{
alerts::messages::NotifyUser,
server::{
actor::messages::{RepoUpdate, ServerUpdate},
ServerActor,
},
};
use derive_more::Deref; use derive_more::Deref;
use kxio::network::Network; use kxio::network::Network;
use std::time::Duration; use std::time::Duration;
use tracing::{info, warn, Instrument}; use tracing::{info, instrument, warn, Instrument};
use git_next_core::{ use git_next_core::{
git::{ git::{
@ -59,6 +65,7 @@ pub struct RepoActor {
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<Recipient<NotifyUser>>,
server_addr: Option<Addr<ServerActor>>,
} }
impl RepoActor { impl RepoActor {
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
@ -71,6 +78,7 @@ impl RepoActor {
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<Recipient<NotifyUser>>,
server_addr: Option<Addr<ServerActor>>,
) -> Self { ) -> Self {
let message_token = messages::MessageToken::default(); let message_token = messages::MessageToken::default();
Self { Self {
@ -90,6 +98,55 @@ impl RepoActor {
sleep_duration, sleep_duration,
log: None, log: None,
notify_user_recipient, notify_user_recipient,
server_addr,
}
}
fn update_tui_branches(&self) {
#[cfg(feature = "tui")]
{
use crate::server::actor::messages::RepoUpdate;
let Some(repo_config) = &self.repo_details.repo_config else {
return;
};
let branches = repo_config.branches().clone();
self.update_tui(RepoUpdate::Branches { branches });
}
}
#[instrument]
fn update_tui_log(&self, log: git::graph::Log) {
info!("ready to send log");
#[cfg(feature = "tui")]
{
info!("sending...");
self.update_tui(RepoUpdate::Log { log });
info!("sent");
}
}
fn alert_tui(&self, alert: impl Into<String>) {
#[cfg(feature = "tui")]
{
self.update_tui(RepoUpdate::Alert {
alert: alert.into(),
});
}
}
fn update_tui(&self, repo_update: RepoUpdate) {
#[cfg(feature = "tui")]
{
let Some(server_addr) = &self.server_addr else {
return;
};
let update = ServerUpdate::RepoUpdate {
forge_alias: self.repo_details.forge.forge_alias().clone(),
repo_alias: self.repo_details.repo_alias.clone(),
repo_update,
};
server_addr.do_send(update);
} }
} }
} }

View file

@ -1,6 +1,28 @@
// //
use crate::repo::branch::find_next_commit_on_dev;
use super::*; use super::*;
fn advance_next_sut(
next: &Commit,
main: &Commit,
dev_commit_history: &[Commit],
repo_details: RepoDetails,
repo_config: RepoConfig,
open_repository: &dyn OpenRepositoryLike,
message_token: MessageToken,
) -> branch::Result<MessageToken> {
let (commit, force) = find_next_commit_on_dev(next, main, dev_commit_history);
branch::advance_next(
commit,
force,
repo_details,
repo_config,
open_repository,
message_token,
)
}
mod when_at_dev { mod when_at_dev {
// next and dev branches are the same // next and dev branches are the same
use super::*; use super::*;
@ -16,7 +38,7 @@ mod when_at_dev {
// no on_push defined - so any call to push will cause an error // no on_push defined - so any call to push will cause an error
let message_token = given::a_message_token(); let message_token = given::a_message_token();
let_assert!( let_assert!(
Err(err) = branch::advance_next( Err(err) = advance_next_sut(
&next, &next,
main, main,
dev_commit_history, dev_commit_history,
@ -51,7 +73,7 @@ mod can_advance {
// no on_push defined - so any call to push will cause an error // no on_push defined - so any call to push will cause an error
let message_token = given::a_message_token(); let message_token = given::a_message_token();
let_assert!( let_assert!(
Err(err) = branch::advance_next( Err(err) = advance_next_sut(
&next, &next,
main, main,
dev_commit_history, dev_commit_history,
@ -82,7 +104,7 @@ mod can_advance {
// no on_push defined - so any call to push will cause an error // no on_push defined - so any call to push will cause an error
let message_token = given::a_message_token(); let message_token = given::a_message_token();
let_assert!( let_assert!(
Err(err) = branch::advance_next( Err(err) = advance_next_sut(
&next, &next,
main, main,
dev_commit_history, dev_commit_history,
@ -122,7 +144,7 @@ mod can_advance {
expect::push(&mut open_repository, Err(git::push::Error::Lock)); expect::push(&mut open_repository, Err(git::push::Error::Lock));
let message_token = given::a_message_token(); let message_token = given::a_message_token();
let_assert!( let_assert!(
Err(err) = branch::advance_next( Err(err) = advance_next_sut(
&next, &next,
main, main,
dev_commit_history, dev_commit_history,
@ -154,7 +176,7 @@ mod can_advance {
expect::push_ok(&mut open_repository); expect::push_ok(&mut open_repository);
let message_token = given::a_message_token(); let message_token = given::a_message_token();
let_assert!( let_assert!(
Ok(mt) = branch::advance_next( Ok(mt) = advance_next_sut(
&next, &next,
main, main,
dev_commit_history, dev_commit_history,

View file

@ -195,6 +195,7 @@ pub fn a_repo_actor(
repository_factory, repository_factory,
std::time::Duration::from_nanos(1), std::time::Duration::from_nanos(1),
None, None,
None,
) )
.with_log(actors_log), .with_log(actors_log),
log, log,

View file

@ -32,7 +32,7 @@ async fn should_store_repo_config_in_actor() -> TestResult {
Ok(()) Ok(())
} }
#[actix::test] #[test_log::test(actix::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();
@ -54,10 +54,6 @@ async fn should_register_webhook() -> TestResult {
//then //then
tracing::debug!(?log, ""); tracing::debug!(?log, "");
log.read().map_err(|e| e.to_string()).map(|l| { log.require_message_containing("send: RegisterWebhook")?;
assert!(l
.iter()
.any(|message| message.contains("send: RegisterWebhook")));
})?;
Ok(()) Ok(())
} }

View file

@ -1,5 +1,6 @@
mod file_updated; mod file_updated;
mod receive_app_config; mod receive_app_config;
mod receive_valid_app_config; mod receive_valid_app_config;
mod server_update;
mod shutdown; mod shutdown;
mod subscribe_updates; mod subscribe_updates;

View file

@ -8,7 +8,7 @@ use crate::{
alerts::messages::UpdateShout, alerts::messages::UpdateShout,
repo::{messages::CloneRepo, RepoActor}, repo::{messages::CloneRepo, RepoActor},
server::actor::{ server::actor::{
messages::{ReceiveValidAppConfig, ValidAppConfig}, messages::{ReceiveValidAppConfig, ServerUpdate, ValidAppConfig},
ServerActor, ServerActor,
}, },
webhook::{ webhook::{
@ -21,7 +21,7 @@ use crate::{
impl Handler<ReceiveValidAppConfig> for ServerActor { impl Handler<ReceiveValidAppConfig> for ServerActor {
type Result = (); type Result = ();
fn handle(&mut self, msg: ReceiveValidAppConfig, _ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: ReceiveValidAppConfig, ctx: &mut Self::Context) -> Self::Result {
let ValidAppConfig { let ValidAppConfig {
app_config, app_config,
socket_address, socket_address,
@ -37,6 +37,7 @@ impl Handler<ReceiveValidAppConfig> for ServerActor {
let webhook_router = WebhookRouterActor::default().start(); 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 notify_user_recipient = self.alerts.clone().recipient();
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,6 +47,7 @@ impl Handler<ReceiveValidAppConfig> for ServerActor {
&server_storage, &server_storage,
listen_url, listen_url,
&notify_user_recipient, &notify_user_recipient,
server_addr.clone(),
) )
.into_iter() .into_iter()
.map(start_repo_actor) .map(start_repo_actor)
@ -69,9 +71,18 @@ impl Handler<ReceiveValidAppConfig> for ServerActor {
WebhookActor::new(socket_address, webhook_router.recipient()).start(); WebhookActor::new(socket_address, webhook_router.recipient()).start();
self.webhook_actor_addr.replace(webhook_actor_addr); self.webhook_actor_addr.replace(webhook_actor_addr);
let shout = app_config.shout().clone(); let shout = app_config.shout().clone();
self.app_config.replace(app_config); self.app_config.replace(app_config.clone());
self.do_send(
ServerUpdate::AppConfigLoaded {
app_config: ValidAppConfig {
app_config,
socket_address,
storage: server_storage,
},
},
ctx,
);
self.alerts.do_send(UpdateShout::new(shout)); self.alerts.do_send(UpdateShout::new(shout));
self.send_server_updates();
} }
} }

View file

@ -0,0 +1,14 @@
use actix::Handler;
//
use crate::server::{actor::messages::ServerUpdate, ServerActor};
impl Handler<ServerUpdate> for ServerActor {
type Result = ();
fn handle(&mut self, msg: ServerUpdate, _ctx: &mut Self::Context) -> Self::Result {
self.subscribers.iter().for_each(move |subscriber| {
subscriber.do_send(msg.clone());
});
}
}

View file

@ -1,12 +1,13 @@
//
use actix::{Message, Recipient}; use actix::{Message, Recipient};
//-
use derive_more::Constructor; use derive_more::Constructor;
use git_next_core::{ use git_next_core::{
git::graph::Log, git::{self, forge::commit::Status, graph::Log, Commit},
message, message,
server::{AppConfig, Storage}, server::{AppConfig, Storage},
ForgeAlias, RepoAlias, RepoBranches, webhook::{push::Branch, Push},
ForgeAlias, RepoAlias, RepoBranches, RepoConfig,
}; };
use std::net::SocketAddr; use std::net::SocketAddr;
@ -40,20 +41,57 @@ message!(Shutdown, "Notification to shutdown the server actor");
#[derive(Clone, Debug, PartialEq, Eq, Message)] #[derive(Clone, Debug, PartialEq, Eq, Message)]
#[rtype(result = "()")] #[rtype(result = "()")]
pub enum ServerUpdate { pub enum ServerUpdate {
/// Status of a repo /// List of all configured forges and aliases
UpdateRepoSummary { AppConfigLoaded { app_config: ValidAppConfig },
RepoUpdate {
forge_alias: ForgeAlias, forge_alias: ForgeAlias,
repo_alias: RepoAlias, repo_alias: RepoAlias,
repo_update: RepoUpdate,
},
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum RepoUpdate {
Branches {
branches: RepoBranches, branches: RepoBranches,
},
Log {
log: Log, log: Log,
}, },
/// remove a repo ValidateRepo,
RemoveRepo { Okay {
forge_alias: ForgeAlias, main: Commit,
repo_alias: RepoAlias, next: Commit,
dev: Commit,
}, },
/// test message Alert {
Ping, alert: String,
},
CheckingCI,
AdvancingNext {
commit: Option<git::Commit>,
force: git::push::Force,
},
AdvancingMain {
commit: git::Commit,
},
Opening,
LoadingConfigFromRepo,
ReceiveCIStatus {
status: Status,
},
ReceiveRepoConfig {
repo_config: RepoConfig,
},
RegisteringWebhook,
UnregisteringWebhook,
WebhookReceived {
branch: Branch,
push: Push,
},
RegisteredWebhook,
Opened,
} }
message!( message!(

View file

@ -118,6 +118,7 @@ impl ServerActor {
server_storage: &Storage, server_storage: &Storage,
listen_url: &ListenUrl, listen_url: &ListenUrl,
notify_user_recipient: &Recipient<NotifyUser>, notify_user_recipient: &Recipient<NotifyUser>,
server_addr: Option<Addr<Self>>,
) -> Vec<(ForgeAlias, RepoAlias, RepoActor)> { ) -> Vec<(ForgeAlias, RepoAlias, RepoActor)> {
let span = let span =
tracing::info_span!("create_forge_repos", name = %forge_name, config = %forge_config); tracing::info_span!("create_forge_repos", name = %forge_name, config = %forge_config);
@ -125,8 +126,13 @@ impl ServerActor {
let _guard = span.enter(); let _guard = span.enter();
tracing::info!("Creating Forge"); tracing::info!("Creating Forge");
let mut repos = vec![]; let mut repos = vec![];
let creator = let creator = self.create_actor(
self.create_actor(forge_name, forge_config.clone(), server_storage, listen_url); forge_name,
forge_config.clone(),
server_storage,
listen_url,
server_addr,
);
for (repo_alias, server_repo_config) in forge_config.repos() { for (repo_alias, server_repo_config) in forge_config.repos() {
let forge_repo = creator(( let forge_repo = creator((
repo_alias, repo_alias,
@ -148,6 +154,7 @@ 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>>,
) -> impl Fn( ) -> impl Fn(
(RepoAlias, &ServerRepoConfig, Recipient<NotifyUser>), (RepoAlias, &ServerRepoConfig, Recipient<NotifyUser>),
) -> (ForgeAlias, RepoAlias, RepoActor) { ) -> (ForgeAlias, RepoAlias, RepoActor) {
@ -194,6 +201,7 @@ impl ServerActor {
repository_factory.duplicate(), repository_factory.duplicate(),
sleep_duration, sleep_duration,
Some(notify_user_recipient), Some(notify_user_recipient),
server_addr.clone(),
); );
(forge_name.clone(), repo_alias, actor) (forge_name.clone(), repo_alias, actor)
} }
@ -242,10 +250,4 @@ impl ServerActor {
ctx.address().do_send(msg); ctx.address().do_send(msg);
} }
} }
fn send_server_updates(&self) {
self.subscribers.iter().for_each(|subscriber| {
subscriber.do_send(ServerUpdate::Ping);
});
}
} }

View file

@ -17,7 +17,7 @@ pub use actor::ServerActor;
use git_next_core::git::RepositoryFactory; use git_next_core::git::RepositoryFactory;
use anyhow::{Context, Result}; use color_eyre::{eyre::Context, Result};
use kxio::{fs::FileSystem, network::Network}; use kxio::{fs::FileSystem, network::Network};
use tracing::info; use tracing::info;
@ -48,7 +48,12 @@ pub fn start(
repo: Box<dyn RepositoryFactory>, repo: Box<dyn RepositoryFactory>,
sleep_duration: std::time::Duration, sleep_duration: std::time::Duration,
) -> Result<()> { ) -> Result<()> {
if !ui { if ui {
#[cfg(feature = "tui")]
{
crate::tui::logging::initialize_logging()?;
}
} else {
init_logging(); init_logging();
} }
@ -59,7 +64,6 @@ pub fn start(
info!("Starting Server..."); info!("Starting Server...");
let server = let server =
ServerActor::new(fs.clone(), net.clone(), alerts_addr, repo, sleep_duration).start(); ServerActor::new(fs.clone(), net.clone(), alerts_addr, repo, sleep_duration).start();
server.do_send(FileUpdated);
info!("Starting File Watcher..."); info!("Starting File Watcher...");
#[allow(clippy::expect_used)] #[allow(clippy::expect_used)]
@ -75,18 +79,18 @@ pub fn start(
let (tx_shutdown, rx_shutdown) = channel::<()>(); let (tx_shutdown, rx_shutdown) = channel::<()>();
let tui_addr = tui::Tui::new(tx_shutdown).start(); let tui_addr = tui::Tui::new(tx_shutdown).start();
// tui_addr.do_send(tui::Tick);
let _ = tui_addr.send(tui::Tick).await;
server.do_send(SubscribeToUpdates::new(tui_addr.clone().recipient())); server.do_send(SubscribeToUpdates::new(tui_addr.clone().recipient()));
server.do_send(FileUpdated); // update file after ui subscription in place
loop { loop {
let _ = tui_addr.send(tui::Tick).await; let _ = tui_addr.send(tui::Tick).await;
if rx_shutdown.try_recv().is_ok() { if rx_shutdown.try_recv().is_ok() {
break; break;
} }
// actix_rt::task::yield_now().await; actix_rt::time::sleep(Duration::from_millis(16)).await;
} }
} }
} else { } else {
server.do_send(FileUpdated);
info!("Server running - Press Ctrl-C to stop..."); info!("Server running - Press Ctrl-C to stop...");
let _ = signal::ctrl_c().await; let _ = signal::ctrl_c().await;
info!("Ctrl-C received, shutting down..."); info!("Ctrl-C received, shutting down...");
@ -95,7 +99,7 @@ pub fn start(
// shutdown // shutdown
fw_shutdown.store(true, Ordering::Relaxed); fw_shutdown.store(true, Ordering::Relaxed);
server.do_send(crate::server::actor::messages::Shutdown); server.do_send(crate::server::actor::messages::Shutdown);
actix_rt::time::sleep(std::time::Duration::from_millis(200)).await; actix_rt::time::sleep(std::time::Duration::from_millis(10)).await;
System::current().stop(); System::current().stop();
}; };

View file

@ -0,0 +1,121 @@
# TUI Actor
- Maintains it's own copy of data for holding state
- Is notified of update via actor messages from the Server and Repo Actors
- State is somewhat heirarchical
## State
```rust
enum TuiState {
Initial,
Configured {
app_config: ValidAppConfig,
forges: BTreeMap<ForgeAlias, RepoState>,
},
}
enum RepoState {
Identified { repo_alias: RepoAlias },
Configured { repo_alias: RepoAlias, branches: RepoBranches },
Ready { repo_alias: RepoAlias, branches: RepoBranches, main: Commit, next: Commit, dev: Commit, log: Log },
Alert { repo_alias: RepoAlias, message: String, branches: RepoBranches, main: Commit, next: Commit, dev: Commit, log: Log },
}
```
## State Transitions:
### `TuiState`
```mermaid
stateDiagram-v2
* --> Initial
Initial --> Configured
```
- `message!(Configure, ValidAppConfig, "Initialise UI with valid config");`
### `RepoState`
```mermaid
stateDiagram-v2
* --> Identified
Identified --> Configured
Identified --> Ready
Configured --> Ready
Ready --> Alert
Configured --> Alert
Identified --> Alert
Alert --> Ready
```
- `Identified` - from AppConfig where a repo alias is listed, but repo config needs to be loaded from `.git-next.toml`
- `Configured` - as `Identified` but either branches are identified in server config, OR the `.git-next.toml` file has been loaded
- `Ready` - as `Configured` but the branch positions have been validated and do not require user intervention
- `Alert` - as `Ready` but user intervention is required
## Widget
Initial mock up of UI. Possibly add some borders and padding as it looks a little squached together.
```
+ gh
- jo
+ test (main/next/dev)
- tasyn (main/next/dev)
* 12ab32f (dev) added feature X
* bce43b1 (next) added feature Y
* f43e379 (main) added feature Z
- git-next (main/next/dev) DEV NOT AHEAD OF MAIN - rebase onto main
* 239cefd (main/next) fix bug A
* b4c290a (dev)
```
Adding a border around open forges:
```
+ gh
- jo --------------------------------------------------------------------+
| + test (main/next/dev) |
| - tasyn (main/next/dev) |
| * 12ab32f (dev) added feature X |
| * bce43b1 (next) added feature Y |
| * f43e379 (main) added feature Z |
| - git-next (main/next/dev) DEV NOT AHEAD OF MAIN - rebase onto main |
| * 239cefd (main/next) fix bug A |
| * b4c290a (dev) |
+------------------------------------------------------------------------+
```
Adding a border around open forges and repos (I like this one the best):
```
+ gh
- jo --------------------------------------------------------------------+
| + test (main/next/dev) |
| - tasyn (main/next/dev) ---------------------------------------------+ |
| | * 12ab32f (dev) added feature X | |
| | * bce43b1 (next) added feature Y | |
| | * f43e379 (main) added feature Z | |
| +--------------------------------------------------------------------+ |
| - git-next (main/next/dev) DEV NOT AHEAD OF MAIN - rebase onto main -+ |
| | * 239cefd (main/next) fix bug A | |
| | * b4c290a (dev) | |
| +--------------------------------------------------------------------+ |
+------------------------------------------------------------------------+
```
## Logging
- tui-logger to create an optional panel to show the normal server logs
## Branch Graph
- tui-nodes ?
## Scrolling
- tui-scrollview
## Tree View
- tui-tree-widget

View file

@ -1,27 +1,88 @@
use std::time::Instant; //
use actix::Handler; use actix::Handler;
use crate::{server::actor::messages::ServerUpdate, tui::Tui}; use crate::{
server::actor::messages::{RepoUpdate, ServerUpdate},
tui::{actor::ServerState, Tui},
};
//
impl Handler<ServerUpdate> for Tui { impl Handler<ServerUpdate> for Tui {
type Result = (); type Result = ();
fn handle(&mut self, msg: ServerUpdate, _ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: ServerUpdate, _ctx: &mut Self::Context) -> Self::Result {
self.state.tap();
match msg { match msg {
ServerUpdate::UpdateRepoSummary { ServerUpdate::AppConfigLoaded { app_config } => {
self.state.mode = ServerState::from(app_config);
}
ServerUpdate::RepoUpdate {
forge_alias, forge_alias,
repo_alias, repo_alias,
branches, repo_update,
log, } => {
} => todo!(), if let ServerState::Configured { forges } = &mut self.state.mode {
ServerUpdate::RemoveRepo { let Some(forge_state) = forges.get_mut(&forge_alias) else {
forge_alias, return;
repo_alias, };
} => todo!(), let Some(repo_state) = forge_state.repos.get_mut(&repo_alias) else {
ServerUpdate::Ping => { return;
self.last_ping = Instant::now(); };
match repo_update {
RepoUpdate::Branches { branches } => {
repo_state.update_branches(branches);
}
RepoUpdate::Log { log } => {
repo_state.update_log(log);
}
RepoUpdate::ValidateRepo => repo_state.update_message("polling..."),
RepoUpdate::Okay { main, next, dev } => {
repo_state.clear_alert();
repo_state.update_message("okay");
*repo_state = repo_state.clone().ready(main, next, dev);
}
RepoUpdate::Alert { alert } => {
repo_state.alert(alert);
}
RepoUpdate::CheckingCI => {
repo_state.update_message("Checking CI status");
}
RepoUpdate::AdvancingNext {
commit: _,
force: _,
} => (),
RepoUpdate::AdvancingMain { commit } => {
repo_state.update_message(format!("advancing main to {commit}"));
}
RepoUpdate::Opening => {
repo_state.update_message("opening...");
}
RepoUpdate::Opened => {
repo_state.update_message("opened");
}
RepoUpdate::LoadingConfigFromRepo => {
repo_state.update_message("loading config from repo...");
}
RepoUpdate::ReceiveCIStatus { status } => {
repo_state.update_message(format!("ci status: {status:?}"));
}
RepoUpdate::ReceiveRepoConfig { repo_config: _ } => {
repo_state.update_message("loaded config from repo");
}
RepoUpdate::RegisteringWebhook => {
repo_state.update_message("registering webhook...");
}
RepoUpdate::UnregisteringWebhook => {
repo_state.update_message("unregistering webhook...");
}
RepoUpdate::WebhookReceived { branch, push: _ } => {
repo_state.update_message(format!("webhook update: {branch:?}"));
}
RepoUpdate::RegisteredWebhook => {
repo_state.update_message("registered webhook");
}
}
}
} }
} }
} }

View file

@ -1,12 +1,5 @@
// //
use std::{borrow::BorrowMut, time::Instant}; use actix::Handler;
use actix::{ActorContext, Handler};
use ratatui::{
crossterm::event::{self, KeyCode, KeyEventKind},
style::Stylize as _,
widgets::Paragraph,
};
use crate::tui::actor::{messages::Tick, Tui}; use crate::tui::actor::{messages::Tick, Tui};
@ -14,44 +7,9 @@ impl Handler<Tick> for Tui {
type Result = std::io::Result<()>; type Result = std::io::Result<()>;
fn handle(&mut self, _msg: Tick, ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, _msg: Tick, ctx: &mut Self::Context) -> Self::Result {
if let Some(terminal) = self.terminal.borrow_mut() { self.state.tap();
terminal.draw(|frame| { self.draw()?;
let area = frame.area(); self.handle_input(ctx)?;
frame.render_widget(
Paragraph::new(format!(
"(press 'q' to quit) Ping:[{:?}] UI:[{:?}]",
self.last_ping,
Instant::now()
))
.white()
.on_blue(),
area,
);
})?;
} else {
eprintln!("No terminal setup");
}
if event::poll(std::time::Duration::from_millis(16))? {
if let event::Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Char('q') => {
// execute!(stderr(), LeaveAlternateScreen)?;
// disable_raw_mode()?;
ctx.stop();
if let Err(err) = self.signal_shutdown.send(()) {
tracing::error!(?err, "Failed to signal shutdown");
}
}
KeyCode::Esc => {
//
}
_ => (),
}
}
}
}
Ok(()) Ok(())
} }
} }

View file

@ -1,4 +1,4 @@
// //
use git_next_core::message; use git_next_core::message;
message!(Tick => std::io::Result<()>, "Start the TUI"); message!(Tick => std::io::Result<()>, "Update the TUI");

View file

@ -1,56 +1,33 @@
// //
mod handlers; mod handlers;
pub mod messages; pub mod messages;
mod model;
use std::{ use std::sync::mpsc::Sender;
io::{stderr, Stderr},
sync::mpsc::Sender,
time::Instant,
};
use actix::{Actor, Context}; use actix::{Actor, ActorContext as _, Context};
pub use model::*;
use ratatui::{ use ratatui::{
crossterm::{ crossterm::event::{self, KeyCode, KeyEventKind},
execute, DefaultTerminal,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
},
prelude::CrosstermBackend,
Terminal,
}; };
#[derive(Debug)] #[derive(Debug)]
pub struct Tui { pub struct Tui {
terminal: Option<Terminal<CrosstermBackend<Stderr>>>, terminal: Option<DefaultTerminal>,
signal_shutdown: Sender<()>, signal_shutdown: Sender<()>,
last_ping: Instant, pub state: State,
} }
impl Actor for Tui { impl Actor for Tui {
type Context = Context<Self>; type Context = Context<Self>;
fn started(&mut self, _ctx: &mut Self::Context) { fn started(&mut self, _ctx: &mut Self::Context) {
match init() { self.terminal.replace(ratatui::init());
Err(err) => {
eprintln!("Failed to enable raw mode: {err:?}");
}
Ok(terminal) => {
self.terminal.replace(terminal);
}
}
} }
fn stopped(&mut self, _ctx: &mut Self::Context) { fn stopped(&mut self, _ctx: &mut Self::Context) {
if let Err(err) = restore() { self.terminal.take();
match std::env::consts::OS { ratatui::restore();
"linux" | "macos" => {
eprintln!(
"Failed to restore terminal: Type `reset` to restore terminal: {err:?}"
);
}
"windows" => {
println!("Failed to restore terminal: Reopen a new terminal: {err:?}");
}
_ => println!("Failed to restore terminal: {err:?}"),
}
}
} }
} }
impl Tui { impl Tui {
@ -58,18 +35,46 @@ impl Tui {
Self { Self {
terminal: None, terminal: None,
signal_shutdown, signal_shutdown,
last_ping: Instant::now(), state: State::initial(),
} }
} }
}
fn init() -> std::io::Result<Terminal<CrosstermBackend<Stderr>>> { pub const fn state(&self) -> &State {
execute!(stderr(), EnterAlternateScreen)?; &self.state
enable_raw_mode()?; }
Terminal::new(CrosstermBackend::new(stderr()))
}
fn restore() -> std::io::Result<()> { fn draw(&mut self) -> std::io::Result<()> {
execute!(stderr(), LeaveAlternateScreen)?; let t = self.terminal.take();
disable_raw_mode() if let Some(mut terminal) = t {
terminal.draw(|frame| {
frame.render_widget(self.state(), frame.area());
})?;
self.terminal = Some(terminal);
} else {
eprintln!("No terminal setup");
}
Ok(())
}
fn handle_input(&self, ctx: &mut <Self as actix::Actor>::Context) -> std::io::Result<()> {
if event::poll(std::time::Duration::from_millis(16))? {
if let event::Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Char('q') => {
ctx.stop();
if let Err(err) = self.signal_shutdown.send(()) {
tracing::error!(?err, "Failed to signal shutdown");
}
}
KeyCode::Esc => {
//
}
_ => (),
}
}
}
}
Ok(())
}
} }

View file

@ -0,0 +1,373 @@
//
use ratatui::{
layout::Alignment,
prelude::{Buffer, Rect},
style::Stylize as _,
symbols::border,
text::Line,
widgets::{block::Title, Block, Paragraph, Widget},
};
use git_next_core::{
git::{self, graph::Log, Commit},
ForgeAlias, RepoAlias, RepoBranches,
};
use tracing::info;
use std::{collections::BTreeMap, fmt::Display, time::Instant};
use crate::{server::actor::messages::ValidAppConfig, tui::components::ConfiguredAppWidget};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct State {
last_update: Instant,
started: Instant,
pub mode: ServerState,
}
impl State {
pub fn initial() -> Self {
Self {
last_update: Instant::now(),
started: Instant::now(),
mode: ServerState::Initial { tick: 0 },
}
}
pub fn tap(&mut self) {
self.last_update = Instant::now();
if let ServerState::Initial { tick } = &mut self.mode {
*tick += 1;
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ServerState {
/// UI has started but has no information on the state of the server
Initial { tick: usize }, // NOTE: for use with throbber-widgets-tui ?
/// The application configuration has been loaded, individual forges and repos have their own
/// states
Configured {
forges: BTreeMap<ForgeAlias, ForgeState>,
},
}
impl ServerState {
pub fn update_branches(
&mut self,
forge_alias: &ForgeAlias,
repo_alias: &RepoAlias,
branches: RepoBranches,
) {
if let Self::Configured { forges } = self {
let Some(forge_state) = forges.get_mut(forge_alias) else {
return;
};
let Some(repo_state) = forge_state.repos.get_mut(repo_alias) else {
return;
};
match repo_state {
RepoState::Configured {
branches: state_branches,
..
}
| RepoState::Ready {
branches: state_branches,
..
} => *state_branches = branches,
RepoState::Identified { .. } => (),
}
}
}
pub fn update_log(&mut self, forge_alias: &ForgeAlias, repo_alias: &RepoAlias, log: Log) {
if let Self::Configured { forges } = self {
let Some(forge_state) = forges.get_mut(forge_alias) else {
return;
};
let Some(repo_state) = forge_state.repos.get_mut(repo_alias) else {
return;
};
match repo_state {
RepoState::Ready { log: state_log, .. } => *state_log = log,
RepoState::Identified { .. } | RepoState::Configured { .. } => (),
}
}
}
}
impl From<ValidAppConfig> for ServerState {
fn from(app_config: ValidAppConfig) -> Self {
Self::Configured {
forges: app_config
.app_config
.forges()
.map(|(forge_alias, config)| {
(
forge_alias,
config
.repos()
.map(|(repo_alias, server_repo_config)| {
(repo_alias, server_repo_config.repo_config())
})
.map(
|(repo_alias, option_repo_config)| match option_repo_config {
Some(rc) => (
repo_alias.clone(),
RepoState::Configured {
repo_alias,
message: "configured".into(),
messages: Vec::new(),
alert: None,
branches: rc.branches().clone(),
histories: None,
log: git::graph::Log::default(),
},
),
None => (
repo_alias.clone(),
RepoState::Identified {
repo_alias,
message: "identified".into(),
messages: Vec::new(),
alert: None,
},
),
},
)
.collect::<Vec<_>>(),
)
})
.map(|(forge_alias, vec_repo_alias_state)| {
let forge_state: ForgeState = ForgeState {
alias: forge_alias.clone(),
view_state: ViewState::default(),
repos: vec_repo_alias_state.into_iter().collect::<BTreeMap<_, _>>(),
};
(forge_alias, forge_state)
})
.collect::<BTreeMap<_, _>>(),
}
}
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub enum ViewState {
Collapsed,
#[default]
Expanded,
}
impl Display for ViewState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let view_state = match self {
Self::Collapsed => "+",
Self::Expanded => "-",
};
write!(f, "{view_state}")
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct ForgeState {
pub alias: ForgeAlias,
pub view_state: ViewState,
pub repos: BTreeMap<RepoAlias, RepoState>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum RepoState {
Identified {
repo_alias: RepoAlias,
message: String,
messages: Vec<String>,
alert: Option<String>,
},
Configured {
repo_alias: RepoAlias,
message: String,
messages: Vec<String>,
alert: Option<String>,
branches: RepoBranches,
histories: Option<git::commit::Histories>,
log: Log,
},
Ready {
repo_alias: RepoAlias,
message: String,
messages: Vec<String>,
alert: Option<String>,
branches: RepoBranches,
histories: Option<git::commit::Histories>,
view_state: ViewState,
main: Commit,
next: Commit,
dev: Commit,
log: Log,
},
}
impl RepoState {
#[tracing::instrument]
pub fn update_branches(&mut self, branches: RepoBranches) {
match self {
Self::Configured {
branches: state_branches,
..
}
| Self::Ready {
branches: state_branches,
..
} => {
*state_branches = branches;
}
Self::Identified { .. } => (),
}
}
#[tracing::instrument]
pub fn update_log(&mut self, log: Log) {
match self {
Self::Configured { log: state_log, .. } | Self::Ready { log: state_log, .. } => {
*state_log = log;
}
Self::Identified { .. } => {
info!("git graph log ignored by ui");
}
}
}
#[tracing::instrument]
pub fn update_message(&mut self, msg: impl Into<String> + std::fmt::Debug) {
tracing::info!("new tui message");
let msg: String = msg.into();
match self {
Self::Identified {
message, messages, ..
}
| Self::Configured {
message, messages, ..
}
| Self::Ready {
message, messages, ..
} => {
messages.push(msg.clone());
*message = msg;
}
}
}
#[tracing::instrument]
pub fn clear_alert(&mut self) {
match self {
Self::Identified { .. } | Self::Configured { .. } => (),
Self::Ready { alert, .. } => *alert = None,
}
}
#[tracing::instrument]
pub fn alert(&mut self, msg: impl Into<String> + std::fmt::Debug) {
let msg: String = msg.into();
tracing::info!(%msg, "new tui alert");
self.update_message("ALERT");
match self {
Self::Identified { alert, .. }
| Self::Configured { alert, .. }
| Self::Ready { alert, .. } => *alert = Some(msg),
}
}
pub fn ready(self, main: Commit, next: Commit, dev: Commit) -> Self {
match self {
Self::Identified {
repo_alias,
message,
messages,
alert,
} => Self::Identified {
repo_alias,
message,
messages,
alert,
},
Self::Configured {
repo_alias,
message,
messages,
alert,
branches,
histories,
log,
} => Self::Ready {
repo_alias,
message,
messages,
alert,
branches,
histories,
view_state: ViewState::Expanded,
main,
next,
dev,
log,
},
Self::Ready {
repo_alias,
message,
messages,
alert,
branches,
histories,
view_state,
log,
.. // drop existing main, next and dev to use parameters
} => Self::Ready {
repo_alias,
message,
messages,
alert,
branches,
histories,
view_state,
main,
next,
dev,
log,
},
}
}
}
impl Widget for &State {
fn render(self, area: Rect, buf: &mut Buffer)
where
Self: Sized,
{
let block = Block::bordered()
.title(Title::from(" Git-Next ".bold()).alignment(Alignment::Center))
.title(
Title::from(Line::from(vec![
" [q]uit ".into(),
format!(
"{}s ",
self.last_update.duration_since(self.started).as_secs()
)
.into(),
]))
.alignment(Alignment::Center)
.position(ratatui::widgets::block::Position::Bottom),
)
.border_set(border::THICK);
let interior = block.inner(area);
block.render(area, buf);
match &self.mode {
ServerState::Initial { tick } => Paragraph::new(format!("Loading...{tick}"))
.centered()
.render(interior, buf),
ServerState::Configured { forges } => {
ConfiguredAppWidget { forges }.render(interior, buf);
}
}
}
}

View file

@ -0,0 +1,52 @@
use std::collections::BTreeMap;
use git_next_core::ForgeAlias;
use ratatui::{
buffer::Buffer,
layout::{Constraint, Direction, Layout, Rect},
text::Line,
widgets::Widget,
};
use crate::tui::actor::{ForgeState, ViewState};
use super::forge::ForgeWidget;
pub struct ConfiguredAppWidget<'a> {
pub forges: &'a BTreeMap<ForgeAlias, ForgeState>,
}
impl<'a> Widget for ConfiguredAppWidget<'a> {
fn render(self, area: Rect, buf: &mut Buffer)
where
Self: Sized,
{
let layout_app = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![Constraint::Length(1), Constraint::Fill(1)])
.split(area);
Line::from(format!("Forges: ({})", self.forges.keys().len())).render(layout_app[0], buf);
let layout_forge_list = Layout::default()
.direction(Direction::Vertical)
.constraints(
self.forges
.iter()
.map(|(_alias, state)| match state.view_state {
ViewState::Collapsed => Constraint::Length(1),
ViewState::Expanded => Constraint::Fill(1),
}),
)
.split(layout_app[1]);
self.forges
.iter()
.map(|(forge_alias, state)| ForgeWidget {
forge_alias,
repos: &state.repos,
view_state: state.view_state,
})
.enumerate()
.for_each(|(i, w)| w.render(layout_forge_list[i], buf));
}
}

View file

@ -0,0 +1,15 @@
//
use git_next_core::ForgeAlias;
use ratatui::{buffer::Buffer, layout::Rect, text::Text, widgets::Widget};
pub struct CollapsedForgeWidget<'a> {
pub forge_alias: &'a ForgeAlias,
}
impl<'a> Widget for CollapsedForgeWidget<'a> {
fn render(self, area: Rect, buf: &mut Buffer)
where
Self: Sized,
{
Text::from(format!("- {}", self.forge_alias)).render(area, buf);
}
}

View file

@ -0,0 +1,40 @@
//
use std::collections::BTreeMap;
use git_next_core::{ForgeAlias, RepoAlias};
use ratatui::{
buffer::Buffer,
layout::{Alignment, Constraint, Direction, Layout, Rect},
widgets::{block::Title, Block, Borders, Widget},
};
use crate::tui::{actor::RepoState, components::repo::RepoWidget};
pub struct ExpandedForgeWidget<'a> {
pub forge_alias: &'a ForgeAlias,
pub repos: &'a BTreeMap<RepoAlias, RepoState>,
}
impl<'a> Widget for ExpandedForgeWidget<'a> {
fn render(self, area: Rect, buf: &mut Buffer)
where
Self: Sized,
{
let block = Block::default()
.title(Title::from(self.forge_alias.to_string()).alignment(Alignment::Center))
.borders(Borders::ALL);
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints(
self.repos
.iter()
.map(|(_alias, _state)| Constraint::Fill(1)),
)
.split(block.inner(area));
block.render(area, buf);
self.repos
.values()
.map(|repo_state| RepoWidget { repo_state })
.enumerate()
.for_each(|(i, w)| w.render(layout[i], buf));
}
}

View file

@ -0,0 +1,34 @@
//
mod collapsed;
mod expanded;
use std::collections::BTreeMap;
use git_next_core::{ForgeAlias, RepoAlias};
use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget};
use crate::tui::actor::{RepoState, ViewState};
pub struct ForgeWidget<'a> {
pub forge_alias: &'a ForgeAlias,
pub repos: &'a BTreeMap<RepoAlias, RepoState>,
pub view_state: ViewState,
}
impl<'a> Widget for ForgeWidget<'a> {
fn render(self, area: Rect, buf: &mut Buffer)
where
Self: Sized,
{
match self.view_state {
ViewState::Collapsed => collapsed::CollapsedForgeWidget {
forge_alias: self.forge_alias,
}
.render(area, buf),
ViewState::Expanded => expanded::ExpandedForgeWidget {
forge_alias: self.forge_alias,
repos: self.repos,
}
.render(area, buf),
}
}
}

View file

@ -0,0 +1,25 @@
use git_next_core::git::graph::Log;
//
use ratatui::{
text::{Line, Text},
widgets::{Paragraph, Widget},
};
pub struct CommitLog<'a> {
pub log: &'a Log,
}
impl<'a> Widget for CommitLog<'a> {
fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer)
where
Self: Sized,
{
Paragraph::new(Text::from(
self.log
.iter()
.map(ToString::to_string)
.map(Line::from)
.collect::<Vec<_>>(),
))
.render(area, buf);
}
}

View file

@ -0,0 +1,8 @@
//
mod configured_app;
mod forge;
mod history;
mod repo;
pub use configured_app::ConfiguredAppWidget;
pub use history::CommitLog;

View file

@ -0,0 +1,43 @@
//
use git_next_core::{git, RepoAlias, RepoBranches};
use ratatui::{
buffer::Buffer,
layout::{Constraint, Direction, Layout, Rect},
widgets::{Block, Borders, Widget},
};
use crate::tui::components::CommitLog;
use super::{identity::Identity, messages::Messages};
pub struct ConfiguredRepoWidget<'a> {
pub repo_alias: &'a RepoAlias,
pub message: &'a str,
pub messages: &'a Vec<String>,
pub alert: Option<&'a str>,
pub branches: &'a RepoBranches,
pub log: &'a git::graph::Log,
}
impl<'a> Widget for ConfiguredRepoWidget<'a> {
fn render(self, area: Rect, buf: &mut Buffer)
where
Self: Sized,
{
let block = Block::default()
.title(Identity::new(
self.repo_alias,
self.alert,
self.message,
Some(self.branches),
))
.borders(Borders::ALL);
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![Constraint::Fill(1), Constraint::Fill(1)])
.split(block.inner(area));
block.render(area, buf);
Messages::new(self.messages).render(layout[0], buf);
CommitLog { log: self.log }.render(layout[1], buf);
}
}

View file

@ -0,0 +1,34 @@
//
use git_next_core::RepoAlias;
use ratatui::{
buffer::Buffer,
layout::Rect,
widgets::{Block, Borders, Widget},
};
use super::{identity::Identity, messages::Messages};
pub struct IdentifiedRepoWidget<'a> {
pub repo_alias: &'a RepoAlias,
pub message: &'a str,
pub messages: &'a Vec<String>,
pub alert: Option<&'a str>,
}
impl<'a> Widget for IdentifiedRepoWidget<'a> {
fn render(self, area: Rect, buf: &mut Buffer)
where
Self: Sized,
{
let block = Block::default()
.title(Identity::new(
self.repo_alias,
self.alert,
self.message,
None,
))
.borders(Borders::ALL);
Messages::new(self.messages).render(block.inner(area), buf);
block.render(area, buf);
}
}

View file

@ -0,0 +1,61 @@
//
use std::fmt::Display;
use git_next_core::{RepoAlias, RepoBranches};
use ratatui::{
layout::Alignment,
text::Text,
widgets::{block::Title, Widget},
};
pub struct Identity<'a> {
pub repo_alias: &'a RepoAlias,
pub alert: Option<&'a str>,
pub message: &'a str,
pub repo_branches: Option<&'a RepoBranches>,
}
impl<'a> Identity<'a> {
pub const fn new(
repo_alias: &'a RepoAlias,
alert: Option<&'a str>,
message: &'a str,
repo_branches: Option<&'a RepoBranches>,
) -> Self {
Self {
repo_alias,
alert,
message,
repo_branches,
}
}
}
impl<'a> Widget for Identity<'a> {
fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer)
where
Self: Sized,
{
Text::from(self.to_string()).render(area, buf);
}
}
impl<'a> Display for Identity<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let repo_alias = self.repo_alias;
let alert = self.alert.unwrap_or("");
let message = self.message;
let text = self.repo_branches.map_or_else(
|| format!("{repo_alias} {alert} (_/_/_) [{message}]"),
|branches| {
let main = branches.main();
let next = branches.next();
let dev = branches.dev();
format!("{repo_alias} {alert} ({main}/{next}/{dev}) [{message}]")
},
);
f.write_str(text.as_str())
}
}
impl<'a> From<Identity<'a>> for Title<'a> {
fn from(identity: Identity<'a>) -> Self {
Self::from(identity.to_string()).alignment(Alignment::Left)
}
}

View file

@ -0,0 +1,26 @@
//
use ratatui::{
text::{Line, Text},
widgets::Widget,
};
pub struct Messages<'a>(&'a Vec<String>);
impl<'a> Messages<'a> {
pub const fn new(messages: &'a Vec<String>) -> Self {
Self(messages)
}
}
impl<'a> Widget for Messages<'a> {
fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer)
where
Self: Sized,
{
Text::from(
self.0
.iter()
.map(|m| Line::from(format!("- {m}")))
.collect::<Vec<_>>(),
)
.render(area, buf);
}
}

View file

@ -0,0 +1,86 @@
//
mod configured;
mod identified;
mod identity;
mod messages;
mod ready;
use std::string::String;
use configured::ConfiguredRepoWidget;
use identified::IdentifiedRepoWidget;
use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget};
use ready::ReadyRepoWidget;
use crate::tui::actor::RepoState;
pub struct RepoWidget<'a> {
pub repo_state: &'a RepoState,
}
impl<'a> Widget for RepoWidget<'a> {
fn render(self, area: Rect, buf: &mut Buffer)
where
Self: Sized,
{
match self.repo_state {
RepoState::Identified {
repo_alias,
message,
messages,
alert,
} => {
IdentifiedRepoWidget {
repo_alias,
message,
messages,
alert: alert.as_ref().map(String::as_str),
}
.render(area, buf);
}
RepoState::Configured {
repo_alias,
message,
messages,
alert,
branches,
log,
..
} => ConfiguredRepoWidget {
repo_alias,
message,
messages,
alert: alert.as_ref().map(String::as_str),
branches,
log,
}
.render(area, buf),
RepoState::Ready {
repo_alias,
message,
messages,
alert,
branches,
log,
// view_state,
// main,
// next,
// dev,
..
} => ReadyRepoWidget {
repo_alias,
message,
messages,
alert: alert.as_ref().map(String::as_str),
branches,
log,
// view_state,
// main,
// next,
// dev,
}
.render(area, buf),
};
}
}

View file

@ -0,0 +1,49 @@
//
use git_next_core::{RepoAlias, RepoBranches};
use ratatui::{
buffer::Buffer,
layout::{Constraint, Direction, Layout, Rect},
widgets::{Block, Borders, Widget},
};
use crate::{git, tui::components::CommitLog};
use super::{identity::Identity, messages::Messages};
pub struct ReadyRepoWidget<'a> {
pub repo_alias: &'a RepoAlias,
pub message: &'a str,
pub messages: &'a Vec<String>,
pub alert: Option<&'a str>,
pub branches: &'a RepoBranches,
pub log: &'a git::graph::Log,
// pub view_state: &'a actor::ViewState,
// pub main: &'a Commit,
// pub next: &'a Commit,
// pub dev: &'a Commit,
}
impl<'a> Widget for ReadyRepoWidget<'a> {
fn render(self, area: Rect, buf: &mut Buffer)
where
Self: Sized,
{
let block = Block::default()
.title(Identity::new(
self.repo_alias,
self.alert,
self.message,
Some(self.branches),
))
.borders(Borders::ALL);
let layout_repo = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![Constraint::Fill(1), Constraint::Fill(1)])
.split(block.inner(area));
block.render(area, buf);
Messages::new(self.messages).render(layout_repo[0], buf);
CommitLog { log: self.log }.render(layout_repo[1], buf);
}
}

View file

@ -0,0 +1,86 @@
//
use std::path::PathBuf;
use color_eyre::eyre::Result;
use directories::ProjectDirs;
use lazy_static::lazy_static;
use tracing_error::ErrorLayer;
use tracing_subscriber::{self, layer::SubscriberExt, util::SubscriberInitExt, Layer};
lazy_static! {
pub static ref PROJECT_NAME: String = env!("CARGO_CRATE_NAME").to_uppercase();
pub static ref DATA_FOLDER: Option<PathBuf> =
std::env::var(format!("{}_DATA", PROJECT_NAME.clone()))
.ok()
.map(PathBuf::from);
pub static ref LOG_ENV: String = format!("{}_LOGLEVEL", PROJECT_NAME.clone());
pub static ref LOG_FILE: String = format!("{}.log", env!("CARGO_PKG_NAME"));
}
fn project_directory() -> Option<ProjectDirs> {
ProjectDirs::from("net", "kemitix", env!("CARGO_PKG_NAME"))
}
pub fn get_data_dir() -> PathBuf {
let directory = DATA_FOLDER.clone().map_or_else(
|| {
project_directory().map_or_else(
|| PathBuf::from(".").join(".data"),
|proj_dirs| proj_dirs.data_local_dir().to_path_buf(),
)
},
|data_folder| data_folder,
);
directory
}
pub fn initialize_logging() -> Result<()> {
let directory = get_data_dir();
std::fs::create_dir_all(directory.clone())?;
let log_path = directory.join(LOG_FILE.clone());
let log_file = std::fs::File::create(log_path)?;
std::env::set_var(
"RUST_LOG",
std::env::var("RUST_LOG")
.or_else(|_| std::env::var(LOG_ENV.clone()))
.unwrap_or_else(|_| format!("{}=info", env!("CARGO_CRATE_NAME"))),
);
let file_subscriber = tracing_subscriber::fmt::layer()
.with_file(true)
.with_line_number(true)
.with_writer(log_file)
.with_target(false)
.with_ansi(false)
.with_filter(tracing_subscriber::filter::EnvFilter::from_default_env());
tracing_subscriber::registry()
.with(file_subscriber)
.with(ErrorLayer::default())
.init();
Ok(())
}
/// Similar to the `std::dbg!` macro, but generates `tracing` events rather
/// than printing to stdout.
///
/// By default, the verbosity level for the generated events is `DEBUG`, but
/// this can be customized.
#[macro_export]
macro_rules! trace_dbg {
(target: $target:expr, level: $level:expr, $ex:expr) => {{
match $ex {
value => {
tracing::event!(target: $target, $level, ?value, stringify!($ex));
value
}
}
}};
(level: $level:expr, $ex:expr) => {
trace_dbg!(target: module_path!(), level: $level, $ex)
};
(target: $target:expr, $ex:expr) => {
trace_dbg!(target: $target, level: tracing::Level::DEBUG, $ex)
};
($ex:expr) => {
trace_dbg!(level: tracing::Level::DEBUG, $ex)
};
}

View file

@ -1,5 +1,7 @@
// //
mod actor; mod actor;
pub mod components;
pub mod logging;
pub use actor::messages::Tick; pub use actor::messages::Tick;
pub use actor::Tui; pub use actor::Tui;

View file

@ -47,7 +47,8 @@ impl ServerRepoConfig {
} }
/// Returns a `RepoConfig` from the server configuration if ALL THREE branches were provided /// Returns a `RepoConfig` from the server configuration if ALL THREE branches were provided
pub(crate) fn repo_config(&self) -> Option<RepoConfig> { #[must_use]
pub fn repo_config(&self) -> Option<RepoConfig> {
match (&self.main, &self.next, &self.dev) { match (&self.main, &self.next, &self.dev) {
(Some(main), Some(next), Some(dev)) => Some(RepoConfig::new( (Some(main), Some(next), Some(dev)) => Some(RepoConfig::new(
RepoBranches::new(main.to_string(), next.to_string(), dev.to_string()), RepoBranches::new(main.to_string(), next.to_string(), dev.to_string()),

View file

@ -2,7 +2,7 @@
use crate::config::{BranchName, RepoBranches}; use crate::config::{BranchName, RepoBranches};
use derive_more::Constructor; use derive_more::Constructor;
#[derive(Debug, Constructor, derive_with::With)] #[derive(Clone, Debug, Constructor, PartialEq, Eq, derive_with::With)]
pub struct Push { pub struct Push {
branch: BranchName, branch: BranchName,
sha: String, sha: String,
@ -34,7 +34,7 @@ impl Push {
} }
} }
#[derive(Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub enum Branch { pub enum Branch {
Main, Main,
Next, Next,

View file

@ -62,7 +62,7 @@ newtype!(
"The commit message for a git commit." "The commit message for a git commit."
); );
#[derive(Clone, Debug)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct Histories { pub struct Histories {
pub main: Vec<Commit>, pub main: Vec<Commit>,
pub next: Vec<Commit>, pub next: Vec<Commit>,

View file

@ -1,5 +1,4 @@
// //
use std::borrow::ToOwned; use std::borrow::ToOwned;
use take_until::TakeUntilExt; use take_until::TakeUntilExt;

View file

@ -1,7 +1,7 @@
// //
use crate::{git, git::repository::open::OpenRepositoryLike, BranchName}; use crate::{git, git::repository::open::OpenRepositoryLike, BranchName};
#[derive(Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub enum Force { pub enum Force {
No, No,
From(git::GitRef), From(git::GitRef),

View file

@ -94,7 +94,8 @@ pub trait OpenRepositoryLike: std::fmt::Debug + Sync {
/// ///
/// # Errors /// # Errors
/// ///
/// Will return `Err` if there are any network connectivity issues with the remote server. /// Will return `Err` if there are any problems with the branch name being invalid, or any
/// corruption of the git repository.
fn commit_log( fn commit_log(
&self, &self,
branch_name: &BranchName, branch_name: &BranchName,

View file

@ -1,3 +1,5 @@
use std::fmt::Display;
// //
use crate::{git::Commit, BranchName, ForgeAlias, RepoAlias}; use crate::{git::Commit, BranchName, ForgeAlias, RepoAlias};
use serde_json::json; use serde_json::json;
@ -115,3 +117,18 @@ impl UserNotification {
} }
} }
} }
impl Display for UserNotification {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let message = match self {
Self::CICheckFailed { commit, .. } => format!("CI Check Failed [{commit}]"),
Self::RepoConfigLoadFailure { reason, .. } => {
format!("Failed to load repo config: {reason}")
}
Self::WebhookRegistration { reason, .. } => {
format!("Failed to register webhook: {reason}")
}
Self::DevNotBasedOnMain { .. } => "Dev not based on main".to_string(),
};
write!(f, "{message}")
}
}

View file

@ -1,6 +1,8 @@
use tracing::{debug, instrument};
// //
use crate::{ use crate::{
git::{self, graph::log, repository::open::OpenRepositoryLike, RepoDetails, UserNotification}, git::{self, repository::open::OpenRepositoryLike, RepoDetails, UserNotification},
BranchName, RepoConfig, BranchName, RepoConfig,
}; };
@ -27,6 +29,7 @@ pub fn validate(
open_repository: &dyn OpenRepositoryLike, open_repository: &dyn OpenRepositoryLike,
repo_details: &git::RepoDetails, repo_details: &git::RepoDetails,
repo_config: &RepoConfig, repo_config: &RepoConfig,
log: git::graph::Log,
) -> Result<Positions> { ) -> Result<Positions> {
let main_branch = repo_config.branches().main(); let main_branch = repo_config.branches().main();
let next_branch = repo_config.branches().next(); let next_branch = repo_config.branches().next();
@ -61,7 +64,7 @@ pub fn validate(
main_branch, main_branch,
dev_commit: dev, dev_commit: dev,
main_commit: main, main_commit: main,
log: log(repo_details), log,
}, },
)); ));
} }
@ -129,13 +132,23 @@ fn is_based_on(commits: &[git::commit::Commit], needle: &git::Commit) -> bool {
commits.iter().any(|commit| commit == needle) commits.iter().any(|commit| commit == needle)
} }
fn get_commit_histories( /// Returns the commit logs for the main, next and dev branches
///
/// # Errors
///
/// Will return `Err` if there are any problems with the branch names being invalid, or any
/// corruption of the git repository.
#[instrument]
pub fn get_commit_histories(
open_repository: &dyn OpenRepositoryLike, open_repository: &dyn OpenRepositoryLike,
repo_config: &RepoConfig, repo_config: &RepoConfig,
) -> git::commit::log::Result<git::commit::Histories> { ) -> git::commit::log::Result<git::commit::Histories> {
debug!("main...");
let main = (open_repository.commit_log(&repo_config.branches().main(), &[]))?; let main = (open_repository.commit_log(&repo_config.branches().main(), &[]))?;
let main_head = [main[0].clone()]; let main_head = [main[0].clone()];
debug!("next");
let next = open_repository.commit_log(&repo_config.branches().next(), &main_head)?; let next = open_repository.commit_log(&repo_config.branches().next(), &main_head)?;
debug!("dev");
let dev = open_repository.commit_log(&repo_config.branches().dev(), &main_head)?; let dev = open_repository.commit_log(&repo_config.branches().dev(), &main_head)?;
let histories = git::commit::Histories { main, next, dev }; let histories = git::commit::Histories { main, next, dev };
Ok(histories) Ok(histories)

View file

@ -75,7 +75,12 @@ mod positions {
); );
let repo_config = given::a_repo_config(); let repo_config = given::a_repo_config();
let result = validate(&*repository, &repo_details, &repo_config); let result = validate(
&*repository,
&repo_details,
&repo_config,
git::graph::Log::default(),
);
println!("{result:?}"); println!("{result:?}");
let_assert!(Err(err) = result, "validate"); let_assert!(Err(err) = result, "validate");
@ -115,7 +120,12 @@ mod positions {
"open repo" "open repo"
); );
let result = validate(&*open_repository, &repo_details, &repo_config); let result = validate(
&*open_repository,
&repo_details,
&repo_config,
git::graph::Log::default(),
);
println!("{result:?}"); println!("{result:?}");
assert!(matches!( assert!(matches!(
@ -154,7 +164,12 @@ mod positions {
"open repo" "open repo"
); );
let result = validate(&*open_repository, &repo_details, &repo_config); let result = validate(
&*open_repository,
&repo_details,
&repo_config,
git::graph::Log::default(),
);
println!("{result:?}"); println!("{result:?}");
assert!(matches!( assert!(matches!(
@ -193,7 +208,12 @@ mod positions {
"open repo" "open repo"
); );
let result = validate(&*open_repository, &repo_details, &repo_config); let result = validate(
&*open_repository,
&repo_details,
&repo_config,
git::graph::Log::default(),
);
println!("{result:?}"); println!("{result:?}");
assert!(matches!( assert!(matches!(
@ -240,7 +260,12 @@ mod positions {
//when //when
let_assert!( let_assert!(
Err(err) = validate(&*open_repository, &repo_details, &repo_config), Err(err) = validate(
&*open_repository,
&repo_details,
&repo_config,
git::graph::Log::default()
),
"validate" "validate"
); );
@ -325,7 +350,12 @@ mod positions {
//when //when
let_assert!( let_assert!(
Err(err) = validate(&*open_repository, &repo_details, &repo_config), Err(err) = validate(
&*open_repository,
&repo_details,
&repo_config,
git::graph::Log::default()
),
"validate" "validate"
); );
@ -394,7 +424,12 @@ mod positions {
//when //when
let_assert!( let_assert!(
Err(err) = validate(&*open_repository, &repo_details, &repo_config), Err(err) = validate(
&*open_repository,
&repo_details,
&repo_config,
git::graph::Log::default()
),
"validate" "validate"
); );
@ -481,7 +516,12 @@ mod positions {
//when //when
let_assert!( let_assert!(
Err(err) = validate(&*open_repository, &repo_details, &repo_config), Err(err) = validate(
&*open_repository,
&repo_details,
&repo_config,
git::graph::Log::default()
),
"validate" "validate"
); );
@ -567,7 +607,12 @@ mod positions {
//when //when
let_assert!( let_assert!(
Ok(positions) = validate(&*open_repository, &repo_details, &repo_config), Ok(positions) = validate(
&*open_repository,
&repo_details,
&repo_config,
git::graph::Log::default()
),
"validate" "validate"
); );
@ -626,7 +671,12 @@ mod positions {
//when //when
let_assert!( let_assert!(
Ok(positions) = validate(&*open_repository, &repo_details, &repo_config), Ok(positions) = validate(
&*open_repository,
&repo_details,
&repo_config,
git::graph::Log::default()
),
"validate" "validate"
); );