snakommit 0.1.1 → 0.1.2

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.
@@ -30,113 +30,122 @@ module Snakommit
30
30
  def commit_flow
31
31
  return { error: 'Not in a Git repository' } unless Git.in_repo?
32
32
 
33
- puts @pastel.cyan(BANNER)
34
- puts "=" * BANNER.length
35
-
36
- # Get the status of the repository with performance monitoring
37
- repo_status = @monitor.measure(:repository_status) do
38
- {
39
- unstaged: @git.unstaged_files,
40
- untracked: @git.untracked_files,
41
- staged: @git.staged_files
42
- }
43
- end
44
-
45
- unstaged = repo_status[:unstaged]
46
- untracked = repo_status[:untracked]
47
- staged = repo_status[:staged]
48
-
49
- # Check if there are any changes to work with
50
- if unstaged.empty? && untracked.empty? && staged.empty?
51
- return { error: 'No changes detected in the repository' }
52
- end
53
-
54
- # Show file status
55
- puts "\nRepository status:"
56
- puts "- #{staged.length} file(s) staged for commit"
57
- puts "- #{unstaged.length} file(s) modified but not staged"
58
- puts "- #{untracked.length} untracked file(s)"
59
-
60
- # Report performance if debug is enabled
61
- if ENV['SNAKOMMIT_DEBUG']
62
- puts "\nPerformance report:"
63
- @monitor.report.each { |line| puts " #{line}" }
64
- end
65
-
66
- # Check for saved selections from a previous run
67
- saved_selections = @git.get_saved_selections
68
- if saved_selections && !saved_selections.empty?
69
- puts "\nFound selections from a previous session."
70
- if @tty_prompt.yes?("Would you like to use your previous file selections?", default: true)
71
- # Stage the previously selected files
72
- stage_files(@git, saved_selections)
33
+ loop do
34
+ puts @pastel.cyan(BANNER)
35
+ puts "=" * BANNER.length
36
+
37
+ # Get repository status
38
+ repo_status = @monitor.measure(:repository_status) do
39
+ { unstaged: @git.unstaged_files, untracked: @git.untracked_files, staged: @git.staged_files }
40
+ end
41
+
42
+ unstaged, untracked, staged = repo_status.values_at(:unstaged, :untracked, :staged)
43
+
44
+ # Exit if no changes detected
45
+ if unstaged.empty? && untracked.empty? && staged.empty?
46
+ return { error: 'No changes detected in the repository' }
47
+ end
48
+
49
+ # Show file status
50
+ puts "\nRepository status:"
51
+ puts "- #{staged.length} file(s) staged for commit"
52
+ puts "- #{unstaged.length} file(s) modified but not staged"
53
+ puts "- #{untracked.length} untracked file(s)"
54
+
55
+ if ENV['SNAKOMMIT_DEBUG']
56
+ puts "\nPerformance report:"
57
+ @monitor.report.each { |line| puts " #{line}" }
58
+ end
59
+
60
+ # Handle saved selections
61
+ saved_selections = @git.get_saved_selections
62
+ if saved_selections&.any?
63
+ puts "\nFound selections from a previous session."
64
+ if @tty_prompt.yes?("Would you like to use your previous file selections?", default: true)
65
+ stage_files(@git, saved_selections)
66
+ else
67
+ @git.clear_saved_selections
68
+ select_files(@git)
69
+ end
73
70
  else
74
- # If user doesn't want to use previous selections, clear them
75
- @git.clear_saved_selections
76
- # Always show file selection to user
77
71
  select_files(@git)
78
72
  end
79
- else
80
- # Always show file selection to user
81
- select_files(@git)
82
- end
83
-
84
- # Re-check staged files after selection
85
- current_staged = @git.staged_files
86
-
87
- # After file selection, check if we have staged files
88
- if current_staged.empty?
89
- return { error: 'No changes staged for commit. Please select files to commit.' }
90
- end
73
+
74
+ # Check if any files are staged
75
+ current_staged = @git.staged_files
76
+ if current_staged.empty?
77
+ puts "\n#{@pastel.red('Error:')} No changes staged for commit. Please select files to commit."
78
+ next if @tty_prompt.yes?("Do you want to select files again?", default: true)
79
+ return { error: 'No changes staged for commit. Please select files to commit.' }
80
+ end
91
81
 
