Class: CommitGpt::CommitAi
- Inherits:
-
Object
- Object
- CommitGpt::CommitAi
- Includes:
- DiffHelpers
- Defined in:
- lib/commitgpt/commit_ai.rb
Overview
Commit AI roboter based on GPT-3
Constant Summary collapse
- COMMIT_FORMATS =
Commit format templates
{ 'simple' => '<commit message>', 'conventional' => '<type>[optional (<scope>)]: <commit message>', 'gitmoji' => ':emoji: <commit message>' }.freeze
- DETAILED_EXAMPLE =
Worked example of the detailed shape. Models follow a sample far more reliably than a description of one.
<<~EXAMPLE Here is an example of the expected output, showing the exact shape only. Your subject line must still follow the format above: feat(dfb): store and expand DFB event and speed logs Persist DFB device logs ingested from the proactive evt (event log) and lge (speed stats) topics, one record per message. - Add dfb_event_logs and dfb_speed_stats tables and models, each with an idempotent unique index so re-delivery and overlapping backfills are safe. - Add the canonical event-code table and the loader that expands a device event byte into a human label and tags; unknown codes fall back to "Unknown Event". - Associate both log tables on Product. - Update README_DFB.md to document the new cloud tables. EXAMPLE
- CONVENTIONAL_TYPES =
Conventional commit types based on aicommits implementation
{ 'docs' => 'Documentation only changes', 'style' => 'Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)', 'refactor' => 'A code change that improves code structure without changing functionality (renaming, restructuring classes/methods, extracting functions, etc)', 'perf' => 'A code change that improves performance', 'test' => 'Adding missing tests or correcting existing tests', 'build' => 'Changes that affect the build system or external dependencies', 'ci' => 'Changes to our CI configuration files and scripts', 'chore' => "Other changes that don't modify src or test files", 'revert' => 'Reverts a previous commit', 'feat' => 'A new feature', 'fix' => 'A bug fix' }.freeze
- GITMOJI_TYPES =
Gitmoji mappings based on gitmoji.dev
{ '๐จ' => 'Improve structure / format of the code', 'โก' => 'Improve performance', '๐ฅ' => 'Remove code or files', '๐' => 'Fix a bug', '๐' => 'Critical hotfix', 'โจ' => 'Introduce new features', '๐' => 'Add or update documentation', '๐' => 'Deploy stuff', '๐' => 'Add or update the UI and style files', '๐' => 'Begin a project', 'โ ' => 'Add, update, or pass tests', '๐' => 'Fix security or privacy issues', '๐' => 'Add or update secrets', '๐' => 'Release / Version tags', '๐จ' => 'Fix compiler / linter warnings', '๐ง' => 'Work in progress', '๐' => 'Fix CI Build', 'โฌ๏ธ' => 'Downgrade dependencies', 'โฌ๏ธ' => 'Upgrade dependencies', '๐' => 'Pin dependencies to specific versions', '๐ท' => 'Add or update CI build system', '๐' => 'Add or update analytics or track code', 'โป๏ธ' => 'Refactor code', 'โ' => 'Add a dependency', 'โ' => 'Remove a dependency', '๐ง' => 'Add or update configuration files', '๐จ' => 'Add or update development scripts', '๐' => 'Internationalization and localization', 'โ๏ธ' => 'Fix typos', '๐ฉ' => 'Write bad code that needs to be improved', 'โช' => 'Revert changes', '๐' => 'Merge branches', '๐ฆ' => 'Add or update compiled files or packages', '๐ฝ' => 'Update code due to external API changes', '๐' => 'Move or rename resources (e.g.: files, paths, routes)', '๐' => 'Add or update license', '๐ฅ' => 'Introduce breaking changes', '๐ฑ' => 'Add or update assets', 'โฟ' => 'Improve accessibility', '๐ก' => 'Add or update comments in source code', '๐ป' => 'Write code drunkenly', '๐ฌ' => 'Add or update text and literals', '๐' => 'Perform database related changes', '๐' => 'Add or update logs', '๐' => 'Remove logs', '๐ฅ' => 'Add or update contributor(s)', '๐ธ' => 'Improve user experience / usability', '๐' => 'Make architectural changes', '๐ฑ' => 'Work on responsive design', '๐คก' => 'Mock things', '๐ฅ' => 'Add or update an easter egg', '๐' => 'Add or update a .gitignore file', '๐ธ' => 'Add or update snapshots', 'โ' => 'Perform experiments', '๐' => 'Improve SEO', '๐ท' => 'Add or update types', '๐ฑ' => 'Add or update seed files', '๐ฉ' => 'Add, update, or remove feature flags', '๐ฅ ' => 'Catch errors', '๐ซ' => 'Add or update animations and transitions', '๐' => 'Deprecate code that needs to be cleaned up', '๐' => 'Work on code related to authorization, roles and permissions', '๐ฉน' => 'Simple fix for a non-critical issue', '๐ง' => 'Data exploration/inspection', 'โฐ' => 'Remove dead code', '๐งช' => 'Add a failing test', '๐' => 'Add or update business logic', '๐ฉบ' => 'Add or update healthcheck', '๐งฑ' => 'Infrastructure related changes', '๐งโ๐ป' => 'Improve developer experience', '๐ธ' => 'Add sponsorships or money related infrastructure', '๐งต' => 'Add or update code related to multithreading or concurrency', '๐ฆบ' => 'Add or update code related to validation' }.freeze
Constants included from DiffHelpers
Instance Attribute Summary collapse
-
#api_key ⇒ Object
readonly
Returns the value of attribute api_key.
-
#base_url ⇒ Object
readonly
Returns the value of attribute base_url.
-
#commit_format ⇒ Object
readonly
Returns the value of attribute commit_format.
-
#diff_len ⇒ Object
readonly
Returns the value of attribute diff_len.
-
#model ⇒ Object
readonly
Returns the value of attribute model.
Instance Method Summary collapse
- #aicm(verbose: false) ⇒ Object
-
#content_prefix ⇒ Object
Header printed before the streamed message starts arriving.
-
#content_suffix ⇒ Object
Closing quote that matches content_prefix; a body has no quote to close.
-
#detailed? ⇒ Boolean
True when the message should carry a body, not just a subject line.
-
#finalize_message(full_content) ⇒ Object
Trim the model output down to what git should receive.
-
#git_editor ⇒ Object
Editor git will actually use, resolved through its own precedence chain.
-
#initialize ⇒ CommitAi
constructor
A new instance of CommitAi.
- #list_models ⇒ Object
-
#max_content_chars ⇒ Object
Streaming cutoff.
- #reasoning_disabled_max_tokens ⇒ Object
-
#strip_code_fence(text) ⇒ Object
Models like to wrap the whole message in a fence despite being told not to.
-
#strip_reasoning(content) ⇒ Object
Some models stream their chain of thought inside content instead of the reasoning_content field, either wrapped in
... or with only the closing tag when the chat template pre-fills the opener.
Methods included from DiffHelpers
#detect_lock_file_changes, #git_diff, #prompt_diff_handling, #prompt_no_staged_changes, #split_diff_by_length
Constructor Details
#initialize ⇒ CommitAi
Returns a new instance of CommitAi.
140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 |
# File 'lib/commitgpt/commit_ai.rb', line 140 def initialize provider_config = ConfigManager.get_active_provider_config if provider_config @api_key = provider_config['api_key'] @base_url = provider_config['base_url'] @model = provider_config['model'] @diff_len = provider_config['diff_len'] || 32_768 else @api_key = nil @base_url = nil @model = nil @diff_len = 32_768 end @commit_format = ConfigManager.get_commit_format @commit_detail = ConfigManager.get_commit_detail end |
Instance Attribute Details
#api_key ⇒ Object (readonly)
Returns the value of attribute api_key.
19 20 21 |
# File 'lib/commitgpt/commit_ai.rb', line 19 def api_key @api_key end |
#base_url ⇒ Object (readonly)
Returns the value of attribute base_url.
19 20 21 |
# File 'lib/commitgpt/commit_ai.rb', line 19 def base_url @base_url end |
#commit_format ⇒ Object (readonly)
Returns the value of attribute commit_format.
19 20 21 |
# File 'lib/commitgpt/commit_ai.rb', line 19 def commit_format @commit_format end |
#diff_len ⇒ Object (readonly)
Returns the value of attribute diff_len.
19 20 21 |
# File 'lib/commitgpt/commit_ai.rb', line 19 def diff_len @diff_len end |
#model ⇒ Object (readonly)
Returns the value of attribute model.
19 20 21 |
# File 'lib/commitgpt/commit_ai.rb', line 19 def model @model end |
Instance Method Details
#aicm(verbose: false) ⇒ Object
224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 |
# File 'lib/commitgpt/commit_ai.rb', line 224 def aicm(verbose: false) exit(1) unless welcome diff = git_diff || exit(1) if verbose puts "โ Git diff (#{diff.length} chars):".cyan puts diff puts "\n" end loop do = (diff) || exit(1) action = confirm_commit() case action when :commit # Pass argv directly: a body with quotes, backticks or newlines must # not be re-interpreted by the shell puts "\nโ Executing: git commit -m <message>".yellow system('git', 'commit', '-m', ) puts "\n\n" puts `git log -1` break when :regenerate puts "\n" next when :edit # Let git open the message in the user's editor: it honours the # GIT_EDITOR/core.editor/VISUAL/EDITOR chain, handles multi-line # bodies, and aborts by itself if the message is left empty puts "\nโ Opening #{git_editor} with the generated message...".yellow if system('git', 'commit', '-e', '-m', ) puts "\n" puts `git log -1` else puts 'โ Commit aborted.'.red end break when :exit puts 'โ Exit without commit.'.yellow break end end end |
#content_prefix ⇒ Object
Header printed before the streamed message starts arriving
186 187 188 |
# File 'lib/commitgpt/commit_ai.rb', line 186 def content_prefix detailed? ? "โฆ Commit message:\n" : 'โฆ Commit message: git commit -am "' end |
#content_suffix ⇒ Object
Closing quote that matches content_prefix; a body has no quote to close
191 192 193 |
# File 'lib/commitgpt/commit_ai.rb', line 191 def content_suffix detailed? ? '' : '"' end |
#detailed? ⇒ Boolean
True when the message should carry a body, not just a subject line
160 161 162 |
# File 'lib/commitgpt/commit_ai.rb', line 160 def detailed? @commit_detail == 'detailed' end |
#finalize_message(full_content) ⇒ Object
Trim the model output down to what git should receive
209 210 211 212 213 214 215 216 217 218 219 220 221 222 |
# File 'lib/commitgpt/commit_ai.rb', line 209 def (full_content) content = strip_reasoning(full_content) unless detailed? # Take only the first non-empty line to avoid repetition or multi-line garbage first_line = content.split("\n").map(&:strip).reject(&:empty?).first return first_line&.gsub(/\A["']|["']\z/, '') || '' end body = strip_code_fence(content.strip).gsub(/\A["']|["']\z/, '').strip # Keep the subject/body/bullets structure but drop trailing spaces and # collapse the extra blank lines models like to emit between sections body.split("\n").map(&:rstrip).join("\n").gsub(/\n{3,}/, "\n\n") end |
#git_editor ⇒ Object
Editor git will actually use, resolved through its own precedence chain
196 197 198 199 |
# File 'lib/commitgpt/commit_ai.rb', line 196 def git_editor editor = `git var GIT_EDITOR 2>/dev/null`.strip editor.empty? ? 'your editor' : editor end |
#list_models ⇒ Object
268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 |
# File 'lib/commitgpt/commit_ai.rb', line 268 def list_models headers = { 'Content-Type' => 'application/json', 'User-Agent' => "Ruby/#{RUBY_VERSION}" } headers['Authorization'] = "Bearer #{@api_key}" if @api_key begin response = HTTParty.get("#{@base_url}/models", headers: headers) models = response['data'] || [] models.each { |m| puts m['id'] } rescue StandardError => e puts "โ Failed to list models: #{e.}".red end end |
#max_content_chars ⇒ Object
Streaming cutoff. Generous because models that ignore disable_reasoning
spend it on inline
166 167 168 |
# File 'lib/commitgpt/commit_ai.rb', line 166 def max_content_chars detailed? ? 6000 : 1500 end |
#reasoning_disabled_max_tokens ⇒ Object
170 171 172 |
# File 'lib/commitgpt/commit_ai.rb', line 170 def reasoning_disabled_max_tokens detailed? ? 1200 : 300 end |
#strip_code_fence(text) ⇒ Object
Models like to wrap the whole message in a fence despite being told not to
202 203 204 205 206 |
# File 'lib/commitgpt/commit_ai.rb', line 202 def strip_code_fence(text) return text unless text.start_with?('```') text.sub(/\A```[a-zA-Z]*\n?/, '').sub(/\n?```\s*\z/, '') end |
#strip_reasoning(content) ⇒ Object
Some models stream their chain of thought inside content instead of the
reasoning_content field, either wrapped in
178 179 180 181 182 183 |
# File 'lib/commitgpt/commit_ai.rb', line 178 def strip_reasoning(content) text = content.gsub(%r{<think(?:ing)?>.*?</think(?:ing)?>}m, '') text = text.rpartition(%r{</think(?:ing)?>}).last if text.match?(%r{</think(?:ing)?>}) # Opener with no closer: the model never came back from thinking text.sub(/<think(?:ing)?>.*\z/m, '') end |