mirror of
https://github.com/Llewellynvdm/starship.git
synced 2025-04-10 10:51:51 +00:00
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)".
This commit is contained in:
parent
9126d78d0e
commit
9f337d15e7
@ -194,6 +194,7 @@ $vcsh\
|
|||||||
$git_branch\
|
$git_branch\
|
||||||
$git_commit\
|
$git_commit\
|
||||||
$git_state\
|
$git_state\
|
||||||
|
$git_metrics\
|
||||||
$git_status\
|
$git_status\
|
||||||
$hg_branch\
|
$hg_branch\
|
||||||
$docker_context\
|
$docker_context\
|
||||||
@ -1232,6 +1233,48 @@ format = '[\($state( $progress_current of $progress_total)\)]($style) '
|
|||||||
cherry_pick = "[🍒 PICKING](bold red)"
|
cherry_pick = "[🍒 PICKING](bold red)"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Git Metrics
|
||||||
|
|
||||||
|
The `git_metrics` module will show the number of added and deleted lines in
|
||||||
|
the current git repository.
|
||||||
|
|
||||||
|
::: tip
|
||||||
|
|
||||||
|
This module is disabled by default.
|
||||||
|
To enable it, set `disabled` to `false` in your configuration file.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
| Option | Default | Description |
|
||||||
|
| ------------------------- | -------------------------------------------------------------------- | ---------------------------------------|
|
||||||
|
| `added_style` | `"bold green"` | The style for the added count. |
|
||||||
|
| `deleted_style` | `"bold red"` | The style for the deleted count. |
|
||||||
|
| `format` | `'[+$added]($added_style) [-$deleted]($deleted_style) '` | The format for the module. |
|
||||||
|
| `disabled` | `true` | Disables the `git_metrics` module. |
|
||||||
|
|
||||||
|
### Variables
|
||||||
|
|
||||||
|
| Variable | Example | Description |
|
||||||
|
| ---------------- | ---------- | ----------------------------------- |
|
||||||
|
| added | `1` | The current number of added lines |
|
||||||
|
| deleted | `2` | The current number of deleted lines |
|
||||||
|
| added_style\* | | Mirrors the value of option `added_style` |
|
||||||
|
| deleted_style\* | | Mirrors the value of option `deleted_style` |
|
||||||
|
|
||||||
|
\*: This variable can only be used as a part of a style string
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# ~/.config/starship.toml
|
||||||
|
|
||||||
|
[git_metrics]
|
||||||
|
added_style = "bold blue"
|
||||||
|
format = '[+$added]($added_style)/[-$deleted]($deleted_style) '
|
||||||
|
```
|
||||||
|
|
||||||
## Git Status
|
## Git Status
|
||||||
|
|
||||||
The `git_status` module shows symbols representing the state of the repo in your
|
The `git_status` module shows symbols representing the state of the repo in your
|
||||||
|
23
src/configs/git_metrics.rs
Normal file
23
src/configs/git_metrics.rs
Normal file
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -23,6 +23,7 @@ pub mod erlang;
|
|||||||
pub mod gcloud;
|
pub mod gcloud;
|
||||||
pub mod git_branch;
|
pub mod git_branch;
|
||||||
pub mod git_commit;
|
pub mod git_commit;
|
||||||
|
pub mod git_metrics;
|
||||||
pub mod git_state;
|
pub mod git_state;
|
||||||
pub mod git_status;
|
pub mod git_status;
|
||||||
pub mod go;
|
pub mod go;
|
||||||
@ -95,6 +96,7 @@ pub struct FullConfig<'a> {
|
|||||||
gcloud: gcloud::GcloudConfig<'a>,
|
gcloud: gcloud::GcloudConfig<'a>,
|
||||||
git_branch: git_branch::GitBranchConfig<'a>,
|
git_branch: git_branch::GitBranchConfig<'a>,
|
||||||
git_commit: git_commit::GitCommitConfig<'a>,
|
git_commit: git_commit::GitCommitConfig<'a>,
|
||||||
|
git_metrics: git_metrics::GitMetricsConfig<'a>,
|
||||||
git_state: git_state::GitStateConfig<'a>,
|
git_state: git_state::GitStateConfig<'a>,
|
||||||
git_status: git_status::GitStatusConfig<'a>,
|
git_status: git_status::GitStatusConfig<'a>,
|
||||||
golang: go::GoConfig<'a>,
|
golang: go::GoConfig<'a>,
|
||||||
@ -164,6 +166,7 @@ impl<'a> Default for FullConfig<'a> {
|
|||||||
gcloud: Default::default(),
|
gcloud: Default::default(),
|
||||||
git_branch: Default::default(),
|
git_branch: Default::default(),
|
||||||
git_commit: Default::default(),
|
git_commit: Default::default(),
|
||||||
|
git_metrics: Default::default(),
|
||||||
git_state: Default::default(),
|
git_state: Default::default(),
|
||||||
git_status: Default::default(),
|
git_status: Default::default(),
|
||||||
golang: Default::default(),
|
golang: Default::default(),
|
||||||
|
@ -26,6 +26,7 @@ pub const PROMPT_ORDER: &[&str] = &[
|
|||||||
"git_branch",
|
"git_branch",
|
||||||
"git_commit",
|
"git_commit",
|
||||||
"git_state",
|
"git_state",
|
||||||
|
"git_metrics",
|
||||||
"git_status",
|
"git_status",
|
||||||
"hg_branch",
|
"hg_branch",
|
||||||
"docker_context",
|
"docker_context",
|
||||||
|
@ -28,6 +28,7 @@ pub const ALL_MODULES: &[&str] = &[
|
|||||||
"gcloud",
|
"gcloud",
|
||||||
"git_branch",
|
"git_branch",
|
||||||
"git_commit",
|
"git_commit",
|
||||||
|
"git_metrics",
|
||||||
"git_state",
|
"git_state",
|
||||||
"git_status",
|
"git_status",
|
||||||
"golang",
|
"golang",
|
||||||
|
275
src/modules/git_metrics.rs
Normal file
275
src/modules/git_metrics.rs
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -337,7 +337,7 @@ mod tests {
|
|||||||
|
|
||||||
// Ensure on the expected branch.
|
// Ensure on the expected branch.
|
||||||
// If build environment has `init.defaultBranch` global set
|
// 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(
|
run_git_cmd(
|
||||||
&["checkout", "-b", "master"],
|
&["checkout", "-b", "master"],
|
||||||
Some(path),
|
Some(path),
|
||||||
|
@ -18,6 +18,7 @@ mod erlang;
|
|||||||
mod gcloud;
|
mod gcloud;
|
||||||
mod git_branch;
|
mod git_branch;
|
||||||
mod git_commit;
|
mod git_commit;
|
||||||
|
mod git_metrics;
|
||||||
mod git_state;
|
mod git_state;
|
||||||
mod git_status;
|
mod git_status;
|
||||||
mod golang;
|
mod golang;
|
||||||
@ -98,6 +99,7 @@ pub fn handle<'a>(module: &str, context: &'a Context) -> Option<Module<'a>> {
|
|||||||
"gcloud" => gcloud::module(context),
|
"gcloud" => gcloud::module(context),
|
||||||
"git_branch" => git_branch::module(context),
|
"git_branch" => git_branch::module(context),
|
||||||
"git_commit" => git_commit::module(context),
|
"git_commit" => git_commit::module(context),
|
||||||
|
"git_metrics" => git_metrics::module(context),
|
||||||
"git_state" => git_state::module(context),
|
"git_state" => git_state::module(context),
|
||||||
"git_status" => git_status::module(context),
|
"git_status" => git_status::module(context),
|
||||||
"golang" => golang::module(context),
|
"golang" => golang::module(context),
|
||||||
@ -180,6 +182,7 @@ pub fn description(module: &str) -> &'static str {
|
|||||||
"gcloud" => "The current GCP client configuration",
|
"gcloud" => "The current GCP client configuration",
|
||||||
"git_branch" => "The active branch of the repo in your current directory",
|
"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_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_state" => "The current git operation, and it's progress",
|
||||||
"git_status" => "Symbol representing the state of the repo",
|
"git_status" => "Symbol representing the state of the repo",
|
||||||
"golang" => "The currently installed version of Golang",
|
"golang" => "The currently installed version of Golang",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user