From 8d2256ab1d0ba288fb6ba9b9248bc2210ca01059 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20=C3=89vrard?= Date: Sat, 31 Dec 2022 15:53:55 +0100 Subject: [PATCH] feat(hg_branch): Add support for mercurial topics and find hg root dir (#4771) * feat(hg_branch): Add support for mercurial topics and find hg root dir * Fix clippy errors * Use crate::utils::read_file * Update config-schema.json * Extend PathExt to retrieve device ID of Path * Break hg root search when switching to another device * Fix clippy and formatting errors * Update docs/config/README.md Co-authored-by: David Knaack * Update src/modules/utils/path.rs Co-authored-by: David Knaack * Update src/configs/hg_branch.rs Co-authored-by: David Knaack * Update hg_branch description * Revert to lazy loading, use truncate_text from utils and use fake topic * Format code and fix clippy error * Revert to previous test string as topic is optional in the config * Fix doc formatting * Stub device_id for windows * Update config-schema.json * Update src/modules/hg_branch.rs Co-authored-by: David Knaack * Do not use unwrap in device_id * Fix formatter error * Use dev under non linux unixes Co-authored-by: David Knaack --- .github/config-schema.json | 4 +- docs/config/README.md | 29 ++++++------- src/configs/hg_branch.rs | 2 +- src/modules/hg_branch.rs | 86 +++++++++++++++++++++++++------------- src/modules/mod.rs | 2 +- src/modules/utils/path.rs | 25 +++++++++++ 6 files changed, 100 insertions(+), 48 deletions(-) diff --git a/.github/config-schema.json b/.github/config-schema.json index 7924c0ce..e4111cbc 100644 --- a/.github/config-schema.json +++ b/.github/config-schema.json @@ -750,7 +750,7 @@ "hg_branch": { "default": { "disabled": true, - "format": "on [$symbol$branch]($style) ", + "format": "on [$symbol$branch(:$topic)]($style) ", "style": "bold purple", "symbol": " ", "truncation_length": 9223372036854775807, @@ -3537,7 +3537,7 @@ "type": "string" }, "format": { - "default": "on [$symbol$branch]($style) ", + "default": "on [$symbol$branch(:$topic)]($style) ", "type": "string" }, "truncation_length": { diff --git a/docs/config/README.md b/docs/config/README.md index f8b27bfd..1fc4968f 100644 --- a/docs/config/README.md +++ b/docs/config/README.md @@ -2643,26 +2643,27 @@ style = 'bold dimmed green' ## Mercurial Branch -The `hg_branch` module shows the active branch of the repo in your current directory. +The `hg_branch` module shows the active branch and topic of the repo in your current directory. ### Options -| Option | Default | Description | -| ------------------- | -------------------------------- | -------------------------------------------------------------------------------------------- | -| `symbol` | `' '` | The symbol used before the hg bookmark or branch name of the repo in your current directory. | -| `style` | `'bold purple'` | The style for the module. | -| `format` | `'on [$symbol$branch]($style) '` | The format for the module. | -| `truncation_length` | `2^63 - 1` | Truncates the hg branch name to `N` graphemes | -| `truncation_symbol` | `'…'` | The symbol used to indicate a branch name was truncated. | -| `disabled` | `true` | Disables the `hg_branch` module. | +| Option | Default | Description | +| ------------------- | ----------------------------------------- | -------------------------------------------------------------------------------------------- | +| `symbol` | `' '` | The symbol used before the hg bookmark or branch name of the repo in your current directory. | +| `style` | `'bold purple'` | The style for the module. | +| `format` | `'on [$symbol$branch(:$topic)]($style) '` | The format for the module. | +| `truncation_length` | `2^63 - 1` | Truncates the hg branch / topic name to `N` graphemes | +| `truncation_symbol` | `'…'` | The symbol used to indicate a branch name was truncated. | +| `disabled` | `true` | Disables the `hg_branch` module. | ### Variables -| Variable | Example | Description | -| -------- | -------- | ------------------------------------ | -| branch | `master` | The active mercurial branch | -| symbol | | Mirrors the value of option `symbol` | -| style\* | | Mirrors the value of option `style` | +| Variable | Example | Description | +| -------- | --------- | ------------------------------------ | +| branch | `master` | The active mercurial branch | +| topic | `feature` | The active mercurial topic | +| symbol | | Mirrors the value of option `symbol` | +| style\* | | Mirrors the value of option `style` | *: This variable can only be used as a part of a style string diff --git a/src/configs/hg_branch.rs b/src/configs/hg_branch.rs index 9e45fe5f..fff150c9 100644 --- a/src/configs/hg_branch.rs +++ b/src/configs/hg_branch.rs @@ -21,7 +21,7 @@ impl<'a> Default for HgBranchConfig<'a> { HgBranchConfig { symbol: " ", style: "bold purple", - format: "on [$symbol$branch]($style) ", + format: "on [$symbol$branch(:$topic)]($style) ", truncation_length: std::i64::MAX, truncation_symbol: "…", disabled: true, diff --git a/src/modules/hg_branch.rs b/src/modules/hg_branch.rs index 3aefd4b6..cb2adecd 100644 --- a/src/modules/hg_branch.rs +++ b/src/modules/hg_branch.rs @@ -1,20 +1,18 @@ -use unicode_segmentation::UnicodeSegmentation; +use std::io::{Error, ErrorKind}; +use std::path::Path; +use super::utils::truncate::truncate_text; use super::{Context, Module, ModuleConfig}; use crate::configs::hg_branch::HgBranchConfig; use crate::formatter::StringFormatter; +use crate::modules::utils::path::PathExt; +use crate::utils::read_file; /// Creates a module with the Hg bookmark or branch in the current directory /// /// Will display the bookmark or branch name if the current directory is an hg repo pub fn module<'a>(context: &'a Context) -> Option> { - let is_hg_repo = context.try_begin_scan()?.set_folders(&[".hg"]).is_match(); - - if !is_hg_repo { - return None; - } - let mut module = context.new_module("hg_branch"); let config: HgBranchConfig = HgBranchConfig::try_load(module.config); @@ -34,16 +32,16 @@ pub fn module<'a>(context: &'a Context) -> Option> { config.truncation_length as usize }; - let branch_name = - get_hg_current_bookmark(context).unwrap_or_else(|| get_hg_branch_name(context)); + let repo_root = get_hg_repo_root(context).ok()?; + let branch_name = get_hg_current_bookmark(repo_root).unwrap_or_else(|_| { + get_hg_branch_name(repo_root).unwrap_or_else(|_| String::from("default")) + }); - let truncated_graphemes = get_graphemes(&branch_name, len); - // The truncation symbol should only be added if we truncated - let truncated_and_symbol = if len < graphemes_len(&branch_name) { - let truncation_symbol = get_graphemes(config.truncation_symbol, 1); - truncated_graphemes + truncation_symbol.as_str() + let branch_graphemes = truncate_text(&branch_name, len, config.truncation_symbol); + let topic_graphemes = if let Ok(topic) = get_hg_topic_name(repo_root) { + truncate_text(&topic, len, config.truncation_symbol) } else { - truncated_graphemes + String::from("") }; let parsed = StringFormatter::new(config.format).and_then(|formatter| { @@ -57,7 +55,8 @@ pub fn module<'a>(context: &'a Context) -> Option> { _ => None, }) .map(|variable| match variable { - "branch" => Some(Ok(truncated_and_symbol.as_str())), + "branch" => Some(Ok(branch_graphemes.as_str())), + "topic" => Some(Ok(topic_graphemes.as_str())), _ => None, }) .parse(None, Some(context)) @@ -74,26 +73,33 @@ pub fn module<'a>(context: &'a Context) -> Option> { Some(module) } -fn get_hg_branch_name(ctx: &Context) -> String { - std::fs::read_to_string(ctx.current_dir.join(".hg").join("branch")) - .map_or_else(|_| "default".to_string(), |s| s.trim().into()) +fn get_hg_repo_root<'a>(ctx: &'a Context) -> Result<&'a Path, Error> { + let dir = ctx.current_dir.as_path(); + let dev_id = dir.device_id(); + for root_dir in dir.ancestors() { + if dev_id != root_dir.device_id() { + break; + } + if root_dir.join(".hg").is_dir() { + return Ok(root_dir); + } + } + Err(Error::new(ErrorKind::Other, "No .hg found!")) } -fn get_hg_current_bookmark(ctx: &Context) -> Option { - std::fs::read_to_string(ctx.current_dir.join(".hg").join("bookmarks.current")) - .map(|s| s.trim().into()) - .ok() +fn get_hg_branch_name(hg_root: &Path) -> Result { + match read_file(hg_root.join(".hg").join("branch")) { + Ok(b) => Ok(b.trim().to_string()), + Err(e) => Err(e), + } } -fn get_graphemes(text: &str, length: usize) -> String { - UnicodeSegmentation::graphemes(text, true) - .take(length) - .collect::>() - .concat() +fn get_hg_current_bookmark(hg_root: &Path) -> Result { + read_file(hg_root.join(".hg").join("bookmarks.current")) } -fn graphemes_len(text: &str) -> usize { - UnicodeSegmentation::graphemes(text, true).count() +fn get_hg_topic_name(hg_root: &Path) -> Result { + read_file(hg_root.join(".hg").join("topic")) } #[cfg(test)] @@ -186,6 +192,26 @@ mod tests { tempdir.close() } + #[test] + #[ignore] + fn test_hg_topic() -> io::Result<()> { + let tempdir = fixture_repo(FixtureProvider::Hg)?; + let repo_dir = tempdir.path(); + fs::write(repo_dir.join(".hg").join("topic"), "feature")?; + + let actual = ModuleRenderer::new("hg_branch") + .path(repo_dir.to_str().unwrap()) + .config(toml::toml! { + [hg_branch] + format = "$topic" + disabled = false + }) + .collect(); + + assert_eq!(Some(String::from("feature")), actual); + tempdir.close() + } + #[test] #[ignore] fn test_default_truncation_symbol() -> io::Result<()> { diff --git a/src/modules/mod.rs b/src/modules/mod.rs index 64442937..53c36ec9 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -249,7 +249,7 @@ pub fn description(module: &str) -> &'static str { "haskell" => "The selected version of the Haskell toolchain", "haxe" => "The currently installed version of Haxe", "helm" => "The currently installed version of Helm", - "hg_branch" => "The active branch of the repo in your current directory", + "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", "jobs" => "The current number of jobs running", diff --git a/src/modules/utils/path.rs b/src/modules/utils/path.rs index 2bbb02b5..5f5ad484 100644 --- a/src/modules/utils/path.rs +++ b/src/modules/utils/path.rs @@ -13,6 +13,8 @@ pub trait PathExt { /// E.g. `\\?\UNC\server\share\foo` => `\foo` /// E.g. `/foo/bar` => `/foo/bar` fn without_prefix(&self) -> &Path; + /// Get device / volume info + fn device_id(&self) -> Option; } #[cfg(windows)] @@ -80,6 +82,11 @@ impl PathExt for Path { let (_, path) = normalize::normalize_path(self); path } + + fn device_id(&self) -> Option { + // Maybe it should use unimplemented! + Some(42u64) + } } // NOTE: Windows path prefixes are only parsed on Windows. @@ -100,6 +107,24 @@ impl PathExt for Path { fn without_prefix(&self) -> &Path { self } + + #[cfg(target_os = "linux")] + fn device_id(&self) -> Option { + use std::os::linux::fs::MetadataExt; + match self.metadata() { + Ok(m) => Some(m.st_dev()), + Err(_) => None, + } + } + + #[cfg(all(unix, not(target_os = "linux")))] + fn device_id(&self) -> Option { + use std::os::unix::fs::MetadataExt; + match self.metadata() { + Ok(m) => Some(m.dev()), + Err(_) => None, + } + } } #[cfg(test)]