summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAlexander Gonzalez <alexfertel97@gmail.com>2021-07-10 16:54:34 -0400
committerGitHub <noreply@github.com>2021-07-10 16:54:34 -0400
commit9f337d15e7efb6010066088d7f0a611a6ecb5d79 (patch)
treed8d32c63272997a81244151e430381930d89a857 /src
parent9126d78d0e102b925fce1dabd0b6f837e4e055e1 (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.rs23
-rw-r--r--src/configs/mod.rs3
-rw-r--r--src/configs/starship_root.rs1
-rw-r--r--src/module.rs1
-rw-r--r--src/modules/git_metrics.rs275
-rw-r--r--src/modules/git_state.rs2
-rw-r--r--src/modules/mod.rs3
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",