92
- # Divider
93
- puts "\n" + "-" * 40
94
-
95
- # Now get the commit info
96
- commit_info = @monitor.measure(:get_commit_info) do
97
- get_commit_info
98
- end
99
- return commit_info if commit_info[:error]
82
+ puts "\n" + "-" * 40
83
+
84
+ # Get commit info
85
+ commit_info = @monitor.measure(:get_commit_info) { get_commit_info }
86
+
87
+ if commit_info[:error]
88
+ if commit_info[:error] == 'Commit aborted'
89
+ current_staged = @git.staged_files
90
+ if current_staged.any?
91
+ puts "\nThere are still #{current_staged.length} file(s) staged."
92
+
93
+ if @tty_prompt.yes?("Do you want to continue with these staged files?", default: true)
94
+ next
95
+ elsif @tty_prompt.yes?("Do you want to unstage all files?", default: false)
96
+ unstage_files(@git, current_staged)
97
+ puts "All files have been unstaged."
98
+ next
99
+ else
100
+ return commit_info
101
+ end
102
+ else
103
+ next if @tty_prompt.yes?("Do you want to start over?", default: true)
104
+ return commit_info
105
+ end
106
+ else
107
+ return commit_info
108
+ end
109
+ end
100
110
 
101
- # Format the commit message
102
- message = format_commit_message(commit_info)
103
-
104
- # Show which files will be committed
105
- puts "\nFiles to be committed:"
106
- staged_for_commit = @git.staged_files
107
- staged_for_commit.each do |file|
108
- puts "- #{file}"
109
- end
110
-
111
- # Preview the commit message
112
- puts "\nCommit message preview:"
113
- puts "-" * 40
114
- puts message
115
- puts "-" * 40
116
-
117
- # Confirm the commit
118
- return { error: 'Commit aborted by user' } unless @tty_prompt.yes?('Do you want to proceed with this commit?', default: true)
119
-
120
- # Commit the changes
121
- spinner = TTY::Spinner.new("[:spinner] Committing changes... ", format: :dots)
122
- spinner.auto_spin
123
-
124
- @monitor.measure(:git_commit) do
125
- @git.commit(message)
126
- end
127
-
128
- spinner.success("Changes committed successfully!")
129
-
130
- # Final success message
131
- puts "\n✓ Successfully committed: #{message.split("\n").first}"
132
-
133
- # Show performance stats in debug mode
134
- if ENV['SNAKOMMIT_DEBUG']
135
- puts "\nFinal performance report:"
136
- @monitor.report.each { |line| puts " #{line}" }
111
+ # Format and preview commit message
112
+ message = format_commit_message(commit_info)
113
+
114
+ puts "\nFiles to be committed:"
115
+ @git.staged_files.each { |file| puts "- #{file}" }
116
+
117
+ puts "\nCommit message preview:"
118
+ puts "-" * 40
119
+ puts message
120
+ puts "-" * 40
121
+
122
+ unless @tty_prompt.yes?('Do you want to proceed with this commit?', default: true)
123
+ next if @tty_prompt.yes?("Do you want to start over?", default: true)
124
+ return { error: 'Commit aborted by user' }
125
+ end
126
+
127
+ # Perform commit
128
+ spinner = TTY::Spinner.new("[:spinner] Committing changes... ", format: :dots)
129
+ spinner.auto_spin
130
+
131
+ @monitor.measure(:git_commit) { @git.commit(message) }
132
+
133
+ spinner.success("Changes committed successfully!")
134
+ puts "\n✓ Successfully committed: #{message.split("\n").first}"
135
+
136
+ if ENV['SNAKOMMIT_DEBUG']
137
+ puts "\nFinal performance report:"
138
+ @monitor.report.each { |line| puts " #{line}" }
139
+ end
140
+
141
+ return { success: true, message: message }
137
142
  end
138
-
139
- { success: true, message: message }
143
+ rescue PromptError => e
144
+ puts "\n#{@pastel.red('Error:')} #{e.message}"
145
+ { error: e.message }
146
+ rescue TTY::Reader::InputInterrupt
147
+ puts "\nCommit process interrupted by user."
148
+ { error: 'Commit aborted by user' }
140
149
  rescue Git::GitError => e
