Merge pull request #13 from jmattaa/feature/proj-config

Add project configuaration.
This commit is contained in:
Sahaj Jain 2024-11-15 17:25:09 +05:30 committed by GitHub
commit ec15a678e8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 273 additions and 85 deletions

2
.gitignore vendored
View file

@ -1 +1,3 @@
/target
lumen.config.json

View file

@ -15,6 +15,7 @@ lumen is a command-line tool that uses AI to generate commit messages, summarise
- Generate commit message for staged changes
- Generate summary for changes in a git commit by providing its [SHA-1](https://graphite.dev/guides/git-hash)
- Generate summary for changes in git diff (staged/unstaged)
- Ask questions about a specific change
- Fuzzy-search for the commit to generate a summary
- Free and unlimited - no API key required to work out of the box
- Pretty output formatting enabled by Markdown

View file

@ -77,20 +77,6 @@ impl AIPrompt {
return Err(AIPromptError("`draft` is only supported for diffs".into()));
};
let conventional_types = r#"{
"docs": "Documentation only changes",
"style": "Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)",
"refactor": "A code change that neither fixes a bug nor adds a feature",
"perf": "A code change that improves performance",
"test": "Adding missing tests or correcting existing tests",
"build": "Changes that affect the build system or external dependencies",
"ci": "Changes to our CI configuration files and scripts",
"chore": "Other changes that don't modify src or test files",
"revert": "Reverts a previous commit",
"feat": "A new feature",
"fix": "A bug fix"
}"#;
let system_prompt = String::from(
"You are a commit message generator that follows these rules:\
\n1. Write in present tense\
@ -115,7 +101,7 @@ impl AIPrompt {
\nExclude anything unnecessary such as translation. Your entire response will be passed directly into git commit.\
\n\nCode diff:\n```diff\n{}\n```",
context,
conventional_types,
command.draft_config.commit_types,
diff.diff
);

View file

@ -2,13 +2,17 @@ use std::io::Write;
use async_trait::async_trait;
use crate::{error::LumenError, git_entity::GitEntity, provider::LumenProvider};
use crate::{
config::configuration::DraftConfig, error::LumenError, git_entity::GitEntity,
provider::LumenProvider,
};
use super::Command;
pub struct DraftCommand {
pub git_entity: GitEntity,
pub context: Option<String>,
pub draft_config: DraftConfig,
}
#[async_trait]

View file

@ -4,9 +4,10 @@ use explain::ExplainCommand;
use list::ListCommand;
use std::process::Stdio;
use crate::config::configuration::DraftConfig;
use crate::error::LumenError;
use crate::git_entity::git_diff::GitDiff;
use crate::git_entity::{GitEntity};
use crate::git_entity::GitEntity;
use crate::provider::LumenProvider;
pub mod draft;
@ -20,7 +21,7 @@ pub enum CommandType {
query: Option<String>,
},
List,
Draft(Option<String>),
Draft(Option<String>, DraftConfig),
}
#[async_trait]
@ -35,9 +36,10 @@ impl CommandType {
Box::new(ExplainCommand { git_entity, query })
}
CommandType::List => Box::new(ListCommand),
CommandType::Draft(context) => Box::new(DraftCommand {
CommandType::Draft(context, draft_config) => Box::new(DraftCommand {
context,
git_entity: GitEntity::Diff(GitDiff::new(true)?),
draft_config
}),
})
}

74
src/config/cli.rs Normal file
View file

@ -0,0 +1,74 @@
use clap::{command, Parser, Subcommand, ValueEnum};
use std::str::FromStr;
#[derive(Parser)]
#[command(name = "lumen")]
#[command(about = "AI-powered CLI tool for git commit summaries", long_about = None)]
#[command(version)]
pub struct Cli {
#[arg(value_enum, short = 'p', long = "provider")]
pub provider: Option<ProviderType>,
#[arg(short = 'k', long = "api-key")]
pub api_key: Option<String>,
#[arg(short = 'm', long = "model")]
pub model: Option<String>,
#[command(subcommand)]
pub command: Commands,
}
#[derive(Copy, Clone, PartialEq, Eq, ValueEnum, Debug)]
pub enum ProviderType {
Openai,
Phind,
Groq,
Claude,
Ollama,
}
impl FromStr for ProviderType {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"openai" => Ok(ProviderType::Openai),
"phind" => Ok(ProviderType::Phind),
"groq" => Ok(ProviderType::Groq),
"claude" => Ok(ProviderType::Claude),
"ollama" => Ok(ProviderType::Ollama),
_ => Err(format!("Unknown provider: {}", s)),
}
}
}
#[derive(Subcommand)]
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>,
/// Explain current diff
#[arg(long, group = "target")]
diff: bool,
/// Use staged diff
#[arg(long)]
staged: bool,
/// Ask a question instead of summary
#[arg(short, long)]
query: Option<String>,
},
/// List all commits in an interactive fuzzy-finder, and summarize the changes
List,
/// Generate a commit message for the staged changes
Draft {
/// Add context to communicate intent
#[arg(short, long)]
context: Option<String>,
},
}

