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