use crate::git::FileStatus;
use crate::stats::ChangeCouplingAnalysis;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct SplitGroup {
pub files: Vec<String>,
pub reason: String,
pub suggested_title: String,
}
#[derive(Debug, Clone)]
pub struct SplitSuggestion {
pub groups: Vec<SplitGroup>,
}
const MIN_FILES_FOR_SPLIT: usize = 10;
pub fn suggest_splits(
statuses: &[FileStatus],
coupling: Option<&ChangeCouplingAnalysis>,
) -> Option<SplitSuggestion> {
let staged: Vec<&FileStatus> = statuses.iter().filter(|s| s.kind.is_staged()).collect();
if staged.len() < MIN_FILES_FOR_SPLIT {
return None;
}
let mut dir_groups: HashMap<String, Vec<String>> = HashMap::new();
for s in &staged {
let dir = s.path.split('/').next().unwrap_or("root").to_string();
dir_groups.entry(dir).or_default().push(s.path.clone());
}
if dir_groups.len() < 2 {
return None;
}
let mut groups: Vec<SplitGroup> = Vec::new();
let mut misc_files: Vec<String> = Vec::new();
for (dir, files) in &dir_groups {
if files.len() < 3 {
misc_files.extend(files.clone());
} else {
groups.push(SplitGroup {
files: files.clone(),
reason: format!("Files in {}/", dir),
suggested_title: format!("Update {} module", dir),
});
}
}
if !misc_files.is_empty() {
groups.push(SplitGroup {
files: misc_files,
reason: "Miscellaneous changes".to_string(),
suggested_title: "Minor updates".to_string(),
});
}
if let Some(_coupling) = coupling {
}
if groups.len() >= 2 {
Some(SplitSuggestion { groups })
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::git::FileStatusKind;
fn make_staged(path: &str) -> FileStatus {
FileStatus {
path: path.to_string(),
kind: FileStatusKind::StagedNew,
}
}
fn make_unstaged(path: &str) -> FileStatus {
FileStatus {
path: path.to_string(),
kind: FileStatusKind::Modified,
}
}
#[test]
fn test_suggest_splits_empty() {
assert!(suggest_splits(&[], None).is_none());
}
#[test]
fn test_suggest_splits_too_few_files() {
let statuses: Vec<FileStatus> = (0..5)
.map(|i| make_staged(&format!("src/file{}.rs", i)))
.collect();
assert!(suggest_splits(&statuses, None).is_none());
}
#[test]
fn test_suggest_splits_exactly_threshold() {
let statuses: Vec<FileStatus> = (0..10)
.map(|i| make_staged(&format!("src/file{}.rs", i)))
.collect();
assert!(suggest_splits(&statuses, None).is_none());
}
#[test]
fn test_suggest_splits_single_directory() {
let statuses: Vec<FileStatus> = (0..15)
.map(|i| make_staged(&format!("src/file{}.rs", i)))
.collect();
assert!(suggest_splits(&statuses, None).is_none());
}
#[test]
fn test_suggest_splits_multiple_directories() {
let mut statuses = Vec::new();
for i in 0..6 {
statuses.push(make_staged(&format!("src/file{}.rs", i)));
}
for i in 0..6 {
statuses.push(make_staged(&format!("tests/test{}.rs", i)));
}
let result = suggest_splits(&statuses, None);
assert!(result.is_some());
let suggestion = result.unwrap();
assert!(suggestion.groups.len() >= 2);
}
#[test]
fn test_suggest_splits_ignores_unstaged() {
let mut statuses = Vec::new();
for i in 0..6 {
statuses.push(make_staged(&format!("src/file{}.rs", i)));
}
for i in 0..6 {
statuses.push(make_unstaged(&format!("tests/test{}.rs", i)));
}
assert!(suggest_splits(&statuses, None).is_none());
}
#[test]
fn test_suggest_splits_small_groups_go_to_misc() {
let mut statuses = Vec::new();
for i in 0..5 {
statuses.push(make_staged(&format!("src/file{}.rs", i)));
}
for i in 0..4 {
statuses.push(make_staged(&format!("tests/test{}.rs", i)));
}
statuses.push(make_staged("docs/readme.md"));
statuses.push(make_staged("ci/pipeline.yml"));
let result = suggest_splits(&statuses, None);
assert!(result.is_some());
let suggestion = result.unwrap();
assert!(suggestion.groups.len() >= 3);
let misc = suggestion
.groups
.iter()
.find(|g| g.reason == "Miscellaneous changes");
assert!(misc.is_some());
assert_eq!(misc.unwrap().files.len(), 2);
}
#[test]
fn test_suggest_splits_three_directories() {
let mut statuses = Vec::new();
for i in 0..4 {
statuses.push(make_staged(&format!("src/file{}.rs", i)));
}
for i in 0..4 {
statuses.push(make_staged(&format!("tests/test{}.rs", i)));
}
for i in 0..4 {
statuses.push(make_staged(&format!("docs/doc{}.md", i)));
}
let result = suggest_splits(&statuses, None);
assert!(result.is_some());
assert_eq!(result.unwrap().groups.len(), 3);
}
#[test]
fn test_split_group_has_suggested_title() {
let mut statuses = Vec::new();
for i in 0..6 {
statuses.push(make_staged(&format!("src/file{}.rs", i)));
}
for i in 0..6 {
statuses.push(make_staged(&format!("tests/test{}.rs", i)));
}
let result = suggest_splits(&statuses, None).unwrap();
for group in &result.groups {
assert!(!group.suggested_title.is_empty());
assert!(!group.reason.is_empty());
}
}
}