git_worktree_cli/core/
project.rs1use crate::error::{Error, Result};
7use std::fs;
8use std::path::{Path, PathBuf};
9
10#[derive(Debug, Clone)]
12pub struct Project {
13 pub root: PathBuf,
15 pub git_dir: PathBuf,
17}
18
19impl Project {
20 pub fn find() -> Result<Self> {
22 let root = find_project_root()?;
23 let git_dir = find_git_directory_from(&root)?;
24 Ok(Self { root, git_dir })
25 }
26
27 pub fn find_from(start_path: &Path) -> Result<Self> {
29 let root = find_project_root_from(start_path)?;
30 let git_dir = find_git_directory_from(&root)?;
31 Ok(Self { root, git_dir })
32 }
33
34 pub fn bare_repo_dir(&self) -> Result<PathBuf> {
36 find_existing_worktree(&self.root)
37 }
38}
39
40pub fn find_project_root() -> Result<PathBuf> {
42 let current_dir = std::env::current_dir().map_err(Error::Io)?;
43 find_project_root_from(¤t_dir)
44}
45
46pub fn find_project_root_from(start_path: &Path) -> Result<PathBuf> {
48 let mut search_path = start_path.to_path_buf();
49
50 loop {
51 if search_path.join("git-worktree-config.jsonc").exists() {
53 if search_path.file_name().and_then(|n| n.to_str()) == Some("main") {
56 if let Some(parent) = search_path.parent() {
57 return Ok(parent.to_path_buf());
58 }
59 }
60 return Ok(search_path);
61 }
62
63 if search_path.join("main").join("git-worktree-config.jsonc").exists() {
66 return Ok(search_path);
67 }
68
69 if !search_path.pop() {
70 break;
71 }
72 }
73
74 if let Ok(Some(_)) = crate::git::get_git_root() {
76 Err(Error::Other(
77 "Found git repository but no git-worktree-config.jsonc. This doesn't appear to be a worktree project."
78 .to_string(),
79 ))
80 } else {
81 Err(Error::ProjectRootNotFound)
82 }
83}
84
85pub fn find_git_directory() -> Result<PathBuf> {
87 let project_root = find_project_root()?;
88 find_git_directory_from(&project_root)
89}
90
91pub fn find_git_directory_from(project_root: &Path) -> Result<PathBuf> {
93 if project_root.join(".git").exists() {
96 return Ok(project_root.to_path_buf());
97 }
98
99 let entries = fs::read_dir(project_root).map_err(Error::Io)?;
100
101 for entry in entries {
102 let entry = entry.map_err(Error::Io)?;
103 if entry.file_type().map_err(Error::Io)?.is_dir() {
104 let dir_path = entry.path();
105 if dir_path.join(".git").exists() {
106 return Ok(dir_path);
108 }
109 }
110 }
111
112 Err(Error::GitDirectoryNotFound)
113}
114
115pub fn find_existing_worktree(project_root: &Path) -> Result<PathBuf> {
120 let root_git_path = project_root.join(".git");
123 if root_git_path.exists() {
124 if root_git_path.is_file() {
125 return Ok(project_root.to_path_buf());
127 } else if root_git_path.is_dir() {
128 }
131 }
132
133 let entries = fs::read_dir(project_root).map_err(Error::Io)?;
134
135 let mut main_repo: Option<PathBuf> = None;
136
137 if root_git_path.exists() && root_git_path.is_dir() {
139 main_repo = Some(project_root.to_path_buf());
140 }
141
142 for entry in entries {
143 let entry = entry.map_err(Error::Io)?;
144 if entry.file_type().map_err(Error::Io)?.is_dir() {
145 let dir_path = entry.path();
146 let git_path = dir_path.join(".git");
147
148 if git_path.exists() {
149 if git_path.is_file() {
150 return Ok(dir_path);
152 } else if git_path.is_dir() {
153 main_repo = Some(dir_path);
155 }
156 }
157 }
158 }
159
160 main_repo.ok_or_else(|| {
162 Error::Other(format!(
163 "No existing git directory found in project at {}. Have you run 'gwt init' yet?",
164 project_root.display()
165 ))
166 })
167}
168
169pub fn is_orphaned_worktree(path: &Path) -> bool {
174 let git_file = path.join(".git");
175
176 if !git_file.is_file() {
178 return false;
179 }
180
181 let Ok(content) = fs::read_to_string(&git_file) else {
183 return false;
184 };
185
186 let Some(gitdir_line) = content.lines().find(|line| line.starts_with("gitdir: ")) else {
188 return false;
189 };
190
191 let gitdir_path = gitdir_line.trim_start_matches("gitdir: ").trim();
192
193 !Path::new(gitdir_path).exists()
195}
196
197pub fn find_valid_git_directory(project_root: &Path) -> Result<PathBuf> {
202 let root_git_path = project_root.join(".git");
204 if root_git_path.is_dir() {
205 return Ok(project_root.to_path_buf());
206 }
207
208 let entries = fs::read_dir(project_root).map_err(Error::Io)?;
210
211 for entry in entries {
212 let entry = entry.map_err(Error::Io)?;
213 if entry.file_type().map_err(Error::Io)?.is_dir() {
214 let dir_path = entry.path();
215 let git_path = dir_path.join(".git");
216
217 if git_path.is_dir() {
218 return Ok(dir_path);
220 } else if git_path.is_file() && !is_orphaned_worktree(&dir_path) {
221 return Ok(dir_path);
223 }
224 }
225 }
226
227 Err(Error::Other(
228 "No valid git directory found in project".to_string()
229 ))
230}
231
232pub fn clean_branch_name(branch: &str) -> &str {
234 branch.trim().strip_prefix("refs/heads/").unwrap_or(branch.trim())
235}