use crate::cli::output::Output;
use crate::core::manifest::Manifest;
use crate::core::repo::RepoInfo;
use std::collections::BTreeMap;
use std::path::PathBuf;
pub fn run_group_list(workspace_root: &PathBuf, manifest: &Manifest) -> anyhow::Result<()> {
Output::header("Repository Groups");
println!();
let repos: Vec<RepoInfo> = manifest
.repos
.iter()
.filter_map(|(name, config)| RepoInfo::from_config(name, config, workspace_root))
.collect();
let mut groups: BTreeMap<String, Vec<String>> = BTreeMap::new();
let mut ungrouped: Vec<String> = Vec::new();
for repo in &repos {
if repo.groups.is_empty() {
ungrouped.push(repo.name.clone());
} else {
for group in &repo.groups {
groups
.entry(group.clone())
.or_default()
.push(repo.name.clone());
}
}
}
if groups.is_empty() && ungrouped.is_empty() {
Output::info("No repositories found.");
return Ok(());
}
if groups.is_empty() {
Output::info("No groups defined. Add 'groups' to repos in the manifest.");
println!();
}
for (group_name, mut members) in groups {
members.sort();
println!(" {} ({})", Output::repo_name(&group_name), members.len());
for member in &members {
println!(" {}", member);
}
println!();
}
if !ungrouped.is_empty() {
ungrouped.sort();
println!(" ungrouped ({})", ungrouped.len());
for member in &ungrouped {
println!(" {}", member);
}
println!();
}
Ok(())
}
pub fn run_group_add(
workspace_root: &PathBuf,
group: &str,
repos: &[String],
) -> anyhow::Result<()> {
let manifest_path = find_manifest_path(workspace_root)?;
let content = std::fs::read_to_string(&manifest_path)?;
let mut manifest: serde_yaml::Value = serde_yaml::from_str(&content)?;
let repos_section = manifest
.get_mut("repos")
.ok_or_else(|| anyhow::anyhow!("No 'repos' section found in manifest"))?
.as_mapping_mut()
.ok_or_else(|| anyhow::anyhow!("'repos' is not a mapping"))?;
let mut added_count = 0;
let mut already_count = 0;
for repo_name in repos {
if let Some(repo) = repos_section.get_mut(serde_yaml::Value::String(repo_name.clone())) {
let repo_map = repo
.as_mapping_mut()
.ok_or_else(|| anyhow::anyhow!("Repository '{}' is not a mapping", repo_name))?;
let groups_key = serde_yaml::Value::String("groups".to_string());
let groups = repo_map
.entry(groups_key.clone())
.or_insert_with(|| serde_yaml::Value::Sequence(vec![]));
let groups_seq = groups
.as_sequence_mut()
.ok_or_else(|| anyhow::anyhow!("'groups' is not an array in '{}'", repo_name))?;
let group_value = serde_yaml::Value::String(group.to_string());
if groups_seq.contains(&group_value) {
Output::info(&format!("{}: already in group '{}'", repo_name, group));
already_count += 1;
} else {
groups_seq.push(group_value);
Output::success(&format!("{}: added to group '{}'", repo_name, group));
added_count += 1;
}
} else {
Output::error(&format!("Repository '{}' not found in manifest", repo_name));
}
}
let yaml = serde_yaml::to_string(&manifest)?;
std::fs::write(&manifest_path, yaml)?;
println!();
if added_count > 0 {
Output::success(&format!(
"Added {} repo(s) to group '{}' (updated: {})",
added_count,
group,
manifest_path.display()
));
}
if already_count > 0 && added_count == 0 {
Output::info(&format!("All repos already in group '{}'", group));
}
Ok(())
}
pub fn run_group_remove(
workspace_root: &PathBuf,
group: &str,
repos: &[String],
) -> anyhow::Result<()> {
let manifest_path = find_manifest_path(workspace_root)?;
let content = std::fs::read_to_string(&manifest_path)?;
let mut manifest: serde_yaml::Value = serde_yaml::from_str(&content)?;
let repos_section = manifest
.get_mut("repos")
.ok_or_else(|| anyhow::anyhow!("No 'repos' section found in manifest"))?
.as_mapping_mut()
.ok_or_else(|| anyhow::anyhow!("'repos' is not a mapping"))?;
let mut removed_count = 0;
let mut not_in_count = 0;
for repo_name in repos {
if let Some(repo) = repos_section.get_mut(serde_yaml::Value::String(repo_name.clone())) {
let repo_map = repo
.as_mapping_mut()
.ok_or_else(|| anyhow::anyhow!("Repository '{}' is not a mapping", repo_name))?;
let groups_key = serde_yaml::Value::String("groups".to_string());
if let Some(groups) = repo_map.get_mut(&groups_key) {
if let Some(groups_seq) = groups.as_sequence_mut() {
let group_value = serde_yaml::Value::String(group.to_string());
let original_len = groups_seq.len();
groups_seq.retain(|v| v != &group_value);
if groups_seq.len() < original_len {
Output::success(&format!("{}: removed from group '{}'", repo_name, group));
removed_count += 1;
if groups_seq.is_empty() {
repo_map.remove(&groups_key);
}
} else {
Output::info(&format!("{}: not in group '{}'", repo_name, group));
not_in_count += 1;
}
} else {
Output::info(&format!("{}: not in group '{}'", repo_name, group));
not_in_count += 1;
}
} else {
Output::info(&format!("{}: has no groups", repo_name));
not_in_count += 1;
}
} else {
Output::error(&format!("Repository '{}' not found in manifest", repo_name));
}
}
let yaml = serde_yaml::to_string(&manifest)?;
std::fs::write(&manifest_path, yaml)?;
println!();
if removed_count > 0 {
Output::success(&format!(
"Removed {} repo(s) from group '{}' (updated: {})",
removed_count,
group,
manifest_path.display()
));
}
if not_in_count > 0 && removed_count == 0 {
Output::info(&format!("No repos were in group '{}'", group));
}
Ok(())
}
pub fn run_group_create(_workspace_root: &PathBuf, name: &str) -> anyhow::Result<()> {
Output::info("Groups are created implicitly when you add repos to them.");
println!();
Output::info(&format!(
"To create group '{}', run: gr group add {} <repo-name>",
name, name
));
Ok(())
}
fn find_manifest_path(workspace_root: &PathBuf) -> anyhow::Result<PathBuf> {
let gitgrip_path = workspace_root
.join(".gitgrip")
.join("manifests")
.join("manifest.yaml");
if gitgrip_path.exists() {
return Ok(gitgrip_path);
}
let repo_path = workspace_root
.join(".repo")
.join("manifests")
.join("manifest.yaml");
if repo_path.exists() {
return Ok(repo_path);
}
anyhow::bail!(
"No manifest.yaml found. Expected at:\n {}\n {}",
gitgrip_path.display(),
repo_path.display()
)
}