git_toolbox 0.7.1 → 0.8.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a9a83558777ee286d804e64a714388d13b4ac16e15e724580f7957ea422ba8a2
4
- data.tar.gz: 8e76c6784c12e03f9cab918099a219e5e823441b90356c68b4cabc8e0bcf13fe
3
+ metadata.gz: 500f22d2000495865a5671476122c580ea0e5fdb6ebf2804ce42d209828a1841
4
+ data.tar.gz: 70a3c312974ce5ee382a8c6793325bf676dba7fb40206ed828f56c2019ce9d64
5
5
  SHA512:
6
- metadata.gz: 89d357ca5cf79577e6a05e3d5aff6dccb227c593565d8c5fe7d634d7a76b2cbe568a3c3db6cc49c58d0edfe4418d5da99f39fb1c944dfd14e2267dcb35d3a5bf
7
- data.tar.gz: d5a06e808f81790b745ddf2ca031d6ca9c318e13c563aa78262488ec32ab8b98fca0b5d089b5f97b1ae321e76ca3825cd4018248b791490f85fb4c78dfaf1080
6
+ metadata.gz: ce3a643b6a110c7b70e7ddc33649548b15f064e61dbe35c13fa5eba3fa70b7c71a5118185ee88b89aab83f74602b605d9175b4633e58c933ae30ebfae97c2dab
7
+ data.tar.gz: 5125a3a0af8ab4a6590af9166c95d442c1dae5f89648f1d94224ff5a4d3e18fcaf21ec5e9d52b1f50939340d17d60ddd5cd37fa7bd6bd16b4e7ce88f2ac868da
data/bin/get CHANGED
@@ -5,4 +5,4 @@ require 'rubygems'
5
5
  require 'bundler/setup'
6
6
  require 'get'
7
7
 
8
- Get.main
8
+ Get.instance.main
@@ -0,0 +1,60 @@
1
+ # Get is a toolbox based on git which simplifies the adoption of conventions and some git commands.
2
+ # Copyright (C) 2023 Alex Speranza
3
+
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU Lesser General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program and the additional permissions granted by
16
+ # the Lesser GPL. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ # frozen_string_literal: true
19
+
20
+ require 'open3'
21
+
22
+ # Module for simplify command execution.
23
+ module CommandIssuer
24
+ # A class containing the result of a command, including the exit status and the command executed.
25
+ class CommandResult
26
+ attr_reader :command, :exit_status, :output, :error
27
+
28
+ def initialize(command_string, exit_status, standard_output, standard_error)
29
+ @command = command_string
30
+ @exit_status = exit_status
31
+ @output = standard_output
32
+ @error = standard_error
33
+ end
34
+ end
35
+
36
+ def self.run(executable, *args)
37
+ full_path_executable = CommandIssuer.send(:find, executable)
38
+ command = [full_path_executable, *args].join(' ').strip
39
+ output, error, status = Open3.capture3(command)
40
+ CommandResult.new(command, status.exitstatus, output, error)
41
+ end
42
+
43
+ class << self
44
+ private
45
+
46
+ # Checks if the given executable exists. If it does not exists,
47
+ # an error message will be displayed and the program will exit.
48
+ # Based on https://stackoverflow.com/a/5471032
49
+ def find(executable)
50
+ exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
51
+ ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
52
+ exts.each do |ext|
53
+ exe = File.join(path, "#{executable}#{ext}")
54
+ return exe if File.executable?(exe) && !File.directory?(exe)
55
+ end
56
+ end
57
+ Common.error("'#{executable}' was not found in PATH.")
58
+ end
59
+ end
60
+ end
@@ -51,4 +51,74 @@ module Common
51
51
  action.call if action.respond_to?('call')
52
52
  exit(exit_code)
53
53
  end
