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
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:
parent
f504b62ff6
commit
5d9915bdbd
57 changed files with 2269 additions and 644 deletions
1004
Cargo.lock
generated
1004
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -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"
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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<()> {
|
||||||
|
|
|
@ -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)]
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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}");
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
@ -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(),
|
||||||
);
|
);
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}"));
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
¬ify_user_recipient,
|
¬ify_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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
14
crates/cli/src/server/actor/handlers/server_update.rs
Normal file
14
crates/cli/src/server/actor/handlers/server_update.rs
Normal 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());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -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!(
|
||||||
|
|
|
@ -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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
121
crates/cli/src/tui/README.md
Normal file
121
crates/cli/src/tui/README.md
Normal 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
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
373
crates/cli/src/tui/actor/model.rs
Normal file
373
crates/cli/src/tui/actor/model.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
52
crates/cli/src/tui/components/configured_app.rs
Normal file
52
crates/cli/src/tui/components/configured_app.rs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
15
crates/cli/src/tui/components/forge/collapsed.rs
Normal file
15
crates/cli/src/tui/components/forge/collapsed.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
40
crates/cli/src/tui/components/forge/expanded.rs
Normal file
40
crates/cli/src/tui/components/forge/expanded.rs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
34
crates/cli/src/tui/components/forge/mod.rs
Normal file
34
crates/cli/src/tui/components/forge/mod.rs
Normal 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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
25
crates/cli/src/tui/components/history.rs
Normal file
25
crates/cli/src/tui/components/history.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
8
crates/cli/src/tui/components/mod.rs
Normal file
8
crates/cli/src/tui/components/mod.rs
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
//
|
||||||
|
mod configured_app;
|
||||||
|
mod forge;
|
||||||
|
mod history;
|
||||||
|
mod repo;
|
||||||
|
|
||||||
|
pub use configured_app::ConfiguredAppWidget;
|
||||||
|
pub use history::CommitLog;
|
43
crates/cli/src/tui/components/repo/configured.rs
Normal file
43
crates/cli/src/tui/components/repo/configured.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
34
crates/cli/src/tui/components/repo/identified.rs
Normal file
34
crates/cli/src/tui/components/repo/identified.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
61
crates/cli/src/tui/components/repo/identity.rs
Normal file
61
crates/cli/src/tui/components/repo/identity.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
26
crates/cli/src/tui/components/repo/messages.rs
Normal file
26
crates/cli/src/tui/components/repo/messages.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
86
crates/cli/src/tui/components/repo/mod.rs
Normal file
86
crates/cli/src/tui/components/repo/mod.rs
Normal 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),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
49
crates/cli/src/tui/components/repo/ready.rs
Normal file
49
crates/cli/src/tui/components/repo/ready.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
86
crates/cli/src/tui/logging.rs
Normal file
86
crates/cli/src/tui/logging.rs
Normal 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)
|
||||||
|
};
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
|
@ -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()),
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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>,
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
use std::borrow::ToOwned;
|
use std::borrow::ToOwned;
|
||||||
|
|
||||||
use take_until::TakeUntilExt;
|
use take_until::TakeUntilExt;
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue