summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock78
-rw-r--r--lib/libgfold/Cargo.toml2
-rw-r--r--lib/libgfold/src/collector.rs22
-rw-r--r--lib/libgfold/src/collector/target.rs4
-rw-r--r--lib/libgfold/src/lib.rs44
-rw-r--r--lib/libgfold/src/repository_view.rs128
-rw-r--r--lib/libgfold/src/repository_view/submodule_view.rs16
-rw-r--r--lib/libgfold/src/status.rs111
8 files changed, 246 insertions, 159 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 02a676f..e532f84 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -34,15 +34,15 @@ dependencies = [
[[package]]
name = "anstyle"
-version = "1.0.0"
+version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d"
+checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd"
[[package]]
name = "anstyle-parse"
-version = "0.2.0"
+version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee"
+checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333"
dependencies = [
"utf8parse",
]
@@ -119,9 +119,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clap"
-version = "4.3.3"
+version = "4.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ca8f255e4b8027970e78db75e78831229c9815fdbfa67eb1a1b777a62e24b4a0"
+checksum = "d9394150f5b4273a1763355bd1c2ec54cc5a2593f790587bcd6b2c947cfa9211"
dependencies = [
"clap_builder",
"clap_derive",
@@ -130,9 +130,9 @@ dependencies = [
[[package]]
name = "clap_builder"
-version = "4.3.3"
+version = "4.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "acd4f3c17c83b0ba34ffbc4f8bbd74f079413f747f84a6f89292f138057e36ab"
+checksum = "9a78fbdd3cc2914ddf37ba444114bc7765bbdcb55ec9cbe6fa054f0137400717"
dependencies = [
"anstream",
"anstyle",
@@ -150,7 +150,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
- "syn 2.0.18",
+ "syn 2.0.22",
]
[[package]]
@@ -262,6 +262,12 @@ dependencies = [
]
[[package]]
+name = "equivalent"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1"
+
+[[package]]
name = "errno"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -330,9 +336,9 @@ dependencies = [
[[package]]
name = "gimli"
-version = "0.27.2"
+version = "0.27.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4"
+checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e"
[[package]]
name = "git2"
@@ -349,9 +355,9 @@ dependencies = [
[[package]]
name = "hashbrown"
-version = "0.12.3"
+version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
[[package]]
name = "heck"
@@ -392,11 +398,11 @@ dependencies = [
[[package]]
name = "indexmap"
-version = "1.9.3"
+version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
+checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
dependencies = [
- "autocfg",
+ "equivalent",
"hashbrown",
]
@@ -449,9 +455,9 @@ dependencies = [
[[package]]
name = "libc"
-version = "0.2.146"
+version = "0.2.147"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f92be4933c13fd498862a9e02a3055f8a8d9c039ce33db97306fd5a6caa7f29b"
+checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
[[package]]
name = "libgfold"
@@ -594,9 +600,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
-version = "1.0.60"
+version = "1.0.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dec2b086b7a862cf4de201096214fa870344cf922b2b30c167badb3af3195406"
+checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb"
dependencies = [
"unicode-ident",
]
@@ -716,14 +722,14 @@ checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.18",
+ "syn 2.0.22",
]
[[package]]
name = "serde_json"
-version = "1.0.96"
+version = "1.0.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1"
+checksum = "46266871c240a00b8f503b877622fe33430b3c7d963bdc0f2adc511e54a1eae3"
dependencies = [
"itoa",
"ryu",
@@ -732,9 +738,9 @@ dependencies = [
[[package]]
name = "serde_spanned"
-version = "0.6.2"
+version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "93107647184f6027e3b7dcb2e11034cf95ffa1e3a682c67951963ac69c1c007d"
+checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186"
dependencies = [
"serde",
]
@@ -780,9 +786,9 @@ dependencies = [
[[package]]
name = "syn"
-version = "2.0.18"
+version = "2.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e"
+checksum = "2efbeae7acf4eabd6bcdcbd11c92f45231ddda7539edc7806bd1a04a03b24616"
dependencies = [
"proc-macro2",
"quote",
@@ -829,7 +835,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.18",
+ "syn 2.0.22",
]
[[package]]
@@ -849,9 +855,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "toml"
-version = "0.7.4"
+version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d6135d499e69981f9ff0ef2167955a5333c35e36f6937d382974566b3d5b94ec"
+checksum = "1ebafdf5ad1220cb59e7d17cf4d2c72015297b75b19a10472f99b89225089240"
dependencies = [
"serde",
"serde_spanned",
@@ -861,18 +867,18 @@ dependencies = [
[[package]]
name = "toml_datetime"
-version = "0.6.2"
+version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5a76a9312f5ba4c2dec6b9161fdf25d87ad8a09256ccea5a556fef03c706a10f"
+checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
-version = "0.19.10"
+version = "0.19.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2380d56e8670370eee6566b0bfd4265f65b3f432e8c6d85623f728d4fa31f739"
+checksum = "266f016b7f039eec8a1a80dfe6156b633d208b9fccca5e4db1d6775b0c4e34a7"
dependencies = [
"indexmap",
"serde",
@@ -1030,9 +1036,9 @@ checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
[[package]]
name = "winnow"
-version = "0.4.6"
+version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "61de7bac303dc551fe038e2b3cef0f571087a47571ea6e79a87692ac99b99699"
+checksum = "ca0ace3845f0d96209f0375e6d367e3eb87eb65d27d445bdc9f1843a26f39448"
dependencies = [
"memchr",
]
diff --git a/lib/libgfold/Cargo.toml b/lib/libgfold/Cargo.toml
index 7b1c183..ba1a158 100644
--- a/lib/libgfold/Cargo.toml
+++ b/lib/libgfold/Cargo.toml
@@ -15,7 +15,6 @@ readme = "../../README.md"
repository = "https://github.com/nickgerace/gfold/"
[dependencies]
-anyhow = { workspace = true }
log = { workspace = true }
serde = { workspace = true }
thiserror = { workspace = true }
@@ -24,6 +23,7 @@ git2 = { version = "0.17", default_features = false }
rayon = "1.7"
[dev-dependencies]
+anyhow = { workspace = true }
env_logger = { version = "0.10", features = ["humantime"], default_features = false }
pretty_assertions = "1.3"
tempfile = "3.5"
diff --git a/lib/libgfold/src/collector.rs b/lib/libgfold/src/collector.rs
index b80066e..ca80682 100644
--- a/lib/libgfold/src/collector.rs
+++ b/lib/libgfold/src/collector.rs
@@ -4,11 +4,24 @@ use rayon::prelude::*;
use std::collections::BTreeMap;
use std::path::Path;
use target::TargetCollector;
+use thiserror::Error;
-use crate::repository_view::RepositoryView;
+use crate::repository_view::{RepositoryView, RepositoryViewError, RepositoryViewResult};
mod target;
+#[allow(missing_docs)]
+#[derive(Error, Debug)]
+pub enum CollectorError {
+ #[error(transparent)]
+ FromRepositoryView(#[from] RepositoryViewError),
+ #[error(transparent)]
+ FromStdIo(#[from] std::io::Error),
+}
+
+/// The result type used when multiple kinds of errors can be encountered during collection.
+pub type CollectorResult<T> = Result<T, CollectorError>;
+
/// This type represents a [`BTreeMap`] using an optional [`String`] for keys, which represents the
/// parent directory for a group of reports ([`Vec<RepositoryView>`]). The values corresponding to those keys
/// are the actual groups of reports.
@@ -17,9 +30,10 @@ mod target;
/// sorted keys.
pub type RepositoryCollection = BTreeMap<Option<String>, Vec<RepositoryView>>;
-type UnprocessedRepositoryView = anyhow::Result<RepositoryView>;
+type UnprocessedRepositoryView = RepositoryViewResult<RepositoryView>;
/// A unit struct that provides [`Self::run()`], which is used to generated [`RepositoryCollection`].
+#[derive(Debug)]
pub struct RepositoryCollector;
impl RepositoryCollector {
@@ -28,7 +42,7 @@ impl RepositoryCollector {
path: &Path,
include_email: bool,
include_submodules: bool,
- ) -> anyhow::Result<RepositoryCollection> {
+ ) -> CollectorResult<RepositoryCollection> {
let unprocessed = TargetCollector::run(path.to_path_buf())?
.par_iter()
.map(|path| RepositoryView::new(path, include_email, include_submodules))
@@ -45,7 +59,7 @@ impl RepositoryCollector {
processed.insert(view.parent, views);
}
}
- Err(e) => return Err(e),
+ Err(e) => return Err(e.into()),
}
}
Ok(processed)
diff --git a/lib/libgfold/src/collector/target.rs b/lib/libgfold/src/collector/target.rs
index f13219d..6c2537d 100644
--- a/lib/libgfold/src/collector/target.rs
+++ b/lib/libgfold/src/collector/target.rs
@@ -11,12 +11,12 @@ use std::{fs, io};
type UnprocessedTarget = io::Result<MaybeTarget>;
/// A unit struct used to centralizing target collection method(s).
-pub struct TargetCollector;
+pub(crate) struct TargetCollector;
impl TargetCollector {
/// Generate targets for a given [`PathBuf`] based on its children (recursively). We use
/// recursion paired with [`rayon`] since we prioritize speed over memory use.
- pub fn run(path: PathBuf) -> io::Result<Vec<PathBuf>> {
+ pub(crate) fn run(path: PathBuf) -> io::Result<Vec<PathBuf>> {
let entries: Vec<DirEntry> = match fs::read_dir(&path) {
Ok(read_dir) => read_dir.filter_map(|r| r.ok()).collect(),
Err(e) => {
diff --git a/lib/libgfold/src/lib.rs b/lib/libgfold/src/lib.rs
index dc47736..ac79603 100644
--- a/lib/libgfold/src/lib.rs
+++ b/lib/libgfold/src/lib.rs
@@ -1,4 +1,31 @@
-// #![warn(missing_docs, clippy::missing_errors_doc, clippy::missing_panics_doc)]
+//! **libgfold** provides the ability to find a minimal set of user-relevant information for git
+//! repositories on a local filesystem.
+//!
+//! This library powers [**gfold**](https://github.com/nickgerace/gfold).
+
+#![warn(
+ missing_debug_implementations,
+ missing_docs,
+ missing_doc_code_examples,
+ rust_2018_idioms,
+ unreachable_pub,
+ bad_style,
+ dead_code,
+ improper_ctypes,
+ non_shorthand_field_patterns,
+ no_mangle_generic_items,
+ overflowing_literals,
+ path_statements,
+ patterns_in_fns_without_body,
+ private_in_public,
+ unconditional_recursion,
+ unused,
+ unused_allocation,
+ unused_comparisons,
+ unused_parens,
+ while_true,
+ clippy::missing_panics_doc
+)]
pub mod collector;
pub mod repository_view;
@@ -13,7 +40,6 @@ pub use status::Status;
mod tests {
use super::*;
- use anyhow::anyhow;
use git2::ErrorCode;
use git2::Oid;
use git2::Repository;
@@ -105,7 +131,7 @@ mod tests {
let expected_views_key = root
.path()
.to_str()
- .ok_or_else(|| anyhow!("could not convert PathBuf to &str"))?
+ .expect("could not convert PathBuf to &str")
.to_string();
let mut expected_views = vec![
RepositoryView::finalize(
@@ -139,7 +165,7 @@ mod tests {
// Add nested views to the expected collection.
let nested_expected_views_key = nested
.to_str()
- .ok_or_else(|| anyhow!("could not convert PathBuf to &str"))?
+ .expect("could not convert PathBuf to &str")
.to_string();
let mut nested_expected_views_raw = vec![
RepositoryView::finalize(
@@ -195,23 +221,19 @@ mod tests {
Ok(())
}
- fn create_directory<P: AsRef<Path>>(parent: P, name: &str) -> anyhow::Result<PathBuf> {
+ fn create_directory<P: AsRef<Path>>(parent: P, name: &str) -> io::Result<PathBuf> {
let parent = parent.as_ref();
let new_directory = parent.join(name);
if let Err(e) = fs::create_dir(&new_directory) {
if e.kind() != io::ErrorKind::AlreadyExists {
- return Err(anyhow!(
- "could not create directory ({:?}) due to error kind: {:?}",
- &new_directory,
- e.kind()
- ));
+ return Err(e);
}
}
Ok(new_directory)
}
- fn create_file<P: AsRef<Path>>(parent: P) -> anyhow::Result<()> {
+ fn create_file<P: AsRef<Path>>(parent: P) -> io::Result<()> {
let parent = parent.as_ref();
File::create(parent.join("file"))?;
Ok(())
diff --git a/lib/libgfold/src/repository_view.rs b/lib/libgfold/src/repository_view.rs
index 64cf3f3..2057725 100644
--- a/lib/libgfold/src/repository_view.rs
+++ b/lib/libgfold/src/repository_view.rs
@@ -1,16 +1,30 @@
-use git2::{ErrorCode, Reference, Remote, Repository, StatusOptions};
+//! This module contains [`RepositoryView`], which provides the [`Status`](crate::status::Status)
+//! and general overview of the state of a given Git repository.
+
+use git2::Repository;
use log::{debug, error, trace};
use serde::{Deserialize, Serialize};
+use std::io;
use std::path::{Path, PathBuf};
use submodule_view::SubmoduleView;
use thiserror::Error;
-use crate::status::Status;
+use crate::repository_view::submodule_view::SubmoduleError;
+use crate::status::{Status, StatusError};
mod submodule_view;
+#[allow(missing_docs)]
#[derive(Error, Debug)]
pub enum RepositoryViewError {
+ #[error(transparent)]
+ FromGit2(#[from] git2::Error),
+ #[error(transparent)]
+ FromStdIo(#[from] io::Error),
+ #[error(transparent)]
+ FromSubmodule(#[from] SubmoduleError),
+ #[error(transparent)]
+ FromStatus(#[from] StatusError),
#[error("received None (Option<&OsStr>) for file name: {0}")]
FileNameNotFound(PathBuf),
#[error("could not convert file name (&OsStr) to &str: {0}")]
@@ -21,6 +35,9 @@ pub enum RepositoryViewError {
PathToStrConversionFailure(PathBuf),
}
+#[allow(missing_docs)]
+pub type RepositoryViewResult<T> = Result<T, RepositoryViewError>;
+
/// A collection of results for a Git repository at a given path.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct RepositoryView {
@@ -36,7 +53,9 @@ pub struct RepositoryView {
/// The remote origin URL. The value will be `None` if the URL cannot be found.
pub url: Option<String>,
+ /// The email used in either the local or global config for the repository.
pub email: Option<String>,
+ /// Views of submodules found within the repository.
pub submodules: Vec<SubmoduleView>,
}
@@ -46,7 +65,7 @@ impl RepositoryView {
repo_path: &Path,
include_email: bool,
include_submodules: bool,
- ) -> anyhow::Result<RepositoryView> {
+ ) -> RepositoryViewResult<RepositoryView> {
debug!(
"attempting to generate collector for repository_view at path: {:?}",
repo_path
@@ -68,7 +87,7 @@ impl RepositoryView {
}
Err(e) => return Err(e.into()),
};
- let (status, head, remote) = RepositoryView::find_status(&repo)?;
+ let (status, head, remote) = Status::find(&repo)?;
let submodules = if include_submodules {
SubmoduleView::list(&repo)?
@@ -97,16 +116,17 @@ impl RepositoryView {
"finalized collector collection for repository_view at path: {:?}",
repo_path
);
- Ok(RepositoryView::finalize(
+ RepositoryView::finalize(
repo_path,
Some(branch.to_string()),
status,
url,
email,
submodules,
- )?)
+ )
}
+ /// Assemble a [`RepositoryView`] with metadata for a given repository.
pub fn finalize(
path: &Path,
branch: Option<String>,
@@ -121,7 +141,7 @@ impl RepositoryView {
None => {
return Err(RepositoryViewError::FileNameToStrConversionFailure(
path.to_path_buf(),
- ))
+ ));
}
},
None => return Err(RepositoryViewError::FileNameNotFound(path.to_path_buf())),
@@ -132,7 +152,7 @@ impl RepositoryView {
None => {
return Err(RepositoryViewError::PathToStrConversionFailure(
s.to_path_buf(),
- ))
+ ));
}
},
None => None,
@@ -153,98 +173,6 @@ impl RepositoryView {
})
}
- /// Find the [`Status`] for a given [`Repository`](git2::Repository). The
- /// [`head`](Option<git2::Reference>) and [`remote`](Option<git2::Remote>) are also returned.
- pub fn find_status(
- repo: &Repository,
- ) -> anyhow::Result<(Status, Option<Reference>, Option<Remote>)> {
- let head = match repo.head() {
- Ok(head) => Some(head),
- Err(ref e)
- if e.code() == ErrorCode::UnbornBranch || e.code() == ErrorCode::NotFound =>
- {
- None
- }
- Err(e) => return Err(e.into()),
- };
-
- // Greedily chooses a remote if "origin" is not found.
- let (remote, remote_name) = match repo.find_remote("origin") {
- Ok(origin) => (Some(origin), Some("origin".to_string())),
- Err(e) if e.code() == ErrorCode::NotFound => Self::choose_remote_greedily(repo)?,
- Err(e) => return Err(e.into()),
- };
-
- // We'll include all untracked files and directories in the status options.
- let mut opts = StatusOptions::new();
- opts.include_untracked(true).recurse_untracked_dirs(true);
-
- // If "head" is "None" and statuses are empty, then the repository_view must be clean because there
- // are no commits to push.
- let status = match repo.statuses(Some(&mut opts)) {
- Ok(v) if v.is_empty() => match &head {
- Some(head) => match remote_name {
- Some(remote_name) => {
- match RepositoryView::is_unpushed(repo, head, &remote_name)? {
- true => Status::Unpushed,
- false => Status::Clean,
- }
- }
- None => Status::Clean,
- },
- None => Status::Clean,
- },
- Ok(_) => Status::Unclean,
- Err(e) if e.code() == ErrorCode::BareRepo => Status::Bare,
- Err(e) => return Err(e.into()),
- };
-
- Ok((status, head, remote))
- }
-
- fn choose_remote_greedily(
- repository: &Repository,
- ) -> Result<(Option<Remote>, Option<String>), git2::Error> {
- let remotes = repository.remotes()?;
- Ok(match remotes.get(0) {
- Some(remote_name) => (
- Some(repository.find_remote(remote_name)?),
- Some(remote_name.to_string()),
- ),
- None => (None, None),
- })
- }
-
- /// Checks if local commit(s) on the current branch have not yet been pushed to the remote.
- fn is_unpushed(
- repo: &Repository,
- head: &Reference,
- remote_name: &str,
- ) -> Result<bool, git2::Error> {
- let local_head = head.peel_to_commit()?;
- let remote = format!(
- "{}/{}",
- remote_name,
- match head.shorthand() {
- Some(v) => v,
- None => {
- debug!("assuming unpushed; could not determine shorthand for head");
- return Ok(true);
- }
- }
- );
- let remote_head = match repo.resolve_reference_from_short_name(&remote) {
- Ok(reference) => reference.peel_to_commit()?,
- Err(e) => {
- debug!("assuming unpushed; could not resolve remote reference from short name (ignored error: {})", e);
- return Ok(true);
- }
- };
- Ok(
- matches!(repo.graph_ahead_behind(local_head.id(), remote_head.id()), Ok(number_unique_commits) if number_unique_commits.0 > 0),
- )
- }
-
/// Find the "user.email" value in the local or global Git config. The
/// [`Repository::config()`] method will look for a local config first and fallback to
/// global, as needed. Absorb and log any and all errors as the email field is non-critical to
diff --git a/lib/libgfold/src/repository_view/submodule_view.rs b/lib/libgfold/src/repository_view/submodule_view.rs
index 54f0502..0ec0a0c 100644
--- a/lib/libgfold/src/repository_view/submodule_view.rs
+++ b/lib/libgfold/src/repository_view/submodule_view.rs
@@ -4,17 +4,25 @@ use git2::Repository;
use log::error;
use serde::Deserialize;
use serde::Serialize;
+use std::io;
use thiserror::Error;
-use crate::repository_view::RepositoryView;
-use crate::status::Status;
+use crate::status::{Status, StatusError};
#[derive(Error, Debug)]
pub enum SubmoduleError {
#[error("submodule name is invalid UTF-8")]
SubmoduleNameInvalid,
+ #[error(transparent)]
+ FromGit2(#[from] git2::Error),
+ #[error(transparent)]
+ FromStdIo(#[from] io::Error),
+ #[error(transparent)]
+ FromStatus(#[from] StatusError),
}
+type SubmoduleResult<T> = Result<T, SubmoduleError>;
+
/// The view of a submodule with a [`Repository`].
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct SubmoduleView {
@@ -24,12 +32,12 @@ pub struct SubmoduleView {
impl SubmoduleView {
/// Generate a list of [`submodule view(s)`](Self) for a given [`Repository`].
- pub fn list(repo: &Repository) -> anyhow::Result<Vec<Self>> {
+ pub fn list(repo: &Repository) -> SubmoduleResult<Vec<Self>> {
let mut submodules = Vec::new();
for submodule in repo.submodules()? {
match submodule.open() {
Ok(subrepo) => {
- let (status, _, _) = RepositoryView::find_status(&subrepo)?;
+ let (status, _, _) = Status::find(&subrepo)?;
let name = submodule
.name()
.ok_or(SubmoduleError::SubmoduleNameInvalid)?;
diff --git a/lib/libgfold/src/status.rs b/lib/libgfold/src/status.rs
index e6e104a..7ffd638 100644
--- a/lib/libgfold/src/status.rs
+++ b/lib/libgfold/src/status.rs
@@ -1,19 +1,38 @@
//! This module contains the [`crate::status::Status`] type.
+use git2::{ErrorCode, Reference, Remote, Repository, StatusOptions};
+use log::debug;
use serde::{Deserialize, Serialize};
+use thiserror::Error;
+
+#[allow(missing_docs)]
+#[derive(Error, Debug)]
+pub enum StatusError {
+ #[error(transparent)]
+ FromGit2(#[from] git2::Error),
+}
+
+#[allow(missing_docs)]
+pub type StatusResult<T> = Result<T, StatusError>;
/// A summarized interpretation of the status of a Git working tree.
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum Status {
+ /// Corresponds to a "bare" working tree.
Bare,
+ /// Corresponds to a "clean" working tree.
Clean,
+ /// Corresponds to an "unclean" working tree.
Unclean,
+ /// Provided if the state of the working tree could neither be found nor determined.
Unknown,
+ /// Indicates that there is at least one commit not pushed to the remote from the working tree.
Unpushed,
}
impl Status {
- pub fn as_str(&self) -> &str {
+ /// Converts the enum into a borrowed, static `str`.
+ pub fn as_str(&self) -> &'static str {
match self {
Self::Bare => "bare",
Self::Clean => "clean",
@@ -22,4 +41,94 @@ impl Status {
Self::Unpushed => "unpushed",
}
}
+
+ /// Find the [`Status`] for a given [`Repository`](git2::Repository). The
+ /// [`head`](Option<git2::Reference>) and [`remote`](Option<git2::Remote>) are also returned.
+ pub fn find(
+ repo: &Repository,
+ ) -> StatusResult<(Status, Option<Reference<'_>>, Option<Remote<'_>>)> {
+ let head = match repo.head() {
+ Ok(head) => Some(head),
+ Err(ref e)
+ if e.code() == ErrorCode::UnbornBranch || e.code() == ErrorCode::NotFound =>
+ {
+ None
+ }
+ Err(e) => return Err(e.into()),
+ };
+
+ // Greedily chooses a remote if "origin" is not found.
+ let (remote, remote_name) = match repo.find_remote("origin") {
+ Ok(origin) => (Some(origin), Some("origin".to_string())),
+ Err(e) if e.code() == ErrorCode::NotFound => Self::choose_remote_greedily(repo)?,
+ Err(e) => return Err(e.into()),
+ };
+
+ // We'll include all untracked files and directories in the status options.
+ let mut opts = StatusOptions::new();
+ opts.include_untracked(true).recurse_untracked_dirs(true);
+
+ // If "head" is "None" and statuses are empty, then the repository_view must be clean because there
+ // are no commits to push.
+ let status = match repo.statuses(Some(&mut opts)) {
+ Ok(v) if v.is_empty() => match &head {
+ Some(head) => match remote_name {
+ Some(remote_name) => match Self::is_unpushed(repo, head, &remote_name)? {
+ true => Status::Unpushed,
+ false => Status::Clean,
+ },
+ None => Status::Clean,
+ },
+ None => Status::Clean,
+ },
+ Ok(_) => Status::Unclean,
+ Err(e) if e.code() == ErrorCode::BareRepo => Status::Bare,
+ Err(e) => return Err(e.into()),
+ };
+
+ Ok((status, head, remote))
+ }
+
+ // Checks if local commit(s) on the current branch have not yet been pushed to the remote.
+ fn is_unpushed(
+ repo: &Repository,
+ head: &Reference<'_>,
+ remote_name: &str,
+ ) -> Result<bool, git2::Error> {
+ let local_head = head.peel_to_commit()?;
+ let remote = format!(
+ "{}/{}",
+ remote_name,
+ match head.shorthand() {
+ Some(v) => v,
+ None => {
+ debug!("assuming unpushed; could not determine shorthand for head");
+ return Ok(true);
+ }
+ }
+ );
+ let remote_head = match repo.resolve_reference_from_short_name(&remote) {
+ Ok(reference) => reference.peel_to_commit()?,
+ Err(e) => {
+ debug!("assuming unpushed; could not resolve remote reference from short name (ignored error: {})", e);
+ return Ok(true);
+ }
+ };
+ Ok(
+ matches!(repo.graph_ahead_behind(local_head.id(), remote_head.id()), Ok(number_unique_commits) if number_unique_commits.0 > 0),
+ )
+ }
+
+ fn choose_remote_greedily(
+ repository: &Repository,
+ ) -> Result<(Option<Remote<'_>>, Option<String>), git2::Error> {
+ let remotes = repository.remotes()?;
+ Ok(match remotes.get(0) {
+ Some(remote_name) => (
+ Some(repository.find_remote(remote_name)?),
+ Some(remote_name.to_string()),
+ ),
+ None => (None, None),
+ })
+ }
}