Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/respect-git-info-exclude.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@biomejs/biome": minor
Comment thread
ematipico marked this conversation as resolved.
---

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.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Lead with the Fixed [#4822](...) prefix.

Since issue #4822 exists for this bug, the changeset description should start with the standard Fixed [#NUMBER](issue link): ... form so it lines up with the rest of the changelog.

🩹 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 [#NUMBER](issue link): ...'."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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.
🧰 Tools
🪛 LanguageTool

[grammar] ~5-~5: There seems to be a noun/verb agreement error. Did you mean “excludes” or “excluded”?
Context: ...inor --- Biome now applies Git's local exclude file when VCS ignore files are enabled....

(SINGULAR_NOUN_VERB_AGREEMENT)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.changeset/respect-git-info-exclude.md at line 5, Update the changeset
description to start with the required prefix "Fixed [`#4822`](issue link):" so it
follows the bug-fix convention; edit the .changeset/respect-git-info-exclude.md
content and prepend "Fixed [`#4822`](...): " (replace ... with the actual issue
URL) before the existing sentence about applying Git's local exclude file.

80 changes: 80 additions & 0 deletions crates/biome_cli/tests/cases/vcs_ignored_files.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,86 @@ fn ignore_vcs_os_independent_parse() {
));
}

#[test]
fn ignore_git_info_exclude() {
let mut fs = TemporaryFs::new("ignore_git_info_exclude");
let mut console = BufferConsole::default();

fs.create_file(
"biome.json",
r#"{
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
}
}"#,
);

fs.create_file(".gitignore", "");
fs.create_file(".git/info/exclude", "file2.js\n");

fs.create_file("file1.js", UNFORMATTED);
fs.create_file("file2.js", UNFORMATTED);

let result = run_cli_with_dyn_fs(
Box::new(fs.create_os()),
&mut console,
Args::from(["format", "--write", fs.cli_path()].as_slice()),
);

assert!(result.is_ok(), "run_cli returned {result:?}");

assert_cli_snapshot(SnapshotPayload::new(
module_path!(),
"ignore_git_info_exclude",
fs.create_mem(),
console,
result,
));
}

#[test]
fn ignore_git_common_info_exclude() {
let mut fs = TemporaryFs::new("ignore_git_common_info_exclude");
let mut console = BufferConsole::default();

fs.create_file(
"biome.json",
r#"{
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
}
}"#,
);

fs.create_file(".gitignore", "");
fs.create_file(".git", "gitdir: actual.git/worktrees/current\n");
fs.create_file("actual.git/worktrees/current/commondir", "../..\n");
fs.create_file("actual.git/info/exclude", "file2.js\n");

fs.create_file("file1.js", UNFORMATTED);
fs.create_file("file2.js", UNFORMATTED);

let result = run_cli_with_dyn_fs(
Box::new(fs.create_os()),
&mut console,
Args::from(["format", "--write", fs.cli_path()].as_slice()),
);

assert!(result.is_ok(), "run_cli returned {result:?}");

assert_cli_snapshot(SnapshotPayload::new(
module_path!(),
"ignore_git_common_info_exclude",
fs.create_mem(),
console,
result,
));
}

#[test]
fn ignore_vcs_ignored_file_via_cli() {
let mut fs = TemporaryFs::new("ignore_vcs_ignored_file_via_cli");
Expand Down
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.
```
4 changes: 2 additions & 2 deletions crates/biome_configuration/src/vcs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ pub struct VcsConfiguration {
#[deserializable(bail_on_error)]
pub client_kind: Option<VcsClientKind>,

/// Whether Biome should use the VCS ignore file. When [true], Biome will ignore the files
/// specified in the ignore file.
/// Whether Biome should use VCS ignore files. When [true], Biome will ignore files
/// specified in `.gitignore`, `.ignore`, and Git's local exclude file.
#[cfg_attr(
feature = "cli",
bpaf(long("vcs-use-ignore-file"), argument("true|false"))
Expand Down
103 changes: 88 additions & 15 deletions crates/biome_service/src/workspace/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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));
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I would prefer if read_git_info_exclude wouldn't pollute ignore_file_contents, and instead it would read and parse the content, and would return the lines we need. It seems duplicate work, but it would make things well separated.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Updated. read_git_info_exclude_patterns now reads and parses .git/info/exclude separately and returns the patterns, so the root ignore file contents stay separate.

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(),
)?;
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/@biomejs/biome/configuration_schema.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.