n2b 2.0.0 → 2.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 +4 -4
- data/lib/n2b/base.rb +139 -18
- data/lib/n2b/cli.rb +7 -4
- data/lib/n2b/config/models.yml +19 -3
- data/lib/n2b/jira_client.rb +2 -1
- data/lib/n2b/llm/gemini.rb +6 -1
- data/lib/n2b/llm/vertex_ai.rb +225 -0
- data/lib/n2b/merge_cli.rb +3 -0
- data/lib/n2b/model_config.rb +8 -0
- data/lib/n2b/version.rb +1 -1
- data/lib/n2b.rb +1 -0
- metadata +18 -4
- data/bin/branch-audit.sh +0 -397
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 904b66daf8fd1cdb0a4fba961fc0431dbf5bf841c022d3b31b52daa7c549f02d
|
4
|
+
data.tar.gz: e89ae0125af9f1c2c40ac2ad14f0a5ced1dc6bd882e88aa81ccf34b2d958528f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 528eefd2847a246bcc5282b1ea360477737c9c85e8643fadce3be1eaaa6c7b64464e9cdba1d1a59d8bcad4eec25fd912dd5e9d04e1abbd5f31ab39c646b9b363
|
7
|
+
data.tar.gz: abcee779b759722282f5b48046c97418d215e0ff528b9c6ea76be4cca09335256606150eb7c18c3a2bff978559f5560768b0fdf02c35bbcd05a65023de3ff4b0
|
data/lib/n2b/base.rb
CHANGED
@@ -46,13 +46,19 @@ module N2B
|
|
46
46
|
# Determine if it's effectively a first-time setup for core LLM details
|
47
47
|
is_first_time_core_setup = config['llm'].nil?
|
48
48
|
|
49
|
-
|
49
|
+
# Check if configuration is incomplete based on provider type
|
50
|
+
config_incomplete = config['llm'].nil? || reconfigure ||
|
51
|
+
(config['llm'] == 'vertexai' && (config['vertex_credential_file'].nil? || config['vertex_credential_file'].empty?)) ||
|
52
|
+
(config['llm'] == 'ollama' && false) || # Ollama doesn't require API key, so never incomplete due to missing key
|
53
|
+
(['claude', 'openai', 'gemini', 'openrouter'].include?(config['llm']) && (api_key.nil? || api_key == ''))
|
54
|
+
|
55
|
+
if config_incomplete
|
50
56
|
puts "--- N2B Core LLM Configuration ---"
|
51
|
-
print "Choose a language model to use (1:claude, 2:openai, 3:gemini, 4:openrouter, 5:ollama) [current: #{config['llm']}]: "
|
57
|
+
print "Choose a language model to use (1:claude, 2:openai, 3:gemini (API Key), 4:openrouter, 5:ollama, 6:vertexai (Credential File)) [current: #{config['llm']}]: "
|
52
58
|
llm_choice = $stdin.gets.chomp
|
53
59
|
llm_choice = config['llm'] if llm_choice.empty? && !config['llm'].nil? # Keep current if input is empty
|
54
60
|
|
55
|
-
unless ['claude', 'openai', 'gemini', 'openrouter', 'ollama', '1', '2', '3', '4', '5'].include?(llm_choice)
|
61
|
+
unless ['claude', 'openai', 'gemini', 'openrouter', 'ollama', 'vertexai', '1', '2', '3', '4', '5', '6'].include?(llm_choice)
|
56
62
|
puts "Invalid language model selection. Defaulting to 'claude' or previous if available."
|
57
63
|
llm_choice = config['llm'] || 'claude' # Fallback
|
58
64
|
end
|
@@ -60,10 +66,11 @@ module N2B
|
|
60
66
|
selected_llm = case llm_choice
|
61
67
|
when '1', 'claude' then 'claude'
|
62
68
|
when '2', 'openai' then 'openai'
|
63
|
-
when '3', 'gemini' then 'gemini'
|
69
|
+
when '3', 'gemini' then 'gemini' # This is now explicitly API Key based Gemini
|
64
70
|
when '4', 'openrouter' then 'openrouter'
|
65
71
|
when '5', 'ollama' then 'ollama'
|
66
|
-
|
72
|
+
when '6', 'vertexai' then 'vertexai' # New Vertex AI (credential file)
|
73
|
+
else config['llm'] || 'claude'
|
67
74
|
end
|
68
75
|
config['llm'] = selected_llm
|
69
76
|
|
@@ -71,18 +78,76 @@ module N2B
|
|
71
78
|
puts "\n--- Ollama Specific Configuration ---"
|
72
79
|
puts "Ollama typically runs locally and doesn't require an API key."
|
73
80
|
|
74
|
-
# Use new model configuration system
|
75
81
|
current_model = config['model']
|
76
|
-
model_choice = N2B::ModelConfig.get_model_choice(selected_llm, current_model)
|
82
|
+
model_choice = N2B::ModelConfig.get_model_choice(selected_llm, current_model)
|
77
83
|
config['model'] = model_choice
|
78
84
|
|
79
85
|
current_ollama_api_url = config['ollama_api_url'] || N2M::Llm::Ollama::DEFAULT_OLLAMA_API_URI
|
80
86
|
print "Enter Ollama API base URL [current: #{current_ollama_api_url}]: "
|
81
87
|
ollama_api_url_input = $stdin.gets.chomp
|
82
88
|
config['ollama_api_url'] = ollama_api_url_input.empty? ? current_ollama_api_url : ollama_api_url_input
|
83
|
-
|
84
|
-
|
85
|
-
#
|
89
|
+
|
90
|
+
config.delete('access_key')
|
91
|
+
config.delete('gemini_credential_file') # old key
|
92
|
+
config.delete('vertex_credential_file') # new key for Vertex
|
93
|
+
|
94
|
+
elsif selected_llm == 'vertexai'
|
95
|
+
puts "\n--- Vertex AI (Credential File) Specific Configuration ---"
|
96
|
+
current_vertex_credential_file = config['vertex_credential_file']
|
97
|
+
print "Enter your Google Cloud credential file path for Vertex AI #{current_vertex_credential_file ? '[leave blank to keep current]' : ''}: "
|
98
|
+
vertex_credential_file_input = $stdin.gets.chomp
|
99
|
+
if !vertex_credential_file_input.empty?
|
100
|
+
config['vertex_credential_file'] = vertex_credential_file_input
|
101
|
+
elsif current_vertex_credential_file
|
102
|
+
config['vertex_credential_file'] = current_vertex_credential_file
|
103
|
+
else
|
104
|
+
config['vertex_credential_file'] = nil
|
105
|
+
end
|
106
|
+
|
107
|
+
# Ask for Vertex AI region
|
108
|
+
current_vertex_location = config['vertex_location'] || 'us-central1'
|
109
|
+
puts "\nVertex AI Region Selection:"
|
110
|
+
puts "Common regions:"
|
111
|
+
puts " 1. us-central1 (Iowa, USA) [default]"
|
112
|
+
puts " 2. us-east1 (South Carolina, USA)"
|
113
|
+
puts " 3. us-west1 (Oregon, USA)"
|
114
|
+
puts " 4. europe-west1 (Belgium)"
|
115
|
+
puts " 5. europe-west4 (Netherlands)"
|
116
|
+
puts " 6. asia-northeast1 (Tokyo, Japan)"
|
117
|
+
puts " 7. asia-southeast1 (Singapore)"
|
118
|
+
puts " 8. custom (enter your own region)"
|
119
|
+
print "Choose region (1-8) or enter region name [current: #{current_vertex_location}]: "
|
120
|
+
|
121
|
+
region_choice = $stdin.gets.chomp
|
122
|
+
if region_choice.empty?
|
123
|
+
config['vertex_location'] = current_vertex_location
|
124
|
+
else
|
125
|
+
case region_choice
|
126
|
+
when '1' then config['vertex_location'] = 'us-central1'
|
127
|
+
when '2' then config['vertex_location'] = 'us-east1'
|
128
|
+
when '3' then config['vertex_location'] = 'us-west1'
|
129
|
+
when '4' then config['vertex_location'] = 'europe-west1'
|
130
|
+
when '5' then config['vertex_location'] = 'europe-west4'
|
131
|
+
when '6' then config['vertex_location'] = 'asia-northeast1'
|
132
|
+
when '7' then config['vertex_location'] = 'asia-southeast1'
|
133
|
+
when '8'
|
134
|
+
print "Enter custom region (e.g., europe-west2): "
|
135
|
+
custom_region = $stdin.gets.chomp
|
136
|
+
config['vertex_location'] = custom_region.empty? ? current_vertex_location : custom_region
|
137
|
+
else
|
138
|
+
# Treat as direct region input
|
139
|
+
config['vertex_location'] = region_choice
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
config.delete('access_key')
|
144
|
+
config.delete('ollama_api_url')
|
145
|
+
config.delete('gemini_credential_file') # old key
|
146
|
+
|
147
|
+
current_model = config['model']
|
148
|
+
model_choice = N2B::ModelConfig.get_model_choice(selected_llm, current_model)
|
149
|
+
config['model'] = model_choice
|
150
|
+
else # API Key based LLMs: claude, openai, gemini (API Key), openrouter
|
86
151
|
puts "\n--- #{selected_llm.capitalize} Specific Configuration ---"
|
87
152
|
current_api_key = config['access_key']
|
88
153
|
print "Enter your #{selected_llm} API key #{ current_api_key.nil? || current_api_key.empty? ? '' : '[leave blank to keep current]' }: "
|
@@ -94,9 +159,12 @@ module N2B
|
|
94
159
|
puts "API key is required for #{selected_llm}."
|
95
160
|
exit 1
|
96
161
|
end
|
162
|
+
config.delete('vertex_credential_file') # new key for Vertex
|
163
|
+
config.delete('ollama_api_url')
|
164
|
+
config.delete('gemini_credential_file') # old key
|
97
165
|
|
98
166
|
current_model = config['model']
|
99
|
-
model_choice = N2B::ModelConfig.get_model_choice(selected_llm, current_model)
|
167
|
+
model_choice = N2B::ModelConfig.get_model_choice(selected_llm, current_model)
|
100
168
|
config['model'] = model_choice
|
101
169
|
|
102
170
|
if selected_llm == 'openrouter'
|
@@ -336,23 +404,76 @@ module N2B
|
|
336
404
|
def validate_config(config)
|
337
405
|
errors = []
|
338
406
|
|
339
|
-
# Validate LLM
|
407
|
+
# Validate LLM provider selection
|
340
408
|
if config['llm'].nil? || config['llm'].empty?
|
341
409
|
errors << "LLM provider not specified"
|
342
410
|
end
|
343
411
|
|
344
|
-
# Validate model name
|
412
|
+
# Validate model name (existing logic can likely stay as is)
|
345
413
|
if config['model'].nil? || config['model'].empty?
|
346
414
|
errors << "Model not specified"
|
347
|
-
elsif config['model'].length < 3
|
415
|
+
elsif config['model'].length < 3 # Example of an existing check
|
348
416
|
errors << "Model name '#{config['model']}' seems too short - might be invalid"
|
349
|
-
elsif %w[y n yes no true false].include?(config['model'].downcase)
|
417
|
+
elsif %w[y n yes no true false].include?(config['model'].downcase) # Example
|
350
418
|
errors << "Model name '#{config['model']}' appears to be a boolean response rather than a model name"
|
351
419
|
end
|
352
420
|
|
353
|
-
#
|
354
|
-
|
355
|
-
|
421
|
+
# Provider-specific validation
|
422
|
+
case config['llm']
|
423
|
+
when 'gemini' # API Key based Gemini
|
424
|
+
if config['access_key'].nil? || config['access_key'].empty?
|
425
|
+
errors << "API key missing for gemini provider"
|
426
|
+
end
|
427
|
+
if config['vertex_credential_file'] && !config['vertex_credential_file'].empty?
|
428
|
+
errors << "Vertex AI credential file should not be present when gemini (API Key) provider is selected"
|
429
|
+
end
|
430
|
+
# Ensure old gemini_credential_file key is also not present
|
431
|
+
if config['gemini_credential_file'] && !config['gemini_credential_file'].empty?
|
432
|
+
errors << "Old gemini_credential_file key should not be present. Use 'gemini' (API key) or 'vertexai' (credential file)."
|
433
|
+
end
|
434
|
+
|
435
|
+
when 'vertexai' # Credential File based Vertex AI
|
436
|
+
if config['vertex_credential_file'].nil? || config['vertex_credential_file'].empty?
|
437
|
+
errors << "Credential file path for Vertex AI not provided"
|
438
|
+
else
|
439
|
+
# Ensure File.exist? is only called if the path is not nil and not empty
|
440
|
+
unless File.exist?(config['vertex_credential_file'])
|
441
|
+
errors << "Credential file missing or invalid at #{config['vertex_credential_file']} for Vertex AI"
|
442
|
+
end
|
443
|
+
end
|
444
|
+
if config['access_key'] && !config['access_key'].empty?
|
445
|
+
errors << "API key (access_key) should not be present when Vertex AI provider is selected"
|
446
|
+
end
|
447
|
+
# Ensure old gemini_credential_file key is also not present
|
448
|
+
if config['gemini_credential_file'] && !config['gemini_credential_file'].empty?
|
449
|
+
errors << "Old gemini_credential_file key should not be present when Vertex AI is selected."
|
450
|
+
end
|
451
|
+
|
452
|
+
when 'ollama'
|
453
|
+
# No specific key/credential validation for Ollama here, but ensure others are not present
|
454
|
+
if config['access_key'] && !config['access_key'].empty?
|
455
|
+
errors << "API key (access_key) should not be present when Ollama provider is selected"
|
456
|
+
end
|
457
|
+
if config['vertex_credential_file'] && !config['vertex_credential_file'].empty?
|
458
|
+
errors << "Vertex AI credential file should not be present when Ollama provider is selected"
|
459
|
+
end
|
460
|
+
if config['gemini_credential_file'] && !config['gemini_credential_file'].empty?
|
461
|
+
errors << "Old gemini_credential_file key should not be present when Ollama is selected."
|
462
|
+
end
|
463
|
+
# Ollama specific validations like ollama_api_url could be here if needed
|
464
|
+
|
465
|
+
when 'claude', 'openai', 'openrouter' # Other API key based providers
|
466
|
+
if config['access_key'].nil? || config['access_key'].empty?
|
467
|
+
errors << "API key missing for #{config['llm']} provider"
|
468
|
+
end
|
469
|
+
if config['vertex_credential_file'] && !config['vertex_credential_file'].empty?
|
470
|
+
errors << "Vertex AI credential file should not be present for #{config['llm']} provider"
|
471
|
+
end
|
472
|
+
if config['gemini_credential_file'] && !config['gemini_credential_file'].empty?
|
473
|
+
errors << "Old gemini_credential_file key should not be present for #{config['llm']} provider."
|
474
|
+
end
|
475
|
+
# else
|
476
|
+
# This case means llm is nil or an unknown provider, already handled by the first check.
|
356
477
|
end
|
357
478
|
|
358
479
|
# Validate editor configuration (optional, so more like warnings or info)
|
data/lib/n2b/cli.rb
CHANGED
@@ -296,13 +296,14 @@ REQUIREMENTS_BLOCK
|
|
296
296
|
end
|
297
297
|
|
298
298
|
def append_to_llm_history_file(commands)
|
299
|
-
File.open(
|
299
|
+
File.open(self.class.history_file, 'a') do |file|
|
300
300
|
file.puts(commands)
|
301
301
|
end
|
302
302
|
end
|
303
|
-
|
303
|
+
|
304
304
|
def read_llm_history_file
|
305
|
-
|
305
|
+
history_file_path = self.class.history_file
|
306
|
+
history = File.read(history_file_path) if File.exist?(history_file_path)
|
306
307
|
history ||= ''
|
307
308
|
# limit to 20 most recent commands
|
308
309
|
history.split("\n").last(20).join("\n")
|
@@ -316,8 +317,10 @@ REQUIREMENTS_BLOCK
|
|
316
317
|
N2B::Llm::OpenAi.new(config)
|
317
318
|
when 'claude'
|
318
319
|
N2B::Llm::Claude.new(config)
|
319
|
-
when 'gemini'
|
320
|
+
when 'gemini' # This is for API Key based Gemini
|
320
321
|
N2B::Llm::Gemini.new(config)
|
322
|
+
when 'vertexai' # This is for Credential File based Vertex AI
|
323
|
+
N2B::Llm::VertexAi.new(config)
|
321
324
|
when 'openrouter'
|
322
325
|
N2B::Llm::OpenRouter.new(config)
|
323
326
|
when 'ollama'
|
data/lib/n2b/config/models.yml
CHANGED
@@ -25,9 +25,25 @@ openai:
|
|
25
25
|
|
26
26
|
gemini:
|
27
27
|
suggested:
|
28
|
-
gemini-2.
|
29
|
-
gemini-2.
|
30
|
-
|
28
|
+
gemini-2.0-flash: "gemini-2.0-flash-001"
|
29
|
+
gemini-2.0-flash-lite: "gemini-2.0-flash-lite-001"
|
30
|
+
gemini-2.5-flash: "gemini-2.5-flash-preview"
|
31
|
+
gemini-2.5-pro: "gemini-2.5-pro-preview"
|
32
|
+
# Legacy models (may not be available in all regions)
|
33
|
+
gemini-2.5-flash-05-20: "gemini-2.5-flash-preview-05-20"
|
34
|
+
gemini-2.5-pro-05-06: "gemini-2.5-pro-preview-05-06"
|
35
|
+
default: "gemini-2.0-flash"
|
36
|
+
|
37
|
+
vertexai:
|
38
|
+
suggested:
|
39
|
+
gemini-2.0-flash: "gemini-2.0-flash-001"
|
40
|
+
gemini-2.0-flash-lite: "gemini-2.0-flash-lite-001"
|
41
|
+
gemini-2.5-flash: "gemini-2.5-flash-preview"
|
42
|
+
gemini-2.5-pro: "gemini-2.5-pro-preview"
|
43
|
+
# Legacy models (may not be available in all regions)
|
44
|
+
gemini-2.5-flash-05-20: "gemini-2.5-flash-preview-05-20"
|
45
|
+
gemini-2.5-pro-05-06: "gemini-2.5-pro-preview-05-06"
|
46
|
+
default: "gemini-2.0-flash"
|
31
47
|
|
32
48
|
openrouter:
|
33
49
|
suggested:
|
data/lib/n2b/jira_client.rb
CHANGED
@@ -2,6 +2,7 @@ require 'net/http'
|
|
2
2
|
require 'uri'
|
3
3
|
require 'json'
|
4
4
|
require 'base64'
|
5
|
+
require 'time'
|
5
6
|
require_relative 'template_engine'
|
6
7
|
|
7
8
|
module N2B
|
@@ -390,7 +391,7 @@ module N2B
|
|
390
391
|
else
|
391
392
|
formatted_date = created
|
392
393
|
end
|
393
|
-
rescue
|
394
|
+
rescue => e
|
394
395
|
formatted_date = created
|
395
396
|
end
|
396
397
|
|
data/lib/n2b/llm/gemini.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'net/http'
|
2
2
|
require 'json'
|
3
3
|
require 'uri'
|
4
|
+
# Removed googleauth require
|
4
5
|
require_relative '../model_config'
|
5
6
|
|
6
7
|
module N2B
|
@@ -9,7 +10,7 @@ module N2B
|
|
9
10
|
API_URI = URI.parse('https://generativelanguage.googleapis.com/v1beta/models')
|
10
11
|
|
11
12
|
def initialize(config)
|
12
|
-
@config = config
|
13
|
+
@config = config # Used for access_key and model
|
13
14
|
end
|
14
15
|
|
15
16
|
def get_model_name
|
@@ -29,6 +30,8 @@ module N2B
|
|
29
30
|
request = Net::HTTP::Post.new(uri)
|
30
31
|
request.content_type = 'application/json'
|
31
32
|
|
33
|
+
# Removed Authorization header and token fetching logic
|
34
|
+
|
32
35
|
request.body = JSON.dump({
|
33
36
|
"contents" => [{
|
34
37
|
"parts" => [{
|
@@ -82,6 +85,8 @@ module N2B
|
|
82
85
|
request = Net::HTTP::Post.new(uri)
|
83
86
|
request.content_type = 'application/json'
|
84
87
|
|
88
|
+
# Removed Authorization header and token fetching logic
|
89
|
+
|
85
90
|
request.body = JSON.dump({
|
86
91
|
"contents" => [{
|
87
92
|
"parts" => [{
|
@@ -0,0 +1,225 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'json'
|
3
|
+
require 'uri'
|
4
|
+
require 'googleauth' # For service account authentication
|
5
|
+
require_relative '../model_config'
|
6
|
+
require_relative '../errors'
|
7
|
+
|
8
|
+
module N2B
|
9
|
+
module Llm
|
10
|
+
class VertexAi
|
11
|
+
# Vertex AI API endpoint format
|
12
|
+
DEFAULT_LOCATION = 'us-central1'
|
13
|
+
COMMON_LOCATIONS = [
|
14
|
+
'us-central1', # Iowa, USA
|
15
|
+
'us-east1', # South Carolina, USA
|
16
|
+
'us-west1', # Oregon, USA
|
17
|
+
'europe-west1', # Belgium
|
18
|
+
'europe-west4', # Netherlands
|
19
|
+
'asia-northeast1', # Tokyo, Japan
|
20
|
+
'asia-southeast1' # Singapore
|
21
|
+
].freeze
|
22
|
+
|
23
|
+
# HTTP timeout in seconds
|
24
|
+
REQUEST_TIMEOUT = 60
|
25
|
+
|
26
|
+
def initialize(config)
|
27
|
+
@config = config # Contains 'vertex_credential_file' and 'model'
|
28
|
+
@project_id = nil
|
29
|
+
@location = DEFAULT_LOCATION
|
30
|
+
load_project_info
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def load_project_info
|
36
|
+
# Extract project_id from the credential file
|
37
|
+
credential_data = JSON.parse(File.read(@config['vertex_credential_file']))
|
38
|
+
@project_id = credential_data['project_id']
|
39
|
+
|
40
|
+
# Allow location override from config, with intelligent defaults
|
41
|
+
@location = determine_location
|
42
|
+
rescue JSON::ParserError => e
|
43
|
+
raise N2B::LlmApiError.new("Invalid JSON in credential file: #{e.message}")
|
44
|
+
rescue Errno::ENOENT => e
|
45
|
+
raise N2B::LlmApiError.new("Credential file not found: #{e.message}")
|
46
|
+
rescue => e
|
47
|
+
raise N2B::LlmApiError.new("Failed to load project info from credential file: #{e.message}")
|
48
|
+
end
|
49
|
+
|
50
|
+
def determine_location
|
51
|
+
# 1. Use explicit config if provided
|
52
|
+
return @config['vertex_location'] if @config['vertex_location']
|
53
|
+
|
54
|
+
# 2. Try to detect from project_id patterns (some projects have region hints)
|
55
|
+
# 3. Default to us-central1 but provide helpful error message if it fails
|
56
|
+
DEFAULT_LOCATION
|
57
|
+
end
|
58
|
+
|
59
|
+
def build_api_uri(model)
|
60
|
+
"https://#{@location}-aiplatform.googleapis.com/v1/projects/#{@project_id}/locations/#{@location}/publishers/google/models/#{model}:generateContent"
|
61
|
+
end
|
62
|
+
|
63
|
+
public
|
64
|
+
|
65
|
+
def get_model_name
|
66
|
+
# Resolve model name using the centralized configuration for 'vertexai'
|
67
|
+
model_name = N2B::ModelConfig.resolve_model('vertexai', @config['model'])
|
68
|
+
if model_name.nil? || model_name.empty?
|
69
|
+
# Fallback to default if no model specified for vertexai
|
70
|
+
model_name = N2B::ModelConfig.resolve_model('vertexai', N2B::ModelConfig.default_model('vertexai'))
|
71
|
+
end
|
72
|
+
# If still no model, a generic default could be used, or an error raised.
|
73
|
+
# For now, assume ModelConfig handles returning a usable default or nil.
|
74
|
+
# If ModelConfig.resolve_model can return nil and that's an issue, add handling here.
|
75
|
+
# For example, if model_name is still nil, raise an error or use a hardcoded default.
|
76
|
+
# Let's assume ModelConfig provides a valid model or a sensible default from models.yml.
|
77
|
+
model_name
|
78
|
+
end
|
79
|
+
|
80
|
+
def make_request(content)
|
81
|
+
model = get_model_name
|
82
|
+
raise N2B::LlmApiError.new("No model configured for Vertex AI.") if model.nil? || model.empty?
|
83
|
+
|
84
|
+
uri = URI.parse(build_api_uri(model))
|
85
|
+
|
86
|
+
request = Net::HTTP::Post.new(uri)
|
87
|
+
request.content_type = 'application/json'
|
88
|
+
|
89
|
+
begin
|
90
|
+
scope = 'https://www.googleapis.com/auth/cloud-platform'
|
91
|
+
authorizer = Google::Auth::ServiceAccountCredentials.make_creds(
|
92
|
+
json_key_io: File.open(@config['vertex_credential_file']),
|
93
|
+
scope: scope
|
94
|
+
)
|
95
|
+
access_token = authorizer.fetch_access_token!['access_token']
|
96
|
+
request['Authorization'] = "Bearer #{access_token}"
|
97
|
+
rescue StandardError => e
|
98
|
+
raise N2B::LlmApiError.new("Vertex AI - Failed to obtain Google Cloud access token: #{e.message}")
|
99
|
+
end
|
100
|
+
|
101
|
+
request.body = JSON.dump({
|
102
|
+
"contents" => [{
|
103
|
+
"role" => "user",
|
104
|
+
"parts" => [{
|
105
|
+
"text" => content
|
106
|
+
}]
|
107
|
+
}],
|
108
|
+
"generationConfig" => {
|
109
|
+
"responseMimeType" => "application/json" # Requesting JSON output from the LLM
|
110
|
+
}
|
111
|
+
})
|
112
|
+
|
113
|
+
begin
|
114
|
+
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
|
115
|
+
http.read_timeout = REQUEST_TIMEOUT
|
116
|
+
http.open_timeout = 30
|
117
|
+
http.request(request)
|
118
|
+
end
|
119
|
+
rescue Net::TimeoutError, Net::ReadTimeout, Net::OpenTimeout => e
|
120
|
+
error_msg = "Vertex AI request timed out (region: #{@location}): #{e.message}"
|
121
|
+
error_msg += "\n\nThis might be a region issue. Try reconfiguring with 'n2b -c' and select a different region."
|
122
|
+
error_msg += "\nFor EU users, try: europe-west1 (Belgium) or europe-west4 (Netherlands)"
|
123
|
+
error_msg += "\nCommon regions: #{COMMON_LOCATIONS.join(', ')}"
|
124
|
+
raise N2B::LlmApiError.new(error_msg)
|
125
|
+
rescue => e
|
126
|
+
raise N2B::LlmApiError.new("Vertex AI network error: #{e.message}")
|
127
|
+
end
|
128
|
+
|
129
|
+
if response.code != '200'
|
130
|
+
error_msg = "Vertex AI LLM API Error: #{response.code} #{response.message} - #{response.body}"
|
131
|
+
if response.code == '404'
|
132
|
+
error_msg += "\n\nThis might be a region or model availability issue. Current region: #{@location}"
|
133
|
+
error_msg += "\nNote: Google models via Vertex AI are not available in all regions."
|
134
|
+
error_msg += "\nTry reconfiguring with 'n2b -c' and:"
|
135
|
+
error_msg += "\n 1. Select a different region (Common regions: #{COMMON_LOCATIONS.join(', ')})"
|
136
|
+
error_msg += "\n 2. Choose a different model (some models are only available in specific regions)"
|
137
|
+
end
|
138
|
+
raise N2B::LlmApiError.new(error_msg)
|
139
|
+
end
|
140
|
+
|
141
|
+
parsed_response = JSON.parse(response.body)
|
142
|
+
# Vertex AI response structure is the same as Gemini API
|
143
|
+
answer = parsed_response['candidates'].first['content']['parts'].first['text']
|
144
|
+
|
145
|
+
begin
|
146
|
+
if answer.strip.start_with?('{') && answer.strip.end_with?('}')
|
147
|
+
answer = JSON.parse(answer) # LLM returned JSON as a string
|
148
|
+
else
|
149
|
+
# If not JSON, wrap it as per existing Gemini class (for CLI compatibility)
|
150
|
+
answer = { 'explanation' => answer, 'code' => nil }
|
151
|
+
end
|
152
|
+
rescue JSON::ParserError
|
153
|
+
answer = { 'explanation' => answer, 'code' => nil }
|
154
|
+
end
|
155
|
+
answer
|
156
|
+
end
|
157
|
+
|
158
|
+
def analyze_code_diff(prompt_content)
|
159
|
+
model = get_model_name
|
160
|
+
raise N2B::LlmApiError.new("No model configured for Vertex AI.") if model.nil? || model.empty?
|
161
|
+
|
162
|
+
uri = URI.parse(build_api_uri(model))
|
163
|
+
|
164
|
+
request = Net::HTTP::Post.new(uri)
|
165
|
+
request.content_type = 'application/json'
|
166
|
+
|
167
|
+
begin
|
168
|
+
scope = 'https://www.googleapis.com/auth/cloud-platform'
|
169
|
+
authorizer = Google::Auth::ServiceAccountCredentials.make_creds(
|
170
|
+
json_key_io: File.open(@config['vertex_credential_file']),
|
171
|
+
scope: scope
|
172
|
+
)
|
173
|
+
access_token = authorizer.fetch_access_token!['access_token']
|
174
|
+
request['Authorization'] = "Bearer #{access_token}"
|
175
|
+
rescue StandardError => e
|
176
|
+
raise N2B::LlmApiError.new("Vertex AI - Failed to obtain Google Cloud access token for diff analysis: #{e.message}")
|
177
|
+
end
|
178
|
+
|
179
|
+
request.body = JSON.dump({
|
180
|
+
"contents" => [{
|
181
|
+
"role" => "user",
|
182
|
+
"parts" => [{
|
183
|
+
"text" => prompt_content
|
184
|
+
}]
|
185
|
+
}],
|
186
|
+
"generationConfig" => {
|
187
|
+
"responseMimeType" => "application/json" # Expecting JSON response from LLM
|
188
|
+
}
|
189
|
+
})
|
190
|
+
|
191
|
+
begin
|
192
|
+
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
|
193
|
+
http.read_timeout = REQUEST_TIMEOUT
|
194
|
+
http.open_timeout = 30
|
195
|
+
http.request(request)
|
196
|
+
end
|
197
|
+
rescue Net::TimeoutError, Net::ReadTimeout, Net::OpenTimeout => e
|
198
|
+
error_msg = "Vertex AI diff analysis timed out (region: #{@location}): #{e.message}"
|
199
|
+
error_msg += "\n\nThis might be a region issue. Try reconfiguring with 'n2b -c' and select a different region."
|
200
|
+
error_msg += "\nFor EU users, try: europe-west1 (Belgium) or europe-west4 (Netherlands)"
|
201
|
+
error_msg += "\nCommon regions: #{COMMON_LOCATIONS.join(', ')}"
|
202
|
+
raise N2B::LlmApiError.new(error_msg)
|
203
|
+
rescue => e
|
204
|
+
raise N2B::LlmApiError.new("Vertex AI network error during diff analysis: #{e.message}")
|
205
|
+
end
|
206
|
+
|
207
|
+
if response.code != '200'
|
208
|
+
error_msg = "Vertex AI LLM API Error for diff analysis: #{response.code} #{response.message} - #{response.body}"
|
209
|
+
if response.code == '404'
|
210
|
+
error_msg += "\n\nThis might be a region or model availability issue. Current region: #{@location}"
|
211
|
+
error_msg += "\nNote: Google models via Vertex AI are not available in all regions."
|
212
|
+
error_msg += "\nTry reconfiguring with 'n2b -c' and:"
|
213
|
+
error_msg += "\n 1. Select a different region (Common regions: #{COMMON_LOCATIONS.join(', ')})"
|
214
|
+
error_msg += "\n 2. Choose a different model (some models are only available in specific regions)"
|
215
|
+
end
|
216
|
+
raise N2B::LlmApiError.new(error_msg)
|
217
|
+
end
|
218
|
+
|
219
|
+
parsed_response = JSON.parse(response.body)
|
220
|
+
# Return the raw JSON string from the 'text' field, CLI will parse it.
|
221
|
+
parsed_response['candidates'].first['content']['parts'].first['text']
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
data/lib/n2b/merge_cli.rb
CHANGED
@@ -844,6 +844,7 @@ REQUIREMENTS_BLOCK
|
|
844
844
|
when 'openai' then N2B::Llm::OpenAi.new(config)
|
845
845
|
when 'claude' then N2B::Llm::Claude.new(config)
|
846
846
|
when 'gemini' then N2B::Llm::Gemini.new(config)
|
847
|
+
when 'vertexai' then N2B::Llm::VertexAi.new(config)
|
847
848
|
when 'openrouter' then N2B::Llm::OpenRouter.new(config)
|
848
849
|
when 'ollama' then N2B::Llm::Ollama.new(config)
|
849
850
|
else raise N2B::Error, "Unsupported LLM service for analysis: #{llm_service_name}"
|
@@ -1083,6 +1084,8 @@ REQUIREMENTS_BLOCK
|
|
1083
1084
|
N2B::Llm::Claude.new(config)
|
1084
1085
|
when 'gemini'
|
1085
1086
|
N2B::Llm::Gemini.new(config)
|
1087
|
+
when 'vertexai'
|
1088
|
+
N2B::Llm::VertexAi.new(config)
|
1086
1089
|
when 'openrouter'
|
1087
1090
|
N2B::Llm::OpenRouter.new(config)
|
1088
1091
|
when 'ollama'
|
data/lib/n2b/model_config.rb
CHANGED
@@ -17,6 +17,14 @@ module N2B
|
|
17
17
|
'claude' => { 'suggested' => { 'sonnet' => 'claude-3-sonnet-20240229' }, 'default' => 'sonnet' },
|
18
18
|
'openai' => { 'suggested' => { 'gpt-4o-mini' => 'gpt-4o-mini' }, 'default' => 'gpt-4o-mini' },
|
19
19
|
'gemini' => { 'suggested' => { 'gemini-flash' => 'gemini-2.0-flash' }, 'default' => 'gemini-flash' },
|
20
|
+
'vertexai' => {
|
21
|
+
'suggested' => {
|
22
|
+
'gemini-2.0-flash' => 'gemini-2.0-flash-001',
|
23
|
+
'gemini-2.5-flash' => 'gemini-2.5-flash-preview',
|
24
|
+
'gemini-2.5-flash-05-20' => 'gemini-2.5-flash-preview-05-20'
|
25
|
+
},
|
26
|
+
'default' => 'gemini-2.0-flash'
|
27
|
+
},
|
20
28
|
'openrouter' => { 'suggested' => { 'gpt-4o' => 'openai/gpt-4o' }, 'default' => 'gpt-4o' },
|
21
29
|
'ollama' => { 'suggested' => { 'llama3' => 'llama3' }, 'default' => 'llama3' }
|
22
30
|
}
|
data/lib/n2b/version.rb
CHANGED
data/lib/n2b.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: n2b
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Stefan Nothegger
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-06-
|
11
|
+
date: 2025-06-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: json
|
@@ -24,6 +24,20 @@ dependencies:
|
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '2.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: googleauth
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.2'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.2'
|
27
41
|
- !ruby/object:Gem::Dependency
|
28
42
|
name: bundler
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -81,7 +95,6 @@ extensions: []
|
|
81
95
|
extra_rdoc_files: []
|
82
96
|
files:
|
83
97
|
- README.md
|
84
|
-
- bin/branch-audit.sh
|
85
98
|
- bin/n2b
|
86
99
|
- bin/n2b-diff
|
87
100
|
- bin/n2b-test-github
|
@@ -99,6 +112,7 @@ files:
|
|
99
112
|
- lib/n2b/llm/ollama.rb
|
100
113
|
- lib/n2b/llm/open_ai.rb
|
101
114
|
- lib/n2b/llm/open_router.rb
|
115
|
+
- lib/n2b/llm/vertex_ai.rb
|
102
116
|
- lib/n2b/merge_cli.rb
|
103
117
|
- lib/n2b/merge_conflict_parser.rb
|
104
118
|
- lib/n2b/message_utils.rb
|
@@ -133,7 +147,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
133
147
|
- !ruby/object:Gem::Version
|
134
148
|
version: '0'
|
135
149
|
requirements: []
|
136
|
-
rubygems_version: 3.5.
|
150
|
+
rubygems_version: 3.5.22
|
137
151
|
signing_key:
|
138
152
|
specification_version: 4
|
139
153
|
summary: AI-powered development toolkit with merge conflict resolution and Jira integration
|
data/bin/branch-audit.sh
DELETED
@@ -1,397 +0,0 @@
|
|
1
|
-
#!/bin/bash
|
2
|
-
|
3
|
-
# Branch Audit Script - Safely analyze all branches for unmerged code
|
4
|
-
# Author: Augment Agent
|
5
|
-
# Purpose: Find valuable code in branches without touching main
|
6
|
-
|
7
|
-
set -e # Exit on any error
|
8
|
-
|
9
|
-
# Colors for output
|
10
|
-
RED='\033[0;31m'
|
11
|
-
GREEN='\033[0;32m'
|
12
|
-
YELLOW='\033[1;33m'
|
13
|
-
BLUE='\033[0;34m'
|
14
|
-
NC='\033[0m' # No Color
|
15
|
-
|
16
|
-
# Configuration
|
17
|
-
MAIN_BRANCH="main"
|
18
|
-
REPORT_FILE="branch_audit_report_$(date +%Y-%m-%d_%H-%M-%S).md"
|
19
|
-
TEMP_DIR="temp_branch_audit_$$"
|
20
|
-
WORKTREE_DIR="worktree_audit_$$"
|
21
|
-
|
22
|
-
# Arrays to store results
|
23
|
-
declare -a MERGEABLE_BRANCHES=()
|
24
|
-
declare -a CONFLICTED_BRANCHES=()
|
25
|
-
declare -a EMPTY_BRANCHES=()
|
26
|
-
declare -a ERROR_BRANCHES=()
|
27
|
-
|
28
|
-
# Cleanup function
|
29
|
-
cleanup() {
|
30
|
-
echo -e "${YELLOW}🧹 Cleaning up temporary files...${NC}"
|
31
|
-
if [ -d "$TEMP_DIR" ]; then
|
32
|
-
rm -rf "$TEMP_DIR"
|
33
|
-
fi
|
34
|
-
if [ -d "$WORKTREE_DIR" ]; then
|
35
|
-
git worktree remove "$WORKTREE_DIR" 2>/dev/null || true
|
36
|
-
rm -rf "$WORKTREE_DIR" 2>/dev/null || true
|
37
|
-
fi
|
38
|
-
}
|
39
|
-
|
40
|
-
# Set up cleanup trap
|
41
|
-
trap cleanup EXIT
|
42
|
-
|
43
|
-
# Function to log with timestamp
|
44
|
-
log() {
|
45
|
-
echo -e "${BLUE}[$(date '+%H:%M:%S')]${NC} $1"
|
46
|
-
}
|
47
|
-
|
48
|
-
# Function to check if branch exists
|
49
|
-
branch_exists() {
|
50
|
-
git show-ref --verify --quiet "refs/heads/$1" 2>/dev/null
|
51
|
-
}
|
52
|
-
|
53
|
-
# Function to check if remote branch exists
|
54
|
-
remote_branch_exists() {
|
55
|
-
git show-ref --verify --quiet "refs/remotes/$1" 2>/dev/null
|
56
|
-
}
|
57
|
-
|
58
|
-
# Function to get unique commits in branch vs main
|
59
|
-
get_unique_commits() {
|
60
|
-
local branch="$1"
|
61
|
-
git log --oneline "${MAIN_BRANCH}..${branch}" 2>/dev/null || echo ""
|
62
|
-
}
|
63
|
-
|
64
|
-
# Function to get file changes summary
|
65
|
-
get_file_changes() {
|
66
|
-
local branch="$1"
|
67
|
-
git diff --name-status "${MAIN_BRANCH}...${branch}" 2>/dev/null || echo ""
|
68
|
-
}
|
69
|
-
|
70
|
-
# Function to check if fast-forward merge is possible
|
71
|
-
can_fast_forward() {
|
72
|
-
local branch="$1"
|
73
|
-
local merge_base=$(git merge-base "$MAIN_BRANCH" "$branch" 2>/dev/null || echo "")
|
74
|
-
local main_commit=$(git rev-parse "$MAIN_BRANCH" 2>/dev/null || echo "")
|
75
|
-
|
76
|
-
if [ -z "$merge_base" ] || [ -z "$main_commit" ]; then
|
77
|
-
return 1
|
78
|
-
fi
|
79
|
-
|
80
|
-
# If merge base equals main commit, then it's a fast-forward
|
81
|
-
[ "$merge_base" = "$main_commit" ]
|
82
|
-
}
|
83
|
-
|
84
|
-
# Function to analyze a single branch
|
85
|
-
analyze_branch() {
|
86
|
-
local branch="$1"
|
87
|
-
local branch_type="$2" # "local" or "remote"
|
88
|
-
|
89
|
-
log "Analyzing ${branch_type} branch: $branch"
|
90
|
-
|
91
|
-
# Skip main branch and its variants
|
92
|
-
if [[ "$branch" =~ ^(main|master|origin/main|origin/master)$ ]]; then
|
93
|
-
log "Skipping main branch: $branch"
|
94
|
-
return
|
95
|
-
fi
|
96
|
-
|
97
|
-
# For remote branches, create local tracking branch if needed
|
98
|
-
local local_branch="$branch"
|
99
|
-
if [ "$branch_type" = "remote" ]; then
|
100
|
-
local_branch="${branch#origin/}"
|
101
|
-
if ! branch_exists "$local_branch"; then
|
102
|
-
log "Creating local tracking branch for $branch"
|
103
|
-
git branch "$local_branch" "$branch" 2>/dev/null || {
|
104
|
-
ERROR_BRANCHES+=("$branch (failed to create local branch)")
|
105
|
-
return
|
106
|
-
}
|
107
|
-
fi
|
108
|
-
fi
|
109
|
-
|
110
|
-
# Get unique commits
|
111
|
-
local unique_commits=$(get_unique_commits "$local_branch")
|
112
|
-
local commit_count=0
|
113
|
-
if [ -n "$unique_commits" ] && [ "$unique_commits" != "" ]; then
|
114
|
-
commit_count=$(echo "$unique_commits" | wc -l | tr -d ' ')
|
115
|
-
fi
|
116
|
-
|
117
|
-
if [ "$commit_count" -eq 0 ]; then
|
118
|
-
log "Branch $branch has no unique commits"
|
119
|
-
EMPTY_BRANCHES+=("$branch")
|
120
|
-
return
|
121
|
-
fi
|
122
|
-
|
123
|
-
# Check if fast-forward is possible
|
124
|
-
if can_fast_forward "$local_branch"; then
|
125
|
-
log "✅ Branch $branch can be fast-forward merged ($commit_count commits)"
|
126
|
-
MERGEABLE_BRANCHES+=("$branch|$commit_count|$unique_commits")
|
127
|
-
else
|
128
|
-
log "⚠️ Branch $branch has conflicts or diverged ($commit_count commits)"
|
129
|
-
CONFLICTED_BRANCHES+=("$branch|$commit_count|$unique_commits")
|
130
|
-
fi
|
131
|
-
}
|
132
|
-
|
133
|
-
# Main execution starts here
|
134
|
-
main() {
|
135
|
-
echo -e "${GREEN}🔍 Starting Branch Audit Analysis${NC}"
|
136
|
-
echo -e "${BLUE}Repository: $(pwd)${NC}"
|
137
|
-
echo -e "${BLUE}Main branch: $MAIN_BRANCH${NC}"
|
138
|
-
echo -e "${BLUE}Report file: $REPORT_FILE${NC}"
|
139
|
-
echo ""
|
140
|
-
|
141
|
-
# Verify we're in a git repository
|
142
|
-
if ! git rev-parse --git-dir > /dev/null 2>&1; then
|
143
|
-
echo -e "${RED}❌ Error: Not in a git repository${NC}"
|
144
|
-
exit 1
|
145
|
-
fi
|
146
|
-
|
147
|
-
# Verify main branch exists
|
148
|
-
if ! git show-ref --verify --quiet "refs/heads/$MAIN_BRANCH"; then
|
149
|
-
echo -e "${RED}❌ Error: Main branch '$MAIN_BRANCH' not found${NC}"
|
150
|
-
exit 1
|
151
|
-
fi
|
152
|
-
|
153
|
-
# Fetch latest from remote
|
154
|
-
log "Fetching latest changes from remote..."
|
155
|
-
git fetch --all --prune
|
156
|
-
|
157
|
-
# Create temporary directory
|
158
|
-
mkdir -p "$TEMP_DIR"
|
159
|
-
|
160
|
-
# Get current branch to restore later
|
161
|
-
CURRENT_BRANCH=$(git branch --show-current)
|
162
|
-
|
163
|
-
# Ensure we're on main branch for analysis
|
164
|
-
log "Switching to main branch for analysis..."
|
165
|
-
git checkout "$MAIN_BRANCH" >/dev/null 2>&1
|
166
|
-
|
167
|
-
# Get all local branches
|
168
|
-
log "Analyzing local branches..."
|
169
|
-
while IFS= read -r branch; do
|
170
|
-
branch=$(echo "$branch" | sed 's/^[* ] //' | xargs)
|
171
|
-
analyze_branch "$branch" "local"
|
172
|
-
done < <(git branch | grep -v "^\*.*$MAIN_BRANCH$")
|
173
|
-
|
174
|
-
# Get all remote branches
|
175
|
-
log "Analyzing remote branches..."
|
176
|
-
while IFS= read -r branch; do
|
177
|
-
branch=$(echo "$branch" | sed 's|^ remotes/||' | xargs)
|
178
|
-
# Skip if we already have this as a local branch
|
179
|
-
local_name="${branch#origin/}"
|
180
|
-
if ! branch_exists "$local_name"; then
|
181
|
-
analyze_branch "$branch" "remote"
|
182
|
-
fi
|
183
|
-
done < <(git branch -r | grep -v "HEAD\|$MAIN_BRANCH$")
|
184
|
-
|
185
|
-
# Restore original branch
|
186
|
-
if [ -n "$CURRENT_BRANCH" ] && [ "$CURRENT_BRANCH" != "$MAIN_BRANCH" ]; then
|
187
|
-
log "Restoring original branch: $CURRENT_BRANCH"
|
188
|
-
git checkout "$CURRENT_BRANCH" >/dev/null 2>&1
|
189
|
-
fi
|
190
|
-
|
191
|
-
# Generate report
|
192
|
-
generate_report
|
193
|
-
|
194
|
-
echo ""
|
195
|
-
echo -e "${GREEN}✅ Branch audit completed!${NC}"
|
196
|
-
echo -e "${BLUE}📄 Report saved to: $REPORT_FILE${NC}"
|
197
|
-
}
|
198
|
-
|
199
|
-
# Function to generate detailed report
|
200
|
-
generate_report() {
|
201
|
-
log "Generating detailed report..."
|
202
|
-
|
203
|
-
cat > "$REPORT_FILE" << EOF
|
204
|
-
# Branch Audit Report
|
205
|
-
**Generated:** $(date)
|
206
|
-
**Repository:** $(pwd)
|
207
|
-
**Main Branch:** $MAIN_BRANCH
|
208
|
-
|
209
|
-
## 📊 Executive Summary
|
210
|
-
|
211
|
-
- **Mergeable Branches (Fast-Forward):** ${#MERGEABLE_BRANCHES[@]}
|
212
|
-
- **Conflicted Branches (Manual Review):** ${#CONFLICTED_BRANCHES[@]}
|
213
|
-
- **Empty Branches (No Changes):** ${#EMPTY_BRANCHES[@]}
|
214
|
-
- **Error Branches:** ${#ERROR_BRANCHES[@]}
|
215
|
-
- **Total Analyzed:** $((${#MERGEABLE_BRANCHES[@]} + ${#CONFLICTED_BRANCHES[@]} + ${#EMPTY_BRANCHES[@]} + ${#ERROR_BRANCHES[@]}))
|
216
|
-
|
217
|
-
---
|
218
|
-
|
219
|
-
EOF
|
220
|
-
|
221
|
-
# Mergeable branches section
|
222
|
-
if [ ${#MERGEABLE_BRANCHES[@]} -gt 0 ]; then
|
223
|
-
cat >> "$REPORT_FILE" << EOF
|
224
|
-
## ✅ Mergeable Branches (Fast-Forward Possible)
|
225
|
-
|
226
|
-
These branches can be safely merged into main without conflicts:
|
227
|
-
|
228
|
-
EOF
|
229
|
-
for branch_info in "${MERGEABLE_BRANCHES[@]}"; do
|
230
|
-
IFS='|' read -r branch commit_count commits <<< "$branch_info"
|
231
|
-
cat >> "$REPORT_FILE" << EOF
|
232
|
-
### 🌿 \`$branch\`
|
233
|
-
- **Unique Commits:** $commit_count
|
234
|
-
- **File Changes:** $(get_file_changes "$branch" | wc -l) files
|
235
|
-
- **Recent Commits:**
|
236
|
-
\`\`\`
|
237
|
-
$commits
|
238
|
-
\`\`\`
|
239
|
-
- **Files Modified:**
|
240
|
-
\`\`\`
|
241
|
-
$(get_file_changes "$branch" | head -10)
|
242
|
-
$([ $(get_file_changes "$branch" | wc -l) -gt 10 ] && echo "... and $(($(get_file_changes "$branch" | wc -l) - 10)) more files")
|
243
|
-
\`\`\`
|
244
|
-
|
245
|
-
**Recommendation:** ✅ Safe to merge with \`git merge $branch\`
|
246
|
-
|
247
|
-
---
|
248
|
-
|
249
|
-
EOF
|
250
|
-
done
|
251
|
-
fi
|
252
|
-
|
253
|
-
# Conflicted branches section
|
254
|
-
if [ ${#CONFLICTED_BRANCHES[@]} -gt 0 ]; then
|
255
|
-
cat >> "$REPORT_FILE" << EOF
|
256
|
-
## ⚠️ Conflicted Branches (Manual Review Required)
|
257
|
-
|
258
|
-
These branches have diverged from main and may have conflicts:
|
259
|
-
|
260
|
-
EOF
|
261
|
-
for branch_info in "${CONFLICTED_BRANCHES[@]}"; do
|
262
|
-
IFS='|' read -r branch commit_count commits <<< "$branch_info"
|
263
|
-
cat >> "$REPORT_FILE" << EOF
|
264
|
-
### 🔀 \`$branch\`
|
265
|
-
- **Unique Commits:** $commit_count
|
266
|
-
- **File Changes:** $(get_file_changes "$branch" | wc -l) files
|
267
|
-
- **Recent Commits:**
|
268
|
-
\`\`\`
|
269
|
-
$commits
|
270
|
-
\`\`\`
|
271
|
-
- **Files Modified:**
|
272
|
-
\`\`\`
|
273
|
-
$(get_file_changes "$branch" | head -10)
|
274
|
-
$([ $(get_file_changes "$branch" | wc -l) -gt 10 ] && echo "... and $(($(get_file_changes "$branch" | wc -l) - 10)) more files")
|
275
|
-
\`\`\`
|
276
|
-
|
277
|
-
**Recommendation:** ⚠️ Manual review required - check for conflicts before merging
|
278
|
-
|
279
|
-
---
|
280
|
-
|
281
|
-
EOF
|
282
|
-
done
|
283
|
-
fi
|
284
|
-
|
285
|
-
# Empty branches section
|
286
|
-
if [ ${#EMPTY_BRANCHES[@]} -gt 0 ]; then
|
287
|
-
cat >> "$REPORT_FILE" << EOF
|
288
|
-
## 🗑️ Empty Branches (Safe to Delete)
|
289
|
-
|
290
|
-
These branches have no unique commits compared to main:
|
291
|
-
|
292
|
-
EOF
|
293
|
-
for branch in "${EMPTY_BRANCHES[@]}"; do
|
294
|
-
cat >> "$REPORT_FILE" << EOF
|
295
|
-
- \`$branch\` - No unique changes
|
296
|
-
EOF
|
297
|
-
done
|
298
|
-
cat >> "$REPORT_FILE" << EOF
|
299
|
-
|
300
|
-
**Cleanup Commands:**
|
301
|
-
\`\`\`bash
|
302
|
-
# Delete local empty branches
|
303
|
-
EOF
|
304
|
-
for branch in "${EMPTY_BRANCHES[@]}"; do
|
305
|
-
if [[ ! "$branch" =~ ^origin/ ]]; then
|
306
|
-
echo "git branch -d $branch" >> "$REPORT_FILE"
|
307
|
-
fi
|
308
|
-
done
|
309
|
-
cat >> "$REPORT_FILE" << EOF
|
310
|
-
|
311
|
-
# Delete remote empty branches (be careful!)
|
312
|
-
EOF
|
313
|
-
for branch in "${EMPTY_BRANCHES[@]}"; do
|
314
|
-
if [[ "$branch" =~ ^origin/ ]]; then
|
315
|
-
remote_branch="${branch#origin/}"
|
316
|
-
echo "git push origin --delete $remote_branch" >> "$REPORT_FILE"
|
317
|
-
fi
|
318
|
-
done
|
319
|
-
cat >> "$REPORT_FILE" << EOF
|
320
|
-
\`\`\`
|
321
|
-
|
322
|
-
---
|
323
|
-
|
324
|
-
EOF
|
325
|
-
fi
|
326
|
-
|
327
|
-
# Error branches section
|
328
|
-
if [ ${#ERROR_BRANCHES[@]} -gt 0 ]; then
|
329
|
-
cat >> "$REPORT_FILE" << EOF
|
330
|
-
## ❌ Error Branches
|
331
|
-
|
332
|
-
These branches encountered errors during analysis:
|
333
|
-
|
334
|
-
EOF
|
335
|
-
for branch in "${ERROR_BRANCHES[@]}"; do
|
336
|
-
cat >> "$REPORT_FILE" << EOF
|
337
|
-
- \`$branch\`
|
338
|
-
EOF
|
339
|
-
done
|
340
|
-
cat >> "$REPORT_FILE" << EOF
|
341
|
-
|
342
|
-
**Recommendation:** Manual investigation required
|
343
|
-
|
344
|
-
---
|
345
|
-
|
346
|
-
EOF
|
347
|
-
fi
|
348
|
-
|
349
|
-
# Summary and recommendations
|
350
|
-
cat >> "$REPORT_FILE" << EOF
|
351
|
-
## 🎯 Recommended Actions
|
352
|
-
|
353
|
-
### Immediate Actions (Safe)
|
354
|
-
1. **Merge fast-forward branches:** ${#MERGEABLE_BRANCHES[@]} branches ready
|
355
|
-
2. **Delete empty branches:** ${#EMPTY_BRANCHES[@]} branches can be removed
|
356
|
-
3. **Review conflicted branches:** ${#CONFLICTED_BRANCHES[@]} branches need manual review
|
357
|
-
|
358
|
-
### Cleanup Script
|
359
|
-
\`\`\`bash
|
360
|
-
# Run this script to clean up empty branches
|
361
|
-
EOF
|
362
|
-
|
363
|
-
# Generate cleanup commands for empty branches
|
364
|
-
for branch in "${EMPTY_BRANCHES[@]}"; do
|
365
|
-
if [[ ! "$branch" =~ ^origin/ ]]; then
|
366
|
-
echo "git branch -d $branch" >> "$REPORT_FILE"
|
367
|
-
fi
|
368
|
-
done
|
369
|
-
|
370
|
-
cat >> "$REPORT_FILE" << EOF
|
371
|
-
\`\`\`
|
372
|
-
|
373
|
-
### Merge Script for Fast-Forward Branches
|
374
|
-
\`\`\`bash
|
375
|
-
# Run these commands to merge ready branches
|
376
|
-
EOF
|
377
|
-
|
378
|
-
# Generate merge commands for mergeable branches
|
379
|
-
for branch_info in "${MERGEABLE_BRANCHES[@]}"; do
|
380
|
-
IFS='|' read -r branch commit_count commits <<< "$branch_info"
|
381
|
-
if [[ ! "$branch" =~ ^origin/ ]]; then
|
382
|
-
echo "git merge $branch # $commit_count commits" >> "$REPORT_FILE"
|
383
|
-
fi
|
384
|
-
done
|
385
|
-
|
386
|
-
cat >> "$REPORT_FILE" << EOF
|
387
|
-
\`\`\`
|
388
|
-
|
389
|
-
---
|
390
|
-
*Report generated by branch-audit.sh on $(date)*
|
391
|
-
EOF
|
392
|
-
|
393
|
-
log "Report generated: $REPORT_FILE"
|
394
|
-
}
|
395
|
-
|
396
|
-
# Run main function
|
397
|
-
main "$@"
|