summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--src/git.rs310
-rw-r--r--src/git/tests.rs12
2 files changed, 169 insertions, 153 deletions
diff --git a/src/git.rs b/src/git.rs
index df9614a..c892306 100644
--- a/src/git.rs
+++ b/src/git.rs
@@ -38,11 +38,57 @@ use crate::marker::Initializer;
 #[cfg(test)]
 mod tests;
 
-/// Represents a local git repo.
-pub struct Git {
+mod sealed { 
+    pub trait Sealed {
+        fn is_repo_path_valid(filename: &str, sha256: bool) -> bool;
+    }
+}
+
+/// A repository kind.
+pub trait RepoKind: sealed::Sealed {
+}
+
+/// A permanent repository used to cache remote objects.
+pub struct CacheRepo {
+    _non_exhaustive: (),
+}
+
+impl sealed::Sealed for CacheRepo {
+    fn is_repo_path_valid(filename: &str, sha256: bool) -> bool {
+        if sha256 {
+            NamePurpose::CacheRepo64.is_fit(filename)
+        } else {
+            NamePurpose::CacheRepo.is_fit(filename)
+        }
+    }
+}
+
+impl RepoKind for CacheRepo {
+}
+
+/// A temporary repository used to fetch remote objects.
+pub struct FetchRepo {
+    pending_branches: BTreeSet<String>,
+}
+
+impl sealed::Sealed for FetchRepo {
+    fn is_repo_path_valid(filename: &str, sha256: bool) -> bool {
+        if sha256 {
+            NamePurpose::WorkRepo64.is_fit(filename)
+        } else {
+            NamePurpose::WorkRepo.is_fit(filename)
+        }
+    }
+}
+
+impl RepoKind for FetchRepo {
+}
+
+/// A local git repository.
+pub struct Git<T: RepoKind> {
     path: PathBuf,
-    pending_branches: Option<BTreeSet<String>>,
     sha256: bool,
+    inner: T,
 }
 
 /// Error returned by operations on a git repo.
@@ -101,7 +147,7 @@ impl_trait! {
 }
 
 /// RAII transaction guard for merging forked repos in with_work_repos.
-struct Merger<'a>(&'a mut Git, Vec<Git>);
+struct Merger<'a>(&'a mut Git<CacheRepo>, Vec<Git<FetchRepo>>);
 
 impl From<io::Error> for GitErrorInner {
     fn from(e: io::Error) -> Self {
@@ -164,7 +210,7 @@ impl_trait! {
 impl_trait! {
     impl<'a> Merger<'a> {
         /// Returns a shared, immutable reference to the main repo.
-        fn main(&self) -> &Git {
+        fn main(&self) -> &Git<CacheRepo> {
             &*self.0
         }
 
@@ -177,7 +223,7 @@ impl_trait! {
             // check for conflicts first!
             let mut branches = BTreeSet::<&String>::new();
             for work in &*self {
-                for branch in work.pending_branches.as_ref().unwrap() {
+                for branch in &work.inner.pending_branches {
                     if !branches.insert(branch) {
                         panic!("Branch {} is in conflict!", branch);
                     }
@@ -191,8 +237,7 @@ impl_trait! {
                     .strip_prefix("ganarchy-fetch-").unwrap()
                     .strip_suffix(".git").unwrap()
                     .to_owned();
-                let pending = repo.pending_branches.take().unwrap();
-                for branch in pending {
+                for branch in std::mem::take(&mut repo.inner.pending_branches) {
                     let len = branch.len();
                     let fetch_head = branch + "-" + &repo_id;
                     let branch = &fetch_head[..len];
@@ -212,16 +257,16 @@ impl_trait! {
 
         /// Accesses the work repos.
         impl trait std::ops::Deref {
-            type Target = Vec<Git>;
+            type Target = Vec<Git<FetchRepo>>;
 
-            fn deref(&self) -> &Vec<Git> {
+            fn deref(&self) -> &Vec<Git<FetchRepo>> {
                 &self.1
             }
         }
 
         /// Accesses the work repos.
         impl trait std::ops::DerefMut {
-            fn deref_mut(&mut self) -> &mut Vec<Git> {
+            fn deref_mut(&mut self) -> &mut Vec<Git<FetchRepo>> {
                 &mut self.1
             }
         }
@@ -240,27 +285,25 @@ impl_trait! {
 }
 
 /// Initializer operations on the `Git` struct.
-impl Git {
+impl Git<CacheRepo> {
     /// Creates a new instance of the `Git` struct, with the path as given.
-    pub fn at_path<T: AsRef<Path>>(_: Initializer, path: T) -> Option<Git> {
+    pub fn at_path<T: AsRef<Path>>(_: Initializer, path: T) -> Option<Git<CacheRepo>> {
         let path = path.as_ref();
-        let filename = path.file_name()?.to_str()?;
+        // using `?` for side-effects.
+        let _ = path.file_name()?.to_str()?;
         // TODO SHA-2
-        NamePurpose::CacheRepo.is_fit(filename).then(|| Git {
+        Some(Git {
             path: path.into(),
-            pending_branches: None,
             sha256: false,
-        })
+            inner: CacheRepo {
+                _non_exhaustive: (),
+            },
+        }).filter(Self::is_path_valid)
     }
 }
 
-/// Operations on a git repo.
-///
-/// # Race conditions
-///
-/// These operate on the filesystem. Calling them from multiple threads
-/// can result in data corruption.
-impl Git {
+/// Operations on a cache repo.
+impl Git<CacheRepo> {
     /// Creates the given number of work repos, and calls the closure to run
     /// operations on them.
     ///
@@ -273,7 +316,7 @@ impl Git {
     /// # Panics
     ///
     /// Panics if a merge conflict is detected. Specifically, if two work repos
-    /// modify the same work branch. Also panics if this isn't a cache repo.
+    /// modify the same work branch.
     ///
     /// # "Poisoning"
     ///
@@ -281,8 +324,7 @@ impl Git {
     /// deleted. Instead, future calls to this method will return a GitError.
     pub fn with_work_repos<F, R>(&mut self, count: usize, f: F)
         -> Result<R, GitError>
-    where F: FnOnce(&mut [Git]) -> Result<R, GitError> {
-        assert!(self.is_cache_repo());
+    where F: FnOnce(&mut [Git<FetchRepo>]) -> Result<R, GitError> {
         // create some Git structs
         let mut work_repos = Vec::with_capacity(count);
         for id in 0..count {
@@ -290,10 +332,12 @@ impl Git {
             new_path.set_file_name(format!("ganarchy-fetch-{}.git", id));
             let git = Git {
                 path: new_path,
-                pending_branches: Some(Default::default()),
+                inner: FetchRepo {
+                    pending_branches: Default::default(),
+                },
                 sha256: self.sha256,
             };
-            assert!(git.is_work_repo());
+            assert!(git.is_path_valid());
             work_repos.push(git);
         }
         // create the on-disk stuff
@@ -309,37 +353,8 @@ impl Git {
         merger.merge().and(Ok(result))
     }
 
-    /// Fetches branch `from_ref` from source `from` into branch `branch`.
-    ///
-    /// The fetch used is a force-fetch.
-    ///
-    /// # Panics
-    ///
-    /// Panics if called on a non-work repo, if `from` starts with `-`, if
-    /// `branch` isn't a cache branch, or if `from_ref` starts with `-`.
-    pub fn fetch_source(&mut self, from: &str, branch: &str, from_ref: &str)
-        -> Result<(), GitError>
-    {
-        assert!(self.is_work_repo());
-        assert!(!from.starts_with("-"));
-        assert!(!from_ref.starts_with("-"));
-        assert!(NamePurpose::WorkBranch.is_fit(branch));
-        let _output = self.cmd(|args| {
-            args.arg("fetch");
-            args.arg(from);
-            args.arg(format!("+{}:{}", from_ref, branch));
-        })?;
-        self.pending_branches.as_mut().unwrap().insert(branch.into());
-        Ok(())
-    }
-
     /// Initializes this repo.
-    ///
-    /// # Panics
-    ///
-    /// Panics if called on a non-cache repo.
     pub fn ensure_exists(&mut self) -> Result<(), GitError> {
-        assert!(self.is_cache_repo());
         let _output = self.cmd_init(|_| {})?;
         Ok(())
     }
@@ -348,12 +363,11 @@ impl Git {
     ///
     /// # Panics
     ///
-    /// Panics if this isn't a cache branch on a cache repo or if commit isn't
+    /// Panics if this isn't a cache branch or if commit isn't
     /// a commit.
     pub fn check_history(&self, branch: &str, commit: &str)
         -> Result<(), GitError>
     {
-        assert!(self.is_cache_repo());
         assert!(NamePurpose::CacheBranch.is_fit(branch));
         assert!(self.is_commit_hash(commit));
         let _output = self.cmd(|args| {
@@ -364,55 +378,40 @@ impl Git {
         })?;
         Ok(())
     }
+}
 
-    /// Checks if the given branch is a valid branch.
+/// Operations on a fetch repo.
+impl Git<FetchRepo> {
+    /// Fetches branch `from_ref` from source `from` into branch `branch`.
     ///
-    /// Note: "HEAD" is **not** a branch.
+    /// The fetch used is a force-fetch.
     ///
     /// # Panics
     ///
-    /// Panics if `branch` starts with `-`.
-    pub fn check_branch(&self, branch: &str) -> Result<(), GitError> {
-        assert!(!branch.starts_with("-"));
-        let mut output = self.cmd(|args| {
-            args.arg("check-ref-format");
-            args.arg("--branch");
-            args.arg(branch);
+    /// Panics if `from` starts with `-`, if
+    /// `branch` isn't a cache branch, or if `from_ref` starts with `-`.
+    pub fn fetch_source(&mut self, from: &str, branch: &str, from_ref: &str)
+        -> Result<(), GitError>
+    {
+        assert!(!from.starts_with("-"));
+        assert!(!from_ref.starts_with("-"));
+        assert!(NamePurpose::WorkBranch.is_fit(branch));
+        let _output = self.cmd(|args| {
+            args.arg("fetch");
+            args.arg(from);
+            args.arg(format!("+{}:{}", from_ref, branch));
         })?;
-        // perf: Vec::default doesn't allocate.
-        let stdout = std::mem::take(&mut output.stdout);
-        let stdout = String::from_utf8(stdout);
-        match stdout.as_ref().map(|x| x.strip_prefix(branch)) {
-            Ok(Some("")) | Ok(Some("\n")) | Ok(Some("\r\n")) => {
-                return Ok(())
-            },
-            _ => (),
-        }
-        output.stdout = match stdout {
-            Ok(e) => e.into_bytes(),
-            Err(e) => e.into_bytes(),
-        };
-        let v = vec![
-            OsString::from("git"),
-            "check-ref-format".into(),
-            "--branch".into(),
-            branch.into(),
-        ];
-        Err(GitError::new(output, v))
+        self.inner.pending_branches.insert(branch.into());
+        Ok(())
     }
 
     /// Returns the number of commits removed and the number of added between
     /// from and to, respectively.
-    ///
-    /// # Panics
-    ///
-    /// Panics if called on a non-work repo.
     pub fn get_counts(&self, from: &str, to: &str)
         -> Result<(u64, u64), GitError>
     {
-        // if called on a cache repo, `from` may no longer exist.
-        // this check makes sure `from` has not been garbage-collected.
-        assert!(self.is_work_repo());
+        // if called on a cache repo, `from` may no longer exist. the FetchRepo
+        // requirement makes sure `from` has not been garbage-collected.
         assert!(self.is_commit_hash(from));
         assert!(self.is_commit_hash(to));
         let mut output = self.cmd(|args| {
@@ -452,6 +451,45 @@ impl Git {
         ];
         Err(GitError::new(output, v))
     }
+}
+
+/// Generic Git operations.
+impl<T: RepoKind> Git<T> {
+    /// Checks if the given branch is a valid branch.
+    ///
+    /// Note: "HEAD" is **not** a branch.
+    ///
+    /// # Panics
+    ///
+    /// Panics if `branch` starts with `-`.
+    pub fn check_branch(&self, branch: &str) -> Result<(), GitError> {
+        assert!(!branch.starts_with("-"));
+        let mut output = self.cmd(|args| {
+            args.arg("check-ref-format");
+            args.arg("--branch");
+            args.arg(branch);
+        })?;
+        // perf: Vec::default doesn't allocate.
+        let stdout = std::mem::take(&mut output.stdout);
+        let stdout = String::from_utf8(stdout);
+        match stdout.as_ref().map(|x| x.strip_prefix(branch)) {
+            Ok(Some("")) | Ok(Some("\n")) | Ok(Some("\r\n")) => {
+                return Ok(())
+            },
+            _ => (),
+        }
+        output.stdout = match stdout {
+            Ok(e) => e.into_bytes(),
+            Err(e) => e.into_bytes(),
+        };
+        let v = vec![
+            OsString::from("git"),
+            "check-ref-format".into(),
+            "--branch".into(),
+            branch.into(),
+        ];
+        Err(GitError::new(output, v))
+    }
 
     /// Returns the commit hash at the given target.
     ///
@@ -526,22 +564,20 @@ impl Git {
     }
 }
 
-/// Private operations on a git repo.
-impl Git {
+/// Private operations on a git cache repo.
+impl Git<CacheRepo> {
     /// Fetches branch `from_branch` from work repo `from` into branch `branch`.
     ///
     /// The fetch used is a force-fetch.
     ///
     /// # Panics
     ///
-    /// Panics if this isn't a cache repo, if `from` isn't a work repo, if
+    /// Panics if
     /// `branch` isn't a fetch head or if `from_branch` isn't a cache branch.
-    fn fetch_work(&mut self, from: &Git, branch: &str, from_branch: &str)
+    fn fetch_work(&mut self, from: &Git<FetchRepo>, branch: &str, from_branch: &str)
         -> Result<(), GitError>
     {
         assert_eq!(self.sha256, from.sha256);
-        assert!(self.is_cache_repo());
-        assert!(from.is_work_repo());
         assert!(NamePurpose::CacheBranch.is_fit(from_branch));
         assert!(NamePurpose::CacheFetchHead.is_fit(branch));
         let _output = self.cmd(|args| {
@@ -556,12 +592,11 @@ impl Git {
     ///
     /// # Panics
     ///
-    /// Panics if this isn't a cache repo, if `old_name` isn't a fetch head,
+    /// Panics if `old_name` isn't a fetch head,
     /// or if `new_name` isn't a cache branch.
     fn replace(&mut self, old_name: &str, new_name: &str)
         -> Result<(), GitError>
     {
-        assert!(self.is_cache_repo());
         assert!(NamePurpose::CacheBranch.is_fit(new_name));
         assert!(NamePurpose::CacheFetchHead.is_fit(old_name));
         let _output = self.cmd(|args| {
@@ -572,14 +607,27 @@ impl Git {
         Ok(())
     }
 
+    /// Makes a shared clone of this local repo into the given work repo.
+    ///
+    /// Equivalent to `git clone --bare --shared`, which is very dangerous!
+    fn fork(&self, into: &mut Git<FetchRepo>) -> Result<(), GitError> {
+        // check that this is a cache repo
+        assert_eq!(self.sha256, into.sha256);
+        let _output = into.cmd_clone_from(&self.path, |args| {
+            args.arg("--shared");
+        })?;
+        Ok(())
+    }
+}
+
+/// Private operations on a git fetch repo.
+impl Git<FetchRepo> {
     /// Deletes work branch `branch`.
     ///
     /// # Panics
     ///
-    /// Panics if the branch isn't a work branch or if this isn't a work
-    /// repo.
+    /// Panics if the branch isn't a work branch.
     fn rm_branch(&mut self, branch: &str) -> Result<(), GitError> {
-        assert!(self.is_work_repo());
         assert!(NamePurpose::WorkBranch.is_fit(branch));
         let _output = self.cmd(|args| {
             args.arg("branch");
@@ -588,32 +636,8 @@ impl Git {
         Ok(())
     }
 
-    /// Makes a shared clone of this lcoal repo into the given work repo.
-    ///
-    /// Equivalent to `git clone --bare --shared`, which is very dangerous!
-    ///
-    /// # Panics
-    ///
-    /// Panics if this repo isn't a cache repo, and/or if the given repo isn't
-    /// a work repo.
-    fn fork(&self, into: &mut Git) -> Result<(), GitError> {
-        // check that this is a cache repo
-        assert_eq!(self.sha256, into.sha256);
-        assert!(self.is_cache_repo());
-        assert!(into.is_work_repo());
-        let _output = into.cmd_clone_from(&self.path, |args| {
-            args.arg("--shared");
-        })?;
-        Ok(())
-    }
-
     /// Deletes this repo.
-    ///
-    /// # Panics
-    ///
-    /// Panics if called on a non-work repo.
     fn delete(self) -> Result<(), GitError> {
-        assert!(self.is_work_repo());
         fs::remove_dir_all(&self.path).map_err(|e| {
             let args = vec![
                 "(synthetic)".into(),
@@ -627,25 +651,11 @@ impl Git {
 }
 
 /// Helpers.
-impl Git {
-    /// Returns true if this is a cache repo.
-    fn is_cache_repo(&self) -> bool {
-        let filename = self.path.file_name().unwrap().to_str();
-        if self.sha256 {
-            NamePurpose::CacheRepo64.is_fit(filename.unwrap())
-        } else {
-            NamePurpose::CacheRepo.is_fit(filename.unwrap())
-        }
-    }
-
-    /// Returns true if this is a work repo.
-    fn is_work_repo(&self) -> bool {
+impl<T: RepoKind> Git<T> {
+    /// Returns true if this repo's path is valid.
+    fn is_path_valid(&self) -> bool {
         let filename = self.path.file_name().unwrap().to_str();
-        if self.sha256 {
-            NamePurpose::WorkRepo64.is_fit(filename.unwrap())
-        } else {
-            NamePurpose::WorkRepo.is_fit(filename.unwrap())
-        }
+        T::is_repo_path_valid(filename.unwrap(), self.sha256)
     }
 
     /// Returns true if the string is a commit hash.
@@ -661,7 +671,7 @@ impl Git {
 }
 
 /// Raw commands on a git repo.
-impl Git {
+impl<T: RepoKind> Git<T> {
     /// Runs a command for initializing this git repo.
     ///
     /// Always uses `--bare`.
diff --git a/src/git/tests.rs b/src/git/tests.rs
index ef6c763..16eeb93 100644
--- a/src/git/tests.rs
+++ b/src/git/tests.rs
@@ -129,7 +129,9 @@ mod low_level {
         // have to create a Git manually for this one. ah well. :(
         let git_clone = Git {
             path: clone.path().into(),
-            pending_branches: None,
+            inner: CacheRepo {
+                _non_exhaustive: (),
+            },
             sha256: false,
         };
         git_clone.cmd_clone_from(repo.path(), |args| {
@@ -173,7 +175,9 @@ mod privates {
         // have to create a Git manually for this one. ah well. :(
         let mut git_clone = Git {
             path: clone.path().into(),
-            pending_branches: Some(Default::default()),
+            inner: FetchRepo {
+                pending_branches: Default::default(),
+            },
             sha256: false,
         };
         git.fork(&mut git_clone).unwrap();
@@ -190,7 +194,9 @@ mod privates {
         // have to create a Git manually for this one. ah well. :(
         let mut git_clone = Git {
             path: clone.path().into(),
-            pending_branches: Some(Default::default()),
+            inner: FetchRepo {
+                pending_branches: Default::default(),
+            },
             sha256: false,
         };
         git.fork(&mut git_clone).unwrap();