lib/metasploit/version/cli.rb
#
# Standard library
#
require 'pathname'
#
# Gems
#
require 'thor'
#
# Project
#
require 'metasploit/version'
require 'metasploit/version/version'
# Command-line interface for `metasploit-version`. Used to run commands for managing the semantic version of a project.
class Metasploit::Version::CLI < Thor
include Thor::Actions
#
# CONSTANTS
#
# Name of this gem, for use in other projects that call `metasploit-version install`.
GEM_NAME = 'metasploit-version'
# Matches pre-existing development dependency on metasploit-version for updating.
DEVELOPMENT_DEPENDENCY_REGEXP = /spec\.add_development_dependency\s+(?<quote>"|')#{GEM_NAME}\k<quote>/
#
# Class options
#
class_option :force,
default: false,
desc: 'Force overwriting conflicting files',
type: :boolean
class_option :skip,
default: false,
desc: 'Skip conflicting files',
type: :boolean
#
# Configuration
#
root = Pathname.new(__FILE__).parent.parent.parent.parent
source_root root.join('app', 'templates')
#
# Commands
#
desc 'install',
'Install metasploit-version and sets up files'
long_desc(
"Adds 'metasploit-version' as a development dependency in this project's gemspec OR updates the semantic version requirement; " \
"adds semantic versioning version.rb file."
)
option :major,
banner: 'MAJOR',
default: 0,
desc: 'Major version number',
type: :numeric
option :minor,
banner: 'MINOR',
default: 0,
desc: 'Minor version number, scoped to MAJOR version number.',
type: :numeric
option :patch,
banner: 'PATCH',
default: 1,
desc: 'Patch version number, scoped to MAJOR and MINOR version numbers.',
type: :numeric
option :bundle_install,
default: true,
desc: '`bundle install` after adding `metasploit-version` as a development dependency so you can ' \
'immediately run `rake spec` afterwards. Use `--no-bundle-install` if you want to add other gems to ' \
'the gemspec or Gemfile before installing or you\'re just rerunning install to update the templated ' \
'files and the dependencies are already in your bundle.',
type: :boolean
option :github_owner,
default: 'rapid7',
desc: 'The owner of the github repo for this gem. Used to generate links in CONTRIBUTING.md',
type: :string
option :ruby_versions,
default: ['jruby', 'ruby-2.1'],
desc: 'Ruby versions that the gem should be released for on rubygems.org as part of CONTRIBUTING.md',
type: :array
# Adds 'metasploit-version' as a development dependency in this project's gemspec.
#
# @return [void]
def install
ensure_development_dependency
template('lib/versioned/version.rb.tt', "lib/#{namespaced_path}/version.rb")
install_bundle
template('CHANGELOG.md.tt', 'CHANGELOG.md')
template('CONTRIBUTING.md.tt', 'CONTRIBUTING.md')
template('RELEASING.md.tt', 'RELEASING.md')
template('UPGRADING.md.tt', 'UPGRADING.md')
setup_rspec
end
private
# Capitalizes words by converting the first character of `word` to upper case.
#
# @param word [String] a lower case string
# @return [String]
def capitalize(word)
word[0, 1].upcase + word[1 .. -1]
end
# The line injected into the gemspec by {#ensure_development_dependency}
#
# @return [String]
def development_dependency_line
" spec.add_development_dependency '#{GEM_NAME}', '#{version_requirement}'\n"
end
# Ensures that the {#gemspec_path} contains a development dependency on {GEM_NAME}.
#
# Adds `spec.add_development_dependency 'metasploit_version', '~> <semantic version requirement>'` if {#gemspec_path}
# does not have such an entry. Otherwise, updates the `<semantic version requirement>` to match this version of
# `metasploit-version`.
#
# @return [void]
# @raise (see #gemspec_path)
def ensure_development_dependency
path = gemspec_path
gem_specification = Gem::Specification.load(path)
metasploit_version = gem_specification.dependencies.find { |dependency|
dependency.name == GEM_NAME
}
lines = []
if metasploit_version
if metasploit_version.requirements_list.include? '>= 0'
shell.say "Adding #{GEM_NAME} as a development dependency to "
else
shell.say "Updating #{GEM_NAME} requirements in "
end
shell.say path
File.open(path) do |f|
f.each_line do |line|
match = line.match(DEVELOPMENT_DEPENDENCY_REGEXP)
if match
lines << development_dependency_line
else
lines << line
end
end
end
else
end_index = nil
lines = []
open(path) do |f|
line_index = 0
f.each_line do |line|
lines << line
if line =~ /^\s*end\s*$/
end_index = line_index
end
line_index += 1
end
end
lines.insert(end_index, development_dependency_line)
end
File.open(path, 'w') do |f|
lines.each do |line|
f.write(line)
end
end
end
# The name of the gemspec in the current working directory.
#
# @return [String] relative path to the current working directory's gemspec.
# @raise [SystemExit] if no gemspec is found
def gemspec_path
unless instance_variable_defined? :@gemspec
path = "#{name}.gemspec"
unless File.exist?(path)
shell.say 'No gemspec found'
exit 1
end
@gemspec_path = path
end
@gemspec_path
end
# The URL of the github repository. Used to calculate the fork and issues URL in `CONTRIBUTING.md`.
#
# @return [String] https url to github repository
def github_url
@github_url ||= "https://github.com/#{options[:github_owner]}/#{name}"
end
# `bundle install` if the :bundle_install options is `true`
#
# @return [void]
def install_bundle
if options[:bundle_install]
system('bundle', 'install')
end
end
# The name of the gem.
#
# @return [String] name of the gem. Assumed to be the name of the pwd as it should match the repository name.
def name
@name ||= File.basename(Dir.pwd)
end
# The fully-qualified namespace for the gem.
#
# @param [String]
def namespace_name
@namespace_name ||= namespaces.join('::')
end
# List of `Module#name`s making up the {#namespace_name the fully-qualifed namespace for the gem}.
#
# @return [Array<String>]
def namespaces
unless instance_variable_defined? :@namespaces
underscored_words = name.split('_')
capitalized_underscored_words = underscored_words.map { |underscored_word|
capitalize(underscored_word)
}
capitalized_hyphenated_name = capitalized_underscored_words.join
hyphenated_words = capitalized_hyphenated_name.split('-')
@namespaces = hyphenated_words.map { |hyphenated_word|
capitalize(hyphenated_word)
}
end
@namespaces
end
# The relative path of the gem under `lib`.
#
# @return [String] Format of `[<parent>/]*<child>`
def namespaced_path
@namespaced_path ||= name.tr('-', '/')
end
# The prerelease version.
#
# @return [nil] if on master or HEAD
# @return [String] if on a branch
def prerelease
unless instance_variable_defined? :@prerelease
branch = Metasploit::Version::Branch.current
parsed = Metasploit::Version::Branch.parse(branch)
if parsed.is_a? Hash
prerelease = parsed[:prerelease]
if prerelease
@prerelease = prerelease
end
end
end
@prerelease
end
# Generates `.rspec`, `Rakefile`, `version_spec.rb`, `<namespace>_spec.rb` and `spec/spec_helper.rb`
#
# @return [void]
def setup_rspec
template('.rspec.tt', '.rspec')
template('Rakefile.tt', 'Rakefile')
template('spec/lib/versioned/version_spec.rb.tt', "spec/#{version_path.sub(/\.rb$/, '_spec.rb')}")
template('spec/lib/versioned_spec.rb.tt', "spec/lib/#{namespaced_path}_spec.rb")
template('spec/spec_helper.rb.tt', 'spec/spec_helper.rb')
end
# Path to the `version.rb` for the gem.
#
# @return [String]
def version_path
@version_path ||= "lib/#{namespaced_path}/version.rb"
end
# The version requirement on the `metasploit-version` development dependency in {#development_dependency_line}.
#
# @return [String]
def version_requirement
if defined? Metasploit::Version::Version::PRERELEASE
# require exactly this pre-release in case there are multiple prereleases for the same
# version number due to parallel branches.
"= #{Metasploit::Version::GEM_VERSION}"
elsif Metasploit::Version::Version::MAJOR < 1
# can only allow the PATCH to wiggle pre-1.0.0
"~> #{Metasploit::Version::Version::MAJOR}.#{Metasploit::Version::Version::MINOR}.#{Metasploit::Version::Version::PATCH}"
else
# can allow the MINOR to wiggle 1.0.0+
"~> #{Metasploit::Version::Version::MAJOR}.#{Metasploit::Version::Version::MINOR}"
end
end
end