forge/cmd/
update.rs
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_config::{Config, impl_figment_convert_basic};
10use std::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 if let Ok(dep_id) = DepIdentifier::resolve_type(&git, dep_path, override_tag) {
64 let prev = foundry_lock.override_dep(rel_path, dep_id)?;
65 prev_dep_ids.insert(rel_path.to_owned(), prev);
66 } else {
67 sh_warn!(
68 "Could not r#override submodule at {} with tag {}, try using forge install",
69 rel_path.display(),
70 override_tag
71 )?;
72 }
73 }
74 }
75
76 let git = Git::new(&root);
78 let update_paths = self.update_dep_paths(&foundry_lock);
79 trace!(?update_paths, "updating deps at");
80
81 if self.recursive {
82 git.submodule_update(self.force, true, false, true, update_paths)?;
84 } else {
85 let is_empty = update_paths.is_empty();
86
87 git.submodule_update(self.force, true, false, false, update_paths)?;
89
90 if !is_empty {
91 git.submodule_foreach(false, "git submodule update --init --progress --recursive")?;
94 }
95 }
96
97 if dep_overrides.is_empty() {
103 let branch_overrides = foundry_lock
104 .iter_mut()
105 .filter_map(|(path, dep_id)| {
106 if dep_id.is_branch() && dep_id.overridden() {
107 return Some((path, dep_id));
108 }
109 None
110 })
111 .collect::<Vec<_>>();
112
113 for (path, dep_id) in branch_overrides {
114 let (curr_rev, curr_branch) = git.current_rev_branch(&root.join(path))?;
115 let name = dep_id.name();
116 if curr_branch != name {
118 let warn_msg = format!(
119 r#"Lockfile sync warning
120 Lockfile is tracking branch {name} for submodule at {path:?}, but the submodule is currently on {curr_branch}.
121 Checking out branch {name} for submodule at {path:?}."#,
122 );
123 let _ = sh_warn!("{}", warn_msg);
124 git.checkout_at(name, &root.join(path)).wrap_err(format!(
125 "Could not checkout branch {name} for submodule at {}",
126 path.display()
127 ))?;
128 }
129
130 let prev = std::mem::replace(
132 dep_id,
133 DepIdentifier::Branch {
134 name: name.to_string(),
135 rev: curr_rev,
136 r#override: true,
137 },
138 );
139 prev_dep_ids.insert(path.to_owned(), prev);
140 }
141 }
142
143 for (path, dep_id) in foundry_lock.iter() {
145 git.checkout_at(dep_id.checkout_id(), &root.join(path))?;
146 }
147
148 if out_of_sync_deps.is_some_and(|o| !o.is_empty())
149 || foundry_lock.iter().any(|(_, dep_id)| dep_id.overridden())
150 {
151 foundry_lock.write()?;
152 }
153
154 for (path, prev) in prev_dep_ids {
156 let curr = foundry_lock.get(&path).unwrap();
157 sh_println!(
158 "Updated dep at '{}', (from: {prev}, to: {curr})",
159 path.display().green(),
160 prev = prev,
161 curr = curr.yellow()
162 )?;
163 }
164
165 Ok(())
166 }
167
168 fn update_dep_paths(&self, foundry_lock: &Lockfile<'_>) -> Vec<PathBuf> {
170 foundry_lock
171 .iter()
172 .filter_map(|(path, dep_id)| {
173 if dep_id.overridden() {
174 return Some(path.to_path_buf());
175 }
176 None
177 })
178 .collect()
179 }
180}
181
182#[allow(clippy::type_complexity)]
185pub fn dependencies_paths(
186 deps: &[Dependency],
187 config: &Config,
188) -> Result<(PathBuf, Vec<PathBuf>, HashMap<PathBuf, String>)> {
189 let git_root = Git::root_of(&config.root)?;
190 let libs = config.install_lib_dir();
191
192 if deps.is_empty() {
193 return Ok((git_root, Vec::new(), HashMap::default()));
194 }
195
196 let mut paths = Vec::with_capacity(deps.len());
197 let mut overrides = HashMap::with_capacity_and_hasher(deps.len(), Default::default());
198 for dep in deps {
199 let name = dep.name();
200 let dep_path = libs.join(name);
201 if !dep_path.exists() {
202 eyre::bail!("Could not find dependency {name:?} in {}", dep_path.display());
203 }
204 let rel_path = dep_path
205 .strip_prefix(&git_root)
206 .wrap_err("Library directory is not relative to the repository root")?;
207
208 if let Some(tag) = &dep.tag {
209 overrides.insert(dep_path.to_owned(), tag.to_owned());
210 }
211 paths.push(rel_path.to_owned());
212 }
213 Ok((git_root, paths, overrides))
214}