1use std::collections::HashMap;
4
5use crate::config::ConfigSet;
6use crate::error::Result;
7use crate::objects::{parse_commit, ObjectId};
8use crate::odb::Odb;
9use crate::parallel::{try_par_map, ParallelHashError, ParallelOp, ParallelOptions, ThreadPolicy};
10
11use super::core::diff_trees_with_renames_for_pathspecs_and_policy;
12use super::detail::{DiffDetail, DiffTotals};
13use super::files::{build_file_diffs, DiffFilesOutput};
14use super::options::DiffOptions;
15use super::Diff;
16use super::DiffEntry;
17
18#[derive(Debug, Clone)]
20pub struct CommitFilesManyOptions {
21 pub diff_options: DiffOptions,
23 pub detail: DiffDetail,
25 pub skip_merge_patches: bool,
27}
28
29#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct CommitFilesResult {
32 pub output: DiffFilesOutput,
34 pub parent_count: usize,
36}
37
38struct CommitWork {
39 tree: ObjectId,
40 parent_tree: Option<ObjectId>,
41 parent_count: usize,
42}
43
44fn rename_threshold(options: &DiffOptions) -> u32 {
45 options.renames.threshold_percent().unwrap_or(101)
46}
47
48fn filter_entries(
49 repo: &crate::repo::Repository,
50 entries: Vec<DiffEntry>,
51 pathspecs: &[String],
52) -> Result<Vec<DiffEntry>> {
53 if pathspecs.is_empty() {
54 return Ok(entries);
55 }
56 let matcher = repo.pathspec_matcher(pathspecs)?;
57 Ok(entries
58 .into_iter()
59 .filter(|entry| {
60 let path = entry
61 .new_path
62 .as_deref()
63 .or(entry.old_path.as_deref())
64 .unwrap_or("");
65 matcher.matches_change(path, entry.old_path.as_deref())
66 })
67 .collect())
68}
69
70fn parent_tree_for_commit(
71 odb: &Odb,
72 parent: &ObjectId,
73 trees_by_commit: &HashMap<ObjectId, ObjectId>,
74) -> Result<ObjectId> {
75 if let Some(tree) = trees_by_commit.get(parent) {
76 return Ok(*tree);
77 }
78 let raw = odb.read(parent)?;
79 Ok(parse_commit(&raw.data)?.tree)
80}
81
82fn totals_for_detail(files: &[super::detail::FileDiff], detail: DiffDetail) -> Option<DiffTotals> {
83 match detail {
84 DiffDetail::Stat | DiffDetail::Patch => Some(DiffTotals::from_files(files)),
85 DiffDetail::NameOnly | DiffDetail::NameStatus => Some(DiffTotals {
86 files_changed: files.len(),
87 insertions: 0,
88 deletions: 0,
89 }),
90 }
91}
92
93fn commit_files_many_parallel_options(
99 thread_policy: &ThreadPolicy,
100 config: &ConfigSet,
101) -> ParallelOptions {
102 let batch_policy = if thread_policy.threads_override == Some(1) {
103 *thread_policy
104 } else {
105 ThreadPolicy {
106 threads_override: thread_policy.threads_override.or(Some(0)),
107 min_items_override: thread_policy.min_items_override,
108 min_total_bytes_override: thread_policy.min_total_bytes_override,
109 }
110 };
111 let mut par_opts = batch_policy.resolve(ParallelOp::DiffContent, config);
112 par_opts.min_items = par_opts.min_items.min(2);
113 par_opts.min_total_bytes = None;
114 par_opts
115}
116
117fn diff_one_commit(
118 diff: &Diff,
119 work: &CommitWork,
120 pathspecs: &[String],
121 opts: &CommitFilesManyOptions,
122 config: &ConfigSet,
123 thread_policy: &ThreadPolicy,
124) -> Result<CommitFilesResult> {
125 if opts.skip_merge_patches && opts.detail == DiffDetail::Patch && work.parent_count > 1 {
126 return Ok(CommitFilesResult {
127 output: DiffFilesOutput {
128 files: Vec::new(),
129 totals: Some(DiffTotals {
130 files_changed: 0,
131 insertions: 0,
132 deletions: 0,
133 }),
134 inexact_rename_skipped: None,
135 },
136 parent_count: work.parent_count,
137 });
138 }
139
140 let repo = diff.repository();
141 let odb = repo.odb();
142 let pathspec_matcher = if pathspecs.is_empty() {
143 None
144 } else {
145 Some(repo.pathspec_matcher(pathspecs)?)
146 };
147 let mut entries = diff_trees_with_renames_for_pathspecs_and_policy(
148 odb,
149 work.parent_tree.as_ref(),
150 Some(&work.tree),
151 "",
152 pathspecs,
153 pathspec_matcher.as_ref(),
154 rename_threshold(&opts.diff_options),
155 config,
156 thread_policy,
157 )?;
158 if rename_threshold(&opts.diff_options) >= 100 {
159 super::sort_entries_by_path(&mut entries);
160 }
161 entries = filter_entries(diff.repository(), entries, pathspecs)?;
162 let mut output = build_file_diffs(
163 odb,
164 entries,
165 opts.detail,
166 opts.diff_options
167 .clone()
168 .with_renames(crate::diff::RenameDetection::Off),
169 config,
170 thread_policy,
171 )?;
172 if output.totals.is_none() {
173 output.totals = totals_for_detail(&output.files, opts.detail);
174 }
175 Ok(CommitFilesResult {
176 output,
177 parent_count: work.parent_count,
178 })
179}
180
181#[derive(Debug, Clone)]
183pub struct CommitFilesManyInput {
184 pub oid: ObjectId,
186 pub parents: Vec<ObjectId>,
188 pub tree: ObjectId,
190}
191
192impl Diff<'_> {
193 pub fn commit_files_many(
205 &self,
206 oids: &[ObjectId],
207 pathspecs: &[String],
208 opts: CommitFilesManyOptions,
209 config: &ConfigSet,
210 thread_policy: &ThreadPolicy,
211 ) -> Result<Vec<CommitFilesResult>> {
212 if oids.is_empty() {
213 return Ok(Vec::new());
214 }
215 let odb = self.repository().odb();
216 let mut inputs = Vec::with_capacity(oids.len());
217 for oid in oids {
218 let raw = odb.read(oid)?;
219 let commit = parse_commit(&raw.data)?;
220 inputs.push(CommitFilesManyInput {
221 oid: *oid,
222 parents: commit.parents.clone(),
223 tree: commit.tree,
224 });
225 }
226 self.commit_files_many_work(&inputs, pathspecs, opts, config, thread_policy)
227 }
228
229 pub fn commit_files_many_work(
235 &self,
236 inputs: &[CommitFilesManyInput],
237 pathspecs: &[String],
238 opts: CommitFilesManyOptions,
239 config: &ConfigSet,
240 thread_policy: &ThreadPolicy,
241 ) -> Result<Vec<CommitFilesResult>> {
242 if inputs.is_empty() {
243 return Ok(Vec::new());
244 }
245 let repo = self.repository();
246 let odb = repo.odb();
247 let mut trees_by_commit: HashMap<ObjectId, ObjectId> = HashMap::with_capacity(inputs.len());
248 for input in inputs {
249 trees_by_commit.insert(input.oid, input.tree);
250 }
251
252 let mut work_items: Vec<CommitWork> = Vec::with_capacity(inputs.len());
253 for (i, input) in inputs.iter().enumerate() {
254 let parent_count = input.parents.len();
255 let parent_tree = if parent_count == 0 {
256 None
257 } else if i + 1 < inputs.len() && input.parents[0] == inputs[i + 1].oid {
258 Some(inputs[i + 1].tree)
259 } else {
260 Some(parent_tree_for_commit(
261 odb,
262 &input.parents[0],
263 &trees_by_commit,
264 )?)
265 };
266 work_items.push(CommitWork {
267 tree: input.tree,
268 parent_tree,
269 parent_count,
270 });
271 }
272
273 odb.with_pack_read_context(|| {
274 let par_opts = commit_files_many_parallel_options(thread_policy, config);
275 try_par_map(&work_items, par_opts, 0, |work| {
276 diff_one_commit(self, work, pathspecs, &opts, config, thread_policy)
277 })
278 .map_err(|err| match err {
279 ParallelHashError::Task(e) => e,
280 })
281 })
282 }
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288 use crate::objects::{serialize_commit, CommitData, ObjectKind, TreeEntry};
289 use crate::repo::{InitOptions, Repository};
290
291 fn tree_with(repo: &Repository, entries: &[(&str, &[u8])]) -> ObjectId {
292 let mut tes = Vec::new();
293 for (name, data) in entries {
294 let oid = repo.odb().write(ObjectKind::Blob, data).expect("blob");
295 tes.push(TreeEntry {
296 mode: 0o100644,
297 name: name.as_bytes().to_vec(),
298 oid,
299 });
300 }
301 let data = crate::objects::serialize_tree(&tes);
302 repo.odb().write(ObjectKind::Tree, &data).expect("tree")
303 }
304
305 fn write_commit(
306 repo: &Repository,
307 tree: ObjectId,
308 parents: Vec<ObjectId>,
309 msg: &str,
310 ) -> ObjectId {
311 let ident = "A <a@example.com> 0 +0000";
312 let commit = CommitData {
313 tree,
314 parents,
315 author: ident.into(),
316 committer: ident.into(),
317 author_raw: Vec::new(),
318 committer_raw: Vec::new(),
319 encoding: None,
320 message: format!("{msg}\n"),
321 raw_message: None,
322 extra_headers: Vec::new(),
323 };
324 repo.odb()
325 .write(ObjectKind::Commit, &serialize_commit(&commit))
326 .expect("commit")
327 }
328
329 #[test]
330 fn commit_files_many_matches_single_commit_calls() -> Result<()> {
331 let dir = tempfile::tempdir().expect("tempdir");
332 let repo = Repository::init(dir.path(), &InitOptions::default())?;
333 let tree1 = tree_with(&repo, &[("f.txt", b"a\n")]);
334 let root = write_commit(&repo, tree1, vec![], "root");
335 let tree2 = tree_with(&repo, &[("f.txt", b"b\n")]);
336 let child = write_commit(&repo, tree2, vec![root], "child");
337
338 let config = ConfigSet::default();
339 let policy = ThreadPolicy::default();
340 let diff_options = DiffOptions::default();
341 let opts = CommitFilesManyOptions {
342 diff_options,
343 detail: DiffDetail::Stat,
344 skip_merge_patches: true,
345 };
346
347 let many = repo
348 .diff()
349 .commit_files_many(&[child], &[], opts.clone(), &config, &policy)?;
350 assert_eq!(many.len(), 1);
351
352 let entries = diff_trees_with_renames_for_pathspecs_and_policy(
353 repo.odb(),
354 Some(&tree1),
355 Some(&tree2),
356 "",
357 &[],
358 None,
359 101,
360 &config,
361 &policy,
362 )?;
363 let single = build_file_diffs(
364 repo.odb(),
365 entries,
366 opts.detail,
367 opts.diff_options
368 .clone()
369 .with_renames(crate::diff::RenameDetection::Off),
370 &config,
371 &policy,
372 )?;
373 assert_eq!(many[0].output.files, single.files);
374
375 let policy_serial = ThreadPolicy::with_threads_override(Some(1));
376 let policy_par = ThreadPolicy::with_threads_override(Some(4))
377 .min_items_override(Some(1))
378 .min_total_bytes_override(Some(0));
379 let tree3 = tree_with(&repo, &[("f.txt", b"c\n")]);
380 let mid = write_commit(&repo, tree3, vec![child], "mid");
381 let tree4 = tree_with(&repo, &[("f.txt", b"d\n")]);
382 let tip = write_commit(&repo, tree4, vec![mid], "tip");
383 let batch = [tip, mid, child];
384 let many_par =
385 repo.diff()
386 .commit_files_many(&batch, &[], opts.clone(), &config, &policy_par)?;
387 let many_serial =
388 repo.diff()
389 .commit_files_many(&batch, &[], opts, &config, &policy_serial)?;
390 assert_eq!(many_par.len(), 3);
391 assert_eq!(many_par, many_serial);
392 Ok(())
393 }
394}