git_auto 0.1.0
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/CHANGELOG.md +5 -0
 - data/LICENSE.txt +21 -0
 - data/README.md +113 -0
 - data/exe/git_auto +7 -0
 - data/lib/git_auto/cli.rb +79 -0
 - data/lib/git_auto/commands/commit_message_command.rb +315 -0
 - data/lib/git_auto/commands/config_command.rb +175 -0
 - data/lib/git_auto/commands/history_analysis_command.rb +87 -0
 - data/lib/git_auto/commands/setup_command.rb +113 -0
 - data/lib/git_auto/config/credential_store.rb +73 -0
 - data/lib/git_auto/config/settings.rb +95 -0
 - data/lib/git_auto/errors.rb +12 -0
 - data/lib/git_auto/formatters/diff_formatter.rb +49 -0
 - data/lib/git_auto/formatters/diff_summarizer.rb +99 -0
 - data/lib/git_auto/formatters/message_formatter.rb +53 -0
 - data/lib/git_auto/services/ai_service.rb +395 -0
 - data/lib/git_auto/services/git_service.rb +115 -0
 - data/lib/git_auto/services/history_service.rb +150 -0
 - data/lib/git_auto/validators/commit_message_validator.rb +89 -0
 - data/lib/git_auto/version.rb +5 -0
 - data/lib/git_auto.rb +52 -0
 - metadata +268 -0
 
| 
         @@ -0,0 +1,175 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require "tty-prompt"
         
     | 
| 
      
 4 
     | 
    
         
            +
            require "colorize"
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
            module GitAuto
         
     | 
| 
      
 7 
     | 
    
         
            +
              module Commands
         
     | 
| 
      
 8 
     | 
    
         
            +
                class ConfigCommand
         
     | 
| 
      
 9 
     | 
    
         
            +
                  def initialize
         
     | 
| 
      
 10 
     | 
    
         
            +
                    @prompt = TTY::Prompt.new
         
     | 
| 
      
 11 
     | 
    
         
            +
                    @credential_store = Config::CredentialStore.new
         
     | 
| 
      
 12 
     | 
    
         
            +
                    @settings = Config::Settings.new
         
     | 
| 
      
 13 
     | 
    
         
            +
                  end
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
                  def execute(args = [])
         
     | 
| 
      
 16 
     | 
    
         
            +
                    if args.empty?
         
     | 
| 
      
 17 
     | 
    
         
            +
                      interactive_config
         
     | 
| 
      
 18 
     | 
    
         
            +
                    else
         
     | 
| 
      
 19 
     | 
    
         
            +
                      handle_config_args(args)
         
     | 
| 
      
 20 
     | 
    
         
            +
                    end
         
     | 
| 
      
 21 
     | 
    
         
            +
                  end
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
                  private
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
                  def handle_config_args(args)
         
     | 
| 
      
 26 
     | 
    
         
            +
                    case args[0]
         
     | 
| 
      
 27 
     | 
    
         
            +
                    when "get"
         
     | 
| 
      
 28 
     | 
    
         
            +
                      get_setting(args[1])
         
     | 
| 
      
 29 
     | 
    
         
            +
                    when "set"
         
     | 
| 
      
 30 
     | 
    
         
            +
                      set_setting(args[1], args[2])
         
     | 
| 
      
 31 
     | 
    
         
            +
                    else
         
     | 
| 
      
 32 
     | 
    
         
            +
                      puts "❌ Unknown command: #{args[0]}".red
         
     | 
| 
      
 33 
     | 
    
         
            +
                      puts "Usage: git_auto config [get|set] <key> [value]"
         
     | 
| 
      
 34 
     | 
    
         
            +
                      exit 1
         
     | 
| 
      
 35 
     | 
    
         
            +
                    end
         
     | 
| 
      
 36 
     | 
    
         
            +
                  end
         
     | 
| 
      
 37 
     | 
    
         
            +
             
     | 
| 
      
 38 
     | 
    
         
            +
                  def get_setting(key)
         
     | 
| 
      
 39 
     | 
    
         
            +
                    if key.nil?
         
     | 
| 
      
 40 
     | 
    
         
            +
                      puts "❌ Missing key".red
         
     | 
| 
      
 41 
     | 
    
         
            +
                      puts "Usage: git_auto config get <key>"
         
     | 
| 
      
 42 
     | 
    
         
            +
                      exit 1
         
     | 
| 
      
 43 
     | 
    
         
            +
                    end
         
     | 
| 
      
 44 
     | 
    
         
            +
             
     | 
| 
      
 45 
     | 
    
         
            +
                    value = @settings.get(key.to_sym)
         
     | 
| 
      
 46 
     | 
    
         
            +
                    if value.nil?
         
     | 
| 
      
 47 
     | 
    
         
            +
                      puts "❌ Setting '#{key}' not found".red
         
     | 
| 
      
 48 
     | 
    
         
            +
                      exit 1
         
     | 
| 
      
 49 
     | 
    
         
            +
                    end
         
     | 
| 
      
 50 
     | 
    
         
            +
             
     | 
| 
      
 51 
     | 
    
         
            +
                    puts value
         
     | 
| 
      
 52 
     | 
    
         
            +
                  end
         
     | 
| 
      
 53 
     | 
    
         
            +
             
     | 
| 
      
 54 
     | 
    
         
            +
                  def set_setting(key, value)
         
     | 
| 
      
 55 
     | 
    
         
            +
                    if key.nil? || value.nil?
         
     | 
| 
      
 56 
     | 
    
         
            +
                      puts "❌ Missing key or value".red
         
     | 
| 
      
 57 
     | 
    
         
            +
                      puts "Usage: git_auto config set <key> <value>"
         
     | 
| 
      
 58 
     | 
    
         
            +
                      exit 1
         
     | 
| 
      
 59 
     | 
    
         
            +
                    end
         
     | 
| 
      
 60 
     | 
    
         
            +
             
     | 
| 
      
 61 
     | 
    
         
            +
                    @settings.set(key.to_sym, value)
         
     | 
| 
      
 62 
     | 
    
         
            +
                    puts "✓ Setting '#{key}' updated to '#{value}'".green
         
     | 
| 
      
 63 
     | 
    
         
            +
                  end
         
     | 
| 
      
 64 
     | 
    
         
            +
             
     | 
| 
      
 65 
     | 
    
         
            +
                  def interactive_config
         
     | 
| 
      
 66 
     | 
    
         
            +
                    puts "\n⚙️  GitAuto Configuration".blue
         
     | 
| 
      
 67 
     | 
    
         
            +
             
     | 
| 
      
 68 
     | 
    
         
            +
                    loop do
         
     | 
| 
      
 69 
     | 
    
         
            +
                      choice = main_menu
         
     | 
| 
      
 70 
     | 
    
         
            +
                      break if choice == "exit"
         
     | 
| 
      
 71 
     | 
    
         
            +
             
     | 
| 
      
 72 
     | 
    
         
            +
                      case choice
         
     | 
| 
      
 73 
     | 
    
         
            +
                      when "show"
         
     | 
| 
      
 74 
     | 
    
         
            +
                        display_configuration
         
     | 
| 
      
 75 
     | 
    
         
            +
                      when "provider"
         
     | 
| 
      
 76 
     | 
    
         
            +
                        configure_ai_provider
         
     | 
| 
      
 77 
     | 
    
         
            +
                      when "model"
         
     | 
| 
      
 78 
     | 
    
         
            +
                        configure_ai_model
         
     | 
| 
      
 79 
     | 
    
         
            +
                      when "api_key"
         
     | 
| 
      
 80 
     | 
    
         
            +
                        configure_api_key
         
     | 
