verco 6.5.4

A simple Git/Mercurial/PlasticSCM tui client based on keyboard shortcuts
use std::path::{Path, PathBuf};

use crate::backend::{
    Backend, BackendResult, BranchEntry, FileStatus, LogEntry, Process, RevisionEntry,
    RevisionInfo, StatusInfo, TagEntry,
};

pub struct Hg;

impl Hg {
    pub fn try_new() -> Option<(PathBuf, Self)> {
        let output = Process::spawn("hg", &["root"]).ok()?.wait().ok()?;
        let root = Path::new(output.trim()).into();
        Some((root, Self))
    }
}

impl Backend for Hg {
    fn status(&self) -> BackendResult<StatusInfo> {
        let header = Process::spawn("hg", &["summary"])?;
        let output = Process::spawn("hg", &["status"])?;

        let header = header.wait()?.lines().next().unwrap_or("").into();
        let output = output.wait()?;

        let mut entries = Vec::new();
        for line in output.lines() {
            let mut splits = line.splitn(2, ' ');
            let status = parse_file_status(splits.next().unwrap_or("").trim());
            let name = splits.next().unwrap_or("").into();

            entries.push(RevisionEntry::new(name, status));
        }

        Ok(StatusInfo { header, entries })
    }

    fn commit(&self, message: &str, entries: &[RevisionEntry]) -> BackendResult<()> {
        if entries.is_empty() {
            Process::spawn("hg", &["commit", "--addremove", "-m", message])?.wait()?;
        } else {
            let mut args = Vec::new();
            args.push("remove");
            for entry in entries {
                if let FileStatus::Missing | FileStatus::Deleted = entry.status {
                    args.push(&entry.name);
                }
            }
            let remove = Process::spawn("hg", &args)?;

            args.clear();
            args.push("add");
            for entry in entries {
                if let FileStatus::Untracked = entry.status {
                    args.push(&entry.name);
                }
            }
            let add = Process::spawn("hg", &args)?;

            remove.wait()?;
            add.wait()?;

            Process::spawn("hg", &["commit", "-m", message])?.wait()?;
        }

        Ok(())
    }

    fn discard(&self, entries: &[RevisionEntry]) -> BackendResult<()> {
        if entries.is_empty() {
            Process::spawn("hg", &["revert", "-C", "--all"])?.wait()?;
            Process::spawn("hg", &["purge"])?.wait()?;
        } else {
            let mut args = Vec::new();
            args.push("purge");
            for entry in entries {
                if let FileStatus::Untracked = entry.status {
                    args.push(&entry.name);
                }
            }
            let purge = Process::spawn("hg", &args)?;

            args.clear();
            args.push("revert");
            args.push("-C");
            for entry in entries {
                if !matches!(entry.status, FileStatus::Untracked) {
                    args.push(&entry.name);
                }
            }
            let revert = Process::spawn("hg", &args)?;

            purge.wait()?;
            revert.wait()?;
        }

        Ok(())
    }

    fn diff(&self, revision: Option<&str>, entries: &[RevisionEntry]) -> BackendResult<String> {
        match revision {
            Some(revision) => {
                if entries.is_empty() {
                    Process::spawn("hg", &["diff", "--change", revision])?.wait()
                } else {
                    let mut args = Vec::new();
                    args.push("diff");
                    args.push("--change");
                    args.push(revision);
                    args.push("--");
                    for entry in entries {
                        args.push(&entry.name);
                    }

                    Process::spawn("hg", &args)?.wait()
                }
            }
            None => {
                if entries.is_empty() {
                    Process::spawn("hg", &["diff"])?.wait()
                } else {
                    let mut args = Vec::new();
                    args.push("diff");
                    args.push("--");
                    for entry in entries {
                        args.push(&entry.name);
                    }
                    Process::spawn("hg", &args)?.wait()
                }
            }
        }
    }

    fn resolve_taking_ours(&self, entries: &[RevisionEntry]) -> BackendResult<()> {
        if entries.is_empty() {
            Process::spawn("hg", &["resolve", "--all", "--tool", "internal:local"])?.wait()?;
        } else {
            if !entries
                .iter()
                .any(|e| matches!(e.status, FileStatus::Unmerged))
            {
                return Ok(());
            }

            let mut args = Vec::new();
            args.push("resolve");
            args.push("--tool");
            args.push("internal:local");
            args.push("--");

            for entry in entries {
                if let FileStatus::Unmerged = entry.status {
                    args.push(&entry.name);
                }
            }

            Process::spawn("hg", &args)?.wait()?;
        }

        Ok(())
    }

    fn resolve_taking_theirs(&self, entries: &[RevisionEntry]) -> BackendResult<()> {
        if entries.is_empty() {
            Process::spawn("hg", &["resolve", "--all", "--tool", "internal:other"])?.wait()?;
        } else {
            if !entries
                .iter()
                .any(|e| matches!(e.status, FileStatus::Unmerged))
            {
                return Ok(());
            }

            let mut args = Vec::new();
            args.push("resolve");
            args.push("--tool");
            args.push("internal:other");
            args.push("--");

            for entry in entries {
                if let FileStatus::Unmerged = entry.status {
                    args.push(&entry.name);
                }
            }

            Process::spawn("hg", &args)?.wait()?;
        }

        Ok(())
    }

