radicle_cli/commands/
diff.rs

1use 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                // HEAD vs. index.
115                repo.diff_tree_to_index(Some(&head), None, Some(&mut opts))
116            } else {
117                // Working tree vs. index.
118                repo.diff_index_to_workdir(None, None)
119            }
120        }
121        [commit] => {
122            let commit = commit.tree()?;
123            if options.staged {
124                // Commit vs. index.
125                repo.diff_tree_to_index(Some(&commit), None, Some(&mut opts))
126            } else {
127                // Commit vs. working tree.
128                repo.diff_tree_to_workdir(Some(&commit), Some(&mut opts))
129            }
130        }
131        [left, right] => {
132            // Commit vs. commit.
133            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}