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.
- checksums.yaml +4 -4
- data/.github/workflows/cd.yml +81 -0
- data/.github/workflows/ci.yml +73 -0
- data/.ruby-version +1 -0
- data/Gemfile.lock +12 -10
- data/README.md +32 -55
- data/Rakefile +10 -10
- data/lib/snakommit/cli.rb +43 -79
- data/lib/snakommit/config.rb +80 -108
- data/lib/snakommit/git.rb +1 -1
- data/lib/snakommit/hooks.rb +2 -3
- data/lib/snakommit/performance.rb +12 -84
- data/lib/snakommit/prompt.rb +218 -202
- data/lib/snakommit/templates.rb +1 -4
- data/lib/snakommit/version.rb +1 -1
- data/snakommit.gemspec +1 -1
- metadata +8 -9
- data/CHANGELOG.md +0 -55
data/lib/snakommit/config.rb
CHANGED
@@ -21,7 +21,7 @@ module Snakommit
|
|
21
21
|
{ 'name' => 'perf', 'description' => 'A code change that improves performance' },
|
22
22
|
{ 'name' => 'test', 'description' => 'Adding missing tests or correcting existing tests' },
|
23
23
|
{ 'name' => 'build', 'description' => 'Changes that affect the build system or external dependencies' },
|
24
|
-
{ 'name' => 'ci', 'description' => 'Changes to our CI configuration files and scripts' },
|
24
|
+
{ 'name' => 'ci/cd', 'description' => 'Changes to our CI/CD configuration files and scripts' },
|
25
25
|
{ 'name' => 'chore', 'description' => 'Other changes that don\'t modify src or test files' }
|
26
26
|
],
|
27
27
|
'scopes' => [],
|
@@ -29,126 +29,98 @@ module Snakommit
|
|
29
29
|
'max_body_line_length' => 72
|
30
30
|
}.freeze
|
31
31
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
# @raise [ConfigError] If configuration can't be loaded
|
39
|
-
def self.load
|
40
|
-
create_default_config unless File.exist?(CONFIG_FILE)
|
41
|
-
|
42
|
-
# Check if config file has been modified since last load
|
43
|
-
current_mtime = File.mtime(CONFIG_FILE) rescue nil
|
44
|
-
|
45
|
-
# Return cached config if it exists and file hasn't been modified
|
46
|
-
if @config_cache && @config_last_modified == current_mtime
|
47
|
-
return @config_cache.dup
|
32
|
+
class << self
|
33
|
+
# Initialize class variables
|
34
|
+
def setup_cache
|
35
|
+
@config_cache ||= {}
|
36
|
+
@config_last_modified ||= nil
|
37
|
+
true
|
48
38
|
end
|
49
|
-
|
50
|
-
# Load and cache the configuration
|
51
|
-
@config_cache = YAML.load_file(CONFIG_FILE) || {}
|
52
|
-
@config_last_modified = current_mtime
|
53
|
-
|
54
|
-
# Return a copy to prevent unintentional modifications
|
55
|
-
@config_cache.dup
|
56
|
-
rescue Errno::EACCES, Errno::ENOENT => e
|
57
|
-
raise ConfigError, "Could not load configuration: #{e.message}"
|
58
|
-
rescue => e
|
59
|
-
raise ConfigError, "Unexpected error loading configuration: #{e.message}"
|
60
|
-
end
|
61
39
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
40
|
+
def load
|
41
|
+
create_default_config unless File.exist?(CONFIG_FILE)
|
42
|
+
|
43
|
+
# Use cached config if file hasn't been modified
|
44
|
+
current_mtime = File.mtime(CONFIG_FILE) rescue nil
|
45
|
+
# Ensure cache is initialized
|
46
|
+
setup_cache
|
47
|
+
return @config_cache.dup if @config_cache && @config_last_modified == current_mtime
|
48
|
+
|
49
|
+
# Load and cache the configuration
|
50
|
+
@config_cache = YAML.load_file(CONFIG_FILE) || {}
|
51
|
+
@config_last_modified = current_mtime
|
52
|
+
|
53
|
+
@config_cache.dup
|
54
|
+
rescue Errno::EACCES, Errno::ENOENT => e
|
55
|
+
raise ConfigError, "Could not load configuration: #{e.message}"
|
56
|
+
rescue => e
|
57
|
+
raise ConfigError, "Unexpected error loading configuration: #{e.message}"
|
74
58
|
end
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
59
|
+
|
60
|
+
def create_default_config
|
61
|
+
# Ensure config directory exists
|
62
|
+
config_dir = File.dirname(CONFIG_FILE)
|
63
|
+
FileUtils.mkdir_p(config_dir) unless Dir.exist?(config_dir)
|
64
|
+
|
65
|
+
# Write default config if file doesn't exist
|
66
|
+
unless File.exist?(CONFIG_FILE)
|
79
67
|
File.write(CONFIG_FILE, DEFAULT_CONFIG.to_yaml)
|
80
68
|
|
81
69
|
# Update cache
|
82
70
|
@config_cache = DEFAULT_CONFIG.dup
|
83
71
|
@config_last_modified = File.mtime(CONFIG_FILE) rescue nil
|
84
|
-
rescue Errno::EACCES => e
|
85
|
-
raise ConfigError, "Permission denied creating config file: #{e.message}"
|
86
|
-
rescue => e
|
87
|
-
raise ConfigError, "Unexpected error creating config file: #{e.message}"
|
88
72
|
end
|
73
|
+
|
74
|
+
true
|
75
|
+
rescue Errno::EACCES => e
|
76
|
+
raise ConfigError, "Permission denied: #{e.message}"
|
77
|
+
rescue => e
|
78
|
+
raise ConfigError, "Failed to create config file: #{e.message}"
|
89
79
|
end
|
90
80
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
File.write(CONFIG_FILE, config.to_yaml)
|
81
|
+
def update(updates)
|
82
|
+
config = load.merge(updates)
|
83
|
+
|
84
|
+
# Backup and write updated config
|
85
|
+
backup_config if File.exist?(CONFIG_FILE)
|
86
|
+
File.write(CONFIG_FILE, config.to_yaml)
|
87
|
+
|
88
|
+
# Update cache
|
89
|
+
@config_cache = config.dup
|
90
|
+
@config_last_modified = File.mtime(CONFIG_FILE) rescue nil
|
91
|
+
|
92
|
+
config
|
93
|
+
rescue => e
|
94
|
+
raise ConfigError, "Failed to update configuration: #{e.message}"
|
95
|
+
end
|
107
96
|
|
108
|
-
|
109
|
-
|
110
|
-
|
97
|
+
def get(key, default = nil)
|
98
|
+
load.fetch(key, default)
|
99
|
+
end
|
111
100
|
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
end
|
125
|
-
|
126
|
-
# Reset configuration to defaults
|
127
|
-
# @return [Hash] The default configuration
|
128
|
-
def self.reset
|
129
|
-
backup_config if File.exist?(CONFIG_FILE)
|
130
|
-
File.write(CONFIG_FILE, DEFAULT_CONFIG.to_yaml)
|
101
|
+
def reset
|
102
|
+
backup_config if File.exist?(CONFIG_FILE)
|
103
|
+
File.write(CONFIG_FILE, DEFAULT_CONFIG.to_yaml)
|
104
|
+
|
105
|
+
# Update cache
|
106
|
+
@config_cache = DEFAULT_CONFIG.dup
|
107
|
+
@config_last_modified = File.mtime(CONFIG_FILE) rescue nil
|
108
|
+
|
109
|
+
DEFAULT_CONFIG.dup
|
110
|
+
rescue => e
|
111
|
+
raise ConfigError, "Failed to reset configuration: #{e.message}"
|
112
|
+
end
|
131
113
|
|
132
|
-
|
133
|
-
@config_cache = DEFAULT_CONFIG.dup
|
134
|
-
@config_last_modified = File.mtime(CONFIG_FILE) rescue nil
|
114
|
+
private
|
135
115
|
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
#
|
145
|
-
def self.backup_config
|
146
|
-
backup_file = "#{CONFIG_FILE}.bak"
|
147
|
-
FileUtils.cp(CONFIG_FILE, backup_file)
|
148
|
-
backup_file
|
149
|
-
rescue => e
|
150
|
-
warn "Warning: Failed to backup configuration: #{e.message}"
|
151
|
-
nil
|
152
|
-
end
|
116
|
+
def backup_config
|
117
|
+
backup_file = "#{CONFIG_FILE}.bak"
|
118
|
+
FileUtils.cp(CONFIG_FILE, backup_file)
|
119
|
+
backup_file
|
120
|
+
rescue => e
|
121
|
+
warn "Warning: Failed to backup configuration: #{e.message}"
|
122
|
+
nil
|
123
|
+
end
|
124
|
+
end # End of class << self
|
153
125
|
end
|
154
|
-
end
|
126
|
+
end
|
data/lib/snakommit/git.rb
CHANGED
@@ -102,7 +102,7 @@ module Snakommit
|
|
102
102
|
# Commit with the given message
|
103
103
|
def commit(message)
|
104
104
|
with_temp_file(message) do |message_file|
|
105
|
-
|
105
|
+
_, stderr, status = Open3.capture3('git', 'commit', '-F', message_file)
|
106
106
|
|
107
107
|
# Clear any saved selections after successful commit
|
108
108
|
clear_saved_selections
|
data/lib/snakommit/hooks.rb
CHANGED
@@ -205,11 +205,10 @@ module Snakommit
|
|
205
205
|
end
|
206
206
|
|
207
207
|
false
|
208
|
-
rescue
|
208
|
+
rescue StandardError
|
209
209
|
# If we can't read the file, it's not our hook
|
210
210
|
false
|
211
211
|
end
|
212
|
-
|
213
212
|
# Backup an existing hook file
|
214
213
|
# @param hook_path [String] Path to the hook file
|
215
214
|
# @return [String, nil] Path to the backup file or nil if no backup created
|
@@ -255,4 +254,4 @@ module Snakommit
|
|
255
254
|
false
|
256
255
|
end
|
257
256
|
end
|
258
|
-
end
|
257
|
+
end
|
@@ -7,10 +7,9 @@ module Snakommit
|
|
7
7
|
class Performance
|
8
8
|
# Cache for expensive Git operations
|
9
9
|
class Cache
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
def initialize(max_size = 100, ttl = 300) # 5 minutes TTL by default
|
10
|
+
attr_reader :max_size, :ttl
|
11
|
+
|
12
|
+
def initialize(max_size = 100, ttl = 300)
|
14
13
|
@cache = {}
|
15
14
|
@max_size = max_size
|
16
15
|
@ttl = ttl
|
@@ -18,9 +17,6 @@ module Snakommit
|
|
18
17
|
@misses = 0
|
19
18
|
end
|
20
19
|
|
21
|
-
# Get a value from the cache
|
22
|
-
# @param key [Object] Cache key
|
23
|
-
# @return [Object, nil] Cached value or nil if not found or expired
|
24
20
|
def get(key)
|
25
21
|
return nil unless @cache.key?(key)
|
26
22
|
entry = @cache[key]
|
@@ -34,10 +30,6 @@ module Snakommit
|
|
34
30
|
entry[:value]
|
35
31
|
end
|
36
32
|
|
37
|
-
# Set a value in the cache
|
38
|
-
# @param key [Object] Cache key
|
39
|
-
# @param value [Object] Value to cache
|
40
|
-
# @return [Object] The value that was cached
|
41
33
|
def set(key, value)
|
42
34
|
cleanup if @cache.size >= @max_size
|
43
35
|
@cache[key] = { value: value, timestamp: Time.now }
|
@@ -45,22 +37,15 @@ module Snakommit
|
|
45
37
|
value
|
46
38
|
end
|
47
39
|
|
48
|
-
# Remove a specific key from the cache
|
49
|
-
# @param key [Object] Cache key to invalidate
|
50
|
-
# @return [nil]
|
51
40
|
def invalidate(key)
|
52
41
|
@cache.delete(key)
|
53
42
|
nil
|
54
43
|
end
|
55
44
|
|
56
|
-
# Clear the entire cache
|
57
|
-
# @return [Hash] Empty hash
|
58
45
|
def clear
|
59
46
|
@cache = {}
|
60
47
|
end
|
61
48
|
|
62
|
-
# Get cache stats
|
63
|
-
# @return [Hash] Cache statistics
|
64
49
|
def stats
|
65
50
|
{
|
66
51
|
size: @cache.size,
|
@@ -72,8 +57,6 @@ module Snakommit
|
|
72
57
|
}
|
73
58
|
end
|
74
59
|
|
75
|
-
# Calculate cache hit rate
|
76
|
-
# @return [Float] Cache hit rate as a percentage
|
77
60
|
def hit_rate
|
78
61
|
total = @hits + @misses
|
79
62
|
return 0.0 if total.zero?
|
@@ -82,17 +65,14 @@ module Snakommit
|
|
82
65
|
|
83
66
|
private
|
84
67
|
|
85
|
-
# Clean up expired or oldest entries when cache is full
|
86
|
-
# @return [nil]
|
87
68
|
def cleanup
|
88
69
|
# Remove expired entries first
|
89
|
-
|
90
|
-
@cache.delete_if { |k, _| expired_keys.include?(k) }
|
70
|
+
@cache.delete_if { |_, v| Time.now - v[:timestamp] > @ttl }
|
91
71
|
|
92
72
|
# If still too large, remove oldest entries
|
93
73
|
if @cache.size >= @max_size
|
94
74
|
sorted_keys = @cache.sort_by { |_, v| v[:timestamp] }.map(&:first)
|
95
|
-
sorted_keys
|
75
|
+
sorted_keys.first(@cache.size - @max_size / 2).each { |k| @cache.delete(k) }
|
96
76
|
end
|
97
77
|
|
98
78
|
nil
|
@@ -101,25 +81,19 @@ module Snakommit
|
|
101
81
|
|
102
82
|
# Batch processing for Git operations
|
103
83
|
class BatchProcessor
|
104
|
-
|
105
|
-
|
84
|
+
attr_reader :batch_size, :total_processed, :batch_count
|
85
|
+
|
106
86
|
def initialize(batch_size = 100)
|
107
87
|
@batch_size = batch_size
|
108
88
|
@total_processed = 0
|
109
89
|
@batch_count = 0
|
110
90
|
end
|
111
91
|
|
112
|
-
# Process files in batches
|
113
|
-
# @param files [Array<String>] List of files to process
|
114
|
-
# @param batch_size [Integer, nil] Optional override for batch size
|
115
|
-
# @yield [batch] Yields each batch of files for processing
|
116
|
-
# @yieldparam batch [Array<String>] A batch of files
|
117
|
-
# @return [Array] Combined results from all batches
|
118
92
|
def process_files(files, batch_size = nil, &block)
|
119
93
|
size = batch_size || @batch_size
|
120
94
|
results = []
|
121
95
|
|
122
|
-
files.each_slice(size).
|
96
|
+
files.each_slice(size).each do |batch|
|
123
97
|
@batch_count += 1
|
124
98
|
batch_result = block.call(batch)
|
125
99
|
@total_processed += batch.size
|
@@ -129,8 +103,6 @@ module Snakommit
|
|
129
103
|
results
|
130
104
|
end
|
131
105
|
|
132
|
-
# Get batch processing stats
|
133
|
-
# @return [Hash] Batch processing statistics
|
134
106
|
def stats
|
135
107
|
{
|
136
108
|
batch_size: @batch_size,
|
@@ -140,8 +112,6 @@ module Snakommit
|
|
140
112
|
}
|
141
113
|
end
|
142
114
|
|
143
|
-
# Calculate average batch size
|
144
|
-
# @return [Float] Average batch size
|
145
115
|
def average_batch_size
|
146
116
|
return 0.0 if @batch_count.zero?
|
147
117
|
@total_processed.to_f / @batch_count
|
@@ -150,10 +120,8 @@ module Snakommit
|
|
150
120
|
|
151
121
|
# Helper for parallel processing where appropriate
|
152
122
|
class ParallelHelper
|
153
|
-
# Check if parallel processing is available
|
154
|
-
# @return [Boolean] True if the parallel gem is available
|
155
123
|
def self.available?
|
156
|
-
begin
|
124
|
+
@available ||= begin
|
157
125
|
require 'parallel'
|
158
126
|
true
|
159
127
|
rescue LoadError
|
@@ -161,49 +129,34 @@ module Snakommit
|
|
161
129
|
end
|
162
130
|
end
|
163
131
|
|
164
|
-
# Process items in parallel if possible, otherwise sequentially
|
165
|
-
# @param items [Array] Items to process
|
166
|
-
# @param options [Hash] Options for parallel processing
|
167
|
-
# @option options [Integer] :threshold Minimum number of items to use parallel processing
|
168
|
-
# @option options [Integer] :workers Number of workers to use (defaults to processor count)
|
169
|
-
# @yield [item] Block to process each item
|
170
|
-
# @yieldparam item [Object] An item to process
|
171
|
-
# @return [Array] Results of processing all items
|
172
132
|
def self.process(items, options = {}, &block)
|
173
133
|
threshold = options.delete(:threshold) || 10
|
174
134
|
workers = options.delete(:workers) || processor_count
|
175
135
|
|
176
136
|
if available? && items.size > threshold
|
177
137
|
require 'parallel'
|
178
|
-
Parallel.map(items, { in_processes: workers }.merge(options)
|
138
|
+
Parallel.map(items, { in_processes: workers }.merge(options), &block)
|
179
139
|
else
|
180
140
|
items.map(&block)
|
181
141
|
end
|
182
142
|
end
|
183
143
|
|
184
|
-
# Get number of available processors
|
185
|
-
# @return [Integer] Number of processors available
|
186
144
|
def self.processor_count
|
187
145
|
if defined?(Etc) && Etc.respond_to?(:nprocessors)
|
188
146
|
Etc.nprocessors
|
189
147
|
else
|
190
|
-
2
|
148
|
+
2
|
191
149
|
end
|
192
150
|
end
|
193
151
|
end
|
194
152
|
|
195
153
|
# Performance monitoring and reporting
|
196
154
|
class Monitor
|
197
|
-
# Initialize a new monitor
|
198
155
|
def initialize
|
199
156
|
@timings = {}
|
200
157
|
@counts = {}
|
201
158
|
end
|
202
159
|
|
203
|
-
# Measure execution time of a block
|
204
|
-
# @param label [String, Symbol] Label for the measurement
|
205
|
-
# @yield Block to measure
|
206
|
-
# @return [Object] Result of the block
|
207
160
|
def measure(label)
|
208
161
|
start_time = Time.now
|
209
162
|
result = yield
|
@@ -218,8 +171,6 @@ module Snakommit
|
|
218
171
|
result
|
219
172
|
end
|
220
173
|
|
221
|
-
# Get a report of all timings
|
222
|
-
# @return [Array<String>] Formatted timing report lines
|
223
174
|
def report
|
224
175
|
@timings.sort_by { |_, v| -v }.map do |k, v|
|
225
176
|
count = @counts[k]
|
@@ -228,8 +179,6 @@ module Snakommit
|
|
228
179
|
end
|
229
180
|
end
|
230
181
|
|
231
|
-
# Reset all timings
|
232
|
-
# @return [nil]
|
233
182
|
def reset
|
234
183
|
@timings.clear
|
235
184
|
@counts.clear
|
@@ -239,11 +188,6 @@ module Snakommit
|
|
239
188
|
|
240
189
|
# Benchmarking utility for snakommit operations
|
241
190
|
class Benchmark
|
242
|
-
# Run a benchmark test
|
243
|
-
# @param label [String] Label for the benchmark
|
244
|
-
# @param iterations [Integer] Number of iterations to run
|
245
|
-
# @yield Block to benchmark
|
246
|
-
# @return [Hash] Benchmark results
|
247
191
|
def self.run(label, iterations = 1)
|
248
192
|
results = {}
|
249
193
|
|
@@ -262,12 +206,6 @@ module Snakommit
|
|
262
206
|
results
|
263
207
|
end
|
264
208
|
|
265
|
-
# Compare performance of multiple implementations
|
266
|
-
# @param options [Hash] Options for comparison
|
267
|
-
# @option options [Integer] :iterations Number of iterations
|
268
|
-
# @option options [Boolean] :verbose Print results
|
269
|
-
# @yield Block that returns a hash of callable objects to compare
|
270
|
-
# @return [Hash] Comparison results
|
271
209
|
def self.compare(options = {})
|
272
210
|
iterations = options[:iterations] || 100
|
273
211
|
verbose = options[:verbose] || false
|
@@ -292,25 +230,15 @@ module Snakommit
|
|
292
230
|
|
293
231
|
# Memory usage tracking
|
294
232
|
class Memory
|
295
|
-
# Get current memory usage in KB
|
296
|
-
# @return [Integer] Memory usage in KB
|
297
233
|
def self.usage
|
298
234
|
case RbConfig::CONFIG['host_os']
|
299
|
-
when /linux/
|
235
|
+
when /linux/, /darwin/
|
300
236
|
`ps -o rss= -p #{Process.pid}`.to_i
|
301
|
-
when /darwin/
|
302
|
-
`ps -o rss= -p #{Process.pid}`.to_i
|
303
|
-
when /windows|mswin|mingw/
|
304
|
-
# Not implemented for Windows
|
305
|
-
0
|
306
237
|
else
|
307
238
|
0
|
308
239
|
end
|
309
240
|
end
|
310
241
|
|
311
|
-
# Measure memory usage before and after a block execution
|
312
|
-
# @yield Block to measure
|
313
|
-
# @return [Hash] Memory usage statistics
|
314
242
|
def self.measure
|
315
243
|
before = usage
|
316
244
|
result = yield
|