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