Skip to content

Commit

Permalink
Add follow_symlinks option
Browse files Browse the repository at this point in the history
Settings this to false can fix hanging on symlinks to slow/inaccessible
filesystems.
  • Loading branch information
lutzky committed Oct 1, 2023
1 parent 7e82cb4 commit 4fbcb4d
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 13 deletions.
4 changes: 4 additions & 0 deletions .github/config-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1794,6 +1794,10 @@
"default": true,
"type": "boolean"
},
"follow_symlinks": {
"default": true,
"type": "boolean"
},
"palette": {
"type": [
"string",
Expand Down
8 changes: 8 additions & 0 deletions docs/config/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,14 @@ This is the list of prompt-wide configuration options.
| `add_newline` | `true` | Inserts blank line between shell prompts. |
| `palette` | `''` | Sets which color palette from `palettes` to use. |
| `palettes` | `{}` | Collection of color palettes that assign [colors](/advanced-config/#style-strings) to user-defined names. Note that color palettes cannot reference their own color definitions. |
| `follow_symlinks` | `true` | Follows symlinks to check if they're directories; used in modules such as git. |

::: tip

If you have symlinks to networked filesystems, consider setting
`follow_symlinks` to `false`.

:::

### Example

Expand Down
2 changes: 2 additions & 0 deletions src/configs/starship_root.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub struct StarshipRootConfig {
pub scan_timeout: u64,
pub command_timeout: u64,
pub add_newline: bool,
pub follow_symlinks: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub palette: Option<String>,
pub palettes: HashMap<String, Palette>,
Expand Down Expand Up @@ -134,6 +135,7 @@ impl Default for StarshipRootConfig {
scan_timeout: 30,
command_timeout: 500,
add_newline: true,
follow_symlinks: true,
palette: None,
palettes: HashMap::default(),
}
Expand Down
96 changes: 83 additions & 13 deletions src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,11 @@ impl<'a> Context<'a> {
pub fn dir_contents(&self) -> Result<&DirContents, std::io::Error> {
self.dir_contents.get_or_try_init(|| {
let timeout = self.root_config.scan_timeout;
DirContents::from_path_with_timeout(&self.current_dir, Duration::from_millis(timeout))
DirContents::from_path_with_timeout(
&self.current_dir,
Duration::from_millis(timeout),
self.root_config.follow_symlinks,
)
})
}

Expand Down Expand Up @@ -479,11 +483,15 @@ pub struct DirContents {

impl DirContents {
#[cfg(test)]
fn from_path(base: &Path) -> Result<Self, std::io::Error> {
Self::from_path_with_timeout(base, Duration::from_secs(30))
fn from_path(base: &Path, follow_symlinks: bool) -> Result<Self, std::io::Error> {
Self::from_path_with_timeout(base, Duration::from_secs(30), follow_symlinks)
}

fn from_path_with_timeout(base: &Path, timeout: Duration) -> Result<Self, std::io::Error> {
fn from_path_with_timeout(
base: &Path,
timeout: Duration,
follow_symlinks: bool,
) -> Result<Self, std::io::Error> {
let start = Instant::now();

let mut folders: HashSet<PathBuf> = HashSet::new();
Expand All @@ -501,7 +509,15 @@ impl DirContents {
.filter_map(|(_, entry)| entry.ok())
.for_each(|entry| {
let path = PathBuf::from(entry.path().strip_prefix(base).unwrap());
if entry.path().is_dir() {

let is_dir = match follow_symlinks {
true => entry.path().is_dir(),
false => fs::symlink_metadata(entry.path())
.map(|m| m.is_dir())
.unwrap_or(false),
};

if is_dir {
folders.insert(path);
} else {
if !path.to_string_lossy().starts_with('.') {
Expand Down Expand Up @@ -897,10 +913,63 @@ mod tests {
Ok(dir)
}

#[test]
fn test_scan_dir_no_symlinks() -> Result<(), Box<dyn std::error::Error>> {
#[cfg(not(target_os = "windows"))]
use std::os::unix::fs::symlink;
#[cfg(target_os = "windows")]
use std::os::windows::fs::symlink_dir as symlink;

let d = testdir(&["file"])?;
fs::create_dir(d.path().join("folder"))?;

symlink(d.path().join("folder"), d.path().join("link_to_folder"))?;
symlink(d.path().join("file"), d.path().join("link_to_file"))?;

let dc_following_symlinks = DirContents::from_path(d.path(), true)?;

assert!(ScanDir {
dir_contents: &dc_following_symlinks,
files: &["link_to_file"],
extensions: &[],
folders: &[],
}
.is_match());

assert!(ScanDir {
dir_contents: &dc_following_symlinks,
files: &[],
extensions: &[],
folders: &["link_to_folder"],
}
.is_match());

let dc_not_following_symlinks = DirContents::from_path(d.path(), false)?;

assert!(ScanDir {
dir_contents: &dc_not_following_symlinks,
files: &["link_to_file"],
extensions: &[],
folders: &[],
}
.is_match());

assert!(!ScanDir {
dir_contents: &dc_not_following_symlinks,
files: &[],
extensions: &[],
folders: &["link_to_folder"],
}
.is_match());

Ok(())
}

#[test]
fn test_scan_dir() -> Result<(), Box<dyn std::error::Error>> {
let empty = testdir(&[])?;
let empty_dc = DirContents::from_path(empty.path())?;
let follow_symlinks = true;
let empty_dc = DirContents::from_path(empty.path(), follow_symlinks)?;

assert!(!ScanDir {
dir_contents: &empty_dc,
Expand All @@ -912,7 +981,7 @@ mod tests {
empty.close()?;

let rust = testdir(&["README.md", "Cargo.toml", "src/main.rs"])?;
let rust_dc = DirContents::from_path(rust.path())?;
let rust_dc = DirContents::from_path(rust.path(), follow_symlinks)?;
assert!(!ScanDir {
dir_contents: &rust_dc,
files: &["package.json"],
Expand All @@ -923,7 +992,7 @@ mod tests {
rust.close()?;

let java = testdir(&["README.md", "src/com/test/Main.java", "pom.xml"])?;
let java_dc = DirContents::from_path(java.path())?;
let java_dc = DirContents::from_path(java.path(), follow_symlinks)?;
assert!(!ScanDir {
dir_contents: &java_dc,
files: &["package.json"],
Expand All @@ -934,7 +1003,7 @@ mod tests {
java.close()?;

let node = testdir(&["README.md", "node_modules/lodash/main.js", "package.json"])?;
let node_dc = DirContents::from_path(node.path())?;
let node_dc = DirContents::from_path(node.path(), follow_symlinks)?;
assert!(ScanDir {
dir_contents: &node_dc,
files: &["package.json"],
Expand All @@ -945,7 +1014,7 @@ mod tests {
node.close()?;

let tarballs = testdir(&["foo.tgz", "foo.tar.gz"])?;
let tarballs_dc = DirContents::from_path(tarballs.path())?;
let tarballs_dc = DirContents::from_path(tarballs.path(), follow_symlinks)?;
assert!(ScanDir {
dir_contents: &tarballs_dc,
files: &[],
Expand All @@ -956,7 +1025,7 @@ mod tests {
tarballs.close()?;

let dont_match_ext = testdir(&["foo.js", "foo.ts"])?;
let dont_match_ext_dc = DirContents::from_path(dont_match_ext.path())?;
let dont_match_ext_dc = DirContents::from_path(dont_match_ext.path(), follow_symlinks)?;
assert!(!ScanDir {
dir_contents: &dont_match_ext_dc,
files: &[],
Expand All @@ -967,7 +1036,7 @@ mod tests {
dont_match_ext.close()?;

let dont_match_file = testdir(&["goodfile", "evilfile"])?;
let dont_match_file_dc = DirContents::from_path(dont_match_file.path())?;
let dont_match_file_dc = DirContents::from_path(dont_match_file.path(), follow_symlinks)?;
assert!(!ScanDir {
dir_contents: &dont_match_file_dc,
files: &["goodfile", "!notfound", "!evilfile"],
Expand All @@ -978,7 +1047,8 @@ mod tests {
dont_match_file.close()?;

let dont_match_folder = testdir(&["gooddir/somefile", "evildir/somefile"])?;
let dont_match_folder_dc = DirContents::from_path(dont_match_folder.path())?;
let dont_match_folder_dc =
DirContents::from_path(dont_match_folder.path(), follow_symlinks)?;
assert!(!ScanDir {
dir_contents: &dont_match_folder_dc,
files: &[],
Expand Down

0 comments on commit 4fbcb4d

Please sign in to comment.