141
150
  puts "\nError: #{e.message}"
142
151
  { error: "Git error: #{e.message}" }
@@ -150,91 +159,70 @@ module Snakommit
150
159
  # Select files to add
151
160
  def select_files(git)
152
161
  begin
153
- # Get file lists with performance monitoring
154
162
  repo_status = @monitor.measure(:get_files_for_selection) do
155
- {
156
- unstaged: git.unstaged_files,
157
- untracked: git.untracked_files,
158
- staged: git.staged_files
159
- }
163
+ { unstaged: git.unstaged_files, untracked: git.untracked_files, staged: git.staged_files }
160
164
  end
161
165
 
162
- unstaged = repo_status[:unstaged]
163
- untracked = repo_status[:untracked]
164
- staged = repo_status[:staged]
165
-
166
- # Combine all files that could be staged
166
+ unstaged, untracked, staged = repo_status.values_at(:unstaged, :untracked, :staged)
167
167
  all_stageable_files = unstaged + untracked
168
168
 
169
- # If there are no files to stage or unstage, return early
169
+ # No files to work with
170
170
  if all_stageable_files.empty? && staged.empty?
171
171
  puts "No changes detected in the repository."
172
172
  return
173
173
  end
174
174
 
175
- # If we only have staged files but nothing new to stage
176
- if all_stageable_files.empty? && !staged.empty?
175
+ # Only staged files present
176
+ if all_stageable_files.empty? && staged.any?
177
177
  puts "\nCurrently staged files:"
178
178
  staged.each { |file| puts "- #{file}" }
179
179
 
180
- # Ask if user wants to unstage any files
181
- if @tty_prompt.yes?("Do you want to unstage any files?", default: false)
182
- unstage_files(git, staged)
183
- end
180
+ unstage_files(git, staged) if @tty_prompt.yes?("Do you want to unstage any files?", default: false)
184
181
  return
185
182
  end
186
183
 
187
- # First show the user what's currently staged
184
+ # Show currently staged files
188
185
  unless staged.empty?
189
186
  puts "\nCurrently staged files:"
190
187
  staged.each { |file| puts "- #{file}" }
191
188
  end
192
189
 
193
- # Create options for the file selection menu
190
+ # Build options for file selection
194
191
  options = []
192
+ options << { name: "[ ALL FILES ]", value: :all_files } if all_stageable_files.any?
195
193
 
196
- # Add "ALL FILES" option at the top if we have unstaged or untracked files
197
- if !all_stageable_files.empty?
198
- options << { name: "[ ALL FILES ]", value: :all_files }
199
- end
200
-
201
- # Add modified files with index numbers
202
194
  unstaged.each_with_index do |file, idx|
203
195
  options << { name: "#{idx+1}. Modified: #{file}", value: file }
204
196
  end
205
197
 
206
- # Add untracked files with continuing index numbers
207
198
  untracked.each_with_index do |file, idx|
208
199
  options << { name: "#{unstaged.length + idx + 1}. Untracked: #{file}", value: file }
209
200
  end
210
201
 
211
- # Skip if no options
212
202
  if options.empty?
213
203
  puts "No files available to stage."
214
204
  return
215
205
  end
216
206
 
217
- # Prompt user to select files
207
+ # Get user selections
218
208
  puts "\nSelect files to stage for commit:"
219
209
  selected = @tty_prompt.multi_select("Choose files (use space to select, enter to confirm):", options, per_page: 15, echo: true)
220
210
 
221
- # Check if anything was selected
211
+ # Handle no selection
222
212
  if selected.empty?
223
213
  puts "No files selected for staging."
224
214
 
225
- # If we already have staged files, ask if user wants to continue with those
226
215
  unless staged.empty?
227
216
  puts "You already have #{staged.length} file(s) staged."
228
- return unless @tty_prompt.yes?("Do you want to select files again?", default: true)
229
- return select_files(git) # Recursive call to try again
217
+ return select_files(git) if @tty_prompt.yes?("Do you want to select files again?", default: true)
230
218
  end
231
219
 
232
220
  return
233
221
  end
234
222
 
223
+ # Process selection
235
224
  puts "\nSelected files to stage:"