160
src/config/configuration.rs Normal file
View file

@ -0,0 +1,160 @@
use crate::config::cli::ProviderType;
use crate::error::LumenError;
use serde::{Deserialize, Deserializer};
use std::collections::HashMap;
use std::env;
use std::fs;
use crate::Cli;
#[derive(Debug, Deserialize)]
pub struct LumenConfig {
#[serde(
default = "default_ai_provider",
deserialize_with = "deserialize_ai_provider"
)]
pub ai_provider: ProviderType,
#[serde(default = "default_model")]
pub model: String,
#[serde(default = "default_api_key")]
pub api_key: String,
#[serde(default = "default_draft_config")]
pub draft: DraftConfig,
#[serde(default)]
pub explain: Option<ExplainConfig>,
#[serde(default)]
pub list: Option<ListConfig>,
}
#[derive(Debug, Deserialize, Default)]
pub struct DraftConfig {
#[serde(
default = "default_commit_types",
deserialize_with = "deserialize_commit_types"
)]
pub commit_types: String,
}
#[derive(Debug, Deserialize, Default)]
pub struct ExplainConfig {
// Add explain-specific settings
}
#[derive(Debug, Deserialize, Default)]
pub struct ListConfig {
// Add list-specific settings
}
fn default_ai_provider() -> ProviderType {
env::var("LUMEN_AI_PROVIDER")
.unwrap_or_else(|_| "phind".to_string())
.parse()
.unwrap_or(ProviderType::Phind)
}
fn deserialize_ai_provider<'de, D>(deserializer: D) -> Result<ProviderType, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
s.parse().map_err(serde::de::Error::custom)
}
fn default_model() -> String {
env::var("LUMEN_AI_MODEL").unwrap_or_else(|_| "".to_string())
}
fn default_api_key() -> String {
env::var("LUMEN_API_KEY").unwrap_or_else(|_| "".to_string())
}
fn default_commit_types() -> String {
r#"{
"docs": "Documentation only changes",
"style": "Changes that do not affect the meaning of the code",
"refactor": "A code change that neither fixes a bug nor adds a feature",
"perf": "A code change that improves performance",
"test": "Adding missing tests or correcting existing tests",
"build": "Changes that affect the build system or external dependencies",
"ci": "Changes to our CI configuration files and scripts",
"chore": "Other changes that don't modify src or test files",
"revert": "Reverts a previous commit",
"feat": "A new feature",
"fix": "A bug fix"
}"#
.to_string()
}
fn deserialize_commit_types<'de, D>(deserializer: D) -> Result<String, D::Error>
where
D: Deserializer<'de>,
{
let commit_types_map: HashMap<String, String> = HashMap::deserialize(deserializer)?;
serde_json::to_string(&commit_types_map).map_err(serde::de::Error::custom)
}
fn default_draft_config() -> DraftConfig {
DraftConfig {
commit_types: default_commit_types(),
}
}
impl LumenConfig {
pub fn Build(cli: &Cli) -> Result<Self, LumenError> {
let config_path = "./lumen.config.json";
let config = match LumenConfig::from_file(config_path) {
Ok(config) => config,
Err(e) => return Err(e),
};
let ai_provider: ProviderType = cli
.provider
.or_else(|| Some(config.ai_provider))
.unwrap_or(default_ai_provider());
let api_key: String = cli.api_key.clone().unwrap_or(config.api_key);
let model: String = cli.model.clone().unwrap_or(config.model);
Ok(LumenConfig {
ai_provider,
model,
api_key,
draft: config.draft,
explain: None,
list: None,
})
}
pub fn from_file(file_path: &str) -> Result<Self, LumenError> {
let content = match fs::read_to_string(file_path) {
Ok(content) => content,
// FILE DOSENT EXIST
Err(_) => return Ok(LumenConfig::default()),
};
match serde_json::from_str::<LumenConfig>(&content) {
Ok(config) => Ok(config),
Err(e) => {
Err(LumenError::InvalidConfiguration(e.to_string()))
}
}
}
}
impl Default for LumenConfig {
fn default() -> Self {
LumenConfig {
ai_provider: default_ai_provider(),
model: default_model(),
api_key: default_api_key(),
draft: default_draft_config(),
explain: None,
list: None,
}
}
}

