summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorNick Young <nick@nickwb.net>2019-09-06 02:45:04 +1000
committerMatan Kushner <hello@matchai.me>2019-09-05 12:45:04 -0400
commiteb724279da21feca8438a40249d1b2d47e8ca312 (patch)
treede4642c75b612b0bc817905cf90e711846c3eee5
parent4f17bae3157d8ddd82af7c630f5ba65de0e8cb90 (diff)
feat: Adds Git State module for showing "REBASING 2/3", etc. (#276)
- Adds the git_state module. - Adds git_state to the default prompt order - Updates the documentation to describe the git_state module
-rw-r--r--docs/config/README.md53
-rw-r--r--src/module.rs1
-rw-r--r--src/modules/git_state.rs166
-rw-r--r--src/modules/mod.rs2
-rw-r--r--src/print.rs1
-rw-r--r--src/utils.rs3
-rw-r--r--tests/testsuite/common.rs5
-rw-r--r--tests/testsuite/git_state.rs177
-rw-r--r--tests/testsuite/main.rs1
9 files changed, 396 insertions, 13 deletions
diff --git a/docs/config/README.md b/docs/config/README.md
index 944545246..702d21a51 100644
--- a/docs/config/README.md
+++ b/docs/config/README.md
@@ -72,6 +72,7 @@ default_prompt_order = [
"hostname",
"directory",
"git_branch",
+ "git_state",
"git_status",
"package",
"nodejs",
@@ -79,6 +80,7 @@ default_prompt_order = [
"rust",
"python",
"golang",
+ "nix_shell",
"cmd_duration",
"line_break",
"jobs",
@@ -230,6 +232,37 @@ truncation_length = "4"
truncation_symbol = ""
```
+## Git State
+
+The `git_state` module will show in directories which are part of a git
+repository, and where there is an operation in progress, such as: _REBASING_,
+_BISECTING_, etc. If there is progress information (e.g., REBASING 3/10),
+that information will be shown too.
+
+### Options
+
+| Variable | Default | Description |
+| ------------------ | ------------------ | ---------------------------------------------------------------------------------------------------------------- |
+| `rebase` | `"REBASING"` | The text displayed when a `rebase` is in progress. |
+| `merge` | `"MERGING"` | The text displayed when a `merge` is in progress. |
+| `revert` | `"REVERTING"` | The text displayed when a `revert` is in progress. |
+| `cherry_pick` | `"CHERRY-PICKING"` | The text displayed when a `cherry-pick` is in progress. |
+| `bisect` | `"BISECTING"` | The text displayed when a `bisect` is in progress. |
+| `am` | `"AM"` | The text displayed when an `apply-mailbox` (`git am`) is in progress. |
+| `am_or_rebase` | `"AM/REBASE"` | The text displayed when an ambiguous `apply-mailbox` or `rebase` is in progress. |
+| `progress_divider` | `"/"` | The symbol or text which will separate the current and total progress amounts. (e.g., `" of "`, for `"3 of 10"`) |
+| `disabled` | `false` | Disables the `git_state` module. |
+
+### Example
+
+```toml
+# ~/.config/starship.toml
+
+[git_state]
+progress_divider = " of "
+cherry_pick = "🍒 PICKING"
+```
+
## Git Status
The `git_status` module shows symbols representing the state of the repo in your
@@ -276,12 +309,12 @@ The `hostname` module shows the system hostname.
### Options
-| Variable | Default | Description |
-| ------------ | ------- | ------------------------------------------------------- |
-| `ssh_only` | `true` | Only show hostname when connected to an SSH session. |
-| `prefix` | `""` | Prefix to display immediately before the hostname. |
-| `suffix` | `""` | Suffix to display immediately after the hostname. |
-| `disabled` | `false` | Disables the `hostname` module. |
+| Variable | Default | Description |
+| ---------- | ------- | ---------------------------------------------------- |
+| `ssh_only` | `true` | Only show hostname when connected to an SSH session. |
+| `prefix` | `""` | Prefix to display immediately before the hostname. |
+| `suffix` | `""` | Suffix to display immediately after the hostname. |
+| `disabled` | `false` | Disables the `hostname` module. |
### Example
@@ -378,10 +411,10 @@ The module will be shown if any of the following conditions are met:
### Options
-| Variable | Default | Description |
-| ---------- | ------- | --------------------------- |
-| `symbol` | `"💎 "` | The symbol used before displaying the version of Ruby. |
-| `disabled` | `false` | Disables the `ruby` module. |
+| Variable | Default | Description |
+| ---------- | ------- | ------------------------------------------------------ |
+| `symbol` | `"💎 "` | The symbol used before displaying the version of Ruby. |
+| `disabled` | `false` | Disables the `ruby` module. |
### Example
diff --git a/src/module.rs b/src/module.rs
index 28c309c6d..ab1783e4f 100644
--- a/src/module.rs
+++ b/src/module.rs
@@ -12,6 +12,7 @@ pub const ALL_MODULES: &[&str] = &[
"cmd_duration",
"directory",
"git_branch",
+ "git_state",
"git_status",
"golang",
"hostname",
diff --git a/src/modules/git_state.rs b/src/modules/git_state.rs
new file mode 100644
index 000000000..55f3d4c11
--- /dev/null
+++ b/src/modules/git_state.rs
@@ -0,0 +1,166 @@
+use ansi_term::Color;
+use git2::{Repository, RepositoryState};
+use std::path::Path;
+
+use super::{Context, Module};
+
+/// Creates a module with the state of the git repository at the current directory
+///
+/// During a git operation it will show: REBASING, BISECTING, MERGING, etc.
+/// If the progress information is available (e.g. rebasing 3/10), it will show that too.
+pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
+ let mut module = context.new_module("git_state")?;
+
+ let repo_root = context.repo_root.as_ref()?;
+ let mut repository = Repository::open(repo_root).ok()?;
+ let state_description = get_state_description(&mut repository);
+
+ if let StateDescription::Clean = state_description {
+ return None;
+ }
+
+ module.get_prefix().set_value("(");
+ module.get_suffix().set_value(") ");
+ module.set_style(Color::Yellow.bold());
+
+ let label = match state_description {
+ StateDescription::Label(label) => label,
+ StateDescription::LabelAndProgress(label, _) => label,
+ // Should only be possible if you've added a new variant to StateDescription
+ _ => panic!("Expected to have a label at this point in the control flow."),
+ };
+
+ module.new_segment(label.segment_name, label.message_default);
+
+ if let StateDescription::LabelAndProgress(_, progress) = state_description {
+ module.new_segment("progress_current", &format!(" {}", progress.current));
+ module.new_segment("progress_divider", "/");
+ module.new_segment("progress_total", &format!("{}", progress.total));
+ }
+
+ Some(module)
+}
+
+static MERGE_LABEL: StateLabel = StateLabel {
+ segment_name: "merge",
+ message_default: "MERGING",
+};
+
+static REVERT_LABEL: StateLabel = StateLabel {
+ segment_name: "revert",
+ message_default: "REVERTING",
+};
+
+static CHERRY_LABEL: StateLabel = StateLabel {
+ segment_name: "cherry_pick",
+ message_default: "CHERRY-PICKING",
+};
+
+static BISECT_LABEL: StateLabel = StateLabel {
+ segment_name: "bisect",
+ message_default: "BISECTING",
+};
+
+static AM_LABEL: StateLabel = StateLabel {
+ segment_name: "am",
+ message_default: "AM",
+};
+
+static REBASE_LABEL: StateLabel = StateLabel {
+ segment_name: "rebase",
+ message_default: "REBASING",
+};
+
+static AM_OR_REBASE_LABEL: StateLabel = StateLabel {
+ segment_name: "am_or_rebase",
+ message_default: "AM/REBASE",
+};
+
+fn get_state_description(repository: &mut Repository) -> StateDescription {
+ match repository.state() {
+ RepositoryState::Clean => StateDescription::Clean,
+ RepositoryState::Merge => StateDescription::Label(&MERGE_LABEL),
+ RepositoryState::Revert => StateDescription::Label(&REVERT_LABEL),
+ RepositoryState::RevertSequence => StateDescription::Label(&REVERT_LABEL),
+ RepositoryState::CherryPick => StateDescription::Label(&CHERRY_LABEL),
+ RepositoryState::CherryPickSequence => StateDescription::Label(&CHERRY_LABEL),
+ RepositoryState::Bisect => StateDescription::Label(&BISECT_LABEL),
+ RepositoryState::ApplyMailbox => StateDescription::Label(&AM_LABEL),
+ RepositoryState::ApplyMailboxOrRebase => StateDescription::Label(&AM_OR_REBASE_LABEL),
+ RepositoryState::Rebase => describe_rebase(repository),
+ RepositoryState::RebaseInteractive => describe_rebase(repository),
+ RepositoryState::RebaseMerge => describe_rebase(repository),
+ }
+}
+
+fn describe_rebase(repository: &mut Repository) -> StateDescription {
+ /*
+ * Sadly, libgit2 seems to have some issues with reading the state of
+ * interactive rebases. So, instead, we'll poke a few of the .git files
+ * ourselves. This might be worth re-visiting this in the future...
+ *
+ * The following is based heavily on: https://github.com/magicmonty/bash-git-prompt
+ */
+
+ let just_label = StateDescription::Label(&REBASE_LABEL);
+
+ let dot_git = repository
+ .workdir()
+ .and_then(|d| Some(d.join(Path::new(".git"))));
+
+ let dot_git = match dot_git {
+ None => {
+ // We didn't find the .git directory.
+ // Something very odd is going on. We'll just back away slowly.
+ return just_label;
+ }
+ Some(path) => path,
+ };
+
+ let has_path = |relative_path: &str| {
+ let path = dot_git.join(Path::new(relative_path));
+ path.exists()
+ };
+
+ let file_to_usize = |relative_path: &str| {
+ let path = dot_git.join(Path::new(relative_path));
+ let contents = crate::utils::read_file(path).ok()?;
+ let quantity = contents.trim().parse::<usize>().ok()?;
+ Some(quantity)
+ };
+
+ let paths_to_progress = |current_path: &str, total_path: &str| {
+ let current = file_to_usize(current_path)?;
+ let total = file_to_usize(total_path)?;
+ Some(StateProgress { current, total })
+ };
+
+ let progress = if has_path("rebase-merge") {
+ paths_to_progress("rebase-merge/msgnum", "rebase-merge/end")
+ } else if has_path("rebase-apply") {
+ paths_to_progress("rebase-apply/next", "rebase-apply/last")
+ } else {
+ None
+ };
+
+ match progress {
+ None => just_label,
+ Some(progress) => StateDescription::LabelAndProgress(&REBASE_LABEL, progress),
+ }
+}
+
+enum StateDescription {
+ Clean,
+ Label(&'static StateLabel),
+ LabelAndProgress(&'static StateLabel, StateProgress),
+}
+
+struct StateLabel {
+ segment_name: &'static str,
+ message_default: &'static str,
+}
+
+struct StateProgress {
+ current: usize,
+ total: usize,
+}
diff --git a/src/modules/mod.rs b/src/modules/mod.rs
index 23d82601f..83a7d1c40 100644
--- a/src/modules/mod.rs
+++ b/src/modules/mod.rs
@@ -3,6 +3,7 @@ mod character;
mod cmd_duration;
mod directory;
mod git_branch;
+mod git_state;
mod git_status;
mod golang;
mod hostname;
@@ -34,6 +35,7 @@ pub fn handle<'a>(module: &str, context: &'a Context) -> Option<Module<'a>> {
"line_break" => line_break::module(context),
"package" => package::module(context),
"git_branch" => git_branch::module(context),
+ "git_state" => git_state::module(context),
"git_status" => git_status::module(context),
"username" => username::module(context),
#[cfg(feature = "battery")]
diff --git a/src/print.rs b/src/print.rs
index 74997b26d..1ea1648ff 100644
--- a/src/print.rs
+++ b/src/print.rs
@@ -16,6 +16,7 @@ const DEFAULT_PROMPT_ORDER: &[&str] = &[
"hostname",
"directory",
"git_branch",
+ "git_state",
"git_status",
"package",
"nodejs",
diff --git a/src/utils.rs b/src/utils.rs
index 4a0337ae6..c873f45aa 100644
--- a/src/utils.rs
+++ b/src/utils.rs
@@ -1,8 +1,9 @@
use std::fs::File;
use std::io::{Read, Result};
+use std::path::Path;
/// Return the string contents of a file
-pub fn read_file(file_name: &str) -> Result<String> {
+pub fn read_file<P: AsRef<Path>>(file_name: P) -> Result<String> {
let mut file = File::open(file_name)?;
let mut data = String::new();
diff --git a/tests/testsuite/common.rs b/tests/testsuite/common.rs
index 0f611a209..6698a1d94 100644
--- a/tests/testsuite/common.rs
+++ b/tests/testsuite/common.rs
@@ -2,7 +2,7 @@ use lazy_static::lazy_static;
use std::io::prelude::*;
use std::path::{Path, PathBuf};
use std::process::Command;
-use std::{env, io, process};
+use std::{env, fs, io, process};
lazy_static! {
static ref MANIFEST_DIR: &'static Path = Path::new(env!("CARGO_MANIFEST_DIR"));
@@ -25,7 +25,8 @@ pub fn render_prompt() -> process::Command {
/// Render a specific starship module by name
pub fn render_module(module_name: &str) -> process::Command {
- let mut command = process::Command::new("./target/debug/starship");
+ let binary = fs::canonicalize("./target/debug/starship").unwrap();
+ let mut command = process::Command::new(binary);
command
.arg("module")
diff --git a/tests/testsuite/git_state.rs b/tests/testsuite/git_state.rs
new file mode 100644
index 000000000..2de7d5029
--- /dev/null
+++ b/tests/testsuite/git_state.rs
@@ -0,0 +1,177 @@
+use super::common;
+use std::ffi::OsStr;
+use std::fs::OpenOptions;
+use std::io::{self, Error, ErrorKind, Write};
+use std::process::{Command, Stdio};
+
+#[test]
+#[ignore]
+fn shows_rebasing() -> io::Result<()> {
+ let repo_dir = create_repo_with_conflict()?;
+ let path = path_str(&repo_dir)?;
+
+ run_git_cmd(&["rebase", "other-branch"], Some(path), false)?;
+
+ let output = common::render_module("git_state")
+ .current_dir(path)
+ .output()?;
+ let text = String::from_utf8(output.stdout).unwrap();
+ assert!(text.contains("REBASING 1/1"));
+
+ Ok(())
+}
+
+#[test]
+#[ignore]
+fn shows_merging() -> io::Result<()> {
+ let repo_dir = create_repo_with_conflict()?;
+ let path = path_str(&repo_dir)?;
+
+ run_git_cmd(&["merge", "other-branch"], Some(path), false)?;
+
+ let output = common::render_module("git_state")
+ .current_dir(path)
+ .output()?;
+ let text = String::from_utf8(output.stdout).unwrap();
+ assert!(text.contains("MERGING"));
+
+ Ok(())
+}
+
+#[test]
+#[ignore]
+fn shows_cherry_picking() -> io::Result<()> {
+ let repo_dir = create_repo_with_conflict()?;
+ let path = path_str(&repo_dir)?;
+
+ run_git_cmd(&["cherry-pick", "other-branch"], Some(path), false)?;
+
+ let output = common::render_module("git_state")
+ .current_dir(path)
+ .output()?;
+ let text = String::from_utf8(output.stdout).unwrap();
+ assert!(text.contains("CHERRY-PICKING"));
+
+ Ok(())
+}
+
+#[test]
+#[ignore]
+fn shows_bisecting() -> io::Result<()> {
+ let repo_dir = create_repo_with_conflict()?;
+ let path = path_str(&repo_dir)?;
+
+ run_git_cmd(&["bisect", "start"], Some(path), false)?;
+
+ let output = common::render_module("git_state")
+ .current_dir(path)
+ .output()?;
+ let text = String::from_utf8(output.stdout).unwrap();
+ assert!(text.contains("BISECTING"));
+
+ Ok(())
+}
+
+#[test]
+#[ignore]
+fn shows_reverting() -> io::Result<()> {
+ let repo_dir = create_repo_with_conflict()?;
+ let path = path_str(&repo_dir)?;
+
+ run_git_cmd(&["revert", "--no-commit", "HEAD~1"], Some(path), false)?;
+
+ let output = common::render_module("git_state")
+ .current_dir(path)
+ .output()?;
+ let text = String::from_utf8(output.stdout).unwrap();
+ assert!(text.contains("REVERTING"));
+
+ Ok(())
+}
+
+fn run_git_cmd<A, S>(args: A, dir: Option<&str>, expect_ok: 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 expect_ok && !status.success() {
+ Err(Error::from(ErrorKind::Other))
+ } else {
+ Ok(())
+ }
+}
+
+fn create_repo_with_conflict() -> io::Result<tempfile::TempDir> {
+ let repo_dir = common::new_tempdir()?;
+ let path = path_str(&repo_dir)?;
+ let conflicted_file = repo_dir.path().join("the_file");
+
+ let write_file = |text: &str| {
+ let mut file = OpenOptions::new()
+ .write(true)
+ .create(true)
+ .truncate(true)
+ .open(&conflicted_file)?;
+ write!(file, "{}", text)
+ };
+
+ // Initialise a new git repo
+ run_git_cmd(&["init", "--quiet", path], 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,
+ )?;
+
+ // Write a file on master and commit it
+ write_file("Version A")?;
+ run_git_cmd(&["add", "the_file"], Some(path), true)?;
+ run_git_cmd(&["commit", "--message", "Commit A"], Some(path), true)?;
+
+ // Switch to another branch, and commit a change to the file
+ run_git_cmd(&["checkout", "-b", "other-branch"], Some(path), true)?;
+ write_file("Version B")?;
+ run_git_cmd(
+ &["commit", "--all", "--message", "Commit B"],
+ Some(path),
+ true,
+ )?;
+
+ // Switch back to master, and commit a third change to the file
+ run_git_cmd(&["checkout", "master"], Some(path), true)?;
+ write_file("Version C")?;
+ run_git_cmd(
+ &["commit", "--all", "--message", "Commit C"],
+ Some(path),
+ true,
+ )?;
+
+ Ok(repo_dir)
+}
+
+fn path_str(repo_dir: &tempfile::TempDir) -> io::Result<&str> {
+ repo_dir
+ .path()
+ .to_str()
+ .ok_or_else(|| Error::from(ErrorKind::Other))
+}
diff --git a/tests/testsuite/main.rs b/tests/testsuite/main.rs
index 82da9cccf..bf64f3099 100644
--- a/tests/testsuite/main.rs
+++ b/tests/testsuite/main.rs
@@ -4,6 +4,7 @@ mod common;
mod configuration;
mod directory;
mod git_branch;
+mod git_state;
mod git_status;
mod golang;
mod hostname;