ai_git 0.2.0 → 1.0.1

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.
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+ # lib/ai_git/secrets.rb
3
+ #
4
+ # @purpose Screen the staged paths and diff for credentials before the
5
+ # diff leaves the machine, splitting blocking hits from warnings.
6
+ # @exports AIGit::Secrets: RISKY_PATHS, RISKY_CONTENT,
7
+ # SUSPICIOUS_ASSIGNMENT, .scan.
8
+ # @sideEffects None.
9
+ # @notes Only added lines are scanned: removing a secret is not a leak.
10
+ # A bare key/secret/password assignment warns rather than blocks,
11
+ # because the pattern also matches ordinary code.
12
+
13
+ module AIGit
14
+ module Secrets
15
+ module_function
16
+
17
+ RISKY_PATHS = {
18
+ "an environment file" => %r{(\A|/)\.env(\.[^/]+)?\z},
19
+ "an SSH private key" => %r{(\A|/)id_(rsa|dsa|ecdsa|ed25519)\z},
20
+ "a private key file" => /\.(pem|key|p12|pfx|jks|keystore)\z/i,
21
+ "a credentials file" => %r{(\A|/)(credentials|\.netrc|\.npmrc|\.pypirc|\.htpasswd)\z},
22
+ "a secrets file" => %r{(\A|/)secrets?\.(ya?ml|json|toml)\z}i
23
+ }.freeze
24
+
25
+ RISKY_CONTENT = {
26
+ "a private key block" => /-----BEGIN [A-Z ]*PRIVATE KEY-----/,
27
+ "an AWS access key id" => /\bAKIA[0-9A-Z]{16}\b/,
28
+ "a GitHub token" => /\b(gh[posur]_[A-Za-z0-9]{16,}|github_pat_[A-Za-z0-9_]{20,})\b/,
29
+ "an OpenAI-style API key" => /\bsk-[A-Za-z0-9_-]{20,}\b/,
30
+ "a Slack token" => /\bxox[abprs]-[A-Za-z0-9-]{10,}\b/,
31
+ "a Google API key" => /\bAIza[0-9A-Za-z_-]{35}\b/
32
+ }.freeze
33
+
34
+ SUSPICIOUS_ASSIGNMENT = /
35
+ \b(api[_-]?key|secret|password|passwd|token|access[_-]?key)\b
36
+ \s*[:=]\s*["'][^"']{8,}["']
37
+ /ix.freeze
38
+
39
+ def scan(staged_files, diff)
40
+ { blocking: blocking_findings(staged_files, diff), warnings: warning_findings(diff) }
41
+ end
42
+
43
+ def blocking_findings(staged_files, diff)
44
+ paths = staged_files.to_s.lines.map(&:strip).reject(&:empty?)
45
+ added = added_lines(diff)
46
+
47
+ findings = paths.flat_map do |path|
48
+ RISKY_PATHS.filter_map { |label, pattern| "#{path} looks like #{label}" if path.match?(pattern) }
49
+ end
50
+
51
+ findings + RISKY_CONTENT.filter_map do |label, pattern|
52
+ "added lines contain what looks like #{label}" if added.match?(pattern)
53
+ end
54
+ end
55
+
56
+ def warning_findings(diff)
57
+ return [] unless added_lines(diff).match?(SUSPICIOUS_ASSIGNMENT)
58
+
59
+ ["added lines assign a value to a key/secret/password/token name"]
60
+ end
61
+
62
+ def added_lines(diff)
63
+ diff.to_s.lines.select { |line| line.start_with?("+") && !line.start_with?("+++") }.join
64
+ end
65
+ end
66
+ end
data/lib/ai_git/ui.rb ADDED
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+ # lib/ai_git/ui.rb
3
+ #
4
+ # @purpose Render every line the CLI prints, adding ANSI color only when
5
+ # the terminal and the configuration both allow it.
6
+ # @exports AIGit::UI: CODES, .color?, .no_color?, .paint, .bold, .dim,
7
+ # .kv, .heading, .info, .success, .warning, .error.
8
+ # @dependencies ai_git/config: supplies the no_color setting from
9
+ # ~/.ai_git/config.yml.
10
+ # @sideEffects Writes to stdout and stderr; inspects $stdout.tty?.
11
+ # @notes Color is off whenever the config file sets no_color or stdout
12
+ # is not a terminal, so piped output stays plain. A config file
13
+ # that fails to load also turns color off rather than raising, so
14
+ # that error itself can still be printed.
15
+
16
+ require_relative "config"
17
+
18
+ module AIGit
19
+ module UI
20
+ module_function
21
+
22
+ CODES = {
23
+ bold: 1, dim: 2, red: 31, green: 32, yellow: 33, blue: 34, cyan: 36, gray: 90
24
+ }.freeze
25
+
26
+ def color?
27
+ return false if no_color?
28
+
29
+ $stdout.tty?
30
+ end
31
+
32
+ def no_color?
33
+ AIGit::Config.no_color?
34
+ rescue StandardError
35
+ true
36
+ end
37
+
38
+ def paint(text, *styles)
39
+ return text.to_s unless color?
40
+
41
+ codes = styles.map { |style| CODES[style] }.compact
42
+ return text.to_s if codes.empty?
43
+
44
+ "\e[#{codes.join(';')}m#{text}\e[0m"
45
+ end
46
+
47
+ def bold(text)
48
+ paint(text, :bold)
49
+ end
50
+
51
+ def dim(text)
52
+ paint(text, :dim)
53
+ end
54
+
55
+ def kv(key, value)
56
+ puts "#{paint("#{key}:", :bold)} #{value}"
57
+ end
58
+
59
+ def heading(text)
60
+ puts paint(text, :bold, :cyan)
61
+ end
62
+
63
+ def info(text)
64
+ puts text
65
+ end
66
+
67
+ def success(text)
68
+ puts paint(text, :green)
69
+ end
70
+
71
+ def warning(text)
72
+ warn paint(text, :yellow)
73
+ end
74
+
75
+ def error(text)
76
+ $stderr.puts paint(text, :red) # rubocop:disable Style/StderrPuts
77
+ end
78
+ end
79
+ end
@@ -1,5 +1,11 @@
1
1
  # frozen_string_literal: true
