sem_ver_components 0.0.1

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