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    /// Initialize a Vyper project template.
41    #[arg(long, conflicts_with = "template")]
42    pub vyper: bool,
43
44    /// Use the parent git repository instead of initializing a new one.
45    /// Only valid if the target is in a git repository.
46    #[arg(long, conflicts_with = "template")]
47    pub use_parent_git: bool,
48
49    /// Do not create example contracts (Counter.sol, Counter.t.sol, Counter.s.sol).
50    #[arg(long, conflicts_with = "template")]
51    pub empty: bool,
52
53    #[command(flatten)]
54    pub install: DependencyInstallOpts,
55}
56
57impl InitArgs {
58    pub fn run(self) -> Result<()> {
59        let Self {
60            root,
61            template,
62            branch,
63            install,
64            offline,
65            force,
66            vscode,
67            use_parent_git,
68            vyper,
69            empty,
70        } = self;
71        let DependencyInstallOpts { shallow, no_git, commit } = install;
72
73        // create the root dir if it does not exist
74        if !root.exists() {
75            fs::create_dir_all(&root)?;
76        }
77        let root = dunce::canonicalize(root)?;
78        let git = Git::new(&root).shallow(shallow);
79
80        // if a template is provided, then this command initializes a git repo,
81        // fetches the template repo, and resets the git history to the head of the fetched
82        // repo with no other history
83        if let Some(template) = template {
84            let template = if template.contains("://") {
85                template
86            } else if template.starts_with("github.com/") {
87                "https://".to_string() + &template
88            } else {
89                "https://github.com/".to_string() + &template
90            };
91            sh_println!("Initializing {} from {}...", root.display(), template)?;
92            // initialize the git repository
93            git.init()?;
94
95            // fetch the template - always fetch shallow for templates since git history will be
96            // collapsed. gitmodules will be initialized after the template is fetched
97            git.fetch(true, &template, branch)?;
98
99            // reset git history to the head of the template
100            // first get the commit hash that was fetched
101            let commit_hash = git.commit_hash(true, "FETCH_HEAD")?;
102            // format a commit message for the new repo
103            let commit_msg = format!("chore: init from {template} at {commit_hash}");
104            // get the hash of the FETCH_HEAD with the new commit message
105            let new_commit_hash = git.commit_tree("FETCH_HEAD^{tree}", Some(commit_msg))?;
106            // reset head of this repo to be the head of the template repo
107            git.reset(true, new_commit_hash)?;
108
109            // if shallow, just initialize submodules
110            if shallow {
111                git.submodule_init()?;
112            } else {
113                // if not shallow, initialize and clone submodules (without fetching latest)
114                git.submodule_update(false, false, true, true, std::iter::empty::<PathBuf>())?;
115            }
116        } else {
117            // if target is not empty
118            if root.read_dir().is_ok_and(|mut i| i.next().is_some()) {
119                if !force {
120                    eyre::bail!(
121                        "Cannot run `init` on a non-empty directory.\n\
122                        Run with the `--force` flag to initialize regardless."
123                    );
124                }
125                sh_warn!("Target directory is not empty, but `--force` was specified")?;
126            }
127
128            // ensure git status is clean before generating anything
129            if !no_git && commit && !force && git.is_in_repo()? {
130                git.ensure_clean()?;
131            }
132
133            sh_println!("Initializing {}...", root.display())?;
134
135            // make the dirs
136            let src = root.join("src");
137            fs::create_dir_all(&src)?;
138
139            let test = root.join("test");
140            fs::create_dir_all(&test)?;
141
142            let script = root.join("script");
143            fs::create_dir_all(&script)?;
144
145            // Only create example contracts if not disabled
146            if !empty {
147                if vyper {
148                    // write the contract file
149                    let contract_path = src.join("Counter.vy");
150                    fs::write(
151                        contract_path,
152                        include_str!("../../assets/vyper/CounterTemplate.vy"),
153                    )?;
154                    let interface_path = src.join("ICounter.sol");
155                    fs::write(
156                        interface_path,
157                        include_str!("../../assets/vyper/ICounterTemplate.sol"),
158                    )?;
159
160                    // write the tests
161                    let contract_path = test.join("Counter.t.sol");
162                    fs::write(
163                        contract_path,
164                        include_str!("../../assets/vyper/CounterTemplate.t.sol"),
165                    )?;
166
167                    // write the script
168                    let contract_path = script.join("Counter.s.sol");
169                    fs::write(
170                        contract_path,
171                        include_str!("../../assets/vyper/CounterTemplate.s.sol"),
172                    )?;
173                } else {
174                    // write the contract file
175                    let contract_path = src.join("Counter.sol");
176                    fs::write(
177                        contract_path,
178                        include_str!("../../assets/solidity/CounterTemplate.sol"),
179                    )?;
180
181                    // write the tests
182                    let contract_path = test.join("Counter.t.sol");
183                    fs::write(
184                        contract_path,
185                        include_str!("../../assets/solidity/CounterTemplate.t.sol"),
186                    )?;
187
188                    // write the script
189                    let contract_path = script.join("Counter.s.sol");
190                    fs::write(
191                        contract_path,
192                        include_str!("../../assets/solidity/CounterTemplate.s.sol"),
193                    )?;
194                }
195            }
196
197            // Write the default README file
198            let readme_path = root.join("README.md");
199            fs::write(readme_path, include_str!("../../assets/README.md"))?;
200
201            // write foundry.toml, if it doesn't exist already
202            let dest = root.join(Config::FILE_NAME);
203            let mut config = Config::load_with_root(&root)?;
204            if !dest.exists() {
205                fs::write(dest, config.clone().into_basic().to_string_pretty()?)?;
206            }
207            let git = self.install.git(&config);
208
209            // set up the repo
210            if !no_git {
211                init_git_repo(git, commit, use_parent_git, vyper)?;
212            }
213
214            // install forge-std
215            if !offline {
216                if root.join("lib/forge-std").exists() {
217                    sh_warn!("\"lib/forge-std\" already exists, skipping install...")?;
218                    self.install.install(&mut config, vec![])?;
219                } else {
220                    let dep = "https://github.com/foundry-rs/forge-std".parse()?;
221                    self.install.install(&mut config, vec![dep])?;
222                }
223            }
224
225            // init vscode settings
226            if vscode {
227                init_vscode(&root)?;
228            }
229        }
230
231        sh_println!("{}", "    Initialized forge project".green())?;
232        Ok(())
233    }
234}
235
236/// Initialises `root` as a git repository, if it isn't one already, unless 'use_parent_git' is
237/// true.
238///
239/// Creates `.gitignore` and `.github/workflows/test.yml`, if they don't exist already.
240///
241/// Commits everything in `root` if `commit` is true.
242fn init_git_repo(git: Git<'_>, commit: bool, use_parent_git: bool, vyper: bool) -> Result<()> {
243    // `git init`
244    if !git.is_in_repo()? || (!use_parent_git && !git.is_repo_root()?) {
245        git.init()?;
246    }
247
248    // .gitignore
249    let gitignore = git.root.join(".gitignore");
250    if !gitignore.exists() {
251        fs::write(gitignore, include_str!("../../assets/.gitignoreTemplate"))?;
252    }
253
254    // github workflow
255    let workflow = git.root.join(".github/workflows/test.yml");
256    if !workflow.exists() {
257        fs::create_dir_all(workflow.parent().unwrap())?;
258
259        if vyper {
260            fs::write(workflow, include_str!("../../assets/vyper/workflowTemplate.yml"))?;
261        } else {
262            fs::write(workflow, include_str!("../../assets/solidity/workflowTemplate.yml"))?;
263        }
264    }
265
266    // commit everything
267    if commit {
268        git.add(Some("--all"))?;
269        git.commit("chore: forge init")?;
270    }
271
272    Ok(())
273}
274
275/// initializes the `.vscode/settings.json` file
276fn init_vscode(root: &Path) -> Result<()> {
277    let remappings_file = root.join("remappings.txt");
278    if !remappings_file.exists() {
279        let mut remappings = Remapping::find_many(&root.join("lib"))
280            .into_iter()
281            .map(|r| r.into_relative(root).to_relative_remapping().to_string())
282            .collect::<Vec<_>>();
283        if !remappings.is_empty() {
284            remappings.sort();
285            let content = remappings.join("\n");
286            fs::write(remappings_file, content)?;
287        }
288    }
289
290    let vscode_dir = root.join(".vscode");
291    let settings_file = vscode_dir.join("settings.json");
292    let mut settings = if !vscode_dir.is_dir() {
293        fs::create_dir_all(&vscode_dir)?;
294        serde_json::json!({})
295    } else if settings_file.exists() {
296        foundry_compilers::utils::read_json_file(&settings_file)?
297    } else {
298        serde_json::json!({})
299    };
300
301    let obj = settings.as_object_mut().expect("Expected settings object");
302    // insert [vscode-solidity settings](https://github.com/juanfranblanco/vscode-solidity)
303    let src_key = "solidity.packageDefaultDependenciesContractsDirectory";
304    if !obj.contains_key(src_key) {
305        obj.insert(src_key.to_string(), serde_json::Value::String("src".to_string()));
306    }
307    let lib_key = "solidity.packageDefaultDependenciesDirectory";
308    if !obj.contains_key(lib_key) {
309        obj.insert(lib_key.to_string(), serde_json::Value::String("lib".to_string()));
310    }
311
312    let content = serde_json::to_string_pretty(&settings)?;
313    fs::write(settings_file, content)?;
314
315    Ok(())
316}