schmersion 1.0.0

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,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Schmersion
4
+ class Message
5
+
6
+ REGEX = /^([\w-]+)(?:\(([\w-]+)\))?(!)?:\s(.*?)(?:\(#(\d+)\))?$/.freeze
7
+ FOOTER_COLON_REGEX = /^([\w-]+:|BREAKING CHANGE:)\s*(.*)$/.freeze
8
+ FOOTER_TICKET_REGEX = /^([\w-]+)\s+(#.*)$/.freeze
9
+
10
+ attr_reader :header
11
+ attr_reader :footers
12
+ attr_reader :type
13
+ attr_reader :scope
14
+ attr_reader :description
15
+ attr_reader :body
16
+ attr_reader :pull_request_id
17
+ attr_reader :breaking_changes
18
+
19
+ def initialize(raw_message)
20
+ @raw_message = clean(raw_message)
21
+
22
+ parts = @raw_message.split("\n\n").map(&:strip)
23
+
24
+ @header = parts.shift
25
+ @paragraphs = parts
26
+ @breaking_changes = []
27
+ @footers = []
28
+
29
+ parse_header
30
+ parse_footers
31
+
32
+ @body = @paragraphs.join("\n\n")
33
+ end
34
+
35
+ def valid?
36
+ !@type.nil?
37
+ end
38
+
39
+ def breaking_change?
40
+ @breaking_change == true ||
41
+ @breaking_changes.size.positive?
42
+ end
43
+
44
+ private
45
+
46
+ def parse_header
47
+ match = @header.match(REGEX)
48
+ return unless match
49
+
50
+ @type = match[1]
51
+ @scope = match[2]
52
+ @description = match[4].strip
53
+ @pull_request_id = match[5]
54
+
55
+ @breaking_change = true if match[3] == '!'
56
+
57
+ match
58
+ end
59
+
60
+ def parse_footers
61
+ footer = []
62
+ @paragraphs.last&.each_line do |line|
63
+ case line.strip
64
+ when FOOTER_COLON_REGEX, FOOTER_TICKET_REGEX
65
+ if footer.any?
66
+ handle_footer(footer)
67
+ footer = []
68
+ end
69
+ footer << "#{Regexp.last_match(1)} #{Regexp.last_match(2)}"
70
+ else
71
+ footer << line.strip
72
+ end
73
+ end
74
+
75
+ handle_footer(footer) if footer.any?
76
+
77
+ @paragraphs.pop if @footers.any? || @breaking_changes.any?
78
+ end
79
+
80
+ def handle_footer(footer_lines = [])
81
+ footer = footer_lines.join("\n")
82
+
83
+ if footer.start_with?('BREAKING CHANGE:')
84
+ @breaking_changes << footer.delete_prefix('BREAKING CHANGE:').strip
85
+ else
86
+ @footers << footer
87
+ end
88
+ end
89
+
90
+ def clean(message)
91
+ message.sub(/.*-----END PGP SIGNATURE-----\n\n/m, '')
92
+ end
93
+
94
+ end
95
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Schmersion
4
+ class MessageValidator
5
+
6
+ attr_reader :errors
7
+
8
+ def initialize(config, message)
9
+ @config = config
10
+ @message = message
11
+ @errors = []
12
+ validate_all
13
+ end
14
+
15
+ def valid?
16
+ @errors.empty?
17
+ end
18
+
19
+ def validate_all
20
+ validate_valid_format
21
+ return unless @message.valid?
22
+
23
+ validate_type
24
+ validate_scope
25
+ validate_description_presence
26
+ validate_description_length
27
+ end
28
+
29
+ private
30
+
31
+ def validate_valid_format
32
+ return if @message.valid?
33
+
34
+ errors << 'The commit message is not in a valid format'
35
+ end
36
+
37
+ def validate_type
38
+ return if @message.type.nil?
39
+ return if @config.valid_type?(@message.type)
40
+
41
+ errors << "Type (#{@message.type}) is not valid"
42
+ end
43
+
44
+ def validate_scope
45
+ return if @message.scope.nil?
46
+ return if @config.valid_scope?(@message.scope)
47
+
48
+ errors << "Scope (#{@message.scope}) is not valid"
49
+ end
50
+
51
+ def validate_description_presence
52
+ return if @message.description&.size&.positive?
53
+
54
+ errors << 'A description (text after the type) must be provided'
55
+ end
56
+
57
+ def validate_description_length
58
+ return if @message.description.nil?
59
+ return if @message.description.size.zero?
60
+ return if @message.description.size <= @config.linting[:max_description_length]
61
+
62
+ errors << "Commit description must be less than #{@config.linting[:max_description_length]} " \
63
+ "characters (currently #{@message.description.size})"
64
+ end
65
+
66
+ end
67
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'schmersion/formatters'
4
+
5
+ module Schmersion
6
+ class Releaser
7
+
8
+ COLORS = [:blue, :red, :yellow, :magenta, :green, :cyan].freeze
9
+
10
+ def initialize(repo, **options)
11
+ @repo = repo
12
+ @options = options
13
+ @exports = {}
14
+ end
15
+
16
+ def release
17
+ check_for_for_existing_version
18
+ generate_exports
19
+ preview_exports
20
+ save_exports
21
+ commit
22
+ tag
23
+ display_prompt
24
+ end
25
+
26
+ private
27
+
28
+ def check_for_for_existing_version
29
+ if @repo.version?(version.version)
30
+ raise Error, "#{version.version} already exists in this repository"
31
+ end
32
+
33
+ true
34
+ end
35
+
36
+ def generate_exports
37
+ return if skip?(:export)
38
+
39
+ @exports = {}
40
+ @repo.config.exports.each do |formatter|
41
+ output = formatter.generate(@repo, version)
42
+ @exports[formatter] = output
43
+ end
44
+ @exports
45
+ end
46
+
47
+ def preview_exports
48
+ return if skip?(:export)
49
+ return unless dry_run?
50
+
51
+ @exports.each_with_index do |(formatter, output), index|
52
+ color = COLORS[index % COLORS.size]
53
+ puts formatter.filename.colorize(color: :white, background: color)
54
+ output.split("\n").each do |line|
55
+ print '> '.colorize(color: color)
56
+ puts line
57
+ end
58
+ end
59
+ end
60
+
61
+ def save_exports
62
+ return if skip?(:export)
63
+
64
+ @exports.each do |formatter, output|
65
+ action 'save', formatter.filename do
66
+ formatter.insert(output)
67
+ end
68
+ end
69
+ end
70
+
71
+ def commit
72
+ return if skip?(:commit)
73
+
74
+ action 'commit', version.commit_message do
75
+ @repo.repo.reset
76
+ @exports.each_key do |formatter|
77
+ @repo.repo.add(formatter.filename)
78
+ end
79
+ @repo.repo.commit(version.commit_message, allow_empty: true)
80
+ end
81
+ end
82
+
83
+ def tag
84
+ return if skip?(:tag)
85
+
86
+ action 'tag', version.version.to_s do
87
+ @repo.repo.add_tag(version.version.to_s)
88
+ end
89
+ end
90
+
91
+ def display_prompt
92
+ puts
93
+ puts "Release of #{version.version} completed".white.on_green
94
+
95
+ return if skip?(:tag) && skip?(:commit)
96
+
97
+ print 'Now run '
98
+ print "git push --follow-tags origin #{@repo.current_branch}".cyan
99
+ puts ' to publish'
100
+ end
101
+
102
+ def version
103
+ @version ||= begin
104
+ @repo.pending_version(
105
+ override_version: @options[:version],
106
+ version_options: {
107
+ pre: @options[:pre],
108
+ breaking_change_not_major: @repo.config.version_options['breaking_change_not_major']
109
+ }
110
+ )[1]
111
+ end
112
+ end
113
+
114
+ def dry_run?
115
+ @options[:dry_run] == true
116
+ end
117
+
118
+ def action_color
119
+ dry_run? ? :magenta : :green
120
+ end
121
+
122
+ def action(action, text)
123
+ yield unless dry_run?
124
+ print "#{action}: ".colorize(action_color)
125
+ puts text
126
+ end
127
+
128
+ def skip?(type)
129
+ return false if @options[:skips].nil?
130
+
131
+ @options[:skips].include?(type)
132
+ end
133
+
134
+ end
135
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+ require 'git'
5
+ require 'semantic'
6
+ require 'schmersion/commit_parser'
7
+ require 'schmersion/commit'
8
+ require 'schmersion/version'
9
+ require 'schmersion/config'
10
+ require 'schmersion/hosts'
11
+
12
+ module Schmersion
13
+ class Repo
14
+
15
+ attr_reader :path
16
+ attr_reader :repo
17
+
18
+ def initialize(path)
19
+ @path = path
20
+ @repo = Git.open(path)
21
+ rescue ArgumentError => e
22
+ if e.message =~ /path does not exist/
23
+ raise Error, "No git repository found at #{path}"
24
+ end
25
+
26
+ raise
27
+ end
28
+
29
+ def path_for(*join)
30
+ File.join(@path, *join)
31
+ end
32
+
33
+ def config
34
+ @config ||= load_config(['.schmersion.yaml', '.schmersion.yml'])
35
+ end
36
+
37
+ def origin
38
+ @origin ||= @repo.remotes.find { |r| r.name == 'origin' }&.url
39
+ end
40
+
41
+ def host
42
+ return nil if origin.nil?
43
+
44
+ Hosts.host_for_url(origin)
45
+ end
46
+
47
+ # Get the pending version for the currently checked out branch
48
+ # for the repository.
49
+ def pending_version(from: nil, to: 'HEAD', **options)
50
+ options[:version_options] ||= {}
51
+
52
+ if from.nil?
53
+ from_version = versions.last
54
+ else
55
+ from_version = versions.find { |v, _| v.to_s == from }
56
+ if from_version.nil?
57
+ raise Error, "Could not find existing version named #{from}"
58
+ end
59
+ end
60
+
61
+ if from_version
62
+ previous_version, previous_version_commit = from_version
63
+ end
64
+
65
+ parser = CommitParser.new(@repo, previous_version_commit&.ref || :start, to)
66
+ if v = options[:override_version]
67
+ begin
68
+ next_version = Semantic::Version.new(v)
69
+ rescue ArgumentError => e
70
+ if e.message =~ /not a valid SemVer/
71
+ raise Error, "'#{v}' is not a valid version"
72
+ end
73
+
74
+ raise
75
+ end
76
+ else
77
+ next_version = parser.next_version_after(previous_version, **options[:version_options])
78
+ end
79
+
80
+ [
81
+ previous_version,
82
+ Version.new(self, next_version, parser)
83
+ ]
84
+ end
85
+
86
+ def commits(start_commit, end_commit, **options)
87
+ parser = CommitParser.new(@repo, start_commit, end_commit, **options)
88
+ parser.commits
89
+ end
90
+
91
+ def current_branch
92
+ @repo.branch.name
93
+ end
94
+
95
+ def version?(version)
96
+ @repo.tag(version.to_s).is_a?(Git::Object::Tag)
97
+ rescue Git::GitTagNameDoesNotExist
98
+ false
99
+ end
100
+
101
+ def versions
102
+ versions = @repo.tags.each_with_object([]) do |tag, array|
103
+ commit = @repo.gcommit(tag.sha)
104
+ version = Semantic::Version.new(tag.name)
105
+ array << [version, Commit.new(commit)]
106
+ rescue ArgumentError => e
107
+ raise unless e.message =~ /not a valid SemVer/
108
+ end
109
+ versions.sort_by { |_, c| c.date }
110
+ end
111
+
112
+ private
113
+
114
+ def load_config(filenames)
115
+ filenames.each do |filename|
116
+ path = path_for(filename)
117
+ if File.file?(path)
118
+ return Config.new(::YAML.load_file(path))
119
+ end
120
+ end
121
+
122
+ warn 'No config file was found, using defaults'
123
+ Config.new({})
124
+ end
125
+
126
+ end
127
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Schmersion
4
+
5
+ VERSION_FILE_ROOT = File.expand_path('../../VERSION', __dir__)
6
+ if File.file?(VERSION_FILE_ROOT)
7
+ VERSION = File.read(VERSION_FILE_ROOT).strip.sub(/\Av/, '')
8
+ else
9
+ VERSION = '0.0.0.dev'
10
+ end
11
+
12
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Schmersion
4
+ class Version
5
+
6
+ attr_reader :version
7
+ attr_reader :commit_parser
8
+
9
+ def initialize(repo, version, commit_parser)
10
+ @repo = repo
11
+ @version = version
12
+ @commit_parser = commit_parser
13
+ end
14
+
15
+ def commits
16
+ @commit_parser.commits
17
+ end
18
+
19
+ def start_commit
20
+ @commit_parser.start_commit
21
+ end
22
+
23
+ def end_commit
24
+ @commit_parser.end_commit
25
+ end
26
+
27
+ def commit_message
28
+ "chore(release): #{version}"
29
+ end
30
+
31
+ end
32
+ end