Skip to content

Commit c7e6b37

Browse files
committed
Fix normalization of exclude option and root directory edge case in gitignore handling
1 parent c33f8c9 commit c7e6b37

File tree

4 files changed

+109
-21
lines changed

4 files changed

+109
-21
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.2.3] - 2024-09-19
9+
10+
### Fixed
11+
12+
- Fixed the normalization of the `exclude` option when the path is `.` or `./`.
13+
- Fixed the edge case where the root directory is ignored by the `.gitignore` when the path is `.` or `./` and the `gitignore` has `.*` as an ignore pattern.
14+
815
## [0.2.2] - 2024-09-18
916

1017
### Added

src/codebase/mod.rs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,13 @@ impl CodebaseBuilder {
138138
Logger::trace("No gitignore impacting current branch");
139139
}
140140

141+
// Edge case: gitignore has ".*" pattern (ignoring all dotfiles)
142+
// and the root directory is '.', do not skip the root directory
143+
let is_entry_root = entry.path() == from;
141144
// Is the entry excluded by the gitignore?
142-
if maybe_gitignore.map_or(false, |gitignore| gitignore.is_excluded(&path)) {
145+
if maybe_gitignore.map_or(false, |gitignore| gitignore.is_excluded(&path))
146+
&& !is_entry_root
147+
{
143148
Logger::debug("Entry is excluded by the gitignore");
144149

145150
// If it's a directory, skip it entirely
@@ -576,4 +581,30 @@ mod tests {
576581
.iter()
577582
.any(|item| item.path.file_name().unwrap() == "config.log"));
578583
}
584+
585+
// Edge cases
586+
587+
fn create_dot_root_edge_case_structure(root: &Path) {
588+
// Root level
589+
create_file(&root.join(".gitignore"), ".*");
590+
create_file(&root.join("root.txt"), "root content");
591+
}
592+
593+
#[tokio::test]
594+
async fn test_dot_root_edge_case() {
595+
ensure_logger();
596+
let temp_dir = TempDir::new().unwrap();
597+
create_dot_root_edge_case_structure(temp_dir.path());
598+
599+
let codebase = CodebaseBuilder::new()
600+
.consider_gitignores(true)
601+
.build(temp_dir.path().to_path_buf())
602+
.await
603+
.unwrap();
604+
605+
let root_leaves: Vec<_> = codebase.tree.collect_local_leaves();
606+
assert!(root_leaves
607+
.iter()
608+
.any(|item| item.path.file_name().unwrap() == "root.txt"));
609+
}
579610
}

src/main.rs

Lines changed: 13 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ pub mod gitignore;
88
pub mod logger;
99
pub mod os;
1010
pub mod tree;
11+
pub mod utils;
1112

1213
use codebase::CodebaseBuilder;
1314
use error::{CunwError, Result};
@@ -17,6 +18,7 @@ use logger::Logger;
1718
/// why we should consider these files but if you want
1819
/// to include them you can use `--dangerously-allow-dot-git-traversal` flag.
1920
const GIT_RELATED_IGNORE_PATTERNS: [&str; 2] = ["**/.git", "./**/.git"];
21+
const BASE_PATH_EDGE_CASES: [&str; 2] = [".", "./"];
2022

2123
#[tokio::main]
2224
async fn main() -> Result<()> {
@@ -33,34 +35,25 @@ async fn main() -> Result<()> {
3335
// Build the excluded paths
3436
let mut excluded_paths = GlobSetBuilder::new();
3537
if let Some(exclude) = args.exclude {
36-
// We normalize the path so that the glob pattern
37-
// begins with the base path.
38-
// We also ensure that we normalize only if needed.
39-
let base = args.path.clone();
40-
let base = base.to_str().unwrap();
41-
let base = {
42-
if base.ends_with('/') {
43-
base.to_string()
44-
} else {
45-
format!("{}/", base)
46-
}
47-
};
4838
for glob in exclude {
49-
let excluded_paths_with_base = {
39+
// Edge case, if the path starts with '.' or './'
40+
let excluded_path = {
5041
let original_glob = glob.glob();
51-
if !original_glob.starts_with(&base) {
52-
if original_glob.starts_with('/') {
53-
let glob = original_glob.strip_prefix('/').unwrap().to_string();
54-
format!("{}{}", base, glob)
42+
if let Some(path_prefix) =
43+
utils::start_with_one_of(&args.path.to_str().unwrap(), &BASE_PATH_EDGE_CASES)
44+
{
45+
if let Some(glob_prefix) =
46+
utils::start_with_one_of(&original_glob, &BASE_PATH_EDGE_CASES)
47+
{
48+
original_glob.replacen(glob_prefix, path_prefix, 1)
5549
} else {
56-
let glob = original_glob.to_string();
57-
format!("{}{}", base, glob)
50+
format!("./{}", original_glob)
5851
}
5952
} else {
6053
original_glob.to_string()
6154
}
6255
};
63-
let glob = Glob::new(&excluded_paths_with_base).unwrap();
56+
let glob = Glob::new(&excluded_path).unwrap();
6457
excluded_paths.add(glob);
6558
}
6659
}

src/utils.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/// Checks if the given `snippet` starts with any of the provided `prefixes`.
2+
///
3+
/// # Arguments
4+
///
5+
/// * `snippet` - A string slice that will be checked.
6+
/// * `prefixes` - A slice of string slices representing the prefixes to check against.
7+
///
8+
/// # Returns
9+
///
10+
/// * `Some(prefix)` if the `snippet` starts with any of the `prefixes`, where `prefix` is the
11+
/// first matching prefix.
12+
/// * `None` otherwise.
13+
///
14+
/// # Examples
15+
///
16+
/// ```
17+
/// let snippet = "hello world";
18+
/// let prefixes = ["he", "wo"];
19+
/// assert_eq!(start_with_one_of(snippet, &prefixes), Some("he"));
20+
/// ```
21+
pub fn start_with_one_of<'a>(snippet: &str, prefixes: &[&'a str]) -> Option<&'a str> {
22+
for prefix in prefixes {
23+
if snippet.starts_with(prefix) {
24+
return Some(prefix);
25+
}
26+
}
27+
None
28+
}
29+
30+
/// Checks if the given `snippet` ends with any of the provided `suffixes`.
31+
///
32+
/// # Arguments
33+
///
34+
/// * `snippet` - A string slice that will be checked.
35+
/// * `suffixes` - A slice of string slices representing the suffixes to check against.
36+
///
37+
/// # Returns
38+
///
39+
/// * `Some(suffix)` if the `snippet` ends with any of the `suffixes`, where `suffix` is the
40+
/// first matching suffix.
41+
/// * `None` otherwise.
42+
///
43+
/// # Examples
44+
///
45+
/// ```
46+
/// let snippet = "hello world";
47+
/// let suffixes = ["ld", "lo"];
48+
/// assert_eq!(end_with_one_of(snippet, &suffixes), Some("ld"));
49+
/// ```
50+
pub fn end_with_one_of<'a>(snippet: &str, suffixes: &[&'a str]) -> Option<&'a str> {
51+
for suffix in suffixes {
52+
if snippet.ends_with(suffix) {
53+
return Some(suffix);
54+
}
55+
}
56+
None
57+
}

0 commit comments

Comments
 (0)