1use std::path::Path;
4
5use crate::config::ConfigSet;
6use crate::diff::combined_diff_patch::{
7 format_combined_structured_body, structured_combined_diff_body, CombinedDiffWsOptions,
8 CombinedStructuredTextDiff,
9};
10use crate::diff::combined_tree_diff::{
11 combined_diff_paths_trees, combined_parent_status_char, CombinedDiffPath,
12 CombinedTreeDiffOptions,
13};
14use crate::diff::detail::{DiffDetail, DiffTotals, FileDiff, LineStat};
15use crate::diff::files::{build_file_diffs, DiffFilesOutput};
16use crate::diff::merge_diff::{
17 blob_text_for_diff, enrich_combined_path_renames, format_combined_binary, is_binary_for_diff,
18 read_blob_at_path,
19};
20use crate::diff::{DiffEntry, DiffOptions, DiffStatus};
21use crate::error::Result;
22use crate::objects::pack_store::PackReadContextGuard;
23use crate::objects::{parse_commit, ObjectId};
24use crate::odb::Odb;
25use crate::parallel::{try_par_map_with_local, ParallelHashError, ParallelOp, ThreadPolicy};
26use crate::repo::Repository;
27use std::sync::Arc;
28
29#[non_exhaustive]
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
32pub enum DiffMerges {
33 FirstParent,
35 #[default]
37 Combined,
38}
39
40impl DiffMerges {
41 pub fn parse(value: &str) -> std::result::Result<Self, String> {
47 match value {
48 "first-parent" | "first_parent" => Ok(Self::FirstParent),
49 "combined" | "dense-combined" | "dense_combined" => Ok(Self::Combined),
50 other => Err(format!(
51 "unknown --diff-merges mode '{other}' (expected first-parent or combined)"
52 )),
53 }
54 }
55}
56
57fn combined_ws(_options: &DiffOptions) -> CombinedDiffWsOptions {
58 CombinedDiffWsOptions::default()
59}
60
61fn combined_status_letters(path: &CombinedDiffPath) -> String {
62 path.parents
63 .iter()
64 .map(|s| combined_parent_status_char(s.status))
65 .collect()
66}
67
68fn file_diff_from_combined_path(path: CombinedDiffPath) -> FileDiff {
69 let combined_status = combined_status_letters(&path);
70 FileDiff {
71 path: path.path,
72 old_path: None,
73 status: DiffStatus::Modified,
74 old_mode: None,
75 new_mode: None,
76 binary: false,
77 similarity: None,
78 stat: None,
79 structured: None,
80 combined: None,
81 combined_status: Some(combined_status),
82 }
83}
84
85fn filter_combined_paths(paths: Vec<String>, pathspecs: &[String]) -> Vec<String> {
86 if pathspecs.is_empty() {
87 return paths;
88 }
89 paths
90 .into_iter()
91 .filter(|p| crate::pathspec::path_allowed_by_pathspec_list(pathspecs, p))
92 .collect()
93}
94
95#[allow(clippy::too_many_arguments)]
101pub fn commit_diff_files(
102 repo: &Repository,
103 commit_oid: &ObjectId,
104 merges: DiffMerges,
105 detail: DiffDetail,
106 options: &DiffOptions,
107 pathspecs: &[String],
108 config: &ConfigSet,
109 thread_policy: &ThreadPolicy,
110) -> Result<DiffFilesOutput> {
111 let data = repo.odb().read(commit_oid)?.data;
112 let commit = parse_commit(&data)?;
113 if commit.parents.len() < 2 || merges == DiffMerges::FirstParent {
114 let old_tree = match commit.parents.first() {
115 Some(parent) => Some(repo.odb().commit_tree(parent)?),
116 None => None,
117 };
118 let entries = tree_diff_entries(
119 repo.odb(),
120 old_tree.as_ref(),
121 Some(&commit.tree),
122 "",
123 config,
124 thread_policy,
125 )?;
126 let filtered = filter_entries(repo, entries, pathspecs)?;
127 let mut out = build_file_diffs(
128 repo.odb(),
129 filtered,
130 detail,
131 options
132 .clone()
133 .with_renames(crate::diff::RenameDetection::Off),
134 config,
135 thread_policy,
136 )?;
137 out.files.sort_by(|a, b| a.path.cmp(&b.path));
138 return Ok(out);
139 }
140
141 combined_commit_diff_files(
142 repo,
143 &commit.tree,
144 &commit.parents,
145 detail,
146 options,
147 pathspecs,
148 config,
149 thread_policy,
150 )
151}
152
153fn filter_entries(
154 repo: &Repository,
155 entries: Vec<DiffEntry>,
156 pathspecs: &[String],
157) -> Result<Vec<DiffEntry>> {
158 if pathspecs.is_empty() {
159 return Ok(entries);
160 }
161 let matcher = repo.pathspec_matcher(pathspecs)?;
162 Ok(entries
163 .into_iter()
164 .filter(|entry| {
165 let path = entry
166 .new_path
167 .as_deref()
168 .or(entry.old_path.as_deref())
169 .unwrap_or("");
170 matcher.matches_change(path, entry.old_path.as_deref())
171 })
172 .collect())
173}
174
175pub(crate) fn commit_first_parent_diff_entries(
181 repo: &Repository,
182 commit: &crate::objects::CommitData,
183 pathspecs: &[String],
184 config: &ConfigSet,
185 thread_policy: &ThreadPolicy,
186) -> Result<Vec<DiffEntry>> {
187 let odb = repo.odb();
188 let old_tree = match commit.parents.first() {
189 Some(parent) => Some(odb.commit_tree(parent)?),
190 None => None,
191 };
192 let entries = tree_diff_entries(
193 odb,
194 old_tree.as_ref(),
195 Some(&commit.tree),
196 "",
197 config,
198 thread_policy,
199 )?;
200 filter_entries(repo, entries, pathspecs)
201}
202
203fn tree_diff_entries(
204 odb: &Odb,
205 old_tree: Option<&ObjectId>,
206 new_tree: Option<&ObjectId>,
207 prefix: &str,
208 config: &ConfigSet,
209 thread_policy: &ThreadPolicy,
210) -> Result<Vec<DiffEntry>> {
211 let threshold = crate::diff::renames_from_config(config)
212 .threshold_percent()
213 .unwrap_or(crate::diff::RENAME_DETECTION_DISABLED);
214 crate::diff::diff_trees_with_renames_and_policy(
215 odb,
216 old_tree,
217 new_tree,
218 prefix,
219 threshold,
220 config,
221 thread_policy,
222 )
223}
224
225#[allow(clippy::too_many_arguments)]
226fn combined_commit_diff_files(
227 repo: &Repository,
228 result_tree: &ObjectId,
229 parent_commits: &[ObjectId],
230 detail: DiffDetail,
231 options: &DiffOptions,
232 pathspecs: &[String],
233 config: &ConfigSet,
234 thread_policy: &ThreadPolicy,
235) -> Result<DiffFilesOutput> {
236 let odb = repo.odb();
237 let mut parent_trees = Vec::with_capacity(parent_commits.len());
238 for p in parent_commits {
239 parent_trees.push(odb.commit_tree(p)?);
240 }
241
242 let walk = CombinedTreeDiffOptions::default();
243 let parent_tree_opts: Vec<Option<ObjectId>> = parent_trees.iter().copied().map(Some).collect();
244 let mut paths = combined_diff_paths_trees(odb, result_tree, &parent_tree_opts, &walk, None)?;
245 let path_names: Vec<String> = paths.iter().map(|p| p.path.clone()).collect();
246 let filtered_names = filter_combined_paths(path_names, pathspecs);
247 paths.retain(|p| filtered_names.iter().any(|n| n == &p.path));
248
249 for path in &mut paths {
250 enrich_combined_path_renames(odb, path, &parent_trees, result_tree, 50);
251 }
252
253 if detail == DiffDetail::NameOnly {
254 let files: Vec<FileDiff> = paths
255 .into_iter()
256 .map(|path| FileDiff {
257 path: path.path,
258 old_path: None,
259 status: DiffStatus::Modified,
260 old_mode: None,
261 new_mode: None,
262 binary: false,
263 similarity: None,
264 stat: None,
265 structured: None,
266 combined: None,
267 combined_status: None,
268 })
269 .collect();
270 return Ok(DiffFilesOutput {
271 files,
272 totals: None,
273 });
274 }
275 if detail == DiffDetail::NameStatus {
276 let files: Vec<FileDiff> = paths
277 .into_iter()
278 .map(file_diff_from_combined_path)
279 .collect();
280 return Ok(DiffFilesOutput {
281 files,
282 totals: None,
283 });
284 }
285
286 let git_dir = repo.git_dir();
287 let ws = combined_ws(options);
288 let context = options.context_lines;
289 let par_opts = thread_policy.resolve(ParallelOp::DiffContent, config);
290 let store = odb.pack_store_for(odb.objects_dir());
291 let result_tree = *result_tree;
292 let rows = try_par_map_with_local(
293 &paths,
294 par_opts,
295 0,
296 || PackReadContextGuard::install(Arc::clone(&store)),
297 |_ctx, path| {
298 combined_path_file_diff(git_dir, config, odb, path, result_tree, detail, context, ws)
299 },
300 );
301 let rows = match rows {
302 Ok(rows) => rows,
303 Err(ParallelHashError::Task(err)) => return Err(err),
304 };
305
306 let mut files = Vec::with_capacity(rows.len());
307 let mut insertions = 0usize;
308 let mut deletions = 0usize;
309 for (file, stat) in rows {
310 if let Some(s) = stat {
311 insertions += s.insertions;
312 deletions += s.deletions;
313 }
314 files.push(file);
315 }
316 files.sort_by(|a, b| a.path.cmp(&b.path));
317
318 let totals = if detail == DiffDetail::Stat {
319 Some(DiffTotals {
320 files_changed: files.len(),
321 insertions,
322 deletions,
323 })
324 } else {
325 None
326 };
327 Ok(DiffFilesOutput { files, totals })
328}
329
330#[allow(clippy::too_many_arguments)]
331fn combined_path_file_diff(
332 git_dir: &Path,
333 config: &ConfigSet,
334 odb: &Odb,
335 path: &CombinedDiffPath,
336 result_tree: ObjectId,
337 detail: DiffDetail,
338 context: usize,
339 ws: CombinedDiffWsOptions,
340) -> Result<(FileDiff, Option<LineStat>)> {
341 let (parent_blobs, result_blob) = load_combined_path_blobs(odb, path, &result_tree)?;
342 let binary =
343 combined_path_is_binary_from_blobs(git_dir, &path.path, &parent_blobs, &result_blob);
344 if binary {
345 let parent_oids: Vec<ObjectId> = path.parents.iter().map(|s| s.oid).collect();
346 let _patch = format_combined_binary(&path.path, &parent_oids, &path.merge_oid, 7, true);
347 return Ok((
348 FileDiff {
349 path: path.path.clone(),
350 old_path: None,
351 status: DiffStatus::Modified,
352 old_mode: None,
353 new_mode: None,
354 binary: true,
355 similarity: None,
356 stat: if detail == DiffDetail::Stat {
357 Some(LineStat::default())
358 } else {
359 None
360 },
361 structured: None,
362 combined: None,
363 combined_status: Some(combined_status_letters(path)),
364 },
365 None,
366 ));
367 }
368
369 let mut parent_texts = Vec::with_capacity(path.parents.len());
370 for (i, side) in path.parents.iter().enumerate() {
371 let text = if side.oid.is_zero() {
372 String::new()
373 } else {
374 blob_text_for_diff(git_dir, config, &path.path, &parent_blobs[i], false)
375 };
376 parent_texts.push(text);
377 }
378 let result_text = blob_text_for_diff(git_dir, config, &path.path, &result_blob, false);
379 let combined = structured_combined_diff_body(&parent_texts, &result_text, context, true, ws);
380 let stat = match detail {
381 DiffDetail::Stat => Some(combined.line_stat()),
382 _ => None,
383 };
384 Ok((
385 FileDiff {
386 path: path.path.clone(),
387 old_path: None,
388 status: DiffStatus::Modified,
389 old_mode: None,
390 new_mode: None,
391 binary: false,
392 similarity: None,
393 stat,
394 structured: None,
395 combined: Some(combined),
396 combined_status: Some(combined_status_letters(path)),
397 },
398 stat,
399 ))
400}
401
402fn load_combined_path_blobs(
403 odb: &Odb,
404 path: &CombinedDiffPath,
405 result_tree: &ObjectId,
406) -> Result<(Vec<Vec<u8>>, Vec<u8>)> {
407 let mut parent_blobs = Vec::with_capacity(path.parents.len());
408 for side in &path.parents {
409 if side.oid.is_zero() {
410 parent_blobs.push(Vec::new());
411 } else {
412 parent_blobs.push(odb.read(&side.oid)?.data);
413 }
414 }
415 let result_blob = read_blob_at_path(odb, result_tree, &path.path).unwrap_or_default();
416 Ok((parent_blobs, result_blob))
417}
418
419fn combined_path_is_binary_from_blobs(
420 git_dir: &Path,
421 path: &str,
422 parent_blobs: &[Vec<u8>],
423 result_blob: &[u8],
424) -> bool {
425 if is_binary_for_diff(git_dir, path, result_blob) {
426 return true;
427 }
428 parent_blobs
429 .iter()
430 .any(|blob| !blob.is_empty() && is_binary_for_diff(git_dir, path, blob))
431}
432
433#[must_use]
435pub fn combined_structured_patch_body(diff: &CombinedStructuredTextDiff) -> String {
436 format_combined_structured_body(diff)
437}