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