git_toolbox 0.7.1 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/get +1 -1
- data/lib/get/commons/command_issuer.rb +60 -0
- data/lib/get/commons/common.rb +70 -0
- data/lib/get/commons/git.rb +31 -14
- data/lib/get/commons/http_client.rb +79 -0
- data/lib/get/subcommand/changelog/changelog.rb +56 -61
- data/lib/get/subcommand/command.rb +58 -6
- data/lib/get/subcommand/commit/commit.rb +58 -61
- data/lib/get/subcommand/commit/prompt.rb +25 -23
- data/lib/get/subcommand/complete/bash_completion.rb +7 -6
- data/lib/get/subcommand/complete/complete.rb +31 -36
- data/lib/get/subcommand/describe/change.rb +25 -27
- data/lib/get/subcommand/describe/describe.rb +113 -115
- data/lib/get/subcommand/describe/docker/docker.rb +55 -58
- data/lib/get/subcommand/describe/metadata.rb +16 -19
- data/lib/get/subcommand/describe/prerelease.rb +12 -13
- data/lib/get/subcommand/init/init.rb +43 -44
- data/lib/get/subcommand/license/license.rb +78 -56
- data/lib/get/subcommand/license/license_retriever.rb +38 -23
- data/lib/get/subcommand/tree/tree.rb +48 -42
- data/lib/get/version.rb +1 -3
- data/lib/get.rb +47 -34
- metadata +16 -14
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 500f22d2000495865a5671476122c580ea0e5fdb6ebf2804ce42d209828a1841
|
4
|
+
data.tar.gz: 70a3c312974ce5ee382a8c6793325bf676dba7fb40206ed828f56c2019ce9d64
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ce3a643b6a110c7b70e7ddc33649548b15f064e61dbe35c13fa5eba3fa70b7c71a5118185ee88b89aab83f74602b605d9175b4633e58c933ae30ebfae97c2dab
|
7
|
+
data.tar.gz: 5125a3a0af8ab4a6590af9166c95d442c1dae5f89648f1d94224ff5a4d3e18fcaf21ec5e9d52b1f50939340d17d60ddd5cd37fa7bd6bd16b4e7ce88f2ac868da
|
data/bin/get
CHANGED
@@ -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
|
data/lib/get/commons/common.rb
CHANGED
@@ -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
|
data/lib/get/commons/git.rb
CHANGED
@@ -17,23 +17,17 @@
|
|
17
17
|
|
18
18
|
# frozen_string_literal: true
|
19
19
|
|
20
|
-
|
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 =
|
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
|
-
|
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
|
-
|
45
|
-
|
46
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
21
|
-
|
22
|
-
|
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(
|
89
|
-
@
|
90
|
-
|
91
|
-
|
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
|
-
|
20
|
+
require 'singleton'
|
21
|
+
|
22
|
+
# Base class for (sub)commands.
|
21
23
|
class Command
|
22
|
-
|
24
|
+
include Singleton
|
25
|
+
|
26
|
+
attr_reader :usage, :description, :action, :subcommands, :option_parser
|
23
27
|
|
24
28
|
protected
|
25
29
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
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
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
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(
|
80
|
-
@
|
81
|
-
|
82
|
-
|
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
|