1use 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
14pub type DepMap = HashMap<PathBuf, DepIdentifier>;
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct Lockfile<'a> {
20 #[serde(flatten)]
22 deps: DepMap,
23 #[serde(skip)]
25 git: Option<&'a Git<'a>>,
26 #[serde(skip)]
28 lockfile_path: PathBuf,
29}
30
31impl<'a> Lockfile<'a> {
32 pub fn new(project_root: &Path) -> Self {
38 Self { deps: HashMap::default(), git: None, lockfile_path: project_root.join(FOUNDRY_LOCK) }
39 }
40
41 pub fn with_git(mut self, git: &'a Git<'_>) -> Self {
43 self.git = Some(git);
44 self
45 }
46
47 pub fn sync(&mut self, lib: &Path) -> Result<Option<DepMap>> {
56 match self.read() {
57 Ok(_) => {}
58 Err(e) => {
59 if !e.to_string().contains("Lockfile not found") {
60 return Err(e);
61 }
62 }
63 }
64
65 if let Some(git) = &self.git {
66 let submodules = git.submodules()?;
67
68 if submodules.is_empty() {
69 trace!("No submodules found. Skipping sync.");
70 return Ok(None);
71 }
72
73 let modules_with_branch = git
74 .read_submodules_with_branch(&Git::root_of(git.root)?, lib.file_name().unwrap())?;
75
76 let mut out_of_sync: DepMap = HashMap::default();
77 for sub in &submodules {
78 let rel_path = sub.path();
79 let rev = sub.rev();
80
81 let entry = self.deps.entry(rel_path.to_path_buf());
82
83 match entry {
84 Entry::Occupied(e) => {
85 if e.get().rev() != rev {
86 out_of_sync.insert(rel_path.to_path_buf(), e.get().clone());
87 }
88 }
89 Entry::Vacant(e) => {
90 let maybe_branch = modules_with_branch.get(rel_path).map(|b| b.to_string());
93
94 trace!(?maybe_branch, submodule = ?rel_path, "submodule branch");
95 if let Some(branch) = maybe_branch {
96 let dep_id = DepIdentifier::Branch {
97 name: branch,
98 rev: rev.to_string(),
99 r#override: false,
100 };
101 e.insert(dep_id.clone());
102 out_of_sync.insert(rel_path.to_path_buf(), dep_id);
103 continue;
104 }
105
106 let dep_id = DepIdentifier::Rev { rev: rev.to_string(), r#override: false };
107 trace!(submodule=?rel_path, ?dep_id, "submodule dep_id");
108 e.insert(dep_id.clone());
109 out_of_sync.insert(rel_path.to_path_buf(), dep_id);
110 }
111 }
112 }
113
114 return Ok(if out_of_sync.is_empty() { None } else { Some(out_of_sync) });
115 }
116
117 Ok(None)
118 }
119
120 pub fn read(&mut self) -> Result<()> {
124 if !self.lockfile_path.exists() {
125 return Err(eyre::eyre!("Lockfile not found at {}", self.lockfile_path.display()));
126 }
127
128 let lockfile_str = foundry_common::fs::read_to_string(&self.lockfile_path)?;
129
130 self.deps = serde_json::from_str(&lockfile_str)?;
131
132 trace!(lockfile = ?self.deps, "loaded lockfile");
133
134 Ok(())
135 }
136
137 pub fn write(&self) -> Result<()> {
139 let ordered_deps: BTreeMap<_, _> = self.deps.clone().into_iter().collect();
140 foundry_common::fs::write_pretty_json_file(&self.lockfile_path, &ordered_deps)?;
141 trace!(at= ?self.lockfile_path, "wrote lockfile");
142
143 Ok(())
144 }
145
146 pub fn insert(&mut self, path: PathBuf, dep_id: DepIdentifier) {
151 self.deps.insert(path, dep_id);
152 }
153
154 pub fn get(&self, path: &Path) -> Option<&DepIdentifier> {
156 self.deps.get(path)
157 }
158
159 pub fn remove(&mut self, path: &Path) -> Option<DepIdentifier> {
163 self.deps.remove(path)
164 }
165
166 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_override();
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 pub fn len(&self) -> usize {
191 self.deps.len()
192 }
193
194 pub fn is_empty(&self) -> bool {
196 self.deps.is_empty()
197 }
198
199 pub fn iter(&self) -> impl Iterator<Item = (&PathBuf, &DepIdentifier)> {
201 self.deps.iter()
202 }
203
204 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#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
222pub enum DepIdentifier {
223 #[serde(rename = "branch")]
226 Branch {
227 name: String,
228 rev: String,
229 #[serde(skip)]
230 r#override: bool,
231 },
232 #[serde(rename = "tag")]
237 Tag {
238 name: String,
239 rev: String,
240 #[serde(skip)]
241 r#override: bool,
242 },
243 #[serde(rename = "rev", untagged)]
247 Rev {
248 rev: String,
249 #[serde(skip)]
250 r#override: bool,
251 },
252}
253
254impl DepIdentifier {
255 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 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 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 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 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 pub fn mark_override(&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 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 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 use std::fs;
344 use tempfile::tempdir;
345
346 #[test]
347 fn serde_dep_identifier() {
348 let branch = DepIdentifier::Branch {
349 name: "main".to_string(),
350 rev: "b7954c3e9ce1d487b49489f5800f52f4b77b7351".to_string(),
351 r#override: false,
352 };
353
354 let tag = DepIdentifier::Tag {
355 name: "v0.1.0".to_string(),
356 rev: "b7954c3e9ce1d487b49489f5800f52f4b77b7351".to_string(),
357 r#override: false,
358 };
359
360 let rev = DepIdentifier::Rev {
361 rev: "b7954c3e9ce1d487b49489f5800f52f4b77b7351".to_string(),
362 r#override: false,
363 };
364
365 let branch_str = serde_json::to_string(&branch).unwrap();
366 let tag_str = serde_json::to_string(&tag).unwrap();
367 let rev_str = serde_json::to_string(&rev).unwrap();
368
369 assert_eq!(
370 branch_str,
371 r#"{"branch":{"name":"main","rev":"b7954c3e9ce1d487b49489f5800f52f4b77b7351"}}"#
372 );
373 assert_eq!(
374 tag_str,
375 r#"{"tag":{"name":"v0.1.0","rev":"b7954c3e9ce1d487b49489f5800f52f4b77b7351"}}"#
376 );
377 assert_eq!(rev_str, r#"{"rev":"b7954c3e9ce1d487b49489f5800f52f4b77b7351"}"#);
378
379 let branch_de: DepIdentifier = serde_json::from_str(&branch_str).unwrap();
380 let tag_de: DepIdentifier = serde_json::from_str(&tag_str).unwrap();
381 let rev_de: DepIdentifier = serde_json::from_str(&rev_str).unwrap();
382
383 assert_eq!(branch, branch_de);
384 assert_eq!(tag, tag_de);
385 assert_eq!(rev, rev_de);
386 }
387
388 #[test]
389 fn test_write_ordered_deps() {
390 let dir = tempdir().unwrap();
391 let mut lockfile = Lockfile::new(dir.path());
392 lockfile.insert(
393 PathBuf::from("z_dep"),
394 DepIdentifier::Rev { rev: "3".to_string(), r#override: false },
395 );
396 lockfile.insert(
397 PathBuf::from("a_dep"),
398 DepIdentifier::Rev { rev: "1".to_string(), r#override: false },
399 );
400 lockfile.insert(
401 PathBuf::from("c_dep"),
402 DepIdentifier::Rev { rev: "2".to_string(), r#override: false },
403 );
404 let _ = lockfile.write();
405 let contents = fs::read_to_string(lockfile.lockfile_path).unwrap();
406 let expected = r#"{
407 "a_dep": {
408 "rev": "1"
409 },
410 "c_dep": {
411 "rev": "2"
412 },
413 "z_dep": {
414 "rev": "3"
415 }
416}"#;
417 assert_eq!(contents.trim(), expected.trim());
418
419 let mut lockfile = Lockfile::new(dir.path());
420 lockfile.read().unwrap();
421 lockfile.insert(
422 PathBuf::from("x_dep"),
423 DepIdentifier::Rev { rev: "4".to_string(), r#override: false },
424 );
425 let _ = lockfile.write();
426 let contents = fs::read_to_string(lockfile.lockfile_path).unwrap();
427 let expected = r#"{
428 "a_dep": {
429 "rev": "1"
430 },
431 "c_dep": {
432 "rev": "2"
433 },
434 "x_dep": {
435 "rev": "4"
436 },
437 "z_dep": {
438 "rev": "3"
439 }
440}"#;
441 assert_eq!(contents.trim(), expected.trim());
442 }
443}