|
|
@@ -53,34 +53,48 @@ pub struct File<'dir> {
|
|
|
/// However, *directories* that get passed in will produce files that
|
|
|
/// contain a reference to it, which is used in certain operations (such
|
|
|
/// as looking up a file's Git status).
|
|
|
- pub dir: Option<&'dir Dir>,
|
|
|
-}
|
|
|
-
|
|
|
-/// A file’s name is derived from its string. This needs to handle directories
|
|
|
-/// such as `/` or `..`, which have no `file_name` component. So instead, just
|
|
|
-/// use the last component as the name.
|
|
|
-pub fn path_filename(path: &Path) -> String {
|
|
|
- match path.components().next_back() {
|
|
|
- Some(back) => back.as_os_str().to_string_lossy().to_string(),
|
|
|
- None => path.display().to_string(), // use the path as fallback
|
|
|
- }
|
|
|
+ pub parent_dir: Option<&'dir Dir>,
|
|
|
}
|
|
|
|
|
|
impl<'dir> File<'dir> {
|
|
|
- pub fn new(path: &Path, parent: Option<&'dir Dir>, mut filename: Option<String>) -> IOResult<File<'dir>> {
|
|
|
+ pub fn new(path: PathBuf, parent_dir: Option<&'dir Dir>, mut filename: Option<String>) -> IOResult<File<'dir>> {
|
|
|
if filename.is_none() {
|
|
|
- filename = Some(path_filename(path));
|
|
|
+ filename = Some(File::filename(&path));
|
|
|
}
|
|
|
|
|
|
- let metadata = fs::symlink_metadata(path)?;
|
|
|
+ let metadata = fs::symlink_metadata(&path)?;
|
|
|
+ let ext = File::ext(&path);
|
|
|
|
|
|
- Ok(File {
|
|
|
- path: path.to_path_buf(),
|
|
|
- dir: parent,
|
|
|
- metadata: metadata,
|
|
|
- ext: ext(path),
|
|
|
- name: filename.unwrap(),
|
|
|
- })
|
|
|
+ Ok(File { path, parent_dir, metadata, ext, name: filename.unwrap() })
|
|
|
+ }
|
|
|
+
|
|
|
+ /// A file’s name is derived from its string. This needs to handle directories
|
|
|
+ /// such as `/` or `..`, which have no `file_name` component. So instead, just
|
|
|
+ /// use the last component as the name.
|
|
|
+ pub fn filename(path: &Path) -> String {
|
|
|
+ match path.components().next_back() {
|
|
|
+ Some(back) => back.as_os_str().to_string_lossy().to_string(),
|
|
|
+ None => path.display().to_string(), // use the path as fallback
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Extract an extension from a file path, if one is present, in lowercase.
|
|
|
+ ///
|
|
|
+ /// The extension is the series of characters after the last dot. This
|
|
|
+ /// deliberately counts dotfiles, so the ".git" folder has the extension "git".
|
|
|
+ ///
|
|
|
+ /// ASCII lowercasing is used because these extensions are only compared
|
|
|
+ /// against a pre-compiled list of extensions which are known to only exist
|
|
|
+ /// within ASCII, so it's alright.
|
|
|
+ fn ext(path: &Path) -> Option<String> {
|
|
|
+ use std::ascii::AsciiExt;
|
|
|
+
|
|
|
+ let name = match path.file_name() {
|
|
|
+ Some(f) => f.to_string_lossy().to_string(),
|
|
|
+ None => return None,
|
|
|
+ };
|
|
|
+
|
|
|
+ name.rfind('.').map(|p| name[p+1..].to_ascii_lowercase())
|
|
|
}
|
|
|
|
|
|
/// Whether this file is a directory on the filesystem.
|
|
|
@@ -145,7 +159,7 @@ impl<'dir> File<'dir> {
|
|
|
if path.is_absolute() {
|
|
|
path.to_path_buf()
|
|
|
}
|
|
|
- else if let Some(dir) = self.dir {
|
|
|
+ else if let Some(dir) = self.parent_dir {
|
|
|
dir.join(&*path)
|
|
|
}
|
|
|
else if let Some(parent) = self.path.parent() {
|
|
|
@@ -172,26 +186,22 @@ impl<'dir> File<'dir> {
|
|
|
// this file -- which could be absolute or relative -- to the path
|
|
|
// we actually look up and turn into a `File` -- which needs to be
|
|
|
// absolute to be accessible from any directory.
|
|
|
- let display_path = match fs::read_link(&self.path) {
|
|
|
- Ok(path) => path,
|
|
|
- Err(e) => return FileTarget::Err(e),
|
|
|
+ let path = match fs::read_link(&self.path) {
|
|
|
+ Ok(p) => p,
|
|
|
+ Err(e) => return FileTarget::Err(e),
|
|
|
};
|
|
|
|
|
|
- let target_path = self.reorient_target_path(&*display_path);
|
|
|
+ let absolute_path = self.reorient_target_path(&path);
|
|
|
|
|
|
// Use plain `metadata` instead of `symlink_metadata` - we *want* to
|
|
|
// follow links.
|
|
|
- if let Ok(metadata) = fs::metadata(&target_path) {
|
|
|
- FileTarget::Ok(File {
|
|
|
- dir: None,
|
|
|
- ext: ext(&display_path),
|
|
|
- metadata: metadata,
|
|
|
- name: path_filename(&display_path),
|
|
|
- path: display_path,
|
|
|
- })
|
|
|
+ if let Ok(metadata) = fs::metadata(&absolute_path) {
|
|
|
+ let ext = File::ext(&path);
|
|
|
+ let name = File::filename(&path);
|
|
|
+ FileTarget::Ok(File { parent_dir: None, path, ext, metadata, name })
|
|
|
}
|
|
|
else {
|
|
|
- FileTarget::Broken(display_path)
|
|
|
+ FileTarget::Broken(path)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -355,7 +365,7 @@ impl<'dir> File<'dir> {
|
|
|
pub fn git_status(&self) -> f::Git {
|
|
|
use std::env::current_dir;
|
|
|
|
|
|
- match self.dir {
|
|
|
+ match self.parent_dir {
|
|
|
None => f::Git { staged: f::GitStatus::NotModified, unstaged: f::GitStatus::NotModified },
|
|
|
Some(d) => {
|
|
|
let cwd = match current_dir() {
|
|
|
@@ -377,26 +387,6 @@ impl<'a> AsRef<File<'a>> for File<'a> {
|
|
|
}
|
|
|
|
|
|
|
|
|
-/// Extract an extension from a file path, if one is present, in lowercase.
|
|
|
-///
|
|
|
-/// The extension is the series of characters after the last dot. This
|
|
|
-/// deliberately counts dotfiles, so the ".git" folder has the extension "git".
|
|
|
-///
|
|
|
-/// ASCII lowercasing is used because these extensions are only compared
|
|
|
-/// against a pre-compiled list of extensions which are known to only exist
|
|
|
-/// within ASCII, so it's alright.
|
|
|
-fn ext(path: &Path) -> Option<String> {
|
|
|
- use std::ascii::AsciiExt;
|
|
|
-
|
|
|
- let name = match path.file_name() {
|
|
|
- Some(f) => f.to_string_lossy().to_string(),
|
|
|
- None => return None,
|
|
|
- };
|
|
|
-
|
|
|
- name.rfind('.').map(|p| name[p+1..].to_ascii_lowercase())
|
|
|
-}
|
|
|
-
|
|
|
-
|
|
|
/// The result of following a symlink.
|
|
|
pub enum FileTarget<'dir> {
|
|
|
|
|
|
@@ -458,22 +448,59 @@ mod modes {
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
-mod test {
|
|
|
- use super::ext;
|
|
|
+mod ext_test {
|
|
|
+ use super::File;
|
|
|
use std::path::Path;
|
|
|
|
|
|
#[test]
|
|
|
fn extension() {
|
|
|
- assert_eq!(Some("dat".to_string()), ext(Path::new("fester.dat")))
|
|
|
+ assert_eq!(Some("dat".to_string()), File::ext(Path::new("fester.dat")))
|
|
|
}
|
|
|
|
|
|
#[test]
|
|
|
fn dotfile() {
|
|
|
- assert_eq!(Some("vimrc".to_string()), ext(Path::new(".vimrc")))
|
|
|
+ assert_eq!(Some("vimrc".to_string()), File::ext(Path::new(".vimrc")))
|
|
|
}
|
|
|
|
|
|
#[test]
|
|
|
fn no_extension() {
|
|
|
- assert_eq!(None, ext(Path::new("jarlsberg")))
|
|
|
+ assert_eq!(None, File::ext(Path::new("jarlsberg")))
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+#[cfg(test)]
|
|
|
+mod filename_test {
|
|
|
+ use super::File;
|
|
|
+ use std::path::Path;
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn file() {
|
|
|
+ assert_eq!("fester.dat", File::filename(Path::new("fester.dat")))
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn no_path() {
|
|
|
+ assert_eq!("foo.wha", File::filename(Path::new("/var/cache/foo.wha")))
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn here() {
|
|
|
+ assert_eq!(".", File::filename(Path::new(".")))
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn there() {
|
|
|
+ assert_eq!("..", File::filename(Path::new("..")))
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn everywhere() {
|
|
|
+ assert_eq!("..", File::filename(Path::new("./..")))
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn topmost() {
|
|
|
+ assert_eq!("/", File::filename(Path::new("/")))
|
|
|
}
|
|
|
}
|