use std::io::{self, Write};
use std::time::Duration;
use console::Style;
use indicatif::{ProgressBar, ProgressStyle};
use crate::channel::{NotesRef, ReviewChannel};
use crate::error::AppError;
use crate::remote::RemoteName;
use crate::ui::interactive_stderr;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum SyncStep {
CheckRemote,
CheckLocal,
Fetch,
Merge,
Push,
Cleanup,
}
impl SyncStep {
const fn message(self) -> &'static str {
match self {
Self::CheckRemote => "Checking remote review notes",
Self::CheckLocal => "Checking local review notes",
Self::Fetch => "Fetching review notes",
Self::Merge => "Merging review notes",
Self::Push => "Pushing review notes",
Self::Cleanup => "Cleaning up temporary notes ref",
}
}
const fn gerund(self) -> &'static str {
match self {
Self::CheckRemote => "checking remote review notes",
Self::CheckLocal => "checking local review notes",
Self::Fetch => "fetching review notes",
Self::Merge => "merging review notes",
Self::Push => "pushing review notes",
Self::Cleanup => "cleaning up temporary notes ref",
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum SyncOutcome {
FetchedMergedPushed,
PushedLocalOnly,
NothingToSync,
}
impl SyncOutcome {
const fn details(self) -> &'static str {
match self {
Self::FetchedMergedPushed => "fetched, merged, and pushed",
Self::PushedLocalOnly => "pushed local notes; remote ref did not exist",
Self::NothingToSync => "nothing to sync",
}
}
const fn summary_verb(self) -> &'static str {
match self {
Self::FetchedMergedPushed => "Synced",
Self::PushedLocalOnly => "Pushed",
Self::NothingToSync => "No",
}
}
}
#[derive(Debug)]
pub(crate) struct SyncContext<'a> {
pub channel: &'a ReviewChannel,
pub remote: &'a RemoteName,
pub notes_ref: &'a NotesRef,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct SyncReport {
pub channel: ReviewChannel,
pub remote: RemoteName,
pub notes_ref: NotesRef,
pub outcome: SyncOutcome,
}
pub(crate) trait SyncProgress {
fn started(&mut self, context: &SyncContext<'_>) -> Result<(), AppError>;
fn step_started(&mut self, step: SyncStep) -> Result<(), AppError>;
fn step_finished(&mut self, step: SyncStep) -> Result<(), AppError>;
fn step_failed(&mut self, step: SyncStep) -> Result<(), AppError>;
fn finished(&mut self, report: &SyncReport) -> Result<(), AppError>;
}
pub(crate) enum SyncProgressReporter {
Spinner(SpinnerSyncProgress),
Plain(PlainSyncProgress),
}
impl SyncProgressReporter {
pub(crate) fn from_environment(color: bool) -> Self {
if interactive_stderr() {
Self::Spinner(SpinnerSyncProgress::new(color))
} else {
Self::Plain(PlainSyncProgress::new(color))
}
}
}
impl SyncProgress for SyncProgressReporter {
fn started(&mut self, context: &SyncContext<'_>) -> Result<(), AppError> {
match self {
Self::Spinner(progress) => progress.started(context),
Self::Plain(progress) => progress.started(context),
}
}
fn step_started(&mut self, step: SyncStep) -> Result<(), AppError> {
match self {
Self::Spinner(progress) => progress.step_started(step),
Self::Plain(progress) => progress.step_started(step),
}
}
fn step_finished(&mut self, step: SyncStep) -> Result<(), AppError> {
match self {
Self::Spinner(progress) => progress.step_finished(step),
Self::Plain(progress) => progress.step_finished(step),
}
}
fn step_failed(&mut self, step: SyncStep) -> Result<(), AppError> {
match self {
Self::Spinner(progress) => progress.step_failed(step),
Self::Plain(progress) => progress.step_failed(step),
}
}
fn finished(&mut self, report: &SyncReport) -> Result<(), AppError> {
match self {
Self::Spinner(progress) => progress.finished(report),
Self::Plain(progress) => progress.finished(report),
}
}
}
pub(crate) struct SpinnerSyncProgress {
spinner: ProgressBar,
color: bool,
}
impl SpinnerSyncProgress {
fn new(color: bool) -> Self {
let spinner = ProgressBar::new_spinner().with_style(ProgressStyle::default_spinner());
spinner.enable_steady_tick(Duration::from_millis(80));
Self { spinner, color }
}
}
impl SyncProgress for SpinnerSyncProgress {
fn started(&mut self, context: &SyncContext<'_>) -> Result<(), AppError> {
self.spinner.set_message(format!(
"Syncing {} for channel {} via {}",
context.notes_ref, context.channel, context.remote
));
Ok(())
}
fn step_started(&mut self, step: SyncStep) -> Result<(), AppError> {
self.spinner
.set_message(paint(step.message(), UiStyle::Yellow, self.color));
Ok(())
}
fn step_finished(&mut self, _step: SyncStep) -> Result<(), AppError> {
Ok(())
}
fn step_failed(&mut self, step: SyncStep) -> Result<(), AppError> {
self.spinner.finish_with_message(format!(
"{} Failed while {}",
paint("✗", UiStyle::Red, self.color),
step.gerund()
));
Ok(())
}
fn finished(&mut self, report: &SyncReport) -> Result<(), AppError> {
self.spinner
.finish_with_message(final_summary(report, self.color));
Ok(())
}
}
pub(crate) struct PlainSyncProgress {
color: bool,
}
impl PlainSyncProgress {
const fn new(color: bool) -> Self {
Self { color }
}
}
impl SyncProgress for PlainSyncProgress {
fn started(&mut self, _context: &SyncContext<'_>) -> Result<(), AppError> {
Ok(())
}
fn step_started(&mut self, _step: SyncStep) -> Result<(), AppError> {
Ok(())
}
fn step_finished(&mut self, _step: SyncStep) -> Result<(), AppError> {
Ok(())
}
fn step_failed(&mut self, step: SyncStep) -> Result<(), AppError> {
let mut stderr = io::stderr().lock();
writeln!(
stderr,
"{} Failed while {}",
paint("✗", UiStyle::Red, self.color),
step.gerund()
)?;
Ok(())
}
fn finished(&mut self, report: &SyncReport) -> Result<(), AppError> {
let mut stderr = io::stderr().lock();
writeln!(stderr, "{}", final_summary(report, self.color))?;
writeln!(stderr, " ref: {}", report.notes_ref)?;
writeln!(stderr, " result: {}", report.outcome.details())?;
Ok(())
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum UiStyle {
Green,
Yellow,
Red,
}
impl UiStyle {
const fn console_style(self) -> Style {
match self {
Self::Green => Style::new().green(),
Self::Yellow => Style::new().yellow(),
Self::Red => Style::new().red(),
}
}
}
fn final_summary(report: &SyncReport, color_enabled: bool) -> String {
match report.outcome {
SyncOutcome::NothingToSync => format!(
"{} No review notes to sync for channel {} via {}",
paint("✓", UiStyle::Green, color_enabled),
report.channel,
report.remote
),
SyncOutcome::FetchedMergedPushed | SyncOutcome::PushedLocalOnly => format!(
"{} {} review notes for channel {} via {}",
paint("✓", UiStyle::Green, color_enabled),
report.outcome.summary_verb(),
report.channel,
report.remote
),
}
}
fn paint(text: &str, style: UiStyle, enabled: bool) -> String {
style
.console_style()
.for_stderr()
.force_styling(enabled)
.apply_to(text)
.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::channel::ReviewChannel;
use crate::remote::RemoteNameSource;
fn report(outcome: SyncOutcome) -> SyncReport {
let channel = ReviewChannel::new("default").expect("valid channel");
SyncReport {
notes_ref: channel.notes_ref().clone(),
channel,
remote: RemoteName::new("origin", RemoteNameSource::Cli).expect("valid remote"),
outcome,
}
}
#[test]
fn final_summary_describes_completed_sync() {
assert_eq!(
final_summary(&report(SyncOutcome::FetchedMergedPushed), false),
"✓ Synced review notes for channel default via origin"
);
}
#[test]
fn final_summary_describes_noop_sync() {
assert_eq!(
final_summary(&report(SyncOutcome::NothingToSync), false),
"✓ No review notes to sync for channel default via origin"
);
}
}