semverve 0.1.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.
- checksums.yaml +7 -0
- data/.github/workflows/ruby.yml +37 -0
- data/.gitignore +3 -0
- data/.ruby-version +1 -0
- data/CONTRIBUTING.md +155 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +93 -0
- data/LICENSE +21 -0
- data/README.md +551 -0
- data/Rakefile +45 -0
- data/lib/semverve/configuration.rb +471 -0
- data/lib/semverve/docs_publisher/task.rb +166 -0
- data/lib/semverve/docs_publisher.rb +370 -0
- data/lib/semverve/error.rb +8 -0
- data/lib/semverve/file_list_resolver.rb +66 -0
- data/lib/semverve/formats/module_constants.rb +140 -0
- data/lib/semverve/formats/simple_string.rb +72 -0
- data/lib/semverve/formats.rb +27 -0
- data/lib/semverve/generator.rb +66 -0
- data/lib/semverve/presets.rb +126 -0
- data/lib/semverve/project_metadata.rb +153 -0
- data/lib/semverve/railtie.rb +16 -0
- data/lib/semverve/semantic_version.rb +116 -0
- data/lib/semverve/task.rb +409 -0
- data/lib/semverve/version.rb +50 -0
- data/lib/semverve/version_code_references.rb +244 -0
- data/lib/semverve/version_file.rb +142 -0
- data/lib/semverve/version_metadata.rb +358 -0
- data/lib/semverve/version_references.rb +427 -0
- data/lib/semverve.rb +30 -0
- data/semverve.gemspec +36 -0
- metadata +156 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "error"
|
|
4
|
+
require_relative "formats"
|
|
5
|
+
|
|
6
|
+
module Semverve
|
|
7
|
+
##
|
|
8
|
+
# Reads and updates an existing version file.
|
|
9
|
+
class VersionFile
|
|
10
|
+
##
|
|
11
|
+
# Result of attempting to update a version file.
|
|
12
|
+
class UpdateResult
|
|
13
|
+
##
|
|
14
|
+
# Version parsed before the update.
|
|
15
|
+
#
|
|
16
|
+
# @return [Semverve::SemanticVersion]
|
|
17
|
+
attr_reader :previous_version
|
|
18
|
+
|
|
19
|
+
##
|
|
20
|
+
# Version requested by the update.
|
|
21
|
+
#
|
|
22
|
+
# @return [Semverve::SemanticVersion]
|
|
23
|
+
attr_reader :version
|
|
24
|
+
|
|
25
|
+
##
|
|
26
|
+
# Initializes an update result.
|
|
27
|
+
#
|
|
28
|
+
# @param [Semverve::SemanticVersion] previous_version
|
|
29
|
+
# @param [Semverve::SemanticVersion] version
|
|
30
|
+
# @param [Boolean] changed
|
|
31
|
+
#
|
|
32
|
+
# @return [Semverve::VersionFile::UpdateResult]
|
|
33
|
+
def initialize(previous_version:, version:, changed:)
|
|
34
|
+
@previous_version = previous_version
|
|
35
|
+
@version = version
|
|
36
|
+
@changed = changed
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
##
|
|
40
|
+
# Whether the version file was changed.
|
|
41
|
+
#
|
|
42
|
+
# @return [Boolean]
|
|
43
|
+
def changed?
|
|
44
|
+
@changed
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
##
|
|
49
|
+
# Initializes a version-file reader and writer.
|
|
50
|
+
#
|
|
51
|
+
# @param [Semverve::ResolvedConfiguration] configuration
|
|
52
|
+
#
|
|
53
|
+
# @return [Semverve::VersionFile]
|
|
54
|
+
def initialize(configuration)
|
|
55
|
+
@configuration = configuration
|
|
56
|
+
@format = Formats.fetch(configuration.format)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
##
|
|
60
|
+
# Current semantic version parsed from the version file.
|
|
61
|
+
#
|
|
62
|
+
# @return [Semverve::SemanticVersion]
|
|
63
|
+
def current
|
|
64
|
+
format.parse(read, path: path)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
##
|
|
68
|
+
# Increments one semantic-version level and writes the result.
|
|
69
|
+
#
|
|
70
|
+
# @param [Symbol, String] level
|
|
71
|
+
#
|
|
72
|
+
# @return [Semverve::VersionFile::UpdateResult]
|
|
73
|
+
def increment(level)
|
|
74
|
+
update { |version| version.increment(level) }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
##
|
|
78
|
+
# Sets the version file to an explicit semantic version.
|
|
79
|
+
#
|
|
80
|
+
# @param [Semverve::SemanticVersion] version
|
|
81
|
+
#
|
|
82
|
+
# @return [Semverve::VersionFile::UpdateResult]
|
|
83
|
+
def set(version)
|
|
84
|
+
update { version }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
##
|
|
90
|
+
# Resolved configuration used to locate the version file.
|
|
91
|
+
#
|
|
92
|
+
# @return [Semverve::ResolvedConfiguration]
|
|
93
|
+
attr_reader :configuration
|
|
94
|
+
|
|
95
|
+
##
|
|
96
|
+
# Format handler used to parse and replace version content.
|
|
97
|
+
#
|
|
98
|
+
# @return [Semverve::Formats::ModuleConstants, Semverve::Formats::SimpleString]
|
|
99
|
+
attr_reader :format
|
|
100
|
+
|
|
101
|
+
##
|
|
102
|
+
# Absolute path to the configured version file.
|
|
103
|
+
#
|
|
104
|
+
# @return [String]
|
|
105
|
+
def path
|
|
106
|
+
configuration.absolute_version_file
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
##
|
|
110
|
+
# Reads the configured version file.
|
|
111
|
+
#
|
|
112
|
+
# @return [String]
|
|
113
|
+
def read
|
|
114
|
+
unless File.file?(path)
|
|
115
|
+
raise Error, "Could not find version file #{path}. Run rake semverve:generate or set config.version_file."
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
File.read(path)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
##
|
|
122
|
+
# Updates the version file with the version returned by the block.
|
|
123
|
+
#
|
|
124
|
+
# @yieldparam [Semverve::SemanticVersion] version
|
|
125
|
+
#
|
|
126
|
+
# @return [Semverve::VersionFile::UpdateResult]
|
|
127
|
+
def update
|
|
128
|
+
content = read
|
|
129
|
+
previous_version = format.parse(content, path: path)
|
|
130
|
+
next_version = yield previous_version
|
|
131
|
+
changed = previous_version != next_version
|
|
132
|
+
|
|
133
|
+
File.write(path, format.replace(content, next_version, path: path)) if changed
|
|
134
|
+
|
|
135
|
+
UpdateResult.new(
|
|
136
|
+
previous_version: previous_version,
|
|
137
|
+
version: next_version,
|
|
138
|
+
changed: changed
|
|
139
|
+
)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler"
|
|
4
|
+
require "rubygems"
|
|
5
|
+
|
|
6
|
+
require_relative "semantic_version"
|
|
7
|
+
|
|
8
|
+
module Semverve
|
|
9
|
+
##
|
|
10
|
+
# Checks generated gem metadata against the configured version file.
|
|
11
|
+
class VersionMetadata
|
|
12
|
+
##
|
|
13
|
+
# Literal gemspec version assignments that can be safely rewritten.
|
|
14
|
+
#
|
|
15
|
+
# @return [Regexp]
|
|
16
|
+
GEMSPEC_LITERAL_PATTERN = /^(\s*\w+\.version\s*=\s*)(["'])(\d+\.\d+\.\d+)(\2)/
|
|
17
|
+
|
|
18
|
+
##
|
|
19
|
+
# A metadata version mismatch.
|
|
20
|
+
class Finding
|
|
21
|
+
##
|
|
22
|
+
# Path relative to the configured project root.
|
|
23
|
+
#
|
|
24
|
+
# @return [String]
|
|
25
|
+
attr_reader :path
|
|
26
|
+
|
|
27
|
+
##
|
|
28
|
+
# One-based line number.
|
|
29
|
+
#
|
|
30
|
+
# @return [Integer]
|
|
31
|
+
attr_reader :line
|
|
32
|
+
|
|
33
|
+
##
|
|
34
|
+
# One-based column number.
|
|
35
|
+
#
|
|
36
|
+
# @return [Integer]
|
|
37
|
+
attr_reader :column
|
|
38
|
+
|
|
39
|
+
##
|
|
40
|
+
# Metadata semantic version.
|
|
41
|
+
#
|
|
42
|
+
# @return [Semverve::SemanticVersion]
|
|
43
|
+
attr_reader :version
|
|
44
|
+
|
|
45
|
+
##
|
|
46
|
+
# Output label for the finding.
|
|
47
|
+
#
|
|
48
|
+
# @return [String]
|
|
49
|
+
attr_reader :label
|
|
50
|
+
|
|
51
|
+
##
|
|
52
|
+
# Initializes a finding.
|
|
53
|
+
#
|
|
54
|
+
# @param [String] path
|
|
55
|
+
# @param [Integer] line
|
|
56
|
+
# @param [Integer] column
|
|
57
|
+
# @param [Semverve::SemanticVersion] version
|
|
58
|
+
# @param [String] label
|
|
59
|
+
#
|
|
60
|
+
# @return [Semverve::VersionMetadata::Finding]
|
|
61
|
+
def initialize(path:, line:, column:, version:, label:)
|
|
62
|
+
@path = path
|
|
63
|
+
@line = line
|
|
64
|
+
@column = column
|
|
65
|
+
@version = version
|
|
66
|
+
@label = label
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
##
|
|
71
|
+
# Result of fixing metadata version mismatches.
|
|
72
|
+
class FixResult
|
|
73
|
+
##
|
|
74
|
+
# Files changed by the fix.
|
|
75
|
+
#
|
|
76
|
+
# @return [Array<String>]
|
|
77
|
+
attr_reader :changed_files
|
|
78
|
+
|
|
79
|
+
##
|
|
80
|
+
# Number of literal replacements made.
|
|
81
|
+
#
|
|
82
|
+
# @return [Integer]
|
|
83
|
+
attr_reader :replacement_count
|
|
84
|
+
|
|
85
|
+
##
|
|
86
|
+
# Whether bundle lock was run.
|
|
87
|
+
#
|
|
88
|
+
# @return [Boolean]
|
|
89
|
+
attr_reader :bundle_lock_ran
|
|
90
|
+
|
|
91
|
+
##
|
|
92
|
+
# Initializes a fix result.
|
|
93
|
+
#
|
|
94
|
+
# @param [Array<String>] changed_files
|
|
95
|
+
# @param [Integer] replacement_count
|
|
96
|
+
# @param [Boolean] bundle_lock_ran
|
|
97
|
+
#
|
|
98
|
+
# @return [Semverve::VersionMetadata::FixResult]
|
|
99
|
+
def initialize(changed_files:, replacement_count:, bundle_lock_ran:)
|
|
100
|
+
@changed_files = changed_files
|
|
101
|
+
@replacement_count = replacement_count
|
|
102
|
+
@bundle_lock_ran = bundle_lock_ran
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
##
|
|
107
|
+
# Initializes metadata version checking.
|
|
108
|
+
#
|
|
109
|
+
# @param [Semverve::ResolvedConfiguration] configuration
|
|
110
|
+
# @param [Semverve::SemanticVersion] current_version
|
|
111
|
+
#
|
|
112
|
+
# @return [Semverve::VersionMetadata]
|
|
113
|
+
def initialize(configuration, current_version)
|
|
114
|
+
@configuration = configuration
|
|
115
|
+
@current_version = current_version
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
##
|
|
119
|
+
# Metadata mismatches.
|
|
120
|
+
#
|
|
121
|
+
# @return [Array<Semverve::VersionMetadata::Finding>]
|
|
122
|
+
def findings
|
|
123
|
+
[gemspec_finding, lockfile_finding].compact
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
##
|
|
127
|
+
# Fixes safe metadata mismatches.
|
|
128
|
+
#
|
|
129
|
+
# @return [Semverve::VersionMetadata::FixResult]
|
|
130
|
+
def fix
|
|
131
|
+
changed_files = []
|
|
132
|
+
replacement_count = fix_gemspec_literal
|
|
133
|
+
|
|
134
|
+
changed_files << relative_path(gemspec_file) if replacement_count.positive?
|
|
135
|
+
|
|
136
|
+
bundle_lock_ran = lockfile_finding ? run_bundle_lock : false
|
|
137
|
+
|
|
138
|
+
FixResult.new(
|
|
139
|
+
changed_files: changed_files,
|
|
140
|
+
replacement_count: replacement_count,
|
|
141
|
+
bundle_lock_ran: bundle_lock_ran
|
|
142
|
+
)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
private
|
|
146
|
+
|
|
147
|
+
##
|
|
148
|
+
# Resolved Semverve configuration.
|
|
149
|
+
#
|
|
150
|
+
# @return [Semverve::ResolvedConfiguration]
|
|
151
|
+
attr_reader :configuration
|
|
152
|
+
|
|
153
|
+
##
|
|
154
|
+
# Current gem version.
|
|
155
|
+
#
|
|
156
|
+
# @return [Semverve::SemanticVersion]
|
|
157
|
+
attr_reader :current_version
|
|
158
|
+
|
|
159
|
+
##
|
|
160
|
+
# Absolute configured project root.
|
|
161
|
+
#
|
|
162
|
+
# @return [String]
|
|
163
|
+
def root
|
|
164
|
+
configuration.root
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
##
|
|
168
|
+
# Finding for a gemspec mismatch.
|
|
169
|
+
#
|
|
170
|
+
# @return [Semverve::VersionMetadata::Finding, nil]
|
|
171
|
+
def gemspec_finding
|
|
172
|
+
return unless gemspec_file && gemspec_version
|
|
173
|
+
return if gemspec_version == current_version
|
|
174
|
+
|
|
175
|
+
line, column = gemspec_position
|
|
176
|
+
|
|
177
|
+
Finding.new(
|
|
178
|
+
path: relative_path(gemspec_file),
|
|
179
|
+
line: line,
|
|
180
|
+
column: column,
|
|
181
|
+
version: gemspec_version,
|
|
182
|
+
label: "gemspec version"
|
|
183
|
+
)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
##
|
|
187
|
+
# Finding for a lockfile mismatch.
|
|
188
|
+
#
|
|
189
|
+
# @return [Semverve::VersionMetadata::Finding, nil]
|
|
190
|
+
def lockfile_finding
|
|
191
|
+
return unless File.file?(lockfile_path)
|
|
192
|
+
|
|
193
|
+
version = lockfile_version
|
|
194
|
+
return unless version
|
|
195
|
+
return if version == current_version
|
|
196
|
+
|
|
197
|
+
line, column = lockfile_position(version)
|
|
198
|
+
|
|
199
|
+
Finding.new(
|
|
200
|
+
path: "Gemfile.lock",
|
|
201
|
+
line: line,
|
|
202
|
+
column: column,
|
|
203
|
+
version: version,
|
|
204
|
+
label: "locked version"
|
|
205
|
+
)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
##
|
|
209
|
+
# Absolute gemspec path for the configured gem.
|
|
210
|
+
#
|
|
211
|
+
# @return [String, nil]
|
|
212
|
+
def gemspec_file
|
|
213
|
+
@gemspec_file ||= begin
|
|
214
|
+
files = Dir.glob(File.join(root, "*.gemspec"))
|
|
215
|
+
|
|
216
|
+
if files.one?
|
|
217
|
+
files.first
|
|
218
|
+
else
|
|
219
|
+
files.find do |path|
|
|
220
|
+
spec = load_gemspec(path)
|
|
221
|
+
spec&.name == configuration.gem_name
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
##
|
|
228
|
+
# Version loaded from the matching gemspec.
|
|
229
|
+
#
|
|
230
|
+
# @return [Semverve::SemanticVersion, nil]
|
|
231
|
+
def gemspec_version
|
|
232
|
+
@gemspec_version ||= begin
|
|
233
|
+
spec = load_gemspec(gemspec_file)
|
|
234
|
+
|
|
235
|
+
SemanticVersion.parse(spec.version.to_s) if spec&.version
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
##
|
|
240
|
+
# Loads a gemspec.
|
|
241
|
+
#
|
|
242
|
+
# @param [String, nil] path
|
|
243
|
+
#
|
|
244
|
+
# @return [Gem::Specification, nil]
|
|
245
|
+
def load_gemspec(path)
|
|
246
|
+
return unless path
|
|
247
|
+
|
|
248
|
+
Gem::Specification.load(path)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
##
|
|
252
|
+
# Line and column for gemspec output.
|
|
253
|
+
#
|
|
254
|
+
# @return [Array(Integer, Integer)]
|
|
255
|
+
def gemspec_position
|
|
256
|
+
lines = File.readlines(gemspec_file)
|
|
257
|
+
|
|
258
|
+
lines.each_with_index do |line, index|
|
|
259
|
+
literal_match = line.match(GEMSPEC_LITERAL_PATTERN)
|
|
260
|
+
return [index + 1, literal_match.begin(3) + 1] if literal_match
|
|
261
|
+
|
|
262
|
+
assignment_index = line.index(/^\s*\w+\.version\s*=/)
|
|
263
|
+
return [index + 1, assignment_index + 1] if assignment_index
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
[1, 1]
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
##
|
|
270
|
+
# Version parsed from Gemfile.lock.
|
|
271
|
+
#
|
|
272
|
+
# @return [Semverve::SemanticVersion, nil]
|
|
273
|
+
def lockfile_version
|
|
274
|
+
spec = lockfile_parser.specs.find { |candidate| candidate.name == configuration.gem_name }
|
|
275
|
+
return unless spec
|
|
276
|
+
|
|
277
|
+
SemanticVersion.parse(spec.version.to_s)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
##
|
|
281
|
+
# Parsed Gemfile.lock.
|
|
282
|
+
#
|
|
283
|
+
# @return [Bundler::LockfileParser]
|
|
284
|
+
def lockfile_parser
|
|
285
|
+
Bundler::LockfileParser.new(File.read(lockfile_path))
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
##
|
|
289
|
+
# Line and column for lockfile output.
|
|
290
|
+
#
|
|
291
|
+
# @param [Semverve::SemanticVersion] version
|
|
292
|
+
#
|
|
293
|
+
# @return [Array(Integer, Integer)]
|
|
294
|
+
def lockfile_position(version)
|
|
295
|
+
pattern = /^\s*#{Regexp.escape(configuration.gem_name)}\s+\(#{Regexp.escape(version.to_s)}\)/
|
|
296
|
+
|
|
297
|
+
File.readlines(lockfile_path).each_with_index do |line, index|
|
|
298
|
+
match = line.match(pattern)
|
|
299
|
+
return [index + 1, line.index(version.to_s) + 1] if match
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
[1, 1]
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
##
|
|
306
|
+
# Fixes a literal gemspec version assignment.
|
|
307
|
+
#
|
|
308
|
+
# @return [Integer]
|
|
309
|
+
def fix_gemspec_literal
|
|
310
|
+
return 0 unless gemspec_file
|
|
311
|
+
|
|
312
|
+
content = File.read(gemspec_file)
|
|
313
|
+
replacement_count = 0
|
|
314
|
+
|
|
315
|
+
fixed = content.lines.map do |line|
|
|
316
|
+
match = line.match(GEMSPEC_LITERAL_PATTERN)
|
|
317
|
+
next line unless match
|
|
318
|
+
|
|
319
|
+
version = SemanticVersion.parse(match[3])
|
|
320
|
+
next line if version == current_version
|
|
321
|
+
|
|
322
|
+
replacement_count += 1
|
|
323
|
+
line.sub(GEMSPEC_LITERAL_PATTERN) { "#{$1}#{$2}#{current_version}#{$4}" }
|
|
324
|
+
end.join
|
|
325
|
+
|
|
326
|
+
File.write(gemspec_file, fixed) if replacement_count.positive?
|
|
327
|
+
|
|
328
|
+
replacement_count
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
##
|
|
332
|
+
# Runs bundle lock through the configured command runner.
|
|
333
|
+
#
|
|
334
|
+
# @return [Boolean]
|
|
335
|
+
def run_bundle_lock
|
|
336
|
+
configuration.command_runner.call("bundle lock")
|
|
337
|
+
true
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
##
|
|
341
|
+
# Absolute Gemfile.lock path.
|
|
342
|
+
#
|
|
343
|
+
# @return [String]
|
|
344
|
+
def lockfile_path
|
|
345
|
+
File.join(root, "Gemfile.lock")
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
##
|
|
349
|
+
# Path relative to the project root.
|
|
350
|
+
#
|
|
351
|
+
# @param [String] path
|
|
352
|
+
#
|
|
353
|
+
# @return [String]
|
|
354
|
+
def relative_path(path)
|
|
355
|
+
path.delete_prefix("#{root}/")
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
end
|