This commit is contained in:
2025-11-25 05:51:55 +01:00
parent de7e1abcba
commit b09e3ec0e1
3 changed files with 107 additions and 90 deletions

View File

@@ -1,6 +1,8 @@
module codewalker module codewalker
import arrays import arrays
import os
import incubaid.herolib.core.pathlib
// Default ignore patterns based on .gitignore conventions // Default ignore patterns based on .gitignore conventions
const default_gitignore = ' const default_gitignore = '
@@ -48,8 +50,63 @@ Thumbs.db
*.log *.log
' '
pub fn find_ignore_patterns() []string { // find_ignore_patterns collects all .gitignore patterns from current directory up to repository root
//
// Walks up the directory tree using parent_find_advanced to locate all .gitignore files,
// stopping when it encounters the .git directory (repository root).
// Patterns are collected from:
// 1. Default ignore patterns (built-in)
// 2. All .gitignore files found from current directory to repository root
// 3. Filter out comments (lines starting with '#') and empty lines
//
// Parameters:
// - start_path: Optional starting directory path (defaults to current working directory if empty)
//
// Returns:
// - Combined, sorted, unique ignore patterns from all sources
// - Error if path operations fail (file not found, permission denied, etc.)
//
// Examples:
// // Use current working directory
// patterns := find_ignore_patterns()!
//
// // Use specific project directory
// patterns := find_ignore_patterns('/home/user/myproject')!
pub fn find_ignore_patterns(start_path string) ![]string {
mut patterns := default_gitignore.split_into_lines() mut patterns := default_gitignore.split_into_lines()
// Use provided path or current working directory
mut search_from := start_path
if search_from == '' { // If an empty string was passed for start_path, use current working directory
search_from = os.getwd()
}
mut current_path := pathlib.get(search_from)
// Find all .gitignore files up the tree until we hit .git directory (repo root)
mut gitignore_paths := current_path.parent_find_advanced('.gitignore', '.git')!
// Read and collect patterns from all found .gitignore files
for mut gitignore_path in gitignore_paths {
if gitignore_path.is_file() {
content := gitignore_path.read() or {
// Skip files that can't be read (permission issues, etc.)
continue
}
gitignore_lines := content.split_into_lines()
for line in gitignore_lines {
trimmed := line.trim_space()
// Skip empty lines and comment lines
if trimmed != '' && !trimmed.starts_with('#') {
patterns << trimmed
}
}
}
}
// Sort and get unique patterns to remove duplicates
patterns.sort() patterns.sort()
patterns = arrays.uniq(patterns) patterns = arrays.uniq(patterns)

View File

@@ -13,25 +13,17 @@ fn filemap_get_from_path(path string, content_read bool) !FileMap {
source: path source: path
} }
ignore_patterns := find_ignore_patterns(path)!
// List all files using pathlib with both default and custom ignore patterns // List all files using pathlib with both default and custom ignore patterns
mut file_list := dir.list( mut file_list := dir.list(
recursive: true recursive: true
ignore_default: true filter_ignore: ignore_patterns
regex_ignore: ignore_patterns
)! )!
// Process files with additional scoped ignore checking
for mut file in file_list.paths { for mut file in file_list.paths {
if file.is_file() { if file.is_file() {
relpath := file.path_relative(path)! relpath := file.path_relative(path)!
// Check scoped ignore patterns (from .gitignore/.heroignore in subdirectories)
if cw.scoped_ignore.is_ignored(relpath) {
continue
}
if content_read { if content_read {
content := file.read()! content := file.read()!
fm.content[relpath] = content fm.content[relpath] = content
@@ -44,84 +36,8 @@ fn filemap_get_from_path(path string, content_read bool) !FileMap {
return fm return fm
} }
// load_ignore_files reads .gitignore and .heroignore files and builds scoped patterns
fn (mut cw CodeWalker) load_ignore_files(root_path string) ! {
mut root := pathlib.get(root_path)
if !root.is_dir() {
return
}
// List all files to find ignore files
mut all_files := root.list(
recursive: true
ignore_default: false
)!
for mut p in all_files.paths {
if p.is_file() {
name := p.name()
if name == '.gitignore' || name == '.heroignore' {
relpath := p.path_relative(root_path)!
// Get the directory containing this ignore file
mut scope := relpath
if scope.contains('/') {
scope = scope.all_before_last('/')
} else {
scope = ''
}
content := p.read()!
cw.scoped_ignore.add_for_scope(scope, content)
}
}
}
}
// parse_header robustly extracts block type and filename from header line
// Handles variable `=` count, spaces, and case-insensitivity
// Example: ` ===FILE: myfile.txt ===` $(BlockKind.file, "myfile.txt")
fn parse_header(line string) !(BlockKind, string) {
cleaned := line.trim_space()
// Must have = and content
if !cleaned.contains('=') {
return BlockKind.end, ''
}
// Strip leading and trailing = (any count), preserving spaces between
mut content := cleaned.trim_left('=').trim_space()
content = content.trim_right('=').trim_space()
if content.len == 0 {
return BlockKind.end, ''
}
// Check for END marker
if content.to_lower() == 'end' {
return BlockKind.end, ''
}
// Parse FILE or FILECHANGE
if content.contains(':') {
kind_str := content.all_before(':').to_lower().trim_space()
filename := content.all_after(':').trim_space()
if filename.len < 1 {
return error('Invalid filename: empty after colon')
}
match kind_str {
'file' { return BlockKind.file, filename }
'filechange' { return BlockKind.filechange, filename }
else { return BlockKind.end, '' }
}
}
return BlockKind.end, ''
}
// filemap_get_from_content parses FileMap from string with ===FILE:name=== format // filemap_get_from_content parses FileMap from string with ===FILE:name=== format
fn (mut cw CodeWalker) filemap_get_from_content(content string) !FileMap { fn filemap_get_from_content(content string) !FileMap {
mut fm := FileMap{} mut fm := FileMap{}
mut current_kind := BlockKind.end mut current_kind := BlockKind.end

View File

@@ -0,0 +1,44 @@
module codewalker
// parse_header robustly extracts block type and filename from header line
// Handles variable `=` count, spaces, and case-insensitivity
// Example: ` ===FILE: myfile.txt ===` $(BlockKind.file, "myfile.txt")
fn parse_header(line string) !(BlockKind, string) {
cleaned := line.trim_space()
// Must have = and content
if !cleaned.contains('=') {
return BlockKind.end, ''
}
// Strip leading and trailing = (any count), preserving spaces between
mut content := cleaned.trim_left('=').trim_space()
content = content.trim_right('=').trim_space()
if content.len == 0 {
return BlockKind.end, ''
}
// Check for END marker
if content.to_lower() == 'end' {
return BlockKind.end, ''
}
// Parse FILE or FILECHANGE
if content.contains(':') {
kind_str := content.all_before(':').to_lower().trim_space()
filename := content.all_after(':').trim_space()
if filename.len < 1 {
return error('Invalid filename: empty after colon')
}
match kind_str {
'file' { return BlockKind.file, filename }
'filechange' { return BlockKind.filechange, filename }
else { return BlockKind.end, '' }
}
}
return BlockKind.end, ''
}