diff options
author | Benjamin Sago <ogham@bsago.me> | 2017-09-02 21:22:24 +0100 |
---|---|---|
committer | Benjamin Sago <ogham@bsago.me> | 2017-09-02 21:59:15 +0100 |
commit | 265f93f7cd9b4560af64b606075a623b3fdf2105 (patch) | |
tree | 55e50dffe82059afad15f114b7fdde42afbaced2 | |
parent | d86fc4286be613aab5a0a8ca70682c005eb71fa1 (diff) | |
parent | c60ea36a315860cf41bb8f43cab514c746216f67 (diff) |
Merge branch 'late-git-discovery'
This merges in the new Git code, which now uses a global cache rather than being per-repository. This lets exa keep the Git column when listing files outside of a directory and when in recursive or tree views.
Fixes #24 and #183.
32 files changed, 610 insertions, 179 deletions
diff --git a/Vagrantfile b/Vagrantfile index 7335be9..f6107da 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -480,10 +480,7 @@ Vagrant.configure(2) do |config| touch $dir/that-file done - touch -t #{some_date} "#{test_dir}/attributes" # there's probably - touch -t #{some_date} "#{test_dir}/attributes"/* # a better - touch -t #{some_date} "#{test_dir}/attributes"/*/* # way to - touch -t #{some_date} "#{test_dir}/attributes"/*/*/* # do this + find "#{test_dir}/attributes" -exec touch {} -t #{some_date} \\; # I want to use the following to test, # but it only works on macos: @@ -519,12 +516,43 @@ Vagrant.configure(2) do |config| echo "more modifications!" | tee edits/unstaged edits/both additions/edited touch additions/unstaged - - touch -t #{some_date} "#{test_dir}/git/"*/* + find "#{test_dir}/git" -exec touch {} -t #{some_date} \\; sudo chown #{user}:#{user} -R "#{test_dir}/git" EOF + # A second Git repository + # for testing two at once + config.vm.provision :shell, privileged: false, inline: <<-EOF + set -xe + mkdir -p "#{test_dir}/git2/deeply/nested/directory" + cd "#{test_dir}/git2" + git init + + touch "deeply/nested/directory/upd8d" + git add "deeply/nested/directory/upd8d" + git commit -m "Automated test commit" + + echo "Now with contents" > "deeply/nested/directory/upd8d" + touch "deeply/nested/directory/l8st" + + echo -e "target\n*.mp3" > ".gitignore" + mkdir "ignoreds" + touch "ignoreds/music.mp3" + touch "ignoreds/music.m4a" + + mkdir "target" + touch "target/another ignored file" + + mkdir "deeply/nested/repository" + cd "deeply/nested/repository" + git init + touch subfile + + find "#{test_dir}/git2" -exec touch {} -t #{some_date} \\; + sudo chown #{user}:#{user} -R "#{test_dir}/git2" + EOF + # Hidden and dot file testcases. # We need to set the permissions of `.` and `..` because they actually # get displayed in the output here, so this has to come last. @@ -30,6 +30,7 @@ use std::path::{Component, PathBuf}; use ansi_term::{ANSIStrings, Style}; use fs::{Dir, File}; +use fs::feature::git::GitCache; use options::{Options, Vars}; pub use options::Misfire; use output::{escape, lines, grid, grid_details, details, View, Mode}; @@ -55,6 +56,11 @@ pub struct Exa<'args, 'w, W: Write + 'w> { /// List of the free command-line arguments that should correspond to file /// names (anything that isn’t an option). pub args: Vec<&'args OsStr>, + + /// A global Git cache, if the option was passed in. + /// This has to last the lifetime of the program, because the user might + /// want to list several directories in the same repository. + pub git: Option<GitCache>, } /// The “real” environment variables type. @@ -67,14 +73,33 @@ impl Vars for LiveVars { } } +/// Create a Git cache populated with the arguments that are going to be +/// listed before they’re actually listed, if the options demand it. +fn git_options(options: &Options, args: &[&OsStr]) -> Option<GitCache> { + if options.should_scan_for_git() { + Some(args.iter().map(|os| PathBuf::from(os)).collect()) + } + else { + None + } +} + impl<'args, 'w, W: Write + 'w> Exa<'args, 'w, W> { pub fn new<I>(args: I, writer: &'w mut W) -> Result<Exa<'args, 'w, W>, Misfire> where I: Iterator<Item=&'args OsString> { - Options::parse(args, &LiveVars).map(move |(options, args)| { + Options::parse(args, &LiveVars).map(move |(options, mut args)| { debug!("Dir action from arguments: {:#?}", options.dir_action); debug!("Filter from arguments: {:#?}", options.filter); debug!("View from arguments: {:#?}", options.view.mode); - Exa { options, writer, args } + + // List the current directory by default, like ls. + // This has to be done here, otherwise git_options won’t see it. + if args.is_empty() { + args = vec![ OsStr::new(".") ]; + } + + let git = git_options(&options, &args); + Exa { options, writer, args, git } }) } @@ -83,11 +108,6 @@ impl<'args, 'w, W: Write + 'w> Exa<'args, 'w, W> { let mut dirs = Vec::new(); let mut exit_status = 0; - // List the current directory by default, like ls. - if self.args.is_empty() { - self.args = vec![ OsStr::new(".") ]; - } - for file_path in &self.args { match File::new(PathBuf::from(file_path), None, None) { Err(e) => { @@ -96,7 +116,7 @@ impl<'args, 'w, W: Write + 'w> Exa<'args, 'w, W> { }, Ok(f) => { if f.is_directory() && !self.options.dir_action.treat_dirs_as_files() { - match f.to_dir(self.options.should_scan_for_git()) { + match f.to_dir() { Ok(d) => dirs.push(d), Err(e) => writeln!(stderr(), "{:?}: {}", file_path, e)?, } @@ -156,7 +176,7 @@ impl<'args, 'w, W: Write + 'w> Exa<'args, 'w, W> { let mut child_dirs = Vec::new(); for child_dir in children.iter().filter(|f| f.is_directory()) { - match child_dir.to_dir(false) { + match child_dir.to_dir() { Ok(d) => child_dirs.push(d), Err(e) => writeln!(stderr(), "{}: {}", child_dir.path.display(), e)?, } @@ -192,10 +212,10 @@ impl<'args, 'w, W: Write + 'w> Exa<'args, 'w, W> { grid::Render { files, colours, style, opts }.render(self.writer) } Mode::Details(ref opts) => { - details::Render { dir, files, colours, style, opts, filter: &self.options.filter, recurse: self.options.dir_action.recurse_options() }.render(self.writer) + details::Render { dir, files, colours, style, opts, filter: &self.options.filter, recurse: self.options.dir_action.recurse_options() }.render(self.git.as_ref(), self.writer) } Mode::GridDetails(ref opts) => { - grid_details::Render { dir, files, colours, style, grid: &opts.grid, details: &opts.details, filter: &self.options.filter, row_threshold: opts.row_threshold }.render(self.writer) + grid_details::Render { dir, files, colours, style, grid: &opts.grid, details: &opts.details, filter: &self.options.filter, row_threshold: opts.row_threshold }.render(self.git.as_ref(), self.writer) } } } diff --git a/src/fs/dir.rs b/src/fs/dir.rs index 77345d9..c0ba0ea 100644 --- a/src/fs/dir.rs +++ b/src/fs/dir.rs @@ -3,8 +3,7 @@ use std::fs; use std::path::{Path, PathBuf}; use std::slice::Iter as SliceIter; -use fs::feature::Git; -use fs::{File, fields}; +use fs::File; /// A **Dir** provides a cached list of the file paths in a directory that's @@ -20,10 +19,6 @@ pub struct Dir { /// The path that was read. pub path: PathBuf, - - /// Holds a `Git` object if scanning for Git repositories is switched on, - /// and this directory happens to contain one. - git: Option<Git>, } impl Dir { @@ -36,15 +31,14 @@ impl Dir { /// The `read_dir` iterator doesn’t actually yield the `.` and `..` /// entries, so if the user wants to see them, we’ll have to add them /// ourselves after the files have been read. - pub fn read_dir(path: PathBuf, git: bool) -> IOResult<Dir> { + pub fn read_dir(path: PathBuf) -> IOResult<Dir> { info!("Reading directory {:?}", &path); let contents: Vec<PathBuf> = try!(fs::read_dir(&path)? - .map(|result| result.map(|entry| entry.path())) - .collect()); + .map(|result| result.map(|entry| entry.path())) + .collect()); - let git = if git { Git::scan(&path).ok() } else { None }; - Ok(Dir { contents, path, git }) + Ok(Dir { contents, path }) } /// Produce an iterator of IO results of trying to read all the files in @@ -67,20 +61,6 @@ impl Dir { pub fn join(&self, child: &Path) -> PathBuf { self.path.join(child) } - - /// Return whether there's a Git repository on or above this directory. - pub fn has_git_repo(&self) -> bool { - self.git.is_some() - } - - /// Get a string describing the Git status of the given file. - pub fn git_status(&self, path: &Path, prefix_lookup: bool) -> fields::Git { - match (&self.git, prefix_lookup) { - (&Some(ref git), false) => git.status(path), - (&Some(ref git), true) => git.dir_status(path), - (&None, _) => fields::Git::empty() - } - } } diff --git a/src/fs/feature/git.rs b/src/fs/feature/git.rs index 0bfc24e..6ecf8e9 100644 --- a/src/fs/feature/git.rs +++ b/src/fs/feature/git.rs @@ -1,57 +1,276 @@ +//! Getting the Git status of files and directories. + use std::path::{Path, PathBuf}; +use std::sync::Mutex; use git2; use fs::fields as f; -/// Container of Git statuses for all the files in this folder's Git repository. -pub struct Git { - statuses: Vec<(PathBuf, git2::Status)>, +/// A **Git cache** is assembled based on the user’s input arguments. +/// +/// This uses vectors to avoid the overhead of hashing: it’s not worth it when the +/// expected number of Git repositories per exa invocation is 0 or 1... +pub struct GitCache { + + /// A list of discovered Git repositories and their paths. + repos: Vec<GitRepo>, + + /// Paths that we’ve confirmed do not have Git repositories underneath them. + misses: Vec<PathBuf>, } -impl Git { +impl GitCache { + pub fn has_anything_for(&self, index: &Path) -> bool { + self.repos.iter().any(|e| e.has_path(index)) + } - /// Discover a Git repository on or above this directory, scanning it for - /// the files' statuses if one is found. - pub fn scan(path: &Path) -> Result<Git, git2::Error> { - info!("Scanning for Git repository under {:?}", path); + pub fn get(&self, index: &Path, prefix_lookup: bool) -> f::Git { + self.repos.iter() + .find(|e| e.has_path(index)) + .map(|repo| repo.search(index, prefix_lookup)) + .unwrap_or_default() + } +} - let repo = git2::Repository::discover(path)?; - let workdir = match repo.workdir() { - Some(w) => w, - None => return Ok(Git { statuses: vec![] }), // bare repo +use std::iter::FromIterator; +impl FromIterator<PathBuf> for GitCache { + fn from_iter<I: IntoIterator<Item=PathBuf>>(iter: I) -> Self { + let iter = iter.into_iter(); + let mut git = GitCache { + repos: Vec::with_capacity(iter.size_hint().0), + misses: Vec::new(), }; - let statuses = repo.statuses(None)?.iter() - .map(|e| (workdir.join(Path::new(e.path().unwrap())), e.status())) - .collect(); + for path in iter { + if git.misses.contains(&path) { + debug!("Skipping {:?} because it already came back Gitless", path); + } + else if git.repos.iter().any(|e| e.has_path(&path)) { + debug!("Skipping {:?} because we already queried it", path); + } + else { + match GitRepo::discover(path) { + Ok(r) => { + if let Some(mut r2) = git.repos.iter_mut().find(|e| e.has_workdir(&r.workdir)) { + debug!("Adding to existing repo (workdir matches with {:?})", r2.workdir); + r2.extra_paths.push(r.original_path); + continue; + } - Ok(Git { statuses: statuses }) + debug!("Discovered new Git repo"); + git.repos.push(r); + }, + Err(miss) => git.misses.push(miss), + } + } + } + + git } +} + + + + +/// A **Git repository** is one we’ve discovered somewhere on the filesystem. +pub struct GitRepo { + + /// The queryable contents of the repository: either a `git2` repo, or the + /// cached results from when we queried it last time. + contents: Mutex<GitContents>, - /// Get the status for the file at the given path, if present. - pub fn status(&self, path: &Path) -> f::Git { - let status = self.statuses.iter() - .find(|p| p.0.as_path() == path); - match status { - Some(&(_, s)) => f::Git { staged: index_status(s), unstaged: working_tree_status(s) }, - None => f::Git { staged: f::GitStatus::NotModified, unstaged: f::GitStatus::NotModified } + /// The working directory of this repository. + /// This is used to check whether two repositories are the same. + workdir: PathBuf, + + /// The path that was originally checked to discover this repository. + /// This is as important as the extra_paths (it gets checked first), but + /// is separate to avoid having to deal with a non-empty Vec. + original_path: PathBuf, + + /// Any other paths that were checked only to result in this same + /// repository. + extra_paths: Vec<PathBuf>, +} + +/// A repository’s queried state. +enum GitContents { + + /// All the interesting Git stuff goes through this. + Before { repo: git2::Repository }, + + /// Temporary value used in `repo_to_statuses` so we can move the + /// repository out of the `Before` variant. + Processing, + + /// The data we’ve extracted from the repository, but only after we’ve + /// actually done so. + After { statuses: Git } +} + +impl GitRepo { + + /// Searches through this repository for a path (to a file or directory, + /// depending on the prefix-lookup flag) and returns its Git status. + /// + /// Actually querying the `git2` repository for the mapping of paths to + /// Git statuses is only done once, and gets cached so we don't need to + /// re-query the entire repository the times after that. + /// + /// The temporary `Processing` enum variant is used after the `git2` + /// repository is moved out, but before the results have been moved in! + /// See https://stackoverflow.com/q/45985827/3484614 + fn search(&self, index: &Path, prefix_lookup: bool) -> f::Git { + use self::GitContents::*; + use std::mem::replace; + + let mut contents = self.contents.lock().unwrap(); + if let After { ref statuses } = *contents { + debug!("Git repo {:?} has been found in cache", &self.workdir); + return statuses.status(index, prefix_lookup); } + + debug!("Querying Git repo {:?} for the first time", &self.workdir); + let repo = replace(&mut *contents, Processing).inner_repo(); + let statuses = repo_to_statuses(repo, &self.workdir); + let result = statuses.status(index, prefix_lookup); + let _processing = replace(&mut *contents, After { statuses }); + result + } + + /// Whether this repository has the given working directory. + fn has_workdir(&self, path: &Path) -> bool { + self.workdir == path + } + + /// Whether this repository cares about the given path at all. + fn has_path(&self, path: &Path) -> bool { + path.starts_with(&self.original_path) || self.extra_paths.iter().any(|e| path.starts_with(e)) + } + + /// Searches for a Git repository at any point above the given path. + /// Returns the original buffer if none is found. + fn discover(path: PathBuf) -> Result<GitRepo, PathBuf> { + info!("Searching for Git repository above {:?}", path); + let repo = match git2::Repository::discover(&path) { + Ok(r) => r, + Err(e) => { + error!("Error discovering Git repositories: {:?}", e); + return Err(path); + } + }; + + match repo.workdir().map(|wd| wd.to_path_buf()) { + Some(workdir) => { + let contents = Mutex::new(GitContents::Before { repo }); + Ok(GitRepo { contents, workdir, original_path: path, extra_paths: Vec::new() }) + }, + None => { + warn!("Repository has no workdir?"); + Err(path) + } + } + } +} + + +impl GitContents { + /// Assumes that the repository hasn’t been queried, and extracts it + /// (consuming the value) if it has. This is needed because the entire + /// enum variant gets replaced when a repo is queried (see above). + fn inner_repo(self) -> git2::Repository { + if let GitContents::Before { repo } = self { + repo + } + else { + unreachable!("Tried to extract a non-Repository") + } + } +} + +/// Iterates through a repository’s statuses, consuming it and returning the +/// mapping of files to their Git status. +/// We will have already used the working directory at this point, so it gets +/// passed in rather than deriving it from the `Repository` again. +fn repo_to_statuses(repo: git2::Repository, workdir: &Path) -> Git { + let mut statuses = Vec::new(); + + info!("Getting Git statuses for repo with workdir {:?}", workdir); + match repo.statuses(None) { + Ok(es) => { + for e in es.iter() { + let path = workdir.join(Path::new(e.path().unwrap())); + let elem = (path, e.status()); + statuses.push(elem); + } + }, + Err(e) => error!("Error looking up Git statuses: {:?}", e), + } + + Git { statuses } +} + +// The `repo.statuses` call above takes a long time. exa debug output: +// +// 20.311276 INFO:exa::fs::feature::git: Getting Git statuses for repo with workdir "/vagrant/" +// 20.799610 DEBUG:exa::output::table: Getting Git status for file "./Cargo.toml" +// +// Even inserting another logging line immediately afterwards doesn't make it +// look any faster. + + +/// Container of Git statuses for all the files in this folder’s Git repository. +struct Git { + statuses: Vec<(PathBuf, git2::Status)>, +} + +impl Git { + + /// Get either the file or directory status for the given path. + /// “Prefix lookup” means that it should report an aggregate status of all + /// paths starting with the given prefix (in other words, a directory). + fn status(&self, index: &Path, prefix_lookup: bool) -> f::Git { + if prefix_lookup { self.dir_status(index) } + else { self.file_status(index) } + } + + /// Get the status for the file at the given path. + fn file_status(&self, file: &Path) -> f::Git { + let path = reorient(file); + self.statuses.iter() + .find(|p| p.0.as_path() == path) + .map(|&(_, s)| f::Git { staged: index_status(s), unstaged: working_tree_status(s) }) + .unwrap_or_default() } /// Get the combined status for all the files whose paths begin with the /// path that gets passed in. This is used for getting the status of - /// directories, which don't really have an 'official' status. - pub fn dir_status(&self, dir: &Path) -> f::Git { + /// directories, which don’t really have an ‘official’ status. + fn dir_status(&self, dir: &Path) -> f::Git { + let path = reorient(dir); let s = self.statuses.iter() - .filter(|p| p.0.starts_with(dir)) + .filter(|p| p.0.starts_with(&path)) .fold(git2::Status::empty(), |a, b| a | b.1); f::Git { staged: index_status(s), unstaged: working_tree_status(s) } } } +/// Converts a path to an absolute path based on the current directory. +/// Paths need to be absolute for them to be compared properly, otherwise +/// you’d ask a repo about “./README.md” but it only knows about +/// “/vagrant/REAMDE.md”, prefixed by the workdir. +fn reorient(path: &Path) -> PathBuf { + use std::env::current_dir; + // I’m not 100% on this func tbh + match current_dir() { + Err(_) => Path::new(".").join(&path), + Ok(dir) => dir.join(&path), + } +} + /// The character to display if the file has been modified, but not staged. fn working_tree_status(status: git2::Status) -> f::GitStatus { match status { @@ -64,7 +283,7 @@ fn working_tree_status(status: git2::Status) -> f::GitStatus { } } -/// The character to display if the file has been modified, and the change +/// The character to display if the file has been modified and the change /// has been staged. fn index_status(status: git2::Status) -> f::GitStatus { match status { diff --git a/src/fs/feature/mod.rs b/src/fs/feature/mod.rs index 72db8e1..e2f5d0c 100644 --- a/src/fs/feature/mod.rs +++ b/src/fs/feature/mod.rs @@ -3,24 +3,39 @@ pub mod xattr; // Git support -#[cfg(feature="git")] mod git; -#[cfg(feature="git")] pub use self::git::Git; - -#[cfg(not(feature="git"))] pub struct Git; -#[cfg(not(feature="git"))] use std::path::Path; -#[cfg(not(feature="git"))] use fs::fields; +#[cfg(feature="git")] pub mod git; #[cfg(not(feature="git"))] -impl Git { - pub fn scan(_: &Path) -> Result<Git, ()> { - Err(()) +pub mod git { + use std::iter::FromIterator; + use std::path::{Path, PathBuf}; + + use fs::fields; + + + pub struct GitCache; + + impl FromIterator<PathBuf> for GitCache { + fn from_iter<I: IntoIterator<Item=PathBuf>>(_iter: I) -> Self { + GitCache + } } - pub fn status(&self, _: &Path) -> fields::Git { - panic!("Tried to access a Git repo without Git support!"); + impl GitCache { + pub fn get(&self, _index: &Path) -> Option<Git> { + panic!("Tried to query a Git cache, but Git support is disabled") + } } - pub fn dir_status(&self, path: &Path) -> fields::Git { - self.status(path) + pub struct Git; + + impl Git { + pub fn status(&self, _: &Path) -> fields::Git { + panic!("Tried to get a Git status, but Git support is disabled") + } + + pub fn dir_status(&self, path: &Path) -> fields::Git { + self.status(path) + } } } diff --git a/src/fs/fields.rs b/src/fs/fields.rs index 75301a6..26d8939 100644 --- a/src/fs/fields.rs +++ b/src/fs/fields.rs @@ -1,3 +1,4 @@ + //! Wrapper types for the values returned from `File`s. //! //! The methods of `File` that return information about the entry on the @@ -206,10 +207,11 @@ pub struct Git { pub unstaged: GitStatus, } -impl Git { +use std::default::Default; +impl Default for Git { /// Create a Git status for a file with nothing done to it. - pub fn empty() -> Git { + fn default() -> Git { Git { staged: GitStatus::NotModified, unstaged: GitStatus::NotModified } } } diff --git a/src/fs/file.rs b/src/fs/file.rs index c07f879..97b3cd6 100644 --- a/src/fs/file.rs +++ b/src/fs/file.rs @@ -19,7 +19,7 @@ use fs::fields as f; /// start and hold on to all the information. pub struct File<'dir> { - /// The filename portion of this file's path, including the extension. + /// The filename portion of this file’s path, including the extension. /// /// This is used to compare against certain filenames (such as checking if /// it’s “Makefile” or something) and to highlight only the filename in @@ -33,26 +33,27 @@ pub struct File<'dir> { /// The path that begat this file. /// - /// Even though the file's name is extracted, the path needs to be kept - /// around, as certain operations involve looking up the file's absolute - /// location (such as the Git status, or searching for compiled files). + /// Even though the file’s name is extracted, the path needs to be kept + /// around, as certain operations involve looking up the file’s absolute + /// location (such as searching for compiled files) or using its original + /// path (following a symlink). pub path: PathBuf, - /// A cached `metadata` call for this file. + /// A cached `metadata` (`stat`) call for this file. /// /// This too is queried multiple times, and is *not* cached by the OS, as - /// it could easily change between invocations - but exa is so short-lived + /// it could easily change between invocations — but exa is so short-lived /// it's better to just cache it. pub metadata: fs::Metadata, - /// A reference to the directory that contains this file, if present. + /// A reference to the directory that contains this file, if any. /// /// Filenames that get passed in on the command-line directly will have no - /// parent directory reference - although they technically have one on the - /// filesystem, we'll never need to look at it, so it'll be `None`. + /// parent directory reference — although they technically have one on the + /// filesystem, we’ll never need to look at it, so it’ll be `None`. /// 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). + /// as looking up compiled files). pub parent_dir: Option<&'dir Dir>, } @@ -88,11 +89,11 @@ impl<'dir> File<'dir> { /// 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". + /// 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. + /// within ASCII, so it’s alright. fn ext(path: &Path) -> Option<String> { use std::ascii::AsciiExt; @@ -110,24 +111,24 @@ impl<'dir> File<'dir> { } /// If this file is a directory on the filesystem, then clone its - /// `PathBuf` for use in one of our own `Dir` objects, and read a list of + /// `PathBuf` for use in one of our own `Dir` values, and read a list of /// its contents. /// - /// Returns an IO error upon failure, but this shouldn't be used to check + /// Returns an IO error upon failure, but this shouldn’t be used to check /// if a `File` is a directory or not! For that, just use `is_directory()`. - pub fn to_dir(&self, scan_for_git: bool) -> IOResult<Dir> { - Dir::read_dir(self.path.clone(), scan_for_git) + pub fn to_dir(&self) -> IOResult<Dir> { + Dir::read_dir(self.path.clone()) } - /// Whether this file is a regular file on the filesystem - that is, not a + /// Whether this file is a regular file on the filesystem — that is, not a /// directory, a link, or anything else treated specially. pub fn is_file(&self) -> bool { self.metadata.is_file() } /// Whether this file is both a regular file *and* executable for the - /// current user. Executable files have different semantics than - /// executable directories, and so should be highlighted differently. + /// current user. An executable file has a different purpose from an + /// executable directory, so they should be highlighted differently. pub fn is_executable_file(&self) -> bool { let bit |