2
+ # lib/ai_git/version.rb
3
+ #
4
+ # @purpose Hold the single source of truth for the gem's version number,
5
+ # read by the gemspec, the CLI, and the release check.
6
+ # @exports AIGit::VERSION.
7
+ # @sideEffects None.
2
8
 
3
9
  module AIGit
4
- VERSION = "0.2.0"
10
+ VERSION = "1.0.1"
5
11
  end
data/lib/ai_git.rb CHANGED
@@ -1,24 +1,81 @@
1
1
  # frozen_string_literal: true
2
+ # lib/ai_git.rb
3
+ #
4
+ # @purpose Library entry point and CLI router: load every component, then
5
+ # dispatch the argument vector to the matching subcommand.
6
+ # @exports AIGit: SUBCOMMANDS, HELP_FLAGS, VERSION_FLAGS, USAGE, .start.
7
+ # @dependencies ai_git/version, ai_git/config, ai_git/ui, ai_git/ai_client,
8
+ # ai_git/git: the components the subcommands build on;
9
+ # ai_git/commands/default, ai_git/commands/config: the two
10
+ # subcommands .start dispatches to.
11
+ # @sideEffects Prints usage or the version to stdout; warns and exits 1 on an
12
+ # unknown subcommand; .start runs the selected subcommand.
13
+ # @notes A first argument starting with "-" is left in place for the
14
+ # default command's parser, so bare flags need no subcommand.
2
15
 
3
16
  require_relative "ai_git/version"
4
17
  require_relative "ai_git/config"
18
+ require_relative "ai_git/ui"
19
+ require_relative "ai_git/ai_client"
5
20
  require_relative "ai_git/git"
6
- require_relative "ai_git/review"
7
- require_relative "ai_git/default"
21
+ require_relative "ai_git/commands/default"
22
+ require_relative "ai_git/commands/config"
8
23
 
9
24
  module AIGit
10
25
  module_function
11
26
 
12
27
  SUBCOMMANDS = {
13
- "review" => AIGit::Review,
14
- "default" => AIGit::Default
28
+ "config" => AIGit::Commands::Config,
29
+ "default" => AIGit::Commands::Default
15
30
  }.freeze
16
31
 
