changepacks_cli/commands/
update.rs1use 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
32pub 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(¤t_dir)?;
40 let repo_root_path = repo.work_dir().context("Not a working directory")?;
41 let changepacks_dir = get_changepacks_dir(¤t_dir)?;
42 let config = get_changepacks_config(¤t_dir).await?;
45 let mut update_map = gen_update_map(¤t_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 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 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 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_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}