dyd 1.7.0

CLI for daily diffing of git repos
Documentation
use crate::git::repo::{Log, Repo};
use regex::Regex;
use serde::de::{value, Deserializer, IntoDeserializer};
use serde::Deserialize;
use std::path::Path;
use std::process::Command;
use std::str::FromStr;

#[derive(Debug)]
pub enum Difftool {
  Git,
  GitHub,
  Fallthrough(String),
}

impl Default for Difftool {
  fn default() -> Self {
    Self::Git
  }
}

impl Difftool {
  pub fn open(&self, root_path: &Path, repo: &Repo, log: &Log) {
    let mut cmd: String = "".to_string();
    let mut args: Vec<String> = vec![];
    let ref_to: String = match repo.branch.clone() {
      Some(branch) => format!("origin/{branch}"),
      None => "HEAD".into(),
    };
    let diff = format!("{}..{ref_to}", log.sha);
    let repo_path = repo.path(root_path).unwrap();

    let cwd = std::env::current_dir()
      .unwrap()
      .into_os_string()
      .into_string()
      .unwrap();

    let mut context = std::collections::HashMap::new();
    context.insert("DYD_PWD".to_string(), cwd.clone());
    context.insert("DIFF".to_string(), diff.clone());
    context.insert("ORIGIN".to_string(), repo.origin.clone());
    context.insert("REF_FROM".to_string(), log.sha.clone());
    context.insert("REF_TO".to_string(), ref_to.clone());
    assert!(envsubst::validate_vars(&context).is_ok());

    let difftool_expansion = envsubst::substitute(self.to_str(repo, &log.sha), &context).unwrap();

    let difftool_parts: Vec<&str> = difftool_expansion.split(' ').collect();
    difftool_parts
      .iter()
      .enumerate()
      .for_each(|(index, value)| {
        if index == 0 {
          cmd = value.to_string();
        } else {
          args.push(value.to_string());
        }
      });

    match Command::new(cmd)
      .args(args)
      .env("DYD_PWD", cwd)
      .env("DIFF", diff)
      .env("REF_FROM", &log.sha)
      .env("REF_TO", ref_to)
      .env("ORIGIN", &repo.origin)
      .current_dir(repo_path)
      .output()
    {
      Ok(_) => (),
      Err(err) => eprintln!("\rError opening difftool:\r\n{err:?}\r\ndifftool: {self}"),
    };
  }

  pub fn to_str(&self, repo: &Repo, from_sha: &String) -> String {
    match self {
      Difftool::Git => "git difftool -g -y ${DIFF}".to_owned(),
      Difftool::GitHub => Difftool::github_diff_url(repo, from_sha),
      Difftool::Fallthrough(difftool) => difftool.clone(),
    }
  }

  fn github_diff_url(repo: &Repo, from_sha: &String) -> String {
    let origin = repo.origin.clone();
    let origin_re = Regex::new(r"(git@|https://)([^:]+)[:/](.+)(?:\.git)$").unwrap();
    let caps = origin_re.captures(&origin).unwrap();
    let url = caps.get(2).unwrap().as_str();
    let repository = caps.get(3).unwrap().as_str();

    let github_url = format!("https://{}/{}", url, repository);
    let ref_to = repo.branch.clone().unwrap_or("HEAD".to_owned());
    format!("open {github_url}/compare/{from_sha}..{ref_to}?diff=split")
  }
}

impl std::fmt::Display for Difftool {
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    match self {
      Difftool::Git => write!(f, "git"),
      Difftool::GitHub => write!(f, "github"),
      Difftool::Fallthrough(difftool) => write!(f, "fallthrough: {difftool}"),
    }
  }
}

impl FromStr for Difftool {
  type Err = value::Error;

  fn from_str(s: &str) -> Result<Self, Self::Err> {
    Self::deserialize(s.into_deserializer())
  }
}

impl<'de> Deserialize<'de> for Difftool {
  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
  where
    D: Deserializer<'de>,
  {
    let s = String::deserialize(deserializer)?;

    let deserialized = if s == "git".to_owned() {
      Self::Git
    } else if s == "github".to_owned() {
      Self::GitHub
    } else {
      Self::Fallthrough(s)
    };
    Ok(deserialized)
  }
}

#[cfg(test)]
mod tests {
  #[test]
  fn difftool_git_to_str() {
    let difftool = super::Difftool::Git;
    let repo = crate::git::repo::Repo {
      name: "test repo".into(),
      origin: "git@github.com:synchronal/dyd.git".into(),
      ..Default::default()
    };
    let from_sha = "abc1234".into();

    let string = difftool.to_str(&repo, &from_sha);
    assert_eq!(string, "git difftool -g -y ${DIFF}")
  }

  #[test]
  fn difftool_github_ssh_to_str() {
    let difftool = super::Difftool::GitHub;
    let repo = crate::git::repo::Repo {
      name: "test repo".into(),
      origin: "git@github.com:synchronal/dyd.git".into(),
      ..Default::default()
    };
    let from_sha = "abc1234".into();

    let string = difftool.to_str(&repo, &from_sha);
    assert_eq!(
      string,
      "open https://github.com/synchronal/dyd/compare/abc1234..HEAD?diff=split"
    )
  }

  #[test]
  fn difftool_github_ssh_branch_to_str() {
    let difftool = super::Difftool::GitHub;
    let repo = crate::git::repo::Repo {
      branch: Some("my-branch".into()),
      name: "test repo".into(),
      origin: "git@github.com:synchronal/dyd.git".into(),
      ..Default::default()
    };
    let from_sha = "abc1234".into();

    let string = difftool.to_str(&repo, &from_sha);
    assert_eq!(
      string,
      "open https://github.com/synchronal/dyd/compare/abc1234..my-branch?diff=split"
    )
  }

  #[test]
  fn difftool_github_https_to_str() {
    let difftool = super::Difftool::GitHub;
    let repo = crate::git::repo::Repo {
      branch: Some("my-branch".into()),
      name: "test repo".into(),
      origin: "https://github.com/synchronal/dyd.git".into(),
      ..Default::default()
    };
    let from_sha = "abc1234".into();

    let string = difftool.to_str(&repo, &from_sha);
    assert_eq!(
      string,
      "open https://github.com/synchronal/dyd/compare/abc1234..my-branch?diff=split"
    )
  }
}