1 /*
2  * libgit2 "diff" example - shows how to use the diff API
3  *
4  * Written by the libgit2 contributors
5  *
6  * To the extent possible under law, the author(s) have dedicated all copyright
7  * and related and neighboring rights to this software to the public domain
8  * worldwide. This software is distributed without any warranty.
9  *
10  * You should have received a copy of the CC0 Public Domain Dedication along
11  * with this software. If not, see
12  * <http://creativecommons.org/publicdomain/zero/1.0/>.
13  */
14 
15 #![deny(warnings)]
16 
17 use git2::{Blob, Diff, DiffOptions, Error, Object, ObjectType, Oid, Repository};
18 use git2::{DiffDelta, DiffFindOptions, DiffFormat, DiffHunk, DiffLine};
19 use std::str;
20 use structopt::StructOpt;
21 
22 #[derive(StructOpt)]
23 #[allow(non_snake_case)]
24 struct Args {
25     #[structopt(name = "from_oid")]
26     arg_from_oid: Option<String>,
27     #[structopt(name = "to_oid")]
28     arg_to_oid: Option<String>,
29     #[structopt(name = "blobs", long)]
30     /// treat from_oid and to_oid as blob ids
31     flag_blobs: bool,
32     #[structopt(name = "patch", short, long)]
33     /// show output in patch format
34     flag_patch: bool,
35     #[structopt(name = "cached", long)]
36     /// use staged changes as diff
37     flag_cached: bool,
38     #[structopt(name = "nocached", long)]
39     /// do not use staged changes
40     flag_nocached: bool,
41     #[structopt(name = "name-only", long)]
42     /// show only names of changed files
43     flag_name_only: bool,
44     #[structopt(name = "name-status", long)]
45     /// show only names and status changes
46     flag_name_status: bool,
47     #[structopt(name = "raw", long)]
48     /// generate the raw format
49     flag_raw: bool,
50     #[structopt(name = "format", long)]
51     /// specify format for stat summary
52     flag_format: Option<String>,
53     #[structopt(name = "color", long)]
54     /// use color output
55     flag_color: bool,
56     #[structopt(name = "no-color", long)]
57     /// never use color output
58     flag_no_color: bool,
59     #[structopt(short = "R")]
60     /// swap two inputs
61     flag_R: bool,
62     #[structopt(name = "text", short = "a", long)]
63     /// treat all files as text
64     flag_text: bool,
65     #[structopt(name = "ignore-space-at-eol", long)]
66     /// ignore changes in whitespace at EOL
67     flag_ignore_space_at_eol: bool,
68     #[structopt(name = "ignore-space-change", short = "b", long)]
69     /// ignore changes in amount of whitespace
70     flag_ignore_space_change: bool,
71     #[structopt(name = "ignore-all-space", short = "w", long)]
72     /// ignore whitespace when comparing lines
73     flag_ignore_all_space: bool,
74     #[structopt(name = "ignored", long)]
75     /// show untracked files
76     flag_ignored: bool,
77     #[structopt(name = "untracked", long)]
78     /// generate diff using the patience algorithm
79     flag_untracked: bool,
80     #[structopt(name = "patience", long)]
81     /// show ignored files as well
82     flag_patience: bool,
83     #[structopt(name = "minimal", long)]
84     /// spend extra time to find smallest diff
85     flag_minimal: bool,
86     #[structopt(name = "stat", long)]
87     /// generate a diffstat
88     flag_stat: bool,
89     #[structopt(name = "numstat", long)]
90     /// similar to --stat, but more machine friendly
91     flag_numstat: bool,
92     #[structopt(name = "shortstat", long)]
93     /// only output last line of --stat
94     flag_shortstat: bool,
95     #[structopt(name = "summary", long)]
96     /// output condensed summary of header info
97     flag_summary: bool,
98     #[structopt(name = "find-renames", short = "M", long)]
99     /// set threshold for findind renames (default 50)
100     flag_find_renames: Option<u16>,
101     #[structopt(name = "find-copies", short = "C", long)]
102     /// set threshold for finding copies (default 50)
103     flag_find_copies: Option<u16>,
104     #[structopt(name = "find-copies-harder", long)]
105     /// inspect unmodified files for sources of copies
106     flag_find_copies_harder: bool,
107     #[structopt(name = "break_rewrites", short = "B", long)]
108     /// break complete rewrite changes into pairs
109     flag_break_rewrites: bool,
110     #[structopt(name = "unified", short = "U", long)]
111     /// lints of context to show
112     flag_unified: Option<u32>,
113     #[structopt(name = "inter-hunk-context", long)]
114     /// maximum lines of change between hunks
115     flag_inter_hunk_context: Option<u32>,
116     #[structopt(name = "abbrev", long)]
117     /// length to abbreviate commits to
118     flag_abbrev: Option<u16>,
119     #[structopt(name = "src-prefix", long)]
120     /// show given source prefix instead of 'a/'
121     flag_src_prefix: Option<String>,
122     #[structopt(name = "dst-prefix", long)]
123     /// show given destinction prefix instead of 'b/'
124     flag_dst_prefix: Option<String>,
125     #[structopt(name = "path", long = "git-dir")]
126     /// path to git repository to use
127     flag_git_dir: Option<String>,
128 }
129 
130 const RESET: &str = "\u{1b}[m";
131 const BOLD: &str = "\u{1b}[1m";
132 const RED: &str = "\u{1b}[31m";
133 const GREEN: &str = "\u{1b}[32m";
134 const CYAN: &str = "\u{1b}[36m";
135 
136 #[derive(PartialEq, Eq, Copy, Clone)]
137 enum Cache {
138     Normal,
139     Only,
140     None,
141 }
142 
line_color(line: &DiffLine) -> Option<&'static str>143 fn line_color(line: &DiffLine) -> Option<&'static str> {
144     match line.origin() {
145         '+' => Some(GREEN),
146         '-' => Some(RED),
147         '>' => Some(GREEN),
148         '<' => Some(RED),
149         'F' => Some(BOLD),
150         'H' => Some(CYAN),
151         _ => None,
152     }
153 }
154 
print_diff_line( _delta: DiffDelta, _hunk: Option<DiffHunk>, line: DiffLine, args: &Args, ) -> bool155 fn print_diff_line(
156     _delta: DiffDelta,
157     _hunk: Option<DiffHunk>,
158     line: DiffLine,
159     args: &Args,
160 ) -> bool {
161     if args.color() {
162         print!("{}", RESET);
163         if let Some(color) = line_color(&line) {
164             print!("{}", color);
165         }
166     }
167     match line.origin() {
168         '+' | '-' | ' ' => print!("{}", line.origin()),
169         _ => {}
170     }
171     print!("{}", str::from_utf8(line.content()).unwrap());
172     true
173 }
174 
run(args: &Args) -> Result<(), Error>175 fn run(args: &Args) -> Result<(), Error> {
176     let path = args.flag_git_dir.as_ref().map(|s| &s[..]).unwrap_or(".");
177     let repo = Repository::open(path)?;
178 
179     // Prepare our diff options based on the arguments given
180     let mut opts = DiffOptions::new();
181     opts.reverse(args.flag_R)
182         .force_text(args.flag_text)
183         .ignore_whitespace_eol(args.flag_ignore_space_at_eol)
184         .ignore_whitespace_change(args.flag_ignore_space_change)
185         .ignore_whitespace(args.flag_ignore_all_space)
186         .include_ignored(args.flag_ignored)
187         .include_untracked(args.flag_untracked)
188         .patience(args.flag_patience)
189         .minimal(args.flag_minimal);
190     if let Some(amt) = args.flag_unified {
191         opts.context_lines(amt);
192     }
193     if let Some(amt) = args.flag_inter_hunk_context {
194         opts.interhunk_lines(amt);
195     }
196     if let Some(amt) = args.flag_abbrev {
197         opts.id_abbrev(amt);
198     }
199     if let Some(ref s) = args.flag_src_prefix {
200         opts.old_prefix(&s);
201     }
202     if let Some(ref s) = args.flag_dst_prefix {
203         opts.new_prefix(&s);
204     }
205     if let Some("diff-index") = args.flag_format.as_ref().map(|s| &s[..]) {
206         opts.id_abbrev(40);
207     }
208 
209     if args.flag_blobs {
210         let b1 = resolve_blob(&repo, args.arg_from_oid.as_ref())?;
211         let b2 = resolve_blob(&repo, args.arg_to_oid.as_ref())?;
212         repo.diff_blobs(
213             b1.as_ref(),
214             None,
215             b2.as_ref(),
216             None,
217             Some(&mut opts),
218             None,
219             None,
220             None,
221             Some(&mut |d, h, l| print_diff_line(d, h, l, args)),
222         )?;
223         if args.color() {
224             print!("{}", RESET);
225         }
226         return Ok(());
227     }
228 
229     // Prepare the diff to inspect
230     let t1 = tree_to_treeish(&repo, args.arg_from_oid.as_ref())?;
231     let t2 = tree_to_treeish(&repo, args.arg_to_oid.as_ref())?;
232     let head = tree_to_treeish(&repo, Some(&"HEAD".to_string()))?.unwrap();
233     let mut diff = match (t1, t2, args.cache()) {
234         (Some(t1), Some(t2), _) => {
235             repo.diff_tree_to_tree(t1.as_tree(), t2.as_tree(), Some(&mut opts))?
236         }
237         (t1, None, Cache::None) => {
238             let t1 = t1.unwrap_or(head);
239             repo.diff_tree_to_workdir(t1.as_tree(), Some(&mut opts))?
240         }
241         (t1, None, Cache::Only) => {
242             let t1 = t1.unwrap_or(head);
243             repo.diff_tree_to_index(t1.as_tree(), None, Some(&mut opts))?
244         }
245         (Some(t1), None, _) => {
246             repo.diff_tree_to_workdir_with_index(t1.as_tree(), Some(&mut opts))?
247         }
248         (None, None, _) => repo.diff_index_to_workdir(None, Some(&mut opts))?,
249         (None, Some(_), _) => unreachable!(),
250     };
251 
252     // Apply rename and copy detection if requested
253     if args.flag_break_rewrites
254         || args.flag_find_copies_harder
255         || args.flag_find_renames.is_some()
256         || args.flag_find_copies.is_some()
257     {
258         let mut opts = DiffFindOptions::new();
259         if let Some(t) = args.flag_find_renames {
260             opts.rename_threshold(t);
261             opts.renames(true);
262         }
263         if let Some(t) = args.flag_find_copies {
264             opts.copy_threshold(t);
265             opts.copies(true);
266         }
267         opts.copies_from_unmodified(args.flag_find_copies_harder)
268             .rewrites(args.flag_break_rewrites);
269         diff.find_similar(Some(&mut opts))?;
270     }
271 
272     // Generate simple output
273     let stats = args.flag_stat | args.flag_numstat | args.flag_shortstat | args.flag_summary;
274     if stats {
275         print_stats(&diff, args)?;
276     }
277     if args.flag_patch || !stats {
278         diff.print(args.diff_format(), |d, h, l| print_diff_line(d, h, l, args))?;
279         if args.color() {
280             print!("{}", RESET);
281         }
282     }
283 
284     Ok(())
285 }
286 
print_stats(diff: &Diff, args: &Args) -> Result<(), Error>287 fn print_stats(diff: &Diff, args: &Args) -> Result<(), Error> {
288     let stats = diff.stats()?;
289     let mut format = git2::DiffStatsFormat::NONE;
290     if args.flag_stat {
291         format |= git2::DiffStatsFormat::FULL;
292     }
293     if args.flag_shortstat {
294         format |= git2::DiffStatsFormat::SHORT;
295     }
296     if args.flag_numstat {
297         format |= git2::DiffStatsFormat::NUMBER;
298     }
299     if args.flag_summary {
300         format |= git2::DiffStatsFormat::INCLUDE_SUMMARY;
301     }
302     let buf = stats.to_buf(format, 80)?;
303     print!("{}", str::from_utf8(&*buf).unwrap());
304     Ok(())
305 }
306 
tree_to_treeish<'a>( repo: &'a Repository, arg: Option<&String>, ) -> Result<Option<Object<'a>>, Error>307 fn tree_to_treeish<'a>(
308     repo: &'a Repository,
309     arg: Option<&String>,
310 ) -> Result<Option<Object<'a>>, Error> {
311     let arg = match arg {
312         Some(s) => s,
313         None => return Ok(None),
314     };
315     let obj = repo.revparse_single(arg)?;
316     let tree = obj.peel(ObjectType::Tree)?;
317     Ok(Some(tree))
318 }
319 
resolve_blob<'a>(repo: &'a Repository, arg: Option<&String>) -> Result<Option<Blob<'a>>, Error>320 fn resolve_blob<'a>(repo: &'a Repository, arg: Option<&String>) -> Result<Option<Blob<'a>>, Error> {
321     let arg = match arg {
322         Some(s) => Oid::from_str(s)?,
323         None => return Ok(None),
324     };
325     repo.find_blob(arg).map(|b| Some(b))
326 }
327 
328 impl Args {
cache(&self) -> Cache329     fn cache(&self) -> Cache {
330         if self.flag_cached {
331             Cache::Only
332         } else if self.flag_nocached {
333             Cache::None
334         } else {
335             Cache::Normal
336         }
337     }
color(&self) -> bool338     fn color(&self) -> bool {
339         self.flag_color && !self.flag_no_color
340     }
diff_format(&self) -> DiffFormat341     fn diff_format(&self) -> DiffFormat {
342         if self.flag_patch {
343             DiffFormat::Patch
344         } else if self.flag_name_only {
345             DiffFormat::NameOnly
346         } else if self.flag_name_status {
347             DiffFormat::NameStatus
348         } else if self.flag_raw {
349             DiffFormat::Raw
350         } else {
351             match self.flag_format.as_ref().map(|s| &s[..]) {
352                 Some("name") => DiffFormat::NameOnly,
353                 Some("name-status") => DiffFormat::NameStatus,
354                 Some("raw") => DiffFormat::Raw,
355                 Some("diff-index") => DiffFormat::Raw,
356                 _ => DiffFormat::Patch,
357             }
358         }
359     }
360 }
361 
main()362 fn main() {
363     let args = Args::from_args();
364     match run(&args) {
365         Ok(()) => {}
366         Err(e) => println!("error: {}", e),
367     }
368 }
369