Skip to content

Commit 94c9bdf

Browse files
authored
Ignore untracked files (#74)
* finding ignored files * skip untracked config * version 0.2.11
1 parent a39af5b commit 94c9bdf

File tree

12 files changed

+222
-2
lines changed

12 files changed

+222
-2
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "codeowners"
3-
version = "0.2.10"
3+
version = "0.2.11"
44
edition = "2024"
55

66
[profile.release]

src/config.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ pub struct Config {
2424

2525
#[serde(default = "default_ignore_dirs")]
2626
pub ignore_dirs: Vec<String>,
27+
28+
#[serde(default = "default_skip_untracked_files")]
29+
pub skip_untracked_files: bool,
2730
}
2831

2932
#[allow(dead_code)]
@@ -60,6 +63,10 @@ fn vendored_gems_path() -> String {
6063
"vendored/".to_string()
6164
}
6265

66+
fn default_skip_untracked_files() -> bool {
67+
true
68+
}
69+
6370
fn default_ignore_dirs() -> Vec<String> {
6471
vec![
6572
".cursor".to_owned(),

src/files.rs

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
use std::{
2+
path::{Path, PathBuf},
3+
process::Command,
4+
};
5+
6+
use core::fmt;
7+
use error_stack::{Context, Result, ResultExt};
8+
9+
#[derive(Debug)]
10+
pub enum Error {
11+
Io,
12+
}
13+
14+
impl fmt::Display for Error {
15+
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
16+
match self {
17+
Error::Io => fmt.write_str("Error::Io"),
18+
}
19+
}
20+
}
21+
22+
impl Context for Error {}
23+
24+
pub(crate) fn untracked_files(base_path: &Path) -> Result<Vec<PathBuf>, Error> {
25+
let output = Command::new("git")
26+
.args(["ls-files", "--others", "--exclude-standard", "--full-name", "-z", "--", "."])
27+
.current_dir(base_path)
28+
.output()
29+
.change_context(Error::Io)?;
30+
31+
if !output.status.success() {
32+
return Ok(Vec::new());
33+
}
34+
35+
let results: Vec<PathBuf> = output
36+
.stdout
37+
.split(|&b| b == b'\0')
38+
.filter(|chunk| !chunk.is_empty())
39+
.map(|rel| std::str::from_utf8(rel).change_context(Error::Io).map(|s| base_path.join(s)))
40+
.collect::<std::result::Result<_, _>>()?;
41+
42+
Ok(results)
43+
}
44+
45+
#[cfg(test)]
46+
mod tests {
47+
use super::*;
48+
49+
#[test]
50+
fn test_untracked_files() {
51+
let tmp_dir = tempfile::tempdir().unwrap();
52+
let untracked = untracked_files(tmp_dir.path()).unwrap();
53+
assert!(untracked.is_empty());
54+
55+
std::process::Command::new("git")
56+
.arg("init")
57+
.current_dir(tmp_dir.path())
58+
.output()
59+
.expect("failed to run git init");
60+
61+
std::fs::write(tmp_dir.path().join("test.txt"), "test").unwrap();
62+
let untracked = untracked_files(tmp_dir.path()).unwrap();
63+
assert!(untracked.len() == 1);
64+
let expected = tmp_dir.path().join("test.txt");
65+
assert!(untracked[0] == expected);
66+
}
67+
68+
#[test]
69+
fn test_untracked_files_with_spaces_and_parens() {
70+
let tmp_dir = tempfile::tempdir().unwrap();
71+
72+
std::process::Command::new("git")
73+
.arg("init")
74+
.current_dir(tmp_dir.path())
75+
.output()
76+
.expect("failed to run git init");
77+
78+
// Nested dirs with spaces and parentheses
79+
let d1 = tmp_dir.path().join("dir with spaces");
80+
let d2 = d1.join("(special)");
81+
std::fs::create_dir_all(&d2).unwrap();
82+
83+
let f1 = d1.join("file (1).txt");
84+
let f2 = d2.join("a b (2).rb");
85+
std::fs::write(&f1, "one").unwrap();
86+
std::fs::write(&f2, "two").unwrap();
87+
88+
let mut untracked = untracked_files(tmp_dir.path()).unwrap();
89+
untracked.sort();
90+
91+
let mut expected = vec![f1, f2];
92+
expected.sort();
93+
94+
assert_eq!(untracked, expected);
95+
}
96+
97+
#[test]
98+
fn test_untracked_files_multiple_files_order_insensitive() {
99+
let tmp_dir = tempfile::tempdir().unwrap();
100+
101+
std::process::Command::new("git")
102+
.arg("init")
103+
.current_dir(tmp_dir.path())
104+
.output()
105+
.expect("failed to run git init");
106+
107+
let f1 = tmp_dir.path().join("a.txt");
108+
let f2 = tmp_dir.path().join("b.txt");
109+
let f3 = tmp_dir.path().join("c.txt");
110+
std::fs::write(&f1, "A").unwrap();
111+
std::fs::write(&f2, "B").unwrap();
112+
std::fs::write(&f3, "C").unwrap();
113+
114+
let mut untracked = untracked_files(tmp_dir.path()).unwrap();
115+
untracked.sort();
116+
117+
let mut expected = vec![f1, f2, f3];
118+
expected.sort();
119+
120+
assert_eq!(untracked, expected);
121+
}
122+
123+
#[test]
124+
fn test_untracked_files_excludes_staged() {
125+
let tmp_dir = tempfile::tempdir().unwrap();
126+
127+
std::process::Command::new("git")
128+
.arg("init")
129+
.current_dir(tmp_dir.path())
130+
.output()
131+
.expect("failed to run git init");
132+
133+
let staged = tmp_dir.path().join("staged.txt");
134+
let unstaged = tmp_dir.path().join("unstaged.txt");
135+
std::fs::write(&staged, "I will be staged").unwrap();
136+
std::fs::write(&unstaged, "I remain untracked").unwrap();
137+
138+
// Stage one file
139+
let add_status = std::process::Command::new("git")
140+
.arg("add")
141+
.arg("staged.txt")
142+
.current_dir(tmp_dir.path())
143+
.output()
144+
.expect("failed to run git add");
145+
assert!(
146+
add_status.status.success(),
147+
"git add failed: {}",
148+
String::from_utf8_lossy(&add_status.stderr)
149+
);
150+
151+
let mut untracked = untracked_files(tmp_dir.path()).unwrap();
152+
untracked.sort();
153+
154+
let expected = vec![unstaged];
155+
assert_eq!(untracked, expected);
156+
}
157+
}

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ pub mod cache;
22
pub(crate) mod common_test;
33
pub mod config;
44
pub mod crosscheck;
5+
pub(crate) mod files;
56
pub mod ownership;
67
pub(crate) mod project;
78
pub mod project_builder;

src/ownership/for_file_fast.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,7 @@ mod tests {
303303
vendored_gems_path: vendored_path.to_string(),
304304
cache_directory: "tmp/cache/codeowners".to_string(),
305305
ignore_dirs: vec![],
306+
skip_untracked_files: false,
306307
}
307308
}
308309

src/project_builder.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use tracing::{instrument, warn};
1313
use crate::{
1414
cache::Cache,
1515
config::Config,
16+
files,
1617
project::{DirectoryCodeownersFile, Error, Package, PackageType, Project, ProjectFile, Team, VendoredGem, deserializers},
1718
project_file_builder::ProjectFileBuilder,
1819
};
@@ -56,13 +57,22 @@ impl<'a> ProjectBuilder<'a> {
5657
let mut builder = WalkBuilder::new(&self.base_path);
5758
builder.hidden(false);
5859
builder.follow_links(false);
60+
5961
// Prune traversal early: skip heavy and irrelevant directories
6062
let ignore_dirs = self.config.ignore_dirs.clone();
6163
let base_path = self.base_path.clone();
64+
let untracked_files = if self.config.skip_untracked_files {
65+
files::untracked_files(&base_path).unwrap_or_default()
66+
} else {
67+
vec![]
68+
};
6269

6370
builder.filter_entry(move |entry: &DirEntry| {
6471
let path = entry.path();
6572
let file_name = entry.file_name().to_str().unwrap_or("");
73+
if !untracked_files.is_empty() && untracked_files.contains(&path.to_path_buf()) {
74+
return false;
75+
}
6676
if let Some(ft) = entry.file_type()
6777
&& ft.is_dir()
6878
&& let Ok(rel) = path.strip_prefix(&base_path)
File renamed without changes.

tests/fixtures/invalid_project/config/code_ownership.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ team_file_glob:
88
- config/teams/**/*.yml
99
vendored_gems_path: gems
1010
unowned_globs:
11+
skip_untracked_files: false

tests/fixtures/multiple-directory-owners/config/code_ownership.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ javascript_package_paths:
88
vendored_gems_path: gems
99
team_file_glob:
1010
- config/teams/**/*.yml
11+
skip_untracked_files: false

0 commit comments

Comments
 (0)