zenops 0.4.2

Declarative system configuration management for shell config and dotfiles.
Documentation
use std::path::Path;

use xshell::{Shell, cmd};
use zenops_safe_relative_path::{SafeRelativePath, SafeRelativePathBuf};

use crate::error::Error;

#[derive(Debug, PartialEq, Clone)]
pub enum GitFileStatus {
    Modified(SafeRelativePathBuf),
    Untracked(SafeRelativePathBuf),
}

pub struct Git<'path, 'shell> {
    path: &'path Path,
    sh: &'shell Shell,
}

impl<'path, 'shell> Git<'path, 'shell> {
    pub fn new(path: &'path Path, sh: &'shell Shell) -> Self {
        Self { path, sh }
    }

    pub fn is_git_repo(&self) -> Result<bool, Error> {
        let Self { path, sh } = self;
        match cmd!(sh, "git -C {path} rev-parse --is-inside-work-tree")
            .quiet()
            .ignore_status()
            .ignore_stderr()
            .read()?
            .trim()
        {
            "" => Ok(false),
            "true" => Ok(true),
            unk => todo!("{unk:?}"),
        }
    }

    pub fn status(&self) -> Result<Vec<GitFileStatus>, Error> {
        let Self { path, sh } = self;
        let mut ret = Vec::new();
        for line in cmd!(sh, "git -C {path} status --porcelain=v2")
            .quiet()
            .read()?
            .lines()
        {
            let mut cur = line;
            let mut next_arg = || {
                if cur.is_empty() {
                    return None;
                }
                let (ret, remain) = cur.split_once(' ').unwrap_or((cur, ""));
                cur = remain;
                Some(ret)
            };
            match next_arg().unwrap() {
                "1" => {
                    let xy_status = next_arg().unwrap();
                    let _submodule_state = next_arg().unwrap();
                    let _mode_head = next_arg().unwrap();
                    let _mode_index = next_arg().unwrap();
                    let _mode_worktree = next_arg().unwrap();
                    let _hash_head = next_arg().unwrap();
                    let _hash_index = next_arg().unwrap();
                    let path = cur;

                    match xy_status {
                        ".M" => ret.push(GitFileStatus::Modified(
                            SafeRelativePath::from_relative_path(path)?.into(),
                        )),
                        _ => todo!("Unknown status {xy_status}"),
                    }
                }
                "2" => todo!(),
                "u" => todo!(),
                "?" => {
                    ret.push(GitFileStatus::Untracked(
                        SafeRelativePath::from_relative_path(cur)?.into(),
                    ));
                }
                "!" => todo!(),
                tag => todo!("unknown tag {tag:?}"),
            }
        }
        Ok(ret)
    }
}

#[derive(clap::Subcommand, Debug)]
pub enum GitCmd {
    Status {
        files: Vec<SafeRelativePathBuf>,
    },
    Diff {
        files: Vec<SafeRelativePathBuf>,
    },
    Add {
        files: Vec<SafeRelativePathBuf>,
    },
    Pull {
        #[arg(short, long, num_args = 0..=1, require_equals = true, default_missing_value = "", value_parser=["", "false", "true", "merges", "interactive"])]
        rebase: Option<String>,
    },
    Commit {
        #[clap(long, short)]
        all: bool,

        #[clap(long, short)]
        message: Option<String>,
    },
    Push {},
}

impl GitCmd {
    pub fn passthru_dispatch_in(
        &self,
        repo_dir: impl AsRef<Path>,
        sh: &Shell,
    ) -> Result<(), Error> {
        let _dir = sh.push_dir(repo_dir);

        match self {
            GitCmd::Status { files } => cmd!(sh, "git status").arg("--").args(files),
            GitCmd::Diff { files } => cmd!(sh, "git diff").arg("--").args(files),
            GitCmd::Add { files } => cmd!(sh, "git add").arg("--").args(files),
            GitCmd::Pull { rebase } => cmd!(sh, "git pull").args(rebase.as_ref().map(|v| {
                if v.is_empty() {
                    "--rebase".to_string()
                } else {
                    format!("--rebase={v}")
                }
            })),
            GitCmd::Commit { all, message } => cmd!(sh, "git commit")
                .args(all.then_some("-a"))
                .args(message.as_ref().into_iter().flat_map(|m| ["-m", m])),
            GitCmd::Push {} => cmd!(sh, "git push"),
        }
        .run()?;

        Ok(())
    }
}