1use crate::{DepIdentifier, DepMap, Lockfile};
2use alloy_primitives::map::HashMap;
3use clap::{Parser, ValueHint};
4use eyre::{Context, Result};
5use foundry_cli::{
6 opts::Dependency,
7 utils::{CommandUtils, Git, LoadConfig},
8};
9use foundry_config::{Config, impl_figment_convert_basic};
10use std::path::{Path, PathBuf};
11use yansi::Paint;
12
13#[derive(Clone, Debug, Parser)]
15pub struct UpdateArgs {
16 dependencies: Vec<Dependency>,
18
19 #[arg(long, value_hint = ValueHint::DirPath, value_name = "PATH")]
24 root: Option<PathBuf>,
25
26 #[arg(short, long)]
28 force: bool,
29
30 #[arg(short, long)]
32 recursive: bool,
33}
34impl_figment_convert_basic!(UpdateArgs);
35
36impl UpdateArgs {
37 pub fn run(self) -> Result<()> {
38 let config = self.load_config()?;
39 let (root, _paths, dep_overrides) = dependencies_paths(&self.dependencies, &config)?;
41 let git = Git::new(&root);
44
45 let mut foundry_lock = Lockfile::new(&config.root).with_git(&git);
46 let out_of_sync_deps = foundry_lock.sync(config.install_lib_dir())?;
47
48 let mut prev_dep_ids: DepMap = HashMap::default();
50 if dep_overrides.is_empty() {
51 foundry_lock.iter_mut().for_each(|(_path, dep_id)| {
53 if let DepIdentifier::Branch { .. } = dep_id {
55 dep_id.mark_override();
56 }
57 });
58 } else {
59 for (dep_path, override_tag) in &dep_overrides {
60 let rel_path = dep_path
61 .strip_prefix(&root)
62 .wrap_err("Dependency path is not relative to the repository root")?;
63
64 if let Ok(mut dep_id) = DepIdentifier::resolve_type(&git, dep_path, override_tag) {
65 let prev = foundry_lock.get(rel_path).cloned();
67
68 if let DepIdentifier::Branch { .. } = dep_id {
70 dep_id.mark_override();
71 }
72
73 foundry_lock.override_dep(rel_path, dep_id)?;
75
76 if let Some(prev) = prev {
78 prev_dep_ids.insert(rel_path.to_owned(), prev);
79 }
80 } else {
81 sh_warn!(
82 "Could not r#override submodule at {} with tag {}, try using forge install",
83 rel_path.display(),
84 override_tag
85 )?;
86 }
87 }
88 }
89
90 let git = Git::new(&root);
92 let update_paths = self.update_dep_paths(&foundry_lock);
93 trace!(?update_paths, "updating deps at");
94
95 if self.recursive {
96 git.submodule_update(self.force, true, false, true, update_paths)?;
98 } else {
99 let is_empty = update_paths.is_empty();
100
101 git.submodule_update(self.force, true, false, false, update_paths)?;
103
104 if !is_empty {
105 git.submodule_foreach(false, "git submodule update --init --progress --recursive")?;
108 }
109 }
110
111 let branch_overrides = foundry_lock
115 .iter_mut()
116 .filter_map(|(path, dep_id)| {
117 if dep_id.is_branch() && dep_id.overridden() {
118 return Some((path, dep_id));
119 }
120 None
121 })
122 .collect::<Vec<_>>();
123
124 for (path, dep_id) in branch_overrides {
125 let submodule_path = root.join(path);
126 let name = dep_id.name();
127
128 Self::fetch_and_checkout_branch(&git, &submodule_path, name)?;
130
131 let (updated_rev, _) = git.current_rev_branch(&submodule_path)?;
133
134 let prev = std::mem::replace(
136 dep_id,
137 DepIdentifier::Branch {
138 name: name.to_string(),
139 rev: updated_rev,
140 r#override: true,
141 },
142 );
143
144 if !prev_dep_ids.contains_key(path) {
147 prev_dep_ids.insert(path.to_owned(), prev);
148 }
149 }
150
151 for (path, dep_id) in foundry_lock.iter() {
154 if dep_id.is_branch() && dep_id.overridden() {
156 continue;
157 }
158 git.checkout_at(dep_id.checkout_id(), &root.join(path))?;
159 }
160
161 if out_of_sync_deps.is_some_and(|o| !o.is_empty())
162 || foundry_lock.iter().any(|(_, dep_id)| dep_id.overridden())
163 {
164 foundry_lock.write()?;
165 }
166
167 for (path, prev) in prev_dep_ids {
169 let curr = foundry_lock.get(&path).unwrap();
170 sh_println!(
171 "Updated dep at '{}', (from: {prev}, to: {curr})",
172 path.display().green(),
173 prev = prev,
174 curr = curr.yellow()
175 )?;
176 }
177
178 Ok(())
179 }
180
181 fn update_dep_paths(&self, foundry_lock: &Lockfile<'_>) -> Vec<PathBuf> {
183 foundry_lock
184 .iter()
185 .filter_map(|(path, dep_id)| {
186 if dep_id.overridden() {
187 return Some(path.to_path_buf());
188 }
189 None
190 })
191 .collect()
192 }
193
194 fn fetch_and_checkout_branch(git: &Git<'_>, path: &Path, branch: &str) -> Result<()> {
196 git.cmd_at(path).args(["fetch", "origin", branch]).exec().wrap_err(format!(
198 "Could not fetch latest changes for branch {} in submodule at {}",
199 branch,
200 path.display()
201 ))?;
202
203 git.cmd_at(path)
206 .args(["checkout", "-B", branch, &format!("origin/{branch}")])
207 .exec()
208 .wrap_err(format!(
209 "Could not checkout and track origin/{} for submodule at {}",
210 branch,
211 path.display()
212 ))?;
213
214 Ok(())
215 }
216}
217
218#[allow(clippy::type_complexity)]
221pub fn dependencies_paths(
222 deps: &[Dependency],
223 config: &Config,
224) -> Result<(PathBuf, Vec<PathBuf>, HashMap<PathBuf, String>)> {
225 let git_root = Git::root_of(&config.root)?;
226 let libs = config.install_lib_dir();
227
228 if deps.is_empty() {
229 return Ok((git_root, Vec::new(), HashMap::default()));
230 }
231
232 let mut paths = Vec::with_capacity(deps.len());
233 let mut overrides = HashMap::with_capacity_and_hasher(deps.len(), Default::default());
234 for dep in deps {
235 let name = dep.name();
236 let dep_path = libs.join(name);
237 if !dep_path.exists() {
238 eyre::bail!("Could not find dependency {name:?} in {}", dep_path.display());
239 }
240 let rel_path = dep_path
241 .strip_prefix(&git_root)
242 .wrap_err("Library directory is not relative to the repository root")?;
243
244 if let Some(tag) = &dep.tag {
245 overrides.insert(dep_path.to_owned(), tag.to_owned());
246 }
247 paths.push(rel_path.to_owned());
248 }
249 Ok((git_root, paths, overrides))
250}