| 
      
 81 
     | 
    
         
            +
                      when "style"
         
     | 
| 
      
 82 
     | 
    
         
            +
                        configure_commit_style
         
     | 
| 
      
 83 
     | 
    
         
            +
                      when "preferences"
         
     | 
| 
      
 84 
     | 
    
         
            +
                        configure_preferences
         
     | 
| 
      
 85 
     | 
    
         
            +
                      when "history"
         
     | 
| 
      
 86 
     | 
    
         
            +
                        configure_history_settings
         
     | 
| 
      
 87 
     | 
    
         
            +
                      end
         
     | 
| 
      
 88 
     | 
    
         
            +
                    end
         
     | 
| 
      
 89 
     | 
    
         
            +
                  end
         
     | 
| 
      
 90 
     | 
    
         
            +
             
     | 
| 
      
 91 
     | 
    
         
            +
                  def main_menu
         
     | 
| 
      
 92 
     | 
    
         
            +
                    @prompt.select("Choose an option:", {
         
     | 
| 
      
 93 
     | 
    
         
            +
                                     "📊 Show current configuration" => "show",
         
     | 
| 
      
 94 
     | 
    
         
            +
                                     "🤖 Configure AI provider" => "provider",
         
     | 
| 
      
 95 
     | 
    
         
            +
                                     "🔧 Configure AI model" => "model",
         
     | 
| 
      
 96 
     | 
    
         
            +
                                     "🔑 Configure API key" => "api_key",
         
     | 
| 
      
 97 
     | 
    
         
            +
                                     "💫 Configure commit style" => "style",
         
     | 
| 
      
 98 
     | 
    
         
            +
                                     "⚙️  Configure preferences" => "preferences",
         
     | 
| 
      
 99 
     | 
    
         
            +
                                     "📜 Configure history settings" => "history",
         
     | 
| 
      
 100 
     | 
    
         
            +
                                     "❌ Exit" => "exit"
         
     | 
| 
      
 101 
     | 
    
         
            +
                                   })
         
     | 
| 
      
 102 
     | 
    
         
            +
                  end
         
     | 
| 
      
 103 
     | 
    
         
            +
             
     | 
| 
      
 104 
     | 
    
         
            +
                  def display_configuration
         
     | 
| 
      
 105 
     | 
    
         
            +
                    puts "\nCurrent Configuration:".blue
         
     | 
| 
      
 106 
     | 
    
         
            +
                    puts "AI Provider: #{@settings.get(:ai_provider)}"
         
     | 
| 
      
 107 
     | 
    
         
            +
                    puts "AI Model: #{@settings.get(:ai_model)}"
         
     | 
| 
      
 108 
     | 
    
         
            +
                    puts "Commit Style: #{@settings.get(:commit_style)}"
         
     | 
| 
      
 109 
     | 
    
         
            +
                    puts "Show Diff: #{@settings.get(:show_diff)}"
         
     | 
| 
      
 110 
     | 
    
         
            +
                    puts "Save History: #{@settings.get(:save_history)}"
         
     | 
| 
      
 111 
     | 
    
         
            +
                    puts "\nPress any key to continue..."
         
     | 
| 
      
 112 
     | 
    
         
            +
                    @prompt.keypress
         
     | 
| 
      
 113 
     | 
    
         
            +
                  end
         
     | 
| 
      
 114 
     | 
    
         
            +
             
     | 
| 
      
 115 
     | 
    
         
            +
                  def configure_ai_provider
         
     | 
| 
      
 116 
     | 
    
         
            +
                    provider = @prompt.select("Choose AI provider:", {
         
     | 
| 
      
 117 
     | 
    
         
            +
                                                "OpenAI (GPT-4, GPT-3.5 Turbo)" => "openai",
         
     | 
| 
      
 118 
     | 
    
         
            +
                                                "Anthropic (Claude 3.5 Sonnet, Claude 3.5 Haiku)" => "claude"
         
     | 
| 
      
 119 
     | 
    
         
            +
                                              })
         
     | 
| 
      
 120 
     | 
    
         
            +
             
     | 
| 
      
 121 
     | 
    
         
            +
                    @settings.save(ai_provider: provider)
         
     | 
| 
      
 122 
     | 
    
         
            +
                    puts "✓ AI provider updated to #{provider}".green
         
     | 
| 
      
 123 
     | 
    
         
            +
             
     | 
| 
      
 124 
     | 
    
         
            +
                    # Check if API key exists for the new provider
         
     | 
| 
      
 125 
     | 
    
         
            +
                    unless @credential_store.api_key_exists?(provider)
         
     | 
| 
      
 126 
     | 
    
         
            +
                      puts "\nNo API key found for #{provider.upcase}. Let's set it up.".blue
         
     | 
| 
      
 127 
     | 
    
         
            +
                      configure_api_key
         
     | 
| 
      
 128 
     | 
    
         
            +
                    end
         
     | 
| 
      
 129 
     | 
    
         
            +
             
     | 
| 
      
 130 
     | 
    
         
            +
                    # Auto-configure model after provider change
         
     | 
| 
      
 131 
     | 
    
         
            +
                    configure_ai_model
         
     | 
| 
      
 132 
     | 
    
         
            +
                  end
         
     | 
| 
      
 133 
     | 
    
         
            +
             
     | 
| 
      
 134 
     | 
    
         
            +
                  def configure_ai_model
         
     | 
| 
      
 135 
     | 
    
         
            +
                    models = Config::Settings::SUPPORTED_PROVIDERS[@settings.get(:ai_provider)][:models]
         
     | 
| 
      
 136 
     | 
    
         
            +
                    model_choices = models.map { |name, value| { name: name, value: value } }
         
     | 
| 
      
 137 
     | 
    
         
            +
             
     | 
| 
      
 138 
     | 
    
         
            +
                    model = @prompt.select("Choose AI model:", model_choices)
         
     | 
| 
      
 139 
     | 
    
         
            +
                    @settings.save(ai_model: model)
         
     | 
| 
      
 140 
     | 
    
         
            +
                    puts "✓ AI model updated to #{model}".green
         
     | 
| 
      
 141 
     | 
    
         
            +
                  end
         
     | 
| 
      
 142 
     | 
    
         
            +
             
     | 
| 
      
 143 
     | 
    
         
            +
                  def configure_api_key
         
     | 
| 
      
 144 
     | 
    
         
            +
                    provider = @settings.get(:ai_provider)
         
     | 
| 
      
 145 
     | 
    
         
            +
                    puts "\nConfiguring API key for #{provider.upcase}".blue
         
     | 
| 
      
 146 
     | 
    
         
            +
             
     | 
| 
      
 147 
     | 
    
         
            +
                    key = @prompt.mask("Enter your API key:")
         
     | 
| 
      
 148 
     | 
    
         
            +
                    @credential_store.store_api_key(key, provider)
         
     | 
| 
      
 149 
     | 
    
         
            +
                    puts "✓ API key updated".green
         
     | 
| 
      
 150 
     | 
    
         
            +
                  end
         
     | 
| 
      
 151 
     | 
    
         
            +
             
     | 
| 
      
 152 
     | 
    
         
            +
                  def configure_commit_style
         
     | 
| 
      
 153 
     | 
    
         
            +
                    style = @prompt.select("Choose commit message style:", {
         
     | 
| 
      
 154 
     | 
    
         
            +
                                             "Conventional (type(scope): description)" => "conventional",
         
     | 
| 
      
 155 
     | 
    
         
            +
                                             "Simple (description only)" => "simple"
         
     | 
| 
      
 156 
     | 
    
         
            +
                                           })
         
     | 
