diff options
author | Alexander Gonzalez <alexfertel97@gmail.com> | 2021-07-10 16:54:34 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-07-10 16:54:34 -0400 |
commit | 9f337d15e7efb6010066088d7f0a611a6ecb5d79 (patch) | |
tree | d8d32c63272997a81244151e430381930d89a857 /src | |
parent | 9126d78d0e102b925fce1dabd0b6f837e4e055e1 (diff) |
feat: Add the `git_metrics` module (#2827)
This PR adds a new module named git_metrics. It shows the added/deleted lines in the current git repository following the format: "[+$added_lines]($added_style) [-$deleted_lines]($deleted_style)".
Diffstat (limited to 'src')
-rw-r--r-- | src/configs/git_metrics.rs | 23 | ||||
-rw-r--r-- | src/configs/mod.rs | 3 | ||||
-rw-r--r-- | src/configs/starship_root.rs | 1 | ||||
-rw-r--r-- | src/module.rs | 1 | ||||
-rw-r--r-- | src/modules/git_metrics.rs | 275 | ||||
-rw-r--r-- | src/modules/git_state.rs | 2 | ||||
-rw-r--r-- | src/modules/mod.rs | 3 |
7 files changed, 307 insertions, 1 deletions
diff --git a/src/configs/git_metrics.rs b/src/configs/git_metrics.rs new file mode 100644 index 000000000..f3e52e200 --- /dev/null +++ b/src/configs/git_metrics.rs @@ -0,0 +1,23 @@ +use crate::config::ModuleConfig; + +use serde::Serialize; +use starship_module_config_derive::ModuleConfig; + +#[derive(Clone, ModuleConfig, Serialize)] +pub struct GitMetricsConfig<'a> { + pub added_style: &'a str, + pub deleted_style: &'a str, + pub format: &'a str, + pub disabled: bool, +} + +impl<'a> Default for GitMetricsConfig<'a> { + fn default() -> Self { + GitMetricsConfig { + added_style: "bold green", + deleted_style: "bold red", + format: "[+$added]($added_style) [-$deleted]($deleted_style) ", + disabled: true, + } + } +} diff --git a/src/configs/mod.rs b/src/configs/mod.rs index ec5434fc3..658c4553b 100644 --- a/src/configs/mod.rs +++ b/src/configs/mod.rs @@ -23,6 +23,7 @@ pub mod erlang; pub mod gcloud; pub mod git_branch; pub mod git_commit; +pub mod git_metrics; pub mod git_state; pub mod git_status; pub mod go; @@ -95,6 +96,7 @@ pub struct FullConfig<'a> { gcloud: gcloud::GcloudConfig<'a>, git_branch: git_branch::GitBranchConfig<'a>, git_commit: git_commit::GitCommitConfig<'a>, + git_metrics: git_metrics::GitMetricsConfig<'a>, git_state: git_state::GitStateConfig<'a>, git_status: git_status::GitStatusConfig<'a>, golang: go::GoConfig<'a>, @@ -164,6 +166,7 @@ impl<'a> Default for FullConfig<'a> { gcloud: Default::default(), git_branch: Default::default(), git_commit: Default::default(), + git_metrics: Default::default(), git_state: Default::default(), git_status: Default::default(), golang: Default::default(), diff --git a/src/configs/starship_root.rs b/src/configs/starship_root.rs index bf656ebea..7d5bfcdab 100644 --- a/src/configs/starship_root.rs +++ b/src/configs/starship_root.rs @@ -26,6 +26,7 @@ pub const PROMPT_ORDER: &[&str] = &[ "git_branch", "git_commit", "git_state", + "git_metrics", "git_status", "hg_branch", "docker_context", diff --git a/src/module.rs b/src/module.rs index 7beb13050..eb8874c4e 100644 --- a/src/module.rs +++ b/src/module.rs @@ -28,6 +28,7 @@ pub const ALL_MODULES: &[&str] = &[ "gcloud", "git_branch", "git_commit", + "git_metrics", "git_state", "git_status", "golang", diff --git a/src/modules/git_metrics.rs b/src/modules/git_metrics.rs new file mode 100644 index 000000000..833faec34 --- /dev/null +++ b/src/modules/git_metrics.rs @@ -0,0 +1,275 @@ +use regex::Regex; + +use crate::{ + config::RootModuleConfig, configs::git_metrics::GitMetricsConfig, formatter::StringFormatter, + module::Module, +}; + +use super::Context; + +/// Creates a module with the current added/deleted lines in the git repository at the +/// current directory +pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> { + let mut module = context.new_module("git_metrics"); + let config: GitMetricsConfig = GitMetricsConfig::try_load(module.config); + + // As we default to disabled=true, we have to check here after loading our config module, + // before it was only checking against whatever is in the config starship.toml + if config.disabled { + return None; + }; + + let repo = context.get_repo().ok()?; + let repo_root = repo.root.as_ref()?; + + let diff = context + .exec_cmd( + "git", + &[ + "-C", + &repo_root.to_string_lossy(), + "--no-optional-locks", + "diff", + "--shortstat", + ], + )? + .stdout; + + let stats = GitDiff::parse(&diff); + + let parsed = StringFormatter::new(config.format).and_then(|formatter| { + formatter + .map_style(|variable| match variable { + "added_style" => Some(Ok(config.added_style)), + "deleted_style" => Some(Ok(config.deleted_style)), + _ => None, + }) + .map(|variable| match variable { + "added" => Some(Ok(stats.added)), + "deleted" => Some(Ok(stats.deleted)), + _ => None, + }) + .parse(None) + }); + + module.set_segments(match parsed { + Ok(segments) => segments, + Err(error) => { + log::warn!("Error in module `git_metrics`:\n{}", error); + return None; + } + }); + + Some(module) +} + +/// Represents the parsed output from a git diff. +struct GitDiff<'a> { + added: &'a str, + deleted: &'a str, +} + +impl<'a> GitDiff<'a> { + /// Returns the first capture group given a regular expression and a string. + /// If it fails to get the capture group it will return "0". + fn get_matched_str(diff: &'a str, re: &Regex) -> &'a str { + match re.captures(diff) { + Some(caps) => caps.get(1).unwrap().as_str(), + _ => "0", + } + } + + /// Parses the result of 'git diff --shortstat' as a `GitDiff` struct. + pub fn parse(diff: &'a str) -> Self { + let added_re = Regex::new(r"(\d+) \w+\(\+\)").unwrap(); + let deleted_re = Regex::new(r"(\d+) \w+\(\-\)").unwrap(); + + Self { + added: GitDiff::get_matched_str(diff, &added_re), + deleted: GitDiff::get_matched_str(diff, &deleted_re), + } + } +} + +#[cfg(test)] +mod tests { + use std::ffi::OsStr; + use std::fs::OpenOptions; + use std::io::{self, Error, ErrorKind, Write}; + use std::path::{Path, PathBuf}; + use std::process::{Command, Stdio}; + + use ansi_term::Color; + + use crate::test::ModuleRenderer; + + #[test] + fn shows_nothing_on_empty_dir() -> io::Result<()> { + let repo_dir = tempfile::tempdir()?; + let path = repo_dir.path(); + + let actual = render_metrics(path); + + let expected = None; + + assert_eq!(expected, actual); + repo_dir.close() + } + + #[test] + fn shows_added_lines() -> io::Result<()> { + let repo_dir = create_repo_with_commit()?; + let path = repo_dir.path(); + + let the_file = path.join("the_file"); + let mut the_file = OpenOptions::new().append(true).open(&the_file)?; + writeln!(the_file, "Added line")?; + the_file.sync_all()?; + + let actual = render_metrics(path); + + let expected = Some(format!( + "{} {} ", + Color::Green.bold().paint("+1"), + Color::Red.bold().paint("-0") + )); + + assert_eq!(expected, actual); + repo_dir.close() + } + + #[test] + fn shows_deleted_lines() -> io::Result<()> { + let repo_dir = create_repo_with_commit()?; + let path = repo_dir.path(); + + let file_path = path.join("the_file"); + write_file(file_path, "First Line\nSecond Line")?; + + let actual = render_metrics(path); + + let expected = Some(format!( + "{} {} ", + Color::Green.bold().paint("+0"), + Color::Red.bold().paint("-1") + )); + + assert_eq!(expected, actual); + repo_dir.close() + } + + #[test] + fn shows_all_changes() -> io::Result<()> { + let repo_dir = create_repo_with_commit()?; + let path = repo_dir.path(); + + let file_path = path.join("the_file"); + write_file(file_path, "\nSecond Line\n\nModified\nAdded")?; + + let actual = render_metrics(path); + + let expected = Some(format!( + "{} {} ", + Color::Green.bold().paint("+4"), + Color::Red.bold().paint("-2") + )); + + assert_eq!(expected, actual); + repo_dir.close() + } + + fn render_metrics(path: &Path) -> Option<String> { + ModuleRenderer::new("git_metrics") + .config(toml::toml! { + [git_metrics] + disabled = false + }) + .path(path) + .collect() + } + + fn run_git_cmd<A, S>(args: A, dir: Option<&Path>, should_succeed: bool) -> io::Result<()> + where + A: IntoIterator<Item = S>, + S: AsRef<OsStr>, + { + let mut command = Command::new("git"); + command + .args(args) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .stdin(Stdio::null()); + + if let Some(dir) = dir { + command.current_dir(dir); + } + + let status = command.status()?; + + if should_succeed && !status.success() { + Err(Error::from(ErrorKind::Other)) + } else { + Ok(()) + } + } + + fn write_file(file: PathBuf, text: &str) -> io::Result<()> { + let mut file = OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(&file)?; + writeln!(file, "{}", text)?; + file.sync_all() + } + + fn create_repo_with_commit() -> io::Result<tempfile::TempDir> { + let repo_dir = tempfile::tempdir()?; + let path = repo_dir.path(); + let file = repo_dir.path().join("the_file"); + + // Initialize a new git repo + run_git_cmd( + &[ + "init", + "--quiet", + path.to_str().expect("Path was not UTF-8"), + ], + None, + true, + )?; + + // Set local author info + run_git_cmd( + &["config", "--local", "user.email", "starship@example.com"], + Some(path), + true, + )?; + run_git_cmd( + &["config", "--local", "user.name", "starship"], + Some(path), + true, + )?; + + // Ensure on the expected branch. + // If build environment has `init.defaultBranch` global set + // it will default to an unknown branch, so need to make & change branch + run_git_cmd( + &["checkout", "-b", "master"], + Some(path), + // command expected to fail if already on the expected branch + false, + )?; + + // Write a file on master and commit it + write_file(file, "First Line\nSecond Line\nThird Line")?; + run_git_cmd(&["add", "the_file"], Some(path), true)?; + run_git_cmd( + &["commit", "--message", "Commit A", "--no-gpg-sign"], + Some(path), + true, + )?; + + Ok(repo_dir) + } +} diff --git a/src/modules/git_state.rs b/src/modules/git_state.rs index cfd259033..f2d1e1c9d 100644 --- a/src/modules/git_state.rs +++ b/src/modules/git_state.rs @@ -337,7 +337,7 @@ mod tests { // Ensure on the expected branch. // If build environment has `init.defaultBranch` global set - // it will default to an unknown branch, so neeed to make & change branch + // it will default to an unknown branch, so need to make & change branch run_git_cmd( &["checkout", "-b", "master"], Some(path), diff --git a/src/modules/mod.rs b/src/modules/mod.rs index f64b016bc..49c746e70 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -18,6 +18,7 @@ mod erlang; mod gcloud; mod git_branch; mod git_commit; +mod git_metrics; mod git_state; mod git_status; mod golang; @@ -98,6 +99,7 @@ pub fn handle<'a>(module: &str, context: &'a Context) -> Option<Module<'a>> { "gcloud" => gcloud::module(context), "git_branch" => git_branch::module(context), "git_commit" => git_commit::module(context), + "git_metrics" => git_metrics::module(context), "git_state" => git_state::module(context), "git_status" => git_status::module(context), "golang" => golang::module(context), @@ -180,6 +182,7 @@ pub fn description(module: &str) -> &'static str { "gcloud" => "The current GCP client configuration", "git_branch" => "The active branch of the repo in your current directory", "git_commit" => "The active commit (and tag if any) of the repo in your current directory", + "git_metrics" => "The currently added/deleted lines in your repo", "git_state" => "The current git operation, and it's progress", "git_status" => "Symbol representing the state of the repo", "golang" => "The currently installed version of Golang", |