Skip to main content

patch/
patch.rs

1//! Print the `git diff` patch between two revisions: `cargo run --example patch`.
2//!
3//! Run without arguments for a disposable fixture, or pass a repository and two revisions.
4use std::io::{self, Write as _};
5use std::path::PathBuf;
6use std::sync::atomic::AtomicBool;
7
8use clap::Parser;
9use girt::config::ConfigInputs;
10use girt::content_diff::DiffLimits;
11use girt::patch::{Limits, Options};
12use girt::revision::Resolver;
13use girt::tree::TreeEditor;
14use girt::workflow::Environment;
15use girt::{
16    InitKind, ObjectFormat, ObjectKind, PackLimits, ReadLimits, Repo, Repository, rewrites,
17};
18
19#[derive(Parser)]
20#[command(about, long_about = None)]
21struct Args {
22    /// Existing repository. Omit to use a disposable fixture.
23    path: Option<PathBuf>,
24    /// Old revision.
25    #[arg(default_value = "HEAD~1")]
26    old: String,
27    /// New revision.
28    #[arg(default_value = "HEAD")]
29    new: String,
30    /// Unchanged lines around each change, as `git diff --unified`.
31    #[arg(short = 'U', long, default_value_t = 3)]
32    unified: usize,
33    /// Lift the default storage and content limits, for a large repository you trust.
34    #[arg(long)]
35    trusted: bool,
36    /// Disable indentation scoring while retaining change-group compaction.
37    #[arg(long)]
38    no_indent_heuristic: bool,
39}
40
41fn main() -> Result<(), Box<dyn std::error::Error>> {
42    let args = Args::parse();
43    let temp = tempfile::tempdir()?;
44    let mut out = io::BufWriter::new(io::stdout().lock());
45
46    let Some(path) = args.path else {
47        // The fixture compares two trees directly; no commits are needed.
48        let repo = Repository::init(ObjectFormat::Sha1, temp.path().join("r"), InitKind::Bare)?;
49        let loose = repo.loose_objects();
50        let mut editor = TreeEditor::new(&loose);
51        editor.set(b"notes.txt", loose.write_blob(b"one\ntwo\nthree\n")?)?;
52        editor.set(b"old name", loose.write_blob(b"moved\ncontent\n")?)?;
53        let old = editor.write()?;
54
55        editor.set(b"notes.txt", loose.write_blob(b"one\n2\nthree")?)?;
56        editor.remove(b"old name")?;
57        editor.set(b"new name", loose.write_blob(b"moved\ncontent\n")?)?;
58        let new = editor.write()?;
59
60        let git = Repo::new(repo);
61        git.write_patch(&mut out, old, new)?;
62        out.flush()?;
63        return Ok(());
64    };
65
66    if args.unified == 3 && !args.trusted && !args.no_indent_heuristic {
67        Repo::discover(path)?.write_patch(&mut out, &args.old, &args.new)?;
68        out.flush()?;
69        return Ok(());
70    }
71
72    // Explicit options and limits go through the core API the handle composes.
73    let environment = Environment::from_process();
74    let repo = Repository::discover_with_config(path, &ConfigInputs::git_default(&environment)?)?;
75
76    let (packs, rewrite_limits, limits) = if args.trusted {
77        let diff = DiffLimits::default()
78            .with_max_input_bytes(usize::MAX)
79            .with_max_lines(usize::MAX)
80            .with_max_trace(usize::MAX)
81            .with_max_work(usize::MAX);
82
83        // Keep the default rename candidate limit, which matches Git's, but lift resources.
84        let rewrites = rewrites::Limits::default()
85            .with_read(ReadLimits::trusted())
86            .with_max_blob_bytes(usize::MAX)
87            .with_max_spans(usize::MAX)
88            .with_max_work(usize::MAX);
89
90        (
91            PackLimits::trusted(),
92            rewrites,
93            Limits::default()
94                .with_read(ReadLimits::trusted())
95                .with_diff(diff),
96        )
97    } else {
98        (
99            PackLimits::default(),
100            rewrites::Limits::default(),
101            Limits::default(),
102        )
103    };
104
105    let objects = repo.objects_with(packs)?;
106    let resolver = Resolver::new(&repo, &objects)?;
107    let old = resolver.resolve_to(&args.old.parse()?, ObjectKind::Tree)?;
108    let new = resolver.resolve_to(&args.new.parse()?, ObjectKind::Tree)?;
109
110    let cancel = AtomicBool::new(false);
111    let comparison = objects.diff_trees_with(
112        Some(old),
113        Some(new),
114        rewrites::Options::default().with_track_empty(true),
115        rewrite_limits,
116        &cancel,
117    )?;
118
119    // Apply `.gitattributes` and `diff.<driver>` settings as `git diff` does.
120    let mut attributes = repo.diff_attributes(&environment)?;
121    objects.write_patch_with(
122        &mut out,
123        &comparison.entries,
124        Options::default()
125            .with_context_lines(args.unified)
126            .with_indent_heuristic(!args.no_indent_heuristic),
127        Some(&mut attributes),
128        limits,
129        &cancel,
130    )?;
131
132    out.flush()?;
133    Ok(())
134}