Skip to main content

forge/
lockfile.rs

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