From 4bca74eca29e159f0d6f27db432927012848408c Mon Sep 17 00:00:00 2001 From: Vegard Skui Date: Sun, 2 Apr 2023 16:37:27 +0200 Subject: [PATCH] feat(fossil): detection of Fossil check-outs in subdirectories (#4910) * Move PathExt::device_id() outside modules module * Add upwards_sibling_scan-function * Fix Fossil check-out detection in subdirectories * Use shared upwards scanning function in hg_branch * Let the caller specify if they're looking for a file or a folder * fix merge --------- Co-authored-by: David Knaack --- src/context.rs | 54 +++++++++++++++++++++++++++++++++++- src/modules/fossil_branch.rs | 24 ++++++++++------ src/modules/hg_branch.rs | 19 ++----------- src/modules/utils/path.rs | 25 ----------------- src/test/mod.rs | 1 + src/utils.rs | 34 +++++++++++++++++++++++ 6 files changed, 106 insertions(+), 51 deletions(-) diff --git a/src/context.rs b/src/context.rs index 0994a8e5..b0bfc0fd 100644 --- a/src/context.rs +++ b/src/context.rs @@ -1,7 +1,7 @@ use crate::config::{ModuleConfig, StarshipConfig}; use crate::configs::StarshipRootConfig; use crate::module::Module; -use crate::utils::{create_command, exec_timeout, read_file, CommandOutput}; +use crate::utils::{create_command, exec_timeout, read_file, CommandOutput, PathExt}; use crate::modules; use crate::utils::{self, home_dir}; @@ -248,6 +248,16 @@ impl<'a> Context<'a> { }) } + /// Begins an ancestor scan at the current directory, see [`ScanAncestors`] for available + /// methods. + pub fn begin_ancestor_scan(&'a self) -> ScanAncestors<'a> { + ScanAncestors { + path: &self.current_dir, + files: &[], + folders: &[], + } + } + /// Will lazily get repo root and branch when a module requests it. pub fn get_repo(&self) -> Result<&Repo, Box> { self.repo @@ -607,6 +617,48 @@ impl<'a> ScanDir<'a> { } } +/// Scans the ancestors of a given path until a directory containing one of the given files or +/// folders is found. +pub struct ScanAncestors<'a> { + path: &'a Path, + files: &'a [&'a str], + folders: &'a [&'a str], +} + +impl<'a> ScanAncestors<'a> { + #[must_use] + pub const fn set_files(mut self, files: &'a [&'a str]) -> Self { + self.files = files; + self + } + + #[must_use] + pub const fn set_folders(mut self, folders: &'a [&'a str]) -> Self { + self.folders = folders; + self + } + + /// Scans upwards starting from the initial path until a directory containing one of the given + /// files or folders is found. + /// + /// The scan does not cross device boundaries. + pub fn scan(&self) -> Option<&'a Path> { + let initial_device_id = self.path.device_id(); + for dir in self.path.ancestors() { + if initial_device_id != dir.device_id() { + break; + } + + if self.files.iter().any(|name| dir.join(name).is_file()) + || self.folders.iter().any(|name| dir.join(name).is_dir()) + { + return Some(dir); + } + } + None + } +} + fn get_current_branch(repository: &Repository) -> Option { let name = repository.head_name().ok()??; let shorthand = name.shorten(); diff --git a/src/modules/fossil_branch.rs b/src/modules/fossil_branch.rs index da4dabbd..7cc4d331 100644 --- a/src/modules/fossil_branch.rs +++ b/src/modules/fossil_branch.rs @@ -22,15 +22,11 @@ pub fn module<'a>(context: &'a Context) -> Option> { } else { ".fslckout" }; - - let is_checkout = context - .try_begin_scan()? + // See if we're in a check-out by scanning upwards for a directory containing the checkout_db file + context + .begin_ancestor_scan() .set_files(&[checkout_db]) - .is_match(); - - if !is_checkout { - return None; - } + .scan()?; let len = if config.truncation_length <= 0 { log::warn!( @@ -143,6 +139,18 @@ mod tests { tempdir.close() } + #[test] + fn test_fossil_branch_subdir() -> io::Result<()> { + let tempdir = fixture_repo(FixtureProvider::Fossil)?; + let checkout_dir = tempdir.path(); + expect_fossil_branch_with_config( + &checkout_dir.join("subdir"), + None, + &[Expect::BranchName("topic-branch"), Expect::NoTruncation], + ); + tempdir.close() + } + #[test] fn test_fossil_branch_configured() -> io::Result<()> { let tempdir = fixture_repo(FixtureProvider::Fossil)?; diff --git a/src/modules/hg_branch.rs b/src/modules/hg_branch.rs index 8a9f84ae..c4c4a2a3 100644 --- a/src/modules/hg_branch.rs +++ b/src/modules/hg_branch.rs @@ -1,4 +1,4 @@ -use std::io::{Error, ErrorKind}; +use std::io::Error; use std::path::Path; use super::utils::truncate::truncate_text; @@ -6,7 +6,6 @@ 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 @@ -32,7 +31,7 @@ pub fn module<'a>(context: &'a Context) -> Option> { config.truncation_length as usize }; - let repo_root = get_hg_repo_root(context).ok()?; + let repo_root = context.begin_ancestor_scan().set_folders(&[".hg"]).scan()?; let branch_name = get_hg_current_bookmark(repo_root).unwrap_or_else(|_| { get_hg_branch_name(repo_root).unwrap_or_else(|_| String::from("default")) }); @@ -73,20 +72,6 @@ pub fn module<'a>(context: &'a Context) -> Option> { Some(module) } -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_branch_name(hg_root: &Path) -> Result { match read_file(hg_root.join(".hg").join("branch")) { Ok(b) => Ok(b.trim().to_string()), diff --git a/src/modules/utils/path.rs b/src/modules/utils/path.rs index 5f5ad484..2bbb02b5 100644 --- a/src/modules/utils/path.rs +++ b/src/modules/utils/path.rs @@ -13,8 +13,6 @@ 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)] @@ -82,11 +80,6 @@ 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. @@ -107,24 +100,6 @@ 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)] diff --git a/src/test/mod.rs b/src/test/mod.rs index 48bb51ad..23b0d6fb 100644 --- a/src/test/mod.rs +++ b/src/test/mod.rs @@ -182,6 +182,7 @@ pub fn fixture_repo(provider: FixtureProvider) -> io::Result { ".fslckout" }; let path = tempfile::tempdir()?; + fs::create_dir(path.path().join("subdir"))?; fs::OpenOptions::new() .create(true) .write(true) diff --git a/src/utils.rs b/src/utils.rs index 54b9e739..aad9f297 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -644,6 +644,40 @@ pub fn encode_to_hex(slice: &[u8]) -> String { String::from_utf8(dst).unwrap() } +pub trait PathExt { + /// Get device / volume info + fn device_id(&self) -> Option; +} + +#[cfg(windows)] +impl PathExt for Path { + fn device_id(&self) -> Option { + // Maybe it should use unimplemented! + Some(42u64) + } +} + +#[cfg(not(windows))] +impl PathExt for Path { + #[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)] mod tests { use super::*;