git_auto 0.1.0 β 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +18 -1
- data/README.md +29 -18
- data/lib/git_auto/commands/commit_message_command.rb +6 -2
- data/lib/git_auto/commands/config_command.rb +1 -0
- data/lib/git_auto/commands/setup_command.rb +1 -0
- data/lib/git_auto/config/settings.rb +12 -11
- data/lib/git_auto/formatters/diff_formatter.rb +1 -1
- data/lib/git_auto/services/ai_service.rb +137 -95
- data/lib/git_auto/services/git_service.rb +33 -51
- data/lib/git_auto/services/history_service.rb +0 -4
- data/lib/git_auto/validators/commit_message_validator.rb +12 -6
- data/lib/git_auto/version.rb +1 -1
- data/lib/git_auto.rb +0 -1
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8ab4962fee719c30fca3c7b4577154fed588495e61b6d95e91bb66ae15f5504e
|
4
|
+
data.tar.gz: 837bac312ba13254250e8b149c97e823557e4099cf7459e40dc993db86468caf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2f1aacf6b7c6635baf6f6bbeafabc980f39b6797201bb9eab4602d1845e52b14d7e0afd957047f305260c695830cffb51b92011d445705daea1712d6b62cc732
|
7
|
+
data.tar.gz: 85948f7190233c795197c49f833c249c55fe6e7f381c601deecc494a9aa1b16c56dfd02cbc268830749196a9a58ba24f69b1eff7adfa692b9d83df76270aecfc
|
data/CHANGELOG.md
CHANGED
@@ -1,4 +1,21 @@
|
|
1
|
-
## [
|
1
|
+
## [0.2.0] - 2024-12-15
|
2
|
+
|
3
|
+
### Added
|
4
|
+
- New "Minimal" commit message style option for more concise commits
|
5
|
+
- Display of AI model used in commit message generation for better transparency
|
6
|
+
|
7
|
+
### Fixed
|
8
|
+
- Improved Git staged changes detection for more reliable operation
|
9
|
+
- Improved commit message validation for both minimal and conventional formats
|
10
|
+
|
11
|
+
### Changed
|
12
|
+
- Updated AI service integration to support minimal commit style across OpenAI and Claude
|
13
|
+
- Improved error handling and user feedback during Git operations
|
14
|
+
|
15
|
+
## [0.1.1] - 2024-12-13
|
16
|
+
|
17
|
+
- Remove debug logging output from API requests for cleaner user experience
|
18
|
+
- Add commented debugging options for developers
|
2
19
|
|
3
20
|
## [0.1.0] - 2024-12-12
|
4
21
|
|
data/README.md
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# GitAuto π€β¨
|
2
2
|
|
3
|
+
[](https://rubygems.org/gems/git_auto)
|
4
|
+
|
3
5
|
> AI-powered commit messages that make sense
|
4
6
|
|
5
7
|
GitAuto is a Ruby gem that streamlines your git workflow by automatically generating meaningful commit messages using AI. Say goodbye to generic commit messages and hello to clear, consistent, and informative descriptions of your changes.
|
@@ -17,8 +19,20 @@ GitAuto is a Ruby gem that streamlines your git workflow by automatically genera
|
|
17
19
|
- Anthropic (Claude 3.5 Sonnet, Claude 3.5 Haiku)
|
18
20
|
- π **Secure Storage**: Your API keys are encrypted using AES-256-CBC and stored securely
|
19
21
|
|
22
|
+
## Requirements βοΈ
|
23
|
+
|
24
|
+
- Ruby >= 3.0.0
|
25
|
+
- Git repository with staged changes
|
26
|
+
- ποΈ One magical ingredient: an API key! Choose your AI companion:
|
27
|
+
- π OpenAI API key ([Get one here](https://platform.openai.com/api-keys))
|
28
|
+
- ποΈ Anthropic API key ([Get one here](https://console.anthropic.com/))
|
29
|
+
|
30
|
+
That's it! Say goodbye to "misc fixes" and hello to commits that actually tell a story. Your future self will thank you! π©β¨
|
31
|
+
|
20
32
|
## Installation π
|
21
33
|
|
34
|
+
Install the gem from [RubyGems](https://rubygems.org/gems/git_auto):
|
35
|
+
|
22
36
|
```bash
|
23
37
|
gem install git_auto
|
24
38
|
```
|
@@ -29,20 +43,6 @@ Or add to your Gemfile:
|
|
29
43
|
gem 'git_auto'
|
30
44
|
```
|
31
45
|
|
32
|
-
## Usage π οΈ
|
33
|
-
|
34
|
-
1. Stage your changes as usual:
|
35
|
-
```bash
|
36
|
-
git add .
|
37
|
-
```
|
38
|
-
|
39
|
-
2. Generate a commit message:
|
40
|
-
```bash
|
41
|
-
git-auto commit
|
42
|
-
```
|
43
|
-
|
44
|
-
3. Review, edit if needed, and confirm!
|
45
|
-
|
46
46
|
## Setup and Configuration π§
|
47
47
|
|
48
48
|
### Initial Setup
|
@@ -84,14 +84,25 @@ GitAuto can also be configured through environment variables:
|
|
84
84
|
- `GIT_AUTO_MODEL`: OpenAI model to use (default: gpt-3.5-turbo)
|
85
85
|
- `GIT_AUTO_SECRET`: Custom encryption key for storing API keys (optional)
|
86
86
|
|
87
|
-
##
|
87
|
+
## Usage π οΈ
|
88
88
|
|
89
|
-
|
90
|
-
|
89
|
+
1. Stage your changes as usual:
|
90
|
+
```bash
|
91
|
+
git add .
|
92
|
+
```
|
93
|
+
|
94
|
+
2. Generate a commit message:
|
95
|
+
```bash
|
96
|
+
git-auto commit
|
97
|
+
```
|
98
|
+
|
99
|
+
3. Review, edit if needed, and confirm!
|
91
100
|
|
92
101
|
## Screenshots πΈ
|
93
102
|
|
94
|
-
|
103
|
+

|
104
|
+

|
105
|
+

|
95
106
|
|
96
107
|
## Roadmap πΊοΈ
|
97
108
|
|
@@ -48,7 +48,7 @@ module GitAuto
|
|
48
48
|
|
49
49
|
# Repository and Change Validation Methods
|
50
50
|
def validate_repository(status)
|
51
|
-
return if status[:
|
51
|
+
return if status[:has_staged_changes]
|
52
52
|
|
53
53
|
puts "βΉοΈ Status:".blue
|
54
54
|
puts " Branch: #{status[:branch]}"
|
@@ -131,7 +131,11 @@ module GitAuto
|
|
131
131
|
end
|
132
132
|
|
133
133
|
def display_message_and_validation(formatted_message, validation)
|
134
|
-
|
134
|
+
provider = @settings.get(:ai_provider)
|
135
|
+
model = @settings.get(:ai_model)
|
136
|
+
model_info = "(#{provider}/#{model})".light_black
|
137
|
+
|
138
|
+
puts "\nπ Generated commit message #{model_info}:".blue
|
135
139
|
puts formatted_message
|
136
140
|
|
137
141
|
display_validation_errors(validation[:errors]) if validation[:errors].any?
|
@@ -151,6 +151,7 @@ module GitAuto
|
|
151
151
|
|
152
152
|
def configure_commit_style
|
153
153
|
style = @prompt.select("Choose commit message style:", {
|
154
|
+
"Minimal (type: description)" => "minimal",
|
154
155
|
"Conventional (type(scope): description)" => "conventional",
|
155
156
|
"Simple (description only)" => "simple"
|
156
157
|
})
|
@@ -87,6 +87,7 @@ module GitAuto
|
|
87
87
|
@prompt.select(
|
88
88
|
"Select default commit message style:",
|
89
89
|
[
|
90
|
+
{ name: "Minimal (type: subject)", value: "minimal" },
|
90
91
|
{ name: "Conventional (type(scope): description)", value: "conventional" },
|
91
92
|
{ name: "Simple (verb + description)", value: "simple" },
|
92
93
|
{ name: "Detailed (summary + bullet points)", value: "detailed" }
|
@@ -6,6 +6,8 @@ require "fileutils"
|
|
6
6
|
module GitAuto
|
7
7
|
module Config
|
8
8
|
class Settings
|
9
|
+
class Error < StandardError; end
|
10
|
+
|
9
11
|
CONFIG_DIR = File.expand_path("~/.git_auto")
|
10
12
|
CONFIG_FILE = File.join(CONFIG_DIR, "config.yml")
|
11
13
|
|
@@ -69,26 +71,25 @@ module GitAuto
|
|
69
71
|
end
|
70
72
|
|
71
73
|
def load_settings
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
DEFAULT_SETTINGS.dup
|
74
|
+
if File.exist?(CONFIG_FILE)
|
75
|
+
YAML.load_file(CONFIG_FILE).transform_keys(&:to_sym)
|
76
|
+
else
|
77
|
+
DEFAULT_SETTINGS.dup
|
78
|
+
end
|
78
79
|
end
|
79
80
|
|
80
81
|
def validate_settings!(options)
|
81
82
|
if options[:ai_provider] && !SUPPORTED_PROVIDERS.key?(options[:ai_provider])
|
82
|
-
raise Error, "Unsupported AI provider
|
83
|
+
raise Error, "Unsupported AI provider: #{options[:ai_provider]}"
|
83
84
|
end
|
84
85
|
|
85
86
|
return unless options[:ai_model]
|
86
87
|
|
87
|
-
provider = options[:ai_provider] ||
|
88
|
-
|
89
|
-
return if
|
88
|
+
provider = options[:ai_provider] || @settings[:ai_provider]
|
89
|
+
valid_models = SUPPORTED_PROVIDERS[provider][:models].values
|
90
|
+
return if valid_models.include?(options[:ai_model])
|
90
91
|
|
91
|
-
raise Error, "Unsupported model
|
92
|
+
raise Error, "Unsupported AI model: #{options[:ai_model]}"
|
92
93
|
end
|
93
94
|
end
|
94
95
|
end
|
@@ -15,7 +15,7 @@ module GitAuto
|
|
15
15
|
current_file = extract_file_name(line)
|
16
16
|
formatted << "\nChanges in #{current_file}:"
|
17
17
|
when /^index |^---|\+\+\+/
|
18
|
-
next
|
18
|
+
next
|
19
19
|
when /^@@ .* @@/
|
20
20
|
formatted << format_hunk_header(line)
|
21
21
|
when /^\+/
|
@@ -66,17 +66,27 @@ module GitAuto
|
|
66
66
|
end
|
67
67
|
|
68
68
|
def get_system_prompt(style, retry_attempt = 0)
|
69
|
-
base_prompt = case style
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
69
|
+
base_prompt = case style.to_s
|
70
|
+
when "minimal"
|
71
|
+
"You are an expert in writing minimal commit messages that follow the format: <type>: <description>\n" \
|
72
|
+
"Rules:\n" \
|
73
|
+
"1. ALWAYS start with a type from the list above\n" \
|
74
|
+
"2. NEVER include a scope\n" \
|
75
|
+
"3. Keep the message under 72 characters\n" \
|
76
|
+
"4. Use lowercase\n" \
|
77
|
+
"5. Use present tense\n" \
|
78
|
+
"6. Be descriptive but concise\n" \
|
79
|
+
"7. Do not include a period at the end"
|
80
|
+
when "conventional"
|
81
|
+
"You are an expert in writing conventional commit messages..."
|
82
|
+
else
|
83
|
+
"You are an expert in writing clear and concise git commit messages..."
|
84
|
+
end
|
75
85
|
|
76
86
|
# Add variation for retries
|
77
|
-
if retry_attempt
|
87
|
+
if retry_attempt.positive?
|
78
88
|
base_prompt += "\nPlease provide a different perspective or approach than previous attempts."
|
79
|
-
base_prompt += "\nBe more #{
|
89
|
+
base_prompt += "\nBe more #{["specific", "detailed", "creative", "concise"].sample} in this attempt."
|
80
90
|
end
|
81
91
|
|
82
92
|
base_prompt
|
@@ -110,14 +120,30 @@ module GitAuto
|
|
110
120
|
# If diff is too large, use the summarized version
|
111
121
|
diff = @diff_summarizer.summarize(diff) if diff.length > MAX_DIFF_SIZE
|
112
122
|
|
113
|
-
if style == "
|
123
|
+
if style.to_s == "minimal"
|
124
|
+
message = case @settings.get(:ai_provider)
|
125
|
+
when "openai"
|
126
|
+
generate_openai_commit_message(diff, style)
|
127
|
+
when "claude"
|
128
|
+
generate_claude_commit_message(diff, style)
|
129
|
+
end
|
130
|
+
|
131
|
+
# Extract type and description from the message
|
132
|
+
if message =~ /^(\w+):\s*(.+)$/
|
133
|
+
type = ::Regexp.last_match(1)
|
134
|
+
description = ::Regexp.last_match(2)
|
135
|
+
return "#{type}: #{description}"
|
136
|
+
end
|
137
|
+
|
138
|
+
return message
|
139
|
+
elsif style.to_s == "conventional" && scope.nil?
|
114
140
|
# Generate both scope and message in one call
|
115
141
|
message = case @settings.get(:ai_provider)
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
142
|
+
when "openai"
|
143
|
+
generate_openai_commit_message(diff, style)
|
144
|
+
when "claude"
|
145
|
+
generate_claude_commit_message(diff, style)
|
146
|
+
end
|
121
147
|
|
122
148
|
# Extract type and scope from the message
|
123
149
|
if message =~ /^(\w+)(?:\(([\w-]+)\))?:\s*(.+)$/
|
@@ -147,7 +173,7 @@ module GitAuto
|
|
147
173
|
rescue StandardError => e
|
148
174
|
retries += 1
|
149
175
|
if retries < MAX_RETRIES
|
150
|
-
sleep(retries * BACKOFF_BASE)
|
176
|
+
sleep(retries * BACKOFF_BASE)
|
151
177
|
retry
|
152
178
|
end
|
153
179
|
raise e
|
@@ -174,24 +200,41 @@ module GitAuto
|
|
174
200
|
|
175
201
|
# Only use temperature variations for retries
|
176
202
|
temperature = retry_attempt ? get_temperature(retry_attempt) : TEMPERATURE_VARIATIONS[0][:openai]
|
177
|
-
commit_types =
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
203
|
+
commit_types = ["feat", "fix", "docs", "style", "refactor", "test", "chore", "perf", "ci", "build",
|
204
|
+
"revert"].join("|")
|
205
|
+
|
206
|
+
system_message = case style.to_s
|
207
|
+
when "minimal"
|
208
|
+
"You are a commit message generator that MUST follow the minimal commit format: <type>: <description>\n" \
|
209
|
+
"Valid types are: #{commit_types}\n" \
|
210
|
+
"Rules:\n" \
|
211
|
+
"1. ALWAYS start with a type from the list above\n" \
|
212
|
+
"2. NEVER include a scope\n" \
|
213
|
+
"3. Keep the message under 72 characters\n" \
|
214
|
+
"4. Use lowercase\n" \
|
215
|
+
"5. Use present tense\n" \
|
216
|
+
"6. Be descriptive but concise\n" \
|
217
|
+
"7. Do not include a period at the end"
|
218
|
+
when "conventional"
|
219
|
+
"You are a commit message generator that MUST follow the conventional commit format: <type>(<scope>): <description>\n" \
|
220
|
+
"Valid types are: #{commit_types}\n" \
|
221
|
+
"Rules:\n" \
|
222
|
+
"1. ALWAYS start with a type from the list above\n" \
|
223
|
+
"2. ALWAYS use the exact format <type>(<scope>): <description>\n" \
|
224
|
+
"3. Keep the message under 72 characters\n" \
|
225
|
+
"4. Use lowercase\n" \
|
226
|
+
"5. Use present tense\n" \
|
227
|
+
"6. Be descriptive but concise\n" \
|
228
|
+
"7. Do not include a period at the end"
|
229
|
+
else
|
230
|
+
"You are an expert in writing clear and concise git commit messages..."
|
231
|
+
end
|
189
232
|
|
190
233
|
user_message = if scope
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
234
|
+
"Generate a conventional commit message with scope '#{scope}' for this diff:\n\n#{diff}"
|
235
|
+
else
|
236
|
+
"Generate a #{style} commit message for this diff:\n\n#{diff}"
|
237
|
+
end
|
195
238
|
|
196
239
|
payload = {
|
197
240
|
model: @settings.get(:ai_model),
|
@@ -202,11 +245,12 @@ module GitAuto
|
|
202
245
|
temperature: temperature
|
203
246
|
}
|
204
247
|
|
205
|
-
|
248
|
+
# Uncomment the following line to see the API request and response details for debugging
|
249
|
+
# log_api_request("openai", payload, temperature) if ENV["DEBUG"]
|
206
250
|
|
207
251
|
response = HTTP.auth("Bearer #{api_key}")
|
208
|
-
|
209
|
-
|
252
|
+
.headers(accept: "application/json")
|
253
|
+
.post(OPENAI_API_URL, json: payload)
|
210
254
|
|
211
255
|
handle_response(response)
|
212
256
|
end
|
@@ -217,33 +261,40 @@ module GitAuto
|
|
217
261
|
|
218
262
|
# Only use temperature variations for retries
|
219
263
|
temperature = retry_attempt ? get_temperature(retry_attempt) : TEMPERATURE_VARIATIONS[0][:claude]
|
220
|
-
|
264
|
+
commit_types = ["feat", "fix", "docs", "style", "refactor", "test", "chore", "perf", "ci", "build",
|
265
|
+
"revert"].join("|")
|
266
|
+
|
267
|
+
system_message = case style.to_s
|
268
|
+
when "minimal"
|
269
|
+
"You are a commit message generator that MUST follow the minimal commit format: <type>: <description>\n" \
|
270
|
+
"Valid types are: #{commit_types}\n" \
|
271
|
+
"Rules:\n" \
|
272
|
+
"1. ALWAYS start with a type from the list above\n" \
|
273
|
+
"2. NEVER include a scope\n" \
|
274
|
+
"3. Keep the message under 72 characters\n" \
|
275
|
+
"4. Use lowercase\n" \
|
276
|
+
"5. Use present tense\n" \
|
277
|
+
"6. Be descriptive but concise\n" \
|
278
|
+
"7. Do not include a period at the end"
|
279
|
+
when "conventional"
|
280
|
+
"You are a commit message generator that MUST follow the conventional commit format: <type>(<scope>): <description>\n" \
|
281
|
+
"Valid types are: #{commit_types}\n" \
|
282
|
+
"Rules:\n" \
|
283
|
+
"1. ALWAYS start with a type from the list above\n" \
|
284
|
+
"2. ALWAYS use the exact format <type>(<scope>): <description>\n" \
|
285
|
+
"3. Keep the message under 72 characters\n" \
|
286
|
+
"4. Use lowercase\n" \
|
287
|
+
"5. Use present tense\n" \
|
288
|
+
"6. Be descriptive but concise\n" \
|
289
|
+
"7. Do not include a period at the end"
|
290
|
+
else
|
291
|
+
"You are an expert in writing clear and concise git commit messages..."
|
292
|
+
end
|
221
293
|
|
222
|
-
commit_types = %w[feat fix docs style refactor test chore perf ci build revert].join('|')
|
223
294
|
user_message = if scope
|
224
|
-
"Generate
|
225
|
-
"Format: <type>: <description>\n" \
|
226
|
-
"Example: feat: add user authentication\n\n" \
|
227
|
-
"Rules:\n" \
|
228
|
-
"1. Keep the commit message under 72 characters\n" \
|
229
|
-
"2. Use lowercase\n" \
|
230
|
-
"3. Use present tense\n" \
|
231
|
-
"4. Make it unique and different from previous suggestions\n" \
|
232
|
-
"5. MUST start with one of the valid types followed by a colon\n\n" \
|
233
|
-
"Here's the diff:\n#{diff}" +
|
234
|
-
previous_suggestions_prompt
|
295
|
+
"Generate a conventional commit message with scope '#{scope}' for this diff:\n\n#{diff}"
|
235
296
|
else
|
236
|
-
"Generate
|
237
|
-
"Format: <type>: <description>\n" \
|
238
|
-
"Example: feat: add user authentication\n\n" \
|
239
|
-
"Rules:\n" \
|
240
|
-
"1. Keep the commit message under 72 characters\n" \
|
241
|
-
"2. Use lowercase\n" \
|
242
|
-
"3. Use present tense\n" \
|
243
|
-
"4. Make it unique and different from previous suggestions\n" \
|
244
|
-
"5. MUST start with one of the valid types followed by a colon\n\n" \
|
245
|
-
"Here's the diff:\n#{diff}" +
|
246
|
-
previous_suggestions_prompt
|
297
|
+
"Generate a #{style} commit message for this diff:\n\n#{diff}"
|
247
298
|
end
|
248
299
|
|
249
300
|
payload = {
|
@@ -252,7 +303,7 @@ module GitAuto
|
|
252
303
|
temperature: temperature,
|
253
304
|
top_k: 50,
|
254
305
|
top_p: 0.9,
|
255
|
-
system:
|
306
|
+
system: system_message,
|
256
307
|
messages: [
|
257
308
|
{
|
258
309
|
role: "user",
|
@@ -266,15 +317,15 @@ module GitAuto
|
|
266
317
|
]
|
267
318
|
}
|
268
319
|
|
269
|
-
|
320
|
+
# Uncomment the following lines to see the API request and response details for debugging
|
321
|
+
# log_api_request("claude", payload, temperature)
|
322
|
+
# log_api_response(response.body)
|
270
323
|
|
271
324
|
response = HTTP.headers({
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
log_api_response(response.body)
|
325
|
+
"Content-Type" => "application/json",
|
326
|
+
"x-api-key" => api_key,
|
327
|
+
"anthropic-version" => "2023-06-01"
|
328
|
+
}).post(CLAUDE_API_URL, json: payload)
|
278
329
|
|
279
330
|
message = handle_response(response)
|
280
331
|
message = message.downcase.strip
|
@@ -291,6 +342,8 @@ module GitAuto
|
|
291
342
|
"simple commit message"
|
292
343
|
when :scope, "scope"
|
293
344
|
"commit scope suggestion"
|
345
|
+
when :minimal, "minimal"
|
346
|
+
"minimal commit message"
|
294
347
|
else
|
295
348
|
"commit message"
|
296
349
|
end
|
@@ -300,45 +353,40 @@ module GitAuto
|
|
300
353
|
case response.code
|
301
354
|
when 200
|
302
355
|
json = JSON.parse(response.body.to_s)
|
303
|
-
puts "Debug - API Response: #{json.inspect}"
|
356
|
+
# puts "Debug - API Response: #{json.inspect}"
|
304
357
|
case @settings.get(:ai_provider)
|
305
358
|
when "openai"
|
306
359
|
message = json.dig("choices", 0, "message", "content")
|
307
360
|
if message.nil? || message.empty?
|
308
|
-
puts "Debug - No content in response: #{json}"
|
361
|
+
# puts "Debug - No content in response: #{json}"
|
309
362
|
raise Error, "No message content in response"
|
310
363
|
end
|
364
|
+
|
311
365
|
message.split("\n").first.strip
|
312
366
|
when "claude"
|
313
367
|
content = json.dig("content", 0, "text")
|
314
|
-
puts "Debug - Claude content: #{content.inspect}"
|
368
|
+
# puts "Debug - Claude content: #{content.inspect}"
|
315
369
|
|
316
370
|
if content.nil? || content.empty?
|
317
|
-
puts "Debug - No content in response: #{json}"
|
371
|
+
# puts "Debug - No content in response: #{json}"
|
318
372
|
raise Error, "No message content in response"
|
319
373
|
end
|
320
374
|
|
321
|
-
# Split into lines and find the commit message
|
322
375
|
lines = content.split("\n").map(&:strip).reject(&:empty?)
|
323
|
-
puts "Debug - Lines: #{lines.inspect}"
|
376
|
+
# puts "Debug - Lines: #{lines.inspect}"
|
324
377
|
|
325
|
-
# Take the first non-empty line as it should be just the commit message
|
326
378
|
message = lines.first
|
327
379
|
|
328
|
-
if message.nil? || !message.match?(/^[a-z]+:/)
|
329
|
-
raise Error, "No valid commit message found in response"
|
330
|
-
end
|
380
|
+
raise Error, "No valid commit message found in response" if message.nil? || !message.match?(/^[a-z]+:/)
|
331
381
|
|
332
382
|
message
|
333
383
|
end
|
334
384
|
when 401
|
335
385
|
raise APIKeyError, "Invalid API key" unless ENV["RACK_ENV"] == "test"
|
336
386
|
|
337
|
-
# Return mock response in test environment
|
338
387
|
@test_call_count ||= 0
|
339
388
|
@test_call_count += 1
|
340
389
|
|
341
|
-
# Simulate rate limiting after 3 calls
|
342
390
|
raise RateLimitError, "Rate limit exceeded. Please try again later." if @test_call_count > 3
|
343
391
|
|
344
392
|
"test commit message"
|
@@ -351,44 +399,38 @@ module GitAuto
|
|
351
399
|
end
|
352
400
|
|
353
401
|
def infer_scope_from_diff(diff)
|
354
|
-
|
355
|
-
files = diff.scan(/^diff --git.*?b\/(.+)$/).flatten
|
402
|
+
files = diff.scan(%r{^diff --git.*?b/(.+)$}).flatten
|
356
403
|
return nil if files.empty?
|
357
404
|
|
358
|
-
# Try to get a meaningful scope from the file paths
|
359
405
|
scopes = files.map do |file|
|
360
|
-
parts = file.split(
|
406
|
+
parts = file.split("/")
|
361
407
|
if parts.length > 1
|
362
|
-
parts.first
|
408
|
+
parts.first
|
363
409
|
else
|
364
|
-
|
365
|
-
basename = File.basename(file, '.*')
|
410
|
+
basename = File.basename(file, ".*")
|
366
411
|
|
367
|
-
# Filter out generic names and keep meaningful ones
|
368
412
|
if basename =~ /^(.*?)\d*$/
|
369
|
-
|
370
|
-
$1
|
413
|
+
::Regexp.last_match(1)
|
371
414
|
else
|
372
|
-
# Keep the full name if it's meaningful
|
373
415
|
basename
|
374
416
|
end
|
375
417
|
end
|
376
418
|
end.compact
|
377
419
|
|
378
420
|
# Filter out overly generic scopes
|
379
|
-
scopes.reject! { |s|
|
421
|
+
scopes.reject! { |s| ["rb", "js", "py", "ts", "css", "html", "md"].include?(s) }
|
380
422
|
return nil if scopes.empty?
|
381
423
|
|
382
424
|
# Return the most common scope
|
383
425
|
scope = scopes.group_by(&:itself)
|
384
|
-
|
385
|
-
|
426
|
+
.max_by { |_, group| group.length }
|
427
|
+
&.first
|
386
428
|
|
387
429
|
# Convert to snake_case if needed
|
388
430
|
scope&.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
389
|
-
|
390
|
-
|
391
|
-
|
431
|
+
&.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
432
|
+
&.tr("-", "_")
|
433
|
+
&.downcase
|
392
434
|
end
|
393
435
|
end
|
394
436
|
end
|
@@ -1,10 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "English"
|
4
|
+
|
4
5
|
module GitAuto
|
5
6
|
module Services
|
6
7
|
class GitService
|
7
|
-
class
|
8
|
+
class Error < StandardError; end
|
8
9
|
|
9
10
|
def get_staged_diff
|
10
11
|
validate_git_repository!
|
@@ -19,46 +20,34 @@ module GitAuto
|
|
19
20
|
def commit(message)
|
20
21
|
validate_git_repository!
|
21
22
|
validate_staged_changes!
|
22
|
-
# Ensure we only use the first line for the commit message
|
23
23
|
first_line = message.split("\n").first.strip
|
24
24
|
execute_git_command("commit", "-m", first_line)
|
25
25
|
end
|
26
26
|
|
27
|
-
def get_commit_history(limit =
|
27
|
+
def get_commit_history(limit = nil)
|
28
28
|
validate_git_repository!
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
end
|
45
|
-
|
46
|
-
def current_branch
|
47
|
-
validate_git_repository!
|
48
|
-
begin
|
49
|
-
execute_git_command("rev-parse", "--abbrev-ref", "HEAD")
|
50
|
-
rescue GitError
|
51
|
-
"main" # Default branch name for new repositories
|
29
|
+
format = "%H%n%s%n%an%n%aI"
|
30
|
+
command = ["log", "--pretty=format:#{format}", "--no-merges"]
|
31
|
+
command << "-#{limit}" if limit
|
32
|
+
|
33
|
+
output = execute_git_command(*command)
|
34
|
+
return [] if output.empty?
|
35
|
+
|
36
|
+
output.split("\n\n").map do |commit|
|
37
|
+
hash, subject, author, date = commit.split("\n")
|
38
|
+
{
|
39
|
+
hash: hash,
|
40
|
+
subject: subject,
|
41
|
+
author: author,
|
42
|
+
date: date
|
43
|
+
}
|
52
44
|
end
|
53
45
|
end
|
54
46
|
|
55
47
|
def repository_status
|
56
|
-
validate_git_repository!
|
57
48
|
{
|
58
|
-
branch: current_branch,
|
59
|
-
staged_files: get_staged_files,
|
60
49
|
has_staged_changes: has_staged_changes?,
|
61
|
-
is_clean:
|
50
|
+
is_clean: is_clean?,
|
62
51
|
has_commits: has_commits?
|
63
52
|
}
|
64
53
|
end
|
@@ -66,49 +55,42 @@ module GitAuto
|
|
66
55
|
private
|
67
56
|
|
68
57
|
def validate_git_repository!
|
69
|
-
return
|
58
|
+
return if File.directory?(".git")
|
70
59
|
|
71
|
-
raise
|
60
|
+
raise Error, "Not a git repository (or any of the parent directories)"
|
72
61
|
end
|
73
62
|
|
74
63
|
def validate_staged_changes!
|
75
|
-
return
|
64
|
+
return if has_staged_changes?
|
76
65
|
|
77
|
-
raise
|
78
|
-
end
|
79
|
-
|
80
|
-
def in_git_repository?
|
81
|
-
Dir.exist?(".git") || !execute_git_command("rev-parse", "--git-dir").empty?
|
82
|
-
rescue StandardError
|
83
|
-
false
|
66
|
+
raise Error, "No changes staged for commit"
|
84
67
|
end
|
85
68
|
|
86
69
|
def has_staged_changes?
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
70
|
+
# git diff --cached --quiet returns:
|
71
|
+
# - exit status 0 (success) if there are no changes
|
72
|
+
# - exit status 1 (failure) if there are changes
|
73
|
+
system("git diff --cached --quiet")
|
74
|
+
!$CHILD_STATUS.success?
|
91
75
|
end
|
92
76
|
|
93
|
-
def
|
77
|
+
def is_clean?
|
94
78
|
execute_git_command("status", "--porcelain").empty?
|
95
79
|
end
|
96
80
|
|
97
81
|
def has_commits?
|
98
82
|
execute_git_command("rev-parse", "--verify", "HEAD")
|
99
83
|
true
|
100
|
-
rescue
|
84
|
+
rescue StandardError
|
101
85
|
false
|
102
86
|
end
|
103
87
|
|
104
88
|
def execute_git_command(*args)
|
105
|
-
|
89
|
+
output = IO.popen(["git", *args], err: [:child, :out], &:read)
|
106
90
|
|
107
|
-
raise
|
91
|
+
raise Error, "Git command failed: git #{args.join(" ")}\n#{output}" unless $CHILD_STATUS.success?
|
108
92
|
|
109
|
-
|
110
|
-
rescue Errno::ENOENT
|
111
|
-
raise GitError, "Git executable not found. Please ensure git is installed and in your PATH"
|
93
|
+
output
|
112
94
|
end
|
113
95
|
end
|
114
96
|
end
|
@@ -26,7 +26,6 @@ module GitAuto
|
|
26
26
|
metadata: metadata
|
27
27
|
})
|
28
28
|
|
29
|
-
# Keep only the last MAX_HISTORY_ENTRIES
|
30
29
|
history = history.take(MAX_HISTORY_ENTRIES)
|
31
30
|
|
32
31
|
save_history(history)
|
@@ -134,12 +133,9 @@ module GitAuto
|
|
134
133
|
end
|
135
134
|
|
136
135
|
def extract_phrases(message)
|
137
|
-
# Extract common verb phrases from the message
|
138
|
-
# Ignore type/scope for conventional commits
|
139
136
|
content = message.sub(/^[a-z]+(\([^)]+\))?:\s*/, "")
|
140
137
|
words = content.downcase.split(/[^a-z]+/).reject(&:empty?)
|
141
138
|
|
142
|
-
# Get 2-3 word phrases
|
143
139
|
phrases = []
|
144
140
|
words.each_cons(2) { |phrase| phrases << phrase.join(" ") }
|
145
141
|
words.each_cons(3) { |phrase| phrases << phrase.join(" ") }
|
@@ -20,9 +20,15 @@ module GitAuto
|
|
20
20
|
"revert" => "Reverts a previous commit"
|
21
21
|
}.freeze
|
22
22
|
|
23
|
+
MINIMAL_COMMIT_PATTERN = /
|
24
|
+
^(?<type>#{TYPES.keys.join("|")}) # Commit type
|
25
|
+
:\s # Colon and space separator
|
26
|
+
(?<description>.+) # Commit description
|
27
|
+
/x
|
28
|
+
|
23
29
|
CONVENTIONAL_COMMIT_PATTERN = %r{
|
24
30
|
^(?<type>#{TYPES.keys.join("|")}) # Commit type
|
25
|
-
(\((?<scope>[a-z0-9/_-]+)\))?
|
31
|
+
(\((?<scope>[a-z0-9/_-]+)\))? # Optional scope in parentheses
|
26
32
|
:\s # Colon and space separator
|
27
33
|
(?<description>.+) # Commit description
|
28
34
|
}x
|
@@ -71,12 +77,12 @@ module GitAuto
|
|
71
77
|
|
72
78
|
errors << "Header exceeds #{HEADER_MAX_LENGTH} characters" if header.length > HEADER_MAX_LENGTH
|
73
79
|
|
74
|
-
# Validate header format for conventional commits
|
75
|
-
|
76
|
-
|
80
|
+
# Validate header format for conventional and minimal commits
|
81
|
+
minimal_pattern = MINIMAL_COMMIT_PATTERN
|
82
|
+
conventional_pattern = CONVENTIONAL_COMMIT_PATTERN
|
77
83
|
|
78
|
-
unless
|
79
|
-
errors << "Header must follow
|
84
|
+
unless minimal_pattern.match?(header) || conventional_pattern.match?(header)
|
85
|
+
errors << "Header must follow either minimal format: <type>: <description> or conventional format: <type>(<scope>): <description>"
|
80
86
|
end
|
81
87
|
|
82
88
|
# Suggest using lowercase for consistency
|
data/lib/git_auto/version.rb
CHANGED
data/lib/git_auto.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: git_auto
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Guillermo Diaz
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-12-
|
11
|
+
date: 2024-12-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: thor
|
@@ -264,5 +264,5 @@ requirements: []
|
|
264
264
|
rubygems_version: 3.5.23
|
265
265
|
signing_key:
|
266
266
|
specification_version: 4
|
267
|
-
summary: AI-powered git commit
|
267
|
+
summary: AI-powered git commit messages using OpenAI or Anthropic APIs
|
268
268
|
test_files: []
|