Skip to main content

git_gardener/git/
worktree.rs

1use git2::{Repository, BranchType};
2use std::path::{Path, PathBuf};
3use crate::error::{GitGardenerError, Result};
4use super::status::{GitStatus, WorktreeStatus};
5
6#[derive(Clone)]
7pub struct WorktreeInfo {
8    pub name: String,
9    pub path: PathBuf,
10    pub branch: String,
11    pub is_prunable: bool,
12    pub status: Option<GitStatus>,
13}
14
15// 🔴 RED: ステータス情報付きWorktreeInfo(まだ基本実装のみ)
16#[derive(Clone)]
17pub struct WorktreeInfoWithStatus {
18    pub name: String,
19    pub path: PathBuf,
20    pub branch: String,
21    pub is_prunable: bool,
22    pub status: GitStatus,
23}
24
25pub struct GitWorktree {
26    repo: Repository,
27}
28
29impl GitWorktree {
30    pub fn new() -> Result<Self> {
31        let repo = Repository::open_from_env()
32            .map_err(|_| GitGardenerError::NotInRepository)?;
33        Ok(Self { repo })
34    }
35    
36    pub fn from_path(path: &Path) -> Result<Self> {
37        let repo = Repository::open(path)
38            .map_err(|_| GitGardenerError::NotInRepository)?;
39        Ok(Self { repo })
40    }
41    
42    pub fn create_worktree(
43        &self,
44        _name: &str,
45        path: &Path,
46        branch_name: &str,
47        create_branch: bool,
48    ) -> Result<()> {
49        // 🟢 GREEN: worktree作成の修正版
50        if create_branch && !self.branch_exists(branch_name)? {
51            // 新しいブランチを作成
52            let head = self.repo.head()?;
53            let commit = head.peel_to_commit()?;
54            let _branch = self.repo.branch(branch_name, &commit, false)?;
55        }
56        
57        // gitコマンドを使用してworktreeを作成(より安定した方法)
58        let output = std::process::Command::new("git")
59            .args(&["worktree", "add", &path.to_string_lossy(), branch_name])
60            .output()
61            .map_err(|e| GitGardenerError::Custom(format!("Failed to execute git worktree add: {}", e)))?;
62        
63        if !output.status.success() {
64            let error_msg = String::from_utf8_lossy(&output.stderr);
65            return Err(GitGardenerError::Custom(format!("git worktree add failed: {}", error_msg)));
66        }
67        
68        Ok(())
69    }
70    
71    pub fn create_worktree_with_commit(
72        &self,
73        _name: &str,
74        path: &Path,
75        branch_name: &str,
76        create_branch: bool,
77        commit: Option<&str>,
78    ) -> Result<()> {
79        // gitコマンドを使用してworktreeを作成
80        let mut args = vec!["worktree", "add"];
81        
82        if create_branch {
83            args.push("-b");
84            args.push(branch_name);
85        }
86        
87        let path_str = path.to_string_lossy();
88        args.push(&path_str);
89        
90        if !create_branch {
91            args.push(branch_name);
92        } else if let Some(commit) = commit {
93            args.push(commit);
94        }
95        
96        let output = std::process::Command::new("git")
97            .args(&args)
98            .output()
99            .map_err(|e| GitGardenerError::Custom(format!("Failed to execute git worktree add: {}", e)))?;
100        
101        if !output.status.success() {
102            let error_msg = String::from_utf8_lossy(&output.stderr);
103            return Err(GitGardenerError::Custom(format!("git worktree add failed: {}", error_msg)));
104        }
105        
106        Ok(())
107    }
108    
109    pub fn list_worktrees(&self) -> Result<Vec<WorktreeInfo>> {
110        let worktrees = self.repo.worktrees()?;
111        let mut infos = Vec::new();
112        
113        for worktree_name in worktrees.iter().flatten() {
114            if let Ok(worktree) = self.repo.find_worktree(worktree_name) {
115                let path = worktree.path();
116                let is_prunable = worktree.is_prunable(None).unwrap_or(false);
117                
118                let branch = self.get_worktree_branch(&worktree)?;
119                
120                // ステータス情報を取得(エラーの場合はNone)
121                let status = GitStatus::from_path(&path).ok();
122                
123                infos.push(WorktreeInfo {
124                    name: worktree_name.to_string(),
125                    path: path.to_path_buf(),
126                    branch,
127                    is_prunable,
128                    status,
129                });
130            }
131        }
132        
133        Ok(infos)
134    }
135    
136    pub fn remove_worktree(&self, name: &str, force: bool) -> Result<()> {
137        // 🟢 GREEN: git worktree removeを使用する修正版
138        let worktrees = self.list_worktrees()?;
139        let worktree_info = worktrees
140            .iter()
141            .find(|w| w.name == name)
142            .ok_or_else(|| GitGardenerError::WorktreeNotFound { 
143                name: name.to_string() 
144            })?;
145        
146        // gitコマンドを使用してworktreeを削除
147        let path_str = worktree_info.path.to_string_lossy();
148        let mut args = vec!["worktree", "remove"];
149        if force {
150            args.push("--force");
151        }
152        args.push(&path_str);
153        
154        let output = std::process::Command::new("git")
155            .args(&args)
156            .output()
157            .map_err(|e| GitGardenerError::Custom(format!("Failed to execute git worktree remove: {}", e)))?;
158        
159        if !output.status.success() {
160            let error_msg = String::from_utf8_lossy(&output.stderr);
161            return Err(GitGardenerError::Custom(format!("git worktree remove failed: {}", error_msg)));
162        }
163        
164        Ok(())
165    }
166    
167    pub fn get_repository_root(&self) -> Result<PathBuf> {
168        Ok(self.repo.workdir()
169            .ok_or_else(|| GitGardenerError::Custom(
170                "Could not determine repository root".to_string()
171            ))?
172            .to_path_buf())
173    }
174    
175    fn get_worktree_branch(&self, worktree: &git2::Worktree) -> Result<String> {
176        let worktree_repo = Repository::open(worktree.path())?;
177        
178        if let Ok(head) = worktree_repo.head() {
179            if let Some(name) = head.shorthand() {
180                return Ok(name.to_string());
181            }
182        }
183        
184        Ok("(unknown)".to_string())
185    }
186    
187    pub fn branch_exists(&self, branch_name: &str) -> Result<bool> {
188        let branches = self.repo.branches(Some(BranchType::Local))?;
189        
190        for branch_result in branches {
191            if let Ok((branch, _)) = branch_result {
192                if let Some(name) = branch.name()? {
193                    if name == branch_name {
194                        return Ok(true);
195                    }
196                }
197            }
198        }
199        
200        Ok(false)
201    }
202    
203    // 🟢 GREEN: マージ済みブランチの判定ロジック実装
204    pub fn is_branch_merged(&self, branch_name: &str, base_branch: &str) -> Result<bool> {
205        // ブランチの参照を取得
206        let branch_ref = format!("refs/heads/{}", branch_name);
207        let base_ref = format!("refs/heads/{}", base_branch);
208        
209        let branch_commit = match self.repo.find_reference(&branch_ref) {
210            Ok(reference) => {
211                let oid = reference.target().ok_or_else(|| {
212                    GitGardenerError::Custom(format!("Branch {} has no target", branch_name))
213                })?;
214                self.repo.find_commit(oid)?
215            }
216            Err(_) => {
217                return Ok(false); // ブランチが存在しない場合はfalse
218            }
219        };
220        
221        let base_commit = match self.repo.find_reference(&base_ref) {
222            Ok(reference) => {
223                let oid = reference.target().ok_or_else(|| {
224                    GitGardenerError::Custom(format!("Base branch {} has no target", base_branch))
225                })?;
226                self.repo.find_commit(oid)?
227            }
228            Err(_) => {
229                return Ok(false); // ベースブランチが存在しない場合はfalse
230            }
231        };
232        
233        // ブランチのコミットがベースブランチから到達可能かチェック
234        let is_ancestor = self.repo.graph_descendant_of(base_commit.id(), branch_commit.id())?;
235        
236        Ok(is_ancestor)
237    }
238    
239    // 🟢 GREEN: 古いworktreeかどうかの判定(実装)
240    pub fn is_worktree_stale(&self, branch_name: &str, days: u32) -> Result<bool> {
241        // ブランチの参照を取得
242        let branch_ref = format!("refs/heads/{}", branch_name);
243        
244        let branch_commit = match self.repo.find_reference(&branch_ref) {
245            Ok(reference) => {
246                let oid = reference.target().ok_or_else(|| {
247                    GitGardenerError::Custom(format!("Branch {} has no target", branch_name))
248                })?;
249                self.repo.find_commit(oid)?
250            }
251            Err(_) => {
252                // ブランチが存在しない場合はfalse(削除できない)
253                return Ok(false);
254            }
255        };
256        
257        // 最後のコミット日時を取得
258        let commit_time = branch_commit.time().seconds();
259        
260        // 現在時刻から指定日数前の時刻を計算
261        let now = std::time::SystemTime::now()
262            .duration_since(std::time::UNIX_EPOCH)
263            .unwrap()
264            .as_secs() as i64;
265        let threshold_time = now - (days as i64 * 24 * 60 * 60);
266        
267        // コミット時刻が閾値より古いかどうかを判定
268        Ok(commit_time < threshold_time)
269    }
270    
271    // 🔴 RED: ステータス情報付きワーキングツリー一覧取得(まだ基本実装のみ)
272    pub fn list_worktrees_with_status(&self) -> Result<Vec<WorktreeInfoWithStatus>> {
273        let worktrees = self.list_worktrees()?;
274        let mut result = Vec::new();
275        
276        for worktree in worktrees {
277            // 各ワーキングツリーのステータスを取得
278            let status = GitStatus::from_path(&worktree.path).unwrap_or_else(|_| {
279                // エラーの場合はデフォルトステータスを使用
280                GitStatus {
281                    working_tree_status: WorktreeStatus::Clean,
282                    has_staged_changes: false,
283                    has_unstaged_changes: false,
284                    last_commit_time: None,
285                    ahead_count: 0,
286                    behind_count: 0,
287                }
288            });
289            
290            result.push(WorktreeInfoWithStatus {
291                name: worktree.name,
292                path: worktree.path,
293                branch: worktree.branch,
294                is_prunable: worktree.is_prunable,
295                status,
296            });
297        }
298        
299        Ok(result)
300    }
301}
302
303#[cfg(test)]
304mod tests {
305    use super::*;
306    use tempfile::tempdir;
307    
308    #[test]
309    fn test_not_in_repository() {
310        let temp_dir = tempdir().unwrap();
311        std::env::set_current_dir(&temp_dir).unwrap();
312        
313        let result = GitWorktree::new();
314        assert!(matches!(result, Err(GitGardenerError::NotInRepository)));
315    }
316}