| 
      
 157 
     | 
    
         
            +
             
     | 
| 
      
 158 
     | 
    
         
            +
                    @settings.set(:commit_style, style)
         
     | 
| 
      
 159 
     | 
    
         
            +
                    puts "✓ Commit style updated to #{style}".green
         
     | 
| 
      
 160 
     | 
    
         
            +
                  end
         
     | 
| 
      
 161 
     | 
    
         
            +
             
     | 
| 
      
 162 
     | 
    
         
            +
                  def configure_preferences
         
     | 
| 
      
 163 
     | 
    
         
            +
                    show_diff = @prompt.yes?("Show diff before committing?")
         
     | 
| 
      
 164 
     | 
    
         
            +
                    @settings.set(:show_diff, show_diff)
         
     | 
| 
      
 165 
     | 
    
         
            +
                    puts "✓ Show diff preference updated".green
         
     | 
| 
      
 166 
     | 
    
         
            +
                  end
         
     | 
| 
      
 167 
     | 
    
         
            +
             
     | 
| 
      
 168 
     | 
    
         
            +
                  def configure_history_settings
         
     | 
| 
      
 169 
     | 
    
         
            +
                    save_history = @prompt.yes?("Save commit history for analysis?")
         
     | 
| 
      
 170 
     | 
    
         
            +
                    @settings.set(:save_history, save_history)
         
     | 
| 
      
 171 
     | 
    
         
            +
                    puts "✓ History settings updated".green
         
     | 
| 
      
 172 
     | 
    
         
            +
                  end
         
     | 
| 
      
 173 
     | 
    
         
            +
                end
         
     | 
| 
      
 174 
     | 
    
         
            +
              end
         
     | 
| 
      
 175 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,87 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module GitAuto
         
     | 
| 
      
 4 
     | 
    
         
            +
              module Commands
         
     | 
| 
      
 5 
     | 
    
         
            +
                class HistoryAnalysisCommand
         
     | 
| 
      
 6 
     | 
    
         
            +
                  def initialize(options = {})
         
     | 
| 
      
 7 
     | 
    
         
            +
                    @limit = options[:limit] || 10
         
     | 
| 
      
 8 
     | 
    
         
            +
                    @git_service = Services::GitService.new
         
     | 
| 
      
 9 
     | 
    
         
            +
                    @prompt = TTY::Prompt.new
         
     | 
| 
      
 10 
     | 
    
         
            +
                    @spinner = TTY::Spinner.new("[:spinner] Analyzing commit history...")
         
     | 
| 
      
 11 
     | 
    
         
            +
                  end
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
                  def execute
         
     | 
| 
      
 14 
     | 
    
         
            +
                    @spinner.auto_spin
         
     | 
| 
      
 15 
     | 
    
         
            +
                    commits = fetch_commits
         
     | 
| 
      
 16 
     | 
    
         
            +
                    analysis = analyze_commits(commits)
         
     | 
| 
      
 17 
     | 
    
         
            +
                    @spinner.success
         
     | 
| 
      
 18 
     | 
    
         
            +
                    display_results(analysis)
         
     | 
| 
      
 19 
     | 
    
         
            +
                  rescue StandardError => e
         
     | 
| 
      
 20 
     | 
    
         
            +
                    puts "❌ Error analyzing commits: #{e.message}".red
         
     | 
| 
      
 21 
     | 
    
         
            +
                    exit 1
         
     | 
| 
      
 22 
     | 
    
         
            +
                  end
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
                  private
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
                  def fetch_commits
         
     | 
| 
      
 27 
     | 
    
         
            +
                    @git_service.get_commit_history(@limit)
         
     | 
| 
      
 28 
     | 
    
         
            +
                  end
         
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
      
 30 
     | 
    
         
            +
                  def analyze_commits(commits)
         
     | 
| 
      
 31 
     | 
    
         
            +
                    {
         
     | 
| 
      
 32 
     | 
    
         
            +
                      total_commits: commits.size,
         
     | 
| 
      
 33 
     | 
    
         
            +
                      types: analyze_types(commits),
         
     | 
| 
      
 34 
     | 
    
         
            +
                      avg_length: average_message_length(commits),
         
     | 
| 
      
 35 
     | 
    
         
            +
                      common_patterns: find_common_patterns(commits)
         
     | 
| 
      
 36 
     | 
    
         
            +
                    }
         
     | 
| 
      
 37 
     | 
    
         
            +
                  end
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
      
 39 
     | 
    
         
            +
                  def analyze_types(commits)
         
     | 
| 
      
 40 
     | 
    
         
            +
                    commits.each_with_object(Hash.new(0)) do |commit, types|
         
     | 
| 
      
 41 
     | 
    
         
            +
                      type = extract_type(commit)
         
     | 
| 
      
 42 
     | 
    
         
            +
                      types[type] += 1
         
     | 
| 
      
 43 
     | 
    
         
            +
                    end
         
     | 
| 
      
 44 
     | 
    
         
            +
                  end
         
     | 
| 
      
 45 
     | 
    
         
            +
             
     | 
| 
      
 46 
     | 
    
         
            +
                  def extract_type(commit)
         
     | 
| 
      
 47 
     | 
    
         
            +
                    return "conventional" if commit.match?(/^(feat|fix|docs|style|refactor|test|chore)(\(.+\))?:/)
         
     | 
| 
      
 48 
     | 
    
         
            +
                    return "detailed" if commit.include?("\n\n")
         
     | 
| 
      
 49 
     | 
    
         
            +
             
     | 
| 
      
 50 
     | 
    
         
            +
                    "simple"
         
     | 
| 
      
 51 
     | 
    
         
            +
                  end
         
     | 
| 
      
 52 
     | 
    
         
            +
             
     | 
| 
      
 53 
     | 
    
         
            +
                  def average_message_length(commits)
         
     | 
| 
      
 54 
     | 
    
         
            +
                    return 0 if commits.empty?
         
     | 
| 
      
 55 
     | 
    
         
            +
             
     | 
| 
      
 56 
     | 
    
         
            +
                    commits.sum(&:length) / commits.size
         
     | 
| 
      
 57 
     | 
    
         
            +
                  end
         
     | 
| 
      
 58 
     | 
    
         
            +
             
     | 
| 
      
 59 
     | 
    
         
            +
                  def find_common_patterns(commits)
         
     | 
| 
      
 60 
     | 
    
         
            +
                    words = commits.flat_map { |c| c.downcase.scan(/\w+/) }
         
     | 
| 
      
 61 
     | 
    
         
            +
                    words.each_with_object(Hash.new(0)) { |word, counts| counts[word] += 1 }
         
     | 
| 
      
 62 
     | 
    
         
            +
                      .sort_by { |_, count| -count }
         
     | 
| 
      
 63 
     | 
    
         
            +
                      .first(5)
         
     | 
| 
      
 64 
     | 
    
         
            +
                      .to_h
         
     | 
| 
      
 65 
     | 
    
         
            +
                  end
         
     | 
| 
      
 66 
     | 
    
         
            +
             
     | 
| 
      
 67 
     | 
    
         
            +
                  def display_results(analysis)
         
     | 
| 
      
 68 
     | 
    
         
            +
                    puts "\n📊 Commit History Analysis".blue
         
     | 
| 
      
 69 
     | 
    
         
            +
                    puts "Total commits analyzed: #{analysis[:total_commits]}"
         
     | 
| 
      
 70 
     | 
    
         
            +
             
     | 
| 
      
 71 
     | 
    
         
            +
                    puts "\n📝 Commit Types:".blue
         
     | 
