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) 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 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 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 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 pub fn insert(&mut self, path: PathBuf, dep_id: DepIdentifier) {
149 self.deps.insert(path, dep_id);
150 }
151
152 pub fn get(&self, path: &Path) -> Option<&DepIdentifier> {
154 self.deps.get(path)
155 }
156
157 pub fn remove(&mut self, path: &Path) -> Option<DepIdentifier> {
161 self.deps.remove(path)
162 }
163
164 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 pub fn len(&self) -> usize {
189 self.deps.len()
190 }
191
192 pub fn is_empty(&self) -> bool {
194 self.deps.is_empty()
195 }
196
197 pub fn iter(&self) -> impl Iterator<Item = (&PathBuf, &DepIdentifier)> {
199 self.deps.iter()
200 }
201
202 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#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
220pub enum DepIdentifier {
221 #[serde(rename = "branch")]
224 Branch {
225 name: String,
226 rev: String,
227 #[serde(skip)]
228 r#override: bool,
229 },
230 #[serde(rename = "tag")]
235 Tag {
236 name: String,
237 rev: String,
238 #[serde(skip)]
239 r#override: bool,
240 },
241 #[serde(rename = "rev", untagged)]
245 Rev {
246 rev: String,
247 #[serde(skip)]
248 r#override: bool,
249 },
250}
251
252impl DepIdentifier {
253 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 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 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 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 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 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 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 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}