radicle_cli/commands/
diff.rs1use std::ffi::OsString;
2
3use anyhow::anyhow;
4
5use radicle::git;
6use radicle::rad;
7use radicle_surf as surf;
8
9use crate::git::pretty_diff::ToPretty as _;
10use crate::git::Rev;
11use crate::terminal as term;
12use crate::terminal::args::{Args, Error, Help};
13use crate::terminal::highlight::Highlighter;
14
15pub const HELP: Help = Help {
16 name: "diff",
17 description: "Show changes between commits",
18 version: env!("RADICLE_VERSION"),
19 usage: r#"
20Usage
21
22 rad diff [<commit>] [--staged] [<option>...]
23 rad diff <commit> [<commit>] [<option>...]
24
25 This command is meant to operate as closely as possible to `git diff`,
26 except its output is optimized for human-readability.
27
28Options
29
30 --unified, -U Context lines to show (default: 5)
31 --staged View staged changes
32 --color Force color output
33 --help Print help
34"#,
35};
36
37pub struct Options {
38 pub commits: Vec<Rev>,
39 pub staged: bool,
40 pub unified: usize,
41 pub color: bool,
42}
43
44impl 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 _ => anyhow::bail!(arg.unexpected()),
69 }
70 }
71
72 Ok((
73 Options {
74 commits,
75 staged,
76 unified,
77 color,
78 },
79 vec![],
80 ))
81 }
82}
83
84pub fn run(options: Options, _ctx: impl term::Context) -> anyhow::Result<()> {
85 crate::warning::deprecated("rad diff", "git diff");
86
87 let repo = rad::repo()?;
88 let oids = options
89 .commits
90 .into_iter()
91 .map(|rev| {
92 repo.revparse_single(rev.as_str())
93 .map_err(|e| anyhow!("unknown object {rev}: {e}"))
94 .and_then(|o| {
95 o.into_commit()
96 .map_err(|_| anyhow!("object {rev} is not a commit"))
97 })
98 })
99 .collect::<Result<Vec<_>, _>>()?;
100
101 let mut opts = git::raw::DiffOptions::new();
102 opts.patience(true)
103 .minimal(true)
104 .context_lines(options.unified as u32);
105
106 let mut find_opts = git::raw::DiffFindOptions::new();
107 find_opts.exact_match_only(true);
108 find_opts.all(true);
109
110 let mut diff = match oids.as_slice() {
111 [] => {
112 if options.staged {
113 let head = repo.head()?.peel_to_tree()?;
114 repo.diff_tree_to_index(Some(&head), None, Some(&mut opts))
116 } else {
117 repo.diff_index_to_workdir(None, None)
119 }
120 }
121 [commit] => {
122 let commit = commit.tree()?;
123 if options.staged {
124 repo.diff_tree_to_index(Some(&commit), None, Some(&mut opts))
126 } else {
127 repo.diff_tree_to_workdir(Some(&commit), Some(&mut opts))
129 }
130 }
131 [left, right] => {
132 let left = left.tree()?;
134 let right = right.tree()?;
135
136 repo.diff_tree_to_tree(Some(&left), Some(&right), Some(&mut opts))
137 }
138 _ => {
139 anyhow::bail!("Too many commits given. See `rad diff --help` for usage.");
140 }
141 }?;
142 diff.find_similar(Some(&mut find_opts))?;
143
144 term::Paint::force(options.color);
145
146 let diff = surf::diff::Diff::try_from(diff)?;
147 let mut hi = Highlighter::default();
148 let pretty = diff.pretty(&mut hi, &(), &repo);
149
150 crate::pager::run(pretty)?;
151
152 Ok(())
153}