Skip to content
Merged
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
8 changes: 7 additions & 1 deletion .github/workflows/audit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,14 @@ on:
jobs:
audit:
runs-on: ubuntu-latest
# cargo-audit's dependency tree can require a newer rustc than this repo's
# pinned rust-toolchain.toml (used to build the actual crate). Override
# with the runner's default stable toolchain just for this job so
# `cargo install cargo-audit` isn't stuck on our pin.
env:
RUSTUP_TOOLCHAIN: stable
steps:
- uses: actions/checkout@v4
- uses: actions-rs/audit-check@v1
- uses: rustsec/audit-check@v2.0.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
79 changes: 19 additions & 60 deletions Cargo.lock

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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "codeowners"
version = "0.3.3"
version = "0.3.4"
edition = "2024"

[profile.release]
Expand Down
12 changes: 12 additions & 0 deletions src/project_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,18 @@ impl<'a> ProjectBuilder<'a> {
let mut builder = WalkBuilder::new(&self.base_path);
builder.hidden(false);
builder.follow_links(false);
// Inclusion is determined by `tracked_files` (git ls-files) below, not by
// git's own ignore semantics. Without disabling these, a *global*
// .gitignore (core.excludesFile), local .gitignore, or .git/info/exclude
// rule can prune a whole directory from the walk even when its contents
// are force-tracked in git, silently dropping otherwise-owned files from
// the generated CODEOWNERS depending on the machine's git config.
// `.ignore` files are left enabled: they're this tool's own opt-in
// exclusion mechanism (see tests/fixtures/valid_project/.ignore) and are
// unrelated to git tracking status.
builder.git_global(false);
builder.git_ignore(false);
builder.git_exclude(false);

// Prune traversal early: skip heavy and irrelevant directories
let ignore_dirs = self.config.ignore_dirs.clone();
Expand Down
50 changes: 50 additions & 0 deletions tests/global_gitignore_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
use assert_cmd::prelude::*;
use std::{error::Error, fs, path::Path, process::Command};

mod common;
use common::{git_add_all_files, setup_fixture_repo};

const FIXTURE: &str = "tests/fixtures/valid_project";

/// A directory can be force-tracked in git even when it matches a
/// gitignore-style rule that lives outside the tracked tree (a personal
/// global `core.excludesFile`, or a local, uncommitted `.git/info/exclude`
/// entry). `generate` must still emit ownership for such directories:
/// inclusion is decided by `tracked_files` (git ls-files), so also honoring
/// git's own ignore semantics at the directory-walk level can silently prune
/// tracked, owned files depending on the machine's git config alone --
/// nothing committed to the repo. `.git/info/exclude` is used here as a
/// reliable stand-in for a developer's global gitignore, which is what
/// originally surfaced this bug (see PR discussion).
#[test]
fn test_generate_ignores_local_exclude_rules_for_tracked_directories() -> Result<(), Box<dyn Error>> {
let temp_dir = setup_fixture_repo(Path::new(FIXTURE));
let project_root = temp_dir.path();
git_add_all_files(project_root);

let exclude_path = project_root.join(".git/info/exclude");
let mut existing = fs::read_to_string(&exclude_path).unwrap_or_default();
existing.push_str("\nruby/app/payroll/\n");
fs::write(&exclude_path, existing)?;

let codeowners_path = project_root.join("tmp/CODEOWNERS");
fs::create_dir_all(codeowners_path.parent().unwrap())?;

Command::cargo_bin("codeowners")?
.arg("--project-root")
.arg(project_root)
.arg("--codeowners-file-path")
.arg(&codeowners_path)
.arg("--no-cache")
.arg("generate")
.assert()
.success();

let actual_codeowners = fs::read_to_string(&codeowners_path)?;
assert!(
actual_codeowners.contains("/ruby/app/payroll/**/** @PayrollTeam"),
"expected ruby/app/payroll ownership to survive a local .git/info/exclude rule matching it, got:\n{actual_codeowners}"
);

Ok(())
}
Loading