diff --git a/.devcontainer.json b/.devcontainer.json new file mode 100644 index 0000000..a09eb8d --- /dev/null +++ b/.devcontainer.json @@ -0,0 +1 @@ +{"image":"mcr.microsoft.com/devcontainers/rust:latest"} \ No newline at end of file diff --git a/.opencode/agent/ci-agent.md b/.opencode/agent/ci-agent.md index 7602053..168d73b 100644 --- a/.opencode/agent/ci-agent.md +++ b/.opencode/agent/ci-agent.md @@ -13,9 +13,9 @@ description: >- mode: subagent tools: - bash: false - write: false - edit: false + bash: true + write: true + edit: true --- You are a CI Agent, a specialized AI agent for CI/CD setup in code-guardian. diff --git a/.opencode/command/atomic-commit.md b/.opencode/command/atomic-commit.md new file mode 100644 index 0000000..d6422d1 --- /dev/null +++ b/.opencode/command/atomic-commit.md @@ -0,0 +1,5 @@ +--- +description: Create atomic commits for staged changes +agent: atomic-commit-creator +--- +Analyze the current staged changes, identify logical units of work, and propose splitting them into atomic commits with descriptive messages. Use git diff --cached to examine changes and suggest selective staging if needed. \ No newline at end of file diff --git a/.rovodev/subagents/clean-code-developer.md b/.rovodev/subagents/clean-code-developer.md new file mode 100644 index 0000000..8b85cec --- /dev/null +++ b/.rovodev/subagents/clean-code-developer.md @@ -0,0 +1,9 @@ +--- +name: clean-code-developer +description: Clean Code Developer +tools: null +model: claude-sonnet-4@20250514 +--- +You are a Clean Code Developer assistant specialized in helping developers write high-quality, maintainable, and efficient code. Your primary objective is to promote best practices in software development, focusing on code readability, modularity, and adherence to solid design principles. You will analyze code, provide constructive feedback, suggest improvements, and help developers refactor their code to meet clean code standards. + +Your role involves identifying code smells, recommending design patterns, ensuring proper separation of concerns, and guiding developers towards writing more elegant and sustainable software solutions. You should be able to work across different programming languages and paradigms, always prioritizing code quality, readability, and long-term maintainability. \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 72ae995..f9b3ee9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,3 @@ -## [0.1.0-alpha] - 2025-10-06 # Changelog All notable changes to this project will be documented in this file. @@ -6,6 +5,21 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.2.0] - 2025-10-09 + +### Added +- Git CLI commands and GitIntegration module for repository operations +- Dev container configuration + +### Fixed +- Text formatter for cross-platform compatibility +- Test updates for match data checking +- Removed enforce_styling from text formatter + +### Changed +- CI agent tools updates and lib.rs cleanups +- Documentation updates including atomic-commit command and git integration demo + ## [0.1.1-alpha] - 2025-10-07 ### Added diff --git a/Cargo.lock b/Cargo.lock index e5d35e3..0941642 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -346,7 +346,6 @@ dependencies = [ "chrono", "code-guardian-core", "colored", - "comfy-table", "csv", "proptest", "serde", diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 342ba8c..9be1d6f 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "code_guardian_cli" -version = "0.1.1-alpha" +version = "0.2.0" edition = "2021" [dependencies] diff --git a/crates/cli/src/git_integration.rs b/crates/cli/src/git_integration.rs new file mode 100644 index 0000000..c8e079f --- /dev/null +++ b/crates/cli/src/git_integration.rs @@ -0,0 +1,263 @@ +use anyhow::{anyhow, Result}; +use std::path::{Path, PathBuf}; +use std::process::Command; + +/// Git integration utilities for Code-Guardian +pub struct GitIntegration; + +impl GitIntegration { + /// Get list of staged files (files in git index) + pub fn get_staged_files(repo_path: &Path) -> Result> { + let output = Command::new("git") + .args(["diff", "--cached", "--name-only", "--diff-filter=ACMR"]) + .current_dir(repo_path) + .output()?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(anyhow!("Git command failed: {}", stderr)); + } + + let stdout = String::from_utf8_lossy(&output.stdout); + let files: Vec = stdout + .lines() + .filter(|line| !line.trim().is_empty()) + .map(|line| repo_path.join(line.trim())) + .filter(|path| path.exists()) // Only include files that still exist + .collect(); + + Ok(files) + } + + /// Get the root directory of the git repository + pub fn get_repo_root(start_path: &Path) -> Result { + let output = Command::new("git") + .args(["rev-parse", "--show-toplevel"]) + .current_dir(start_path) + .output()?; + + if !output.status.success() { + return Err(anyhow!("Not in a git repository or git command failed")); + } + + let stdout = String::from_utf8_lossy(&output.stdout); + let repo_root = stdout.trim(); + Ok(PathBuf::from(repo_root)) + } + + /// Check if the current directory is in a git repository + pub fn is_git_repo(path: &Path) -> bool { + Command::new("git") + .args(["rev-parse", "--git-dir"]) + .current_dir(path) + .output() + .map(|output| output.status.success()) + .unwrap_or(false) + } + + /// Get modified lines for staged files (useful for line-specific scanning) + #[allow(dead_code)] + pub fn get_staged_lines(repo_path: &Path) -> Result> { + let output = Command::new("git") + .args(["diff", "--cached", "--unified=0"]) + .current_dir(repo_path) + .output()?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(anyhow!("Git diff command failed: {}", stderr)); + } + + let stdout = String::from_utf8_lossy(&output.stdout); + Ok(parse_git_diff(&stdout, repo_path)) + } + + /// Install pre-commit hook for Code-Guardian + pub fn install_pre_commit_hook(repo_path: &Path) -> Result<()> { + let hooks_dir = repo_path.join(".git").join("hooks"); + let hook_path = hooks_dir.join("pre-commit"); + + // Create hooks directory if it doesn't exist + std::fs::create_dir_all(&hooks_dir)?; + + // Pre-commit hook script + let hook_script = r#"#!/bin/sh +# Code-Guardian pre-commit hook +# This hook runs Code-Guardian on staged files before commit + +# Check if code-guardian is available +if ! command -v code-guardian >/dev/null 2>&1; then + echo "Error: code-guardian not found in PATH" + echo "Please install code-guardian or add it to your PATH" + exit 1 +fi + +# Run Code-Guardian pre-commit check +exec code-guardian pre-commit --staged-only --fast +"#; + + std::fs::write(&hook_path, hook_script)?; + + // Make the hook executable (Unix-like systems) + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = std::fs::metadata(&hook_path)?.permissions(); + perms.set_mode(0o755); + std::fs::set_permissions(&hook_path, perms)?; + } + + println!("✅ Pre-commit hook installed at: {}", hook_path.display()); + println!("🔧 The hook will run 'code-guardian pre-commit --staged-only --fast' before each commit"); + + Ok(()) + } + + /// Uninstall pre-commit hook + pub fn uninstall_pre_commit_hook(repo_path: &Path) -> Result<()> { + let hook_path = repo_path.join(".git").join("hooks").join("pre-commit"); + + if hook_path.exists() { + // Check if it's our hook before removing + let content = std::fs::read_to_string(&hook_path)?; + if content.contains("Code-Guardian pre-commit hook") { + std::fs::remove_file(&hook_path)?; + println!("✅ Code-Guardian pre-commit hook removed"); + } else { + println!( + "âš ī¸ Pre-commit hook exists but doesn't appear to be Code-Guardian's hook" + ); + println!(" Manual removal required: {}", hook_path.display()); + } + } else { + println!("â„šī¸ No pre-commit hook found"); + } + + Ok(()) + } +} + +/// Represents a staged change in git +#[allow(dead_code)] +#[derive(Debug, Clone)] +pub struct StagedChange { + pub file_path: PathBuf, + pub added_lines: Vec, + pub removed_lines: Vec, +} + +/// Represents a range of lines +#[allow(dead_code)] +#[derive(Debug, Clone)] +pub struct LineRange { + pub start: usize, + pub count: usize, +} + +/// Parse git diff output to extract staged changes +#[allow(dead_code)] +fn parse_git_diff(diff_output: &str, repo_path: &Path) -> Vec { + let mut changes = Vec::new(); + let mut current_file: Option = None; + let mut added_lines = Vec::new(); + let mut removed_lines = Vec::new(); + + for line in diff_output.lines() { + if line.starts_with("diff --git") { + // Save previous file's changes + if let Some(file_path) = current_file.take() { + changes.push(StagedChange { + file_path, + added_lines: std::mem::take(&mut added_lines), + removed_lines: std::mem::take(&mut removed_lines), + }); + } + } else if line.starts_with("+++") { + // Extract new file path + if let Some(path_part) = line.strip_prefix("+++ b/") { + current_file = Some(repo_path.join(path_part)); + } + } else if line.starts_with("@@") { + // Parse hunk header: @@ -old_start,old_count +new_start,new_count @@ + if let Some(hunk_info) = line.strip_prefix("@@").and_then(|s| s.strip_suffix("@@")) { + let parts: Vec<&str> = hunk_info.split_whitespace().collect(); + if parts.len() >= 2 { + // Parse removed lines (-old_start,old_count) + if let Some(removed_part) = parts[0].strip_prefix('-') { + if let Some((start_str, count_str)) = removed_part.split_once(',') { + if let (Ok(start), Ok(count)) = + (start_str.parse::(), count_str.parse::()) + { + if count > 0 { + removed_lines.push(LineRange { start, count }); + } + } + } + } + + // Parse added lines (+new_start,new_count) + if let Some(added_part) = parts[1].strip_prefix('+') { + if let Some((start_str, count_str)) = added_part.split_once(',') { + if let (Ok(start), Ok(count)) = + (start_str.parse::(), count_str.parse::()) + { + if count > 0 { + added_lines.push(LineRange { start, count }); + } + } + } + } + } + } + } + } + + // Don't forget the last file + if let Some(file_path) = current_file { + changes.push(StagedChange { + file_path, + added_lines, + removed_lines, + }); + } + + changes +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::TempDir; + + #[test] + fn test_git_integration_basic() { + // Test that basic structures work + let range = LineRange { start: 5, count: 3 }; + assert_eq!(range.start, 5); + assert_eq!(range.count, 3); + + // Test that we can create staged change + let temp_dir = TempDir::new().unwrap(); + let change = StagedChange { + file_path: temp_dir.path().join("test.rs"), + added_lines: vec![range], + removed_lines: vec![], + }; + + assert_eq!(change.added_lines.len(), 1); + assert_eq!(change.removed_lines.len(), 0); + } + + #[test] + fn test_is_git_repo() { + let temp_dir = TempDir::new().unwrap(); + assert!(!GitIntegration::is_git_repo(temp_dir.path())); + } + + #[test] + fn test_line_range() { + let range = LineRange { start: 5, count: 3 }; + assert_eq!(range.start, 5); + assert_eq!(range.count, 3); + } +} diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs new file mode 100644 index 0000000..6ac7ca2 --- /dev/null +++ b/crates/cli/src/lib.rs @@ -0,0 +1,4 @@ +pub mod git_integration; +pub mod production_handlers; +pub mod scan_handlers; +pub mod utils; diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index e1832cb..5aa6cee 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -8,12 +8,14 @@ use std::path::PathBuf; mod advanced_handlers; mod benchmark; mod comparison_handlers; +mod git_integration; mod production_handlers; mod report_handlers; mod scan_handlers; mod utils; use advanced_handlers::*; +use git_integration::GitIntegration; use production_handlers::*; #[derive(Parser)] @@ -243,6 +245,11 @@ enum Commands { #[arg(long, default_value = "500")] delay: u64, }, + /// Git integration and hook management + Git { + #[command(subcommand)] + action: GitAction, + }, } #[derive(Subcommand)] @@ -349,6 +356,28 @@ enum StackPreset { }, } +#[derive(Subcommand)] +enum GitAction { + /// Install pre-commit hook + InstallHook { + /// Path to git repository (default: current directory) + #[arg(default_value = ".")] + path: PathBuf, + }, + /// Uninstall pre-commit hook + UninstallHook { + /// Path to git repository (default: current directory) + #[arg(default_value = ".")] + path: PathBuf, + }, + /// List staged files that would be scanned + Staged { + /// Path to git repository (default: current directory) + #[arg(default_value = ".")] + path: PathBuf, + }, +} + fn main() -> Result<()> { let cli = Cli::parse(); @@ -435,13 +464,14 @@ fn main() -> Result<()> { format, production, } => handle_lang_scan(languages, path, format, production), - Commands::Stack { preset } => handle_stack_preset(preset), + Commands::Stack { preset } => handle_stack_preset_main(preset), Commands::Watch { path, include, exclude, delay, } => handle_watch(path, include, exclude, delay), + Commands::Git { action } => handle_git(action), } } @@ -484,6 +514,116 @@ fn handle_benchmark(path: Option, quick: bool) -> Result<()> { } } +fn handle_stack_preset_main(preset: StackPreset) -> Result<()> { + match preset { + StackPreset::Web { path, production } => { + let languages = vec![ + "js".to_string(), + "ts".to_string(), + "jsx".to_string(), + "tsx".to_string(), + "vue".to_string(), + "svelte".to_string(), + ]; + handle_lang_scan(languages, path, "text".to_string(), production) + } + StackPreset::Backend { path, production } => { + let languages = vec![ + "py".to_string(), + "java".to_string(), + "go".to_string(), + "cs".to_string(), + "php".to_string(), + "rb".to_string(), + ]; + handle_lang_scan(languages, path, "text".to_string(), production) + } + StackPreset::Fullstack { path, production } => { + let languages = vec![ + "js".to_string(), + "ts".to_string(), + "py".to_string(), + "java".to_string(), + "go".to_string(), + "rs".to_string(), + ]; + handle_lang_scan(languages, path, "text".to_string(), production) + } + StackPreset::Mobile { path, production } => { + let languages = vec![ + "js".to_string(), + "ts".to_string(), + "swift".to_string(), + "kt".to_string(), + "dart".to_string(), + ]; + handle_lang_scan(languages, path, "text".to_string(), production) + } + StackPreset::Systems { path, production } => { + let languages = vec![ + "rs".to_string(), + "cpp".to_string(), + "c".to_string(), + "go".to_string(), + ]; + handle_lang_scan(languages, path, "text".to_string(), production) + } + } +} + +fn handle_git(action: GitAction) -> Result<()> { + match action { + GitAction::InstallHook { path } => { + println!("🔧 Installing Code-Guardian pre-commit hook..."); + + if !GitIntegration::is_git_repo(&path) { + eprintln!("❌ Error: {} is not a git repository", path.display()); + std::process::exit(1); + } + + let repo_root = GitIntegration::get_repo_root(&path)?; + GitIntegration::install_pre_commit_hook(&repo_root)?; + + println!("💡 Usage: The hook will automatically run on 'git commit'"); + println!("💡 Manual run: code-guardian pre-commit --staged-only --fast"); + Ok(()) + } + GitAction::UninstallHook { path } => { + println!("đŸ—‘ī¸ Uninstalling Code-Guardian pre-commit hook..."); + + if !GitIntegration::is_git_repo(&path) { + eprintln!("❌ Error: {} is not a git repository", path.display()); + std::process::exit(1); + } + + let repo_root = GitIntegration::get_repo_root(&path)?; + GitIntegration::uninstall_pre_commit_hook(&repo_root)?; + Ok(()) + } + GitAction::Staged { path } => { + println!("📋 Listing staged files..."); + + if !GitIntegration::is_git_repo(&path) { + eprintln!("❌ Error: {} is not a git repository", path.display()); + std::process::exit(1); + } + + let repo_root = GitIntegration::get_repo_root(&path)?; + let staged_files = GitIntegration::get_staged_files(&repo_root)?; + + if staged_files.is_empty() { + println!("â„šī¸ No staged files found."); + } else { + println!("🔍 Found {} staged file(s):", staged_files.len()); + for (i, file) in staged_files.iter().enumerate() { + println!(" {}. {}", i + 1, file.display()); + } + } + Ok(()) + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/cli/src/production_handlers.rs b/crates/cli/src/production_handlers.rs index 765b028..383a3cd 100644 --- a/crates/cli/src/production_handlers.rs +++ b/crates/cli/src/production_handlers.rs @@ -1,3 +1,4 @@ +use crate::git_integration::GitIntegration; use anyhow::Result; use code_guardian_core::{AlertDetector, ConsoleLogDetector, DebuggerDetector}; use code_guardian_core::{DetectorFactory, Match, PatternDetector, Scanner}; @@ -102,9 +103,48 @@ pub fn handle_pre_commit(path: PathBuf, staged_only: bool, fast: bool) -> Result }; let scanner = Scanner::new(detectors); + let matches = if staged_only { - // TODO: Implement git diff --cached integration - scanner.scan(&path)? + // Check if we're in a git repository + if !GitIntegration::is_git_repo(&path) { + println!("âš ī¸ Not in a git repository. Scanning entire directory instead."); + scanner.scan(&path)? + } else { + // Get repo root and staged files + let repo_root = GitIntegration::get_repo_root(&path)?; + let staged_files = GitIntegration::get_staged_files(&repo_root)?; + + if staged_files.is_empty() { + println!("â„šī¸ No staged files found. Nothing to scan."); + return Ok(()); + } + + println!("🔍 Scanning {} staged file(s)...", staged_files.len()); + if !fast { + for file in &staged_files { + println!(" 📄 {}", file.display()); + } + } + + // Scan only staged files + let mut all_matches = Vec::new(); + for file_path in staged_files { + if file_path.is_file() { + // For now, use the directory scanner on each file's parent + // This is a workaround until we implement file-specific scanning + if let Some(parent) = file_path.parent() { + let file_matches = scanner.scan(parent)?; + // Filter matches to only include the specific file + let filtered_matches: Vec<_> = file_matches + .into_iter() + .filter(|m| m.file_path == file_path.to_string_lossy()) + .collect(); + all_matches.extend(filtered_matches); + } + } + } + all_matches + } } else { scanner.scan(&path)? }; @@ -265,66 +305,6 @@ pub fn handle_lang_scan( Ok(()) } -/// Handle technology stack presets -pub fn handle_stack_preset(preset: crate::StackPreset) -> Result<()> { - use crate::StackPreset; - - match preset { - StackPreset::Web { path, production } => { - let languages = vec![ - "js".to_string(), - "ts".to_string(), - "jsx".to_string(), - "tsx".to_string(), - "vue".to_string(), - "svelte".to_string(), - ]; - handle_lang_scan(languages, path, "text".to_string(), production) - } - StackPreset::Backend { path, production } => { - let languages = vec![ - "py".to_string(), - "java".to_string(), - "go".to_string(), - "cs".to_string(), - "php".to_string(), - "rb".to_string(), - ]; - handle_lang_scan(languages, path, "text".to_string(), production) - } - StackPreset::Fullstack { path, production } => { - let languages = vec![ - "js".to_string(), - "ts".to_string(), - "py".to_string(), - "java".to_string(), - "go".to_string(), - "rs".to_string(), - ]; - handle_lang_scan(languages, path, "text".to_string(), production) - } - StackPreset::Mobile { path, production } => { - let languages = vec![ - "js".to_string(), - "ts".to_string(), - "swift".to_string(), - "kt".to_string(), - "dart".to_string(), - ]; - handle_lang_scan(languages, path, "text".to_string(), production) - } - StackPreset::Systems { path, production } => { - let languages = vec![ - "rs".to_string(), - "cpp".to_string(), - "c".to_string(), - "go".to_string(), - ]; - handle_lang_scan(languages, path, "text".to_string(), production) - } - } -} - /// Handle file watching command pub fn handle_watch( _path: PathBuf, diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index dbf86db..ae5a5bd 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "code-guardian-core" -version = "0.1.1-alpha" +version = "0.2.0" edition = "2021" [dependencies] diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 0c73564..c8960b3 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -293,7 +293,4 @@ mod tests { println!(" {} [{}] {}", m.file_path, m.pattern, m.message); } } - - - } diff --git a/crates/output/Cargo.toml b/crates/output/Cargo.toml index 850b475..cbdb59d 100644 --- a/crates/output/Cargo.toml +++ b/crates/output/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "code-guardian-output" -version = "0.1.1-alpha" +version = "0.2.0" edition = "2021" [dependencies] @@ -12,7 +12,6 @@ thiserror = { workspace = true } anyhow = { workspace = true } chrono = { workspace = true } colored = { workspace = true } -comfy-table = { workspace = true } csv = "1.1" [dev-dependencies] diff --git a/crates/output/src/formatters/text.rs b/crates/output/src/formatters/text.rs index e9755c9..d6ed817 100644 --- a/crates/output/src/formatters/text.rs +++ b/crates/output/src/formatters/text.rs @@ -1,9 +1,8 @@ use super::Formatter; use code_guardian_core::Match; -use comfy_table::{Cell, Table}; -/// Formatter that outputs matches in a plain text table format. -/// Uses a table for structured display. +/// Formatter that outputs matches in a simple text format. +/// Each match is displayed as "file:line:column: pattern - message". pub struct TextFormatter; impl Formatter for TextFormatter { @@ -12,22 +11,14 @@ impl Formatter for TextFormatter { return "No matches found.".to_string(); } - let mut table = Table::new(); - table - .enforce_styling() - .set_header(vec!["File", "Line", "Column", "Pattern", "Message"]); - + let mut output = String::new(); for m in matches { - table.add_row(vec![ - Cell::new(&m.file_path), - Cell::new(m.line_number.to_string()), - Cell::new(m.column.to_string()), - Cell::new(&m.pattern), - Cell::new(&m.message), - ]); + output.push_str(&format!( + "{}:{}:{}: {} - {}\n", + m.file_path, m.line_number, m.column, m.pattern, m.message + )); } - - table.to_string() + output.trim_end().to_string() } } @@ -54,8 +45,8 @@ mod tests { message: "TODO comment".to_string(), }]; let output = formatter.format(&matches); - assert!(output.contains("test.rs")); - assert!(output.contains("TODO")); + let expected = "test.rs:1:1: TODO - TODO comment"; + assert_eq!(output, expected); } #[test] @@ -64,10 +55,10 @@ mod tests { let matches = vec![ Match { file_path: "src/main.rs".to_string(), - line_number: 5, - column: 3, + line_number: 10, + column: 5, pattern: "TODO".to_string(), - message: "TODO: implement feature".to_string(), + message: "Found a TODO".to_string(), }, Match { file_path: "src/lib.rs".to_string(), @@ -78,18 +69,8 @@ mod tests { }, ]; let output = formatter.format(&matches); - // Check that the output contains the expected data - assert!(output.contains("src/main.rs")); - assert!(output.contains("5")); - assert!(output.contains("3")); - assert!(output.contains("TODO")); - assert!(output.contains("TODO: implement feature")); - assert!(output.contains("src/lib.rs")); - assert!(output.contains("10")); - assert!(output.contains("1")); - assert!(output.contains("FIXME")); - assert!(output.contains("FIXME: temporary workaround")); - // Ensure it's a table format + let expected = "src/main.rs:10:5: TODO - Found a TODO\nsrc/lib.rs:10:1: FIXME - FIXME: temporary workaround"; + assert_eq!(output, expected); } #[test] @@ -112,10 +93,8 @@ mod tests { }, ]; let output = formatter.format(&matches); - assert!(output.contains("test.rs")); - assert!(output.contains("test.js")); - assert!(output.contains("TODO")); - assert!(output.contains("FIXME")); + let expected = "test.rs:1:1: TODO - TODO\ntest.js:2:3: FIXME - FIXME"; + assert_eq!(output, expected); } } diff --git a/crates/storage/Cargo.toml b/crates/storage/Cargo.toml index b968863..b6d958a 100644 --- a/crates/storage/Cargo.toml +++ b/crates/storage/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "code-guardian-storage" -version = "0.1.1-alpha" +version = "0.2.0" edition = "2021" [dependencies] diff --git a/examples/git_integration_demo.md b/examples/git_integration_demo.md new file mode 100644 index 0000000..e8cf71d --- /dev/null +++ b/examples/git_integration_demo.md @@ -0,0 +1,111 @@ +# Code-Guardian Git Integration Demo + +This document demonstrates the new git integration features in Code-Guardian. + +## Features + +### 1. Pre-commit Hook Installation + +Install the pre-commit hook to automatically run Code-Guardian before each commit: + +```bash +# Install the hook +code-guardian git install-hook + +# The hook will now run automatically on every commit +git commit -m "Your commit message" +``` + +### 2. Staged Files Scanning + +Scan only staged files instead of the entire repository: + +```bash +# Scan only staged files +code-guardian pre-commit --staged-only + +# Fast scan of staged files (critical issues only) +code-guardian pre-commit --staged-only --fast +``` + +### 3. Git Integration Commands + +```bash +# List currently staged files +code-guardian git staged + +# Install pre-commit hook +code-guardian git install-hook + +# Uninstall pre-commit hook +code-guardian git uninstall-hook +``` + +## Workflow Integration + +### Typical Developer Workflow + +1. **One-time setup**: Install the pre-commit hook + ```bash + code-guardian git install-hook + ``` + +2. **Daily development**: Work normally - the hook runs automatically + ```bash + # Make changes + vim src/main.rs + + # Stage changes + git add src/main.rs + + # Commit (hook runs automatically) + git commit -m "Add new feature" + ``` + +3. **Manual scanning**: Check staged files before committing + ```bash + # See what files are staged + code-guardian git staged + + # Run manual pre-commit check + code-guardian pre-commit --staged-only --fast + ``` + +### CI/CD Integration + +The git integration works seamlessly with CI/CD pipelines: + +```yaml +# .github/workflows/code-quality.yml +- name: Run Code-Guardian on changed files + run: | + # Get list of changed files in PR + git fetch origin main + git diff --name-only origin/main... > changed_files.txt + + # Run Code-Guardian on changed files + code-guardian scan --files-from changed_files.txt +``` + +## Benefits + +- **Faster feedback**: Catch issues at commit time, not in CI +- **Focused scanning**: Only scan files you're actually changing +- **Team adoption**: Automatic enforcement through git hooks +- **Flexible**: Works with both individual commits and CI/CD pipelines + +## Technical Implementation + +The git integration uses: +- `git diff --cached --name-only` to get staged files +- `git rev-parse --show-toplevel` to find repository root +- Native git commands for maximum compatibility +- Graceful fallback to directory scanning when not in a git repo + +## Configuration + +The pre-commit hook uses these default settings: +- `--staged-only`: Only scan staged files +- `--fast`: Quick scan mode (critical issues only) + +You can customize by editing the hook file directly or using a configuration file. \ No newline at end of file