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::{CommandUtils, Git, LoadConfig},
8};
9use foundry_config::{Config, impl_figment_convert_basic};
10use std::path::{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
64                if let Ok(mut dep_id) = DepIdentifier::resolve_type(&git, dep_path, override_tag) {
65                    // Store the previous state before overriding
66                    let prev = foundry_lock.get(rel_path).cloned();
67
68                    // If it's a branch, mark it as overridden so it gets updated below
69                    if let DepIdentifier::Branch { .. } = dep_id {
70                        dep_id.mark_override();
71                    }
72
73                    // Update the lockfile
74                    foundry_lock.override_dep(rel_path, dep_id)?;
75
76                    // Only track as updated if there was a previous dependency
77                    if let Some(prev) = prev {
78                        prev_dep_ids.insert(rel_path.to_owned(), prev);
79                    }
80                } else {
81                    sh_warn!(
82                        "Could not r#override submodule at {} with tag {}, try using forge install",
83                        rel_path.display(),
84                        override_tag
85                    )?;
86                }
87            }
88        }
89
90        // fetch the latest changes for each submodule (recursively if flag is set)
91        let git = Git::new(&root);
92        let update_paths = self.update_dep_paths(&foundry_lock);
93        trace!(?update_paths, "updating deps at");
94
95        if self.recursive {
96            // update submodules recursively
97            git.submodule_update(self.force, true, false, true, update_paths)?;
98        } else {
99            let is_empty = update_paths.is_empty();
100
101            // update submodules
102            git.submodule_update(self.force, true, false, false, update_paths)?;
103
104            if !is_empty {
105                // initialize submodules of each submodule recursively (otherwise direct submodule
106                // dependencies will revert to last commit)
107                git.submodule_foreach(false, "git submodule update --init --progress --recursive")?;
108            }
109        }
110
111        // Update branches to their latest commit from origin
112        // This handles both explicit updates (forge update dep@branch) and
113        // general updates (forge update) for branch-tracked dependencies
114        let branch_overrides = foundry_lock
115            .iter_mut()
116            .filter_map(|(path, dep_id)| {
117                if dep_id.is_branch() && dep_id.overridden() {
118                    return Some((path, dep_id));
119                }
120                None
121            })
122            .collect::<Vec<_>>();
123
124        for (path, dep_id) in branch_overrides {
125            let submodule_path = root.join(path);
126            let name = dep_id.name();
127
128            // Fetch and checkout the latest commit from the remote branch
129            Self::fetch_and_checkout_branch(&git, &submodule_path, name)?;
130
131            // Now get the updated revision after syncing with origin
132            let (updated_rev, _) = git.current_rev_branch(&submodule_path)?;
133
134            // Update the lockfile entry to reflect the latest commit
135            let prev = std::mem::replace(
136                dep_id,
137                DepIdentifier::Branch {
138                    name: name.to_string(),
139                    rev: updated_rev,
140                    r#override: true,
141                },
142            );
143
144            // Only insert if we don't already have a previous state for this path
145            // (e.g., from explicit overrides where we converted tag to branch)
146            if !prev_dep_ids.contains_key(path) {
147                prev_dep_ids.insert(path.to_owned(), prev);
148            }
149        }
150
151        // checkout the submodules at the correct tags
152        // Skip branches that were already updated above to avoid reverting to local branch
153        for (path, dep_id) in foundry_lock.iter() {
154            // Skip branches that were already updated
155            if dep_id.is_branch() && dep_id.overridden() {
156                continue;
157            }
158            git.checkout_at(dep_id.checkout_id(), &root.join(path))?;
159        }
160
161        if out_of_sync_deps.is_some_and(|o| !o.is_empty())
162            || foundry_lock.iter().any(|(_, dep_id)| dep_id.overridden())
163        {
164            foundry_lock.write()?;
165        }
166
167        // Print updates from => to
168        for (path, prev) in prev_dep_ids {
169            let curr = foundry_lock.get(&path).unwrap();
170            sh_println!(
171                "Updated dep at '{}', (from: {prev}, to: {curr})",
172                path.display().green(),
173                prev = prev,
174                curr = curr.yellow()
175            )?;
176        }
177
178        Ok(())
179    }
180
181    /// Returns the `lib/paths` of the dependencies that have been updated/overridden.
182    fn update_dep_paths(&self, foundry_lock: &Lockfile<'_>) -> Vec<PathBuf> {
183        foundry_lock
184            .iter()
185            .filter_map(|(path, dep_id)| {
186                if dep_id.overridden() {
187                    return Some(path.to_path_buf());
188                }
189                None
190            })
191            .collect()
192    }
193
194    /// Fetches and checks out the latest version of a branch from origin
195    fn fetch_and_checkout_branch(git: &Git<'_>, path: &Path, branch: &str) -> Result<()> {
196        // Fetch the latest changes from origin for the branch
197        git.cmd_at(path).args(["fetch", "origin", branch]).exec().wrap_err(format!(
198            "Could not fetch latest changes for branch {} in submodule at {}",
199            branch,
200            path.display()
201        ))?;
202
203        // Checkout and track the remote branch to ensure we have the latest commit
204        // Using checkout -B ensures the local branch tracks origin/branch
205        git.cmd_at(path)
206            .args(["checkout", "-B", branch, &format!("origin/{branch}")])
207            .exec()
208            .wrap_err(format!(
209                "Could not checkout and track origin/{} for submodule at {}",
210                branch,
211                path.display()
212            ))?;
213
214        Ok(())
215    }
216}
217
218/// Returns `(root, paths, overridden_deps_with_abosolute_paths)` where `root` is the root of the
219/// Git repository and `paths` are the relative paths of the dependencies.
220#[allow(clippy::type_complexity)]
221pub fn dependencies_paths(
222    deps: &[Dependency],
223    config: &Config,
224) -> Result<(PathBuf, Vec<PathBuf>, HashMap<PathBuf, String>)> {
225    let git_root = Git::root_of(&config.root)?;
226    let libs = config.install_lib_dir();
227
228    if deps.is_empty() {
229        return Ok((git_root, Vec::new(), HashMap::default()));
230    }
231
232    let mut paths = Vec::with_capacity(deps.len());
233    let mut overrides = HashMap::with_capacity_and_hasher(deps.len(), Default::default());
234    for dep in deps {
235        let name = dep.name();
236        let dep_path = libs.join(name);
237        if !dep_path.exists() {
238            eyre::bail!("Could not find dependency {name:?} in {}", dep_path.display());
239        }
240        let rel_path = dep_path
241            .strip_prefix(&git_root)
242            .wrap_err("Library directory is not relative to the repository root")?;
243
244        if let Some(tag) = &dep.tag {
245            overrides.insert(dep_path.to_owned(), tag.to_owned());
246        }
247        paths.push(rel_path.to_owned());
248    }
249    Ok((git_root, paths, overrides))
250}