| 
      
 72 
     | 
    
         
            +
                    analysis[:types].each do |type, count|
         
     | 
| 
      
 73 
     | 
    
         
            +
                      percentage = (count.to_f / analysis[:total_commits] * 100).round(1)
         
     | 
| 
      
 74 
     | 
    
         
            +
                      puts "#{type}: #{count} (#{percentage}%)"
         
     | 
| 
      
 75 
     | 
    
         
            +
                    end
         
     | 
| 
      
 76 
     | 
    
         
            +
             
     | 
| 
      
 77 
     | 
    
         
            +
                    puts "\n📈 Statistics:".blue
         
     | 
| 
      
 78 
     | 
    
         
            +
                    puts "Average message length: #{analysis[:avg_length]} characters"
         
     | 
| 
      
 79 
     | 
    
         
            +
             
     | 
| 
      
 80 
     | 
    
         
            +
                    puts "\n🔍 Common words:".blue
         
     | 
| 
      
 81 
     | 
    
         
            +
                    analysis[:common_patterns].each do |word, count|
         
     | 
| 
      
 82 
     | 
    
         
            +
                      puts "#{word}: #{count} times"
         
     | 
| 
      
 83 
     | 
    
         
            +
                    end
         
     | 
| 
      
 84 
     | 
    
         
            +
                  end
         
     | 
| 
      
 85 
     | 
    
         
            +
                end
         
     | 
| 
      
 86 
     | 
    
         
            +
              end
         
     | 
| 
      
 87 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,113 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require "tty-prompt"
         
     | 
| 
      
 4 
     | 
    
         
            +
            require "colorize"
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
            module GitAuto
         
     | 
| 
      
 7 
     | 
    
         
            +
              module Commands
         
     | 
| 
      
 8 
     | 
    
         
            +
                class SetupCommand
         
     | 
| 
      
 9 
     | 
    
         
            +
                  def initialize
         
     | 
| 
      
 10 
     | 
    
         
            +
                    @prompt = TTY::Prompt.new
         
     | 
| 
      
 11 
     | 
    
         
            +
                    @credential_store = Config::CredentialStore.new
         
     | 
| 
      
 12 
     | 
    
         
            +
                    @settings = Config::Settings.new
         
     | 
| 
      
 13 
     | 
    
         
            +
                  end
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
                  def execute
         
     | 
| 
      
 16 
     | 
    
         
            +
                    puts "\n🔧 Setting up GitAuto...".blue
         
     | 
| 
      
 17 
     | 
    
         
            +
                    puts "This wizard will help you configure GitAuto with your preferred AI provider.\n"
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
                    # Select AI provider
         
     | 
| 
      
 20 
     | 
    
         
            +
                    configure_ai_provider
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
                    # Configure preferences
         
     | 
| 
      
 23 
     | 
    
         
            +
                    configure_preferences
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
                    puts "\n✅ Setup completed successfully!".green
         
     | 
| 
      
 26 
     | 
    
         
            +
                    display_configuration
         
     | 
| 
      
 27 
     | 
    
         
            +
                  rescue StandardError => e
         
     | 
| 
      
 28 
     | 
    
         
            +
                    puts "\n❌ Setup failed: #{e.message}".red
         
     | 
| 
      
 29 
     | 
    
         
            +
                    exit 1
         
     | 
| 
      
 30 
     | 
    
         
            +
                  end
         
     | 
| 
      
 31 
     | 
    
         
            +
             
     | 
| 
      
 32 
     | 
    
         
            +
                  private
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
                  def configure_ai_provider
         
     | 
| 
      
 35 
     | 
    
         
            +
                    # Select provider
         
     | 
| 
      
 36 
     | 
    
         
            +
                    provider_choices = Config::Settings::SUPPORTED_PROVIDERS.map do |key, info|
         
     | 
| 
      
 37 
     | 
    
         
            +
                      { name: info[:name], value: key }
         
     | 
| 
      
 38 
     | 
    
         
            +
                    end
         
     | 
| 
      
 39 
     | 
    
         
            +
             
     | 
| 
      
 40 
     | 
    
         
            +
                    provider = @prompt.select(
         
     | 
| 
      
 41 
     | 
    
         
            +
                      "Choose your AI provider:",
         
     | 
| 
      
 42 
     | 
    
         
            +
                      provider_choices,
         
     | 
| 
      
 43 
     | 
    
         
            +
                      help: "(Use ↑/↓ and Enter to select)"
         
     | 
| 
      
 44 
     | 
    
         
            +
                    )
         
     | 
| 
      
 45 
     | 
    
         
            +
             
     | 
| 
      
 46 
     | 
    
         
            +
                    # Select model for the chosen provider
         
     | 
| 
      
 47 
     | 
    
         
            +
                    models = Config::Settings::SUPPORTED_PROVIDERS[provider][:models]
         
     | 
| 
      
 48 
     | 
    
         
            +
                    model_choices = models.map { |name, value| { name: name, value: value } }
         
     | 
| 
      
 49 
     | 
    
         
            +
             
     | 
| 
      
 50 
     | 
    
         
            +
                    model = @prompt.select(
         
     | 
| 
      
 51 
     | 
    
         
            +
                      "Choose the AI model:",
         
     | 
| 
      
 52 
     | 
    
         
            +
                      model_choices,
         
     | 
| 
      
 53 
     | 
    
         
            +
                      help: "More capable models may be slower but produce better results"
         
     | 
| 
      
 54 
     | 
    
         
            +
                    )
         
     | 
| 
      
 55 
     | 
    
         
            +
             
     | 
| 
      
 56 
     | 
    
         
            +
                    # Get and validate API key
         
     | 
| 
      
 57 
     | 
    
         
            +
                    provider_name = Config::Settings::SUPPORTED_PROVIDERS[provider][:name]
         
     | 
| 
      
 58 
     | 
    
         
            +
                    puts "\nℹ️  The API key will be securely stored in your system's credential store"
         
     | 
| 
      
 59 
     | 
    
         
            +
                    api_key = @prompt.mask("Enter your #{provider_name} API key:") do |q|
         
     | 
| 
      
 60 
     | 
    
         
            +
                      q.required true
         
     | 
| 
      
 61 
     | 
    
         
            +
                      q.validate(/\S+/, "API key cannot be empty")
         
     | 
| 
      
 62 
     | 
    
         
            +
                    end
         
     | 
| 
      
 63 
     | 
    
         
            +
             
     | 
| 
      
 64 
     | 
    
         
            +
                    # Save configuration
         
     | 
| 
      
 65 
     | 
    
         
            +
                    @settings.save(
         
     | 
| 
      
 66 
     | 
    
         
            +
                      ai_provider: provider,
         
     | 
| 
      
 67 
     | 
    
         
            +
                      ai_model: model
         
     | 
| 
      
 68 
     | 
    
         
            +
                    )
         
     | 
| 
      
 69 
     | 
    
         
            +
                    @credential_store.store_api_key(api_key, provider)
         
     | 
| 
      
 70 
     | 
    
         
            +
             
     | 
| 
      
 71 
     | 
    
         
            +
                    puts "✓ #{provider_name} configured successfully".green
         
     | 
| 
      
 72 
     | 
    
         
            +
                  end
         
     | 
| 
      
 73 
     | 
    
         
            +
             
     | 
| 
      
 74 
     | 
    
         
            +
                  def configure_preferences
         
     | 
| 
      
 75 
     | 
    
         
            +
                    puts "\n🔧 Configuring preferences...".blue
         
     | 
| 
      
 76 
     | 
    
         
            +
             
     | 
