use super::SubrepoInstance;
use anyhow::Result;
use std::collections::{HashMap, HashSet};
#[derive(Debug, PartialEq)]
enum UncommittedState {
AllClean,
AllDirty,
Mixed,
}
#[derive(Debug)]
pub struct SubrepoStatus {
pub name: String,
pub remote_url: String,
pub instances: Vec<SubrepoInstance>,
pub sync_score: f32,
pub unique_commits: usize,
pub has_drift: bool,
}
impl SubrepoStatus {
fn calculate_sync_score(instances: &[SubrepoInstance]) -> (f32, usize) {
let unique_commits = instances
.iter()
.map(|i| &i.commit_hash)
.collect::<HashSet<_>>()
.len();
if instances.len() <= 1 {
return (100.0, unique_commits);
}
let score = ((instances.len() - unique_commits) as f32)
/ ((instances.len() - 1) as f32)
* 100.0;
(score, unique_commits)
}
pub fn new(name: String, remote_url: String, instances: Vec<SubrepoInstance>) -> Self {
let (sync_score, unique_commits) = Self::calculate_sync_score(&instances);
let has_drift = sync_score < 100.0;
SubrepoStatus {
name,
remote_url,
instances,
sync_score,
unique_commits,
has_drift,
}
}
}
pub fn analyze_subrepos() -> Result<Vec<SubrepoStatus>> {
let report = super::validation::validate_subrepos()?;
let mut statuses = Vec::new();
for (remote_url, instances) in report.by_remote {
if instances.len() <= 1 {
continue;
}
let name = instances[0].subrepo_name.clone();
statuses.push(SubrepoStatus::new(name, remote_url, instances));
}
statuses.sort_by(|a, b| a.sync_score.partial_cmp(&b.sync_score).unwrap());
Ok(statuses)
}
pub fn display_drift_summary(statuses: &[SubrepoStatus]) {
let drifted: Vec<_> = statuses.iter().filter(|s| s.has_drift).collect();
if drifted.is_empty() {
return; }
let synced: Vec<_> = statuses.iter().filter(|s| !s.has_drift).collect();
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
println!("🔴 SUBREPO DRIFT ({})", drifted.len());
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
for status in &drifted {
display_drift_summary_item(status);
}
if !synced.is_empty() {
println!("💡 {} subrepos are fully synced.", synced.len());
}
println!("💡 Run 'repos subrepo status' for a detailed analysis.");
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
}
fn display_drift_summary_item(status: &SubrepoStatus) {
println!("{}: {} instances at different commits", status.name, status.instances.len());
let latest_clean = status.instances.iter()
.filter(|i| !i.has_uncommitted)
.max_by_key(|i| i.commit_timestamp);
let latest = status.instances.iter()
.max_by_key(|i| i.commit_timestamp)
.unwrap();
let mut by_commit: std::collections::HashMap<String, Vec<&SubrepoInstance>> = std::collections::HashMap::new();
for instance in &status.instances {
by_commit
.entry(instance.commit_hash.clone())
.or_default()
.push(instance);
}
let mut commits: Vec<_> = by_commit.into_iter().collect();
commits.sort_by(|a, b| {
let a_timestamp = a.1.iter().map(|i| i.commit_timestamp).max().unwrap_or(0);
let b_timestamp = b.1.iter().map(|i| i.commit_timestamp).max().unwrap_or(0);
b_timestamp.cmp(&a_timestamp)
});
for (_commit, instances) in &commits {
for instance in instances {
let is_sync_target = latest_clean.is_some_and(|t| t.commit_hash == instance.commit_hash);
let is_latest = latest.commit_hash == instance.commit_hash;
let prefix = if is_sync_target { "→" } else { " " };
let status_indicator = if instance.has_uncommitted {
"⚠️ uncommitted"
} else {
"✅ clean"
};
let mut suffix = String::new();
if is_latest && !instance.has_uncommitted {
suffix.push_str(" ⬆️ LATEST");
} else if !is_latest {
suffix.push_str(" (outdated)");
}
println!(" {} {} {:30} {}{}",
prefix,
instance.short_hash,
instance.parent_repo,
status_indicator,
suffix
);
}
}
let target_commit = latest_clean.map(|t| &t.short_hash).unwrap_or(&latest.short_hash);
println!(" Sync: repos subrepo sync {} --to {}", status.name, target_commit);
println!();
}
pub fn display_status(statuses: &[SubrepoStatus], show_all: bool) {
if statuses.is_empty() {
println!("\n✅ No shared subrepos found.");
println!(" Run 'repos validate' to see all nested repositories.\n");
return;
}
let drifted: Vec<_> = statuses.iter().filter(|s| s.has_drift).collect();
let synced: Vec<_> = statuses.iter().filter(|s| !s.has_drift).collect();
println!("\n🔍 Analyzing {} shared subrepos...\n", statuses.len());
if !drifted.is_empty() {
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
println!("🔴 SUBREPO DRIFT ({})", drifted.len());
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
for status in &drifted {
display_drift_status(status);
}
}
if show_all && !synced.is_empty() {
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
println!("🟢 SYNCED SUBREPOS ({})", synced.len());
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
for status in synced {
display_synced_status(status);
}
} else if !synced.is_empty() {
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
println!("💡 {} subrepos fully synced (100%)", synced.len());
println!(" Use --all to see them");
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
}
if !drifted.is_empty() {
println!();
println!("🔧 To update a drifted repo to its 'origin/main' branch instead, run:");
println!(" repos subrepo update <name> (e.g., 'repos subrepo update docs-engine')");
}
println!();
}
fn analyze_uncommitted_state(instances: &[SubrepoInstance]) -> UncommittedState {
let uncommitted_count = instances.iter()
.filter(|i| i.has_uncommitted)
.count();
if uncommitted_count == 0 {
UncommittedState::AllClean
} else if uncommitted_count == instances.len() {
UncommittedState::AllDirty
} else {
UncommittedState::Mixed
}
}
fn display_drift_status(status: &SubrepoStatus) {
println!("{}", status.name);
println!(" Remote: {}", status.remote_url);
println!(
" Sync Score: {}% ({} commits across {} repos)",
status.sync_score as u32,
status.unique_commits,
status.instances.len()
);
println!();
let latest_timestamp = status.instances.iter()
.map(|i| i.commit_timestamp)
.max()
.unwrap_or(0);
let latest_clean_timestamp = status.instances.iter()
.filter(|i| !i.has_uncommitted)
.map(|i| i.commit_timestamp)
.max();
let mut by_commit: HashMap<String, Vec<&SubrepoInstance>> = HashMap::new();
for instance in &status.instances {
by_commit
.entry(instance.commit_hash.clone())
.or_default()
.push(instance);
}
let mut commits: Vec<_> = by_commit.into_iter().collect();
commits.sort_by(|a, b| {
let count_cmp = b.1.len().cmp(&a.1.len());
if count_cmp != std::cmp::Ordering::Equal {
return count_cmp;
}
let a_timestamp = a.1.iter().map(|i| i.commit_timestamp).max().unwrap_or(0);
let b_timestamp = b.1.iter().map(|i| i.commit_timestamp).max().unwrap_or(0);
b_timestamp.cmp(&a_timestamp)
});
for (_commit, instances) in &commits {
for instance in instances {
let is_sync_target = latest_clean_timestamp.is_some_and(|t|
instance.commit_timestamp == t && !instance.has_uncommitted
);
let is_latest = instance.commit_timestamp == latest_timestamp;
let prefix = if is_sync_target { "→" } else { " " };
let status_indicator = if instance.has_uncommitted {
"⚠️ uncommitted"
} else {
"✅ clean"
};
let mut suffix = String::new();
if is_latest && !instance.has_uncommitted {
suffix.push_str(" ⬆️ LATEST");
} else if !is_latest {
suffix.push_str(" (outdated)");
}
println!(" {} {} {:width$} {}{}",
prefix,
instance.short_hash,
instance.parent_repo,
status_indicator,
suffix,
width = 30
);
}
}
println!();
let uncommitted_state = analyze_uncommitted_state(&status.instances);
match uncommitted_state {
UncommittedState::AllDirty => {
println!(" ⚠️ All instances have uncommitted changes.");
println!();
let target_instance = status.instances.iter()
.max_by_key(|i| i.commit_timestamp)
.unwrap();
let target_commit = &target_instance.short_hash;
let dirty_repos: Vec<&str> = status.instances.iter()
.map(|i| i.parent_repo.as_str())
.collect();
let repos_desc = if dirty_repos.len() == 1 {
format!("'{}'", dirty_repos[0])
} else {
format!("{} repos", dirty_repos.len())
};
println!(" 💡 EASY FIX (Recommended):");
println!(" repos subrepo sync {} --to {} --stash", status.name, target_commit);
println!(" (Stashes uncommitted changes in {})", repos_desc);
println!();
println!(" 🔥 FORCE FIX (Discards all local changes):");
println!(" repos subrepo sync {} --to {} --force", status.name, target_commit);
}
UncommittedState::Mixed => {
let target_instance = status.instances.iter()
.filter(|i| !i.has_uncommitted)
.max_by_key(|i| i.commit_timestamp)
.unwrap();
let target_commit = &target_instance.short_hash;
let target_repo = &target_instance.parent_repo;
let dirty_repos: Vec<&str> = status.instances.iter()
.filter(|i| i.has_uncommitted)
.map(|i| i.parent_repo.as_str())
.collect();
let dirty_list = if dirty_repos.len() == 1 {
format!("'{}'", dirty_repos[0])
} else {
dirty_repos.iter()
.map(|r| format!("'{}'", r))
.collect::<Vec<_>>()
.join(", ")
};
println!(" 💡 EASY FIX (Recommended):");
println!(" repos subrepo sync {} --to {} --stash", status.name, target_commit);
println!(" (Syncs {} to the clean commit from '{}')", dirty_list, target_repo);
println!();
println!(" 🔥 FORCE FIX (Discards changes in {}):", dirty_list);
println!(" repos subrepo sync {} --to {} --force", status.name, target_commit);
}
UncommittedState::AllClean => {
let latest_commit = status.instances.iter()
.max_by_key(|i| i.commit_timestamp)
.unwrap();
println!(" 🔧 SYNC to latest commit:");
println!(" repos subrepo sync {} --to {}", status.name, latest_commit.short_hash);
}
}
println!();
}
fn display_synced_status(status: &SubrepoStatus) {
println!("{}", status.name);
println!(" Remote: {}", status.remote_url);
println!(" Sync Score: 100% (all at same commit)");
println!();
println!(" {} (all instances)", status.instances[0].short_hash);
println!();
let has_any_uncommitted = status.instances.iter().any(|i| i.has_uncommitted);
if has_any_uncommitted {
for instance in &status.instances {
let status_indicator = if instance.has_uncommitted {
"⚠️ uncommitted"
} else {
"✅ clean"
};
println!(" {:width$} {}",
instance.parent_repo,
status_indicator,
width = 30
);
}
println!();
println!(" ✅ Already synchronized");
println!(" ⚠️ But some have uncommitted changes");
} else {
for instance in &status.instances {
println!(" • {}", instance.parent_repo);
}
println!();
println!(" ✅ Already synchronized and clean");
}
println!();
}