forge/
lockfile.rs

1//! foundry.lock handler type.
2
3use std::{
4    collections::hash_map::Entry,
5    path::{Path, PathBuf},
6};
7
8use alloy_primitives::map::HashMap;
9use eyre::{OptionExt, Result};
10use foundry_cli::utils::Git;
11use serde::{Deserialize, Serialize};
12
13pub const FOUNDRY_LOCK: &str = "foundry.lock";
14
15/// A type alias for a HashMap of dependencies keyed by relative path to the submodule dir.
16pub type DepMap = HashMap<PathBuf, DepIdentifier>;
17
18/// A lockfile handler that keeps track of the dependencies and their current state.
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct Lockfile<'a> {
21    /// A map of the dependencies keyed by relative path to the submodule dir.
22    #[serde(flatten)]
23    deps: DepMap,
24    /// This is optional to handle no-git scencarios.
25    #[serde(skip)]
26    git: Option<&'a Git<'a>>,
27    /// Absolute path to the lockfile.
28    #[serde(skip)]
29    lockfile_path: PathBuf,
30}
31
32impl<'a> Lockfile<'a> {
33    /// Create a new [`Lockfile`] instance.
34    ///
35    /// `project_root` is the absolute path to the project root.
36    ///
37    /// You will need to call [`Lockfile::read`] or [`Lockfile::sync`] to load the lockfile.
38    pub fn new(project_root: &Path) -> Self {
39        Self { deps: HashMap::default(), git: None, lockfile_path: project_root.join(FOUNDRY_LOCK) }
40    }
41
42    /// Set the git instance to be used for submodule operations.
43    pub fn with_git(mut self, git: &'a Git<'_>) -> Self {
44        self.git = Some(git);
45        self
46    }
47
48    /// Sync the foundry.lock file with the current state of `git submodules`.
49    ///
50    /// If the lockfile and git submodules are out of sync, it returns a [`DepMap`] consisting of
51    /// _only_ the out-of-sync dependencies.
52    ///
53    /// This method writes the lockfile to project root if:
54    /// - The lockfile does not exist.
55    /// - The lockfile is out of sync with the git submodules.
56    pub fn sync(&mut self, lib: &Path) -> Result<Option<DepMap>> {
57        match self.read() {
58            Ok(_) => {}
59            Err(e) => {
60                if !e.to_string().contains("Lockfile not found") {
61                    return Err(e);
62                }
63            }
64        }
65
66        if let Some(git) = &self.git {
67            let submodules = git.submodules()?;
68
69            if submodules.is_empty() {
70                trace!("No submodules found. Skipping sync.");
71                return Ok(None);
72            }
73
74            let modules_with_branch = git
75                .read_submodules_with_branch(&Git::root_of(git.root)?, lib.file_name().unwrap())?;
76
77            let mut out_of_sync: DepMap = HashMap::default();
78            for sub in &submodules {
79                let rel_path = sub.path();
80                let rev = sub.rev();
81
82                let entry = self.deps.entry(rel_path.to_path_buf());
83
84                match entry {
85                    Entry::Occupied(e) => {
86                        if e.get().rev() != rev {
87                            out_of_sync.insert(rel_path.to_path_buf(), e.get().clone());
88                        }
89                    }
90                    Entry::Vacant(e) => {
91                        // Check if there is branch specified for the submodule at rel_path in
92                        // .gitmodules
93                        let maybe_branch = modules_with_branch.get(rel_path).map(|b| b.to_string());
94
95                        trace!(?maybe_branch, submodule = ?rel_path, "submodule branch");
96                        if let Some(branch) = maybe_branch {
97                            let dep_id = DepIdentifier::Branch {
98                                name: branch,
99                                rev: rev.to_string(),
100                                r#override: false,
101                            };
102                            e.insert(dep_id.clone());
103                            out_of_sync.insert(rel_path.to_path_buf(), dep_id);
104                            continue;
105                        }
106
107                        let dep_id = DepIdentifier::Rev { rev: rev.to_string(), r#override: false };
108                        trace!(submodule=?rel_path, ?dep_id, "submodule dep_id");
109                        e.insert(dep_id.clone());
110                        out_of_sync.insert(rel_path.to_path_buf(), dep_id);
111                    }
112                }
113            }
114
115            return Ok(if out_of_sync.is_empty() { None } else { Some(out_of_sync) });
116        }
117
118        Ok(None)
119    }
120
121    /// Loads the lockfile from the project root.
122    ///
123    /// Throws an error if the lockfile does not exist.
124    pub fn read(&mut self) -> Result<()> {
125        if !self.lockfile_path.exists() {
126            return Err(eyre::eyre!("Lockfile not found at {}", self.lockfile_path.display()));
127        }
128
129        let lockfile_str = foundry_common::fs::read_to_string(&self.lockfile_path)?;
130
131        self.deps = serde_json::from_str(&lockfile_str)?;
132
133        trace!(lockfile = ?self.deps, "loaded lockfile");
134
135        Ok(())
136    }
137
138    /// Writes the lockfile to the project root.
139    pub fn write(&self) -> Result<()> {
140        foundry_common::fs::write_pretty_json_file(&self.lockfile_path, &self.deps)?;
141        trace!(at= ?self.lockfile_path, "wrote lockfile");
142
143        Ok(())
144    }
145
146    /// Insert a dependency into the lockfile.
147    /// If the dependency already exists, it will be updated.
148    ///
149    /// Note: This does not write the updated lockfile to disk, only inserts the dep in-memory.
150    pub fn insert(&mut self, path: PathBuf, dep_id: DepIdentifier) {
151        self.deps.insert(path, dep_id);
152    }
153
154    /// Get the [`DepIdentifier`] for a submodule at a given path.
155    pub fn get(&self, path: &Path) -> Option<&DepIdentifier> {
156        self.deps.get(path)
157    }
158
159    /// Removes a dependency from the lockfile.
160    ///
161    /// Note: This does not write the updated lockfile to disk, only removes the dep in-memory.
162    pub fn remove(&mut self, path: &Path) -> Option<DepIdentifier> {
163        self.deps.remove(path)
164    }
165
166    /// Override a dependency in the lockfile.
167    ///
168    /// Returns the overridden/previous [`DepIdentifier`].
169    /// This is used in `forge update` to decide whether a dep's tag/branch/rev should be updated.
170    ///
171    /// Throws an error if the dependency is not found in the lockfile.
172    pub fn override_dep(
173        &mut self,
174        dep: &Path,
175        mut new_dep_id: DepIdentifier,
176    ) -> Result<DepIdentifier> {
177        let prev = self
178            .deps
179            .get_mut(dep)
180            .map(|d| {
181                new_dep_id.mark_overide();
182                std::mem::replace(d, new_dep_id)
183            })
184            .ok_or_eyre(format!("Dependency not found in lockfile: {}", dep.display()))?;
185
186        Ok(prev)
187    }
188
189    /// Returns the num of dependencies in the lockfile.
190    pub fn len(&self) -> usize {
191        self.deps.len()
192    }
193
194    /// Returns whether the lockfile is empty.
195    pub fn is_empty(&self) -> bool {
196        self.deps.is_empty()
197    }
198
199    /// Returns an iterator over the lockfile.
200    pub fn iter(&self) -> impl Iterator<Item = (&PathBuf, &DepIdentifier)> {
201        self.deps.iter()
202    }
203
204    /// Returns an mutable iterator over the lockfile.
205    pub fn iter_mut(&mut self) -> impl Iterator<Item = (&PathBuf, &mut DepIdentifier)> {
206        self.deps.iter_mut()
207    }
208
209    pub fn exists(&self) -> bool {
210        self.lockfile_path.exists()
211    }
212}
213
214// Implement .iter() for &LockFile
215
216/// Identifies whether a dependency (submodule) is referenced by a branch,
217/// tag or rev (commit hash).
218///
219/// Each enum variant consists of an `r#override` flag which is used in `forge update` to decide
220/// whether to update a dep or not. This flag is skipped during serialization.
221#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
222pub enum DepIdentifier {
223    /// `name` of the branch and the `rev`  it is currently pointing to.
224    /// Running `forge update`, will update the `name` branch to the latest `rev`.
225    #[serde(rename = "branch")]
226    Branch {
227        name: String,
228        rev: String,
229        #[serde(skip)]
230        r#override: bool,
231    },
232    /// Release tag `name` and the `rev` it is currently pointing to.
233    /// Running `forge update` does not update the tag/rev.
234    /// Dependency will remain pinned to the existing tag/rev unless r#override like so `forge
235    /// update owner/dep@tag=diffent_tag`.
236    #[serde(rename = "tag")]
237    Tag {
238        name: String,
239        rev: String,
240        #[serde(skip)]
241        r#override: bool,
242    },
243    /// Commit hash `rev` the submodule is currently pointing to.
244    /// Running `forge update` does not update the rev.
245    /// Dependency will remain pinned to the existing rev unless r#override.
246    #[serde(rename = "rev", untagged)]
247    Rev {
248        rev: String,
249        #[serde(skip)]
250        r#override: bool,
251    },
252}
253
254impl DepIdentifier {
255    /// Resolves the [`DepIdentifier`] for a submodule at a given path.
256    /// `lib_path` is the absolute path to the submodule.
257    pub fn resolve_type(git: &Git<'_>, lib_path: &Path, s: &str) -> Result<Self> {
258        trace!(lib_path = ?lib_path, resolving_type = ?s, "resolving submodule identifier");
259        // Get the tags for the submodule
260        if git.has_tag(s, lib_path)? {
261            let rev = git.get_rev(s, lib_path)?;
262            return Ok(Self::Tag { name: String::from(s), rev, r#override: false });
263        }
264
265        if git.has_branch(s, lib_path)? {
266            let rev = git.get_rev(s, lib_path)?;
267            return Ok(Self::Branch { name: String::from(s), rev, r#override: false });
268        }
269
270        if git.has_rev(s, lib_path)? {
271            return Ok(Self::Rev { rev: String::from(s), r#override: false });
272        }
273
274        Err(eyre::eyre!("Could not resolve tag type for submodule at path {}", lib_path.display()))
275    }
276
277    /// Get the commit hash of the dependency.
278    pub fn rev(&self) -> &str {
279        match self {
280            Self::Branch { rev, .. } => rev,
281            Self::Tag { rev, .. } => rev,
282            Self::Rev { rev, .. } => rev,
283        }
284    }
285
286    /// Get the name of the dependency.
287    ///
288    /// In case of a Rev, this will return the commit hash.
289    pub fn name(&self) -> &str {
290        match self {
291            Self::Branch { name, .. } => name,
292            Self::Tag { name, .. } => name,
293            Self::Rev { rev, .. } => rev,
294        }
295    }
296
297    /// Get the name/rev to checkout at.
298    pub fn checkout_id(&self) -> &str {
299        match self {
300            Self::Branch { name, .. } => name,
301            Self::Tag { name, .. } => name,
302            Self::Rev { rev, .. } => rev,
303        }
304    }
305
306    /// Marks as dependency as overridden.
307    pub fn mark_overide(&mut self) {
308        match self {
309            Self::Branch { r#override, .. } => *r#override = true,
310            Self::Tag { r#override, .. } => *r#override = true,
311            Self::Rev { r#override, .. } => *r#override = true,
312        }
313    }
314
315    /// Returns whether the dependency has been overridden.
316    pub fn overridden(&self) -> bool {
317        match self {
318            Self::Branch { r#override, .. } => *r#override,
319            Self::Tag { r#override, .. } => *r#override,
320            Self::Rev { r#override, .. } => *r#override,
321        }
322    }
323
324    /// Returns whether the dependency is a branch.
325    pub fn is_branch(&self) -> bool {
326        matches!(self, Self::Branch { .. })
327    }
328}
329
330impl std::fmt::Display for DepIdentifier {
331    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
332        match self {
333            Self::Branch { name, rev, .. } => write!(f, "branch={name}@{rev}"),
334            Self::Tag { name, rev, .. } => write!(f, "tag={name}@{rev}"),
335            Self::Rev { rev, .. } => write!(f, "rev={rev}"),
336        }
337    }
338}
339
340#[cfg(test)]
341mod tests {
342    use super::*;
343
344    #[test]
345    fn serde_dep_identifier() {
346        let branch = DepIdentifier::Branch {
347            name: "main".to_string(),
348            rev: "b7954c3e9ce1d487b49489f5800f52f4b77b7351".to_string(),
349            r#override: false,
350        };
351
352        let tag = DepIdentifier::Tag {
353            name: "v0.1.0".to_string(),
354            rev: "b7954c3e9ce1d487b49489f5800f52f4b77b7351".to_string(),
355            r#override: false,
356        };
357
358        let rev = DepIdentifier::Rev {
359            rev: "b7954c3e9ce1d487b49489f5800f52f4b77b7351".to_string(),
360            r#override: false,
361        };
362
363        let branch_str = serde_json::to_string(&branch).unwrap();
364        let tag_str = serde_json::to_string(&tag).unwrap();
365        let rev_str = serde_json::to_string(&rev).unwrap();
366
367        assert_eq!(
368            branch_str,
369            r#"{"branch":{"name":"main","rev":"b7954c3e9ce1d487b49489f5800f52f4b77b7351"}}"#
370        );
371        assert_eq!(
372            tag_str,
373            r#"{"tag":{"name":"v0.1.0","rev":"b7954c3e9ce1d487b49489f5800f52f4b77b7351"}}"#
374        );
375        assert_eq!(rev_str, r#"{"rev":"b7954c3e9ce1d487b49489f5800f52f4b77b7351"}"#);
376
377        let branch_de: DepIdentifier = serde_json::from_str(&branch_str).unwrap();
378        let tag_de: DepIdentifier = serde_json::from_str(&tag_str).unwrap();
379        let rev_de: DepIdentifier = serde_json::from_str(&rev_str).unwrap();
380
381        assert_eq!(branch, branch_de);
382        assert_eq!(tag, tag_de);
383        assert_eq!(rev, rev_de);
384    }
385}