| 
      
 77 
     | 
    
         
            +
                    commit_style = select_commit_style
         
     | 
| 
      
 78 
     | 
    
         
            +
                    show_diff = @prompt.yes?("Show diff preview before generating commit messages?", default: true)
         
     | 
| 
      
 79 
     | 
    
         
            +
                    save_history = @prompt.yes?("Save commit history for pattern analysis?", default: true)
         
     | 
| 
      
 80 
     | 
    
         
            +
             
     | 
| 
      
 81 
     | 
    
         
            +
                    settings = { commit_style: commit_style, show_diff: show_diff, save_history: save_history }
         
     | 
| 
      
 82 
     | 
    
         
            +
             
     | 
| 
      
 83 
     | 
    
         
            +
                    @settings.save(settings)
         
     | 
| 
      
 84 
     | 
    
         
            +
                  end
         
     | 
| 
      
 85 
     | 
    
         
            +
             
     | 
| 
      
 86 
     | 
    
         
            +
                  def select_commit_style
         
     | 
| 
      
 87 
     | 
    
         
            +
                    @prompt.select(
         
     | 
| 
      
 88 
     | 
    
         
            +
                      "Select default commit message style:",
         
     | 
| 
      
 89 
     | 
    
         
            +
                      [
         
     | 
| 
      
 90 
     | 
    
         
            +
                        { name: "Conventional (type(scope): description)", value: "conventional" },
         
     | 
| 
      
 91 
     | 
    
         
            +
                        { name: "Simple (verb + description)", value: "simple" },
         
     | 
| 
      
 92 
     | 
    
         
            +
                        { name: "Detailed (summary + bullet points)", value: "detailed" }
         
     | 
| 
      
 93 
     | 
    
         
            +
                      ],
         
     | 
| 
      
 94 
     | 
    
         
            +
                      help: "This can be changed later using git_auto config"
         
     | 
| 
      
 95 
     | 
    
         
            +
                    )
         
     | 
| 
      
 96 
     | 
    
         
            +
                  end
         
     | 
| 
      
 97 
     | 
    
         
            +
             
     | 
| 
      
 98 
     | 
    
         
            +
                  def display_configuration
         
     | 
| 
      
 99 
     | 
    
         
            +
                    config = @settings.all
         
     | 
| 
      
 100 
     | 
    
         
            +
                    provider_info = Config::Settings::SUPPORTED_PROVIDERS[config[:ai_provider]]
         
     | 
| 
      
 101 
     | 
    
         
            +
             
     | 
| 
      
 102 
     | 
    
         
            +
                    puts "\nCurrent Configuration:"
         
     | 
| 
      
 103 
     | 
    
         
            +
                    puts "──────────────────────"
         
     | 
| 
      
 104 
     | 
    
         
            +
                    puts "AI Provider: #{provider_info[:name]} (#{config[:ai_provider]})".cyan
         
     | 
| 
      
 105 
     | 
    
         
            +
                    puts "Model: #{config[:ai_model]}".cyan
         
     | 
| 
      
 106 
     | 
    
         
            +
                    puts "Commit Style: #{config[:commit_style]}".cyan
         
     | 
| 
      
 107 
     | 
    
         
            +
                    puts "Show Diff: #{config[:show_diff]}".cyan
         
     | 
| 
      
 108 
     | 
    
         
            +
                    puts "Save History: #{config[:save_history]}".cyan
         
     | 
| 
      
 109 
     | 
    
         
            +
                    puts "\nYou can change these settings anytime using: git_auto config"
         
     | 
| 
      
 110 
     | 
    
         
            +
                  end
         
     | 
| 
      
 111 
     | 
    
         
            +
                end
         
     | 
| 
      
 112 
     | 
    
         
            +
              end
         
     | 
| 
      
 113 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,73 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require "openssl"
         
     | 
| 
      
 4 
     | 
    
         
            +
            require "base64"
         
     | 
| 
      
 5 
     | 
    
         
            +
            require "fileutils"
         
     | 
| 
      
 6 
     | 
    
         
            +
            require "yaml"
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
            module GitAuto
         
     | 
| 
      
 9 
     | 
    
         
            +
              module Config
         
     | 
| 
      
 10 
     | 
    
         
            +
                class CredentialStore
         
     | 
| 
      
 11 
     | 
    
         
            +
                  CREDENTIALS_FILE = File.join(File.expand_path("~/.git_auto"), "credentials.yml")
         
     | 
| 
      
 12 
     | 
    
         
            +
                  ENCRYPTION_KEY = ENV["GIT_AUTO_SECRET"] || "default_development_key"
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
                  def initialize
         
     | 
| 
      
 15 
     | 
    
         
            +
                    ensure_credentials_file
         
     | 
| 
      
 16 
     | 
    
         
            +
                  end
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
                  def store_api_key(key, provider)
         
     | 
| 
      
 19 
     | 
    
         
            +
                    credentials = load_credentials
         
     | 
| 
      
 20 
     | 
    
         
            +
                    credentials[provider.to_s] = encrypt(key)
         
     | 
| 
      
 21 
     | 
    
         
            +
                    save_credentials(credentials)
         
     | 
| 
      
 22 
     | 
    
         
            +
                  end
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
                  def get_api_key(provider)
         
     | 
| 
      
 25 
     | 
    
         
            +
                    credentials = load_credentials
         
     | 
| 
      
 26 
     | 
    
         
            +
                    encrypted_key = credentials[provider.to_s]
         
     | 
| 
      
 27 
     | 
    
         
            +
                    return nil unless encrypted_key
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
                    decrypt(encrypted_key)
         
     | 
| 
      
 30 
     | 
    
         
            +
                  end
         
     | 
| 
      
 31 
     | 
    
         
            +
             
     | 
| 
      
 32 
     | 
    
         
            +
                  def api_key_exists?(provider)
         
     | 
| 
      
 33 
     | 
    
         
            +
                    credentials = load_credentials
         
     | 
| 
      
 34 
     | 
    
         
            +
                    credentials.key?(provider.to_s)
         
     | 
| 
      
 35 
     | 
    
         
            +
                  end
         
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
                  private
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
      
 39 
     | 
    
         
            +
                  def ensure_credentials_file
         
     | 
| 
      
 40 
     | 
    
         
            +
                    dir = File.dirname(CREDENTIALS_FILE)
         
     | 
| 
      
 41 
     | 
    
         
            +
                    FileUtils.mkdir_p(dir) unless File.directory?(dir)
         
     | 
| 
      
 42 
     | 
    
         
            +
                    FileUtils.touch(CREDENTIALS_FILE) unless File.exist?(CREDENTIALS_FILE)
         
     | 
| 
      
 43 
     | 
    
         
            +
                  end
         
     | 
| 
      
 44 
     | 
    
         
            +
             
     | 
| 
      
 45 
     | 
    
         
            +
                  def load_credentials
         
     | 
| 
      
 46 
     | 
    
         
            +
                    content = File.read(CREDENTIALS_FILE).strip
         
     | 
| 
      
 47 
     | 
    
         
            +
                    content.empty? ? {} : YAML.safe_load(content)
         
     | 
| 
      
 48 
     | 
    
         
            +
                  end
         
     | 
| 
      
 49 
     | 
    
         
            +
             
     | 
| 
      
 50 
     | 
    
         
            +
                  def save_credentials(credentials)
         
     | 
| 
      
 51 
     | 
    
         
            +
                    File.write(CREDENTIALS_FILE, YAML.dump(credentials))
         
     | 
| 
      
 52 
     | 
    
         
            +
                  end
         
     | 
| 
      
 53 
     | 
    
         
            +
             
     | 
