changepacks_cli/commands/
update.rs

1use anyhow::{Context, Result};
2use changepacks_core::{Package, Project};
3use changepacks_utils::{
4    apply_reverse_dependencies, clear_update_logs, display_update, find_current_git_repo,
5    find_project_dirs, gen_changepack_result_map, gen_update_map, get_changepacks_config,
6    get_changepacks_dir, get_relative_path,
7};
8use clap::Args;
9
10use crate::{
11    finders::get_finders,
12    options::FormatOptions,
13    prompter::{InquirePrompter, Prompter},
14};
15
16#[derive(Args, Debug)]
17#[command(about = "Check project status")]
18pub struct UpdateArgs {
19    #[arg(short, long)]
20    pub dry_run: bool,
21
22    #[arg(short, long)]
23    pub yes: bool,
24
25    #[arg(long, default_value = "stdout")]
26    pub format: FormatOptions,
27
28    #[arg(short, long, default_value = "false")]
29    pub remote: bool,
30}
31
32/// Update project version
33pub async fn handle_update(args: &UpdateArgs) -> Result<()> {
34    handle_update_with_prompter(args, &InquirePrompter).await
35}
36
37pub async fn handle_update_with_prompter(args: &UpdateArgs, prompter: &dyn Prompter) -> Result<()> {
38    let current_dir = std::env::current_dir()?;
39    let repo = find_current_git_repo(&current_dir)?;
40    let repo_root_path = repo.work_dir().context("Not a working directory")?;
41    let changepacks_dir = get_changepacks_dir(&current_dir)?;
42    // check if config.json exists
43
44    let config = get_changepacks_config(&current_dir).await?;
45    let mut update_map = gen_update_map(&current_dir, &config).await?;
46
47    let mut project_finders = get_finders();
48    let mut all_finders = get_finders();
49
50    find_project_dirs(&repo, &mut project_finders, &config, args.remote).await?;
51    find_project_dirs(&repo, &mut all_finders, &Default::default(), args.remote).await?;
52
53    // Apply reverse dependency updates (workspace:* dependencies)
54    let all_projects: Vec<&Project> = all_finders
55        .iter()
56        .flat_map(|finder| finder.projects())
57        .collect();
58    apply_reverse_dependencies(&mut update_map, &all_projects, repo_root_path);
59
60    if update_map.is_empty() {
61        match args.format {
62            FormatOptions::Stdout => {
63                println!("No updates found");
64            }
65            FormatOptions::Json => {
66                println!("{{}}");
67            }
68        }
69        return Ok(());
70    }
71    if let FormatOptions::Stdout = args.format {
72        println!("Updates found:");
73    }
74
75    let mut update_projects = Vec::new();
76    let mut workspace_projects = Vec::new();
77
78    for finder in project_finders.iter_mut() {
79        for project in finder.projects_mut() {
80            if let Some((update_type, _)) =
81                update_map.get(&get_relative_path(repo_root_path, project.path())?)
82            {
83                update_projects.push((project, update_type.clone()));
84                continue;
85            }
86        }
87    }
88    for finder in all_finders.iter_mut() {
89        for project in finder.projects() {
90            if let Project::Workspace(workspace) = project {
91                workspace_projects.push(workspace);
92            }
93        }
94    }
95    update_projects.sort();
96    if let FormatOptions::Stdout = args.format {
97        for (project, update_type) in update_projects.iter() {
98            println!(
99                "{} {}",
100                project,
101                display_update(project.version(), update_type.clone())?
102            );
103        }
104    }
105    if args.dry_run {
106        match args.format {
107            FormatOptions::Stdout => {
108                println!("Dry run, no updates will be made");
109            }
110            FormatOptions::Json => {
111                println!("{{}}");
112            }
113        }
114        return Ok(());
115    }
116    // confirm
117    let confirm = if args.yes {
118        true
119    } else {
120        prompter.confirm("Are you sure you want to update the projects?")?
121    };
122    if !confirm {
123        match args.format {
124            FormatOptions::Stdout => {
125                println!("Update cancelled");
126            }
127            FormatOptions::Json => {
128                println!("{{}}");
129            }
130        }
131        return Ok(());
132    }
133
134    futures::future::join_all(
135        update_projects
136            .iter_mut()
137            .map(|(project, update_type)| project.update_version(update_type.clone())),
138    )
139    .await
140    .into_iter()
141    .collect::<Result<Vec<_>>>()?;
142
143    let projects: Vec<&dyn Package> = update_projects
144        .iter()
145        .filter_map(|(project, _)| {
146            if let Project::Package(package) = project {
147                Some(package.as_ref())
148            } else {
149                None
150            }
151        })
152        .collect();
153    // update workspace dependencies
154    futures::future::join_all(
155        workspace_projects
156            .iter()
157            .map(|workspace| workspace.update_workspace_dependencies(&projects)),
158    )
159    .await
160    .into_iter()
161    .collect::<Result<Vec<_>>>()?;
162
163    if let FormatOptions::Json = args.format {
164        println!(
165            "{}",
166            serde_json::to_string_pretty(&gen_changepack_result_map(
167                project_finders
168                    .iter()
169                    .flat_map(|finder| finder.projects())
170                    .collect::<Vec<_>>()
171                    .as_slice(),
172                repo_root_path,
173                &mut update_map,
174            )?)?
175        );
176    }
177
178    // Clear files
179    clear_update_logs(&changepacks_dir).await?;
180
181    Ok(())
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187    use clap::Parser;
188
189    #[derive(Parser)]
190    struct TestCli {
191        #[command(flatten)]
192        update: UpdateArgs,
193    }
194
195    #[test]
196    fn test_update_args_default() {
197        let cli = TestCli::parse_from(["test"]);
198        assert!(!cli.update.dry_run);
199        assert!(!cli.update.yes);
200        assert!(matches!(cli.update.format, FormatOptions::Stdout));
201        assert!(!cli.update.remote);
202    }
203
204    #[test]
205    fn test_update_args_with_dry_run() {
206        let cli = TestCli::parse_from(["test", "--dry-run"]);
207        assert!(cli.update.dry_run);
208    }
209
210    #[test]
211    fn test_update_args_with_yes() {
212        let cli = TestCli::parse_from(["test", "--yes"]);
213        assert!(cli.update.yes);
214    }
215
216    #[test]
217    fn test_update_args_with_format_json() {
218        let cli = TestCli::parse_from(["test", "--format", "json"]);
219        assert!(matches!(cli.update.format, FormatOptions::Json));
220    }
221
222    #[test]
223    fn test_update_args_with_remote() {
224        let cli = TestCli::parse_from(["test", "--remote"]);
225        assert!(cli.update.remote);
226    }
227
228    #[test]
229    fn test_update_args_combined() {
230        let cli =
231            TestCli::parse_from(["test", "--dry-run", "--yes", "--format", "json", "--remote"]);
232        assert!(cli.update.dry_run);
233        assert!(cli.update.yes);
234        assert!(matches!(cli.update.format, FormatOptions::Json));
235        assert!(cli.update.remote);
236    }
237}