snakommit 0.1.1
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 +7 -0
- data/.gitignore +104 -0
- data/CHANGELOG.md +55 -0
- data/Gemfile +15 -0
- data/Gemfile.lock +81 -0
- data/LICENSE +21 -0
- data/README.md +275 -0
- data/Rakefile +58 -0
- data/bin/sk +9 -0
- data/bin/snakommit +10 -0
- data/lib/snakommit/cli.rb +371 -0
- data/lib/snakommit/config.rb +154 -0
- data/lib/snakommit/git.rb +212 -0
- data/lib/snakommit/hooks.rb +258 -0
- data/lib/snakommit/performance.rb +328 -0
- data/lib/snakommit/prompt.rb +472 -0
- data/lib/snakommit/templates.rb +146 -0
- data/lib/snakommit/version.rb +5 -0
- data/lib/snakommit.rb +35 -0
- data/snakommit.gemspec +38 -0
- metadata +194 -0
@@ -0,0 +1,472 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'tty-prompt'
|
4
|
+
require 'tty-spinner'
|
5
|
+
require 'tty-screen'
|
6
|
+
require 'pastel'
|
7
|
+
|
8
|
+
module Snakommit
|
9
|
+
# Handles interactive prompts
|
10
|
+
class Prompt
|
11
|
+
class PromptError < StandardError; end
|
12
|
+
|
13
|
+
BANNER = "SNAKOMMIT - A commit manager"
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
@config = Config.load
|
17
|
+
@git = Git.new
|
18
|
+
@tty_prompt = TTY::Prompt.new
|
19
|
+
@templates = Templates.new
|
20
|
+
@batch_processor = Performance::BatchProcessor.new(20) # Batch size optimized for file operations
|
21
|
+
@monitor = Performance::Monitor.new # Performance monitoring
|
22
|
+
validate_config
|
23
|
+
@pastel = Pastel.new
|
24
|
+
@width = TTY::Screen.width > 100 ? 100 : TTY::Screen.width
|
25
|
+
rescue => e
|
26
|
+
raise PromptError, "Failed to initialize prompt: #{e.message}"
|
27
|
+
end
|
28
|
+
|
29
|
+
# Main interactive commit flow
|
30
|
+
def commit_flow
|
31
|
+
return { error: 'Not in a Git repository' } unless Git.in_repo?
|
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)
|
73
|
+
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
|
+
select_files(@git)
|
78
|
+
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
|
91
|
+
|
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]
|
100
|
+
|
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}" }
|
137
|
+
end
|
138
|
+
|
139
|
+
{ success: true, message: message }
|
140
|
+
rescue Git::GitError => e
|
141
|
+
puts "\nError: #{e.message}"
|
142
|
+
{ error: "Git error: #{e.message}" }
|
143
|
+
rescue => e
|
144
|
+
puts "\nError: #{e.message}"
|
145
|
+
{ error: "Error during commit flow: #{e.message}" }
|
146
|
+
end
|
147
|
+
|
148
|
+
private
|
149
|
+
|
150
|
+
# Select files to add
|
151
|
+
def select_files(git)
|
152
|
+
begin
|
153
|
+
# Get file lists with performance monitoring
|
154
|
+
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
|
+
}
|
160
|
+
end
|
161
|
+
|
162
|
+
unstaged = repo_status[:unstaged]
|
163
|
+
untracked = repo_status[:untracked]
|
164
|
+
staged = repo_status[:staged]
|
165
|
+
|
166
|
+
# Combine all files that could be staged
|
167
|
+
all_stageable_files = unstaged + untracked
|
168
|
+
|
169
|
+
# If there are no files to stage or unstage, return early
|
170
|
+
if all_stageable_files.empty? && staged.empty?
|
171
|
+
puts "No changes detected in the repository."
|
172
|
+
return
|
173
|
+
end
|
174
|
+
|
175
|
+
# If we only have staged files but nothing new to stage
|
176
|
+
if all_stageable_files.empty? && !staged.empty?
|
177
|
+
puts "\nCurrently staged files:"
|
178
|
+
staged.each { |file| puts "- #{file}" }
|
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
|
184
|
+
return
|
185
|
+
end
|
186
|
+
|
187
|
+
# First show the user what's currently staged
|
188
|
+
unless staged.empty?
|
189
|
+
puts "\nCurrently staged files:"
|
190
|
+
staged.each { |file| puts "- #{file}" }
|
191
|
+
end
|
192
|
+
|
193
|
+
# Create options for the file selection menu
|
194
|
+
options = []
|
195
|
+
|
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
|
+
unstaged.each_with_index do |file, idx|
|
203
|
+
options << { name: "#{idx+1}. Modified: #{file}", value: file }
|
204
|
+
end
|
205
|
+
|
206
|
+
# Add untracked files with continuing index numbers
|
207
|
+
untracked.each_with_index do |file, idx|
|
208
|
+
options << { name: "#{unstaged.length + idx + 1}. Untracked: #{file}", value: file }
|
209
|
+
end
|
210
|
+
|
211
|
+
# Skip if no options
|
212
|
+
if options.empty?
|
213
|
+
puts "No files available to stage."
|
214
|
+
return
|
215
|
+
end
|
216
|
+
|
217
|
+
# Prompt user to select files
|
218
|
+
puts "\nSelect files to stage for commit:"
|
219
|
+
selected = @tty_prompt.multi_select("Choose files (use space to select, enter to confirm):", options, per_page: 15, echo: true)
|
220
|
+
|
221
|
+
# Check if anything was selected
|
222
|
+
if selected.empty?
|
223
|
+
puts "No files selected for staging."
|
224
|
+
|
225
|
+
# If we already have staged files, ask if user wants to continue with those
|
226
|
+
unless staged.empty?
|
227
|
+
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
|
230
|
+
end
|
231
|
+
|
232
|
+
return
|
233
|
+
end
|
234
|
+
|
235
|
+
puts "\nSelected files to stage:"
|
236
|
+
|
237
|
+
# Handle "ALL FILES" option
|
238
|
+
if selected.include?(:all_files)
|
239
|
+
selected = all_stageable_files
|
240
|
+
puts "- All files (#{selected.length})"
|
241
|
+
else
|
242
|
+
selected.each { |file| puts "- #{file}" }
|
243
|
+
end
|
244
|
+
|
245
|
+
# Add a confirmation step
|
246
|
+
if @tty_prompt.yes?("Proceed with staging these files?", default: true)
|
247
|
+
# Stage the selected files
|
248
|
+
stage_files(git, selected)
|
249
|
+
else
|
250
|
+
puts "Staging canceled by user."
|
251
|
+
return
|
252
|
+
end
|
253
|
+
|
254
|
+
# After staging, check if the user wants to unstage any files
|
255
|
+
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
|
260
|
+
end
|
261
|
+
rescue TTY::Reader::InputInterrupt
|
262
|
+
puts "\nFile selection aborted. Press Ctrl+C again to exit completely or continue."
|
263
|
+
return
|
264
|
+
rescue => e
|
265
|
+
puts "\nError during file selection: #{e.message}"
|
266
|
+
puts "Would you like to try again?"
|
267
|
+
return if @tty_prompt.yes?("Try selecting files again?", default: true)
|
268
|
+
raise PromptError, "Failed to select files: #{e.message}"
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
# Stage the selected files
|
273
|
+
def stage_files(git, selected)
|
274
|
+
# Save the selections for future use
|
275
|
+
git.save_selections(selected)
|
276
|
+
|
277
|
+
spinner = TTY::Spinner.new("[:spinner] Adding files... ", format: :dots)
|
278
|
+
spinner.auto_spin
|
279
|
+
|
280
|
+
# Use batch processing for more efficient staging
|
281
|
+
@monitor.measure(:batch_stage_files) do
|
282
|
+
@batch_processor.process_files(selected) do |batch|
|
283
|
+
# Use parallel helper if available and appropriate
|
284
|
+
Performance::ParallelHelper.process(batch, threshold: 5) do |file|
|
285
|
+
git.add(file)
|
286
|
+
end
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
# Verify staging worked
|
291
|
+
newly_staged = git.staged_files
|
292
|
+
if newly_staged.empty?
|
293
|
+
spinner.error("Failed to stage files!")
|
294
|
+
puts "Warning: No files appear to be staged after add operation."
|
295
|
+
puts "This might be a Git or permission issue."
|
296
|
+
raise PromptError, "Failed to stage files"
|
297
|
+
else
|
298
|
+
spinner.success("Files added to staging area (#{newly_staged.length} file(s))")
|
299
|
+
end
|
300
|
+
rescue => e
|
301
|
+
raise PromptError, "Failed to stage files: #{e.message}"
|
302
|
+
end
|
303
|
+
|
304
|
+
# Unstage selected files
|
305
|
+
def unstage_files(git, staged_files)
|
306
|
+
return if staged_files.empty?
|
307
|
+
|
308
|
+
# Create options for unstaging
|
309
|
+
unstage_options = staged_files.map { |f| { name: f, value: f } }
|
310
|
+
|
311
|
+
# Unstage heading
|
312
|
+
puts "\nUnstage Files:"
|
313
|
+
|
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
|
320
|
+
|
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) }
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
328
|
+
spinner.success("Files unstaged")
|
329
|
+
end
|
330
|
+
rescue => e
|
331
|
+
raise PromptError, "Failed to unstage files: #{e.message}"
|
332
|
+
end
|
333
|
+
|
334
|
+
# Get commit information
|
335
|
+
def get_commit_info
|
336
|
+
info = {}
|
337
|
+
|
338
|
+
puts "\nCommit Details:"
|
339
|
+
|
340
|
+
# Select commit type
|
341
|
+
info[:type] = select_type
|
342
|
+
|
343
|
+
# Enter scope (optional)
|
344
|
+
suggested_scopes = @config['scopes']
|
345
|
+
if suggested_scopes && !suggested_scopes.empty?
|
346
|
+
scope_options = suggested_scopes.map { |s| { name: s, value: s } }
|
347
|
+
scope_options << { name: '[none]', value: nil }
|
348
|
+
|
349
|
+
info[:scope] = @tty_prompt.select('Select the scope of this change (optional, press Enter to skip):',
|
350
|
+
scope_options,
|
351
|
+
per_page: 10)
|
352
|
+
else
|
353
|
+
info[:scope] = @tty_prompt.ask('Enter the scope of this change (optional, press Enter to skip):')
|
354
|
+
end
|
355
|
+
|
356
|
+
# Enter subject
|
357
|
+
info[:subject] = @tty_prompt.ask('Enter a short description:') do |q|
|
358
|
+
q.required true
|
359
|
+
q.validate(/^.{1,#{@config['max_subject_length']}}$/, "Subject must be less than #{@config['max_subject_length']} characters")
|
360
|
+
end
|
361
|
+
|
362
|
+
# Enter longer description (optional)
|
363
|
+
puts "Enter a longer description (optional, press Enter to skip):"
|
364
|
+
puts "Type your message and press Enter when done. Leave empty to skip."
|
365
|
+
|
366
|
+
body_lines = []
|
367
|
+
# Read input until an empty line is entered
|
368
|
+
loop do
|
369
|
+
line = @tty_prompt.ask("")
|
370
|
+
break if line.nil? || line.empty?
|
371
|
+
body_lines << line
|
372
|
+
end
|
373
|
+
|
374
|
+
info[:body] = body_lines
|
375
|
+
|
376
|
+
# Is this a breaking change?
|
377
|
+
info[:breaking] = @tty_prompt.yes?('Is this a breaking change?', default: false)
|
378
|
+
|
379
|
+
# Breaking change description
|
380
|
+
if info[:breaking]
|
381
|
+
info[:breaking_description] = @tty_prompt.ask('Enter breaking change description:') do |q|
|
382
|
+
q.required true
|
383
|
+
end
|
384
|
+
end
|
385
|
+
|
386
|
+
# Any issues closed?
|
387
|
+
if @tty_prompt.yes?('Does this commit close any issues?', default: false)
|
388
|
+
info[:issues] = @tty_prompt.ask('Enter issue references (e.g., "fix #123, close #456"):') do |q|
|
389
|
+
q.required true
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
info
|
394
|
+
rescue Interrupt
|
395
|
+
{ error: 'Commit aborted' }
|
396
|
+
rescue => e
|
397
|
+
{ error: "Failed to gather commit information: #{e.message}" }
|
398
|
+
end
|
399
|
+
|
400
|
+
# Format the commit message according to convention
|
401
|
+
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
|
+
end
|
434
|
+
|
435
|
+
def select_type
|
436
|
+
commit_types = @config['types']
|
437
|
+
|
438
|
+
choices = commit_types.map do |type|
|
439
|
+
value = type['name']
|
440
|
+
|
441
|
+
# Format the display name based on emoji settings
|
442
|
+
if @templates.emoji_enabled?
|
443
|
+
emoji = @templates.get_emoji_for_type(value)
|
444
|
+
# Ensure there's a space between emoji and type
|
445
|
+
name = emoji ? "#{emoji} #{value}: #{type['description']}" : "#{value}: #{type['description']}"
|
446
|
+
else
|
447
|
+
name = "#{value}: #{type['description']}"
|
448
|
+
end
|
449
|
+
|
450
|
+
{ name: name, value: value }
|
451
|
+
end
|
452
|
+
|
453
|
+
@tty_prompt.select('Choose a type:', choices, filter: true, per_page: 10)
|
454
|
+
end
|
455
|
+
|
456
|
+
# Validates the configuration loaded from file
|
457
|
+
def validate_config
|
458
|
+
unless @config.is_a?(Hash) && @config['types'].is_a?(Array)
|
459
|
+
raise PromptError, "Invalid configuration format: 'types' must be an array"
|
460
|
+
end
|
461
|
+
|
462
|
+
if @config['types'].empty?
|
463
|
+
raise PromptError, "Configuration error: No commit types defined"
|
464
|
+
end
|
465
|
+
|
466
|
+
# Ensure all required keys are present
|
467
|
+
@config['max_subject_length'] ||= 100
|
468
|
+
@config['max_body_line_length'] ||= 72
|
469
|
+
@config['scopes'] ||= []
|
470
|
+
end
|
471
|
+
end
|
472
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
require 'fileutils'
|
5
|
+
|
6
|
+
module Snakommit
|
7
|
+
# Manages commit message formatting and emoji options
|
8
|
+
class Templates
|
9
|
+
class TemplateError < StandardError; end
|
10
|
+
|
11
|
+
# Emoji mappings for commit types
|
12
|
+
DEFAULT_EMOJI_MAP = {
|
13
|
+
'feat' => '✨', # sparkles
|
14
|
+
'fix' => '🐛', # bug
|
15
|
+
'docs' => '📝', # memo
|
16
|
+
'style' => '💄', # lipstick
|
17
|
+
'refactor' => '♻️', # recycle
|
18
|
+
'perf' => '⚡️', # zap
|
19
|
+
'test' => '✅', # check mark
|
20
|
+
'build' => '🔧', # wrench
|
21
|
+
'ci' => '👷', # construction worker
|
22
|
+
'chore' => '🔨', # hammer
|
23
|
+
'revert' => '⏪️', # rewind
|
24
|
+
}.freeze
|
25
|
+
|
26
|
+
# Configuration file path
|
27
|
+
CONFIG_FILE = File.join(Snakommit.config_dir, 'emoji_config.yml')
|
28
|
+
|
29
|
+
def initialize
|
30
|
+
ensure_config_directory
|
31
|
+
@emoji_formatted_types = {} # Cache for formatted commit types
|
32
|
+
|
33
|
+
# Initialiser les valeurs par défaut avant de charger la configuration
|
34
|
+
@emoji_enabled = false
|
35
|
+
@emoji_map = DEFAULT_EMOJI_MAP.dup
|
36
|
+
|
37
|
+
load_config
|
38
|
+
end
|
39
|
+
|
40
|
+
def toggle_emoji(enable = nil)
|
41
|
+
return @emoji_enabled if enable.nil? && defined?(@emoji_enabled)
|
42
|
+
|
43
|
+
@emoji_enabled = enable.nil? ? !@emoji_enabled : enable
|
44
|
+
save_config
|
45
|
+
# Clear cached formatted types since the formatting changed
|
46
|
+
@emoji_formatted_types.clear
|
47
|
+
@emoji_enabled
|
48
|
+
end
|
49
|
+
|
50
|
+
def emoji_enabled?
|
51
|
+
@emoji_enabled
|
52
|
+
end
|
53
|
+
|
54
|
+
def format_commit_type(type)
|
55
|
+
return type unless @emoji_enabled
|
56
|
+
return type unless @emoji_map.key?(type)
|
57
|
+
|
58
|
+
# Check cache first
|
59
|
+
@emoji_formatted_types[type] ||= begin
|
60
|
+
emoji = @emoji_map[type]
|
61
|
+
"#{emoji} #{type}"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def get_emoji_for_type(type)
|
66
|
+
@emoji_map[type]
|
67
|
+
end
|
68
|
+
|
69
|
+
def list_emoji_mappings
|
70
|
+
@emoji_map.map { |type, emoji| { type: type, emoji: emoji } }
|
71
|
+
end
|
72
|
+
|
73
|
+
def update_emoji_mapping(type, emoji)
|
74
|
+
unless @emoji_map.key?(type)
|
75
|
+
raise TemplateError, "Unknown commit type: #{type}"
|
76
|
+
end
|
77
|
+
|
78
|
+
@emoji_map[type] = emoji
|
79
|
+
# Clear specific cached entry
|
80
|
+
@emoji_formatted_types.delete(type)
|
81
|
+
save_config
|
82
|
+
end
|
83
|
+
|
84
|
+
def reset_emoji_mappings
|
85
|
+
@emoji_map = DEFAULT_EMOJI_MAP.dup
|
86
|
+
# Clear all cached entries
|
87
|
+
@emoji_formatted_types.clear
|
88
|
+
save_config
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def load_config
|
94
|
+
if File.exist?(CONFIG_FILE)
|
95
|
+
begin
|
96
|
+
load_existing_config
|
97
|
+
rescue => e
|
98
|
+
handle_config_error(e, "Failed to load")
|
99
|
+
initialize_default_config
|
100
|
+
end
|
101
|
+
else
|
102
|
+
initialize_default_config
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def load_existing_config
|
107
|
+
config = YAML.load_file(CONFIG_FILE) || {}
|
108
|
+
@emoji_map = config['emoji_map'] || DEFAULT_EMOJI_MAP.dup
|
109
|
+
@emoji_enabled = !!config['emoji_enabled'] # Convert to boolean
|
110
|
+
end
|
111
|
+
|
112
|
+
def initialize_default_config
|
113
|
+
@emoji_map = DEFAULT_EMOJI_MAP.dup
|
114
|
+
@emoji_enabled = false
|
115
|
+
save_config
|
116
|
+
end
|
117
|
+
|
118
|
+
def save_config
|
119
|
+
config = {
|
120
|
+
'emoji_map' => @emoji_map,
|
121
|
+
'emoji_enabled' => @emoji_enabled
|
122
|
+
}
|
123
|
+
|
124
|
+
begin
|
125
|
+
ensure_config_directory
|
126
|
+
# Écrire le fichier en une seule opération
|
127
|
+
File.write(CONFIG_FILE, config.to_yaml)
|
128
|
+
rescue => e
|
129
|
+
handle_config_error(e, "Failed to save")
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def handle_config_error(error, action_description)
|
134
|
+
warn "Warning: #{action_description} emoji config: #{error.message}"
|
135
|
+
@emoji_map ||= DEFAULT_EMOJI_MAP.dup
|
136
|
+
@emoji_enabled = false unless defined?(@emoji_enabled)
|
137
|
+
end
|
138
|
+
|
139
|
+
def ensure_config_directory
|
140
|
+
config_dir = File.dirname(CONFIG_FILE)
|
141
|
+
FileUtils.mkdir_p(config_dir) unless Dir.exist?(config_dir)
|
142
|
+
rescue Errno::EACCES => e
|
143
|
+
warn "Warning: Permission denied creating config directory: #{e.message}"
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
data/lib/snakommit.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Load version first
|
4
|
+
require 'snakommit/version'
|
5
|
+
|
6
|
+
# Main module for the Snakommit application
|
7
|
+
module Snakommit
|
8
|
+
class Error < StandardError; end
|
9
|
+
|
10
|
+
# Returns the configuration directory path
|
11
|
+
# @return [String] Path to configuration directory
|
12
|
+
def self.config_dir
|
13
|
+
File.join(ENV['HOME'] || Dir.home, '.snakommit')
|
14
|
+
end
|
15
|
+
|
16
|
+
# Returns the current version string
|
17
|
+
# @return [String] Current version
|
18
|
+
def self.version
|
19
|
+
VERSION
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Now require the rest of the modules
|
24
|
+
# Core functionality
|
25
|
+
require 'snakommit/config'
|
26
|
+
require 'snakommit/git'
|
27
|
+
|
28
|
+
# User interface
|
29
|
+
require 'snakommit/prompt'
|
30
|
+
require 'snakommit/cli'
|
31
|
+
|
32
|
+
# Extensions
|
33
|
+
require 'snakommit/performance'
|
34
|
+
require 'snakommit/templates'
|
35
|
+
require 'snakommit/hooks'
|
data/snakommit.gemspec
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
require_relative 'lib/snakommit/version'
|
2
|
+
|
3
|
+
Gem::Specification.new do |spec|
|
4
|
+
spec.name = "snakommit"
|
5
|
+
spec.version = Snakommit::VERSION
|
6
|
+
spec.authors = ["Antonia PL"]
|
7
|
+
spec.email = ["antonia.dev@icloud.com"]
|
8
|
+
|
9
|
+
spec.summary = "A high-performance, interactive commit manager tool similar to Commitizen"
|
10
|
+
spec.description = "Snakommit helps teams maintain consistent commit message formats by guiding developers through the process of creating standardized commit messages"
|
11
|
+
spec.homepage = "https://github.com/antonia-pl/snakommit"
|
12
|
+
spec.license = "MIT"
|
13
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 2.5.0")
|
14
|
+
|
15
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
16
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
17
|
+
spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
|
18
|
+
|
19
|
+
# Specify which files should be added to the gem when it is released.
|
20
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
21
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
22
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
23
|
+
end
|
24
|
+
spec.bindir = "bin"
|
25
|
+
spec.executables = ["snakommit", "sk"]
|
26
|
+
spec.require_paths = ["lib"]
|
27
|
+
|
28
|
+
spec.add_dependency "tty-prompt", "~> 0.23.1"
|
29
|
+
spec.add_dependency "tty-spinner", "~> 0.9.3"
|
30
|
+
spec.add_dependency "tty-color", "~> 0.6.0"
|
31
|
+
spec.add_dependency "git", "~> 1.12"
|
32
|
+
|
33
|
+
spec.add_development_dependency "bundler", "~> 2.0"
|
34
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
35
|
+
spec.add_development_dependency "rspec", "~> 3.10"
|
36
|
+
spec.add_development_dependency "rubocop", "~> 1.25.1"
|
37
|
+
spec.add_development_dependency "parallel", "~> 1.21"
|
38
|
+
end
|