use crate::error::{Error, Result};
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct Project {
pub root: PathBuf,
pub git_dir: PathBuf,
}
impl Project {
pub fn find() -> Result<Self> {
let root = find_project_root()?;
let git_dir = find_git_directory_from(&root)?;
Ok(Self { root, git_dir })
}
pub fn find_from(start_path: &Path) -> Result<Self> {
let root = find_project_root_from(start_path)?;
let git_dir = find_git_directory_from(&root)?;
Ok(Self { root, git_dir })
}
pub fn bare_repo_dir(&self) -> Result<PathBuf> {
find_existing_worktree(&self.root)
}
}
pub fn find_project_root() -> Result<PathBuf> {
let current_dir = std::env::current_dir().map_err(Error::Io)?;
find_project_root_from(¤t_dir)
}
pub fn find_project_root_from(start_path: &Path) -> Result<PathBuf> {
let mut search_path = start_path.to_path_buf();
loop {
if search_path.join("git-worktree-config.jsonc").exists() {
if search_path.file_name().and_then(|n| n.to_str()) == Some("main") {
if let Some(parent) = search_path.parent() {
return Ok(parent.to_path_buf());
}
}
return Ok(search_path);
}
if search_path.join("main").join("git-worktree-config.jsonc").exists() {
return Ok(search_path);
}
if !search_path.pop() {
break;
}
}
if let Ok(Some(_)) = crate::git::get_git_root() {
Err(Error::Other(
"Found git repository but no git-worktree-config.jsonc. This doesn't appear to be a worktree project."
.to_string(),
))
} else {
Err(Error::ProjectRootNotFound)
}
}
pub fn find_git_directory() -> Result<PathBuf> {
let project_root = find_project_root()?;
find_git_directory_from(&project_root)
}
pub fn find_git_directory_from(project_root: &Path) -> Result<PathBuf> {
if project_root.join(".git").exists() {
return Ok(project_root.to_path_buf());
}
let entries = fs::read_dir(project_root).map_err(Error::Io)?;
for entry in entries {
let entry = entry.map_err(Error::Io)?;
if entry.file_type().map_err(Error::Io)?.is_dir() {
let dir_path = entry.path();
if dir_path.join(".git").exists() {
return Ok(dir_path);
}
}
}
Err(Error::GitDirectoryNotFound)
}
pub fn find_existing_worktree(project_root: &Path) -> Result<PathBuf> {
let root_git_path = project_root.join(".git");
if root_git_path.exists() {
if root_git_path.is_file() {
return Ok(project_root.to_path_buf());
} else if root_git_path.is_dir() {
}
}
let entries = fs::read_dir(project_root).map_err(Error::Io)?;
let mut main_repo: Option<PathBuf> = None;
if root_git_path.exists() && root_git_path.is_dir() {
main_repo = Some(project_root.to_path_buf());
}
for entry in entries {
let entry = entry.map_err(Error::Io)?;
if entry.file_type().map_err(Error::Io)?.is_dir() {
let dir_path = entry.path();
let git_path = dir_path.join(".git");
if git_path.exists() {
if git_path.is_file() {
return Ok(dir_path);
} else if git_path.is_dir() {
main_repo = Some(dir_path);
}
}
}
}
main_repo.ok_or_else(|| {
Error::Other(format!(
"No existing git directory found in project at {}. Have you run 'gwt init' yet?",
project_root.display()
))
})
}
pub fn is_orphaned_worktree(path: &Path) -> bool {
let git_file = path.join(".git");
if !git_file.is_file() {
return false;
}
let Ok(content) = fs::read_to_string(&git_file) else {
return false;
};
let Some(gitdir_line) = content.lines().find(|line| line.starts_with("gitdir: ")) else {
return false;
};
let gitdir_path = gitdir_line.trim_start_matches("gitdir: ").trim();
!Path::new(gitdir_path).exists()
}
pub fn find_valid_git_directory(project_root: &Path) -> Result<PathBuf> {
let root_git_path = project_root.join(".git");
if root_git_path.is_dir() {
return Ok(project_root.to_path_buf());
}
let entries = fs::read_dir(project_root).map_err(Error::Io)?;
for entry in entries {
let entry = entry.map_err(Error::Io)?;
if entry.file_type().map_err(Error::Io)?.is_dir() {
let dir_path = entry.path();
let git_path = dir_path.join(".git");
if git_path.is_dir() {
return Ok(dir_path);
} else if git_path.is_file() && !is_orphaned_worktree(&dir_path) {
return Ok(dir_path);
}
}
}
Err(Error::Other(
"No valid git directory found in project".to_string()
))
}
pub fn clean_branch_name(branch: &str) -> &str {
branch.trim().strip_prefix("refs/heads/").unwrap_or(branch.trim())
}