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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 18b1522d4ba03cddfe1536bb6ee06cab7b12c7b3368c1883ebda20825c89673f
4
- data.tar.gz: 62e9d5d3a7f0b89292ed993ab601a63fa91134abb49ecda8bbbc349384a3aff8
3
+ metadata.gz: 8ab4962fee719c30fca3c7b4577154fed588495e61b6d95e91bb66ae15f5504e
4
+ data.tar.gz: 837bac312ba13254250e8b149c97e823557e4099cf7459e40dc993db86468caf
5
5
  SHA512:
6
- metadata.gz: e3ee6ea8c6e9de921c13bb63b0c5d0e9232754b3cca4c39d1bc2de4b6cabb54ad1f0800ced7cafbaa60a13f22d5b5cd0ebff85541912c5b7dd670fd8d0982f86
7
- data.tar.gz: a9f369e2008ed297e21fa88f049ba36ed3446ad72ecdc845537bcd20529c947bc31df69fc7195b554ff7b094ce139ff8717735b9b3b5124099ab86292b3a7bce
6
+ metadata.gz: 2f1aacf6b7c6635baf6f6bbeafabc980f39b6797201bb9eab4602d1845e52b14d7e0afd957047f305260c695830cffb51b92011d445705daea1712d6b62cc732
7
+ data.tar.gz: 85948f7190233c795197c49f833c249c55fe6e7f381c601deecc494a9aa1b16c56dfd02cbc268830749196a9a58ba24f69b1eff7adfa692b9d83df76270aecfc
data/CHANGELOG.md CHANGED
@@ -1,4 +1,21 @@
1
- ## [Unreleased]
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
+ [![Gem Version](https://badge.fury.io/rb/git_auto.svg)](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
- ## Requirements βš™οΈ
87
+ ## Usage πŸ› οΈ
88
88
 
89
- - Ruby >= 3.0.0
90
- - Git repository with staged changes
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
- *Coming soon...*
103
+ ![GitAuto Setup](./screenshots/setup.png)
104
+ ![GitAuto Config](./screenshots/config.png)
105
+ ![GitAuto Commit](./screenshots/commit.png)
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[:is_clean] || status[:has_staged_changes]
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
- puts "\nπŸ“ Generated commit message:".blue
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
- return DEFAULT_SETTINGS.dup unless File.exist?(CONFIG_FILE)
73
-
74
- user_settings = YAML.load_file(CONFIG_FILE) || {}
75
- DEFAULT_SETTINGS.merge(user_settings)
76
- rescue StandardError
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. Available providers: #{SUPPORTED_PROVIDERS.keys.join(", ")}"
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] || get(:ai_provider)
88
- available_models = SUPPORTED_PROVIDERS[provider][:models]
89
- return if available_models.values.include?(options[:ai_model])
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 for #{provider}. Available models: #{available_models.keys.join(", ")}"
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 # Skip index and file indicator lines
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
- when "conventional"
71
- "You are an expert in writing conventional commit messages..."
72
- else
73
- "You are an expert in writing clear and concise git commit messages..."
74
- end
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 > 0
87
+ if retry_attempt.positive?
78
88
  base_prompt += "\nPlease provide a different perspective or approach than previous attempts."
79
- base_prompt += "\nBe more #{%w[specific detailed creative concise].sample} in this attempt."
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 == "conventional" && scope.nil?
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
- when "openai"
117
- generate_openai_commit_message(diff, style)
118
- when "claude"
119
- generate_claude_commit_message(diff, style)
120
- end
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) # Exponential backoff
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 = %w[feat fix docs style refactor test chore perf ci build revert].join('|')
178
-
179
- system_message = "You are a commit message generator that MUST follow the conventional commit format: <type>(<scope>): <description>\n" \
180
- "Valid types are: #{commit_types}\n" \
181
- "Rules:\n" \
182
- "1. ALWAYS start with a type from the list above\n" \
183
- "2. ALWAYS use the exact format <type>(<scope>): <description>\n" \
184
- "3. Keep the message under 72 characters\n" \
185
- "4. Use lowercase\n" \
186
- "5. Use present tense\n" \
187
- "6. Be descriptive but concise\n" \
188
- "7. Do not include a period at the end"
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
- "Generate a conventional commit message with scope '#{scope}' for this diff:\n\n#{diff}"
192
- else
193
- "Generate a conventional commit message for this diff:\n\n#{diff}"
194
- end
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
- log_api_request("openai", payload, temperature) if ENV["DEBUG"]
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
- .headers(accept: "application/json")
209
- .post(OPENAI_API_URL, json: payload)
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
- prompt = retry_attempt ? get_system_prompt(style, retry_attempt) : get_system_prompt(style)
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 ONLY a conventional commit message for this diff. The message MUST start with one of these types: #{commit_types}\n\n" \
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 ONLY a conventional commit message for this diff. The message MUST start with one of these types: #{commit_types}\n\n" \
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: prompt,
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
- log_api_request("claude", payload, temperature)
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
- "Content-Type" => "application/json",
273
- "x-api-key" => api_key,
274
- "anthropic-version" => "2023-06-01"
275
- }).post(CLAUDE_API_URL, json: payload)
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
- # Extract the most common directory or file type from the diff
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 # Use first directory as scope
408
+ parts.first
363
409
  else
