-
-
Notifications
You must be signed in to change notification settings - Fork 988
feat(core): respect git info exclude files #10121
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: next
Are you sure you want to change the base?
Changes from all commits
5b20c77
8e441c6
a5aaf41
ec1b930
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,5 @@ | ||||||
| --- | ||||||
| "@biomejs/biome": minor | ||||||
| --- | ||||||
|
|
||||||
| Biome now applies Git's local exclude file when VCS ignore files are enabled. Files listed in `.git/info/exclude` are skipped the same way as files listed in `.gitignore`, including in linked worktrees. | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Lead with the Since issue 🩹 Suggested fix-Biome now applies Git's local exclude file when VCS ignore files are enabled. Files listed in `.git/info/exclude` are skipped the same way as files listed in `.gitignore`, including in linked worktrees.
+Fixed [`#4822`](https://github.com/biomejs/biome/issues/4822): Biome now applies Git's local exclude file when VCS ignore files are enabled. Files listed in `.git/info/exclude` are skipped the same way as files listed in `.gitignore`, including in linked worktrees.As per coding guidelines: "For bug fixes, start with 'Fixed [ 📝 Committable suggestion
Suggested change
🧰 Tools🪛 LanguageTool[grammar] ~5-~5: There seems to be a noun/verb agreement error. Did you mean “excludes” or “excluded”? (SINGULAR_NOUN_VERB_AGREEMENT) 🤖 Prompt for AI Agents |
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| --- | ||
| source: crates/biome_cli/tests/snap_test.rs | ||
| assertion_line: 520 | ||
| expression: redactor(content) | ||
| --- | ||
| ## `biome.json` | ||
|
|
||
| ```json | ||
| { | ||
| "vcs": { | ||
| "enabled": true, | ||
| "clientKind": "git", | ||
| "useIgnoreFile": true | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ## `.git` | ||
|
|
||
| ```git | ||
| gitdir: actual.git/worktrees/current | ||
|
|
||
| ``` | ||
|
|
||
| ## `.gitignore` | ||
|
|
||
| ```gitignore | ||
|
|
||
| ``` | ||
|
|
||
| ## `actual.git/info/exclude` | ||
|
|
||
| ```git/info/exclude | ||
| file2.js | ||
|
|
||
| ``` | ||
|
|
||
| ## `actual.git/worktrees/current/commondir` | ||
|
|
||
| ```git/worktrees/current/commondir | ||
| ../.. | ||
|
|
||
| ``` | ||
|
|
||
| ## `file1.js` | ||
|
|
||
| ```js | ||
| statement(); | ||
|
|
||
| ``` | ||
|
|
||
| ## `file2.js` | ||
|
|
||
| ```js | ||
| statement( ) | ||
| ``` | ||
|
|
||
| # Emitted Messages | ||
|
|
||
| ```block | ||
| Formatted 2 files in <TIME>. Fixed 2 files. | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| --- | ||
| source: crates/biome_cli/tests/snap_test.rs | ||
| assertion_line: 520 | ||
| expression: redactor(content) | ||
| --- | ||
| ## `biome.json` | ||
|
|
||
| ```json | ||
| { | ||
| "vcs": { | ||
| "enabled": true, | ||
| "clientKind": "git", | ||
| "useIgnoreFile": true | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ## `.git/info/exclude` | ||
|
|
||
| ```git/info/exclude | ||
| file2.js | ||
|
|
||
| ``` | ||
|
|
||
| ## `.gitignore` | ||
|
|
||
| ```gitignore | ||
|
|
||
| ``` | ||
|
|
||
| ## `file1.js` | ||
|
|
||
| ```js | ||
| statement(); | ||
|
|
||
| ``` | ||
|
|
||
| ## `file2.js` | ||
|
|
||
| ```js | ||
| statement( ) | ||
| ``` | ||
|
|
||
| # Emitted Messages | ||
|
|
||
| ```block | ||
| Formatted 2 files in <TIME>. Fixed 2 files. | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -130,6 +130,66 @@ pub struct WorkspaceServer { | |
| notification_tx: watch::Sender<ServiceNotification>, | ||
| } | ||
|
|
||
| fn resolve_git_path(base: &Utf8Path, path: &str) -> Utf8PathBuf { | ||
| let path = Utf8Path::new(path); | ||
| let resolved_path = if path.is_absolute() { | ||
| path.to_path_buf() | ||
| } else { | ||
| base.join(path) | ||
| }; | ||
|
|
||
| normalize_path(&resolved_path) | ||
| } | ||
|
|
||
| fn read_git_info_exclude_patterns( | ||
| fs: &dyn FsWithResolverProxy, | ||
| directory: &Utf8Path, | ||
| ) -> Option<Vec<String>> { | ||
| let git_dir = resolve_git_dir(fs, directory)?; | ||
| let git_common_dir = resolve_git_common_dir(fs, &git_dir); | ||
|
|
||
| // Git treats `$GIT_DIR/info/exclude` as repository-local ignore rules. In a | ||
| // linked worktree, Git resolves this path through `commondir`, so the | ||
| // exclude file is shared from the common Git directory. | ||
| fs.read_file_from_path(git_common_dir.join("info/exclude").as_ref()) | ||
| .map(|content| content.lines().map(String::from).collect()) | ||
| .ok() | ||
| } | ||
|
|
||
| fn resolve_git_dir(fs: &dyn FsWithResolverProxy, directory: &Utf8Path) -> Option<Utf8PathBuf> { | ||
| let dot_git = directory.join(".git"); | ||
|
|
||
| // Linked worktrees and submodules use a `.git` file whose `gitdir:` entry | ||
| // points at the real Git directory. Regular repositories use `.git` as the | ||
| // Git directory itself. | ||
| match fs.read_file_from_path(dot_git.as_ref()) { | ||
| Ok(content) => content.lines().find_map(|line| { | ||
| let gitdir = line.strip_prefix("gitdir:")?.trim(); | ||
| Some(resolve_git_path(directory, gitdir)) | ||
| }), | ||
| Err(_) => fs.path_is_dir(&dot_git).then_some(dot_git), | ||
| } | ||
| } | ||
|
|
||
| fn resolve_git_common_dir(fs: &dyn FsWithResolverProxy, git_dir: &Utf8Path) -> Utf8PathBuf { | ||
| // A linked worktree's gitdir is worktree-specific, while repository-wide | ||
| // files such as `info/exclude` live under the common Git directory. | ||
| // `commondir` stores that path, relative to `git_dir` when not absolute. | ||
| let Ok(content) = fs.read_file_from_path(git_dir.join("commondir").as_ref()) else { | ||
| return git_dir.to_path_buf(); | ||
| }; | ||
|
|
||
| content | ||
| .lines() | ||
| .next() | ||
| .map(str::trim) | ||
| .filter(|line| !line.is_empty()) | ||
| .map_or_else( | ||
| || git_dir.to_path_buf(), | ||
| |commondir| resolve_git_path(git_dir, commondir), | ||
| ) | ||
| } | ||
|
|
||
| /// The `Workspace` object is long-lived, so we want it to be able to cross | ||
| /// unwind boundaries. | ||
| /// In return, we have to make sure operations on the workspace either do not | ||
|
|
@@ -1462,26 +1522,39 @@ impl Workspace for WorkspaceServer { | |
| Some(VcsClientKind::Git) => { | ||
| let gitignore = directory.join(".gitignore"); | ||
| let ignore = directory.join(".ignore"); | ||
| let mut ignore_file_contents = Vec::new(); | ||
| let git_info_exclude = | ||
| read_git_info_exclude_patterns(self.fs.as_ref(), directory.as_ref()); | ||
|
|
||
| let result = self | ||
| .fs | ||
| .read_file_from_path(gitignore.as_ref()) | ||
| .ok() | ||
| .or_else(|| self.fs.read_file_from_path(ignore.as_ref()).ok()); | ||
| match result { | ||
| Some(content) => { | ||
| let lines: Vec<_> = content.lines().collect(); | ||
| settings.vcs_settings.store_root_ignore_patterns( | ||
| directory.as_ref(), | ||
| lines.as_slice(), | ||
| )?; | ||
| } | ||
| None => { | ||
| diagnostics.push(biome_diagnostics::serde::Diagnostic::new( | ||
| VcsDiagnostic::NoIgnoreFileFound(NoIgnoreFileFound { | ||
| path: directory.to_string(), | ||
| }), | ||
| )); | ||
| } | ||
| if let Some(content) = result { | ||
| ignore_file_contents.push(content); | ||
| } | ||
|
|
||
| let mut ignore_file_patterns = ignore_file_contents | ||
| .iter() | ||
| .flat_map(|content| content.lines()) | ||
| .collect::<Vec<_>>(); | ||
| if let Some(git_info_exclude) = git_info_exclude.as_ref() { | ||
| ignore_file_patterns | ||
| .extend(git_info_exclude.iter().map(String::as_str)); | ||
| } | ||
|
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would prefer if
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated. |
||
| if ignore_file_patterns.is_empty() { | ||
| diagnostics.push(biome_diagnostics::serde::Diagnostic::new( | ||
| VcsDiagnostic::NoIgnoreFileFound(NoIgnoreFileFound { | ||
| path: directory.to_string(), | ||
| }), | ||
| )); | ||
| } else { | ||
| settings.vcs_settings.store_root_ignore_patterns( | ||
| directory.as_ref(), | ||
| ignore_file_patterns.as_slice(), | ||
| )?; | ||
| }; | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } | ||
| } | ||
|
|
||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Uh oh!
There was an error while loading. Please reload this page.