#!/usr/bin/env ruby
#
# Usage: gmd [command]
# gmd <GEMNAME> # create the gemspec
# gmd install # locally install the gem
# gmd release # run the release tasks
#
# Creates a new basic gemspec at GEMNAME.gemspec, based on git-config
# info.
#
# Once this has been checked into a project, you can make use of the
# extra tasks provided by gmd.
#
## HELPERS
# Stolen from DataMapper::Inflector
class String
# some_string => SomeString
def camelize
gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
end
def colour(code)
"\e[#{code}m#{self}\e[0m"
end
def red; colour 31; end
def green; colour 32; end
def amber; colour 91; end
def grey; colour 90; end
def underline; colour 4; end
end
def yay(str)
puts str.green
end
def nay(str)
abort str.red
end
def way(str)
warn str.amber
end
def file?(path)
File.exist?(path)
end
def write(path, str)
File.open(path, 'w') {|f| f.puts str }
yay "Wrote #{path}."
end
# Run +commands+ with +vers+ of ruby specified. Uses rbenv!
def run_for(vers, commands)
system "RBENV_VERSION='#{vers}' sh -c '#{commands.map {|i| "rbenv exec #{i}"}.join(' && ')}'"
end
# http://stackoverflow.com/questions/2108727/which-in-ruby-checking-if-program-exists-in-path-from-ruby
def command?(command)
system "which #{ command} > /dev/null 2>&1"
end
# ask_to "Make sandwich", y: -> { make_sandwich }, n: -> { puts "Make it yourself" }
def ask_to(question, opts={})
print question + "? [y/n]: "
ok = $stdin.gets.chomp
if ok == "y"
opts[:y].call if opts.has_key?(:y)
else
opts[:n].call if opts.has_key?(:n)
end
end
def spec
path = Dir['*.gemspec'][0]
if path
Gem::Specification.load(path)
else
nay "No gemspec found, run `gmd <GEMNAME>` to generate"
end
end
def gem
"#{spec.name}-#{spec.version}.gem"
end
def git_conf(opt)
`git config --get #{opt =~ /\./ ? opt : 'user.' + opt}`.chomp
end
def name
ARGV[0]
end
def gemspec
<<END_OF_SPEC
# -*- encoding: utf-8 -*-
require File.expand_path("../lib/#{name}/version", __FILE__)
Gem::Specification.new do |s|
s.name = "#{name}"
s.author = "#{git_conf 'name'}"
s.email = "#{git_conf 'email'}"
s.summary = "A short summary of what it does."
s.homepage = "http://github.com/#{git_conf 'github.user'}/#{name}"
s.version = #{name.camelize}::VERSION
s.license = 'MIT'
s.description = <<-DESC
A long form description. Nicely indented and wrapped at ~70 chars.
Here's a measuring line for you. (Don't keep this in when releasing.)
----------------------------------------------------------------------
DESC
# s.add_dependency 'some-gem', '~> X.X.X'
# s.add_development_dependency 'some-gem', '~> X.X.X'
s.files = %w(README.md Rakefile LICENCE)
s.files += Dir["{bin,lib,man,test,spec}/**/*"] & `git ls-files`.split("\\n")
s.test_files = Dir["{test,spec}/**/*"] & `git ls-files`.split("\\n")
s.executables = %w(#{name})
end
END_OF_SPEC
end
def rakefile
<<END_OF_RAKE
require 'rake/testtask'
GEM_SPEC = eval(File.read('#{name}.gemspec'))
Rake::TestTask.new(:test) do |t|
t.libs << 'lib' << 'test'
t.pattern = 'test/**/*_test.rb'
t.verbose = true
end
# require 'rspec/core/rake_task'
# RSpec::Core::RakeTask.new(:test)
task :man do
ENV['RONN_ORGANIZATION'] = "\#{GEM_SPEC.name} \#{GEM_SPEC.version}"
sh "ronn -5r -stoc man/*.ronn"
end
task :default => :test
END_OF_RAKE
end
def license
<<END_OF_LICENSE
Copyright (c) #{Date.today.year} #{git_conf 'name'}
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
END_OF_LICENSE
end
## TASKS
def write_gemspec
if file? "#{name}.gemspec"
way "Gemspec already exists!"
else
write "#{name}.gemspec", gemspec
end
end
def write_rakefile
if file? "Rakefile"
way "Rakefile already exists!"
else
write "Rakefile", rakefile
end
end
def write_license
if file? "LICENSE"
way "LICENSE already exists!"
else
write "LICENSE", license
end
end
# If `rbenv` is installed, runs `bunlde install && bundle exec rake` under
# each version. This means you must have a Gemfile with "gem 'rake'" in it.
#
# Otherwise it just runs `rake`.
#
# Both of these assume the default task runs all of the tests.
def run_tests
if file?('Rakefile')
if command?('rbenv')
`rbenv versions --bare`.split("\n").each do |vers|
run_for vers, ['bundle install', 'bundle exec rake']
end
else
`rake`
end
else
nay "Rakefile does not exist, generate with `gmd <GEMNAME>`"
end
end
# Tags the last commit with the version number obtained from the gemspec.
def create_tag
if `git diff --cached`.empty?
if `git tag`.split("\n").include?("v#{spec.version}")
nay "Version #{spec.version} has already been tagged"
end
`git tag v#{spec.version}`
`git push --tags`
`git push`
else
nay "Unstaged changes still waiting to be commited"
end
end
# Builds the gem to 'pkg/'
def build_gem
`gem build #{spec.name}.gemspec`
`mkdir -p pkg`
`mv #{gem} pkg/#{gem}`
yay "Gem built."
end
def install_gem
`gem install pkg/#{gem}`
yay "Gem successfully installed."
end
def push_gem
`gem push pkg/#{gem}`
yay "Gem successfully pushed to rubygems."
end
def help
str = <<EOS
Usage: gmd [command]
gmd <name> # Generates a new gemspec, LICENSE, and Rakefile for a gem
# called <name>. The name should be given lower case with
# underscores, ie. my_cool_gem, it is then assumed this is
# for a class called MyCoolGem.
gmd info # Show info on from the gemspec.
gmd install # Builds then locally installs the gem.
gmd release # Release the gem; runs tests, tags, and pushes gem and git.
gmd test # Runs tests (by calling `rake`) for all installed rubies (if
# rbenv installed, otherwise just calls `rake`)
gmd help # Display this help message
EOS
str.split("\n").map do |l|
a, b = l.split("#")
b ? a + "# #{b}".grey : a
end.join("\n")
end
class InfoPrinter
Data = Struct.new(:key, :value)
def initialize(spec)
@spec = spec
end
def to_s
author = Data.new('author:', @spec.authors.join(', '))
email = Data.new('email: ', @spec.email)
web = Data.new('web: ', @spec.homepage)
dependencies = Data.new('dependencies:', format_dependencies(@spec.dependencies))
dev_dependencies = Data.new('dependencies:', format_dependencies(@spec.development_dependencies))
sections = [author, email, web]
sections += [nil, dependencies] unless @spec.dependencies.empty?
sections += [nil, dev_dependencies] unless @spec.development_dependencies.empty?
sections = sections.map {|s|
s.nil? ? "" : section(s)
}
<<EOS
#{header}
#{sections.join("\n")}
EOS
end
def format_dependencies(arr)
arr.map {|d| "#{d.name}, #{d.requirement}"}
end
def format_list(arr)
arr.map {|val| " - #{val}"}
end
def header
" #{spec.name} (#{spec.version.to_s.underline})"
end
def section(data)
case data.value
when String
" #{data.key.grey} #{data.value}"
when Array
" #{data.key.grey}\n#{format_list(data.value).join("\n")}"
end
end
end
def info
InfoPrinter.new(spec).to_s
end
## DELEGATE
abort "Need to supply an argument or command.".red + "\n" + help unless ARGV[0]
case ARGV[0]
when 'help'
puts help
when 'info'
puts info
when 'install'
build_gem
install_gem
when 'release'
run_tests if file?('Rakefile')
ask_to "OK to publish", :n => proc { exit }
ask_to "Create tag for #{spec.name} v#{spec.version}", :y => proc { create_tag }
build_gem
push_gem
when 'test'
run_tests
else
write_gemspec
write_rakefile
end
# A reference of the spec
# (o = optional, ? = required, but sane default set, - = required, X = do not set)
#
# - name String (required)
# - summary String (required)
# - version String (required)
# ? date Time (required, default = "Time.now")
# o author String (optional)
# o authors Array[String] (optional)
# o bindir String (optional, default = "bin")
# o default_exectable String (optional)
# o add_dependency (optional)
# o add_development_dependency (optional)
# o description String (optional)
# o email String/Array[String] (optional)
# o executables Array (optional)
# o extensions Array (optional)
# o extra_rdoc_files Array (optional)
# o files Array (optional)
# o has_rdoc Boolean (optional, default = false)
# o homepage String (optional)
# o license String (optional)
# ? platform String (required, default = "Gem::Platform::Ruby")
# o rdoc_options Array (optional, default = [])
# ? require_paths Array (required, default = ["lib"])
# o required_ruby_version Gem::Version::Requirement (optional, default = "> 0.0.0")
# o requirements Array (optional, default = [])
# o rubyforge_project String (optional)
# X rubygems_version String (do not set, done automatically!)
# X specification_version Integer (do not set, done automatically!)
# o test_files Array (optional, default = [])
#