236
225
 
237
- # Handle "ALL FILES" option
238
226
  if selected.include?(:all_files)
239
227
  selected = all_stageable_files
240
228
  puts "- All files (#{selected.length})"
@@ -242,24 +230,33 @@ module Snakommit
242
230
  selected.each { |file| puts "- #{file}" }
243
231
  end
244
232
 
245
- # Add a confirmation step
233
+ # Confirm and stage
246
234
  if @tty_prompt.yes?("Proceed with staging these files?", default: true)
247
- # Stage the selected files
248
235
  stage_files(git, selected)
249
236
  else
250
237
  puts "Staging canceled by user."
251
238
  return
252
239
  end
253
240
 
254
- # After staging, check if the user wants to unstage any files
241
+ # Offer to unstage
255
242
  newly_staged = git.staged_files
256
- unless newly_staged.empty?
257
- if @tty_prompt.yes?("Do you want to unstage any files?", default: false)
258
- unstage_files(git, newly_staged)
259
- end
243
+ if newly_staged.any? && @tty_prompt.yes?("Do you want to unstage any files?", default: false)
244
+ unstage_files(git, newly_staged)
260
245
  end
261
246
  rescue TTY::Reader::InputInterrupt
262
- puts "\nFile selection aborted. Press Ctrl+C again to exit completely or continue."
247
+ puts "\nFile selection interrupted."
248
+
249
+ # Ask the user if they want to continue or abort
250
+ begin
251
+ unless @tty_prompt.yes?("Do you want to continue with the commit process?", default: false)
252
+ puts "Commit process aborted by user."
253
+ raise PromptError, "Commit aborted by user"
254
+ end
255
+ rescue TTY::Reader::InputInterrupt
256
+ puts "\nCommit process aborted."
257
+ raise PromptError, "Commit aborted by user"
258
+ end
259
+
263
260
  return
264
261
  rescue => e
265
262
  puts "\nError during file selection: #{e.message}"
@@ -305,30 +302,49 @@ module Snakommit
305
302
  def unstage_files(git, staged_files)
306
303
  return if staged_files.empty?
307
304
 
308
- # Create options for unstaging
309
305
  unstage_options = staged_files.map { |f| { name: f, value: f } }
310
-
311
- # Unstage heading
312
306
  puts "\nUnstage Files:"
313
307
 
314
- # Select files to unstage
315
- to_unstage = @tty_prompt.multi_select("Select files to unstage:", unstage_options, per_page: 15)
316
-
317
- unless to_unstage.empty?
318
- spinner = TTY::Spinner.new("[:spinner] Unstaging files... ", format: :dots)
319
- spinner.auto_spin
308
+ begin
309
+ to_unstage = @tty_prompt.multi_select("Select files to unstage:", unstage_options, per_page: 15)
320
310
 
321
- # Use batch processing for more efficient unstaging
322
- @monitor.measure(:batch_unstage_files) do
323
- @batch_processor.process_files(to_unstage) do |batch|
324
- batch.each { |file| git.reset(file) }
311
+ unless to_unstage.empty?
312
+ spinner = TTY::Spinner.new("[:spinner] Unstaging files... ", format: :dots)
313
+ spinner.auto_spin
314
+
315
+ @monitor.measure(:batch_unstage_files) do
316
+ @batch_processor.process_files(to_unstage) do |batch|
317
+ batch.each { |file| git.reset(file) }
318
+ end
319
+ end
320
+
321
+ spinner.success("Files unstaged")
322
+
323
+ # Check if all files unstaged
324
+ remaining_staged = git.staged_files
325
+ if remaining_staged.empty?
326
+ puts "#{@pastel.yellow('Note:')} All files have been unstaged."
327
+
328
+ # Offer to select new files if available
329
+ unstaged_files = git.unstaged_files
330
+ untracked_files = git.untracked_files
331
+
332
+ if (unstaged_files + untracked_files).any?
333
+ begin
334
+ if @tty_prompt.yes?("Do you want to select new files now?", default: true)
335
+ select_files(git)
336
+ end
337
+ rescue Interrupt
338
+ puts "\nFile selection interrupted."
339
+ end
340
+ end
325
341
  end
326
342
  end