| 
      
 54 
     | 
    
         
            +
                  def encrypt(text)
         
     | 
| 
      
 55 
     | 
    
         
            +
                    cipher = OpenSSL::Cipher.new("aes-256-cbc")
         
     | 
| 
      
 56 
     | 
    
         
            +
                    cipher.encrypt
         
     | 
| 
      
 57 
     | 
    
         
            +
                    cipher.key = Digest::SHA256.digest(ENCRYPTION_KEY)
         
     | 
| 
      
 58 
     | 
    
         
            +
                    iv = cipher.random_iv
         
     | 
| 
      
 59 
     | 
    
         
            +
                    encrypted = cipher.update(text) + cipher.final
         
     | 
| 
      
 60 
     | 
    
         
            +
                    Base64.strict_encode64(iv + encrypted)
         
     | 
| 
      
 61 
     | 
    
         
            +
                  end
         
     | 
| 
      
 62 
     | 
    
         
            +
             
     | 
| 
      
 63 
     | 
    
         
            +
                  def decrypt(encrypted_data)
         
     | 
| 
      
 64 
     | 
    
         
            +
                    encrypted = Base64.strict_decode64(encrypted_data)
         
     | 
| 
      
 65 
     | 
    
         
            +
                    decipher = OpenSSL::Cipher.new("aes-256-cbc")
         
     | 
| 
      
 66 
     | 
    
         
            +
                    decipher.decrypt
         
     | 
| 
      
 67 
     | 
    
         
            +
                    decipher.key = Digest::SHA256.digest(ENCRYPTION_KEY)
         
     | 
| 
      
 68 
     | 
    
         
            +
                    decipher.iv = encrypted[0..15]
         
     | 
| 
      
 69 
     | 
    
         
            +
                    decipher.update(encrypted[16..]) + decipher.final
         
     | 
| 
      
 70 
     | 
    
         
            +
                  end
         
     | 
| 
      
 71 
     | 
    
         
            +
                end
         
     | 
| 
      
 72 
     | 
    
         
            +
              end
         
     | 
| 
      
 73 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,95 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require "yaml"
         
     | 
| 
      
 4 
     | 
    
         
            +
            require "fileutils"
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
            module GitAuto
         
     | 
| 
      
 7 
     | 
    
         
            +
              module Config
         
     | 
| 
      
 8 
     | 
    
         
            +
                class Settings
         
     | 
| 
      
 9 
     | 
    
         
            +
                  CONFIG_DIR = File.expand_path("~/.git_auto")
         
     | 
| 
      
 10 
     | 
    
         
            +
                  CONFIG_FILE = File.join(CONFIG_DIR, "config.yml")
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
                  SUPPORTED_PROVIDERS = {
         
     | 
| 
      
 13 
     | 
    
         
            +
                    "claude" => {
         
     | 
| 
      
 14 
     | 
    
         
            +
                      name: "Anthropic (Claude 3.5 Sonnet, Claude 3.5 Haiku)",
         
     | 
| 
      
 15 
     | 
    
         
            +
                      models: {
         
     | 
| 
      
 16 
     | 
    
         
            +
                        "Claude 3.5 Sonnet" => "claude-3-5-sonnet-latest",
         
     | 
| 
      
 17 
     | 
    
         
            +
                        "Claude 3.5 Haiku" => "claude-3-5-haiku-latest"
         
     | 
| 
      
 18 
     | 
    
         
            +
                      }
         
     | 
| 
      
 19 
     | 
    
         
            +
                    },
         
     | 
| 
      
 20 
     | 
    
         
            +
                    "openai" => {
         
     | 
| 
      
 21 
     | 
    
         
            +
                      name: "OpenAI (GPT-4o, GPT-4o mini)",
         
     | 
| 
      
 22 
     | 
    
         
            +
                      models: {
         
     | 
| 
      
 23 
     | 
    
         
            +
                        "GPT-4o" => "gpt-4o",
         
     | 
| 
      
 24 
     | 
    
         
            +
                        "GPT-4o mini" => "gpt-4o-mini"
         
     | 
| 
      
 25 
     | 
    
         
            +
                      }
         
     | 
| 
      
 26 
     | 
    
         
            +
                    }
         
     | 
| 
      
 27 
     | 
    
         
            +
                  }.freeze
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
                  DEFAULT_SETTINGS = {
         
     | 
| 
      
 30 
     | 
    
         
            +
                    commit_style: "conventional",
         
     | 
| 
      
 31 
     | 
    
         
            +
                    ai_provider: "openai",
         
     | 
| 
      
 32 
     | 
    
         
            +
                    ai_model: "gpt-4o",
         
     | 
| 
      
 33 
     | 
    
         
            +
                    show_diff: true,
         
     | 
| 
      
 34 
     | 
    
         
            +
                    save_history: true,
         
     | 
| 
      
 35 
     | 
    
         
            +
                    max_retries: 3
         
     | 
| 
      
 36 
     | 
    
         
            +
                  }.freeze
         
     | 
| 
      
 37 
     | 
    
         
            +
             
     | 
| 
      
 38 
     | 
    
         
            +
                  def initialize
         
     | 
| 
      
 39 
     | 
    
         
            +
                    ensure_config_dir
         
     | 
| 
      
 40 
     | 
    
         
            +
                    @settings = load_settings
         
     | 
| 
      
 41 
     | 
    
         
            +
                  end
         
     | 
| 
      
 42 
     | 
    
         
            +
             
     | 
| 
      
 43 
     | 
    
         
            +
                  def save(options = {})
         
     | 
| 
      
 44 
     | 
    
         
            +
                    validate_settings!(options)
         
     | 
| 
      
 45 
     | 
    
         
            +
                    @settings = @settings.merge(options)
         
     | 
| 
      
 46 
     | 
    
         
            +
                    File.write(CONFIG_FILE, YAML.dump(@settings))
         
     | 
| 
      
 47 
     | 
    
         
            +
                  end
         
     | 
| 
      
 48 
     | 
    
         
            +
             
     | 
| 
      
 49 
     | 
    
         
            +
                  def get(key)
         
     | 
| 
      
 50 
     | 
    
         
            +
                    @settings[key.to_sym]
         
     | 
| 
      
 51 
     | 
    
         
            +
                  end
         
     | 
| 
      
 52 
     | 
    
         
            +
             
     | 
| 
      
 53 
     | 
    
         
            +
                  def all
         
     | 
| 
      
 54 
     | 
    
         
            +
                    @settings
         
     | 
| 
      
 55 
     | 
    
         
            +
                  end
         
     | 
| 
      
 56 
     | 
    
         
            +
             
     | 
| 
      
 57 
     | 
    
         
            +
                  def provider_info
         
     | 
| 
      
 58 
     | 
    
         
            +
                    SUPPORTED_PROVIDERS[get(:ai_provider)]
         
     | 
| 
      
 59 
     | 
    
         
            +
                  end
         
     | 
| 
      
 60 
     | 
    
         
            +
             
     | 
| 
      
 61 
     | 
    
         
            +
                  def available_models
         
     | 
| 
      
 62 
     | 
    
         
            +
                    provider_info[:models]
         
     | 
| 
      
 63 
     | 
    
         
            +
                  end
         
     | 
| 
      
 64 
     | 
    
         
            +
             
     | 
| 
      
 65 
     | 
    
         
            +
                  private
         
     | 
| 
      
 66 
     | 
    
         
            +
             
     | 
| 
      
 67 
     | 
    
         
            +
                  def ensure_config_dir
         
     | 
| 
      
 68 
     | 
    
         
            +
                    FileUtils.mkdir_p(CONFIG_DIR)
         
     | 
