- Introduction
- Architecture Overview
- Process Flow
- Phase-by-Phase Breakdown
- Multi-Step Analysis System
- Implementation Details
- Error Handling & Fallbacks
- Performance Optimization
- Configuration & Customization
Git AI is a sophisticated Rust-based CLI tool that automates the generation of high-quality commit messages by analyzing git diffs through a structured, multi-phase process. The system seamlessly integrates with git hooks to intercept the commit process and generate contextually relevant commit messages using AI.
New in v1.1+: The system now features a parallel git diff analysis algorithm that dramatically improves performance by processing files concurrently instead of sequentially, reducing commit message generation time from ~6.6s to ~4s for single files, with even greater improvements for multi-file commits.
The system consists of several key components:
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Git Command │────▶│ Git AI Hook │────▶│ Multi-Step │
│ (git commit) │ │ (prepare-commit) │ │ Processor │
└─────────────────┘ └──────────────────┘ └─────────────────┘
│ │
▼ ▼
┌──────────────────┐ ┌─────────────────┐
│ Diff Parser │ │ AI Analysis │
│ & Processor │ │ (OpenAI/Local)│
└──────────────────┘ └─────────────────┘
-
CLI Interface (
src/main.rs)- Command-line interface for configuration and hook management
- Handles installation, uninstallation, and configuration commands
-
Git Hook Binary (
src/bin/hook.rs)- Actual git hook executable invoked during commit process
- Manages the prepare-commit-msg hook lifecycle
-
Multi-Step Integration (
src/multi_step_integration.rs)- Orchestrates the multi-phase commit message generation
- Coordinates between different analysis steps
-
Multi-Step Analysis (
src/multi_step_analysis.rs)- Implements file categorization and impact scoring
- Generates commit message candidates
-
Function Calling (
src/function_calling.rs)- Implements OpenAI function calling for structured output
- Ensures consistent response format
graph TD
A[Git Commit Initiated] --> B{Check Arguments}
B -->|No Message| C[Parse Git Diff]
B -->|Message Provided| Z[Use Provided Message]
C --> D[Multi-Step Analysis]
D --> E[File Analysis]
E --> F[Impact Scoring]
F --> G[Message Generation]
G --> H[Best Candidate Selection]
H --> I{API Available?}
I -->|Yes| J[OpenAI Multi-Step]
I -->|No| K[Local Multi-Step]
J --> L{Success?}
K --> M{Success?}
L -->|Yes| N[Apply Message]
L -->|No| K
M -->|Yes| N
M -->|No| O[Single-Step Fallback]
O --> P{Success?}
P -->|Yes| N
P -->|No| Q[Error]
N --> R[Complete Commit]
When git commit is executed without a message:
-
Hook Invocation
// src/bin/hook.rs struct Args { commit_msg_file: PathBuf, // .git/COMMIT_EDITMSG source: Option<Source>, // message, template, merge, squash, commit sha1: Option<String> // HEAD or specific commit SHA }
-
Environment Setup
- Parse command-line arguments
- Initialize logging and profiling
- Load configuration from
~/.config/git-ai/config.toml - Validate OpenAI API key availability
-
Repository Context
- Open git repository from current directory
- Determine commit type (new, amend, merge, etc.)
- Check for existing commit messages
The system generates and processes the git diff:
// src/hook.rs
impl PatchRepository for Repository {
fn to_commit_diff(&self, tree: Option<Tree<'_>>) -> Result<git2::Diff<'_>> {
// Get diff between tree and index (staged changes only)
match tree {
Some(tree) => self.diff_tree_to_index(Some(&tree), None, Some(&mut opts)),
None => {
// If no HEAD, compare against empty tree
let empty_tree = self.find_tree(self.treebuilder(None)?.write()?)?;
self.diff_tree_to_index(Some(&empty_tree), None, Some(&mut opts))
}
}
}
}Diff Processing Features:
- Parallel processing for large diffs
- Token counting and management
- Memory pooling for efficiency
- Chunk-based processing (25 files per chunk)
The system employs multiple sophisticated approaches with intelligent fallbacks:
The latest parallel approach offers significant performance improvements by processing files concurrently:
// src/multi_step_integration.rs
pub async fn generate_commit_message_parallel(
client: &Client<OpenAIConfig>,
model: &str,
diff_content: &str,
max_length: Option<usize>
) -> Result<String> {
// Phase 1: Parse diff and analyze files in parallel
let parsed_files = parse_diff(diff_content)?;
let analysis_futures = parsed_files.iter().map(|file| {
analyze_single_file_simple(client, model, &file.path, &file.operation, &file.diff_content)
});
let analysis_results = join_all(analysis_futures).await;
// Phase 2: Synthesize final commit message from all analyses
synthesize_commit_message(client, model, &successful_analyses, max_length).await
}Key Benefits:
- Performance: ~6.6s → ~4s for single files, ~4.3s vs ~16s for 5-file commits
- Simplicity: Uses plain text completion instead of complex function calling schemas
- Resilience: Continues processing if individual file analyses fail
- Architecture: Two-phase design (parallel analysis → unified synthesis)
// src/multi_step_integration.rs
pub async fn generate_commit_message_multi_step(
client: &Client<OpenAIConfig>,
model: &str,
diff_content: &str,
max_length: Option<usize>
) -> Result<String> {
// Step 1: Parse diff into individual files
let parsed_files = parse_diff(diff_content)?;
// Step 2: Analyze each file
let file_analyses = /* ... */;
// Step 3: Calculate impact scores
let scored_files = call_score_function(/* ... */)?;
// Step 4: Generate candidates
let candidates = call_generate_function(/* ... */)?;
// Step 5: Select best candidate
let final_message = select_best_candidate(/* ... */)?;
Ok(final_message)
}If multi-step fails, the system falls back to a simpler approach using function calling.
The generation process follows these principles:
-
Analyze Functional Significance
- Identify primary changes
- Determine change impact
- Group related modifications
-
Generate Reasoning
- Explain why changes were made
- Justify message selection
- Document decision process
-
Create Message
- Follow conventional commit format
- Stay within character limits (default: 72)
- Focus on most impactful changes
For each modified file, the system performs:
// src/multi_step_analysis.rs
pub fn analyze_file(file_path: &str, diff_content: &str, operation_type: &str) -> FileAnalysisResult {
// Count lines added/removed
let mut lines_added = 0u32;
let mut lines_removed = 0u32;
for line in diff_content.lines() {
if line.starts_with('+') && !line.starts_with("+++") {
lines_added += 1;
} else if line.starts_with('-') && !line.starts_with("---") {
lines_removed += 1;
}
}
// Categorize file
let file_category = categorize_file(file_path);
// Generate summary
let summary = generate_file_summary(file_path, diff_content, operation_type);
FileAnalysisResult {
lines_added,
lines_removed,
file_category,
summary
}
}File Categories:
source: Core application code (weight: 1.0)test: Test files (weight: 0.6)config: Configuration files (weight: 0.8)docs: Documentation (weight: 0.4)binary: Binary files (weight: 0.1)build: Build/dependency files (weight: 0.7)
Impact scoring algorithm:
fn calculate_single_impact_score(file_data: &FileDataForScoring) -> f32 {
let mut score = 0.0f32;
// Base score from operation type
score += match file_data.operation_type.as_str() {
"added" => 0.3,
"modified" => 0.2,
"deleted" => 0.25,
"renamed" => 0.1,
"binary" => 0.05,
_ => 0.1
};
// Score from file category
score += match file_data.file_category.as_str() {
"source" => 0.4,
"test" => 0.2,
"config" => 0.25,
"build" => 0.3,
"docs" => 0.1,
"binary" => 0.05,
_ => 0.1
};
// Score from lines changed (normalized)
let total_lines = file_data.lines_added + file_data.lines_removed;
let line_score = (total_lines as f32 / 100.0).min(0.3);
score += line_score;
score.min(1.0) // Cap at 1.0
}The system includes comprehensive profiling:
// src/profiling.rs
macro_rules! profile {
($name:expr) => {
let _guard = $crate::profiling::ProfileGuard::new($name);
};
}Tracked Metrics:
- Total execution time
- API request/response duration
- Diff parsing time
- Token counting overhead
- File analysis duration
Once the message is generated:
- Write message to commit file
- Clear progress indicators
- Log performance metrics
- Return control to git
The multi-step system provides several advantages:
Instead of processing the entire diff at once, the system:
- Parses individual files
- Analyzes each independently
- Aggregates results intelligently
Three specialized OpenAI function tools:
// Analyze Function
create_analyze_function_tool() // Examines individual files
// Score Function
create_score_function_tool() // Calculates impact scores
// Generate Function
create_generate_function_tool() // Creates message candidatesMultiple message styles are generated:
// Action-focused: "Update authentication logic"
// Component-focused: "auth: improve validation"
// Impact-focused: "New feature for user authentication"The system implements a robust fallback strategy:
Multi-Step OpenAI → Local Multi-Step → Single-Step OpenAI → Error
-
API Failures
- Network timeouts
- Rate limiting
- Invalid responses
-
Token Limits
- Diff too large
- Context overflow
- Model constraints
-
Parsing Errors
- Malformed diffs
- Binary files
- Encoding issues
The parallel analysis algorithm represents a significant architectural improvement over the original sequential multi-step approach, offering dramatic performance gains and simplified API interactions.
The parallel approach employs a true divide-and-conquer strategy organized into two distinct phases:
Phase 1: Parallel Analysis Phase 2: Unified Synthesis
┌─────────────────────────┐ ┌─────────────────────────┐
│ File 1 Analysis │ │ │
│ ├─ analyze_single_file │ │ synthesize_commit_ │
│ └─ Result: Summary │ │ message() │
├─────────────────────────┤ │ │
│ File 2 Analysis │───┤ • Combine summaries │
│ ├─ analyze_single_file │ │ • Generate final msg │
│ └─ Result: Summary │ │ • Apply length limits │
├─────────────────────────┤ │ │
│ File N Analysis │ │ │
│ ├─ analyze_single_file │ │ │
│ └─ Result: Summary │ │ │
└─────────────────────────┘ └─────────────────────────┘
- True Parallelism: Files are analyzed simultaneously using
futures::future::join_all(), not sequentially - Simplified API: Plain text completion instead of complex function calling schemas
- Reduced Round-trips: Single synthesis call replaces 3 sequential API operations
- Better Resilience: Continues processing if individual file analyses fail
pub async fn analyze_single_file_simple(
client: &Client<OpenAIConfig>,
model: &str,
file_path: &str,
operation: &str,
diff_content: &str,
) -> Result<String> {
let system_prompt = "You are a git diff analyzer. Analyze the provided file change and provide a concise summary in 1-2 sentences describing what changed and why it matters.";
let user_prompt = format!(
"File: {}\nOperation: {}\nDiff:\n{}\n\nProvide a concise summary (1-2 sentences):",
file_path, operation, diff_content
);
// Simple text completion (no function calling)
let request = CreateChatCompletionRequestArgs::default()
.model(model)
.messages(/* system and user messages */)
.max_tokens(150u32)
.build()?;
let response = client.chat().create(request).await?;
Ok(response.choices[0].message.content.as_ref().unwrap().trim().to_string())
}pub async fn synthesize_commit_message(
client: &Client<OpenAIConfig>,
model: &str,
analyses: &[(String, String)], // (file_path, summary) pairs
max_length: usize,
) -> Result<String> {
// Build context from all analyses
let mut context = String::new();
context.push_str("File changes summary:\n");
for (file_path, summary) in analyses {
context.push_str(&format!("• {}: {}\n", file_path, summary));
}
let system_prompt = format!(
"Based on the file change summaries, generate a concise commit message ({} chars max) that captures the essential nature of the changes.",
max_length
);
// Single API call for final synthesis
let response = client.chat().create(request).await?;
Ok(response.choices[0].message.content.as_ref().unwrap().trim().to_string())
}| Scenario | Original Sequential | New Parallel | Improvement |
|---|---|---|---|
| Single file | 6.59s | ~4.0s | 39% faster |
| 5 files | ~16s (estimated) | ~4.3s | 73% faster |
| 10 files | ~32s (estimated) | ~4.6s | 86% faster |
The parallel approach provides enhanced resilience:
// Individual file analysis failures don't stop the process
for (result) in analysis_results {
match result {
Ok(summary) => successful_analyses.push(summary),
Err(e) => {
// Log warning but continue with other files
log::warn!("Failed to analyze file: {}", e);
}
}
}
if successful_analyses.is_empty() {
bail!("Failed to analyze any files in parallel");
}
// Continue with successful analyses onlyThe system maintains backward compatibility with graceful fallbacks:
- Primary: Parallel analysis algorithm (new)
- Secondary: Original multi-step approach
- Tertiary: Local generation without API
- Final: Single-step API call
// Process files in parallel chunks
const PARALLEL_CHUNK_SIZE: usize = 25;
files.par_chunks(PARALLEL_CHUNK_SIZE)
.map(|chunk| process_chunk(chunk))
.collect()struct StringPool {
strings: Vec<String>,
capacity: usize
}
// Reuse string allocations
let mut pool = StringPool::new(DEFAULT_STRING_CAPACITY);- Pre-calculate instruction tokens
- Truncate diffs intelligently
- Prioritize high-impact changes
Location: ~/.config/git-ai/config.toml
model = "gpt-4o-mini"
max_tokens = 4096
max_commit_length = 72
openai_api_key = "sk-..."
timeout = 30OPENAI_API_KEY=sk-... # API key
RUST_LOG=debug # Enable debug logging
GIT_AI_MODEL=gpt-4 # Override model# Set model
git-ai config set model gpt-4o-mini
# Set max commit length
git-ai config set max-commit-length 100
# Set API key
git-ai config set openai-api-key sk-...
# Reset to defaults
git-ai config reset-
Intelligent Fallback System
- Multi-step → Local → Single-step
- Ensures commits always succeed
-
Impact-Based Prioritization
- Focuses on functionally significant changes
- Weights files by category and size
-
Comprehensive Logging
- Debug output with timing
- Structured information display
- Performance metrics
-
Token Management
- Careful limit handling
- Intelligent truncation
- Model-aware processing
-
Error Resilience
- Graceful degradation
- Multiple retry strategies
- Clear error messages
<type>: <description>
<optional body>
<optional footer>
feat: Add JWT authentication system
- Implement JWT token generation and validation
- Add middleware for protected routes
- Include comprehensive error handling
- Update package.json with new dependencies
Files changed: 5 (4 source, 1 build)
Impact score: 0.95 (high priority changes)
[DEBUG] Starting multi-step commit message generation
[DEBUG] Parsed 5 files from diff
[DEBUG] Analyzing file: src/auth/jwt.rs
[DEBUG] File analysis complete: +89 -0 lines, category: source
[DEBUG] Calculating impact scores for 5 files
[DEBUG] Generated 3 commit message candidates
[DEBUG] Selected best candidate based on impact analysis
[DEBUG] Total execution time: 1.23s
The Git AI hook system represents a sophisticated approach to automated commit message generation. By combining multi-step analysis, intelligent fallbacks, and comprehensive error handling, it ensures developers always get meaningful commit messages that accurately represent their changes while maintaining high performance and reliability.