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 if let Ok(mut dep_id) = DepIdentifier::resolve_type(&git, dep_path, override_tag) {
79 let prev = foundry_lock.get(rel_path).cloned();
81
82 if let DepIdentifier::Branch { .. } = dep_id {
84 dep_id.mark_override();
85 }
86
87 foundry_lock.override_dep(rel_path, dep_id)?;
89
90 if let Some(prev) = prev {
92 prev_dep_ids.insert(rel_path.to_owned(), prev);
93 }
94 } else {
95 sh_warn!(
96 "Could not r#override submodule at {} with tag {}, try using forge install",
97 rel_path.display(),
98 override_tag
99 )?;
100 }
101 }
102
103 let git = Git::new(&root);
105 let mut update_paths = self.update_dep_paths(&foundry_lock);
106 let initialize_nested = !update_paths.is_empty();
107 if !paths.is_empty() {
110 update_paths.clone_from(&paths);
111 }
112 trace!(?update_paths, "updating deps at");
113
114 git.submodule_update(self.force, true, false, self.recursive, update_paths)?;
115
116 if !self.recursive && initialize_nested && paths.is_empty() {
117 git.submodule_foreach(false, "git submodule update --init --progress --recursive")?;
118 }
119
120 let canonical_root = canonicalize_path(&root)?;
121 let mut checkout_paths = foundry_lock
122 .iter()
123 .map(|(path, _)| path)
124 .filter(|path| paths.is_empty() || paths.contains(path))
125 .collect::<Vec<_>>();
126 checkout_paths.sort();
127 for path in checkout_paths {
128 let target = root.join(path);
129 let initialized = dunce::canonicalize(&target).is_ok_and(|target| {
130 target.starts_with(&canonical_root)
131 && Git::new(&target).is_repo_root().unwrap_or(false)
132 });
133 if !initialized {
134 eyre::bail!(
135 "Dependency at `{}` is not an initialized Git submodule worktree",
136 path.display()
137 );
138 }
139 }
140
141 let branch_overrides = foundry_lock
145 .iter_mut()
146 .filter_map(|(path, dep_id)| {
147 if dep_id.is_branch() && dep_id.overridden() {
148 return Some((path, dep_id));
149 }
150 None
151 })
152 .collect::<Vec<_>>();
153
154 for (path, dep_id) in branch_overrides {
155 let submodule_path = root.join(path);
156 let name = dep_id.name();
157
158 git.fetch_and_checkout_branch(&submodule_path, name)?;
160
161 let (updated_rev, _) = git.current_rev_branch(&submodule_path)?;
163
164 let prev = std::mem::replace(
166 dep_id,
167 DepIdentifier::Branch {
168 name: name.to_string(),
169 rev: updated_rev,
170 r#override: true,
171 },
172 );
173
174 if !prev_dep_ids.contains_key(path) {
177 prev_dep_ids.insert(path.to_owned(), prev);
178 }
179 }
180
181 for (path, dep_id) in foundry_lock.iter() {
184 if !paths.is_empty() && !paths.contains(path) {
186 continue;
187 }
188
189 if dep_id.is_branch() && dep_id.overridden() {
191 continue;
192 }
193 git.checkout_at(dep_id.checkout_id(), &root.join(path))?;
194 }
195
196 if !self.recursive && initialize_nested {
197 for path in &paths {
199 git.root(&root.join(path)).submodule_update(
200 false,
201 false,
202 false,
203 true,
204 std::iter::empty::<PathBuf>(),
205 )?;
206 }
207 }
208
209 if out_of_sync_deps.is_some_and(|o| !o.is_empty())
210 || foundry_lock.iter().any(|(_, dep_id)| dep_id.overridden())
211 {
212 foundry_lock.write()?;
213 }
214
215 for (path, prev) in prev_dep_ids {
217 let curr = foundry_lock.get(&path).unwrap();
218 sh_status!(
219 "Updated dep at '{}', (from: {prev}, to: {curr})",
220 path.display().green(),
221 prev = prev,
222 curr = curr.yellow()
223 )?;
224 }
225
226 Ok(())
227 }
228
229 fn update_dep_paths(&self, foundry_lock: &Lockfile<'_>) -> Vec<PathBuf> {
231 foundry_lock
232 .iter()
233 .filter_map(|(path, dep_id)| {
234 if dep_id.overridden() {
235 return Some(path.clone());
236 }
237 None
238 })
239 .collect()
240 }
241}
242
243#[allow(clippy::type_complexity)]
246pub fn dependencies_paths(
247 deps: &[Dependency],
248 config: &Config,
249) -> Result<(PathBuf, Vec<PathBuf>, HashMap<PathBuf, String>)> {
250 let git_root = Git::root_of(&config.root)?;
251 let libs = config.install_lib_dir();
252
253 if deps.is_empty() {
254 return Ok((git_root, Vec::new(), HashMap::default()));
255 }
256
257 let mut paths = Vec::with_capacity(deps.len());
258 let mut overrides = HashMap::with_capacity_and_hasher(deps.len(), Default::default());
259 for dep in deps {
260 let name = dep.name();
261 let dep_path = libs.join(name);
262 if !dep_path.exists() {
263 eyre::bail!("Could not find dependency {name:?} in {}", dep_path.display());
264 }
265 let rel_path = dep_path
266 .strip_prefix(&git_root)
267 .wrap_err("Library directory is not relative to the repository root")?;
268
269 if let Some(tag) = &dep.tag {
270 overrides.insert(dep_path.clone(), tag.to_owned());
271 }
272 paths.push(rel_path.to_owned());
273 }
274 Ok((git_root, paths, overrides))
275}