forge/cmd/
init.rs

1use super::install::DependencyInstallOpts;
2use clap::{Parser, ValueHint};
3use eyre::Result;
4use foundry_cli::utils::Git;
5use foundry_common::fs;
6use foundry_compilers::artifacts::remappings::Remapping;
7use foundry_config::Config;
8use std::path::{Path, PathBuf};
9use yansi::Paint;
10
11/// CLI arguments for `forge init`.
12#[derive(Clone, Debug, Default, Parser)]
13pub struct InitArgs {
14    /// The root directory of the new project.
15    #[arg(value_hint = ValueHint::DirPath, default_value = ".", value_name = "PATH")]
16    pub root: PathBuf,
17
18    /// The template to start from.
19    #[arg(long, short)]
20    pub template: Option<String>,
21
22    /// Branch argument that can only be used with template option.
23    /// If not specified, the default branch is used.
24    #[arg(long, short, requires = "template")]
25    pub branch: Option<String>,
26
27    /// Do not install dependencies from the network.
28    #[arg(long, conflicts_with = "template", visible_alias = "no-deps")]
29    pub offline: bool,
30
31    /// Create the project even if the specified root directory is not empty.
32    #[arg(long, conflicts_with = "template")]
33    pub force: bool,
34
35    /// Create a .vscode/settings.json file with Solidity settings, and generate a remappings.txt
36    /// file.
37    #[arg(long, conflicts_with = "template")]
38    pub vscode: bool,
39
40    #[command(flatten)]
41    pub install: DependencyInstallOpts,
42}
43
44impl InitArgs {
45    pub fn run(self) -> Result<()> {
46        let Self { root, template, branch, install, offline, force, vscode } = self;
47        let DependencyInstallOpts { shallow, no_git, commit } = install;
48
49        // create the root dir if it does not exist
50        if !root.exists() {
51            fs::create_dir_all(&root)?;
52        }
53        let root = dunce::canonicalize(root)?;
54        let git = Git::new(&root).shallow(shallow);
55
56        // if a template is provided, then this command initializes a git repo,
57        // fetches the template repo, and resets the git history to the head of the fetched
58        // repo with no other history
59        if let Some(template) = template {
60            let template = if template.contains("://") {
61                template
62            } else if template.starts_with("github.com/") {
63                "https://".to_string() + &template
64            } else {
65                "https://github.com/".to_string() + &template
66            };
67            sh_println!("Initializing {} from {}...", root.display(), template)?;
68            // initialize the git repository
69            git.init()?;
70
71            // fetch the template - always fetch shallow for templates since git history will be
72            // collapsed. gitmodules will be initialized after the template is fetched
73            git.fetch(true, &template, branch)?;
74
75            // reset git history to the head of the template
76            // first get the commit hash that was fetched
77            let commit_hash = git.commit_hash(true, "FETCH_HEAD")?;
78            // format a commit message for the new repo
79            let commit_msg = format!("chore: init from {template} at {commit_hash}");
80            // get the hash of the FETCH_HEAD with the new commit message
81            let new_commit_hash = git.commit_tree("FETCH_HEAD^{tree}", Some(commit_msg))?;
82            // reset head of this repo to be the head of the template repo
83            git.reset(true, new_commit_hash)?;
84
85            // if shallow, just initialize submodules
86            if shallow {
87                git.submodule_init()?;
88            } else {
89                // if not shallow, initialize and clone submodules (without fetching latest)
90                git.submodule_update(false, false, true, true, std::iter::empty::<PathBuf>())?;
91            }
92        } else {
93            // if target is not empty
94            if root.read_dir().is_ok_and(|mut i| i.next().is_some()) {
95                if !force {
96                    eyre::bail!(
97                        "Cannot run `init` on a non-empty directory.\n\
98                        Run with the `--force` flag to initialize regardless."
99                    );
100                }
101                sh_warn!("Target directory is not empty, but `--force` was specified")?;
102            }
103
104            // ensure git status is clean before generating anything
105            if !no_git && commit && !force && git.is_in_repo()? {
106                git.ensure_clean()?;
107            }
108
109            sh_println!("Initializing {}...", root.display())?;
110
111            // make the dirs
112            let src = root.join("src");
113            fs::create_dir_all(&src)?;
114
115            let test = root.join("test");
116            fs::create_dir_all(&test)?;
117
118            let script = root.join("script");
119            fs::create_dir_all(&script)?;
120
121            // write the contract file
122            let contract_path = src.join("Counter.sol");
123            fs::write(contract_path, include_str!("../../assets/CounterTemplate.sol"))?;
124            // write the tests
125            let contract_path = test.join("Counter.t.sol");
126            fs::write(contract_path, include_str!("../../assets/CounterTemplate.t.sol"))?;
127            // write the script
128            let contract_path = script.join("Counter.s.sol");
129            fs::write(contract_path, include_str!("../../assets/CounterTemplate.s.sol"))?;
130            // Write the default README file
131            let readme_path = root.join("README.md");
132            fs::write(readme_path, include_str!("../../assets/README.md"))?;
133
134            // write foundry.toml, if it doesn't exist already
135            let dest = root.join(Config::FILE_NAME);
136            let mut config = Config::load_with_root(&root)?;
137            if !dest.exists() {
138                fs::write(dest, config.clone().into_basic().to_string_pretty()?)?;
139            }
140            let git = self.install.git(&config);
141
142            // set up the repo
143            if !no_git {
144                init_git_repo(git, commit)?;
145            }
146
147            // install forge-std
148            if !offline {
149                if root.join("lib/forge-std").exists() {
150                    sh_warn!("\"lib/forge-std\" already exists, skipping install...")?;
151                    self.install.install(&mut config, vec![])?;
152                } else {
153                    let dep = "https://github.com/foundry-rs/forge-std".parse()?;
154                    self.install.install(&mut config, vec![dep])?;
155                }
156            }
157
158            // init vscode settings
159            if vscode {
160                init_vscode(&root)?;
161            }
162        }
163
164        sh_println!("{}", "    Initialized forge project".green())?;
165        Ok(())
166    }
167}
168
169/// Initialises `root` as a git repository, if it isn't one already.
170///
171/// Creates `.gitignore` and `.github/workflows/test.yml`, if they don't exist already.
172///
173/// Commits everything in `root` if `commit` is true.
174fn init_git_repo(git: Git<'_>, commit: bool) -> Result<()> {
175    // git init
176    if !git.is_in_repo()? {
177        git.init()?;
178    }
179
180    // .gitignore
181    let gitignore = git.root.join(".gitignore");
182    if !gitignore.exists() {
183        fs::write(gitignore, include_str!("../../assets/.gitignoreTemplate"))?;
184    }
185
186    // github workflow
187    let workflow = git.root.join(".github/workflows/test.yml");
188    if !workflow.exists() {
189        fs::create_dir_all(workflow.parent().unwrap())?;
190        fs::write(workflow, include_str!("../../assets/workflowTemplate.yml"))?;
191    }
192
193    // commit everything
194    if commit {
195        git.add(Some("--all"))?;
196        git.commit("chore: forge init")?;
197    }
198
199    Ok(())
200}
201
202/// initializes the `.vscode/settings.json` file
203fn init_vscode(root: &Path) -> Result<()> {
204    let remappings_file = root.join("remappings.txt");
205    if !remappings_file.exists() {
206        let mut remappings = Remapping::find_many(&root.join("lib"))
207            .into_iter()
208            .map(|r| r.into_relative(root).to_relative_remapping().to_string())
209            .collect::<Vec<_>>();
210        if !remappings.is_empty() {
211            remappings.sort();
212            let content = remappings.join("\n");
213            fs::write(remappings_file, content)?;
214        }
215    }
216
217    let vscode_dir = root.join(".vscode");
218    let settings_file = vscode_dir.join("settings.json");
219    let mut settings = if !vscode_dir.is_dir() {
220        fs::create_dir_all(&vscode_dir)?;
221        serde_json::json!({})
222    } else if settings_file.exists() {
223        foundry_compilers::utils::read_json_file(&settings_file)?
224    } else {
225        serde_json::json!({})
226    };
227
228    let obj = settings.as_object_mut().expect("Expected settings object");
229    // insert [vscode-solidity settings](https://github.com/juanfranblanco/vscode-solidity)
230    let src_key = "solidity.packageDefaultDependenciesContractsDirectory";
231    if !obj.contains_key(src_key) {
232        obj.insert(src_key.to_string(), serde_json::Value::String("src".to_string()));
233    }
234    let lib_key = "solidity.packageDefaultDependenciesDirectory";
235    if !obj.contains_key(lib_key) {
236        obj.insert(lib_key.to_string(), serde_json::Value::String("lib".to_string()));
237    }
238
239    let content = serde_json::to_string_pretty(&settings)?;
240    fs::write(settings_file, content)?;
241
242    Ok(())
243}