use std::collections::HashSet;
use std::ops::ControlFlow;
use std::path::PathBuf;
use gix::diff::Rewrites;
use gix::revision::walk::Sorting;
use gix::traverse::commit::simple::CommitTimeOrder;
use super::identity::ParticipantResolver;
use super::repo::bstr_to_path;
use super::{diff_err, walk_err};
use crate::vcs::cache::CommitEvent;
use crate::vcs::classify::{self, Classification};
use crate::vcs::error::Error;
use crate::vcs::identity::{AuthorId, BotFilter};
use crate::vcs::options::Options;
type RenameEdges = Vec<(PathBuf, PathBuf)>;
type TouchedFiles = Vec<(PathBuf, u64)>;
pub(crate) fn collect_events(
repo: &gix::Repository,
tip: gix::ObjectId,
options: &Options,
now: i64,
stop_oids: &HashSet<gix::ObjectId>,
) -> Result<(Vec<CommitEvent>, Option<gix::ObjectId>), Error> {
let long_boundary = now.saturating_sub(options.long_window_secs);
let mailmap = repo.open_mailmap();
let bots = if options.exclude_bots {
Some(BotFilter::new(&options.bot_pattern)?)
} else {
None
};
let resolver = ParticipantResolver::new(&mailmap, bots.as_ref());
let rewrites = options.follow_renames.then(Rewrites::default);
let mut cache = repo.diff_resource_cache_for_tree_diff().map_err(diff_err)?;
let mut events = Vec::new();
let mut platform = repo.rev_walk([tip]);
if !options.full_history {
platform = platform.first_parent_only();
}
let walk = platform
.sorting(Sorting::ByCommitTimeCutoff {
order: CommitTimeOrder::NewestFirst,
seconds: long_boundary,
})
.all()
.map_err(walk_err)?;
for info in walk {
let info = info.map_err(walk_err)?;
if stop_oids.contains(&info.id) {
return Ok((events, Some(info.id)));
}
let commit = info.object().map_err(walk_err)?;
let raw_time = commit_seconds(&info, &commit)?;
if raw_time.min(now) < long_boundary {
continue;
}
if let Some(event) = process_commit(
repo, &commit, info.id, raw_time, options, &resolver, rewrites, &mut cache,
)? {
events.push(event);
}
cache.clear_resource_cache();
}
Ok((events, None))
}
#[allow(clippy::too_many_arguments)]
fn process_commit(
repo: &gix::Repository,
commit: &gix::Commit<'_>,
oid: gix::ObjectId,
raw_time: i64,
options: &Options,
resolver: &ParticipantResolver<'_>,
rewrites: Option<Rewrites>,
cache: &mut gix::diff::blob::Platform,
) -> Result<Option<CommitEvent>, Error> {
let mut parent_ids = commit.parent_ids().map(gix::Id::detach);
let first_parent = parent_ids.next();
let is_merge = parent_ids.next().is_some();
if is_merge && !options.include_merges {
return Ok(None);
}
let authors = resolver.participants(commit)?;
if authors.is_empty() {
return Ok(None);
}
let class = classify_commit(commit)?;
let commit_tree = commit.tree().map_err(walk_err)?;
let parent_tree = match first_parent {
Some(parent) => match repo.try_find_object(parent).map_err(walk_err)? {
Some(object) => object
.peel_to_commit()
.map_err(walk_err)?
.tree()
.map_err(walk_err)?,
None => repo.empty_tree(),
},
None => repo.empty_tree(),
};
let (renames, touched) = diff_collect(&parent_tree, &commit_tree, cache, rewrites)?;
Ok(Some(CommitEvent {
oid: oid.to_string(),
time: raw_time,
authors: authors.iter().map(AuthorId::hashed).collect(),
bug_fix: class.bug_fix,
security_fix: class.security_fix,
revert: class.revert,
renames,
touched,
}))
}
fn diff_collect(
parent_tree: &gix::Tree<'_>,
commit_tree: &gix::Tree<'_>,
cache: &mut gix::diff::blob::Platform,
rewrites: Option<Rewrites>,
) -> Result<(RenameEdges, TouchedFiles), Error> {
use gix::object::tree::diff::Change;
let mut renames: RenameEdges = Vec::new();
let mut touched: TouchedFiles = Vec::new();
parent_tree
.changes()
.map_err(diff_err)?
.options(|opts| {
opts.track_path();
opts.track_rewrites(rewrites);
})
.for_each_to_obtain_tree(commit_tree, |change| -> Result<ControlFlow<()>, Error> {
if !change.entry_mode().is_blob() {
return Ok(ControlFlow::Continue(()));
}
let location_path = bstr_to_path(change.location())?;
if let Change::Rewrite {
source_location, ..
} = &change
{
let source_path = bstr_to_path(source_location)?;
renames.push((source_path, location_path.clone()));
}
let Some(line_stats) = change
.diff(cache)
.map_err(diff_err)?
.line_counts()
.map_err(diff_err)?
else {
return Ok(ControlFlow::Continue(()));
};
let churn = u64::from(line_stats.insertions) + u64::from(line_stats.removals);
touched.push((location_path, churn));
Ok(ControlFlow::Continue(()))
})
.map_err(diff_err)?;
Ok((renames, touched))
}
pub(super) fn commit_seconds(
info: &gix::revision::walk::Info<'_>,
commit: &gix::Commit<'_>,
) -> Result<i64, Error> {
match info.commit_time {
Some(seconds) => Ok(seconds),
None => commit.time().map(|t| t.seconds).map_err(walk_err),
}
}
fn classify_commit(commit: &gix::Commit<'_>) -> Result<Classification, Error> {
let message = commit.message_raw().map_err(walk_err)?;
Ok(classify::classify(message))
}