rskit_git/embedded/
mod.rs1pub mod auth;
4mod manage;
5mod read;
6mod write;
7
8use std::path::{Path, PathBuf};
9use std::time::{Duration, SystemTime, UNIX_EPOCH};
10
11use rskit_errors::AppResult;
12
13use crate::core::Repository as RepositoryTrait;
14use crate::error::GitError;
15use crate::types::{Commit, Oid, Reference, Signature};
16
17pub struct Git2Repository {
19 pub(crate) repo: git2::Repository,
20 root: PathBuf,
21}
22
23impl Git2Repository {
24 pub fn root(&self) -> &Path {
26 &self.root
27 }
28}
29
30pub fn open(path: impl AsRef<Path>) -> AppResult<Git2Repository> {
32 let path = path.as_ref();
33 let abs = std::fs::canonicalize(path).map_err(|err| match err.kind() {
34 std::io::ErrorKind::NotFound => GitError::NotFound {
35 path: path.to_path_buf(),
36 },
37 _ => GitError::Internal(git2::Error::from_str(&err.to_string())),
38 })?;
39 let repo = git2::Repository::open(&abs).map_err(|err| {
40 if err.code() == git2::ErrorCode::NotFound {
41 GitError::NotFound { path: abs.clone() }
42 } else {
43 GitError::Internal(err)
44 }
45 })?;
46 let root = repo.workdir().unwrap_or_else(|| repo.path()).to_path_buf();
48 Ok(Git2Repository { repo, root })
49}
50
51pub fn discover(path: impl AsRef<Path>) -> AppResult<Git2Repository> {
53 let path = path.as_ref();
54 let repo = git2::Repository::discover(path).map_err(|err| {
55 if err.code() == git2::ErrorCode::NotFound {
56 GitError::NotFound {
57 path: path.to_path_buf(),
58 }
59 } else {
60 GitError::Internal(err)
61 }
62 })?;
63 let root = repo.workdir().unwrap_or_else(|| repo.path()).to_path_buf();
65 Ok(Git2Repository { repo, root })
66}
67
68pub fn clone(url: &str, path: impl AsRef<Path>) -> AppResult<Git2Repository> {
70 let path = path.as_ref();
71 let repo = git2::Repository::clone(url, path).map_err(GitError::Internal)?;
72 let root = repo
73 .workdir()
74 .map(Path::to_path_buf)
75 .unwrap_or_else(|| path.to_path_buf());
76 Ok(Git2Repository { repo, root })
77}
78
79pub fn init(path: impl AsRef<Path>) -> AppResult<Git2Repository> {
81 let path = path.as_ref();
82 let repo = git2::Repository::init(path).map_err(GitError::Internal)?;
83 let root = repo
84 .workdir()
85 .map(Path::to_path_buf)
86 .unwrap_or_else(|| path.to_path_buf());
87 Ok(Git2Repository { repo, root })
88}
89
90pub fn init_bare(path: impl AsRef<Path>) -> AppResult<Git2Repository> {
92 let path = path.as_ref();
93 let repo = git2::Repository::init_bare(path).map_err(GitError::Internal)?;
94 Ok(Git2Repository {
95 repo,
96 root: path.to_path_buf(),
97 })
98}
99
100impl RepositoryTrait for Git2Repository {
101 fn root(&self) -> &Path {
102 &self.root
103 }
104
105 fn head(&self) -> AppResult<Reference> {
106 let head = self.repo.head().map_err(map_head_error)?;
107 Ok(reference_from_git2(&head))
108 }
109
110 fn resolve_ref(&self, refname: &str) -> AppResult<Oid> {
111 let obj = self
112 .repo
113 .revparse_single(refname)
114 .map_err(|_| GitError::RefNotFound {
115 refname: refname.to_string(),
116 })?;
117 Ok(oid_from_git2(obj.id()))
118 }
119
120 fn is_dirty(&self) -> AppResult<bool> {
121 let statuses = self.repo.statuses(None).map_err(GitError::Internal)?;
122 Ok(!statuses.is_empty())
123 }
124}
125
126pub(crate) fn oid_from_git2(oid: git2::Oid) -> Oid {
127 let mut bytes = [0u8; 20];
128 bytes.copy_from_slice(oid.as_bytes());
129 Oid::from_bytes(bytes)
130}
131
132pub(crate) fn signature_from_git2(signature: &git2::Signature<'_>) -> Signature {
133 Signature {
134 name: signature.name().unwrap_or_default().to_string(),
135 email: signature.email().unwrap_or_default().to_string(),
136 when: system_time_from_git2(signature.when()),
137 }
138}
139
140pub(crate) fn commit_from_git2(commit: &git2::Commit<'_>) -> Commit {
141 Commit {
142 oid: oid_from_git2(commit.id()),
143 author: signature_from_git2(&commit.author()),
144 committer: signature_from_git2(&commit.committer()),
145 message: commit
146 .message()
147 .unwrap_or_default()
148 .trim_end_matches('\n')
149 .trim_end_matches('\0')
150 .to_string(),
151 parents: commit.parent_ids().map(oid_from_git2).collect(),
152 }
153}
154
155fn reference_from_git2(reference: &git2::Reference<'_>) -> Reference {
156 let name = reference.name().unwrap_or("HEAD").to_string();
157 let target = reference
158 .resolve()
159 .ok()
160 .and_then(|resolved| resolved.target())
161 .or_else(|| reference.target())
162 .map(oid_from_git2)
163 .unwrap_or_else(|| Oid::from_bytes([0; 20]));
164 Reference {
165 name,
166 target,
167 is_branch: reference.is_branch(),
168 is_tag: reference.is_tag(),
169 }
170}
171
172fn map_head_error(err: git2::Error) -> GitError {
173 if err.code() == git2::ErrorCode::UnbornBranch || err.code() == git2::ErrorCode::NotFound {
174 GitError::RefNotFound {
175 refname: "HEAD".to_string(),
176 }
177 } else {
178 GitError::Internal(err)
179 }
180}
181
182pub(crate) fn map_remote_error(err: git2::Error) -> GitError {
183 if err.class() == git2::ErrorClass::Net {
184 GitError::Network(err.message().to_string())
185 } else {
186 GitError::Internal(err)
187 }
188}
189
190fn system_time_from_git2(time: git2::Time) -> SystemTime {
191 let seconds = time.seconds();
192 if seconds >= 0 {
193 UNIX_EPOCH + Duration::from_secs(seconds as u64)
194 } else {
195 UNIX_EPOCH - Duration::from_secs(seconds.unsigned_abs())
196 }
197}
198
199#[cfg(test)]
200mod tests {
201 use crate::core::Repository;
202
203 use super::*;
204
205 #[test]
206 fn head_on_unborn_repository_maps_to_ref_not_found() {
207 let root = rskit_testutil::test_workspace!("git2-unborn-head");
208 let repo = init(root.path()).expect("init repo");
209
210 let err = repo.head().expect_err("HEAD is unborn");
211
212 assert_eq!(err.code(), rskit_errors::ErrorCode::NotFound);
213 }
214
215 #[test]
216 fn system_time_from_git2_handles_negative_offsets() {
217 let before_epoch = system_time_from_git2(git2::Time::new(-5, 0));
218
219 assert_eq!(before_epoch, UNIX_EPOCH - Duration::from_secs(5));
220 }
221
222 #[test]
223 fn map_remote_error_distinguishes_network_from_internal_errors() {
224 let net = git2::Error::new(git2::ErrorCode::GenericError, git2::ErrorClass::Net, "down");
225 assert!(matches!(map_remote_error(net), GitError::Network(_)));
226
227 let other = git2::Error::from_str("other");
228 assert!(matches!(map_remote_error(other), GitError::Internal(_)));
229 }
230}