use std::path::PathBuf;
use crate::{
ActivityOptions, Phase, Progress, RpoError,
backend::{GitBackend, WalkOptions},
filters::FilterSet,
frames::{CommitRecord, FileChangeRecord, extension_of},
identity::Canonicalizer,
};
pub struct WalkOutput {
pub commits: Vec<CommitRecord>,
pub file_changes: Vec<FileChangeRecord>,
}
const fn walk_options_for(activity: &ActivityOptions) -> WalkOptions {
WalkOptions {
first_parent_only: activity.first_parent_only,
include_merges: !activity.ignore_merges || activity.first_parent_only,
}
}
pub fn run<B: GitBackend>(
backend: &B,
canonicalizer: &Canonicalizer,
filters: &FilterSet,
activity: &ActivityOptions,
progress: Option<&(dyn Fn(Progress) + Send + Sync)>,
) -> Result<WalkOutput, RpoError> {
let opts = walk_options_for(activity);
if let Some(cb) = progress {
cb(Progress {
phase: Phase::WalkingCommits,
completed: 0,
total: 1,
});
}
tracing::info!("walk: start");
let started = std::time::Instant::now();
let mut commits_out: Vec<CommitRecord> = Vec::new();
let mut changes_out: Vec<FileChangeRecord> = Vec::new();
for commit_res in backend.iter_commits(opts) {
let commit = commit_res?;
let parent = commit.parent_ids.first();
let diff = backend.diff_tree(parent, &commit.id)?;
let (can_an, can_ae) = canonicalizer.canonicalize(&commit.author);
let (can_cn, can_ce) = canonicalizer.canonicalize(&commit.committer);
let mut insertions: u64 = 0;
let mut deletions: u64 = 0;
let mut files_changed: u32 = 0;
for change in &diff {
if !filters.changes_include(&change.path) {
continue;
}
insertions += change.insertions;
deletions += change.deletions;
files_changed += 1;
let classification = filters.classify(&change.path);
changes_out.push(FileChangeRecord {
sha: commit.id.to_hex(),
commit_time_ms: commit.committer.time_ms,
canonical_author_name: can_an.clone(),
canonical_author_email: can_ae.clone(),
canonical_committer_name: can_cn.clone(),
canonical_committer_email: can_ce.clone(),
path: change.path.to_string_lossy().to_string(),
old_path: change
.old_path
.as_ref()
.map(|p| p.to_string_lossy().to_string()),
change_kind: change.kind,
insertions: change.insertions,
deletions: change.deletions,
extension: extension_of(&PathBuf::from(&change.path)),
is_generated: classification.is_generated,
is_vendored: classification.is_vendored,
});
}
commits_out.push(CommitRecord {
sha: commit.id.to_hex(),
short_sha: commit.id.short_hex(),
author_name: commit.author.name.clone(),
author_email: commit.author.email.clone(),
canonical_author_name: can_an,
canonical_author_email: can_ae,
committer_name: commit.committer.name.clone(),
committer_email: commit.committer.email.clone(),
canonical_committer_name: can_cn,
canonical_committer_email: can_ce,
author_time_ms: commit.author.time_ms,
commit_time_ms: commit.committer.time_ms,
parent_count: commit.parent_ids.len() as u32,
is_merge: commit.parent_ids.len() >= 2,
message_subject: commit.message_subject,
files_changed,
insertions,
deletions,
});
}
if let Some(cb) = progress {
cb(Progress {
phase: Phase::WalkingCommits,
completed: 1,
total: 1,
});
}
tracing::info!(
commits = commits_out.len(),
file_changes = changes_out.len(),
elapsed_ms = started.elapsed().as_millis() as u64,
"walk: done"
);
Ok(WalkOutput {
commits: commits_out,
file_changes: changes_out,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ActivityOptions;
fn opts(ignore_merges: bool, first_parent_only: bool) -> ActivityOptions {
ActivityOptions {
ignore_merges,
first_parent_only,
ignore_whitespace: false,
ignore_bots: false,
}
}
#[test]
fn default_excludes_merges_and_walks_all_parents() {
let w = walk_options_for(&opts(true, false));
assert!(!w.first_parent_only);
assert!(!w.include_merges);
}
#[test]
fn include_merges_only() {
let w = walk_options_for(&opts(false, false));
assert!(!w.first_parent_only);
assert!(w.include_merges);
}
#[test]
fn first_parent_only_makes_ignore_merges_moot() {
let w = walk_options_for(&opts(true, true));
assert!(w.first_parent_only);
assert!(
w.include_merges,
"first_parent_only must visit merge commits regardless of ignore_merges"
);
}
#[test]
fn first_parent_only_with_include_merges_is_consistent() {
let w = walk_options_for(&opts(false, true));
assert!(w.first_parent_only);
assert!(w.include_merges);
}
}