From 2385ef2b67a57116ee0c14be821fd4bb50712915 Mon Sep 17 00:00:00 2001 From: Sahaj Jain Date: Sat, 16 Nov 2024 23:24:01 +0530 Subject: [PATCH 1/3] feat: Add commit_reference and support commit ranges for explain --- src/commit_reference.rs | 124 ++++++++++++++++++++++++++++++++++++++++ src/config/cli.rs | 8 ++- src/config/mod.rs | 2 +- src/git_entity/diff.rs | 16 ++++++ src/main.rs | 8 ++- 5 files changed, 152 insertions(+), 6 deletions(-) create mode 100644 src/commit_reference.rs diff --git a/src/commit_reference.rs b/src/commit_reference.rs new file mode 100644 index 0000000..e80d625 --- /dev/null +++ b/src/commit_reference.rs @@ -0,0 +1,124 @@ +use std::str::FromStr; +use thiserror::Error; + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum CommitReference { + Single(String), + Range { from: String, to: String }, +} + +#[derive(Debug, Error)] +pub enum ReferenceParseError { + #[error("empty reference string")] + Empty, +} + +impl FromStr for CommitReference { + type Err = ReferenceParseError; + + fn from_str(s: &str) -> Result { + if s.is_empty() { + return Err(ReferenceParseError::Empty); + } + + // Handle the .. cases + if let Some((from, to)) = s.split_once("..") { + let from = if from.is_empty() { "HEAD" } else { from }; + let to = if to.is_empty() { "HEAD" } else { to }; + + Ok(CommitReference::Range { + from: from.to_string(), + to: to.to_string(), + }) + } else { + Ok(CommitReference::Single(s.to_string())) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use clap::Parser; + + #[derive(Parser, Debug)] + struct TestCli { + reference: CommitReference, + } + + #[test] + fn test_single_commit() { + assert_eq!( + "HEAD".parse::().unwrap(), + CommitReference::Single("HEAD".to_string()) + ); + } + + #[test] + fn test_full_range() { + assert_eq!( + "main..feature".parse::().unwrap(), + CommitReference::Range { + from: "main".to_string(), + to: "feature".to_string(), + } + ); + } + + #[test] + fn test_from_only_range() { + assert_eq!( + "develop..".parse::().unwrap(), + CommitReference::Range { + from: "develop".to_string(), + to: "HEAD".to_string(), + } + ); + } + + #[test] + fn test_to_only_range() { + assert_eq!( + "..feature".parse::().unwrap(), + CommitReference::Range { + from: "HEAD".to_string(), + to: "feature".to_string(), + } + ); + } + + #[test] + fn test_clap_integration() { + // Test full range + let cli = TestCli::try_parse_from(&["test", "main..feature"]).unwrap(); + assert!(matches!( + cli.reference, + CommitReference::Range { from, to } + if from == "main" && to == "feature" + )); + + // Test from-only range + let cli = TestCli::try_parse_from(&["test", "develop.."]).unwrap(); + assert!(matches!( + cli.reference, + CommitReference::Range { from, to } + if from == "develop" && to == "HEAD" + )); + + // Test to-only range + let cli = TestCli::try_parse_from(&["test", "..feature"]).unwrap(); + assert!(matches!( + cli.reference, + CommitReference::Range { from, to } + if from == "HEAD" && to == "feature" + )); + } + + #[test] + fn test_empty_reference() { + assert!(matches!( + "".parse::(), + Err(ReferenceParseError::Empty) + )); + } +} diff --git a/src/config/cli.rs b/src/config/cli.rs index 608ad80..2363f9f 100644 --- a/src/config/cli.rs +++ b/src/config/cli.rs @@ -1,6 +1,8 @@ use clap::{command, Parser, Subcommand, ValueEnum}; use std::str::FromStr; +use crate::commit_reference::CommitReference; + #[derive(Parser)] #[command(name = "lumen")] #[command(about = "AI-powered CLI tool for git commit summaries", long_about = None)] @@ -30,7 +32,7 @@ pub enum ProviderType { Groq, Claude, Ollama, - Openrouter + Openrouter, } impl FromStr for ProviderType { @@ -54,8 +56,8 @@ pub enum Commands { /// Explain the changes in a commit, or the current diff Explain { /// The commit hash to use - #[arg(group = "target")] - sha: Option, + #[arg(group = "target", value_parser = clap::value_parser!(CommitReference))] + reference: Option, /// Explain current diff #[arg(long, group = "target")] diff --git a/src/config/mod.rs b/src/config/mod.rs index 49effa7..c0066d7 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,4 +1,4 @@ -pub mod configuration; pub mod cli; +pub mod configuration; pub use configuration::LumenConfig; diff --git a/src/git_entity/diff.rs b/src/git_entity/diff.rs index 42924dd..0312cf8 100644 --- a/src/git_entity/diff.rs +++ b/src/git_entity/diff.rs @@ -30,4 +30,20 @@ impl Diff { Ok(Diff { staged, diff }) } + + pub fn from_commits_range(from: &str, to: &str) -> Result { + let output = std::process::Command::new("git") + .args(["diff", from, to]) + .output()?; + + let diff = String::from_utf8(output.stdout)?; + if diff.is_empty() { + return Err(DiffError::EmptyDiff { staged: false }.into()); + } + + Ok(Diff { + staged: false, + diff, + }) + } } diff --git a/src/main.rs b/src/main.rs index a782dda..0e68e91 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ use clap::Parser; +use commit_reference::CommitReference; use config::cli::{Cli, Commands}; use config::LumenConfig; use error::LumenError; @@ -7,6 +8,7 @@ use std::process; mod ai_prompt; mod command; +mod commit_reference; mod config; mod error; mod git_entity; @@ -35,15 +37,17 @@ async fn run() -> Result<(), LumenError> { match cli.command { Commands::Explain { - sha, + reference, diff, staged, query, } => { let git_entity = if diff { GitEntity::Diff(Diff::from_working_tree(staged)?) - } else if let Some(sha) = sha { + } else if let Some(CommitReference::Single(sha)) = reference { GitEntity::Commit(Commit::new(sha)?) + } else if let Some(CommitReference::Range { from, to }) = reference { + GitEntity::Diff(Diff::from_commits_range(&from, &to)?) } else { return Err(LumenError::InvalidArguments( "`explain` expects SHA-1 or --diff to be present".into(), From 644b2e684097346d53a415e7ac096b09228280b2 Mon Sep 17 00:00:00 2001 From: Sahaj Jain Date: Fri, 22 Nov 2024 00:35:09 +0530 Subject: [PATCH 2/3] refactor(git_entity): Add Diff variants and update AIPrompt implementation --- src/ai_prompt.rs | 14 +++++++------- src/git_entity/diff.rs | 20 ++++++++++++++------ src/git_entity/mod.rs | 10 +++++++--- 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/src/ai_prompt.rs b/src/ai_prompt.rs index 49fe8e4..78fa311 100644 --- a/src/ai_prompt.rs +++ b/src/ai_prompt.rs @@ -1,6 +1,6 @@ use crate::{ command::{draft::DraftCommand, explain::ExplainCommand}, - git_entity::GitEntity, + git_entity::{diff::Diff, GitEntity}, }; use indoc::{formatdoc, indoc}; use thiserror::Error; @@ -39,15 +39,14 @@ impl AIPrompt { diff = commit.diff } } - GitEntity::Diff(diff) => { + GitEntity::Diff(Diff::WorkingTree { diff, .. } | Diff::CommitsRange { diff, .. }) => { formatdoc! {" Context - Changes: ```diff {diff} ``` - ", - diff = diff.diff + " } } }; @@ -90,8 +89,10 @@ impl AIPrompt { } pub fn build_draft_prompt(command: &DraftCommand) -> Result { - let GitEntity::Diff(diff) = &command.git_entity else { - return Err(AIPromptError("`draft` is only supported for diffs".into())); + let GitEntity::Diff(Diff::WorkingTree { diff, .. }) = &command.git_entity else { + return Err(AIPromptError( + "`draft` is only supported for working tree diffs".into(), + )); }; let system_prompt = String::from(indoc! {" @@ -131,7 +132,6 @@ impl AIPrompt { ``` ", commit_types = command.draft_config.commit_types, - diff = diff.diff }); Ok(AIPrompt { diff --git a/src/git_entity/diff.rs b/src/git_entity/diff.rs index 0312cf8..ecdd6d8 100644 --- a/src/git_entity/diff.rs +++ b/src/git_entity/diff.rs @@ -8,9 +8,16 @@ pub enum DiffError { } #[derive(Clone, Debug)] -pub struct Diff { - pub staged: bool, - pub diff: String, +pub enum Diff { + WorkingTree { + staged: bool, + diff: String, + }, + CommitsRange { + from: String, + to: String, + diff: String, + }, } impl Diff { @@ -28,7 +35,7 @@ impl Diff { return Err(DiffError::EmptyDiff { staged }.into()); } - Ok(Diff { staged, diff }) + Ok(Diff::WorkingTree { staged, diff }) } pub fn from_commits_range(from: &str, to: &str) -> Result { @@ -41,8 +48,9 @@ impl Diff { return Err(DiffError::EmptyDiff { staged: false }.into()); } - Ok(Diff { - staged: false, + Ok(Diff::CommitsRange { + from: from.to_string(), + to: to.to_string(), diff, }) } diff --git a/src/git_entity/mod.rs b/src/git_entity/mod.rs index 3de74c7..bf47d70 100644 --- a/src/git_entity/mod.rs +++ b/src/git_entity/mod.rs @@ -26,10 +26,14 @@ impl GitEntity { date = commit.date, message = commit.message, }, - GitEntity::Diff(diff) => formatdoc! {" - # Entity: Diff{staged}", - staged = if diff.staged { " (staged)" } else { "" } + GitEntity::Diff(Diff::WorkingTree { staged, .. }) => formatdoc! {" + # Entity: Working Tree Diff{staged}", + staged = if *staged { " (staged)" } else { "" } }, + GitEntity::Diff(Diff::CommitsRange { from, to, .. }) => formatdoc! {" + # Entity: Range + `{from}` -> `{to}` + "}, } } } From fece7a8125d4f64da2f13b5d9ceb543aeeb717e8 Mon Sep 17 00:00:00 2001 From: Sahaj Jain Date: Fri, 22 Nov 2024 00:54:35 +0530 Subject: [PATCH 3/3] feat: Add handling for commits range in AI prompt generation --- src/ai_prompt.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/ai_prompt.rs b/src/ai_prompt.rs index 78fa311..5af748f 100644 --- a/src/ai_prompt.rs +++ b/src/ai_prompt.rs @@ -71,7 +71,7 @@ impl AIPrompt { 2. Direct impact " }, - GitEntity::Diff(_) => formatdoc! {" + GitEntity::Diff(Diff::WorkingTree { .. }) => formatdoc! {" {base_content} Provide: @@ -79,6 +79,14 @@ impl AIPrompt { 2. Notable concerns (if any) " }, + GitEntity::Diff(Diff::CommitsRange { .. }) => formatdoc! {" + {base_content} + + Provide: + 1. Core changes made + 2. Direct impact + " + }, }, };