Repository: casey/just
Pull request #2802 (2025-07-01 20:10:28)

Allow global justfile filename to be case insensitive


Patch:
diff --git a/Cargo.toml b/Cargo.toml
index 9e236fe6..35e4f1a8 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -73,6 +73,7 @@ unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing)'] }
 all = { level = "deny", priority = -1 }
 arbitrary-source-item-ordering = "deny"
 enum_glob_use = "allow"
+ignore_without_reason = "allow"
 needless_pass_by_value = "allow"
 pedantic = { level = "deny", priority = -1 }
 similar_names = "allow"
diff --git a/src/search.rs b/src/search.rs
index 09d8f1a5..b93d6087 100644
--- a/src/search.rs
+++ b/src/search.rs
@@ -11,23 +11,18 @@ pub(crate) struct Search {
 }
 
 impl Search {
-  fn global_justfile_paths() -> Vec<PathBuf> {
+  fn global_justfile_paths() -> Vec<(PathBuf, &'static str)> {
     let mut paths = Vec::new();
 
     if let Some(config_dir) = dirs::config_dir() {
-      paths.push(config_dir.join("just").join(DEFAULT_JUSTFILE_NAME));
+      paths.push((config_dir.join("just"), DEFAULT_JUSTFILE_NAME));
     }
 
     if let Some(home_dir) = dirs::home_dir() {
-      paths.push(
-        home_dir
-          .join(".config")
-          .join("just")
-          .join(DEFAULT_JUSTFILE_NAME),
-      );
+      paths.push((home_dir.join(".config").join("just"), DEFAULT_JUSTFILE_NAME));
 
       for justfile_name in JUSTFILE_NAMES {
-        paths.push(home_dir.join(justfile_name));
+        paths.push((home_dir.clone(), justfile_name));
       }
     }
 
@@ -51,11 +46,7 @@ impl Search {
         })
       }
       SearchConfig::GlobalJustfile => Ok(Self {
-        justfile: Self::global_justfile_paths()
-          .iter()
-          .find(|path| path.exists())
-          .cloned()
-          .ok_or(SearchError::GlobalJustfileNotFound)?,
+        justfile: Self::find_global_justfile()?,
         working_directory: Self::project_root(invocation_directory)?,
       }),
       SearchConfig::WithJustfile { justfile } => {
@@ -76,6 +67,26 @@ impl Search {
     }
   }
 
+  fn find_global_justfile() -> SearchResult<PathBuf> {
+    for (directory, filename) in Self::global_justfile_paths() {
+      if let Ok(read_dir) = fs::read_dir(&directory) {
+        for entry in read_dir {
+          let entry = entry.map_err(|io_error| SearchError::Io {
+            io_error,
+            directory: directory.clone(),
+          })?;
+          if let Some(candidate) = entry.file_name().to_str() {
+            if candidate.eq_ignore_ascii_case(filename) {
+              return Ok(entry.path());
+            }
+          }
+        }
+      }
+    }
+
+    Err(SearchError::GlobalJustfileNotFound)
+  }
+
   /// Find justfile starting from parent directory of current justfile
   pub(crate) fn search_parent_directory(&self) -> SearchResult<Self> {
     let parent = self
@@ -150,6 +161,7 @@ impl Search {
         io_error,
         directory: directory.to_owned(),
       })?;
+
       for entry in entries {
         let entry = entry.map_err(|io_error| SearchError::Io {
           io_error,


Test patch:
diff --git a/tests/global.rs b/tests/global.rs
index b6e7caf1..58d2e542 100644
--- a/tests/global.rs
+++ b/tests/global.rs
@@ -63,3 +63,20 @@ fn unix() {
     .stdout("bar\n")
     .run();
 }
+
+#[test]
+#[cfg(all(unix, not(target_os = "macos")))]
+fn case_insensitive() {
+  let tempdir = tempdir();
+
+  let path = tempdir.path().to_owned();
+
+  Test::with_tempdir(tempdir)
+    .no_justfile()
+    .test_round_trip(false)
+    .write("just/JUSTFILE", "@default:\n  echo foo")
+    .env("XDG_CONFIG_HOME", path.to_str().unwrap())
+    .args(["--global-justfile"])
+    .stdout("foo\n")
+    .run();
+}
diff --git a/tests/signals.rs b/tests/signals.rs
index 74ed9920..4b3c27a8 100644
--- a/tests/signals.rs
+++ b/tests/signals.rs
@@ -42,7 +42,7 @@ fn interrupt_test(arguments: &[&str], justfile: &str) {
 }
 
 #[test]
-#[ignore = "Interrupt tests are flaky."]
+#[ignore]
 fn interrupt_shebang() {
   interrupt_test(
     &[],
@@ -55,7 +55,7 @@ fn interrupt_shebang() {
 }
 
 #[test]
-#[ignore = "Interrupt tests are flaky."]
+#[ignore]
 fn interrupt_line() {
   interrupt_test(
     &[],
@@ -67,7 +67,7 @@ fn interrupt_line() {
 }
 
 #[test]
-#[ignore = "Interrupt tests are flaky."]
+#[ignore]
 fn interrupt_backtick() {
   interrupt_test(
     &[],
@@ -81,15 +81,16 @@ fn interrupt_backtick() {
 }
 
 #[test]
-#[ignore = "Interrupt tests are flaky."]
+#[ignore]
 fn interrupt_command() {
   interrupt_test(&["--command", "sleep", "1"], "");
 }
 
+// This test is ignored because it is sensitive to the process signal mask.
+// Programs like `watchexec` and `cargo-watch` change the signal mask to ignore
+// `SIGHUP`, which causes this test to fail.
 #[test]
-#[ignore = "This test is sensitive to the process signal mask. Programs like \
-            `watchexec` and `cargo-watch` change the signal mask to ignore \
-            `SIGHUP`, which causes this test to fail."]
+#[ignore]
 fn forwarding() {
   let just = executable_path("just");
 
@@ -155,7 +156,7 @@ fn forwarding() {
 }
 
 #[test]
-#[ignore = "Includes a 500ms wait is often flakey."]
+#[ignore]
 #[cfg(any(
   target_os = "dragonfly",
   target_os = "freebsd",