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.clone(),
132 config,
133 thread_policy,
134 )?;
135 out.files.sort_by(|a, b| a.path.cmp(&b.path));
136 return Ok(out);
137 }
138
139 combined_commit_diff_files(
140 repo,
141 &commit.tree,
142 &commit.parents,
143 detail,
144 options,
145 pathspecs,
146 config,
147 thread_policy,
148 )
149}
150
151fn filter_entries(
152 repo: &Repository,
153 entries: Vec<DiffEntry>,
154 pathspecs: &[String],
155) -> Result<Vec<DiffEntry>> {
156 if pathspecs.is_empty() {
157 return Ok(entries);
158 }
159 let matcher = repo.pathspec_matcher(pathspecs)?;
160 Ok(entries
161 .into_iter()
162 .filter(|entry| {
163 let path = entry
164 .new_path
165 .as_deref()
166 .or(entry.old_path.as_deref())
167 .unwrap_or("");
168 matcher.matches_change(path, entry.old_path.as_deref())
169 })
170 .collect())
171}
172
173pub(crate) fn commit_first_parent_diff_entries(
179 repo: &Repository,
180 commit: &crate::objects::CommitData,
181 pathspecs: &[String],
182 config: &ConfigSet,
183 thread_policy: &ThreadPolicy,
184) -> Result<Vec<DiffEntry>> {
185 let odb = repo.odb();
186 let old_tree = match commit.parents.first() {
187 Some(parent) => Some(odb.commit_tree(parent)?),
188 None => None,
189 };
190 let entries = tree_diff_entries(
191 odb,
192 old_tree.as_ref(),
193 Some(&commit.tree),
194 "",
195 config,
196 thread_policy,
197 )?;
198 filter_entries(repo, entries, pathspecs)
199}
200
201fn tree_diff_entries(
202 odb: &Odb,
203 old_tree: Option<&ObjectId>,
204 new_tree: Option<&ObjectId>,
205 prefix: &str,
206 config: &ConfigSet,
207 thread_policy: &ThreadPolicy,
208) -> Result<Vec<DiffEntry>> {
209 let threshold = crate::diff::renames_from_config(config)
210 .threshold_percent()
211 .unwrap_or(100);
212 crate::diff::diff_trees_with_renames_and_policy(
213 odb,
214 old_tree,
215 new_tree,
216 prefix,
217 threshold,
218 config,
219 thread_policy,
220 )
221}
222
223#[allow(clippy::too_many_arguments)]
224fn combined_commit_diff_files(
225 repo: &Repository,
226 result_tree: &ObjectId,
227 parent_commits: &[ObjectId],
228 detail: DiffDetail,
229 options: &DiffOptions,
230 pathspecs: &[String],
231 config: &ConfigSet,
232 thread_policy: &ThreadPolicy,
233) -> Result<DiffFilesOutput> {
234 let odb = repo.odb();
235 let mut parent_trees = Vec::with_capacity(parent_commits.len());
236 for p in parent_commits {
237 parent_trees.push(odb.commit_tree(p)?);
238 }
239
240 let walk = CombinedTreeDiffOptions::default();
241 let parent_tree_opts: Vec<Option<ObjectId>> = parent_trees.iter().copied().map(Some).collect();
242 let mut paths = combined_diff_paths_trees(odb, result_tree, &parent_tree_opts, &walk, None)?;
243 let path_names: Vec<String> = paths.iter().map(|p| p.path.clone()).collect();
244 let filtered_names = filter_combined_paths(path_names, pathspecs);
245 paths.retain(|p| filtered_names.iter().any(|n| n == &p.path));
246
247 for path in &mut paths {
248 enrich_combined_path_renames(odb, path, &parent_trees, result_tree, 50);
249 }
250
251 if detail == DiffDetail::NameOnly {
252 let files: Vec<FileDiff> = paths
253 .into_iter()
254 .map(|path| FileDiff {
255 path: path.path,
256 old_path: None,
257 status: DiffStatus::Modified,
258 old_mode: None,
259 new_mode: None,
260 binary: false,
261 similarity: None,
262 stat: None,
263 structured: None,
264 combined: None,
265 combined_status: None,
266 })
267 .collect();
268 return Ok(DiffFilesOutput {
269 files,
270 totals: None,
271 });
272 }
273 if detail == DiffDetail::NameStatus {
274 let files: Vec<FileDiff> = paths
275 .into_iter()
276 .map(file_diff_from_combined_path)
277 .collect();
278 return Ok(DiffFilesOutput {
279 files,
280 totals: None,
281 });
282 }
283
284 let git_dir = repo.git_dir();
285 let ws = combined_ws(options);
286 let context = options.context_lines;
287 let par_opts = thread_policy.resolve(ParallelOp::DiffContent, config);
288 let store = odb.pack_store_for(odb.objects_dir());
289 let result_tree = *result_tree;
290 let rows = try_par_map_with_local(
291 &paths,
292 par_opts,
293 0,
294 || PackReadContextGuard::install(Arc::clone(&store)),
295 |_ctx, path| {
296 combined_path_file_diff(git_dir, config, odb, path, result_tree, detail, context, ws)
297 },
298 );
299 let rows = match rows {
300 Ok(rows) => rows,
301 Err(ParallelHashError::Task(err)) => return Err(err),
302 };
303
304 let mut files = Vec::with_capacity(rows.len());
305 let mut insertions = 0usize;
306 let mut deletions = 0usize;
307 for (file, stat) in rows {
308 if let Some(s) = stat {
309 insertions += s.insertions;
310 deletions += s.deletions;
311 }
312 files.push(file);
313 }
314 files.sort_by(|a, b| a.path.cmp(&b.path));
315
316 let totals = if detail == DiffDetail::Stat {
317 Some(DiffTotals {
318 files_changed: files.len(),
319 insertions,
320 deletions,
321 })
322 } else {
323 None
324 };
325 Ok(DiffFilesOutput { files, totals })
326}
327
328#[allow(clippy::too_many_arguments)]
329fn combined_path_file_diff(
330 git_dir: &Path,
331 config: &ConfigSet,
332 odb: &Odb,
333 path: &CombinedDiffPath,
334 result_tree: ObjectId,
335 detail: DiffDetail,
336 context: usize,
337 ws: CombinedDiffWsOptions,
338) -> Result<(FileDiff, Option<LineStat>)> {
339 let (parent_blobs, result_blob) = load_combined_path_blobs(odb, path, &result_tree)?;
340 let binary =
341 combined_path_is_binary_from_blobs(git_dir, &path.path, &parent_blobs, &result_blob);
342 if binary {
343 let parent_oids: Vec<ObjectId> = path.parents.iter().map(|s| s.oid).collect();
344 let _patch = format_combined_binary(&path.path, &parent_oids, &path.merge_oid, 7, true);
345 return Ok((
346 FileDiff {
347 path: path.path.clone(),
348 old_path: None,
349 status: DiffStatus::Modified,
350 old_mode: None,
351 new_mode: None,
352 binary: true,
353 similarity: None,
354 stat: if detail == DiffDetail::Stat {
355 Some(LineStat::default())
356 } else {
357 None
358 },
359 structured: None,
360 combined: None,
361 combined_status: Some(combined_status_letters(path)),
362 },
363 None,
364 ));
365 }
366
367 let mut parent_texts = Vec::with_capacity(path.parents.len());
368 for (i, side) in path.parents.iter().enumerate() {
369 let text = if side.oid.is_zero() {
370 String::new()
371 } else {
372 blob_text_for_diff(git_dir, config, &path.path, &parent_blobs[i], false)
373 };
374 parent_texts.push(text);
375 }
376 let result_text = blob_text_for_diff(git_dir, config, &path.path, &result_blob, false);
377 let combined = structured_combined_diff_body(&parent_texts, &result_text, context, true, ws);
378 let stat = match detail {
379 DiffDetail::Stat => Some(combined.line_stat()),
380 _ => None,
381 };
382 Ok((
383 FileDiff {
384 path: path.path.clone(),
385 old_path: None,
386 status: DiffStatus::Modified,
387 old_mode: None,
388 new_mode: None,
389 binary: false,
390 similarity: None,
391 stat,
392 structured: None,
393 combined: Some(combined),
394 combined_status: Some(combined_status_letters(path)),
395 },
396 stat,
397 ))
398}
399
400fn load_combined_path_blobs(
401 odb: &Odb,
402 path: &CombinedDiffPath,
403 result_tree: &ObjectId,
404) -> Result<(Vec<Vec<u8>>, Vec<u8>)> {
405 let mut parent_blobs = Vec::with_capacity(path.parents.len());
406 for side in &path.parents {
407 if side.oid.is_zero() {
408 parent_blobs.push(Vec::new());
409 } else {
410 parent_blobs.push(odb.read(&side.oid)?.data);
411 }
412 }
413 let result_blob = read_blob_at_path(odb, result_tree, &path.path).unwrap_or_default();
414 Ok((parent_blobs, result_blob))
415}
416
417fn combined_path_is_binary_from_blobs(
418 git_dir: &Path,
419 path: &str,
420 parent_blobs: &[Vec<u8>],
421 result_blob: &[u8],
422) -> bool {
423 if is_binary_for_diff(git_dir, path, result_blob) {
424 return true;
425 }
426 parent_blobs
427 .iter()
428 .any(|blob| !blob.is_empty() && is_binary_for_diff(git_dir, path, blob))
429}
430
431#[must_use]
433pub fn combined_structured_patch_body(diff: &CombinedStructuredTextDiff) -> String {
434 format_combined_structured_body(diff)
435}