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
4 changes: 4 additions & 0 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# .cargo/config.toml

[target.'cfg(windows)']
rustflags = ["-C", "target-feature=+crt-static"]
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
/docs/site/
/.DS_Store
/.idea/
/.vscode-test/
/.vscode-test/"GEMINI.md"
5 changes: 1 addition & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name = "git-intelligence-message"
version = "1.7.0"
edition = "2024"
description = "An advanced Git commit message generation utility with AI assistance"
description = "An advanced Git commit message generation utility with AI assistance. Supports Windows, macOS, and Linux."
authors = ["Sheldon.Wei<sheldon.sh.hb@gmail.com>"]
license = "MIT"
repository = "https://github.com/davelet/git-intelligence-message"
Expand All @@ -16,9 +16,6 @@ lto = true
codegen-units = 1
panic = "abort"

[target.'cfg(windows)'.rustflags]
rustflags = ["-C", "target-feature=+crt-static"]

[dependencies]
gim-config = "1.0.0"
# gim-config = { git = "https://github.com/davelet/gim-config", branch = "develop" }
Expand Down
95 changes: 95 additions & 0 deletions GEMINI.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Git Intelligence Message (GIM) - Project Context for Qwen

## Project Overview

This project, Git Intelligence Message (GIM), is a command-line utility written in Rust. Its primary function is to automatically generate high-quality Git commit messages by analyzing the changes in the staging area of a Git repository. It leverages an AI model, accessed via an HTTP API, to interpret the code changes (diff) and produce a concise, structured commit message.

Key features include:
- Automatic generation of both a commit subject (type and summary) and a detailed commit body based on staged changes.
- Integration with various AI models (e.g., Qwen, GPT, Gemini) via configurable API endpoints.
- Customizable AI prompts for tailoring the commit message style.
- Subcommands for managing configuration (`ai`, `config`, `prompt`) and performing self-updates (`update`).

## Building and Running

### Prerequisites
- Rust toolchain (e.g., via rustup)
- Git
- An API key for an AI service (e.g., Qwen, OpenAI)

### Setup
1. Clone the repository.
2. Navigate to the project directory.
3. Ensure Rust and Cargo are installed.

### Building
To build the project:
```bash
cargo build
```
To build an optimized release version:
```bash
cargo build --release
```

### Running
The main executable is `gim` (or `gim.exe` on Windows), located in `target/debug/` (or `target/release/`).

Before using GIM for the first time, configure the AI settings:
```bash
# Example for Qwen
gim ai -m qwen2.5-72b-instruct -k YOUR_API_KEY -u https://dashscope.aliyuncs.com/compatible-mode/v1 -l English
```

To generate a commit message for staged changes:
```bash
# Ensure changes are staged first, e.g., `git add .`
gim
# Or, to automatically add all changes and generate a commit:
gim -a
```

Other common commands:
```bash
# Show help
gim --help

# Show help for a specific subcommand
gim prompt --help

# Check for updates
gim update

# View or edit AI prompts
gim prompt
gim prompt -e -t diff

# View current AI configuration
gim ai
```

## Development Conventions

- **Language:** Rust is the primary language, targeting the 2024 edition.
- **CLI Framework:** Uses `clap` for parsing command-line arguments.
- **Configuration:** Uses TOML for configuration files, managed by the `gim-config` crate.
- **HTTP Client:** `reqwest` is used for making asynchronous HTTP requests to AI APIs.
- **Async Runtime:** `tokio` is used for asynchronous operations.
- **Logging:** Basic logging is available via `log` and `pretty_env_logger`.
- **Testing:** Unit tests are included within modules (e.g., `src/cli/http.rs`).

### Code Structure (src/)
- `main.rs`: Entry point, initializes CLI parsing and configuration.
- `cli/`: Contains core CLI logic.
- `command.rs`: Defines the CLI structure and arguments using `clap`.
- `entry.rs`: Main logic for handling commands and orchestrating the commit message generation process. This includes calling Git commands, interacting with the AI via `http.rs`, and executing the final `git commit`.
- `http.rs`: Handles communication with the AI API.
- `prompt.rs`: Manages the customizable AI prompts for generating the diff summary and the commit subject.
- `ai_configer.rs`: Utilities for reading and updating the AI-related configuration.
- `custom_param.rs`: Utilities for managing custom parameters like `lines_limit`.
- `update/`: Logic related to self-updating the tool.
- `verbose.rs`: Utility for controlling verbose output.

### Configuration
- AI settings (model, API key, URL, language) are stored in a TOML configuration file.
- Customizable prompt templates are stored in separate files within the user's configuration directory.
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,23 @@ If you find any issues, please report them to me. I will do my best to fix them

