From ce5b739316d9c81cc41e220bc8d415e866571bda Mon Sep 17 00:00:00 2001 From: wuyangfan Date: Tue, 26 May 2026 11:59:22 +0800 Subject: [PATCH 1/2] feat: respect .gitignore in Jujutsu (.jj) repositories Jujutsu repos use a `.jj` directory instead of `.git` but still rely on `.gitignore` files. Automatically disable require_git when searching inside such repositories, matching the behavior of --no-require-git. Fixes #1817 Co-authored-by: Cursor --- src/main.rs | 15 +++++++++++---- src/walk.rs | 33 ++++++++++++++++++++++++++++++++- tests/tests.rs | 19 +++++++++++++++++++ 3 files changed, 62 insertions(+), 5 deletions(-) diff --git a/src/main.rs b/src/main.rs index f6c0d7e4f..a05475ccb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,7 +15,7 @@ mod walk; use std::env; use std::io::IsTerminal; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::sync::Arc; use anyhow::{Context, Result, anyhow, bail}; @@ -98,7 +98,7 @@ fn run() -> Result { .map(|pat| build_pattern_regex(pat, &opts)) .collect::>>()?; - let config = construct_config(opts, &pattern_regexps)?; + let config = construct_config(opts, &pattern_regexps, &search_paths)?; ensure_use_hidden_option_for_leading_dot_pattern(&config, &pattern_regexps)?; @@ -227,7 +227,11 @@ fn check_path_separator_length(path_separator: Option<&str>) -> Result<()> { } } -fn construct_config(mut opts: Opts, pattern_regexps: &[String]) -> Result { +fn construct_config( + mut opts: Opts, + pattern_regexps: &[String], + search_paths: &[PathBuf], +) -> Result { // The search will be case-sensitive if the command line flag is set or // if any of the patterns has an uppercase character (smart case). let case_sensitive = !opts.ignore_case @@ -295,7 +299,10 @@ fn construct_config(mut opts: Opts, pattern_regexps: &[String]) -> Result, config: Config) -> Result bool { + if no_require_git { + return false; + } + + !search_paths + .iter() + .any(|path| path.ancestors().any(|ancestor| ancestor.join(".jj").is_dir())) +} + #[cfg(test)] mod tests { - use super::search_str_for_entry; + use super::{search_str_for_entry, should_require_git_to_read_vcsignore}; use std::path::{Path, PathBuf}; #[test] @@ -735,4 +753,17 @@ mod tests { PathBuf::from("bar") ); } + + #[test] + fn should_require_git_in_jujutsu_repo() { + let temp = tempfile::tempdir().unwrap(); + let root = temp.path(); + std::fs::create_dir(root.join(".jj")).unwrap(); + + assert!(!should_require_git_to_read_vcsignore( + &[root.join("src")], + false + )); + assert!(!should_require_git_to_read_vcsignore(&[root.join("src")], true)); + } } diff --git a/tests/tests.rs b/tests/tests.rs index aa0919ca0..cf41ce589 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -851,6 +851,25 @@ fn test_custom_ignore_precedence() { te.assert_output(&["--no-ignore", "foo"], "inner/foo"); } +/// Respect .gitignore in Jujutsu repositories with a `.jj` directory (fixes #1817) +#[test] +fn test_jujutsu_repo_respects_gitignore() { + let te = TestEnv::new(DEFAULT_DIRS, DEFAULT_FILES); + + fs::remove_dir(te.test_root().join(".git")).unwrap(); + fs::create_dir(te.test_root().join(".jj")).unwrap(); + + te.assert_output( + &["foo"], + "a.foo + one/b.foo + one/two/c.foo + one/two/C.Foo2 + one/two/three/d.foo + one/two/three/directory_foo/", + ); +} + /// Don't require git to respect gitignore (--no-require-git) #[test] fn test_respect_ignore_files() { From 3267318747440d47ed21bc20f8d5081adf949039 Mon Sep 17 00:00:00 2001 From: wuyangfan Date: Mon, 8 Jun 2026 12:44:45 +0800 Subject: [PATCH 2/2] fix: scope jj gitignore detection to search paths --- src/walk.rs | 49 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/src/walk.rs b/src/walk.rs index c520e1755..b319d3022 100644 --- a/src/walk.rs +++ b/src/walk.rs @@ -1,8 +1,9 @@ use std::borrow::Cow; +use std::env; use std::ffi::OsStr; use std::io::{self, Write}; use std::mem; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex, MutexGuard}; use std::thread; @@ -709,14 +710,27 @@ pub fn should_require_git_to_read_vcsignore( return false; } + let current_dir = env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); !search_paths .iter() - .any(|path| path.ancestors().any(|ancestor| ancestor.join(".jj").is_dir())) + .all(|path| has_jj_ancestor(path, ¤t_dir)) +} + +fn has_jj_ancestor(path: &Path, current_dir: &Path) -> bool { + let search_path = if path.is_absolute() { + Cow::Borrowed(path) + } else { + Cow::Owned(current_dir.join(path)) + }; + + search_path + .ancestors() + .any(|ancestor| ancestor.join(".jj").is_dir()) } #[cfg(test)] mod tests { - use super::{search_str_for_entry, should_require_git_to_read_vcsignore}; + use super::{has_jj_ancestor, search_str_for_entry, should_require_git_to_read_vcsignore}; use std::path::{Path, PathBuf}; #[test] @@ -764,6 +778,33 @@ mod tests { &[root.join("src")], false )); - assert!(!should_require_git_to_read_vcsignore(&[root.join("src")], true)); + assert!(!should_require_git_to_read_vcsignore( + &[root.join("src")], + true + )); + } + + #[test] + fn should_require_git_for_mixed_jujutsu_and_non_jujutsu_paths() { + let jujutsu_temp = tempfile::tempdir().unwrap(); + let normal_temp = tempfile::tempdir().unwrap(); + std::fs::create_dir(jujutsu_temp.path().join(".jj")).unwrap(); + + assert!(should_require_git_to_read_vcsignore( + &[ + jujutsu_temp.path().join("src"), + normal_temp.path().join("src") + ], + false + )); + } + + #[test] + fn detects_jujutsu_repo_for_relative_search_path() { + let temp = tempfile::tempdir().unwrap(); + let root = temp.path(); + std::fs::create_dir(root.join(".jj")).unwrap(); + + assert!(has_jj_ancestor(Path::new("src"), root)); } }