git-lite-version-bump 0.17.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,157 @@
1
+ Maintain your program versions entirely within git. No local files
2
+ required! All versioning information is stored using git tags.
3
+
4
+ This gem contains a command-line tool and set of Rake tasks to increment
5
+ and display your version numbers via git tags, and some associated Ruby code to use
6
+ inside a gemspec or your program to retrieve the current version number, for
7
+ use in builds and at runtime.
8
+
9
+
10
+ # Usage
11
+
12
+ Most of your day-to-day usage of `git-version-bump` will be via the command
13
+ line. When you bump a version, a new tag will be created representing the newly
14
+ incremented version number at the current commit. If no tags currently
15
+ exist, the previous version will be taken to be `0.0.0` and then incremented
16
+ accordingly.
17
+
18
+
19
+ ## On the command line
20
+
21
+ Pretty damned trivial:
22
+
23
+ git version-bump <major|minor|patch|show>
24
+
25
+ You can also shorten the specifier to any unique substring:
26
+
27
+ git version-bump ma
28
+ git version-bump mi
29
+ git version-bump p
30
+ git version-bump s
31
+
32
+ I recommend adding an alias to your `~/.gitconfig` file, for less typing:
33
+
34
+ [alias]
35
+ vb = version-bump
36
+
37
+ You can also add your own release notes to your release tags, by using the
38
+ `-n` (or `--notes`, if you like typing) option:
39
+
40
+ git version-bump -n minor
41
+
42
+ This will open an editor, containing a list of the commits since the last
43
+ release tag, in which you can type your release notes. If you follow
44
+ standard git commit style (a "heading" line, then a blank line, followed by
45
+ free-form text) you're perfectly positioned to use
46
+ [github-release](http://theshed.hezmatt.org/github-release) to make
47
+ gorgeous-looking release announcements to Github.
48
+
49
+
50
+ ## In your `Rakefile`
51
+
52
+ If you'd like to have access to the version-bumping goodness via `rake`, add
53
+ the following line to your `Rakefile`:
54
+
55
+ require 'git-version-bump/rake-tasks'
56
+
57
+ You will now have the following rake tasks available:
58
+
59
+ rake version:bump:major # bump major version (x.y.z -> x+1.0.0)
60
+ rake version:bump:minor # bump minor version (x.y.z -> x.y+1.0)
61
+ rake version:bump:patch # bump patch version (x.y.z -> x.y.z+1)
62
+ rake version:bump:show # Print current version number
63
+
64
+ (Since `version:bump:major` is a lot of typing, there are also shortcuts:
65
+ `v:b:major`, `v:b:maj`, `v:b:minor`, `v:b:min`, `v:b:patch`, `v:b:pat`, and
66
+ `v:b:p`)
67
+
68
+
69
+ ## In your Ruby code
70
+
71
+ To get access to this version information in your code (such as in your
72
+ `gemspec`, or the definition of a `::VERSION` constant), you can `require
73
+ 'git-version-bump'` and use the following methods:
74
+
75
+ GVB.version # Return the entire version string
76
+ GVB.major_version # Return just the 'major' portion of the version
77
+ GVB.minor_version # Return just the 'minor' portion of the version
78
+ GVB.patch_version # Return just the 'patch' portion of the version
79
+ GVB.internal_revision # Return "internal revision" information, or nil
80
+ GVB.date # Return the date of the most recent commit, or
81
+ # today's date if the tree is dirty
82
+
83
+ The "internal revision" is set when the tree is dirty, or when the latest
84
+ git commit doesn't correspond with a tag. In that case, the internal
85
+ revision will describe, in the manner of `git describe`, the full details of
86
+ the version of the code in use. This information will be part of the
87
+ version string provided by `gvb_version`.
88
+
89
+ If any of these methods are called when there isn't a tag or other version
90
+ information available, the version will be assumed to be `0.0.0.1.ENOTAG`
91
+ with a date of `1970-01-01`.
92
+
93
+
94
+ ### In your gemspec
95
+
96
+ Typically, you want to encode your version and commit date into your
97
+ gemspec, like this:
98
+
99
+ Gem::Specification.new do |s|
100
+ s.version = GVB.version
101
+ s.date = GVB.date
102
+
103
+ ...
104
+ end
105
+
106
+ The beauty of this method is that whenever you run a `rake build`, you'll
107
+ get a gem which is *accurately* versioned for the current state of your
108
+ repository. No more wondering if the `foobar-1.2.3` gem installed on your
109
+ system was built from pristine sources, or with that experimental patch you
110
+ were trying out...
111
+
112
+
113
+ ### In your gem
114
+
115
+ If, like me, you're one of those people who likes to be able to easily see
116
+ what version of a library you're running, then you probably like to define a
117
+ `VERSION` constant somewhere in your gem's namespace. That, too, is simple
118
+ to do:
119
+
120
+ require 'git-version-bump'
121
+
122
+ class Foobar
123
+ VERSION = GVB.version
124
+ end
125
+
126
+ This will work correctly inside your git tree, and also in your installed
127
+ gem. Magical!
128
+
129
+ #### For projects using lite tags
130
+
131
+ If you are using GitHub releases for your project or some other method that
132
+ involves light tags (tags with no annotations), you might notice that these
133
+ tags are not detected by git-version-bump by default. If you want these
134
+ commits to be detected then use the following configuration:
135
+
136
+ require 'git-version-bump'
137
+
138
+ class Foobar
139
+ # First parameter is use_local_git, second is include_lite_tags
140
+ VERSION = GVB.version(false, true)
141
+ end
142
+
143
+
144
+ # Contributing
145
+
146
+ Send your pull requests to the [Github
147
+ repo](https://github.com/mpalmer/git-version-bump), or send patches to
148
+ `theshed+git-version-bump@hezmatt.org`. Bug reports can be sent to the same
149
+ place, although I greatly prefer patches.
150
+
151
+
152
+ # Licence
153
+
154
+ Unless otherwise specified, all code in this repository is licenced under
155
+ the terms of the GNU Public Licence, version 3, as published by the Free
156
+ Software Foundation. The full terms of this licence can be found in the
157
+ file LICENCE.
@@ -0,0 +1,17 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+
4
+ Bundler::GemHelper.install_tasks
5
+
6
+ require 'rdoc/task'
7
+
8
+ Rake::RDocTask.new do |rd|
9
+ rd.main = "README.md"
10
+ rd.title = 'git-version-bump'
11
+ rd.rdoc_files.include("README.md", "lib/**/*.rb")
12
+ end
13
+
14
+ task :release do
15
+ sh "git push --follow-tags"
16
+ sh "git release"
17
+ end
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'git-version-bump'
4
+
5
+ if ARGV[0].nil? or
6
+ ARGV[0].empty? or
7
+ (ARGV.length == 1 && (ARGV[0] == "-d" || ARGV[0] == "--dry-run")) or
8
+ ARGV[0] == '-h' or
9
+ ARGV[0] == '--help'
10
+ $stderr.puts <<-EOF.gsub(/^\t\t/, '')
11
+ Usage: git version-bump [-n|--notes] [-d|--dry-run] <major|minor|patch|show>
12
+
13
+ 'major': x.y.z -> x+1.0.0
14
+ 'minor': x.y.z -> x.y+1.0
15
+ 'patch': x.y.z -> x.y.z+1
16
+
17
+ 'show': Display the current GVB version
18
+
19
+ -d, --dry-run: Calculate and return the bump value, but don't update git workspace or remote
20
+ -n, --notes: Prompt for "release notes" to add to the release tag
21
+ -l, --lite-tags: Include non-annotated git tags
22
+ EOF
23
+ end
24
+
25
+ release_notes = ARGV.delete('-n') || ARGV.delete('--notes')
26
+ dry_run = ARGV.delete('-d') || ARGV.delete('--dry-run')
27
+ lite_tags = ARGV.delete('-l') || ARGV.delete('--lite-tags')
28
+
29
+ if ARGV[0].nil? or ARGV[0].empty?
30
+ exit 1
31
+ elsif ARGV[0] == '-h' or ARGV[0] == '--help'
32
+ exit 0
33
+ end
34
+
35
+ result = case ARGV[0].downcase
36
+ when /^maj?o?r?$/
37
+ "#{GVB.major_version(true) + 1}.0.0"
38
+ when /^min?o?r?$/
39
+ "#{GVB.major_version(true)}.#{GVB.minor_version(true)+1}.0"
40
+ when /^pa?t?c?h?$/
41
+ "#{GVB.major_version(true)}.#{GVB.minor_version(true)}.#{GVB.patch_version(true)+1}"
42
+ when /^sh?o?w?$/
43
+ puts GVB.version(true)
44
+ exit 0
45
+ else
46
+ $stderr.puts "Unknown argument: #{ARGV[0]}. Try --help."
47
+ exit 1
48
+ end
49
+
50
+ if dry_run
51
+ puts result
52
+ else
53
+ GVB.tag_version result, release_notes, lite_tags
54
+ puts "Version is now #{GVB.version(true)}."
55
+ end
@@ -0,0 +1,26 @@
1
+ lib = File.expand_path('../lib', __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require 'git-version-bump'
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "git-lite-version-bump"
7
+
8
+ s.version = GVB.version
9
+ s.date = GVB.date
10
+
11
+ s.platform = Gem::Platform::RUBY
12
+ s.required_ruby_version = ">= 1.8.7"
13
+
14
+ s.homepage = "http://github.com/rbclark/git-version-bump"
15
+ s.summary = "Manage your app version entirely via git tags (even lite tags)"
16
+ s.authors = ["Matt Palmer"]
17
+
18
+ s.extra_rdoc_files = ["README.md"]
19
+ s.files = `git ls-files`.split("\n")
20
+ s.executables = ["git-version-bump"]
21
+
22
+ s.add_development_dependency 'github-release'
23
+ s.add_development_dependency 'rake'
24
+ s.add_development_dependency 'bundler'
25
+ s.add_development_dependency 'rdoc'
26
+ end
@@ -0,0 +1,324 @@
1
+ require 'tempfile'
2
+ require 'digest/sha1'
3
+ require 'pathname'
4
+
5
+ module GitVersionBump
6
+ class VersionUnobtainable < StandardError; end
7
+
8
+ DEVNULL = Gem.win_platform? ? "NUL" : "/dev/null"
9
+
10
+ def self.version(use_local_git=false, include_lite_tags=false)
11
+ if use_local_git
12
+ unless git_available?
13
+ raise RuntimeError,
14
+ "GVB.version(use_local_git=true) called, but git isn't installed"
15
+ end
16
+
17
+ sq_git_dir = shell_quoted_string(Dir.pwd)
18
+ else
19
+ sq_git_dir = shell_quoted_string((File.dirname(caller_file) rescue nil || Dir.pwd))
20
+ end
21
+
22
+ git_cmd = "git -C #{sq_git_dir} describe --dirty='.1.dirty.#{Time.now.strftime("%Y%m%d.%H%M%S")}' --match='v[0-9]*.[0-9]*.*[0-9]'"
23
+ git_cmd << " --tags" if include_lite_tags
24
+
25
+ git_ver = `#{git_cmd} 2> #{DEVNULL}`.
26
+ strip.
27
+ gsub(/^v/, '').
28
+ gsub('-', '.')
29
+
30
+ # If git returned success, then it gave us a described version.
31
+ # Success!
32
+ return git_ver if $? == 0
33
+
34
+ # git failed us; we're either not in a git repo or else we've never
35
+ # tagged anything before.
36
+
37
+ # Are we in a git repo with no tags? If so, dump out our
38
+ # super-special version and be done with it, otherwise try to use the
39
+ # gem version.
40
+ system("git -C #{sq_git_dir} status > #{DEVNULL} 2>&1")
41
+ $? == 0 ? "0.0.0.1.ENOTAG" : gem_version(use_local_git)
42
+ end
43
+
44
+ def self.major_version(use_local_git=false, include_lite_tags=false)
45
+ ver = version(use_local_git, include_lite_tags)
46
+ v = ver.split('.')[0]
47
+
48
+ unless v =~ /^[0-9]+$/
49
+ raise ArgumentError,
50
+ "#{v} (part of #{ver.inspect}) is not a numeric version component. Abandon ship!"
51
+ end
52
+
53
+ return v.to_i
54
+ end
55
+
56
+ def self.minor_version(use_local_git=false, include_lite_tags=false)
57
+ ver = version(use_local_git, include_lite_tags)
58
+ v = ver.split('.')[1]
59
+
60
+ unless v =~ /^[0-9]+$/
61
+ raise ArgumentError,
62
+ "#{v} (part of #{ver.inspect}) is not a numeric version component. Abandon ship!"
63
+ end
64
+
65
+ return v.to_i
66
+ end
67
+
68
+ def self.patch_version(use_local_git=false, include_lite_tags=false)
69
+ ver = version(use_local_git, include_lite_tags)
70
+ v = ver.split('.')[2]
71
+
72
+ unless v =~ /^[0-9]+$/
73
+ raise ArgumentError,
74
+ "#{v} (part of #{ver.inspect}) is not a numeric version component. Abandon ship!"
75
+ end
76
+
77
+ return v.to_i
78
+ end
79
+
80
+ def self.internal_revision(use_local_git=false, include_lite_tags=false)
81
+ version(use_local_git, include_lite_tags).split('.', 4)[3].to_s
82
+ end
83
+
84
+ def self.date(use_local_git=false)
85
+ if use_local_git
86
+ unless git_available?
87
+ raise RuntimeError,
88
+ "GVB.date(use_local_git=true), but git is not installed"
89
+ end
90
+
91
+ sq_git_dir = shell_quoted_string(Dir.pwd)
92
+ else
93
+ sq_git_dir = shell_quoted_string((File.dirname(caller_file) rescue nil || Dir.pwd))
94
+ end
95
+
96
+ # Are we in a git tree?
97
+ system("git -C #{sq_git_dir} status > #{DEVNULL} 2>&1")
98
+ if $? == 0
99
+ # Yes, we're in git.
100
+
101
+ if dirty_tree?
102
+ return Time.now.strftime("%F")
103
+ else
104
+ # Clean tree. Date of last commit is needed.
105
+ return `git -C #{sq_git_dir} show --no-show-signature --format=format:%cd --date=short`.lines.first.strip
106
+ end
107
+ else
108
+ if use_local_git
109
+ raise RuntimeError,
110
+ "GVB.date(use_local_git=true) called from non-git location"
111
+ end
112
+
113
+ # Not in git; time to hit the gemspecs
114
+ if spec = caller_gemspec
115
+ return spec.date.strftime("%F")
116
+ end
117
+
118
+ raise RuntimeError,
119
+ "GVB.date called from mysterious, non-gem location."
120
+ end
121
+ end
122
+
123
+ def self.tag_version(v, release_notes = false, include_lite_tags=false)
124
+ if dirty_tree?
125
+ puts "You have uncommitted files. Refusing to tag a dirty tree."
126
+ else
127
+ if release_notes
128
+ # We need to find the tag before this one, so we can list all the commits
129
+ # between the two. This is not a trivial operation.
130
+ git_cmd = 'git describe --always'
131
+ git_cmd << ' --tags' if include_lite_tags
132
+ prev_tag = `#{git_cmd}`.strip.gsub(/-\d+-g[0-9a-f]+$/, '')
133
+
134
+ log_file = Tempfile.new('gvb')
135
+
136
+ log_file.puts <<-EOF.gsub(/^\t\t\t\t\t/, '')
137
+
138
+
139
+
140
+ # Write your release notes above. The first line should be the release name.
141
+ # To help you remember what's in here, the commits since your last release
142
+ # are listed below. This will become v#{v}
143
+ #
144
+ EOF
145
+
146
+ log_file.close
147
+ system("git log --no-show-signature --format='# %h %s' #{prev_tag}..HEAD >>#{log_file.path}")
148
+
149
+ pre_hash = Digest::SHA1.hexdigest(File.read(log_file.path))
150
+ system("git config -e -f #{log_file.path}")
151
+ if Digest::SHA1.hexdigest(File.read(log_file.path)) == pre_hash
152
+ puts "Release notes not edited; aborting"
153
+ log_file.unlink
154
+ return
155
+ end
156
+
157
+ puts "Tagging version #{v}..."
158
+ system("git tag -a -F #{log_file.path} v#{v}")
159
+ log_file.unlink
160
+ else
161
+ # Crikey this is a lot simpler
162
+ system("git tag -a -m 'Version v#{v}' v#{v}")
163
+ end
164
+
165
+ system("git push > #{DEVNULL} 2>&1")
166
+ system("git push --tags > #{DEVNULL} 2>&1")
167
+ end
168
+ end
169
+
170
+ # Calculate a version number based on the date of the most recent git commit.
171
+ #
172
+ # Return a version format string of the form `"0.YYYYMMDD.N"`, where
173
+ # `YYYYMMDD` is the date of the "top-most" commit in the tree, and `N` is
174
+ # the number of other commits also made on that date.
175
+ #
176
+ # This version format is not recommented for general use. It has benefit
177
+ # only in situations where the principles of Semantic Versioning have no
178
+ # real meaning, such as packages where there is little or no concept of
179
+ # "backwards compatibility" (eg packages which only contain images and
180
+ # other assets), or where the package can, for reasons outside that of
181
+ # the package itself, never break backwards compatibility (definitions of
182
+ # binary-packed structures shared amongst multiple systems).
183
+ #
184
+ # The format of this commit-date-based version format allows for a strictly
185
+ # monotonically-increasing version number, aligned with the progression of the
186
+ # underlying git commit log.
187
+ #
188
+ # One limitation of the format is that it doesn't deal with the issue of
189
+ # package builds made from multiple divergent trees. Unlike
190
+ # `git-describe`-based output, there is no "commit hash" identity
191
+ # included in the version string. This is because of (ludicrous)
192
+ # limitations of the Rubygems format definition -- the moment there's a
193
+ # letter in the version number, the package is considered a "pre-release"
194
+ # version. Since hashes are hex, we're boned. Sorry about that. Don't
195
+ # make builds off a branch, basically.
196
+ #
197
+ def self.commit_date_version(use_local_git = false)
198
+ if use_local_git
199
+ unless git_available?
200
+ raise RuntimeError,
201
+ "GVB.commit_date_version(use_local_git=true) called, but git isn't installed"
202
+ end
203
+
204
+ sq_git_dir = shell_quoted_string(Dir.pwd)
205
+ else
206
+ sq_git_dir = shell_quoted_string((File.dirname(caller_file) rescue nil || Dir.pwd))
207
+ end
208
+
209
+ commit_dates = `git -C #{sq_git_dir} log --format=%at`.
210
+ split("\n").
211
+ map { |l| Time.at(Integer(l)).strftime("%Y%m%d") }
212
+
213
+ if $? == 0
214
+ # We got a log; calculate our version number and we're done.
215
+ version_date = commit_dates.first
216
+ commit_count = commit_dates.select { |d| d == version_date }.length - 1
217
+ dirty_suffix = if dirty_tree?
218
+ ".dirty.#{Time.now.strftime("%Y%m%d.%H%M%S")}"
219
+ else
220
+ ""
221
+ end
222
+
223
+ return "0.#{version_date}.#{commit_count}#{dirty_suffix}"
224
+ end
225
+
226
+ # git failed us; either we're not in a git repo or else it's a git
227
+ # repo that's not got any commits.
228
+
229
+ # Are we in a git repo with no tags? If so, dump out our
230
+ # super-special version and be done with it.
231
+ system("git -C #{sq_git_dir} status > #{DEVNULL} 2>&1")
232
+ $? == 0 ? "0.0.0.1.ENOCOMMITS" : gem_version(use_local_git)
233
+ end
234
+
235
+ private
236
+
237
+ def self.git_available?
238
+ system("git --version > #{DEVNULL} 2>&1")
239
+
240
+ $? == 0
241
+ end
242
+
243
+ def self.dirty_tree?
244
+ # Are we in a dirty, dirty tree?
245
+ system("! git diff --no-ext-diff --quiet --exit-code 2> #{DEVNULL} || ! git diff-index --cached --quiet HEAD 2> #{DEVNULL}")
246
+
247
+ $? == 0
248
+ end
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.
257
+ map { |l| l.split(':')[0] }.
258
+ find { |l| l != __FILE__ }
259
+ ).realpath.to_s rescue nil
260
+ end
261
+
262
+ def self.caller_gemspec
263
+ cf = caller_file or return nil
264
+
265
+ # Grovel through all the loaded gems to try and find the gem
266
+ # that contains the caller's file.
267
+ Gem.loaded_specs.values.each do |spec|
268
+ search_dirs = spec.require_paths.map { |d| "#{spec.full_gem_path}/#{d}" } +
269
+ [File.join(spec.full_gem_path, spec.bindir)]
270
+ search_dirs.map! do |d|
271
+ begin
272
+ Pathname(d).realpath.to_s
273
+ rescue Errno::ENOENT
274
+ nil
275
+ end
276
+ end.compact!
277
+
278
+ if search_dirs.find { |d| cf.index(d) == 0 }
279
+ return spec
280
+ end
281
+ end
282
+
283
+ raise VersionUnobtainable,
284
+ "Unable to find gemspec for caller file #{cf}"
285
+ end
286
+
287
+ def self.gem_version(use_local_git = false)
288
+ if use_local_git
289
+ raise VersionUnobtainable,
290
+ "Unable to determine version from local git repo. This should never happen."
291
+ end
292
+
293
+ if spec = caller_gemspec
294
+ return spec.version.to_s
295
+ else
296
+ # If we got here, something went *badly* wrong -- presumably, we
297
+ # weren't called from within a loaded gem, and so we've got *no*
298
+ # idea what's going on. Time to bail!
299
+ if git_available?
300
+ raise VersionUnobtainable,
301
+ "GVB.version(#{use_local_git.inspect}) failed, and I really don't know why."
302
+ else
303
+ raise VersionUnobtainable,
304
+ "GVB.version(#{use_local_git.inspect}) failed; perhaps you need to install git?"
305
+ end
306
+ end
307
+ end
308
+
309
+ def self.shell_quoted_string(dir_string)
310
+ if Gem.win_platform?
311
+ return "\"#{dir_string}\""
312
+ else
313
+ # Shell Quoted, for your convenience
314
+ return "'#{dir_string.gsub("'", "'\\''")}'"
315
+ end
316
+ end
317
+
318
+ private_class_method :shell_quoted_string
319
+
320
+ end
321
+
322
+ GVB = GitVersionBump unless defined? GVB
323
+
324
+ require 'git-version-bump/version'