Skip to content

Commit

Permalink
feat(fossil): detection of Fossil check-outs in subdirectories (#4910)
Browse files Browse the repository at this point in the history
* 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 <davidkna@users.noreply.github.com>
  • Loading branch information
VegardSkui and davidkna committed Apr 2, 2023
1 parent aef799b commit 4bca74e
Show file tree
Hide file tree
Showing 6 changed files with 106 additions and 51 deletions.
54 changes: 53 additions & 1 deletion src/context.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -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<gix::discover::Error>> {
self.repo
Expand Down Expand Up @@ -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<String> {
let name = repository.head_name().ok()??;
let shorthand = name.shorten();
Expand Down
24 changes: 16 additions & 8 deletions src/modules/fossil_branch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,11 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
} 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!(
Expand Down Expand Up @@ -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)?;
Expand Down
19 changes: 2 additions & 17 deletions src/modules/hg_branch.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
use std::io::{Error, ErrorKind};
use std::io::Error;
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
Expand All @@ -32,7 +31,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
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"))
});
Expand Down Expand Up @@ -73,20 +72,6 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
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<String, Error> {
match read_file(hg_root.join(".hg").join("branch")) {
Ok(b) => Ok(b.trim().to_string()),
Expand Down
25 changes: 0 additions & 25 deletions src/modules/utils/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u64>;
}

#[cfg(windows)]
Expand Down Expand Up @@ -82,11 +80,6 @@ impl PathExt for Path {
let (_, path) = normalize::normalize_path(self);
path
}

fn device_id(&self) -> Option<u64> {
// Maybe it should use unimplemented!
Some(42u64)
}
}

// NOTE: Windows path prefixes are only parsed on Windows.
Expand All @@ -107,24 +100,6 @@ impl PathExt for Path {
fn without_prefix(&self) -> &Path {
self
}

#[cfg(target_os = "linux")]
fn device_id(&self) -> Option<u64> {
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<u64> {
use std::os::unix::fs::MetadataExt;
match self.metadata() {
Ok(m) => Some(m.dev()),
Err(_) => None,
}
}
}

#[cfg(test)]
Expand Down
1 change: 1 addition & 0 deletions src/test/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ pub fn fixture_repo(provider: FixtureProvider) -> io::Result<TempDir> {
".fslckout"
};
let path = tempfile::tempdir()?;
fs::create_dir(path.path().join("subdir"))?;
fs::OpenOptions::new()
.create(true)
.write(true)
Expand Down
34 changes: 34 additions & 0 deletions src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u64>;
}

#[cfg(windows)]
impl PathExt for Path {
fn device_id(&self) -> Option<u64> {
// Maybe it should use unimplemented!
Some(42u64)
}
}

#[cfg(not(windows))]
impl PathExt for Path {
#[cfg(target_os = "linux")]
fn device_id(&self) -> Option<u64> {
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<u64> {
use std::os::unix::fs::MetadataExt;
match self.metadata() {
Ok(m) => Some(m.dev()),
Err(_) => None,
}
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down

0 comments on commit 4bca74e

Please sign in to comment.