Skip to main content

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}