Skip to main content

stratum/
lib.rs

1use std::path::Path;
2
3// For now include everything as pub mod to get errors in IDE
4use git_url_parse::GitUrlParseError;
5use thiserror::Error;
6
7mod domain;
8mod repository;
9mod url;
10
11pub use domain::{actor::Actor, commit::Commit, mfile::ModifiedFile};
12pub use repository::{Local, Remote, Repository};
13pub use url::GitUrl;
14
15/// Helper function for opening a local repository given a path P
16pub fn open_repository<P: AsRef<Path>>(p: P) -> Result<Repository<Local>, Error> {
17    Repository::<Local>::new(p)
18}
19
20/// Helper function for cloning a remote repository given a url
21pub fn clone_repository<P: AsRef<Path>>(
22    url: &str,
23    dest: Option<P>,
24) -> Result<Repository<Local>, Error> {
25    Repository::<Remote>::new(url, dest)
26}
27
28#[derive(Debug, Error, PartialEq)]
29pub enum Error {
30    /// An abstraction of git2::Error to raise the error effectively
31    #[error(transparent)]
32    Git(#[from] git2::Error),
33
34    /// A regex related error
35    #[error(transparent)]
36    Regex(#[from] regex::Error),
37
38    /// An abstraction of git-url-parse::GitUrlParseError
39    #[error(transparent)]
40    GitUrlError(#[from] GitUrlParseError),
41
42    /// If a URL can be parsed but is not a valid GitUrl schem
43    #[error("URL scheme was {0}, cannot clone URL.")]
44    UrlScheme(String),
45
46    /// An error associated with a bad path
47    #[error("{0}")]
48    PathError(String),
49
50    /// A cache related error
51    #[error("Cache mutex has been poisoned")]
52    PoisonedCache,
53}
54
55/// Common functionality that can be imported into any and all unit tests
56/// throughout the library
57#[cfg(test)]
58mod common {
59    use std::sync::LazyLock;
60    use std::{fs, path::Path};
61    use tempfile::TempDir;
62
63    pub const EXPECTED_MSG: &str = "commit msg\n\nCo-authored-by: John Doe <john@example.com>\nCo-authored-by: John Doe <john@example.com>\nCo-authored-by: Dave <dave@example.com>";
64    pub const EXPECTED_ACTOR_NAME: &str = "test";
65    pub const EXPECTED_ACTOR_EMAIL: &str = "test@example.com";
66
67    /// Write to a file that exists within a temp directory root
68    fn write_fp(root: &TempDir, path: &str, content: &str) {
69        let fp = root.path().join(path);
70        fs::write(&fp, content).expect("Failed to write to file");
71    }
72
73    /// Write a file to a git index
74    fn write_to_index(index: &mut git2::Index, file: &str) {
75        index
76            .add_path(Path::new(file))
77            .expect("Failed to add file to index");
78        index.write().expect("Failed to write index");
79    }
80
81    /// Write an index to a repository tree
82    fn write_tree<'a>(
83        repo: &'a git2::Repository,
84        index: &mut git2::Index,
85        file: &str,
86    ) -> git2::Tree<'a> {
87        write_to_index(index, file);
88
89        let tree_id = index.write_tree().expect("Failed to write tree");
90        repo.find_tree(tree_id).expect("Failed to find tree")
91    }
92
93    /// Commit a file that has been modified
94    fn commit_file(
95        repo: &git2::Repository,
96        sig: &git2::Signature,
97        file: &str,
98        parent: Option<&git2::Commit<'_>>,
99    ) -> git2::Oid {
100        // Stage file for commit
101        let mut index = repo.index().expect("Failed to get index");
102        let tree = write_tree(repo, &mut index, file);
103
104        let parents = match parent {
105            Some(v) => vec![v],
106            None => vec![],
107        };
108
109        repo.commit(Some("HEAD"), sig, sig, EXPECTED_MSG, &tree, &parents)
110            .expect("Failed to create commit")
111    }
112
113    /// Make a repository with a very basic histroy.
114    fn make_repo(tmpdir: &TempDir) {
115        let repo = git2::Repository::init(tmpdir.path()).expect("Failed to init repo");
116        let sig = git2::Signature::now(EXPECTED_ACTOR_NAME, EXPECTED_ACTOR_EMAIL)
117            .expect("Failed to create actor signature");
118
119        let file = "file.txt";
120        write_fp(tmpdir, file, "Hello World\n");
121        let first_commit_id = commit_file(&repo, &sig, file, None);
122
123        write_fp(tmpdir, file, "Hello World\nFile Update\n");
124        let parent = repo
125            .find_commit(first_commit_id)
126            .expect("Failed to find first commit");
127        commit_file(&repo, &sig, file, Some(&parent));
128    }
129
130    /// Lazily construct the test data into a temp dir that will last the length of
131    /// a single modules test span
132    static TEST_DATA_DIR: LazyLock<TempDir> = LazyLock::new(|| {
133        let dir = TempDir::new().expect("Create temp dir");
134        make_repo(&dir);
135        dir
136    });
137
138    /// The path to the test data directory
139    fn test_data_dir() -> &'static Path {
140        TEST_DATA_DIR.path()
141    }
142
143    /// Init a repository object using the lazily constructed git2 repo
144    ///
145    /// The repository exists in a temporary diectory and is made with some
146    /// expected values that are public constants.
147    ///
148    /// The commit history is two commits long, two commits so that 2/3 of the
149    /// diff creation methods can be tested, this removes the need to mock
150    /// git2::Diff, which may not even be possible.
151    ///
152    /// ## Commit 1
153    ///
154    /// - One file was added, `file.txt`
155    ///     - The string "Hello World\n" was written to this file
156    /// - The commit is authored and committed by: test <test@example.com>
157    /// - The commit message is: "commit msg"
158    ///
159    /// ## Commit 2
160    ///
161    /// - The same file, file.txt is updated
162    ///     - The file now contains the string "Hello World\nFile Update\n"
163    /// - The commit is authored and committed by: test <test@example.com>
164    /// - The commit message is: "commit msg"
165    pub fn init_repo() -> git2::Repository {
166        git2::Repository::open(test_data_dir()).expect("Failed to open temp repo")
167    }
168}