    fn log(&self, skip: usize, len: usize) -> BackendResult<(usize, Vec<LogEntry>)> {
        let template = "\x1f{node|short}\x1f{date|shortdate}\x1f{author|person}\x1f{ifeq(phase,'secret','(secret) ','')}{ifeq(phase,'draft','(draft) ','')}{if(topics,'[{topics}] ')}{tags % '{tag} '}{branch}\x1f{desc}";
        let revset = format!("limit(tip:0,{},{})", len, skip);
        let output = Process::spawn(
            "hg",
            &[
                "log",
                "--config",
                "experimental.graphshorten=True",
                "--graph",
                "--template",
                template,
                "--rev",
                &revset,
            ],
        )?
        .wait()?;

        let mut entries = Vec::new();
        for line in output.lines() {
            if line == "~" {
                continue;
            }

            let mut splits = line.splitn(6, '\x1f');

            let graph = splits.next().unwrap_or("").into();
            let hash = splits.next().unwrap_or("").into();
            let date = splits.next().unwrap_or("").into();
            let author = splits.next().unwrap_or("").into();
            let refs = splits.next().unwrap_or("").into();
            let message = splits.next().unwrap_or("").into();

            entries.push(LogEntry {
                graph,
                hash,
                date,
                author,
                refs,
                message,
            });
        }

        Ok((skip, entries))
    }

    fn checkout(&self, revision: &str) -> BackendResult<()> {
        Process::spawn("hg", &["update", revision])?.wait()?;
        Ok(())
    }

    fn merge(&self, revision: &str) -> BackendResult<()> {
        Process::spawn("hg", &["merge", revision])?.wait()?;
        Ok(())
    }

    fn fetch(&self) -> BackendResult<()> {
        self.pull()
    }

    fn pull(&self) -> BackendResult<()> {
        Process::spawn("hg", &["pull"])?.wait()?;
        Ok(())
    }

    fn push(&self) -> BackendResult<()> {
        Process::spawn("hg", &["push", "--new-branch"])?.wait()?;
        Ok(())
    }

    fn revision_details(&self, revision: &str) -> BackendResult<RevisionInfo> {
        let message = Process::spawn("hg", &["log", "--rev", revision, "--template", "{desc}"])?;
        let output = Process::spawn("hg", &["status", "--change", revision])?;

        let message = message.wait()?.trim().into();
        let output = output.wait()?;

        let mut entries = Vec::new();
        for line in output.lines() {
            let mut splits = line.splitn(2, ' ');
            let status = parse_file_status(splits.next().unwrap_or("").trim());
            let name = splits.next().unwrap_or("").into();

            entries.push(RevisionEntry::new(name, status));
        }

        Ok(RevisionInfo { message, entries })
    }

    fn branches(&self) -> BackendResult<Vec<BranchEntry>> {
        let entries = Process::spawn("hg", &["branches", "--template", "{branch}\x1f#\\n"])?
            .wait()?
            .lines()
            .map(|l| {
                let mut splits = l.splitn(2, '\x1f');
                let name = splits.next().unwrap_or("").into();
                let checked_out = splits.next().unwrap_or("") == "*";
                BranchEntry { name, checked_out }
            })
            .collect();
        Ok(entries)
    }

    fn new_branch(&self, name: &str) -> BackendResult<()> {
        Process::spawn("hg", &["branch", name])?.wait()?;
        Ok(())
    }

    fn delete_branch(&self, name: &str) -> BackendResult<()> {
        let changeset = Process::spawn("hg", &["identify", "--num"])?.wait()?;
        self.checkout(name)?;
        Process::spawn("hg", &["commit", "-m", "close branch", "--close-branch"])?.wait()?;
        self.checkout(&changeset)?;
        Ok(())
    }

    fn tags(&self) -> BackendResult<Vec<TagEntry>> {
        let entries = Process::spawn("hg", &["tags", "--template", "{tag}\\n"])?
            .wait()?
            .lines()
            .map(|l| TagEntry { name: l.into() })
            .collect();
        Ok(entries)
    }

    fn new_tag(&self, name: &str) -> BackendResult<()> {
        Process::spawn("hg", &["tag", "--force", name])?.wait()?;
        Ok(())
    }

    fn delete_tag(&self, name: &str) -> BackendResult<()> {
        Process::spawn("hg", &["tag", "--remove", name])?.wait()?;
        Ok(())
    }
}

fn parse_file_status(s: &str) -> FileStatus {
    match s {
        "?" => FileStatus::Untracked,
        "M" => FileStatus::Modified,
        "A" => FileStatus::Added,
        "R" => FileStatus::Deleted,
        "!" => FileStatus::Missing,
        "I" => FileStatus::Ignored,
        "C" => FileStatus::Clean,
        _ => FileStatus::Unknown(s.into()),
    }
}