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 }