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.
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "yaml"
4
- require "fileutils"
5
- require_relative "provider_presets"
6
- require_relative "string"
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("~/.config/commitgpt")
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, "config.yml")
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, "config.local.yml")
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) unless Dir.exist?(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["active_provider"].nil?
50
+ return nil if config.nil? || config['active_provider'].nil?
51
51
 
52
- active_provider = config["active_provider"]
53
- providers = config["providers"] || []
52
+ active_provider = config['active_provider']
53
+ providers = config['providers'] || []
54
54
 
55
- providers.find { |p| p["name"] == active_provider }
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
- "name" => preset[:value],
78
- "model" => "",
79
- "diff_len" => 32768,
80
- "base_url" => preset[:base_url]
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
- "providers" => providers,
86
- "active_provider" => ""
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
- "name" => preset[:value],
93
- "api_key" => ""
92
+ 'name' => preset[:value],
93
+ 'api_key' => ''
94
94
  }
95
95
  end
96
96
 
97
97
  local_config = {
98
- "providers" => local_providers
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 "▲ Generated default configuration files.".green
106
- puts "▲ Remember to add ~/.config/commitgpt/config.local.yml to your .gitignore".yellow
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["providers"] || []
115
- providers.select { |p| p["api_key"] && !p["api_key"].empty? }
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["providers"].find { |p| p["name"] == provider_name }
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) : { "providers" => [] }
128
- local_provider = local_config["providers"].find { |p| p["name"] == provider_name }
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["providers"] << { "name" => provider_name }.merge(local_attrs)
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["active_provider"] = provider_name
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["providers"] || []
152
- local_providers = local_config["providers"] || []
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["name"] == main_provider["name"] }
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["providers"] = merged_providers
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: "Cerebras", value: "cerebras", base_url: "https://api.cerebras.ai/v1" },
7
- { label: "Ollama", value: "ollama", base_url: "http://127.0.0.1:11434/v1" },
8
- { label: "OpenAI", value: "openai", base_url: "https://api.openai.com/v1" },
9
- { label: "LLaMa.cpp", value: "llamacpp", base_url: "http://127.0.0.1:8080/v1" },
10
- { label: "LM Studio", value: "lmstudio", base_url: "http://127.0.0.1:1234/v1" },
11
- { label: "Llamafile", value: "llamafile", base_url: "http://127.0.0.1:8080/v1" },
12
- { label: "DeepSeek", value: "deepseek", base_url: "https://api.deepseek.com" },
13
- { label: "Groq", value: "groq", base_url: "https://api.groq.com/openai/v1" },
14
- { label: "Mistral", value: "mistral", base_url: "https://api.mistral.ai/v1" },
15
- { label: "Anthropic (Claude)", value: "anthropic", base_url: "https://api.anthropic.com/v1" },
16
- { label: "OpenRouter", value: "openrouter", base_url: "https://openrouter.ai/api/v1" },
17
- { label: "Google AI", value: "gemini", base_url: "https://generativelanguage.googleapis.com/v1beta/openai" }
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