ai_refactor 0.3.1 → 0.4.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/CHANGELOG.md +55 -2
- data/Gemfile +2 -0
- data/Gemfile.lock +5 -1
- data/README.md +63 -37
- data/Rakefile +1 -1
- data/ai_refactor.gemspec +1 -0
- data/exe/ai_refactor +78 -44
- data/lib/ai_refactor/cli.rb +86 -0
- data/lib/ai_refactor/context.rb +33 -0
- data/lib/ai_refactor/file_processor.rb +34 -17
- data/lib/ai_refactor/prompt.rb +84 -0
- data/lib/ai_refactor/prompts/diff.md +17 -0
- data/lib/ai_refactor/prompts/input.md +1 -0
- data/lib/ai_refactor/refactors/base_refactor.rb +176 -0
- data/lib/ai_refactor/refactors/generic.rb +6 -76
- data/lib/ai_refactor/refactors/minitest/write_test_for_class.md +11 -0
- data/lib/ai_refactor/refactors/minitest/write_test_for_class.rb +51 -0
- data/lib/ai_refactor/refactors/project/write_changelog_from_history.md +35 -0
- data/lib/ai_refactor/refactors/project/write_changelog_from_history.rb +50 -0
- data/lib/ai_refactor/refactors/{prompts/rspec_to_minitest_rails.md → rails/minitest/rspec_to_minitest.md} +40 -1
- data/lib/ai_refactor/refactors/rails/minitest/rspec_to_minitest.rb +77 -0
- data/lib/ai_refactor/refactors/rspec/minitest_to_rspec.rb +13 -0
- data/lib/ai_refactor/refactors.rb +13 -5
- data/lib/ai_refactor/{refactors/tests → test_runners}/minitest_runner.rb +1 -1
- data/lib/ai_refactor/{refactors/tests → test_runners}/rspec_runner.rb +1 -1
- data/lib/ai_refactor/{refactors/tests → test_runners}/test_run_diff_report.rb +1 -1
- data/lib/ai_refactor/{refactors/tests → test_runners}/test_run_result.rb +1 -1
- data/lib/ai_refactor/version.rb +1 -1
- data/lib/ai_refactor.rb +13 -8
- metadata +34 -11
- data/lib/ai_refactor/base_refactor.rb +0 -66
- data/lib/ai_refactor/refactors/minitest_to_rspec.rb +0 -11
- data/lib/ai_refactor/refactors/rspec_to_minitest_rails.rb +0 -103
- /data/lib/ai_refactor/refactors/{prompts → rspec}/minitest_to_rspec.md +0 -0
@@ -1,18 +1,19 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "fileutils"
|
3
4
|
require "openai"
|
4
5
|
require "json"
|
5
6
|
|
6
7
|
module AIRefactor
|
7
8
|
class FileProcessor
|
8
|
-
attr_reader :
|
9
|
+
attr_reader :input_file_path, :output_path, :logger, :options
|
9
10
|
|
10
|
-
def initialize(
|
11
|
-
@
|
12
|
-
@prompt_file_path = prompt_file_path
|
11
|
+
def initialize(prompt:, ai_client:, logger:, output_path: nil, options: {})
|
12
|
+
@prompt = prompt
|
13
13
|
@ai_client = ai_client
|
14
14
|
@logger = logger
|
15
15
|
@output_path = output_path
|
16
|
+
@options = options
|
16
17
|
end
|
17
18
|
|
18
19
|
def output_exists?
|
@@ -20,20 +21,26 @@ module AIRefactor
|
|
20
21
|
File.exist?(output_path)
|
21
22
|
end
|
22
23
|
|
23
|
-
def process!
|
24
|
-
logger.debug("Processing #{
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
24
|
+
def process!
|
25
|
+
logger.debug("Processing #{@prompt.input_file_path} with prompt in #{@prompt.prompt_file_path}")
|
26
|
+
logger.debug("Options: #{options.inspect}")
|
27
|
+
messages = @prompt.chat_messages
|
28
|
+
if options[:review_prompt]
|
29
|
+
logger.info "Review prompt:\n"
|
30
|
+
messages.each do |message|
|
31
|
+
logger.info "\n-- Start of prompt for Role #{message[:role]} --\n"
|
32
|
+
logger.info message[:content]
|
33
|
+
logger.info "\n-- End of prompt for Role #{message[:role]} --\n"
|
34
|
+
end
|
35
|
+
return [nil, "Skipped as review prompt was requested", nil]
|
36
|
+
end
|
37
|
+
|
38
|
+
content, finished_reason, usage = generate_next_message(messages, options, ai_max_attempts)
|
32
39
|
|
33
40
|
content = if content && content.length > 0
|
34
41
|
processed = block_given? ? yield(content) : content
|
35
42
|
if output_path
|
36
|
-
|
43
|
+
write_output(output_path, processed)
|
37
44
|
logger.verbose "Wrote output to #{output_path}..."
|
38
45
|
end
|
39
46
|
processed
|
@@ -44,14 +51,18 @@ module AIRefactor
|
|
44
51
|
|
45
52
|
private
|
46
53
|
|
47
|
-
def
|
54
|
+
def ai_max_attempts
|
55
|
+
options[:ai_max_attempts] || 1
|
56
|
+
end
|
57
|
+
|
58
|
+
def generate_next_message(messages, options, attempts_left)
|
48
59
|
logger.verbose "Generate AI output. Generation attempts left: #{attempts_left}"
|
49
60
|
logger.debug "Options: #{options.inspect}"
|
50
61
|
logger.debug "Messages: #{messages.inspect}"
|
51
62
|
|
52
63
|
response = @ai_client.chat(
|
53
64
|
parameters: {
|
54
|
-
model: options[:ai_model] || "gpt-
|
65
|
+
model: options[:ai_model] || "gpt-4",
|
55
66
|
messages: messages,
|
56
67
|
temperature: options[:ai_temperature] || 0.7,
|
57
68
|
max_tokens: options[:ai_max_tokens] || 1500
|
@@ -69,7 +80,7 @@ module AIRefactor
|
|
69
80
|
generate_next_message(messages + [
|
70
81
|
{role: "assistant", content: content},
|
71
82
|
{role: "user", content: "Continue"}
|
72
|
-
],
|
83
|
+
], options, attempts_left - 1)
|
73
84
|
else
|
74
85
|
previous_messages = messages.filter { |m| m[:role] == "assistant" }.map { |m| m[:content] }.join
|
75
86
|
content = if previous_messages.length > 0
|
@@ -80,5 +91,11 @@ module AIRefactor
|
|
80
91
|
[content, finished_reason, response["usage"]]
|
81
92
|
end
|
82
93
|
end
|
94
|
+
|
95
|
+
def write_output(output_path, processed)
|
96
|
+
dir = File.dirname(output_path)
|
97
|
+
FileUtils.mkdir_p(dir) unless File.directory?(dir)
|
98
|
+
File.write(output_path, processed)
|
99
|
+
end
|
83
100
|
end
|
84
101
|
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AIRefactor
|
4
|
+
class Prompt
|
5
|
+
INPUT_FILE_PATH_MARKER = "__{{input_file_path}}__"
|
6
|
+
OUTPUT_FILE_PATH_MARKER = "__{{output_file_path}}__"
|
7
|
+
HEADER_MARKER = "__{{prompt_header}}__"
|
8
|
+
FOOTER_MARKER = "__{{prompt_footer}}__"
|
9
|
+
CONTEXT_MARKER = "__{{context}}__"
|
10
|
+
CONTENT_MARKER = "__{{content}}__"
|
11
|
+
|
12
|
+
attr_reader :input_file_path, :prompt_file_path
|
13
|
+
|
14
|
+
def initialize(options:, logger:, context: nil, input_content: nil, input_path: nil, output_file_path: nil, prompt_file_path: nil, prompt_header: nil, prompt_footer: nil)
|
15
|
+
@input_content = input_content
|
16
|
+
@input_file_path = input_path
|
17
|
+
@output_file_path = output_file_path
|
18
|
+
@prompt_file_path = prompt_file_path
|
19
|
+
@logger = logger
|
20
|
+
@header = prompt_header
|
21
|
+
@footer = prompt_footer
|
22
|
+
@diff = options[:diff]
|
23
|
+
@context = context
|
24
|
+
end
|
25
|
+
|
26
|
+
def chat_messages
|
27
|
+
[
|
28
|
+
{role: "system", content: system_prompt},
|
29
|
+
{role: "user", content: user_prompt}
|
30
|
+
]
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def system_prompt
|
36
|
+
prompt = expand_prompt(system_prompt_template, HEADER_MARKER, @header || "")
|
37
|
+
prompt = expand_prompt(prompt, CONTEXT_MARKER, @context&.prepare_context || "")
|
38
|
+
prompt = expand_prompt(prompt, INPUT_FILE_PATH_MARKER, @input_file_path || "")
|
39
|
+
prompt = expand_prompt(prompt, OUTPUT_FILE_PATH_MARKER, @output_file_path || "")
|
40
|
+
expand_prompt(prompt, FOOTER_MARKER, system_prompt_footer)
|
41
|
+
end
|
42
|
+
|
43
|
+
def system_prompt_template
|
44
|
+
File.read(@prompt_file_path)
|
45
|
+
end
|
46
|
+
|
47
|
+
def system_prompt_footer
|
48
|
+
if @diff && @footer
|
49
|
+
"#{@footer}\n\n#{diff_prompt}"
|
50
|
+
elsif @diff
|
51
|
+
diff_prompt
|
52
|
+
elsif @footer
|
53
|
+
@footer
|
54
|
+
else
|
55
|
+
""
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def diff_prompt
|
60
|
+
File.read(prompt_path("diff.md"))
|
61
|
+
end
|
62
|
+
|
63
|
+
def prompt_path(file)
|
64
|
+
File.join(File.dirname(File.expand_path(__FILE__)), "prompts", file)
|
65
|
+
end
|
66
|
+
|
67
|
+
def user_prompt
|
68
|
+
expand_prompt(input_prompt, CONTENT_MARKER, input_to_process)
|
69
|
+
end
|
70
|
+
|
71
|
+
def input_to_process
|
72
|
+
return File.read(@input_file_path) if @input_file_path
|
73
|
+
@input_content
|
74
|
+
end
|
75
|
+
|
76
|
+
def input_prompt
|
77
|
+
File.read(prompt_path("input.md"))
|
78
|
+
end
|
79
|
+
|
80
|
+
def expand_prompt(prompt, marker, content)
|
81
|
+
prompt.gsub(marker, content)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
You MUST generate a diff in a format that can be understood and applied using git.
|
2
|
+
|
3
|
+
Generate diff hunks that capture the modifications you see. The diff hunks should be in a format that git can understand and apply, including a hunk header and
|
4
|
+
the lines of code that have been modified.
|
5
|
+
Finally, output the generated diff as your answer. Do not provide further instruction.
|
6
|
+
|
7
|
+
Example diff:
|
8
|
+
|
9
|
+
```
|
10
|
+
@@ -27,7 +27,7 @@ module AIRefactor
|
11
|
+
File.read(@prompt_file_path)
|
12
|
+
end
|
13
|
+
|
14
|
+
- def user_prompt
|
15
|
+
+ def user_prompt_with_diff
|
16
|
+
input = File.read(@file_path)
|
17
|
+
```
|
@@ -0,0 +1 @@
|
|
1
|
+
```__{{content}}__```
|
@@ -0,0 +1,176 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AIRefactor
|
4
|
+
module Refactors
|
5
|
+
class BaseRefactor
|
6
|
+
# All subclasses must register themselves with the Registry
|
7
|
+
def self.inherited(subclass)
|
8
|
+
super
|
9
|
+
Refactors.register(subclass)
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.description
|
13
|
+
"(No description provided)"
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.takes_input_files?
|
17
|
+
true
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_reader :input_file, :options, :logger
|
21
|
+
attr_accessor :input_content
|
22
|
+
attr_writer :failed_message
|
23
|
+
|
24
|
+
def initialize(input_file, options, logger)
|
25
|
+
@input_file = input_file
|
26
|
+
@options = options
|
27
|
+
@logger = logger
|
28
|
+
end
|
29
|
+
|
30
|
+
def run
|
31
|
+
raise NotImplementedError
|
32
|
+
end
|
33
|
+
|
34
|
+
def failed_message
|
35
|
+
@failed_message || "Reason not specified"
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def file_processor
|
41
|
+
context = ::AIRefactor::Context.new(files: options[:context_file_paths], text: options[:context_text], logger: logger)
|
42
|
+
prompt = ::AIRefactor::Prompt.new(input_content: input_content, input_path: input_file, output_file_path: output_file_path, prompt_file_path: prompt_file_path, context: context, logger: logger, options: options)
|
43
|
+
AIRefactor::FileProcessor.new(prompt: prompt, ai_client: ai_client, output_path: output_file_path, logger: logger, options: options)
|
44
|
+
end
|
45
|
+
|
46
|
+
def process!(strip_ticks: true)
|
47
|
+
processor = file_processor
|
48
|
+
|
49
|
+
if processor.output_exists?
|
50
|
+
return false unless overwrite_existing_output?(output_file_path)
|
51
|
+
end
|
52
|
+
|
53
|
+
logger.verbose "Processing #{input_file}..."
|
54
|
+
|
55
|
+
begin
|
56
|
+
output_content, finished_reason, usage = processor.process! do |content|
|
57
|
+
if block_given?
|
58
|
+
yield content
|
59
|
+
elsif strip_ticks
|
60
|
+
content.gsub("```ruby", "").gsub("```", "")
|
61
|
+
else
|
62
|
+
content
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
logger.verbose "AI finished, with reason '#{finished_reason}'..."
|
67
|
+
logger.verbose "Used tokens: #{usage["total_tokens"]}".colorize(:light_black) if usage
|
68
|
+
if finished_reason == "length"
|
69
|
+
logger.warn "Translation may contain an incomplete output as the max token length was reached. You can try using the '--continue' option next time to increase the length of generated output."
|
70
|
+
end
|
71
|
+
|
72
|
+
if !output_content || output_content.length == 0
|
73
|
+
logger.warn "Skipping #{input_file}, no translated output..."
|
74
|
+
logger.error "Failed to translate #{input_file}, finished reason #{finished_reason}"
|
75
|
+
self.failed_message = "AI conversion failed, no output was generated"
|
76
|
+
raise NoOutputError, "No output"
|
77
|
+
end
|
78
|
+
|
79
|
+
output_content
|
80
|
+
rescue => e
|
81
|
+
logger.error "Request to AI failed: #{e.message}"
|
82
|
+
logger.warn "Skipping #{input_file}..."
|
83
|
+
self.failed_message = "Request to OpenAI failed"
|
84
|
+
raise e
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def overwrite_existing_output?(output_path)
|
89
|
+
overwrite = options && options[:overwrite]&.downcase
|
90
|
+
answer = if ["y", "n"].include? overwrite
|
91
|
+
overwrite
|
92
|
+
else
|
93
|
+
logger.info "Do you wish to overwrite #{output_path}? (y/n)"
|
94
|
+
$stdin.gets.chomp.downcase
|
95
|
+
end
|
96
|
+
if answer == "y"
|
97
|
+
logger.verbose "Overwriting #{output_path}..."
|
98
|
+
return true
|
99
|
+
end
|
100
|
+
logger.warn "Skipping #{input_file}..."
|
101
|
+
self.failed_message = "Skipped as output file already exists"
|
102
|
+
false
|
103
|
+
end
|
104
|
+
|
105
|
+
def prompt_file_path
|
106
|
+
file = if options && options[:prompt_file_path]&.length&.positive?
|
107
|
+
options[:prompt_file_path]
|
108
|
+
else
|
109
|
+
location = Module.const_source_location(::AIRefactor::Refactors::BaseRefactor.name)
|
110
|
+
File.join(File.dirname(location.first), "#{refactor_name}.md")
|
111
|
+
end
|
112
|
+
file.tap do |prompt|
|
113
|
+
raise "No prompt file '#{prompt}' found for #{refactor_name}" unless File.exist?(prompt)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def output_file_path
|
118
|
+
@output_file_path ||= determine_output_file_path
|
119
|
+
end
|
120
|
+
|
121
|
+
def determine_output_file_path
|
122
|
+
return output_file_path_from_template if output_template_path
|
123
|
+
|
124
|
+
path = options[:output_file_path]
|
125
|
+
return default_output_path unless path
|
126
|
+
|
127
|
+
if path == true
|
128
|
+
input_file
|
129
|
+
else
|
130
|
+
path
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def default_output_path
|
135
|
+
nil
|
136
|
+
end
|
137
|
+
|
138
|
+
def output_template_path
|
139
|
+
options[:output_template_path]
|
140
|
+
end
|
141
|
+
|
142
|
+
def output_file_path_from_template
|
143
|
+
path = output_template_path.gsub("[FILE]", File.basename(input_file))
|
144
|
+
.gsub("[NAME]", File.basename(input_file, ".*"))
|
145
|
+
.gsub("[DIR]", File.dirname(input_file))
|
146
|
+
.gsub("[REFACTOR]", self.class.refactor_name)
|
147
|
+
.gsub("[EXT]", File.extname(input_file))
|
148
|
+
raise "Output template could not be used" unless path.length.positive?
|
149
|
+
path
|
150
|
+
end
|
151
|
+
|
152
|
+
def ai_client
|
153
|
+
@ai_client ||= OpenAI::Client.new
|
154
|
+
end
|
155
|
+
|
156
|
+
def refactor_name
|
157
|
+
self.class.refactor_name
|
158
|
+
end
|
159
|
+
|
160
|
+
class << self
|
161
|
+
def command_line_options
|
162
|
+
[]
|
163
|
+
end
|
164
|
+
|
165
|
+
def refactor_name
|
166
|
+
name.gsub("AIRefactor::Refactors::", "")
|
167
|
+
.gsub(/::/, "/")
|
168
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
169
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
170
|
+
.tr("-", "_")
|
171
|
+
.downcase
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
@@ -7,74 +7,23 @@ module AIRefactor
|
|
7
7
|
logger.verbose "Generic refactor to #{input_file}... (using user supplied prompt #{prompt_file_path})"
|
8
8
|
logger.verbose "Write output to #{output_file_path}..." if output_file_path
|
9
9
|
|
10
|
-
processor = AIRefactor::FileProcessor.new(
|
11
|
-
input_path: input_file,
|
12
|
-
prompt_file_path: prompt_file_path,
|
13
|
-
ai_client: ai_client,
|
14
|
-
logger: logger,
|
15
|
-
output_path: output_file_path
|
16
|
-
)
|
17
|
-
|
18
|
-
if processor.output_exists?
|
19
|
-
return false unless can_overwrite_output_file?(output_file_path)
|
20
|
-
end
|
21
|
-
|
22
|
-
logger.verbose "Converting #{input_file}..."
|
23
|
-
|
24
10
|
begin
|
25
|
-
output_content
|
11
|
+
output_content = process!(strip_ticks: false)
|
26
12
|
rescue => e
|
27
|
-
logger.error "
|
28
|
-
logger.warn "Skipping #{input_file}..."
|
29
|
-
self.failed_message = "Request to OpenAI failed"
|
13
|
+
logger.error "Failed to process #{input_file}: #{e.message}"
|
30
14
|
return false
|
31
15
|
end
|
32
16
|
|
33
|
-
|
34
|
-
logger.verbose "Used tokens: #{usage["total_tokens"]}".colorize(:light_black) if usage
|
35
|
-
|
36
|
-
if finished_reason == "length"
|
37
|
-
logger.warn "Translation may contain an incomplete output as the max token length was reached. You can try using the '--continue' option next time to increase the length of generated output."
|
38
|
-
end
|
39
|
-
|
40
|
-
if !output_content || output_content.length == 0
|
41
|
-
logger.warn "Skipping #{input_file}, no translated output..."
|
42
|
-
logger.error "Failed to translate #{input_file}, finished reason #{finished_reason}"
|
43
|
-
self.failed_message = "AI conversion failed, no output was generated"
|
44
|
-
return false
|
45
|
-
end
|
17
|
+
return false unless output_content
|
46
18
|
|
47
19
|
output_file_path ? true : output_content
|
48
20
|
end
|
49
21
|
|
50
|
-
|
51
|
-
|
52
|
-
def output_file_path
|
53
|
-
return output_file_path_from_template if output_template_path
|
54
|
-
|
55
|
-
path = options[:output_file_path]
|
56
|
-
return unless path
|
57
|
-
|
58
|
-
if path == true
|
59
|
-
input_file
|
60
|
-
else
|
61
|
-
path
|
62
|
-
end
|
63
|
-
end
|
64
|
-
|
65
|
-
def output_template_path
|
66
|
-
options[:output_template_path]
|
22
|
+
def self.description
|
23
|
+
"Generic refactor using user supplied prompt"
|
67
24
|
end
|
68
25
|
|
69
|
-
|
70
|
-
path = output_template_path.gsub("[FILE]", File.basename(input_file))
|
71
|
-
.gsub("[NAME]", File.basename(input_file, ".*"))
|
72
|
-
.gsub("[DIR]", File.dirname(input_file))
|
73
|
-
.gsub("[REFACTOR]", self.class.refactor_name)
|
74
|
-
.gsub("[EXT]", File.extname(input_file))
|
75
|
-
raise "Output template could not be used" unless path.length.positive?
|
76
|
-
path
|
77
|
-
end
|
26
|
+
private
|
78
27
|
|
79
28
|
def prompt_file_path
|
80
29
|
specified_prompt_path = options[:prompt_file_path]
|
@@ -89,25 +38,6 @@ module AIRefactor
|
|
89
38
|
end
|
90
39
|
exit 1
|
91
40
|
end
|
92
|
-
|
93
|
-
class << self
|
94
|
-
def command_line_options
|
95
|
-
[
|
96
|
-
{
|
97
|
-
key: :output_file_path,
|
98
|
-
long: "--output [FILE]",
|
99
|
-
type: String,
|
100
|
-
help: "Write output to file instead of stdout. If no path provided will overwrite input file (will prompt to overwrite existing files)"
|
101
|
-
},
|
102
|
-
{
|
103
|
-
key: :output_template_path,
|
104
|
-
long: "--output-template TEMPLATE",
|
105
|
-
type: String,
|
106
|
-
help: "Write outputs to files instead of stdout. The template is used to create the output name, where the it can have substitutions, '[FILE]', '[NAME]', '[DIR]', '[REFACTOR]' & '[EXT]'. Eg `[DIR]/[NAME]_[REFACTOR][EXT]` (will prompt to overwrite existing files)"
|
107
|
-
}
|
108
|
-
]
|
109
|
-
end
|
110
|
-
end
|
111
41
|
end
|
112
42
|
end
|
113
43
|
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
You are an expert Ruby senior software developer. Write a unit test for the class show below, using minitest and minitest/mock if necessary.
|
2
|
+
Test 100% of the code.
|
3
|
+
|
4
|
+
The path to the file to test is: __{{input_file_path}}__
|
5
|
+
The output file path is: __{{output_file_path}}__
|
6
|
+
|
7
|
+
Only show me the test file code. Do NOT provide any other description of your work. Always enclose the output code in triple backticks (```).
|
8
|
+
|
9
|
+
__{{context}}__
|
10
|
+
|
11
|
+
The class to test is:
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AIRefactor
|
4
|
+
module Refactors
|
5
|
+
module Minitest
|
6
|
+
class WriteTestForClass < BaseRefactor
|
7
|
+
def run
|
8
|
+
logger.verbose "'Write minitest test' refactor for #{input_file}..."
|
9
|
+
logger.verbose "Write output to #{output_file_path}..." if output_file_path
|
10
|
+
|
11
|
+
begin
|
12
|
+
output_content = process!
|
13
|
+
rescue => e
|
14
|
+
logger.error "Failed to process #{input_file}: #{e.message}"
|
15
|
+
return false
|
16
|
+
end
|
17
|
+
|
18
|
+
return false unless output_file_path
|
19
|
+
|
20
|
+
logger.verbose "Generated #{output_file_path} from #{input_file} ..." if output_content
|
21
|
+
|
22
|
+
minitest_runner = AIRefactor::TestRunners::MinitestRunner.new(output_file_path, command_template: "bundle exec ruby __FILE__")
|
23
|
+
|
24
|
+
logger.verbose "Run generated test file #{output_file_path} (#{minitest_runner.command})..."
|
25
|
+
test_run = minitest_runner.run
|
26
|
+
|
27
|
+
if test_run.failed?
|
28
|
+
logger.warn "#{input_file} was translated to #{output_file_path} but the resulting test is failing..."
|
29
|
+
logger.error "Failed to run test, exited with status #{test_run.exitstatus}. Stdout: #{test_run.stdout}\n\nStderr: #{test_run.stderr}\n\n"
|
30
|
+
logger.error "New test failed!", bold: true
|
31
|
+
self.failed_message = "Generated test file failed to run correctly"
|
32
|
+
return false
|
33
|
+
end
|
34
|
+
|
35
|
+
logger.verbose "\nNew test file ran and returned the following results:"
|
36
|
+
logger.verbose ">> Runs: #{test_run.example_count}, Failures: #{test_run.failure_count}, Skips: #{test_run.pending_count}\n"
|
37
|
+
|
38
|
+
output_file_path ? true : output_content
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.description
|
42
|
+
"Write a minitest test for a class"
|
43
|
+
end
|
44
|
+
|
45
|
+
def default_output_path
|
46
|
+
File.join("test", input_file.gsub(/\.rb$/, "_test.rb"))
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
You are a senior Ruby software developer. You are diligent about writing clear changelogs for your users.
|
2
|
+
|
3
|
+
You have a git history of changes to your project. You want to write a changelog from this history.
|
4
|
+
|
5
|
+
The format of the changelog you will create entries for is from keepachangelog.com version 1.0.0.
|
6
|
+
|
7
|
+
Here is an example of a set of changelog entries:
|
8
|
+
|
9
|
+
```
|
10
|
+
## [0.2.0] - 2015-10-06
|
11
|
+
|
12
|
+
### Changed
|
13
|
+
|
14
|
+
- Remove exclusionary mentions of "open source" since this project can
|
15
|
+
benefit both "open" and "closed" source projects equally.
|
16
|
+
|
17
|
+
## [0.1.0] - 2015-10-06
|
18
|
+
|
19
|
+
### Added
|
20
|
+
|
21
|
+
- Answer "Should you ever rewrite a change log?".
|
22
|
+
|
23
|
+
### Changed
|
24
|
+
|
25
|
+
- Improve argument against commit logs.
|
26
|
+
- Start following [SemVer](https://semver.org) properly.
|
27
|
+
|
28
|
+
### Fixed
|
29
|
+
|
30
|
+
- Fix typos in recent CHANGELOG entries.
|
31
|
+
```
|
32
|
+
|
33
|
+
If the history contains any mention of changes of version (such as "Bump version to 1.0.0"), then you should use that version number in a new changelog entry.
|
34
|
+
|
35
|
+
Now write changelog entries from the following git history.
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AIRefactor
|
4
|
+
module Refactors
|
5
|
+
module Project
|
6
|
+
class WriteChangelogFromHistory < BaseRefactor
|
7
|
+
def self.description
|
8
|
+
"Write changelog entries from the git history"
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.takes_input_files?
|
12
|
+
false
|
13
|
+
end
|
14
|
+
|
15
|
+
def run
|
16
|
+
logger.verbose "Creating changelog entries for project from #{options[:git_commit_count] || 3} commits..."
|
17
|
+
logger.verbose "Write output to #{output_file_path}..." if output_file_path
|
18
|
+
|
19
|
+
self.input_content = `git log -#{options[:git_commit_count] || 3} --pretty=format:"%ci %d %s"`.split("\n").map { |line| "- #{line}" }.join("\n")
|
20
|
+
logger.debug "\nInput messages: \n#{input_content}\n\n"
|
21
|
+
begin
|
22
|
+
output_content = process!(strip_ticks: false)
|
23
|
+
rescue => e
|
24
|
+
logger.error "Failed to process: #{e.message}"
|
25
|
+
return false
|
26
|
+
end
|
27
|
+
|
28
|
+
return false unless output_content
|
29
|
+
|
30
|
+
output_file_path ? true : output_content
|
31
|
+
end
|
32
|
+
|
33
|
+
def default_output_path
|
34
|
+
nil
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.command_line_options
|
38
|
+
[
|
39
|
+
{
|
40
|
+
key: :git_commit_count,
|
41
|
+
long: "--commits N",
|
42
|
+
type: Integer,
|
43
|
+
help: "The number of commits to analyse when creating the changelog entries (defaults to 3)"
|
44
|
+
}
|
45
|
+
]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|