1use 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
15pub type DepMap = HashMap<PathBuf, DepIdentifier>;
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct Lockfile<'a> {
21 #[serde(flatten)]
23 deps: DepMap,
24 #[serde(skip)]
26 git: Option<&'a Git<'a>>,
27 #[serde(skip)]
29 lockfile_path: PathBuf,
30}
31
32impl<'a> Lockfile<'a> {
33 pub fn new(project_root: &Path) -> Self {
39 Self { deps: HashMap::default(), git: None, lockfile_path: project_root.join(FOUNDRY_LOCK) }
40 }
41
42 pub fn with_git(mut self, git: &'a Git<'_>) -> Self {
44 self.git = Some(git);
45 self
46 }
47
48 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 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 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 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 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_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 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_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 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
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}