sem_ver_components 0.0.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.
@@ -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
@@ -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,15 @@
1
+ module SemVerComponents
2
+
3
+ class Output
4
+
5
+ # Constructor
6
+ #
7
+ # Parameters::
8
+ # * *local_git* (LocalGit): The git repository
9
+ def initialize(local_git)
10
+ @local_git = local_git
11
+ end
12
+
13
+ end
14
+
15
+ 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
@@ -0,0 +1,5 @@
1
+ module SemVerComponents
2
+
3
+ VERSION = '0.0.1'
4
+
5
+ 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: []