mirror of
https://github.com/jnsahaj/lumen.git
synced 2025-12-23 05:36:48 +00:00
Merge pull request #27 from jnsahaj/feature/explain-range-diff
Feature/explain range diff
This commit is contained in:
commit
ede23bcdff
7 changed files with 187 additions and 21 deletions
|
|
@ -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
|
||||
"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -72,7 +71,7 @@ impl AIPrompt {
|
|||
2. Direct impact
|
||||
"
|
||||
},
|
||||
GitEntity::Diff(_) => formatdoc! {"
|
||||
GitEntity::Diff(Diff::WorkingTree { .. }) => formatdoc! {"
|
||||
{base_content}
|
||||
|
||||
Provide:
|
||||
|
|
@ -80,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
|
||||
"
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -90,8 +97,10 @@ impl AIPrompt {
|
|||
}
|
||||
|
||||
pub fn build_draft_prompt(command: &DraftCommand) -> Result<Self, AIPromptError> {
|
||||
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 +140,6 @@ impl AIPrompt {
|
|||
```
|
||||
",
|
||||
commit_types = command.draft_config.commit_types,
|
||||
diff = diff.diff
|
||||
});
|
||||
|
||||
Ok(AIPrompt {
|
||||
|
|
|
|||
124
src/commit_reference.rs
Normal file
124
src/commit_reference.rs
Normal file
|
|
@ -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<Self, Self::Err> {
|
||||
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::<CommitReference>().unwrap(),
|
||||
CommitReference::Single("HEAD".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_full_range() {
|
||||
assert_eq!(
|
||||
"main..feature".parse::<CommitReference>().unwrap(),
|
||||
CommitReference::Range {
|
||||
from: "main".to_string(),
|
||||
to: "feature".to_string(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_only_range() {
|
||||
assert_eq!(
|
||||
"develop..".parse::<CommitReference>().unwrap(),
|
||||
CommitReference::Range {
|
||||
from: "develop".to_string(),
|
||||
to: "HEAD".to_string(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_only_range() {
|
||||
assert_eq!(
|
||||
"..feature".parse::<CommitReference>().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::<CommitReference>(),
|
||||
Err(ReferenceParseError::Empty)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String>,
|
||||
#[arg(group = "target", value_parser = clap::value_parser!(CommitReference))]
|
||||
reference: Option<CommitReference>,
|
||||
|
||||
/// Explain current diff
|
||||
#[arg(long, group = "target")]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
pub mod configuration;
|
||||
pub mod cli;
|
||||
pub mod configuration;
|
||||
|
||||
pub use configuration::LumenConfig;
|
||||
|
|
|
|||
|
|
@ -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,6 +35,23 @@ 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<Self, LumenError> {
|
||||
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::CommitsRange {
|
||||
from: from.to_string(),
|
||||
to: to.to_string(),
|
||||
diff,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}`
|
||||
"},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue