Skip to content

Override ignore patterns if a file is tracked#53751

Draft
mchisolm0 wants to merge 11 commits intozed-industries:mainfrom
mchisolm0:ignored-vs-tracked-file-coloring
Draft

Override ignore patterns if a file is tracked#53751
mchisolm0 wants to merge 11 commits intozed-industries:mainfrom
mchisolm0:ignored-vs-tracked-file-coloring

Conversation

@mchisolm0
Copy link
Copy Markdown
Collaborator

@mchisolm0 mchisolm0 commented Apr 12, 2026

Current implementation for .gitignore logic fails to respect explicitly tracked files if they match ignore patterns. This is true for repository scoped .gitignore files or one defined globally with core.excludesfile.

The linked issues surfaced the problem when the user tracked a nested file but part of the path matched a .gitignore pattern. This caused the project panel to show the directory/file as ignored even though the file was tracked.

These changes use tracked files to override the list of ignored files/paths.

Self-Review Checklist:

  • I've reviewed my own diff for quality, security, and reliability
  • Unsafe blocks (if any) have justifying comments
  • The content is consistent with the UI/UX checklist
  • Tests cover the new/changed behavior
  • Performance impact has been considered and is acceptable

Closes #51662 and Closes #53208

Release Notes:

  • Fix tracked files showing as ignored for some users

- Introduce tracked_paths on GitRepository and implement for fake and
  real backends
- FakeGitRepository now returns tracked paths from head and index
  contents
- RealGitRepository tracks files by invoking `git ls-files -z --cached`
  and parsing null-delimited output
- Add git_tracked_paths_args helper to build the command
- Fall back to the "git" binary when no system/bundled binary is
  available
- Update tests to reflect tracked paths behavior for nested bin paths
- Introduce tracked_paths on GitRepository and implement for fake and
  real backends
- FakeGitRepository now returns tracked paths from head and index
  contents
- RealGitRepository tracks files by invoking `git ls-files -z --cached`
  and parsing null-delimited output
- Add git_tracked_paths_args helper to build the command
- Fall back to the "git" binary when no system/bundled binary is
  available
…m0/zed into ignored-vs-tracked-file-coloring
@cla-bot cla-bot Bot added the cla-signed The user has signed the Contributor License Agreement label Apr 12, 2026
@zed-community-bot zed-community-bot Bot added the guild Pull requests by someone in Zed Guild. NOTE: the label application is automated via github actions label Apr 12, 2026
@mchisolm0 mchisolm0 changed the title Ignored vs tracked file coloring Override ignore patterns if a file is tracked Apr 12, 2026
@mchisolm0
Copy link
Copy Markdown
Collaborator Author

The test test_tracked_nested_path_is_not_ignored_by_bin_rule already checks the bug found in the linked issue, but the test I added below is actually only somewhat related to the linked issue.

I am removing this test from the current PR since it is actually only somewhat related and currently fails in CI for Windows. The test checks if tracked files refresh correctly when a previously ignored file is added to tracked files.

