git_toolbox 0.4.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3adafc4b788997e3c6f09b238f7638c3fce70099bbf799da197728382ddc7e24
4
+ data.tar.gz: d35020b9402e19053d3c75eae5b6cdfbbab3aea07b53e46c88851417e5096e34
5
+ SHA512:
6
+ metadata.gz: 4a419b04905061a0485434c13dd8c480b587419c5683f89a3b548407d87af918070a495c44fdc1be48143fad39a0f7b1399087935c92f8c4ba57a797e9894aaf
7
+ data.tar.gz: 14cfd956d76eb9eb8717bd561c829756b2ed9b2959d5ddf1187ab5e34ee72a1608a3d41578f6ddd5a4b4028588ed70e82221697b9b28200ee2b5a89d41262d54
data/bin/get ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'rubygems'
5
+ require 'bundler/setup'
6
+ require 'get'
7
+
8
+ Get.main
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/lib/get/common.rb ADDED
@@ -0,0 +1,72 @@
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
+ # Utility module
21
+ module Common
22
+ # Groups: 1 = type, 2 = scope with (), 3 = scope, 4 = breaking change
23
+ CONVENTIONAL_COMMIT_REGEX = /^(\w+)(\((\w+)\))?(!)?:.*/
24
+
25
+ # Check if the command is called while in a git repository.
26
+ # If the command fails, it is assumed to not be in a git repository.
27
+ def self.in_git_repo?
28
+ system('git rev-parse --is-inside-work-tree &>/dev/null')
29
+ end
30
+
31
+ # Print an error message and optionally run a block.
32
+ # Stdout becomes stderr, so every print is performed to stderr.
33
+ # This behavior is wanted as this method is called on errors.
34
+ def self.error(message, &block)
35
+ Common.print_then_do_and_exit("Error: #{message}", 1, block)
36
+ end
37
+
38
+ # Subcommand exception handling for Optimist.
39
+ # Generally subcommands do not have a version to print.
40
+ def self.with_subcommand_exception_handling(parser)
41
+ yield
42
+ rescue Optimist::CommandlineError => e
43
+ parser.die(e.message, nil, e.error_code)
44
+ rescue Optimist::HelpNeeded
45
+ parser.educate
46
+ exit
47
+ rescue Optimist::VersionNeeded
48
+ # Version is not needed in this command
49
+ end
50
+
51
+ # Run a block of code with the list of commits from the given version as an argument.
52
+ # If the block is not given, this method is a nop.
53
+ def self.with_commit_list_from(version = nil, &block)
54
+ return unless block_given?
55
+
56
+ commits_from_version =
57
+ `git --no-pager log --oneline --pretty=format:%s #{version.nil? ? '' : "^#{version}"} HEAD`
58
+ .split("\n")
59
+ block.call(commits_from_version)
60
+ end
61
+
62
+ # Print the given message, execute a block if given,
63
+ # and exit the program with the given exit status.
64
+ # If exit_status is not 0, the stdout is redirected to stderr.
65
+ def self.print_then_do_and_exit(message, exit_code = 0, action = proc {})
66
+ $stdout = $stderr unless exit_code.zero?
67
+
68
+ puts message
69
+ action.call if action.respond_to?('call')
70
+ exit(exit_code)
71
+ end
72
+ end
@@ -0,0 +1,31 @@
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
+ # Base class for (sub)commands. (Sub)Commands should be singletons.
21
+ class Command
22
+ attr_reader :description, :action
23
+
24
+ protected
25
+
26
+ def initialize(usage, description, &action)
27
+ @usage = usage
28
+ @description = description
29
+ @action = action
30
+ end
31
+ end
@@ -0,0 +1,145 @@
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 'English'
21
+ require 'get/common'
22
+ require 'get/subcommand/command'
23
+ require 'get/subcommand/commit/prompt'
24
+
25
+ # Class length is disabled as most of its length is given by formatting.
26
+ # rubocop:disable Metrics/ClassLength
27
+ # Subcommand, it manages the description of the current git repository using semantic version.
28
+ class Commit < Command
29
+ def self.command
30
+ @@command ||= new
31
+ @@command
32
+ end
33
+
34
+ private_class_method :new
35
+
36
+ private
37
+
38
+ include PromptHandler
39
+
40
+ @@command = nil
41
+
42
+ @@usage = 'commit -h|(<subcommand> [<subcommand-options])'
43
+ @@description = 'Create a new semantic commit'
44
+ @@subcommands = {}
45
+ # This block is Optimist configuration. It is as long as the number of options of the command.
46
+ # rubocop:disable Metrics/BlockLength
47
+ @@commit_parser = Optimist::Parser.new do
48
+ subcommand_max_length = @@subcommands.keys.map { |k| k.to_s.length }.max
49
+ usage @@usage
50
+ synopsis <<~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
+ opt :type,
55
+ 'Define the type of the commit. Enabling this option skips the type selection.',
56
+ { type: :string }
57
+ opt :scope,
58
+ 'Define the scope of the commit. Enabling this option skips the scope selection.',
59
+ { type: :string, short: 'S' }
60
+ opt :summary,
61
+ 'Define the summary message of the commit. Enabling this option skips the summary message prompt.',
62
+ { type: :string, short: 's' }
63
+ opt :message,
64
+ 'Define the message body of the commit. Enabling this option skips the message body prompt.',
65
+ { type: :string }
66
+ opt :breaking,
67
+ 'Set the commit to have a breaking change. ' \
68
+ 'Can be negated with "--no-breaking". ' \
69
+ 'Enabling this option skips the breaking change prompt.',
70
+ { type: :flag, short: :none }
71
+ educate_on_error
72
+ stop_on @@subcommands.keys.map(&:to_s)
73
+ end
74
+ # rubocop:enable Metrics/BlockLength
75
+
76
+ def initialize
77
+ super(@@usage, @@description) do
78
+ Common.error 'commit need to be run inside a git repository' unless Common.in_git_repo?
79
+ @options = Common.with_subcommand_exception_handling @@commit_parser do
80
+ @@commit_parser.parse
81
+ end
82
+
83
+ message = full_commit_message
84
+ puts message
85
+ output = `git commit --no-status -m "#{message.gsub('"', '\"')}"`
86
+ Common.error "git commit failed: #{output}" if $CHILD_STATUS.exitstatus.positive?
87
+ rescue Interrupt
88
+ Common.print_then_do_and_exit "\nCommit cancelled"
89
+ end
90
+ end
91
+
92
+ def full_commit_message
93
+ type = commit_type
94
+ scope = commit_scope
95
+ breaking = commit_breaking?
96
+ summary = commit_summary
97
+ body = commit_body
98
+ "#{type}" \
99
+ "#{scope.nil? || scope.empty? ? '' : "(#{scope})"}" \
100
+ "#{breaking ? '!' : ''}" \
101
+ ": #{summary}" \
102
+ "#{body.empty? ? '' : "\n\n#{body}"}"
103
+ end
104
+
105
+ def commit_type
106
+ if @options[:type_given]
107
+ @options[:type]
108
+ else
109
+ ask_for_type
110
+ end.to_s.strip
111
+ end
112
+
113
+ def commit_scope
114
+ if @options[:scope_given]
115
+ @options[:scope]
116
+ else
117
+ ask_for_scope
118
+ end.to_s.strip
119
+ end
120
+
121
+ def commit_breaking?
122
+ if @options[:breaking_given]
123
+ @options[:breaking]
124
+ else
125
+ ask_for_breaking
126
+ end
127
+ end
128
+
129
+ def commit_summary
130
+ if @options[:summary_given]
131
+ @options[:summary]
132
+ else
133
+ ask_for_summary
134
+ end.to_s.strip
135
+ end
136
+
137
+ def commit_body
138
+ if @options[:message_given]
139
+ @options[:message]
140
+ else
141
+ ask_for_message
142
+ end.to_s.strip
143
+ end
144
+ end
145
+ # rubocop:enable Metrics/ClassLength
@@ -0,0 +1,123 @@
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 'highline'
21
+
22
+ # Module for asking to the user informations about a commit message.
23
+ module PromptHandler
24
+ @@cli = HighLine.new
25
+
26
+ @@custom_values_initialized = nil
27
+ @@custom_types = []
28
+ @@custom_scopes = []
29
+
30
+ STRING_VALUE_VALIDATOR = /\s*\S+\s*/
31
+ BODY_END_DELIMITER = "\n\n\n"
32
+
33
+ DEFAULT_TYPES = %i[
34
+ feat
35
+ fix
36
+ build
37
+ chore
38
+ ci
39
+ docs
40
+ style
41
+ refactor
42
+ perf
43
+ test
44
+ ].freeze
45
+
46
+ def ask_for_type
47
+ extract_types_and_scopes
48
+ @@cli.choose do |menu|
49
+ menu.flow = :columns_down
50
+ menu.prompt = 'Choose the type of your commit: '
51
+ DEFAULT_TYPES.union(@@custom_types).each do |type|
52
+ menu.choice(type.to_sym)
53
+ end
54
+ menu.choice('Create a new type (rarely needed)') do |_|
55
+ @@cli.ask('Write the new type to use', String) do |question|
56
+ question.verify_match = true
57
+ question.validate = STRING_VALUE_VALIDATOR
58
+ end
59
+ end
60
+ end
61
+ end
62
+
63
+ def ask_for_scope
64
+ extract_types_and_scopes
65
+ @@cli.choose do |menu|
66
+ menu.flow = :columns_down
67
+ menu.prompt = 'Choose the scope of your commit '
68
+ @@custom_scopes.each do |scope|
69
+ menu.choice(scope.to_sym)
70
+ end
71
+ menu.choice('Create a new scope') do |_|
72
+ @@cli.ask('Write the new scope to use', String) do |question|
73
+ question.verify_match = true
74
+ question.validate = STRING_VALUE_VALIDATOR
75
+ end
76
+ end
77
+ menu.choice('None') { '' }
78
+ end
79
+ end
80
+
81
+ def ask_for_breaking
82
+ @@cli.agree('Does the commit contain a breaking change? (yes/no) ') do |question|
83
+ question.default = false
84
+ end
85
+ end
86
+
87
+ def ask_for_summary
88
+ @@cli.ask('The summary of the commit:') do |question|
89
+ question.verify_match = true
90
+ question.validate = STRING_VALUE_VALIDATOR
91
+ end
92
+ end
93
+
94
+ def ask_for_message
95
+ # This method needs a special implementation as the body message can span multiple lines.
96
+ @@cli.puts('The body of the commit (ends after 3 new lines):')
97
+ @@cli.input.gets(BODY_END_DELIMITER)
98
+ end
99
+
100
+ private
101
+
102
+ FIRST_COMMIT = nil
103
+
104
+ # This method tries to optimize input parsing by performing multiple operations in one go.
105
+ # So its complexity is a bit higher as it needs to make multiple checks.
106
+ # rubocop:disable Metrics/CyclomaticComplexity
107
+ def extract_types_and_scopes
108
+ return unless @@custom_values_initialized.nil?
109
+
110
+ Common.with_commit_list_from(FIRST_COMMIT) do |commit_list|
111
+ commit_list.map do |element|
112
+ match = Common::CONVENTIONAL_COMMIT_REGEX.match(element)
113
+ next if match.nil?
114
+
115
+ type_already_added = DEFAULT_TYPES.include?(match[1].to_sym) || @@custom_types.include?(match[1])
116
+ @@custom_types.append(match[1]) unless type_already_added
117
+ @@custom_scopes.append(match[3]) unless match[3].nil? || @@custom_scopes.include?(match[3])
118
+ end
119
+ end
120
+ @@custom_values_initialized = true
121
+ end
122
+ # rubocop:enable Metrics/CyclomaticComplexity
123
+ end
@@ -0,0 +1,71 @@
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
+ # Module which handles change-related tasks.
21
+ module ChangeHandler
22
+ # Array with change types in ascending order of importance.
23
+ CHANGE_TYPE = %i[NONE PATCH MINOR MAJOR].freeze
24
+
25
+ @@major_trigger = 'is_breaking'
26
+ @@minor_trigger = "type == 'feat'"
27
+ @@patch_trigger = "type == 'fix'"
28
+
29
+ module_function
30
+
31
+ # In this block method arguments can be used by user.
32
+ # Also `eval` is needed to allow users to define their custom triggers.
33
+ # rubocop:disable Lint/UnusedMethodArgument
34
+ # rubocop:disable Security/Eval
35
+ def triggers_major?(type, scope, is_breaking)
36
+ eval(@@major_trigger)
37
+ end
38
+
39
+ def triggers_minor?(type, scope)
40
+ eval(@@minor_trigger)
41
+ end
42
+
43
+ def triggers_patch?(type, scope)
44
+ eval(@@patch_trigger)
45
+ end
46
+ # rubocop:enable Lint/UnusedMethodArgument
47
+ # rubocop:enable Security/Eval
48
+
49
+ # Open String class to inject method to convert a (commit) string into
50
+ # a change.
51
+ class ::String
52
+ # Convert the string (as a conventional commit string) into a change type.
53
+ def to_change
54
+ groups = Common::CONVENTIONAL_COMMIT_REGEX.match(self)
55
+ return :MAJOR if ChangeHandler.triggers_major?(groups[1], groups[3], !groups[4].nil?)
56
+ return :MINOR if ChangeHandler.triggers_minor?(groups[1], groups[2])
57
+ return :PATCH if ChangeHandler.triggers_patch?(groups[1], groups[2])
58
+
59
+ :NONE
60
+ end
61
+ end
62
+
63
+ public
64
+
65
+ def greatest_change_in(commit_list)
66
+ commit_list
67
+ .grep(Common::CONVENTIONAL_COMMIT_REGEX)
68
+ .map(&:to_change)
69
+ .max { |a, b| CHANGE_TYPE.index(a) <=> CHANGE_TYPE.index(b) }
70
+ end
71
+ end
@@ -0,0 +1,230 @@
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 'get/subcommand/command'
21
+ require 'get/subcommand/describe/change'
22
+ require 'get/subcommand/describe/prerelease'
23
+ require 'get/subcommand/describe/metadata'
24
+ require 'get/subcommand/describe/docker/docker'
25
+
26
+ # Class length is disabled as most of its length is given by formatting.
27
+ # rubocop:disable Metrics/ClassLength
28
+ # Subcommand, it manages the description of the current git repository using semantic version.
29
+ class Describe < Command
30
+ def self.command
31
+ @@command ||= new
32
+ @@command
33
+ end
34
+
35
+ private_class_method :new
36
+
37
+ private
38
+
39
+ include ChangeHandler
40
+ include PrereleaseHandler
41
+ include MetadataHandler
42
+
43
+ @@command = nil
44
+
45
+ DEFAULT_RELEASE_VERSION = '0.1.0'
46
+ FULL_SEMANTIC_VERSION_REGEX = /
47
+ ^((0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)) # Stable version, major, minor, patch
48
+ (?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))? # prerelease
49
+ (?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$ # metadata
50
+ /x
51
+
52
+ @@usage = 'describe -h|(<subcommand> [<subcommand-options])'
53
+ @@description = 'Describe the current git repository with semantic version'
54
+ @@subcommands = {
55
+ docker: DescribeDocker.command,
56
+ }
57
+ # This block is Optimist configuration. It is as long as the number of options of the command.
58
+ # rubocop:disable Metrics/BlockLength
59
+ @@describe_parser = Optimist::Parser.new do
60
+ subcommand_max_length = @@subcommands.keys.map { |k| k.to_s.length }.max
61
+ usage @@usage
62
+ synopsis <<~SUBCOMMANDS unless @@subcommands.empty?
63
+ Subcommands:
64
+ #{@@subcommands.keys.map { |k| " #{k.to_s.ljust(subcommand_max_length)} => #{@@subcommands[k].description}" }.join("\n")}
65
+ SUBCOMMANDS
66
+ opt :prerelease,
67
+ 'Describe a prerelease rather than a release',
68
+ short: :none
69
+ opt :exclude_metadata,
70
+ 'Do not include metadata in version.'
71
+ opt :metadata,
72
+ 'Set which metadata to include in the string. ' \
73
+ 'Multiple value can be specified by separating the with a comma \',\'.',
74
+ { type: :string, default: 'sha' }
75
+ opt :major_trigger,
76
+ 'Set the trigger for a major release. ' \
77
+ 'This must be a valid Ruby expression. ' \
78
+ 'In this expression the string values "type" and "scope" ' \
79
+ 'and the boolean value "is_breaking" can be used.',
80
+ { short: :none, type: :string, default: 'is_breaking' }
81
+ opt :minor_trigger,
82
+ 'Set the trigger for a minor release. ' \
83
+ 'This must be a valid Ruby expression. ' \
84
+ 'In this expression the string values "type" and "scope" can be used.',
85
+ { short: :none, type: :string, default: "type == 'feat'" }
86
+ opt :patch_trigger,
87
+ 'Set the trigger for a patch release. ' \
88
+ 'This must be a valid Ruby expression. ' \
89
+ 'In this expression the string values "type" and "scope" can be used.',
90
+ { short: :none, type: :string, default: "type == 'fix'" }
91
+ opt :prerelease_pattern,
92
+ 'Set the pattern of the prerelease. This must contain the placeholder "(p)".',
93
+ { short: :none, type: :string, default: 'dev(p)' }
94
+ opt :old_prerelease_pattern,
95
+ 'Set the pattern of the old prerelease. It is useful for changing prerelease pattern.',
96
+ { short: :none, type: :string, default: 'prerelease-pattern value' }
97
+ opt :diff,
98
+ 'Print also the last version.'
99
+ opt :create_tag,
100
+ 'Create a signed tag with the computed version.',
101
+ { short: :none }
102
+ opt :tag_message,
103
+ 'Add the given message to the tag. Requires "--create-tag".',
104
+ { short: :none, type: :string }
105
+ educate_on_error
106
+ stop_on @@subcommands.keys.map(&:to_s)
107
+ end
108
+ # rubocop:enable Metrics/BlockLength
109
+
110
+ def initialize
111
+ super(@@usage, @@description) do
112
+ Common.error 'describe need to be run inside a git repository' unless Common.in_git_repo?
113
+ @options = Common.with_subcommand_exception_handling @@describe_parser do
114
+ @@describe_parser.parse
115
+ end
116
+ set_options
117
+
118
+ if ARGV.length.positive?
119
+ subcommand = ARGV.shift.to_sym
120
+ if @@subcommands.include?(subcommand)
121
+ @@subcommands[subcommand].action.call(describe_current_commit)
122
+ else
123
+ # This error should not be disabled by -W0
124
+ # rubocop:disable Style/StderrPuts
125
+ $stderr.puts "Error: subcommand '#{subcommand}' unknown."
126
+ # rubocop:enable Style/StderrPuts
127
+ exit 1
128
+ end
129
+ else
130
+ puts describe_current_commit
131
+ end
132
+ end
133
+ end
134
+
135
+ def set_options
136
+ @@major_trigger = @options[:major_trigger] if @options[:major_trigger_given]
137
+ @@minor_trigger = @options[:minor_trigger] if @options[:minor_trigger_given]
138
+ @@patch_trigger = @options[:patch_trigger] if @options[:patch_trigger_given]
139
+ @@old_prerelease_pattern = @options[:old_prerelease_pattern] if @options[:old_prerelease_pattern_given]
140
+ @@prerelease_pattern = @options[:prerelease_pattern] if @options[:prerelease_pattern_given]
141
+ end
142
+
143
+ def describe_current_commit
144
+ return last_version if Common.with_commit_list_from(last_version, &:empty?)
145
+
146
+ puts "Last version: #{last_version}" if @options[:diff]
147
+
148
+ current_commit_version = next_release
149
+ create_signed_tag(current_commit_version) if @options[:create_tag]
150
+
151
+ current_commit_version
152
+ end
153
+
154
+ def next_release
155
+ if @options[:prerelease]
156
+ prepare_prerelease_tag(last_release, last_version)
157
+ else
158
+ prepare_release_tag(last_release)
159
+ end + metadata
160
+ end
161
+
162
+ def prepare_release_tag(last_release)
163
+ updated_stable_version(last_release).to_s
164
+ end
165
+
166
+ def prepare_prerelease_tag(last_release, last_version)
167
+ new_stable_version = updated_stable_version(last_release)
168
+ base_version_match_data = FULL_SEMANTIC_VERSION_REGEX.match(last_version)
169
+ no_changes_from_last_release = base_version_match_data[1] == new_stable_version && base_version_match_data[5].nil?
170
+ Common.error 'No changes from last release' if no_changes_from_last_release
171
+ new_stable_version +
172
+ "-#{updated_prerelease(base_version_match_data[5], need_reset: base_version_match_data[1] != new_stable_version)}"
173
+ end
174
+
175
+ # Returns the last version and caches it for the next calls.
176
+ def last_version
177
+ @last_version ||= `git describe --tags --abbrev=0`.strip
178
+ end
179
+
180
+ # Returns the last release and caches it for the next calls.
181
+ def last_release
182
+ @last_release ||= `git --no-pager tag --list | sed 's/+/_/' | sort -V | sed 's/_/+/' | tail -n 1`.strip
183
+ end
184
+
185
+ def updated_stable_version(stable_version)
186
+ return DEFAULT_RELEASE_VERSION if stable_version.nil?
187
+
188
+ greatest_change_from_stable_version = Common.with_commit_list_from(stable_version) do |commits_from_version|
189
+ greatest_change_in(commits_from_version)
190
+ end
191
+ split_version = stable_version.split('.')
192
+ case greatest_change_from_stable_version
193
+ when :MAJOR
194
+ "#{split_version[0].to_i + 1}.0.0"
195
+ when :MINOR
196
+ "#{split_version[0].to_i}.#{split_version[1].to_i + 1}.0"
197
+ when :PATCH
198
+ "#{split_version[0].to_i}.#{split_version[1].to_i}.#{split_version[2].to_i + 1}"
199
+ else
200
+ "#{split_version[0].to_i}.#{split_version[1].to_i}.#{split_version[2].to_i}"
201
+ end
202
+ end
203
+
204
+ # Return the updated prerelease number
205
+ def updated_prerelease(prerelease = nil, need_reset: false)
206
+ compute_prerelease(prerelease, need_reset: prerelease.nil? || need_reset)
207
+ end
208
+
209
+ # Compute the metadata string
210
+ def metadata
211
+ return '' if @options[:exclude_metadata] || @options[:metadata].empty?
212
+
213
+ "+#{compute_metadata(@options[:metadata])}"
214
+ end
215
+
216
+ def create_signed_tag(computed_version)
217
+ system(
218
+ 'git tag -s ' \
219
+ "#{
220
+ if @options[:tag_message_given]
221
+ "-m #{@options[:tag_message]}"
222
+ else
223
+ ''
224
+ end
225
+ } " \
226
+ "'#{computed_version}'"
227
+ )
228
+ end
229
+ end
230
+ # rubocop:enable Metrics/ClassLength
@@ -0,0 +1,116 @@
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 'get/subcommand/command'
21
+
22
+ # Class length is disabled as most of its length is given by formatting.
23
+ # rubocop:disable Metrics/ClassLength
24
+ # Subcommand, it manages the description of the current git repository using semantic version.
25
+ class DescribeDocker < Command
26
+ def self.command
27
+ @@command ||= new
28
+ @@command
29
+ end
30
+
31
+ private_class_method :new
32
+
33
+ private
34
+
35
+ INCREMENTAL_VERSION_PATTERN = /(((\d+)\.\d+)\.\d+)/
36
+
37
+ @@command = nil
38
+
39
+ @@usage = 'describe docker -h|(<subcommand> [<subcommand-options])'
40
+ @@description = 'Describe the current git repository with a list of version for docker'
41
+ @@subcommands = {}
42
+ # This block is Optimist configuration. It is as long as the number of options of the command.
43
+ # rubocop:disable Metrics/BlockLength
44
+ @@describe_parser = Optimist::Parser.new do
45
+ subcommand_max_length = @@subcommands.keys.map { |k| k.to_s.length }.max
46
+ usage @@usage
47
+ synopsis <<~SUBCOMMANDS unless @@subcommands.empty?
48
+ Subcommands:
49
+ #{@@subcommands.keys.map { |k| " #{k.to_s.ljust(subcommand_max_length)} => #{@@subcommands[k].description}" }.join("\n")}
50
+ SUBCOMMANDS
51
+ opt :separator,
52
+ 'Use the given value as separator for versions',
53
+ { type: :string, default: '\n' }
54
+ opt :not_latest,
55
+ 'Do not include "latest" in the version list.',
56
+ short: :none
57
+ opt :substitute_plus,
58
+ 'Set which character will be used in place of "+".',
59
+ { type: :string, short: :none }
60
+ educate_on_error
61
+ stop_on @@subcommands.keys.map(&:to_s)
62
+ end
63
+ # rubocop:enable Metrics/BlockLength
64
+
65
+ def initialize
66
+ super(@@usage, @@description) do |version|
67
+ Common.error 'describe need to be run inside a git repository' unless Common.in_git_repo?
68
+ @options = Common.with_subcommand_exception_handling @@describe_parser do
69
+ @@describe_parser.parse
70
+ end
71
+ set_options
72
+
73
+ puts version_list_from(version).join(@@separator)
74
+ end
75
+ end
76
+
77
+ def set_options
78
+ @@separator = if @options[:separator_given]
79
+ @options[:separator]
80
+ else
81
+ "\n"
82
+ end
83
+ @@not_latest = @options[:not_latest]
84
+ @@plus_substitution = if @options[:substitute_plus_given]
85
+ @options[:substitute_plus]
86
+ else
87
+ '+'
88
+ end
89
+ end
90
+
91
+ def version_list_from(full_version)
92
+ [
93
+ full_version.sub('+', @@plus_substitution),
94
+ reduced_versions(full_version),
95
+ latest
96
+ ]
97
+ end
98
+
99
+ def reduced_versions(full_version)
100
+ base_version = full_version.partition('+')[0]
101
+ if base_version.include?('-')
102
+ base_version
103
+ else
104
+ INCREMENTAL_VERSION_PATTERN.match(base_version).captures
105
+ end
106
+ end
107
+
108
+ def latest
109
+ if @options[:not_latest]
110
+ []
111
+ else
112
+ ['latest']
113
+ end
114
+ end
115
+ end
116
+ # rubocop:enable Metrics/ClassLength
@@ -0,0 +1,53 @@
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
+ # Module with methods to handle tag metadata.
21
+ #
22
+ # To add a new metadata type, create a new method and link it to a symbol.
23
+ module MetadataHandler
24
+ @@metadata_computers = {}
25
+
26
+ module_function
27
+
28
+ def last_commit_sha
29
+ `git --no-pager log -n 1 --pretty=%h`.strip
30
+ end
31
+
32
+ def current_date
33
+ Time.now.strftime('%0Y%0m%0d')
34
+ end
35
+
36
+ def init_computers
37
+ @@metadata_computers[:sha] = proc { last_commit_sha }
38
+ @@metadata_computers[:date] = proc { current_date }
39
+ end
40
+
41
+ public
42
+
43
+ def compute_metadata(metadata_specs)
44
+ metadata_specs
45
+ .split(',')
46
+ .map { |element| @@metadata_computers[element.to_sym].call }
47
+ .join('-')
48
+ end
49
+
50
+ def self.included(_mod)
51
+ init_computers
52
+ end
53
+ end
@@ -0,0 +1,55 @@
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
+ # Module with methods for managing prerelease updates.
21
+ module PrereleaseHandler
22
+ FIRST_PRERELEASE = 1
23
+ DEFAULT_PRERELEASE_STRING = 'dev'
24
+ PRERELEASE_PLACEHOLDER = '(p)'
25
+
26
+ @@prerelease_pattern = "#{DEFAULT_PRERELEASE_STRING}#{PRERELEASE_PLACEHOLDER}"
27
+ @@old_prerelease_pattern = proc { @@prerelease_pattern }
28
+
29
+ module_function
30
+
31
+ def extract_prerelease_number(current_prerelease)
32
+ actual_old_prerelease_pattern =
33
+ if @@old_prerelease_pattern.respond_to?('call')
34
+ @@old_prerelease_pattern.call
35
+ else
36
+ @@old_prerelease_pattern
37
+ end
38
+ Common.error "The given old pattern does not contains the placeholder '(p)'" unless
39
+ actual_old_prerelease_pattern.include?(PRERELEASE_PLACEHOLDER)
40
+ old_prerelease_regex = actual_old_prerelease_pattern.sub(PRERELEASE_PLACEHOLDER, '(\\d+)')
41
+ begin
42
+ Regexp.new(old_prerelease_regex).match(current_prerelease)[1].to_i
43
+ rescue NoMethodError
44
+ Common.error "The given old prerelease pattern '#{actual_old_prerelease_pattern}' " \
45
+ "does not match the analyzed prerelease: '#{current_prerelease}'."
46
+ end
47
+ end
48
+
49
+ public
50
+
51
+ def compute_prerelease(current_prerelease, need_reset: false)
52
+ new_prerelease = (need_reset ? FIRST_PRERELEASE : (extract_prerelease_number(current_prerelease) + 1)).to_s
53
+ @@prerelease_pattern.sub(PRERELEASE_PLACEHOLDER, new_prerelease)
54
+ end
55
+ end
@@ -0,0 +1,85 @@
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 'English'
21
+ require 'get/common'
22
+ require 'get/subcommand/command'
23
+
24
+ # Class length is disabled as most of its length is given by formatting.
25
+ # rubocop:disable Metrics/ClassLength
26
+ # Subcommand, it allow to create a new repository and add an initial, empty commit to it.
27
+ class Init < Command
28
+ def self.command
29
+ @@command ||= new
30
+ @@command
31
+ end
32
+
33
+ private_class_method :new
34
+
35
+ private
36
+
37
+ include PromptHandler
38
+
39
+ @@command = nil
40
+
41
+ @@usage = 'init -h|(<subcommand> [<subcommand-options])'
42
+ @@description = 'Initialize a new git repository with an initial empty commit'
43
+ @@subcommands = {}
44
+ # This block is Optimist configuration. It is as long as the number of options of the command.
45
+ # rubocop:disable Metrics/BlockLength
46
+ @@commit_parser = Optimist::Parser.new do
47
+ subcommand_max_length = @@subcommands.keys.map { |k| k.to_s.length }.max
48
+ usage @@usage
49
+ synopsis <<~SUBCOMMANDS unless @@subcommands.empty?
50
+ Subcommands:
51
+ #{@@subcommands.keys.map { |k| " #{k.to_s.ljust(subcommand_max_length)} => #{@@subcommands[k].description}" }.join("\n")}
52
+ SUBCOMMANDS
53
+ opt :empty,
54
+ 'Do not create the first, empty commit.'
55
+ educate_on_error
56
+ stop_on @@subcommands.keys.map(&:to_s)
57
+ end
58
+ # rubocop:enable Metrics/BlockLength
59
+
60
+ def initialize
61
+ super(@@usage, @@description) do
62
+ @options = Common.with_subcommand_exception_handling @@commit_parser do
63
+ @@commit_parser.parse
64
+ end
65
+ Common.error 'The current directory is already a git repository' if Common.in_git_repo?
66
+
67
+ init_repository
68
+ end
69
+ end
70
+
71
+ def init_repository
72
+ `git init`
73
+ Common.error 'Failed to init the repository' if $CHILD_STATUS.exitstatus.positive?
74
+
75
+ create_first_commit unless @options[:empty]
76
+
77
+ puts 'Git repository initialized'
78
+ end
79
+
80
+ def create_first_commit
81
+ `git commit --allow-empty -m "chore: initialize repository"`
82
+ Common.error 'Failed to create first commit' if $CHILD_STATUS.exitstatus.positive?
83
+ end
84
+ end
85
+ # rubocop:enable Metrics/ClassLength
@@ -0,0 +1,22 @@
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
+ module Get
21
+ VERSION = '0.4.1'
22
+ end
data/lib/get.rb ADDED
@@ -0,0 +1,67 @@
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 'optimist'
21
+
22
+ require 'get/subcommand/describe/describe'
23
+ require 'get/subcommand/commit/commit'
24
+ require 'get/subcommand/init/init'
25
+ require 'get/version'
26
+ require 'get/common'
27
+
28
+ # Entrypoint of Get
29
+ module Get
30
+ class Error < StandardError; end
31
+
32
+ @@subcommands = {
33
+ describe: Describe.command,
34
+ commit: Commit.command,
35
+ init: Init.command,
36
+ }
37
+ @@option_parser = Optimist::Parser.new do
38
+ subcommand_max_length = @@subcommands.keys.map { |k| k.to_s.length }.max
39
+ usage '-h|-v|(<subcommand> [<subcommand-options])'
40
+ synopsis <<~SUBCOMMANDS unless @@subcommands.empty?
41
+ Subcommands:
42
+ #{@@subcommands.keys.map { |k| " #{k.to_s.ljust(subcommand_max_length)} => #{@@subcommands[k].description}" }.join("\n")}
43
+ SUBCOMMANDS
44
+ version "Get version: #{Get::VERSION}"
45
+ educate_on_error
46
+ stop_on @@subcommands.keys.map(&:to_s)
47
+ end
48
+
49
+ def self.main
50
+ @options = Optimist.with_standard_exception_handling(@@option_parser) do
51
+ @@option_parser.parse
52
+ end
53
+ error 'No command or option specified' if ARGV.empty?
54
+ command = ARGV.shift.to_sym
55
+ if @@subcommands.include?(command)
56
+ @@subcommands[command].action.call
57
+ else
58
+ error "Unknown subcommand '#{command}'"
59
+ end
60
+ end
61
+
62
+ def self.error(message)
63
+ Common.error message do
64
+ @@option_parser.educate
65
+ end
66
+ end
67
+ end
metadata ADDED
@@ -0,0 +1,96 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: git_toolbox
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.4.1
5
+ platform: ruby
6
+ authors:
7
+ - Alex Speranza
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-01-31 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: optimist
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.0'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 3.0.1
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '3.0'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 3.0.1
33
+ - !ruby/object:Gem::Dependency
34
+ name: highline
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: 2.0.3
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: 2.0.3
47
+ description:
48
+ email:
49
+ - alex.speranza@studio.unibo.it
50
+ executables:
51
+ - get
52
+ extensions: []
53
+ extra_rdoc_files: []
54
+ files:
55
+ - bin/get
56
+ - bin/setup
57
+ - lib/get.rb
58
+ - lib/get/common.rb
59
+ - lib/get/subcommand/command.rb
60
+ - lib/get/subcommand/commit/commit.rb
61
+ - lib/get/subcommand/commit/prompt.rb
62
+ - lib/get/subcommand/describe/change.rb
63
+ - lib/get/subcommand/describe/describe.rb
64
+ - lib/get/subcommand/describe/docker/docker.rb
65
+ - lib/get/subcommand/describe/metadata.rb
66
+ - lib/get/subcommand/describe/prerelease.rb
67
+ - lib/get/subcommand/init/init.rb
68
+ - lib/get/version.rb
69
+ homepage: https://github.com/asperan/get
70
+ licenses:
71
+ - LGPL-3.0-or-later
72
+ metadata:
73
+ homepage_uri: https://github.com/asperan/get
74
+ source_code_uri: https://github.com/asperan/get
75
+ allowed_push_host: https://rubygems.org
76
+ rubygems_mfa_required: 'true'
77
+ post_install_message:
78
+ rdoc_options: []
79
+ require_paths:
80
+ - lib
81
+ required_ruby_version: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: 3.1.0
86
+ required_rubygems_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ requirements: []
92
+ rubygems_version: 3.3.7
93
+ signing_key:
94
+ specification_version: 4
95
+ summary: Git Enhancement Toolbox
96
+ test_files: []