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/// CLI arguments for `forge update`.
14#[derive(Clone, Debug, Parser)]
15pub struct UpdateArgs {
16    /// The dependencies you want to update.
17    dependencies: Vec<Dependency>,
18
19    /// The project's root path.
20    ///
21    /// By default root of the Git repository, if in one,
22    /// or the current working directory.
23    #[arg(long, value_hint = ValueHint::DirPath, value_name = "PATH")]
24    root: Option<PathBuf>,
25
26    /// Override the up-to-date check.
27    #[arg(short, long)]
28    force: bool,
29
30    /// Recursively update submodules.
31    #[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        // dep_overrides consists of absolute paths of dependencies and their tags
40        let (root, _paths, dep_overrides) = dependencies_paths(&self.dependencies, &config)?;
41        // Mapping of relative path of lib to its tag type
42        // e.g "lib/forge-std" -> DepIdentifier::Tag { name: "v0.1.0", rev: "1234567" }
43        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        // update the submodules' tags if any overrides are present
49        let mut prev_dep_ids: DepMap = HashMap::default();
50        if dep_overrides.is_empty() {
51            // running `forge update`, update all deps
52            foundry_lock.iter_mut().for_each(|(_path, dep_id)| {
53                // Set r#override flag to true if the dep is a branch
54                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        // fetch the latest changes for each submodule (recursively if flag is set)
77        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            // update submodules recursively
83            git.submodule_update(self.force, true, false, true, update_paths)?;
84        } else {
85            let is_empty = update_paths.is_empty();
86
87            // update submodules
88            git.submodule_update(self.force, true, false, false, update_paths)?;
89
90            if !is_empty {
91                // initialize submodules of each submodule recursively (otherwise direct submodule
92                // dependencies will revert to last commit)
93                git.submodule_foreach(false, "git submodule update --init --progress --recursive")?;
94            }
95        }
96
97        // Branches should get updated to their latest commit on `forge update`.
98        // i.e if previously submodule was tracking branch `main` at rev `1234567` and now the
99        // remote `main` branch is at `7654321`, then submodule should also be updated to `7654321`.
100        // This tracking is automatically handled by git, but we need to update the lockfile entry
101        // to reflect the latest commit.
102        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                // This can occur when the submodule is manually checked out to a different branch.
117                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                // Update the lockfile entry to reflect the latest commit
131                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        // checkout the submodules at the correct tags
144        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        // Print updates from => to
155        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    /// Returns the `lib/paths` of the dependencies that have been updated/overridden.
169    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/// Returns `(root, paths, overridden_deps_with_abosolute_paths)` where `root` is the root of the
183/// Git repository and `paths` are the relative paths of the dependencies.
184#[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}