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                _ => 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
84pub fn run(options: Options, _ctx: impl term::Context) -> anyhow::Result<()> {
85    let repo = rad::repo()?;
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    term::pager::page(pretty)?;
149
150    Ok(())
151}