Skip to content

Commit 50d9b8d

Browse files
committed
feat: add google gemini 2.5 flash preview support
- add gemini as a new ai provider option - implement gemini api integration with proper request/response handling - add gemini-specific error messages and rate limiting - update setup and config commands to support gemini - add tests for gemini integration - bump version to 0.3.0
1 parent 03e64e5 commit 50d9b8d

8 files changed

Lines changed: 257 additions & 14 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.3.0] - 2025-01-06
9+
10+
### Added
11+
- Support for Google Gemini 2.5 Flash Preview AI model
12+
- Gemini-specific error handling and messages
13+
- Integration tests for Gemini API
14+
15+
### Changed
16+
- Updated provider selection in setup and config commands to be dynamic
17+
- Enhanced error messages to be provider-specific
18+
819
## [0.2.2] - 2025-03-22
920

1021
### Fixed

Gemfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
PATH
22
remote: .
33
specs:
4-
git_auto (0.2.1)
4+
git_auto (0.2.2)
55
clipboard (~> 1.3)
66
colorize (~> 1.1)
77
http (~> 5.1)

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ GitAuto is a Ruby gem that streamlines your git workflow by automatically genera
1717
- 🤖 **AI Providers**: Supports multiple AI providers:
1818
- OpenAI (GPT-4o, GPT-4o mini)
1919
- Anthropic (Claude 3.5 Sonnet, Claude 3.5 Haiku)
20+
- Google (Gemini 2.5 Flash)
2021
- 🔒 **Secure Storage**: Your API keys are encrypted using AES-256-CBC and stored securely
2122