| 
      
 69 
     | 
    
         
            +
                  end
         
     | 
| 
      
 70 
     | 
    
         
            +
             
     | 
| 
      
 71 
     | 
    
         
            +
                  def load_settings
         
     | 
| 
      
 72 
     | 
    
         
            +
                    return DEFAULT_SETTINGS.dup unless File.exist?(CONFIG_FILE)
         
     | 
| 
      
 73 
     | 
    
         
            +
             
     | 
| 
      
 74 
     | 
    
         
            +
                    user_settings = YAML.load_file(CONFIG_FILE) || {}
         
     | 
| 
      
 75 
     | 
    
         
            +
                    DEFAULT_SETTINGS.merge(user_settings)
         
     | 
| 
      
 76 
     | 
    
         
            +
                  rescue StandardError
         
     | 
| 
      
 77 
     | 
    
         
            +
                    DEFAULT_SETTINGS.dup
         
     | 
| 
      
 78 
     | 
    
         
            +
                  end
         
     | 
| 
      
 79 
     | 
    
         
            +
             
     | 
| 
      
 80 
     | 
    
         
            +
                  def validate_settings!(options)
         
     | 
| 
      
 81 
     | 
    
         
            +
                    if options[:ai_provider] && !SUPPORTED_PROVIDERS.key?(options[:ai_provider])
         
     | 
| 
      
 82 
     | 
    
         
            +
                      raise Error, "Unsupported AI provider. Available providers: #{SUPPORTED_PROVIDERS.keys.join(", ")}"
         
     | 
| 
      
 83 
     | 
    
         
            +
                    end
         
     | 
| 
      
 84 
     | 
    
         
            +
             
     | 
| 
      
 85 
     | 
    
         
            +
                    return unless options[:ai_model]
         
     | 
| 
      
 86 
     | 
    
         
            +
             
     | 
| 
      
 87 
     | 
    
         
            +
                    provider = options[:ai_provider] || get(:ai_provider)
         
     | 
| 
      
 88 
     | 
    
         
            +
                    available_models = SUPPORTED_PROVIDERS[provider][:models]
         
     | 
| 
      
 89 
     | 
    
         
            +
                    return if available_models.values.include?(options[:ai_model])
         
     | 
| 
      
 90 
     | 
    
         
            +
             
     | 
| 
      
 91 
     | 
    
         
            +
                    raise Error, "Unsupported model for #{provider}. Available models: #{available_models.keys.join(", ")}"
         
     | 
| 
      
 92 
     | 
    
         
            +
                  end
         
     | 
| 
      
 93 
     | 
    
         
            +
                end
         
     | 
| 
      
 94 
     | 
    
         
            +
              end
         
     | 
| 
      
 95 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,12 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module GitAuto
         
     | 
| 
      
 4 
     | 
    
         
            +
              module Errors
         
     | 
| 
      
 5 
     | 
    
         
            +
                class Error < StandardError; end
         
     | 
| 
      
 6 
     | 
    
         
            +
                class MissingAPIKeyError < Error; end
         
     | 
| 
      
 7 
     | 
    
         
            +
                class EmptyDiffError < Error; end
         
     | 
| 
      
 8 
     | 
    
         
            +
                class RateLimitError < Error; end
         
     | 
| 
      
 9 
     | 
    
         
            +
                class APIError < Error; end
         
     | 
| 
      
 10 
     | 
    
         
            +
                class InvalidProviderError < Error; end
         
     | 
| 
      
 11 
     | 
    
         
            +
              end
         
     | 
| 
      
 12 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,49 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module GitAuto
         
     | 
| 
      
 4 
     | 
    
         
            +
              module Formatters
         
     | 
| 
      
 5 
     | 
    
         
            +
                class DiffFormatter
         
     | 
| 
      
 6 
     | 
    
         
            +
                  def format(diff)
         
     | 
| 
      
 7 
     | 
    
         
            +
                    return "No changes" if diff.empty?
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
                    formatted = []
         
     | 
| 
      
 10 
     | 
    
         
            +
                    current_file = nil
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
                    diff.each_line do |line|
         
     | 
| 
      
 13 
     | 
    
         
            +
                      case line
         
     | 
| 
      
 14 
     | 
    
         
            +
                      when /^diff --git/
         
     | 
| 
      
 15 
     | 
    
         
            +
                        current_file = extract_file_name(line)
         
     | 
| 
      
 16 
     | 
    
         
            +
                        formatted << "\nChanges in #{current_file}:"
         
     | 
| 
      
 17 
     | 
    
         
            +
                      when /^index |^---|\+\+\+/
         
     | 
| 
      
 18 
     | 
    
         
            +
                        next # Skip index and file indicator lines
         
     | 
| 
      
 19 
     | 
    
         
            +
                      when /^@@ .* @@/
         
     | 
| 
      
 20 
     | 
    
         
            +
                        formatted << format_hunk_header(line)
         
     | 
| 
      
 21 
     | 
    
         
            +
                      when /^\+/
         
     | 
| 
      
 22 
     | 
    
         
            +
                        formatted << "Added: #{line[1..].strip}"
         
     | 
| 
      
 23 
     | 
    
         
            +
                      when /^-/
         
     | 
| 
      
 24 
     | 
    
         
            +
                        formatted << "Removed: #{line[1..].strip}"
         
     | 
| 
      
 25 
     | 
    
         
            +
                      when /^ /
         
     | 
| 
      
 26 
     | 
    
         
            +
                        formatted << "Context: #{line.strip}" unless line.strip.empty?
         
     | 
| 
      
 27 
     | 
    
         
            +
                      end
         
     | 
| 
      
 28 
     | 
    
         
            +
                    end
         
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
      
 30 
     | 
    
         
            +
                    formatted.join("\n")
         
     | 
| 
      
 31 
     | 
    
         
            +
                  end
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
                  private
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
                  def extract_file_name(line)
         
     | 
| 
      
 36 
     | 
    
         
            +
                    line.match(%r{b/(.+)$})[1]
         
     | 
| 
      
 37 
     | 
    
         
            +
                  end
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
      
 39 
     | 
    
         
            +
                  def format_hunk_header(line)
         
     | 
| 
      
 40 
     | 
    
         
            +
                    match = line.match(/@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@(.*)/)
         
     | 
| 
      
 41 
     | 
    
         
            +
                    return line unless match
         
     | 
| 
      
 42 
     | 
    
         
            +
             
     | 
| 
      
 43 
     | 
    
         
            +
                    line_info = "@ #{match[1]}-#{match[3]}"
         
     | 
| 
      
 44 
     | 
    
         
            +
                    context = match[5].strip
         
     | 
| 
      
 45 
     | 
    
         
            +
                    "\nSection #{line_info} #{context}"
         
     | 
| 
      
 46 
     | 
    
         
            +
                  end
         
     | 
| 
      
 47 
     | 
    
         
            +
                end
         
     | 
| 
      
 48 
     | 
    
         
            +
              end
         
     | 
| 
      
 49 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,99 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module GitAuto
         
     | 
| 
      
 4 
     | 
    
         
            +
              module Formatters
         
     | 
| 
      
 5 
     | 
    
         
            +
                class DiffSummarizer
         
     | 
| 
      
 6 
     | 
    
         
            +
                  FileChange = Struct.new(:name, :additions, :deletions, :key_changes)
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
                  def summarize(diff)
         
     | 
| 
      
 9 
     | 
    
         
            +
                    return "No changes" if diff.empty?
         
     | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
      
 11 
     | 
    
         
            +
                    file_changes = parse_diff(diff)
         
     | 
