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