Skip to main content

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_common::fs::canonicalize_path;
10use foundry_config::{Config, impl_figment_convert_basic};
11use std::path::{Path, PathBuf};
12use yansi::Paint;
13
14/// CLI arguments for `forge update`.
15#[derive(Clone, Debug, Parser)]
16pub struct UpdateArgs {
17    /// The dependencies you want to update.
18    dependencies: Vec<Dependency>,
19
20    /// The project's root path.
21    ///
22    /// By default root of the Git repository, if in one,
23    /// or the current working directory.
24    #[arg(long, value_hint = ValueHint::DirPath, value_name = "PATH")]
25    root: Option<PathBuf>,
26
27    /// Override the up-to-date check.
28    #[arg(short, long)]
29    force: bool,
30
31    /// Recursively update submodules.
32    #[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        // dep_overrides consists of absolute paths of dependencies and their tags
41        let (root, paths, dep_overrides) = dependencies_paths(&self.dependencies, &config)?;
42        // Mapping of relative path of lib to its tag type
43        // e.g "lib/forge-std" -> DepIdentifier::Tag { name: "v0.1.0", rev: "1234567" }
44        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        // Update selected branches without an explicit ref override.
62        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        // Update the submodules' tags if any overrides are present.
72        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            // Resolve every override before changing dependency worktrees or the lockfile.
79            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                        // Branch updates below require a corresponding origin branch.
108                        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                        // Newly published refs may not be available locally yet. Fetching must
116                        // not check out anything before all overrides have been validated.
117                        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            // Store the previous state before overriding
129            let prev = foundry_lock.get(rel_path).cloned();
130
131            // If it's a branch, mark it as overridden so it gets updated below
132            if let DepIdentifier::Branch { .. } = dep_id {
133                dep_id.mark_override();
134            }
135
136            // Update the lockfile
137            foundry_lock.override_dep(rel_path, dep_id)?;
138
139            // Only track as updated if there was a previous dependency
140            if let Some(prev) = prev {
141                prev_dep_ids.insert(rel_path.to_owned(), prev);
142            }
143        }
144
145        // fetch the latest changes for each submodule (recursively if flag is set)
146        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        // Include every selected dependency, including pins that need initialization or recursive
150        // updates. With no selection, preserve the update-all behavior.
151        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        // Update branches to their latest commit from origin
184        // This handles both explicit updates (forge update dep@branch) and
185        // general updates (forge update) for branch-tracked dependencies
186        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            // Fetch and checkout the latest commit from the remote branch
201            git.fetch_and_checkout_branch(&submodule_path, name)?;
202
203            // Now get the updated revision after syncing with origin
204            let (updated_rev, _) = git.current_rev_branch(&submodule_path)?;
205
206            // Update the lockfile entry to reflect the latest commit
207            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            // Only insert if we don't already have a previous state for this path
217            // (e.g., from explicit overrides where we converted tag to branch)
218            if !prev_dep_ids.contains_key(path) {
219                prev_dep_ids.insert(path.to_owned(), prev);
220            }
221        }
222
223        // checkout the submodules at the correct tags
224        // Skip branches that were already updated above to avoid reverting to local branch
225        for (path, dep_id) in foundry_lock.iter() {
226            // Ignore other dependencies if single update.
227            if !paths.is_empty() && !paths.contains(path) {
228                continue;
229            }
230
231            // Skip branches that were already updated
232            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            // Initialize nested submodules after restoring their selected parents' final revisions.
240            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        // Print updates from => to
258        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    /// Returns the `lib/paths` of the dependencies that have been updated/overridden.
272    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/// Returns `(root, paths, overridden_deps_with_abosolute_paths)` where `root` is the root of the
286/// Git repository and `paths` are the relative paths of the dependencies.
287#[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}