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::{Git, LoadConfig},
8};
9use foundry_common::fs::canonicalize_path;
10use foundry_config::{Config, impl_figment_convert_basic};
11use std::path::{Path, PathBuf};
12use yansi::Paint;
13
14#[derive(Clone, Debug, Parser)]
16pub struct UpdateArgs {
17 dependencies: Vec<Dependency>,
19
20 #[arg(long, value_hint = ValueHint::DirPath, value_name = "PATH")]
25 root: Option<PathBuf>,
26
27 #[arg(short, long)]
29 force: bool,
30
31 #[arg(short, long)]
33 recursive: bool,
34}
35impl_figment_convert_basic!(UpdateArgs);
36
37impl UpdateArgs {
38 pub fn run(self) -> Result<()> {
39 let config = self.load_config()?;
40 let (root, paths, dep_overrides) = dependencies_paths(&self.dependencies, &config)?;
42 let git = Git::new(&root);
45
46 let mut foundry_lock = Lockfile::new(&config.root).with_git(&git);
47 let out_of_sync_deps = foundry_lock.sync(config.install_lib_dir())?;
48 let submodules = git.submodules_in(Path::new(""))?;
49 if let Some(path) = foundry_lock
50 .iter()
51 .map(|(path, _)| path)
52 .filter(|path| !submodules.iter().any(|submodule| submodule.path() == *path))
53 .min()
54 {
55 eyre::bail!(
56 "foundry.lock entry `{}` does not match an installed Git submodule",
57 path.display()
58 );
59 }
60
61 for (path, dep_id) in foundry_lock.iter_mut() {
63 if dep_id.is_branch()
64 && (paths.is_empty() || paths.contains(path))
65 && !dep_overrides.contains_key(&root.join(path))
66 {
67 dep_id.mark_override();
68 }
69 }
70
71 let mut prev_dep_ids: DepMap = HashMap::default();
73 for (dep_path, override_tag) in &dep_overrides {
74 let rel_path = dep_path
75 .strip_prefix(&root)
76 .wrap_err("Dependency path is not relative to the repository root")?;
77
78 let mut dep_id = (|| -> Result<DepIdentifier> {
80 let dependency_git = git.root(dep_path);
81 let canonical_root = canonicalize_path(&root)?;
82 if !dunce::canonicalize(dep_path).is_ok_and(|path| {
83 path.starts_with(&canonical_root)
84 && dependency_git.is_repo_root().unwrap_or(false)
85 }) {
86 eyre::bail!(
87 "Dependency at `{}` is not an initialized Git submodule worktree",
88 rel_path.display()
89 );
90 }
91 let resolve = || -> Result<DepIdentifier> {
92 let remote_branch = format!("origin/{override_tag}");
93 if !git.has_tag(override_tag, dep_path)?
94 && dependency_git
95 .remote_branches()?
96 .lines()
97 .any(|branch| branch.trim() == remote_branch)
98 {
99 return Ok(DepIdentifier::Branch {
100 name: override_tag.clone(),
101 rev: git.get_rev(format!("refs/remotes/{remote_branch}"), dep_path)?,
102 r#override: false,
103 });
104 }
105 let dep_id = DepIdentifier::resolve_type(&git, dep_path, override_tag)?;
106 if dep_id.is_branch() {
107 git.get_rev(format!("refs/remotes/{remote_branch}"), dep_path)?;
109 }
110 Ok(dep_id)
111 };
112 match resolve() {
113 Ok(dep_id) => Ok(dep_id),
114 Err(_) => {
115 dependency_git.fetch(false, "origin", None::<&str>)?;
118 resolve()
119 }
120 }
121 })()
122 .wrap_err_with(|| {
123 format!(
124 "Failed to resolve ref `{override_tag}` for dependency `{}`",
125 rel_path.display()
126 )
127 })?;
128 let prev = foundry_lock.get(rel_path).cloned();
130
131 if let DepIdentifier::Branch { .. } = dep_id {
133 dep_id.mark_override();
134 }
135
136 foundry_lock.override_dep(rel_path, dep_id)?;
138
139 if let Some(prev) = prev {
141 prev_dep_ids.insert(rel_path.to_owned(), prev);
142 }
143 }
144
145 let git = Git::new(&root);
147 let mut update_paths = self.update_dep_paths(&foundry_lock);
148 let initialize_nested = !update_paths.is_empty();
149 if !paths.is_empty() {
152 update_paths.clone_from(&paths);
153 }
154 trace!(?update_paths, "updating deps at");
155
156 git.submodule_update(self.force, true, false, self.recursive, update_paths)?;
157
158 if !self.recursive && initialize_nested && paths.is_empty() {
159 git.submodule_foreach(false, "git submodule update --init --progress --recursive")?;
160 }
161
162 let canonical_root = canonicalize_path(&root)?;
163 let mut checkout_paths = foundry_lock
164 .iter()
165 .map(|(path, _)| path)
166 .filter(|path| paths.is_empty() || paths.contains(path))
167 .collect::<Vec<_>>();
168 checkout_paths.sort();
169 for path in checkout_paths {
170 let target = root.join(path);
171 let initialized = dunce::canonicalize(&target).is_ok_and(|target| {
172 target.starts_with(&canonical_root)
173 && Git::new(&target).is_repo_root().unwrap_or(false)
174 });
175 if !initialized {
176 eyre::bail!(
177 "Dependency at `{}` is not an initialized Git submodule worktree",
178 path.display()
179 );
180 }
181 }
182
183 let branch_overrides = foundry_lock
187 .iter_mut()
188 .filter_map(|(path, dep_id)| {
189 if dep_id.is_branch() && dep_id.overridden() {
190 return Some((path, dep_id));
191 }
192 None
193 })
194 .collect::<Vec<_>>();
195
196 for (path, dep_id) in branch_overrides {
197 let submodule_path = root.join(path);
198 let name = dep_id.name();
199
200 git.fetch_and_checkout_branch(&submodule_path, name)?;
202
203 let (updated_rev, _) = git.current_rev_branch(&submodule_path)?;
205
206 let prev = std::mem::replace(
208 dep_id,
209 DepIdentifier::Branch {
210 name: name.to_string(),
211 rev: updated_rev,
212 r#override: true,
213 },
214 );
215
216 if !prev_dep_ids.contains_key(path) {
219 prev_dep_ids.insert(path.to_owned(), prev);
220 }
221 }
222
223 for (path, dep_id) in foundry_lock.iter() {
226 if !paths.is_empty() && !paths.contains(path) {
228 continue;
229 }
230
231 if dep_id.is_branch() && dep_id.overridden() {
233 continue;
234 }
235 git.checkout_at(dep_id.checkout_id(), &root.join(path))?;
236 }
237
238 if !self.recursive && initialize_nested {
239 for path in &paths {
241 git.root(&root.join(path)).submodule_update(
242 false,
243 false,
244 false,
245 true,
246 std::iter::empty::<PathBuf>(),
247 )?;
248 }
249 }
250
251 if out_of_sync_deps.is_some_and(|o| !o.is_empty())
252 || foundry_lock.iter().any(|(_, dep_id)| dep_id.overridden())
253 {
254 foundry_lock.write()?;
255 }
256
257 for (path, prev) in prev_dep_ids {
259 let curr = foundry_lock.get(&path).unwrap();
260 sh_status!(
261 "Updated dep at '{}', (from: {prev}, to: {curr})",
262 path.display().green(),
263 prev = prev,
264 curr = curr.yellow()
265 )?;
266 }
267
268 Ok(())
269 }
270
271 fn update_dep_paths(&self, foundry_lock: &Lockfile<'_>) -> Vec<PathBuf> {
273 foundry_lock
274 .iter()
275 .filter_map(|(path, dep_id)| {
276 if dep_id.overridden() {
277 return Some(path.clone());
278 }
279 None
280 })
281 .collect()
282 }
283}
284
285#[allow(clippy::type_complexity)]
288pub fn dependencies_paths(
289 deps: &[Dependency],
290 config: &Config,
291) -> Result<(PathBuf, Vec<PathBuf>, HashMap<PathBuf, String>)> {
292 let git_root = Git::root_of(&config.root)?;
293 let libs = config.install_lib_dir();
294
295 if deps.is_empty() {
296 return Ok((git_root, Vec::new(), HashMap::default()));
297 }
298
299 let mut paths = Vec::with_capacity(deps.len());
300 let mut overrides = HashMap::with_capacity_and_hasher(deps.len(), Default::default());
301 for dep in deps {
302 let name = dep.name();
303 let dep_path = libs.join(name);
304 if !dep_path.exists() {
305 eyre::bail!("Could not find dependency {name:?} in {}", dep_path.display());
306 }
307 let rel_path = dep_path
308 .strip_prefix(&git_root)
309 .wrap_err("Library directory is not relative to the repository root")?;
310
311 if let Some(tag) = &dep.tag {
312 overrides.insert(dep_path.clone(), tag.to_owned());
313 }
314 paths.push(rel_path.to_owned());
315 }
316 Ok((git_root, paths, overrides))
317}