use std::path::{Path, PathBuf};
const PROJECT_ROOT_MARKERS: &[&str] = &[
".git",
"Cargo.toml",
"package.json",
"tsconfig.json",
"go.mod",
"pyproject.toml",
"setup.py",
"requirements.txt",
"pom.xml",
"build.gradle",
"composer.json",
"Gemfile",
];
#[cfg(windows)]
fn create_command(program: &str) -> std::process::Command {
use std::os::windows::process::CommandExt;
const CREATE_NO_WINDOW: u32 = 0x08000000;
let mut cmd = std::process::Command::new(program);
cmd.creation_flags(CREATE_NO_WINDOW);
cmd
}
#[cfg(not(windows))]
fn create_command(program: &str) -> std::process::Command {
std::process::Command::new(program)
}
pub fn find_project_root(start_path: &Path) -> PathBuf {
if let Some(git_root) = find_git_root(start_path) {
return git_root;
}
if let Some(marker_root) = find_by_markers(start_path) {
return marker_root;
}
start_path.to_path_buf()
}
fn find_git_root(start_path: &Path) -> Option<PathBuf> {
let output = create_command("git")
.args(["rev-parse", "--show-toplevel"])
.current_dir(start_path)
.output()
.ok()?;
if output.status.success() {
let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
Some(PathBuf::from(path))
} else {
None
}
}
fn find_by_markers(start_path: &Path) -> Option<PathBuf> {
let mut current = start_path.to_path_buf();
loop {
for marker in PROJECT_ROOT_MARKERS {
let marker_path = current.join(marker);
if marker_path.exists() {
return Some(current);
}
}
if !current.pop() {
break;
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_find_project_root_with_git() {
let start = std::env::current_dir().unwrap();
let root = find_project_root(&start);
assert!(root.exists());
}
}