ai_refactor 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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