use super::{SubrepoInstance, ValidationReport};
use anyhow::{Context, Result};
use std::path::Path;
use std::process::Command;
fn find_instances_by_name(report: &ValidationReport, name: &str) -> Result<Vec<SubrepoInstance>> {
let mut instances = Vec::new();
for instance_list in report.by_remote.values() {
if let Some(first) = instance_list.first() {
if first.subrepo_name == name {
instances.extend(instance_list.clone());
}
}
}
if instances.is_empty() {
anyhow::bail!("Subrepo '{}' not found or not shared across multiple repos", name);
}
Ok(instances)
}
fn has_uncommitted_changes(path: &Path) -> bool {
let output = Command::new("git")
.args(["-C", path.to_str().unwrap(), "status", "--porcelain"])
.output();
match output {
Ok(out) => !out.stdout.is_empty(),
Err(_) => false,
}
}
fn stash_changes(path: &Path) -> Result<()> {
let output = Command::new("git")
.args([
"-C",
path.to_str().unwrap(),
"stash",
"push",
"-m",
"repos-subrepo-sync: auto-stash",
])
.output()
.context("Failed to run git stash")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("git stash failed: {}", stderr);
}
Ok(())
}
fn checkout_commit(path: &Path, commit: &str) -> Result<()> {
let output = Command::new("git")
.args([
"-C",
path.to_str().unwrap(),
"checkout",
commit,
])
.output()
.context("Failed to run git checkout")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("git checkout failed: {}", stderr);
}
Ok(())
}
fn fetch_latest_commit(path: &Path) -> Result<String> {
let fetch_output = Command::new("git")
.args(["-C", path.to_str().unwrap(), "fetch", "origin"])
.output()
.context("Failed to run git fetch")?;
if !fetch_output.status.success() {
let stderr = String::from_utf8_lossy(&fetch_output.stderr);
anyhow::bail!("git fetch failed: {}", stderr);
}
for branch in &["origin/HEAD", "origin/main", "origin/master"] {
let output = Command::new("git")
.args([
"-C",
path.to_str().unwrap(),
"rev-parse",
branch,
])
.output();
if let Ok(out) = output {
if out.status.success() {
let commit = String::from_utf8(out.stdout)?
.trim()
.to_string();
return Ok(commit);
}
}
}
anyhow::bail!("Could not determine latest commit from remote")
}
pub fn sync_subrepo(name: &str, target_commit: &str, stash: bool, force: bool) -> Result<()> {
let report = super::validation::validate_subrepos()?;
let instances = find_instances_by_name(&report, name)?;
let short_commit = target_commit.chars().take(7).collect::<String>();
println!("\n🔄 Syncing {} to {}...\n", name, short_commit);
let mut success_count = 0;
let mut skip_count = 0;
let mut error_count = 0;
let mut stashed_count = 0;
for instance in &instances {
let has_changes = has_uncommitted_changes(&instance.subrepo_path);
if has_changes {
if stash {
match stash_changes(&instance.subrepo_path) {
Ok(_) => {
stashed_count += 1;
}
Err(e) => {
println!(" ❌ {} (stash failed: {})", instance.parent_repo, e);
error_count += 1;
continue;
}
}
} else if !force {
println!(" ⚠️ {} (uncommitted changes, use --stash or --force)", instance.parent_repo);
skip_count += 1;
continue;
}
}
match checkout_commit(&instance.subrepo_path, target_commit) {
Ok(_) => {
println!(" ✅ {}", instance.parent_repo);
success_count += 1;
}
Err(e) => {
println!(" ❌ {} ({})", instance.parent_repo, e);
error_count += 1;
}
}
}
println!();
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
println!("📊 Sync Summary");
println!(" ✅ {} synced", success_count);
if stashed_count > 0 {
println!(" 📦 {} stashed (changes saved, run 'git stash pop' to restore)", stashed_count);
}
if skip_count > 0 {
println!(" ⚠️ {} skipped (uncommitted changes)", skip_count);
}
if error_count > 0 {
println!(" ❌ {} failed", error_count);
}
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
println!();
if error_count > 0 {
anyhow::bail!("{} repositories failed to sync", error_count);
}
Ok(())
}
pub fn update_subrepo(name: &str, force: bool) -> Result<()> {
let report = super::validation::validate_subrepos()?;
let instances = find_instances_by_name(&report, name)?;
println!("\n🔍 Fetching latest commit for {}...", name);
let latest = fetch_latest_commit(&instances[0].subrepo_path)?;
let short_latest = latest.chars().take(7).collect::<String>();
println!(" Latest commit: {}\n", short_latest);
println!("🔄 Updating {} to {}...\n", name, short_latest);
let mut success_count = 0;
let mut skip_count = 0;
let mut error_count = 0;
for instance in &instances {
if instance.commit_hash == latest {
println!(" ✨ {} (already at latest)", instance.parent_repo);
success_count += 1;
continue;
}
if !force && has_uncommitted_changes(&instance.subrepo_path) {
println!(" ⚠️ {} (uncommitted changes, use --force)", instance.parent_repo);
skip_count += 1;
continue;
}
match fetch_latest_commit(&instance.subrepo_path) {
Ok(commit) => {
match checkout_commit(&instance.subrepo_path, &commit) {
Ok(_) => {
let old_short = instance.short_hash.clone();
println!(" ✅ {} ({} → {})", instance.parent_repo, old_short, short_latest);
success_count += 1;
}
Err(e) => {
println!(" ❌ {} ({})", instance.parent_repo, e);
error_count += 1;
}
}
}
Err(e) => {
println!(" ❌ {} (fetch failed: {})", instance.parent_repo, e);
error_count += 1;
}
}
}
println!();
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
println!("📊 Update Summary");
println!(" ✅ {} updated", success_count);
if skip_count > 0 {
println!(" ⚠️ {} skipped (uncommitted changes)", skip_count);
}
if error_count > 0 {
println!(" ❌ {} failed", error_count);
}
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
println!();
if error_count > 0 {
anyhow::bail!("{} repositories failed to update", error_count);
}
Ok(())
}