Detailed changes
@@ -25578,6 +25578,7 @@ async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestApp
ˇ log('for else')
"});
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
+ cx.wait_for_autoindent_applied().await;
cx.assert_editor_state(indoc! {"
def main():
ˇfor item in items:
@@ -25597,6 +25598,7 @@ async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestApp
// test relative indent is preserved when tab
// for `if`, `elif`, `else`, `while`, `with` and `for`
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
+ cx.wait_for_autoindent_applied().await;
cx.assert_editor_state(indoc! {"
def main():
ˇfor item in items:
@@ -25630,6 +25632,7 @@ async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestApp
ˇ return 0
"});
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
+ cx.wait_for_autoindent_applied().await;
cx.assert_editor_state(indoc! {"
def main():
ˇtry:
@@ -25646,6 +25649,7 @@ async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestApp
// test relative indent is preserved when tab
// for `try`, `except`, `else`, `finally`, `match` and `def`
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
+ cx.wait_for_autoindent_applied().await;
cx.assert_editor_state(indoc! {"
def main():
ˇtry:
@@ -25679,6 +25683,7 @@ async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
cx.update_editor(|editor, window, cx| {
editor.handle_input("else:", window, cx);
});
+ cx.wait_for_autoindent_applied().await;
cx.assert_editor_state(indoc! {"
def main():
if i == 2:
@@ -25696,6 +25701,7 @@ async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
cx.update_editor(|editor, window, cx| {
editor.handle_input("except:", window, cx);
});
+ cx.wait_for_autoindent_applied().await;
cx.assert_editor_state(indoc! {"
def main():
try:
@@ -25715,6 +25721,7 @@ async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
cx.update_editor(|editor, window, cx| {
editor.handle_input("else:", window, cx);
});
+ cx.wait_for_autoindent_applied().await;
cx.assert_editor_state(indoc! {"
def main():
try:
@@ -25738,6 +25745,7 @@ async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
cx.update_editor(|editor, window, cx| {
editor.handle_input("finally:", window, cx);
});
+ cx.wait_for_autoindent_applied().await;
cx.assert_editor_state(indoc! {"
def main():
try:
@@ -25762,6 +25770,7 @@ async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
cx.update_editor(|editor, window, cx| {
editor.handle_input("else:", window, cx);
});
+ cx.wait_for_autoindent_applied().await;
cx.assert_editor_state(indoc! {"
def main():
try:
@@ -25787,6 +25796,7 @@ async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
cx.update_editor(|editor, window, cx| {
editor.handle_input("finally:", window, cx);
});
+ cx.wait_for_autoindent_applied().await;
cx.assert_editor_state(indoc! {"
def main():
try:
@@ -25812,6 +25822,7 @@ async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
cx.update_editor(|editor, window, cx| {
editor.handle_input("except:", window, cx);
});
+ cx.wait_for_autoindent_applied().await;
cx.assert_editor_state(indoc! {"
def main():
try:
@@ -25835,6 +25846,7 @@ async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
cx.update_editor(|editor, window, cx| {
editor.handle_input("except:", window, cx);
});
+ cx.wait_for_autoindent_applied().await;
cx.assert_editor_state(indoc! {"
def main():
try:
@@ -25856,6 +25868,7 @@ async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
cx.update_editor(|editor, window, cx| {
editor.handle_input("else:", window, cx);
});
+ cx.wait_for_autoindent_applied().await;
cx.assert_editor_state(indoc! {"
def main():
for i in range(10):
@@ -25872,6 +25885,7 @@ async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
cx.update_editor(|editor, window, cx| {
editor.handle_input("a", window, cx);
});
+ cx.wait_for_autoindent_applied().await;
cx.assert_editor_state(indoc! {"
def f() -> list[str]:
aˇ
@@ -25885,6 +25899,7 @@ async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
cx.update_editor(|editor, window, cx| {
editor.handle_input(":", window, cx);
});
+ cx.wait_for_autoindent_applied().await;
cx.assert_editor_state(indoc! {"
match 1:
case:ˇ
@@ -25908,6 +25923,7 @@ async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
cx.update_editor(|editor, window, cx| {
editor.newline(&Newline, window, cx);
});
+ cx.wait_for_autoindent_applied().await;
cx.assert_editor_state(indoc! {"
# COMMENT:
ˇ
@@ -25920,7 +25936,7 @@ async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
cx.update_editor(|editor, window, cx| {
editor.newline(&Newline, window, cx);
});
- cx.run_until_parked();
+ cx.wait_for_autoindent_applied().await;
cx.assert_editor_state(indoc! {"
{
ˇ
@@ -25980,6 +25996,7 @@ async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppCo
ˇ}
"});
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
+ cx.wait_for_autoindent_applied().await;
cx.assert_editor_state(indoc! {"
function main() {
ˇfor item in $items; do
@@ -25997,6 +26014,7 @@ async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppCo
"});
// test relative indent is preserved when tab
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
+ cx.wait_for_autoindent_applied().await;
cx.assert_editor_state(indoc! {"
function main() {
ˇfor item in $items; do
@@ -26031,6 +26049,7 @@ async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppCo
ˇ}
"});
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
+ cx.wait_for_autoindent_applied().await;
cx.assert_editor_state(indoc! {"
function handle() {
ˇcase \"$1\" in
@@ -26073,6 +26092,7 @@ async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
ˇ}
"});
cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
+ cx.wait_for_autoindent_applied().await;
cx.assert_editor_state(indoc! {"
function main() {
#ˇ for item in $items; do
@@ -26107,6 +26127,7 @@ async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
cx.update_editor(|editor, window, cx| {
editor.handle_input("else", window, cx);
});
+ cx.wait_for_autoindent_applied().await;
cx.assert_editor_state(indoc! {"
if [ \"$1\" = \"test\" ]; then
echo \"foo bar\"
@@ -26122,6 +26143,7 @@ async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
cx.update_editor(|editor, window, cx| {
editor.handle_input("elif", window, cx);
});
+ cx.wait_for_autoindent_applied().await;
cx.assert_editor_state(indoc! {"
if [ \"$1\" = \"test\" ]; then
echo \"foo bar\"
@@ -26139,6 +26161,7 @@ async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
cx.update_editor(|editor, window, cx| {
editor.handle_input("fi", window, cx);
});
+ cx.wait_for_autoindent_applied().await;
cx.assert_editor_state(indoc! {"
if [ \"$1\" = \"test\" ]; then
echo \"foo bar\"
@@ -26156,6 +26179,7 @@ async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
cx.update_editor(|editor, window, cx| {
editor.handle_input("done", window, cx);
});
+ cx.wait_for_autoindent_applied().await;
cx.assert_editor_state(indoc! {"
while read line; do
echo \"$line\"
@@ -26171,6 +26195,7 @@ async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
cx.update_editor(|editor, window, cx| {
editor.handle_input("done", window, cx);
});
+ cx.wait_for_autoindent_applied().await;
cx.assert_editor_state(indoc! {"
for file in *.txt; do
cat \"$file\"
@@ -26191,6 +26216,7 @@ async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
cx.update_editor(|editor, window, cx| {
editor.handle_input("esac", window, cx);
});
+ cx.wait_for_autoindent_applied().await;
cx.assert_editor_state(indoc! {"
case \"$1\" in
start)
@@ -26213,6 +26239,7 @@ async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
cx.update_editor(|editor, window, cx| {
editor.handle_input("*)", window, cx);
});
+ cx.wait_for_autoindent_applied().await;
cx.assert_editor_state(indoc! {"
case \"$1\" in
start)
@@ -26232,6 +26259,7 @@ async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
cx.update_editor(|editor, window, cx| {
editor.handle_input("fi", window, cx);
});
+ cx.wait_for_autoindent_applied().await;
cx.assert_editor_state(indoc! {"
if [ \"$1\" = \"test\" ]; then
echo \"outer if\"
@@ -26258,6 +26286,7 @@ async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
cx.update_editor(|editor, window, cx| {
editor.newline(&Newline, window, cx);
});
+ cx.wait_for_autoindent_applied().await;
cx.assert_editor_state(indoc! {"
# COMMENT:
ˇ
@@ -26271,7 +26300,7 @@ async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
cx.update_editor(|editor, window, cx| {
editor.newline(&Newline, window, cx);
});
- cx.run_until_parked();
+ cx.wait_for_autoindent_applied().await;
cx.assert_editor_state(indoc! {"
if [ \"$1\" = \"test\" ]; then
@@ -26286,7 +26315,7 @@ async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
cx.update_editor(|editor, window, cx| {
editor.newline(&Newline, window, cx);
});
- cx.run_until_parked();
+ cx.wait_for_autoindent_applied().await;
cx.assert_editor_state(indoc! {"
if [ \"$1\" = \"test\" ]; then
else
@@ -26301,7 +26330,7 @@ async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
cx.update_editor(|editor, window, cx| {
editor.newline(&Newline, window, cx);
});
- cx.run_until_parked();
+ cx.wait_for_autoindent_applied().await;
cx.assert_editor_state(indoc! {"
if [ \"$1\" = \"test\" ]; then
elif
@@ -26315,7 +26344,7 @@ async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
cx.update_editor(|editor, window, cx| {
editor.newline(&Newline, window, cx);
});
- cx.run_until_parked();
+ cx.wait_for_autoindent_applied().await;
cx.assert_editor_state(indoc! {"
for file in *.txt; do
ˇ
@@ -26329,7 +26358,7 @@ async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
cx.update_editor(|editor, window, cx| {
editor.newline(&Newline, window, cx);
});
- cx.run_until_parked();
+ cx.wait_for_autoindent_applied().await;
cx.assert_editor_state(indoc! {"
case \"$1\" in
start)
@@ -26346,7 +26375,7 @@ async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
cx.update_editor(|editor, window, cx| {
editor.newline(&Newline, window, cx);
});
- cx.run_until_parked();
+ cx.wait_for_autoindent_applied().await;
cx.assert_editor_state(indoc! {"
case \"$1\" in
start)
@@ -26362,7 +26391,7 @@ async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
cx.update_editor(|editor, window, cx| {
editor.newline(&Newline, window, cx);
});
- cx.run_until_parked();
+ cx.wait_for_autoindent_applied().await;
cx.assert_editor_state(indoc! {"
function test() {
ˇ
@@ -26376,7 +26405,7 @@ async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
cx.update_editor(|editor, window, cx| {
editor.newline(&Newline, window, cx);
});
- cx.run_until_parked();
+ cx.wait_for_autoindent_applied().await;
cx.assert_editor_state(indoc! {"
echo \"test\";
ˇ
@@ -305,6 +305,12 @@ impl EditorTestContext {
snapshot.anchor_before(ranges[0].start)..snapshot.anchor_after(ranges[0].end)
}
+ pub async fn wait_for_autoindent_applied(&mut self) {
+ if let Some(fut) = self.update_buffer(|buffer, _| buffer.wait_for_autoindent_applied()) {
+ fut.await.ok();
+ }
+ }
+
pub fn set_head_text(&mut self, diff_base: &str) {
self.cx.run_until_parked();
let fs =
@@ -23,6 +23,7 @@ pub const FSMONITOR_DAEMON: &str = "fsmonitor--daemon";
pub const LFS_DIR: &str = "lfs";
pub const COMMIT_MESSAGE: &str = "COMMIT_EDITMSG";
pub const INDEX_LOCK: &str = "index.lock";
+pub const REPO_EXCLUDE: &str = "info/exclude";
actions!(
git,
@@ -13,6 +13,10 @@ pub enum IgnoreStackEntry {
Global {
ignore: Arc<Gitignore>,
},
+ RepoExclude {
+ ignore: Arc<Gitignore>,
+ parent: Arc<IgnoreStackEntry>,
+ },
Some {
abs_base_path: Arc<Path>,
ignore: Arc<Gitignore>,
@@ -21,6 +25,12 @@ pub enum IgnoreStackEntry {
All,
}
+#[derive(Debug)]
+pub enum IgnoreKind {
+ Gitignore(Arc<Path>),
+ RepoExclude,
+}
+
impl IgnoreStack {
pub fn none() -> Self {
Self {
@@ -43,13 +53,19 @@ impl IgnoreStack {
}
}
- pub fn append(self, abs_base_path: Arc<Path>, ignore: Arc<Gitignore>) -> Self {
+ pub fn append(self, kind: IgnoreKind, ignore: Arc<Gitignore>) -> Self {
let top = match self.top.as_ref() {
IgnoreStackEntry::All => self.top.clone(),
- _ => Arc::new(IgnoreStackEntry::Some {
- abs_base_path,
- ignore,
- parent: self.top.clone(),
+ _ => Arc::new(match kind {
+ IgnoreKind::Gitignore(abs_base_path) => IgnoreStackEntry::Some {
+ abs_base_path,
+ ignore,
+ parent: self.top.clone(),
+ },
+ IgnoreKind::RepoExclude => IgnoreStackEntry::RepoExclude {
+ ignore,
+ parent: self.top.clone(),
+ },
}),
};
Self {
@@ -84,6 +100,17 @@ impl IgnoreStack {
ignore::Match::Whitelist(_) => false,
}
}
+ IgnoreStackEntry::RepoExclude { ignore, parent } => {
+ match ignore.matched(abs_path, is_dir) {
+ ignore::Match::None => IgnoreStack {
+ repo_root: self.repo_root.clone(),
+ top: parent.clone(),
+ }
+ .is_abs_path_ignored(abs_path, is_dir),
+ ignore::Match::Ignore(_) => true,
+ ignore::Match::Whitelist(_) => false,
+ }
+ }
IgnoreStackEntry::Some {
abs_base_path,
ignore,
@@ -19,7 +19,8 @@ use futures::{
};
use fuzzy::CharBag;
use git::{
- COMMIT_MESSAGE, DOT_GIT, FSMONITOR_DAEMON, GITIGNORE, INDEX_LOCK, LFS_DIR, status::GitSummary,
+ COMMIT_MESSAGE, DOT_GIT, FSMONITOR_DAEMON, GITIGNORE, INDEX_LOCK, LFS_DIR, REPO_EXCLUDE,
+ status::GitSummary,
};
use gpui::{
App, AppContext as _, AsyncApp, BackgroundExecutor, Context, Entity, EventEmitter, Priority,
@@ -71,6 +72,8 @@ use util::{
};
pub use worktree_settings::WorktreeSettings;
+use crate::ignore::IgnoreKind;
+
pub const FS_WATCH_LATENCY: Duration = Duration::from_millis(100);
/// A set of local or remote files that are being opened as part of a project.
@@ -233,6 +236,9 @@ impl Default for WorkDirectory {
pub struct LocalSnapshot {
snapshot: Snapshot,
global_gitignore: Option<Arc<Gitignore>>,
+ /// Exclude files for all git repositories in the worktree, indexed by their absolute path.
+ /// The boolean indicates whether the gitignore needs to be updated.
+ repo_exclude_by_work_dir_abs_path: HashMap<Arc<Path>, (Arc<Gitignore>, bool)>,
/// All of the gitignore files in the worktree, indexed by their absolute path.
/// The boolean indicates whether the gitignore needs to be updated.
ignores_by_parent_abs_path: HashMap<Arc<Path>, (Arc<Gitignore>, bool)>,
@@ -393,6 +399,7 @@ impl Worktree {
let mut snapshot = LocalSnapshot {
ignores_by_parent_abs_path: Default::default(),
global_gitignore: Default::default(),
+ repo_exclude_by_work_dir_abs_path: Default::default(),
git_repositories: Default::default(),
snapshot: Snapshot::new(
cx.entity_id().as_u64(),
@@ -2565,13 +2572,21 @@ impl LocalSnapshot {
} else {
IgnoreStack::none()
};
+
+ if let Some((repo_exclude, _)) = repo_root
+ .as_ref()
+ .and_then(|abs_path| self.repo_exclude_by_work_dir_abs_path.get(abs_path))
+ {
+ ignore_stack = ignore_stack.append(IgnoreKind::RepoExclude, repo_exclude.clone());
+ }
ignore_stack.repo_root = repo_root;
for (parent_abs_path, ignore) in new_ignores.into_iter().rev() {
if ignore_stack.is_abs_path_ignored(parent_abs_path, true) {
ignore_stack = IgnoreStack::all();
break;
} else if let Some(ignore) = ignore {
- ignore_stack = ignore_stack.append(parent_abs_path.into(), ignore);
+ ignore_stack =
+ ignore_stack.append(IgnoreKind::Gitignore(parent_abs_path.into()), ignore);
}
}
@@ -3646,13 +3661,23 @@ impl BackgroundScanner {
let root_abs_path = self.state.lock().await.snapshot.abs_path.clone();
let repo = if self.scanning_enabled {
- let (ignores, repo) = discover_ancestor_git_repo(self.fs.clone(), &root_abs_path).await;
+ let (ignores, exclude, repo) =
+ discover_ancestor_git_repo(self.fs.clone(), &root_abs_path).await;
self.state
.lock()
.await
.snapshot
.ignores_by_parent_abs_path
.extend(ignores);
+ if let Some(exclude) = exclude {
+ self.state
+ .lock()
+ .await
+ .snapshot
+ .repo_exclude_by_work_dir_abs_path
+ .insert(root_abs_path.as_path().into(), (exclude, false));
+ }
+
repo
} else {
None
@@ -3914,6 +3939,7 @@ impl BackgroundScanner {
let mut relative_paths = Vec::with_capacity(abs_paths.len());
let mut dot_git_abs_paths = Vec::new();
+ let mut work_dirs_needing_exclude_update = Vec::new();
abs_paths.sort_unstable();
abs_paths.dedup_by(|a, b| a.starts_with(b));
{
@@ -3987,6 +4013,18 @@ impl BackgroundScanner {
continue;
};
+ let absolute_path = abs_path.to_path_buf();
+ if absolute_path.ends_with(Path::new(DOT_GIT).join(REPO_EXCLUDE)) {
+ if let Some(repository) = snapshot
+ .git_repositories
+ .values()
+ .find(|repo| repo.common_dir_abs_path.join(REPO_EXCLUDE) == absolute_path)
+ {
+ work_dirs_needing_exclude_update
+ .push(repository.work_directory_abs_path.clone());
+ }
+ }
+
if abs_path.file_name() == Some(OsStr::new(GITIGNORE)) {
for (_, repo) in snapshot
.git_repositories
@@ -4032,6 +4070,19 @@ impl BackgroundScanner {
return;
}
+ if !work_dirs_needing_exclude_update.is_empty() {
+ let mut state = self.state.lock().await;
+ for work_dir_abs_path in work_dirs_needing_exclude_update {
+ if let Some((_, needs_update)) = state
+ .snapshot
+ .repo_exclude_by_work_dir_abs_path
+ .get_mut(&work_dir_abs_path)
+ {
+ *needs_update = true;
+ }
+ }
+ }
+
self.state.lock().await.snapshot.scan_id += 1;
let (scan_job_tx, scan_job_rx) = channel::unbounded();
@@ -4299,7 +4350,8 @@ impl BackgroundScanner {
match build_gitignore(&child_abs_path, self.fs.as_ref()).await {
Ok(ignore) => {
let ignore = Arc::new(ignore);
- ignore_stack = ignore_stack.append(job.abs_path.clone(), ignore.clone());
+ ignore_stack = ignore_stack
+ .append(IgnoreKind::Gitignore(job.abs_path.clone()), ignore.clone());
new_ignore = Some(ignore);
}
Err(error) => {
@@ -4561,11 +4613,24 @@ impl BackgroundScanner {
.await;
if path.is_empty()
- && let Some((ignores, repo)) = new_ancestor_repo.take()
+ && let Some((ignores, exclude, repo)) = new_ancestor_repo.take()
{
log::trace!("updating ancestor git repository");
state.snapshot.ignores_by_parent_abs_path.extend(ignores);
if let Some((ancestor_dot_git, work_directory)) = repo {
+ if let Some(exclude) = exclude {
+ let work_directory_abs_path = self
+ .state
+ .lock()
+ .await
+ .snapshot
+ .work_directory_abs_path(&work_directory);
+
+ state
+ .snapshot
+ .repo_exclude_by_work_dir_abs_path
+ .insert(work_directory_abs_path.into(), (exclude, false));
+ }
state
.insert_git_repository_for_path(
work_directory,
@@ -4663,6 +4728,36 @@ impl BackgroundScanner {
{
let snapshot = &mut self.state.lock().await.snapshot;
let abs_path = snapshot.abs_path.clone();
+
+ snapshot.repo_exclude_by_work_dir_abs_path.retain(
+ |work_dir_abs_path, (exclude, needs_update)| {
+ if *needs_update {
+ *needs_update = false;
+ ignores_to_update.push(work_dir_abs_path.clone());
+
+ if let Some((_, repository)) = snapshot
+ .git_repositories
+ .iter()
+ .find(|(_, repo)| &repo.work_directory_abs_path == work_dir_abs_path)
+ {
+ let exclude_abs_path =
+ repository.common_dir_abs_path.join(REPO_EXCLUDE);
+ if let Ok(current_exclude) = self
+ .executor
+ .block(build_gitignore(&exclude_abs_path, self.fs.as_ref()))
+ {
+ *exclude = Arc::new(current_exclude);
+ }
+ }
+ }
+
+ snapshot
+ .git_repositories
+ .iter()
+ .any(|(_, repo)| &repo.work_directory_abs_path == work_dir_abs_path)
+ },
+ );
+
snapshot
.ignores_by_parent_abs_path
.retain(|parent_abs_path, (_, needs_update)| {
@@ -4717,7 +4812,8 @@ impl BackgroundScanner {
let mut ignore_stack = job.ignore_stack;
if let Some((ignore, _)) = snapshot.ignores_by_parent_abs_path.get(&job.abs_path) {
- ignore_stack = ignore_stack.append(job.abs_path.clone(), ignore.clone());
+ ignore_stack =
+ ignore_stack.append(IgnoreKind::Gitignore(job.abs_path.clone()), ignore.clone());
}
let mut entries_by_id_edits = Vec::new();
@@ -4892,6 +4988,9 @@ impl BackgroundScanner {
let preserve = ids_to_preserve.contains(work_directory_id);
if !preserve {
affected_repo_roots.push(entry.dot_git_abs_path.parent().unwrap().into());
+ snapshot
+ .repo_exclude_by_work_dir_abs_path
+ .remove(&entry.work_directory_abs_path);
}
preserve
});
@@ -4931,8 +5030,10 @@ async fn discover_ancestor_git_repo(
root_abs_path: &SanitizedPath,
) -> (
HashMap<Arc<Path>, (Arc<Gitignore>, bool)>,
+ Option<Arc<Gitignore>>,
Option<(PathBuf, WorkDirectory)>,
) {
+ let mut exclude = None;
let mut ignores = HashMap::default();
for (index, ancestor) in root_abs_path.as_path().ancestors().enumerate() {
if index != 0 {
@@ -4968,6 +5069,7 @@ async fn discover_ancestor_git_repo(
// also mark where in the git repo the root folder is located.
return (
ignores,
+ exclude,
Some((
ancestor_dot_git,
WorkDirectory::AboveProject {
@@ -4979,12 +5081,17 @@ async fn discover_ancestor_git_repo(
};
}
+ let repo_exclude_abs_path = ancestor_dot_git.join(REPO_EXCLUDE);
+ if let Ok(repo_exclude) = build_gitignore(&repo_exclude_abs_path, fs.as_ref()).await {
+ exclude = Some(Arc::new(repo_exclude));
+ }
+
// Reached root of git repository.
break;
}
}
- (ignores, None)
+ (ignores, exclude, None)
}
fn build_diff(
@@ -1,7 +1,7 @@
use crate::{Entry, EntryKind, Event, PathChange, Worktree, WorktreeModelHandle};
use anyhow::Result;
use fs::{FakeFs, Fs, RealFs, RemoveOptions};
-use git::GITIGNORE;
+use git::{DOT_GIT, GITIGNORE, REPO_EXCLUDE};
use gpui::{AppContext as _, BackgroundExecutor, BorrowAppContext, Context, Task, TestAppContext};
use parking_lot::Mutex;
use postage::stream::Stream;
@@ -2412,6 +2412,94 @@ async fn test_global_gitignore(executor: BackgroundExecutor, cx: &mut TestAppCon
});
}
+#[gpui::test]
+async fn test_repo_exclude(executor: BackgroundExecutor, cx: &mut TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(executor);
+ let project_dir = Path::new(path!("/project"));
+ fs.insert_tree(
+ project_dir,
+ json!({
+ ".git": {
+ "info": {
+ "exclude": ".env.*"
+ }
+ },
+ ".env.example": "secret=xxxx",
+ ".env.local": "secret=1234",
+ ".gitignore": "!.env.example",
+ "README.md": "# Repo Exclude",
+ "src": {
+ "main.rs": "fn main() {}",
+ },
+ }),
+ )
+ .await;
+
+ let worktree = Worktree::local(
+ project_dir,
+ true,
+ fs.clone(),
+ Default::default(),
+ true,
+ &mut cx.to_async(),
+ )
+ .await
+ .unwrap();
+ worktree
+ .update(cx, |worktree, _| {
+ worktree.as_local().unwrap().scan_complete()
+ })
+ .await;
+ cx.run_until_parked();
+
+ // .gitignore overrides .git/info/exclude
+ worktree.update(cx, |worktree, _cx| {
+ let expected_excluded_paths = [];
+ let expected_ignored_paths = [".env.local"];
+ let expected_tracked_paths = [".env.example", "README.md", "src/main.rs"];
+ let expected_included_paths = [];
+
+ check_worktree_entries(
+ worktree,
+ &expected_excluded_paths,
+ &expected_ignored_paths,
+ &expected_tracked_paths,
+ &expected_included_paths,
+ );
+ });
+
+ // Ignore statuses are updated when .git/info/exclude file changes
+ fs.write(
+ &project_dir.join(DOT_GIT).join(REPO_EXCLUDE),
+ ".env.example".as_bytes(),
+ )
+ .await
+ .unwrap();
+ worktree
+ .update(cx, |worktree, _| {
+ worktree.as_local().unwrap().scan_complete()
+ })
+ .await;
+ cx.run_until_parked();
+
+ worktree.update(cx, |worktree, _cx| {
+ let expected_excluded_paths = [];
+ let expected_ignored_paths = [];
+ let expected_tracked_paths = [".env.example", ".env.local", "README.md", "src/main.rs"];
+ let expected_included_paths = [];
+
+ check_worktree_entries(
+ worktree,
+ &expected_excluded_paths,
+ &expected_ignored_paths,
+ &expected_tracked_paths,
+ &expected_included_paths,
+ );
+ });
+}
+
#[track_caller]
fn check_worktree_entries(
tree: &Worktree,