Skip to content

Commit c104304

Browse files
committed
fix: address critical issues from code review
This commit addresses the critical and high-priority issues identified in the comprehensive code review. ## Critical Fixes ### 1. Remove panics from library code (types.rs) - Changed `codeowners_entry_to_matcher()` to return `Result<CodeownersEntryMatcher, PatternError>` - Added new `PatternError` type with proper error message formatting - Updated all callers to handle the Result properly ### 2. Fix unwrap() calls in cache.rs - Replaced `unwrap()` with proper error handling using `match` and `filter_map` - Added graceful fallback for files that fail ownership resolution - Changed stdout flush to use `let _ =` to ignore errors ### 3. Fix broken hash calculation (common.rs) - Added exclusion patterns for `.codeowners.cache` files - Implemented double-check in diff callback to skip cache files - Fixed the `and_then` closure returning wrong type ### 4. Add CLI integration tests - Added 22 comprehensive integration tests covering: - Basic commands (help, version, config) - All codeowners subcommands (parse, list-files, list-owners, etc.) - Shell completion generation (bash, zsh, fish) - Various CLI flags and options - Error handling for invalid inputs ## High Priority Fixes ### 5. Improve error Display to include source context - Modified `Display` impl to include source error message in output ### 6. Remove empty start() function (core/mod.rs) - Removed unused `start()` function that did nothing ### 7. Add recursion depth limit to find_codeowners_files - Added MAX_RECURSION_DEPTH constant (100) - Implemented depth tracking to prevent stack overflow on deep directory structures ## Files Changed - types.rs: Return Result from codeowners_entry_to_matcher, add PatternError - cache.rs: Graceful error handling, filter invalid patterns with warnings - infer_owners.rs: Handle Result from matcher conversion - common.rs: Fix hash calculation, add recursion limit - error.rs: Include source in Display - mod.rs: Remove empty function - resolver_bench.rs: Handle Result in benchmarks - test_cli.rs: Add 22 integration tests
1 parent bbcbb97 commit c104304

File tree

8 files changed

+565
-61
lines changed

8 files changed

+565
-61
lines changed

ci/tests/test_cli.rs

Lines changed: 418 additions & 1 deletion
Large diffs are not rendered by default.

codeinput/src/benches/resolver_bench.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ fn create_test_codeowners_entry_matcher(
2727
owners,
2828
tags,
2929
};
30-
codeowners_entry_to_matcher(&entry)
30+
codeowners_entry_to_matcher(&entry).expect("Failed to create matcher in benchmark")
3131
}
3232

3333
fn bench_find_owners_and_tags_simple_pattern(c: &mut Criterion) {

codeinput/src/core/cache.rs

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,24 @@ pub fn build_cache(
2323
let mut owners_map = std::collections::HashMap::new();
2424
let mut tags_map = std::collections::HashMap::new();
2525

26+
// Build matchers, filtering out invalid patterns with warnings
2627
let matched_entries: Vec<CodeownersEntryMatcher> = entries
2728
.iter()
28-
.map(|entry| codeowners_entry_to_matcher(entry))
29+
.filter_map(|entry| {
30+
match codeowners_entry_to_matcher(entry) {
31+
Ok(matcher) => Some(matcher),
32+
Err(e) => {
33+
log::warn!(
34+
"Skipping invalid CODEOWNERS pattern '{}' in {} line {}: {}",
35+
entry.pattern,
36+
entry.source_file.display(),
37+
entry.line_number,
38+
e.message
39+
);
40+
None
41+
}
42+
}
43+
})
2944
.collect();
3045

3146
// Process each file to find owners and tags
@@ -53,16 +68,22 @@ pub fn build_cache(
5368
"\r\x1b[K📁 Processing [{}/{}] {}",
5469
current, total_files, truncated_file
5570
);
56-
std::io::stdout().flush().unwrap();
57-
58-
let (owners, tags) =
59-
find_owners_and_tags_for_file(file_path, &matched_entries).unwrap();
71+
let _ = std::io::stdout().flush(); // Ignore flush errors
72+
73+
// Handle errors gracefully - skip files that can't be processed
74+
let (owners, tags) = match find_owners_and_tags_for_file(file_path, &matched_entries) {
75+
Ok(result) => result,
76+
Err(e) => {
77+
log::warn!("Failed to resolve ownership for {}: {}", file_path.display(), e);
78+
(vec![], vec![])
79+
}
80+
};
6081

6182
// Build file entry
6283
FileEntry {
6384
path: file_path.clone(),
64-
owners: owners.clone(),
65-
tags: tags.clone(),
85+
owners,
86+
tags,
6687
}
6788
})
6889
.collect::<Vec<FileEntry>>()

