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
11 changes: 11 additions & 0 deletions some-diff.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
cat > some_diff.patch <<EOF
diff --git a/example.py b/example.py
index e69de29..4b825dc 100644
--- a/example.py
+++ b/example.py
@@ -0,0 +1,4 @@
+def hello():
+ print("Hello, world!")
+
+hello()
EOF
7 changes: 7 additions & 0 deletions src/cli/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,13 @@ pub enum Command {
#[arg(long)]
force: bool,
},

/// Analyze a diff and categorize changes into conventional commit types.
ContextualChange {
/// Path to the diff file to analyze.
#[arg(value_name = "DIFF_PATH")]
diff_path: PathBuf,
},
}

/// Output format options for generated content.
Expand Down
23 changes: 23 additions & 0 deletions src/cli/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use crate::core::scanner::ProjectScanner;
use crate::plugins::interface::{OutputPluginInput, OutputPluginInterface};
use crate::plugins::manager::PluginManager;
use crate::utils::config::Config;
use crate::context;

/// Main command dispatcher that routes CLI arguments to appropriate handlers.
///
Expand Down Expand Up @@ -44,6 +45,28 @@ pub async fn handle_command(args: Args) -> Result<()> {
} => handle_docs(matrix, format, output_dir, &config).await,
Command::Plugins { detailed } => handle_plugins(detailed, &config).await,
Command::Config { force } => handle_config(force).await,
Command::ContextualChange { diff_path } => {
info!("Starting contextual change detection...");

let diff_content = std::fs::read_to_string(&diff_path)
.map_err(|e| anyhow::anyhow!("Failed to read diff file '{}': {}", diff_path.display(), e))?;

let types = context::categorize_diff(&diff_content);

if types.is_empty() {
warn!("No recognizable change types found. Defaulting to 'chore'.");
println!("- chore");
} else {
println!("Detected change types:");
for t in types {
println!("- {}", t);
}
}

info!("Contextual change detection complete.");
Ok(())
}

}
}

Expand Down
42 changes: 42 additions & 0 deletions src/context.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
pub fn categorize_diff(diff: &str) -> Vec<String> {
let mut categories = vec![];

let lines: Vec<&str> = diff.lines().collect();

let mut added_lines = lines.iter().filter(|l| l.starts_with('+')).map(|l| *l).collect::<Vec<_>>();

if added_lines.iter().any(|line| line.contains("fn") && line.contains("test"))
|| added_lines.iter().any(|line| line.contains("#[test]"))
{
categories.push("test".to_string());
}

if added_lines.iter().any(|line| line.contains("fix") || line.contains("bug") || line.contains("unwrap()")) {
categories.push("fix".to_string());
}

if added_lines.iter().any(|line| line.contains("///") || line.contains("//") || line.contains("doc")) {
categories.push("docs".to_string());
}

if added_lines.iter().any(|line| line.contains("fn ") || line.contains("impl ") || line.contains("struct ")) {
categories.push("feat".to_string());
}

if added_lines.iter().any(|line| line.trim_start_matches('+').trim().is_empty()) {
categories.push("style".to_string());
}

if added_lines.iter().any(|line| line.contains("let ") || line.contains("const ") || line.contains("=")) {
categories.push("refactor".to_string());
}

// If no category was confidently matched, fallback to 'chore'
if categories.is_empty() {
categories.push("chore".to_string());
}

categories.sort();
categories.dedup();
categories
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ pub mod cli;
pub mod core;
pub mod plugins;
pub mod utils;
pub mod context;
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use log::{error, info};

use csd::cli::args::Args;
use csd::cli::commands;
// use csd::context::categorize_diff; // <-- import detector

/// Application entry point.
///
Expand Down
65 changes: 65 additions & 0 deletions tests/context-tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
use csd::context::categorize_diff;

#[test]
fn test_feat_detection() {
let diff = "\
+ fn new_feature() {
+ println!(\"Hello\");
+ }";
let categories = categorize_diff(diff);
assert!(categories.contains(&"feat".to_string()));
}

#[test]
fn test_fix_detection() {
let diff = "\
+ // fix: prevent panic
+ let value = some_option.unwrap();";
let categories = categorize_diff(diff);
assert!(categories.contains(&"fix".to_string()));
}

#[test]
fn test_docs_detection() {
let diff = "\
+ /// Adds two numbers
+ // This is a helper function";
let categories = categorize_diff(diff);
assert!(categories.contains(&"docs".to_string()));
}

#[test]
fn test_test_detection() {
let diff = "\
+ #[test]
+ fn test_example() {
+ assert_eq!(1, 1);
+ }";
let categories = categorize_diff(diff);
assert!(categories.contains(&"test".to_string()));
}

#[test]
fn test_style_detection() {
let diff = "\
+
+ ";
let categories = categorize_diff(diff);
assert!(categories.contains(&"style".to_string()));
}

#[test]
fn test_refactor_detection() {
let diff = "\
+ let x = some_value;";
let categories = categorize_diff(diff);
assert!(categories.contains(&"refactor".to_string()));
}

#[test]
fn test_fallback_to_chore() {
let diff = "\
+ echo 'Hello, world!'";
let categories = categorize_diff(diff);
assert!(categories.contains(&"chore".to_string()));
}