4
src/config/mod.rs Normal file
View file

@ -0,0 +1,4 @@
pub mod configuration;
pub mod cli;
pub use configuration::LumenConfig;

View file

@ -22,6 +22,9 @@ pub enum LumenError {
#[error("Invalid arguments: {0}")]
InvalidArguments(String),
#[error("Invalid configuration: {0}")]
InvalidConfiguration(String),
#[error(transparent)]
IoError(#[from] io::Error),

View file

@ -1,76 +1,17 @@
use clap::{command, Parser, Subcommand, ValueEnum};
use clap::Parser;
use config::cli::{Cli, Commands};
use config::LumenConfig;
use error::LumenError;
use git_entity::{git_commit::GitCommit, git_diff::GitDiff, GitEntity};
use std::process;
mod ai_prompt;
mod command;
mod config;
mod error;
mod git_entity;
mod provider;
#[derive(Parser)]
#[command(name = "lumen")]
#[command(about = "AI-powered CLI tool for git commit summaries", long_about = None)]
struct Cli {
#[arg(
value_enum,
short = 'p',
long = "provider",
env("LUMEN_AI_PROVIDER"),
default_value = "phind"
)]
provider: ProviderType,
#[arg(short = 'k', long = "api-key", env = "LUMEN_API_KEY")]
api_key: Option<String>,
#[arg(short = 'm', long = "model", env = "LUMEN_AI_MODEL")]
model: Option<String>,
#[command(subcommand)]
command: Commands,
}
#[derive(Copy, Clone, PartialEq, Eq, ValueEnum, Debug)]
enum ProviderType {
Openai,
Phind,
Groq,
Claude,
Ollama,
}
#[derive(Subcommand)]
enum Commands {
/// Explain the changes in a commit, or the current diff
Explain {
/// The commit hash to use
#[arg(group = "target")]
sha: Option<String>,
/// Explain current diff
#[arg(long, group = "target")]
diff: bool,
/// Use staged diff
#[arg(long)]
staged: bool,
/// Ask a question instead of summary
#[arg(short, long)]
query: Option<String>,
},
/// List all commits in an interactive fuzzy-finder, and summarize the changes
List,
/// Generate a commit message for the staged changes
Draft {
/// Add context to communicate intent
#[arg(short, long)]
context: Option<String>,
},
}
#[tokio::main]
async fn main() {
if let Err(e) = run().await {
@ -82,7 +23,18 @@ async fn main() {
async fn run() -> Result<(), LumenError> {
let cli = Cli::parse();
let client = reqwest::Client::new();
let provider = provider::LumenProvider::new(client, cli.provider, cli.api_key, cli.model)?;
let config = match LumenConfig::Build(&cli) {
Ok(config) => config,
Err(e) => return Err(e),
};
let provider = provider::LumenProvider::new(
client,
config.ai_provider,
Some(config.api_key),
Some(config.model),
)?;
let command = command::LumenCommand::new(provider);
match cli.command {
@ -109,7 +61,7 @@ async fn run() -> Result<(), LumenError> {
Commands::List => command.execute(command::CommandType::List).await?,
Commands::Draft { context } => {
command
.execute(command::CommandType::Draft(context))
.execute(command::CommandType::Draft(context, config.draft))
.await?
}
}

View file

@ -5,12 +5,12 @@ use ollama::{OllamaConfig, OllamaProvider};
use openai::{OpenAIConfig, OpenAIProvider};
use phind::{PhindConfig, PhindProvider};
use thiserror::Error;
use crate::config::cli::ProviderType;
use crate::{
ai_prompt::{AIPrompt, AIPromptError},
command::{draft::DraftCommand, explain::ExplainCommand},
error::LumenError,
ProviderType,
};
pub mod claude;