mirror of
https://git.proxmox.com/git/rustc
synced 2025-08-13 13:27:08 +00:00
369 lines
12 KiB
Rust
369 lines
12 KiB
Rust
/*
|
|
* 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
|
|
* <http://creativecommons.org/publicdomain/zero/1.0/>.
|
|
*/
|
|
|
|
#![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<String>,
|
|
#[structopt(name = "to_oid")]
|
|
arg_to_oid: Option<String>,
|
|
#[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<String>,
|
|
#[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<u16>,
|
|
#[structopt(name = "find-copies", short = 'C', long)]
|
|
/// set threshold for finding copies (default 50)
|
|
flag_find_copies: Option<u16>,
|
|
#[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<u32>,
|
|
#[structopt(name = "inter-hunk-context", long)]
|
|
/// maximum lines of change between hunks
|
|
flag_inter_hunk_context: Option<u32>,
|
|
#[structopt(name = "abbrev", long)]
|
|
/// length to abbreviate commits to
|
|
flag_abbrev: Option<u16>,
|
|
#[structopt(name = "src-prefix", long)]
|
|
/// show given source prefix instead of 'a/'
|
|
flag_src_prefix: Option<String>,
|
|
#[structopt(name = "dst-prefix", long)]
|
|
/// show given destination prefix instead of 'b/'
|
|
flag_dst_prefix: Option<String>,
|
|
#[structopt(name = "path", long = "git-dir")]
|
|
/// path to git repository to use
|
|
flag_git_dir: Option<String>,
|
|
}
|
|
|
|
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<DiffHunk>,
|
|
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<Option<Object<'a>>, Error> {
|
|
let arg = match arg {
|
|
Some(s) => s,
|
|
None => return Ok(None),
|
|
};
|
|
let obj = repo.revparse_single(arg)?;
|
|
let tree = obj.peel(ObjectType::Tree)?;
|
|
Ok(Some(tree))
|
|
}
|
|
|
|
fn resolve_blob<'a>(repo: &'a Repository, arg: Option<&String>) -> Result<Option<Blob<'a>>, Error> {
|
|
let arg = match arg {
|
|
Some(s) => Oid::from_str(s)?,
|
|
None => return Ok(None),
|
|
};
|
|
repo.find_blob(arg).map(|b| Some(b))
|
|
}
|
|
|
|
impl Args {
|
|
fn cache(&self) -> Cache {
|
|
if self.flag_cached {
|
|
Cache::Only
|
|
} else if self.flag_nocached {
|
|
Cache::None
|
|
} else {
|
|
Cache::Normal
|
|
}
|
|
}
|
|
fn color(&self) -> bool {
|
|
self.flag_color && !self.flag_no_color
|
|
}
|
|
fn diff_format(&self) -> DiffFormat {
|
|
if self.flag_patch {
|
|
DiffFormat::Patch
|
|
} else if self.flag_name_only {
|
|
DiffFormat::NameOnly
|
|
} else if self.flag_name_status {
|
|
DiffFormat::NameStatus
|
|
} else if self.flag_raw {
|
|
DiffFormat::Raw
|
|
} else {
|
|
match self.flag_format.as_ref().map(|s| &s[..]) {
|
|
Some("name") => DiffFormat::NameOnly,
|
|
Some("name-status") => DiffFormat::NameStatus,
|
|
Some("raw") => DiffFormat::Raw,
|
|
Some("diff-index") => DiffFormat::Raw,
|
|
_ => DiffFormat::Patch,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn main() {
|
|
let args = Args::parse();
|
|
match run(&args) {
|
|
Ok(()) => {}
|
|
Err(e) => println!("error: {}", e),
|
|
}
|
|
}
|