git-version-bump 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,386 @@
1
+ require 'tempfile'
2
+ require 'digest/sha1'
3
+ require 'open3'
4
+ require 'pathname'
5
+
6
+ module GitVersionBump
7
+ class VersionUnobtainable < StandardError; end
8
+ class CommandFailure < StandardError
9
+ attr_accessor :output, :exitstatus
10
+
11
+ def initialize(m, output, exitstatus)
12
+ super(m)
13
+ @output, @exitstatus = output, exitstatus
14
+ end
15
+ end
16
+
17
+ VERSION_TAG_GLOB = 'v[0-9]*.[0-9]*.*[0-9]'
18
+ private_constant :VERSION_TAG_GLOB
19
+
20
+ DEVNULL = Gem.win_platform? ? "NUL" : "/dev/null"
21
+ private_constant :DEVNULL
22
+
23
+ def self.version(use_local_dir=false, include_lite_tags=false)
24
+ if use_local_dir
25
+ repo_version(true, include_lite_tags)
26
+ else
27
+ gem_version || repo_version(false, include_lite_tags)
28
+ end.tap { |v| p :GVB_VERSION, v if debug? }
29
+ end
30
+
31
+ def self.major_version(use_local_dir=false, include_lite_tags=false)
32
+ ver = version(use_local_dir, include_lite_tags)
33
+ v = ver.split('.')[0]
34
+
35
+ unless v =~ /^[0-9]+$/
36
+ raise ArgumentError,
37
+ "#{v} (part of #{ver.inspect}) is not a numeric version component. Abandon ship!"
38
+ end
39
+
40
+ return v.to_i
41
+ end
42
+
43
+ def self.minor_version(use_local_dir=false, include_lite_tags=false)
44
+ ver = version(use_local_dir, include_lite_tags)
45
+ v = ver.split('.')[1]
46
+
47
+ unless v =~ /^[0-9]+$/
48
+ raise ArgumentError,
49
+ "#{v} (part of #{ver.inspect}) is not a numeric version component. Abandon ship!"
50
+ end
51
+
52
+ return v.to_i
53
+ end
54
+
55
+ def self.patch_version(use_local_dir=false, include_lite_tags=false)
56
+ ver = version(use_local_dir, include_lite_tags)
57
+ v = ver.split('.')[2]
58
+
59
+ unless v =~ /^[0-9]+$/
60
+ raise ArgumentError,
61
+ "#{v} (part of #{ver.inspect}) is not a numeric version component. Abandon ship!"
62
+ end
63
+
64
+ return v.to_i
65
+ end
66
+
67
+ def self.internal_revision(use_local_dir=false, include_lite_tags=false)
68
+ version(use_local_dir, include_lite_tags).split('.', 4)[3].to_s
69
+ end
70
+
71
+ def self.date(use_local_dir=false, include_lite_tags = false)
72
+ if use_local_dir
73
+ repo_date(true, include_lite_tags)
74
+ else
75
+ gem_date || repo_date(false, include_lite_tags)
76
+ end
77
+ end
78
+
79
+ def self.tag_version(v, release_notes = false, include_lite_tags=false)
80
+ if dirty_tree?
81
+ puts "You have uncommitted files. Refusing to tag a dirty tree."
82
+ return false
83
+ end
84
+ if release_notes
85
+ log_file = Tempfile.new('gvb')
86
+
87
+ begin
88
+ # We need to find the tag before this one, so we can list all the commits
89
+ # between the two. This is not a trivial operation.
90
+ git_cmd = ["git", "describe", "--match=#{VERSION_TAG_GLOB}", "--always"]
91
+ git_cmd << "--tags" if include_lite_tags
92
+
93
+ prev_tag = run_command(git_cmd, "getting previous release tag").strip.gsub(/-\d+-g[0-9a-f]+$/, '')
94
+
95
+ log_file.puts <<-EOF.gsub(/^\t\t\t\t\t/, '')
96
+
97
+
98
+
99
+ # Write your release notes above. The first line should be the release name.
100
+ # To help you remember what's in here, the commits since your last release
101
+ # are listed below. This will become v#{v}
102
+ #
103
+ EOF
104
+ log_file.puts run_command(["git", "log", "--no-show-signature", "--format=# %h %s", "#{prev_tag}..HEAD"], "getting commit range of release")
105
+
106
+ log_file.close
107
+
108
+ pre_hash = Digest::SHA1.hexdigest(File.read(log_file.path))
109
+ run_command(["git", "config", "-e", "-f", log_file.path], "editing release notes", false)
110
+ if Digest::SHA1.hexdigest(File.read(log_file.path)) == pre_hash
111
+ puts "Release notes not edited; not making release"
112
+ log_file.unlink
113
+ return
114
+ end
115
+
116
+ puts "Tagging version #{v}..."
117
+ run_command(["git", "tag", "-a", "-F", log_file.path, "v#{v}"], "tagging release with annotations")
118
+ ensure
119
+ log_file.unlink
120
+ end
121
+ else
122
+ # Crikey this is a lot simpler
123
+ run_command(["git", "tag", "-a", "-m", "Version v#{v}", "v#{v}"], "tagging release")
124
+ end
125
+
126
+ run_command(["git", "push"], "pushing commits to the default remote repository")
127
+ run_command(["git", "push", "--tags"], "pushing tags to the default remote repository")
128
+ end
129
+
130
+ # Calculate a version number based on the date of the most recent git commit.
131
+ #
132
+ # Return a version format string of the form `"0.YYYYMMDD.N"`, where
133
+ # `YYYYMMDD` is the date of the "top-most" commit in the tree, and `N` is
134
+ # the number of other commits also made on that date.
135
+ #
136
+ # This version format is not recommented for general use. It has benefit
137
+ # only in situations where the principles of Semantic Versioning have no
138
+ # real meaning, such as packages where there is little or no concept of
139
+ # "backwards compatibility" (eg packages which only contain images and
140
+ # other assets), or where the package can, for reasons outside that of
141
+ # the package itself, never break backwards compatibility (definitions of
142
+ # binary-packed structures shared amongst multiple systems).
143
+ #
144
+ # The format of this commit-date-based version format allows for a strictly
145
+ # monotonically-increasing version number, aligned with the progression of the
146
+ # underlying git commit log.
147
+ #
148
+ # One limitation of the format is that it doesn't deal with the issue of
149
+ # package builds made from multiple divergent trees. Unlike
150
+ # `git-describe`-based output, there is no "commit hash" identity
151
+ # included in the version string. This is because of (ludicrous)
152
+ # limitations of the Rubygems format definition -- the moment there's a
153
+ # letter in the version number, the package is considered a "pre-release"
154
+ # version. Since hashes are hex, we're boned. Sorry about that. Don't
155
+ # make builds off a branch, basically.
156
+ #
157
+ def self.commit_date_version(use_local_dir = false)
158
+ if use_local_dir
159
+ commit_date_version_string(true)
160
+ else
161
+ gem_version || commit_date_version_string(false)
162
+ end
163
+ end
164
+
165
+ def self.commit_date_version_string(use_local_dir = false)
166
+ commit_dates = run_command(["git", "-C", git_dir(use_local_dir).to_s, "log", "--no-show-signature", "--format=%at"], "getting dates of all commits").
167
+ split("\n").
168
+ map { |l| Time.at(Integer(l)).strftime("%Y%m%d") }
169
+
170
+ version_date = commit_dates.first
171
+ commit_count = commit_dates.select { |d| d == version_date }.length - 1
172
+ dirty_suffix = if dirty_tree?
173
+ ".dirty.#{Time.now.strftime("%Y%m%d.%H%M%S")}"
174
+ else
175
+ ""
176
+ end
177
+
178
+ return "0.#{version_date}.#{commit_count}#{dirty_suffix}"
179
+ rescue CommandFailure => ex
180
+ p :GVB_CDVS_CMD_FAIL, ex.output if debug?
181
+ if ex.output =~ /fatal: your current branch .* does not have any commits yet/
182
+ return "0.0.0.1.ENOCOMMITS"
183
+ else
184
+ raise VersionUnobtainable, "Could not get commit date-based version from git repository at #{git_dir(use_local_dir)}"
185
+ end
186
+ end
187
+
188
+ def self.git_available?
189
+ try_command(["git", "--version"])
190
+ end
191
+ private_class_method :git_available?
192
+
193
+ def self.dirty_tree?(dir='.')
194
+ # Are we in a dirty, dirty tree?
195
+ ! run_command(["git", "-C", dir.to_s, "status", "--porcelain"], "checking for tree cleanliness").empty?
196
+ end
197
+ private_class_method :dirty_tree?
198
+
199
+ # Execute a command, specified as an array.
200
+ #
201
+ # On success, the full output of the command (stdout+stderr, interleaved) is returned unless capture_output is not true.
202
+ # On error, a `CommandFailure` exception is raised.
203
+ #
204
+ def self.run_command(cmd, desc, capture_output = true)
205
+ unless cmd.is_a?(Array)
206
+ raise ArgumentError, "Must pass command line arguments in an array"
207
+ end
208
+
209
+ unless cmd.all? { |s| s.is_a?(String) }
210
+ raise ArgumentError, "Command line arguments must be strings"
211
+ end
212
+
213
+ if debug?
214
+ p :GVB_CMD, desc, cmd
215
+ end
216
+
217
+ if capture_output == true
218
+ out, status = Open3.capture2e({"LC_MESSAGES" => "C"}, *cmd)
219
+ else
220
+ out = '(output not captured)'
221
+ pid = spawn(*cmd)
222
+ (retpid, status) = Process.wait2 pid
223
+ end
224
+
225
+ if status.exitstatus != 0
226
+ raise CommandFailure.new("Failed while #{desc}", out, status.exitstatus)
227
+ else
228
+ out
229
+ end
230
+ end
231
+ private_class_method :run_command
232
+
233
+ # Execute a command, and return whether it succeeded or failed.
234
+ #
235
+ def self.try_command(cmd)
236
+ begin
237
+ run_command(cmd, "try_command")
238
+ true
239
+ rescue CommandFailure
240
+ false
241
+ end
242
+ end
243
+ private_class_method :try_command
244
+
245
+ def self.run_git(git_args, desc, use_local_dir)
246
+ run_command(["git", "-C", git_dir(use_local_dir).to_s] + git_args, desc)
247
+ end
248
+ private_class_method :run_git
249
+
250
+ def self.caller_file
251
+ # Who called us? Because this method gets called from other methods
252
+ # within this file, we can't just look at Gem.location_of_caller, but
253
+ # instead we need to parse the caller stack ourselves to find which
254
+ # gem we're trying to version all over.
255
+ Pathname(
256
+ caller_locations.
257
+ map(&:path).
258
+ tap { |c| p :CALLER_LOCATIONS, c if debug? }.
259
+ find { |l| l != __FILE__ }
260
+ ).realpath rescue nil
261
+ end
262
+ private_class_method :caller_file
263
+
264
+ def self.caller_gemspec
265
+ cf = caller_file or return nil
266
+
267
+ # Grovel through all the loaded gems to try and find the gem
268
+ # that contains the caller's file.
269
+ Gem.loaded_specs.values.each do |spec|
270
+ # On Windows I have encountered gems that already have an absolute
271
+ # path, verify that the path is relative before appending to it
272
+ search_dirs = spec.require_paths.map do |path|
273
+ if Pathname(path).absolute?
274
+ path
275
+ else
276
+ File.join(spec.full_gem_path, path)
277
+ end
278
+ end
279
+ search_dirs << File.join(spec.full_gem_path, spec.bindir)
280
+ search_dirs.map! do |d|
281
+ begin
282
+ Pathname(d).realpath.to_s
283
+ rescue Errno::ENOENT
284
+ nil
285
+ end
286
+ end.compact!
287
+
288
+ if search_dirs.find { |d| cf.to_s.index(d) == 0 }
289
+ return spec
290
+ end
291
+ end
292
+
293
+ if debug?
294
+ p :GVB_NO_GEMSPEC, cf
295
+ end
296
+
297
+ nil
298
+ end
299
+ private_class_method :caller_gemspec
300
+
301
+ def self.gem_version
302
+ return "" if caller_gemspec.nil?
303
+
304
+ caller_gemspec.version.to_s
305
+ end
306
+ private_class_method :gem_version
307
+
308
+ def self.gem_date
309
+ return nil if caller_gemspec.nil?
310
+ return nil if caller_gemspec.date.nil?
311
+
312
+ caller_gemspec.date.strftime("%F")
313
+ end
314
+ private_class_method :gem_version
315
+
316
+ def self.repo_version(use_local_dir, include_lite_tags)
317
+ begin
318
+ run_git(["config", "versionBump.versionOverride"], "getting versionOverride", use_local_dir).chomp
319
+ rescue CommandFailure => ex
320
+ p :NO_OVERRIDE_VERSION, [ex.class, ex.message] if debug?
321
+ repo_version_from_tag(use_local_dir, include_lite_tags)
322
+ end
323
+ end
324
+ private_class_method :repo_version
325
+
326
+ def self.repo_version_from_tag(use_local_dir, include_lite_tags)
327
+ git_cmd = ["git", "-C", git_dir(use_local_dir).to_s, "describe", "--dirty=.1.dirty.#{Time.now.strftime("%Y%m%d.%H%M%S")}", "--match=#{VERSION_TAG_GLOB}"]
328
+ git_cmd << "--tags" if include_lite_tags
329
+
330
+ begin
331
+ run_command(git_cmd, "getting current version descriptor").
332
+ strip.
333
+ gsub(/^v/, '').
334
+ gsub('-', '.')
335
+ rescue CommandFailure => ex
336
+ p :GVB_REPO_VERSION_FAILURE, ex.output if debug?
337
+ if ex.output =~ /fatal: No names found, cannot describe anything/
338
+ # aka "no tags, bro"
339
+ "0.0.0.1.ENOTAG"
340
+ else
341
+ raise VersionUnobtainable, "Could not get version from gemspec or git repository at #{git_dir(use_local_dir)}"
342
+ end
343
+ end
344
+ end
345
+ private_class_method :repo_version_from_tag
346
+
347
+ def self.repo_date(use_local_dir, include_lite_tags)
348
+ begin
349
+ run_git(["config", "versionBump.dateOverride"], "getting dateOverride", use_local_dir).chomp
350
+ rescue CommandFailure => ex
351
+ p :NO_OVERRIDE_DATE, [ex.class, ex.message] if debug?
352
+ repo_date_from_commit(use_local_dir, include_lite_tags)
353
+ end
354
+ end
355
+ private_class_method :repo_date
356
+
357
+ def self.repo_date_from_commit(use_local_dir, include_lite_tags)
358
+ if dirty_tree?(git_dir(use_local_dir))
359
+ Time.now.strftime("%F")
360
+ else
361
+ # Clean tree. Date of last commit is needed.
362
+ (run_command(["git", "-C", git_dir(use_local_dir).to_s, "show", "--no-show-signature", "--format=format:%cd", "--date=short"], "getting date of last commit").lines.first || "").strip
363
+ end
364
+ rescue CommandFailure
365
+ raise VersionUnobtainable, "Could not get commit date from git repository at #{git_dir(use_local_dir)}"
366
+ end
367
+ private_class_method :repo_date_from_commit
368
+
369
+ def self.git_dir(use_local_dir = false)
370
+ if use_local_dir
371
+ Dir.pwd
372
+ else
373
+ (caller_file && caller_file.dirname) || Dir.pwd
374
+ end.tap { |d| p :GVB_GIT_DIR, use_local_dir, d if debug? }
375
+ end
376
+ private_class_method :git_dir
377
+
378
+ def self.debug?
379
+ ENV.key?("GVB_DEBUG")
380
+ end
381
+ private_class_method :debug?
382
+ end
383
+
384
+ GVB = GitVersionBump unless defined? GVB
385
+
386
+ require 'git-version-bump/version'
metadata ADDED
@@ -0,0 +1,110 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: git-version-bump
3
+ version: !ruby/object:Gem::Version
4
+ version: '0'
5
+ platform: ruby
6
+ authors:
7
+ - Matt Palmer
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-09-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: github-release
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rdoc
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description:
70
+ email:
71
+ executables:
72
+ - git-version-bump
73
+ extensions: []
74
+ extra_rdoc_files:
75
+ - README.md
76
+ files:
77
+ - ".github/workflows/release.yml"
78
+ - ".gitignore"
79
+ - Gemfile
80
+ - LICENCE
81
+ - README.md
82
+ - Rakefile
83
+ - bin/git-version-bump
84
+ - git-version-bump.gemspec
85
+ - lib/git-version-bump.rb
86
+ - lib/git-version-bump/rake-tasks.rb
87
+ - lib/git-version-bump/version.rb
88
+ homepage: https://github.com/mpalmer/git-version-bump
89
+ licenses: []
90
+ metadata: {}
91
+ post_install_message:
92
+ rdoc_options: []
93
+ require_paths:
94
+ - lib
95
+ required_ruby_version: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: 2.1.0
100
+ required_rubygems_version: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ requirements: []
106
+ rubygems_version: 3.1.6
107
+ signing_key:
108
+ specification_version: 4
109
+ summary: Manage your app version entirely via git tags
110
+ test_files: []