I look forward to hearing your feedback. Feel free to fire feature request or bug issue if you want and necessary: [Issues](https://github.com/davelet/git-intelligence-message/issues/new).

> This project is currently macOS-only. Contributions to support other platforms (Windows, Linux, etc.) are highly welcome and appreciated.
## Platform Support

This project now officially supports multiple platforms:
- Windows (7 and later)
- macOS (10.12 and later)
- Linux (most distributions)

The application has been tested on these platforms and should work without issues. If you encounter any platform-specific problems, please [report them](https://github.com/davelet/git-intelligence-message/issues/new).

## AI Provider URL Configuration

When configuring the AI provider URL, you can now simply provide the base URL without the full path. The application will automatically append the appropriate path based on the provider:

- For OpenAI: `https://api.openai.com` (automatically becomes `https://api.openai.com/v1/chat/completions`)
- For providers with base URL ending in `/v1`: `https://api.provider.com/v1` (automatically becomes `https://api.provider.com/v1/chat/completions`)
- For providers with full URL: `https://api.provider.com/v1/chat/completions` (used as-is)

This simplifies configuration and makes it more intuitive for users.

Thank you for your interest in this project.
4 changes: 2 additions & 2 deletions src/cli/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,6 @@ pub enum GimCommands {

/// Print config file's location
#[arg(long, default_value_t = false)]
show_location: bool
}
show_location: bool,
},
}
25 changes: 14 additions & 11 deletions src/cli/custom_param.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use gim_config::config;
use std::io::ErrorKind;
use std::io::Result;
use toml::{map::Map, Value};
use toml::{Value, map::Map};

use crate::{
cli::verbose::print_verbose,
Expand Down Expand Up @@ -38,19 +38,22 @@ pub fn set_lines_limit(lines_limit: usize) -> Result<()> {
"get custom config '{}' error: {:?}, return default: {}",
NAME, e, DIFF_SIZE_LIMIT
));
if e.kind() == ErrorKind::NotFound {
if e.to_string() == format!("Section '{}' not found", CUSTOM_SECTION_NAME) {
let mut config = config::get_config().unwrap();
let map = config.as_table_mut().unwrap();
if e.kind() == ErrorKind::NotFound
&& e.to_string() == format!("Section '{}' not found", CUSTOM_SECTION_NAME)
{
let mut config = config::get_config().unwrap();
let map = config.as_table_mut().unwrap();

let mut update_table = Map::new();
update_table.insert(NAME.to_string(), Value::Integer(lines_limit as i64));
map.insert(CUSTOM_SECTION_NAME.to_string(), Value::Table(update_table));
return config::save_config(&mut config);
}
let mut update_table = Map::new();
update_table.insert(NAME.to_string(), Value::Integer(lines_limit as i64));
map.insert(CUSTOM_SECTION_NAME.to_string(), Value::Table(update_table));
return config::save_config(&config);
}
return Err(e);
}
println!("set custom config '{}' done, value: {:?}", NAME, lines_limit);
println!(
"set custom config '{}' done, value: {:?}",
NAME, lines_limit
);
Ok(())
}
81 changes: 49 additions & 32 deletions src/cli/entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ pub async fn run_cli(cli: &GimCli, mut config: toml::Value) {
}
'max: {
if let Some(max) = max {
if *max <= 0 {
if *max == 0 {
eprintln!("Error: --max must be a positive integer");
break 'max;
}
Expand All @@ -49,7 +49,7 @@ pub async fn run_cli(cli: &GimCli, mut config: toml::Value) {
}
'interval: {
if let Some(interval) = interval {
if *interval <= 0 {
if *interval == 0 {
eprintln!("Error: --interval must be a positive integer");
break 'interval;
}
Expand All @@ -61,11 +61,9 @@ pub async fn run_cli(cli: &GimCli, mut config: toml::Value) {
}
}
}
} else {
if let Err(e) = crate::cli::update::check_and_install_update(*force).await {
eprintln!("Failed to update: {}", e);
std::process::exit(1);
}
} else if let Err(e) = crate::cli::update::check_and_install_update(*force).await {
eprintln!("Failed to update: {}", e);
std::process::exit(1);
}
return;
}
Expand Down Expand Up @@ -145,11 +143,11 @@ pub async fn run_cli(cli: &GimCli, mut config: toml::Value) {
eprintln!("Error: {}", e);
}
}
if let Some(lines_limit) = lines_limit {
if let Err(e) = super::custom_param::set_lines_limit(*lines_limit) {
eprintln!("Error: {}", e);
std::process::exit(1);
}
if let Some(lines_limit) = lines_limit
&& let Err(e) = super::custom_param::set_lines_limit(*lines_limit)
{
eprintln!("Error: {}", e);
std::process::exit(1);
}
return;
}
Expand Down Expand Up @@ -233,15 +231,15 @@ pub async fn run_cli(cli: &GimCli, mut config: toml::Value) {
// Add file status information (including deleted files)
let status_info = String::from_utf8_lossy(&diff_output.stdout);
diff_content.push_str(&status_info);
diff_content.push_str("\n");
diff_content.push('\n');

// Add full diff content only for added/modified files
if !full_diff_output.stdout.is_empty() {
diff_content.push_str(
"\nDetailed changes for added/modified files (excluding deleted files):\n",
);
diff_content.push_str(&String::from_utf8_lossy(&full_diff_output.stdout));
diff_content.push_str("\n");
diff_content.push('\n');
}
}
}
Expand All @@ -267,13 +265,13 @@ pub async fn run_cli(cli: &GimCli, mut config: toml::Value) {
// Add file status information (including deleted files)
let status_info = String::from_utf8_lossy(&show_status_output.stdout);
diff_content.push_str(&status_info);
diff_content.push_str("\n");
diff_content.push('\n');

// Add full diff content only for added/modified files
if !show_diff_output.stdout.is_empty() {
diff_content.push_str("\nDetailed changes for added/modified files in last commit (excluding deleted files):\n");
diff_content.push_str(&String::from_utf8_lossy(&show_diff_output.stdout));
diff_content.push_str("\n");
diff_content.push('\n');
}
println!("As '-p' option is enabled, I will amend the last commit message");
}
Expand All @@ -295,7 +293,7 @@ pub async fn run_cli(cli: &GimCli, mut config: toml::Value) {
std::process::exit(1);
}