| 
      
 12 
     | 
    
         
            +
                    generate_summary(file_changes)
         
     | 
| 
      
 13 
     | 
    
         
            +
                  end
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
                  private
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
                  def parse_diff(diff)
         
     | 
| 
      
 18 
     | 
    
         
            +
                    changes = {}
         
     | 
| 
      
 19 
     | 
    
         
            +
                    current_file = nil
         
     | 
| 
      
 20 
     | 
    
         
            +
                    current_changes = nil
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
                    diff.each_line do |line|
         
     | 
| 
      
 23 
     | 
    
         
            +
                      case line
         
     | 
| 
      
 24 
     | 
    
         
            +
                      when /^diff --git/
         
     | 
| 
      
 25 
     | 
    
         
            +
                        changes[current_file] = current_changes if current_file
         
     | 
| 
      
 26 
     | 
    
         
            +
                        current_file = extract_file_name(line)
         
     | 
| 
      
 27 
     | 
    
         
            +
                        current_changes = FileChange.new(current_file, 0, 0, [])
         
     | 
| 
      
 28 
     | 
    
         
            +
                      when /^\+(?!\+\+)/
         
     | 
| 
      
 29 
     | 
    
         
            +
                        next if current_changes.nil?
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
                        current_changes.additions += 1
         
     | 
| 
      
 32 
     | 
    
         
            +
                        content = line[1..].strip
         
     | 
| 
      
 33 
     | 
    
         
            +
                        current_changes.key_changes << "+#{content}" if key_change?(content)
         
     | 
| 
      
 34 
     | 
    
         
            +
                      when /^-(?!--)/
         
     | 
| 
      
 35 
     | 
    
         
            +
                        next if current_changes.nil?
         
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
                        current_changes.deletions += 1
         
     | 
| 
      
 38 
     | 
    
         
            +
                        content = line[1..].strip
         
     | 
| 
      
 39 
     | 
    
         
            +
                        current_changes.key_changes << "-#{content}" if key_change?(content)
         
     | 
| 
      
 40 
     | 
    
         
            +
                      end
         
     | 
| 
      
 41 
     | 
    
         
            +
                    end
         
     | 
| 
      
 42 
     | 
    
         
            +
             
     | 
| 
      
 43 
     | 
    
         
            +
                    # Add the last file's changes
         
     | 
| 
      
 44 
     | 
    
         
            +
                    changes[current_file] = current_changes if current_file
         
     | 
| 
      
 45 
     | 
    
         
            +
             
     | 
| 
      
 46 
     | 
    
         
            +
                    changes
         
     | 
| 
      
 47 
     | 
    
         
            +
                  end
         
     | 
| 
      
 48 
     | 
    
         
            +
             
     | 
| 
      
 49 
     | 
    
         
            +
                  def generate_summary(changes)
         
     | 
| 
      
 50 
     | 
    
         
            +
                    return "No changes" if changes.empty?
         
     | 
| 
      
 51 
     | 
    
         
            +
             
     | 
| 
      
 52 
     | 
    
         
            +
                    total_additions = 0
         
     | 
| 
      
 53 
     | 
    
         
            +
                    total_deletions = 0
         
     | 
| 
      
 54 
     | 
    
         
            +
                    summary = []
         
     | 
| 
      
 55 
     | 
    
         
            +
             
     | 
| 
      
 56 
     | 
    
         
            +
                    summary << "[Summary: Changes across #{changes.size} files]"
         
     | 
| 
      
 57 
     | 
    
         
            +
             
     | 
| 
      
 58 
     | 
    
         
            +
                    changes.each_value do |change|
         
     | 
| 
      
 59 
     | 
    
         
            +
                      total_additions += change.additions
         
     | 
| 
      
 60 
     | 
    
         
            +
                      total_deletions += change.deletions
         
     | 
| 
      
 61 
     | 
    
         
            +
                    end
         
     | 
| 
      
 62 
     | 
    
         
            +
             
     | 
| 
      
 63 
     | 
    
         
            +
                    summary << "Total: +#{total_additions} lines added, -#{total_deletions} lines removed"
         
     | 
| 
      
 64 
     | 
    
         
            +
                    summary << "\nFiles modified:"
         
     | 
| 
      
 65 
     | 
    
         
            +
             
     | 
| 
      
 66 
     | 
    
         
            +
                    changes.each_value do |change|
         
     | 
| 
      
 67 
     | 
    
         
            +
                      summary << "- #{change.name}:"
         
     | 
| 
      
 68 
     | 
    
         
            +
                      if change.key_changes.any?
         
     | 
| 
      
 69 
     | 
    
         
            +
                        change.key_changes.take(5).each do |key_change|
         
     | 
| 
      
 70 
     | 
    
         
            +
                          summary << "  #{key_change}"
         
     | 
| 
      
 71 
     | 
    
         
            +
                        end
         
     | 
| 
      
 72 
     | 
    
         
            +
                        summary << "  [...#{change.key_changes.size - 5} more changes omitted...]" if change.key_changes.size > 5
         
     | 
| 
      
 73 
     | 
    
         
            +
                      else
         
     | 
| 
      
 74 
     | 
    
         
            +
                        summary << "  #{change.additions} additions, #{change.deletions} deletions"
         
     | 
| 
      
 75 
     | 
    
         
            +
                      end
         
     | 
| 
      
 76 
     | 
    
         
            +
                    end
         
     | 
| 
      
 77 
     | 
    
         
            +
             
     | 
| 
      
 78 
     | 
    
         
            +
                    summary << "\n[Note: Some context and minor changes have been omitted for brevity]"
         
     | 
| 
      
 79 
     | 
    
         
            +
                    summary.join("\n")
         
     | 
| 
      
 80 
     | 
    
         
            +
                  end
         
     | 
| 
      
 81 
     | 
    
         
            +
             
     | 
| 
      
 82 
     | 
    
         
            +
                  def extract_file_name(line)
         
     | 
| 
      
 83 
     | 
    
         
            +
                    line.match(%r{b/(.+)$})[1]
         
     | 
| 
      
 84 
     | 
    
         
            +
                  end
         
     | 
| 
      
 85 
     | 
    
         
            +
             
     | 
| 
      
 86 
     | 
    
         
            +
                  def key_change?(line)
         
     | 
| 
      
 87 
     | 
    
         
            +
                    # Consider a change "key" if it matches certain patterns
         
     | 
| 
      
 88 
     | 
    
         
            +
                    return true if line.match?(/^(class|module|def|private|protected|public)/)
         
     | 
| 
      
 89 
     | 
    
         
            +
                    return true if line.match?(/^[A-Z][A-Za-z0-9_]*\s*=/) # Constants
         
     | 
| 
      
 90 
     | 
    
         
            +
                    return true if line.match?(/^\s*attr_(reader|writer|accessor)/)
         
     | 
| 
      
 91 
     | 
    
         
            +
                    return true if line.match?(/^\s*validates?/)
         
     | 
| 
      
 92 
     | 
    
         
            +
                    return true if line.match?(/^\s*has_(many|one|and_belongs_to_many)/)
         
     | 
| 
      
 93 
     | 
    
         
            +
                    return true if line.match?(/^\s*belongs_to/)
         
     | 
| 
      
 94 
     | 
    
         
            +
             
     | 
| 
      
 95 
     | 
    
         
            +
                    false
         
     | 
| 
      
 96 
     | 
    
         
            +
                  end
         
     | 
| 
      
 97 
     | 
    
         
            +
                end
         
     | 
| 
      
 98 
     | 
    
         
            +
              end
         
     | 
| 
      
 99 
     | 
    
         
            +
            end
         
     |