commitgpt 0.3.1 → 0.3.4
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/bin/aicm +1 -1
- data/commitgpt.gemspec +19 -18
- data/lib/commitgpt/cli.rb +28 -23
- data/lib/commitgpt/commit_ai.rb +92 -210
- data/lib/commitgpt/config_manager.rb +34 -34
- data/lib/commitgpt/diff_helpers.rb +149 -0
- data/lib/commitgpt/provider_presets.rb +13 -12
- data/lib/commitgpt/setup_wizard.rb +87 -84
- data/lib/commitgpt/string.rb +19 -0
- data/lib/commitgpt/version.rb +1 -1
- data/lib/commitgpt.rb +2 -2
- metadata +7 -5
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
5
|
-
require_relative
|
|
6
|
-
require_relative
|
|
3
|
+
require 'yaml'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require_relative 'provider_presets'
|
|
6
|
+
require_relative 'string'
|
|
7
7
|
|
|
8
8
|
module CommitGpt
|
|
9
9
|
# Manages configuration files for CommitGPT
|
|
@@ -11,22 +11,22 @@ module CommitGpt
|
|
|
11
11
|
class << self
|
|
12
12
|
# Get the config directory path
|
|
13
13
|
def config_dir
|
|
14
|
-
File.expand_path(
|
|
14
|
+
File.expand_path('~/.config/commitgpt')
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
# Get main config file path
|
|
18
18
|
def main_config_path
|
|
19
|
-
File.join(config_dir,
|
|
19
|
+
File.join(config_dir, 'config.yml')
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
# Get local config file path
|
|
23
23
|
def local_config_path
|
|
24
|
-
File.join(config_dir,
|
|
24
|
+
File.join(config_dir, 'config.local.yml')
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
# Ensure config directory exists
|
|
28
28
|
def ensure_config_dir
|
|
29
|
-
FileUtils.mkdir_p(config_dir)
|
|
29
|
+
FileUtils.mkdir_p(config_dir)
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
# Check if config files exist
|
|
@@ -47,12 +47,12 @@ module CommitGpt
|
|
|
47
47
|
# Get active provider configuration
|
|
48
48
|
def get_active_provider_config
|
|
49
49
|
config = load_config
|
|
50
|
-
return nil if config.nil? || config[
|
|
50
|
+
return nil if config.nil? || config['active_provider'].nil?
|
|
51
51
|
|
|
52
|
-
active_provider = config[
|
|
53
|
-
providers = config[
|
|
52
|
+
active_provider = config['active_provider']
|
|
53
|
+
providers = config['providers'] || []
|
|
54
54
|
|
|
55
|
-
providers.find { |p| p[
|
|
55
|
+
providers.find { |p| p['name'] == active_provider }
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
# Save main config
|
|
@@ -74,36 +74,36 @@ module CommitGpt
|
|
|
74
74
|
# Generate main config with all providers but empty models
|
|
75
75
|
providers = PROVIDER_PRESETS.map do |preset|
|
|
76
76
|
{
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
77
|
+
'name' => preset[:value],
|
|
78
|
+
'model' => '',
|
|
79
|
+
'diff_len' => 32_768,
|
|
80
|
+
'base_url' => preset[:base_url]
|
|
81
81
|
}
|
|
82
82
|
end
|
|
83
83
|
|
|
84
84
|
main_config = {
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
'providers' => providers,
|
|
86
|
+
'active_provider' => ''
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
# Generate local config with empty API keys
|
|
90
90
|
local_providers = PROVIDER_PRESETS.map do |preset|
|
|
91
91
|
{
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
'name' => preset[:value],
|
|
93
|
+
'api_key' => ''
|
|
94
94
|
}
|
|
95
95
|
end
|
|
96
96
|
|
|
97
97
|
local_config = {
|
|
98
|
-
|
|
98
|
+
'providers' => local_providers
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
save_main_config(main_config)
|
|
102
102
|
save_local_config(local_config)
|
|
103
103
|
|
|
104
104
|
# Remind user to add config.local.yml to .gitignore
|
|
105
|
-
puts
|
|
106
|
-
puts
|
|
105
|
+
puts '▲ Generated default configuration files.'.green
|
|
106
|
+
puts '▲ Remember to add ~/.config/commitgpt/config.local.yml to your .gitignore'.yellow
|
|
107
107
|
end
|
|
108
108
|
|
|
109
109
|
# Get list of configured providers (with API keys)
|
|
@@ -111,25 +111,25 @@ module CommitGpt
|
|
|
111
111
|
config = load_config
|
|
112
112
|
return [] if config.nil?
|
|
113
113
|
|
|
114
|
-
providers = config[
|
|
115
|
-
providers.select { |p| p[
|
|
114
|
+
providers = config['providers'] || []
|
|
115
|
+
providers.select { |p| p['api_key'] && !p['api_key'].empty? }
|
|
116
116
|
end
|
|
117
117
|
|
|
118
118
|
# Update provider configuration
|
|
119
119
|
def update_provider(provider_name, main_attrs = {}, local_attrs = {})
|
|
120
120
|
# Update main config
|
|
121
121
|
main_config = YAML.load_file(main_config_path)
|
|
122
|
-
provider = main_config[
|
|
122
|
+
provider = main_config['providers'].find { |p| p['name'] == provider_name }
|
|
123
123
|
provider&.merge!(main_attrs)
|
|
124
124
|
save_main_config(main_config)
|
|
125
125
|
|
|
126
126
|
# Update local config
|
|
127
|
-
local_config = File.exist?(local_config_path) ? YAML.load_file(local_config_path) : {
|
|
128
|
-
local_provider = local_config[
|
|
127
|
+
local_config = File.exist?(local_config_path) ? YAML.load_file(local_config_path) : { 'providers' => [] }
|
|
128
|
+
local_provider = local_config['providers'].find { |p| p['name'] == provider_name }
|
|
129
129
|
if local_provider
|
|
130
130
|
local_provider.merge!(local_attrs)
|
|
131
131
|
else
|
|
132
|
-
local_config[
|
|
132
|
+
local_config['providers'] << { 'name' => provider_name }.merge(local_attrs)
|
|
133
133
|
end
|
|
134
134
|
save_local_config(local_config)
|
|
135
135
|
end
|
|
@@ -137,7 +137,7 @@ module CommitGpt
|
|
|
137
137
|
# Set active provider
|
|
138
138
|
def set_active_provider(provider_name)
|
|
139
139
|
main_config = YAML.load_file(main_config_path)
|
|
140
|
-
main_config[
|
|
140
|
+
main_config['active_provider'] = provider_name
|
|
141
141
|
save_main_config(main_config)
|
|
142
142
|
end
|
|
143
143
|
|
|
@@ -148,15 +148,15 @@ module CommitGpt
|
|
|
148
148
|
result = main_config.dup
|
|
149
149
|
|
|
150
150
|
# Merge provider-specific settings
|
|
151
|
-
main_providers = main_config[
|
|
152
|
-
local_providers = local_config[
|
|
151
|
+
main_providers = main_config['providers'] || []
|
|
152
|
+
local_providers = local_config['providers'] || []
|
|
153
153
|
|
|
154
154
|
merged_providers = main_providers.map do |main_provider|
|
|
155
|
-
local_provider = local_providers.find { |lp| lp[
|
|
155
|
+
local_provider = local_providers.find { |lp| lp['name'] == main_provider['name'] }
|
|
156
156
|
local_provider ? main_provider.merge(local_provider) : main_provider
|
|
157
157
|
end
|
|
158
158
|
|
|
159
|
-
result[
|
|
159
|
+
result['providers'] = merged_providers
|
|
160
160
|
result
|
|
161
161
|
end
|
|
162
162
|
end
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommitGpt
|
|
4
|
+
# Helper methods for handling git diffs
|
|
5
|
+
# rubocop:disable Metrics/ModuleLength
|
|
6
|
+
module DiffHelpers
|
|
7
|
+
# Lock files to exclude from diff but detect changes
|
|
8
|
+
LOCK_FILES = %w[Gemfile.lock package-lock.json yarn.lock pnpm-lock.yaml].freeze
|
|
9
|
+
|
|
10
|
+
def git_diff
|
|
11
|
+
exclusions = LOCK_FILES.map { |f| "\":(exclude)#{f}\"" }.join(' ')
|
|
12
|
+
diff_cached = `git diff --cached . #{exclusions}`.chomp
|
|
13
|
+
diff_unstaged = `git diff . #{exclusions}`.chomp
|
|
14
|
+
|
|
15
|
+
# Detect lock file changes and build summary
|
|
16
|
+
@lock_file_summary = detect_lock_file_changes
|
|
17
|
+
|
|
18
|
+
if !diff_unstaged.empty?
|
|
19
|
+
if diff_cached.empty?
|
|
20
|
+
# Scenario: Only unstaged changes
|
|
21
|
+
choice = prompt_no_staged_changes
|
|
22
|
+
case choice
|
|
23
|
+
when :add_all
|
|
24
|
+
puts '▲ Running git add .'.yellow
|
|
25
|
+
system('git add .')
|
|
26
|
+
diff_cached = `git diff --cached . #{exclusions}`.chomp
|
|
27
|
+
if diff_cached.empty?
|
|
28
|
+
puts '▲ Still no changes to commit.'.red
|
|
29
|
+
return nil
|
|
30
|
+
end
|
|
31
|
+
when :exit
|
|
32
|
+
return nil
|
|
33
|
+
end
|
|
34
|
+
else
|
|
35
|
+
# Scenario: Mixed state (some staged, some not)
|
|
36
|
+
puts '▲ You have both staged and unstaged changes:'.yellow
|
|
37
|
+
|
|
38
|
+
staged_files = `git diff --cached --name-status . #{exclusions}`.chomp
|
|
39
|
+
unstaged_files = `git diff --name-status . #{exclusions}`.chomp
|
|
40
|
+
|
|
41
|
+
puts "\n #{'Staged changes:'.green}"
|
|
42
|
+
puts staged_files.gsub(/^/, ' ')
|
|
43
|
+
|
|
44
|
+
puts "\n #{'Unstaged changes:'.red}"
|
|
45
|
+
puts unstaged_files.gsub(/^/, ' ')
|
|
46
|
+
puts ''
|
|
47
|
+
|
|
48
|
+
prompt = TTY::Prompt.new
|
|
49
|
+
choice = prompt.select('How to proceed?') do |menu|
|
|
50
|
+
menu.choice 'Include unstaged changes (git add .)', :add_all
|
|
51
|
+
menu.choice 'Use staged changes only', :staged_only
|
|
52
|
+
menu.choice 'Exit', :exit
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
case choice
|
|
56
|
+
when :add_all
|
|
57
|
+
puts '▲ Running git add .'.yellow
|
|
58
|
+
system('git add .')
|
|
59
|
+
diff_cached = `git diff --cached . #{exclusions}`.chomp
|
|
60
|
+
when :exit
|
|
61
|
+
return nil
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
elsif diff_cached.empty?
|
|
65
|
+
# Scenario: No changes at all (staged or unstaged)
|
|
66
|
+
# Check if there are ANY unstaged files (maybe untracked?)
|
|
67
|
+
# git status --porcelain includes untracked files
|
|
68
|
+
git_status = `git status --porcelain`.chomp
|
|
69
|
+
if git_status.empty?
|
|
70
|
+
puts '▲ No changes to commit. Working tree clean.'.yellow
|
|
71
|
+
return nil
|
|
72
|
+
else
|
|
73
|
+
# Only untracked files? Or ignored files?
|
|
74
|
+
# If diff_unstaged is empty but git status is not, it usually means untracked files.
|
|
75
|
+
# Let's offer to add them too.
|
|
76
|
+
choice = prompt_no_staged_changes
|
|
77
|
+
case choice
|
|
78
|
+
when :add_all
|
|
79
|
+
puts '▲ Running git add .'.yellow
|
|
80
|
+
system('git add .')
|
|
81
|
+
diff_cached = `git diff --cached . #{exclusions}`.chomp
|
|
82
|
+
when :exit
|
|
83
|
+
return nil
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
diff = diff_cached
|
|
89
|
+
|
|
90
|
+
# Prepend lock file summary to diff if present
|
|
91
|
+
diff = "#{@lock_file_summary}\n\n#{diff}" if @lock_file_summary
|
|
92
|
+
|
|
93
|
+
if diff.length > diff_len
|
|
94
|
+
choice = prompt_diff_handling(diff.length, diff_len)
|
|
95
|
+
case choice
|
|
96
|
+
when :truncate
|
|
97
|
+
puts "▲ Truncating diff to #{diff_len} chars...".yellow
|
|
98
|
+
diff = diff[0...diff_len]
|
|
99
|
+
when :unlimited
|
|
100
|
+
puts "▲ Using full diff (#{diff.length} chars)...".yellow
|
|
101
|
+
when :exit
|
|
102
|
+
return nil
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
diff
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def prompt_no_staged_changes
|
|
110
|
+
puts '▲ No staged changes found (but unstaged/untracked files exist).'.yellow
|
|
111
|
+
prompt = TTY::Prompt.new
|
|
112
|
+
begin
|
|
113
|
+
prompt.select('Choose an option:') do |menu|
|
|
114
|
+
menu.choice "Run 'git add .' to stage all changes", :add_all
|
|
115
|
+
menu.choice 'Exit (stage files manually)', :exit
|
|
116
|
+
end
|
|
117
|
+
rescue TTY::Reader::InputInterrupt, Interrupt
|
|
118
|
+
:exit
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def prompt_diff_handling(current_len, max_len)
|
|
123
|
+
puts "▲ The diff is too large (#{current_len} chars, max #{max_len}).".yellow
|
|
124
|
+
prompt = TTY::Prompt.new
|
|
125
|
+
begin
|
|
126
|
+
prompt.select('Choose an option:') do |menu|
|
|
127
|
+
menu.choice "Use first #{max_len} characters to generate commit message", :truncate
|
|
128
|
+
menu.choice 'Use unlimited characters (may fail or be slow)', :unlimited
|
|
129
|
+
menu.choice 'Exit', :exit
|
|
130
|
+
end
|
|
131
|
+
rescue TTY::Reader::InputInterrupt, Interrupt
|
|
132
|
+
:exit
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def detect_lock_file_changes
|
|
137
|
+
# Check both staged and unstaged changes for lock files
|
|
138
|
+
staged_files = `git diff --cached --name-only`.chomp.split("\n")
|
|
139
|
+
unstaged_files = `git diff --name-only`.chomp.split("\n")
|
|
140
|
+
changed_files = (staged_files + unstaged_files).uniq
|
|
141
|
+
|
|
142
|
+
updated_locks = LOCK_FILES.select { |lock| changed_files.include?(lock) }
|
|
143
|
+
return nil if updated_locks.empty?
|
|
144
|
+
|
|
145
|
+
updated_locks.map { |f| "#{f} updated (dependency changes)" }.join(', ')
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
# rubocop:enable Metrics/ModuleLength
|
|
149
|
+
end
|
|
@@ -3,17 +3,18 @@
|
|
|
3
3
|
module CommitGpt
|
|
4
4
|
# Provider presets for common AI providers
|
|
5
5
|
PROVIDER_PRESETS = [
|
|
6
|
-
{ label:
|
|
7
|
-
{ label:
|
|
8
|
-
{ label:
|
|
9
|
-
{ label:
|
|
10
|
-
{ label:
|
|
11
|
-
{ label:
|
|
12
|
-
{ label:
|
|
13
|
-
{ label:
|
|
14
|
-
{ label:
|
|
15
|
-
{ label:
|
|
16
|
-
{ label:
|
|
17
|
-
{ label:
|
|
6
|
+
{ label: 'Anthropic (Claude)', value: 'anthropic', base_url: 'https://api.anthropic.com/v1' },
|
|
7
|
+
{ label: 'Cerebras', value: 'cerebras', base_url: 'https://api.cerebras.ai/v1' },
|
|
8
|
+
{ label: 'DeepSeek', value: 'deepseek', base_url: 'https://api.deepseek.com' },
|
|
9
|
+
{ label: 'Google AI', value: 'gemini', base_url: 'https://generativelanguage.googleapis.com/v1beta/openai' },
|
|
10
|
+
{ label: 'Groq', value: 'groq', base_url: 'https://api.groq.com/openai/v1' },
|
|
11
|
+
{ label: 'LLaMa.cpp', value: 'llamacpp', base_url: 'http://127.0.0.1:8080/v1' },
|
|
12
|
+
{ label: 'LM Studio', value: 'lmstudio', base_url: 'http://127.0.0.1:1234/v1' },
|
|
13
|
+
{ label: 'Llamafile', value: 'llamafile', base_url: 'http://127.0.0.1:8080/v1' },
|
|
14
|
+
{ label: 'Mistral', value: 'mistral', base_url: 'https://api.mistral.ai/v1' },
|
|
15
|
+
{ label: 'NVIDIA NIM', value: 'nvidia_nim', base_url: 'https://integrate.api.nvidia.com/v1' },
|
|
16
|
+
{ label: 'Ollama', value: 'ollama', base_url: 'http://127.0.0.1:11434/v1' },
|
|
17
|
+
{ label: 'OpenAI', value: 'openai', base_url: 'https://api.openai.com/v1' },
|
|
18
|
+
{ label: 'OpenRouter', value: 'openrouter', base_url: 'https://openrouter.ai/api/v1' }
|
|
18
19
|
].freeze
|
|
19
20
|
end
|