use super::path_utils::canonicalize_path;
use super::types::{ProjectError, ProjectRootMode};
use std::path::{Path, PathBuf};
#[must_use]
pub fn find_git_root(file_path: &Path) -> Option<PathBuf> {
let mut current = file_path;
loop {
let git_dir = current.join(".git");
if git_dir.exists() {
return Some(current.to_path_buf());
}
match current.parent() {
Some(parent) => current = parent,
None => break, }
}
None
}
pub fn resolve_index_root(
file_path: &Path,
mode: ProjectRootMode,
workspace_folders: &[PathBuf],
) -> Result<PathBuf, ProjectError> {
match mode {
ProjectRootMode::GitRoot => resolve_git_root_mode(file_path, workspace_folders),
ProjectRootMode::WorkspaceFolder => {
resolve_workspace_folder_mode(file_path, workspace_folders)
}
ProjectRootMode::WorkspaceRoot => resolve_workspace_root_mode(file_path, workspace_folders),
}
}
fn resolve_git_root_mode(
file_path: &Path,
workspace_folders: &[PathBuf],
) -> Result<PathBuf, ProjectError> {
if let Some(git_root) = find_git_root(file_path) {
log::debug!(
"Found git root for '{}': '{}'",
file_path.display(),
git_root.display()
);
return Ok(git_root);
}
if !workspace_folders.is_empty() {
if let Some(folder) = find_containing_workspace_folder(file_path, workspace_folders) {
log::info!(
"No git root for '{}', using workspace folder '{}'",
file_path.display(),
folder.display()
);
return Ok(folder);
}
let first_folder = &workspace_folders[0];
log::warn!(
"File '{}' outside all workspace folders, using first folder '{}' as root",
file_path.display(),
first_folder.display()
);
return Ok(first_folder.clone());
}
if let Some(parent) = file_path.parent() {
if parent.as_os_str().is_empty() {
log::info!(
"No workspace folders, using current directory as root for '{}'",
file_path.display()
);
return Ok(PathBuf::from("."));
}
log::info!(
"No workspace folders, using parent directory '{}' as root",
parent.display()
);
return Ok(parent.to_path_buf());
}
Err(ProjectError::no_git_root(file_path))
}
fn resolve_workspace_folder_mode(
file_path: &Path,
workspace_folders: &[PathBuf],
) -> Result<PathBuf, ProjectError> {
if workspace_folders.is_empty() {
return file_path
.parent()
.map(Path::to_path_buf)
.ok_or_else(|| ProjectError::no_git_root(file_path));
}
if let Some(folder) = find_containing_workspace_folder(file_path, workspace_folders) {
return Ok(folder);
}
let first_folder = &workspace_folders[0];
log::warn!(
"File '{}' outside all workspace folders, using first folder '{}' as root",
file_path.display(),
first_folder.display()
);
Ok(first_folder.clone())
}
fn resolve_workspace_root_mode(
file_path: &Path,
workspace_folders: &[PathBuf],
) -> Result<PathBuf, ProjectError> {
if workspace_folders.is_empty() {
return file_path
.parent()
.map(Path::to_path_buf)
.ok_or_else(|| ProjectError::no_git_root(file_path));
}
Ok(workspace_folders[0].clone())
}
fn find_containing_workspace_folder(
file_path: &Path,
workspace_folders: &[PathBuf],
) -> Option<PathBuf> {
for folder in workspace_folders {
if file_path.starts_with(folder) {
return Some(folder.clone());
}
}
None
}
pub fn canonicalize_and_resolve(
file_path: &Path,
mode: ProjectRootMode,
workspace_folders: &[PathBuf],
) -> Result<PathBuf, ProjectError> {
let canonical = canonicalize_path(file_path)
.map_err(|e| ProjectError::canonicalization_failed(file_path, e))?;
resolve_index_root(&canonical, mode, workspace_folders)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn tempdir_outside_git_repo() -> TempDir {
#[cfg(unix)]
fn is_in_git_repo(path: &Path) -> bool {
path.ancestors()
.any(|ancestor| ancestor.join(".git").is_dir())
}
#[cfg(unix)]
{
for base in [Path::new("/var/tmp"), Path::new("/dev/shm")] {
if base.is_dir()
&& !is_in_git_repo(base)
&& let Ok(tmp) = TempDir::new_in(base)
{
return tmp;
}
}
}
TempDir::new().expect("create temp dir")
}
fn setup_git_repo(temp: &TempDir) -> PathBuf {
let git_dir = temp.path().join(".git");
std::fs::create_dir(&git_dir).unwrap();
temp.path().to_path_buf()
}
#[test]
fn test_find_git_root_exists() {
let temp = TempDir::new().unwrap();
let repo_root = setup_git_repo(&temp);
let subdir = repo_root.join("src");
std::fs::create_dir(&subdir).unwrap();
let file = subdir.join("main.rs");
std::fs::write(&file, "fn main() {}").unwrap();
let git_root = find_git_root(&file);
assert!(git_root.is_some());
assert_eq!(git_root.unwrap(), repo_root);
}
#[test]
fn test_find_git_root_not_exists() {
let temp = tempdir_outside_git_repo();
let file = temp.path().join("loose_file.rs");
std::fs::write(&file, "fn main() {}").unwrap();
let git_root = find_git_root(&file);
assert!(git_root.is_none());
}
#[test]
fn test_find_git_root_nested_repos() {
let temp = TempDir::new().unwrap();
let outer_git = temp.path().join(".git");
std::fs::create_dir(&outer_git).unwrap();
let inner = temp.path().join("inner");
std::fs::create_dir(&inner).unwrap();
let inner_git = inner.join(".git");
std::fs::create_dir(&inner_git).unwrap();
let file = inner.join("lib.rs");
std::fs::write(&file, "pub fn foo() {}").unwrap();
let git_root = find_git_root(&file);
assert!(git_root.is_some());
assert_eq!(git_root.unwrap(), inner);
}
#[test]
fn test_resolve_git_root_mode_with_git() {
let temp = TempDir::new().unwrap();
let repo_root = setup_git_repo(&temp);
let file = repo_root.join("file.rs");
std::fs::write(&file, "").unwrap();
let result = resolve_git_root_mode(&file, &[]).unwrap();
assert_eq!(result, repo_root);
}
#[test]
fn test_resolve_git_root_mode_no_git_with_workspace() {
let temp = tempdir_outside_git_repo();
let file = temp.path().join("file.rs");
std::fs::write(&file, "").unwrap();
let workspace_folders = vec![temp.path().to_path_buf()];
let result = resolve_git_root_mode(&file, &workspace_folders).unwrap();
assert_eq!(result, temp.path());
}
#[test]
fn test_resolve_git_root_mode_file_outside_workspace() {
let temp1 = tempdir_outside_git_repo();
let temp2 = tempdir_outside_git_repo();
let file = temp1.path().join("file.rs");
std::fs::write(&file, "").unwrap();
let workspace_folders = vec![temp2.path().to_path_buf()];
let result = resolve_git_root_mode(&file, &workspace_folders).unwrap();
assert_eq!(result, temp2.path());
}
#[test]
fn test_resolve_git_root_mode_single_file_mode() {
let temp = tempdir_outside_git_repo();
let file = temp.path().join("file.rs");
std::fs::write(&file, "").unwrap();
let result = resolve_git_root_mode(&file, &[]).unwrap();
assert_eq!(result, temp.path());
}
#[test]
fn test_resolve_workspace_folder_mode() {
let temp = TempDir::new().unwrap();
let folder1 = temp.path().join("proj1");
let folder2 = temp.path().join("proj2");
std::fs::create_dir(&folder1).unwrap();
std::fs::create_dir(&folder2).unwrap();
let file = folder1.join("src").join("main.rs");
std::fs::create_dir_all(file.parent().unwrap()).unwrap();
std::fs::write(&file, "").unwrap();
let workspace_folders = vec![folder1.clone(), folder2];
let result = resolve_workspace_folder_mode(&file, &workspace_folders).unwrap();
assert_eq!(result, folder1);
}
#[test]
fn test_resolve_workspace_root_mode() {
let temp = TempDir::new().unwrap();
let folder1 = temp.path().join("proj1");
let folder2 = temp.path().join("proj2");
std::fs::create_dir(&folder1).unwrap();
std::fs::create_dir(&folder2).unwrap();
let file = folder2.join("file.rs");
std::fs::write(&file, "").unwrap();
let workspace_folders = vec![folder1.clone(), folder2];
let result = resolve_workspace_root_mode(&file, &workspace_folders).unwrap();
assert_eq!(result, folder1);
}
#[test]
fn test_resolve_index_root_delegates_correctly() {
let temp = TempDir::new().unwrap();
let repo_root = setup_git_repo(&temp);
let file = repo_root.join("file.rs");
std::fs::write(&file, "").unwrap();
let result = resolve_index_root(&file, ProjectRootMode::GitRoot, &[]).unwrap();
assert_eq!(result, repo_root);
let result = resolve_index_root(&file, ProjectRootMode::WorkspaceFolder, &[]).unwrap();
assert_eq!(result, repo_root);
let result = resolve_index_root(&file, ProjectRootMode::WorkspaceRoot, &[]).unwrap();
assert_eq!(result, repo_root);
}
#[test]
fn test_find_containing_workspace_folder() {
let temp = TempDir::new().unwrap();
let folder1 = temp.path().join("a");
let folder2 = temp.path().join("b");
std::fs::create_dir(&folder1).unwrap();
std::fs::create_dir(&folder2).unwrap();
let file_in_a = folder1.join("file.rs");
let file_in_b = folder2.join("file.rs");
let file_outside = temp.path().join("file.rs");
let workspace_folders = vec![folder1.clone(), folder2.clone()];
assert_eq!(
find_containing_workspace_folder(&file_in_a, &workspace_folders),
Some(folder1)
);
assert_eq!(
find_containing_workspace_folder(&file_in_b, &workspace_folders),
Some(folder2)
);
assert_eq!(
find_containing_workspace_folder(&file_outside, &workspace_folders),
None
);
}
#[test]
fn test_workspace_folder_order_preserved() {
let temp = TempDir::new().unwrap();
let folder_z = temp.path().join("z_folder");
let folder_a = temp.path().join("a_folder");
std::fs::create_dir(&folder_z).unwrap();
std::fs::create_dir(&folder_a).unwrap();
let file_outside = temp.path().join("file.rs");
std::fs::write(&file_outside, "").unwrap();
let folders = vec![folder_z.clone(), folder_a];
let result = resolve_workspace_folder_mode(&file_outside, &folders).unwrap();
assert_eq!(result, folder_z);
}
#[test]
fn test_canonicalize_and_resolve() {
let temp = TempDir::new().unwrap();
let repo_root = setup_git_repo(&temp);
let file = repo_root.join("file.rs");
std::fs::write(&file, "").unwrap();
let non_canonical = repo_root.join(".").join("file.rs");
let result = canonicalize_and_resolve(&non_canonical, ProjectRootMode::GitRoot, &[]);
assert!(result.is_ok());
let resolved = result.unwrap();
assert_eq!(
canonicalize_path(&resolved).unwrap(),
canonicalize_path(&repo_root).unwrap()
);
}
}