feat(module): add jj module to display repository status

This commit is contained in:
Vladimir Petrzhikovskii 2024-02-06 00:18:49 +01:00
parent 3949697c0d
commit d9acbbcb93
No known key found for this signature in database
GPG Key ID: 71CA26A690EB2EE6
8 changed files with 480 additions and 0 deletions

View File

@ -868,6 +868,26 @@
}
]
},
"jj_status": {
"default": {
"branch_style": "purple",
"change_id_prefix_style": "purple",
"change_id_suffix_style": "bright-black",
"commit_id_prefix_style": "blue",
"commit_id_suffix_style": "bright-black",
"disabled": false,
"divergent_symbol": " 💥",
"format": "\\[$symbol[$change_id_prefix]($change_id_prefix_style)[$change_id_suffix]($change_id_suffix_style) [$commit_id_prefix]($commit_id_prefix_style)[$commit_id_suffix]($commit_id_suffix_style) on [$branch]($branch_style)$no_description_symbol$divergent_symbol\\]",
"no_description_symbol": " 📝",
"symbol": "🍐 ",
"truncation_length": 8
},
"allOf": [
{
"$ref": "#/definitions/JJConfig"
}
]
},
"jobs": {
"default": {
"disabled": false,
@ -3939,6 +3959,58 @@
},
"additionalProperties": false
},
"JJConfig": {
"type": "object",
"properties": {
"symbol": {
"default": "🍐 ",
"type": "string"
},
"format": {
"default": "\\[$symbol[$change_id_prefix]($change_id_prefix_style)[$change_id_suffix]($change_id_suffix_style) [$commit_id_prefix]($commit_id_prefix_style)[$commit_id_suffix]($commit_id_suffix_style) on [$branch]($branch_style)$no_description_symbol$divergent_symbol\\]",
"type": "string"
},
"disabled": {
"default": false,
"type": "boolean"
},
"change_id_prefix_style": {
"default": "purple",
"type": "string"
},
"change_id_suffix_style": {
"default": "bright-black",
"type": "string"
},
"commit_id_prefix_style": {
"default": "blue",
"type": "string"
},
"commit_id_suffix_style": {
"default": "bright-black",
"type": "string"
},
"truncation_length": {
"default": 8,
"type": "integer",
"format": "uint8",
"minimum": 0.0
},
"no_description_symbol": {
"default": " 📝",
"type": "string"
},
"divergent_symbol": {
"default": " 💥",
"type": "string"
},
"branch_style": {
"default": "purple",
"type": "string"
}
},
"additionalProperties": false
},
"JobsConfig": {
"type": "object",
"properties": {

View File

@ -275,6 +275,7 @@ $directory\
$vcsh\
$fossil_branch\
$fossil_metrics\
$jj_status\
$git_branch\
$git_commit\
$git_state\
@ -2445,6 +2446,78 @@ number_threshold = 4
symbol_threshold = 0
```
## JJ
The `jj` module reflects the current status of your Jujutsu repository. It shows
different symbols and formats depending on the state of your work in progress,
including changes, commits, branch, and more.
The `jj` module is only shown when you're in a directory that is part of a
Jujutsu repository.
The default functionality includes displaying:
- current change id
- current commit id
::: warning
This module requires the Jujutsu command line tool to be installed and
accessible in your system's PATH for it to function correctly.
:::
### Options
| Option | Default | Description |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `symbol` | `'🍐 '` | The string used before displaying the repository status. |
| `no_description_symbol` | `' 📝'` | Is displayed when change description is empty. |
| `divergent_symbol` | `' 💥'` | Is displayed when commit is in [divergent](https://github.com/martinvonz/jj/blob/main/docs/glossary.md#divergent-change) state. |
| `format` | `'\[$symbol[$change_id_prefix]($change_id_prefix_style)[$change_id_suffix]($change_id_suffix_style) [$commit_id_prefix]($commit_id_prefix_style)[$commit_id_suffix]($commit_id_suffix_style) on [$branch]($branch_style)$no_description_symbol$divergent_symbol\]'` | The format for rendering the module. `symbol`, `style`, and other variables can be included. |
| `disabled` | `false` | Disables the `jj` module. |
| `change_id_prefix_style` | `'purple'` | The style for uniq part of change id. |
| `change_id_suffix_style` | `'bright-black'` | The style for the rest of change id. |
| `commit_id_prefix_style` | `'blue'` | The style for unique part of commit hash. |
| `commit_id_suffix_style` | `'bright-black'` | The style for the rest of commit hash. |
| `truncation_length` | `8` | Truncates the commit and change id to `truncation_length` characters. |
| `branch_style` | `'purple'` | The style for the branch name. |
### Variables
```
[🍐 rnyynlop 811ca1a4 on push-rnyynlopprno*]
```
| Variable | Example | Description |
| ------------------ | --------- | ----------------------------------------------------------------- |
| `symbol` | `🍐` | Mirrors the value of option `symbol`. |
| `change_id_prefix` | `r` | Unique part of the change id, styled by `change_id_prefix_style`. |
| `change_id_suffix` | `nyynlop` | The rest of the change id, styled by `change_id_suffix_style`. |
| `commit_id_prefix` | `811` | Unique part of the commit id, styled by `commit_id_prefix_style`. |
| `commit_id_suffix` | `ca1a4` | The rest of the commit id, styled by `commit_id_suffix_style`. |
| `branch` | `master` | The current branch name. |
### Example
```toml
# ~/.config/starship.toml
[jj]
symbol = "🍐 "
style = "purple"
format = "\\[$symbol[$change_id_prefix]($change_id_prefix_style)[$change_id_suffix]($change_id_suffix_style) [$commit_id_prefix]($commit_id_prefix_style)[$commit_id_suffix]($commit_id_suffix_style) on [$branch]($branch_style)$no_description_symbol$divergent_symbol\\]"
disabled = false
change_id_prefix_style = "purple"
change_id_suffix_style = "bright-black"
commit_id_prefix_style = "blue"
commit_id_suffix_style = "bright-black"
truncation_length = 8
no_description_symbol = "📝"
divergent_symbol = "💥"
branch_style = "purple"
```
## Julia
The `julia` module shows the currently installed version of [Julia](https://julialang.org/).

40
src/configs/jj.rs Normal file
View File

@ -0,0 +1,40 @@
use serde::{Deserialize, Serialize};
#[derive(Clone, Deserialize, Serialize, Debug)]
#[cfg_attr(
feature = "config-schema",
derive(schemars::JsonSchema),
schemars(deny_unknown_fields)
)]
#[serde(default)]
pub struct JJConfig<'a> {
pub symbol: &'a str,
pub format: &'a str,
pub disabled: bool,
pub change_id_prefix_style: &'a str,
pub change_id_suffix_style: &'a str,
pub commit_id_prefix_style: &'a str,
pub commit_id_suffix_style: &'a str,
pub truncation_length: u8,
pub no_description_symbol: &'a str,
pub divergent_symbol: &'a str,
pub branch_style: &'a str,
}
impl<'a> Default for JJConfig<'a> {
fn default() -> Self {
JJConfig {
symbol: "🍐 ",
format: "\\[$symbol[$change_id_prefix]($change_id_prefix_style)[$change_id_suffix]($change_id_suffix_style) [$commit_id_prefix]($commit_id_prefix_style)[$commit_id_suffix]($commit_id_suffix_style) on [$branch]($branch_style)$no_description_symbol$divergent_symbol\\]",
disabled: false,
change_id_prefix_style: "purple", // magenta
change_id_suffix_style: "bright-black",
commit_id_prefix_style: "blue",
commit_id_suffix_style: "bright-black",
truncation_length: 8,
no_description_symbol: " 📝",
divergent_symbol: " 💥",
branch_style: "purple",
}
}
}

View File

@ -45,6 +45,7 @@ pub mod helm;
pub mod hg_branch;
pub mod hostname;
pub mod java;
pub mod jj;
pub mod jobs;
pub mod julia;
pub mod kotlin;
@ -196,6 +197,8 @@ pub struct FullConfig<'a> {
#[serde(borrow)]
java: java::JavaConfig<'a>,
#[serde(borrow)]
jj_status: jj::JJConfig<'a>,
#[serde(borrow)]
jobs: jobs::JobsConfig<'a>,
#[serde(borrow)]
julia: julia::JuliaConfig<'a>,

View File

@ -41,6 +41,7 @@ pub const PROMPT_ORDER: &[&str] = &[
"vcsh",
"fossil_branch",
"fossil_metrics",
"jj_status",
"git_branch",
"git_commit",
"git_state",

View File

@ -52,6 +52,7 @@ pub const ALL_MODULES: &[&str] = &[
"hg_branch",
"hostname",
"java",
"jj_status",
"jobs",
"julia",
"kotlin",

287
src/modules/jj.rs Normal file
View File

@ -0,0 +1,287 @@
use super::{Context, Module, ModuleConfig};
use crate::configs::jj::JJConfig;
use crate::formatter::string_formatter::StringFormatterError;
use crate::formatter::StringFormatter;
use std::borrow::Cow;
/// Creates a module with the jj status in the current directory
///
/// Will display the current branch, commit and change id of the jj repository in the current directory
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
let mut module = context.new_module("jj_status");
let config = JJConfig::try_load(module.config);
// check if the current directory is a jj repo
let repo_root = context
.exec_cmd("jj", &["root", "--ignore-working-copy"])?
.stdout;
let template = jj_template(config.truncation_length);
let output = context.exec_cmd(
"jj",
&[
"log",
"-r",
"@",
"-T",
template.as_str(),
"--no-graph",
"--ignore-working-copy",
"--no-pager",
"--color",
"never",
"-R",
repo_root.trim(),
],
)?;
let parsed = JJStatus::new(&output.stdout)?;
let formatter = match StringFormatter::new(config.format) {
Ok(formatter) => formatter,
Err(e) => {
log::error!("Error in parsing format string for jj module {}", e);
return None;
}
};
let formatted = formatter
.map_meta(|s, _| match s {
"symbol" => Some(config.symbol),
"divergent_symbol" => parsed
.divergent
.eq("true")
.then_some(config.divergent_symbol),
"no_description_symbol" => parsed
.description
.is_empty()
.then_some(config.no_description_symbol),
_ => None,
})
.map_style(|s| {
Some(Ok(match s {
"change_id_prefix_style" => config.change_id_prefix_style,
"change_id_suffix_style" => config.change_id_suffix_style,
"commit_id_prefix_style" => config.commit_id_prefix_style,
"commit_id_suffix_style" => config.commit_id_suffix_style,
"branch_style" => config.branch_style,
_ => return None,
}))
})
.map(|v| parsed.format(v))
.parse(None, Some(context));
let formatted = match formatted {
Ok(formatted) => formatted,
Err(e) => {
log::error!("Error in parsing format string for jj module {}", e);
return None;
}
};
module.set_segments(formatted);
Some(module)
}
fn jj_template(len: u8) -> String {
format!(
r##"change_id.shortest({len}).prefix() ++ "#," ++ change_id.shortest({len}).rest() ++ "#," ++ commit_id.shortest({len}).prefix() ++ "#," ++ commit_id.shortest({len}).rest() ++ "#," ++ divergent ++ "#," ++ description ++ "#," ++ branches.join(",")"##,
)
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct JJStatus<'s> {
change_id_prefix: &'s str,
change_id_suffix: &'s str,
commit_id_prefix: &'s str,
commit_id_suffix: &'s str,
divergent: &'s str,
description: &'s str,
branches: &'s str,
}
impl<'s> JJStatus<'s> {
fn new(input: &'s str) -> Option<JJStatus<'s>> {
let expected_parts = jj_template(8).matches("#,").count() + 1;
let parsed: Vec<&str> = input.split("#,").collect();
if parsed.len() != expected_parts {
log::error!(
"expected at least {expected_parts} parts, got {}",
parsed.len()
);
return None;
}
let [change_id_prefix, change_id_suffix, commit_id_prefix, commit_id_suffix, divergent, description, branches] =
parsed[..]
else {
return None;
};
Some(Self {
change_id_prefix,
change_id_suffix,
commit_id_prefix,
commit_id_suffix,
divergent,
description,
branches,
})
}
fn format(&self, var: &str) -> Option<Result<Cow<str>, StringFormatterError>> {
let Self {
change_id_prefix,
change_id_suffix,
commit_id_prefix,
commit_id_suffix,
branches,
description,
..
} = self;
let branch = if branches.is_empty() {
"<no branch>"
} else {
branches
};
match var {
"change_id_prefix" => Some(Ok(Cow::Borrowed(change_id_prefix))),
"change_id_suffix" => Some(Ok(Cow::Borrowed(change_id_suffix))),
"commit_id_prefix" => Some(Ok(Cow::Borrowed(commit_id_prefix))),
"commit_id_suffix" => Some(Ok(Cow::Borrowed(commit_id_suffix))),
"branch" => Some(Ok(Cow::Borrowed(branch))),
"description" => Some(Ok(Cow::Borrowed(description))),
_ => None,
}
}
}
#[cfg(test)]
mod test {
static PATH_TO_REPO: &'static str = "/path/to/repo";
use crate::modules::jj::JJStatus;
use crate::test::ModuleRenderer;
use crate::utils::CommandOutput;
fn jj_log(output: &str) -> (String, Option<CommandOutput>) {
(
format!(
"jj log -r @ -T {} --no-graph --ignore-working-copy --no-pager --color never -R {PATH_TO_REPO}",
crate::modules::jj::jj_template(8)
),
Some(CommandOutput {
stdout: output.to_string(),
stderr: "".to_string(),
}),
)
}
fn is_jj_repo() -> &'static str {
"jj root --ignore-working-copy"
}
#[test]
fn not_a_jj_repo() {
let actual = ModuleRenderer::new("jj_status")
.cmd(is_jj_repo(), None)
.collect();
let expected = None;
assert_eq!(expected, actual);
}
#[test]
fn test_actual_jj_repo() {
let (mocked_jj_log_command, mocked_jj_log_output) =
jj_log("kxx#,zlovo#,be9#,d1825#,false#,#,br1,br2");
let repo_root = Some(CommandOutput {
stdout: PATH_TO_REPO.to_string(),
stderr: "".to_string(),
});
let actual = ModuleRenderer::new("jj_status")
.cmd(is_jj_repo(), repo_root.clone())
.cmd(mocked_jj_log_command.as_str(), mocked_jj_log_output)
.collect()
.unwrap();
assert_eq!("[🍐 \u{1b}[35mkxx\u{1b}[90mzlovo\u{1b}[0m \u{1b}[34mbe9\u{1b}[90md1825\u{1b}[0m on \u{1b}[35mbr1,br2\u{1b}[0m 📝]", actual);
}
#[test]
fn test_actual_jj_repo_with_divergence() {
// Mock input to represent a JJ repository status with divergence
let (mocked_jj_log_command, mocked_jj_log_output) =
jj_log("kxx#,zlovo#,be9#,d1825#,true#,#,br1,br2");
let repo_root = Some(CommandOutput {
stdout: PATH_TO_REPO.to_string(),
stderr: "".to_string(),
});
let actual = ModuleRenderer::new("jj_status")
.cmd(is_jj_repo(), repo_root)
.cmd(mocked_jj_log_command.as_str(), mocked_jj_log_output)
.collect()
.unwrap();
assert_eq!("[🍐 \u{1b}[35mkxx\u{1b}[90mzlovo\u{1b}[0m \u{1b}[34mbe9\u{1b}[90md1825\u{1b}[0m on \u{1b}[35mbr1,br2\u{1b}[0m 📝 💥]", actual);
}
#[test]
fn test_parser() {
let input = "kxx#,zlovo#,be9#,d1825#,false#,#,br1,br2";
let expected = JJStatus {
change_id_prefix: "kxx",
change_id_suffix: "zlovo",
commit_id_prefix: "be9",
commit_id_suffix: "d1825",
divergent: "false",
description: "",
branches: "br1,br2",
};
let actual = JJStatus::new(input).unwrap();
assert_eq!(expected, actual);
let input = "kxx#,zlovo#,be9#,d1825#,false#,#,";
let expected = JJStatus {
change_id_prefix: "kxx",
change_id_suffix: "zlovo",
commit_id_prefix: "be9",
commit_id_suffix: "d1825",
divergent: "false",
branches: "",
description: "",
};
let actual = JJStatus::new(input).unwrap();
assert_eq!(expected, actual);
}
#[test]
fn test_minimal_input() {
let jj_status = JJStatus::new("a#,b#,c#,d#,true#,e#,f").unwrap();
assert_eq!(jj_status.change_id_prefix, "a");
assert_eq!(jj_status.change_id_suffix, "b");
assert_eq!(jj_status.commit_id_prefix, "c");
assert_eq!(jj_status.commit_id_suffix, "d");
assert_eq!(jj_status.divergent, "true");
assert_eq!(jj_status.description, "e");
assert_eq!(jj_status.branches, "f");
}
#[test]
fn test_no_branches() {
let jj_status = JJStatus::new("a#,b#,c#,d#,false#,e#,").unwrap();
let formatted_branch = jj_status.format("branch").unwrap().unwrap();
assert_eq!(formatted_branch, "<no branch>");
}
#[test]
fn test_incorrect_input_format() {
let jj_status = JJStatus::new("a,b,c");
assert!(jj_status.is_none());
}
#[test]
fn test_multiple_branches() {
let jj_status = JJStatus::new("1#,2#,3#,4#,false#,desc#,branch1,branch2").unwrap();
let formatted_branch = jj_status.format("branch").unwrap().unwrap();
assert_eq!(formatted_branch, "branch1,branch2");
}
#[test]
fn test_divergent_true() {
let jj_status = JJStatus::new("1#,2#,3#,4#,true#,desc#,branch").unwrap();
assert_eq!(jj_status.divergent, "true");
}
}

View File

@ -90,6 +90,7 @@ mod zig;
#[cfg(feature = "battery")]
mod battery;
mod jj;
mod typst;
#[cfg(feature = "battery")]
@ -149,6 +150,7 @@ pub fn handle<'a>(module: &str, context: &'a Context) -> Option<Module<'a>> {
"hg_branch" => hg_branch::module(context),
"hostname" => hostname::module(context),
"java" => java::module(context),
"jj_status" => jj::module(context),
"jobs" => jobs::module(context),
"julia" => julia::module(context),
"kotlin" => kotlin::module(context),
@ -267,6 +269,7 @@ pub fn description(module: &str) -> &'static str {
"hg_branch" => "The active branch and topic of the repo in your current directory",
"hostname" => "The system hostname",
"java" => "The currently installed version of Java",
"jj_status" => "The status of the current jj repo",
"jobs" => "The current number of jobs running",
"julia" => "The currently installed version of Julia",
"kotlin" => "The currently installed version of Kotlin",