2223
## Requirements ⚙️
@@ -26,6 +27,7 @@ GitAuto is a Ruby gem that streamlines your git workflow by automatically genera
2627
- 🎟️ One magical ingredient: an API key! Choose your AI companion:
2728
- 🔑 OpenAI API key ([Get one here](https://platform.openai.com/api-keys))
2829
- 🗝️ Anthropic API key ([Get one here](https://console.anthropic.com/))
30+
- 🌟 Google Gemini API key ([Get one here](https://makersuite.google.com/app/apikey))
2931

3032
That's it! Say goodbye to "misc fixes" and hello to commits that actually tell a story. Your future self will thank you! 🎩✨
3133

@@ -108,8 +110,9 @@ git-auto commit
108110

109111
Here's what we're planning for future releases:
110112

111-
- 🤖 Support for Google Gemini AI
112113
- 📝 Automatic PR description generation
114+
- 🎯 Custom commit message templates
115+
- 🔄 Integration with Git hooks
113116
- More exciting features coming soon!
114117

115118
## Contributing 🤝

lib/git_auto/commands/config_command.rb

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ def set_setting(key, value)
6565
when "claude_api_key"
6666
@credential_store.store_api_key(value, "claude")
6767
puts "✓ Claude API key updated".green
68+
when "gemini_api_key"
69+
@credential_store.store_api_key(value, "gemini")
70+
puts "✓ Gemini API key updated".green
6871
else
6972
@settings.set(key.to_sym, value)
7073
puts "✓ Setting '#{key}' updated to '#{value}'".green
@@ -122,10 +125,12 @@ def display_configuration
122125
end
123126

124127
def configure_ai_provider
125-
provider = @prompt.select("Choose AI provider:", {
126-
"OpenAI (GPT-4, GPT-3.5 Turbo)" => "openai",
127-
"Anthropic (Claude 3.5 Sonnet, Claude 3.5 Haiku)" => "claude"
128-
})
128+
# Use the supported providers from settings
129+
provider_choices = Config::Settings::SUPPORTED_PROVIDERS.map do |key, info|
130+
{ name: info[:name], value: key }
131+
end
132+
133+
provider = @prompt.select("Choose AI provider:", provider_choices)
129134

130135
@settings.save(ai_provider: provider)
131136
puts "✓ AI provider updated to #{provider}".green

lib/git_auto/config/settings.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ class Error < StandardError; end
2525
"GPT-4o" => "gpt-4o",
2626
"GPT-4o mini" => "gpt-4o-mini"
2727
}
28+
},
29+
"gemini" => {
30+
name: "Google (Gemini 2.5 Flash)",
31+
models: {
32+
"Gemini 2.5 Flash Preview" => "gemini-2.5-flash-preview-05-20"
33+
}
2834
}
2935
}.freeze
3036

lib/git_auto/services/ai_service.rb

Lines changed: 147 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ class RateLimitError < GitAuto::Errors::RateLimitError; end
1414

1515
OPENAI_API_URL = "https://api.openai.com/v1/chat/completions"
1616
CLAUDE_API_URL = "https://api.anthropic.com/v1/messages"
17+
GEMINI_API_URL = "https://generativelanguage.googleapis.com/v1beta/models"
1718
MAX_DIFF_SIZE = 10_000
1819
MAX_RETRIES = 3
1920
BACKOFF_BASE = 2
@@ -25,10 +26,10 @@ def reset_temperature
2526
end
2627

2728
TEMPERATURE_VARIATIONS = [
28-
{ openai: 0.7, claude: 0.7 },
29-
{ openai: 0.8, claude: 0.8 },
30-
{ openai: 0.9, claude: 0.9 },
31-
{ openai: 1.0, claude: 1.0 }
29+
{ openai: 0.7, claude: 0.7, gemini: 0.7 },
30+
{ openai: 0.8, claude: 0.8, gemini: 0.8 },
31+
{ openai: 0.9, claude: 0.9, gemini: 0.9 },
32+
{ openai: 1.0, claude: 1.0, gemini: 1.0 }
3233
].freeze
3334

3435
def initialize(settings)
@@ -169,6 +170,8 @@ def generate_commit_message(diff, style: :conventional, scope: nil)
169170
generate_openai_commit_message(diff, style, scope)
170171
when "claude"
171172
generate_claude_commit_message(diff, style, scope)
173+
when "gemini"
174+
generate_gemini_commit_message(diff, style, scope)
172175
else
173176
raise GitAuto::Errors::InvalidProviderError, "Invalid AI provider specified"
174177
end
@@ -192,8 +195,7 @@ def add_suggestion(message)
192195
def previous_suggestions_prompt
193196
return "" if @previous_suggestions.empty?
194197

195-
"\nPrevious suggestions that you MUST NOT repeat:\n" +
196-
@previous_suggestions.map { |s| "- #{s}" }.join("\n")
198+
"\nPrevious suggestions that you MUST NOT repeat:\n#{@previous_suggestions.map { |s| "- #{s}" }.join("\n")}"
197199
end
198200

199201
def generate_openai_commit_message(diff, style, scope = nil, retry_attempt = nil)
@@ -388,6 +390,110 @@ def generate_claude_commit_message(diff, style, scope = nil, retry_attempt = nil
388390
add_suggestion(message)
389391
end
390392

393+
def generate_gemini_commit_message(diff, style, scope = nil, retry_attempt = nil)
394+
api_key = @credential_store.get_api_key("gemini")
395+
raise APIKeyError, "Gemini API key is not set. Please set it using `git_auto config`" unless api_key
396+
397+
# Only use temperature variations for retries
398+
temperature = retry_attempt ? get_temperature(retry_attempt) : TEMPERATURE_VARIATIONS[0][:gemini]
399+
commit_types = ["feat", "fix", "docs", "style", "refactor", "test", "chore", "perf", "ci", "build",
400+
"revert"].join("|")
401+
402+
system_message = case style.to_s
403+
when "minimal"
404+
"You are a commit message generator that MUST follow the minimal commit format: <type>: <description>\n" \
405+
"Valid types are: #{commit_types}\n" \
406+
"Rules:\n" \
407+
"1. ALWAYS start with a type from the list above\n" \
408+
"2. NEVER include a scope\n" \
409+
"3. Keep the message under 72 characters\n" \
410+
"4. ALWAYS use lowercase - this is mandatory\n" \
411+
"5. Use present tense\n" \
412+
"6. Be descriptive but concise\n" \
413+
"7. Do not include a period at the end"
414+
when "conventional"
415+
"You are a commit message generator that MUST follow these rules EXACTLY:\n" \
416+
"1. ONLY output a single line containing the commit message\n" \
417+
"2. Use format: <type>(<scope>): <description>\n" \
418+
"3. Valid types are: #{commit_types}\n" \
419+
"4. Keep under 72 characters\n" \
420+
"5. ALWAYS use lowercase - this is mandatory\n" \
421+
"6. Use present tense\n" \
422+
"7. Be descriptive but concise\n" \
423+
"8. No period at the end\n" \
424+
"9. NO explanations or additional text\n" \
425+
"10. NO markdown formatting"
426+
when "detailed"
427+
"You are a commit message generator that MUST follow this format EXACTLY:\n" \
428+
"<summary line>\n" \
429+
"\n" \
430+
"<detailed description>\n" \
431+
"\n" \
432+
"Rules:\n" \
433+
"1. First line is a summary under 72 characters\n" \
434+
"2. ALWAYS use lowercase - this is mandatory\n" \
435+
"3. ALWAYS include a blank line after the summary\n" \
436+
"4. ALWAYS include a detailed description explaining:\n" \
437+
" - What changes were made\n" \
438+
" - Why the changes were necessary\n" \
439+
" - Any technical details worth noting\n" \
440+
"5. Use bullet points for multiple changes\n" \
441+
"6. Use present tense\n" \
442+
"7. You can use periods in the detailed description\n" \
443+
"8. NO explanations or additional text\n" \
444+
"9. NO markdown formatting"
445+
else
446+
"You are an expert in writing clear and concise git commit messages.\n" \
447+
"Rules:\n" \
448+
"1. Keep the message under 72 characters\n" \
449+
"2. ALWAYS use lowercase - this is mandatory\n" \
450+
"3. Use present tense\n" \
451+
"4. Be descriptive but concise\n" \
452+
"5. Do not include a period at the end"
453+
end
454+
455+
user_message = if scope
456+
"Generate a conventional commit message with scope '#{scope}' for this diff:\n\n#{diff}"
457+
else
458+
"Generate a #{style} commit message for this diff:\n\n#{diff}"
459+
end
460+
461+
model = @settings.get(:ai_model)
462+
url = "#{GEMINI_API_URL}/#{model}:generateContent?key=#{api_key}"
463+
464+
payload = {
465+
contents: [
466+
{
467+
parts: [
468+
{
469+
text: "#{system_message}\n\n#{user_message}"
470+
}
471+
]
472+
}
473+
],
474+
generationConfig: {
475+
temperature: temperature,
476+
topK: 40,
477+
topP: 0.95,
478+
candidateCount: 1,
479+
maxOutputTokens: 1024
480+
}
481+
}
482+
483+
log_api_request("gemini", payload, temperature) if @debug_mode
484+
485+
response = HTTP.headers({
486+
"Content-Type" => "application/json"
487+
}).post(url, json: payload)
488+
489+
log_api_response(response.body) if @debug_mode
490+
491+
message = handle_response(response)
492+
message = message.downcase.strip
493+
message = message.sub(/\.$/, "") # Remove trailing period if present
494+
add_suggestion(message)
495+
end
496+
391497
def style_description(style, scope)
392498
case style
393499
when :conventional, "conventional"
@@ -436,6 +542,21 @@ def handle_response(response)
436542
raise Error, "No valid commit message found in response" if commit_message.nil?
437543
commit_message.strip
438544
end
545+
546+
when "gemini"
547+
content = json.dig("candidates", 0, "content", "parts", 0, "text")
548+
raise Error, "No message content in response" if content.nil? || content.empty?
549+
550+
# For detailed style, keep the full message
551+
if @settings.get(:commit_style) == "detailed"
552+
content.strip
553+
else
554+
# Clean up the response and extract just the commit message
555+
lines = content.strip.split("\n")
556+
# Find the first line that looks like a commit message
557+
commit_line = lines.find { |line| line.match(/^(feat|fix|docs|style|refactor|test|chore|perf|ci|build|revert)/) }
558+
commit_line || lines.first.strip
559+
end
439560
end
440561
when 401
441562
raise APIKeyError, "Invalid API key" unless ENV["RACK_ENV"] == "test"
@@ -447,8 +568,27 @@ def handle_response(response)
447568

448569
"test commit message"
449570

571+
when 403
572+
# Gemini-specific error for invalid API key
573+
provider = @settings.get(:ai_provider)
574+
if provider == "gemini"
575+
raise APIKeyError, "Invalid Gemini API key. Please check your API key at https://makersuite.google.com/app/apikey"
576+
else
577+
raise APIKeyError, "Access forbidden. Please check your API key."
578+
end
579+
450580
when 429
451-
raise RateLimitError, "Rate limit exceeded"
581+
provider = @settings.get(:ai_provider)
582+
case provider
583+
when "gemini"
584+
raise RateLimitError, "Gemini API rate limit exceeded. Please wait a moment and try again."
585+
when "openai"
586+
raise RateLimitError, "OpenAI API rate limit exceeded. Please try again later."
587+
when "claude"
588+
raise RateLimitError, "Claude API rate limit exceeded. Please try again later."
589+
else
590+
raise RateLimitError, "Rate limit exceeded"
591+
end
452592
else
453593
raise Error, "API request failed with status #{response.code}: #{response.body}"
454594
end

lib/git_auto/version.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# frozen_string_literal: true
22

33
module GitAuto
4-
VERSION = "0.2.2"
4+
VERSION = "0.3.0"
55
end
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# frozen_string_literal: true
2+
3+
require "spec_helper"
4+
5+
RSpec.describe GitAuto::Services::AIService do
6+
let(:settings) { instance_double(GitAuto::Config::Settings) }
7+
let(:credential_store) { instance_double(GitAuto::Config::CredentialStore) }
8+
let(:service) { described_class.new(settings) }
9+
let(:diff) { "diff --git a/file.rb b/file.rb\n+puts 'Hello World'" }
10+
11+
before do
12+
allow(GitAuto::Config::CredentialStore).to receive(:new).and_return(credential_store)
13+
allow(GitAuto::Services::HistoryService).to receive(:new).and_return(
14+
instance_double(GitAuto::Services::HistoryService)
15+
)
16+
allow(settings).to receive(:get).with(:ai_provider).and_return("gemini")
17+
allow(settings).to receive(:get).with(:ai_model).and_return("gemini-2.5-flash")
18+
allow(settings).to receive(:get).with(:commit_style).and_return("conventional")
19+
allow(settings).to receive(:set)
20+
end
21+
22+
describe "Gemini integration" do
23+
context "with valid API key" do
24+
before do
25+
allow(credential_store).to receive(:get_api_key).with("gemini").and_return("test-api-key")
26+
end
27+
28+
it "generates commit message using Gemini API" do
29+
stub_request(:post, %r{https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent})
30+
.to_return(
31+
status: 200,
32+
body: {
33+
candidates: [
34+
{
35+
content: {
36+
parts: [
37+
{ text: "feat: add hello world output" }
38+
]
39+
}
40+
}
41+
]
42+
}.to_json,
43+
headers: { "Content-Type" => "application/json" }
44+
)
45+
46+
message = service.generate_commit_message(diff, style: :conventional)
47+
expect(message).to eq("feat: add hello world output")
48+
end
49+
50+
it "handles Gemini API errors gracefully" do
51+
stub_request(:post, %r{https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent})
52+
.to_return(status: 403, body: { error: { message: "Invalid API key" } }.to_json)
53+
54+
expect { service.generate_commit_message(diff) }
55+
.to raise_error(GitAuto::Services::AIService::APIKeyError, /Invalid Gemini API key/)
56+
end
57+
58+
it "handles rate limiting" do
59+
stub_request(:post, %r{https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent})
60+
.to_return(status: 429, body: { error: { message: "Rate limit exceeded" } }.to_json)
61+
62+
expect { service.generate_commit_message(diff) }
63+
.to raise_error(GitAuto::Services::AIService::RateLimitError, /Gemini API rate limit exceeded/)
64+
end
65+
end
66+
67+
context "with missing API key" do
68+
before do
69+
allow(credential_store).to receive(:get_api_key).with("gemini").and_return(nil)
70+
end
71+
72+
it "raises API key error" do
73+
expect { service.generate_commit_message(diff) }
74+
.to raise_error(GitAuto::Services::AIService::APIKeyError, /Gemini API key is not set/)
75+
end
76+
end
77+
end
78+
end

0 commit comments

Comments
 (0)