feat(tui): (experimental) show repo state, messages and git log
Some checks failed
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Rust / build (push) Failing after 4m16s

This commit is contained in:
Paul Campbell 2024-08-12 21:25:24 +01:00 committed by Paul Campbell
parent f504b62ff6
commit e348b79e7b
56 changed files with 2280 additions and 638 deletions

1004
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,10 +3,13 @@ use actix::prelude::*;
use tracing::{debug, Instrument as _};
use crate::repo::{
do_send,
messages::{CheckCIStatus, ReceiveCIStatus},
RepoActor,
use crate::{
repo::{
do_send,
messages::{CheckCIStatus, ReceiveCIStatus},
RepoActor,
},
server::actor::messages::RepoUpdate,
};
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 {
crate::repo::logger(self.log.as_ref(), "start: CheckCIStatus");
let addr = ctx.address();
let forge = self.forge.duplicate();
let next = msg.unwrap();
let log = self.log.clone();
self.update_tui(RepoUpdate::CheckingCI);
// get the status - pass, fail, pending (all others map to fail, e.g. error)
async move {
let status = forge.commit_status(&next).await;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,16 +2,20 @@
use actix::prelude::*;
use tracing::instrument;
use crate::repo::{
do_send,
messages::{ValidateRepo, WebhookRegistered},
RepoActor,
use crate::{
repo::{
do_send,
messages::{ValidateRepo, WebhookRegistered},
RepoActor,
},
server::actor::messages::RepoUpdate,
};
impl Handler<WebhookRegistered> for RepoActor {
type Result = ();
#[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 {
self.update_tui(RepoUpdate::RegisteredWebhook);
self.webhook_id.replace(msg.webhook_id().clone());
self.webhook_auth.replace(msg.webhook_auth().clone());
do_send(

View file

@ -1,16 +1,23 @@
//
use actix::prelude::*;
use crate::alerts::messages::NotifyUser;
use crate::{
alerts::messages::NotifyUser,
server::{
actor::messages::{RepoUpdate, ServerUpdate},
ServerActor,
},
};
use derive_more::Deref;
use kxio::network::Network;
use std::time::Duration;
use tracing::{info, warn, Instrument};
use tracing::{info, instrument, warn, Instrument};
use git_next_core::{
git::{
self,
repository::{factory::RepositoryFactory, open::OpenRepositoryLike},
validation::positions::get_commit_histories,
UserNotification,
},
server::ListenUrl,
@ -59,6 +66,7 @@ pub struct RepoActor {
forge: Box<dyn git::ForgeLike>,
log: Option<ActorLog>,
notify_user_recipient: Option<Recipient<NotifyUser>>,
server_addr: Option<Addr<ServerActor>>,
}
impl RepoActor {
#[allow(clippy::too_many_arguments)]
@ -71,6 +79,7 @@ impl RepoActor {
repository_factory: Box<dyn RepositoryFactory>,
sleep_duration: std::time::Duration,
notify_user_recipient: Option<Recipient<NotifyUser>>,
server_addr: Option<Addr<ServerActor>>,
) -> Self {
let message_token = messages::MessageToken::default();
Self {
@ -90,6 +99,62 @@ impl RepoActor {
sleep_duration,
log: None,
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 Some(open_repo) = self.open_repository.as_ref() else {
return;
};
let branches = repo_config.branches().clone();
let histories = get_commit_histories(&**open_repo, repo_config).ok();
self.update_tui(RepoUpdate::Branches {
branches,
histories,
});
}
}
#[instrument]
fn update_tui_log(&self, log: git::graph::Log) {
info!("ready to send log");
#[cfg(feature = "tui")]
{
info!("sending...");
self.update_tui(RepoUpdate::Log { log });
info!("sent");
}
}
fn alert_tui(&self, alert: impl Into<String>) {
#[cfg(feature = "tui")]
{
self.update_tui(RepoUpdate::Alert {
alert: alert.into(),
});
}
}
fn update_tui(&self, repo_update: RepoUpdate) {
#[cfg(feature = "tui")]
{
let Some(server_addr) = &self.server_addr else {
return;
};
let update = ServerUpdate::RepoUpdate {
forge_alias: self.repo_details.forge.forge_alias().clone(),
repo_alias: self.repo_details.repo_alias.clone(),
repo_update,
};
server_addr.do_send(update);
}
}
}

View file

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

View file

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

View file

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

View file

@ -8,7 +8,7 @@ use crate::{
alerts::messages::UpdateShout,
repo::{messages::CloneRepo, RepoActor},
server::actor::{
messages::{ReceiveValidAppConfig, ValidAppConfig},
messages::{ReceiveValidAppConfig, ServerUpdate, ValidAppConfig},
ServerActor,
},
webhook::{
@ -21,7 +21,7 @@ use crate::{
impl Handler<ReceiveValidAppConfig> for ServerActor {
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 {
app_config,
socket_address,
@ -37,6 +37,7 @@ impl Handler<ReceiveValidAppConfig> for ServerActor {
let webhook_router = WebhookRouterActor::default().start();
let listen_url = app_config.listen().url();
let notify_user_recipient = self.alerts.clone().recipient();
let server_addr = Some(ctx.address());
// Forge Actors
for (forge_alias, forge_config) in app_config.forges() {
let repo_actors = self
@ -46,6 +47,7 @@ impl Handler<ReceiveValidAppConfig> for ServerActor {
&server_storage,
listen_url,
&notify_user_recipient,
server_addr.clone(),
)
.into_iter()
.map(start_repo_actor)
@ -69,9 +71,18 @@ impl Handler<ReceiveValidAppConfig> for ServerActor {
WebhookActor::new(socket_address, webhook_router.recipient()).start();
self.webhook_actor_addr.replace(webhook_actor_addr);
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.send_server_updates();
}
}

View file

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

View file

@ -1,12 +1,13 @@
//
use actix::{Message, Recipient};
//-
use derive_more::Constructor;
use git_next_core::{
git::graph::Log,
git::{self, forge::commit::Status, graph::Log, Commit},
message,
server::{AppConfig, Storage},
ForgeAlias, RepoAlias, RepoBranches,
webhook::{push::Branch, Push},
ForgeAlias, RepoAlias, RepoBranches, RepoConfig,
};
use std::net::SocketAddr;
@ -40,20 +41,58 @@ message!(Shutdown, "Notification to shutdown the server actor");
#[derive(Clone, Debug, PartialEq, Eq, Message)]
#[rtype(result = "()")]
pub enum ServerUpdate {
/// Status of a repo
UpdateRepoSummary {
/// List of all configured forges and aliases
AppConfigLoaded { app_config: ValidAppConfig },
RepoUpdate {
forge_alias: ForgeAlias,
repo_alias: RepoAlias,
repo_update: RepoUpdate,
},
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum RepoUpdate {
Branches {
branches: RepoBranches,
histories: Option<git::commit::Histories>,
},
Log {
log: Log,
},
/// remove a repo
RemoveRepo {
forge_alias: ForgeAlias,
repo_alias: RepoAlias,
ValidateRepo,
Okay {
main: Commit,
next: Commit,
dev: Commit,
},
/// test message
Ping,
Alert {
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!(

View file

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

View file

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

View file

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

View file

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

View file

@ -1,12 +1,5 @@
//
use std::{borrow::BorrowMut, time::Instant};
use actix::{ActorContext, Handler};
use ratatui::{
crossterm::event::{self, KeyCode, KeyEventKind},
style::Stylize as _,
widgets::Paragraph,
};
use actix::Handler;
use crate::tui::actor::{messages::Tick, Tui};
@ -14,44 +7,9 @@ impl Handler<Tick> for Tui {
type Result = std::io::Result<()>;
fn handle(&mut self, _msg: Tick, ctx: &mut Self::Context) -> Self::Result {
if let Some(terminal) = self.terminal.borrow_mut() {
terminal.draw(|frame| {
let area = frame.area();
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 => {
//
}
_ => (),
}
}
}
}
self.state.tap();
self.draw()?;
self.handle_input(ctx)?;
Ok(())
}
}

View file

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

View file

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

View file

@ -0,0 +1,380 @@
//
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,
histories: Option<git::commit::Histories>,
) {
match self {
Self::Configured {
branches: state_branches,
histories: state_histories,
..
}
| Self::Ready {
branches: state_branches,
histories: state_histories,
..
} => {
*state_branches = branches;
*state_histories = histories;
}
Self::Identified { .. } => (),
}
}
#[tracing::instrument]
pub fn update_log(&mut self, log: Log) {
match self {
Self::Configured { log: state_log, .. } | Self::Ready { log: state_log, .. } => {
*state_log = log;
}
Self::Identified { .. } => {
info!("git graph log ignored by ui");
}
}
}
#[tracing::instrument]
pub fn update_message(&mut self, msg: impl Into<String> + std::fmt::Debug) {
tracing::info!("new tui message");
let msg: String = msg.into();
match self {
Self::Identified {
message, messages, ..
}
| Self::Configured {
message, messages, ..
}
| Self::Ready {
message, messages, ..
} => {
messages.push(msg.clone());
*message = msg;
}
}
}
#[tracing::instrument]
pub fn clear_alert(&mut self) {
match self {
Self::Identified { .. } | Self::Configured { .. } => (),
Self::Ready { alert, .. } => *alert = None,
}
}
#[tracing::instrument]
pub fn alert(&mut self, msg: impl Into<String> + std::fmt::Debug) {
let msg: String = msg.into();
tracing::info!(%msg, "new tui alert");
self.update_message("ALERT");
match self {
Self::Identified { alert, .. }
| Self::Configured { alert, .. }
| Self::Ready { alert, .. } => *alert = Some(msg),
}
}
pub fn ready(self, main: Commit, next: Commit, dev: Commit) -> Self {
match self {
Self::Identified {
repo_alias,
message,
messages,
alert,
} => Self::Identified {
repo_alias,
message,
messages,
alert,
},
Self::Configured {
repo_alias,
message,
messages,
alert,
branches,
histories,
log,
} => Self::Ready {
repo_alias,
message,
messages,
alert,
branches,
histories,
view_state: ViewState::Expanded,
main,
next,
dev,
log,
},
Self::Ready {
repo_alias,
message,
messages,
alert,
branches,
histories,
view_state,
log,
.. // drop existing main, next and dev to use parameters
} => Self::Ready {
repo_alias,
message,
messages,
alert,
branches,
histories,
view_state,
main,
next,
dev,
log,
},
}
}
}
impl Widget for &State {
fn render(self, area: Rect, buf: &mut Buffer)
where
Self: Sized,
{
let block = Block::bordered()
.title(Title::from(" Git-Next ".bold()).alignment(Alignment::Center))
.title(
Title::from(Line::from(vec![
" [q]uit ".into(),
format!(
"{}s ",
self.last_update.duration_since(self.started).as_secs()
)
.into(),
]))
.alignment(Alignment::Center)
.position(ratatui::widgets::block::Position::Bottom),
)
.border_set(border::THICK);
let interior = block.inner(area);
block.render(area, buf);
match &self.mode {
ServerState::Initial { tick } => Paragraph::new(format!("Loading...{tick}"))
.centered()
.render(interior, buf),
ServerState::Configured { forges } => {
ConfiguredAppWidget { forges }.render(interior, buf);
}
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -94,7 +94,8 @@ pub trait OpenRepositoryLike: std::fmt::Debug + Sync {
///
/// # 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(
&self,
branch_name: &BranchName,

View file

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

View file

@ -1,6 +1,6 @@
//
use crate::{
git::{self, graph::log, repository::open::OpenRepositoryLike, RepoDetails, UserNotification},
git::{self, repository::open::OpenRepositoryLike, RepoDetails, UserNotification},
BranchName, RepoConfig,
};
@ -27,6 +27,7 @@ pub fn validate(
open_repository: &dyn OpenRepositoryLike,
repo_details: &git::RepoDetails,
repo_config: &RepoConfig,
log: git::graph::Log,
) -> Result<Positions> {
let main_branch = repo_config.branches().main();
let next_branch = repo_config.branches().next();
@ -61,7 +62,7 @@ pub fn validate(
main_branch,
dev_commit: dev,
main_commit: main,
log: log(repo_details),
log,
},
));
}
@ -129,7 +130,13 @@ fn is_based_on(commits: &[git::commit::Commit], needle: &git::Commit) -> bool {
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.
pub fn get_commit_histories(
open_repository: &dyn OpenRepositoryLike,
repo_config: &RepoConfig,
) -> git::commit::log::Result<git::commit::Histories> {

View file

@ -75,7 +75,12 @@ mod positions {
);
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:?}");
let_assert!(Err(err) = result, "validate");
@ -115,7 +120,12 @@ mod positions {
"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:?}");
assert!(matches!(
@ -154,7 +164,12 @@ mod positions {
"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:?}");
assert!(matches!(
@ -193,7 +208,12 @@ mod positions {
"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:?}");
assert!(matches!(
@ -240,7 +260,12 @@ mod positions {
//when
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"
);
@ -325,7 +350,12 @@ mod positions {
//when
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"
);
@ -394,7 +424,12 @@ mod positions {
//when
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"
);
@ -481,7 +516,12 @@ mod positions {
//when
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"
);
@ -567,7 +607,12 @@ mod positions {
//when
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"
);
@ -626,7 +671,12 @@ mod positions {
//when
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"
);