thoughts_tool/git/
utils.rs1use crate::error::ThoughtsError;
2use anyhow::Result;
3use git2::Repository;
4use std::path::{Path, PathBuf};
5use tracing::debug;
6
7pub fn get_current_repo() -> Result<PathBuf> {
9 let current_dir = std::env::current_dir()?;
10 find_repo_root(¤t_dir)
11}
12
13pub fn find_repo_root(start_path: &Path) -> Result<PathBuf> {
15 let repo = Repository::discover(start_path).map_err(|_| ThoughtsError::NotInGitRepo)?;
16
17 let workdir = repo
18 .workdir()
19 .ok_or_else(|| anyhow::anyhow!("Repository has no working directory"))?;
20
21 Ok(workdir.to_path_buf())
22}
23
24pub fn is_worktree(repo_path: &Path) -> Result<bool> {
26 let _repo = Repository::open(repo_path)?;
27
28 let git_path = repo_path.join(".git");
30 if git_path.is_file() {
31 debug!("Found .git file, this is a worktree");
33 return Ok(true);
34 }
35
36 Ok(false)
37}
38
39pub fn get_main_repo_for_worktree(worktree_path: &Path) -> Result<PathBuf> {
41 let _repo = Repository::open(worktree_path)?;
42
43 let git_file = worktree_path.join(".git");
46 if git_file.is_file() {
47 let contents = std::fs::read_to_string(&git_file)?;
48 if let Some(gitdir_line) = contents.lines().find(|l| l.starts_with("gitdir:")) {
49 let gitdir = gitdir_line.trim_start_matches("gitdir:").trim();
50 let gitdir_path = PathBuf::from(gitdir);
51
52 if let Some(parent) = gitdir_path.parent()
54 && let Some(parent_parent) = parent.parent()
55 && parent_parent.ends_with(".git")
56 && let Some(main_repo) = parent_parent.parent()
57 {
58 debug!("Found main repo at: {:?}", main_repo);
59 return Ok(main_repo.to_path_buf());
60 }
61 }
62 }
63
64 Ok(worktree_path.to_path_buf())
66}
67
68pub fn get_control_repo_root(start_path: &Path) -> Result<PathBuf> {
71 let repo_root = find_repo_root(start_path)?;
72 if is_worktree(&repo_root)? {
73 Ok(get_main_repo_for_worktree(&repo_root).unwrap_or(repo_root))
75 } else {
76 Ok(repo_root)
77 }
78}
79
80pub fn get_current_control_repo_root() -> Result<PathBuf> {
82 let cwd = std::env::current_dir()?;
83 get_control_repo_root(&cwd)
84}
85
86pub fn is_git_repo(path: &Path) -> bool {
88 Repository::open(path).is_ok()
89}
90
91#[allow(dead_code)]
93pub fn init_repo(path: &Path) -> Result<Repository> {
95 Ok(Repository::init(path)?)
96}
97
98pub fn get_remote_url(repo_path: &Path) -> Result<String> {
100 let repo = Repository::open(repo_path)
101 .map_err(|e| anyhow::anyhow!("Failed to open git repository at {:?}: {}", repo_path, e))?;
102
103 let remote = repo
104 .find_remote("origin")
105 .map_err(|_| anyhow::anyhow!("No 'origin' remote found"))?;
106
107 remote
108 .url()
109 .ok_or_else(|| anyhow::anyhow!("Remote 'origin' has no URL"))
110 .map(|s| s.to_string())
111}
112
113pub fn get_current_branch(repo_path: &Path) -> Result<String> {
115 let repo = Repository::open(repo_path)
116 .map_err(|e| anyhow::anyhow!("Failed to open git repository at {:?}: {}", repo_path, e))?;
117
118 let head = repo
119 .head()
120 .map_err(|e| anyhow::anyhow!("Failed to get HEAD reference: {}", e))?;
121
122 if head.is_branch() {
123 Ok(head.shorthand().unwrap_or("unknown").to_string())
124 } else {
125 Ok("detached".to_string())
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132 use tempfile::TempDir;
133
134 #[test]
135 fn test_is_git_repo() {
136 let temp_dir = TempDir::new().unwrap();
137 let repo_path = temp_dir.path();
138
139 assert!(!is_git_repo(repo_path));
140
141 Repository::init(repo_path).unwrap();
142 assert!(is_git_repo(repo_path));
143 }
144
145 #[test]
146 fn test_get_current_branch() {
147 let temp_dir = TempDir::new().unwrap();
148 let repo_path = temp_dir.path();
149
150 let repo = Repository::init(repo_path).unwrap();
152
153 let sig = git2::Signature::now("Test", "test@example.com").unwrap();
155 let tree_id = {
156 let mut index = repo.index().unwrap();
157 index.write_tree().unwrap()
158 };
159 let tree = repo.find_tree(tree_id).unwrap();
160 repo.commit(Some("HEAD"), &sig, &sig, "Initial commit", &tree, &[])
161 .unwrap();
162
163 let branch = get_current_branch(repo_path).unwrap();
165 assert!(branch == "master" || branch == "main");
166
167 let head = repo.head().unwrap();
169 let commit = head.peel_to_commit().unwrap();
170 repo.branch("feature-branch", &commit, false).unwrap();
171 repo.set_head("refs/heads/feature-branch").unwrap();
172 repo.checkout_head(None).unwrap();
173
174 let branch = get_current_branch(repo_path).unwrap();
175 assert_eq!(branch, "feature-branch");
176
177 let commit_oid = commit.id();
179 repo.set_head_detached(commit_oid).unwrap();
180 let branch = get_current_branch(repo_path).unwrap();
181 assert_eq!(branch, "detached");
182 }
183}