1use std::path::{Path, PathBuf};
4use std::process::{Command, Output};
5
6pub use fallow_types::churn::ChurnTrend;
7use rustc_hash::FxHashMap;
8
9pub type ChurnSpawnHook = fn(&mut Command) -> std::io::Result<Output>;
11
12#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct SinceDuration {
15 pub git_after: String,
17 pub display: String,
19}
20
21impl From<fallow_core::churn::SinceDuration> for SinceDuration {
22 fn from(duration: fallow_core::churn::SinceDuration) -> Self {
23 Self {
24 git_after: duration.git_after,
25 display: duration.display,
26 }
27 }
28}
29
30impl From<&SinceDuration> for fallow_core::churn::SinceDuration {
31 fn from(duration: &SinceDuration) -> Self {
32 Self {
33 git_after: duration.git_after.clone(),
34 display: duration.display.clone(),
35 }
36 }
37}
38
39#[derive(Debug, Clone, Copy, PartialEq)]
41pub struct AuthorContribution {
42 pub commits: u32,
44 pub weighted_commits: f64,
46 pub first_commit_ts: u64,
48 pub last_commit_ts: u64,
50}
51
52impl From<fallow_core::churn::AuthorContribution> for AuthorContribution {
53 fn from(author: fallow_core::churn::AuthorContribution) -> Self {
54 Self {
55 commits: author.commits,
56 weighted_commits: author.weighted_commits,
57 first_commit_ts: author.first_commit_ts,
58 last_commit_ts: author.last_commit_ts,
59 }
60 }
61}
62
63#[derive(Debug, Clone)]
65pub struct FileChurn {
66 pub path: PathBuf,
68 pub commits: u32,
70 pub weighted_commits: f64,
72 pub lines_added: u32,
74 pub lines_deleted: u32,
76 pub trend: ChurnTrend,
78 pub authors: FxHashMap<u32, AuthorContribution>,
80}
81
82impl From<fallow_core::churn::FileChurn> for FileChurn {
83 fn from(file: fallow_core::churn::FileChurn) -> Self {
84 Self {
85 path: file.path,
86 commits: file.commits,
87 weighted_commits: file.weighted_commits,
88 lines_added: file.lines_added,
89 lines_deleted: file.lines_deleted,
90 trend: file.trend,
91 authors: file
92 .authors
93 .into_iter()
94 .map(|(index, author)| (index, AuthorContribution::from(author)))
95 .collect(),
96 }
97 }
98}
99
100#[derive(Debug, Clone)]
102pub struct ChurnResult {
103 pub files: FxHashMap<PathBuf, FileChurn>,
105 pub shallow_clone: bool,
107 pub author_pool: Vec<String>,
109}
110
111impl From<fallow_core::churn::ChurnResult> for ChurnResult {
112 fn from(result: fallow_core::churn::ChurnResult) -> Self {
113 Self {
114 files: result
115 .files
116 .into_iter()
117 .map(|(path, file)| (path, FileChurn::from(file)))
118 .collect(),
119 shallow_clone: result.shallow_clone,
120 author_pool: result.author_pool,
121 }
122 }
123}
124
125pub fn set_spawn_hook(hook: ChurnSpawnHook) {
127 fallow_core::churn::set_spawn_hook(hook);
128}
129
130pub fn parse_since(input: &str) -> Result<SinceDuration, String> {
136 fallow_core::churn::parse_since(input).map(SinceDuration::from)
137}
138
139#[must_use]
141pub fn analyze_churn(root: &Path, since: &SinceDuration) -> Option<ChurnResult> {
142 let since = fallow_core::churn::SinceDuration::from(since);
143 fallow_core::churn::analyze_churn(root, &since).map(ChurnResult::from)
144}
145
146pub fn analyze_churn_from_file(path: &Path, root: &Path) -> Result<ChurnResult, String> {
152 fallow_core::churn::analyze_churn_from_file(path, root).map(ChurnResult::from)
153}
154
155#[must_use]
157pub fn is_git_repo(root: &Path) -> bool {
158 fallow_core::churn::is_git_repo(root)
159}
160
161#[must_use]
163pub fn analyze_churn_cached(
164 root: &Path,
165 since: &SinceDuration,
166 cache_dir: &Path,
167 no_cache: bool,
168) -> Option<(ChurnResult, bool)> {
169 let since = fallow_core::churn::SinceDuration::from(since);
170 fallow_core::churn::analyze_churn_cached(root, &since, cache_dir, no_cache)
171 .map(|(result, cache_hit)| (ChurnResult::from(result), cache_hit))
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 #[test]
179 fn parse_since_returns_engine_owned_duration() {
180 let duration = parse_since("6m").expect("duration should parse");
181 assert_eq!(duration.git_after, "6 months ago");
182 assert_eq!(duration.display, "6 months");
183 }
184
185 #[test]
186 fn churn_result_converts_from_core_without_leaking_type() {
187 let mut authors = FxHashMap::default();
188 authors.insert(
189 0,
190 fallow_core::churn::AuthorContribution {
191 commits: 2,
192 weighted_commits: 1.5,
193 first_commit_ts: 10,
194 last_commit_ts: 20,
195 },
196 );
197 let mut files = FxHashMap::default();
198 files.insert(
199 PathBuf::from("/repo/src/a.ts"),
200 fallow_core::churn::FileChurn {
201 path: PathBuf::from("/repo/src/a.ts"),
202 commits: 2,
203 weighted_commits: 1.5,
204 lines_added: 10,
205 lines_deleted: 4,
206 trend: ChurnTrend::Stable,
207 authors,
208 },
209 );
210 let result = ChurnResult::from(fallow_core::churn::ChurnResult {
211 files,
212 shallow_clone: true,
213 author_pool: vec!["dev@example.com".to_string()],
214 });
215
216 let file = result
217 .files
218 .get(&PathBuf::from("/repo/src/a.ts"))
219 .expect("converted file churn");
220 assert!(result.shallow_clone);
221 assert_eq!(result.author_pool, ["dev@example.com"]);
222 assert_eq!(file.commits, 2);
223 assert_eq!(file.authors[&0].last_commit_ts, 20);
224 }
225}