gitgrip/git/
git2_backend.rs1use std::path::{Path, PathBuf};
9
10use git2::Repository;
11
12use super::backend::{GitBackend, GitRepo};
13use super::remote::SafePullResult;
14use super::status::RepoStatusInfo;
15use super::GitError;
16
17pub struct Git2Backend;
21
22impl GitBackend for Git2Backend {
23 fn open_repo(&self, path: &Path) -> Result<Box<dyn GitRepo>, GitError> {
24 let repo = super::open_repo(path)?;
25 Ok(Box::new(Git2Repo::new(repo)))
26 }
27
28 fn clone_repo(
29 &self,
30 url: &str,
31 path: &Path,
32 branch: Option<&str>,
33 ) -> Result<Box<dyn GitRepo>, GitError> {
34 let repo = super::clone_repo(url, path, branch)?;
35 Ok(Box::new(Git2Repo::new(repo)))
36 }
37
38 fn is_git_repo(&self, path: &Path) -> bool {
39 super::is_git_repo(path)
40 }
41}
42
43struct Git2Repo {
48 repo: Repository,
49 workdir: PathBuf,
51}
52
53impl Git2Repo {
54 fn new(repo: Repository) -> Self {
55 let workdir = super::get_workdir(&repo).to_path_buf();
56 Self { repo, workdir }
57 }
58}
59
60impl GitRepo for Git2Repo {
61 fn workdir(&self) -> &Path {
64 &self.workdir
65 }
66
67 fn current_branch(&self) -> Result<String, GitError> {
68 super::get_current_branch(&self.repo)
69 }
70
71 fn head_commit_id(&self) -> Result<String, GitError> {
72 let head = self
73 .repo
74 .head()
75 .map_err(|e| GitError::Reference(e.to_string()))?;
76 let oid = head
77 .target()
78 .ok_or_else(|| GitError::Reference("HEAD has no target".to_string()))?;
79 Ok(oid.to_string())
80 }
81
82 fn create_and_checkout_branch(&self, name: &str) -> Result<(), GitError> {
85 super::branch::create_and_checkout_branch(&self.repo, name)
86 }
87
88 fn checkout_branch(&self, name: &str) -> Result<(), GitError> {
89 super::branch::checkout_branch(&self.repo, name)
90 }
91
92 fn branch_exists(&self, name: &str) -> bool {
93 super::branch::branch_exists(&self.repo, name)
94 }
95
96 fn remote_branch_exists(&self, name: &str, remote: &str) -> bool {
97 super::branch::remote_branch_exists(&self.repo, name, remote)
98 }
99
100 fn delete_branch(&self, name: &str, force: bool) -> Result<(), GitError> {
101 super::branch::delete_local_branch(&self.repo, name, force)
102 }
103
104 fn list_local_branches(&self) -> Result<Vec<String>, GitError> {
105 super::branch::list_local_branches(&self.repo)
106 }
107
108 fn fetch(&self, remote: &str) -> Result<(), GitError> {
111 super::remote::fetch_remote(&self.repo, remote)
112 }
113
114 fn pull(&self, remote: &str) -> Result<(), GitError> {
115 super::remote::pull_latest(&self.repo, remote)
116 }
117
118 fn push(&self, branch: &str, remote: &str, set_upstream: bool) -> Result<(), GitError> {
119 super::remote::push_branch(&self.repo, branch, remote, set_upstream)
120 }
121
122 fn get_remote_url(&self, remote: &str) -> Result<Option<String>, GitError> {
123 super::remote::get_remote_url(&self.repo, remote)
124 }
125
126 fn status(&self) -> Result<RepoStatusInfo, GitError> {
129 super::status::get_status_info(&self.repo)
130 }
131
132 fn reset_hard(&self, target: &str) -> Result<(), GitError> {
133 super::remote::reset_hard(&self.repo, target)
134 }
135
136 fn safe_pull(&self, default_branch: &str, remote: &str) -> Result<SafePullResult, GitError> {
137 super::remote::safe_pull_latest(&self.repo, default_branch, remote)
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144 use std::fs;
145 use std::process::Command;
146 use tempfile::TempDir;
147
148 fn setup_git_repo() -> TempDir {
149 let temp = TempDir::new().unwrap();
150 let dir = temp.path();
151
152 Command::new("git")
153 .args(["init", "-b", "main"])
154 .current_dir(dir)
155 .output()
156 .unwrap();
157 Command::new("git")
158 .args(["config", "user.email", "test@example.com"])
159 .current_dir(dir)
160 .output()
161 .unwrap();
162 Command::new("git")
163 .args(["config", "user.name", "Test User"])
164 .current_dir(dir)
165 .output()
166 .unwrap();
167 fs::write(dir.join("README.md"), "# Test").unwrap();
168 Command::new("git")
169 .args(["add", "README.md"])
170 .current_dir(dir)
171 .output()
172 .unwrap();
173 Command::new("git")
174 .args(["commit", "-m", "Initial commit"])
175 .current_dir(dir)
176 .output()
177 .unwrap();
178
179 temp
180 }
181
182 #[test]
183 fn test_backend_open_and_read_head() {
184 let temp = setup_git_repo();
185 let backend = Git2Backend;
186
187 let repo = backend.open_repo(temp.path()).unwrap();
188 assert_eq!(repo.current_branch().unwrap(), "main");
189 assert!(!repo.head_commit_id().unwrap().is_empty());
190 assert_eq!(
192 repo.workdir().canonicalize().unwrap(),
193 temp.path().canonicalize().unwrap()
194 );
195 }
196
197 #[test]
198 fn test_backend_is_git_repo() {
199 let temp = setup_git_repo();
200 let backend = Git2Backend;
201
202 assert!(backend.is_git_repo(temp.path()));
203
204 let non_repo = TempDir::new().unwrap();
205 assert!(!backend.is_git_repo(non_repo.path()));
206 }
207
208 #[test]
209 fn test_backend_branch_operations() {
210 let temp = setup_git_repo();
211 let backend = Git2Backend;
212 let repo = backend.open_repo(temp.path()).unwrap();
213
214 assert!(!repo.branch_exists("feature"));
215
216 repo.create_and_checkout_branch("feature").unwrap();
217 assert!(repo.branch_exists("feature"));
218 assert_eq!(repo.current_branch().unwrap(), "feature");
219
220 let branches = repo.list_local_branches().unwrap();
221 assert!(branches.contains(&"main".to_string()));
222 assert!(branches.contains(&"feature".to_string()));
223
224 repo.checkout_branch("main").unwrap();
225 assert_eq!(repo.current_branch().unwrap(), "main");
226
227 repo.delete_branch("feature", false).unwrap();
228 assert!(!repo.branch_exists("feature"));
229 }
230
231 #[test]
232 fn test_backend_status() {
233 let temp = setup_git_repo();
234 let backend = Git2Backend;
235 let repo = backend.open_repo(temp.path()).unwrap();
236
237 let status = repo.status().unwrap();
238 assert!(status.is_clean);
239
240 fs::write(temp.path().join("new.txt"), "hello").unwrap();
242 let status = repo.status().unwrap();
243 assert!(!status.is_clean);
244 assert_eq!(status.untracked.len(), 1);
245 }
246
247 #[test]
248 fn test_backend_open_nonexistent_fails() {
249 let backend = Git2Backend;
250 assert!(backend.open_repo(Path::new("/nonexistent")).is_err());
251 }
252}