54
+
55
+ # Add an instance attribute (with a default value) to a module.
56
+ # It is intended to be called in the body of a module definition:
57
+ # module MyModule
58
+ # DEFAULT_VALUE = 1
59
+ # Common.module_instance_attr(self, my_variable, DEFAULT_VALUE)
60
+ # end
61
+ # produces the code:
62
+ # module MyModule
63
+ # instance_variable_set(:@my_variable, 1)
64
+ # def self.my_variable
65
+ # instance_variable_get(:@my_variable)
66
+ # end
67
+ #
68
+ # def self.my_variable=(value)
69
+ # instance_variable_set(:@my_variable, value)
70
+ # end
71
+ # end
72
+ def self.module_instance_attr(mod, name, default_value = nil)
73
+ mod.module_eval(<<~CODE, __FILE__, __LINE__ + 1)
74
+ # module MyModule
75
+ # instance_variable_set(:@my_variable, 1)
76
+ # def self.my_variable
77
+ # instance_variable_get(:@my_variable)
78
+ # end
79
+ #
80
+ # def self.my_variable=(value)
81
+ # instance_variable_set(:@my_variable, value)
82
+ # end
83
+ # end
84
+
85
+ instance_variable_set(:@#{name}, #{default_value})
86
+
87
+ def self.#{name}
88
+ instance_variable_get(:@#{name})
89
+ end
90
+
91
+ def self.#{name}=(value)
92
+ instance_variable_set(:@#{name}, value)
93
+ end
94
+ CODE
95
+ end
96
+
97
+ def self.module_instance_value(mod, name, value)
98
+ mod.module_eval(<<~CODE, __FILE__, __LINE__ + 1)
99
+ # module MyModule
100
+ # instance_variable_set(:@my_variable, 1)
101
+ # def self.my_variable
102
+ # instance_variable_get(:@my_variable)
103
+ # end
104
+ # end
105
+
106
+ instance_variable_set(:@#{name}, #{value})
107
+
108
+ def self.#{name}
109
+ instance_variable_get(:@#{name})
110
+ end
111
+ CODE
112
+ end
113
+
114
+ # Add a new 'MOD_REF' constant with the module symbol for a shorter variable name.
115
+ def self.add_module_self_reference(mod)
116
+ mod.module_eval(<<~CODE, __FILE__, __LINE__ + 1)
117
+ # module MyModule
118
+ # MOD_REF = MyModule
119
+ # end
120
+
121
+ MOD_REF = #{mod.name}
122
+ CODE
123
+ end
54
124
  end
@@ -17,23 +17,17 @@
17
17
 
18
18
  # frozen_string_literal: true
19
19
 
20
- require 'English'
21
- require 'get/commons/common'
20
+ require_relative './command_issuer'
22
21
 
23
22
  # Utility module
24
23
  module Git
25
24
  # Groups: 1 = type, 2 = scope with (), 3 = scope, 4 = breaking change, 5 = summary
26
- CONVENTIONAL_COMMIT_REGEX = /^(\w+)(\(([\w-]+)\))?(!)?:(.*)/
25
+ CONVENTIONAL_COMMIT_REGEX = %r{^(\w+)(\(([\w/-]+)\))?(!)?:(.*)}
27
26
 
28
27
  # Check if the command is called while in a git repository.
29
28
  # If the command fails, it is assumed to not be in a git repository.
30
29
  def self.in_repo?
31
- system('git rev-parse --is-inside-work-tree &>/dev/null')
32
- case $CHILD_STATUS.exitstatus
33
- when 0 then true
34
- when 127 then Common.error '"git" is not installed.'
35
- else false
36
- end
30
+ CommandIssuer.run('git', 'rev-parse', '--is-inside-work-tree').exit_status.zero?
37
31
  end
38
32
 
39
33
  # Run a block of code with the list of commits from the given version as an argument.
@@ -41,19 +35,42 @@ module Git
41
35
  def self.with_commit_list_from(version = nil, &block)
42
36
  return unless block_given?
43
37
 
44
- commits_from_version =
45
- `git --no-pager log --oneline --pretty=format:%s #{version.nil? ? '' : "^#{version}"} HEAD`
46
- .split("\n")
38
+ command_result = CommandIssuer.run(
39
+ 'git',
40
+ '--no-pager',
41
+ 'log',
42
+ '--oneline',
43
+ '--pretty=format:%s',
44
+ version.nil? ? '' : "^#{version} HEAD"
45
+ )
46
+ commits_from_version = if command_result.exit_status.zero?
47
+ command_result.output.split("\n")
48
+ else
49
+ []
50
+ end
47
51
  block.call(commits_from_version)
48
52
  end
49
53
 
50
54
  # Returns the last version and caches it for the next calls.
51
55
  def self.last_version
52
- @@last_version ||= `git describe --tags --abbrev=0`.strip
56
+ @last_version ||=
57
+ CommandIssuer.run('git', 'describe', '--tags', '--abbrev=0')
58
+ .then { |result| result.output.strip if result.exit_status.zero? }
53
59
  end
54
60
 
55
61
  # Returns the last release and caches it for the next calls.
56
62
  def self.last_release
57
- @@last_release ||= `git --no-pager tag --list | sed 's/+/_/' | sort -V | sed 's/_/+/' | tail -n 1`.strip
63
+ @last_release ||=
64
+ CommandIssuer.run('git', '--no-pager', 'tag', '--list')
65
+ .then do |value|
66
+ unless value.output.empty?
67
+ value.output
68
+ .split("\n")
69
+ .map { |str| str.sub('+', '_') }
70
+ .sort
71
+ .map { |str| str.sub('_', '+') }
72
+ .last
73
+ end
74
+ end
58
75
  end
59
76
  end
@@ -0,0 +1,79 @@
1
+ # Get is a toolbox based on git which simplifies the adoption of conventions and some git commands.
2
+ # Copyright (C) 2023 Alex Speranza
3
+
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU Lesser General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program and the additional permissions granted by
16
+ # the Lesser GPL. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ # frozen_string_literal: true
19
+
20
+ require 'net/http'
21
+ require 'uri'
22
+
23
+ # Client HTTP which allows to perform GET request and follow redirections.
24
+ class HTTPClient
25
+ include Singleton
26
+
27
+ # Number of attempts to try when following a redirection link.
28
+ MAX_ATTEMPTS = 10
29
+
30
+ # Perform a get request to an address, following the redirections at most MAX_ATTEMPTS times.
31
+ def http_get_request(address)
32
+ uri = URI.parse(address)
33
+
34
+ # Code based on https://shadow-file.blogspot.com/2009/03/handling-http-redirection-in-ruby.html
35
+ attempts = 0
36
+ until attempts >= MAX_ATTEMPTS
37
+ attempts += 1
38
+
39
+ resp = build_http(uri)
40
+ .request(Net::HTTP::Get.new(uri.path == '' ? '/' : uri.path))
41
+ return resp if resp.is_a?(Net::HTTPSuccess) || resp.header['location'].nil?
42
+
43
+ uri = updated_uri(uri, resp.header['location'])
44
+ end
45
+ nil
46
+ end
47
+
48
+ def response_error_message(response)
49
+ if response.nil?
50
+ 'too many redirections'
51
+ else
52
+ "#{response.code} #{response.message}"
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def build_http(uri)
59
+ Net::HTTP.new(uri.host, uri.port)
60
+ .tap { |http| http.open_timeout = 10 }
61
+ .tap { |http| http.read_timeout = 10 }
62
+ .tap do |http|
63
+ if uri.instance_of? URI::HTTPS
64
+ http.use_ssl = true
65
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
66
+ end
67
+ end
68
+ end
69
+
70
+ def updated_uri(old_uri, location)
71
+ URI.parse(location).then do |value|
72
+ if value.relative?
73
+ old_uri + location
74
+ else
75
+ value
76
+ end
77
+ end
78
+ end
79
+ end
@@ -17,22 +17,14 @@
17
17
 
18
18
  # frozen_string_literal: true
19
19
 
20
- require 'English'
21
- require 'get/commons/common'
22
- require 'get/commons/git'
23
- require 'get/subcommand/command'
20
+ require_relative '../../commons/common'
21
+ require_relative '../../commons/git'
22
+ require_relative '../command'
24
23
 
25
24
  # Class length is disabled as most of its length is given by formatting.
26
25
  # rubocop:disable Metrics/ClassLength
27
26
  # Subcommand, generates a changelog.
28
27
  class Changelog < Command
29
- def self.command
30
- @@command ||= new
31
- @@command
32
- end
33
-
34
- private_class_method :new
35
-
36
28
  private
37
29
 
38
30
  UNDEFINED_SCOPE = 'other'
@@ -44,56 +36,11 @@ class Changelog < Command
44
36
  item: '- %s'
45
37
  }.freeze
46
38
 
47
- @@command = nil
48
-
49
- @@usage = 'changelog -h|(<subcommand> [<subcommand-options])'
50
- @@description = 'Generate a changelog. Format options require a "%s" where the content must be.'
51
- @@subcommands = {}
52
- # This block is Optimist configuration. It is as long as the number of options of the command.
53
- # rubocop:disable Metrics/BlockLength
54
- @@option_parser = Optimist::Parser.new do
55
- subcommand_max_length = @@subcommands.keys.map { |k| k.to_s.length }.max
56
- subcommand_section = <<~SUBCOMMANDS unless @@subcommands.empty?
57
- Subcommands:
58
- #{@@subcommands.keys.map { |k| " #{k.to_s.ljust(subcommand_max_length)} => #{@@subcommands[k].description}" }.join("\n")}
59
- SUBCOMMANDS
60
- usage @@usage
61
- synopsis @@description + (subcommand_section.nil? ? '' : "\n") + subcommand_section.to_s
62
- opt :latest,
63
- 'Generate the changelog from the latest version rather than the latest release'
64
- opt :title_format,
65
- 'Set the symbol for the title.',
66
- { type: :string, short: 'T', default: '# %s' }
67
- opt :type_format,
68
- 'Set the symbol for the commit types.',
69
- { type: :string, short: 't', default: '= %s' }
70
- opt :scope_format,
71
- 'Set the symbol for the commit scopes.',
72
- { type: :string, short: 's', default: '- %s' }
73
- opt :list_format,
74
- 'Set the symbol for lists.',
75
- { type: :string, short: 'l', default: '%s' }
76
- opt :item_format,
77
- 'Set the symbol for list items.',
78
- { type: :string, short: 'i', default: '* %s' }
79
- opt :markdown,
80
- 'Shortcut for `-T "# %s" -t "## %s" -s "### %s" -l "%s" -i "- %s"`. ' \
81
- 'Can be overwritten by the single options.'
82
- educate_on_error
83
- stop_on @@subcommands.keys.map(&:to_s)
84
- end
85
- # rubocop:enable Metrics/BlockLength
86
-
87
39
  def initialize
88
- super(@@usage, @@description) do
89
- @options = Common.with_subcommand_exception_handling @@option_parser do
90
- @@option_parser.parse
91
- end
92
- Common.error 'changelog need to be run inside a git repository' unless Git.in_repo?
93
- @format = {}
94
- set_format
95
-
96
- puts changelog_from(@options[:latest] ? Git.last_version : Git.last_release)
40
+ super() do
41
+ @usage = 'changelog -h|(<subcommand> [<subcommand-options])'
42
+ @description = 'Generate a changelog. Format options require a "%s" where the content must be.'
43
+ @subcommands = {}
97
44
  end
98
45
  end
99
46
 
@@ -139,7 +86,7 @@ class Changelog < Command
139
86
  formatted_types = []
140
87
  changelog.except('feat', 'fix').each { |key, value| formatted_types.push(format_type(key, value)) }
141
88
  <<~CHANGELOG
142
- #{@format[:title].sub('%s', "Changelog from version #{from_version}")}
89
+ #{@format[:title].sub('%s', "Changelog from #{from_version.nil? ? 'first commit' : "version #{from_version}"}")}
143
90
  #{(formatted_features + formatted_fixes + formatted_types).join("\n").strip}
144
91
  CHANGELOG
145
92
  end
@@ -161,5 +108,53 @@ class Changelog < Command
161
108
  #{@format[:list].sub('%s', formatted_commits.join("\n"))}
162
109
  SCOPE
163
110
  end
111
+
112
+ protected
113
+
114
+ def setup_option_parser
115
+ @option_parser = Optimist::Parser.new(
116
+ @usage,
117
+ full_description,
118
+ stop_condition
119
+ ) do |usage_header, description, stop_condition|
120
+ usage usage_header
121
+ synopsis description
122
+ opt :latest,
123
+ 'Generate the changelog from the latest version rather than the latest release'
124
+ opt :title_format,
125
+ 'Set the symbol for the title.',
126
+ { type: :string, short: 'T', default: '# %s' }
127
+ opt :type_format,
128
+ 'Set the symbol for the commit types.',
129
+ { type: :string, short: 't', default: '= %s' }
130
+ opt :scope_format,
131
+ 'Set the symbol for the commit scopes.',
132
+ { type: :string, short: 's', default: '- %s' }
133
+ opt :list_format,
134
+ 'Set the symbol for lists.',
135
+ { type: :string, short: 'l', default: '%s' }
136
+ opt :item_format,
137
+ 'Set the symbol for list items.',
138
+ { type: :string, short: 'i', default: '* %s' }
139
+ opt :markdown,
140
+ 'Shortcut for `-T "# %s" -t "## %s" -s "### %s" -l "%s" -i "- %s"`. ' \
141
+ 'Can be overwritten by the single options.'
142
+ educate_on_error
143
+ stop_on stop_condition
144
+ end
145
+ end
146
+
147
+ def setup_action
148
+ @action = lambda do
149
+ @options = Common.with_subcommand_exception_handling @option_parser do
150
+ @option_parser.parse
151
+ end
152
+ Common.error 'changelog need to be run inside a git repository' unless Git.in_repo?
153
+ @format = {}
154
+ set_format
155
+
156
+ puts changelog_from(@options[:latest] ? Git.last_version : Git.last_release)
157
+ end
158
+ end
164
159
  end
165
160
  # rubocop:enable Metrics/ClassLength
@@ -17,15 +17,67 @@
17
17
 
18
18
  # frozen_string_literal: true
19
19
 
20
- # Base class for (sub)commands. (Sub)Commands should be singletons.
20
+ require 'singleton'
21
+
22
+ # Base class for (sub)commands.
21
23
  class Command
22
- attr_reader :description, :action
24
+ include Singleton
25
+
26
+ attr_reader :usage, :description, :action, :subcommands, :option_parser
23
27
 
24
28
  protected
25
29
 
26
- def initialize(usage, description, &action)
27
- @usage = usage
28
- @description = description
29
- @action = action
30
+ attr_writer :usage, :description, :action, :subcommands, :option_parser
31
+
32
+ def initialize
33
+ super
34
+ yield self if block_given?
35
+ setup_option_parser
36
+ if @option_parser.nil?
37
+ raise("No variable '@option_parser' has been created in the option_parser setup of command #{self.class.name}.")
38
+ end
39
+
40
+ setup_action
41
+ return unless @action.nil?
42
+
43
+ raise("No variable '@action' has been created in the action setup of the command #{self.class.name}")
44
+ end
45
+
46
+ @description = ''
47
+ @subcommands = {}
48
+
49
+ # This method must be overridden by subclasses to create a new option parser in a '@option_parser' variable.
50
+ # Do not call 'super' in the new implementation.
51
+ def setup_option_parser
52
+ raise("Error: command #{self.class.name} do not have a defined option parser.")
53
+ end
54
+
55
+ # This method must be overridden by subclasses to create a new option parser in a '@action' variable.
56
+ # Do not call 'super' in the new implementation.
57
+ def setup_action
58
+ raise("Error: command #{self.class.name} do not have a defined action.")
59
+ end
60
+
61
+ def full_description
62
+ description + if subcommands.empty?
63
+ ''
64
+ else
65
+ subcommand_max_length = subcommands.keys.map { |k| k.to_s.length }.max || 0
66
+ <<~SUBCOMMANDS.chomp
67
+ \n
68
+ Subcommands:
69
+ #{subcommands.keys.map { |k| " #{k.to_s.ljust(subcommand_max_length)} => #{subcommands[k].description}" }.join("\n")}
70
+ SUBCOMMANDS
71
+ end
72
+ end
73
+
74
+ def stop_condition
75
+ subcommands.keys.map(&:to_s)
76
+ end
77
+
78
+ def educated_error(message)
79
+ Common.error message do
80
+ @option_parser.educate
81
+ end
30
82
  end
31
83
  end
@@ -17,77 +17,24 @@
17
17
 
18
18
  # frozen_string_literal: true
19
19
 
20
- require 'English'
21
- require 'get/commons/common'
22
- require 'get/commons/git'
23
- require 'get/subcommand/command'
24
- require 'get/subcommand/commit/prompt'
20
+ require_relative '../../commons/common'
21
+ require_relative '../../commons/git'
22
+ require_relative '../command'
23
+ require_relative './prompt'
25
24
 
26
25
  # Class length is disabled as most of its length is given by formatting.
27
26
  # rubocop:disable Metrics/ClassLength
28
27
  # Subcommand, it manages the description of the current git repository using semantic version.
29
28
  class Commit < Command
30
- def self.command
31
- @@command ||= new
32
- @@command
33
- end
34
-
35
- private_class_method :new
36
-
37
29
  private
38
30
 
39
31
  include PromptHandler
40
32
 
41
- @@command = nil
42
-
43
- @@usage = 'commit -h|(<subcommand> [<subcommand-options])'
44
- @@description = 'Create a new semantic commit.'
45
- @@subcommands = {}
46
- # This block is Optimist configuration. It is as long as the number of options of the command.
47
- # rubocop:disable Metrics/BlockLength
48
- @@option_parser = Optimist::Parser.new do
49
- subcommand_max_length = @@subcommands.keys.map { |k| k.to_s.length }.max
50
- subcommand_section = <<~SUBCOMMANDS unless @@subcommands.empty?
51
- Subcommands:
52
- #{@@subcommands.keys.map { |k| " #{k.to_s.ljust(subcommand_max_length)} => #{@@subcommands[k].description}" }.join("\n")}
53
- SUBCOMMANDS
54
- usage @@usage
55
- synopsis @@description + (subcommand_section.nil? ? '' : "\n") + subcommand_section.to_s
56
- opt :type,
57
- 'Define the type of the commit. Enabling this option skips the type selection.',
58
- { type: :string }
59
- opt :scope,
60
- 'Define the scope of the commit. Enabling this option skips the scope selection.',
61
- { type: :string, short: 'S' }
62
- opt :summary,
63
- 'Define the summary message of the commit. Enabling this option skips the summary message prompt.',
64
- { type: :string, short: 's' }
65
- opt :message,
66
- 'Define the message body of the commit. Enabling this option skips the message body prompt.',
67
- { type: :string }
68
- opt :breaking,
69
- 'Set the commit to have a breaking change. ' \
70
- 'Can be negated with "--no-breaking". ' \
71
- 'Enabling this option skips the breaking change prompt.',
72
- { type: :flag, short: :none }
73
- educate_on_error
74
- stop_on @@subcommands.keys.map(&:to_s)
75
- end
76
- # rubocop:enable Metrics/BlockLength
77
-
78
33
  def initialize
79
- super(@@usage, @@description) do
80
- @options = Common.with_subcommand_exception_handling @@option_parser do
81
- @@option_parser.parse
82
- end
83
- Common.error 'commit need to be run inside a git repository' unless Git.in_repo?
84
-
85
- message = full_commit_message
86
- puts message
87
- output = `git commit --no-status -m "#{message.gsub('"', '\"')}"`
88
- Common.error "git commit failed: #{output}" if $CHILD_STATUS.exitstatus.positive?
89
- rescue Interrupt
90
- Common.print_then_do_and_exit "\nCommit cancelled"
34
+ super() do
35
+ @usage = 'commit -h|(<subcommand> [<subcommand-options])'
36
+ @description = 'Create a new semantic commit.'
37
+ @subcommands = {}
91
38
  end
92
39
  end
93
40
 
@@ -143,5 +90,55 @@ class Commit < Command
143
90
  ask_for_message
144
91
  end.to_s.strip
145
92
  end
93
+
94
+ protected
95
+
96
+ def setup_option_parser
97
+ @option_parser = Optimist::Parser.new(
98
+ @usage,
99
+ full_description,
100
+ stop_condition
101
+ ) do |usage_header, description, stop_condition|
102
+ usage usage_header
103
+ synopsis description
104
+ opt :type,
105
+ 'Define the type of the commit. Enabling this option skips the type selection.',
106
+ { type: :string }
107
+ opt :scope,
108
+ 'Define the scope of the commit. Enabling this option skips the scope selection.',
109
+ { type: :string, short: 'S' }
110
+ opt :summary,
111
+ 'Define the summary message of the commit. Enabling this option skips the summary message prompt.',
112
+ { type: :string, short: 's' }
113
+ opt :message,
114
+ 'Define the message body of the commit. Enabling this option skips the message body prompt.',
115
+ { type: :string }
116
+ opt :breaking,
117
+ 'Set the commit to have a breaking change. ' \
118
+ 'Can be negated with "--no-breaking". ' \
119
+ 'Enabling this option skips the breaking change prompt.',
120
+ { type: :flag, short: :none }
121
+ opt :quiet,
122
+ 'Disable the print of the complete message.'
123
+ educate_on_error
124
+ stop_on stop_condition
125
+ end
126
+ end
127
+
128
+ def setup_action
129
+ @action = lambda do
130
+ @options = Common.with_subcommand_exception_handling @option_parser do
131
+ @option_parser.parse
132
+ end
133
+ Common.error 'commit need to be run inside a git repository' unless Git.in_repo?
134
+
135
+ message = full_commit_message
136
+ puts message unless @options[:quiet]
137
+ command_result = CommandIssuer.run('git', 'commit', '--no-status', '-m', "\"#{message.gsub('"', '\"')}\"")
138
+ Common.error "git commit failed: #{command_result.output}" if command_result.exit_status.positive?
139
+ rescue Interrupt
140
+ Common.print_then_do_and_exit "\nCommit cancelled"
141
+ end
142
+ end
146
143
  end
147
144
  # rubocop:enable Metrics/ClassLength