git_auto 0.1.1 → 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: 75fb5c81e9b1d07b927497d979188dc2e936711395a8b9aa6919065d6ed0903b
4
- data.tar.gz: 612044fb323c934e36209942ef16004e7acdb7e441641da6b4e016a976a4cd37
3
+ metadata.gz: 8ab4962fee719c30fca3c7b4577154fed588495e61b6d95e91bb66ae15f5504e
4
+ data.tar.gz: 837bac312ba13254250e8b149c97e823557e4099cf7459e40dc993db86468caf
5
5
  SHA512:
6
- metadata.gz: 578fd8cf70256433e05bb75ea58df3f216170e5f5f20f8b54547c8f23fd17758637eb2c5aa0fc895cf5c69c8ec32f1db23cb6c1da67773d93747917e722ea2fe
7
- data.tar.gz: dd583910a1e13dcc2deb729e4a2d78ed60efde03617e6521370f11a2efcf094718ff1257e203660eb243e61ea250934f6a6a1f2d14a8cf06e014226159ede5aa
6
+ metadata.gz: 2f1aacf6b7c6635baf6f6bbeafabc980f39b6797201bb9eab4602d1845e52b14d7e0afd957047f305260c695830cffb51b92011d445705daea1712d6b62cc732
7
+ data.tar.gz: 85948f7190233c795197c49f833c249c55fe6e7f381c601deecc494a9aa1b16c56dfd02cbc268830749196a9a58ba24f69b1eff7adfa692b9d83df76270aecfc
data/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
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
+
1
15
  ## [0.1.1] - 2024-12-13
2
16
 
3
17
  - Remove debug logging output from API requests for cleaner user experience
@@ -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" }
@@ -67,7 +67,7 @@ module GitAuto
67
67
  private
68
68
 
69
69
  def ensure_config_dir
70
- FileUtils.mkdir_p(CONFIG_DIR) unless Dir.exist?(CONFIG_DIR)
70
+ FileUtils.mkdir_p(CONFIG_DIR)
71
71
  end
72
72
 
73
73
  def load_settings
@@ -83,13 +83,13 @@ module GitAuto
83
83
  raise Error, "Unsupported AI provider: #{options[:ai_provider]}"
84
84
  end
85
85
 
86
- if options[:ai_model]
87
- provider = options[:ai_provider] || @settings[:ai_provider]
88
- valid_models = SUPPORTED_PROVIDERS[provider][:models].values
89
- unless valid_models.include?(options[:ai_model])
90
- raise Error, "Unsupported AI model: #{options[:ai_model]}"
91
- end
92
- end
86
+ return unless options[:ai_model]
87
+
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])
91
+
92
+ raise Error, "Unsupported AI model: #{options[:ai_model]}"
93
93
  end
94
94
  end
95
95
  end
@@ -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*(.+)$/
@@ -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),
@@ -206,8 +249,8 @@ module GitAuto
206
249
  # log_api_request("openai", payload, temperature) if ENV["DEBUG"]
207
250
 
208
251
  response = HTTP.auth("Bearer #{api_key}")
209
- .headers(accept: "application/json")
210
- .post(OPENAI_API_URL, json: payload)
252
+ .headers(accept: "application/json")
253
+ .post(OPENAI_API_URL, json: payload)
211
254
 
212
255
  handle_response(response)
213
256
  end
@@ -218,33 +261,40 @@ module GitAuto
218
261
 
219
262
  # Only use temperature variations for retries
220
263
  temperature = retry_attempt ? get_temperature(retry_attempt) : TEMPERATURE_VARIATIONS[0][:claude]
221
- 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
222
293
 
223
- commit_types = %w[feat fix docs style refactor test chore perf ci build revert].join('|')
224
294
  user_message = if scope
225
- "Generate ONLY a conventional commit message for this diff. The message MUST start with one of these types: #{commit_types}\n\n" \
226
- "Format: <type>: <description>\n" \
227
- "Example: feat: add user authentication\n\n" \
228
- "Rules:\n" \
229
- "1. Keep the commit message under 72 characters\n" \
230
- "2. Use lowercase\n" \
231
- "3. Use present tense\n" \
232
- "4. Make it unique and different from previous suggestions\n" \
233
- "5. MUST start with one of the valid types followed by a colon\n\n" \
234
- "Here's the diff:\n#{diff}" +
235
- previous_suggestions_prompt
295
+ "Generate a conventional commit message with scope '#{scope}' for this diff:\n\n#{diff}"
236
296
  else
237
- "Generate ONLY a conventional commit message for this diff. The message MUST start with one of these types: #{commit_types}\n\n" \
238
- "Format: <type>: <description>\n" \
239
- "Example: feat: add user authentication\n\n" \
240
- "Rules:\n" \
241
- "1. Keep the commit message under 72 characters\n" \
242
- "2. Use lowercase\n" \
243
- "3. Use present tense\n" \
244
- "4. Make it unique and different from previous suggestions\n" \
245
- "5. MUST start with one of the valid types followed by a colon\n\n" \
246
- "Here's the diff:\n#{diff}" +
247
- previous_suggestions_prompt
297
+ "Generate a #{style} commit message for this diff:\n\n#{diff}"
248
298
  end
249
299
 
