/*
* libgit2 "diff" example - shows how to use the diff API
*
* Written by the libgit2 contributors
*
* To the extent possible under law, the author(s) have dedicated all copyright
* and related and neighboring rights to this software to the public domain
* worldwide. This software is distributed without any warranty.
*
* You should have received a copy of the CC0 Public Domain Dedication along
* with this software. If not, see
* .
*/
#![deny(warnings)]
use clap::Parser;
use git2::{Blob, Diff, DiffOptions, Error, Object, ObjectType, Oid, Repository};
use git2::{DiffDelta, DiffFindOptions, DiffFormat, DiffHunk, DiffLine};
use std::str;
#[derive(Parser)]
#[allow(non_snake_case)]
struct Args {
#[structopt(name = "from_oid")]
arg_from_oid: Option,
#[structopt(name = "to_oid")]
arg_to_oid: Option,
#[structopt(name = "blobs", long)]
/// treat from_oid and to_oid as blob ids
flag_blobs: bool,
#[structopt(name = "patch", short, long)]
/// show output in patch format
flag_patch: bool,
#[structopt(name = "cached", long)]
/// use staged changes as diff
flag_cached: bool,
#[structopt(name = "nocached", long)]
/// do not use staged changes
flag_nocached: bool,
#[structopt(name = "name-only", long)]
/// show only names of changed files
flag_name_only: bool,
#[structopt(name = "name-status", long)]
/// show only names and status changes
flag_name_status: bool,
#[structopt(name = "raw", long)]
/// generate the raw format
flag_raw: bool,
#[structopt(name = "format", long)]
/// specify format for stat summary
flag_format: Option,
#[structopt(name = "color", long)]
/// use color output
flag_color: bool,
#[structopt(name = "no-color", long)]
/// never use color output
flag_no_color: bool,
#[structopt(short = 'R')]
/// swap two inputs
flag_R: bool,
#[structopt(name = "text", short = 'a', long)]
/// treat all files as text
flag_text: bool,
#[structopt(name = "ignore-space-at-eol", long)]
/// ignore changes in whitespace at EOL
flag_ignore_space_at_eol: bool,
#[structopt(name = "ignore-space-change", short = 'b', long)]
/// ignore changes in amount of whitespace
flag_ignore_space_change: bool,
#[structopt(name = "ignore-all-space", short = 'w', long)]
/// ignore whitespace when comparing lines
flag_ignore_all_space: bool,
#[structopt(name = "ignored", long)]
/// show untracked files
flag_ignored: bool,
#[structopt(name = "untracked", long)]
/// generate diff using the patience algorithm
flag_untracked: bool,
#[structopt(name = "patience", long)]
/// show ignored files as well
flag_patience: bool,
#[structopt(name = "minimal", long)]
/// spend extra time to find smallest diff
flag_minimal: bool,
#[structopt(name = "stat", long)]
/// generate a diffstat
flag_stat: bool,
#[structopt(name = "numstat", long)]
/// similar to --stat, but more machine friendly
flag_numstat: bool,
#[structopt(name = "shortstat", long)]
/// only output last line of --stat
flag_shortstat: bool,
#[structopt(name = "summary", long)]
/// output condensed summary of header info
flag_summary: bool,
#[structopt(name = "find-renames", short = 'M', long)]
/// set threshold for finding renames (default 50)
flag_find_renames: Option,
#[structopt(name = "find-copies", short = 'C', long)]
/// set threshold for finding copies (default 50)
flag_find_copies: Option,
#[structopt(name = "find-copies-harder", long)]
/// inspect unmodified files for sources of copies
flag_find_copies_harder: bool,
#[structopt(name = "break_rewrites", short = 'B', long)]
/// break complete rewrite changes into pairs
flag_break_rewrites: bool,
#[structopt(name = "unified", short = 'U', long)]
/// lints of context to show
flag_unified: Option,
#[structopt(name = "inter-hunk-context", long)]
/// maximum lines of change between hunks
flag_inter_hunk_context: Option,
#[structopt(name = "abbrev", long)]
/// length to abbreviate commits to
flag_abbrev: Option,
#[structopt(name = "src-prefix", long)]
/// show given source prefix instead of 'a/'
flag_src_prefix: Option,
#[structopt(name = "dst-prefix", long)]
/// show given destination prefix instead of 'b/'
flag_dst_prefix: Option,
#[structopt(name = "path", long = "git-dir")]
/// path to git repository to use
flag_git_dir: Option,
}
const RESET: &str = "\u{1b}[m";
const BOLD: &str = "\u{1b}[1m";
const RED: &str = "\u{1b}[31m";
const GREEN: &str = "\u{1b}[32m";
const CYAN: &str = "\u{1b}[36m";
#[derive(PartialEq, Eq, Copy, Clone)]
enum Cache {
Normal,
Only,
None,
}
fn line_color(line: &DiffLine) -> Option<&'static str> {
match line.origin() {
'+' => Some(GREEN),
'-' => Some(RED),
'>' => Some(GREEN),
'<' => Some(RED),
'F' => Some(BOLD),
'H' => Some(CYAN),
_ => None,
}
}
fn print_diff_line(
_delta: DiffDelta,
_hunk: Option,
line: DiffLine,
args: &Args,
) -> bool {
if args.color() {
print!("{}", RESET);
if let Some(color) = line_color(&line) {
print!("{}", color);
}
}
match line.origin() {
'+' | '-' | ' ' => print!("{}", line.origin()),
_ => {}
}
print!("{}", str::from_utf8(line.content()).unwrap());
true
}
fn run(args: &Args) -> Result<(), Error> {
let path = args.flag_git_dir.as_ref().map(|s| &s[..]).unwrap_or(".");
let repo = Repository::open(path)?;
// Prepare our diff options based on the arguments given
let mut opts = DiffOptions::new();
opts.reverse(args.flag_R)
.force_text(args.flag_text)
.ignore_whitespace_eol(args.flag_ignore_space_at_eol)
.ignore_whitespace_change(args.flag_ignore_space_change)
.ignore_whitespace(args.flag_ignore_all_space)
.include_ignored(args.flag_ignored)
.include_untracked(args.flag_untracked)
.patience(args.flag_patience)
.minimal(args.flag_minimal);
if let Some(amt) = args.flag_unified {
opts.context_lines(amt);
}
if let Some(amt) = args.flag_inter_hunk_context {
opts.interhunk_lines(amt);
}
if let Some(amt) = args.flag_abbrev {
opts.id_abbrev(amt);
}
if let Some(ref s) = args.flag_src_prefix {
opts.old_prefix(&s);
}
if let Some(ref s) = args.flag_dst_prefix {
opts.new_prefix(&s);
}
if let Some("diff-index") = args.flag_format.as_ref().map(|s| &s[..]) {
opts.id_abbrev(40);
}
if args.flag_blobs {
let b1 = resolve_blob(&repo, args.arg_from_oid.as_ref())?;
let b2 = resolve_blob(&repo, args.arg_to_oid.as_ref())?;
repo.diff_blobs(
b1.as_ref(),
None,
b2.as_ref(),
None,
Some(&mut opts),
None,
None,
None,
Some(&mut |d, h, l| print_diff_line(d, h, l, args)),
)?;
if args.color() {
print!("{}", RESET);
}
return Ok(());
}
// Prepare the diff to inspect
let t1 = tree_to_treeish(&repo, args.arg_from_oid.as_ref())?;
let t2 = tree_to_treeish(&repo, args.arg_to_oid.as_ref())?;
let head = tree_to_treeish(&repo, Some(&"HEAD".to_string()))?.unwrap();
let mut diff = match (t1, t2, args.cache()) {
(Some(t1), Some(t2), _) => {
repo.diff_tree_to_tree(t1.as_tree(), t2.as_tree(), Some(&mut opts))?
}
(t1, None, Cache::None) => {
let t1 = t1.unwrap_or(head);
repo.diff_tree_to_workdir(t1.as_tree(), Some(&mut opts))?
}
(t1, None, Cache::Only) => {
let t1 = t1.unwrap_or(head);
repo.diff_tree_to_index(t1.as_tree(), None, Some(&mut opts))?
}
(Some(t1), None, _) => {
repo.diff_tree_to_workdir_with_index(t1.as_tree(), Some(&mut opts))?
}
(None, None, _) => repo.diff_index_to_workdir(None, Some(&mut opts))?,
(None, Some(_), _) => unreachable!(),
};
// Apply rename and copy detection if requested
if args.flag_break_rewrites
|| args.flag_find_copies_harder
|| args.flag_find_renames.is_some()
|| args.flag_find_copies.is_some()
{
let mut opts = DiffFindOptions::new();
if let Some(t) = args.flag_find_renames {
opts.rename_threshold(t);
opts.renames(true);
}
if let Some(t) = args.flag_find_copies {
opts.copy_threshold(t);
opts.copies(true);
}
opts.copies_from_unmodified(args.flag_find_copies_harder)
.rewrites(args.flag_break_rewrites);
diff.find_similar(Some(&mut opts))?;
}
// Generate simple output
let stats = args.flag_stat | args.flag_numstat | args.flag_shortstat | args.flag_summary;
if stats {
print_stats(&diff, args)?;
}
if args.flag_patch || !stats {
diff.print(args.diff_format(), |d, h, l| print_diff_line(d, h, l, args))?;
if args.color() {
print!("{}", RESET);
}
}
Ok(())
}
fn print_stats(diff: &Diff, args: &Args) -> Result<(), Error> {
let stats = diff.stats()?;
let mut format = git2::DiffStatsFormat::NONE;
if args.flag_stat {
format |= git2::DiffStatsFormat::FULL;
}
if args.flag_shortstat {
format |= git2::DiffStatsFormat::SHORT;
}
if args.flag_numstat {
format |= git2::DiffStatsFormat::NUMBER;
}
if args.flag_summary {
format |= git2::DiffStatsFormat::INCLUDE_SUMMARY;
}
let buf = stats.to_buf(format, 80)?;
print!("{}", str::from_utf8(&*buf).unwrap());
Ok(())
}
fn tree_to_treeish<'a>(
repo: &'a Repository,
arg: Option<&String>,
) -> Result