32
+ HELP_FLAGS = %w[-h --help help].freeze
33
+ VERSION_FLAGS = %w[-v --version].freeze
34
+
35
+ USAGE = <<~USAGE
36
+ Usage: ai_git [subcommand] [options]
37
+
38
+ Subcommands:
39
+ (none) Generate a commit message, commit, and push staged files
40
+ config Show the resolved provider configuration
41
+
42
+ Options:
43
+ -n, --dry-run Print the generated message and change nothing
44
+ --no-push Commit locally without pushing
45
+ -y, --yes Skip the confirmation prompt (unattended)
46
+ -f, --force Proceed despite secret or remote-server warnings
47
+ -h, --help Show this message
48
+ -v, --version Print version
49
+
50
+ On a terminal ai_git asks before committing: accept, edit, regenerate or
51
+ quit. Piped or scripted runs commit and push unattended.
52
+
53
+ Configuration (~/.ai_git/config.yml, or config.yaml):
54
+ model_name: ggml-org/gemma-4-E4B-it-GGUF:Q8_0 Model to prompt
55
+ base_url: http://127.0.0.1:8080 llama.cpp server
56
+ no_color: true Disable colored output
57
+
58
+ Run `ai_git config` to see the resolved settings and the file they
59
+ come from.
60
+ USAGE
61
+
17
62
  def start(args)
18
- command = args.first || "default"
63
+ args = args.dup
64
+ first = args.first
65
+
66
+ return puts(USAGE) if first && HELP_FLAGS.include?(first)
67
+ return puts(VERSION) if first && VERSION_FLAGS.include?(first)
19
68
 
20
- raise "Unknown subcommand: #{command}" unless SUBCOMMANDS.key?(command)
69
+ command = "default"
70
+ if first && !first.start_with?("-")
71
+ unless SUBCOMMANDS.key?(first)
72
+ warn "Unknown subcommand: #{first}"
73
+ warn USAGE
74
+ exit 1
75
+ end
76
+ command = args.shift
77
+ end
21
78
 
22
- SUBCOMMANDS[command].call
79
+ SUBCOMMANDS[command].call(args)
23
80
  end
24
81
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ai_git
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kaíque Kandy Koga
@@ -9,23 +9,36 @@ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies: []
12
- description: AI‑powered Git using SLMs
12
+ description: Generate Git commit messages from staged changes via a local llama.cpp
13
+ server, then commit and push.
13
14
  executables:
14
15
  - ai_git
15
16
  extensions: []
16
17
  extra_rdoc_files: []
17
18
  files:
19
+ - LICENSE
20
+ - README.md
18
21
  - bin/ai_git
22
+ - doc/RELEASE.md
23
+ - doc/USAGE.md
19
24
  - lib/ai_git.rb
25
+ - lib/ai_git/ai_client.rb
26
+ - lib/ai_git/commands/config.rb
27
+ - lib/ai_git/commands/default.rb
20
28
  - lib/ai_git/config.rb
21
- - lib/ai_git/default.rb
22
29
  - lib/ai_git/git.rb
23
- - lib/ai_git/review.rb
30
+ - lib/ai_git/options.rb
31
+ - lib/ai_git/prompt.rb
32
+ - lib/ai_git/secrets.rb
33
+ - lib/ai_git/ui.rb
24
34
  - lib/ai_git/version.rb
25
35
  homepage: https://github.com/kaiquekandykoga/ai_git
26
36
  licenses:
27
37
  - BSD-3-Clause
28
- metadata: {}
38
+ metadata:
39
+ source_code_uri: https://github.com/kaiquekandykoga/ai_git
40
+ bug_tracker_uri: https://github.com/kaiquekandykoga/ai_git/issues
41
+ rubygems_mfa_required: 'true'
29
42
  rdoc_options: []
30
43
  require_paths:
31
44
  - lib
@@ -40,7 +53,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
40
53
  - !ruby/object:Gem::Version
41
54
  version: '0'
42
55
  requirements: []
43
- rubygems_version: 4.0.9
56
+ rubygems_version: 4.0.16
44
57
  specification_version: 4
45
- summary: AIpowered Git using SLMs
58
+ summary: AI-powered Git commit messages using a local LLM
46
59
  test_files: []
