stratum/repository/mod.rs
1use std::{marker::PhantomData, path::Path};
2
3use crate::{Commit, Error, GitUrl};
4
5mod utils;
6
7/// Unit struct indicating that a repository is remote to the file system
8pub struct Remote;
9/// Unit struct indicating that a repository is local to the file system
10pub struct Local;
11
12/// A Git Repository which can be mined.
13///
14/// Two marker variations Local and Remote. Local is the variant which exists on
15/// the local filesystem and can therefore be mined. Remote, is of course not on
16/// the local filesystem and is represented via its remote url. A remote Repository
17/// upon instantiation will be cloned and returned as a Local variant such that it
18/// can be mined.
19///
20/// ## Examples
21///
22/// 1. Traversing a local repository from HEAD
23///
24/// ```no_run
25/// # use std::{path::PathBuf, str::FromStr};
26/// use stratum::{Repository, Local};
27///
28/// let p = PathBuf::from_str("~/repository/").unwrap();
29/// let repo = Repository::<Local>::new(p).unwrap();
30///
31/// for commit in repo.traverse_commits().unwrap() {
32/// let commit = commit.unwrap();
33/// println!("Commit {} was authored by {:?}", commit.hash(), commit.author().name());
34/// }
35/// ```
36///
37/// 2. Traversing a remote repository from HEAD
38///
39/// ```no_run
40/// # use std::{path::PathBuf, str::FromStr};
41/// use stratum::{Repository, Remote};
42///
43/// let repo = Repository::<Remote>::new::<PathBuf>(
44/// "https://server.example/owner/repo.git",
45/// None
46/// ).unwrap();
47/// for commit in repo.traverse_commits().unwrap() {
48/// let commit = commit.unwrap();
49/// println!("Commit {} was authored by {:?}", commit.hash(), commit.author().name());
50/// }
51/// ```
52///
53/// 3. Extracting HEAD from a local repository
54///
55/// ```no_run
56/// # use std::{path::PathBuf, str::FromStr};
57/// use stratum::{Repository, Local};
58///
59/// let p = PathBuf::from_str("~/repository/").unwrap();
60/// let repo = Repository::<Local>::new(p).unwrap();
61///
62/// println!("HEAD was authored by {:?}", repo.head().unwrap().author().name());
63/// ```
64///
65/// 4. Using the helper functions
66///
67/// ```no_run
68/// # use std::{path::PathBuf, str::FromStr};
69/// use stratum::open_repository;
70///
71/// let p = PathBuf::from_str("~/repository/").unwrap();
72/// // use clone_repository for the remote helper fucntion
73/// let repo = open_repository(p);
74///
75/// println!("HEAD was authored by {:?}", repo.unwrap().head().unwrap().author().name());
76/// ```
77pub struct Repository<Location = Local> {
78 repo: git2::Repository,
79 location: PhantomData<Location>,
80}
81
82impl Repository<Local> {
83 /// Instatiate a new Repository from a path on the local file system
84 pub fn new<P>(path: P) -> Result<Self, Error>
85 where
86 P: AsRef<Path>,
87 {
88 if !path.as_ref().is_dir() {
89 return Err(Error::PathError("{path} is not a directory".to_string()));
90 }
91
92 let git_repo = git2::Repository::open(path).map_err(Error::Git)?;
93 Ok(Self {
94 repo: git_repo,
95 location: PhantomData::<Local>,
96 })
97 }
98
99 /// Read access into the underlying git2 object
100 pub fn raw(&self) -> &git2::Repository {
101 &self.repo
102 }
103
104 /// Traverse the repositories commit graph from HEAD
105 pub fn traverse_commits(
106 &self,
107 ) -> Result<impl Iterator<Item = Result<Commit<'_>, Error>>, Error> {
108 let mut walker = self.raw().revwalk().map_err(Error::Git)?;
109 walker.push_head().map_err(Error::Git)?;
110 self.iterate_walker(walker)
111 }
112
113 /// Traverse the repositories commit graph from a specified commit hash
114 pub fn traverse_from(
115 &self,
116 oid: &str,
117 ) -> Result<impl Iterator<Item = Result<Commit<'_>, Error>>, Error> {
118 let oid = git2::Oid::from_str(oid).map_err(Error::Git)?;
119
120 let mut walker = self.raw().revwalk().map_err(Error::Git)?;
121 walker.push(oid).map_err(Error::Git)?;
122 self.iterate_walker(walker)
123 }
124
125 /// Return head as a stratum commit
126 pub fn head(&self) -> Result<Commit<'_>, Error> {
127 let head = self
128 .repo
129 .head()
130 .map_err(Error::Git)?
131 .peel_to_commit()
132 .map_err(Error::Git)?;
133 Ok(Commit::new(head, self))
134 }
135
136 /// Return a single commit object based on a given oid/hash
137 pub fn single(&self, oid: &str) -> Result<Commit<'_>, Error> {
138 let git_commit = self
139 .repo
140 .find_commit(git2::Oid::from_str(oid).map_err(Error::Git)?)
141 .map_err(Error::Git)?;
142 Ok(Commit::new(git_commit, self))
143 }
144
145 fn iterate_walker(
146 &self,
147 walker: git2::Revwalk<'_>,
148 ) -> Result<impl Iterator<Item = Result<Commit<'_>, Error>>, Error> {
149 Ok(walker.map(|result| {
150 result.map_err(Error::Git).and_then(|oid| {
151 self.raw()
152 .find_commit(oid)
153 .map_err(Error::Git)
154 .map(|git_commit| Commit::new(git_commit, self))
155 })
156 }))
157 }
158}
159
160impl Repository<Remote> {
161 /// Instatiate a new Repository from a remote URL, returning the Local
162 /// variant after cloning the repository into `dest`.
163 ///
164 /// Type of clone to perform will be automatically resolved based on the
165 /// URL.
166 pub fn new<P>(url: &str, dest: Option<P>) -> Result<Repository<Local>, Error>
167 where
168 P: AsRef<Path>,
169 {
170 let git_url = GitUrl::parse(url)?;
171 // If ok_or block is hit, then scheme is None, hence pass string version
172 // of None for a useful error message
173 let scheme = git_url
174 .scheme()
175 .ok_or(Error::UrlScheme("None".to_string()))?;
176
177 match scheme {
178 "http" | "https" => Repository::from_https(url, dest),
179 "ssh" => Repository::from_ssh(url, dest),
180 _ => Err(Error::UrlScheme(scheme.to_string())),
181 }
182 }
183
184 /// Clone the given repository via the http or https protocol into the given
185 /// destination
186 pub fn from_https<P>(url: &str, dest: Option<P>) -> Result<Repository<Local>, Error>
187 where
188 P: AsRef<Path>,
189 {
190 // Don't shadow url, slice needed to clone repo
191 let git_url = GitUrl::parse(url)?;
192 let dest = utils::resolve_destination(&git_url, dest);
193
194 let git_repo = git2::Repository::clone(url, dest).map_err(Error::Git)?;
195
196 Ok(Repository {
197 repo: git_repo,
198 location: PhantomData::<Local>,
199 })
200 }
201
202 pub fn from_ssh<P>(_url: &str, _dest: Option<P>) -> Result<Repository<Local>, Error>
203 where
204 P: AsRef<Path>,
205 {
206 todo!(
207 "SSH cloning is not yet supported, attempt cloning via a http/https URL or clone manually"
208 )
209 }
210}
211
212// Define a helper function to be used in testing
213#[cfg(test)]
214impl Repository<Local> {
215 /// Instantiate a repository from a git2::Repository
216 pub fn from_repository(repo: git2::Repository) -> Self {
217 Self {
218 repo,
219 location: PhantomData::<Local>,
220 }
221 }
222}
223
224#[cfg(test)]
225mod test {
226 use super::*;
227 use tempfile::{NamedTempFile, TempDir};
228
229 #[test]
230 fn test_fail_on_bad_dir() {
231 let fp = NamedTempFile::new().expect("Failed to make tempfile");
232 assert!(Repository::<Local>::new(fp.path()).is_err())
233 }
234
235 #[test]
236 fn test_fail_on_bad_git_dir() {
237 let dir = TempDir::new().expect("Failed to make tempdir");
238 assert!(Repository::<Local>::new(dir.path()).is_err())
239 }
240
241 //TODO: Should I test cloning and so on here or in integration tests?
242}