let config_result = get_validated_ai_config(cli.auto_add, changes.len() > 0);
let config_result = get_validated_ai_config(cli.auto_add, !changes.is_empty());
if config_result.is_none() {
return;
}
Expand All @@ -318,7 +316,10 @@ pub async fn run_cli(cli: &GimCli, mut config: toml::Value) {
)
.await;
if let Err(e) = res {
ai_generating_error(&format!("Error: {}", e), cli.auto_add && changes.len() > 0);
ai_generating_error(
&format!("Error: {}", e),
cli.auto_add && !changes.is_empty(),
);
return;
}
let file_changes = res.unwrap();
Expand Down Expand Up @@ -433,25 +434,29 @@ fn handle_prompt_command(
} else {
// Open the directory with default file manager
if cfg!(target_os = "macos") {
Command::new("open")
if let Err(e) = Command::new("open")
.arg("-R") // Reveal in Finder
.arg(&file_path)
.status()?;
.status()
{
eprintln!("Failed to open file in Finder: {}", e);
}
} else if cfg!(target_os = "windows") {
Command::new("explorer")
if let Err(e) = Command::new("explorer")
.arg("/select,")
.arg(&file_path)
.status()?;
.status()
{
eprintln!("Failed to open file in Explorer: {}", e);
}
} else {
// Linux and others
if let Err(_) = Command::new("xdg-open")
if Command::new("xdg-open")
.arg(file_path.parent().unwrap_or_else(|| ".".as_ref()))
.status()
.is_err()
{
return Err(
"Failed to open file manager. Please specify an editor with --editor"
.into(),
);
eprintln!("Failed to open file manager. Please specify an editor with --editor");
}
}
}
Expand Down Expand Up @@ -490,15 +495,27 @@ fn open_config_directory() -> Result<(), Box<dyn std::error::Error>> {
let config_dir = directory::config_dir()?;
// Open the directory with default file manager
if cfg!(target_os = "macos") {
Command::new("open")
// .arg("-R") // Reveal in Finder
if let Err(e) = Command::new("open")
.arg(&config_dir)
.status()?;
.status()
{
eprintln!("Failed to open config directory in Finder: {}", e);
}
} else if cfg!(target_os = "windows") {
Command::new("explorer").arg(&config_dir).status()?;
if let Err(e) = Command::new("explorer")
.arg(&config_dir)
.status()
{
eprintln!("Failed to open config directory in Explorer: {}", e);
}
} else {
// Linux and others
Command::new("xdg-open").arg(&config_dir).status()?;
if let Err(e) = Command::new("xdg-open")
.arg(&config_dir)
.status()
{
eprintln!("Failed to open config directory: {}", e);
}
}
Ok(())
}
Expand Down
Loading