summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile11
-rw-r--r--README.md30
-rw-r--r--src/gfold.rs188
-rw-r--r--src/lib.rs225
-rw-r--r--src/main.rs25
-rw-r--r--src/util.rs17
6 files changed, 260 insertions, 236 deletions
diff --git a/Makefile b/Makefile
index b2d8e9b..30b9645 100644
--- a/Makefile
+++ b/Makefile
@@ -8,6 +8,8 @@ MAKEPATH:=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
NAME:=gfold
VERSION:=0.5.2
+all: build
+
run:
@cd $(MAKEPATH); cargo run -- ..
@@ -20,16 +22,15 @@ install:
install-local:
cargo install --path $(MAKEPATH)
-build: fmt test
+build: pre-build
cd $(MAKEPATH); cargo build
-build-release: fmt test
+build-release: pre-build
cd $(MAKEPATH); cargo build --release
-fmt:
+pre-build:
cd $(MAKEPATH); cargo fmt
-
-test:
+ cd $(MAKEPATH); cargo clippy
cd $(MAKEPATH); cargo test
tree:
diff --git a/README.md b/README.md
index a01bd6a..ccc64ea 100644
--- a/README.md
+++ b/README.md
@@ -30,7 +30,7 @@ However, if you would like to target another directory, you can pass that path (
There are multiple ways to install ```gfold```, but here are the currently recommended methods...
-### 1) Download a GitHub Binary
+### 1. Download a GitHub Binary
Most likely, the easiest method of obtaining ```gfold``` is via the [latest GitHub release](https://github.com/nickgerace/gfold/releases/latest).
@@ -47,30 +47,34 @@ You may have to reload your shell in order to see ```gfold``` in your ```PATH```
#### Advanced Management
You can use symbolic links to swap between versions, and manage multiple at a time.
-Here is a full install workflow...
+Here is a full install workflow example...
```bash
-wget https://github.com/nickgerace/gfold/releases/download/$VERSION/gfold-linux-gnu-amd64)
-mv gfold-linux-gnu-amd64 gfold-$VERSION
+VERSION=0.5.2
+PLATFORM=linux-gnu-amd64
+
+wget https://github.com/nickgerace/gfold/releases/download/$VERSION/gfold-$PLATFORM
+mv gfold-$PLATFORM gfold-$VERSION
chmod +x gfold-$VERSION
-sudo mkdir /usr/local/gfold/
-sudo mv gfold-$VERSION /usr/local/gfold/
+
+mkdir /usr/local/gfold/
+mv gfold-$VERSION /usr/local/gfold/
ln -s /usr/local/gfold/gfold-$VERSION /usr/local/bin/gfold
```
Now, you can add/remove versions of the binary from ```/usr/local/gfold/```, and change the symbolic link as needed.
-### 2) AUR (Arch User Repository)
+### 2. Arch User Repository (AUR)
This application is available for all Linux distributions that support installing packages from the AUR.
-Special thanks to [orhun](https://github.com/orhun) for [maintaining](https://github.com/orhun/PKGBUILDs) these packages.
- [gfold](https://aur.archlinux.org/packages/gfold/) (builds from source)
+- [gfold-bin](https://aur.archlinux.org/packages/gfold-bin/) (uses the GitHub release binary)
- [gfold-git](https://aur.archlinux.org/packages/gfold-git/) (VCS/development package)
Many people choose to use an [AUR helper](https://wiki.archlinux.org/index.php/AUR_helpers), such as [yay](https://github.com/Jguer/yay) (example: ```yay -S gfold```), in order to install their AUR packages.
-### 3) Cargo Install
+### 3. Cargo Install
You can build from source with ```cargo``` by executing the following...
@@ -100,11 +104,11 @@ gfold -r $HOME/path/to/multiple/repositories
## Compatibility
-All external crates were vetted for support on all three major desktop platforms.
-```gfold``` is tested for the latest versions of the following systems, but may work on more...
+```gfold```, and its external crates, support all three major desktop platforms.
+It is tested for the latest versions of the following systems, but may work on more...
- **Linux**: ```linux-gnu-amd64```
-- **macOS**: ```darwin-amd64```
+- **macOS**: ```macos-amd64```
- **Windows 10**: ```windows-amd64```
## Changelog
@@ -120,5 +124,5 @@ It follows the [Keep a Changelog](https://keepachangelog.com/) format.
## Special Thanks
- [@yaahc](https://github.com/yaahc) (mentoring)
-- [@orhun](https://github.com/orhun) (maintaining AUR packages)
+- [@orhun](https://github.com/orhun) (maintaining [all three AUR packages](https://github.com/orhun/PKGBUILDs))
- [@jrcichra](https://github.com/jrcichra) (adding multi-OS support to the original CI pipeline)
diff --git a/src/gfold.rs b/src/gfold.rs
deleted file mode 100644
index 2af9385..0000000
--- a/src/gfold.rs
+++ /dev/null
@@ -1,188 +0,0 @@
-/*
- * gfold
- * https://github.com/nickgerace/gfold
- * Author: Nick Gerace
- * License: Apache 2.0
- */
-
-use git2::{ErrorClass, ErrorCode, Repository, StatusOptions};
-use prettytable::{format, Table};
-
-use std::fs;
-use std::path::Path;
-
-use crate::util::is_git_repo;
-
-struct TableWrapper {
- path_string: String,
- table: Table,
-}
-
-struct Results {
- recursive: bool,
- tables: Vec<TableWrapper>,
-}
-
-impl Results {
- fn print_results(self) {
- if self.recursive && self.tables.len() > 1 {
- for table_wrapper in self.tables {
- println!("\n{}", table_wrapper.path_string);
- table_wrapper.table.printstd();
- }
- } else if self.tables.len() == 1 {
- self.tables[0].table.printstd();
- } else {
- println!("There are no results to display.");
- }
- }
-
- fn sort_results(&mut self) {
- // FIXME: find a way to do this without "clone()".
- self.tables.sort_by_key(|table| table.path_string.clone());
- }
-
- fn execute_on_path(&mut self, path: &Path) {
- // FIXME: find ways to add concurrent programming (tokio, async, etc.) to this section.
- // In such implementation, sort the results at the end after concurrent operations conclude.
- let path_entries = fs::read_dir(path).expect("failed to get sub directories");
- let mut repos = Vec::new();
-
- for entry in path_entries {
- let subpath = &entry.expect("failed to get DirEntry").path();
- if subpath.is_dir() {
- if is_git_repo(subpath) {
- repos.push(subpath.to_owned());
- } else if self.recursive {
- self.execute_on_path(&subpath);
- }
- }
- }
- if repos.is_empty() {
- return;
- }
-
- // Alphabetically sort the repository paths, and create a mutable Table object. For
- // every path, we will create a Table containing its results.
- repos.sort();
- let mut table = Table::new();
- table.set_format(
- format::FormatBuilder::new()
- .column_separator(' ')
- .padding(0, 1)
- .build(),
- );
-
- for repo in repos {
- let repo_obj = Repository::open(&repo).expect("failed to open");
-
- // This match cascade combats the error: remote 'origin' does not exist. If we
- // encounter this specific error, then we "continue" to the next iteration.
- let origin = match repo_obj.find_remote("origin") {
- Ok(origin) => origin,
- Err(error) if error.class() == ErrorClass::Config => continue,
- Err(error) => panic!("{}", error),
- };
- let url = match origin.url() {
- Some(url) => url,
- None => "none",
- };
- let head = repo_obj.head().expect("failed get head");
- let branch = match head.shorthand() {
- Some(head) => head,
- None => "none",
- };
-
- // If the repository is bare, then we return "None". This addresses GitHub issue #11
- // (https://github.com/nickgerace/gfold/issues/11), and special thanks to @yaahc_ for
- // the recommendation to use a "match guard" here. We also use the Option type instead
- // to handle the "None" case later.
- let mut opts = StatusOptions::new();
- let statuses = match repo_obj.statuses(Some(&mut opts)) {
- Ok(statuses) => Some(statuses),
- Err(error)
- if error.code() == ErrorCode::BareRepo
- && error.class() == ErrorClass::Repository =>
- {
- None
- }
- Err(error) => panic!("failed to get statuses: {}", error),
- };
-
- let formatted_name = Path::new(&repo)
- .strip_prefix(path)
- .expect("failed to format name from Path object");
- let str_name = match formatted_name.to_str() {
- Some(x) => x,
- None => "none",
- };
-
- match statuses {
- Some(statuses) if statuses.is_empty() => {
- table.add_row(row![Flb->str_name, Fgl->"clean", Fl->branch, Fl->url])
- }
- Some(_) => table.add_row(row![Flb->str_name, Fyl->"unclean", Fl->branch, Fl->url]),
- None => table.add_row(row![Flb->str_name, Frl->"bare", Fl->branch, Fl->url]),
- };
- }
-
- // Only perform the following actions if the Table object is not empty. We only want
- // results for directories that contain repositories. Push the resulting TableWrapper
- // object aftering creating a heap-allocated string for the path name.
- if !table.is_empty() {
- let path_string = path
- .to_str()
- .expect("could not convert &Path object to &str object");
- let table_wrapper = TableWrapper {
- path_string: path_string.to_string(),
- table: table,
- };
- self.tables.push(table_wrapper);
- }
- }
-}
-
-pub fn harness(path: &Path, recursive: bool, skip_sort: bool) {
- let mut results = Results {
- recursive: recursive,
- tables: Vec::new(),
- };
- results.execute_on_path(&path);
- if !skip_sort {
- results.sort_results();
- }
- results.print_results();
-}
-
-#[cfg(test)]
-mod tests {
- use super::harness;
- use std::env::current_dir;
-
- #[test]
- fn current_directory() {
- let current_dir = current_dir().expect("failed to get CWD");
- harness(&current_dir, false, false);
- }
-
- #[test]
- fn parent_directory() {
- let mut current_dir = current_dir().expect("failed to get CWD");
- current_dir.pop();
- harness(&current_dir, false, false);
- }
-
- #[test]
- fn parent_directory_recursive() {
- let mut current_dir = current_dir().expect("failed to get CWD");
- current_dir.pop();
- harness(&current_dir, true, false);
- }
-
- #[test]
- fn parent_directory_recursive_skip_sort() {
- let mut current_dir = current_dir().expect("failed to get CWD");
- current_dir.pop();
- harness(&current_dir, true, true);
- }
-}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..674a75a
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,225 @@
+/*
+ * gfold
+ * https://github.com/nickgerace/gfold
+ * Author: Nick Gerace
+ * License: Apache 2.0
+ */
+
+#[macro_use]
+extern crate prettytable;
+
+use std::cmp::Ordering;
+use std::error::Error;
+use std::fs;
+use std::path::Path;
+use std::path::PathBuf;
+
+/// Creating a ```Results``` object requires using this ```struct``` as a pre-requisite.
+struct Config {
+ recursive: bool,
+ skip_sort: bool,
+}
+
+/// This ```struct``` is a wrapper around the ```prettytable::Table``` object.
+/// It exists to provide a label for the table.
+struct TableWrapper {
+ path_string: String,
+ table: prettytable::Table,
+}
+
+/// Contains all tables with results for each directory.
+struct Results(Vec<TableWrapper>);
+
+impl Results {
+ /// Create a new ```Results``` object with a given path and config.
+ fn new(path: &Path, config: &Config) -> Result<Results, Box<dyn Error>> {
+ let mut results = Results(Vec::new());
+ results.execute_in_directory(&config, path)?;
+ if !&config.skip_sort {
+ results.sort_results();
+ }
+ Ok(results)
+ }
+
+ /// Load results into the calling ```Results``` object via a given path and config.
+ /// This function may be called recursively.
+ fn execute_in_directory(&mut self, config: &Config, dir: &Path) -> Result<(), Box<dyn Error>> {
+ // FIXME: find ways to add concurrent programming (tokio, async, etc.) to this section.
+ let path_entries = fs::read_dir(dir)?;
+ let mut repos = Vec::new();
+
+ for entry in path_entries {
+ let subpath = &entry?.path();
+ if subpath.is_dir() {
+ if git2::Repository::open(subpath).is_ok() {
+ repos.push(subpath.to_owned());
+ } else if config.recursive {
+ self.execute_in_directory(&config, &subpath)?;
+ }
+ }
+ }
+ if !repos.is_empty() {
+ if !&config.skip_sort {
+ repos.sort();
+ }
+ // If a table was successfully created with the given repositories, add the table.
+ if let Some(table_wrapper) = create_table_from_paths(repos, &dir) {
+ self.0.push(table_wrapper);
+ }
+ }
+ Ok(())
+ }
+
+ /// Sort the results alphabetically using ```sort_by_key```.
+ /// This function will only perform the sort if there are at least two ```TableWrapper``` objects.
+ fn sort_results(&mut self) {
+ if self.0.len() >= 2 {
+ // FIXME: find a way to do this without "clone()".
+ self.0.sort_by_key(|table| table.path_string.clone());
+ }
+ }
+
+ /// Iterate through every table and print each to STDOUT.
+ /// If there is only one table, this function avoids using a loop.
+ fn print_results(self) {
+ match self.0.len().cmp(&1) {
+ Ordering::Greater => {
+ for table_wrapper in self.0 {
+ println!("\n{}", table_wrapper.path_string);
+ table_wrapper.table.printstd();
+ }
+ }
+ Ordering::Equal => {
+ self.0[0].table.printstd();
+ }
+ Ordering::Less => {
+ println!("There are no results to display.");
+ }
+ };
+ }
+}
+
+/// Create a ```TableWrapper``` object from a given vector of paths (```Vec<PathBuf>```).
+fn create_table_from_paths(repos: Vec<PathBuf>, path: &Path) -> Option<TableWrapper> {
+ // For every path, we will create a mutable Table containing its results.
+ let mut table = prettytable::Table::new();
+ table.set_format(
+ prettytable::format::FormatBuilder::new()
+ .column_separator(' ')
+ .padding(0, 1)
+ .build(),
+ );
+
+ // FIXME: handle all panic and fatal scenarios in this loop.
+ for repo in repos {
+ let repo_obj = git2::Repository::open(&repo).expect("failed to open");
+
+ // This match cascade combats the error: remote 'origin' does not exist. If we
+ // encounter this specific error, then we "continue" to the next iteration.
+ let origin = match repo_obj.find_remote("origin") {
+ Ok(origin) => origin,
+ Err(error) if error.class() == git2::ErrorClass::Config => continue,
+ Err(error) => panic!("{}", error),
+ };
+ let url = match origin.url() {
+ Some(url) => url,
+ None => "none",
+ };
+ let head = repo_obj.head().expect("failed get head");
+ let branch = match head.shorthand() {
+ Some(head) => head,
+ None => "none",
+ };
+
+ // Special thanks to @yaahc_ for the original recommendation to use a "match guard" here.
+ // The code has evolved since the original implementation, but the core idea still stands!
+ let mut opts = git2::StatusOptions::new();
+ let statuses = match repo_obj.statuses(Some(&mut opts)) {
+ Ok(statuses) => Ok(Some(statuses)),
+ Err(error)
+ if error.code() == git2::ErrorCode::BareRepo
+ && error.class() == git2::ErrorClass::Repository =>
+ {
+ Ok(None)
+ }
+ Err(error) => Err(error),
+ };
+
+ let formatted_name = Path::new(&repo)
+ .strip_prefix(path)
+ .expect("failed to format name from Path object");
+ let str_name = match formatted_name.to_str() {
+ Some(x) => x,
+ None => "none",
+ };
+
+ match statuses {
+ Ok(statuses_contents) => match statuses_contents {
+ Some(statuses_contents) if statuses_contents.is_empty() => {
+ table.add_row(row![Flb->str_name, Fgl->"clean", Fl->branch, Fl->url])
+ }
+ Some(_) => table.add_row(row![Flb->str_name, Fyl->"unclean", Fl->branch, Fl->url]),
+ None => table.add_row(row![Flb->str_name, Frl->"bare", Fl->branch, Fl->url]),
+ },
+ Err(_) => table.add_row(row![Flb->str_name, Frl->"error", Fl->branch, Fl->url]),
+ };
+ }
+
+ // After looping over all the paths, check if the table contains any rows. We perform this
+ // check because we only want results for directories that contain Git repositories. Push
+ // the resulting TableWrapper object after creating a heap-allocated string for the path name.
+ if table.is_empty() {
+ return None;
+ }
+ let path_string = path
+ .to_str()
+ .expect("could not convert &Path object to &str object");
+ Some(TableWrapper {
+ path_string: path_string.to_string(),
+ table,
+ })
+}
+
+/// This function is the primary driver for this file, ```lib.rs```.
+pub fn run(path: &Path, recursive: bool, skip_sort: bool) -> Result<(), Box<dyn Error>> {
+ let config = Config {
+ recursive,
+ skip_sort,
+ };
+ let results = Results::new(path, &config)?;
+ results.print_results();
+ Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::env;
+
+ #[test]
+ fn current_directory() {
+ let current_dir = env::current_dir().expect("failed to get CWD");
+ assert_ne!(run(&current_dir, false, false).is_err(), true);
+ }
+
+ #[test]
+ fn parent_directory() {
+ let mut current_dir = env::current_dir().expect("failed to get CWD");
+ current_dir.pop();
+ assert_ne!(run(&current_dir, false, false).is_err(), true);
+ }
+
+ #[test]
+ fn parent_directory_recursive() {
+ let mut current_dir = env::current_dir().expect("failed to get CWD");
+ current_dir.pop();
+ assert_ne!(run(&current_dir, true, false).is_err(), true);
+ }
+
+ #[test]
+ fn parent_directory_recursive_skip_sort() {
+ let mut current_dir = env::current_dir().expect("failed to get CWD");
+ current_dir.pop();
+ assert_ne!(run(&current_dir, true, true).is_err(), true);
+ }
+}
diff --git a/src/main.rs b/src/main.rs
index f5d4650..887bfc8 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -5,23 +5,17 @@
* License: Apache 2.0
*/
-#[macro_use]
-extern crate prettytable;
-
-use structopt::StructOpt;
-
-use std::env::current_dir;
+use std::env;
use std::path::PathBuf;
+use std::process;
-mod gfold;
-mod util;
-use gfold::harness;
+use structopt::StructOpt;
#[derive(StructOpt, Debug)]
#[structopt(
name = "gfold",
about = "https://github.com/nickgerace/gfold\n\n\
-This application helps your organize multiple Git repositories via CLI.\n\
+This application helps you keep track of multiple Git repositories via CLI.\n\
By default, it displays relevant information for all repos in the current\n\
working directory."
)]
@@ -34,14 +28,19 @@ struct Opt {
skip_sort: bool,
}
+/// This file, ```main.rs```, serves as the primary driver for the ```gfold``` library.
+/// It is intended to be used as a command-line interface.
fn main() {
- let mut path = current_dir().expect("failed to get CWD");
+ let mut path = env::current_dir().expect("failed to get CWD");
let opt = Opt::from_args();
if let Some(provided_path) = opt.path {
path.push(provided_path)
};
-
path = path.canonicalize().expect("failed to canonicalize path");
- harness(&path, opt.recursive, opt.skip_sort);
+
+ if let Err(error) = gfold::run(&path, opt.recursive, opt.skip_sort) {
+ eprintln!("Encountered fatal error: {}", error);
+ process::exit(1);
+ };
}
diff --git a/src/util.rs b/src/util.rs
deleted file mode 100644
index 0e097e6..0000000
--- a/src/util.rs
+++ /dev/null
@@ -1,17 +0,0 @@
-/*
- * gfold
- * https://github.com/nickgerace/gfold
- * Author: Nick Gerace
- * License: Apache 2.0
- */
-
-use git2::Repository;
-
-use std::path::Path;
-
-pub fn is_git_repo(target: &Path) -> bool {
- match Repository::open(target) {
- Ok(_) => true,
- Err(_) => false,
- }
-}