Ignore file name case while searching for justfile (#436)

This commit is contained in:
Rostyslav Shevtsiv 2019-06-02 08:38:03 +03:00 committed by Casey Rodarmor
parent 24311b7c0b
commit 7f06bc68d4
8 changed files with 304 additions and 103 deletions

View File

@ -11,7 +11,7 @@ image:https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg[say thanks,link=htt
(非官方中文文档,link:https://github.com/chinanf-boy/just-zh[这里],快看过来!) (非官方中文文档,link:https://github.com/chinanf-boy/just-zh[这里],快看过来!)
Commands are stored in a file called `justfile` or `Justfile` with syntax inspired by `make`: Commands are stored in a file called `justfile` with syntax inspired by `make`:
```make ```make
build: build:
@ -126,7 +126,9 @@ another-recipe:
@echo 'This is another recipe.' @echo 'This is another recipe.'
``` ```
When you invoke `just` it looks for a `justfile` in the current directory and upwards, so you can invoke it from any subdirectory of your project. When you invoke `just` it looks for file `justfile` in the current directory and upwards, so you can invoke it from any subdirectory of your project.
The search for a `justfile` is case insensitive, so any case, like `Justfile`, `JUSTFILE`, or `JuStFiLe`, will work.
Running `just` with no arguments runs the first recipe in the `justfile`: Running `just` with no arguments runs the first recipe in the `justfile`:
@ -744,7 +746,7 @@ if exists("did_load_filetypes")
endif endif
augroup filetypedetect augroup filetypedetect
au BufNewFile,BufRead Justfile,justfile setf make au BufNewFile,BufRead justfile setf make
augroup END augroup END
``` ```

View File

@ -20,41 +20,27 @@ pub(crate) use log::warn;
pub(crate) use tempdir::TempDir; pub(crate) use tempdir::TempDir;
pub(crate) use unicode_width::UnicodeWidthChar; pub(crate) use unicode_width::UnicodeWidthChar;
// Modules
pub(crate) use crate::search;
// Functions
pub(crate) use crate::{ pub(crate) use crate::{
alias::Alias,
alias_resolver::AliasResolver,
assignment_evaluator::AssignmentEvaluator,
assignment_resolver::AssignmentResolver,
color::Color,
compilation_error::CompilationError,
compilation_error_kind::CompilationErrorKind,
configuration::Configuration,
expression::Expression,
fragment::Fragment,
function::Function,
function_context::FunctionContext,
functions::Functions,
interrupt_guard::InterruptGuard,
interrupt_handler::InterruptHandler,
justfile::Justfile,
lexer::Lexer,
load_dotenv::load_dotenv, load_dotenv::load_dotenv,
misc::{default, empty}, misc::{default, empty},
parameter::Parameter, };
parser::Parser,
position::Position, // Structs and enums
recipe::Recipe, pub(crate) use crate::{
recipe_context::RecipeContext, alias::Alias, alias_resolver::AliasResolver, assignment_evaluator::AssignmentEvaluator,
recipe_resolver::RecipeResolver, assignment_resolver::AssignmentResolver, color::Color, compilation_error::CompilationError,
runtime_error::RuntimeError, compilation_error_kind::CompilationErrorKind, configuration::Configuration,
shebang::Shebang, expression::Expression, fragment::Fragment, function::Function,
state::State, function_context::FunctionContext, functions::Functions, interrupt_guard::InterruptGuard,
string_literal::StringLiteral, interrupt_handler::InterruptHandler, justfile::Justfile, lexer::Lexer, parameter::Parameter,
token::Token, parser::Parser, position::Position, recipe::Recipe, recipe_context::RecipeContext,
token_kind::TokenKind, recipe_resolver::RecipeResolver, runtime_error::RuntimeError, search_error::SearchError,
use_color::UseColor, shebang::Shebang, state::State, string_literal::StringLiteral, token::Token,
variables::Variables, token_kind::TokenKind, use_color::UseColor, variables::Variables, verbosity::Verbosity,
verbosity::Verbosity,
}; };
pub type CompilationResult<'a, T> = Result<T, CompilationError<'a>>; pub type CompilationResult<'a, T> = Result<T, CompilationError<'a>>;

View File

@ -43,6 +43,8 @@ mod recipe_context;
mod recipe_resolver; mod recipe_resolver;
mod run; mod run;
mod runtime_error; mod runtime_error;
mod search;
mod search_error;
mod shebang; mod shebang;
mod state; mod state;
mod string_literal; mod string_literal;

View File

@ -273,44 +273,30 @@ pub fn run() {
); );
} }
} else { } else {
let name; let current_dir = match env::current_dir() {
'outer: loop { Ok(current_dir) => current_dir,
for candidate in &["justfile", "Justfile"] { Err(io_error) => die!("Error getting current dir: {}", io_error),
match fs::metadata(candidate) { };
Ok(metadata) => { match search::justfile(&current_dir) {
if metadata.is_file() { Ok(name) => {
name = *candidate; if matches.is_present("EDIT") {
break 'outer; edit(name);
} }
} text = fs::read_to_string(&name)
Err(error) => { .unwrap_or_else(|error| die!("Error reading justfile: {}", error));
if error.kind() != io::ErrorKind::NotFound {
die!("Error fetching justfile metadata: {}", error) let parent = name.parent().unwrap();
}
} if let Err(error) = env::set_current_dir(&parent) {
die!(
"Error changing directory to {}: {}",
parent.display(),
error
);
} }
} }
Err(search_error) => die!("{}", search_error),
match env::current_dir() {
Ok(pathbuf) => {
if pathbuf.as_os_str() == "/" {
die!("No justfile found.");
}
}
Err(error) => die!("Error getting current dir: {}", error),
}
if let Err(error) = env::set_current_dir("..") {
die!("Error changing directory: {}", error);
}
} }
if matches.is_present("EDIT") {
edit(name);
}
text =
fs::read_to_string(name).unwrap_or_else(|error| die!("Error reading justfile: {}", error));
} }
let justfile = Parser::parse(&text).unwrap_or_else(|error| { let justfile = Parser::parse(&text).unwrap_or_else(|error| {

163
src/search.rs Normal file
View File

@ -0,0 +1,163 @@
use crate::common::*;
use std::fs;
use std::path::{Path, PathBuf};
const FILENAME: &str = "justfile";
pub fn justfile(directory: &Path) -> Result<PathBuf, SearchError> {
let mut candidates = Vec::new();
let dir = fs::read_dir(directory).map_err(|io_error| SearchError::Io {
io_error,
directory: directory.to_owned(),
})?;
for entry in dir {
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_dir) = directory.parent() {
justfile(parent_dir)
} else {
Err(SearchError::NotFound)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempdir::TempDir;
#[test]
fn not_found() {
let tmp = TempDir::new("just-test-justfile-search")
.expect("test justfile search: failed to create temporary directory");
match search::justfile(tmp.path()) {
Err(SearchError::NotFound) => {
assert!(true);
}
_ => panic!("No justfile found error was expected"),
}
}
#[test]
fn multiple_candidates() {
let tmp = TempDir::new("just-test-justfile-search")
.expect("test justfile search: failed to create temporary directory");
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 = TempDir::new("just-test-justfile-search")
.expect("test justfile search: failed to create temporary directory");
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 = TempDir::new("just-test-justfile-search")
.expect("test justfile search: failed to create temporary directory");
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 = TempDir::new("just-test-justfile-search")
.expect("test justfile search: failed to create temporary directory");
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 = TempDir::new("just-test-justfile-search")
.expect("test justfile search: failed to create temporary directory");
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"),
}
}
}

62
src/search_error.rs Normal file
View File

@ -0,0 +1,62 @@
use std::{fmt, io, path::PathBuf};
use crate::misc::And;
pub enum SearchError {
MultipleCandidates {
candidates: Vec<PathBuf>,
},
Io {
directory: PathBuf,
io_error: io::Error,
},
NotFound,
}
impl fmt::Display for SearchError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
SearchError::Io {
directory,
io_error,
} => write!(
f,
"I/O error reading directory `{}`: {}",
directory.display(),
io_error
),
SearchError::MultipleCandidates { candidates } => write!(
f,
"Multiple candidate justfiles found in `{}`: {}",
candidates[0].parent().unwrap().display(),
And(
&candidates
.iter()
.map(|candidate| format!("`{}`", candidate.file_name().unwrap().to_string_lossy()))
.collect::<Vec<String>>()
),
),
SearchError::NotFound => write!(f, "No justfile found"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn multiple_candidates_formatting() {
let error = SearchError::MultipleCandidates {
candidates: vec![
PathBuf::from("/foo/justfile"),
PathBuf::from("/foo/JUSTFILE"),
],
};
assert_eq!(
error.to_string(),
"Multiple candidate justfiles found in `/foo`: `justfile` and `JUSTFILE`"
)
}
}

View File

@ -62,38 +62,6 @@ fn test_capitalized_justfile_search() {
search_test(path, &[]); search_test(path, &[]);
} }
#[test]
fn test_capitalization_priority() {
let tmp = TempDir::new("just-test-justfile-search")
.expect("test justfile search: failed to create temporary directory");
let mut path = tmp.path().to_path_buf();
path.push("justfile");
fs::write(&path, "default:\n\techo ok").unwrap();
path.pop();
path.push("Justfile");
fs::write(&path, "default:\n\techo fail").unwrap();
path.pop();
// if we see "default\n\techo fail" in `justfile` then we're running
// in a case insensitive filesystem, so just bail
path.push("justfile");
if fs::read_to_string(&path).unwrap() == "default:\n\techo fail" {
return;
}
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");
path.push("c");
fs::create_dir(&path).expect("test justfile search: failed to create intermediary directory");
path.push("d");
fs::create_dir(&path).expect("test justfile search: failed to create intermediary directory");
search_test(path, &[]);
}
#[test] #[test]
fn test_upwards_path_argument() { fn test_upwards_path_argument() {
let tmp = TempDir::new("just-test-justfile-search") let tmp = TempDir::new("just-test-justfile-search")

View File

@ -5,7 +5,7 @@ use tempdir::TempDir;
/// Test that just runs with the correct working directory when invoked with /// Test that just runs with the correct working directory when invoked with
/// `--justfile` but not `--working-directory` /// `--justfile` but not `--working-directory`
#[test] #[test]
fn justfile_without_working_directory() -> Result<(), Box<Error>> { fn justfile_without_working_directory() -> Result<(), Box<dyn Error>> {
let tmp = TempDir::new("just-integration")?; let tmp = TempDir::new("just-integration")?;
let justfile = tmp.path().join("justfile"); let justfile = tmp.path().join("justfile");
let data = tmp.path().join("data"); let data = tmp.path().join("data");
@ -30,3 +30,35 @@ fn justfile_without_working_directory() -> Result<(), Box<Error>> {
Ok(()) Ok(())
} }
/// Test that just invokes commands from the directory in which the justfile is found
#[test]
fn change_working_directory_to_justfile_parent() -> Result<(), Box<dyn Error>> {
let tmp = TempDir::new("just-integration")?;
let justfile = tmp.path().join("justfile");
fs::write(
&justfile,
"foo = `cat data`\ndefault:\n echo {{foo}}\n cat data",
)?;
let data = tmp.path().join("data");
fs::write(&data, "found it")?;
let subdir = tmp.path().join("subdir");
fs::create_dir(&subdir)?;
let output = Command::new(executable_path("just"))
.current_dir(subdir)
.output()?;
if !output.status.success() {
panic!("just invocation failed: {}", output.status)
}
let stdout = String::from_utf8(output.stdout).unwrap();
assert_eq!(stdout, "found it\nfound it");
Ok(())
}