250
300
  payload = {
@@ -253,7 +303,7 @@ module GitAuto
253
303
  temperature: temperature,
254
304
  top_k: 50,
255
305
  top_p: 0.9,
256
- system: prompt,
306
+ system: system_message,
257
307
  messages: [
258
308
  {
259
309
  role: "user",
@@ -272,10 +322,10 @@ module GitAuto
272
322
  # log_api_response(response.body)
273
323
 
274
324
  response = HTTP.headers({
275
- "Content-Type" => "application/json",
276
- "x-api-key" => api_key,
277
- "anthropic-version" => "2023-06-01"
278
- }).post(CLAUDE_API_URL, json: payload)
325
+ "Content-Type" => "application/json",
326
+ "x-api-key" => api_key,
327
+ "anthropic-version" => "2023-06-01"
328
+ }).post(CLAUDE_API_URL, json: payload)
279
329
 
280
330
  message = handle_response(response)
281
331
  message = message.downcase.strip
@@ -292,6 +342,8 @@ module GitAuto
292
342
  "simple commit message"
293
343
  when :scope, "scope"
294
344
  "commit scope suggestion"
345
+ when :minimal, "minimal"
346
+ "minimal commit message"
295
347
  else
296
348
  "commit message"
297
349
  end
@@ -309,6 +361,7 @@ module GitAuto
309
361
  # puts "Debug - No content in response: #{json}"
310
362
  raise Error, "No message content in response"
311
363
  end
364
+
312
365
  message.split("\n").first.strip
313
366
  when "claude"
314
367
  content = json.dig("content", 0, "text")
@@ -324,9 +377,7 @@ module GitAuto
324
377
 
325
378
  message = lines.first
326
379
 
327
- if message.nil? || !message.match?(/^[a-z]+:/)
328
- raise Error, "No valid commit message found in response"
329
- end
380
+ raise Error, "No valid commit message found in response" if message.nil? || !message.match?(/^[a-z]+:/)
330
381
 
331
382
  message
332
383
  end
@@ -348,18 +399,18 @@ module GitAuto
348
399
  end
349
400
 
350
401
  def infer_scope_from_diff(diff)
351
- files = diff.scan(/^diff --git.*?b\/(.+)$/).flatten
402
+ files = diff.scan(%r{^diff --git.*?b/(.+)$}).flatten
352
403
  return nil if files.empty?
353
404
 
354
405
  scopes = files.map do |file|
355
- parts = file.split('/')
406
+ parts = file.split("/")
356
407
  if parts.length > 1
357
408
  parts.first
358
409
  else
359
- basename = File.basename(file, '.*')
410
+ basename = File.basename(file, ".*")
360
411
 
361
412
  if basename =~ /^(.*?)\d*$/
362
- $1
413
+ ::Regexp.last_match(1)
363
414
  else
364
415
  basename
365
416
  end
@@ -367,19 +418,19 @@ module GitAuto
367
418
  end.compact
368
419
 
369
420
  # Filter out overly generic scopes
370
- 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) }
371
422
  return nil if scopes.empty?
372
423
 
373
424
  # Return the most common scope
374
425
  scope = scopes.group_by(&:itself)
375
- .max_by { |_, group| group.length }
376
- &.first
426
+ .max_by { |_, group| group.length }
427
+ &.first
377
428
 
378
429
  # Convert to snake_case if needed
379
430
  scope&.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
380
- &.gsub(/([a-z\d])([A-Z])/, '\1_\2')
381
- &.tr('-', '_')
382
- &.downcase
431
+ &.gsub(/([a-z\d])([A-Z])/, '\1_\2')
432
+ &.tr("-", "_")
433
+ &.downcase
383
434
  end
384
435
  end
385
436
  end
@@ -26,7 +26,7 @@ module GitAuto
26
26
 
27
27
  def get_commit_history(limit = nil)
28
28
  validate_git_repository!
29
- format = '%H%n%s%n%an%n%aI'
29
+ format = "%H%n%s%n%an%n%aI"
30
30
  command = ["log", "--pretty=format:#{format}", "--no-merges"]
31
31
  command << "-#{limit}" if limit
32
32
 
@@ -55,20 +55,23 @@ module GitAuto
55
55
  private
56
56
 
57
57
  def validate_git_repository!
58
- unless File.directory?(".git")
59
- raise Error, "Not a git repository (or any of the parent directories)"
60
- end
58
+ return if File.directory?(".git")
59
+
60
+ raise Error, "Not a git repository (or any of the parent directories)"
61
61
  end
62
62
 
63
63
  def validate_staged_changes!
64
- unless has_staged_changes?
65
- raise Error, "No changes staged for commit"
66
- end
64
+ return if has_staged_changes?
65
+
66
+ raise Error, "No changes staged for commit"
67
67
  end
68
68
 
69
69
  def has_staged_changes?
70
- !execute_git_command("diff", "--cached", "--quiet")
71
- $CHILD_STATUS.exitstatus == 1
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?
72
75
  end
73
76
 
74
77
  def is_clean?
@@ -85,9 +88,7 @@ module GitAuto
85
88
  def execute_git_command(*args)
86
89
  output = IO.popen(["git", *args], err: [:child, :out], &:read)
87
90
 
88
- unless $CHILD_STATUS.success?
89
- raise Error, "Git command failed: git #{args.join(' ')}\n#{output}"
90
- end
91
+ raise Error, "Git command failed: git #{args.join(" ")}\n#{output}" unless $CHILD_STATUS.success?
91
92
 
92
93
  output
93
94
  end
@@ -20,6 +20,12 @@ 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
31
  (\((?<scope>[a-z0-9/_-]+)\))? # Optional scope in parentheses
@@ -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.1"
4
+ VERSION = "0.2.0"
5
5
  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.1
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-15 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