pijul 1.0.0-beta.17

A distributed version control system.
use std::collections::{BTreeMap, BTreeSet};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::sync::Arc;

use canonical_path::CanonicalPathBuf;
use clap::Parser;
use pijul_core::change::{ChangeHeader, Hunk, Local, LocalChange};
use pijul_core::{MutTxnT, TxnT};
use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};

use crate::commands::common_opts::RepoAndChannel;
use crate::commands::get_channel;

#[derive(Parser, Debug)]
pub struct Status {
    #[clap(flatten)]
    pub base: RepoAndChannel,
    /// Only show status for these paths
    pub prefixes: Vec<PathBuf>,
}

impl Status {
    pub fn repository_path(&mut self) -> Option<&Path> {
        self.base.repo_path()
    }

    pub fn run(mut self, config: &pijul_config::Config) -> Result<(), anyhow::Error> {
        let repo = self.base.find_root()?;

        let use_color = super::diff::is_colored(config);
        let mut out = StandardStream::stdout(if use_color {
            ColorChoice::Auto
        } else {
            ColorChoice::Never
        });

        // Channel header
        {
            let txn = repo.pristine.txn_begin()?;
            let current = txn.current_channel().ok();
            writeln!(out, "On channel: {}", current.unwrap_or("(none)"))?;
        }

        let txn = repo.pristine.arc_txn_begin()?;
        let channel_name = get_channel(self.base.channel(), &*txn.read()).0.to_string();
        let channel_sm: pijul_core::small_string::SmallString = channel_name.parse()?;
        let channel = txn.write().open_or_create_channel(&channel_sm)?;

        // Record working copy state
        let mut state = pijul_core::RecordBuilder::new();
        if self.prefixes.is_empty() {
            state.record(
                txn.clone(),
                pijul_core::Algorithm::default(),
                false,
                &pijul_core::DEFAULT_SEPARATOR,
                channel.clone(),
                &repo.working_copy,
                &repo.changes,
                "",
                std::thread::available_parallelism()?.get(),
            )?;
        } else {
            self.fill_relative_prefixes()?;
            repo.working_copy.record_prefixes(
                txn.clone(),
                pijul_core::Algorithm::default(),
                channel.clone(),
                &repo.changes,
                &mut state,
                CanonicalPathBuf::canonicalize(&repo.path)?,
                &self.prefixes,
                false,
                std::thread::available_parallelism()?.get(),
                0,
            )?;
        }
        let rec = state.finish();
        let actions: Vec<_> = {
            let txn_ = txn.read();
            rec.actions
                .into_iter()
                .map(|r| r.globalize(&*txn_).unwrap())
                .collect()
        };
        let contents = if let Ok(c) = Arc::try_unwrap(rec.contents) {
            c.into_inner()
        } else {
            unreachable!()
        };
        let change = LocalChange::make_change(
            &*txn.read(),
            &channel,
            actions,
            contents,
            ChangeHeader::default(),
            Vec::new(),
        )?;

        // Collect per-file labels from hunks
        let mut by_file: BTreeMap<String, BTreeSet<&'static str>> = BTreeMap::new();
        for hunk in change.changes.iter() {
            let (path, label) = match hunk {
                Hunk::Edit {
                    local: Local { path, .. },
                    ..
                }
                | Hunk::Replacement {
                    local: Local { path, .. },
                    ..
                } => (path.clone(), "modified"),
                Hunk::FileAdd { path, .. } => (path.clone(), "new file"),
                Hunk::FileDel { path, .. } => (path.clone(), "deleted"),
                Hunk::FileUndel { path, .. } => (path.clone(), "restored"),
                Hunk::FileMove { path, .. } => (path.clone(), "moved"),
                Hunk::SolveNameConflict { path, .. } => (path.clone(), "conflict resolved"),
                Hunk::UnsolveNameConflict { path, .. } => (path.clone(), "conflict"),
                Hunk::SolveOrderConflict {
                    local: Local { path, .. },
                    ..
                } => (path.clone(), "conflict resolved"),
                Hunk::UnsolveOrderConflict {
                    local: Local { path, .. },
                    ..
                } => (path.clone(), "conflict"),
                Hunk::ResurrectZombies {
                    local: Local { path, .. },
                    ..
                } => (path.clone(), "zombie fixed"),
                Hunk::AddRoot { .. } | Hunk::DelRoot { .. } => continue,
            };
            by_file.entry(path).or_default().insert(label);
        }

        // "Changes not recorded:" section
        if !by_file.is_empty() {
            writeln!(out)?;
            out.set_color(ColorSpec::new().set_bold(true))?;
            writeln!(out, "Changes not recorded:")?;
            out.reset()?;
            out.set_color(ColorSpec::new().set_dimmed(true))?;
            writeln!(
                out,
                "  (use \"pijul record [file]...\" to record into a change)"
            )?;
            writeln!(out, "  (use \"pijul reset [file]...\" to discard changes)")?;
            out.reset()?;
            writeln!(out)?;

            let label_width = by_file
                .values()
                .flat_map(|v| v.iter().map(|s| s.len()))
                .max()
                .unwrap_or(0);
            for (path, labels) in &by_file {
                let label = labels.iter().cloned().collect::<Vec<_>>().join(", ");
                out.set_color(ColorSpec::new().set_fg(Some(Color::Red)))?;
                writeln!(
                    out,
                    "        {:width$}   {}",
                    label,
                    path,
                    width = label_width
                )?;
                out.reset()?;
            }
        }

        // "Untracked files:" section
        let untracked: Vec<PathBuf> =
            super::diff::untracked(&repo, txn.clone())?.collect::<Result<_, _>>()?;
        if !untracked.is_empty() {
            writeln!(out)?;
            out.set_color(ColorSpec::new().set_bold(true))?;
            writeln!(out, "Untracked files:")?;
            out.reset()?;
            out.set_color(ColorSpec::new().set_dimmed(true))?;
            writeln!(out, "  (use \"pijul add <file>...\" to track)")?;
            out.reset()?;
            writeln!(out)?;
            for path in &untracked {
                out.set_color(ColorSpec::new().set_fg(Some(Color::Red)))?;
                writeln!(out, "        {}", path.display())?;
                out.reset()?;
            }
        }

        if by_file.is_empty() && untracked.is_empty() {
            writeln!(out)?;
            writeln!(out, "Nothing to record, working copy is clean.")?;
        }

        Ok(())
    }

    fn fill_relative_prefixes(&mut self) -> Result<(), anyhow::Error> {
        let cwd = std::env::current_dir()?;
        for p in self.prefixes.iter_mut() {
            if p.is_relative() {
                *p = cwd.join(&p);
            }
        }
        Ok(())
    }
}