codeinput/src/core/commands/infer_owners.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,11 @@ fn filter_unowned_files(
132132
};
133133

134134
let mut unowned_files = Vec::new();
135-
let matchers: Vec<_> = cache.entries.iter().map(codeowners_entry_to_matcher).collect();
135+
let matchers: Vec<_> = cache
136+
.entries
137+
.iter()
138+
.filter_map(|e| codeowners_entry_to_matcher(e).ok())
139+
.collect();
136140
for file in files {
137141
let (owners, _tags) = find_owners_and_tags_for_file(&file, &matchers)?;
138142
if owners.is_empty() || owners.iter().all(|o| o.owner_type == OwnerType::Unowned) {
@@ -156,7 +160,11 @@ fn analyze_file_ownership(
156160
// Get existing owners from cache
157161
let existing_owners = match cache {
158162
Some(cache) => {
159-
let matchers: Vec<_> = cache.entries.iter().map(codeowners_entry_to_matcher).collect();
163+
let matchers: Vec<_> = cache
164+
.entries
165+
.iter()
166+
.filter_map(|e| codeowners_entry_to_matcher(e).ok())
167+
.collect();
160168
let (owners, _tags) = find_owners_and_tags_for_file(file_path, &matchers).unwrap_or_default();
161169
owners
162170
},

codeinput/src/core/common.rs

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,24 @@ use std::path::{Path, PathBuf};
66

77
use super::types::{CodeownersEntry, Owner, Tag};
88

9+
/// Maximum recursion depth for finding CODEOWNERS files (prevents stack overflow on deep structures)
10+
const MAX_RECURSION_DEPTH: usize = 100;
11+
912
/// Find CODEOWNERS files recursively in the given directory and its subdirectories
1013
pub fn find_codeowners_files<P: AsRef<Path>>(base_path: P) -> Result<Vec<PathBuf>> {
14+
find_codeowners_files_impl(base_path.as_ref(), 0)
15+
}
16+
17+
fn find_codeowners_files_impl(base_path: &Path, depth: usize) -> Result<Vec<PathBuf>> {
18+
if depth > MAX_RECURSION_DEPTH {
19+
log::warn!(
20+
"Maximum recursion depth ({}) reached while searching for CODEOWNERS files at {}",
21+
MAX_RECURSION_DEPTH,
22+
base_path.display()
23+
);
24+
return Ok(Vec::new());
25+
}
26+
1127
let mut result = Vec::new();
1228
if let Ok(entries) = std::fs::read_dir(base_path) {
1329
for entry in entries.flatten() {
@@ -21,7 +37,7 @@ pub fn find_codeowners_files<P: AsRef<Path>>(base_path: P) -> Result<Vec<PathBuf
2137
{
2238
result.push(path);
2339
} else if path.is_dir() {
24-
result.extend(find_codeowners_files(path)?);
40+
result.extend(find_codeowners_files_impl(&path, depth + 1)?);
2541
}
2642
}
2743
}
@@ -67,6 +83,12 @@ pub fn collect_tags(entries: &[CodeownersEntry]) -> Vec<Tag> {
6783
tags.into_iter().collect()
6884
}
6985

86+
/// Files to exclude from the repository hash calculation (these are generated files)
87+
const HASH_EXCLUDED_PATTERNS: &[&str] = &[
88+
".codeowners.cache",
89+
"*.codeowners.cache",
90+
];
91+
7092
pub fn get_repo_hash(repo_path: &Path) -> Result<[u8; 32]> {
7193
let repo = Repository::open(repo_path)
7294
.map_err(|e| Error::with_source("Failed to open repo", Box::new(e)))?;
@@ -75,7 +97,7 @@ pub fn get_repo_hash(repo_path: &Path) -> Result<[u8; 32]> {
7597
let head_oid = repo
7698
.head()
7799
.and_then(|r| r.resolve())
78-
.and_then(|r| Ok(r.target()))
100+
.map(|r| r.target())
79101
.unwrap_or(None);
80102

81103
// 2. Get index/staging area tree hash
@@ -87,16 +109,35 @@ pub fn get_repo_hash(repo_path: &Path) -> Result<[u8; 32]> {
87109
.write_tree()
88110
.map_err(|e| Error::with_source("Failed to write index tree", Box::new(e)))?;
89111

90-
// 3. Calculate hash of unstaged changes
91-
// TODO: this doesn't work and also we need to exclude .codeowners.cache file
92-
// otherwise the hash will change every time we parse the repo
112+
// 3. Calculate hash of unstaged changes, excluding cache files
93113
let unstaged_hash = {
114+
let mut diff_opts = DiffOptions::new();
115+
diff_opts.include_untracked(true);
116+
117+
// Add pathspec exclusions for cache files
118+
for pattern in HASH_EXCLUDED_PATTERNS {
119+
diff_opts.pathspec(format!(":(exclude){}", pattern));
120+
}
121+
94122
let diff = repo
95-
.diff_index_to_workdir(None, Some(DiffOptions::new().include_untracked(true)))
123+
.diff_index_to_workdir(None, Some(&mut diff_opts))
96124
.map_err(|e| Error::with_source("Failed to get diff", Box::new(e)))?;
97125

98126
let mut hasher = Sha256::new();
99-
diff.print(DiffFormat::Patch, |_, _, line| {
127+
diff.print(DiffFormat::Patch, |delta, _, line| {
128+
// Double-check: skip any cache files that might have slipped through
129+
if let Some(path) = delta.new_file().path() {
130+
let path_str = path.to_string_lossy();
131+
if HASH_EXCLUDED_PATTERNS.iter().any(|p| {
132+
if p.starts_with('*') {
133+
path_str.ends_with(&p[1..])
134+
} else {
135+
path_str == *p
136+
}
137+
}) {
138+
return true; // Skip this file
139+
}
140+
}
100141
hasher.update(line.content());
101142
true
102143
})

codeinput/src/core/mod.rs

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,3 @@ pub mod resolver;
1010
pub(crate) mod smart_iter;
1111
pub mod tag_resolver;
1212
pub mod types;
13-
14-
use crate::utils::error::Result;
15-
16-
pub fn start() -> Result<()> {
17-
// does nothing
18-
19-
Ok(())
20-
}

codeinput/src/core/types.rs

Lines changed: 55 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -51,53 +51,74 @@ pub struct CodeownersEntryMatcher {
5151
pub override_matcher: Override,
5252
}
5353

54+
/// Error type for pattern matching failures
55+
#[derive(Debug)]
56+
pub struct PatternError {
57+
pub pattern: String,
58+
pub source_file: PathBuf,
59+
pub message: String,
60+
}
61+
62+
impl std::fmt::Display for PatternError {
63+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64+
write!(
65+
f,
66+
"Invalid pattern '{}' in {}: {}",
67+
self.pattern,
68+
self.source_file.display(),
69+
self.message
70+
)
71+
}
72+
}
73+
74+
impl std::error::Error for PatternError {}
75+
76+
/// Converts a CodeownersEntry to a CodeownersEntryMatcher with pattern compilation.
77+
///
78+
/// # Errors
79+
///
80+
/// Returns a `PatternError` if:
81+
/// - The entry's source file has no parent directory
82+
/// - The pattern is invalid and cannot be compiled
83+
/// - The override matcher fails to build
5484
#[cfg(feature = "ignore")]
55-
pub fn codeowners_entry_to_matcher(entry: &CodeownersEntry) -> CodeownersEntryMatcher {
56-
let codeowners_dir = match entry.source_file.parent() {
57-
Some(dir) => dir,
58-
None => {
59-
eprintln!(
60-
"CODEOWNERS entry has no parent directory: {}",
61-
entry.source_file.display()
62-
);
63-
panic!("Invalid CODEOWNERS entry without parent directory");
64-
}
65-
};
85+
pub fn codeowners_entry_to_matcher(
86+
entry: &CodeownersEntry,
87+
) -> Result<CodeownersEntryMatcher, PatternError> {
88+
let codeowners_dir = entry.source_file.parent().ok_or_else(|| PatternError {
89+
pattern: entry.pattern.clone(),
90+
source_file: entry.source_file.clone(),
91+
message: "CODEOWNERS entry has no parent directory".to_string(),
92+
})?;
6693

6794
let mut builder = ignore::overrides::OverrideBuilder::new(codeowners_dir);
6895

6996
// Transform directory patterns to match GitHub CODEOWNERS behavior
7097
let pattern = normalize_codeowners_pattern(&entry.pattern);
7198

72-
if let Err(e) = builder.add(&pattern) {
73-
eprintln!(
74-
"Invalid pattern '{}' (normalized from '{}') in {}: {}",
75-
pattern,
76-
entry.pattern,
77-
entry.source_file.display(),
78-
e
79-
);
80-
panic!("Invalid CODEOWNERS entry pattern");
81-
}
82-
let override_matcher: Override = match builder.build() {
83-
Ok(o) => o,
84-
Err(e) => {
85-
eprintln!(
86-
"Failed to build override for pattern '{}': {}",
87-
entry.pattern, e
88-
);
89-
panic!("Failed to build CODEOWNERS entry matcher");
90-
}
91-
};
99+
builder.add(&pattern).map_err(|e| PatternError {
100+
pattern: entry.pattern.clone(),
101+
source_file: entry.source_file.clone(),
102+
message: format!(
103+
"Invalid pattern '{}' (normalized from '{}'): {}",
104+
pattern, entry.pattern, e
105+
),
106+
})?;
107+
108+
let override_matcher = builder.build().map_err(|e| PatternError {
109+
pattern: entry.pattern.clone(),
110+
source_file: entry.source_file.clone(),
111+
message: format!("Failed to build override matcher: {}", e),
112+
})?;
92113

93-
CodeownersEntryMatcher {
114+
Ok(CodeownersEntryMatcher {
94115
source_file: entry.source_file.clone(),
95116
line_number: entry.line_number,
96117
pattern: entry.pattern.clone(),
97118
owners: entry.owners.clone(),
98119
tags: entry.tags.clone(),
99120
override_matcher,
100-
}
121+
})
101122
}
102123

103124
/// Detailed owner representation
@@ -355,7 +376,7 @@ mod tests {
355376
tags: vec![],
356377
};
357378

358-
let matcher = codeowners_entry_to_matcher(&entry);
379+
let matcher = codeowners_entry_to_matcher(&entry).unwrap();
359380

360381
// Test files that should match
361382
let test_files = vec![

codeinput/src/utils/error.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@ pub struct Error {
1616
// Implement the Display trait for our Error type.
1717
impl fmt::Display for Error {
1818
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
19-
write!(f, "{}", self.msg)
19+
write!(f, "{}", self.msg)?;
20+
if let Some(ref source) = self.source {
21+
write!(f, ": {}", source)?;
22+
}
23+
Ok(())
2024
}
2125
}
2226

0 commit comments

Comments
 (0)