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,370 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# TODO: Turn this into its own gem.
|
|
4
|
+
|
|
5
|
+
require "fileutils"
|
|
6
|
+
require "open3"
|
|
7
|
+
require "tmpdir"
|
|
8
|
+
|
|
9
|
+
require_relative "error"
|
|
10
|
+
|
|
11
|
+
module Semverve
|
|
12
|
+
##
|
|
13
|
+
# Publishes generated documentation to a Git branch through a temporary
|
|
14
|
+
# worktree.
|
|
15
|
+
class DocsPublisher
|
|
16
|
+
##
|
|
17
|
+
# Small command runner used by the publisher.
|
|
18
|
+
class Shell
|
|
19
|
+
##
|
|
20
|
+
# Runs a command and raises when it fails.
|
|
21
|
+
#
|
|
22
|
+
# @param [Array<String>] command
|
|
23
|
+
# @param [String, nil] chdir
|
|
24
|
+
#
|
|
25
|
+
# @return [String]
|
|
26
|
+
def run(command, chdir: nil)
|
|
27
|
+
stdout, stderr, status = capture_command(command, chdir: chdir)
|
|
28
|
+
return stdout if status.success?
|
|
29
|
+
|
|
30
|
+
raise Error, "Command failed: #{command.join(" ")}\n#{stderr}"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
##
|
|
34
|
+
# Captures a command's standard output and raises when it fails.
|
|
35
|
+
#
|
|
36
|
+
# @param [Array<String>] command
|
|
37
|
+
# @param [String, nil] chdir
|
|
38
|
+
#
|
|
39
|
+
# @return [String]
|
|
40
|
+
def capture(command, chdir: nil)
|
|
41
|
+
run(command, chdir: chdir)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
##
|
|
45
|
+
# Whether a command exits successfully.
|
|
46
|
+
#
|
|
47
|
+
# @param [Array<String>] command
|
|
48
|
+
# @param [String, nil] chdir
|
|
49
|
+
#
|
|
50
|
+
# @return [Boolean]
|
|
51
|
+
def success?(command, chdir: nil)
|
|
52
|
+
_stdout, _stderr, status = capture_command(command, chdir: chdir)
|
|
53
|
+
status.success?
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
##
|
|
59
|
+
# Captures a command, omitting +chdir+ when none was provided.
|
|
60
|
+
#
|
|
61
|
+
# @param [Array<String>] command
|
|
62
|
+
# @param [String, nil] chdir
|
|
63
|
+
#
|
|
64
|
+
# @return [Array(String, String, Process::Status)]
|
|
65
|
+
def capture_command(command, chdir:)
|
|
66
|
+
options = chdir ? {chdir: chdir} : {}
|
|
67
|
+
|
|
68
|
+
Open3.capture3(*command, **options)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
##
|
|
73
|
+
# Source project root.
|
|
74
|
+
#
|
|
75
|
+
# @return [String]
|
|
76
|
+
attr_accessor :root
|
|
77
|
+
|
|
78
|
+
##
|
|
79
|
+
# Directory containing generated documentation, relative to +root+.
|
|
80
|
+
#
|
|
81
|
+
# @return [String]
|
|
82
|
+
attr_accessor :source_dir
|
|
83
|
+
|
|
84
|
+
##
|
|
85
|
+
# Documentation directory on the publishing branch.
|
|
86
|
+
#
|
|
87
|
+
# @return [String]
|
|
88
|
+
attr_accessor :target_dir
|
|
89
|
+
|
|
90
|
+
##
|
|
91
|
+
# Branch that receives generated documentation.
|
|
92
|
+
#
|
|
93
|
+
# @return [String]
|
|
94
|
+
attr_accessor :branch
|
|
95
|
+
|
|
96
|
+
##
|
|
97
|
+
# Remote used when pushing the publishing branch.
|
|
98
|
+
#
|
|
99
|
+
# @return [String]
|
|
100
|
+
attr_accessor :remote
|
|
101
|
+
|
|
102
|
+
##
|
|
103
|
+
# Commit message for generated documentation updates.
|
|
104
|
+
#
|
|
105
|
+
# @return [String]
|
|
106
|
+
attr_accessor :commit_message
|
|
107
|
+
|
|
108
|
+
##
|
|
109
|
+
# Optional path for the temporary worktree.
|
|
110
|
+
#
|
|
111
|
+
# @return [String, nil]
|
|
112
|
+
attr_accessor :worktree_path
|
|
113
|
+
|
|
114
|
+
##
|
|
115
|
+
# Whether dirty source working trees are allowed.
|
|
116
|
+
#
|
|
117
|
+
# @return [Boolean]
|
|
118
|
+
attr_accessor :allow_dirty
|
|
119
|
+
|
|
120
|
+
##
|
|
121
|
+
# Whether the publishing branch should be pushed.
|
|
122
|
+
#
|
|
123
|
+
# @return [Boolean]
|
|
124
|
+
attr_accessor :push
|
|
125
|
+
|
|
126
|
+
##
|
|
127
|
+
# Whether to report changes without committing or pushing.
|
|
128
|
+
#
|
|
129
|
+
# @return [Boolean]
|
|
130
|
+
attr_accessor :dry_run
|
|
131
|
+
|
|
132
|
+
##
|
|
133
|
+
# Command runner used for Git commands.
|
|
134
|
+
#
|
|
135
|
+
# @return [#run, #capture, #success?]
|
|
136
|
+
attr_accessor :command_runner
|
|
137
|
+
|
|
138
|
+
##
|
|
139
|
+
# Output stream for status messages.
|
|
140
|
+
#
|
|
141
|
+
# @return [#puts]
|
|
142
|
+
attr_accessor :output
|
|
143
|
+
|
|
144
|
+
##
|
|
145
|
+
# Initializes a documentation publisher.
|
|
146
|
+
#
|
|
147
|
+
# @yieldparam [Semverve::DocsPublisher] publisher
|
|
148
|
+
#
|
|
149
|
+
# @return [Semverve::DocsPublisher]
|
|
150
|
+
def initialize
|
|
151
|
+
@root = Dir.pwd
|
|
152
|
+
@source_dir = "docs"
|
|
153
|
+
@target_dir = "docs"
|
|
154
|
+
@branch = "gh-pages"
|
|
155
|
+
@remote = "origin"
|
|
156
|
+
@commit_message = "Update generated documentation"
|
|
157
|
+
@worktree_path = nil
|
|
158
|
+
@allow_dirty = false
|
|
159
|
+
@push = true
|
|
160
|
+
@dry_run = false
|
|
161
|
+
@command_runner = Shell.new
|
|
162
|
+
@output = $stdout
|
|
163
|
+
|
|
164
|
+
yield self if block_given?
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
##
|
|
168
|
+
# Publishes generated documentation.
|
|
169
|
+
#
|
|
170
|
+
# @return [Boolean] whether documentation changes were found
|
|
171
|
+
def publish
|
|
172
|
+
validate!
|
|
173
|
+
ensure_clean_source_worktree unless allow_dirty
|
|
174
|
+
|
|
175
|
+
with_worktree do |worktree|
|
|
176
|
+
sync_docs_to(worktree)
|
|
177
|
+
|
|
178
|
+
unless publishing_worktree_changed?(worktree)
|
|
179
|
+
output.puts "Documentation is already current on #{branch}."
|
|
180
|
+
return false
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
if dry_run
|
|
184
|
+
output.puts "Documentation changes detected for #{branch}; dry run did not commit or push."
|
|
185
|
+
return true
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
commit_docs(worktree)
|
|
189
|
+
push_docs(worktree) if push
|
|
190
|
+
output.puts "Published documentation to #{remote}/#{branch}."
|
|
191
|
+
true
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
private
|
|
196
|
+
|
|
197
|
+
##
|
|
198
|
+
# Validates publishing configuration.
|
|
199
|
+
#
|
|
200
|
+
# @return [void]
|
|
201
|
+
def validate!
|
|
202
|
+
raise Error, "Documentation source directory does not exist: #{source_path}." unless File.directory?(source_path)
|
|
203
|
+
|
|
204
|
+
if target_dir.nil? || target_dir.empty? || target_dir == "." || target_dir.start_with?("/")
|
|
205
|
+
raise Error, "target_dir must be a relative directory such as \"docs\"."
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
##
|
|
210
|
+
# Absolute source project root.
|
|
211
|
+
#
|
|
212
|
+
# @return [String]
|
|
213
|
+
def source_root
|
|
214
|
+
@source_root ||= File.expand_path(root)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
##
|
|
218
|
+
# Absolute source documentation directory.
|
|
219
|
+
#
|
|
220
|
+
# @return [String]
|
|
221
|
+
def source_path
|
|
222
|
+
File.expand_path(source_dir, source_root)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
##
|
|
226
|
+
# Ensures the source working tree is clean.
|
|
227
|
+
#
|
|
228
|
+
# @return [void]
|
|
229
|
+
def ensure_clean_source_worktree
|
|
230
|
+
status = git_capture(source_root, "status", "--porcelain")
|
|
231
|
+
return if status.empty?
|
|
232
|
+
|
|
233
|
+
raise Error, "Working tree must be clean before publishing documentation. Commit, stash, or set allow_dirty."
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
##
|
|
237
|
+
# Yields a temporary publishing worktree and removes it afterward.
|
|
238
|
+
#
|
|
239
|
+
# @yieldparam [String] worktree
|
|
240
|
+
#
|
|
241
|
+
# @return [Object]
|
|
242
|
+
def with_worktree
|
|
243
|
+
temporary_path = worktree_path || Dir.mktmpdir("semverve-docs-publish-")
|
|
244
|
+
temporary_worktree = worktree_path.nil?
|
|
245
|
+
worktree_added = false
|
|
246
|
+
|
|
247
|
+
if temporary_worktree
|
|
248
|
+
FileUtils.rm_rf(temporary_path)
|
|
249
|
+
elsif File.exist?(temporary_path)
|
|
250
|
+
raise Error, "Worktree path already exists: #{temporary_path}."
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
add_worktree(temporary_path)
|
|
254
|
+
worktree_added = true
|
|
255
|
+
yield temporary_path
|
|
256
|
+
ensure
|
|
257
|
+
remove_worktree(temporary_path) if temporary_path && worktree_added
|
|
258
|
+
FileUtils.rm_rf(temporary_path) if temporary_path && temporary_worktree
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
##
|
|
262
|
+
# Adds a worktree for the publishing branch.
|
|
263
|
+
#
|
|
264
|
+
# @param [String] path
|
|
265
|
+
#
|
|
266
|
+
# @return [void]
|
|
267
|
+
def add_worktree(path)
|
|
268
|
+
if local_branch?
|
|
269
|
+
git_run(source_root, "worktree", "add", path, branch)
|
|
270
|
+
elsif remote_branch?
|
|
271
|
+
git_run(source_root, "worktree", "add", "-b", branch, path, "#{remote}/#{branch}")
|
|
272
|
+
else
|
|
273
|
+
raise Error, "Could not find #{branch} locally or at #{remote}/#{branch}."
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
##
|
|
278
|
+
# Removes a worktree.
|
|
279
|
+
#
|
|
280
|
+
# @param [String] path
|
|
281
|
+
#
|
|
282
|
+
# @return [void]
|
|
283
|
+
def remove_worktree(path)
|
|
284
|
+
git_run(source_root, "worktree", "remove", "--force", path) if File.directory?(path)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
##
|
|
288
|
+
# Whether the publishing branch exists locally.
|
|
289
|
+
#
|
|
290
|
+
# @return [Boolean]
|
|
291
|
+
def local_branch?
|
|
292
|
+
command_runner.success?(["git", "show-ref", "--verify", "--quiet", "refs/heads/#{branch}"], chdir: source_root)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
##
|
|
296
|
+
# Whether the publishing branch exists as a remote-tracking branch.
|
|
297
|
+
#
|
|
298
|
+
# @return [Boolean]
|
|
299
|
+
def remote_branch?
|
|
300
|
+
command_runner.success?(["git", "show-ref", "--verify", "--quiet", "refs/remotes/#{remote}/#{branch}"], chdir: source_root)
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
##
|
|
304
|
+
# Copies generated documentation into the publishing worktree.
|
|
305
|
+
#
|
|
306
|
+
# @param [String] worktree
|
|
307
|
+
#
|
|
308
|
+
# @return [void]
|
|
309
|
+
def sync_docs_to(worktree)
|
|
310
|
+
target_path = File.expand_path(target_dir, worktree)
|
|
311
|
+
|
|
312
|
+
FileUtils.rm_rf(target_path)
|
|
313
|
+
FileUtils.mkdir_p(File.dirname(target_path))
|
|
314
|
+
FileUtils.cp_r(source_path, target_path)
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
##
|
|
318
|
+
# Whether the publishing worktree has documentation changes.
|
|
319
|
+
#
|
|
320
|
+
# @param [String] worktree
|
|
321
|
+
#
|
|
322
|
+
# @return [Boolean]
|
|
323
|
+
def publishing_worktree_changed?(worktree)
|
|
324
|
+
!git_capture(worktree, "status", "--porcelain", "--", target_dir).empty?
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
##
|
|
328
|
+
# Commits documentation changes in the publishing worktree.
|
|
329
|
+
#
|
|
330
|
+
# @param [String] worktree
|
|
331
|
+
#
|
|
332
|
+
# @return [void]
|
|
333
|
+
def commit_docs(worktree)
|
|
334
|
+
git_run(worktree, "add", target_dir)
|
|
335
|
+
git_run(worktree, "commit", "-m", commit_message)
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
##
|
|
339
|
+
# Pushes documentation changes.
|
|
340
|
+
#
|
|
341
|
+
# @param [String] worktree
|
|
342
|
+
#
|
|
343
|
+
# @return [void]
|
|
344
|
+
def push_docs(worktree)
|
|
345
|
+
git_run(worktree, "push", remote, branch)
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
##
|
|
349
|
+
# Runs a Git command in a directory.
|
|
350
|
+
#
|
|
351
|
+
# @param [String] directory
|
|
352
|
+
# @param [Array<String>] arguments
|
|
353
|
+
#
|
|
354
|
+
# @return [String]
|
|
355
|
+
def git_run(directory, *arguments)
|
|
356
|
+
command_runner.run(["git", *arguments], chdir: directory)
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
##
|
|
360
|
+
# Captures a Git command in a directory.
|
|
361
|
+
#
|
|
362
|
+
# @param [String] directory
|
|
363
|
+
# @param [Array<String>] arguments
|
|
364
|
+
#
|
|
365
|
+
# @return [String]
|
|
366
|
+
def git_capture(directory, *arguments)
|
|
367
|
+
command_runner.capture(["git", *arguments], chdir: directory)
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Semverve
|
|
4
|
+
##
|
|
5
|
+
# Resolves configured Rake::FileList entries relative to a project root.
|
|
6
|
+
class FileListResolver
|
|
7
|
+
##
|
|
8
|
+
# Initializes file-list resolution.
|
|
9
|
+
#
|
|
10
|
+
# @param [String] root
|
|
11
|
+
# @param [Rake::FileList] file_list
|
|
12
|
+
#
|
|
13
|
+
# @return [Semverve::FileListResolver]
|
|
14
|
+
def initialize(root:, file_list:)
|
|
15
|
+
@root = root
|
|
16
|
+
@file_list = file_list
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
##
|
|
20
|
+
# Absolute files from the configured file list.
|
|
21
|
+
#
|
|
22
|
+
# @return [Array<String>]
|
|
23
|
+
def files
|
|
24
|
+
configured_files.map { |path| File.expand_path(path, root) }
|
|
25
|
+
.select { |path| File.file?(path) }
|
|
26
|
+
.uniq
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
##
|
|
32
|
+
# Absolute project root.
|
|
33
|
+
#
|
|
34
|
+
# @return [String]
|
|
35
|
+
attr_reader :root
|
|
36
|
+
|
|
37
|
+
##
|
|
38
|
+
# Configured Rake file list.
|
|
39
|
+
#
|
|
40
|
+
# @return [Rake::FileList]
|
|
41
|
+
attr_reader :file_list
|
|
42
|
+
|
|
43
|
+
##
|
|
44
|
+
# Configured files expanded relative to the root.
|
|
45
|
+
#
|
|
46
|
+
# @return [Array<String>]
|
|
47
|
+
def configured_files
|
|
48
|
+
Dir.chdir(root) do
|
|
49
|
+
file_list.to_a.flat_map { |path| expand_file_list_entry(path) }
|
|
50
|
+
.reject { |path| file_list.excluded_from_list?(path) }
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
##
|
|
55
|
+
# Expands glob-like entries that were appended to a Rake::FileList.
|
|
56
|
+
#
|
|
57
|
+
# @param [String] path
|
|
58
|
+
#
|
|
59
|
+
# @return [Array<String>]
|
|
60
|
+
def expand_file_list_entry(path)
|
|
61
|
+
matches = Dir.glob(path)
|
|
62
|
+
|
|
63
|
+
matches.empty? ? [path] : matches
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../error"
|
|
4
|
+
require_relative "../semantic_version"
|
|
5
|
+
|
|
6
|
+
module Semverve
|
|
7
|
+
module Formats
|
|
8
|
+
##
|
|
9
|
+
# Handles version files with MAJOR, MINOR, and PATCH constants.
|
|
10
|
+
class ModuleConstants
|
|
11
|
+
##
|
|
12
|
+
# Mapping of semantic-version parts to Ruby constant names.
|
|
13
|
+
#
|
|
14
|
+
# @return [Hash<Symbol, String>]
|
|
15
|
+
CONSTANTS = {
|
|
16
|
+
major: "MAJOR",
|
|
17
|
+
minor: "MINOR",
|
|
18
|
+
patch: "PATCH"
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
##
|
|
22
|
+
# Parses a semantic version from module-constant content.
|
|
23
|
+
#
|
|
24
|
+
# @param [String] content
|
|
25
|
+
# @param [String] path
|
|
26
|
+
#
|
|
27
|
+
# @return [Semverve::SemanticVersion]
|
|
28
|
+
def parse(content, path:)
|
|
29
|
+
SemanticVersion.new(
|
|
30
|
+
major: constant_value(content, path, :major),
|
|
31
|
+
minor: constant_value(content, path, :minor),
|
|
32
|
+
patch: constant_value(content, path, :patch)
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
##
|
|
37
|
+
# Replaces MAJOR, MINOR, and PATCH values in module-constant content.
|
|
38
|
+
#
|
|
39
|
+
# @param [String] content
|
|
40
|
+
# @param [Semverve::SemanticVersion] version
|
|
41
|
+
# @param [String] path
|
|
42
|
+
#
|
|
43
|
+
# @return [String]
|
|
44
|
+
def replace(content, version, path:)
|
|
45
|
+
CONSTANTS.reduce(content) do |updated, (part, constant)|
|
|
46
|
+
pattern = /^(\s*#{constant}\s*=\s*)\d+/
|
|
47
|
+
value = version.public_send(part)
|
|
48
|
+
|
|
49
|
+
unless updated.match?(pattern)
|
|
50
|
+
raise Error, "Could not find #{constant} in #{path}."
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
updated.sub(pattern) { "#{$1}#{value}" }
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
##
|
|
58
|
+
# Generates a module-constant version file.
|
|
59
|
+
#
|
|
60
|
+
# @param [Semverve::SemanticVersion] version
|
|
61
|
+
# @param [String] module_name
|
|
62
|
+
#
|
|
63
|
+
# @return [String]
|
|
64
|
+
def generate(version, module_name:)
|
|
65
|
+
<<~RUBY
|
|
66
|
+
# frozen_string_literal: true
|
|
67
|
+
|
|
68
|
+
##
|
|
69
|
+
# Namespace for #{module_name}.
|
|
70
|
+
module #{module_name}
|
|
71
|
+
##
|
|
72
|
+
# Semantic version information for #{module_name}.
|
|
73
|
+
module Version
|
|
74
|
+
##
|
|
75
|
+
# Major version.
|
|
76
|
+
#
|
|
77
|
+
# @return [Integer]
|
|
78
|
+
MAJOR = #{version.major}
|
|
79
|
+
|
|
80
|
+
##
|
|
81
|
+
# Minor version.
|
|
82
|
+
#
|
|
83
|
+
# @return [Integer]
|
|
84
|
+
MINOR = #{version.minor}
|
|
85
|
+
|
|
86
|
+
##
|
|
87
|
+
# Patch version.
|
|
88
|
+
#
|
|
89
|
+
# @return [Integer]
|
|
90
|
+
PATCH = #{version.patch}
|
|
91
|
+
|
|
92
|
+
module_function
|
|
93
|
+
|
|
94
|
+
##
|
|
95
|
+
# Version as +[MAJOR, MINOR, PATCH]+
|
|
96
|
+
#
|
|
97
|
+
# @return [Array<Integer>]
|
|
98
|
+
def to_a
|
|
99
|
+
[MAJOR, MINOR, PATCH]
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
##
|
|
103
|
+
# Version as +MAJOR.MINOR.PATCH+
|
|
104
|
+
#
|
|
105
|
+
# @return [String]
|
|
106
|
+
def to_s
|
|
107
|
+
to_a.join(".")
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
##
|
|
112
|
+
# Full gem version string.
|
|
113
|
+
#
|
|
114
|
+
# @return [String]
|
|
115
|
+
VERSION = #{module_name}::Version.to_s
|
|
116
|
+
end
|
|
117
|
+
RUBY
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
private
|
|
121
|
+
|
|
122
|
+
##
|
|
123
|
+
# Extracts one semantic-version constant from content.
|
|
124
|
+
#
|
|
125
|
+
# @param [String] content
|
|
126
|
+
# @param [String] path
|
|
127
|
+
# @param [Symbol] part
|
|
128
|
+
#
|
|
129
|
+
# @return [Integer]
|
|
130
|
+
def constant_value(content, path, part)
|
|
131
|
+
constant = CONSTANTS.fetch(part)
|
|
132
|
+
match = content.match(/^\s*#{constant}\s*=\s*(\d+)/)
|
|
133
|
+
|
|
134
|
+
return match[1].to_i if match
|
|
135
|
+
|
|
136
|
+
raise Error, "Could not parse #{path} as module format. Expected #{constant} = <integer>."
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../error"
|
|
4
|
+
require_relative "../semantic_version"
|
|
5
|
+
|
|
6
|
+
module Semverve
|
|
7
|
+
module Formats
|
|
8
|
+
##
|
|
9
|
+
# Handles version files that define a single top-level +VERSION+ string.
|
|
10
|
+
class SimpleString
|
|
11
|
+
##
|
|
12
|
+
# Pattern for locating a simple +VERSION+ assignment.
|
|
13
|
+
#
|
|
14
|
+
# @return [Regexp]
|
|
15
|
+
PATTERN = /^(\s*VERSION\s*=\s*)(["'])(\d+\.\d+\.\d+)(\2)/
|
|
16
|
+
|
|
17
|
+
##
|
|
18
|
+
# Parses a semantic version from simple string content.
|
|
19
|
+
#
|
|
20
|
+
# @param [String] content
|
|
21
|
+
# @param [String] path
|
|
22
|
+
#
|
|
23
|
+
# @return [Semverve::SemanticVersion]
|
|
24
|
+
def parse(content, path:)
|
|
25
|
+
match = content.match(PATTERN)
|
|
26
|
+
|
|
27
|
+
return SemanticVersion.parse(match[3]) if match
|
|
28
|
+
|
|
29
|
+
raise Error, "Could not parse #{path} as simple format. Expected VERSION = \"MAJOR.MINOR.PATCH\"."
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
##
|
|
33
|
+
# Replaces the semantic version in simple string content.
|
|
34
|
+
#
|
|
35
|
+
# @param [String] content
|
|
36
|
+
# @param [Semverve::SemanticVersion] version
|
|
37
|
+
# @param [String] path
|
|
38
|
+
#
|
|
39
|
+
# @return [String]
|
|
40
|
+
def replace(content, version, path:)
|
|
41
|
+
unless content.match?(PATTERN)
|
|
42
|
+
raise Error, "Could not parse #{path} as simple format. Expected VERSION = \"MAJOR.MINOR.PATCH\"."
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
content.sub(PATTERN) { "#{$1}#{$2}#{version}#{$4}" }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
##
|
|
49
|
+
# Generates a simple version file.
|
|
50
|
+
#
|
|
51
|
+
# @param [Semverve::SemanticVersion] version
|
|
52
|
+
# @param [String] module_name
|
|
53
|
+
#
|
|
54
|
+
# @return [String]
|
|
55
|
+
def generate(version, module_name:)
|
|
56
|
+
<<~RUBY
|
|
57
|
+
# frozen_string_literal: true
|
|
58
|
+
|
|
59
|
+
##
|
|
60
|
+
# Namespace for #{module_name}.
|
|
61
|
+
module #{module_name}
|
|
62
|
+
##
|
|
63
|
+
# Full gem version string.
|
|
64
|
+
#
|
|
65
|
+
# @return [String]
|
|
66
|
+
VERSION = "#{version}"
|
|
67
|
+
end
|
|
68
|
+
RUBY
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "formats/module_constants"
|
|
4
|
+
require_relative "formats/simple_string"
|
|
5
|
+
|
|
6
|
+
module Semverve
|
|
7
|
+
##
|
|
8
|
+
# Format handlers for parsing, replacing, and generating version files.
|
|
9
|
+
module Formats
|
|
10
|
+
##
|
|
11
|
+
# Returns the handler for a configured version-file format.
|
|
12
|
+
#
|
|
13
|
+
# @param [Symbol, String] name
|
|
14
|
+
#
|
|
15
|
+
# @return [Semverve::Formats::ModuleConstants, Semverve::Formats::SimpleString]
|
|
16
|
+
def self.fetch(name)
|
|
17
|
+
case name.to_sym
|
|
18
|
+
when :module
|
|
19
|
+
ModuleConstants.new
|
|
20
|
+
when :simple
|
|
21
|
+
SimpleString.new
|
|
22
|
+
else
|
|
23
|
+
raise Error, "Unknown version format #{name.inspect}. Use :module or :simple."
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|