sem_ver_components 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/sem_ver_git +43 -0
- data/lib/sem_ver_components/local_git.rb +65 -0
- data/lib/sem_ver_components/output.rb +15 -0
- data/lib/sem_ver_components/outputs/info.rb +39 -0
- data/lib/sem_ver_components/outputs/semantic_release_analyze.rb +33 -0
- data/lib/sem_ver_components/outputs/semantic_release_generate_notes.rb +104 -0
- data/lib/sem_ver_components/plugins.rb +43 -0
- data/lib/sem_ver_components/version.rb +5 -0
- metadata +81 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 350d90498079e7485e8eab0897b6d02c92c8aa9bddea07d04e31425dcef60152
|
4
|
+
data.tar.gz: 4414c65c293c951d94346108c67523430229e22b2764205877a6c10e350b02b8
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a5b77dd8f100c03fe7b77f09adb0291c478095458d35756a3f10799e2d66e423c219bab025a81d19d9e382bc66db08cc56a1fb5a78ff5dcfcf178263f52092ab
|
7
|
+
data.tar.gz: e6ce0c0d97f7cdc3c336da1a28af412a037a3ca7ca278ca3d07adc4911672a17b2f947821078fff81f9ef751635ce75baea736308d82d0d5beadad0beb8c3e34
|
data/bin/sem_ver_git
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'optparse'
|
3
|
+
require 'sem_ver_components/plugins'
|
4
|
+
require 'sem_ver_components/local_git'
|
5
|
+
require 'sem_ver_components/version'
|
6
|
+
require 'sem_ver_components/output'
|
7
|
+
|
8
|
+
# Default values
|
9
|
+
git_repo = '.'
|
10
|
+
git_from = nil
|
11
|
+
git_to = 'HEAD'
|
12
|
+
output = :info
|
13
|
+
output_plugins = SemVerComponents::Plugins.new(:outputs)
|
14
|
+
OptionParser.new do |opts|
|
15
|
+
opts.banner = "Usage: #{File.basename($0)} [options]"
|
16
|
+
opts.on('-f', '--from GIT_REF', 'Git reference from which commits are to be analyzed (defaults to first commit)') do |git_ref|
|
17
|
+
git_from = git_ref
|
18
|
+
end
|
19
|
+
opts.on('-h', '--help', 'Display this help') do
|
20
|
+
puts opts
|
21
|
+
exit 0
|
22
|
+
end
|
23
|
+
opts.on('-o', '--output OUTPUT', "Specify the output format of the analysis. Possible values are #{output_plugins.list.sort.join(', ')} (defauts to #{output})") do |output_str|
|
24
|
+
output = output_str.to_sym
|
25
|
+
end
|
26
|
+
opts.on('-r', '--repo GIT_URL', "Specify the Git URL of the repository to analyze (defaults to #{git_repo})") do |git_url|
|
27
|
+
git_repo = git_url
|
28
|
+
end
|
29
|
+
opts.on('-t', '--to GIT_REF', "Git reference to which commits are to be analyzed (defaults to #{git_to})") do |git_ref|
|
30
|
+
git_to = git_ref
|
31
|
+
end
|
32
|
+
opts.on('-v', '--version', 'Display version') do
|
33
|
+
puts "#{File.basename($0)} v#{SemVerComponents::VERSION}"
|
34
|
+
exit 0
|
35
|
+
end
|
36
|
+
end.parse!
|
37
|
+
|
38
|
+
raise "Unknown parameters: #{ARGV.join(' ')}" unless ARGV.empty?
|
39
|
+
|
40
|
+
local_git = SemVerComponents::LocalGit.new(git_repo, git_from, git_to)
|
41
|
+
commits_info = local_git.analyze_commits
|
42
|
+
|
43
|
+
output_plugins[output].new(local_git).process(commits_info)
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'git'
|
2
|
+
|
3
|
+
module SemVerComponents
|
4
|
+
|
5
|
+
class LocalGit
|
6
|
+
|
7
|
+
attr_reader *%i[git_from git_to git]
|
8
|
+
|
9
|
+
# Constructor
|
10
|
+
#
|
11
|
+
# Parameters::
|
12
|
+
# * *git_repo* (String): The git repository to analyze
|
13
|
+
# * *git_from* (String or nil): The git from ref
|
14
|
+
# * *git_to* (String): The git to ref
|
15
|
+
def initialize(git_repo, git_from, git_to)
|
16
|
+
@git_repo = git_repo
|
17
|
+
@git_from = git_from
|
18
|
+
@git_to = git_to
|
19
|
+
@git = Git.open(@git_repo)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Get full the git log.
|
23
|
+
# Keep a cache of it.
|
24
|
+
#
|
25
|
+
# Result::
|
26
|
+
# * Array< Git::Object::Commit >: Full git log
|
27
|
+
def git_log
|
28
|
+
@git_log = @git.log(nil) unless defined?(@git_log)
|
29
|
+
@git_log
|
30
|
+
end
|
31
|
+
|
32
|
+
# Semantically analyze commits.
|
33
|
+
#
|
34
|
+
# Result::
|
35
|
+
# * Array< Hash<Symbol, Object> >: The commits information:
|
36
|
+
# * *components_bump_levels* (Hash<String or nil, Integer>): Set of bump levels (0: patch, 1: minor, 2: major) per component name (nil for global)
|
37
|
+
# * *commit* (Git::Object::Commit): Corresponding git commit
|
38
|
+
def analyze_commits
|
39
|
+
git_log.between(git_from.nil? ? git_log.last.sha : git_from, git_to).map do |git_commit|
|
40
|
+
# Analyze the message
|
41
|
+
# Always consider a minimum of global patch bump per commit.
|
42
|
+
components_bump_levels = { nil => [0] }
|
43
|
+
git_commit.message.scan(/\[([^\]]+)\]/).flatten(1).each do |commit_label|
|
44
|
+
commit_type, component = commit_label =~ /^(.+)\((.+)\)$/ ? [$1, $2] : [commit_label, nil]
|
45
|
+
components_bump_levels[component] = [] unless components_bump_levels.key?(component)
|
46
|
+
components_bump_levels[component] <<
|
47
|
+
case commit_type.downcase
|
48
|
+
when 'feat', 'feature'
|
49
|
+
1
|
50
|
+
when 'break', 'breaking'
|
51
|
+
2
|
52
|
+
else
|
53
|
+
0
|
54
|
+
end
|
55
|
+
end
|
56
|
+
{
|
57
|
+
commit: git_commit,
|
58
|
+
components_bump_levels: Hash[components_bump_levels.map { |component, component_bump_levels| [component, component_bump_levels.max] }]
|
59
|
+
}
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module SemVerComponents
|
2
|
+
|
3
|
+
module Outputs
|
4
|
+
|
5
|
+
class Info < Output
|
6
|
+
|
7
|
+
# Process commits info
|
8
|
+
#
|
9
|
+
# Parameters::
|
10
|
+
# * *commits_info* (Array< Hash<Symbol, Object> >): List of commits info:
|
11
|
+
# * *components_bump_levels* (Hash<String or nil, Integer>): Set of bump levels (0: patch, 1: minor, 2: major) per component name (nil for global)
|
12
|
+
# * *commit* (Git::Object::Commit): Corresponding git commit
|
13
|
+
def process(commits_info)
|
14
|
+
# Display bump levels per component
|
15
|
+
commits_info.inject({}) do |components_bump_levels, commit_info|
|
16
|
+
components_bump_levels.merge(commit_info[:components_bump_levels]) do |_component, bump_level_1, bump_level_2|
|
17
|
+
[bump_level_1, bump_level_2].max
|
18
|
+
end
|
19
|
+
end.each do |component, bump_level|
|
20
|
+
puts "#{component.nil? ? 'Global' : component}: Bump #{
|
21
|
+
case bump_level
|
22
|
+
when 0
|
23
|
+
'patch'
|
24
|
+
when 1
|
25
|
+
'minor'
|
26
|
+
when 2
|
27
|
+
'major'
|
28
|
+
else
|
29
|
+
raise "Invalid bump level: #{bump_level}"
|
30
|
+
end
|
31
|
+
} version"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module SemVerComponents
|
2
|
+
|
3
|
+
module Outputs
|
4
|
+
|
5
|
+
class SemanticReleaseAnalyze < Output
|
6
|
+
|
7
|
+
# Process commits info
|
8
|
+
#
|
9
|
+
# Parameters::
|
10
|
+
# * *commits_info* (Array< Hash<Symbol, Object> >): List of commits info:
|
11
|
+
# * *components_bump_levels* (Hash<String or nil, Integer>): Set of bump levels (0: patch, 1: minor, 2: major) per component name (nil for global)
|
12
|
+
# * *commit* (Git::Object::Commit): Corresponding git commit
|
13
|
+
def process(commits_info)
|
14
|
+
bump_level = commits_info.map { |commit_info| commit_info[:components_bump_levels].values }.flatten(1).max
|
15
|
+
puts(
|
16
|
+
case bump_level
|
17
|
+
when 0
|
18
|
+
'patch'
|
19
|
+
when 1
|
20
|
+
'minor'
|
21
|
+
when 2
|
22
|
+
'major'
|
23
|
+
else
|
24
|
+
raise "Invalid bump level: #{bump_level}"
|
25
|
+
end
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
3
|
+
module SemVerComponents
|
4
|
+
|
5
|
+
module Outputs
|
6
|
+
|
7
|
+
class SemanticReleaseGenerateNotes < Output
|
8
|
+
|
9
|
+
# Process commits info
|
10
|
+
#
|
11
|
+
# Parameters::
|
12
|
+
# * *commits_info* (Array< Hash<Symbol, Object> >): List of commits info:
|
13
|
+
# * *components_bump_levels* (Hash<String or nil, Integer>): Set of bump levels (0: patch, 1: minor, 2: major) per component name (nil for global)
|
14
|
+
# * *commit* (Git::Object::Commit): Corresponding git commit
|
15
|
+
def process(commits_info)
|
16
|
+
# Compute new version
|
17
|
+
new_version =
|
18
|
+
if @local_git.git_from.nil?
|
19
|
+
'0.0.1'
|
20
|
+
elsif @local_git.git_from =~ /^v(\d+)\.(\d+)\.(\d+)$/
|
21
|
+
major = Integer($1)
|
22
|
+
minor = Integer($2)
|
23
|
+
patch = Integer($3)
|
24
|
+
bump_level = commits_info.map { |commit_info| commit_info[:components_bump_levels].values }.flatten(1).max
|
25
|
+
case bump_level
|
26
|
+
when 0
|
27
|
+
patch += 1
|
28
|
+
when 1
|
29
|
+
minor += 1
|
30
|
+
patch = 0
|
31
|
+
when 2
|
32
|
+
major += 1
|
33
|
+
minor = 0
|
34
|
+
patch = 0
|
35
|
+
else
|
36
|
+
raise "Invalid bump level: #{bump_level}"
|
37
|
+
end
|
38
|
+
"#{major}.#{minor}.#{patch}"
|
39
|
+
else
|
40
|
+
raise "Can't generate release notes from a git ref that is not a semantic release (#{@local_git.git_from})"
|
41
|
+
end
|
42
|
+
git_url = @local_git.git.remote('origin').url
|
43
|
+
git_url = git_url[0..-5] if git_url.end_with?('.git')
|
44
|
+
# Group commits per bump level, per component
|
45
|
+
# Hash< String or nil, Hash< Integer, Array<Git::Object::Commit> >
|
46
|
+
commits_per_component = {}
|
47
|
+
# Also reference commits to be ignored: commits that are part of a merge commit
|
48
|
+
merged_commits = []
|
49
|
+
commits_info.each do |commit_info|
|
50
|
+
git_commit = commit_info[:commit]
|
51
|
+
commit_info[:components_bump_levels].each do |component, bump_level|
|
52
|
+
commits_per_component[component] = {} unless commits_per_component.key?(component)
|
53
|
+
commits_per_component[component][bump_level] = [] unless commits_per_component[component].key?(bump_level)
|
54
|
+
commits_per_component[component][bump_level] << git_commit
|
55
|
+
end
|
56
|
+
# In the case of a merge commit, reference all commits that are part of this merge commit, directly from the graph
|
57
|
+
merged_commits.concat(@local_git.git_log.between(@local_git.git.merge_base(*git_commit.parents.map(&:sha)).first.sha, git_commit.sha)[1..-1].map(&:sha)) if git_commit.parents.size > 1
|
58
|
+
end
|
59
|
+
puts "# [v#{new_version}](#{git_url}/compare/#{@local_git.git_from}...v#{new_version}) (#{Time.now.utc.strftime('%F %T')})"
|
60
|
+
puts
|
61
|
+
commits_per_component.sort_by { |component, _component_info| component || '' }.each do |(component, component_info)|
|
62
|
+
puts "## #{component.nil? ? 'Global changes' : "Changes for #{component}"}\n" if commits_per_component.size > 1 || !component.nil?
|
63
|
+
component_info.each do |bump_level, commits|
|
64
|
+
puts "### #{
|
65
|
+
case bump_level
|
66
|
+
when 0
|
67
|
+
'Patches'
|
68
|
+
when 1
|
69
|
+
'Features'
|
70
|
+
when 2
|
71
|
+
'Breaking changes'
|
72
|
+
else
|
73
|
+
raise "Invalid bump level: #{bump_level}"
|
74
|
+
end
|
75
|
+
}"
|
76
|
+
puts
|
77
|
+
# Gather an ordered set of commit lines (with the corresponding commit sha) in order to not duplicate the info when there are merge commits
|
78
|
+
# Hash< String, String >
|
79
|
+
commit_lines = {}
|
80
|
+
commits.each do |commit|
|
81
|
+
# Don't put merged commits as we consider the changelog should contain the merge commit comment.
|
82
|
+
next if merged_commits.include?(commit.sha)
|
83
|
+
message_lines = commit.message.split("\n")
|
84
|
+
commit_line = message_lines.first
|
85
|
+
if commit_line =~ /^Merge pull request .+$/
|
86
|
+
# Consider the next line as commit line
|
87
|
+
next_line = message_lines[1..-1].join("\n").strip.split("\n").first
|
88
|
+
commit_line = next_line unless next_line.nil?
|
89
|
+
end
|
90
|
+
commit_lines[commit_line] = commit.sha
|
91
|
+
end
|
92
|
+
commit_lines.each do |commit_line, commit_sha|
|
93
|
+
puts "* [#{commit_line}](#{git_url}/commit/#{commit_sha})"
|
94
|
+
end
|
95
|
+
puts
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module SemVerComponents
|
2
|
+
|
3
|
+
class Plugins
|
4
|
+
|
5
|
+
# Constructor
|
6
|
+
#
|
7
|
+
# Parameters::
|
8
|
+
# * *plugins_type* (Symbol): Plugins type we are parsing
|
9
|
+
def initialize(plugins_type)
|
10
|
+
@plugins_type = plugins_type
|
11
|
+
@plugins = Hash[Dir.glob("#{__dir__}/#{plugins_type}/*.rb").map do |plugin_file|
|
12
|
+
plugin_name = File.basename(plugin_file, '.rb').to_sym
|
13
|
+
require "#{__dir__}/#{plugins_type}/#{plugin_name}.rb"
|
14
|
+
[
|
15
|
+
plugin_name,
|
16
|
+
SemVerComponents.
|
17
|
+
const_get(plugins_type.to_s.split('_').collect(&:capitalize).join.to_sym).
|
18
|
+
const_get(plugin_name.to_s.split('_').collect(&:capitalize).join.to_sym)
|
19
|
+
]
|
20
|
+
end]
|
21
|
+
end
|
22
|
+
|
23
|
+
# List available plugin names
|
24
|
+
#
|
25
|
+
# Result::
|
26
|
+
# * Array<Symbol>: Available plugin names
|
27
|
+
def list
|
28
|
+
@plugins.keys
|
29
|
+
end
|
30
|
+
|
31
|
+
# Get a plugin class
|
32
|
+
#
|
33
|
+
# Parameters::
|
34
|
+
# * *plugin_name* (Symbol): The plugin name
|
35
|
+
# Result::
|
36
|
+
# * Class: The corresponding plugin class
|
37
|
+
def [](plugin_name)
|
38
|
+
@plugins[plugin_name]
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
metadata
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sem_ver_components
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Muriel Salvan
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-01-14 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: git
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.8'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.8'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rspec
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '3.10'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '3.10'
|
41
|
+
description: Tools helping in maintaining semantic versioning at a components level
|
42
|
+
instead of a global package-only level.
|
43
|
+
email:
|
44
|
+
- muriel@x-aeon.com
|
45
|
+
executables:
|
46
|
+
- sem_ver_git
|
47
|
+
extensions: []
|
48
|
+
extra_rdoc_files: []
|
49
|
+
files:
|
50
|
+
- bin/sem_ver_git
|
51
|
+
- lib/sem_ver_components/local_git.rb
|
52
|
+
- lib/sem_ver_components/output.rb
|
53
|
+
- lib/sem_ver_components/outputs/info.rb
|
54
|
+
- lib/sem_ver_components/outputs/semantic_release_analyze.rb
|
55
|
+
- lib/sem_ver_components/outputs/semantic_release_generate_notes.rb
|
56
|
+
- lib/sem_ver_components/plugins.rb
|
57
|
+
- lib/sem_ver_components/version.rb
|
58
|
+
homepage: https://github.com/Muriel-Salvan/sem_ver_components
|
59
|
+
licenses:
|
60
|
+
- BSD-3-Clause
|
61
|
+
metadata: {}
|
62
|
+
post_install_message:
|
63
|
+
rdoc_options: []
|
64
|
+
require_paths:
|
65
|
+
- lib
|
66
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
67
|
+
requirements:
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: '0'
|
71
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
requirements: []
|
77
|
+
rubygems_version: 3.1.2
|
78
|
+
signing_key:
|
79
|
+
specification_version: 4
|
80
|
+
summary: Apply semantic versioning to various components of a same package
|
81
|
+
test_files: []
|