Files
empeve/src/commands/update.rs
vikingowl 6be61df8a0 fix: address code quality issues, validation gaps, and add test coverage
Phase 1 - Code Quality:
- Rename script/script.rs to script/types.rs (module inception fix)
- Apply Clippy lint fixes (is_none_or, is_some_and, char patterns, etc.)
- Implement FromStr for CatalogFile and Category
- Add filtered_targets() and filtered_repos() helpers to Config

Phase 2 - Validation & Error Handling:
- Add validate_repo_identifier() for GitHub shorthand validation
- Fix first-run setup to fail gracefully if no targets configured
- Improve import to collect failures and only save on success
- Add AssetInstallResult for detailed install failure tracking
- Fix lockfile timestamp documentation (Unix epoch, not RFC 3339)
- Add comprehensive RevType heuristics documentation
- Add checkout warning when local modifications will be discarded

Phase 3 - Test Coverage:
- Add tempfile, assert_cmd, predicates dev dependencies
- Add security tests (symlink boundaries, copy mode)
- Add git operations tests (init, head_commit, RevType parsing)
- Add lockfile tests (roundtrip, lock/get operations)
- Add CLI integration tests (help, validation, duplicates)
- Add config validation tests for new helper methods

All 48 tests pass, clippy clean, release build verified.
2026-01-26 10:19:10 +01:00

113 lines
3.6 KiB
Rust

use colored::Colorize;
use crate::config::Config;
use crate::error::Result;
use crate::paths::Paths;
use crate::repo::{Repository, git_ops::{RevType, UpdateResult}};
use crate::ui::create_spinner;
/// Execute the `update` command - fetch and update all repositories
pub fn execute(repos_filter: Option<Vec<String>>) -> Result<()> {
let paths = Paths::new()?;
let config = Config::load(&paths.config_file)?;
if config.repos.is_empty() {
println!("{}", "No repositories configured.".yellow());
return Ok(());
}
let mut updated = 0;
let mut up_to_date = 0;
let mut errors = 0;
for entry in config.filtered_repos(repos_filter.as_deref()) {
let repo = Repository::from_entry(entry.clone(), &paths);
if !repo.is_cloned {
println!(
"{} {} (not installed, run 'install' first)",
"Skipping".yellow(),
entry.repo.cyan()
);
continue;
}
let spinner = create_spinner(&format!("Checking {}...", entry.repo));
// Get current commit before fetch
let before_commit = repo.current_commit()?.unwrap_or_default();
// Fetch updates
if let Err(e) = repo.fetch() {
spinner.finish_with_message(format!("{} {} {}", "".red(), entry.repo.cyan(), "fetch failed".red()));
eprintln!(" {}: {}", "Error".red(), e);
errors += 1;
continue;
}
// Check for updates
match repo.update() {
Ok(UpdateResult::Updated(new_commit)) => {
spinner.finish_with_message(format!(
"{} {} {} ({}{})",
"".green(),
entry.repo.cyan(),
"updated".green(),
&before_commit[..7.min(before_commit.len())].dimmed(),
&new_commit[..7.min(new_commit.len())].green()
));
updated += 1;
}
Ok(UpdateResult::UpToDate) => {
spinner.finish_with_message(format!(
"{} {} {}",
"".dimmed(),
entry.repo.cyan(),
"up to date".dimmed()
));
up_to_date += 1;
}
Ok(UpdateResult::Pinned) => {
let pin_info = match repo.rev_type() {
RevType::Commit(c) => format!("commit {}", &c[..7.min(c.len())]),
RevType::Tag(t) => format!("tag {}", t),
_ => "pinned".to_string(),
};
spinner.finish_with_message(format!(
"{} {} {} ({})",
"📌".dimmed(),
entry.repo.cyan(),
"pinned".dimmed(),
pin_info.dimmed()
));
up_to_date += 1;
}
Err(e) => {
spinner.finish_with_message(format!(
"{} {} {}",
"".red(),
entry.repo.cyan(),
"update failed".red()
));
eprintln!(" {}: {}", "Error".red(), e);
errors += 1;
}
}
}
println!();
println!(
"{} {} updated, {} up to date, {} errors",
"Done!".green().bold(),
updated.to_string().cyan(),
up_to_date.to_string().dimmed(),
if errors > 0 {
errors.to_string().red()
} else {
errors.to_string().dimmed()
}
);
Ok(())
}