just/src/search.rs
Casey Rodarmor aefdcea7d0
Gargantuan refactor (#522)
- Instead of changing the current directory with `env::set_current_dir`
  to be implicitly inherited by subprocesses, we now use
  `Command::current_dir` to set it explicitly. This feels much better,
  since we aren't dependent on the implicit state of the process's
  current directory.

- Subcommand execution is much improved.

- Added a ton of tests for config parsing, config execution, working
  dir, and search dir.

- Error messages are improved. Many more will be colored.

- The Config is now onwed, instead of borrowing from the arguments and
  the `clap::ArgMatches` object. This is a huge ergonomic improvement,
  especially in tests, and I don't think anyone will notice.

- `--edit` now uses `$VISUAL`, `$EDITOR`, or `vim`, in that order,
  matching git, which I think is what most people will expect.

- Added a cute `tmptree!{}` macro, for creating temporary directories
  populated with directories and files for tests.

- Admitted that grammer is LL(k) and I don't know what `k` is.
2019-11-09 21:43:20 -08:00

224 lines
5.8 KiB
Rust

use crate::common::*;
const FILENAME: &str = "justfile";
pub(crate) struct Search {
pub(crate) justfile: PathBuf,
pub(crate) working_directory: PathBuf,
}
impl Search {
pub(crate) fn search(
search_config: &SearchConfig,
invocation_directory: &Path,
) -> SearchResult<Search> {
match search_config {
SearchConfig::FromInvocationDirectory => {
let justfile = Self::justfile(&invocation_directory)?;
let working_directory = Self::working_directory_from_justfile(&justfile)?;
Ok(Search {
justfile,
working_directory,
})
}
SearchConfig::FromSearchDirectory { search_directory } => {
let justfile = Self::justfile(search_directory)?;
let working_directory = Self::working_directory_from_justfile(&justfile)?;
Ok(Search {
justfile,
working_directory,
})
}
SearchConfig::WithJustfile { justfile } => {
let justfile: PathBuf = justfile.to_path_buf();
let working_directory = Self::working_directory_from_justfile(&justfile)?;
Ok(Search {
justfile,
working_directory,
})
}
SearchConfig::WithJustfileAndWorkingDirectory {
justfile,
working_directory,
} => Ok(Search {
justfile: justfile.to_path_buf(),
working_directory: working_directory.to_path_buf(),
}),
}
}
fn justfile(directory: &Path) -> SearchResult<PathBuf> {
let mut candidates = Vec::new();
let entries = fs::read_dir(directory).map_err(|io_error| SearchError::Io {
io_error,
directory: directory.to_owned(),
})?;
for entry in entries {
let entry = entry.map_err(|io_error| SearchError::Io {
io_error,
directory: directory.to_owned(),
})?;
if let Some(name) = entry.file_name().to_str() {
if name.eq_ignore_ascii_case(FILENAME) {
candidates.push(entry.path());
}
}
}
if candidates.len() == 1 {
Ok(candidates.pop().unwrap())
} else if candidates.len() > 1 {
Err(SearchError::MultipleCandidates { candidates })
} else if let Some(parent) = directory.parent() {
Self::justfile(parent)
} else {
Err(SearchError::NotFound)
}
}
fn working_directory_from_justfile(justfile: &Path) -> SearchResult<PathBuf> {
let justfile_canonical = justfile
.canonicalize()
.context(search_error::Canonicalize { path: justfile })?;
Ok(
justfile_canonical
.parent()
.ok_or_else(|| SearchError::JustfileHadNoParent {
path: justfile_canonical.clone(),
})?
.to_owned(),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn not_found() {
let tmp = testing::tempdir();
match Search::justfile(tmp.path()) {
Err(SearchError::NotFound) => {
assert!(true);
}
_ => panic!("No justfile found error was expected"),
}
}
#[test]
fn multiple_candidates() {
let tmp = testing::tempdir();
let mut path = tmp.path().to_path_buf();
path.push(FILENAME);
fs::write(&path, "default:\n\techo ok").unwrap();
path.pop();
path.push(FILENAME.to_uppercase());
if let Ok(_) = fs::File::open(path.as_path()) {
// We are in case-insensitive file system
return;
}
fs::write(&path, "default:\n\techo ok").unwrap();
path.pop();
match Search::justfile(path.as_path()) {
Err(SearchError::MultipleCandidates { .. }) => {
assert!(true);
}
_ => panic!("Multiple candidates error was expected"),
}
}
#[test]
fn found() {
let tmp = testing::tempdir();
let mut path = tmp.path().to_path_buf();
path.push(FILENAME);
fs::write(&path, "default:\n\techo ok").unwrap();
path.pop();
match Search::justfile(path.as_path()) {
Ok(_path) => {
assert!(true);
}
_ => panic!("No errors were expected"),
}
}
#[test]
fn found_spongebob_case() {
let tmp = testing::tempdir();
let mut path = tmp.path().to_path_buf();
let spongebob_case = FILENAME
.chars()
.enumerate()
.map(|(i, c)| {
if i % 2 == 0 {
c.to_ascii_uppercase()
} else {
c
}
})
.collect::<String>();
path.push(spongebob_case);
fs::write(&path, "default:\n\techo ok").unwrap();
path.pop();
match Search::justfile(path.as_path()) {
Ok(_path) => {
assert!(true);
}
_ => panic!("No errors were expected"),
}
}
#[test]
fn found_from_inner_dir() {
let tmp = testing::tempdir();
let mut path = tmp.path().to_path_buf();
path.push(FILENAME);
fs::write(&path, "default:\n\techo ok").unwrap();
path.pop();
path.push("a");
fs::create_dir(&path).expect("test justfile search: failed to create intermediary directory");
path.push("b");
fs::create_dir(&path).expect("test justfile search: failed to create intermediary directory");
match Search::justfile(path.as_path()) {
Ok(_path) => {
assert!(true);
}
_ => panic!("No errors were expected"),
}
}
#[test]
fn found_and_stopped_at_first_justfile() {
let tmp = testing::tempdir();
let mut path = tmp.path().to_path_buf();
path.push(FILENAME);
fs::write(&path, "default:\n\techo ok").unwrap();
path.pop();
path.push("a");
fs::create_dir(&path).expect("test justfile search: failed to create intermediary directory");
path.push(FILENAME);
fs::write(&path, "default:\n\techo ok").unwrap();
path.pop();
path.push("b");
fs::create_dir(&path).expect("test justfile search: failed to create intermediary directory");
match Search::justfile(path.as_path()) {
Ok(found_path) => {
path.pop();
path.push(FILENAME);
assert_eq!(found_path, path);
}
_ => panic!("No errors were expected"),
}
}
}