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
1 change: 1 addition & 0 deletions docs/public/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ Recursively scan a directory and parse all supported source files.
```rust
pub struct ParseOptions {
pub include_tests: bool, // Include test files (default: true)
pub respect_gitignore: bool, // Respect .gitignore files (default: true)
pub exclude: Vec<String>, // Glob patterns to exclude
pub max_file_size: usize, // Maximum file size in bytes (default: 10 MB)
}
Expand Down
4 changes: 4 additions & 0 deletions docs/public/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,17 @@ acb compile ./src --exclude="*test*" --exclude="vendor"

# Write coverage report
acb compile ./src --coverage-report coverage.json

# Parse files even if matched by .gitignore
acb compile ./src --no-gitignore
```

| Option | Description |
|--------|-------------|
| `-o, --output <path>` | Output file path (default: `<dirname>.acb` in current dir) |
| `-e, --exclude <glob>` | Glob patterns to exclude (may be repeated) |
| `--include-tests` | Include test files in compilation (default: true) |
| `--no-gitignore` | Disable `.gitignore` filtering during file discovery |
| `--coverage-report <path>` | Write ingestion coverage report JSON |

### `acb info`
Expand Down
2 changes: 2 additions & 0 deletions docs/public/command-surface.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,15 @@ acb budget
acb compile <repo-path> -o graph.acb
acb compile <repo-path> --exclude "target" --exclude "node_modules"
acb compile <repo-path> --coverage-report coverage.json
acb compile <repo-path> --no-gitignore
```

Common options:

- `--output <file.acb>`
- `--exclude <glob>` (repeatable)
- `--include-tests`
- `--no-gitignore`
- `--coverage-report <path>`

## `acb query` types
Expand Down
9 changes: 9 additions & 0 deletions src/cli/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ pub enum Command {
/// acb compile ./src
/// acb compile ./src -o myapp.acb
/// acb compile ./src --exclude="*test*" --exclude="vendor"
/// acb compile ./src --no-gitignore
#[command(alias = "build")]
Compile {
/// Path to the source directory to compile.
Expand All @@ -151,6 +152,10 @@ pub enum Command {
#[arg(long, default_value_t = true)]
include_tests: bool,

/// Disable `.gitignore` filtering during file discovery.
#[arg(long)]
no_gitignore: bool,

/// Write ingestion coverage report JSON to this path.
#[arg(long)]
coverage_report: Option<PathBuf>,
Expand Down Expand Up @@ -415,12 +420,14 @@ pub fn run(cli: Cli) -> Result<(), Box<dyn std::error::Error>> {
output,
exclude,
include_tests,
no_gitignore,
coverage_report,
}) => cmd_compile(
path,
output.as_deref(),
exclude,
*include_tests,
*no_gitignore,
coverage_report.as_deref(),
&cli,
),
Expand Down Expand Up @@ -1096,6 +1103,7 @@ fn cmd_compile(
output: Option<&std::path::Path>,
exclude: &[String],
include_tests: bool,
no_gitignore: bool,
coverage_report: Option<&Path>,
cli: &Cli,
) -> Result<(), Box<dyn std::error::Error>> {
Expand Down Expand Up @@ -1134,6 +1142,7 @@ fn cmd_compile(
// Build parse options.
let mut opts = ParseOptions {
include_tests,
respect_gitignore: !no_gitignore,
..ParseOptions::default()
};
for pat in exclude {
Expand Down
8 changes: 7 additions & 1 deletion src/parse/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ pub struct ParseOptions {
pub exclude: Vec<String>,
/// Include test files.
pub include_tests: bool,
/// Respect `.gitignore` files during file discovery.
pub respect_gitignore: bool,
/// Maximum file size to parse (bytes).
pub max_file_size: usize,
}
Expand All @@ -48,6 +50,7 @@ impl Default for ParseOptions {
"**/build/**".into(),
],
include_tests: true,
respect_gitignore: true,
max_file_size: 10 * 1024 * 1024, // 10MB
}
}
Expand Down Expand Up @@ -285,7 +288,10 @@ impl Parser {
let mut files = Vec::new();
let mut coverage = ParseCoverageStats::default();

let walker = WalkBuilder::new(root).hidden(true).git_ignore(true).build();
let walker = WalkBuilder::new(root)
.hidden(true)
.git_ignore(options.respect_gitignore)
.build();

for entry in walker {
let entry = match entry {
Expand Down
47 changes: 47 additions & 0 deletions tests/phase2_parsing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use std::path::Path;

use agentic_codebase::parse::{ParseOptions, Parser};
use agentic_codebase::types::{CodeUnitType, Language, Visibility};
use tempfile::TempDir;

// ============================================================
// Helper functions
Expand All @@ -25,6 +26,19 @@ fn parse_test_file(relative: &str) -> Vec<agentic_codebase::parse::RawCodeUnit>
parser.parse_file(&path, &content).expect("Parse failed")
}

fn create_gitignore_fixture() -> TempDir {
let dir = TempDir::new().expect("tempdir");
std::fs::create_dir_all(dir.path().join(".git").join("info")).expect("create .git/info");
std::fs::create_dir_all(dir.path().join("ignored")).expect("create ignored dir");

std::fs::write(dir.path().join(".gitignore"), "ignored/\n").expect("write .gitignore");
std::fs::write(dir.path().join("main.rs"), "pub fn root() {}\n").expect("write main.rs");
std::fs::write(dir.path().join("ignored").join("extra.rs"), "pub fn extra() {}\n")
.expect("write ignored file");

dir
}

fn find_unit_by_name<'a>(
units: &'a [agentic_codebase::parse::RawCodeUnit],
name: &str,
Expand Down Expand Up @@ -77,6 +91,7 @@ fn test_parse_options_default() {
let opts = ParseOptions::default();
assert!(opts.languages.is_empty());
assert!(opts.include_tests);
assert!(opts.respect_gitignore);
assert_eq!(opts.max_file_size, 10 * 1024 * 1024);
assert!(!opts.exclude.is_empty());
assert!(opts.exclude.iter().any(|e| e.contains("node_modules")));
Expand Down Expand Up @@ -841,6 +856,38 @@ fn test_parse_directory_exclude_tests() {
assert!(result.stats.files_skipped > 0 || result.stats.files_parsed > 0);
}

#[test]
fn test_parse_directory_respect_gitignore_toggle() {
let parser = Parser::new();
let fixture = create_gitignore_fixture();

let default_opts = ParseOptions::default();
let default_result = parser
.parse_directory(fixture.path(), &default_opts)
.expect("parse_directory with gitignore failed");
assert_eq!(default_result.stats.files_parsed, 1);
assert_eq!(default_result.stats.coverage.files_candidate, 1);

let no_gitignore_opts = ParseOptions {
respect_gitignore: false,
..Default::default()
};
let no_gitignore_result = parser
.parse_directory(fixture.path(), &no_gitignore_opts)
.expect("parse_directory without gitignore failed");

assert!(
no_gitignore_result.stats.coverage.files_seen >= default_result.stats.coverage.files_seen,
"disabling gitignore should not reduce files_seen"
);
assert!(
no_gitignore_result.stats.coverage.files_candidate
> default_result.stats.coverage.files_candidate,
"disabling gitignore should include ignored candidates"
);
assert_eq!(no_gitignore_result.stats.files_parsed, 2);
}

#[test]
fn test_parse_directory_stats() {
let parser = Parser::new();
Expand Down
85 changes: 85 additions & 0 deletions tests/phase6_cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,33 @@ mod tests {
dir
}

fn create_gitignore_fixture_dir() -> TempDir {
let dir = TempDir::new().unwrap();
fs::create_dir_all(dir.path().join(".git").join("info")).unwrap();
fs::create_dir_all(dir.path().join("ignored")).unwrap();

fs::write(dir.path().join(".gitignore"), "ignored/\n").unwrap();
fs::write(dir.path().join("main.rs"), "pub fn root() {}\n").unwrap();
fs::write(dir.path().join("ignored").join("extra.rs"), "pub fn extra() {}\n").unwrap();

dir
}

fn read_coverage_counts(path: &std::path::Path) -> (u64, u64) {
let raw = fs::read_to_string(path).unwrap();
let payload: serde_json::Value = serde_json::from_str(&raw).unwrap();
let coverage = payload.get("coverage").expect("coverage object");
let files_seen = coverage
.get("files_seen")
.and_then(|v| v.as_u64())
.expect("files_seen");
let files_candidate = coverage
.get("files_candidate")
.and_then(|v| v.as_u64())
.expect("files_candidate");
(files_seen, files_candidate)
}

/// Compile a sample directory and return the path to the .acb output.
fn compile_sample() -> (TempDir, PathBuf) {
let src_dir = create_sample_rust_dir();
Expand Down Expand Up @@ -160,6 +187,64 @@ fn test_cli_compile_with_exclude() {
assert!(acb_path.exists());
}

#[test]
fn test_cli_compile_no_gitignore() {
let src_dir = create_gitignore_fixture_dir();
let out_dir = TempDir::new().unwrap();

let default_acb = out_dir.path().join("default.acb");
let no_gitignore_acb = out_dir.path().join("no-gitignore.acb");
let default_cov = out_dir.path().join("default-coverage.json");
let no_gitignore_cov = out_dir.path().join("no-gitignore-coverage.json");

let default_output = Command::new(acb_bin())
.args([
"compile",
src_dir.path().to_str().unwrap(),
"-o",
default_acb.to_str().unwrap(),
"--coverage-report",
default_cov.to_str().unwrap(),
])
.output()
.unwrap();
assert!(
default_output.status.success(),
"compile default failed: {}",
String::from_utf8_lossy(&default_output.stderr)
);

let no_gitignore_output = Command::new(acb_bin())
.args([
"compile",
src_dir.path().to_str().unwrap(),
"-o",
no_gitignore_acb.to_str().unwrap(),
"--no-gitignore",
"--coverage-report",
no_gitignore_cov.to_str().unwrap(),
])
.output()
.unwrap();
assert!(
no_gitignore_output.status.success(),
"compile --no-gitignore failed: {}",
String::from_utf8_lossy(&no_gitignore_output.stderr)
);

let (default_seen, default_candidate) = read_coverage_counts(&default_cov);
let (no_gitignore_seen, no_gitignore_candidate) = read_coverage_counts(&no_gitignore_cov);

assert!(
no_gitignore_seen >= default_seen,
"--no-gitignore should not reduce files_seen (default={default_seen}, no_gitignore={no_gitignore_seen})"
);
assert!(
no_gitignore_candidate > default_candidate,
"--no-gitignore should include ignored candidates (default={default_candidate}, no_gitignore={no_gitignore_candidate})"
);
}

#[test]
fn test_cli_compile_output_flag() {
let src_dir = create_sample_rust_dir();
Expand Down