327
-
328
- spinner.success("Files unstaged")
343
+ rescue TTY::Reader::InputInterrupt
344
+ puts "\nUnstaging files aborted."
345
+ rescue => e
346
+ raise PromptError, "Failed to unstage files: #{e.message}"
329
347
  end
330
- rescue => e
331
- raise PromptError, "Failed to unstage files: #{e.message}"
332
348
  end
333
349
 
334
350
  # Get commit information
@@ -337,12 +353,11 @@ module Snakommit
337
353
 
338
354
  puts "\nCommit Details:"
339
355
 
340
- # Select commit type
341
356
  info[:type] = select_type
342
357
 
343
- # Enter scope (optional)
358
+ # Handle scope selection
344
359
  suggested_scopes = @config['scopes']
345
- if suggested_scopes && !suggested_scopes.empty?
360
+ if suggested_scopes&.any?
346
361
  scope_options = suggested_scopes.map { |s| { name: s, value: s } }
347
362
  scope_options << { name: '[none]', value: nil }
348
363
 
@@ -353,18 +368,17 @@ module Snakommit
353
368
  info[:scope] = @tty_prompt.ask('Enter the scope of this change (optional, press Enter to skip):')
354
369
  end
355
370
 
356
- # Enter subject
371
+ # Get subject
357
372
  info[:subject] = @tty_prompt.ask('Enter a short description:') do |q|
358
373
  q.required true
