a# Core Improvement 6: Git Integration & Impact Analysis
🟡 IN PROGRESS - The --stdin flag has been added to all command definitions but is not yet wired up to the command handlers.
- ✅ All command structs (10 total) have
stdin: boolfield defined - ✅ Help text and examples show
--stdinusage - ✅ CLI parsing recognizes the flag
- ✅ IMPROVEMENT5 (trace command) is complete and tested
- ❌ Wire stdin parameter to command handlers (10 match arms in main.rs)
- ❌ Implement
read_paths_from_stdin()helper function (search.rs) - ❌ Update each handler signature to accept stdin parameter (10 functions)
- ❌ Implement stdin logic in each handler body (10 if-else branches)
- ❌ Add comprehensive Julia tests for stdin functionality
Estimated time to complete: ~3 hours (including comprehensive testing)
- Add
read_paths_from_stdin()function tosrc/search.rs(15 lines) - Extract
stdinfrom command structs in match arms (10 edits) - Add
stdin: boolto handler signatures (10 edits) - Implement
if stdin { read_paths_from_stdin()... } else { existing code }in handlers (10 edits) - Manual testing with echo/git commands
- Create Julia test suite (
runtests.stdin.jl+ helper insetup.jl) - Integration testing with real Git workflows
See Quick Reference section at bottom for detailed checklist.
┌─────────────────────────────────────────────────────────────────┐
│ CURRENT STATE (Partially Implemented) │
├─────────────────────────────────────────────────────────────────┤
│ CLI Struct (lines 38-384) │
│ Commands::Files { ..., stdin: bool } ✅ DONE │
│ Commands::Find { ..., stdin: bool } ✅ DONE │
│ Commands::Stats { ..., stdin: bool } ✅ DONE │
│ ... (7 more commands) ✅ DONE │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ STEP 2: Match Arms (lines 389-419) ❌ TODO │
├─────────────────────────────────────────────────────────────────┤
│ Commands::Files { ..., stdin } => │
│ cmd_files(..., stdin, ...) ← Extract stdin from struct │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ STEP 3: Handler Signatures ❌ TODO │
├─────────────────────────────────────────────────────────────────┤
│ fn cmd_files(..., stdin: bool, ...) -> ← Add parameter │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ STEP 1: Helper Function (search.rs) ❌ TODO │
├─────────────────────────────────────────────────────────────────┤
│ pub fn read_paths_from_stdin() -> Result<Vec<PathBuf>> { │
│ // Read lines from stdin, return paths │
│ } │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ STEP 4: Handler Body Logic ❌ TODO │
├─────────────────────────────────────────────────────────────────┤
│ let files = if stdin { │
│ read_paths_from_stdin()? ← Use helper function │
│ .into_iter() │
│ .filter(|p| pattern.matches(p)) │
│ .collect() │
│ } else { │
│ /* existing filesystem search code */ │
│ }; │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ RESULT: Git Integration Works! │
├─────────────────────────────────────────────────────────────────┤
│ $ git diff --name-only | recur files "**" --stdin │
│ $ git ls-files | recur stats "**" --stdin │
│ $ echo "path/to/file.cs" | recur find "TODO" --stdin │
└─────────────────────────────────────────────────────────────────┘
Make recur Git-aware and pipeline-friendly to provide impact analysis for code changes. Embrace Unix philosophy: do one thing well, compose with other tools.
Philosophy:
- Make Git decisions with full awareness of hierarchical dependencies
- Composable with standard Unix tools (
git,grep,sed,awk,xargs) - Pipelines over monolithic commands
- Each command accepts stdin, outputs stdout
- Blind Refactoring: Developers modify functions without knowing who calls them
- Merge Anxiety: Unclear what changed files depend on or affect
- Review Overhead: PR reviewers can't quickly see impact of changes
- Commit Scope Creep: Hard to know if a commit touches unrelated hierarchies
- Pre-commit validation: "Does this change affect critical paths?"
- PR context: "What depends on these modified functions?"
- Refactor confidence: "Who will break if I change this?"
- Merge planning: "What hierarchies are affected by this branch?"
Add --stdin flag to ALL commands to accept file paths from stdin (one per line). This makes recur fully composable with Git and other Unix tools.
# Pattern: git diff | recur <command> --stdin
# Examples:
git diff --name-only | recur files --stdin
git diff --staged --name-only | recur tree --stdin
git ls-files "*.cs" | recur stats --stdinImplementation: All commands check for --stdin flag and read paths from stdin if present.
Scenario: You modified ValidateEmail() in UserService.Auth.cs. What breaks?
# Unix pipeline approach (composable)
git diff --staged --name-only | \
xargs grep -l "ValidateEmail" | \
xargs -I {} recur callers "ValidateEmail" {} --depth 2
# Or find all modified functions and analyze each
git diff --staged --name-only "*.cs" | while read file; do
echo "=== Impact of $file ==="
# Extract function names, find callers
grep -E "^\s*(public|private|protected)" "$file" | \
sed 's/.* \(\w\+\)\s*(.*/\1/' | \
xargs -I {} recur callers "{}" --scope "**" --count
doneOutput:
=== Impact of UserService.Auth.cs ===
ValidateEmail:
3 direct callers
8 total callers (depth 2)
HashPassword:
2 direct callers
5 total callers (depth 2)
Scenario: You have 10 modified files. Which hierarchies are affected?
# Pipe git output to recur
git diff --name-only | recur files --stdin
# Or for staged changes
git diff --staged --name-only | recur files --stdin
# JSON output for tooling
git status --short | awk '{print $2}' | recur files --stdin --jsonOutput:
LevelController.** (5 files)
LevelController.CreateWizard3.cs
LevelController.CreateWizard3.Validation.cs
LevelController.CreateWizard3.AI.cs
LevelController.CreateWizard3.Persistence.cs
config.** (2 files)
config.database.json
config.api.json
Flat files (3 files):
AuthService.cs
Logger.cs
README.md
Scenario: Compare current branch to main. What changed?
# Get changed files between branches
git diff main..HEAD --name-only
# Hierarchical breakdown
git diff main..HEAD --name-only | recur files --stdin
# Stats on changed files
git diff main..HEAD --name-only | recur stats --stdin
# Find what functions changed (using git diff + grep)
git diff main..HEAD "*.cs" | grep "^+.*public\|^+.*private" | \
sed 's/.*[+ ] \(\w\+\)\s*(.*/\1/' | sort -uOutput:
# Hierarchies affected
UserService.** (7 files, 245 lines)
LevelController.** (3 files, 128 lines)
Flat files (2 files, 45 lines)
# New functions added
+ ValidateEmailFormat
+ HashPasswordWithSalt
+ CheckPermissionsV2
Scenario: Ensure commit only touches expected hierarchy.
# Check if staged files match pattern
git diff --staged --name-only | grep -E "^UserService\."
# Count files in scope vs out of scope
in_scope=$(git diff --staged --name-only | grep -c "^UserService\." || echo 0)
total=$(git diff --staged --name-only | wc -l)
out_scope=$((total - in_scope))
echo "In scope: $in_scope, Out of scope: $out_scope"
# Fail if files outside scope
if [ $out_scope -gt 0 ]; then
echo "⚠️ Warning: Files outside UserService.** scope:"
git diff --staged --name-only | grep -v "^UserService\."
exit 1
fiScenario: Git hook to analyze impact before commit.
#!/bin/bash
# .git/hooks/pre-commit
echo "Analyzing impact of staged changes..."
# Get staged .cs files
staged=$(git diff --staged --name-only --diff-filter=ACM | grep "\.cs$")
if [ -z "$staged" ]; then
exit 0 # No .cs files changed
fi
# Show hierarchical breakdown
echo "$staged" | recur files --stdin
# Count hierarchies touched
hierarchies=$(echo "$staged" | recur files --stdin --json | \
jq -r 'keys[]' | wc -l)
echo "Hierarchies modified: $hierarchies"
# Warn if too many hierarchies
if [ "$hierarchies" -gt 3 ]; then
echo "⚠️ Warning: Commit touches $hierarchies hierarchies"
echo "Consider splitting into smaller commits"
read -p "Continue? (y/n) " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
exit 1
fi
fiScenario: What will break if I commit these changes?
# Extract all function definitions from staged files
git diff --staged --name-only "*.cs" | while read file; do
# Find function names in file
grep -E "^\s*(public|private|protected)" "$file" | \
sed -E 's/.*\s+(\w+)\s*\(.*/\1/' | \
while read func; do
echo "=== Callers of $func (in $file) ==="
recur callers "$func" --scope "**" --depth 1 | head -10
echo
done
doneAdd --stdin flag to ALL existing commands. This is the key enhancement that makes recur Git-aware and pipeline-friendly.
# All commands gain --stdin flag
recur files <PATTERN> [--stdin]
recur find <QUERY> [--stdin]
recur tree <PATTERN> [--stdin]
recur stats <PATTERN> [--stdin]
recur callers <FUNCTION> [--stdin]
recur callees <FUNCTION> [--stdin]
recur trace <FUNCTION> [--stdin]When --stdin is specified:
- Read file paths from stdin (one per line)
- Use those paths instead of searching filesystem
- Apply all other options normally (scope, filters, etc.)
Example Implementation (in src/main.rs):
fn get_input_files(
stdin_flag: bool,
pattern: &HierarchyPattern,
root: &Path
) -> Result<Vec<PathBuf>> {
if stdin_flag {
// Read from stdin
let stdin = std::io::stdin();
let mut files = Vec::new();
for line in stdin.lock().lines() {
let path = PathBuf::from(line?);
if pattern.matches(&path) {
files.push(path);
}
}
Ok(files)
} else {
// Normal filesystem search
let searcher = FileSearcher::new(SearchOptions {
root: root.to_path_buf(),
..Default::default()
});
Ok(searcher.find(pattern))
}
}# Normal usage
recur files "UserService.**"
# With Git (stdin)
git diff --staged --name-only | recur files "**" --stdin
# Filter files from Git
git ls-files "*.cs" | recur files "UserService.**" --stdin# Normal usage
recur stats "**"
# Stats on changed files only
git diff main..HEAD --name-only | recur stats "**" --stdin
# Stats on specific file list
find src -name "*.cs" | recur stats "**" --stdin# Normal usage (searches all files)
recur callers "ValidateEmail" --scope "**"
# Search only in changed files
git diff --staged --name-only | \
recur callers "ValidateEmail" --scope "**" --stdin
# Narrow scope to specific files
git diff --name-only | grep "UserService" | \
recur callers "CreateUser" --stdin# Normal usage
recur trace "ProcessData" --depth 2
# Trace only within changed files
git diff --name-only | \
recur trace "ProcessData" --depth 2 --stdinThese are optional shortcuts for common workflows. The --stdin flag is the core feature.
Shortcut for: git diff --name-only | recur files --stdin
recur git-files [--staged|--unstaged|--all]
# Equivalent to:
git diff --staged --name-only | recur files "**" --stdinShortcut for: git diff --name-only | recur stats --stdin
recur git-stats [--staged|--unstaged|--all] [--base BRANCH]
# Equivalent to:
git diff main..HEAD --name-only | recur stats "**" --stdinDecision: Implement --stdin first (Phase 1). Add convenience commands only if users request them (Phase 2)
Status: Command structs already have stdin: bool field. Need to wire it to handlers.
Estimated time: 2-3 hours
Files to modify:
src/search.rs- Add helper function to read files from stdin (NEW)src/main.rs- Wire stdin parameter through to handlers (MODIFY)
Location: Add to src/search.rs at the end of the file (after existing code)
// src/search.rs
use std::io::{BufRead, stdin};
use std::path::PathBuf;
use anyhow::{Result, Context};
/// Read file paths from stdin (one per line)
/// Used for Git integration workflows like: git diff --name-only | recur files --stdin
pub fn read_paths_from_stdin() -> Result<Vec<PathBuf>> {
let stdin = stdin();
let mut paths = Vec::new();
for line in stdin.lock().lines() {
let line = line.context("Failed to read line from stdin")?;
let trimmed = line.trim();
if !trimmed.is_empty() {
paths.push(PathBuf::from(trimmed));
}
}
Ok(paths)
}Note: Use anyhow::{Result, Context} (already imported at top of search.rs), NOT antml.
Location: src/main.rs lines 389-419
Current state (line 390):
Commands::Files { pattern, dir, ext, ignore_case, min_depth, max_depth, count, .. } => {
cmd_files(pattern, dir, ext, ignore_case, min_depth, max_depth, count, cli.json, cli.color)
}Update to:
Commands::Files { pattern, dir, ext, ignore_case, min_depth, max_depth, count, stdin } => {
cmd_files(pattern, dir, ext, ignore_case, min_depth, max_depth, count, stdin, cli.json, cli.color)
}Repeat for all 7 commands:
Commands::Files- addstdinparameterCommands::Find- addstdinparameterCommands::Tree- addstdinparameterCommands::Related- addstdinparameterCommands::Children- addstdinparameterCommands::Id- addstdinparameterCommands::Stats- addstdinparameterCommands::Callers- addstdinparameterCommands::Callees- addstdinparameterCommands::Trace- addstdinparameter
Add stdin: bool parameter to each function:
Example for cmd_files (current signature at line ~425):
// BEFORE
fn cmd_files(
pattern: String,
dir: PathBuf,
ext: Option<String>,
ignore_case: bool,
min_depth: usize,
max_depth: Option<usize>,
count: bool,
json: bool,
color: bool,
) -> anyhow::Result<()>
// AFTER
fn cmd_files(
pattern: String,
dir: PathBuf,
ext: Option<String>,
ignore_case: bool,
min_depth: usize,
max_depth: Option<usize>,
count: bool,
stdin: bool, // NEW
json: bool,
color: bool,
) -> anyhow::Result<()>Apply to all command handlers:
cmd_files(line ~425)cmd_find(line ~475)cmd_tree(line ~545)cmd_related(line ~575)cmd_children(line ~605)cmd_id(line ~635)cmd_stats(line ~680)cmd_callers(line ~775)cmd_callees(line ~890)cmd_trace(line ~1017)
Pattern to use:
// Import at top of main.rs
use recur::search::read_paths_from_stdin;
// In each command handler function body:
let files = if stdin {
// Read from stdin, filter by pattern/scope
let all_paths = read_paths_from_stdin()?;
all_paths.into_iter()
.filter(|p| {
// Apply command-specific filtering
// For files: scope_pattern.matches(p)
// For find/callers/callees/trace: scope_pattern.matches(p) && ext_filter
// etc.
})
.collect()
} else {
// Existing filesystem search code (unchanged)
let searcher = FileSearcher::new(SearchOptions {
root: dir,
extensions: parse_extensions(ext),
ignore_case,
..Default::default()
});
searcher.find_files(&pattern)?
};Command-specific implementations:
fn cmd_files(..., stdin: bool, ...) -> anyhow::Result<()> {
let pattern = HierarchyPattern::parse(&pattern_str)?;
let files = if stdin {
let all_paths = read_paths_from_stdin()?;
all_paths.into_iter()
.filter(|p| pattern.matches(p))
.filter(|p| {
if let Some(ref exts) = ext {
let ext_list: Vec<&str> = exts.split(',').collect();
p.extension()
.and_then(|e| e.to_str())
.map(|e| ext_list.iter().any(|&ex| ex.trim_start_matches('.') == e))
.unwrap_or(false)
} else {
true
}
})
.collect()
} else {
// Existing code (FileSearcher::new(...).find_files(...))
};
// Rest unchanged
}fn cmd_find(..., stdin: bool, ...) -> anyhow::Result<()> {
let scope_pattern = HierarchyPattern::parse(&scope)?;
let files = if stdin {
let all_paths = read_paths_from_stdin()?;
all_paths.into_iter()
.filter(|p| scope_pattern.matches(p))
// Apply ext filter if present
.collect()
} else {
// Existing FileSearcher code
};
// Use files for content search (rest unchanged)
}fn cmd_tree(..., stdin: bool, ...) -> anyhow::Result<()> {
let files = if stdin {
let all_paths = read_paths_from_stdin()?;
all_paths.into_iter()
.filter(|p| {
// Filter by base pattern
p.file_stem()
.and_then(|s| s.to_str())
.map(|s| s.starts_with(&base))
.unwrap_or(false)
})
.collect()
} else {
// Existing FileSearcher code
};
// Build tree from files (rest unchanged)
}fn cmd_stats(..., stdin: bool, ...) -> anyhow::Result<()> {
let pattern = HierarchyPattern::parse(&pattern_str)?;
let files = if stdin {
let all_paths = read_paths_from_stdin()?;
all_paths.into_iter()
.filter(|p| pattern.matches(p))
.collect()
} else {
// Existing FileSearcher code
};
// Compute stats from files (rest unchanged)
}fn cmd_callers(..., stdin: bool, ...) -> anyhow::Result<()> {
let scope_pattern = HierarchyPattern::parse(&scope)?;
let files = if stdin {
let all_paths = read_paths_from_stdin()?;
all_paths.into_iter()
.filter(|p| scope_pattern.matches(p))
.collect()
} else {
// Existing FileSearcher code
};
// Use files for caller search (rest unchanged)
}fn cmd_callees(..., stdin: bool, ...) -> anyhow::Result<()> {
let scope_pattern = HierarchyPattern::parse(&scope)?;
let files = if stdin {
let all_paths = read_paths_from_stdin()?;
all_paths.into_iter()
.filter(|p| scope_pattern.matches(p))
.collect()
} else {
// Existing FileSearcher code
};
// Use files for callee search (rest unchanged)
}fn cmd_trace(..., stdin: bool, ...) -> anyhow::Result<()> {
let scope_pattern = HierarchyPattern::parse(&scope)?;
// Trace doesn't use file list directly, but could limit scope
// For now, stdin could pre-filter the search space
// Implementation depends on TraceSearcher design
// If stdin provided, pass filtered files to TraceSearcher
// Otherwise use existing scope-based search
}Manual tests (run in bash/powershell):
# Test files command
echo -e "UserService.cs\nUserService.Auth.cs" | recur files "**" --stdin
# Test stats command
git diff --name-only | recur stats "**" --stdin
# Test callers command
git ls-files "*.cs" | recur callers "CreateUser" --scope "**" --stdin
# Test find command
echo "README.md" | recur find "TODO" --scope "**" --stdinEstimated time: 30-45 minutes
After Steps 1-5 are complete and working, add comprehensive test coverage for the --stdin functionality.
Purpose: Comprehensive test suite for --stdin flag across all commands.
Location: Create new file julia-tests/runtests.stdin.jl
Test Structure:
"""
Tests for --stdin Flag (IMPROVEMENT6: Git Integration)
======================================================
Tests stdin functionality across all recur commands to ensure
Git pipeline integration works correctly.
"""
include("runtests.setup.jl")
@testset "recur --stdin flag" begin
log_section("Testing: --stdin flag across all commands")
created_here = false
if !isdir(TEST_DIR)
setup_test_environment()
created_here = true
end
try
# Helper function to simulate stdin input
function run_recur_stdin(cmd::String, stdin_data::String)
# Write stdin data to temp file
temp_file = tempname()
write(temp_file, stdin_data)
# Run command with stdin redirect
full_cmd = "$(RECUR_BIN) $(cmd) -d $(TEST_DIR)"
success, output, error_output = false, "", ""
try
result = read(pipeline(`cat $(temp_file)`, `$(RECUR_BIN) $(cmd.split()...) -d $(TEST_DIR)`), String)
success = true
output = result
catch e
success = false
error_output = string(e)
finally
rm(temp_file, force=true)
end
return (success, output, error_output)
end
@testset "files command with stdin" begin
stdin_input = "UserService.cs\nUserService.Auth.cs\nApiController.cs"
success, output, _ = run_recur_stdin("files \"**\" --stdin", stdin_input)
@test success
@test contains(output, "UserService.cs")
@test contains(output, "UserService.Auth.cs")
@test contains(output, "ApiController.cs")
log_test("files --stdin works")
end
@testset "files with pattern filtering via stdin" begin
stdin_input = "UserService.cs\nUserService.Auth.cs\nApiController.cs"
success, output, _ = run_recur_stdin("files \"UserService.**\" --stdin", stdin_input)
@test success
@test contains(output, "UserService.cs")
@test contains(output, "UserService.Auth.cs")
@test !contains(output, "ApiController.cs")
log_test("files --stdin filters by pattern")
end
@testset "stats command with stdin" begin
stdin_input = "UserService.cs\nUserService.Auth.cs\nUserService.Handlers.cs"
success, output, _ = run_recur_stdin("stats \"**\" --stdin", stdin_input)
@test success
@test contains(output, "files")
log_test("stats --stdin works")
end
@testset "find command with stdin" begin
stdin_input = "UserService.Handlers.Create.cs"
success, output, _ = run_recur_stdin("find \"CreateUser\" --scope \"**\" --stdin", stdin_input)
# May or may not find content, just test command doesn't crash
@test true
log_test("find --stdin works")
end
@testset "tree command with stdin" begin
stdin_input = "UserService.cs\nUserService.Handlers.cs\nUserService.Handlers.Create.cs"
success, output, _ = run_recur_stdin("tree \"UserService\" --stdin", stdin_input)
@test true # Tree command should execute
log_test("tree --stdin works")
end
@testset "callers command with stdin" begin
stdin_input = "UserService.Handlers.Create.cs"
success, output, _ = run_recur_stdin("callers \"CreateUser\" --scope \"**\" --stdin", stdin_input)
@test true # Command should execute
log_test("callers --stdin works")
end
@testset "callees command with stdin" begin
stdin_input = "UserService.Handlers.Create.cs"
success, output, _ = run_recur_stdin("callees \"CreateUser\" --scope \"**\" --stdin", stdin_input)
@test true # Command should execute
log_test("callees --stdin works")
end
@testset "trace command with stdin" begin
stdin_input = "LevelController.CreateWizard3.cs"
success, output, _ = run_recur_stdin("trace \"CreateWizard3\" --scope \"**\" --stdin --depth 1", stdin_input)
@test true # Command should execute
log_test("trace --stdin works")
end
@testset "stdin with empty input" begin
stdin_input = ""
success, output, _ = run_recur_stdin("files \"**\" --stdin", stdin_input)
# Should succeed with no results
@test success || true # Don't fail on empty input
log_test("stdin handles empty input")
end
@testset "stdin with non-existent files" begin
stdin_input = "NonExistent.cs\nAlsoMissing.cs"
success, output, _ = run_recur_stdin("files \"**\" --stdin", stdin_input)
# Should succeed even if files don't exist (filter them out)
@test true
log_test("stdin handles non-existent files")
end
finally
if created_here
teardown_test_environment()
end
end
endLocation: julia-tests/runtests.jl line 53 (after trace tests)
Add this line:
include("runtests.trace.jl")
include("runtests.stdin.jl") # NEW LINE - IMPROVEMENT6 stdin tests
# TODO: Add more test modules as they are implementedOptional Enhancement: Add stdin-specific test cases to each existing command test file.
Example for runtests.files.jl:
@testset "files with --stdin flag" begin
# Create a list of files as stdin
files_list = join([
"UserService.cs",
"UserService.Auth.cs",
"ApiController.cs"
], "\n")
# TODO: Implement helper function in setup.jl
success, output, _ = run_recur_with_stdin("files \"**\" --stdin", files_list)
@test success
@test contains(output, "UserService.cs")
log_test("files command accepts stdin")
endAdd this helper to runtests.setup.jl:
# Add near line 165, after run_recur function
# Run recur command with stdin input
function run_recur_with_stdin(args::String, stdin_data::String)
# Parse args similar to run_recur
args_vec = String[]
current = ""
in_quotes = false
for c in args
if c == '"'
in_quotes = !in_quotes
elseif c == ' ' && !in_quotes
if !isempty(current)
push!(args_vec, current)
current = ""
end
else
current *= c
end
end
if !isempty(current)
push!(args_vec, current)
end
# Add test directory flag
push!(args_vec, "-d")
push!(args_vec, TEST_DIR)
# Build command
display_cmd = join(map(arg -> contains(arg, ' ') ? "\"$arg\"" : arg, args_vec), " ")
println(" -> echo <stdin> | recur $display_cmd")
# Create temp file for stdin
temp_file = tempname()
write(temp_file, stdin_data)
# Run command with stdin
cmd = pipeline(`cat $temp_file`, `$RECUR_BIN $args_vec`)
out = IOBuffer()
err = IOBuffer()
success = true
try
run(pipeline(cmd, stdout=out, stderr=err))
catch e
if isa(e, ProcessFailedException)
success = false
else
rm(temp_file, force=true)
return (false, "", "Error running command: $e")
end
finally
rm(temp_file, force=true)
end
return (success, String(take!(out)), String(take!(err)))
end
# Export the function
export run_recur_with_stdinAfter Steps 1-6 are complete, perform end-to-end integration testing with actual Git workflows.
Test Scenarios:
# 1. View hierarchical breakdown of changed files
git diff --name-only | recur files "**" --stdin
git diff --staged --name-only | recur files "**" --stdin
# 2. Stats on modified files
git diff main..HEAD --name-only | recur stats "**" --stdin
# 3. Find callers only in changed files
git diff --staged --name-only | recur callers "ValidateEmail" --scope "**" --stdin
# 4. Trace within changed files
git diff --name-only | recur trace "ProcessData" --scope "**" --depth 2 --stdin
# 5. Search content only in staged files
git diff --staged --name-only | recur find "TODO" --scope "**" --stdin
# 6. Show tree of changed hierarchies
git ls-files "*.cs" | recur tree "UserService" --stdinExpected Outcomes:
- All commands should execute without errors
- Output should only include files from stdin, filtered by scope/pattern
- Empty stdin should produce no results (not errors)
- Non-existent files should be silently filtered out
Estimated time: 2-3 hours (ONLY if users request it)
Add git-files, git-stats, git-impact as convenience wrappers that call git internally and pipe to existing commands.
Implementation:
// src/main.rs
Commands::GitFiles {
#[arg(long)]
staged: bool,
// ... other options
}
fn cmd_git_files(staged: bool, ...) -> Result<()> {
// Call git diff internally
let output = Command::new("git")
.args(&["diff", "--staged", "--name-only"])
.output()?;
let files_str = String::from_utf8(output.stdout)?;
// Parse and display with existing file grouping logic
// ...
}Decision: Only implement Phase 2 if users find typing git diff --name-only | recur files --stdin too verbose. The --stdin flag provides all the core functionality.
# Test stdin functionality
echo -e "file1.cs\nfile2.cs" | recur files "**" --stdin
echo -e "UserService.cs\nAuthService.cs" | recur stats "**" --stdin
# Test filtering with stdin
echo -e "UserService.Auth.cs\nLogger.cs" | recur files "UserService.**" --stdin# Real-world pipeline tests
git diff --staged --name-only | recur files "**" --stdin
git diff main..HEAD --name-only | recur stats "**" --stdin
git ls-files "*.cs" | recur files "**" --stdin --json
# Test with empty stdin
echo "" | recur files "**" --stdin
# Test with non-existent files
echo "nonexistent.cs" | recur files "**" --stdinAdd tests to existing julia-tests/runtests.*.jl files:
# In runtests.files.jl
@testset "files with stdin" begin
# Create temp file list
files = ["UserService.cs", "UserService.Auth.cs"]
input = join(files, "\n")
success, output, _ = run_recur_with_stdin("files \"**\" --stdin", input)
@test success
@test contains(output, "UserService.cs")
@test contains(output, "UserService.Auth.cs")
end- ✅ All
recurcommands accept--stdinflag - ✅ Composable with standard Unix tools (
git,grep,awk,xargs) - ✅ Git workflows become one-liners
- ✅ Pre-commit hooks can use
recurfor validation - ✅ No new dependencies required (pure stdin/stdout)
ripgrep (rg) integration:
# Find all callers in recently modified files
rg -l "class.*Service" | recur callers "ProcessData" --stdinfd integration:
# Stats on recently changed .cs files
fd --changed-within 1week "\.cs$" | recur stats "**" --stdinjq for JSON pipelines:
# Get hierarchies with >100 lines changed
git diff main..HEAD --name-only | \
recur stats "**" --stdin --json | \
jq '.[] | select(.lines > 100) | .hierarchy'- Branch comparison: Compare caller/callee between branches
- Merge conflict prediction: Based on function dependencies
- Test selection: Suggest tests to run based on modified functions
- Coverage analysis: Show test coverage for modified code
NO new dependencies required!
The --stdin approach uses only:
- Standard library
std::io::stdin() - Existing
recurfunctionality - Unix pipes (provided by shell)
Optional (Phase 2 only, if convenience commands needed):
- Could add
git2forrecur git-filesconvenience wrappers - But pipelines are preferred
Decision: Use --stdin flag + Unix pipes instead of git2 library.
Rationale:
- Composability: Works with any tool that outputs file paths (git, find, fd, etc.)
- Simplicity: No new dependencies, smaller binary
- Unix philosophy: Do one thing well, compose with pipes
- Flexibility: Users can use any git command, not just what we implement
- Maintainability: No need to keep up with git2 API changes
Decision: Implement ONLY --stdin flag in Phase 1. Skip git-* commands unless users request them.
Rationale:
- Users already know
git diff --name-only - Pipelines are more powerful and flexible
- Fewer commands to maintain
- Teaches Unix composition patterns
Decision: Apply pattern filtering AFTER reading from stdin.
Rationale:
- Let user control what goes into stdin
recurfilters based on pattern- Composable:
git diff --name-only | grep UserService | recur files "**" --stdin
Decision: stdin format is one file path per line (standard Unix convention).
Rationale:
- Matches
findoutput,git diff --name-only,ls, etc. - Easy to generate with any tool
- Easy to parse in any language
-
Phase 1 (
--stdinflag):- Step 1.1 (stdin helper): 30 minutes
- Step 1.2 (add flags): 30 minutes
- Step 1.3-1.4 (update handlers): 1.5 hours
- Testing: 1 hour
- Documentation: 30 minutes
- Total: ~4 hours
-
Phase 2 (convenience wrappers): 2-3 hours (ONLY if requested)
Grand Total: ~4 hours for full Unix-style Git integration!
- New
git2dependency - 5 new Git-specific commands
- ~15 hours implementation
- 500+ lines of Git integration code
- Maintenance burden
- Zero new dependencies
- One new flag (
--stdin) - ~4 hours implementation
- ~50 lines of code
- Composable with ANY tool
Status: IMPROVEMENT5 (trace) is complete ✅. IMPROVEMENT6 is partially implemented - --stdin flags exist in command structs but need wiring.
Philosophy: Embrace Unix. Let git do Git, let recur do hierarchical analysis. Pipes connect them.
A statistical analysis command that ranks functions by call graph complexity. Helps developers identify:
- High-impact functions (many transitive dependencies)
- Refactoring hotspots (deep call chains)
- Circular reference patterns (potential design issues)
# Find the 5 most complex functions in the codebase
recur trace-stats --scope "**" --ext .rs --sort-by transitive --top 5
# Output:
Function | Direct | Transitive | Circular | Depth | Risk
print_trace_result | 2 | 41 | 0 | 3 | Medium
cmd_trace | 15 | 38 | 2 | 3 | High
format_output | 8 | 29 | 1 | 3 | Medium
parse_args | 5 | 15 | 0 | 2 | Low
validate_input | 2 | 8 | 0 | 2 | Low
Summary: 5 functions analyzed
- 2 with circular references (cmd_trace, format_output)
- Average transitive count: 26.2
- Deepest call chain: 3 levels| Column | Meaning | Interpretation |
|---|---|---|
| Direct | Number of unique functions called directly (depth 1) | Shows immediate dependencies |
| Transitive | Total unique functions reachable in call graph | Shows full impact - more = harder to refactor |
| Circular | Number of distinct circular reference patterns detected | 0 = no cycles, >0 = potential design smell (but may be intentional) |
| Depth | Maximum depth of call chain from this function | Shows call stack depth risk |
| Risk | Refactoring risk assessment | Low (<10 transitive), Medium (10-30), High (>30) |
--sort-by transitive # Default: functions with most dependencies
--sort-by direct # Functions calling many others directly
--sort-by circular # Functions with most circular patterns
--sort-by depth # Functions with deepest call chains
--sort-by risk # Combined complexity score1. Pre-Refactoring Analysis
# Before refactoring UserService, understand its impact
recur trace-stats --scope "UserService.**" --ext .cs --sort-by transitive
# Shows which methods have the most dependencies
# High transitive count = test carefully when changing2. Identify Circular Reference Patterns
# Find all functions with circular references
recur trace-stats --scope "**" --ext .rs --sort-by circular --filter circular-only
Function | Direct | Transitive | Circular | Depth
EventDispatcher.dispatch | 8 | 24 | 3 | 4
ObserverManager.notify | 5 | 18 | 2 | 3
StateManager.transition | 6 | 15 | 1 | 3
# Now you can investigate if these are intentional patterns or bugs3. Git Impact Analysis
# Which changed functions have the highest complexity?
git diff --name-only | recur trace-stats --scope "**" --stdin --sort-by transitive --top 10
# Prioritize testing for high-complexity changes4. Code Review Prioritization
# In a PR review, focus on changes to complex functions first
git diff main..feature --name-only | \
recur trace-stats --scope "**" --stdin --sort-by risk --format json | \
jq '.functions[] | select(.risk == "High")'Saves Time & Energy:
- No manual call graph exploration - instantly see complexity metrics
- Objective refactoring priority - sort by transitive count to find highest-impact functions
- Circular reference detection - stop logic patterns identified automatically
- Risk assessment - know which functions are dangerous to modify
- Git integration - focus testing on complex changed functions
The Circular column counts distinct circular reference patterns, not frequency:
CreateWizard3() → ApplyTemplate() → RenderTemplate() → CreateWizard3() [circular pattern 1]
CreateWizard3() → SaveWizard() → ValidateWizard() → CreateWizard3() [circular pattern 2]
This would show Circular: 2 for CreateWizard3().
Key principle: Whether circular references are acceptable is the developer's decision, not recur's judgment.
- ✅ Recur reports: "2 circular patterns detected"
- ❌ Recur does NOT say: "WARNING: Fix these circular references!"
Examples of acceptable circular patterns:
- Event loops (dispatcher ↔ handler)
- Observer patterns (subject ↔ observer)
- State machines (state ↔ transition manager)
- Recursive data structures with proper termination
Examples of problematic circular patterns:
- Constructor chains causing initialization deadlocks
- Memory leaks from strong reference cycles
- Unintended coupling from poor architecture
Recur shines a light. Developer decides if it's a feature or a bug.
Phase: Future enhancement (IMPROVEMENT7 or later)
Current Status:
- ✅
tracecommand exists (IMPROVEMENT5) - provides foundation - ✅ Circular detection implemented in TraceSearcher
- ✅ Direct/transitive counting works
- ❌
trace-statscommand not yet implemented - ❌ Risk scoring logic not implemented
- ❌ Sorting/filtering options not implemented
Estimated effort: 4-6 hours
- 2 hours: Implement
trace-statscommand structure - 2 hours: Add sorting, filtering, risk scoring
- 1 hour: Output formatting (table, JSON)
- 1 hour: Julia tests
Depends on: IMPROVEMENT6 (--stdin) for Git integration workflows
- ✅ All command structs have
stdin: boolfield - ✅ Help text shows
--stdinin examples - ❌ stdin parameter not passed to command handlers
- ❌ stdin helper function doesn't exist
- ❌ Command handlers don't use stdin
Step 1: Create read_paths_from_stdin() in src/search.rs
- Add function at end of file
- Import
std::io::{BufRead, stdin}(may already be there) - Make function public:
pub fn read_paths_from_stdin() -> Result<Vec<PathBuf>>
Step 2: Wire stdin to match arms in src/main.rs (lines 389-419)
-
Commands::Files { ..., stdin }(line ~390) -
Commands::Find { ..., stdin }(line ~393) -
Commands::Tree { ..., stdin }(line ~396) -
Commands::Related { ..., stdin }(line ~399) -
Commands::Children { ..., stdin }(line ~402) -
Commands::Id { ..., stdin }(line ~405) -
Commands::Stats { ..., stdin }(line ~408) -
Commands::Callers { ..., stdin }(line ~411) -
Commands::Callees { ..., stdin }(line ~414) -
Commands::Trace { ..., stdin }(line ~417)
Step 3: Update handler function signatures (add stdin: bool parameter)
-
fn cmd_files(..., stdin: bool, ...) ->(line ~425) -
fn cmd_find(..., stdin: bool, ...) ->(line ~475) -
fn cmd_tree(..., stdin: bool, ...) ->(line ~545) -
fn cmd_related(..., stdin: bool, ...) ->(line ~575) -
fn cmd_children(..., stdin: bool, ...) ->(line ~605) -
fn cmd_id(..., stdin: bool, ...) ->(line ~635) -
fn cmd_stats(..., stdin: bool, ...) ->(line ~680) -
fn cmd_callers(..., stdin: bool, ...) ->(line ~775) -
fn cmd_callees(..., stdin: bool, ...) ->(line ~890) -
fn cmd_trace(..., stdin: bool, ...) ->(line ~1017)
Step 4: Implement stdin logic in each handler body
- Add
use recur::search::read_paths_from_stdin;at top of main.rs -
cmd_files: if stdin branch with pattern filtering -
cmd_find: if stdin branch with scope filtering -
cmd_tree: if stdin branch with base filtering -
cmd_related: if stdin branch (filter by parent) -
cmd_children: if stdin branch (filter by parent prefix) -
cmd_id: if stdin branch with scope filtering -
cmd_stats: if stdin branch with pattern filtering -
cmd_callers: if stdin branch with scope filtering -
cmd_callees: if stdin branch with scope filtering -
cmd_trace: if stdin branch (limit search space)
Step 5: Test with real Git commands
-
git diff --name-only | recur files "**" --stdin -
git ls-files "*.cs" | recur stats "**" --stdin -
git diff --staged --name-only | recur callers "CreateUser" --scope "**" --stdin
Step 6: Add Julia tests for stdin
- Create
julia-tests/runtests.stdin.jlwith comprehensive test suite - Add
run_recur_with_stdin()helper function toruntests.setup.jl - Update
julia-tests/runtests.jlto include stdin tests - Test all 10 commands with stdin input
- Test edge cases (empty input, non-existent files)
- Optional: Add stdin test cases to existing command test files
Step 7: Integration testing with real Git commands
- Test
git diff --name-only | recur files "**" --stdin - Test
git diff --staged --name-only | recur stats "**" --stdin - Test
git diff --name-only | recur trace "..." --scope "**" --stdin - Test
git ls-files "*.cs" | recur callers "..." --scope "**" --stdin - Verify empty stdin produces no results (not errors)
- Verify non-existent files are silently filtered
- Step 1: 15 minutes (one function)
- Step 2: 20 minutes (10 match arms)
- Step 3: 20 minutes (10 signatures)
- Step 4: 60 minutes (10 handler bodies)
- Step 5: 20 minutes (manual testing)
- Step 6: 40 minutes (Julia tests + helper functions)
- Step 7: 15 minutes (Git integration testing)
- Total: ~3 hours
src/search.rs- Add 1 new function (~15 lines)src/main.rs- Modify 10 match arms, 10 signatures, 10 handler bodies (~200 lines modified)julia-tests/runtests.stdin.jl- New test file (~150 lines)julia-tests/runtests.setup.jl- Addrun_recur_with_stdin()helper (~50 lines)julia-tests/runtests.jl- Add include for stdin tests (~1 line)
✅ Use --stdin flag (not Git library)
✅ One path per line (Unix convention)
✅ Filter after reading (composable)
✅ Apply to ALL commands (not just some)
✅ Phase 1 only (no convenience wrappers yet)