1use std::borrow::Cow;
4use std::collections::HashMap;
5use std::io::Write;
6use std::sync::Arc;
7
8use super::structured::{StructuredFileDiff, StructuredLine, StructuredLineKind};
9use crate::attributes::load_gitattributes_for_check_attr_source;
10use crate::commit_pretty::{
11 abbrev_hex, approximate_object_count, auto_abbrev_len_for_object_count,
12};
13use crate::config::ConfigSet;
14use crate::diff::options::DiffOptions;
15use crate::error::{Error, Result};
16use crate::index::Index;
17use crate::merge_file;
18use crate::objects::pack_store::PackReadContextGuard;
19use crate::objects::CommitData;
20use crate::objects::FileMode;
21use crate::objects::{parse_commit, ObjectId, ObjectKind};
22use crate::odb::Odb;
23use crate::oid_abbrev_cache::OidAbbrevCache;
24use crate::parallel::{run_ordered_batches, ParallelOp};
25use crate::quote_path::format_diff_path_with_prefix;
26use crate::repo::Repository;
27use crate::userdiff::{matcher_for_path_parsed, FuncnameMatcher};
28use crate::worktree_rules::WorktreeRules;
29
30use super::merge_commit::commit_first_parent_diff_entries;
31use super::merge_commit::DiffMerges;
32use super::tree_content::load_odb_blob_side_for_diff;
33use super::{empty_blob_oid, load_worktree_side_for_diff, zero_oid, DiffEntry, DiffStatus};
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
37pub enum LogPatchCommitHeader {
38 #[default]
40 Full,
41 FormatPercentH,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
47pub enum LogPatchBinaryFormat {
48 #[default]
50 LogShortMessage,
51 GitApplyBinaryPatch,
53}
54
55#[derive(Debug, Clone)]
57pub struct LogPatchOptions {
58 pub diff: DiffOptions,
60 pub abbrev_len: usize,
62 pub commit_header: LogPatchCommitHeader,
64 pub binary_format: LogPatchBinaryFormat,
66 pub pathspecs: Vec<String>,
68 pub diff_merges: DiffMerges,
70}
71
72impl LogPatchOptions {
73 #[must_use]
75 pub fn git_default(config: &ConfigSet) -> Self {
76 Self {
77 diff: DiffOptions::from_config(config),
78 abbrev_len: 7,
79 commit_header: LogPatchCommitHeader::Full,
80 binary_format: LogPatchBinaryFormat::default(),
81 pathspecs: Vec::new(),
82 diff_merges: DiffMerges::Combined,
83 }
84 }
85}
86
87#[derive(Debug, Clone)]
89pub struct PatchTextOptions {
90 pub diff: DiffOptions,
92 pub abbrev_len: usize,
94}
95
96impl PatchTextOptions {
97 pub fn from_repo(repo: &Repository, diff: DiffOptions) -> Result<Self> {
103 let config = repo.config()?.clone();
104 Ok(Self {
105 diff,
106 abbrev_len: resolve_core_abbrev_len(&config, repo.odb())?,
107 })
108 }
109}
110
111pub fn write_patch<W: Write>(
119 repo: &Repository,
120 entries: &[DiffEntry],
121 opts: &PatchTextOptions,
122 out: &mut W,
123) -> Result<()> {
124 if entries.is_empty() {
125 return Ok(());
126 }
127 let odb = repo.odb();
128 super::tree_content::prefetch_tree_diff_blobs(odb, entries)?;
129 let config = repo.config()?.clone();
130 let thread_policy = repo.thread_policy();
131 let log_opts = LogPatchOptions {
132 diff: opts.diff.clone(),
133 abbrev_len: opts.abbrev_len,
134 commit_header: LogPatchCommitHeader::FormatPercentH,
135 binary_format: LogPatchBinaryFormat::GitApplyBinaryPatch,
136 pathspecs: Vec::new(),
137 diff_merges: DiffMerges::FirstParent,
138 };
139 let abbrev_len = opts.abbrev_len;
140 let mut abbrev_cache = OidAbbrevCache::new(abbrev_len);
141 let oids = entries.iter().flat_map(|e| [e.old_oid, e.new_oid]);
142 abbrev_cache.warm_for_patch_batch(repo, oids, &thread_policy, &config)?;
143 let mut par_opts = thread_policy.resolve(ParallelOp::DiffContent, &config);
144 par_opts.min_items = 0;
145 par_opts.min_total_bytes = None;
146 let n_workers = par_opts.threads.get();
147 if n_workers <= 1 || entries.len() < 2 {
148 for entry in entries {
149 write_diff_entry_patch(
150 out,
151 repo,
152 odb,
153 entry,
154 &log_opts,
155 &mut abbrev_cache,
156 None,
157 LogPatchBinaryFormat::GitApplyBinaryPatch,
158 &config,
159 )?;
160 }
161 return Ok(());
162 }
163 let store = odb.pack_store_for(odb.objects_dir());
164 let warmed_cache = abbrev_cache;
165 struct PatchWriteWorker {
166 _read: PackReadContextGuard,
167 abbrev: OidAbbrevCache,
168 }
169 run_ordered_batches(
170 entries,
171 1,
172 n_workers,
173 || PatchWriteWorker {
174 _read: PackReadContextGuard::install(Arc::clone(&store)),
175 abbrev: warmed_cache.clone(),
176 },
177 |worker, entry| {
178 let mut buf = Vec::new();
179 write_diff_entry_patch(
180 &mut buf,
181 repo,
182 odb,
183 entry,
184 &log_opts,
185 &mut worker.abbrev,
186 None,
187 LogPatchBinaryFormat::GitApplyBinaryPatch,
188 &config,
189 )?;
190 Ok(buf)
191 },
192 |_batch, chunks: Vec<Vec<u8>>| {
193 for chunk in chunks {
194 out.write_all(&chunk)?;
195 }
196 Ok(())
197 },
198 )
199}
200
201pub fn write_patch_structured<W: Write>(
210 repo: &Repository,
211 items: &[super::tree_content::TreeDiffStructured],
212 opts: &PatchTextOptions,
213 out: &mut W,
214) -> Result<()> {
215 if items.is_empty() {
216 return Ok(());
217 }
218 let odb = repo.odb();
219 let config = repo.config()?.clone();
220 let thread_policy = repo.thread_policy();
221 let log_opts = LogPatchOptions {
222 diff: opts.diff.clone(),
223 abbrev_len: opts.abbrev_len,
224 commit_header: LogPatchCommitHeader::FormatPercentH,
225 binary_format: LogPatchBinaryFormat::GitApplyBinaryPatch,
226 pathspecs: Vec::new(),
227 diff_merges: DiffMerges::FirstParent,
228 };
229 let mut abbrev_cache = OidAbbrevCache::new(opts.abbrev_len);
230 let oids = items
231 .iter()
232 .flat_map(|item| [item.entry.old_oid, item.entry.new_oid]);
233 abbrev_cache.warm_for_patch_batch(repo, oids, &thread_policy, &config)?;
234 for item in items {
235 write_diff_entry_patch_from_structured(
236 out,
237 repo,
238 odb,
239 &item.entry,
240 &log_opts,
241 &mut abbrev_cache,
242 item.binary,
243 &item.structured,
244 )?;
245 }
246 Ok(())
247}
248
249fn write_structured_line<W: Write>(out: &mut W, prefix: char, line: &StructuredLine) -> Result<()> {
250 write!(out, "{prefix}")?;
251 for segment in &line.segments {
252 write!(out, "{}", segment.text)?;
253 }
254 Ok(())
255}
256
257fn format_git_diff_hunk_side(start_1based: usize, line_count: usize) -> String {
258 if line_count == 1 {
259 format!("{start_1based}")
260 } else {
261 format!("{start_1based},{line_count}")
262 }
263}
264
265fn write_structured_unified_body<W: Write>(
266 out: &mut W,
267 git_old: &str,
268 git_new: &str,
269 old_mode: u32,
270 new_mode: u32,
271 structured: &StructuredFileDiff,
272) -> Result<()> {
273 const QUOTE_FULLY: bool = true;
274 let old_path = if old_mode == 0 {
275 "/dev/null".to_owned()
276 } else {
277 format_diff_path_with_prefix("a/", git_old, QUOTE_FULLY)
278 };
279 let new_path = if new_mode == 0 {
280 "/dev/null".to_owned()
281 } else {
282 format_diff_path_with_prefix("b/", git_new, QUOTE_FULLY)
283 };
284 writeln!(out, "--- {old_path}")?;
285 writeln!(out, "+++ {new_path}")?;
286 for hunk in &structured.hunks {
287 writeln!(
288 out,
289 "@@ -{} +{} @@",
290 format_git_diff_hunk_side(hunk.old_start, hunk.old_lines),
291 format_git_diff_hunk_side(hunk.new_start, hunk.new_lines),
292 )?;
293 for line in &hunk.lines {
294 let prefix = match line.kind {
295 StructuredLineKind::Context => ' ',
296 StructuredLineKind::Add => '+',
297 StructuredLineKind::Del => '-',
298 };
299 if line.no_newline_at_eof {
300 write_structured_line(out, prefix, line)?;
301 writeln!(out)?;
302 writeln!(out, "\\ No newline at end of file")?;
303 } else {
304 write_structured_line(out, prefix, line)?;
305 writeln!(out)?;
306 }
307 }
308 }
309 Ok(())
310}
311
312#[allow(clippy::too_many_arguments)]
313fn write_diff_entry_patch_from_structured<W: Write>(
314 out: &mut W,
315 repo: &Repository,
316 odb: &Odb,
317 entry: &DiffEntry,
318 opts: &LogPatchOptions,
319 abbrev_cache: &mut OidAbbrevCache,
320 is_binary: bool,
321 structured: &StructuredFileDiff,
322) -> Result<()> {
323 write_diff_entry_patch_headers(out, repo, entry, opts, abbrev_cache, is_binary)?;
324 let old_mode = entry.old_mode.bits();
325 let new_mode = entry.new_mode.bits();
326 let git_old = entry
327 .old_path
328 .as_deref()
329 .or(entry.new_path.as_deref())
330 .unwrap_or("dev/null");
331 let git_new = entry
332 .new_path
333 .as_deref()
334 .or(entry.old_path.as_deref())
335 .unwrap_or("dev/null");
336 if entry.old_oid == entry.new_oid {
337 return Ok(());
338 }
339 let use_structured = !is_binary
340 && !structured.hunks.is_empty()
341 && !structured.old_encoding_lossy
342 && !structured.new_encoding_lossy;
343 if use_structured {
344 write_structured_unified_body(out, git_old, git_new, old_mode, new_mode, structured)?;
345 return Ok(());
346 }
347 let path = entry.new_path.as_deref().or(entry.old_path.as_deref());
348 let force_read = is_binary;
349 let old_bytes = read_blob_side(odb, entry.old_oid, old_mode == 0, path, force_read)?;
350 let new_bytes = read_blob_side(odb, entry.new_oid, new_mode == 0, path, force_read)?;
351 if old_bytes == new_bytes {
352 return Ok(());
353 }
354 if is_binary {
355 crate::apply::write_git_binary_patch_sections(out, &old_bytes, &new_bytes)?;
356 return Ok(());
357 }
358 write_patch_unified_body_from_bytes(
359 out, &old_bytes, &new_bytes, git_old, git_new, old_mode, new_mode, opts,
360 )
361}
362
363#[allow(clippy::too_many_arguments)]
365fn write_patch_unified_body_from_bytes<W: Write>(
366 out: &mut W,
367 old_bytes: &[u8],
368 new_bytes: &[u8],
369 git_old: &str,
370 git_new: &str,
371 old_mode: u32,
372 new_mode: u32,
373 opts: &LogPatchOptions,
374) -> Result<()> {
375 const QUOTE_FULLY: bool = true;
376 let byte_oriented = !crate::commit_encoding::is_strict_utf8(old_bytes)
377 || !crate::commit_encoding::is_strict_utf8(new_bytes);
378 let (old_text, new_text) = if byte_oriented {
379 (
380 crate::commit_encoding::decode_latin1(old_bytes),
381 crate::commit_encoding::decode_latin1(new_bytes),
382 )
383 } else {
384 let old_s = std::str::from_utf8(old_bytes)
385 .map_err(|_| crate::error::Error::CorruptObject("blob is not valid UTF-8".into()))?;
386 let new_s = std::str::from_utf8(new_bytes)
387 .map_err(|_| crate::error::Error::CorruptObject("blob is not valid UTF-8".into()))?;
388 (old_s.to_owned(), new_s.to_owned())
389 };
390 let old_path = if old_mode == 0 {
391 "/dev/null".to_owned()
392 } else {
393 git_old.to_owned()
394 };
395 let new_path = if new_mode == 0 {
396 "/dev/null".to_owned()
397 } else {
398 git_new.to_owned()
399 };
400 let d = &opts.diff;
401 if byte_oriented {
402 let mut patch_buf = Vec::new();
403 super::write_unified_diff_from_diff_options(
404 &mut patch_buf,
405 &old_text,
406 &new_text,
407 &old_path,
408 &new_path,
409 "a/",
410 "b/",
411 None,
412 d,
413 QUOTE_FULLY,
414 )?;
415 let patch_str = std::str::from_utf8(&patch_buf)
416 .map_err(|_| crate::error::Error::CorruptObject("patch re-encoding failed".into()))?;
417 out.write_all(&crate::commit_encoding::encode_latin1_lossy(patch_str))?;
418 } else {
419 super::write_unified_diff_from_diff_options(
420 out,
421 &old_text,
422 &new_text,
423 &old_path,
424 &new_path,
425 "a/",
426 "b/",
427 None,
428 d,
429 QUOTE_FULLY,
430 )?;
431 }
432 Ok(())
433}
434
435fn resolve_core_abbrev_len(config: &ConfigSet, odb: &Odb) -> Result<usize> {
436 if let Some(v) = config.get("core.abbrev") {
437 if v.eq_ignore_ascii_case("auto") {
438 let count = approximate_object_count(odb)?;
439 return Ok(auto_abbrev_len_for_object_count(count));
440 }
441 if let Ok(n) = v.parse::<i64>() {
442 return Ok((n as usize).clamp(4, 40));
443 }
444 }
445 let count = approximate_object_count(odb)?;
446 Ok(auto_abbrev_len_for_object_count(count))
447}
448
449fn effective_log_patch_options(
450 repo: &Repository,
451 opts: &LogPatchOptions,
452) -> Result<LogPatchOptions> {
453 let config = repo.config()?.clone();
454 let mut out = opts.clone();
455 out.abbrev_len = resolve_core_abbrev_len(&config, repo.odb())?;
456 Ok(out)
457}
458
459fn log_patch_funcname_matcher(
460 repo: &Repository,
461 config: &ConfigSet,
462 tree_oid: &ObjectId,
463 rel_path: &str,
464) -> Result<Option<FuncnameMatcher>> {
465 let parsed = load_gitattributes_for_check_attr_source(repo, tree_oid)?;
466 matcher_for_path_parsed(config, &parsed.rules, &parsed.macros, rel_path, false)
467 .map_err(crate::error::Error::Message)
468}
469
470pub fn write_commit_log_patch<W: Write>(
476 repo: &Repository,
477 out: &mut W,
478 oid: &ObjectId,
479 opts: &LogPatchOptions,
480) -> Result<()> {
481 let odb = repo.odb();
482 let raw = odb.read(oid)?;
483 let commit = parse_commit(&raw.data)?;
484 let parent_tree = parent_tree_for_commit(odb, &commit, None)?;
485 let opts = effective_log_patch_options(repo, opts)?;
486 let mut abbrev_cache = OidAbbrevCache::new(opts.abbrev_len);
487 write_parsed_commit_log_patch(
488 out,
489 repo,
490 odb,
491 oid,
492 &commit,
493 parent_tree,
494 &opts,
495 &mut abbrev_cache,
496 )
497}
498
499pub fn write_commits_log_patch<W: Write>(
509 repo: &Repository,
510 out: &mut W,
511 oids: &[ObjectId],
512 opts: &LogPatchOptions,
513) -> Result<()> {
514 if oids.is_empty() {
515 return Ok(());
516 }
517 let odb = repo.odb();
518 let mut trees_by_commit: HashMap<ObjectId, ObjectId> = HashMap::with_capacity(oids.len());
519 let mut parsed: Vec<(ObjectId, CommitData)> = Vec::with_capacity(oids.len());
520 for oid in oids {
521 let raw = odb.read(oid)?;
522 let commit = parse_commit(&raw.data)?;
523 trees_by_commit.insert(*oid, commit.tree);
524 parsed.push((*oid, commit));
525 }
526 let opts = effective_log_patch_options(repo, opts)?;
527 let mut abbrev_cache = OidAbbrevCache::new(opts.abbrev_len);
528 for (i, (oid, commit)) in parsed.iter().enumerate() {
529 if i > 0 && opts.commit_header == LogPatchCommitHeader::Full {
530 writeln!(out)?;
531 }
532 let parent_tree = parent_tree_for_commit(odb, commit, Some(&trees_by_commit))?;
533 write_parsed_commit_log_patch(
534 out,
535 repo,
536 odb,
537 oid,
538 commit,
539 parent_tree,
540 &opts,
541 &mut abbrev_cache,
542 )?;
543 }
544 Ok(())
545}
546
547fn parent_tree_for_commit(
548 odb: &Odb,
549 commit: &CommitData,
550 trees_by_commit: Option<&HashMap<ObjectId, ObjectId>>,
551) -> Result<Option<ObjectId>> {
552 if commit.parents.is_empty() {
553 return Ok(None);
554 }
555 let parent = commit.parents[0];
556 if let Some(map) = trees_by_commit {
557 if let Some(tree) = map.get(&parent) {
558 return Ok(Some(*tree));
559 }
560 }
561 let parent_raw = odb.read(&parent)?;
562 Ok(Some(parse_commit(&parent_raw.data)?.tree))
563}
564
565fn abbrev_for_index_line(
566 cache: &mut OidAbbrevCache,
567 repo: &Repository,
568 oid: &ObjectId,
569 min_len: usize,
570) -> Result<String> {
571 if *oid == zero_oid() {
572 return Ok(abbrev_hex(oid, min_len));
573 }
574 cache.abbrev(repo, oid)
575}
576
577fn abbrev_index_pair(
578 cache: &mut OidAbbrevCache,
579 repo: &Repository,
580 old_oid: &ObjectId,
581 new_oid: &ObjectId,
582 min_len: usize,
583) -> Result<(String, String)> {
584 let old = abbrev_for_index_line(cache, repo, old_oid, min_len)?;
585 let new = abbrev_for_index_line(cache, repo, new_oid, min_len)?;
586 let len = old.len().max(new.len());
587 let pad = |oid: &ObjectId, abbrev: String| {
588 if abbrev.len() >= len {
589 return abbrev;
590 }
591 let hex = oid.to_hex();
592 hex[..len.min(hex.len())].to_string()
593 };
594 Ok((pad(old_oid, old), pad(new_oid, new)))
595}
596
597#[allow(clippy::too_many_arguments)]
598fn write_parsed_commit_log_patch<W: Write>(
599 out: &mut W,
600 repo: &Repository,
601 odb: &Odb,
602 oid: &ObjectId,
603 commit: &CommitData,
604 _parent_tree: Option<ObjectId>,
605 opts: &LogPatchOptions,
606 abbrev_cache: &mut OidAbbrevCache,
607) -> Result<()> {
608 let is_merge = commit.parents.len() > 1;
609 let omit_merge_patch = is_merge && opts.diff_merges == DiffMerges::Combined;
610 match opts.commit_header {
611 LogPatchCommitHeader::Full => {
612 writeln!(out, "commit {oid}")?;
613 if is_merge {
614 let mut parts = Vec::with_capacity(commit.parents.len());
615 for parent in &commit.parents {
616 parts.push(abbrev_for_index_line(
617 abbrev_cache,
618 repo,
619 parent,
620 opts.abbrev_len,
621 )?);
622 }
623 writeln!(out, "Merge: {}", parts.join(" "))?;
624 }
625 write_commit_header_and_message(out, &commit.author, &commit.message)?;
626 if omit_merge_patch {
627 return Ok(());
628 }
629 writeln!(out)?;
630 }
631 LogPatchCommitHeader::FormatPercentH => {
632 writeln!(out, "{oid}")?;
633 if omit_merge_patch {
634 return Ok(());
635 }
636 writeln!(out)?;
637 }
638 }
639 let config = repo.config()?.clone();
640 let thread_policy = repo.thread_policy();
641 let entries =
642 commit_first_parent_diff_entries(repo, commit, &opts.pathspecs, &config, &thread_policy)?;
643 for entry in entries {
644 write_diff_entry_patch(
645 out,
646 repo,
647 odb,
648 &entry,
649 opts,
650 abbrev_cache,
651 Some(&commit.tree),
652 opts.binary_format,
653 &config,
654 )?;
655 }
656 Ok(())
657}
658
659pub fn write_commit_header_and_message<W: Write>(
661 out: &mut W,
662 author_ident: &str,
663 message: &str,
664) -> Result<()> {
665 writeln!(out, "Author: {}", format_ident_display(author_ident))?;
666 writeln!(out, "Date: {}", format_log_date(author_ident))?;
667 writeln!(out)?;
668 for line in message.lines() {
669 writeln!(out, " {line}")?;
670 }
671 Ok(())
672}
673
674fn format_ident_display(ident: &str) -> String {
675 let name = ident.find('<').map_or(ident.trim(), |i| ident[..i].trim());
676 let email = ident
677 .find('<')
678 .and_then(|start| ident.find('>').map(|end| &ident[start..=end]))
679 .unwrap_or("");
680 format!("{name} {email}")
681}
682
683fn format_log_date(ident: &str) -> String {
684 let parts: Vec<&str> = ident.rsplitn(3, ' ').collect();
685 if parts.len() < 2 {
686 return String::new();
687 }
688 let ts_str = parts[1];
689 let offset_str = parts[0];
690 let Ok(ts) = ts_str.parse::<i64>() else {
691 return format!("{ts_str} {offset_str}");
692 };
693 let tz_bytes = offset_str.as_bytes();
694 let tz_secs: i64 = if tz_bytes.len() >= 5 {
695 let sign = if tz_bytes[0] == b'-' { -1 } else { 1 };
696 let h: i64 = offset_str[1..3].parse().unwrap_or(0);
697 let m: i64 = offset_str[3..5].parse().unwrap_or(0);
698 sign * (h * 3600 + m * 60)
699 } else {
700 0
701 };
702 let adjusted = ts + tz_secs;
703 let dt = time::OffsetDateTime::from_unix_timestamp(adjusted)
704 .unwrap_or(time::OffsetDateTime::UNIX_EPOCH);
705 let weekday = match dt.weekday() {
706 time::Weekday::Monday => "Mon",
707 time::Weekday::Tuesday => "Tue",
708 time::Weekday::Wednesday => "Wed",
709 time::Weekday::Thursday => "Thu",
710 time::Weekday::Friday => "Fri",
711 time::Weekday::Saturday => "Sat",
712 time::Weekday::Sunday => "Sun",
713 };
714 let month = match dt.month() {
715 time::Month::January => "Jan",
716 time::Month::February => "Feb",
717 time::Month::March => "Mar",
718 time::Month::April => "Apr",
719 time::Month::May => "May",
720 time::Month::June => "Jun",
721 time::Month::July => "Jul",
722 time::Month::August => "Aug",
723 time::Month::September => "Sep",
724 time::Month::October => "Oct",
725 time::Month::November => "Nov",
726 time::Month::December => "Dec",
727 };
728 format!(
729 "{weekday} {month} {} {:02}:{:02}:{:02} {} {offset_str}",
730 dt.day(),
731 dt.hour(),
732 dt.minute(),
733 dt.second(),
734 dt.year()
735 )
736}
737
738pub fn write_diff_entry_log_patch<W: Write>(
744 out: &mut W,
745 repo: &Repository,
746 odb: &Odb,
747 entry: &DiffEntry,
748 opts: &LogPatchOptions,
749) -> Result<()> {
750 let config = repo.config()?.clone();
751 let mut abbrev_cache = OidAbbrevCache::new(opts.abbrev_len);
752 write_diff_entry_patch(
753 out,
754 repo,
755 odb,
756 entry,
757 opts,
758 &mut abbrev_cache,
759 None,
760 opts.binary_format,
761 &config,
762 )
763}
764
765fn index_oid_full(oid: &ObjectId) -> String {
766 oid.to_hex()
767}
768
769#[allow(clippy::too_many_arguments)]
770fn write_diff_entry_patch_headers<W: Write>(
771 out: &mut W,
772 repo: &Repository,
773 entry: &DiffEntry,
774 opts: &LogPatchOptions,
775 abbrev_cache: &mut OidAbbrevCache,
776 index_full_oid: bool,
777) -> Result<()> {
778 let git_old = entry
779 .old_path
780 .as_deref()
781 .or(entry.new_path.as_deref())
782 .unwrap_or("dev/null");
783 let git_new = entry
784 .new_path
785 .as_deref()
786 .or(entry.old_path.as_deref())
787 .unwrap_or("dev/null");
788 const QUOTE_FULLY: bool = true;
789 writeln!(
790 out,
791 "diff --git {} {}",
792 format_diff_path_with_prefix("a/", git_old, QUOTE_FULLY),
793 format_diff_path_with_prefix("b/", git_new, QUOTE_FULLY)
794 )?;
795
796 let old_mode = entry.old_mode.bits();
797 let new_mode = entry.new_mode.bits();
798
799 if old_mode == 0 {
800 writeln!(out, "new file mode {}", entry.new_mode)?;
801 } else if new_mode == 0 {
802 writeln!(out, "deleted file mode {}", entry.old_mode)?;
803 }
804
805 match entry.status {
806 DiffStatus::Renamed => {
807 writeln!(out, "similarity index {}%", entry.score.unwrap_or(100))?;
808 if let Some(path) = &entry.old_path {
809 writeln!(out, "rename from {path}")?;
810 }
811 if let Some(path) = &entry.new_path {
812 writeln!(out, "rename to {path}")?;
813 }
814 }
815 DiffStatus::Copied => {
816 writeln!(out, "similarity index {}%", entry.score.unwrap_or(100))?;
817 if let Some(path) = &entry.old_path {
818 writeln!(out, "copy from {path}")?;
819 }
820 if let Some(path) = &entry.new_path {
821 writeln!(out, "copy to {path}")?;
822 }
823 }
824 _ if old_mode != 0 && new_mode != 0 && old_mode != new_mode => {
825 writeln!(out, "old mode {}", entry.old_mode)?;
826 writeln!(out, "new mode {}", entry.new_mode)?;
827 }
828 _ => {}
829 }
830
831 if entry.old_oid != entry.new_oid {
832 let index_mode = if new_mode != 0 {
833 entry.new_mode
834 } else {
835 entry.old_mode
836 };
837 let mut index_pair = |old: &ObjectId, new: &ObjectId| -> Result<(String, String)> {
838 if index_full_oid {
839 Ok((index_oid_full(old), index_oid_full(new)))
840 } else {
841 abbrev_index_pair(abbrev_cache, repo, old, new, opts.abbrev_len)
842 }
843 };
844 match entry.status {
845 DiffStatus::Added => {
846 let (old_a, new_a) = index_pair(&zero_oid(), &entry.new_oid)?;
847 writeln!(out, "index {old_a}..{new_a}")?;
848 }
849 DiffStatus::Deleted => {
850 let (old_a, new_a) = index_pair(&entry.old_oid, &zero_oid())?;
851 writeln!(out, "index {old_a}..{new_a}")?;
852 }
853 DiffStatus::Renamed | DiffStatus::Copied => {
854 let (old_a, new_a) = index_pair(&entry.old_oid, &entry.new_oid)?;
855 writeln!(out, "index {old_a}..{new_a}")?;
856 }
857 _ => {
858 let (old_a, new_a) = index_pair(&entry.old_oid, &entry.new_oid)?;
859 writeln!(out, "index {old_a}..{new_a} {index_mode}")?;
860 }
861 }
862 }
863 Ok(())
864}
865
866#[allow(clippy::too_many_arguments)]
867fn write_diff_entry_patch<W: Write>(
868 out: &mut W,
869 repo: &Repository,
870 odb: &Odb,
871 entry: &DiffEntry,
872 opts: &LogPatchOptions,
873 abbrev_cache: &mut OidAbbrevCache,
874 new_tree: Option<&ObjectId>,
875 binary_format: LogPatchBinaryFormat,
876 config: &ConfigSet,
877) -> Result<()> {
878 let old_mode = entry.old_mode.bits();
879 let new_mode = entry.new_mode.bits();
880 let path = entry.new_path.as_deref().or(entry.old_path.as_deref());
881 let force_read = binary_format == LogPatchBinaryFormat::GitApplyBinaryPatch;
882 let (old_bytes, old_bin) =
883 load_patch_blob_side(odb, entry.old_oid, old_mode == 0, path, force_read)?;
884 let (new_bytes, new_bin) =
885 load_patch_blob_side(odb, entry.new_oid, new_mode == 0, path, force_read)?;
886 write_diff_entry_patch_with_resolved_bytes(
887 out,
888 repo,
889 entry,
890 opts,
891 abbrev_cache,
892 binary_format,
893 config,
894 old_bin || new_bin,
895 &old_bytes,
896 &new_bytes,
897 new_tree,
898 )
899}
900
901#[allow(clippy::too_many_arguments)]
902fn write_diff_entry_patch_with_resolved_bytes<W: Write>(
903 out: &mut W,
904 repo: &Repository,
905 entry: &DiffEntry,
906 opts: &LogPatchOptions,
907 abbrev_cache: &mut OidAbbrevCache,
908 binary_format: LogPatchBinaryFormat,
909 config: &ConfigSet,
910 side_known_binary: bool,
911 old_bytes: &[u8],
912 new_bytes: &[u8],
913 new_tree: Option<&ObjectId>,
914) -> Result<()> {
915 const QUOTE_FULLY: bool = true;
916 let git_old = entry
917 .old_path
918 .as_deref()
919 .or(entry.new_path.as_deref())
920 .unwrap_or("dev/null");
921 let git_new = entry
922 .new_path
923 .as_deref()
924 .or(entry.old_path.as_deref())
925 .unwrap_or("dev/null");
926 let old_mode = entry.old_mode.bits();
927 let new_mode = entry.new_mode.bits();
928 let is_binary =
929 side_known_binary || merge_file::is_binary(old_bytes) || merge_file::is_binary(new_bytes);
930 let index_full_oid = is_binary && binary_format == LogPatchBinaryFormat::GitApplyBinaryPatch;
931 write_diff_entry_patch_headers(out, repo, entry, opts, abbrev_cache, index_full_oid)?;
932
933 if old_bytes == new_bytes && entry.old_oid == entry.new_oid {
934 return Ok(());
935 }
936
937 if is_binary {
938 match binary_format {
939 LogPatchBinaryFormat::LogShortMessage => {
940 let old_side = if old_mode == 0 {
941 "/dev/null".to_owned()
942 } else {
943 format_diff_path_with_prefix("a/", git_old, QUOTE_FULLY)
944 };
945 let new_side = if new_mode == 0 {
946 "/dev/null".to_owned()
947 } else {
948 format_diff_path_with_prefix("b/", git_new, QUOTE_FULLY)
949 };
950 writeln!(out, "Binary files {old_side} and {new_side} differ")?;
951 }
952 LogPatchBinaryFormat::GitApplyBinaryPatch => {
953 crate::apply::write_git_binary_patch_sections(out, old_bytes, new_bytes)?;
954 }
955 }
956 return Ok(());
957 }
958
959 if binary_format == LogPatchBinaryFormat::GitApplyBinaryPatch && new_tree.is_none() {
960 return write_patch_unified_body_from_bytes(
961 out, old_bytes, new_bytes, git_old, git_new, old_mode, new_mode, opts,
962 );
963 }
964 let byte_oriented = !crate::commit_encoding::is_strict_utf8(old_bytes)
965 || !crate::commit_encoding::is_strict_utf8(new_bytes);
966 let (old_text, new_text) = if byte_oriented {
967 (
968 crate::commit_encoding::decode_latin1(old_bytes),
969 crate::commit_encoding::decode_latin1(new_bytes),
970 )
971 } else {
972 let old_s = std::str::from_utf8(old_bytes)
973 .map_err(|_| crate::error::Error::CorruptObject("blob is not valid UTF-8".into()))?;
974 let new_s = std::str::from_utf8(new_bytes)
975 .map_err(|_| crate::error::Error::CorruptObject("blob is not valid UTF-8".into()))?;
976 (old_s.to_owned(), new_s.to_owned())
977 };
978 let old_path = if old_mode == 0 {
979 "/dev/null".to_owned()
980 } else {
981 git_old.to_owned()
982 };
983 let new_path = if new_mode == 0 {
984 "/dev/null".to_owned()
985 } else {
986 git_new.to_owned()
987 };
988 let d = &opts.diff;
989 let funcname_matcher = if let Some(tree) = new_tree {
990 log_patch_funcname_matcher(repo, config, tree, git_new)?
991 } else {
992 None
993 };
994 let funcname_ref = funcname_matcher.as_ref();
995 if byte_oriented {
996 let mut patch_buf = Vec::new();
997 super::write_unified_diff_from_diff_options(
998 &mut patch_buf,
999 &old_text,
1000 &new_text,
1001 &old_path,
1002 &new_path,
1003 "a/",
1004 "b/",
1005 funcname_ref,
1006 d,
1007 QUOTE_FULLY,
1008 )?;
1009 let patch_str = std::str::from_utf8(&patch_buf)
1010 .map_err(|_| crate::error::Error::CorruptObject("patch re-encoding failed".into()))?;
1011 out.write_all(&crate::commit_encoding::encode_latin1_lossy(patch_str))?;
1012 } else {
1013 super::write_unified_diff_from_diff_options(
1014 out,
1015 &old_text,
1016 &new_text,
1017 &old_path,
1018 &new_path,
1019 "a/",
1020 "b/",
1021 funcname_ref,
1022 d,
1023 QUOTE_FULLY,
1024 )?;
1025 }
1026 Ok(())
1027}
1028
1029#[derive(Debug, Clone, PartialEq, Eq)]
1037pub enum PatchSide {
1038 Bytes(Vec<u8>),
1040 Blob,
1043 Worktree,
1046}
1047
1048#[derive(Debug, Clone)]
1050pub struct PatchResolvedSide {
1051 pub entry: DiffEntry,
1053 pub old: PatchSide,
1055 pub new: PatchSide,
1057}
1058
1059pub fn write_patch_resolved<W: Write>(
1081 repo: &Repository,
1082 files: &[PatchResolvedSide],
1083 opts: &PatchTextOptions,
1084 out: &mut W,
1085) -> Result<()> {
1086 if files.is_empty() {
1087 return Ok(());
1088 }
1089 let config = repo.config()?.clone();
1090 let thread_policy = repo.thread_policy();
1091 let log_opts = LogPatchOptions {
1092 diff: opts.diff.clone(),
1093 abbrev_len: opts.abbrev_len,
1094 commit_header: LogPatchCommitHeader::FormatPercentH,
1095 binary_format: LogPatchBinaryFormat::GitApplyBinaryPatch,
1096 pathspecs: Vec::new(),
1097 diff_merges: DiffMerges::FirstParent,
1098 };
1099 let mut abbrev_cache = OidAbbrevCache::new(opts.abbrev_len);
1100 let oids = files
1101 .iter()
1102 .flat_map(|f| [f.entry.old_oid, f.entry.new_oid]);
1103 abbrev_cache.warm_for_patch_batch(repo, oids, &thread_policy, &config)?;
1104 let mut worktree = None;
1105 for file in files {
1106 let old = resolve_patch_side(repo, file, EntrySide::Old, &mut worktree)?;
1107 let new = resolve_patch_side(repo, file, EntrySide::New, &mut worktree)?;
1108 let entry = if merge_file::is_binary(&old) || merge_file::is_binary(&new) {
1109 Cow::Owned(entry_with_content_ids(repo, file, &old, &new)?)
1110 } else {
1111 Cow::Borrowed(&file.entry)
1112 };
1113 write_diff_entry_patch_with_resolved_bytes(
1114 out,
1115 repo,
1116 &entry,
1117 &log_opts,
1118 &mut abbrev_cache,
1119 LogPatchBinaryFormat::GitApplyBinaryPatch,
1120 &config,
1121 false,
1122 &old,
1123 &new,
1124 None,
1125 )?;
1126 }
1127 Ok(())
1128}
1129
1130#[derive(Clone, Copy)]
1131enum EntrySide {
1132 Old,
1133 New,
1134}
1135
1136impl EntrySide {
1137 fn of(self, file: &PatchResolvedSide) -> (&PatchSide, FileMode, ObjectId) {
1138 let e = &file.entry;
1139 match self {
1140 Self::Old => (&file.old, e.old_mode, e.old_oid),
1141 Self::New => (&file.new, e.new_mode, e.new_oid),
1142 }
1143 }
1144
1145 fn path(self, entry: &DiffEntry) -> &str {
1146 match self {
1147 Self::Old => entry.old_path.as_deref().unwrap_or_else(|| entry.path()),
1148 Self::New => entry.path(),
1149 }
1150 }
1151}
1152
1153struct WorktreeSideReader<'r> {
1155 work_tree: &'r std::path::Path,
1156 index: Index,
1157 rules: WorktreeRules,
1158}
1159
1160impl<'r> WorktreeSideReader<'r> {
1161 fn open(repo: &'r Repository) -> Result<Self> {
1162 let work_tree = repo.work_tree().ok_or(Error::RequiresWorkTree)?;
1163 let index = repo.index_handle().load()?;
1164 let rules = WorktreeRules::from_repository(repo, &index)?;
1165 Ok(Self {
1166 work_tree,
1167 index,
1168 rules,
1169 })
1170 }
1171
1172 fn read(&self, odb: &Odb, path: &str, mode: u32) -> Result<Vec<u8>> {
1173 let index_entry = self.index.get(&crate::repo_relpath::storage_bytes(path), 0);
1174 Ok(load_worktree_side_for_diff(
1175 odb,
1176 self.work_tree,
1177 path,
1178 mode,
1179 self.rules.config(),
1180 index_entry,
1181 Some(&self.rules),
1182 true,
1183 )?
1184 .0
1185 .data)
1186 }
1187}
1188
1189fn resolve_patch_side<'f, 'r>(
1190 repo: &'r Repository,
1191 file: &'f PatchResolvedSide,
1192 side: EntrySide,
1193 worktree: &mut Option<WorktreeSideReader<'r>>,
1194) -> Result<Cow<'f, [u8]>> {
1195 let (source, mode, oid) = side.of(file);
1196 let mode = mode.bits();
1197 if mode == 0 {
1198 return Ok(Cow::Borrowed(&[]));
1199 }
1200 match source {
1201 PatchSide::Bytes(bytes) => Ok(Cow::Borrowed(bytes)),
1202 PatchSide::Blob => Ok(Cow::Owned(read_blob_side(
1203 repo.odb(),
1204 oid,
1205 false,
1206 Some(side.path(&file.entry)),
1207 true,
1208 )?)),
1209 PatchSide::Worktree => {
1210 let reader = match worktree {
1211 Some(reader) => reader,
1212 None => worktree.insert(WorktreeSideReader::open(repo)?),
1213 };
1214 Ok(Cow::Owned(reader.read(
1215 repo.odb(),
1216 side.path(&file.entry),
1217 mode,
1218 )?))
1219 }
1220 }
1221}
1222
1223fn entry_with_content_ids(
1225 repo: &Repository,
1226 file: &PatchResolvedSide,
1227 old: &[u8],
1228 new: &[u8],
1229) -> Result<DiffEntry> {
1230 let content_id = |side: EntrySide, bytes: &[u8]| -> Result<ObjectId> {
1231 let (source, mode, oid) = side.of(file);
1232 if mode.is_absent() {
1233 return Ok(zero_oid());
1234 }
1235 match source {
1236 PatchSide::Blob => Ok(oid),
1237 PatchSide::Bytes(_) | PatchSide::Worktree => {
1238 repo.odb().hash_algo().hash_object(ObjectKind::Blob, bytes)
1239 }
1240 }
1241 };
1242 let mut entry = file.entry.clone();
1243 entry.old_oid = content_id(EntrySide::Old, old)?;
1244 entry.new_oid = content_id(EntrySide::New, new)?;
1245 Ok(entry)
1246}
1247
1248fn load_patch_blob_side(
1249 odb: &Odb,
1250 oid: ObjectId,
1251 is_missing: bool,
1252 path: Option<&str>,
1253 force_read_content: bool,
1254) -> Result<(Vec<u8>, bool)> {
1255 if is_missing || oid == zero_oid() || oid == empty_blob_oid() {
1256 return Ok((Vec::new(), false));
1257 }
1258 let (side, binary) = load_odb_blob_side_for_diff(odb, &oid, path, None, force_read_content)?;
1259 Ok((side.data, binary))
1260}
1261
1262fn read_blob_side(
1263 odb: &Odb,
1264 oid: ObjectId,
1265 is_missing: bool,
1266 path: Option<&str>,
1267 force_read_content: bool,
1268) -> Result<Vec<u8>> {
1269 Ok(load_patch_blob_side(odb, oid, is_missing, path, force_read_content)?.0)
1270}
1271
1272#[cfg(test)]
1273mod tests {
1274 use super::*;
1275 use std::io::Cursor;
1276
1277 use crate::hash::hash_object;
1278 use crate::objects::HashAlgo;
1279
1280 #[test]
1281 fn log_short_message_skips_big_blob_reads() -> Result<()> {
1282 let dir = tempfile::tempdir().expect("tempdir");
1283 let repo = Repository::init(dir.path(), &crate::repo::InitOptions::default())?;
1284 std::fs::write(
1285 repo.git_dir().join("config"),
1286 "[core]\n\tbigFileThreshold = 4k\n",
1287 )?;
1288 let odb = repo.odb();
1289 let old = odb.write(ObjectKind::Blob, &vec![b'o'; 5000])?;
1290 let new = odb.write(ObjectKind::Blob, &vec![b'n'; 5001])?;
1291 let entry = DiffEntry {
1292 status: DiffStatus::Modified,
1293 old_path: Some("asset.bin".into()),
1294 new_path: Some("asset.bin".into()),
1295 old_oid: old,
1296 new_oid: new,
1297 old_mode: FileMode::REGULAR,
1298 new_mode: FileMode::REGULAR,
1299 score: None,
1300 };
1301 let metrics = odb.hot_path_metrics();
1302 metrics.reset_blob_content_reads();
1303 metrics.set_blob_counting(true);
1304 let mut out = Cursor::new(Vec::new());
1305 let mut opts = LogPatchOptions::git_default(&ConfigSet::default());
1306 opts.binary_format = LogPatchBinaryFormat::LogShortMessage;
1307 write_diff_entry_log_patch(&mut out, &repo, odb, &entry, &opts)?;
1308 metrics.set_blob_counting(false);
1309 let text = String::from_utf8(out.into_inner()).expect("utf-8");
1310 assert!(text.contains("Binary files"));
1311 assert_eq!(metrics.blob_content_reads(), 0);
1312 Ok(())
1313 }
1314
1315 #[test]
1316 fn copied_diff_emits_copy_headers() -> Result<()> {
1317 let blob_oid = hash_object(HashAlgo::Sha1, ObjectKind::Blob, b"x").expect("hash");
1318 let entry = DiffEntry {
1319 status: DiffStatus::Copied,
1320 old_path: Some("src/a.txt".into()),
1321 new_path: Some("dst/b.txt".into()),
1322 old_oid: blob_oid,
1323 new_oid: blob_oid,
1324 old_mode: FileMode::REGULAR,
1325 new_mode: FileMode::REGULAR,
1326 score: Some(100),
1327 };
1328 let dir = tempfile::tempdir().expect("tempdir");
1329 let repo = Repository::init(dir.path(), &crate::repo::InitOptions::default())?;
1330 repo.odb().write(ObjectKind::Blob, b"x")?;
1331 let mut out = Cursor::new(Vec::new());
1332 let opts = LogPatchOptions::git_default(&ConfigSet::default());
1333 write_diff_entry_log_patch(&mut out, &repo, repo.odb(), &entry, &opts)?;
1334 let text = String::from_utf8(out.into_inner()).expect("utf-8");
1335 assert!(text.contains("copy from src/a.txt"));
1336 assert!(text.contains("copy to dst/b.txt"));
1337 assert!(!text.contains("rename from"));
1338 Ok(())
1339 }
1340}