359
374
  q.validate(/^.{1,#{@config['max_subject_length']}}$/, "Subject must be less than #{@config['max_subject_length']} characters")
360
375
  end
361
376
 
362
- # Enter longer description (optional)
377
+ # Get body text
363
378
  puts "Enter a longer description (optional, press Enter to skip):"
364
379
  puts "Type your message and press Enter when done. Leave empty to skip."
365
380
 
366
381
  body_lines = []
367
- # Read input until an empty line is entered
368
382
  loop do
369
383
  line = @tty_prompt.ask("")
370
384
  break if line.nil? || line.empty?
@@ -373,17 +387,15 @@ module Snakommit
373
387
 
374
388
  info[:body] = body_lines
375
389
 
376
- # Is this a breaking change?
390
+ # Breaking changes
377
391
  info[:breaking] = @tty_prompt.yes?('Is this a breaking change?', default: false)
378
-
379
- # Breaking change description
380
392
  if info[:breaking]
381
393
  info[:breaking_description] = @tty_prompt.ask('Enter breaking change description:') do |q|
382
394
  q.required true
383
395
  end
384
396
  end
385
397
 
386
- # Any issues closed?
398
+ # Issue references
387
399
  if @tty_prompt.yes?('Does this commit close any issues?', default: false)
388
400
  info[:issues] = @tty_prompt.ask('Enter issue references (e.g., "fix #123, close #456"):') do |q|
389
401
  q.required true
@@ -392,6 +404,25 @@ module Snakommit
392
404
 
393
405
  info
394
406
  rescue Interrupt
407
+ # Improved interrupt handling
408
+ puts "\n#{@pastel.yellow('Interruption:')} Commit process interrupted."
409
+
410
+ # Check if there are staged files
411
+ staged_files = @git.staged_files
412
+ if staged_files.any?
413
+ puts "There are currently #{staged_files.length} file(s) staged."
414
+
415
+ # Offer user to unstage files
416
+ begin
417
+ if @tty_prompt.yes?("Do you want to unstage these files?", default: false)
418
+ unstage_files(@git, staged_files)
419
+ puts "All files have been unstaged."
420
+ end
421
+ rescue Interrupt
422
+ puts "\nInterruption detected. Aborting commit process."
423
+ end
424
+ end
425
+
395
426
  { error: 'Commit aborted' }
396
427
  rescue => e
397
428
  { error: "Failed to gather commit information: #{e.message}" }
@@ -399,37 +430,23 @@ module Snakommit
399
430
 
400
431
  # Format the commit message according to convention
401
432
  def format_commit_message(info)
402
- begin
403
- header = ''
404
-
405
- # Format the type and scope
406
- commit_type = @templates.emoji_enabled? ? @templates.format_commit_type(info[:type]) : info[:type]
407
-
408
- if info[:scope] && !info[:scope].empty?
409
- header = "#{commit_type}(#{info[:scope]}): #{info[:subject]}"
410
- else
411
- header = "#{commit_type}: #{info[:subject]}"
412
- end
413
-
414
- # Format the body
415
- body = info[:body].empty? ? '' : "\n\n#{info[:body].join("\n")}"
416
-
417
- # Format breaking change
418
- breaking = ''
419
- if info[:breaking]
420
- breaking = "\n\nBREAKING CHANGE: #{info[:breaking_description]}"
421
- end
422
-
423
- # Format issues
424
- issues = info[:issues] ? "\n\n#{info[:issues]}" : ''
425
-
426
- # Return the full commit message
427
- message = "#{header}#{body}#{breaking}#{issues}"
428
-
429
- message
430
- rescue => e
431
- raise PromptError, "Failed to format commit message: #{e.message}"
432
- end
433
+ # Format header (type, scope, subject)
434
+ commit_type = @templates.emoji_enabled? ? @templates.format_commit_type(info[:type]) : info[:type]
435
+
436
+ header = if info[:scope] && !info[:scope].empty?
437
+ "#{commit_type}(#{info[:scope]}): #{info[:subject]}"
438
+ else
439
+ "#{commit_type}: #{info[:subject]}"
440
+ end
441
+
442
+ # Format body and additional sections
443
+ body = info[:body].empty? ? '' : "\n\n#{info[:body].join("\n")}"
444
+ breaking = info[:breaking] ? "\n\nBREAKING CHANGE: #{info[:breaking_description]}" : ''
445
+ issues = info[:issues] ? "\n\n#{info[:issues]}" : ''
446
+
447
+ "#{header}#{body}#{breaking}#{issues}"
448
+ rescue => e
449
+ raise PromptError, "Failed to format commit message: #{e.message}"
433
450
  end
434
451
 
435
452
  def select_type
@@ -441,7 +458,6 @@ module Snakommit
441
458
  # Format the display name based on emoji settings
442
459
  if @templates.emoji_enabled?
443
460
  emoji = @templates.get_emoji_for_type(value)
444
- # Ensure there's a space between emoji and type
445
461
  name = emoji ? "#{emoji} #{value}: #{type['description']}" : "#{value}: #{type['description']}"
446
462
  else
447
463
  name = "#{value}: #{type['description']}"
@@ -18,7 +18,7 @@ module Snakommit
18
18
  'perf' => '⚡️', # zap
19
19
  'test' => '✅', # check mark
20
20
  'build' => '🔧', # wrench
21
- 'ci' => '👷', # construction worker
21
+ 'ci/cd' => '👷', # construction worker
22
22
  'chore' => '🔨', # hammer
23
23
  'revert' => '⏪️', # rewind
24
24
  }.freeze
@@ -29,8 +29,6 @@ module Snakommit
29
29
  def initialize
30
30
  ensure_config_directory
31
31
  @emoji_formatted_types = {} # Cache for formatted commit types
32
-
33
- # Initialiser les valeurs par défaut avant de charger la configuration
34
32
  @emoji_enabled = false
35
33
  @emoji_map = DEFAULT_EMOJI_MAP.dup
36
34
 
@@ -123,7 +121,6 @@ module Snakommit
123
121
 
124
122
  begin
125
123
  ensure_config_directory
126
- # Écrire le fichier en une seule opération
127
124
  File.write(CONFIG_FILE, config.to_yaml)
128
125
  rescue => e
129
126
  handle_config_error(e, "Failed to save")
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Snakommit
4
- VERSION = '0.1.1'
4
+ VERSION = '0.1.2'
5
5
  end
data/snakommit.gemspec CHANGED
@@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
10
10
  spec.description = "Snakommit helps teams maintain consistent commit message formats by guiding developers through the process of creating standardized commit messages"
11
11
  spec.homepage = "https://github.com/antonia-pl/snakommit"
12
12
  spec.license = "MIT"
13
- spec.required_ruby_version = Gem::Requirement.new(">= 2.5.0")
13
+ spec.required_ruby_version = Gem::Requirement.new(">= 3.0.0")
14
14
 
15
15
  spec.metadata["homepage_uri"] = spec.homepage
16
16
  spec.metadata["source_code_uri"] = spec.homepage