Test being dropped from this PR
#[gpui::test]
async fn test_tracking_nested_path_updates_ignored_bin_dir(cx: &mut TestAppContext) {
    init_test(cx);

    let fs = FakeFs::new(cx.background_executor.clone());
    fs.insert_tree(
        path!("/root"),
        json!({
            ".git": {},
            ".gitignore": "bin/\n",
            "bin": {
                "ignored-file": "",
            },
            "images": {
                "simple-bridge": {
                    "src": {
                        "rootfs": {
                            "bin": {
                                "simple-bridge": "#!/bin/sh\n",
                            }
                        }
                    }
                }
            }
        }),
    )
    .await;
    fs.set_head_and_index_for_repo(
        path!("/root/.git").as_ref(),
        &[(".gitignore", "bin/\n".into())],
    );

    let worktree = Worktree::local(
        Path::new("/root"),
        true,
        fs.clone(),
        Default::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();
    cx.read(|cx| worktree.read(cx).as_local().unwrap().scan_complete())
        .await;
    worktree.flush_fs_events(cx).await;
    worktree
        .update(cx, |tree, cx| {
            tree.load_file(rel_path("bin/ignored-file"), cx)
        })
        .await
        .unwrap();

    worktree.read_with(cx, |worktree, _| {
        let root_bin = worktree.entry_for_path(rel_path("bin")).unwrap();
        let nested_bin = worktree
            .entry_for_path(rel_path("images/simple-bridge/src/rootfs/bin"))
            .unwrap();
        assert!(root_bin.is_ignored);
        assert!(nested_bin.is_ignored);
        assert!(
            worktree
                .entry_for_path(rel_path(
                    "images/simple-bridge/src/rootfs/bin/simple-bridge"
                ))
                .is_none(),
            "tracked file should not be visible before the repository starts tracking it"
        );
    });

    fs.set_head_and_index_for_repo(
        path!("/root/.git").as_ref(),
        &[
            (".gitignore", "bin/\n".into()),
            (
                "images/simple-bridge/src/rootfs/bin/simple-bridge",
                "#!/bin/sh\n".into(),
            ),
        ],
    );

    worktree.flush_fs_events(cx).await;
    cx.read(|cx| worktree.read(cx).as_local().unwrap().scan_complete())
        .await;
    worktree.flush_fs_events(cx).await;

    worktree.update(cx, |worktree, _cx| {
        check_worktree_entries(
            worktree,
            &[],
            &["bin", "bin/ignored-file"],
            &[
                ".gitignore",
                "images/simple-bridge/src/rootfs/bin",
                "images/simple-bridge/src/rootfs/bin/simple-bridge",
            ],
            &[],
        );
    });
}

I have tested this manually on Linux, and the previously ignored file gets correctly shown as tracked once it is added as a tracked file.

@mchisolm0
Copy link
Copy Markdown
Collaborator Author

mchisolm0 commented Apr 20, 2026

I had also added this test. I am removing it since I feel test_tracked_nested_path_is_not_ignored_by_bin_rule already provides enough protection.

Project Panel Hides Ignored Files Test
#[gpui::test]
async fn test_tracked_nested_bin_path_stays_visible_when_hide_gitignore_is_enabled(
    cx: &mut gpui::TestAppContext,
) {
    init_test(cx);

    let fs = FakeFs::new(cx.background_executor.clone());
    fs.insert_tree(
        "/project_root",
        json!({
            ".git": {},
            ".gitignore": "bin/\n",
            "bin": {
                "ignored-file": "",
            },
            "images": {
                "simple-bridge": {
                    "src": {
                        "rootfs": {
                            "bin": {
                                "simple-bridge": "#!/bin/sh\n",
                            }
                        }
                    }
                }
            },
        }),
    )
    .await;
    fs.set_head_and_index_for_repo(
        path!("/project_root/.git").as_ref(),
        &[
            (".gitignore", "bin/\n".into()),
            (
                "images/simple-bridge/src/rootfs/bin/simple-bridge",
                "#!/bin/sh\n".into(),
            ),
        ],
    );

    let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
    let workspace = window
        .read_with(cx, |mw, _| mw.workspace().clone())
        .unwrap();
    let cx = &mut VisualTestContext::from_window(window.into(), cx);
    cx.update(|_, cx| {
        let settings = *ProjectPanelSettings::get_global(cx);
        ProjectPanelSettings::override_global(
            ProjectPanelSettings {
                hide_gitignore: true,
                ..settings
            },
            cx,
        );
    });

    let panel = workspace.update_in(cx, ProjectPanel::new);
    cx.run_until_parked();

    let initial_entries = visible_entries_as_strings(&panel, 0..20, cx);
    assert!(
        !initial_entries.iter().any(|entry| entry == "    > bin"),
        "root ignored bin directory should stay hidden when hide_gitignore is enabled: {initial_entries:?}"
    );

    for path in [
        "project_root/images",
        "project_root/images/simple-bridge",
        "project_root/images/simple-bridge/src",
        "project_root/images/simple-bridge/src/rootfs",
    ] {
        toggle_expand_dir(&panel, path, cx);
    }

    let expanded_entries = visible_entries_as_strings(&panel, 0..20, cx);
    assert!(
        expanded_entries.iter().any(|entry| entry.trim() == "> bin"),
        "tracked nested bin directory should be visible before it is expanded: {expanded_entries:?}"
    );

    toggle_expand_dir(
        &panel,
        "project_root/images/simple-bridge/src/rootfs/bin",
        cx,
    );

    let expanded_entries = visible_entries_as_strings(&panel, 0..20, cx);
    assert!(
        expanded_entries
            .iter()
            .any(|entry| entry == "                          simple-bridge"),
        "tracked nested file should remain visible in the project panel: {expanded_entries:?}"
    );
}
Project Test Checking Ignore with Real FS
#[gpui::test]
async fn test_tracked_nested_bin_path_is_not_ignored_with_real_fs(cx: &mut gpui::TestAppContext) {
    init_test(cx);
    cx.executor().allow_parking();

    let root = TempTree::new(json!({
        ".gitignore": "bin/\n",
        "bin": {
            "ignored-root-file": "ignored"
        },
        "images": {
            "simple-bridge": {
                "src": {
                    "rootfs": {
                        "bin": {
                            "simple-bridge": "tracked"
                        }
                    }
                }
            }
        }
    }));
    let work_dir = root.path();

    let repo = git_init(work_dir);
    git_add(".gitignore", &repo);
    git_add("images/simple-bridge/src/rootfs/bin/simple-bridge", &repo);
    git_commit("Initial commit", &repo);

    let project = Project::test(Arc::new(RealFs::new(None, cx.executor())), [work_dir], cx).await;

    let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
    tree.flush_fs_events(cx).await;
    project
        .update(cx, |project, cx| project.git_scans_complete(cx))
        .await;
    cx.executor().run_until_parked();

    let repository = project.read_with(cx, |project, cx| {
        project.repositories(cx).values().next().unwrap().clone()
    });

    cx.read(|cx| {
        assert_entry_git_state(
            tree.read(cx),
            repository.read(cx),
            "images/simple-bridge/src/rootfs/bin/simple-bridge",
            None,
            false,
        );
        assert!(
            !tree
                .read(cx)
                .entry_for_path(&rel_path("images/simple-bridge/src/rootfs/bin"))
                .unwrap()
                .is_ignored,
            "tracked nested bin directory should not be ignored"
        );
    });
}

@@ -10599,7 +10592,7 @@ async fn test_update_gitignore(cx: &mut gpui::TestAppContext) {

cx.executor().run_until_parked();
cx.read(|cx| {
assert_entry_git_state(tree.read(cx), repository.read(cx), "a.xml", None, true);
assert_entry_git_state(tree.read(cx), repository.read(cx), "a.xml", None, false);
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on the linked issue, a tracked file that matches a .gitignore pattern should be treated as tracked.

Since...
crates/project/tests/integration/project_tests.rs line 10553

fs.set_head_and_index_for_repo(
    path!("/root/.git").as_ref(),
    &[
        (".gitignore", "*.txt\n".into()),
        ("a.xml", "<a></a>".into()),
    ],
);

The test's assertion here should be "a.xml" is not ignored, but I wonder if that means I need to update the test to make sure it still checks the originally intended behavior.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla-signed The user has signed the Contributor License Agreement guild Pull requests by someone in Zed Guild. NOTE: the label application is automated via github actions

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Global core.excludesfile patterns can cause a tracked file to appear ignored Issue with highlighting for .gitignore files/directories

1 participant