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