git-version-bump 0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []