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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +55 -2
  3. data/Gemfile +2 -0
  4. data/Gemfile.lock +5 -1
  5. data/README.md +63 -37
  6. data/Rakefile +1 -1
  7. data/ai_refactor.gemspec +1 -0
  8. data/exe/ai_refactor +78 -44
  9. data/lib/ai_refactor/cli.rb +86 -0
  10. data/lib/ai_refactor/context.rb +33 -0
  11. data/lib/ai_refactor/file_processor.rb +34 -17
  12. data/lib/ai_refactor/prompt.rb +84 -0
  13. data/lib/ai_refactor/prompts/diff.md +17 -0
  14. data/lib/ai_refactor/prompts/input.md +1 -0
  15. data/lib/ai_refactor/refactors/base_refactor.rb +176 -0
  16. data/lib/ai_refactor/refactors/generic.rb +6 -76
  17. data/lib/ai_refactor/refactors/minitest/write_test_for_class.md +11 -0
  18. data/lib/ai_refactor/refactors/minitest/write_test_for_class.rb +51 -0
  19. data/lib/ai_refactor/refactors/project/write_changelog_from_history.md +35 -0
  20. data/lib/ai_refactor/refactors/project/write_changelog_from_history.rb +50 -0
  21. data/lib/ai_refactor/refactors/{prompts/rspec_to_minitest_rails.md → rails/minitest/rspec_to_minitest.md} +40 -1
  22. data/lib/ai_refactor/refactors/rails/minitest/rspec_to_minitest.rb +77 -0
  23. data/lib/ai_refactor/refactors/rspec/minitest_to_rspec.rb +13 -0
  24. data/lib/ai_refactor/refactors.rb +13 -5
  25. data/lib/ai_refactor/{refactors/tests → test_runners}/minitest_runner.rb +1 -1
  26. data/lib/ai_refactor/{refactors/tests → test_runners}/rspec_runner.rb +1 -1
  27. data/lib/ai_refactor/{refactors/tests → test_runners}/test_run_diff_report.rb +1 -1
  28. data/lib/ai_refactor/{refactors/tests → test_runners}/test_run_result.rb +1 -1
  29. data/lib/ai_refactor/version.rb +1 -1
  30. data/lib/ai_refactor.rb +13 -8
  31. metadata +34 -11
  32. data/lib/ai_refactor/base_refactor.rb +0 -66
  33. data/lib/ai_refactor/refactors/minitest_to_rspec.rb +0 -11
  34. data/lib/ai_refactor/refactors/rspec_to_minitest_rails.rb +0 -103
  35. /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 :file_path, :output_path, :logger
9
+ attr_reader :input_file_path, :output_path, :logger, :options
9
10
 
10
- def initialize(input_path:, prompt_file_path:, ai_client:, logger:, output_path: nil)
11
- @file_path = input_path
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!(options)
24
- logger.debug("Processing #{file_path} with prompt in #{@prompt_file_path}")
25
- prompt = File.read(@prompt_file_path)
26
- input = File.read(@file_path)
27
- messages = [
28
- {role: "system", content: prompt},
29
- {role: "user", content: "Convert: ```#{input}```"}
30
- ]
31
- content, finished_reason, usage = generate_next_message(messages, prompt, options, options[:ai_max_attempts] || 3)
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
- File.write(output_path, processed)
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 generate_next_message(messages, prompt, options, attempts_left)
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-3.5-turbo",
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
- ], prompt, options, attempts_left - 1)
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, finished_reason, usage = processor.process!(options)
11
+ output_content = process!(strip_ticks: false)
26
12
  rescue => e
27
- logger.error "Request to OpenAI failed: #{e.message}"
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
- logger.verbose "OpenAI finished, with reason '#{finished_reason}'..."
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
- private
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
- def output_file_path_from_template
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