debtmap/risk/context/git_history/
batched_function.rs1use super::blame_cache::{FileBlameCache, extract_authors_for_range};
8use super::function_level::{CommitInfo, FunctionHistory, calculate_function_history_with_authors};
9use super::git2_provider::{self, Git2Repository};
10use crate::time_span;
11use anyhow::Result;
12use dashmap::DashMap;
13use rayon::prelude::*;
14use std::collections::HashMap;
15use std::path::{Path, PathBuf};
16use std::sync::atomic::{AtomicUsize, Ordering};
17use std::time::Instant;
18
19#[derive(Debug, Clone)]
21pub struct FunctionPreloadTarget {
22 pub file: PathBuf,
23 pub name: String,
24 pub line_range: (usize, usize),
25}
26
27#[derive(Debug, Clone, PartialEq, Eq, Hash)]
29pub struct FunctionHistoryKey {
30 pub file: PathBuf,
31 pub name: String,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum GitPreloadPhase {
37 Commits,
39 BlameFiles,
41}
42
43pub type ProgressCallback<'a> = &'a (dyn Fn(GitPreloadPhase, usize, usize) + Send + Sync);
45
46pub struct BatchedFunctionGitHistory {
48 histories: DashMap<FunctionHistoryKey, FunctionHistory>,
49}
50
51pub struct FunctionPreloadResult {
53 pub functions: BatchedFunctionGitHistory,
54 pub file_history: super::batched::BatchedGitHistory,
55}
56
57impl BatchedFunctionGitHistory {
58 pub fn build(
59 repo: &Git2Repository,
60 blame_cache: &FileBlameCache,
61 targets: &[FunctionPreloadTarget],
62 progress_cb: Option<ProgressCallback<'_>>,
63 ) -> Result<FunctionPreloadResult> {
64 time_span!("git_function_history_preload");
65 let start = Instant::now();
66
67 let by_file = group_targets_by_file(targets);
68 let file_count = by_file.len();
69 let file_names: HashMap<PathBuf, Vec<String>> = by_file
70 .iter()
71 .map(|(file, fns)| (file.clone(), fns.iter().map(|(n, _)| n.clone()).collect()))
72 .collect();
73
74 let scan = git2_provider::compute_repo_function_histories(
75 repo.repo_path(),
76 &file_names,
77 progress_cb,
78 )?;
79 let records = scan.functions;
80 let file_history = super::batched::BatchedGitHistory::from_commit_scans(&scan.file_scans);
81
82 let histories: DashMap<FunctionHistoryKey, FunctionHistory> = DashMap::new();
83 let processed_files = AtomicUsize::new(0);
84 if let Some(cb) = progress_cb {
85 cb(GitPreloadPhase::BlameFiles, 0, file_count);
86 }
87
88 by_file.par_iter().for_each(|(file, functions)| {
89 let blame_data = blame_cache.get_or_fetch(file).ok();
90 for (name, line_range) in functions {
91 let Some(record) = records.get(&(file.clone(), name.clone())) else {
92 continue;
93 };
94 if record.introduction_oid.is_none() {
95 continue;
96 }
97 let (start_line, end_line) = *line_range;
98 let blame_authors = blame_data
99 .as_ref()
100 .map(|data| extract_authors_for_range(data, start_line, end_line))
101 .unwrap_or_default();
102 let modification_commits: Vec<CommitInfo> = record
103 .modifications
104 .iter()
105 .map(|s| CommitInfo {
106 hash: s.hash.to_string(),
107 date: Some(s.date),
108 message: s.message.clone(),
109 author: s.author_email.clone(),
110 })
111 .collect();
112 let history = calculate_function_history_with_authors(
113 record.introduction_oid.map(|o| o.to_string()),
114 record.introduction_date,
115 &modification_commits,
116 blame_authors,
117 );
118 histories.insert(
119 FunctionHistoryKey {
120 file: file.clone(),
121 name: name.clone(),
122 },
123 history,
124 );
125 }
126
127 let done = processed_files.fetch_add(1, Ordering::Relaxed) + 1;
128 if let Some(cb) = progress_cb
129 && (done % 10 == 0 || done == file_count)
130 {
131 cb(GitPreloadPhase::BlameFiles, done, file_count);
132 }
133 });
134
135 log::info!(
136 "Preloaded {} function histories from {} files in {:?}",
137 histories.len(),
138 file_count,
139 start.elapsed()
140 );
141
142 Ok(FunctionPreloadResult {
143 functions: Self { histories },
144 file_history,
145 })
146 }
147
148 pub fn get(&self, file: &Path, function_name: &str) -> Option<FunctionHistory> {
149 self.histories
150 .get(&FunctionHistoryKey {
151 file: file.to_path_buf(),
152 name: function_name.to_string(),
153 })
154 .map(|entry| entry.clone())
155 }
156
157 pub fn len(&self) -> usize {
158 self.histories.len()
159 }
160
161 pub fn is_empty(&self) -> bool {
162 self.histories.is_empty()
163 }
164}
165
166type FileTargetMap = HashMap<PathBuf, Vec<(String, (usize, usize))>>;
167
168fn group_targets_by_file(targets: &[FunctionPreloadTarget]) -> FileTargetMap {
169 let mut by_file: FileTargetMap = HashMap::new();
170 for target in targets {
171 if target.name.is_empty() {
172 continue;
173 }
174 let entries = by_file.entry(target.file.clone()).or_default();
175 if entries.iter().any(|(name, _)| name == &target.name) {
176 continue;
177 }
178 entries.push((target.name.clone(), target.line_range));
179 }
180 by_file
181}