diff options
author | SoniEx2 <endermoneymod@gmail.com> | 2021-09-04 22:50:46 -0300 |
---|---|---|
committer | SoniEx2 <endermoneymod@gmail.com> | 2021-09-04 22:50:46 -0300 |
commit | c1048a75ce98198fe9e286389b219ad0b62c8f19 (patch) | |
tree | 19a75b8dc3b5196f218bb5b655e639f6efa4837f /src | |
parent | d3b15a5d4f4c1b1f01117b0623ab4c8763e52ae4 (diff) |
Use more typestate
Diffstat (limited to 'src')
-rw-r--r-- | src/git.rs | 310 | ||||
-rw-r--r-- | src/git/tests.rs | 12 |
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(); |