foundry_cli/utils/
mod.rs

1use alloy_json_abi::JsonAbi;
2use alloy_primitives::{U256, map::HashMap};
3use alloy_provider::{Provider, network::AnyNetwork};
4use eyre::{ContextCompat, Result};
5use foundry_common::{
6    provider::{ProviderBuilder, RetryProvider},
7    shell,
8};
9use foundry_config::{Chain, Config};
10use itertools::Itertools;
11use regex::Regex;
12use serde::de::DeserializeOwned;
13use std::{
14    ffi::OsStr,
15    path::{Path, PathBuf},
16    process::{Command, Output, Stdio},
17    str::FromStr,
18    sync::LazyLock,
19    time::{Duration, SystemTime, UNIX_EPOCH},
20};
21use tracing_subscriber::prelude::*;
22
23mod cmd;
24pub use cmd::*;
25
26mod suggestions;
27pub use suggestions::*;
28
29mod abi;
30pub use abi::*;
31
32mod allocator;
33pub use allocator::*;
34
35// reexport all `foundry_config::utils`
36#[doc(hidden)]
37pub use foundry_config::utils::*;
38
39/// Deterministic fuzzer seed used for gas snapshots and coverage reports.
40///
41/// The keccak256 hash of "foundry rulez"
42pub const STATIC_FUZZ_SEED: [u8; 32] = [
43    0x01, 0x00, 0xfa, 0x69, 0xa5, 0xf1, 0x71, 0x0a, 0x95, 0xcd, 0xef, 0x94, 0x88, 0x9b, 0x02, 0x84,
44    0x5d, 0x64, 0x0b, 0x19, 0xad, 0xf0, 0xe3, 0x57, 0xb8, 0xd4, 0xbe, 0x7d, 0x49, 0xee, 0x70, 0xe6,
45];
46
47/// Regex used to parse `.gitmodules` file and capture the submodule path and branch.
48pub static SUBMODULE_BRANCH_REGEX: LazyLock<Regex> =
49    LazyLock::new(|| Regex::new(r#"\[submodule "([^"]+)"\](?:[^\[]*?branch = ([^\s]+))"#).unwrap());
50/// Regex used to parse `git submodule status` output.
51pub static SUBMODULE_STATUS_REGEX: LazyLock<Regex> =
52    LazyLock::new(|| Regex::new(r"^[\s+-]?([a-f0-9]+)\s+([^\s]+)(?:\s+\([^)]+\))?$").unwrap());
53
54/// Useful extensions to [`std::path::Path`].
55pub trait FoundryPathExt {
56    /// Returns true if the [`Path`] ends with `.t.sol`
57    fn is_sol_test(&self) -> bool;
58
59    /// Returns true if the  [`Path`] has a `sol` extension
60    fn is_sol(&self) -> bool;
61
62    /// Returns true if the  [`Path`] has a `yul` extension
63    fn is_yul(&self) -> bool;
64}
65
66impl<T: AsRef<Path>> FoundryPathExt for T {
67    fn is_sol_test(&self) -> bool {
68        self.as_ref()
69            .file_name()
70            .and_then(|s| s.to_str())
71            .map(|s| s.ends_with(".t.sol"))
72            .unwrap_or_default()
73    }
74
75    fn is_sol(&self) -> bool {
76        self.as_ref().extension() == Some(std::ffi::OsStr::new("sol"))
77    }
78
79    fn is_yul(&self) -> bool {
80        self.as_ref().extension() == Some(std::ffi::OsStr::new("yul"))
81    }
82}
83
84/// Initializes a tracing Subscriber for logging
85pub fn subscriber() {
86    let registry = tracing_subscriber::Registry::default()
87        .with(tracing_subscriber::EnvFilter::from_default_env());
88    #[cfg(feature = "tracy")]
89    let registry = registry.with(tracing_tracy::TracyLayer::default());
90    registry.with(tracing_subscriber::fmt::layer()).init()
91}
92
93pub fn abi_to_solidity(abi: &JsonAbi, name: &str) -> Result<String> {
94    let s = abi.to_sol(name, None);
95    let s = forge_fmt::format(&s)?;
96    Ok(s)
97}
98
99/// Returns a [RetryProvider] instantiated using [Config]'s
100/// RPC
101pub fn get_provider(config: &Config) -> Result<RetryProvider> {
102    get_provider_builder(config)?.build()
103}
104
105/// Returns a [ProviderBuilder] instantiated using [Config] values.
106///
107/// Defaults to `http://localhost:8545` and `Mainnet`.
108pub fn get_provider_builder(config: &Config) -> Result<ProviderBuilder> {
109    let url = config.get_rpc_url_or_localhost_http()?;
110    let mut builder = ProviderBuilder::new(url.as_ref());
111
112    builder = builder.accept_invalid_certs(config.eth_rpc_accept_invalid_certs);
113
114    if let Ok(chain) = config.chain.unwrap_or_default().try_into() {
115        builder = builder.chain(chain);
116    }
117
118    if let Some(jwt) = config.get_rpc_jwt_secret()? {
119        builder = builder.jwt(jwt.as_ref());
120    }
121
122    if let Some(rpc_timeout) = config.eth_rpc_timeout {
123        builder = builder.timeout(Duration::from_secs(rpc_timeout));
124    }
125
126    if let Some(rpc_headers) = config.eth_rpc_headers.clone() {
127        builder = builder.headers(rpc_headers);
128    }
129
130    Ok(builder)
131}
132
133pub async fn get_chain<P>(chain: Option<Chain>, provider: P) -> Result<Chain>
134where
135    P: Provider<AnyNetwork>,
136{
137    match chain {
138        Some(chain) => Ok(chain),
139        None => Ok(Chain::from_id(provider.get_chain_id().await?)),
140    }
141}
142
143/// Parses an ether value from a string.
144///
145/// The amount can be tagged with a unit, e.g. "1ether".
146///
147/// If the string represents an untagged amount (e.g. "100") then
148/// it is interpreted as wei.
149pub fn parse_ether_value(value: &str) -> Result<U256> {
150    Ok(if value.starts_with("0x") {
151        U256::from_str_radix(value, 16)?
152    } else {
153        alloy_dyn_abi::DynSolType::coerce_str(&alloy_dyn_abi::DynSolType::Uint(256), value)?
154            .as_uint()
155            .wrap_err("Could not parse ether value from string")?
156            .0
157    })
158}
159
160/// Parses a `T` from a string using [`serde_json::from_str`].
161pub fn parse_json<T: DeserializeOwned>(value: &str) -> serde_json::Result<T> {
162    serde_json::from_str(value)
163}
164
165/// Parses a `Duration` from a &str
166pub fn parse_delay(delay: &str) -> Result<Duration> {
167    let delay = if delay.ends_with("ms") {
168        let d: u64 = delay.trim_end_matches("ms").parse()?;
169        Duration::from_millis(d)
170    } else {
171        let d: f64 = delay.parse()?;
172        let delay = (d * 1000.0).round();
173        if delay.is_infinite() || delay.is_nan() || delay.is_sign_negative() {
174            eyre::bail!("delay must be finite and non-negative");
175        }
176
177        Duration::from_millis(delay as u64)
178    };
179    Ok(delay)
180}
181
182/// Returns the current time as a [`Duration`] since the Unix epoch.
183pub fn now() -> Duration {
184    SystemTime::now().duration_since(UNIX_EPOCH).expect("time went backwards")
185}
186
187/// Runs the `future` in a new [`tokio::runtime::Runtime`]
188pub fn block_on<F: Future>(future: F) -> F::Output {
189    let rt = tokio::runtime::Runtime::new().expect("could not start tokio rt");
190    rt.block_on(future)
191}
192
193/// Loads a dotenv file, from the cwd and the project root, ignoring potential failure.
194///
195/// We could use `warn!` here, but that would imply that the dotenv file can't configure
196/// the logging behavior of Foundry.
197///
198/// Similarly, we could just use `eprintln!`, but colors are off limits otherwise dotenv is implied
199/// to not be able to configure the colors. It would also mess up the JSON output.
200pub fn load_dotenv() {
201    let load = |p: &Path| {
202        dotenvy::from_path(p.join(".env")).ok();
203    };
204
205    // we only want the .env file of the cwd and project root
206    // `find_project_root` calls `current_dir` internally so both paths are either both `Ok` or
207    // both `Err`
208    if let (Ok(cwd), Ok(prj_root)) = (std::env::current_dir(), find_project_root(None)) {
209        load(&prj_root);
210        if cwd != prj_root {
211            // prj root and cwd can be identical
212            load(&cwd);
213        }
214    };
215}
216
217/// Sets the default [`yansi`] color output condition.
218pub fn enable_paint() {
219    let enable = yansi::Condition::os_support() && yansi::Condition::tty_and_color_live();
220    yansi::whenever(yansi::Condition::cached(enable));
221}
222
223/// This force installs the default crypto provider.
224///
225/// This is necessary in case there are more than one available backends enabled in rustls (ring,
226/// aws-lc-rs).
227///
228/// This should be called high in the main fn.
229///
230/// See also:
231///   <https://github.com/snapview/tokio-tungstenite/issues/353#issuecomment-2455100010>
232///   <https://github.com/awslabs/aws-sdk-rust/discussions/1257>
233pub fn install_crypto_provider() {
234    // https://github.com/snapview/tokio-tungstenite/issues/353
235    rustls::crypto::ring::default_provider()
236        .install_default()
237        .expect("Failed to install default rustls crypto provider");
238}
239
240/// Useful extensions to [`std::process::Command`].
241pub trait CommandUtils {
242    /// Returns the command's output if execution is successful, otherwise, throws an error.
243    fn exec(&mut self) -> Result<Output>;
244
245    /// Returns the command's stdout if execution is successful, otherwise, throws an error.
246    fn get_stdout_lossy(&mut self) -> Result<String>;
247}
248
249impl CommandUtils for Command {
250    #[track_caller]
251    fn exec(&mut self) -> Result<Output> {
252        trace!(command=?self, "executing");
253
254        let output = self.output()?;
255
256        trace!(code=?output.status.code(), ?output);
257
258        if output.status.success() {
259            Ok(output)
260        } else {
261            let stdout = String::from_utf8_lossy(&output.stdout);
262            let stdout = stdout.trim();
263            let stderr = String::from_utf8_lossy(&output.stderr);
264            let stderr = stderr.trim();
265            let msg = if stdout.is_empty() {
266                stderr.to_string()
267            } else if stderr.is_empty() {
268                stdout.to_string()
269            } else {
270                format!("stdout:\n{stdout}\n\nstderr:\n{stderr}")
271            };
272
273            let mut name = self.get_program().to_string_lossy();
274            if let Some(arg) = self.get_args().next() {
275                let arg = arg.to_string_lossy();
276                if !arg.starts_with('-') {
277                    let name = name.to_mut();
278                    name.push(' ');
279                    name.push_str(&arg);
280                }
281            }
282
283            let mut err = match output.status.code() {
284                Some(code) => format!("{name} exited with code {code}"),
285                None => format!("{name} terminated by a signal"),
286            };
287            if !msg.is_empty() {
288                err.push(':');
289                err.push(if msg.lines().count() == 0 { ' ' } else { '\n' });
290                err.push_str(&msg);
291            }
292            Err(eyre::eyre!(err))
293        }
294    }
295
296    #[track_caller]
297    fn get_stdout_lossy(&mut self) -> Result<String> {
298        let output = self.exec()?;
299        let stdout = String::from_utf8_lossy(&output.stdout);
300        Ok(stdout.trim().into())
301    }
302}
303
304#[derive(Clone, Copy, Debug)]
305pub struct Git<'a> {
306    pub root: &'a Path,
307    pub quiet: bool,
308    pub shallow: bool,
309}
310
311impl<'a> Git<'a> {
312    #[inline]
313    pub fn new(root: &'a Path) -> Self {
314        Self { root, quiet: shell::is_quiet(), shallow: false }
315    }
316
317    #[inline]
318    pub fn from_config(config: &'a Config) -> Self {
319        Self::new(config.root.as_path())
320    }
321
322    pub fn root_of(relative_to: &Path) -> Result<PathBuf> {
323        let output = Self::cmd_no_root()
324            .current_dir(relative_to)
325            .args(["rev-parse", "--show-toplevel"])
326            .get_stdout_lossy()?;
327        Ok(PathBuf::from(output))
328    }
329
330    pub fn clone_with_branch(
331        shallow: bool,
332        from: impl AsRef<OsStr>,
333        branch: impl AsRef<OsStr>,
334        to: Option<impl AsRef<OsStr>>,
335    ) -> Result<()> {
336        Self::cmd_no_root()
337            .stderr(Stdio::inherit())
338            .args(["clone", "--recurse-submodules"])
339            .args(shallow.then_some("--depth=1"))
340            .args(shallow.then_some("--shallow-submodules"))
341            .arg("-b")
342            .arg(branch)
343            .arg(from)
344            .args(to)
345            .exec()
346            .map(drop)
347    }
348
349    pub fn clone(
350        shallow: bool,
351        from: impl AsRef<OsStr>,
352        to: Option<impl AsRef<OsStr>>,
353    ) -> Result<()> {
354        Self::cmd_no_root()
355            .stderr(Stdio::inherit())
356            .args(["clone", "--recurse-submodules"])
357            .args(shallow.then_some("--depth=1"))
358            .args(shallow.then_some("--shallow-submodules"))
359            .arg(from)
360            .args(to)
361            .exec()
362            .map(drop)
363    }
364
365    pub fn fetch(
366        self,
367        shallow: bool,
368        remote: impl AsRef<OsStr>,
369        branch: Option<impl AsRef<OsStr>>,
370    ) -> Result<()> {
371        self.cmd()
372            .stderr(Stdio::inherit())
373            .arg("fetch")
374            .args(shallow.then_some("--no-tags"))
375            .args(shallow.then_some("--depth=1"))
376            .arg(remote)
377            .args(branch)
378            .exec()
379            .map(drop)
380    }
381
382    #[inline]
383    pub fn root(self, root: &Path) -> Git<'_> {
384        Git { root, ..self }
385    }
386
387    #[inline]
388    pub fn quiet(self, quiet: bool) -> Self {
389        Self { quiet, ..self }
390    }
391
392    /// True to perform shallow clones
393    #[inline]
394    pub fn shallow(self, shallow: bool) -> Self {
395        Self { shallow, ..self }
396    }
397
398    pub fn checkout(self, recursive: bool, tag: impl AsRef<OsStr>) -> Result<()> {
399        self.cmd()
400            .arg("checkout")
401            .args(recursive.then_some("--recurse-submodules"))
402            .arg(tag)
403            .exec()
404            .map(drop)
405    }
406
407    pub fn checkout_at(self, tag: impl AsRef<OsStr>, at: &Path) -> Result<()> {
408        self.cmd_at(at).arg("checkout").arg(tag).exec().map(drop)
409    }
410
411    pub fn init(self) -> Result<()> {
412        self.cmd().arg("init").exec().map(drop)
413    }
414
415    pub fn current_rev_branch(self, at: &Path) -> Result<(String, String)> {
416        let rev = self.cmd_at(at).args(["rev-parse", "HEAD"]).get_stdout_lossy()?;
417        let branch =
418            self.cmd_at(at).args(["rev-parse", "--abbrev-ref", "HEAD"]).get_stdout_lossy()?;
419        Ok((rev, branch))
420    }
421
422    #[expect(clippy::should_implement_trait)] // this is not std::ops::Add clippy
423    pub fn add<I, S>(self, paths: I) -> Result<()>
424    where
425        I: IntoIterator<Item = S>,
426        S: AsRef<OsStr>,
427    {
428        self.cmd().arg("add").args(paths).exec().map(drop)
429    }
430
431    pub fn reset(self, hard: bool, tree: impl AsRef<OsStr>) -> Result<()> {
432        self.cmd().arg("reset").args(hard.then_some("--hard")).arg(tree).exec().map(drop)
433    }
434
435    pub fn commit_tree(
436        self,
437        tree: impl AsRef<OsStr>,
438        msg: Option<impl AsRef<OsStr>>,
439    ) -> Result<String> {
440        self.cmd()
441            .arg("commit-tree")
442            .arg(tree)
443            .args(msg.as_ref().is_some().then_some("-m"))
444            .args(msg)
445            .get_stdout_lossy()
446    }
447
448    pub fn rm<I, S>(self, force: bool, paths: I) -> Result<()>
449    where
450        I: IntoIterator<Item = S>,
451        S: AsRef<OsStr>,
452    {
453        self.cmd().arg("rm").args(force.then_some("--force")).args(paths).exec().map(drop)
454    }
455
456    pub fn commit(self, msg: &str) -> Result<()> {
457        let output = self
458            .cmd()
459            .args(["commit", "-m", msg])
460            .args(cfg!(any(test, debug_assertions)).then_some("--no-gpg-sign"))
461            .output()?;
462        if !output.status.success() {
463            let stdout = String::from_utf8_lossy(&output.stdout);
464            let stderr = String::from_utf8_lossy(&output.stderr);
465            // ignore "nothing to commit" error
466            let msg = "nothing to commit, working tree clean";
467            if !(stdout.contains(msg) || stderr.contains(msg)) {
468                return Err(eyre::eyre!(
469                    "failed to commit (code={:?}, stdout={:?}, stderr={:?})",
470                    output.status.code(),
471                    stdout.trim(),
472                    stderr.trim()
473                ));
474            }
475        }
476        Ok(())
477    }
478
479    pub fn is_in_repo(self) -> std::io::Result<bool> {
480        self.cmd().args(["rev-parse", "--is-inside-work-tree"]).status().map(|s| s.success())
481    }
482
483    pub fn is_clean(self) -> Result<bool> {
484        self.cmd().args(["status", "--porcelain"]).exec().map(|out| out.stdout.is_empty())
485    }
486
487    pub fn has_branch(self, branch: impl AsRef<OsStr>, at: &Path) -> Result<bool> {
488        self.cmd_at(at)
489            .args(["branch", "--list", "--no-color"])
490            .arg(branch)
491            .get_stdout_lossy()
492            .map(|stdout| !stdout.is_empty())
493    }
494
495    pub fn has_tag(self, tag: impl AsRef<OsStr>, at: &Path) -> Result<bool> {
496        self.cmd_at(at)
497            .args(["tag", "--list"])
498            .arg(tag)
499            .get_stdout_lossy()
500            .map(|stdout| !stdout.is_empty())
501    }
502
503    pub fn has_rev(self, rev: impl AsRef<OsStr>, at: &Path) -> Result<bool> {
504        self.cmd_at(at)
505            .args(["cat-file", "-t"])
506            .arg(rev)
507            .get_stdout_lossy()
508            .map(|stdout| &stdout == "commit")
509    }
510
511    pub fn get_rev(self, tag_or_branch: impl AsRef<OsStr>, at: &Path) -> Result<String> {
512        self.cmd_at(at).args(["rev-list", "-n", "1"]).arg(tag_or_branch).get_stdout_lossy()
513    }
514
515    pub fn ensure_clean(self) -> Result<()> {
516        if self.is_clean()? {
517            Ok(())
518        } else {
519            Err(eyre::eyre!(
520                "\
521The target directory is a part of or on its own an already initialized git repository,
522and it requires clean working and staging areas, including no untracked files.
523
524Check the current git repository's status with `git status`.
525Then, you can track files with `git add ...` and then commit them with `git commit`,
526ignore them in the `.gitignore` file."
527            ))
528        }
529    }
530
531    pub fn commit_hash(self, short: bool, revision: &str) -> Result<String> {
532        self.cmd()
533            .arg("rev-parse")
534            .args(short.then_some("--short"))
535            .arg(revision)
536            .get_stdout_lossy()
537    }
538
539    pub fn tag(self) -> Result<String> {
540        self.cmd().arg("tag").get_stdout_lossy()
541    }
542
543    /// Returns the tag the commit first appeared in.
544    ///
545    /// E.g Take rev = `abc1234`. This commit can be found in multiple releases (tags).
546    /// Consider releases: `v0.1.0`, `v0.2.0`, `v0.3.0` in chronological order, `rev` first appeared
547    /// in `v0.2.0`.
548    ///
549    /// Hence, `tag_for_commit("abc1234")` will return `v0.2.0`.
550    pub fn tag_for_commit(self, rev: &str, at: &Path) -> Result<Option<String>> {
551        self.cmd_at(at)
552            .args(["tag", "--contains"])
553            .arg(rev)
554            .get_stdout_lossy()
555            .map(|stdout| stdout.lines().next().map(str::to_string))
556    }
557
558    /// Returns a list of tuples of submodule paths and their respective branches.
559    ///
560    /// This function reads the `.gitmodules` file and returns the paths of all submodules that have
561    /// a branch. The paths are relative to the Git::root_of(git.root) and not lib/ directory.
562    ///
563    /// `at` is the dir in which the `.gitmodules` file is located, this is the git root.
564    /// `lib` is name of the directory where the submodules are located.
565    pub fn read_submodules_with_branch(
566        self,
567        at: &Path,
568        lib: &OsStr,
569    ) -> Result<HashMap<PathBuf, String>> {
570        // Read the .gitmodules file
571        let gitmodules = foundry_common::fs::read_to_string(at.join(".gitmodules"))?;
572
573        let paths = SUBMODULE_BRANCH_REGEX
574            .captures_iter(&gitmodules)
575            .map(|cap| {
576                let path_str = cap.get(1).unwrap().as_str();
577                let path = PathBuf::from_str(path_str).unwrap();
578                trace!(path = %path.display(), "unstripped path");
579
580                // Keep only the components that come after the lib directory.
581                // This needs to be done because the lockfile uses paths relative foundry project
582                // root whereas .gitmodules use paths relative to the git root which may not be the
583                // project root. e.g monorepo.
584                // Hence, if path is lib/solady, then `lib/solady` is kept. if path is
585                // packages/contract-bedrock/lib/solady, then `lib/solady` is kept.
586                let lib_pos = path.components().find_position(|c| c.as_os_str() == lib);
587                let path = path
588                    .components()
589                    .skip(lib_pos.map(|(i, _)| i).unwrap_or(0))
590                    .collect::<PathBuf>();
591
592                let branch = cap.get(2).unwrap().as_str().to_string();
593                (path, branch)
594            })
595            .collect::<HashMap<_, _>>();
596
597        Ok(paths)
598    }
599
600    pub fn has_missing_dependencies<I, S>(self, paths: I) -> Result<bool>
601    where
602        I: IntoIterator<Item = S>,
603        S: AsRef<OsStr>,
604    {
605        self.cmd()
606            .args(["submodule", "status"])
607            .args(paths)
608            .get_stdout_lossy()
609            .map(|stdout| stdout.lines().any(|line| line.starts_with('-')))
610    }
611
612    /// Returns true if the given path has no submodules by checking `git submodule status`
613    pub fn has_submodules<I, S>(self, paths: I) -> Result<bool>
614    where
615        I: IntoIterator<Item = S>,
616        S: AsRef<OsStr>,
617    {
618        self.cmd()
619            .args(["submodule", "status"])
620            .args(paths)
621            .get_stdout_lossy()
622            .map(|stdout| stdout.trim().lines().next().is_some())
623    }
624
625    pub fn submodule_add(
626        self,
627        force: bool,
628        url: impl AsRef<OsStr>,
629        path: impl AsRef<OsStr>,
630    ) -> Result<()> {
631        self.cmd()
632            .stderr(self.stderr())
633            .args(["submodule", "add"])
634            .args(self.shallow.then_some("--depth=1"))
635            .args(force.then_some("--force"))
636            .arg(url)
637            .arg(path)
638            .exec()
639            .map(drop)
640    }
641
642    pub fn submodule_update<I, S>(
643        self,
644        force: bool,
645        remote: bool,
646        no_fetch: bool,
647        recursive: bool,
648        paths: I,
649    ) -> Result<()>
650    where
651        I: IntoIterator<Item = S>,
652        S: AsRef<OsStr>,
653    {
654        self.cmd()
655            .stderr(self.stderr())
656            .args(["submodule", "update", "--progress", "--init"])
657            .args(self.shallow.then_some("--depth=1"))
658            .args(force.then_some("--force"))
659            .args(remote.then_some("--remote"))
660            .args(no_fetch.then_some("--no-fetch"))
661            .args(recursive.then_some("--recursive"))
662            .args(paths)
663            .exec()
664            .map(drop)
665    }
666
667    pub fn submodule_foreach(self, recursive: bool, cmd: impl AsRef<OsStr>) -> Result<()> {
668        self.cmd()
669            .stderr(self.stderr())
670            .args(["submodule", "foreach"])
671            .args(recursive.then_some("--recursive"))
672            .arg(cmd)
673            .exec()
674            .map(drop)
675    }
676
677    /// If the status is prefix with `-`, the submodule is not initialized.
678    ///
679    /// Ref: <https://git-scm.com/docs/git-submodule#Documentation/git-submodule.txt-status--cached--recursive--ltpathgt82308203>
680    pub fn submodules_unintialized(self) -> Result<bool> {
681        self.cmd()
682            .args(["submodule", "status"])
683            .get_stdout_lossy()
684            .map(|stdout| stdout.lines().any(|line| line.starts_with('-')))
685    }
686
687    /// Initializes the git submodules.
688    pub fn submodule_init(self) -> Result<()> {
689        self.cmd().stderr(self.stderr()).args(["submodule", "init"]).exec().map(drop)
690    }
691
692    pub fn submodules(&self) -> Result<Submodules> {
693        self.cmd().args(["submodule", "status"]).get_stdout_lossy().map(|stdout| stdout.parse())?
694    }
695
696    pub fn submodule_sync(self) -> Result<()> {
697        self.cmd().stderr(self.stderr()).args(["submodule", "sync"]).exec().map(drop)
698    }
699
700    pub fn cmd(self) -> Command {
701        let mut cmd = Self::cmd_no_root();
702        cmd.current_dir(self.root);
703        cmd
704    }
705
706    pub fn cmd_at(self, path: &Path) -> Command {
707        let mut cmd = Self::cmd_no_root();
708        cmd.current_dir(path);
709        cmd
710    }
711
712    pub fn cmd_no_root() -> Command {
713        let mut cmd = Command::new("git");
714        cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
715        cmd
716    }
717
718    // don't set this in cmd() because it's not wanted for all commands
719    fn stderr(self) -> Stdio {
720        if self.quiet { Stdio::piped() } else { Stdio::inherit() }
721    }
722}
723
724/// Deserialized `git submodule status lib/dep` output.
725#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
726pub struct Submodule {
727    /// Current commit hash the submodule is checked out at.
728    rev: String,
729    /// Relative path to the submodule.
730    path: PathBuf,
731}
732
733impl Submodule {
734    pub fn new(rev: String, path: PathBuf) -> Self {
735        Self { rev, path }
736    }
737
738    pub fn rev(&self) -> &str {
739        &self.rev
740    }
741
742    pub fn path(&self) -> &PathBuf {
743        &self.path
744    }
745}
746
747impl FromStr for Submodule {
748    type Err = eyre::Report;
749
750    fn from_str(s: &str) -> Result<Self> {
751        let caps = SUBMODULE_STATUS_REGEX
752            .captures(s)
753            .ok_or_else(|| eyre::eyre!("Invalid submodule status format"))?;
754
755        Ok(Self {
756            rev: caps.get(1).unwrap().as_str().to_string(),
757            path: PathBuf::from(caps.get(2).unwrap().as_str()),
758        })
759    }
760}
761
762/// Deserialized `git submodule status` output.
763#[derive(Debug, Clone, PartialEq, Eq)]
764pub struct Submodules(pub Vec<Submodule>);
765
766impl Submodules {
767    pub fn len(&self) -> usize {
768        self.0.len()
769    }
770
771    pub fn is_empty(&self) -> bool {
772        self.0.is_empty()
773    }
774}
775
776impl FromStr for Submodules {
777    type Err = eyre::Report;
778
779    fn from_str(s: &str) -> Result<Self> {
780        let subs = s.lines().map(str::parse).collect::<Result<Vec<Submodule>>>()?;
781        Ok(Self(subs))
782    }
783}
784
785impl<'a> IntoIterator for &'a Submodules {
786    type Item = &'a Submodule;
787    type IntoIter = std::slice::Iter<'a, Submodule>;
788
789    fn into_iter(self) -> Self::IntoIter {
790        self.0.iter()
791    }
792}
793#[cfg(test)]
794mod tests {
795    use super::*;
796    use foundry_common::fs;
797    use std::{env, fs::File, io::Write};
798    use tempfile::tempdir;
799
800    #[test]
801    fn parse_submodule_status() {
802        let s = "+8829465a08cac423dcf59852f21e448449c1a1a8 lib/openzeppelin-contracts (v4.8.0-791-g8829465a)";
803        let sub = Submodule::from_str(s).unwrap();
804        assert_eq!(sub.rev(), "8829465a08cac423dcf59852f21e448449c1a1a8");
805        assert_eq!(sub.path(), Path::new("lib/openzeppelin-contracts"));
806
807        let s = "-8829465a08cac423dcf59852f21e448449c1a1a8 lib/openzeppelin-contracts";
808        let sub = Submodule::from_str(s).unwrap();
809        assert_eq!(sub.rev(), "8829465a08cac423dcf59852f21e448449c1a1a8");
810        assert_eq!(sub.path(), Path::new("lib/openzeppelin-contracts"));
811
812        let s = "8829465a08cac423dcf59852f21e448449c1a1a8 lib/openzeppelin-contracts";
813        let sub = Submodule::from_str(s).unwrap();
814        assert_eq!(sub.rev(), "8829465a08cac423dcf59852f21e448449c1a1a8");
815        assert_eq!(sub.path(), Path::new("lib/openzeppelin-contracts"));
816    }
817
818    #[test]
819    fn parse_multiline_submodule_status() {
820        let s = r#"+d3db4ef90a72b7d24aa5a2e5c649593eaef7801d lib/forge-std (v1.9.4-6-gd3db4ef)
821+8829465a08cac423dcf59852f21e448449c1a1a8 lib/openzeppelin-contracts (v4.8.0-791-g8829465a)
822"#;
823        let subs = Submodules::from_str(s).unwrap().0;
824        assert_eq!(subs.len(), 2);
825        assert_eq!(subs[0].rev(), "d3db4ef90a72b7d24aa5a2e5c649593eaef7801d");
826        assert_eq!(subs[0].path(), Path::new("lib/forge-std"));
827        assert_eq!(subs[1].rev(), "8829465a08cac423dcf59852f21e448449c1a1a8");
828        assert_eq!(subs[1].path(), Path::new("lib/openzeppelin-contracts"));
829    }
830
831    #[test]
832    fn foundry_path_ext_works() {
833        let p = Path::new("contracts/MyTest.t.sol");
834        assert!(p.is_sol_test());
835        assert!(p.is_sol());
836        let p = Path::new("contracts/Greeter.sol");
837        assert!(!p.is_sol_test());
838    }
839
840    // loads .env from cwd and project dir, See [`find_project_root()`]
841    #[test]
842    fn can_load_dotenv() {
843        let temp = tempdir().unwrap();
844        Git::new(temp.path()).init().unwrap();
845        let cwd_env = temp.path().join(".env");
846        fs::create_file(temp.path().join("foundry.toml")).unwrap();
847        let nested = temp.path().join("nested");
848        fs::create_dir(&nested).unwrap();
849
850        let mut cwd_file = File::create(cwd_env).unwrap();
851        let mut prj_file = File::create(nested.join(".env")).unwrap();
852
853        cwd_file.write_all("TESTCWDKEY=cwd_val".as_bytes()).unwrap();
854        cwd_file.sync_all().unwrap();
855
856        prj_file.write_all("TESTPRJKEY=prj_val".as_bytes()).unwrap();
857        prj_file.sync_all().unwrap();
858
859        let cwd = env::current_dir().unwrap();
860        env::set_current_dir(nested).unwrap();
861        load_dotenv();
862        env::set_current_dir(cwd).unwrap();
863
864        assert_eq!(env::var("TESTCWDKEY").unwrap(), "cwd_val");
865        assert_eq!(env::var("TESTPRJKEY").unwrap(), "prj_val");
866    }
867
868    #[test]
869    fn test_read_gitmodules_regex() {
870        let gitmodules = r#"
871        [submodule "lib/solady"]
872        path = lib/solady
873        url = ""
874        branch = v0.1.0
875        [submodule "lib/openzeppelin-contracts"]
876        path = lib/openzeppelin-contracts
877        url = ""
878        branch = v4.8.0-791-g8829465a
879        [submodule "lib/forge-std"]
880        path = lib/forge-std
881        url = ""
882"#;
883
884        let paths = SUBMODULE_BRANCH_REGEX
885            .captures_iter(gitmodules)
886            .map(|cap| {
887                (
888                    PathBuf::from_str(cap.get(1).unwrap().as_str()).unwrap(),
889                    String::from(cap.get(2).unwrap().as_str()),
890                )
891            })
892            .collect::<HashMap<_, _>>();
893
894        assert_eq!(paths.get(Path::new("lib/solady")).unwrap(), "v0.1.0");
895        assert_eq!(
896            paths.get(Path::new("lib/openzeppelin-contracts")).unwrap(),
897            "v4.8.0-791-g8829465a"
898        );
899
900        let no_branch_gitmodules = r#"
901        [submodule "lib/solady"]
902        path = lib/solady
903        url = ""
904        [submodule "lib/openzeppelin-contracts"]
905        path = lib/openzeppelin-contracts
906        url = ""
907        [submodule "lib/forge-std"]
908        path = lib/forge-std
909        url = ""
910"#;
911        let paths = SUBMODULE_BRANCH_REGEX
912            .captures_iter(no_branch_gitmodules)
913            .map(|cap| {
914                (
915                    PathBuf::from_str(cap.get(1).unwrap().as_str()).unwrap(),
916                    String::from(cap.get(2).unwrap().as_str()),
917                )
918            })
919            .collect::<HashMap<_, _>>();
920
921        assert!(paths.is_empty());
922
923        let branch_in_between = r#"
924        [submodule "lib/solady"]
925        path = lib/solady
926        url = ""
927        [submodule "lib/openzeppelin-contracts"]
928        path = lib/openzeppelin-contracts
929        url = ""
930        branch = v4.8.0-791-g8829465a
931        [submodule "lib/forge-std"]
932        path = lib/forge-std
933        url = ""
934        "#;
935
936        let paths = SUBMODULE_BRANCH_REGEX
937            .captures_iter(branch_in_between)
938            .map(|cap| {
939                (
940                    PathBuf::from_str(cap.get(1).unwrap().as_str()).unwrap(),
941                    String::from(cap.get(2).unwrap().as_str()),
942                )
943            })
944            .collect::<HashMap<_, _>>();
945
946        assert_eq!(paths.len(), 1);
947        assert_eq!(
948            paths.get(Path::new("lib/openzeppelin-contracts")).unwrap(),
949            "v4.8.0-791-g8829465a"
950        );
951    }
952}