revision 1.1.4

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 94dfd966e2e44d185a37be41bb1fe745455f0e6d
4
+ data.tar.gz: cf971a767a20dbc24696adff7bbf634e821e8ea0
5
+ SHA512:
6
+ metadata.gz: 7e5ec6a596855ad4f50c5ee1307c9cfac3e4c7fe4918f6e4c03b00879c8e7824caa1675bea87d9ac4b0601b165bf6f6b8e1dd2c7e5e95b31e3f6450fe759d994
7
+ data.tar.gz: 6b71b8ed50740a488904d66b35e52bdcfb14dd5ec5bfcd71cef1cd387e94cdbfd8816584a60fec3342e6133820c108a53cf299dc6964e03207b205fa28a08440
data/.gitignore ADDED
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
12
+ Gemfile.lock
13
+
14
+ README.html
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.4.2
5
+ before_install: gem install bundler -v 1.16.0
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ #git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in revision.gemspec
6
+ gemspec
data/README.org ADDED
@@ -0,0 +1,269 @@
1
+ #+TITLE: Revision management gem
2
+ #+AUTHOR: Cormac Cannon
3
+ #+EMAIL: cormac.cannon@neuromoddevices.com
4
+ #+LANGUAGE: en
5
+
6
+ # Generic properties
7
+ # :eval never-export
8
+ # ... Prevent auto evaluation of blocks on export. Evaluate them interactively instead
9
+ # :results verbatim
10
+ # ... Output results verbatim rather than autodetecting (which usually ends up tabular). Override per script using :results tabular
11
+ #+PROPERTY: header-args :results verbatim :noweb yes :exports both :eval never-export
12
+ # #+PROPERTY: header-args :results verbatim :noweb yes :exports both
13
+
14
+ # HTML EXPORT SETUP
15
+
16
+ # 1. Auto-export to html on save (N.B. This doesn't work?)
17
+ # This can be done per-session by calling 'toggle-org-html-export-on-save'
18
+ # Local variables:
19
+ # eval: (add-hook 'after-save-hook 'org-html-export-to-html t t)
20
+ # End:
21
+
22
+ # 2. Apply a theme
23
+ #+SETUPFILE: org-themes/theme-bigblow.setup
24
+ # or alternatively #+SETUPFILE: theme-readtheorg.setup
25
+
26
+ * Overview
27
+
28
+ This gem automates revision management for source projects. The tool is language agnostic (used for C, ruby and matlab projects, to date -- though you're
29
+ probably better off using bundler for ruby projects). It supports per-project configuration using a yaml-format file called =releasables.yaml= located at
30
+ the project root.
31
+
32
+ It currently supports the following functionality
33
+ - Manage 3-component revision IDs embedded natively in source file
34
+ - Embeds/updates/extracts changelog in source file header/footer comments
35
+ - Automatically prompts for a changelog entry each time a revision identifier is incremented
36
+ - Optionally commits and tags changes to a git repo after an update to the revision ID
37
+ - Builds and archives projects in zip format (including release notes and arbitrary release artefacts defined
38
+
39
+ * Installation
40
+
41
+ #+BEGIN_NOTE
42
+ The gem hasn't been uploaded to rubygems.org as yet.
43
+ #+END_NOTE
44
+
45
+ ** From rubygems.org
46
+
47
+ #+BEGIN_SRC sh
48
+ gem install revision
49
+ #+END_SRC
50
+
51
+ ** From source
52
+
53
+ *** Checkout
54
+
55
+ #+BEGIN_SRC sh
56
+ gem install bundler
57
+ git clone git@git.nmd.ie:gem/revision
58
+ cd revision
59
+ #+END_SRC
60
+
61
+ *** Install
62
+
63
+ #+BEGIN_SRC sh
64
+ bundle install
65
+ bundle exec rake install
66
+ #+END_SRC
67
+
68
+ #+RESULTS:
69
+ #+begin_example
70
+ Using rake 10.5.0
71
+ Using bundler 1.16.0
72
+ Using coderay 1.1.2
73
+ Using diff-lcs 1.3
74
+ Using git 1.3.0
75
+ Using method_source 0.9.0
76
+ Using pry 0.11.3
77
+ Using rubyzip 1.2.1
78
+ Using thor 0.19.4
79
+ Using revision 1.0.0 from source at `.`
80
+ Using rspec-support 3.7.0
81
+ Using rspec-core 3.7.0
82
+ Using rspec-expectations 3.7.0
83
+ Using rspec-mocks 3.7.0
84
+ Using rspec 3.7.0
85
+ Bundle complete! 5 Gemfile dependencies, 15 gems now installed.
86
+ Use `bundle info [gemname]` to see where a bundled gem is installed.
87
+ revision 1.0.0 built to pkg/revision-1.0.0.gem.
88
+ revision (1.0.0) installed.
89
+ #+end_example
90
+
91
+ * Usage
92
+
93
+ ** Supported Commands
94
+ Run the executable with no arguments to get usage instructions in your console window...
95
+
96
+ #+BEGIN_SRC sh
97
+ revision
98
+ #+END_SRC
99
+
100
+ #+RESULTS:
101
+ #+begin_example
102
+ Loading releasable definitions from /home/cormacc/nextcloud/nmd/gem/revision/releasables.yaml ...
103
+ Commands:
104
+ revision archive # Archive releasables
105
+ revision changelog # Display change log on stdout
106
+ revision help [COMMAND] # Describe available commands or one specific command
107
+ revision info # Display info for all defined releasables
108
+ revision major # Increment major revision index
109
+ revision minor # Increment minor revision index
110
+ revision patch # Increment patch revision index
111
+ revision tag # Commit the current revision to a local git repo ...
112
+
113
+ Options:
114
+ [--dryrun], [--no-dryrun]
115
+ [--id=ID]
116
+
117
+ #+end_example
118
+
119
+ #+BEGIN_NOTE
120
+ The tool can be run from any subfolder of a project root -- it will traverse the tree until it finds
121
+ an ancestor containing =releasables.yaml= OR can go no further (in which case it throws an error).
122
+ #+END_NOTE
123
+
124
+ *** Operating on multiple releasables
125
+
126
+ A single =releasables.yaml= file can define multiple releasables, either implicitly (via inclusion) or explicitly
127
+ (see [[Configuration]] section below for examples). In this context, the ~info~ and ~archive~ commands
128
+ will operate on all defined releasables, whereas the remaining commands will require the releasable
129
+ to be specified using the ~--id=~ option, e.g.
130
+
131
+ #+BEGIN_SRC sh
132
+ revision minor --id=firmware
133
+ #+END_SRC
134
+
135
+ ** Configuration
136
+
137
+ YAML syntax is used for the configuration file. The [[Syntax]] and [[Examples]] sections below should provide sufficient
138
+ introduction to the limit subset of language features required to use this tool, however further info
139
+ may be found at the following links:
140
+
141
+ - http://docs.ansible.com/ansible/latest/YAMLSyntax.html
142
+ - http://www.yaml.org/start.html
143
+
144
+ *** Syntax
145
+
146
+ The =releasables.yaml= file should contain a top level ~:releasables~ node.
147
+ Under this, you can create a list (or YAML /sequence/) of releasable definitions.
148
+
149
+ Each sequence item begins with a ~-~ and contains either a link to a folder containing its own =releasables.yaml=
150
+ defining one or more releasables ...
151
+
152
+ #+BEGIN_SRC yaml
153
+ - :folder: relative/path/to/a/releasable/folder
154
+ #+END_SRC
155
+
156
+ ... or a single inline releasable definition.
157
+
158
+ #+BEGIN_NOTE
159
+ The lines beginning with '#' are explanatory comments
160
+ #+END_NOTE
161
+
162
+ #+BEGIN_SRC yaml
163
+ - :id: my_releasable
164
+ :revision:
165
+ # Source file containing the revision identifier
166
+ # This will also include changelog entries, embedded as comments
167
+ :src: lib/revision/version.rb
168
+ # Regex matching the source revision identifier. Must contain the following named capture groups
169
+ # - major, minor, patch :: Numeric (uint) sequences representing the three revision ID components
170
+ # - sep1, sep2 :: the characters separating the revision components
171
+ # - prefix, postfix :: sufficient syntactic context to match the revision ID uniquely
172
+ # N.B. this regex matches the version ID from the standard bundler gem skeleton,
173
+ # e.g. VERSION = "1.1.0"
174
+ :regex: (?<prefix>VERSION = ")(?<major>\d+)(?<sep1>\.)(?<minor>\d+)(?<sep2>\.)(?<patch>\d+)(?<postfix>")
175
+ # Comment char for the project language -- prefixed to each line of changelog entries
176
+ # Quotes only necessary here to prevent # being interpreted as the beginning of a YAML comment
177
+ :comment_prefix: "#"
178
+ # Sequence of build steps -- each item should be a valid shell command, prefixed with the YAML sequence item token, '- '
179
+ :build_steps:
180
+ - bundle exec rake install
181
+ # Sequence defining the files (build artefacts) to package in the release archive.
182
+ # Each artefact definition must include a :src: key/value pair.
183
+ # An optional :dest: value may be provided to rename the file during packaging, or just (as in the first entry below)
184
+ # to flatten the folder structure.
185
+ # Any <VER> (or <REV>) in the :src: or :dest: placeholders wil be replaced with the current revision ID
186
+ # The revision archive will also include the revision history extracted as a text file
187
+ :artefacts:
188
+ # A binary artefact -- the
189
+ - :src: pkg/revision-<VER>.gem
190
+ :dest: revision-<VER>.gem
191
+ # This document -- the :dest: value defaults to duplicating :src: if not specified
192
+ - :src: README.org
193
+ #+END_SRC
194
+
195
+ **** TODO (or at least consider) add support for overridable defaults
196
+ e.g. via a =.releasables= configuration file in the user home dir.
197
+ Though this would be bad for collaborative development as the config file would live outside source control.
198
+ Possibly useful to support inclusion of a controlled configuration file instead? Primarily to define
199
+ a revision regex and comment prefix for a group of related releasables....
200
+
201
+ *** Examples
202
+ **** C project
203
+
204
+ #+BEGIN_NOTE
205
+ The ~:regex:~ and ~:comment_prefix:~ keys are absent in the C example below. This project started life as
206
+ managing some embedded C projects, and the default values reflect this.
207
+ #+END_NOTE
208
+
209
+ #+BEGIN_SRC yaml
210
+ :releasables:
211
+ - :id: mbt_cd_firmware
212
+ :revision:
213
+ :src: src/FirmwareRevision.c
214
+ :build_steps:
215
+ - make --jobs=8 -f Makefile CONF=bootloadable
216
+ :artefacts:
217
+ - :src: dist/bootloadable/production/firmware.production.hex
218
+ :dest: mbt_cd_firmware_v<REV>.bootloadable.hex
219
+ - :src: dist/default/production/firmware.production.hex
220
+ :dest: mbt_cd_firmware_v<REV>.standalone.hex
221
+ #+END_SRC
222
+
223
+ **** Ruby project
224
+
225
+ #+BEGIN_SRC yaml
226
+ :releasables:
227
+ - :id: revision
228
+ :revision:
229
+ :src: lib/revision/version.rb
230
+ :regex: (?<prefix>VERSION = ")(?<major>\d+)(?<sep1>\.)(?<minor>\d+)(?<sep2>\.)(?<patch>\d+)(?<postfix>")
231
+ :comment_prefix: "#"
232
+ :build_steps:
233
+ - bundle exec rake install
234
+ :artefacts:
235
+ - :src: pkg/revision-<VER>.gem
236
+ #+END_SRC
237
+
238
+ *** Heirarchical project
239
+ Rather than defining the releasable parameters inline, a =releasables.yaml= list entry can contain a (relative or absolute) link to another folder containing it's own =releasables.yaml=.
240
+
241
+ i.e assuming the earlier examples were in folders =examples/c= and =examples/ruby= relative to a common root, a separate =releasables.yaml=
242
+ at that root could include them as follows...
243
+
244
+ #+BEGIN_SRC yaml
245
+ :releasables:
246
+ - :folder: examples/c
247
+ - :folder: examples/ruby
248
+ #+END_SRC
249
+
250
+ **** TODO consider supporting a higher-level aggregate revision ID
251
+
252
+ #+BEGIN_SRC yaml
253
+ :revision:
254
+ :src: release_log.txt
255
+ :releasables:
256
+ - :folder: examples/c
257
+ - :folder: examples/ruby
258
+ #+END_SRC
259
+
260
+ * Development
261
+
262
+ After checking out the repo, run =bin/setup= to install dependencies. Then, run =rake spec= to run the tests. You can also run =bin/console= for an interactive prompt that will allow you to experiment.
263
+
264
+ To install this gem onto your local machine, run =bundle exec rake install=. To release a new version, update the version number in =version.rb=, and then run =bundle exec rake release=, which will create a git tag for the version, push git commits and tags, and push the =.gem= file to [rubygems.org](https://rubygems.org).
265
+
266
+
267
+ * Contributing
268
+
269
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/revision.
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "revision"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/exe/revision ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'revision'
4
+
5
+ Revision::CLI.start(ARGV)
@@ -0,0 +1,101 @@
1
+ require 'thor'
2
+
3
+ require_relative 'releasable'
4
+ require_relative 'info'
5
+ require_relative 'errors'
6
+
7
+ module Revision
8
+ class CLI < Thor
9
+ class_option :dryrun, :type => :boolean, :default =>false
10
+ class_option :id, :default => nil #Initialized after loading definition
11
+
12
+ def initialize(*args)
13
+ super
14
+ #TODO Update this to traverse up the folder heirarchy until we find a releasables.yaml
15
+ wd = Dir.getwd
16
+ loop do
17
+ begin
18
+ @releasables = Releasable.from_folder(wd)
19
+ break
20
+ rescue Errors::NoDefinition
21
+ break if wd == File.expand_path('..',wd)
22
+ puts "No releasable found in #{wd}, trying parent..."
23
+ wd = File.expand_path('..',wd)
24
+ next
25
+ end
26
+ end
27
+ raise 'No definition file found in this directory or its ancestors' if @releasables.nil? || @releasables.empty?
28
+ @id = options[:id] || @releasables.keys[0]
29
+ end
30
+
31
+ desc "info", 'Display info for all defined releasables'
32
+ def info
33
+ puts "Found #{@releasables.values.length} releasables"
34
+ puts ''
35
+ puts @releasables.values.map {|r| r.to_s}.join("\n\n")
36
+ puts ''
37
+ puts "Default releasable ID: #{@id}"
38
+ end
39
+
40
+ desc "patch", "Increment patch revision index"
41
+ def patch
42
+ do_increment('patch')
43
+ end
44
+
45
+ desc "minor", "Increment minor revision index"
46
+ def minor
47
+ do_increment('minor')
48
+ end
49
+
50
+ desc "major", "Increment major revision index"
51
+ def major
52
+ do_increment('major')
53
+ end
54
+
55
+ desc "archive", "Archive releasables"
56
+ def archive
57
+ selected = options[:id].nil? ? @releasables.values : [@releasables[options[:id]]]
58
+ puts "Archiving #{selected.length} releasables..."
59
+ selected.each do |r|
60
+ r.build
61
+ r.archive
62
+ end
63
+ end
64
+
65
+ desc "changelog", "Display change log on stdout"
66
+ def changelog
67
+ select_one.output_changelog($stdout)
68
+ end
69
+
70
+ desc "tag", "Commit the current revision to a local git repo and add a version tag"
71
+ def tag
72
+ select_one.tag
73
+ end
74
+
75
+ private
76
+
77
+ def select_one
78
+ raise "Please specify one of #{@releasables.keys}" if options[:id].nil? && @releasables.keys.length > 1
79
+ @releasables[@id]
80
+ end
81
+
82
+ def do_increment(type)
83
+ r = select_one
84
+ increment_method = "#{type}_increment!"
85
+ say "Incrementing #{r.revision} to #{r.revision.public_send(increment_method)}"
86
+ options[:dryrun] ? r.revision.write_to_file : r.revision.write!
87
+ say ""
88
+ say "The automatic commit / tag step assumes you're only checking in changes to existing files"
89
+ say "You can answer 'n' at the prompt and use 'revision tag' to generate a commit with the latest changelog entry and an associated tag after manually adding any new files to the repo"
90
+ say ""
91
+ if ask("Commit changes to existing files and add a Git tag (y/N)?").upcase=='Y'
92
+ r.tag
93
+ if ask("Push changes/tag to origin (Y/n)?").upcase=='N' || !r.push
94
+ say "To push from the command line, type 'git push --tags' at a shell prompt"
95
+ end
96
+ end
97
+ end
98
+
99
+ end
100
+
101
+ end
@@ -0,0 +1,10 @@
1
+ module Revision
2
+ module Errors
3
+ class Base < StandardError; end
4
+ class NoDefinition < Base
5
+ def initialize(root)
6
+ super("No definition file (#{Releasable::CONFIG_FILE_NAME}) found at root #{root}")
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,137 @@
1
+ require_relative 'releasable'
2
+ require_relative 'string_case'
3
+
4
+
5
+ class Revision::Info
6
+ DEFAULT_REGEX = /(?<prefix>\s*Info\s+const\s+\S+_REVISION\s*=\s*\{\s*\{\s*)(?<major>\d+)(?<sep1>\s*,\s*)(?<minor>\d+)(?<sep2>\s*,\s*)(?<patch>\d+)(?<postfix>\s*\}\s*\};\s*)/
7
+ DEFAULT_COMMENT_PREFIX = ' *'.freeze
8
+ CHANGELOG_START_TAG = '<BEGIN CHANGELOG>'.freeze
9
+ CHANGELOG_END_TAG = '<END CHANGELOG>'.freeze
10
+ CHANGELOG_START = /.*#{CHANGELOG_START_TAG}.*/
11
+ CHANGELOG_END = /.*#{CHANGELOG_END_TAG}.*/
12
+
13
+ attr_accessor :major, :minor, :patch
14
+ attr_accessor :src
15
+ attr_accessor :regex
16
+ attr_accessor :comment_prefix
17
+
18
+ def initialize(file, regex: nil, comment_prefix: nil)
19
+ @src=file
20
+ @regex = regex.nil? ? DEFAULT_REGEX : /#{regex}/
21
+ @comment_prefix = comment_prefix || DEFAULT_COMMENT_PREFIX
22
+ matched = false
23
+ File.open(@src).each_line do |line|
24
+ if line =~ @regex
25
+ @major = Regexp.last_match[:major].to_i
26
+ @minor = Regexp.last_match[:minor].to_i
27
+ @patch = Regexp.last_match[:patch].to_i
28
+ matched = true
29
+ break
30
+ end
31
+ end
32
+ raise "Failed to match against #{@regex}" unless matched
33
+ end
34
+
35
+ def patch_increment!
36
+ @patch += 1
37
+ self
38
+ end
39
+
40
+ def minor_increment!
41
+ @minor += 1
42
+ @patch = 0
43
+ self
44
+ end
45
+
46
+ def major_increment!
47
+ @major += 1
48
+ @minor = 0
49
+ @patch = 0
50
+ self
51
+ end
52
+
53
+ def new_changelog_placeholder
54
+ placeholder = [@comment_prefix,CHANGELOG_START_TAG,CHANGELOG_END_TAG].join("\n#{@comment_prefix} ")
55
+ # Handle C block comments as a special case for legacy project support
56
+ # Could generalise the comment definition, but feels like unnecessary complexity...
57
+ if @src =~ /\.(c|cpp|h|hpp)$/ && !@comment_prefix =~ /#{Regexp.escape('//')}/
58
+ placeholder = "/**\n${placeholder}\n*/"
59
+ end
60
+ placeholder + "\n"
61
+ end
62
+
63
+ def write(output_file_name)
64
+
65
+ ref_info = self.class.new(@src, regex: @regex)
66
+ raise 'No revision identifiers incremented' if ref_info.to_s == self.to_s
67
+
68
+ entry = get_changelog_entry
69
+
70
+ text = File.read(@src)
71
+ text.gsub!(@regex) { |match| "#{$~[:prefix]}#{@major}#{$~[:sep1]}#{@minor}#{$~[:sep2]}#{@patch}#{$~[:postfix]}" }
72
+
73
+ #Insert start/end tags if not present
74
+ text += new_changelog unless text.match(CHANGELOG_START)
75
+
76
+ text.gsub!(CHANGELOG_START) { |match| [match, format_changelog_entry(entry)].join("\n") }
77
+
78
+ File.open(output_file_name, 'w') { |f| f.write(text) }
79
+ end
80
+
81
+ def write!
82
+ write(@src)
83
+ end
84
+
85
+ def to_s
86
+ "#{@major}.#{@minor}.#{@patch}"
87
+ end
88
+
89
+ def strip_comment_prefix(line)
90
+ line.gsub(/^\s*#{Regexp.escape(@comment_prefix)}\s?/,'')
91
+ end
92
+
93
+ def changelog
94
+ in_changelog = false
95
+ File.open(@src).each_line do |line|
96
+ if in_changelog
97
+ break if line =~ CHANGELOG_END
98
+ yield strip_comment_prefix(line.chomp)
99
+ else
100
+ in_changelog = line =~ CHANGELOG_START
101
+ end
102
+ end
103
+ end
104
+
105
+ def last_changelog_entry
106
+ in_entry = false
107
+ lines = []
108
+ changelog do |line|
109
+ if line.length > 0
110
+ in_entry = true
111
+ lines << line
112
+ else
113
+ break if in_entry
114
+ end
115
+ end
116
+ lines
117
+ end
118
+
119
+ # Prefixes the entry with an empty line, then prefixes each line with comment chars
120
+ # and converts the line entries to a single string
121
+ def format_changelog_entry(entry_lines)
122
+ entry_lines.unshift('').map { |line| "#{@comment_prefix} #{line}"}.join("\n")
123
+ end
124
+
125
+ def get_changelog_entry
126
+ entry_lines = []
127
+ entry_lines << "Version #{self} (#{Time.now.strftime("%d %b %Y")})"
128
+ puts('Changelog entry (one item per line / empty line to end)')
129
+ puts('N.B. Git commit entry will use revision ID and first line of entry')
130
+ while line = $stdin.readline.strip
131
+ break if line.length == 0
132
+ entry_lines << "- #{line}"
133
+ end
134
+ entry_lines
135
+ end
136
+
137
+ end
@@ -0,0 +1,152 @@
1
+ require 'pathname'
2
+ require 'yaml'
3
+ require 'zip'
4
+ require 'git'
5
+
6
+ module Revision
7
+
8
+ class Releasable
9
+
10
+ BUILD_DIR_BASE_NAME = "dist"
11
+ BUILD_CONFIGURATION_DEFAULT = "default"
12
+ BUILD_TARGET_DEFAULT = "production"
13
+ # RELATIVE_PATH_TO_BOOTLOADER = File.join("..","bootloader")
14
+ RELATIVE_PATH_TO_BOOTLOADER = "bootloader"
15
+
16
+ CONFIG_FILE_NAME = 'releasables.yaml'.freeze
17
+
18
+ REVISION_PLACEHOLDER = /<REV>|<VER>/
19
+
20
+ attr_reader :root, :id, :revision, :build_steps, :artefacts
21
+
22
+ # Load a file in yaml format containing one or more releasable definitions
23
+ # @param root [String] An optional root directory argument
24
+ # @return [Hash] Contents of the yaml file
25
+ def self.load_definitions(root: nil)
26
+ root ||= Dir.getwd
27
+ config_file = File.join(root, CONFIG_FILE_NAME)
28
+ raise Errors::NoDefinition.new(root) unless File.exist?(config_file)
29
+ puts "Loading releasable definitions from #{config_file} ..."
30
+ YAML.load_file(config_file)
31
+ end
32
+
33
+ # Instantiate the Releasables defined in releasables.yaml
34
+ def self.from_folder(root = Dir.getwd)
35
+ config = load_definitions(:root=>root)
36
+
37
+ releasables = {}
38
+ config[:releasables].each do |config_entry|
39
+ if config_entry[:folder]
40
+ #Load entries from a nested releasable definition file
41
+ releasables = releasables.merge(from_folder(File.join(root, config_entry[:folder])))
42
+ else
43
+ r = new(root: root, config: config_entry)
44
+ releasables[r.id] = r
45
+ end
46
+ end
47
+ releasables
48
+ end
49
+
50
+ def initialize(root: nil, config: {})
51
+
52
+ root ||= Dir.getwd
53
+ @root = Pathname.new(root).realpath
54
+ @id = config[:id] || File.basename(@root)
55
+ @revision = Info.new(File.join(@root,config[:revision][:src]), regex: config[:revision][:regex], comment_prefix: config[:revision][:comment_prefix])
56
+ @build_steps = config[:build_steps]
57
+ @artefacts = config[:artefacts]
58
+ @artefacts.each { |a| a[:dest] ||= a[:src] }
59
+ end
60
+
61
+ def to_s
62
+ <<~EOT
63
+ #{@id} v#{@revision} @ #{@root}
64
+
65
+ Build pipeline:
66
+ - #{@build_steps.join("\n - ")}
67
+
68
+ Build artefacts:
69
+ #{artefacts.map{ |a| "- #{a[:src]}\n => #{a[:dest]}" }.join("\n")}
70
+ EOT
71
+ end
72
+
73
+ def build
74
+ puts "Executing #{@build_steps.length} build steps..."
75
+ Dir.chdir(@root) do
76
+ @build_steps.each_with_index do |step, index|
77
+ puts "... (#{index+1}/#{@build_steps.length}) #{step}"
78
+ system(step)
79
+ puts "WARNING: build step #{index}: #{step} exit status #{$?.exitstatus}" unless $?.exitstatus.zero?
80
+ end
81
+ end
82
+ end
83
+
84
+ def tag
85
+ Dir.chdir(@root) do
86
+ tag_id = "v#{revision}"
87
+ changelog_entry = @revision.last_changelog_entry
88
+ #Insert a blank line between the revision header and release notes, as per git commit best practice
89
+ commit_lines = ["#{tag_id} #{changelog_entry[1]}", '']
90
+ if changelog_entry.length > 2
91
+ commit_lines << "Also..."
92
+ commit_lines += changelog_entry[2..-1]
93
+ end
94
+ commit_message = commit_lines.join("\n")
95
+ g = Git.init
96
+ puts "Committing..."
97
+ puts commit_message
98
+ g.commit_all(commit_message)
99
+ puts "Tagging as #{tag_id}"
100
+ g.add_tag(tag_id)
101
+ end
102
+ end
103
+
104
+ def push
105
+ pushed = false
106
+ Dir.chdir(@root) do
107
+ g = Git.init
108
+ begin
109
+ g.push('origin', g.current_branch, tags: true)
110
+ pushed = true
111
+ rescue GitExecuteError => e
112
+ puts "ERROR :: Cannot push to origin :: #{e}"
113
+ end
114
+ end
115
+ pushed
116
+ end
117
+
118
+ def archive_name
119
+ "#{@id}_v#{@revision}.zip"
120
+ end
121
+
122
+ def changelog_name
123
+ "#{@id}_revision_history_v#{@revision}.txt"
124
+ end
125
+
126
+ def archive
127
+ puts "Packaging #{@artefacts.length} build artefacts as #{archive_name}..."
128
+ if File.exist?(archive_name)
129
+ puts "... deleting existing archive"
130
+ File.delete(archive_name)
131
+ end
132
+ Zip::File.open(archive_name, Zip::File::CREATE) do |zipfile|
133
+ @artefacts.each_with_index do |a, index|
134
+ src = a[:src].gsub(REVISION_PLACEHOLDER, @revision.to_s)
135
+ dest = a[:dest].gsub(REVISION_PLACEHOLDER, @revision.to_s)
136
+ puts "... (#{index+1}/#{@artefacts.length}) #{src} => #{dest}"
137
+ zipfile.add(dest, File.join(@root, src))
138
+ end
139
+ puts "... embedding revision history as #{changelog_name} "
140
+ zipfile.get_output_stream(changelog_name) { |os| output_changelog(os)}
141
+ end
142
+ end
143
+
144
+ def output_changelog(output_stream)
145
+ output_stream.puts "Revision history for #{@id} version #{@revision}"
146
+ output_stream.puts ""
147
+ @revision.changelog {|line| output_stream.puts(line)}
148
+ end
149
+
150
+ end
151
+
152
+ end
@@ -0,0 +1,36 @@
1
+ class String
2
+ # ruby mutation methods have the expectation to return self if a mutation occurred, nil otherwise. (see http://www.ruby-doc.org/core-1.9.3/String.html#method-i-gsub-21)
3
+ def to_underscore!
4
+ gsub!(/::/, '/')
5
+ gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
6
+ gsub!(/([a-z\d])([A-Z])/,'\1_\2')
7
+ tr!("-", "_")
8
+ end
9
+
10
+ ##
11
+ # Converts _SnakeCase_ to _snake_case_
12
+ def to_snake_case!
13
+ to_underscore!
14
+ downcase!
15
+ end
16
+
17
+ ##
18
+ # Converts _ScreamingSnakeCase to _SCREAMING_SNAKE_CASE_
19
+ def to_screaming_snake_case!
20
+ to_underscore!
21
+ upcase!
22
+ end
23
+
24
+ def to_underscore
25
+ dup.tap { |s| s.to_underscore! }
26
+ end
27
+
28
+ def to_snake_case
29
+ dup.tap { |s| s.to_snake_case! }
30
+ end
31
+
32
+ def to_screaming_snake_case
33
+ dup.tap { |s| s.to_screaming_snake_case! }
34
+ end
35
+
36
+ end
@@ -0,0 +1,45 @@
1
+ # Defines the revision ID for the revision gem
2
+ module Revision
3
+ VERSION = "1.1.4"
4
+ end
5
+
6
+ # <BEGIN CHANGELOG>
7
+ #
8
+ # Version 1.1.4 (14 Dec 2017)
9
+ # - Minor message body reformatting
10
+ #
11
+ # Version 1.1.3 (14 Dec 2017)
12
+ # - Eliminated duplication of version ID in commit message body
13
+ #
14
+ # Version 1.1.2 (14 Dec 2017)
15
+ # - Removed redundant ':: ' from commit message headline
16
+ #
17
+ # Version 1.1.1 (14 Dec 2017)
18
+ # - Added git connection failure handling
19
+ # - Revision commit message now includes first line of changelog entry
20
+ # - Updated configuration syntax for consistency (:revision: :file: -> :revision: :src:)
21
+ #
22
+ # Version 1.1.0 (13 Dec 2017)
23
+ # - Updated to optionally push tags to the repo
24
+ #
25
+ # Version 1.0.1 (13 Dec 2017)
26
+ # - Corrected revision placeholder handling when archiving build artefacts
27
+ # - Added proper high-level usage documentation
28
+ #
29
+ # Version 1.0.0 (12 Dec 2017)
30
+ # - First fully functional release with new config file
31
+ #
32
+ # Version 0.1.4 (12 Dec 2017)
33
+ # - boo
34
+ # - hoo
35
+ # - hoo
36
+ #
37
+ # Version 0.1.3 (12 Dec 2017)
38
+ # - boo
39
+ #
40
+ # Version 0.1.2 (12 Dec 2017)
41
+ # - wahoo!
42
+ #
43
+ # Version 0.1.1 (12 Dec 2017)
44
+ # - Wahey!
45
+ # <END CHANGELOG>
data/lib/revision.rb ADDED
@@ -0,0 +1,6 @@
1
+ require "revision/version"
2
+
3
+ module Revision
4
+ require 'revision/cli'
5
+ require 'revision/errors'
6
+ end
data/releasables.yaml ADDED
@@ -0,0 +1,10 @@
1
+ :releasables:
2
+ - :id: revision
3
+ :revision:
4
+ :src: lib/revision/version.rb
5
+ :regex: (?<prefix>VERSION = ")(?<major>\d+)(?<sep1>\.)(?<minor>\d+)(?<sep2>\.)(?<patch>\d+)(?<postfix>")
6
+ :comment_prefix: "#"
7
+ :build_steps:
8
+ - bundle exec rake install
9
+ :artefacts:
10
+ - :src: pkg/revision-<VER>.gem
data/revision.gemspec ADDED
@@ -0,0 +1,40 @@
1
+ lib = File.expand_path("../lib", __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require "revision/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "revision"
7
+ spec.version = Revision::VERSION
8
+ spec.authors = ["Cormac Cannon"]
9
+ spec.email = ["cormac.cannon@neuromoddevices.com"]
10
+
11
+ spec.summary = %q{Language-agnostic revision management tool}
12
+ spec.description = %q{Updates project revision identifiers in software source files and associated change log. Can also build and package project archives as a zip and optionally commit, tag and push to a Git repo.}
13
+ # spec.homepage = "TBC"
14
+
15
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
16
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
17
+ # if spec.respond_to?(:metadata)
18
+ # spec.metadata["allowed_push_host"] = "http://gems.nmd.ie"
19
+ # else
20
+ # raise "RubyGems 2.0 or newer is required to protect against " \
21
+ # "public gem pushes."
22
+ # end
23
+
24
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
25
+ f.match(%r{^(test|spec|features)/})
26
+ end
27
+ spec.bindir = "exe"
28
+ spec.executables = spec.files.grep(%r{^#{spec.bindir}/}) { |f| File.basename(f) }
29
+
30
+ spec.require_paths = ["lib"]
31
+
32
+ spec.add_runtime_dependency 'thor', '~> 0.19.1'
33
+ spec.add_runtime_dependency 'rubyzip'
34
+ spec.add_runtime_dependency 'git'
35
+
36
+ spec.add_development_dependency "bundler", "~> 1.16"
37
+ spec.add_development_dependency "rake", "~> 10.0"
38
+ spec.add_development_dependency "rspec", "~> 3.0"
39
+ spec.add_development_dependency "pry"
40
+ end
metadata ADDED
@@ -0,0 +1,162 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: revision
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.1.4
5
+ platform: ruby
6
+ authors:
7
+ - Cormac Cannon
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2017-12-14 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: thor
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.19.1
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.19.1
27
+ - !ruby/object:Gem::Dependency
28
+ name: rubyzip
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
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: git
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
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: bundler
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.16'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.16'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '10.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '10.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '3.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '3.0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: pry
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ description: Updates project revision identifiers in software source files and associated
112
+ change log. Can also build and package project archives as a zip and optionally
113
+ commit, tag and push to a Git repo.
114
+ email:
115
+ - cormac.cannon@neuromoddevices.com
116
+ executables:
117
+ - revision
118
+ extensions: []
119
+ extra_rdoc_files: []
120
+ files:
121
+ - ".gitignore"
122
+ - ".rspec"
123
+ - ".travis.yml"
124
+ - Gemfile
125
+ - README.org
126
+ - Rakefile
127
+ - bin/console
128
+ - bin/setup
129
+ - exe/revision
130
+ - lib/revision.rb
131
+ - lib/revision/cli.rb
132
+ - lib/revision/errors.rb
133
+ - lib/revision/info.rb
134
+ - lib/revision/releasable.rb
135
+ - lib/revision/string_case.rb
136
+ - lib/revision/version.rb
137
+ - releasables.yaml
138
+ - revision.gemspec
139
+ homepage:
140
+ licenses: []
141
+ metadata: {}
142
+ post_install_message:
143
+ rdoc_options: []
144
+ require_paths:
145
+ - lib
146
+ required_ruby_version: !ruby/object:Gem::Requirement
147
+ requirements:
148
+ - - ">="
149
+ - !ruby/object:Gem::Version
150
+ version: '0'
151
+ required_rubygems_version: !ruby/object:Gem::Requirement
152
+ requirements:
153
+ - - ">="
154
+ - !ruby/object:Gem::Version
155
+ version: '0'
156
+ requirements: []
157
+ rubyforge_project:
158
+ rubygems_version: 2.6.13
159
+ signing_key:
160
+ specification_version: 4
161
+ summary: Language-agnostic revision management tool
162
+ test_files: []