@@ -1,177 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "benchmark"
4
- require "net/http"
5
- require "uri"
6
- require "json"
7
- require_relative "version"
8
- require_relative "git"
9
-
10
- module AIGit
11
- module Default
12
- module_function
13
-
14
- def escape_json(string)
15
- string.gsub("\\", "\\\\")
16
- .gsub('"', '\"')
17
- .gsub("\n", '\\n')
18
- .gsub("\r", '\\r')
19
- .gsub("\t", '\\t')
20
- end
21
-
22
- def generate_commit_message(diff, model_name)
23
- raise "No staged changes to generate commit message for" if diff.to_s.strip.empty?
24
-
25
- prompt = <<~PROMPT
26
- You are an expert Git commit message writer. Output ONLY the commit message — no explanations, no markdown, no backticks, no preamble.
27
-
28
- Here are the changes:
29
- #{diff}
30
-
31
- STRICT OUTPUT FORMAT (follow exactly):
32
-
33
- <short imperative title, max 72 chars>
34
-
35
- <blank line>
36
-
37
- ## Summary
38
- <2–4 bullet points covering the most important changes. Each bullet starts with a verb.>
39
-
40
- ## Why
41
- <1–3 sentences explaining the motivation or context behind the change. Omit if the reason is obvious.>
42
-
43
- RULES:
44
- - Title line: short, specific, imperative mood (e.g. "Add JWT login with refresh token support"). Avoid vague titles like "Update stuff" or "Fix bug".
45
- - Summary bullets: describe WHAT changed, not HOW the code looks. Focus on behaviour and impact.
46
- - Why section: explain the problem being solved or the goal being achieved. Skip if it adds no value.
47
- - No filler phrases ("this commit", "this PR", "as per discussion").
48
- - No line should exceed 72 characters.
49
-
50
- EXAMPLES OF GOOD OUTPUT:
51
-
52
- Add JWT-based login with refresh token support
53
-
54
- ## Summary
55
- - Implement login endpoint with access and refresh token issuance
56
- - Add token refresh route with rotation and expiry validation
57
- - Protect private routes via middleware that verifies access tokens
58
- - Store refresh tokens using encrypted HTTP-only cookies
59
-
60
- ## Why
61
- Users were being logged out on every page reload. Refresh tokens allow
62
- sessions to persist securely without requiring re-authentication.
63
-
64
- ---
65
-
66
- Prevent nil crash when user preferences are missing
67
-
68
- ## Summary
69
- - Add nil guard in ReportGenerator#process before accessing preferences
70
- - Fall back to system defaults when preferences object is absent
71
-
72
- ## Why
73
- Reports were raising NoMethodError in production for users created
74
- before the preferences feature shipped.
75
-
76
- ---
77
-
78
- Now generate the commit message:
79
- PROMPT
80
-
81
- if AIGit::Config.request_format == :ollama
82
- json_body = {
83
- model: model_name,
84
- prompt: prompt,
85
- stream: false,
86
- temperature: 0.3,
87
- top_p: 0.9,
88
- stop: ["\n\n\n", "```", "Here is", "The commit message"],
89
- num_predict: 400
90
- }.to_json
91
-
92
- uri = URI("#{AIGit::Config.base_url}#{AIGit::Config.endpoint}")
93
- request = Net::HTTP::Post.new(uri)
94
- request["Content-Type"] = "application/json"
95
- request.body = json_body
96
-
97
- response = Net::HTTP.start(uri.host, uri.port, read_timeout: 120) do |http|
98
- http.request(request)
99
- end
100
-
101
- raise "Failed to connect to #{AIGit::Config.provider}. Is it running?" unless response.is_a?(Net::HTTPSuccess)
102
-
103
- data = JSON.parse(response.body)
104
- message = data["response"].to_s.strip
105
- else # Jan AI
106
- json_body = {
107
- model: model_name,
108
- messages: [{ role: "user", content: prompt }],
109
- stream: false,
110
- temperature: 0.3
111
- }.to_json
112
-
113
- uri = URI("#{AIGit::Config.base_url}#{AIGit::Config.endpoint}")
114
- request = Net::HTTP::Post.new(uri)
115
- request["Content-Type"] = "application/json"
116
- request.body = json_body
117
-
118
- response = Net::HTTP.start(uri.host, uri.port, read_timeout: 120) do |http|
119
- http.request(request)
120
- end
121
-
122
- raise "Failed to connect to #{AIGit::Config.provider}. Is it running?" unless response.is_a?(Net::HTTPSuccess)
123
-
124
- data = JSON.parse(response.body)
125
- message = data["choices"][0]["message"]["content"].to_s.strip
126
- end
127
-
128
- message = message.gsub(/^(Here is|The commit message is|```|json|markdown)/i, "")
129
- .gsub(/^>\s*/, "")
130
- .gsub(/\\n/, "\n")
131
- .strip
132
-
133
- lines = message.lines.map(&:strip)
134
- lines.reject! { |line| line.match?(/^(Here|Output|Generated|Based on|The changes)/i) }
135
-
136
- message = lines.join("\n").strip
137
-
138
- message = "chore: update code" if message.lines.count < 1 || message.strip.empty?
139
-
140
- message
141
- end
142
-
143
- def call
144
- provider = AIGit::Config.provider
145
- model_name = AIGit::Config.model_name
146
-
147
- staged = AIGit::Git.staged_files
148
- abort "Error: No staged files. Use `git add` first." if staged.to_s.strip.empty?
149
-
150
- diff = AIGit::Git.diff
151
- branch = AIGit::Git.current_branch
152
-
153
- puts "\e[1mAI Provider:\e[0m #{provider}"
154
- puts "\e[1mModel Name:\e[0m #{model_name}"
155
- puts "\e[1mStaged Files:\e[0m #{staged}"
156
- puts "\e[1mBranch:\e[0m #{branch}"
157
- puts "\e[1mAI Generating Commit Message\e[0m"
158
-
159
- result = Benchmark.measure do
160
- message = generate_commit_message(diff, model_name)
161
- message = message.gsub(/\n{2,}/, "\n")
162
-
163
- puts "\e[1mCommit Message:\e[0m\n\n#{message}\n"
164
-
165
- escaped_msg = message.gsub(/[\\"`$]/) { |c| "\\#{c}" }
166
- AIGit::Git.run_command("git", "commit -m \"#{escaped_msg}\"")
167
- puts "\e[1mGit Commited\e[0m"
168
-
169
- AIGit::Git.run_command("git", "push -u origin HEAD")
170
- puts "\e[1mGit Pushed\e[0m"
171
- end
172
-
173
- puts "\e[1mBenchmark\e[0m"
174
- puts result
175
- end
176
- end
177
- end
data/lib/ai_git/review.rb DELETED
@@ -1,173 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "benchmark"
4
- require "net/http"
5
- require "uri"
6
- require "json"
7
- require_relative "version"
8
- require_relative "git"
9
-
10
- module AIGit
11
- module Review
12
- module_function
13
-
14
- def generate_review(diff, model_name)
15
- raise "No staged changes to review" if diff.to_s.strip.empty?
16
-
17
- prompt = <<~PROMPT
18
- You are a senior software engineer conducting a thorough code review. Output ONLY the review — no explanations, no markdown preamble, no backticks, no preamble.
19
-
20
- Here are the staged changes:
21
- #{diff}
22
-
23
- STRICT OUTPUT FORMAT (follow exactly):
24
-
25
- ## Summary
26
- <2–5 bullet points describing what this change does at a high level. Each bullet starts with a verb.>
27
-
28
- ## Suggestions
29
- <For each issue or improvement, use this format:>
30
-
31
- **<File and location, e.g. "app/models/user.rb">**
32
- - <Concise description of the issue or suggestion. Start with a verb. Be specific.>
33
- - <Another suggestion for the same file, if applicable.>
34
-
35
- <Repeat the file block for each file with suggestions. Omit files that look clean.>
36
-
37
- ## Verdict
38
- <One of: ✅ Looks good | ⚠️ Minor issues | 🚨 Needs attention>
39
- <One sentence explaining the verdict.>
40
-
41
- RULES:
42
- - Summary: describe WHAT the change does, not HOW the code looks. Focus on behaviour and intent.
43
- - Suggestions: flag real issues — bugs, edge cases, security problems, missing error handling, naming confusion, performance concerns, missing tests. Do NOT invent problems. If a file is clean, omit it entirely.
44
- - Be direct and constructive. No filler like "great job" or "consider possibly maybe".
45
- - No line should exceed 72 characters.
46
- - If the diff is a minor or trivial change (e.g. version bump, typo fix), keep the review brief.
47
-
48
- EXAMPLES OF GOOD OUTPUT:
49
-
50
- ## Summary
51
- - Add password reset flow with tokenised email verification
52
- - Introduce PasswordResetMailer with expiry-aware token links
53
- - Add DB migration for reset_token and reset_token_expires_at columns
54
-
55
- ## Suggestions
56
-
57
- **app/models/user.rb**
58
- - Ensure reset_token is invalidated after successful use to
59
- prevent token reuse attacks.
60
- - Add an index on reset_token column for fast lookup queries.
61
-
62
- **app/controllers/passwords_controller.rb**
63
- - Handle the case where the token has expired with a user-facing
64
- error message rather than a silent redirect.
65
-
66
- ## Verdict
67
- ⚠️ Minor issues
68
- Core logic is solid but token invalidation and expiry feedback
69
- need addressing before merge.
70
-
71
- ---
72
-
73
- ## Summary
74
- - Fix nil guard in ReportGenerator when preferences are absent
75
-
76
- ## Suggestions
77
-
78
- ## Verdict
79
- ✅ Looks good
80
- Safe defensive fix with no side effects.
81
-
82
- ---
83
-
84
- Now review the staged changes:
85
- PROMPT
86
-
87
- if AIGit::Config.request_format == :ollama
88
- json_body = {
89
- model: model_name,
90
- prompt: prompt,
91
- stream: false,
92
- temperature: 0.2,
93
- top_p: 0.9,
94
- stop: ["\n\n\n", "```", "Here is", "The review"],
95
- num_predict: 600
96
- }.to_json
97
-
98
- uri = URI("#{AIGit::Config.base_url}#{AIGit::Config.endpoint}")
99
- request = Net::HTTP::Post.new(uri)
100
- request["Content-Type"] = "application/json"
101
- request.body = json_body
102
-
103
- response = Net::HTTP.start(uri.host, uri.port, read_timeout: 120) do |http|
104
- http.request(request)
105
- end
106
-
107
- raise "Failed to connect to #{AIGit::Config.provider}. Is it running?" unless response.is_a?(Net::HTTPSuccess)
108
-
109
- data = JSON.parse(response.body)
110
- review = data["response"].to_s.strip
111
- else # Jan AI
112
- json_body = {
113
- model: model_name,
114
- messages: [{ role: "user", content: prompt }],
115
- stream: false,
116
- temperature: 0.2
117
- }.to_json
118
-
119
- uri = URI("#{AIGit::Config.base_url}#{AIGit::Config.endpoint}")
120
- request = Net::HTTP::Post.new(uri)
121
- request["Content-Type"] = "application/json"
122
- request.body = json_body
123
-
124
- response = Net::HTTP.start(uri.host, uri.port, read_timeout: 120) do |http|
125
- http.request(request)
126
- end
127
-
128
- raise "Failed to connect to #{AIGit::Config.provider}. Is it running?" unless response.is_a?(Net::HTTPSuccess)
129
-
130
- data = JSON.parse(response.body)
131
- review = data["choices"][0]["message"]["content"].to_s.strip
132
- end
133
-
134
- review = review.gsub(/^(Here is|The review is|```|json|markdown)/i, "")
135
- .gsub(/^>\s*/, "")
136
- .gsub(/\\n/, "\n")
137
- .strip
138
-
139
- lines = review.lines.map(&:strip)
140
- lines.reject! { |line| line.match?(/^(Here|Output|Generated|Based on|The changes)/i) }
141
-
142
- review = lines.join("\n").strip
143
- review = "No review generated." if review.empty?
144
-
145
- review
146
- end
147
-
148
- def call
149
- provider = AIGit::Config.provider
150
- model_name = AIGit::Config.model_name
151
-
152
- staged = AIGit::Git.staged_files
153
- abort "Error: No staged files. Use `git add` first." if staged.to_s.strip.empty?
154
-
155
- diff = AIGit::Git.diff
156
- branch = AIGit::Git.current_branch
157
-
158
- puts "\e[1mAI Provider:\e[0m #{provider}"
159
- puts "\e[1mModel Name:\e[0m #{model_name}"
160
- puts "\e[1mStaged Files:\e[0m #{staged}"
161
- puts "\e[1mBranch:\e[0m #{branch}"
162
- puts "\e[1mAI Reviewing Changes\e[0m"
163
-
164
- result = Benchmark.measure do
165
- review = generate_review(diff, model_name)
166
- puts "\e[1mCode Review:\e[0m\n\n#{review}\n"
167
- end
168
-
169
- puts "\e[1mBenchmark\e[0m"
170
- puts result
171
- end
172
- end
173
- end