364
- # For files in root, use the basename without extension
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
- # Remove any trailing numbers
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| %w[rb js py ts css html md].include?(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
- .max_by { |_, group| group.length }
385
- &.first
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
- &.gsub(/([a-z\d])([A-Z])/, '\1_\2')
390
- &.tr('-', '_')
391
- &.downcase
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 GitError < StandardError; end
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 = 10)
27
+ def get_commit_history(limit = nil)
28
28
  validate_git_repository!
29
- # Check if there are any commits
30
- if has_commits?
31
- execute_git_command("log", "--pretty=format:%h|%s|%an|%ad", "--date=short",
32
- "-#{limit}").split("\n").map do |line|
33
- hash, subject, author, date = line.split("|")
34
- {
35
- hash: hash,
36
- subject: subject,
37
- author: author,
38
- date: date
39
- }
40
- end
41
- else
42
- []
43
- end
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: working_directory_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 true if in_git_repository?
58
+ return if File.directory?(".git")
70
59
 
71
- raise GitError, "Not a git repository. Initialize one with 'git init'"
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 true if has_staged_changes?
64
+ return if has_staged_changes?
76
65
 
77
- raise GitError, "No changes staged for commit. Use 'git add' to stage changes"
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
- execute_git_command("diff", "--cached", "--quiet")
88
- false
89
- rescue GitError
90
- true
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 working_directory_clean?
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 GitError
84
+ rescue StandardError
101
85
  false
102
86
  end
103
87
 
104
88
  def execute_git_command(*args)
105
- result = IO.popen(["git", *args], err: [:child, :out], &:read)
89
+ output = IO.popen(["git", *args], err: [:child, :out], &:read)
106
90
 
107
- raise GitError, "Git command failed: #{result.strip}" unless $CHILD_STATUS.success?
91
+ raise Error, "Git command failed: git #{args.join(" ")}\n#{output}" unless $CHILD_STATUS.success?
108
92
 
109
- result.strip
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/_-]+)\))? # Optional scope in parentheses
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
- valid_types = TYPES.keys.join('|')
76
- pattern = %r{^(?:#{valid_types})\([a-z0-9/_-]+\)?: .+$}
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 pattern.match?(header)
79
- errors << "Header must follow conventional commit format: <type>(<scope>): <description>"
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GitAuto
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/git_auto.rb CHANGED
@@ -42,7 +42,6 @@ module GitAuto
42
42
  end
43
43
  end
44
44
 
45
- # Register the hooks with RubyGems
46
45
  Gem.post_install do |installer|
47
46
  GitAuto.install if installer.spec.name == "git_auto"
48
47
  end
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.1.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-14 00:00:00.000000000 Z
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 message generator
267
+ summary: AI-powered git commit messages using OpenAI or Anthropic APIs
268
268
  test_files: []