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            if let Ok(mut dep_id) = DepIdentifier::resolve_type(&git, dep_path, override_tag) {
79                // Store the previous state before overriding
80                let prev = foundry_lock.get(rel_path).cloned();
81
82                // If it's a branch, mark it as overridden so it gets updated below
83                if let DepIdentifier::Branch { .. } = dep_id {
84                    dep_id.mark_override();
85                }
86
87                // Update the lockfile
88                foundry_lock.override_dep(rel_path, dep_id)?;
89
90                // Only track as updated if there was a previous dependency
91                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        // fetch the latest changes for each submodule (recursively if flag is set)
104        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        // Include every selected dependency, including pins that need initialization or recursive
108        // updates. With no selection, preserve the update-all behavior.
109        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        // Update branches to their latest commit from origin
142        // This handles both explicit updates (forge update dep@branch) and
143        // general updates (forge update) for branch-tracked dependencies
144        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            // Fetch and checkout the latest commit from the remote branch
159            git.fetch_and_checkout_branch(&submodule_path, name)?;
160
161            // Now get the updated revision after syncing with origin
162            let (updated_rev, _) = git.current_rev_branch(&submodule_path)?;
163
164            // Update the lockfile entry to reflect the latest commit
165            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            // Only insert if we don't already have a previous state for this path
175            // (e.g., from explicit overrides where we converted tag to branch)
176            if !prev_dep_ids.contains_key(path) {
177                prev_dep_ids.insert(path.to_owned(), prev);
178            }
179        }
180
181        // checkout the submodules at the correct tags
182        // Skip branches that were already updated above to avoid reverting to local branch
183        for (path, dep_id) in foundry_lock.iter() {
184            // Ignore other dependencies if single update.
185            if !paths.is_empty() && !paths.contains(path) {
186                continue;
187            }
188
189            // Skip branches that were already updated
190            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            // Initialize nested submodules after restoring their selected parents' final revisions.
198            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        // Print updates from => to
216        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    /// Returns the `lib/paths` of the dependencies that have been updated/overridden.
230    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/// Returns `(root, paths, overridden_deps_with_abosolute_paths)` where `root` is the root of the
244/// Git repository and `paths` are the relative paths of the dependencies.
245#[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}