1use alloy_json_abi::JsonAbi;
2use alloy_primitives::{Address, U256, map::HashMap};
3use alloy_provider::{Network, Provider, RootProvider, network::AnyNetwork};
4use eyre::{ContextCompat, Result};
5use foundry_common::{provider::ProviderBuilder, shell};
6use foundry_config::{Chain, Config};
7use itertools::Itertools;
8use path_slash::PathExt;
9use regex::Regex;
10use serde::de::DeserializeOwned;
11use std::{
12 collections::{BTreeMap, BTreeSet},
13 ffi::{OsStr, OsString},
14 path::{Path, PathBuf},
15 process::{Command, Output, Stdio},
16 str::FromStr,
17 sync::{LazyLock, OnceLock},
18 time::{Duration, SystemTime, UNIX_EPOCH},
19};
20use tracing_subscriber::{EnvFilter, prelude::*, reload};
21
22mod cmd;
23pub use cmd::*;
24
25mod suggestions;
26pub use suggestions::*;
27
28mod abi;
29pub use abi::*;
30
31mod allocator;
32pub use allocator::*;
33
34mod tempo;
35pub use tempo::*;
36
37#[doc(hidden)]
39pub use foundry_config::utils::*;
40
41pub const STATIC_FUZZ_SEED: [u8; 32] = [
45 0x01, 0x00, 0xfa, 0x69, 0xa5, 0xf1, 0x71, 0x0a, 0x95, 0xcd, 0xef, 0x94, 0x88, 0x9b, 0x02, 0x84,
46 0x5d, 0x64, 0x0b, 0x19, 0xad, 0xf0, 0xe3, 0x57, 0xb8, 0xd4, 0xbe, 0x7d, 0x49, 0xee, 0x70, 0xe6,
47];
48
49pub static SUBMODULE_BRANCH_REGEX: LazyLock<Regex> =
51 LazyLock::new(|| Regex::new(r#"\[submodule "([^"]+)"\](?:[^\[]*?branch = ([^\s]+))"#).unwrap());
52pub static SUBMODULE_STATUS_REGEX: LazyLock<Regex> =
54 LazyLock::new(|| Regex::new(r"^[\s+-]?([a-f0-9]+)\s+([^\s]+)(?:\s+\([^)]+\))?$").unwrap());
55
56static FILTER_RELOAD_HANDLE: OnceLock<reload::Handle<EnvFilter, tracing_subscriber::Registry>> =
58 OnceLock::new();
59
60pub trait FoundryPathExt {
62 fn is_sol_test(&self) -> bool;
64
65 fn is_sol(&self) -> bool;
67
68 fn is_yul(&self) -> bool;
70}
71
72impl<T: AsRef<Path>> FoundryPathExt for T {
73 fn is_sol_test(&self) -> bool {
74 self.as_ref()
75 .file_name()
76 .and_then(|s| s.to_str())
77 .map(|s| s.ends_with(".t.sol"))
78 .unwrap_or_default()
79 }
80
81 fn is_sol(&self) -> bool {
82 self.as_ref().extension() == Some(std::ffi::OsStr::new("sol"))
83 }
84
85 fn is_yul(&self) -> bool {
86 self.as_ref().extension() == Some(std::ffi::OsStr::new("yul"))
87 }
88}
89
90pub fn subscriber() {
95 let (filter_layer, reload_handle) = reload::Layer::new(env_filter());
96 let registry = tracing_subscriber::Registry::default().with(filter_layer);
97 #[cfg(feature = "tracy")]
98 let registry = registry.with(tracing_tracy::TracyLayer::default());
99 registry.with(tracing_subscriber::fmt::layer().with_writer(std::io::stderr)).init();
100 let _ = FILTER_RELOAD_HANDLE.set(reload_handle);
101}
102
103pub fn update_tracing_filter(directives: &str) {
108 let Some(handle) = FILTER_RELOAD_HANDLE.get() else {
109 return;
110 };
111 let Ok(new_filter) = directives.parse::<EnvFilter>() else {
112 return;
113 };
114 let _ = handle.reload(new_filter);
115}
116
117fn env_filter() -> EnvFilter {
118 const DEFAULT_DIRECTIVES: &[&str] = &include!("./default_directives.txt");
119 let mut filter = EnvFilter::from_default_env();
120 for &directive in DEFAULT_DIRECTIVES {
121 filter = filter.add_directive(directive.parse().unwrap());
122 }
123 filter
124}
125
126pub fn get_provider(config: &Config) -> Result<RootProvider<AnyNetwork>> {
128 get_provider_builder(config)?.build()
129}
130
131pub fn get_provider_builder(config: &Config) -> Result<ProviderBuilder> {
135 ProviderBuilder::from_config(config)
136}
137
138pub async fn get_chain<N, P>(chain: Option<Chain>, provider: P) -> Result<Chain>
139where
140 N: Network,
141 P: Provider<N>,
142{
143 match chain {
144 Some(chain) => Ok(chain),
145 None => Ok(Chain::from_id(provider.get_chain_id().await?)),
146 }
147}
148
149pub fn parse_ether_value(value: &str) -> Result<U256> {
156 Ok(if value.starts_with("0x") || value.starts_with("0X") {
157 U256::from_str(value)?
158 } else {
159 alloy_dyn_abi::DynSolType::coerce_str(&alloy_dyn_abi::DynSolType::Uint(256), value)?
160 .as_uint()
161 .wrap_err("Could not parse ether value from string")?
162 .0
163 })
164}
165
166pub fn parse_json<T: DeserializeOwned>(value: &str) -> serde_json::Result<T> {
168 serde_json::from_str(value)
169}
170
171pub fn parse_delay(delay: &str) -> Result<Duration> {
173 let delay = if delay.ends_with("ms") {
174 let d: u64 = delay.trim_end_matches("ms").parse()?;
175 Duration::from_millis(d)
176 } else {
177 let d: f64 = delay.parse()?;
178 let delay = (d * 1000.0).round();
179 if delay.is_infinite() || delay.is_nan() || delay.is_sign_negative() {
180 eyre::bail!("delay must be finite and non-negative");
181 }
182
183 Duration::from_millis(delay as u64)
184 };
185 Ok(delay)
186}
187
188pub fn now() -> Duration {
190 SystemTime::now().duration_since(UNIX_EPOCH).expect("time went backwards")
191}
192
193pub fn common_setup() {
195 install_crypto_provider();
196 crate::handler::install();
197 load_dotenv();
198 enable_paint();
199}
200
201pub fn load_dotenv() {
209 let load = |p: &Path| {
210 dotenvy::from_path(p.join(".env")).ok();
211 };
212
213 if let (Ok(cwd), Ok(prj_root)) = (std::env::current_dir(), find_project_root(None)) {
217 load(&prj_root);
218 if cwd != prj_root {
219 load(&cwd);
221 }
222 };
223}
224
225pub fn enable_paint() {
227 let enable = yansi::Condition::os_support() && yansi::Condition::tty_and_color_live();
228 yansi::whenever(yansi::Condition::cached(enable));
229}
230
231pub fn install_crypto_provider() {
242 rustls::crypto::ring::default_provider()
244 .install_default()
245 .expect("Failed to install default rustls crypto provider");
246}
247
248pub async fn fetch_abi_from_etherscan(
250 address: Address,
251 config: &foundry_config::Config,
252) -> Result<Vec<(JsonAbi, String)>> {
253 let chain = config.chain.unwrap_or_default();
254 let client = config
255 .get_etherscan_config_with_chain(Some(chain))?
256 .ok_or_else(|| eyre::eyre!("No Etherscan API key configured for chain {chain}"))?
257 .into_client_with_no_proxy(config.eth_rpc_no_proxy)?;
258 let source = client.contract_source_code(address).await?;
259 source.items.into_iter().map(|item| Ok((item.abi()?, item.contract_name))).collect()
260}
261
262pub trait CommandUtils {
264 fn exec(&mut self) -> Result<Output>;
266
267 fn get_stdout_lossy(&mut self) -> Result<String>;
269}
270
271impl CommandUtils for Command {
272 #[track_caller]
273 fn exec(&mut self) -> Result<Output> {
274 trace!(command=?self, "executing");
275
276 let output = self.output()?;
277
278 trace!(code=?output.status.code(), ?output);
279
280 if output.status.success() {
281 Ok(output)
282 } else {
283 let stdout = String::from_utf8_lossy(&output.stdout);
284 let stdout = stdout.trim();
285 let stderr = String::from_utf8_lossy(&output.stderr);
286 let stderr = stderr.trim();
287 let msg = if stdout.is_empty() {
288 stderr.to_string()
289 } else if stderr.is_empty() {
290 stdout.to_string()
291 } else {
292 format!("stdout:\n{stdout}\n\nstderr:\n{stderr}")
293 };
294
295 let mut name = self.get_program().to_string_lossy();
296 if let Some(arg) = self.get_args().next() {
297 let arg = arg.to_string_lossy();
298 if !arg.starts_with('-') {
299 let name = name.to_mut();
300 name.push(' ');
301 name.push_str(&arg);
302 }
303 }
304
305 let mut err = match output.status.code() {
306 Some(code) => format!("{name} exited with code {code}"),
307 None => format!("{name} terminated by a signal"),
308 };
309 if !msg.is_empty() {
310 err.push(':');
311 err.push(if msg.lines().count() == 1 { ' ' } else { '\n' });
312 err.push_str(&msg);
313 }
314 Err(eyre::eyre!(err))
315 }
316 }
317
318 #[track_caller]
319 fn get_stdout_lossy(&mut self) -> Result<String> {
320 let output = self.exec()?;
321 let stdout = String::from_utf8_lossy(&output.stdout);
322 Ok(stdout.trim().into())
323 }
324}
325
326#[derive(Clone, Copy, Debug)]
327pub struct Git<'a> {
328 pub root: &'a Path,
329 pub quiet: bool,
330 pub shallow: bool,
331}
332
333impl<'a> Git<'a> {
334 pub fn new(root: &'a Path) -> Self {
335 Self { root, quiet: shell::is_quiet(), shallow: false }
336 }
337
338 pub fn from_config(config: &'a Config) -> Self {
339 Self::new(config.root.as_path())
340 }
341
342 pub fn root_of(relative_to: &Path) -> Result<PathBuf> {
343 let output = Self::cmd_no_root()
344 .current_dir(relative_to)
345 .args(["rev-parse", "--show-toplevel"])
346 .get_stdout_lossy()?;
347 Ok(PathBuf::from(output))
348 }
349
350 pub fn clone_with_branch(
351 shallow: bool,
352 from: impl AsRef<OsStr>,
353 branch: impl AsRef<OsStr>,
354 to: Option<impl AsRef<OsStr>>,
355 ) -> Result<()> {
356 Self::cmd_no_root()
357 .stderr(Stdio::inherit())
358 .args(["clone", "--recurse-submodules"])
359 .args(shallow.then_some("--depth=1"))
360 .args(shallow.then_some("--shallow-submodules"))
361 .arg("-b")
362 .arg(branch)
363 .arg(from)
364 .args(to)
365 .exec()
366 .map(drop)
367 }
368
369 pub fn clone(
370 shallow: bool,
371 from: impl AsRef<OsStr>,
372 to: Option<impl AsRef<OsStr>>,
373 ) -> Result<()> {
374 Self::cmd_no_root()
375 .stderr(Stdio::inherit())
376 .args(["clone", "--recurse-submodules"])
377 .args(shallow.then_some("--depth=1"))
378 .args(shallow.then_some("--shallow-submodules"))
379 .arg(from)
380 .args(to)
381 .exec()
382 .map(drop)
383 }
384
385 pub fn fetch(
386 self,
387 shallow: bool,
388 remote: impl AsRef<OsStr>,
389 branch: Option<impl AsRef<OsStr>>,
390 ) -> Result<()> {
391 self.cmd()
392 .stderr(Stdio::inherit())
393 .arg("fetch")
394 .args(shallow.then_some("--no-tags"))
395 .args(shallow.then_some("--depth=1"))
396 .arg(remote)
397 .args(branch)
398 .exec()
399 .map(drop)
400 }
401
402 pub const fn root(self, root: &Path) -> Git<'_> {
403 Git { root, ..self }
404 }
405
406 pub const fn quiet(self, quiet: bool) -> Self {
407 Self { quiet, ..self }
408 }
409
410 pub const fn shallow(self, shallow: bool) -> Self {
412 Self { shallow, ..self }
413 }
414
415 pub fn checkout(self, recursive: bool, tag: impl AsRef<OsStr>) -> Result<()> {
416 self.cmd()
417 .arg("checkout")
418 .args(recursive.then_some("--recurse-submodules"))
419 .arg(tag)
420 .exec()
421 .map(drop)
422 }
423
424 pub fn head(self) -> Result<String> {
426 self.cmd().args(["rev-parse", "HEAD"]).get_stdout_lossy()
427 }
428
429 pub fn checkout_at(self, tag: impl AsRef<OsStr>, at: &Path) -> Result<()> {
430 self.cmd_at(at).arg("checkout").arg(tag).exec().map(drop)
431 }
432
433 pub fn init(self) -> Result<()> {
434 self.cmd().arg("init").exec().map(drop)
435 }
436
437 pub fn current_rev_branch(self, at: &Path) -> Result<(String, String)> {
438 let rev = self.cmd_at(at).args(["rev-parse", "HEAD"]).get_stdout_lossy()?;
439 let branch =
440 self.cmd_at(at).args(["rev-parse", "--abbrev-ref", "HEAD"]).get_stdout_lossy()?;
441 Ok((rev, branch))
442 }
443
444 #[expect(clippy::should_implement_trait)] pub fn add<I, S>(self, paths: I) -> Result<()>
446 where
447 I: IntoIterator<Item = S>,
448 S: AsRef<OsStr>,
449 {
450 self.cmd().arg("add").args(paths).exec().map(drop)
451 }
452
453 pub fn add_literal(self, path: &Path) -> Result<()> {
454 self.cmd().args(["--literal-pathspecs", "add", "--"]).arg(path).exec().map(drop)
455 }
456
457 pub fn reset(self, hard: bool, tree: impl AsRef<OsStr>) -> Result<()> {
458 self.cmd().arg("reset").args(hard.then_some("--hard")).arg(tree).exec().map(drop)
459 }
460
461 pub fn commit_tree(
462 self,
463 tree: impl AsRef<OsStr>,
464 msg: Option<impl AsRef<OsStr>>,
465 ) -> Result<String> {
466 self.cmd()
467 .arg("commit-tree")
468 .arg(tree)
469 .args(msg.as_ref().is_some().then_some("-m"))
470 .args(msg)
471 .get_stdout_lossy()
472 }
473
474 pub fn rm<I, S>(self, force: bool, paths: I) -> Result<()>
475 where
476 I: IntoIterator<Item = S>,
477 S: AsRef<OsStr>,
478 {
479 self.cmd().arg("rm").args(force.then_some("--force")).args(paths).exec().map(drop)
480 }
481
482 pub fn remove_index_path(self, path: &Path) -> Result<()> {
483 self.cmd()
484 .args(["--literal-pathspecs", "rm", "--cached", "--force", "--"])
485 .arg(path)
486 .exec()
487 .map(drop)
488 }
489
490 pub fn commit(self, msg: &str) -> Result<()> {
491 let output = self
492 .cmd()
493 .args(["commit", "-m", msg])
494 .args(cfg!(any(test, debug_assertions)).then_some("--no-gpg-sign"))
495 .output()?;
496 if !output.status.success() {
497 let stdout = String::from_utf8_lossy(&output.stdout);
498 let stderr = String::from_utf8_lossy(&output.stderr);
499 let msg = "nothing to commit, working tree clean";
501 if !(stdout.contains(msg) || stderr.contains(msg)) {
502 return Err(eyre::eyre!(
503 "failed to commit (code={:?}, stdout={:?}, stderr={:?})",
504 output.status.code(),
505 stdout.trim(),
506 stderr.trim()
507 ));
508 }
509 }
510 Ok(())
511 }
512
513 pub fn is_in_repo(self) -> std::io::Result<bool> {
514 self.cmd().args(["rev-parse", "--is-inside-work-tree"]).status().map(|s| s.success())
515 }
516
517 pub fn is_repo_root(self) -> Result<bool> {
518 self.cmd().args(["rev-parse", "--show-cdup"]).get_stdout_lossy().map(|s| s.is_empty())
519 }
520
521 pub fn is_clean(self) -> Result<bool> {
522 self.cmd().args(["status", "--porcelain"]).exec().map(|out| out.stdout.is_empty())
523 }
524
525 pub fn is_path_clean(self, path: &Path) -> Result<bool> {
526 self.cmd()
527 .args(["--literal-pathspecs", "status", "--porcelain", "--"])
528 .arg(path)
529 .exec()
530 .map(|out| out.stdout.is_empty())
531 }
532
533 pub fn has_branch(self, branch: impl AsRef<OsStr>, at: &Path) -> Result<bool> {
534 self.cmd_at(at)
535 .args(["branch", "--list", "--no-color"])
536 .arg(branch)
537 .get_stdout_lossy()
538 .map(|stdout| !stdout.is_empty())
539 }
540
541 pub fn has_tag(self, tag: impl AsRef<OsStr>, at: &Path) -> Result<bool> {
542 self.cmd_at(at)
543 .args(["tag", "--list"])
544 .arg(tag)
545 .get_stdout_lossy()
546 .map(|stdout| !stdout.is_empty())
547 }
548
549 pub fn has_rev(self, rev: impl AsRef<OsStr>, at: &Path) -> Result<bool> {
550 self.cmd_at(at)
551 .args(["cat-file", "-t"])
552 .arg(rev)
553 .get_stdout_lossy()
554 .map(|stdout| &stdout == "commit")
555 }
556
557 pub fn get_rev(self, tag_or_branch: impl AsRef<OsStr>, at: &Path) -> Result<String> {
558 self.cmd_at(at).args(["rev-list", "-n", "1"]).arg(tag_or_branch).get_stdout_lossy()
559 }
560
561 pub fn ensure_clean(self) -> Result<()> {
562 if self.is_clean()? {
563 Ok(())
564 } else {
565 Err(eyre::eyre!(
566 "\
567The target directory is a part of or on its own an already initialized git repository,
568and it requires clean working and staging areas, including no untracked files.
569
570Check the current git repository's status with `git status`.
571Then, you can track files with `git add ...` and then commit them with `git commit`,
572ignore them in the `.gitignore` file."
573 ))
574 }
575 }
576
577 pub fn commit_hash(self, short: bool, revision: &str) -> Result<String> {
578 self.cmd()
579 .arg("rev-parse")
580 .args(short.then_some("--short"))
581 .arg(revision)
582 .get_stdout_lossy()
583 }
584
585 pub fn tag(self) -> Result<String> {
586 self.cmd().arg("tag").get_stdout_lossy()
587 }
588
589 pub fn tag_for_commit(self, rev: &str, at: &Path) -> Result<Option<String>> {
597 self.cmd_at(at)
598 .args(["tag", "--contains"])
599 .arg(rev)
600 .get_stdout_lossy()
601 .map(|stdout| stdout.lines().next().map(str::to_string))
602 }
603
604 pub fn read_submodules_with_branch(
612 self,
613 at: &Path,
614 lib: &OsStr,
615 ) -> Result<HashMap<PathBuf, String>> {
616 let gitmodules = foundry_common::fs::read_to_string(at.join(".gitmodules"))?;
618
619 let paths = SUBMODULE_BRANCH_REGEX
620 .captures_iter(&gitmodules)
621 .map(|cap| {
622 let path_str = cap.get(1).unwrap().as_str();
623 let path = PathBuf::from_str(path_str).unwrap();
624 trace!(path = %path.display(), "unstripped path");
625
626 let lib_pos = path.components().find_position(|c| c.as_os_str() == lib);
633 let path = path
634 .components()
635 .skip(lib_pos.map(|(i, _)| i).unwrap_or(0))
636 .collect::<PathBuf>();
637
638 let branch = cap.get(2).unwrap().as_str().to_string();
639 (path, branch)
640 })
641 .collect::<HashMap<_, _>>();
642
643 Ok(paths)
644 }
645
646 pub fn has_missing_dependencies<I, S>(self, paths: I) -> Result<bool>
647 where
648 I: IntoIterator<Item = S>,
649 S: AsRef<OsStr>,
650 {
651 let paths = paths.into_iter().map(|path| path.as_ref().to_owned()).collect::<Vec<_>>();
652 if self.submodules_initialized(&paths).unwrap_or(false) {
653 return Ok(false);
654 }
655
656 self.cmd()
657 .args(["submodule", "status"])
658 .args(&paths)
659 .get_stdout_lossy()
660 .map(|stdout| stdout.lines().any(|line| line.starts_with('-')))
661 }
662
663 fn submodules_initialized(self, paths: &[OsString]) -> Result<bool> {
665 let Some(root) = self.root.ancestors().find(|root| root.join(".git").exists()) else {
666 return Ok(false);
667 };
668 if paths.iter().any(|path| {
669 Path::new(path)
670 .components()
671 .any(|component| matches!(component, std::path::Component::ParentDir))
672 }) {
673 return Ok(false);
674 }
675 let relative_root = self.root.strip_prefix(root).unwrap_or_else(|_| Path::new(""));
676 let gitmodules = root.join(".gitmodules");
677 if !gitmodules.is_file() {
678 return Ok(false);
679 }
680
681 let output = Command::new("git")
682 .args(["config", "--null", "--file"])
683 .arg(gitmodules)
684 .args(["--get-regexp", r"^submodule\..*\.path$"])
685 .get_stdout_lossy()?;
686
687 for entry in output.split_terminator('\0') {
688 let (_, path) = entry
689 .split_once('\n')
690 .ok_or_else(|| eyre::eyre!("invalid submodule path config"))?;
691 let path = Path::new(path);
692 let matches = paths.is_empty()
693 || paths.iter().any(|prefix| {
694 let prefix = Path::new(prefix);
695 if prefix.is_absolute() {
696 prefix.strip_prefix(root).is_ok_and(|prefix| path.starts_with(prefix))
697 } else {
698 path.starts_with(relative_root.join(prefix))
699 }
700 });
701 if matches && !root.join(path).join(".git").exists() {
702 return Ok(false);
703 }
704 }
705 Ok(true)
706 }
707
708 pub fn has_submodules<I, S>(self, paths: I) -> Result<bool>
710 where
711 I: IntoIterator<Item = S>,
712 S: AsRef<OsStr>,
713 {
714 self.cmd()
715 .args(["submodule", "status"])
716 .args(paths)
717 .get_stdout_lossy()
718 .map(|stdout| stdout.trim().lines().next().is_some())
719 }
720
721 pub fn submodule_add(
722 self,
723 force: bool,
724 url: impl AsRef<OsStr>,
725 path: impl AsRef<OsStr>,
726 ) -> Result<()> {
727 self.cmd()
728 .arg("--literal-pathspecs")
729 .stderr(self.stderr())
730 .args(["submodule", "add"])
731 .args(self.shallow.then_some("--depth=1"))
732 .args(force.then_some("--force"))
733 .arg(url)
734 .arg(path)
735 .exec()
736 .map(drop)
737 }
738
739 pub fn submodule_update<I, S>(
740 self,
741 force: bool,
742 remote: bool,
743 no_fetch: bool,
744 recursive: bool,
745 paths: I,
746 ) -> Result<()>
747 where
748 I: IntoIterator<Item = S>,
749 S: AsRef<OsStr>,
750 {
751 self.cmd()
752 .stderr(self.stderr())
753 .args(["submodule", "update", "--progress", "--init"])
754 .args(self.shallow.then_some("--depth=1"))
755 .args(force.then_some("--force"))
756 .args(remote.then_some("--remote"))
757 .args(no_fetch.then_some("--no-fetch"))
758 .args(recursive.then_some("--recursive"))
759 .args(paths)
760 .exec()
761 .map(drop)
762 }
763
764 pub fn submodule_foreach(self, recursive: bool, cmd: impl AsRef<OsStr>) -> Result<()> {
765 self.cmd()
766 .stderr(self.stderr())
767 .args(["submodule", "foreach"])
768 .args(recursive.then_some("--recursive"))
769 .arg(cmd)
770 .exec()
771 .map(drop)
772 }
773
774 pub fn submodules_uninitialized(self) -> Result<bool> {
778 self.has_missing_dependencies(std::iter::empty::<&OsStr>())
781 }
782
783 pub fn submodule_init(self) -> Result<()> {
785 self.cmd().stderr(self.stderr()).args(["submodule", "init"]).exec().map(drop)
786 }
787
788 pub fn submodules(&self) -> Result<Submodules> {
789 self.cmd().args(["submodule", "status"]).get_stdout_lossy().map(|stdout| stdout.parse())?
790 }
791
792 pub fn submodules_in(&self, path: &Path) -> Result<Vec<SubmoduleCheckout>> {
794 self.submodules_in_worktree(path, self.root, Path::new(""))
795 }
796
797 pub fn submodules_in_worktree(
800 &self,
801 path: &Path,
802 worktree_root: &Path,
803 worktree_prefix: &Path,
804 ) -> Result<Vec<SubmoduleCheckout>> {
805 let pathspec = if path.as_os_str().is_empty() { Path::new(".") } else { path };
806 let output = self
807 .cmd()
808 .args(["--literal-pathspecs", "ls-files", "--stage", "-z", "--"])
809 .arg(pathspec)
810 .exec()?;
811 let (_, mappings) = self.submodule_mappings_at(worktree_root)?;
812 let mut gitlinks = BTreeMap::new();
813 for entry in output.stdout.split(|byte| *byte == 0).filter(|entry| !entry.is_empty()) {
814 let Some(separator) = entry.iter().position(|byte| *byte == b'\t') else {
815 return Err(eyre::eyre!("invalid index entry"));
816 };
817 let mut fields = std::str::from_utf8(&entry[..separator])?.split_ascii_whitespace();
818 let Some(mode) = fields.next() else {
819 return Err(eyre::eyre!("invalid index entry"));
820 };
821 let Some(rev) = fields.next() else {
822 return Err(eyre::eyre!("invalid index entry"));
823 };
824 let Some(stage) = fields.next() else {
825 return Err(eyre::eyre!("invalid index entry"));
826 };
827 if fields.next().is_some() {
828 return Err(eyre::eyre!("invalid index entry"));
829 }
830 if mode != "160000" {
831 continue;
832 }
833
834 let submodule_path = PathBuf::from(std::str::from_utf8(&entry[separator + 1..])?);
835 let worktree_path =
836 foundry_common::fs::normalize_path(&worktree_prefix.join(&submodule_path));
837 if !mappings.contains(&worktree_path) {
838 gitlinks.insert(
839 submodule_path.clone(),
840 SubmoduleCheckout {
841 status: SubmoduleCheckoutStatus::MissingMapping,
842 rev: rev.to_string(),
843 path: submodule_path,
844 },
845 );
846 continue;
847 }
848 if stage != "0" {
849 gitlinks.insert(
850 submodule_path.clone(),
851 SubmoduleCheckout {
852 status: SubmoduleCheckoutStatus::Conflicted,
853 rev: rev.to_string(),
854 path: submodule_path,
855 },
856 );
857 continue;
858 }
859
860 let status = self
861 .cmd()
862 .args(["--literal-pathspecs", "submodule", "status", "--"])
863 .arg(&submodule_path)
864 .get_stdout_lossy()?;
865 let (status, rev) = match status.as_bytes().first() {
866 Some(b'-') => (SubmoduleCheckoutStatus::Uninitialized, &status[1..]),
867 Some(b'+') => (SubmoduleCheckoutStatus::Modified, &status[1..]),
868 Some(b'U') => (SubmoduleCheckoutStatus::Conflicted, &status[1..]),
869 Some(_) => (SubmoduleCheckoutStatus::Current, status.as_str()),
870 None => return Err(eyre::eyre!("missing submodule status")),
871 };
872 let rev = rev
873 .split_ascii_whitespace()
874 .next()
875 .ok_or_else(|| eyre::eyre!("invalid submodule status"))?;
876 gitlinks.insert(
877 submodule_path.clone(),
878 SubmoduleCheckout { status, rev: rev.to_string(), path: submodule_path },
879 );
880 }
881 Ok(gitlinks.into_values().collect())
882 }
883
884 pub fn submodule_sync(self) -> Result<()> {
885 self.cmd().stderr(self.stderr()).args(["submodule", "sync"]).exec().map(drop)
886 }
887
888 pub fn submodule_url(self, path: &Path) -> Result<Option<String>> {
890 self.cmd()
891 .args(["config", "--get", &format!("submodule.{}.url", path.to_slash_lossy())])
892 .get_stdout_lossy()
893 .map(|url| Some(url.trim().to_string()))
894 }
895
896 pub fn has_submodule_mapping(self, path: &Path) -> Result<bool> {
898 let (names, paths) = self.submodule_mappings()?;
899 Ok(names.contains(path) || paths.contains(path))
900 }
901
902 fn submodule_mappings(self) -> Result<(BTreeSet<PathBuf>, BTreeSet<PathBuf>)> {
903 self.submodule_mappings_at(self.root)
904 }
905
906 fn submodule_mappings_at(self, root: &Path) -> Result<(BTreeSet<PathBuf>, BTreeSet<PathBuf>)> {
907 let gitmodules = root.join(".gitmodules");
908 if !gitmodules.exists() {
909 return Ok(Default::default());
910 }
911
912 let output = self
913 .cmd()
914 .args(["config", "--null", "--file"])
915 .arg(gitmodules)
916 .args(["--get-regexp", r"^submodule\..*"])
917 .output()?;
918 match output.status.code() {
919 Some(0) => {
920 let mut names = BTreeSet::new();
921 let mut paths = BTreeSet::new();
922 for entry in
923 output.stdout.split(|byte| *byte == 0).filter(|entry| !entry.is_empty())
924 {
925 let Some(separator) = entry.iter().position(|byte| *byte == b'\n') else {
926 return Err(eyre::eyre!("invalid submodule mapping entry"));
927 };
928 let key = std::str::from_utf8(&entry[..separator])?;
929 let value = std::str::from_utf8(&entry[separator + 1..])?;
930 let Some(key) = key.strip_prefix("submodule.") else { continue };
931 let Some((name, field)) = key.rsplit_once('.') else { continue };
932 names.insert(PathBuf::from(name));
933 if field == "path" {
934 paths.insert(PathBuf::from(value));
935 }
936 }
937 Ok((names, paths))
938 }
939 Some(1) => Ok(Default::default()),
940 _ => Err(eyre::eyre!(
941 "failed to inspect .gitmodules: {}",
942 String::from_utf8_lossy(&output.stderr).trim()
943 )),
944 }
945 }
946
947 pub fn is_gitlink(self, path: &Path) -> Result<bool> {
949 self.cmd().args(["ls-files", "--stage", "-z", "--"]).arg(path).exec().map(|output| {
950 let expected_path = path.to_slash_lossy();
951 output.stdout.split(|byte| *byte == 0).any(|entry| {
952 entry.starts_with(b"160000 ")
953 && entry
954 .iter()
955 .position(|byte| *byte == b'\t')
956 .and_then(|separator| entry.get(separator + 1..))
957 == Some(expected_path.as_bytes())
958 })
959 })
960 }
961
962 pub fn has_index_entries(self, path: &Path) -> Result<bool> {
964 self.cmd()
965 .args(["--literal-pathspecs", "ls-files", "--stage", "-z", "--"])
966 .arg(path)
967 .exec()
968 .map(|output| !output.stdout.is_empty())
969 }
970
971 pub fn is_normal_tracked_file(self, path: &Path) -> Result<bool> {
973 let output = self
974 .cmd()
975 .args(["--literal-pathspecs", "ls-files", "--stage", "-v", "-z", "--"])
976 .arg(path)
977 .exec()?;
978 let mut entries = output.stdout.split(|byte| *byte == 0).filter(|entry| !entry.is_empty());
979 let Some(entry) = entries.next() else { return Ok(false) };
980 if entries.next().is_some() {
981 return Ok(false);
982 }
983 let Some(separator) = entry.iter().position(|byte| *byte == b'\t') else {
984 return Err(eyre::eyre!("invalid index entry"));
985 };
986 if entry.get(separator + 1..) != Some(path.to_slash_lossy().as_bytes()) {
987 return Ok(false);
988 }
989 let mut fields = std::str::from_utf8(&entry[..separator])?.split_ascii_whitespace();
990 if fields.next() != Some("H") || !matches!(fields.next(), Some("100644" | "100755")) {
991 return Ok(false);
992 }
993 let Some(index_hash) = fields.next() else { return Ok(false) };
994 if fields.next() != Some("0") || fields.next().is_some() {
995 return Ok(false);
996 }
997 let worktree_hash = self.cmd().args(["hash-object", "--"]).arg(path).get_stdout_lossy()?;
998 Ok(worktree_hash.trim() == index_hash)
999 }
1000
1001 pub fn has_submodule_config(self, path: &Path) -> Result<bool> {
1003 let pattern = format!(r"^submodule\.{}\.", regex::escape(&path.to_slash_lossy()));
1004 let output = self.cmd().args(["config", "--local", "--get-regexp", &pattern]).output()?;
1005 match output.status.code() {
1006 Some(0) => Ok(true),
1007 Some(1) => Ok(false),
1008 _ => Err(eyre::eyre!(
1009 "failed to inspect submodule config: {}",
1010 String::from_utf8_lossy(&output.stderr).trim()
1011 )),
1012 }
1013 }
1014
1015 pub fn remove_submodule_config(self, path: &Path) -> Result<()> {
1017 if self.has_submodule_config(path)? {
1018 let section = format!("submodule.{}", path.to_slash_lossy());
1019 self.cmd().args(["config", "--local", "--remove-section", §ion]).exec()?;
1020 }
1021 Ok(())
1022 }
1023
1024 pub fn absolute_git_dir(self) -> Result<PathBuf> {
1026 self.cmd().args(["rev-parse", "--absolute-git-dir"]).get_stdout_lossy().map(PathBuf::from)
1027 }
1028
1029 pub fn remote_url(self, name: &str) -> Option<String> {
1031 self.cmd().args(["remote", "get-url", name]).get_stdout_lossy().ok()
1032 }
1033
1034 pub fn set_submodule_branch(self, rel_path: &Path, branch: &str) -> Result<()> {
1036 self.cmd().args(["submodule", "set-branch", "-b", branch]).arg(rel_path).exec().map(drop)
1037 }
1038
1039 pub fn remote_branches(self) -> Result<String> {
1041 self.cmd().args(["branch", "-r"]).get_stdout_lossy()
1042 }
1043
1044 pub fn fetch_and_checkout_branch(self, at: &Path, branch: &str) -> Result<()> {
1046 self.cmd_at(at).args(["fetch", "origin", branch]).exec().map_err(|e| {
1047 eyre::eyre!(
1048 "Could not fetch latest changes for branch {branch} in submodule at {}: {e}",
1049 at.display()
1050 )
1051 })?;
1052 self.cmd_at(at)
1053 .args(["checkout", "-B", branch, &format!("origin/{branch}")])
1054 .exec()
1055 .map_err(|e| {
1056 eyre::eyre!(
1057 "Could not checkout and track origin/{branch} for submodule at {}: {e}",
1058 at.display()
1059 )
1060 })?;
1061 Ok(())
1062 }
1063
1064 fn cmd(self) -> Command {
1065 let mut cmd = Self::cmd_no_root();
1066 cmd.current_dir(self.root);
1067 cmd
1068 }
1069
1070 fn cmd_at(self, path: &Path) -> Command {
1071 let mut cmd = Self::cmd_no_root();
1072 cmd.current_dir(path);
1073 cmd
1074 }
1075
1076 fn cmd_no_root() -> Command {
1077 let mut cmd = Command::new("git");
1078 cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
1079 cmd
1080 }
1081
1082 fn stderr(self) -> Stdio {
1084 if self.quiet { Stdio::piped() } else { Stdio::inherit() }
1085 }
1086}
1087
1088#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
1090pub struct Submodule {
1091 rev: String,
1093 path: PathBuf,
1095}
1096
1097impl Submodule {
1098 pub const fn new(rev: String, path: PathBuf) -> Self {
1099 Self { rev, path }
1100 }
1101
1102 pub fn rev(&self) -> &str {
1103 &self.rev
1104 }
1105
1106 pub const fn path(&self) -> &PathBuf {
1107 &self.path
1108 }
1109}
1110
1111#[derive(Debug, Clone, PartialEq, Eq)]
1113pub struct SubmoduleCheckout {
1114 status: SubmoduleCheckoutStatus,
1115 rev: String,
1116 path: PathBuf,
1117}
1118
1119impl SubmoduleCheckout {
1120 pub const fn status(&self) -> SubmoduleCheckoutStatus {
1121 self.status
1122 }
1123
1124 pub fn rev(&self) -> &str {
1125 &self.rev
1126 }
1127
1128 pub const fn path(&self) -> &PathBuf {
1129 &self.path
1130 }
1131}
1132
1133#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1135pub enum SubmoduleCheckoutStatus {
1136 Current,
1138 Uninitialized,
1140 Modified,
1142 Conflicted,
1144 MissingMapping,
1146}
1147
1148impl FromStr for Submodule {
1149 type Err = eyre::Report;
1150
1151 fn from_str(s: &str) -> Result<Self> {
1152 let caps = SUBMODULE_STATUS_REGEX
1153 .captures(s)
1154 .ok_or_else(|| eyre::eyre!("Invalid submodule status format"))?;
1155
1156 Ok(Self {
1157 rev: caps.get(1).unwrap().as_str().to_string(),
1158 path: PathBuf::from(caps.get(2).unwrap().as_str()),
1159 })
1160 }
1161}
1162
1163#[derive(Debug, Clone, PartialEq, Eq)]
1165pub struct Submodules(pub Vec<Submodule>);
1166
1167impl Submodules {
1168 pub const fn len(&self) -> usize {
1169 self.0.len()
1170 }
1171
1172 pub const fn is_empty(&self) -> bool {
1173 self.0.is_empty()
1174 }
1175}
1176
1177impl FromStr for Submodules {
1178 type Err = eyre::Report;
1179
1180 fn from_str(s: &str) -> Result<Self> {
1181 let subs = s.lines().map(str::parse).collect::<Result<Vec<Submodule>>>()?;
1182 Ok(Self(subs))
1183 }
1184}
1185
1186impl<'a> IntoIterator for &'a Submodules {
1187 type Item = &'a Submodule;
1188 type IntoIter = std::slice::Iter<'a, Submodule>;
1189
1190 fn into_iter(self) -> Self::IntoIter {
1191 self.0.iter()
1192 }
1193}
1194#[cfg(test)]
1195mod tests {
1196 use super::*;
1197 use foundry_common::fs;
1198 use std::{env, fs::File, io::Write};
1199 use tempfile::tempdir;
1200
1201 #[test]
1202 fn parse_submodule_status() {
1203 let s = "+8829465a08cac423dcf59852f21e448449c1a1a8 lib/openzeppelin-contracts (v4.8.0-791-g8829465a)";
1204 let sub = Submodule::from_str(s).unwrap();
1205 assert_eq!(sub.rev(), "8829465a08cac423dcf59852f21e448449c1a1a8");
1206 assert_eq!(sub.path(), Path::new("lib/openzeppelin-contracts"));
1207
1208 let s = "-8829465a08cac423dcf59852f21e448449c1a1a8 lib/openzeppelin-contracts";
1209 let sub = Submodule::from_str(s).unwrap();
1210 assert_eq!(sub.rev(), "8829465a08cac423dcf59852f21e448449c1a1a8");
1211 assert_eq!(sub.path(), Path::new("lib/openzeppelin-contracts"));
1212
1213 let s = "8829465a08cac423dcf59852f21e448449c1a1a8 lib/openzeppelin-contracts";
1214 let sub = Submodule::from_str(s).unwrap();
1215 assert_eq!(sub.rev(), "8829465a08cac423dcf59852f21e448449c1a1a8");
1216 assert_eq!(sub.path(), Path::new("lib/openzeppelin-contracts"));
1217 }
1218
1219 #[test]
1220 fn parse_multiline_submodule_status() {
1221 let s = r#"+d3db4ef90a72b7d24aa5a2e5c649593eaef7801d lib/forge-std (v1.9.4-6-gd3db4ef)
1222+8829465a08cac423dcf59852f21e448449c1a1a8 lib/openzeppelin-contracts (v4.8.0-791-g8829465a)
1223"#;
1224 let subs = Submodules::from_str(s).unwrap().0;
1225 assert_eq!(subs.len(), 2);
1226 assert_eq!(subs[0].rev(), "d3db4ef90a72b7d24aa5a2e5c649593eaef7801d");
1227 assert_eq!(subs[0].path(), Path::new("lib/forge-std"));
1228 assert_eq!(subs[1].rev(), "8829465a08cac423dcf59852f21e448449c1a1a8");
1229 assert_eq!(subs[1].path(), Path::new("lib/openzeppelin-contracts"));
1230 }
1231
1232 #[test]
1233 fn deserialize_submodule() {
1234 let submodule: Submodule = serde_json::from_str(
1235 r#"{"rev":"8829465a08cac423dcf59852f21e448449c1a1a8","path":"lib/dep"}"#,
1236 )
1237 .unwrap();
1238 assert_eq!(submodule.rev(), "8829465a08cac423dcf59852f21e448449c1a1a8");
1239 assert_eq!(submodule.path(), Path::new("lib/dep"));
1240 assert_eq!(
1241 serde_json::to_value(submodule).unwrap(),
1242 serde_json::json!({
1243 "rev": "8829465a08cac423dcf59852f21e448449c1a1a8",
1244 "path": "lib/dep",
1245 })
1246 );
1247 }
1248
1249 #[test]
1250 fn skips_submodule_status_if_dependencies_are_initialized() {
1251 let tmp = tempdir().unwrap();
1252 let root = tmp.path();
1253 std::fs::create_dir(root.join(".git")).unwrap();
1254 std::fs::write(
1255 root.join(".gitmodules"),
1256 r#"[submodule "lib/forge-std"]
1257 path = lib/forge-std
1258 url = https://github.com/foundry-rs/forge-std
1259"#,
1260 )
1261 .unwrap();
1262 std::fs::create_dir_all(root.join("lib/forge-std/.git")).unwrap();
1263
1264 let git = Git::new(root);
1265 assert!(git.submodules_initialized(&["lib".into()]).unwrap());
1266 assert!(git.submodules_initialized(&[root.join("lib").into()]).unwrap());
1267
1268 let nested = root.join("packages/contracts");
1269 std::fs::create_dir_all(&nested).unwrap();
1270 assert!(!Git::new(&nested).submodules_initialized(&["../../lib".into()]).unwrap());
1271
1272 assert!(!git.has_missing_dependencies(["lib"]).unwrap());
1274
1275 std::fs::remove_dir(root.join("lib/forge-std/.git")).unwrap();
1276 assert!(!git.submodules_initialized(&["lib".into()]).unwrap());
1277 }
1278
1279 #[test]
1280 fn foundry_path_ext_works() {
1281 let p = Path::new("contracts/MyTest.t.sol");
1282 assert!(p.is_sol_test());
1283 assert!(p.is_sol());
1284 let p = Path::new("contracts/Greeter.sol");
1285 assert!(!p.is_sol_test());
1286 }
1287
1288 #[test]
1289 fn parse_ether_value_accepts_hex_prefixed_wei() {
1290 assert_eq!(parse_ether_value("0x10").unwrap(), U256::from(16));
1291 assert_eq!(parse_ether_value("0X10").unwrap(), U256::from(16));
1292 assert_eq!(parse_ether_value("0x12").unwrap(), U256::from(0x12));
1293 assert_eq!(parse_ether_value("0xff").unwrap(), U256::from(0xff));
1294 assert_eq!(parse_ether_value("100").unwrap(), U256::from(100));
1295 assert_eq!(parse_ether_value("1ether").unwrap(), U256::from(1000000000000000000u128));
1296 }
1297
1298 #[test]
1300 fn can_load_dotenv() {
1301 let temp = tempdir().unwrap();
1302 Git::new(temp.path()).init().unwrap();
1303 let cwd_env = temp.path().join(".env");
1304 fs::create_file(temp.path().join("foundry.toml")).unwrap();
1305 let nested = temp.path().join("nested");
1306 fs::create_dir(&nested).unwrap();
1307
1308 let mut cwd_file = File::create(cwd_env).unwrap();
1309 let mut prj_file = File::create(nested.join(".env")).unwrap();
1310
1311 cwd_file.write_all(b"TESTCWDKEY=cwd_val").unwrap();
1312 cwd_file.sync_all().unwrap();
1313
1314 prj_file.write_all(b"TESTPRJKEY=prj_val").unwrap();
1315 prj_file.sync_all().unwrap();
1316
1317 let cwd = env::current_dir().unwrap();
1318 env::set_current_dir(nested).unwrap();
1319 load_dotenv();
1320 env::set_current_dir(cwd).unwrap();
1321
1322 assert_eq!(env::var("TESTCWDKEY").unwrap(), "cwd_val");
1323 assert_eq!(env::var("TESTPRJKEY").unwrap(), "prj_val");
1324 }
1325
1326 #[test]
1327 fn test_read_gitmodules_regex() {
1328 let gitmodules = r#"
1329 [submodule "lib/solady"]
1330 path = lib/solady
1331 url = ""
1332 branch = v0.1.0
1333 [submodule "lib/openzeppelin-contracts"]
1334 path = lib/openzeppelin-contracts
1335 url = ""
1336 branch = v4.8.0-791-g8829465a
1337 [submodule "lib/forge-std"]
1338 path = lib/forge-std
1339 url = ""
1340"#;
1341
1342 let paths = SUBMODULE_BRANCH_REGEX
1343 .captures_iter(gitmodules)
1344 .map(|cap| {
1345 (
1346 PathBuf::from_str(cap.get(1).unwrap().as_str()).unwrap(),
1347 String::from(cap.get(2).unwrap().as_str()),
1348 )
1349 })
1350 .collect::<HashMap<_, _>>();
1351
1352 assert_eq!(paths.get(Path::new("lib/solady")).unwrap(), "v0.1.0");
1353 assert_eq!(
1354 paths.get(Path::new("lib/openzeppelin-contracts")).unwrap(),
1355 "v4.8.0-791-g8829465a"
1356 );
1357
1358 let no_branch_gitmodules = r#"
1359 [submodule "lib/solady"]
1360 path = lib/solady
1361 url = ""
1362 [submodule "lib/openzeppelin-contracts"]
1363 path = lib/openzeppelin-contracts
1364 url = ""
1365 [submodule "lib/forge-std"]
1366 path = lib/forge-std
1367 url = ""
1368"#;
1369 let paths = SUBMODULE_BRANCH_REGEX
1370 .captures_iter(no_branch_gitmodules)
1371 .map(|cap| {
1372 (
1373 PathBuf::from_str(cap.get(1).unwrap().as_str()).unwrap(),
1374 String::from(cap.get(2).unwrap().as_str()),
1375 )
1376 })
1377 .collect::<HashMap<_, _>>();
1378
1379 assert!(paths.is_empty());
1380
1381 let branch_in_between = r#"
1382 [submodule "lib/solady"]
1383 path = lib/solady
1384 url = ""
1385 [submodule "lib/openzeppelin-contracts"]
1386 path = lib/openzeppelin-contracts
1387 url = ""
1388 branch = v4.8.0-791-g8829465a
1389 [submodule "lib/forge-std"]
1390 path = lib/forge-std
1391 url = ""
1392 "#;
1393
1394 let paths = SUBMODULE_BRANCH_REGEX
1395 .captures_iter(branch_in_between)
1396 .map(|cap| {
1397 (
1398 PathBuf::from_str(cap.get(1).unwrap().as_str()).unwrap(),
1399 String::from(cap.get(2).unwrap().as_str()),
1400 )
1401 })
1402 .collect::<HashMap<_, _>>();
1403
1404 assert_eq!(paths.len(), 1);
1405 assert_eq!(
1406 paths.get(Path::new("lib/openzeppelin-contracts")).unwrap(),
1407 "v4.8.0-791-g8829465a"
1408 );
1409 }
1410}