verco 6.1.0

A simple Git/Mercurial/PlasticSCM tui client based on keyboard shortcuts
use std::{
    path::PathBuf,
    process::{Child, Command, Stdio},
    sync::Arc,
};

use crate::ui::{Drawer, SelectEntryDraw};

pub mod git;
pub mod hg;
pub mod plastic;

pub type BackendResult<T> = std::result::Result<T, String>;

#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum FileStatus {
    Modified,
    Added,
    Deleted,
    Renamed,
    Untracked,
    Copied,
    Unmerged,
    Missing,
    Ignored,
    Clean,
}
impl FileStatus {
    pub const fn max_len() -> usize {
        9
    }

    pub fn as_str(&self) -> &str {
        match self {
            Self::Modified => "modified",
            Self::Added => "added",
            Self::Deleted => "deleted",
            Self::Renamed => "renamed",
            Self::Untracked => "untracked",
            Self::Copied => "copied",
            Self::Unmerged => "unmerged",
            Self::Missing => "missing",
            Self::Ignored => "ignored",
            Self::Clean => "clean",
        }
    }
}

pub struct StatusInfo {
    pub header: String,
    pub entries: Vec<RevisionEntry>,
}

pub struct RevisionInfo {
    pub message: String,
    pub entries: Vec<RevisionEntry>,
}

#[derive(Clone)]
pub struct RevisionEntry {
    pub name: String,
    pub status: FileStatus,
}

pub struct LogEntry {
    pub graph: String,
    pub hash: String,
    pub date: String,
    pub author: String,
    pub refs: String,
    pub message: String,
}

pub struct BranchEntry {
    pub name: String,
    pub checked_out: bool,
}

pub struct TagEntry {
    pub name: String,
}

#[derive(Clone)]
pub struct SelectableRevisionEntry {
    pub selected: bool,
    pub name: String,
    pub status: FileStatus,
}
impl From<RevisionEntry> for SelectableRevisionEntry {
    fn from(other: RevisionEntry) -> Self {
        Self {
            selected: false,
            name: other.name,
            status: other.status,
        }
    }
}
impl SelectEntryDraw for SelectableRevisionEntry {
    fn draw(&self, drawer: &mut Drawer, _: bool, _: bool) -> usize {
        const NAME_TOO_LONG_PREFIX: &str = "...";

        let name_available_width = (drawer.viewport_size.0 as usize)
            .saturating_sub(2 + 1 + FileStatus::max_len() + 1 + 1 + NAME_TOO_LONG_PREFIX.len());

        let (name_prefix, trimmed_name) =
            match self.name.char_indices().nth_back(name_available_width) {
                Some((i, _)) => (NAME_TOO_LONG_PREFIX, &self.name[i..]),
                None => ("", &self.name[..]),
            };

        let selected_text = if self.selected { '+' } else { ' ' };
        drawer.fmt(format_args!(
            "{} [{:>width$}] {}{}",
            selected_text,
            self.status.as_str(),
            name_prefix,
            trimmed_name,
            width = FileStatus::max_len(),
        ));

        1
    }
}

pub trait Backend: 'static + Send + Sync {
    fn status(&self) -> BackendResult<StatusInfo>;
    fn commit(&self, message: &str, entries: &[RevisionEntry]) -> BackendResult<()>;
    fn discard(&self, entries: &[RevisionEntry]) -> BackendResult<()>;
    fn diff(&self, revision: Option<&str>, entries: &[RevisionEntry]) -> BackendResult<String>;
    fn resolve_taking_ours(&self, entries: &[RevisionEntry]) -> BackendResult<()>;
    fn resolve_taking_theirs(&self, entries: &[RevisionEntry]) -> BackendResult<()>;

    fn log(&self, start: usize, len: usize) -> BackendResult<Vec<LogEntry>>;
    fn checkout(&self, revision: &str) -> BackendResult<()>;
    fn merge(&self, revision: &str) -> BackendResult<()>;
    fn fetch(&self) -> BackendResult<()>;
    fn pull(&self) -> BackendResult<()>;
    fn push(&self) -> BackendResult<()>;

    fn revision_details(&self, revision: &str) -> BackendResult<RevisionInfo>;

    fn branches(&self) -> BackendResult<Vec<BranchEntry>>;
    fn new_branch(&self, name: &str) -> BackendResult<()>;
    fn delete_branch(&self, name: &str) -> BackendResult<()>;

    fn tags(&self) -> BackendResult<Vec<TagEntry>>;
    fn new_tag(&self, name: &str) -> BackendResult<()>;
    fn delete_tag(&self, name: &str) -> BackendResult<()>;
}

pub struct Process(Child);
impl Process {
    pub fn spawn(command_name: &str, args: &[&str]) -> BackendResult<Self> {
        let mut command = Command::new(command_name);
        command.args(args);
        command.stdin(Stdio::null());
        command.stdout(Stdio::piped());
        command.stderr(Stdio::piped());

        match command.spawn() {
            Ok(child) => Ok(Self(child)),
            Err(error) => Err(format!(
                "could not spawn process '{}': {}",
                command_name, error
            )),
        }
    }

    pub fn wait(self) -> BackendResult<String> {
        let output = match self.0.wait_with_output() {
            Ok(output) => output,
            Err(error) => return Err(format!("could not wait for process: {}", error)),
        };

        let stdout = String::from_utf8_lossy(&output.stdout);
        if output.status.success() {
            Ok(stdout.into())
        } else {
            let stderr = String::from_utf8_lossy(&output.stderr);
            let mut error = String::new();
            error.push_str(&stdout);
            error.push('\n');
            error.push_str(&stderr);
            Err(error)
        }
    }
}

pub fn backend_from_current_repository() -> Option<(PathBuf, Arc<dyn Backend>)> {
    if let Some((root, git)) = git::Git::try_new() {
        Some((root, Arc::new(git)))
    } else if let Some((root, hg)) = hg::Hg::try_new() {
        Some((root, Arc::new(hg)))
    } else if let Some((root, plastic)) = plastic::Plastic::try_new() {
        Some((root, Arc::new(plastic)))
    } else {
        None
    }
}