/ radicle-cli / src / commands / diff.rs
diff.rs
  1  use std::ffi::OsString;
  2  
  3  use anyhow::anyhow;
  4  
  5  use radicle::git;
  6  use radicle::rad;
  7  use radicle_surf as surf;
  8  
  9  use crate::git::pretty_diff::ToPretty as _;
 10  use crate::git::Rev;
 11  use crate::terminal as term;
 12  use crate::terminal::args::{Args, Error, Help};
 13  use crate::terminal::highlight::Highlighter;
 14  use crate::terminal::{Constraint, Element as _};
 15  
 16  pub const HELP: Help = Help {
 17      name: "diff",
 18      description: "Show changes between commits",
 19      version: env!("CARGO_PKG_VERSION"),
 20      usage: r#"
 21  Usage
 22  
 23      rad diff [<commit>] [--staged] [<option>...]
 24      rad diff <commit> [<commit>] [<option>...]
 25  
 26      This command is meant to operate as closely as possible to `git diff`,
 27      except its output is optimized for human-readability.
 28  
 29  Options
 30  
 31      --staged        View staged changes
 32      --color         Force color output
 33      --help          Print help
 34  "#,
 35  };
 36  
 37  pub struct Options {
 38      pub commits: Vec<Rev>,
 39      pub staged: bool,
 40      pub unified: usize,
 41      pub color: bool,
 42  }
 43  
 44  impl Args for Options {
 45      fn from_args(args: Vec<OsString>) -> anyhow::Result<(Self, Vec<OsString>)> {
 46          use lexopt::prelude::*;
 47  
 48          let mut parser = lexopt::Parser::from_args(args);
 49          let mut commits = Vec::new();
 50          let mut staged = false;
 51          let mut unified = 5;
 52          let mut color = false;
 53  
 54          while let Some(arg) = parser.next()? {
 55              match arg {
 56                  Long("unified") | Short('U') => {
 57                      let val = parser.value()?;
 58                      unified = term::args::number(&val)?;
 59                  }
 60                  Long("staged") | Long("cached") => staged = true,
 61                  Long("color") => color = true,
 62                  Long("help") | Short('h') => return Err(Error::Help.into()),
 63                  Value(val) => {
 64                      let rev = term::args::rev(&val)?;
 65  
 66                      commits.push(rev);
 67                  }
 68                  _ => return Err(anyhow::anyhow!(arg.unexpected())),
 69              }
 70          }
 71  
 72          Ok((
 73              Options {
 74                  commits,
 75                  staged,
 76                  unified,
 77                  color,
 78              },
 79              vec![],
 80          ))
 81      }
 82  }
 83  
 84  pub fn run(options: Options, _ctx: impl term::Context) -> anyhow::Result<()> {
 85      let (repo, _) = rad::cwd()?;
 86      let oids = options
 87          .commits
 88          .into_iter()
 89          .map(|rev| {
 90              repo.revparse_single(rev.as_str())
 91                  .map_err(|e| anyhow!("unknown object {rev}: {e}"))
 92                  .and_then(|o| {
 93                      o.into_commit()
 94                          .map_err(|_| anyhow!("object {rev} is not a commit"))
 95                  })
 96          })
 97          .collect::<Result<Vec<_>, _>>()?;
 98  
 99      let mut opts = git::raw::DiffOptions::new();
100      opts.patience(true)
101          .minimal(true)
102          .context_lines(options.unified as u32);
103  
104      let mut find_opts = git::raw::DiffFindOptions::new();
105      find_opts.exact_match_only(true);
106      find_opts.all(true);
107  
108      let mut diff = match oids.as_slice() {
109          [] => {
110              if options.staged {
111                  let head = repo.head()?.peel_to_tree()?;
112                  // HEAD vs. index.
113                  repo.diff_tree_to_index(Some(&head), None, Some(&mut opts))
114              } else {
115                  // Working tree vs. index.
116                  repo.diff_index_to_workdir(None, None)
117              }
118          }
119          [commit] => {
120              let commit = commit.tree()?;
121              if options.staged {
122                  // Commit vs. index.
123                  repo.diff_tree_to_index(Some(&commit), None, Some(&mut opts))
124              } else {
125                  // Commit vs. working tree.
126                  repo.diff_tree_to_workdir(Some(&commit), Some(&mut opts))
127              }
128          }
129          [left, right] => {
130              // Commit vs. commit.
131              let left = left.tree()?;
132              let right = right.tree()?;
133  
134              repo.diff_tree_to_tree(Some(&left), Some(&right), Some(&mut opts))
135          }
136          _ => {
137              anyhow::bail!("Too many commits given. See `rad diff --help` for usage.");
138          }
139      }?;
140      diff.find_similar(Some(&mut find_opts))?;
141  
142      term::Paint::force(options.color);
143  
144      let diff = surf::diff::Diff::try_from(diff)?;
145      let mut hi = Highlighter::default();
146      let pretty = diff.pretty(&mut hi, &(), &repo);
147  
148      pretty.write(Constraint::from_env().unwrap_or_default());
149  
150      Ok(())
151  }