schmersion 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Schmersion
4
+ class Error < StandardError
5
+ end
6
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Schmersion
4
+ class Formatter
5
+
6
+ attr_reader :filename
7
+
8
+ def initialize(filename, options = {})
9
+ @filename = filename
10
+ @options = options
11
+ end
12
+
13
+ # Generate the output which should be included in the export. This
14
+ # should return a string which will also be displayed when doing
15
+ # dry-runs of a release.
16
+ # rubocop:disable Lint/UnusedMethodArgument
17
+ def generate(repo, version)
18
+ ''
19
+ end
20
+ # rubocop:enable Lint/UnusedMethodArgument
21
+
22
+ # Insert a given part into the given source file path
23
+ #
24
+ # @param part [String]
25
+ # @return [void]
26
+ def insert(part)
27
+ end
28
+
29
+ end
30
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'schmersion/formatters/markdown'
4
+ require 'schmersion/formatters/yaml'
5
+
6
+ module Schmersion
7
+ module Formatters
8
+
9
+ FORMATTERS = {
10
+ yaml: YAML,
11
+ markdown: Markdown
12
+ }.freeze
13
+
14
+ end
15
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'schmersion/formatter'
4
+
5
+ module Schmersion
6
+ module Formatters
7
+ class Markdown < Formatter
8
+
9
+ def generate(repo, version)
10
+ lines = []
11
+ lines << "## #{version.version}\n"
12
+
13
+ sections_for_commits(version.commits).each do |section, commits|
14
+ lines << "### #{section['title']}\n"
15
+ commits.each do |_, scope_commits|
16
+ scope_commits.sort_by { |c| c.message.description.upcase }.each do |commit|
17
+ lines << commit_line(repo, commit)
18
+ end
19
+ end
20
+ lines << nil
21
+ end
22
+ lines.join("\n")
23
+ end
24
+
25
+ def insert(part)
26
+ text_to_insert = "#{header}\n#{part}"
27
+
28
+ unless File.file?(@filename)
29
+ File.write(@filename, text_to_insert)
30
+ return
31
+ end
32
+
33
+ # If we already have a changelog markdown file, we're going to remove
34
+ # everyting up to the first level 2 heading and replace it with the
35
+ # header and the new version's data.
36
+ contents = File.read(@filename)
37
+ contents_without_header = contents.sub(/\A.*?##/m, '##')
38
+ File.write(@filename, "#{text_to_insert}\n#{contents_without_header}")
39
+
40
+ true
41
+ end
42
+
43
+ private
44
+
45
+ def header
46
+ lines = []
47
+ lines << "# #{@options['description']}\n"
48
+ lines << @options['description'] if @options['description']
49
+ lines << nil
50
+ lines.join("\n")
51
+ end
52
+
53
+ def sections_for_commits(commits)
54
+ return [] if @options['sections'].empty?
55
+
56
+ @options['sections'].each_with_object([]) do |section, array|
57
+ section_commits = commits.select do |commit|
58
+ section['types'].nil? ||
59
+ !section['types'].is_a?(Array) ||
60
+ section['types'].include?(commit.message.type)
61
+ end
62
+
63
+ next if section_commits.empty?
64
+
65
+ section_commits = section_commits.group_by do |c|
66
+ c.message.scope
67
+ end
68
+
69
+ section_commits = section_commits.sort_by do |s, _|
70
+ s&.upcase || 'ZZZZZZ'
71
+ end
72
+
73
+ array << [section, section_commits]
74
+ end
75
+ end
76
+
77
+ def capitalize_as_required(string)
78
+ return string unless @options['capitalize_strings']
79
+
80
+ string.sub(/\A([a-z])/) { Regexp.last_match(1).upcase }
81
+ end
82
+
83
+ def commit_url_for(repo, ref)
84
+ if @options['urls']&.key?('commit')
85
+ template = @options.dig('urls', 'commit')
86
+ return nil if template.nil?
87
+
88
+ return template.gsub('$REF', ref)
89
+ end
90
+
91
+ repo.host&.url_for_commit(ref)
92
+ end
93
+
94
+ def commit_line(repo, commit)
95
+ first_line = '- '
96
+ first_line += "**#{commit.message.scope}:** " if commit.message.scope
97
+ first_line += capitalize_as_required(commit.message.description)
98
+ if url = commit_url_for(repo, commit.ref)
99
+ first_line += ' ('
100
+ first_line += "[#{commit.ref[0, 6]}](#{url})"
101
+ first_line += ')'
102
+ end
103
+ first_line
104
+ end
105
+
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+ require 'schmersion/formatter'
5
+
6
+ module Schmersion
7
+ module Formatters
8
+ class YAML < Formatter
9
+
10
+ DEFAULT_STRUCTURE = [].to_yaml.freeze
11
+
12
+ def generate(_, version)
13
+ commits = version.commits.sort_by { |c| c.message.description.upcase }
14
+
15
+ commits = commits.each_with_object([]) do |commit, array|
16
+ next unless include_type?(commit.message.type)
17
+
18
+ array << commit_to_hash(commit)
19
+ end
20
+
21
+ {
22
+ 'version' => version.version.to_s,
23
+ 'commits' => commits
24
+ }.to_yaml
25
+ end
26
+
27
+ def insert(part)
28
+ unless File.file?(@filename)
29
+ File.write(@filename, DEFAULT_STRUCTURE)
30
+ end
31
+
32
+ part_as_hash = ::YAML.safe_load(part)
33
+ existing_yaml = ::YAML.load_file(@filename)
34
+ existing_yaml = [] unless existing_yaml.is_a?(Array)
35
+ existing_yaml.prepend(part_as_hash)
36
+ File.write(@filename, existing_yaml.to_yaml)
37
+ end
38
+
39
+ private
40
+
41
+ def commit_to_hash(commit)
42
+ {
43
+ 'ref' => commit.ref,
44
+ 'date' => commit.date.to_s,
45
+ 'type' => commit.message.type,
46
+ 'scope' => commit.message.scope,
47
+ 'description' => commit.message.description,
48
+ 'breaking_change' => commit.message.breaking_change?,
49
+ 'breaking_changes' => commit.message.breaking_changes,
50
+ 'pull_request_id' => commit.message.pull_request_id,
51
+ 'footers' => commit.message.footers
52
+ }
53
+ end
54
+
55
+ def include_type?(type)
56
+ return true if @options[:types].nil?
57
+
58
+ @options[:types].include(type.to_s)
59
+ end
60
+
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Schmersion
4
+ class Helpers
5
+
6
+ class << self
7
+
8
+ def print_commit_list(commits, prefix: '')
9
+ commits.each do |commit|
10
+ print prefix
11
+ print '['
12
+ print commit.ref[0, 10].blue
13
+ print ']'
14
+ print ' '
15
+ print commit.message.type.green
16
+ if commit.message.scope
17
+ print '('
18
+ print commit.message.scope.magenta
19
+ print ')'
20
+ end
21
+ if commit.message.breaking_change?
22
+ print ' ! '.white.on_red
23
+ end
24
+ print ': '
25
+ puts commit.message.description
26
+ end
27
+ end
28
+
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Schmersion
4
+ class Host
5
+
6
+ def initialize(url)
7
+ @url = url
8
+ end
9
+
10
+ def url_for_commit(commit_ref)
11
+ end
12
+
13
+ def url_for_comparison(ref1, ref2)
14
+ end
15
+
16
+ class << self
17
+
18
+ def suitable?(_)
19
+ false
20
+ end
21
+
22
+ end
23
+
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'schmersion/hosts/github'
4
+
5
+ module Schmersion
6
+ module Hosts
7
+
8
+ HOSTS = [GitHub].freeze
9
+
10
+ class << self
11
+
12
+ def host_for_url(url)
13
+ HOSTS.each do |host|
14
+ if host.suitable?(url)
15
+ return host.new(url)
16
+ end
17
+ end
18
+
19
+ nil
20
+ end
21
+
22
+ end
23
+
24
+ end
25
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'schmersion/host'
4
+
5
+ module Schmersion
6
+ module Hosts
7
+ class GitHub < Host
8
+
9
+ SSH_REGEXP = /\Agit@github\.com:([\w-]+)\/([\w-]+)(\.git)?\z/.freeze
10
+ HTTP_REGEXP = /\Ahttps:\/\/github\.com\/([\w-]+)\/([\w-]+)(\.git)?\z/.freeze
11
+
12
+ class << self
13
+
14
+ def suitable?(url)
15
+ !!(url.match(SSH_REGEXP) || url.match(HTTP_REGEXP))
16
+ end
17
+
18
+ end
19
+
20
+ def initialize(url)
21
+ super
22
+ get_user_and_repo(url)
23
+ @base_url = "https://github.com/#{@user}/#{@repo}"
24
+ end
25
+
26
+ def url_for_commit(ref)
27
+ "#{@base_url}/commit/#{ref}"
28
+ end
29
+
30
+ def url_for_comparison(ref1, ref2)
31
+ "#{@base_url}/compare/#{ref1}..#{ref2}"
32
+ end
33
+
34
+ private
35
+
36
+ def get_user_and_repo(url)
37
+ if m = url.match(SSH_REGEXP)
38
+ @user = m[1]
39
+ @repo = m[2]
40
+ elsif m = url.match(HTTP_REGEXP)
41
+ @user = m[1]
42
+ @repo = m[2]
43
+ else
44
+ raise Error, 'Could not determine appropriate details for repository from URL'
45
+ end
46
+ end
47
+
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'schmersion/message'
4
+ require 'schmersion/message_validator'
5
+
6
+ module Schmersion
7
+ class Linter
8
+
9
+ def initialize(repo)
10
+ @repo = repo
11
+ end
12
+
13
+ def prepare(path, source = nil)
14
+ previous_commit_message = read_previous_commit_message
15
+
16
+ return unless source.nil?
17
+
18
+ unless File.file?(path)
19
+ raise Error, "No commit message file at the given path (#{path})"
20
+ end
21
+
22
+ lines = []
23
+
24
+ if previous_commit_message
25
+ lines << previous_commit_message
26
+ lines << ''
27
+ else
28
+ lines << "\n"
29
+ end
30
+
31
+ lines << '# ====================================================================='
32
+ lines << '# Commit messages must conform to conventional commit formatting.'
33
+ lines << '# This means each commit message must be prefixed with an appropriate'
34
+ lines << '# type and, optionally, a scope. Your message will be validated before'
35
+ lines << '# the commit will be created. '
36
+ lines << '#'
37
+
38
+ add_list_for(lines, :types)
39
+
40
+ unless @repo.config.scopes.empty?
41
+ lines << '#'
42
+ add_list_for(lines, :scopes)
43
+ end
44
+
45
+ lines << '# ====================================================================='
46
+
47
+ lines = lines.join("\n")
48
+
49
+ contents = File.read(path)
50
+ File.write(path, "#{lines}\n#\n#{contents.strip}")
51
+ end
52
+
53
+ def validate_file(path)
54
+ unless File.file?(path)
55
+ raise Error, "No commit message file at the given path (#{path})"
56
+ end
57
+
58
+ contents = get_commit_message_from_file(File.read(path))
59
+ return [] if contents.length.zero?
60
+
61
+ message = Message.new(contents)
62
+
63
+ validator = MessageValidator.new(@repo.config, message)
64
+
65
+ unless validator.valid?
66
+ # Save the commit message to a file if its invalid.
67
+ save_commit_message(contents)
68
+ end
69
+
70
+ validator.errors
71
+ end
72
+
73
+ def setup(force: false)
74
+ create_hook('lint-prepare', 'prepare-commit-msg', force: force)
75
+ create_hook('lint-validate', 'commit-msg', force: force)
76
+ end
77
+
78
+ private
79
+
80
+ def read_previous_commit_message
81
+ path = '.git/SCHMERSION_EDITMSG'
82
+ return unless File.file?(path)
83
+
84
+ contents = File.read(path)
85
+ FileUtils.rm(path)
86
+ contents
87
+ end
88
+
89
+ def save_commit_message(message)
90
+ File.write('.git/SCHMERSION_EDITMSG', message)
91
+ end
92
+
93
+ def add_list_for(lines, type)
94
+ lines << "# The following #{type.to_s.upcase} are available to choose from:"
95
+ lines << '#'
96
+ @repo.config.public_send(type).sort.each_slice(3) do |names|
97
+ types = names.map { |t| " * #{t.to_s.ljust(16)}" }.join
98
+ lines << "# #{types}".strip
99
+ end
100
+ end
101
+
102
+ def get_commit_message_from_file(contents)
103
+ contents = contents.split('------------------------ >8 ------------------------', 2)[0]
104
+ contents.split("\n").reject { |l| l.start_with?('#') }.join("\n").strip
105
+ end
106
+
107
+ # Returns the path to schmersion for use in commit files.
108
+ # For production installs, we'll just call it `schmersion` and hope that it is
109
+ # included in the path. For development, we'll use the path to the binary in
110
+ # repository.
111
+ def path_to_schmersion
112
+ if development?
113
+ return File.join(schmersion_root, 'bin', 'schmersion')
114
+ end
115
+
116
+ 'schmersion'
117
+ end
118
+
119
+ def development?
120
+ !File.file?(File.join(schmersion_root, 'VERSION'))
121
+ end
122
+
123
+ def schmersion_root
124
+ File.expand_path('../../', __dir__)
125
+ end
126
+
127
+ def hook_file_contents(command)
128
+ <<~FILE
129
+ #!/bin/bash
130
+
131
+ #{path_to_schmersion} #{command} $1 $2 $3
132
+ FILE
133
+ end
134
+
135
+ def create_hook(command, name, force: false)
136
+ hook_path = File.join(@repo.path, '.git', 'hooks', name)
137
+ if File.file?(hook_path) && !force
138
+ raise Error, "Cannot install hook into #{name} because a hook file already exists."
139
+ end
140
+
141
+ File.write(hook_path, hook_file_contents(command))
142
+ FileUtils.chmod('u+x', hook_path)
143
+ hook_path
144
+ end
145
+
146
+ end
147
+ end