git_toolbox 0.4.1

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