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::{empty_blob_oid, read_worktree_side_bytes_for_diff, zero_oid, DiffEntry, DiffStatus};
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
36pub enum LogPatchCommitHeader {
37 #[default]
39 Full,
40 FormatPercentH,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
46pub enum LogPatchBinaryFormat {
47 #[default]
49 LogShortMessage,
50 GitApplyBinaryPatch,
52}
53
54#[derive(Debug, Clone)]
56pub struct LogPatchOptions {
57 pub diff: DiffOptions,
59 pub abbrev_len: usize,
61 pub commit_header: LogPatchCommitHeader,
63 pub binary_format: LogPatchBinaryFormat,
65 pub pathspecs: Vec<String>,
67 pub diff_merges: DiffMerges,
69}
70
71impl LogPatchOptions {
72 #[must_use]
74 pub fn git_default(config: &ConfigSet) -> Self {
75 Self {
76 diff: DiffOptions::from_config(config),
77 abbrev_len: 7,
78 commit_header: LogPatchCommitHeader::Full,
79 binary_format: LogPatchBinaryFormat::default(),
80 pathspecs: Vec::new(),
81 diff_merges: DiffMerges::Combined,
82 }
83 }
84}
85
86#[derive(Debug, Clone)]
88pub struct PatchTextOptions {
89 pub diff: DiffOptions,
91 pub abbrev_len: usize,
93}
94
95impl PatchTextOptions {
96 pub fn from_repo(repo: &Repository, diff: DiffOptions) -> Result<Self> {
102 let config = repo.config()?.clone();
103 Ok(Self {
104 diff,
105 abbrev_len: resolve_core_abbrev_len(&config, repo.odb())?,
106 })
107 }
108}
109
110pub fn write_patch<W: Write>(
118 repo: &Repository,
119 entries: &[DiffEntry],
120 opts: &PatchTextOptions,
121 out: &mut W,
122) -> Result<()> {
123 if entries.is_empty() {
124 return Ok(());
125 }
126 let odb = repo.odb();
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 old_bytes = read_blob_side(odb, entry.old_oid, old_mode == 0)?;
346 let new_bytes = read_blob_side(odb, entry.new_oid, new_mode == 0)?;
347 if old_bytes == new_bytes {
348 return Ok(());
349 }
350 if is_binary {
351 crate::apply::write_git_binary_patch_sections(out, &old_bytes, &new_bytes)?;
352 return Ok(());
353 }
354 write_patch_unified_body_from_bytes(
355 out, &old_bytes, &new_bytes, git_old, git_new, old_mode, new_mode, opts,
356 )
357}
358
359#[allow(clippy::too_many_arguments)]
361fn write_patch_unified_body_from_bytes<W: Write>(
362 out: &mut W,
363 old_bytes: &[u8],
364 new_bytes: &[u8],
365 git_old: &str,
366 git_new: &str,
367 old_mode: u32,
368 new_mode: u32,
369 opts: &LogPatchOptions,
370) -> Result<()> {
371 const QUOTE_FULLY: bool = true;
372 let byte_oriented = !crate::commit_encoding::is_strict_utf8(old_bytes)
373 || !crate::commit_encoding::is_strict_utf8(new_bytes);
374 let (old_text, new_text) = if byte_oriented {
375 (
376 crate::commit_encoding::decode_latin1(old_bytes),
377 crate::commit_encoding::decode_latin1(new_bytes),
378 )
379 } else {
380 let old_s = std::str::from_utf8(old_bytes)
381 .map_err(|_| crate::error::Error::CorruptObject("blob is not valid UTF-8".into()))?;
382 let new_s = std::str::from_utf8(new_bytes)
383 .map_err(|_| crate::error::Error::CorruptObject("blob is not valid UTF-8".into()))?;
384 (old_s.to_owned(), new_s.to_owned())
385 };
386 let old_path = if old_mode == 0 {
387 "/dev/null".to_owned()
388 } else {
389 git_old.to_owned()
390 };
391 let new_path = if new_mode == 0 {
392 "/dev/null".to_owned()
393 } else {
394 git_new.to_owned()
395 };
396 let d = &opts.diff;
397 if byte_oriented {
398 let mut patch_buf = Vec::new();
399 super::write_unified_diff_from_diff_options(
400 &mut patch_buf,
401 &old_text,
402 &new_text,
403 &old_path,
404 &new_path,
405 "a/",
406 "b/",
407 None,
408 d,
409 QUOTE_FULLY,
410 )?;
411 let patch_str = std::str::from_utf8(&patch_buf)
412 .map_err(|_| crate::error::Error::CorruptObject("patch re-encoding failed".into()))?;
413 out.write_all(&crate::commit_encoding::encode_latin1_lossy(patch_str))?;
414 } else {
415 super::write_unified_diff_from_diff_options(
416 out,
417 &old_text,
418 &new_text,
419 &old_path,
420 &new_path,
421 "a/",
422 "b/",
423 None,
424 d,
425 QUOTE_FULLY,
426 )?;
427 }
428 Ok(())
429}
430
431fn resolve_core_abbrev_len(config: &ConfigSet, odb: &Odb) -> Result<usize> {
432 if let Some(v) = config.get("core.abbrev") {
433 if v.eq_ignore_ascii_case("auto") {
434 let count = approximate_object_count(odb)?;
435 return Ok(auto_abbrev_len_for_object_count(count));
436 }
437 if let Ok(n) = v.parse::<i64>() {
438 return Ok((n as usize).clamp(4, 40));
439 }
440 }
441 let count = approximate_object_count(odb)?;
442 Ok(auto_abbrev_len_for_object_count(count))
443}
444
445fn effective_log_patch_options(
446 repo: &Repository,
447 opts: &LogPatchOptions,
448) -> Result<LogPatchOptions> {
449 let config = repo.config()?.clone();
450 let mut out = opts.clone();
451 out.abbrev_len = resolve_core_abbrev_len(&config, repo.odb())?;
452 Ok(out)
453}
454
455fn log_patch_funcname_matcher(
456 repo: &Repository,
457 config: &ConfigSet,
458 tree_oid: &ObjectId,
459 rel_path: &str,
460) -> Result<Option<FuncnameMatcher>> {
461 let parsed = load_gitattributes_for_check_attr_source(repo, tree_oid)?;
462 matcher_for_path_parsed(config, &parsed.rules, &parsed.macros, rel_path, false)
463 .map_err(crate::error::Error::Message)
464}
465
466pub fn write_commit_log_patch<W: Write>(
472 repo: &Repository,
473 out: &mut W,
474 oid: &ObjectId,
475 opts: &LogPatchOptions,
476) -> Result<()> {
477 let odb = repo.odb();
478 let raw = odb.read(oid)?;
479 let commit = parse_commit(&raw.data)?;
480 let parent_tree = parent_tree_for_commit(odb, &commit, None)?;
481 let opts = effective_log_patch_options(repo, opts)?;
482 let mut abbrev_cache = OidAbbrevCache::new(opts.abbrev_len);
483 write_parsed_commit_log_patch(
484 out,
485 repo,
486 odb,
487 oid,
488 &commit,
489 parent_tree,
490 &opts,
491 &mut abbrev_cache,
492 )
493}
494
495pub fn write_commits_log_patch<W: Write>(
505 repo: &Repository,
506 out: &mut W,
507 oids: &[ObjectId],
508 opts: &LogPatchOptions,
509) -> Result<()> {
510 if oids.is_empty() {
511 return Ok(());
512 }
513 let odb = repo.odb();
514 let mut trees_by_commit: HashMap<ObjectId, ObjectId> = HashMap::with_capacity(oids.len());
515 let mut parsed: Vec<(ObjectId, CommitData)> = Vec::with_capacity(oids.len());
516 for oid in oids {
517 let raw = odb.read(oid)?;
518 let commit = parse_commit(&raw.data)?;
519 trees_by_commit.insert(*oid, commit.tree);
520 parsed.push((*oid, commit));
521 }
522 let opts = effective_log_patch_options(repo, opts)?;
523 let mut abbrev_cache = OidAbbrevCache::new(opts.abbrev_len);
524 for (i, (oid, commit)) in parsed.iter().enumerate() {
525 if i > 0 && opts.commit_header == LogPatchCommitHeader::Full {
526 writeln!(out)?;
527 }
528 let parent_tree = parent_tree_for_commit(odb, commit, Some(&trees_by_commit))?;
529 write_parsed_commit_log_patch(
530 out,
531 repo,
532 odb,
533 oid,
534 commit,
535 parent_tree,
536 &opts,
537 &mut abbrev_cache,
538 )?;
539 }
540 Ok(())
541}
542
543fn parent_tree_for_commit(
544 odb: &Odb,
545 commit: &CommitData,
546 trees_by_commit: Option<&HashMap<ObjectId, ObjectId>>,
547) -> Result<Option<ObjectId>> {
548 if commit.parents.is_empty() {
549 return Ok(None);
550 }
551 let parent = commit.parents[0];
552 if let Some(map) = trees_by_commit {
553 if let Some(tree) = map.get(&parent) {
554 return Ok(Some(*tree));
555 }
556 }
557 let parent_raw = odb.read(&parent)?;
558 Ok(Some(parse_commit(&parent_raw.data)?.tree))
559}
560
561fn abbrev_for_index_line(
562 cache: &mut OidAbbrevCache,
563 repo: &Repository,
564 oid: &ObjectId,
565 min_len: usize,
566) -> Result<String> {
567 if *oid == zero_oid() {
568 return Ok(abbrev_hex(oid, min_len));
569 }
570 cache.abbrev(repo, oid)
571}
572
573fn abbrev_index_pair(
574 cache: &mut OidAbbrevCache,
575 repo: &Repository,
576 old_oid: &ObjectId,
577 new_oid: &ObjectId,
578 min_len: usize,
579) -> Result<(String, String)> {
580 let old = abbrev_for_index_line(cache, repo, old_oid, min_len)?;
581 let new = abbrev_for_index_line(cache, repo, new_oid, min_len)?;
582 let len = old.len().max(new.len());
583 let pad = |oid: &ObjectId, abbrev: String| {
584 if abbrev.len() >= len {
585 return abbrev;
586 }
587 let hex = oid.to_hex();
588 hex[..len.min(hex.len())].to_string()
589 };
590 Ok((pad(old_oid, old), pad(new_oid, new)))
591}
592
593#[allow(clippy::too_many_arguments)]
594fn write_parsed_commit_log_patch<W: Write>(
595 out: &mut W,
596 repo: &Repository,
597 odb: &Odb,
598 oid: &ObjectId,
599 commit: &CommitData,
600 _parent_tree: Option<ObjectId>,
601 opts: &LogPatchOptions,
602 abbrev_cache: &mut OidAbbrevCache,
603) -> Result<()> {
604 let is_merge = commit.parents.len() > 1;
605 let omit_merge_patch = is_merge && opts.diff_merges == DiffMerges::Combined;
606 match opts.commit_header {
607 LogPatchCommitHeader::Full => {
608 writeln!(out, "commit {oid}")?;
609 if is_merge {
610 let mut parts = Vec::with_capacity(commit.parents.len());
611 for parent in &commit.parents {
612 parts.push(abbrev_for_index_line(
613 abbrev_cache,
614 repo,
615 parent,
616 opts.abbrev_len,
617 )?);
618 }
619 writeln!(out, "Merge: {}", parts.join(" "))?;
620 }
621 write_commit_header_and_message(out, &commit.author, &commit.message)?;
622 if omit_merge_patch {
623 return Ok(());
624 }
625 writeln!(out)?;
626 }
627 LogPatchCommitHeader::FormatPercentH => {
628 writeln!(out, "{oid}")?;
629 if omit_merge_patch {
630 return Ok(());
631 }
632 writeln!(out)?;
633 }
634 }
635 let config = repo.config()?.clone();
636 let thread_policy = repo.thread_policy();
637 let entries =
638 commit_first_parent_diff_entries(repo, commit, &opts.pathspecs, &config, &thread_policy)?;
639 for entry in entries {
640 write_diff_entry_patch(
641 out,
642 repo,
643 odb,
644 &entry,
645 opts,
646 abbrev_cache,
647 Some(&commit.tree),
648 opts.binary_format,
649 &config,
650 )?;
651 }
652 Ok(())
653}
654
655pub fn write_commit_header_and_message<W: Write>(
657 out: &mut W,
658 author_ident: &str,
659 message: &str,
660) -> Result<()> {
661 writeln!(out, "Author: {}", format_ident_display(author_ident))?;
662 writeln!(out, "Date: {}", format_log_date(author_ident))?;
663 writeln!(out)?;
664 for line in message.lines() {
665 writeln!(out, " {line}")?;
666 }
667 Ok(())
668}
669
670fn format_ident_display(ident: &str) -> String {
671 let name = ident.find('<').map_or(ident.trim(), |i| ident[..i].trim());
672 let email = ident
673 .find('<')
674 .and_then(|start| ident.find('>').map(|end| &ident[start..=end]))
675 .unwrap_or("");
676 format!("{name} {email}")
677}
678
679fn format_log_date(ident: &str) -> String {
680 let parts: Vec<&str> = ident.rsplitn(3, ' ').collect();
681 if parts.len() < 2 {
682 return String::new();
683 }
684 let ts_str = parts[1];
685 let offset_str = parts[0];
686 let Ok(ts) = ts_str.parse::<i64>() else {
687 return format!("{ts_str} {offset_str}");
688 };
689 let tz_bytes = offset_str.as_bytes();
690 let tz_secs: i64 = if tz_bytes.len() >= 5 {
691 let sign = if tz_bytes[0] == b'-' { -1 } else { 1 };
692 let h: i64 = offset_str[1..3].parse().unwrap_or(0);
693 let m: i64 = offset_str[3..5].parse().unwrap_or(0);
694 sign * (h * 3600 + m * 60)
695 } else {
696 0
697 };
698 let adjusted = ts + tz_secs;
699 let dt = time::OffsetDateTime::from_unix_timestamp(adjusted)
700 .unwrap_or(time::OffsetDateTime::UNIX_EPOCH);
701 let weekday = match dt.weekday() {
702 time::Weekday::Monday => "Mon",
703 time::Weekday::Tuesday => "Tue",
704 time::Weekday::Wednesday => "Wed",
705 time::Weekday::Thursday => "Thu",
706 time::Weekday::Friday => "Fri",
707 time::Weekday::Saturday => "Sat",
708 time::Weekday::Sunday => "Sun",
709 };
710 let month = match dt.month() {
711 time::Month::January => "Jan",
712 time::Month::February => "Feb",
713 time::Month::March => "Mar",
714 time::Month::April => "Apr",
715 time::Month::May => "May",
716 time::Month::June => "Jun",
717 time::Month::July => "Jul",
718 time::Month::August => "Aug",
719 time::Month::September => "Sep",
720 time::Month::October => "Oct",
721 time::Month::November => "Nov",
722 time::Month::December => "Dec",
723 };
724 format!(
725 "{weekday} {month} {} {:02}:{:02}:{:02} {} {offset_str}",
726 dt.day(),
727 dt.hour(),
728 dt.minute(),
729 dt.second(),
730 dt.year()
731 )
732}
733
734pub fn write_diff_entry_log_patch<W: Write>(
740 out: &mut W,
741 repo: &Repository,
742 odb: &Odb,
743 entry: &DiffEntry,
744 opts: &LogPatchOptions,
745) -> Result<()> {
746 let config = repo.config()?.clone();
747 let mut abbrev_cache = OidAbbrevCache::new(opts.abbrev_len);
748 write_diff_entry_patch(
749 out,
750 repo,
751 odb,
752 entry,
753 opts,
754 &mut abbrev_cache,
755 None,
756 opts.binary_format,
757 &config,
758 )
759}
760
761fn index_oid_full(oid: &ObjectId) -> String {
762 oid.to_hex()
763}
764
765#[allow(clippy::too_many_arguments)]
766fn write_diff_entry_patch_headers<W: Write>(
767 out: &mut W,
768 repo: &Repository,
769 entry: &DiffEntry,
770 opts: &LogPatchOptions,
771 abbrev_cache: &mut OidAbbrevCache,
772 index_full_oid: bool,
773) -> Result<()> {
774 let git_old = entry
775 .old_path
776 .as_deref()
777 .or(entry.new_path.as_deref())
778 .unwrap_or("dev/null");
779 let git_new = entry
780 .new_path
781 .as_deref()
782 .or(entry.old_path.as_deref())
783 .unwrap_or("dev/null");
784 const QUOTE_FULLY: bool = true;
785 writeln!(
786 out,
787 "diff --git {} {}",
788 format_diff_path_with_prefix("a/", git_old, QUOTE_FULLY),
789 format_diff_path_with_prefix("b/", git_new, QUOTE_FULLY)
790 )?;
791
792 let old_mode = entry.old_mode.bits();
793 let new_mode = entry.new_mode.bits();
794
795 if old_mode == 0 {
796 writeln!(out, "new file mode {}", entry.new_mode)?;
797 } else if new_mode == 0 {
798 writeln!(out, "deleted file mode {}", entry.old_mode)?;
799 }
800
801 match entry.status {
802 DiffStatus::Renamed => {
803 writeln!(out, "similarity index {}%", entry.score.unwrap_or(100))?;
804 if let Some(path) = &entry.old_path {
805 writeln!(out, "rename from {path}")?;
806 }
807 if let Some(path) = &entry.new_path {
808 writeln!(out, "rename to {path}")?;
809 }
810 }
811 DiffStatus::Copied => {
812 writeln!(out, "similarity index {}%", entry.score.unwrap_or(100))?;
813 if let Some(path) = &entry.old_path {
814 writeln!(out, "copy from {path}")?;
815 }
816 if let Some(path) = &entry.new_path {
817 writeln!(out, "copy to {path}")?;
818 }
819 }
820 _ if old_mode != 0 && new_mode != 0 && old_mode != new_mode => {
821 writeln!(out, "old mode {}", entry.old_mode)?;
822 writeln!(out, "new mode {}", entry.new_mode)?;
823 }
824 _ => {}
825 }
826
827 if entry.old_oid != entry.new_oid {
828 let index_mode = if new_mode != 0 {
829 entry.new_mode
830 } else {
831 entry.old_mode
832 };
833 let mut index_pair = |old: &ObjectId, new: &ObjectId| -> Result<(String, String)> {
834 if index_full_oid {
835 Ok((index_oid_full(old), index_oid_full(new)))
836 } else {
837 abbrev_index_pair(abbrev_cache, repo, old, new, opts.abbrev_len)
838 }
839 };
840 match entry.status {
841 DiffStatus::Added => {
842 let (old_a, new_a) = index_pair(&zero_oid(), &entry.new_oid)?;
843 writeln!(out, "index {old_a}..{new_a}")?;
844 }
845 DiffStatus::Deleted => {
846 let (old_a, new_a) = index_pair(&entry.old_oid, &zero_oid())?;
847 writeln!(out, "index {old_a}..{new_a}")?;
848 }
849 DiffStatus::Renamed | DiffStatus::Copied => {
850 let (old_a, new_a) = index_pair(&entry.old_oid, &entry.new_oid)?;
851 writeln!(out, "index {old_a}..{new_a}")?;
852 }
853 _ => {
854 let (old_a, new_a) = index_pair(&entry.old_oid, &entry.new_oid)?;
855 writeln!(out, "index {old_a}..{new_a} {index_mode}")?;
856 }
857 }
858 }
859 Ok(())
860}
861
862#[allow(clippy::too_many_arguments)]
863fn write_diff_entry_patch<W: Write>(
864 out: &mut W,
865 repo: &Repository,
866 odb: &Odb,
867 entry: &DiffEntry,
868 opts: &LogPatchOptions,
869 abbrev_cache: &mut OidAbbrevCache,
870 new_tree: Option<&ObjectId>,
871 binary_format: LogPatchBinaryFormat,
872 config: &ConfigSet,
873) -> Result<()> {
874 let old_mode = entry.old_mode.bits();
875 let new_mode = entry.new_mode.bits();
876 let old_bytes = read_blob_side(odb, entry.old_oid, old_mode == 0)?;
877 let new_bytes = read_blob_side(odb, entry.new_oid, new_mode == 0)?;
878 write_diff_entry_patch_with_resolved_bytes(
879 out,
880 repo,
881 entry,
882 opts,
883 abbrev_cache,
884 binary_format,
885 config,
886 &old_bytes,
887 &new_bytes,
888 new_tree,
889 )
890}
891
892#[allow(clippy::too_many_arguments)]
893fn write_diff_entry_patch_with_resolved_bytes<W: Write>(
894 out: &mut W,
895 repo: &Repository,
896 entry: &DiffEntry,
897 opts: &LogPatchOptions,
898 abbrev_cache: &mut OidAbbrevCache,
899 binary_format: LogPatchBinaryFormat,
900 config: &ConfigSet,
901 old_bytes: &[u8],
902 new_bytes: &[u8],
903 new_tree: Option<&ObjectId>,
904) -> Result<()> {
905 const QUOTE_FULLY: bool = true;
906 let git_old = entry
907 .old_path
908 .as_deref()
909 .or(entry.new_path.as_deref())
910 .unwrap_or("dev/null");
911 let git_new = entry
912 .new_path
913 .as_deref()
914 .or(entry.old_path.as_deref())
915 .unwrap_or("dev/null");
916 let old_mode = entry.old_mode.bits();
917 let new_mode = entry.new_mode.bits();
918 let is_binary = merge_file::is_binary(old_bytes) || merge_file::is_binary(new_bytes);
919 let index_full_oid = is_binary && binary_format == LogPatchBinaryFormat::GitApplyBinaryPatch;
920 write_diff_entry_patch_headers(out, repo, entry, opts, abbrev_cache, index_full_oid)?;
921
922 if old_bytes == new_bytes {
923 return Ok(());
924 }
925
926 if is_binary {
927 match binary_format {
928 LogPatchBinaryFormat::LogShortMessage => {
929 let old_side = if old_mode == 0 {
930 "/dev/null".to_owned()
931 } else {
932 format_diff_path_with_prefix("a/", git_old, QUOTE_FULLY)
933 };
934 let new_side = if new_mode == 0 {
935 "/dev/null".to_owned()
936 } else {
937 format_diff_path_with_prefix("b/", git_new, QUOTE_FULLY)
938 };
939 writeln!(out, "Binary files {old_side} and {new_side} differ")?;
940 }
941 LogPatchBinaryFormat::GitApplyBinaryPatch => {
942 crate::apply::write_git_binary_patch_sections(out, old_bytes, new_bytes)?;
943 }
944 }
945 return Ok(());
946 }
947
948 if binary_format == LogPatchBinaryFormat::GitApplyBinaryPatch && new_tree.is_none() {
949 return write_patch_unified_body_from_bytes(
950 out, old_bytes, new_bytes, git_old, git_new, old_mode, new_mode, opts,
951 );
952 }
953 let byte_oriented = !crate::commit_encoding::is_strict_utf8(old_bytes)
954 || !crate::commit_encoding::is_strict_utf8(new_bytes);
955 let (old_text, new_text) = if byte_oriented {
956 (
957 crate::commit_encoding::decode_latin1(old_bytes),
958 crate::commit_encoding::decode_latin1(new_bytes),
959 )
960 } else {
961 let old_s = std::str::from_utf8(old_bytes)
962 .map_err(|_| crate::error::Error::CorruptObject("blob is not valid UTF-8".into()))?;
963 let new_s = std::str::from_utf8(new_bytes)
964 .map_err(|_| crate::error::Error::CorruptObject("blob is not valid UTF-8".into()))?;
965 (old_s.to_owned(), new_s.to_owned())
966 };
967 let old_path = if old_mode == 0 {
968 "/dev/null".to_owned()
969 } else {
970 git_old.to_owned()
971 };
972 let new_path = if new_mode == 0 {
973 "/dev/null".to_owned()
974 } else {
975 git_new.to_owned()
976 };
977 let d = &opts.diff;
978 let funcname_matcher = if let Some(tree) = new_tree {
979 log_patch_funcname_matcher(repo, config, tree, git_new)?
980 } else {
981 None
982 };
983 let funcname_ref = funcname_matcher.as_ref();
984 if byte_oriented {
985 let mut patch_buf = Vec::new();
986 super::write_unified_diff_from_diff_options(
987 &mut patch_buf,
988 &old_text,
989 &new_text,
990 &old_path,
991 &new_path,
992 "a/",
993 "b/",
994 funcname_ref,
995 d,
996 QUOTE_FULLY,
997 )?;
998 let patch_str = std::str::from_utf8(&patch_buf)
999 .map_err(|_| crate::error::Error::CorruptObject("patch re-encoding failed".into()))?;
1000 out.write_all(&crate::commit_encoding::encode_latin1_lossy(patch_str))?;
1001 } else {
1002 super::write_unified_diff_from_diff_options(
1003 out,
1004 &old_text,
1005 &new_text,
1006 &old_path,
1007 &new_path,
1008 "a/",
1009 "b/",
1010 funcname_ref,
1011 d,
1012 QUOTE_FULLY,
1013 )?;
1014 }
1015 Ok(())
1016}
1017
1018#[derive(Debug, Clone, PartialEq, Eq)]
1026pub enum PatchSide {
1027 Bytes(Vec<u8>),
1029 Blob,
1032 Worktree,
1035}
1036
1037#[derive(Debug, Clone)]
1039pub struct PatchResolvedSide {
1040 pub entry: DiffEntry,
1042 pub old: PatchSide,
1044 pub new: PatchSide,
1046}
1047
1048pub fn write_patch_resolved<W: Write>(
1070 repo: &Repository,
1071 files: &[PatchResolvedSide],
1072 opts: &PatchTextOptions,
1073 out: &mut W,
1074) -> Result<()> {
1075 if files.is_empty() {
1076 return Ok(());
1077 }
1078 let config = repo.config()?.clone();
1079 let thread_policy = repo.thread_policy();
1080 let log_opts = LogPatchOptions {
1081 diff: opts.diff.clone(),
1082 abbrev_len: opts.abbrev_len,
1083 commit_header: LogPatchCommitHeader::FormatPercentH,
1084 binary_format: LogPatchBinaryFormat::GitApplyBinaryPatch,
1085 pathspecs: Vec::new(),
1086 diff_merges: DiffMerges::FirstParent,
1087 };
1088 let mut abbrev_cache = OidAbbrevCache::new(opts.abbrev_len);
1089 let oids = files
1090 .iter()
1091 .flat_map(|f| [f.entry.old_oid, f.entry.new_oid]);
1092 abbrev_cache.warm_for_patch_batch(repo, oids, &thread_policy, &config)?;
1093 let mut worktree = None;
1094 for file in files {
1095 let old = resolve_patch_side(repo, file, EntrySide::Old, &mut worktree)?;
1096 let new = resolve_patch_side(repo, file, EntrySide::New, &mut worktree)?;
1097 let entry = if merge_file::is_binary(&old) || merge_file::is_binary(&new) {
1098 Cow::Owned(entry_with_content_ids(repo, file, &old, &new)?)
1099 } else {
1100 Cow::Borrowed(&file.entry)
1101 };
1102 write_diff_entry_patch_with_resolved_bytes(
1103 out,
1104 repo,
1105 &entry,
1106 &log_opts,
1107 &mut abbrev_cache,
1108 LogPatchBinaryFormat::GitApplyBinaryPatch,
1109 &config,
1110 &old,
1111 &new,
1112 None,
1113 )?;
1114 }
1115 Ok(())
1116}
1117
1118#[derive(Clone, Copy)]
1119enum EntrySide {
1120 Old,
1121 New,
1122}
1123
1124impl EntrySide {
1125 fn of(self, file: &PatchResolvedSide) -> (&PatchSide, FileMode, ObjectId) {
1126 let e = &file.entry;
1127 match self {
1128 Self::Old => (&file.old, e.old_mode, e.old_oid),
1129 Self::New => (&file.new, e.new_mode, e.new_oid),
1130 }
1131 }
1132
1133 fn path(self, entry: &DiffEntry) -> &str {
1134 match self {
1135 Self::Old => entry.old_path.as_deref().unwrap_or_else(|| entry.path()),
1136 Self::New => entry.path(),
1137 }
1138 }
1139}
1140
1141struct WorktreeSideReader<'r> {
1143 work_tree: &'r std::path::Path,
1144 index: Index,
1145 rules: WorktreeRules,
1146}
1147
1148impl<'r> WorktreeSideReader<'r> {
1149 fn open(repo: &'r Repository) -> Result<Self> {
1150 let work_tree = repo.work_tree().ok_or(Error::RequiresWorkTree)?;
1151 let index = repo.index_handle().load()?;
1152 let rules = WorktreeRules::from_repository(repo, &index)?;
1153 Ok(Self {
1154 work_tree,
1155 index,
1156 rules,
1157 })
1158 }
1159
1160 fn read(&self, odb: &Odb, path: &str, mode: u32) -> Result<Vec<u8>> {
1161 let index_entry = self.index.get(&crate::repo_relpath::storage_bytes(path), 0);
1162 read_worktree_side_bytes_for_diff(
1163 odb,
1164 self.work_tree,
1165 path,
1166 mode,
1167 self.rules.config(),
1168 index_entry,
1169 Some(&self.rules),
1170 )
1171 }
1172}
1173
1174fn resolve_patch_side<'f, 'r>(
1175 repo: &'r Repository,
1176 file: &'f PatchResolvedSide,
1177 side: EntrySide,
1178 worktree: &mut Option<WorktreeSideReader<'r>>,
1179) -> Result<Cow<'f, [u8]>> {
1180 let (source, mode, oid) = side.of(file);
1181 let mode = mode.bits();
1182 if mode == 0 {
1183 return Ok(Cow::Borrowed(&[]));
1184 }
1185 match source {
1186 PatchSide::Bytes(bytes) => Ok(Cow::Borrowed(bytes)),
1187 PatchSide::Blob => Ok(Cow::Owned(read_blob_side(repo.odb(), oid, false)?)),
1188 PatchSide::Worktree => {
1189 let reader = match worktree {
1190 Some(reader) => reader,
1191 None => worktree.insert(WorktreeSideReader::open(repo)?),
1192 };
1193 Ok(Cow::Owned(reader.read(
1194 repo.odb(),
1195 side.path(&file.entry),
1196 mode,
1197 )?))
1198 }
1199 }
1200}
1201
1202fn entry_with_content_ids(
1204 repo: &Repository,
1205 file: &PatchResolvedSide,
1206 old: &[u8],
1207 new: &[u8],
1208) -> Result<DiffEntry> {
1209 let content_id = |side: EntrySide, bytes: &[u8]| -> Result<ObjectId> {
1210 let (source, mode, oid) = side.of(file);
1211 if mode.is_absent() {
1212 return Ok(zero_oid());
1213 }
1214 match source {
1215 PatchSide::Blob => Ok(oid),
1216 PatchSide::Bytes(_) | PatchSide::Worktree => {
1217 repo.odb().hash_algo().hash_object(ObjectKind::Blob, bytes)
1218 }
1219 }
1220 };
1221 let mut entry = file.entry.clone();
1222 entry.old_oid = content_id(EntrySide::Old, old)?;
1223 entry.new_oid = content_id(EntrySide::New, new)?;
1224 Ok(entry)
1225}
1226
1227fn read_blob_side(odb: &Odb, oid: ObjectId, is_missing: bool) -> Result<Vec<u8>> {
1228 if is_missing || oid == zero_oid() || oid == empty_blob_oid() {
1229 return Ok(Vec::new());
1230 }
1231 let obj = odb.read(&oid)?;
1232 if obj.kind != ObjectKind::Blob {
1233 return Ok(Vec::new());
1234 }
1235 Ok(obj.data)
1236}
1237
1238#[cfg(test)]
1239mod tests {
1240 use super::*;
1241 use std::io::Cursor;
1242
1243 use crate::hash::hash_object;
1244 use crate::objects::HashAlgo;
1245
1246 #[test]
1247 fn copied_diff_emits_copy_headers() -> Result<()> {
1248 let blob_oid = hash_object(HashAlgo::Sha1, ObjectKind::Blob, b"x").expect("hash");
1249 let entry = DiffEntry {
1250 status: DiffStatus::Copied,
1251 old_path: Some("src/a.txt".into()),
1252 new_path: Some("dst/b.txt".into()),
1253 old_oid: blob_oid,
1254 new_oid: blob_oid,
1255 old_mode: FileMode::REGULAR,
1256 new_mode: FileMode::REGULAR,
1257 score: Some(100),
1258 };
1259 let dir = tempfile::tempdir().expect("tempdir");
1260 let repo = Repository::init(dir.path(), &crate::repo::InitOptions::default())?;
1261 repo.odb().write(ObjectKind::Blob, b"x")?;
1262 let mut out = Cursor::new(Vec::new());
1263 let opts = LogPatchOptions::git_default(&ConfigSet::default());
1264 write_diff_entry_log_patch(&mut out, &repo, repo.odb(), &entry, &opts)?;
1265 let text = String::from_utf8(out.into_inner()).expect("utf-8");
1266 assert!(text.contains("copy from src/a.txt"));
1267 assert!(text.contains("copy to dst/b.txt"));
1268 assert!(!text.contains("rename from"));
1269 Ok(())
1270 }
1271}