Skip to main content

xianyun_cli/commands/
update.rs

1//! `caproni update`: update all repository source code.
2//!
3//! Port of Go's `cmd/update` package.
4
5use std::sync::Arc;
6
7use anyhow::Context as _;
8use clap::Args;
9use xianyun_core::config::caproni::load_from_context;
10use xianyun_core::config::global::Context;
11use xianyun_executor::exec::CommandRunner;
12use xianyun_update::updater::{UpdateOptions as UpdaterOptions, UpdateStrategy, Updater};
13
14/// A filter predicate over repository names, as returned by
15/// [`build_repo_filter`].
16pub type RepoFilter = Box<dyn Fn(&str) -> bool + Send + Sync>;
17
18#[derive(Args, Debug)]
19pub struct UpdateArgs {
20    /// Check repository states without making any changes.
21    #[arg(short = 'n', long = "verify-only", visible_alias = "dry-run")]
22    pub verify_only: bool,
23
24    /// Automatically stash uncommitted changes before updating.
25    #[arg(long = "stash")]
26    pub stash: bool,
27
28    /// Update only the specified repository.
29    #[arg(long = "repo")]
30    pub repo: Option<String>,
31
32    /// Skip the specified repository.
33    #[arg(long = "skip")]
34    pub skip: Option<String>,
35
36    /// Suppress detailed output, only show summary.
37    #[arg(short = 'q', long = "quiet")]
38    pub quiet: bool,
39}
40
41pub fn run(args: &UpdateArgs, ctx: &Context) -> anyhow::Result<()> {
42    let executor: Arc<dyn CommandRunner + Send + Sync> =
43        Arc::new(xianyun_executor::exec::Client::new());
44
45    run_update(
46        ctx,
47        &executor,
48        UpdateCommandOptions {
49            verify_only: args.verify_only,
50            stash: args.stash,
51            repo_filter: args.repo.clone().unwrap_or_default(),
52            skip_filter: args.skip.clone().unwrap_or_default(),
53            quiet: args.quiet,
54        },
55    )
56}
57
58/// Port of `update.UpdateOptions`: the resolved `clap` flags for this
59/// command, kept separate from `xianyun_update::updater::UpdateOptions`
60/// (which this maps into) so `run_update` doesn't need a `clap::Args`
61/// dependency.
62pub struct UpdateCommandOptions {
63    pub verify_only: bool,
64    pub stash: bool,
65    pub repo_filter: String,
66    pub skip_filter: String,
67    pub quiet: bool,
68}
69
70/// Port of `update.runUpdate`: loads the configuration, builds a repo
71/// filter from `--repo`/`--skip`, updates every repository, prints the
72/// result table/summary/suggestions, and reports a non-zero exit code if
73/// any repository failed.
74///
75/// # Errors
76///
77/// Returns an error if configuration loading fails. Once configuration
78/// has loaded, a per-repository update failure is reported via the
79/// returned table/exit code, not an `Err`.
80pub fn run_update(
81    ctx: &Context,
82    executor: &Arc<dyn CommandRunner + Send + Sync>,
83    opt: UpdateCommandOptions,
84) -> anyhow::Result<()> {
85    let container = load_from_context(ctx).context("loading configuration")?;
86
87    let strategy = if opt.verify_only {
88        UpdateStrategy::VerifyOnly
89    } else {
90        UpdateStrategy::FullUpdate
91    };
92
93    let filter = build_repo_filter(&opt.repo_filter, &opt.skip_filter);
94
95    let updater_options = UpdaterOptions {
96        strategy,
97        stash: opt.stash,
98        filter,
99        quiet: opt.quiet,
100        skip_prepare_hooks: false,
101    };
102
103    let git_backend: Arc<dyn xianyun_backends::git::Backend + Send + Sync> =
104        Arc::new(xianyun_backends::git::Client::new(executor.clone()));
105
106    let updater = Updater::new(git_backend, updater_options);
107
108    let (results, success) = updater.update_repositories(executor.as_ref(), &container);
109
110    print!(
111        "{}",
112        xianyun_update::updater::format_table(&results, opt.quiet)
113    );
114    print!("{}", xianyun_update::updater::format_summary(&results));
115    print!("{}", xianyun_update::updater::format_suggestions(&results));
116
117    if !success {
118        return Err(crate::exitcode::CliExit::new(1).into());
119    }
120
121    Ok(())
122}
123
124/// Builds a repository filter closure from `--repo`/`--skip` selectors.
125///
126/// Port of Go's `cmd/update.buildRepoFilter`. Returns `None` when neither
127/// filter is set (meaning "include everything"), matching Go's `nil`
128/// `RepoFilterFunc` return. This is pure argument-processing logic with no
129/// dependency on config loading, cluster drivers, or deployment state, so
130/// it's implemented (and tested) directly rather than left as a red-phase
131/// stub.
132#[must_use]
133pub fn build_repo_filter(repo_filter: &str, skip_filter: &str) -> Option<RepoFilter> {
134    if repo_filter.is_empty() && skip_filter.is_empty() {
135        return None;
136    }
137
138    let repo_filter = repo_filter.to_string();
139    let skip_filter = skip_filter.to_string();
140
141    Some(Box::new(move |repo_name: &str| {
142        if !repo_filter.is_empty() && repo_name != repo_filter {
143            return false;
144        }
145
146        if !skip_filter.is_empty() && repo_name == skip_filter {
147            return false;
148        }
149
150        true
151    }))
152}