use std::collections::HashMap;
use crate::config::ConfigSet;
use crate::error::Result;
use crate::objects::{parse_commit, ObjectId};
use crate::odb::Odb;
use crate::parallel::{try_par_map, ParallelHashError, ParallelOp, ParallelOptions, ThreadPolicy};
use super::core::diff_trees_with_renames_and_policy;
use super::detail::{DiffDetail, DiffTotals};
use super::files::{build_file_diffs, DiffFilesOutput};
use super::options::DiffOptions;
use super::Diff;
use super::DiffEntry;
#[derive(Debug, Clone)]
pub struct CommitFilesManyOptions {
pub diff_options: DiffOptions,
pub detail: DiffDetail,
pub skip_merge_patches: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CommitFilesResult {
pub output: DiffFilesOutput,
pub parent_count: usize,
}
struct CommitWork {
tree: ObjectId,
parent_tree: Option<ObjectId>,
parent_count: usize,
}
fn rename_threshold(options: &DiffOptions) -> u32 {
options.renames.threshold_percent().unwrap_or(101)
}
fn filter_entries(
repo: &crate::repo::Repository,
entries: Vec<DiffEntry>,
pathspecs: &[String],
) -> Vec<DiffEntry> {
if pathspecs.is_empty() {
return entries;
}
entries
.into_iter()
.filter(|e| entry_matches_pathspecs(repo, e, pathspecs))
.collect()
}
fn entry_matches_pathspecs(
repo: &crate::repo::Repository,
entry: &DiffEntry,
pathspecs: &[String],
) -> bool {
let path = entry
.new_path
.as_deref()
.or(entry.old_path.as_deref())
.unwrap_or("");
if repo.paths_match_pathspecs(path, pathspecs) {
return true;
}
if let Some(old) = entry.old_path.as_deref() {
if old != path && repo.paths_match_pathspecs(old, pathspecs) {
return true;
}
}
false
}
fn parent_tree_for_commit(
odb: &Odb,
parent: &ObjectId,
trees_by_commit: &HashMap<ObjectId, ObjectId>,
) -> Result<ObjectId> {
if let Some(tree) = trees_by_commit.get(parent) {
return Ok(*tree);
}
let raw = odb.read(parent)?;
Ok(parse_commit(&raw.data)?.tree)
}
fn totals_for_detail(files: &[super::detail::FileDiff], detail: DiffDetail) -> Option<DiffTotals> {
match detail {
DiffDetail::Stat | DiffDetail::Patch => Some(DiffTotals::from_files(files)),
DiffDetail::NameOnly | DiffDetail::NameStatus => Some(DiffTotals {
files_changed: files.len(),
insertions: 0,
deletions: 0,
}),
}
}
fn commit_files_many_parallel_options(
thread_policy: &ThreadPolicy,
config: &ConfigSet,
) -> ParallelOptions {
let batch_policy = if thread_policy.threads_override == Some(1) {
*thread_policy
} else {
ThreadPolicy {
threads_override: thread_policy.threads_override.or(Some(0)),
min_items_override: thread_policy.min_items_override,
min_total_bytes_override: thread_policy.min_total_bytes_override,
}
};
let mut par_opts = batch_policy.resolve(ParallelOp::DiffContent, config);
par_opts.min_items = par_opts.min_items.min(2);
par_opts.min_total_bytes = None;
par_opts
}
fn diff_one_commit(
diff: &Diff,
work: &CommitWork,
pathspecs: &[String],
opts: &CommitFilesManyOptions,
config: &ConfigSet,
thread_policy: &ThreadPolicy,
) -> Result<CommitFilesResult> {
if opts.skip_merge_patches && opts.detail == DiffDetail::Patch && work.parent_count > 1 {
return Ok(CommitFilesResult {
output: DiffFilesOutput {
files: Vec::new(),
totals: Some(DiffTotals {
files_changed: 0,
insertions: 0,
deletions: 0,
}),
},
parent_count: work.parent_count,
});
}
let odb = diff.repository().odb();
let mut entries = diff_trees_with_renames_and_policy(
odb,
work.parent_tree.as_ref(),
Some(&work.tree),
"",
rename_threshold(&opts.diff_options),
config,
thread_policy,
)?;
if rename_threshold(&opts.diff_options) >= 100 {
entries.sort_by(|a, b| a.path().cmp(b.path()));
}
entries = filter_entries(diff.repository(), entries, pathspecs);
let mut output = build_file_diffs(
odb,
entries,
opts.detail,
opts.diff_options.clone(),
config,
thread_policy,
)?;
if output.totals.is_none() {
output.totals = totals_for_detail(&output.files, opts.detail);
}
Ok(CommitFilesResult {
output,
parent_count: work.parent_count,
})
}
#[derive(Debug, Clone)]
pub struct CommitFilesManyInput {
pub oid: ObjectId,
pub parents: Vec<ObjectId>,
pub tree: ObjectId,
}
impl Diff<'_> {
pub fn commit_files_many(
&self,
oids: &[ObjectId],
pathspecs: &[String],
opts: CommitFilesManyOptions,
config: &ConfigSet,
thread_policy: &ThreadPolicy,
) -> Result<Vec<CommitFilesResult>> {
if oids.is_empty() {
return Ok(Vec::new());
}
let odb = self.repository().odb();
let mut inputs = Vec::with_capacity(oids.len());
for oid in oids {
let raw = odb.read(oid)?;
let commit = parse_commit(&raw.data)?;
inputs.push(CommitFilesManyInput {
oid: *oid,
parents: commit.parents.clone(),
tree: commit.tree,
});
}
self.commit_files_many_work(&inputs, pathspecs, opts, config, thread_policy)
}
pub fn commit_files_many_work(
&self,
inputs: &[CommitFilesManyInput],
pathspecs: &[String],
opts: CommitFilesManyOptions,
config: &ConfigSet,
thread_policy: &ThreadPolicy,
) -> Result<Vec<CommitFilesResult>> {
if inputs.is_empty() {
return Ok(Vec::new());
}
let repo = self.repository();
let odb = repo.odb();
let mut trees_by_commit: HashMap<ObjectId, ObjectId> = HashMap::with_capacity(inputs.len());
for input in inputs {
trees_by_commit.insert(input.oid, input.tree);
}
let mut work_items: Vec<CommitWork> = Vec::with_capacity(inputs.len());
for (i, input) in inputs.iter().enumerate() {
let parent_count = input.parents.len();
let parent_tree = if parent_count == 0 {
None
} else if i + 1 < inputs.len() && input.parents[0] == inputs[i + 1].oid {
Some(inputs[i + 1].tree)
} else {
Some(parent_tree_for_commit(
odb,
&input.parents[0],
&trees_by_commit,
)?)
};
work_items.push(CommitWork {
tree: input.tree,
parent_tree,
parent_count,
});
}
odb.with_pack_read_context(|| {
let par_opts = commit_files_many_parallel_options(thread_policy, config);
try_par_map(&work_items, par_opts, 0, |work| {
diff_one_commit(self, work, pathspecs, &opts, config, thread_policy)
})
.map_err(|err| match err {
ParallelHashError::Task(e) => e,
})
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::objects::{serialize_commit, CommitData, ObjectKind, TreeEntry};
use crate::repo::{InitOptions, Repository};
fn tree_with(repo: &Repository, entries: &[(&str, &[u8])]) -> ObjectId {
let mut tes = Vec::new();
for (name, data) in entries {
let oid = repo.odb().write(ObjectKind::Blob, data).expect("blob");
tes.push(TreeEntry {
mode: 0o100644,
name: name.as_bytes().to_vec(),
oid,
});
}
let data = crate::objects::serialize_tree(&tes);
repo.odb().write(ObjectKind::Tree, &data).expect("tree")
}
fn write_commit(
repo: &Repository,
tree: ObjectId,
parents: Vec<ObjectId>,
msg: &str,
) -> ObjectId {
let ident = "A <a@example.com> 0 +0000";
let commit = CommitData {
tree,
parents,
author: ident.into(),
committer: ident.into(),
author_raw: Vec::new(),
committer_raw: Vec::new(),
encoding: None,
message: format!("{msg}\n"),
raw_message: None,
extra_headers: Vec::new(),
};
repo.odb()
.write(ObjectKind::Commit, &serialize_commit(&commit))
.expect("commit")
}
#[test]
fn commit_files_many_matches_single_commit_calls() -> Result<()> {
let dir = tempfile::tempdir().expect("tempdir");
let repo = Repository::init(dir.path(), &InitOptions::default())?;
let tree1 = tree_with(&repo, &[("f.txt", b"a\n")]);
let root = write_commit(&repo, tree1, vec![], "root");
let tree2 = tree_with(&repo, &[("f.txt", b"b\n")]);
let child = write_commit(&repo, tree2, vec![root], "child");
let config = ConfigSet::default();
let policy = ThreadPolicy::default();
let diff_options = DiffOptions::default();
let opts = CommitFilesManyOptions {
diff_options,
detail: DiffDetail::Stat,
skip_merge_patches: true,
};
let many = repo
.diff()
.commit_files_many(&[child], &[], opts.clone(), &config, &policy)?;
assert_eq!(many.len(), 1);
let entries = diff_trees_with_renames_and_policy(
repo.odb(),
Some(&tree1),
Some(&tree2),
"",
101,
&config,
&policy,
)?;
let single = build_file_diffs(
repo.odb(),
entries,
opts.detail,
opts.diff_options.clone(),
&config,
&policy,
)?;
assert_eq!(many[0].output.files, single.files);
let policy_serial = ThreadPolicy::with_threads_override(Some(1));
let policy_par = ThreadPolicy::with_threads_override(Some(4))
.min_items_override(Some(1))
.min_total_bytes_override(Some(0));
let tree3 = tree_with(&repo, &[("f.txt", b"c\n")]);
let mid = write_commit(&repo, tree3, vec![child], "mid");
let tree4 = tree_with(&repo, &[("f.txt", b"d\n")]);
let tip = write_commit(&repo, tree4, vec![mid], "tip");
let batch = [tip, mid, child];
let many_par =
repo.diff()
.commit_files_many(&batch, &[], opts.clone(), &config, &policy_par)?;
let many_serial =
repo.diff()
.commit_files_many(&batch, &[], opts, &config, &policy_serial)?;
assert_eq!(many_par.len(), 3);
assert_eq!(many_par, many_serial);
Ok(())
}
}