Filled out the project, added Ezmlm module + spec.
This commit is contained in:
parent
4b12c97f6b
commit
00d3974363
15 changed files with 1548 additions and 0 deletions
29
LICENSE
Normal file
29
LICENSE
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
Copyright (c) 2008, LAIKA, Inc.
|
||||||
|
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without modification, are
|
||||||
|
permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
* Redistributions in binary form must reproduce the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer in the documentation and/or
|
||||||
|
other materials provided with the distribution.
|
||||||
|
|
||||||
|
* Neither the name of LAIKA, nor the names of its contributors may be used to
|
||||||
|
endorse or promote products derived from this software without specific prior
|
||||||
|
written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||||
|
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||||
|
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||||
|
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||||
|
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||||
|
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
45
README
Normal file
45
README
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
= ThingFish
|
||||||
|
|
||||||
|
== Authors
|
||||||
|
|
||||||
|
* Michael Granger <mgranger@laika.com>
|
||||||
|
* Jeremiah Jordan <jjordan@laika.com>
|
||||||
|
|
||||||
|
== Installation
|
||||||
|
|
||||||
|
=== Requirements
|
||||||
|
|
||||||
|
Ruby-Ezmlm is tested using Ruby 1.8.6:
|
||||||
|
|
||||||
|
* Ruby (>= 1.8.6): http://www.ruby-lang.org/en/downloads/
|
||||||
|
|
||||||
|
Other versions may work, but are not tested.
|
||||||
|
|
||||||
|
=== Ruby Modules
|
||||||
|
|
||||||
|
There are two ways you can install Ruby-Ezmlm. The easiest is to use RubyGems:
|
||||||
|
|
||||||
|
$ sudo gem install -y ruby-ezmlm
|
||||||
|
|
||||||
|
If you'd rather install from source, you'll need these:
|
||||||
|
|
||||||
|
* Tmail (>= 1.2.3.1): http://tmail.rubyforge.org/
|
||||||
|
|
||||||
|
Once these are installed:
|
||||||
|
|
||||||
|
$ tar -zxvf ezmlm-0.0.1.tgz
|
||||||
|
$ cd thingfish-0.0.1
|
||||||
|
$ su -
|
||||||
|
# rake install
|
||||||
|
|
||||||
|
If you want to help out with development, run tests, or generate documentation,
|
||||||
|
you'll need a few more things:
|
||||||
|
|
||||||
|
* RSpec (>= 1.0.5): http://rspec.rubyforge.org/
|
||||||
|
* rcov (>= 0.7.0): http://eigenclass.org/hiki.rb?rcov
|
||||||
|
|
||||||
|
If you have RubyGems installed, you can install these automatically via the
|
||||||
|
+install_dependencies+ task of the Rakefile:
|
||||||
|
|
||||||
|
$ sudo rake install_dependencies
|
||||||
|
|
||||||
146
Rakefile
Normal file
146
Rakefile
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
#!rake -*- ruby -*-
|
||||||
|
#
|
||||||
|
# Ruby-Ezmlm rakefile
|
||||||
|
#
|
||||||
|
# Based on Ben Bleything's Rakefile for Linen (URL?)
|
||||||
|
#
|
||||||
|
# Copyright (c) 2007 LAIKA, Inc.
|
||||||
|
#
|
||||||
|
# Mistakes:
|
||||||
|
# * Michael Granger <mgranger@laika.com>
|
||||||
|
#
|
||||||
|
|
||||||
|
BEGIN {
|
||||||
|
require 'pathname'
|
||||||
|
basedir = Pathname.new( __FILE__ ).dirname
|
||||||
|
libdir = basedir + 'lib'
|
||||||
|
|
||||||
|
$LOAD_PATH.unshift( libdir.to_s ) unless $LOAD_PATH.include?( libdir.to_s )
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
require 'rubygems'
|
||||||
|
|
||||||
|
require 'rake'
|
||||||
|
require 'tmpdir'
|
||||||
|
require 'pathname'
|
||||||
|
|
||||||
|
$dryrun = false
|
||||||
|
|
||||||
|
# Pathname constants
|
||||||
|
BASEDIR = Pathname.new( __FILE__ ).expand_path.dirname.relative_path_from( Pathname.getwd )
|
||||||
|
BINDIR = BASEDIR + 'bin'
|
||||||
|
LIBDIR = BASEDIR + 'lib'
|
||||||
|
DOCSDIR = BASEDIR + 'docs'
|
||||||
|
VARDIR = BASEDIR + 'var'
|
||||||
|
WWWDIR = VARDIR + 'www'
|
||||||
|
MANUALDIR = DOCSDIR + 'manual'
|
||||||
|
RDOCDIR = DOCSDIR + 'rdoc'
|
||||||
|
STATICWWWDIR = WWWDIR + 'static'
|
||||||
|
PKGDIR = BASEDIR + 'pkg'
|
||||||
|
ARTIFACTS_DIR = Pathname.new( ENV['CC_BUILD_ARTIFACTS'] || '' )
|
||||||
|
RAKE_TASKDIR = BASEDIR + 'rake'
|
||||||
|
|
||||||
|
TEXT_FILES = %w( Rakefile README LICENSE ).
|
||||||
|
collect {|filename| BASEDIR + filename }
|
||||||
|
|
||||||
|
SPECDIR = BASEDIR + 'spec'
|
||||||
|
SPEC_FILES = Pathname.glob( SPECDIR + '**/*_spec.rb' ).
|
||||||
|
delete_if {|item| item =~ /\.svn/ }
|
||||||
|
# Ideally, this should be automatically generated.
|
||||||
|
SPEC_EXCLUDES = 'spec,monkeypatches,/Library/Ruby,/var/lib,/usr/local/lib'
|
||||||
|
|
||||||
|
BIN_FILES = Pathname.glob( BINDIR + '*').
|
||||||
|
delete_if {|item| item =~ /\.svn/ }
|
||||||
|
LIB_FILES = Pathname.glob( LIBDIR + '**/*.rb').
|
||||||
|
delete_if {|item| item =~ /\.svn/ }
|
||||||
|
|
||||||
|
RELEASE_FILES = BIN_FILES + TEXT_FILES + LIB_FILES + SPEC_FILES
|
||||||
|
|
||||||
|
require RAKE_TASKDIR + 'helpers.rb'
|
||||||
|
|
||||||
|
### Package constants
|
||||||
|
PKG_NAME = 'ruby-ezmlm'
|
||||||
|
PKG_VERSION_FROM = LIBDIR + 'ezmlm.rb'
|
||||||
|
PKG_VERSION = find_pattern_in_file( /VERSION = '(\d+\.\d+\.\d+)'/, PKG_VERSION_FROM ).first
|
||||||
|
PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
|
||||||
|
|
||||||
|
RELEASE_NAME = "REL #{PKG_VERSION}"
|
||||||
|
|
||||||
|
require RAKE_TASKDIR + 'svn.rb'
|
||||||
|
require RAKE_TASKDIR + 'verifytask.rb'
|
||||||
|
|
||||||
|
if Rake.application.options.trace
|
||||||
|
$trace = true
|
||||||
|
log "$trace is enabled"
|
||||||
|
end
|
||||||
|
|
||||||
|
if Rake.application.options.dryrun
|
||||||
|
$dryrun = true
|
||||||
|
log "$dryrun is enabled"
|
||||||
|
Rake.application.options.dryrun = false
|
||||||
|
end
|
||||||
|
|
||||||
|
### Project Gemspec
|
||||||
|
GEMSPEC = Gem::Specification.new do |gem|
|
||||||
|
pkg_build = get_svn_rev( BASEDIR ) || 0
|
||||||
|
|
||||||
|
gem.name = PKG_NAME
|
||||||
|
gem.version = "%s.%s" % [ PKG_VERSION, pkg_build ]
|
||||||
|
|
||||||
|
gem.summary = "A Ruby programmatic interface to ezmlm-idx"
|
||||||
|
gem.description = "Ruby-Ezmlm is a Ruby programmatic interface to ezmlm-idx " +
|
||||||
|
"mailing lists, message archives, and command-line tools."
|
||||||
|
|
||||||
|
gem.authors = "Michael Granger, Jeremiah Jordan"
|
||||||
|
gem.email = "opensource@laika.com"
|
||||||
|
gem.homepage = "http://opensource.laika.com/wiki/ruby-ezmlm"
|
||||||
|
|
||||||
|
gem.rubyforge_project = 'laika'
|
||||||
|
|
||||||
|
gem.has_rdoc = true
|
||||||
|
|
||||||
|
gem.files = RELEASE_FILES.
|
||||||
|
collect {|f| f.relative_path_from(BASEDIR).to_s }
|
||||||
|
gem.test_files = SPEC_FILES.
|
||||||
|
collect {|f| f.relative_path_from(BASEDIR).to_s }
|
||||||
|
|
||||||
|
gem.add_dependency( 'tmail', '>= 1.2.3.1' )
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# Load task plugins
|
||||||
|
Pathname.glob( RAKE_TASKDIR + '*.rb' ).each do |tasklib|
|
||||||
|
trace "Loading task lib #{tasklib}"
|
||||||
|
require tasklib
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
### Default task
|
||||||
|
task :default => [:clean, :spec, 'coverage:verify', :package]
|
||||||
|
|
||||||
|
|
||||||
|
### Task: clean
|
||||||
|
desc "Clean pkg, coverage, and rdoc; remove .bak files"
|
||||||
|
task :clean => [ :clobber_rdoc, :clobber_package, :clobber_coverage ] do
|
||||||
|
files = FileList['**/*.bak']
|
||||||
|
files.clear_exclude
|
||||||
|
File.rm( files ) unless files.empty?
|
||||||
|
FileUtils.rm_rf( 'artifacts' )
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
### Cruisecontrol task
|
||||||
|
desc "Cruisecontrol build"
|
||||||
|
task :cruise => [:clean, :coverage, :package] do |task|
|
||||||
|
raise "Artifacts dir not set." if ARTIFACTS_DIR.to_s.empty?
|
||||||
|
artifact_dir = ARTIFACTS_DIR.cleanpath
|
||||||
|
artifact_dir.mkpath
|
||||||
|
|
||||||
|
$stderr.puts "Copying coverage stats..."
|
||||||
|
FileUtils.cp_r( 'coverage', artifact_dir )
|
||||||
|
|
||||||
|
$stderr.puts "Copying packages..."
|
||||||
|
FileUtils.cp_r( FileList['pkg/*'].to_a, artifact_dir )
|
||||||
|
end
|
||||||
|
|
||||||
61
lib/ezmlm.rb
Normal file
61
lib/ezmlm.rb
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
#!/usr/bin/ruby
|
||||||
|
#
|
||||||
|
# A Ruby programmatic interface to the ezmlm-idx mailing list system
|
||||||
|
#
|
||||||
|
# == Version
|
||||||
|
#
|
||||||
|
# $Id$
|
||||||
|
#
|
||||||
|
# == Authors
|
||||||
|
#
|
||||||
|
# * Michael Granger <mgranger@laika.com>
|
||||||
|
# * Jeremiah Jordan <jjordan@laika.com>
|
||||||
|
#
|
||||||
|
# :include: LICENSE
|
||||||
|
#
|
||||||
|
#---
|
||||||
|
#
|
||||||
|
# Please see the file LICENSE in the base directory for licensing details.
|
||||||
|
#
|
||||||
|
|
||||||
|
require 'pathname'
|
||||||
|
|
||||||
|
|
||||||
|
### Toplevel namespace module
|
||||||
|
module Ezmlm
|
||||||
|
|
||||||
|
# SVN Revision
|
||||||
|
SVNRev = %q$Rev$
|
||||||
|
|
||||||
|
# SVN Id
|
||||||
|
SVNId = %q$Id$
|
||||||
|
|
||||||
|
# Package version
|
||||||
|
VERSION = '0.0.1'
|
||||||
|
|
||||||
|
|
||||||
|
require 'ezmlm/list'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
###############
|
||||||
|
module_function
|
||||||
|
###############
|
||||||
|
|
||||||
|
|
||||||
|
### Iterate over each directory that looks like an Ezmlm list in the specified +listsdir+ and
|
||||||
|
### yield it as an Ezmlm::List object.
|
||||||
|
def each_list( listsdir )
|
||||||
|
listsdir = Pathname.new( listsdir )
|
||||||
|
Pathname.glob( listsdir + '*' ) do |entry|
|
||||||
|
next unless entry.directory?
|
||||||
|
next unless ( entry + 'mailinglist' ).exist?
|
||||||
|
|
||||||
|
yield( Ezmlm::List.new(entry) )
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
end # module Ezmlm
|
||||||
|
|
||||||
|
# vim: set nosta noet ts=4 sw=4:
|
||||||
30
lib/ezmlm/list.rb
Normal file
30
lib/ezmlm/list.rb
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
#!/usr/bin/ruby
|
||||||
|
#
|
||||||
|
# A Ruby interface to a single Ezmlm-idx mailing list directory
|
||||||
|
#
|
||||||
|
# == Version
|
||||||
|
#
|
||||||
|
# $Id$
|
||||||
|
#
|
||||||
|
# == Authors
|
||||||
|
#
|
||||||
|
# * Michael Granger <mgranger@laika.com>
|
||||||
|
# * Jeremiah Jordan <jjordan@laika.com>
|
||||||
|
#
|
||||||
|
# :include: LICENSE
|
||||||
|
#
|
||||||
|
#---
|
||||||
|
#
|
||||||
|
# Please see the file LICENSE in the base directory for licensing details.
|
||||||
|
#
|
||||||
|
|
||||||
|
require 'pathname'
|
||||||
|
require 'ezmlm'
|
||||||
|
|
||||||
|
|
||||||
|
### A Ruby interface to an ezmlm-idx mailing list directory
|
||||||
|
class Ezmlm::List
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
# vim: set nosta noet ts=4 sw=4:
|
||||||
353
rake/helpers.rb
Normal file
353
rake/helpers.rb
Normal file
|
|
@ -0,0 +1,353 @@
|
||||||
|
#####################################################################
|
||||||
|
### G L O B A L H E L P E R F U N C T I O N S
|
||||||
|
#####################################################################
|
||||||
|
|
||||||
|
require 'pathname'
|
||||||
|
require 'readline'
|
||||||
|
require 'open3'
|
||||||
|
|
||||||
|
# Set some ANSI escape code constants (Shamelessly stolen from Perl's
|
||||||
|
# Term::ANSIColor by Russ Allbery <rra@stanford.edu> and Zenin <zenin@best.com>
|
||||||
|
ANSI_ATTRIBUTES = {
|
||||||
|
'clear' => 0,
|
||||||
|
'reset' => 0,
|
||||||
|
'bold' => 1,
|
||||||
|
'dark' => 2,
|
||||||
|
'underline' => 4,
|
||||||
|
'underscore' => 4,
|
||||||
|
'blink' => 5,
|
||||||
|
'reverse' => 7,
|
||||||
|
'concealed' => 8,
|
||||||
|
|
||||||
|
'black' => 30, 'on_black' => 40,
|
||||||
|
'red' => 31, 'on_red' => 41,
|
||||||
|
'green' => 32, 'on_green' => 42,
|
||||||
|
'yellow' => 33, 'on_yellow' => 43,
|
||||||
|
'blue' => 34, 'on_blue' => 44,
|
||||||
|
'magenta' => 35, 'on_magenta' => 45,
|
||||||
|
'cyan' => 36, 'on_cyan' => 46,
|
||||||
|
'white' => 37, 'on_white' => 47
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
CLEAR_TO_EOL = "\e[K"
|
||||||
|
CLEAR_CURRENT_LINE = "\e[2K"
|
||||||
|
|
||||||
|
|
||||||
|
### Output a logging message
|
||||||
|
def log( *msg )
|
||||||
|
output = colorize( msg.flatten.join(' '), 'cyan' )
|
||||||
|
$deferr.puts( output )
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
### Output a logging message if tracing is on
|
||||||
|
def trace( *msg )
|
||||||
|
return unless $trace
|
||||||
|
output = colorize( msg.flatten.join(' '), 'yellow' )
|
||||||
|
$deferr.puts( output )
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
### Run the specified command +cmd+ with system(), failing if the execution
|
||||||
|
### fails.
|
||||||
|
def run( *cmd )
|
||||||
|
cmd.flatten!
|
||||||
|
|
||||||
|
log( cmd.collect {|part| part =~ /\s/ ? part.inspect : part} )
|
||||||
|
if $dryrun
|
||||||
|
$deferr.puts "(dry run mode)"
|
||||||
|
else
|
||||||
|
system( *cmd )
|
||||||
|
unless $?.success?
|
||||||
|
fail "Command failed: [%s]" % [cmd.join(' ')]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
### Open a pipe to a process running the given +cmd+ and call the given block with it.
|
||||||
|
def pipeto( *cmd )
|
||||||
|
$DEBUG = true
|
||||||
|
|
||||||
|
cmd.flatten!
|
||||||
|
log( "Opening a pipe to: ", cmd.collect {|part| part =~ /\s/ ? part.inspect : part} )
|
||||||
|
if $dryrun
|
||||||
|
$deferr.puts "(dry run mode)"
|
||||||
|
else
|
||||||
|
open( '|-', 'w+' ) do |io|
|
||||||
|
|
||||||
|
# Parent
|
||||||
|
if io
|
||||||
|
yield( io )
|
||||||
|
|
||||||
|
# Child
|
||||||
|
else
|
||||||
|
exec( *cmd )
|
||||||
|
fail "Command failed: [%s]" % [cmd.join(' ')]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
### Download the file at +sourceuri+ via HTTP and write it to +targetfile+.
|
||||||
|
def download( sourceuri, targetfile )
|
||||||
|
oldsync = $defout.sync
|
||||||
|
$defout.sync = true
|
||||||
|
require 'net/http'
|
||||||
|
require 'uri'
|
||||||
|
|
||||||
|
targetpath = Pathname.new( targetfile )
|
||||||
|
|
||||||
|
log "Downloading %s to %s" % [sourceuri, targetfile]
|
||||||
|
targetpath.open( File::WRONLY|File::TRUNC|File::CREAT, 0644 ) do |ofh|
|
||||||
|
|
||||||
|
url = URI.parse( sourceuri )
|
||||||
|
downloaded = false
|
||||||
|
limit = 5
|
||||||
|
|
||||||
|
until downloaded or limit.zero?
|
||||||
|
Net::HTTP.start( url.host, url.port ) do |http|
|
||||||
|
req = Net::HTTP::Get.new( url.path )
|
||||||
|
|
||||||
|
http.request( req ) do |res|
|
||||||
|
if res.is_a?( Net::HTTPSuccess )
|
||||||
|
log "Downloading..."
|
||||||
|
res.read_body do |buf|
|
||||||
|
ofh.print( buf )
|
||||||
|
end
|
||||||
|
downloaded = true
|
||||||
|
puts "done."
|
||||||
|
|
||||||
|
elsif res.is_a?( Net::HTTPRedirection )
|
||||||
|
url = URI.parse( res['location'] )
|
||||||
|
log "...following redirection to: %s" % [ url ]
|
||||||
|
limit -= 1
|
||||||
|
sleep 0.2
|
||||||
|
next
|
||||||
|
|
||||||
|
else
|
||||||
|
res.error!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return targetpath
|
||||||
|
ensure
|
||||||
|
$defout.sync = oldsync
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
### Return the fully-qualified path to the specified +program+ in the PATH.
|
||||||
|
def which( program )
|
||||||
|
ENV['PATH'].split(/:/).
|
||||||
|
collect {|dir| Pathname.new(dir) + program }.
|
||||||
|
find {|path| path.exist? && path.executable? }
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
### Create a string that contains the ANSI codes specified and return it
|
||||||
|
def ansi_code( *attributes )
|
||||||
|
attributes.flatten!
|
||||||
|
attributes.collect! {|at| at.to_s }
|
||||||
|
# $deferr.puts "Returning ansicode for TERM = %p: %p" %
|
||||||
|
# [ ENV['TERM'], attributes ]
|
||||||
|
return '' unless /(?:vt10[03]|xterm(?:-color)?|linux|screen)/i =~ ENV['TERM']
|
||||||
|
attributes = ANSI_ATTRIBUTES.values_at( *attributes ).compact.join(';')
|
||||||
|
|
||||||
|
# $deferr.puts " attr is: %p" % [attributes]
|
||||||
|
if attributes.empty?
|
||||||
|
return ''
|
||||||
|
else
|
||||||
|
return "\e[%sm" % attributes
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
### Colorize the given +string+ with the specified +attributes+ and return it, handling
|
||||||
|
### line-endings, color reset, etc.
|
||||||
|
def colorize( *args )
|
||||||
|
string = ''
|
||||||
|
|
||||||
|
if block_given?
|
||||||
|
string = yield
|
||||||
|
else
|
||||||
|
string = args.shift
|
||||||
|
end
|
||||||
|
|
||||||
|
ending = string[/(\s)$/] || ''
|
||||||
|
string = string.rstrip
|
||||||
|
|
||||||
|
return ansi_code( args.flatten ) + string + ansi_code( 'reset' ) + ending
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
### Output the specified <tt>msg</tt> as an ANSI-colored error message
|
||||||
|
### (white on red).
|
||||||
|
def error_message( msg, details='' )
|
||||||
|
$deferr.puts colorize( 'bold', 'white', 'on_red' ) { msg } + details
|
||||||
|
end
|
||||||
|
alias :error :error_message
|
||||||
|
|
||||||
|
|
||||||
|
### Highlight and embed a prompt control character in the given +string+ and return it.
|
||||||
|
def make_prompt_string( string )
|
||||||
|
return CLEAR_CURRENT_LINE + colorize( 'bold', 'green' ) { string + ' ' }
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
### Output the specified <tt>prompt_string</tt> as a prompt (in green) and
|
||||||
|
### return the user's input with leading and trailing spaces removed. If a
|
||||||
|
### test is provided, the prompt will repeat until the test returns true.
|
||||||
|
### An optional failure message can also be passed in.
|
||||||
|
def prompt( prompt_string, failure_msg="Try again." ) # :yields: response
|
||||||
|
prompt_string.chomp!
|
||||||
|
prompt_string << ":" unless /\W$/.match( prompt_string )
|
||||||
|
response = nil
|
||||||
|
|
||||||
|
begin
|
||||||
|
prompt = make_prompt_string( prompt_string )
|
||||||
|
response = Readline.readline( prompt ) || ''
|
||||||
|
response.strip!
|
||||||
|
if block_given? && ! yield( response )
|
||||||
|
error_message( failure_msg + "\n\n" )
|
||||||
|
response = nil
|
||||||
|
end
|
||||||
|
end while response.nil?
|
||||||
|
|
||||||
|
return response
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
### Prompt the user with the given <tt>prompt_string</tt> via #prompt,
|
||||||
|
### substituting the given <tt>default</tt> if the user doesn't input
|
||||||
|
### anything. If a test is provided, the prompt will repeat until the test
|
||||||
|
### returns true. An optional failure message can also be passed in.
|
||||||
|
def prompt_with_default( prompt_string, default, failure_msg="Try again." )
|
||||||
|
response = nil
|
||||||
|
|
||||||
|
begin
|
||||||
|
response = prompt( "%s [%s]" % [ prompt_string, default ] )
|
||||||
|
response = default if response.empty?
|
||||||
|
|
||||||
|
if block_given? && ! yield( response )
|
||||||
|
error_message( failure_msg + "\n\n" )
|
||||||
|
response = nil
|
||||||
|
end
|
||||||
|
end while response.nil?
|
||||||
|
|
||||||
|
return response
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
### Display a description of a potentially-dangerous task, and prompt
|
||||||
|
### for confirmation. If the user answers with anything that begins
|
||||||
|
### with 'y', yield to the block, else raise with an error.
|
||||||
|
def ask_for_confirmation( description )
|
||||||
|
puts description
|
||||||
|
|
||||||
|
answer = prompt_with_default( "Continue?", 'n' ) do |input|
|
||||||
|
input =~ /^[yn]/i
|
||||||
|
end
|
||||||
|
|
||||||
|
case answer
|
||||||
|
when /^y/i
|
||||||
|
yield
|
||||||
|
else
|
||||||
|
error "Aborted."
|
||||||
|
fail
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
### Search line-by-line in the specified +file+ for the given +regexp+, returning the
|
||||||
|
### first match, or nil if no match was found. If the +regexp+ has any capture groups,
|
||||||
|
### those will be returned in an Array, else the whole matching line is returned.
|
||||||
|
def find_pattern_in_file( regexp, file )
|
||||||
|
rval = nil
|
||||||
|
|
||||||
|
File.open( file, 'r' ).each do |line|
|
||||||
|
if (( match = regexp.match(line) ))
|
||||||
|
rval = match.captures.empty? ? match[0] : match.captures
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return rval
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
### Search line-by-line in the output of the specified +cmd+ for the given +regexp+,
|
||||||
|
### returning the first match, or nil if no match was found. If the +regexp+ has any
|
||||||
|
### capture groups, those will be returned in an Array, else the whole matching line
|
||||||
|
### is returned.
|
||||||
|
def find_pattern_in_pipe( regexp, *cmd )
|
||||||
|
output = []
|
||||||
|
|
||||||
|
Open3.popen3( *cmd ) do |stdin, stdout, stderr|
|
||||||
|
stdin.close
|
||||||
|
|
||||||
|
output << stdout.gets until stdout.eof?
|
||||||
|
output << stderr.gets until stderr.eof?
|
||||||
|
end
|
||||||
|
|
||||||
|
result = output.find { |line| regexp.match(line) }
|
||||||
|
return $1 || result
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
### Extract all the non Rake-target arguments from ARGV and return them.
|
||||||
|
def get_target_args
|
||||||
|
args = ARGV.reject {|arg| Rake::Task.task_defined?(arg) }
|
||||||
|
return args
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
require 'rubygems/dependency_installer'
|
||||||
|
require 'rubygems/source_index'
|
||||||
|
require 'rubygems/requirement'
|
||||||
|
require 'rubygems/doc_manager'
|
||||||
|
|
||||||
|
### Install the specified +gems+ if they aren't already installed.
|
||||||
|
def install_gems( *gems )
|
||||||
|
gems.flatten!
|
||||||
|
|
||||||
|
defaults = Gem::DependencyInstaller::DEFAULT_OPTIONS.merge({
|
||||||
|
:generate_rdoc => true,
|
||||||
|
:generate_ri => true,
|
||||||
|
:install_dir => Gem.dir,
|
||||||
|
:format_executable => false,
|
||||||
|
:test => false,
|
||||||
|
:version => Gem::Requirement.default,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Check for root
|
||||||
|
if Process.euid != 0
|
||||||
|
$stderr.puts "This probably won't work, as you aren't root, but I'll try anyway"
|
||||||
|
end
|
||||||
|
|
||||||
|
gemindex = Gem::SourceIndex.from_installed_gems
|
||||||
|
|
||||||
|
gems.each do |gemname|
|
||||||
|
if (( specs = gemindex.search(gemname) )) && ! specs.empty?
|
||||||
|
log "Version %s of %s is already installed; skipping..." %
|
||||||
|
[ specs.first.version, specs.first.name ]
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
|
log "Trying to install #{gemname.inspect}..."
|
||||||
|
installer = Gem::DependencyInstaller.new
|
||||||
|
installer.install( gemname )
|
||||||
|
|
||||||
|
installer.installed_gems.each do |spec|
|
||||||
|
log "Installed: %s" % [ spec.full_name ]
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
32
rake/packaging.rb
Normal file
32
rake/packaging.rb
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
#
|
||||||
|
# Packaging Rake Tasks
|
||||||
|
#
|
||||||
|
#
|
||||||
|
|
||||||
|
require 'rake/packagetask'
|
||||||
|
require 'rake/gempackagetask'
|
||||||
|
|
||||||
|
Rake::GemPackageTask.new( GEMSPEC ) do |task|
|
||||||
|
task.gem_spec = GEMSPEC
|
||||||
|
task.need_tar = false
|
||||||
|
task.need_tar_gz = true
|
||||||
|
task.need_tar_bz2 = true
|
||||||
|
task.need_zip = true
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
### Task: install
|
||||||
|
task :install_gem => [:package] do
|
||||||
|
$stderr.puts
|
||||||
|
installer = Gem::Installer.new( %{pkg/#{PKG_FILE_NAME}.gem} )
|
||||||
|
installer.install
|
||||||
|
end
|
||||||
|
|
||||||
|
### Task: uninstall
|
||||||
|
task :uninstall_gem => [:clean] do
|
||||||
|
uninstaller = Gem::Uninstaller.new( PKG_FILE_NAME )
|
||||||
|
uninstaller.uninstall
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
25
rake/rdoc.rb
Normal file
25
rake/rdoc.rb
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
#
|
||||||
|
# RDoc Rake tasks
|
||||||
|
# $Id$
|
||||||
|
#
|
||||||
|
|
||||||
|
require 'rake/rdoctask'
|
||||||
|
|
||||||
|
### Task: rdoc
|
||||||
|
Rake::RDocTask.new do |rdoc|
|
||||||
|
rdoc.rdoc_dir = 'docs/api'
|
||||||
|
rdoc.title = "%s -- %s" % [ GEMSPEC.name, GEMSPEC.summary ]
|
||||||
|
|
||||||
|
rdoc.options += [
|
||||||
|
'-w', '4',
|
||||||
|
'-SHN',
|
||||||
|
'-i', BASEDIR.to_s,
|
||||||
|
'-f', 'darkfish',
|
||||||
|
'-m', 'README',
|
||||||
|
'-W', 'http://opensource.laika.com/browser/ruby-ezmlm/trunk/'
|
||||||
|
]
|
||||||
|
|
||||||
|
rdoc.rdoc_files.include 'README'
|
||||||
|
rdoc.rdoc_files.include LIB_FILES.collect {|f| f.relative_path_from(BASEDIR).to_s }
|
||||||
|
end
|
||||||
|
|
||||||
59
rake/style.rb
Normal file
59
rake/style.rb
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
#
|
||||||
|
# Style Fixup Rake Tasks
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
|
||||||
|
### Coding style checks and fixes
|
||||||
|
namespace :style do
|
||||||
|
|
||||||
|
BLANK_LINE = /^\s*$/
|
||||||
|
GOOD_INDENT = /^(\t\s*)?\S/
|
||||||
|
|
||||||
|
# A list of the files that have legitimate leading whitespace, etc.
|
||||||
|
PROBLEM_FILES = [ SPECDIR + 'config_spec.rb' ]
|
||||||
|
|
||||||
|
desc "Check source files for inconsistent indent and fix them"
|
||||||
|
task :fix_indent do
|
||||||
|
files = LIB_FILES + SPEC_FILES
|
||||||
|
|
||||||
|
badfiles = Hash.new {|h,k| h[k] = [] }
|
||||||
|
|
||||||
|
trace "Checking files for indentation"
|
||||||
|
files.each do |file|
|
||||||
|
if PROBLEM_FILES.include?( file )
|
||||||
|
trace " skipping problem file #{file}..."
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
|
trace " #{file}"
|
||||||
|
linecount = 0
|
||||||
|
file.each_line do |line|
|
||||||
|
linecount += 1
|
||||||
|
|
||||||
|
# Skip blank lines
|
||||||
|
next if line =~ BLANK_LINE
|
||||||
|
|
||||||
|
# If there's a line with incorrect indent, note it and skip to the
|
||||||
|
# next file
|
||||||
|
if line !~ GOOD_INDENT
|
||||||
|
trace " Bad line %d: %p" % [ linecount, line ]
|
||||||
|
badfiles[file] << [ linecount, line ]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if badfiles.empty?
|
||||||
|
log "No indentation problems found."
|
||||||
|
else
|
||||||
|
log "Found incorrect indent in #{badfiles.length} files:\n "
|
||||||
|
badfiles.each do |file, badlines|
|
||||||
|
log " #{file}:\n" +
|
||||||
|
" " + badlines.collect {|badline| "%5d: %p" % badline }.join( "\n " )
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
328
rake/svn.rb
Normal file
328
rake/svn.rb
Normal file
|
|
@ -0,0 +1,328 @@
|
||||||
|
#####################################################################
|
||||||
|
### S U B V E R S I O N T A S K S A N D H E L P E R S
|
||||||
|
#####################################################################
|
||||||
|
|
||||||
|
require 'pp'
|
||||||
|
require 'yaml'
|
||||||
|
require 'English'
|
||||||
|
|
||||||
|
# Strftime format for tags/releases
|
||||||
|
TAG_TIMESTAMP_FORMAT = '%Y%m%d-%H%M%S'
|
||||||
|
TAG_TIMESTAMP_PATTERN = /\d{4}\d{2}\d{2}-\d{6}/
|
||||||
|
|
||||||
|
RELEASE_VERSION_PATTERN = /\d+\.\d+\.\d+/
|
||||||
|
|
||||||
|
DEFAULT_EDITOR = 'vi'
|
||||||
|
DEFAULT_KEYWORDS = %w[Date Rev Author URL Id]
|
||||||
|
KEYWORDED_FILEDIRS = %w[applets bin etc lib misc]
|
||||||
|
KEYWORDED_FILEPATTERN = /^(?:Rakefile|.*\.(?:rb|js|html|template))$/i
|
||||||
|
|
||||||
|
COMMIT_MSG_FILE = 'commit-msg.txt'
|
||||||
|
|
||||||
|
###
|
||||||
|
### Subversion-specific Helpers
|
||||||
|
###
|
||||||
|
|
||||||
|
### Return a new tag for the given time
|
||||||
|
def make_new_tag( time=Time.now )
|
||||||
|
return time.strftime( TAG_TIMESTAMP_FORMAT )
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
### Get the subversion information for the current working directory as
|
||||||
|
### a hash.
|
||||||
|
def get_svn_info( dir='.' )
|
||||||
|
info = IO.read( '|-' ) or exec 'svn', 'info', dir
|
||||||
|
return YAML.load( info ) # 'svn info' outputs valid YAML! Yay!
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
### Get a list of the objects registered with subversion under the specified directory and
|
||||||
|
### return them as an Array of Pathame objects.
|
||||||
|
def get_svn_filelist( dir='.' )
|
||||||
|
list = IO.read( '|-' ) or exec 'svn', 'st', '-v', '--ignore-externals', dir
|
||||||
|
|
||||||
|
# Split into lines, filter out the unknowns, and grab the filenames as Pathnames
|
||||||
|
# :FIXME: This will break if we ever put in a file with spaces in its name. This
|
||||||
|
# will likely be the least of our worries if we do so, however, so it's not worth
|
||||||
|
# the additional complexity to make it handle that case. If we do need that, there's
|
||||||
|
# always the --xml output for 'svn st'...
|
||||||
|
return list.split( $/ ).
|
||||||
|
reject {|line| line =~ /^\?/ }.
|
||||||
|
collect {|fn| Pathname(fn[/\S+$/]) }
|
||||||
|
end
|
||||||
|
|
||||||
|
### Return the URL to the repository root for the specified +dir+.
|
||||||
|
def get_svn_repo_root( dir='.' )
|
||||||
|
info = get_svn_info( dir )
|
||||||
|
return info['URL'].sub( %r{/trunk$}, '' )
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
### Return the Subversion URL to the given +dir+.
|
||||||
|
def get_svn_url( dir='.' )
|
||||||
|
info = get_svn_info( dir )
|
||||||
|
return info['URL']
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
### Return the path of the specified +dir+ under the svn root of the
|
||||||
|
### checkout.
|
||||||
|
def get_svn_path( dir='.' )
|
||||||
|
root = get_svn_repo_root( dir )
|
||||||
|
url = get_svn_url( dir )
|
||||||
|
|
||||||
|
return url.sub( root + '/', '' )
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
### Return the keywords for the specified array of +files+ as a Hash keyed by filename.
|
||||||
|
def get_svn_keyword_map( files )
|
||||||
|
cmd = ['svn', 'pg', 'svn:keywords', *files]
|
||||||
|
|
||||||
|
# trace "Executing: svn pg svn:keywords " + files.join(' ')
|
||||||
|
output = IO.read( '|-' ) or exec( 'svn', 'pg', 'svn:keywords', *files )
|
||||||
|
|
||||||
|
kwmap = {}
|
||||||
|
output.split( "\n" ).each do |line|
|
||||||
|
next if line !~ /\s+-\s+/
|
||||||
|
path, keywords = line.split( /\s+-\s+/, 2 )
|
||||||
|
kwmap[ path ] = keywords.split
|
||||||
|
end
|
||||||
|
|
||||||
|
return kwmap
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
### Return the latest revision number of the specified +dir+ as an Integer.
|
||||||
|
def get_svn_rev( dir='.' )
|
||||||
|
info = get_svn_info( dir )
|
||||||
|
return info['Revision']
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
### Return a list of the entries at the specified Subversion url. If
|
||||||
|
### no +url+ is specified, it will default to the list in the URL
|
||||||
|
### corresponding to the current working directory.
|
||||||
|
def svn_ls( url=nil )
|
||||||
|
url ||= get_svn_url()
|
||||||
|
list = IO.read( '|-' ) or exec 'svn', 'ls', url
|
||||||
|
|
||||||
|
trace 'svn ls of %s: %p' % [url, list] if $trace
|
||||||
|
|
||||||
|
return [] if list.nil? || list.empty?
|
||||||
|
return list.split( $INPUT_RECORD_SEPARATOR )
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
### Return the URL of the latest timestamp in the tags directory.
|
||||||
|
def get_latest_svn_timestamp_tag
|
||||||
|
rooturl = get_svn_repo_root()
|
||||||
|
tagsurl = rooturl + '/tags'
|
||||||
|
|
||||||
|
tags = svn_ls( tagsurl ).grep( TAG_TIMESTAMP_PATTERN ).sort
|
||||||
|
return nil if tags.nil? || tags.empty?
|
||||||
|
return tagsurl + '/' + tags.last
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
### Get a subversion diff of the specified targets and return it. If no targets are
|
||||||
|
### specified, the current directory will be diffed instead.
|
||||||
|
def get_svn_diff( *targets )
|
||||||
|
targets << BASEDIR if targets.empty?
|
||||||
|
trace "Getting svn diff for targets: %p" % [targets]
|
||||||
|
log = IO.read( '|-' ) or exec 'svn', 'diff', *(targets.flatten)
|
||||||
|
|
||||||
|
return log
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
### Return the URL of the latest timestamp in the tags directory.
|
||||||
|
def get_latest_release_tag
|
||||||
|
rooturl = get_svn_repo_root()
|
||||||
|
releaseurl = rooturl + '/releases'
|
||||||
|
|
||||||
|
tags = svn_ls( releaseurl ).grep( RELEASE_VERSION_PATTERN ).sort_by do |tag|
|
||||||
|
tag.split('.').collect {|i| Integer(i) }
|
||||||
|
end
|
||||||
|
return nil if tags.empty?
|
||||||
|
|
||||||
|
return releaseurl + '/' + tags.last
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
### Extract a diff from the specified subversion working +dir+, rewrite its
|
||||||
|
### file lines as Trac links, and return it.
|
||||||
|
def make_svn_commit_log( dir='.' )
|
||||||
|
editor_prog = ENV['EDITOR'] || ENV['VISUAL'] || DEFAULT_EDITOR
|
||||||
|
|
||||||
|
diff = IO.read( '|-' ) or exec 'svn', 'diff'
|
||||||
|
fail "No differences." if diff.empty?
|
||||||
|
|
||||||
|
return diff
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
###
|
||||||
|
### Tasks
|
||||||
|
###
|
||||||
|
|
||||||
|
desc "Subversion tasks"
|
||||||
|
namespace :svn do
|
||||||
|
|
||||||
|
desc "Copy the HEAD revision of the current trunk/ to tags/ with a " +
|
||||||
|
"current timestamp."
|
||||||
|
task :tag do
|
||||||
|
svninfo = get_svn_info()
|
||||||
|
tag = make_new_tag()
|
||||||
|
svntrunk = svninfo['URL']
|
||||||
|
svntagdir = svninfo['URL'].sub( %r{trunk$}, 'tags' )
|
||||||
|
svntag = svntagdir + '/' + tag
|
||||||
|
|
||||||
|
desc = "Tagging trunk as #{svntag}"
|
||||||
|
ask_for_confirmation( desc ) do
|
||||||
|
msg = prompt_with_default( "Commit log: ", "Tagging for code push" )
|
||||||
|
run 'svn', 'cp', '-m', msg, svntrunk, svntag
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
desc "Copy the most recent tag to releases/#{PKG_VERSION}"
|
||||||
|
task :release do
|
||||||
|
last_tag = get_latest_svn_timestamp_tag()
|
||||||
|
svninfo = get_svn_info()
|
||||||
|
release = PKG_VERSION
|
||||||
|
svnrel = svninfo['URL'] + '/releases'
|
||||||
|
svnrelease = svnrel + '/' + release
|
||||||
|
|
||||||
|
if last_tag.nil?
|
||||||
|
error "There are no tags in the repository"
|
||||||
|
fail
|
||||||
|
end
|
||||||
|
|
||||||
|
releases = svn_ls( svnrel )
|
||||||
|
trace "Releases: %p" % [releases]
|
||||||
|
if releases.include?( release )
|
||||||
|
error "Version #{release} already has a branch (#{svnrelease}). Did you mean" +
|
||||||
|
"to increment the version in #{PKG_VERSION_FROM}?"
|
||||||
|
fail
|
||||||
|
else
|
||||||
|
trace "No #{svnrel} version currently exists"
|
||||||
|
end
|
||||||
|
|
||||||
|
desc = "Release tag\n #{last_tag}\nto\n #{svnrelease}"
|
||||||
|
ask_for_confirmation( desc ) do
|
||||||
|
msg = prompt_with_default( "Commit log: ", "Branching for release" )
|
||||||
|
run 'svn', 'cp', '-m', msg, last_tag, svnrelease
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
### Task for debugging the #get_target_args helper
|
||||||
|
task :show_targets do
|
||||||
|
$stdout.puts "Targets from ARGV (%p): %p" % [ARGV, get_target_args()]
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
desc "Generate a commit log"
|
||||||
|
task :commitlog => [COMMIT_MSG_FILE]
|
||||||
|
|
||||||
|
desc "Show the (pre-edited) commit log for the current directory"
|
||||||
|
task :show_commitlog => [COMMIT_MSG_FILE] do
|
||||||
|
ask_for_confirmation( "Confirm? " ) do
|
||||||
|
args = get_target_args()
|
||||||
|
puts get_svn_diff( *args )
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
file COMMIT_MSG_FILE do
|
||||||
|
args = get_target_args()
|
||||||
|
diff = get_svn_diff( *args )
|
||||||
|
|
||||||
|
File.open( COMMIT_MSG_FILE, File::WRONLY|File::EXCL|File::CREAT ) do |fh|
|
||||||
|
fh.print( diff )
|
||||||
|
end
|
||||||
|
|
||||||
|
editor = ENV['EDITOR'] || ENV['VISUAL'] || DEFAULT_EDITOR
|
||||||
|
system editor, COMMIT_MSG_FILE
|
||||||
|
unless $?.success?
|
||||||
|
fail "Editor exited uncleanly."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
desc "Update from Subversion"
|
||||||
|
task :update do
|
||||||
|
run 'svn', 'up', '--ignore-externals'
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
desc "Check in all the changes in your current working copy"
|
||||||
|
task :checkin => ['svn:update', 'coverage:verify', 'svn:fix_keywords', COMMIT_MSG_FILE] do
|
||||||
|
targets = get_target_args()
|
||||||
|
$deferr.puts '---', File.read( COMMIT_MSG_FILE ), '---'
|
||||||
|
ask_for_confirmation( "Continue with checkin?" ) do
|
||||||
|
run 'svn', 'ci', '-F', COMMIT_MSG_FILE, targets
|
||||||
|
rm_f COMMIT_MSG_FILE
|
||||||
|
end
|
||||||
|
end
|
||||||
|
task :commit => :checkin
|
||||||
|
task :ci => :checkin
|
||||||
|
|
||||||
|
|
||||||
|
task :clean do
|
||||||
|
rm_f COMMIT_MSG_FILE
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
desc "Check and fix any missing keywords for any files in the project which need them"
|
||||||
|
task :fix_keywords do
|
||||||
|
log "Checking subversion keywords..."
|
||||||
|
paths = get_svn_filelist( BASEDIR ).
|
||||||
|
select {|path| path.file? && path.to_s =~ KEYWORDED_FILEPATTERN }
|
||||||
|
|
||||||
|
trace "Looking at %d paths for keywords:\n %p" % [paths.length, paths]
|
||||||
|
kwmap = get_svn_keyword_map( paths )
|
||||||
|
|
||||||
|
buf = ''
|
||||||
|
PP.pp( kwmap, buf, 132 )
|
||||||
|
trace "keyword map is: %s" % [buf]
|
||||||
|
|
||||||
|
files_needing_fixups = paths.find_all do |path|
|
||||||
|
(kwmap[path.to_s] & DEFAULT_KEYWORDS) != DEFAULT_KEYWORDS
|
||||||
|
end
|
||||||
|
|
||||||
|
unless files_needing_fixups.empty?
|
||||||
|
$deferr.puts "Files needing keyword fixes: ",
|
||||||
|
files_needing_fixups.collect {|f|
|
||||||
|
" %s: %s" % [f, kwmap[f] ? kwmap[f].join(' ') : "(no keywords)"]
|
||||||
|
}
|
||||||
|
ask_for_confirmation( "Will add default keywords to these files." ) do
|
||||||
|
run 'svn', 'ps', 'svn:keywords', DEFAULT_KEYWORDS.join(' '), *files_needing_fixups
|
||||||
|
end
|
||||||
|
else
|
||||||
|
log "Keywords are all up to date."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
task :debug_helpers do
|
||||||
|
methods = [
|
||||||
|
:make_new_tag,
|
||||||
|
:get_svn_info,
|
||||||
|
:get_svn_repo_root,
|
||||||
|
:get_svn_url,
|
||||||
|
:get_svn_path,
|
||||||
|
:svn_ls,
|
||||||
|
:get_latest_svn_timestamp_tag,
|
||||||
|
]
|
||||||
|
maxlen = methods.collect {|sym| sym.to_s.length }.max
|
||||||
|
|
||||||
|
methods.each do |meth|
|
||||||
|
res = send( meth )
|
||||||
|
puts "%*s => %p" % [ maxlen, colorize(meth.to_s, :cyan), res ]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
127
rake/testing.rb
Normal file
127
rake/testing.rb
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
#
|
||||||
|
# Testing Rake Tasks
|
||||||
|
# $Id$
|
||||||
|
#
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
# Keep these tasks optional by handling LoadErrors with stub task
|
||||||
|
# replacements.
|
||||||
|
begin
|
||||||
|
gem 'rspec', '>= 1.1.1'
|
||||||
|
require 'spec/rake/spectask'
|
||||||
|
|
||||||
|
COMMON_SPEC_OPTS = ['-c', '-f', 's']
|
||||||
|
|
||||||
|
### Task: spec
|
||||||
|
Spec::Rake::SpecTask.new( :spec ) do |task|
|
||||||
|
task.spec_files = SPEC_FILES
|
||||||
|
task.libs += [LIBDIR]
|
||||||
|
task.spec_opts = COMMON_SPEC_OPTS
|
||||||
|
end
|
||||||
|
task :test => [:spec]
|
||||||
|
|
||||||
|
|
||||||
|
namespace :spec do
|
||||||
|
desc "Generate HTML output for a spec run"
|
||||||
|
Spec::Rake::SpecTask.new( :html ) do |task|
|
||||||
|
task.spec_files = SPEC_FILES
|
||||||
|
task.spec_opts = ['-f','h', '-D']
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "Generate plain-text output for a CruiseControl.rb build"
|
||||||
|
Spec::Rake::SpecTask.new( :text ) do |task|
|
||||||
|
task.spec_files = SPEC_FILES
|
||||||
|
task.spec_opts = ['-f','p']
|
||||||
|
end
|
||||||
|
end
|
||||||
|
rescue LoadError => err
|
||||||
|
task :no_rspec do
|
||||||
|
$stderr.puts "Testing tasks not defined: RSpec rake tasklib not available: %s" %
|
||||||
|
[ err.message ]
|
||||||
|
end
|
||||||
|
|
||||||
|
task :spec => :no_rspec
|
||||||
|
namespace :spec do
|
||||||
|
task :autotest => :no_rspec
|
||||||
|
task :html => :no_rspec
|
||||||
|
task :text => :no_rspec
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### RCov (via RSpec) tasks
|
||||||
|
begin
|
||||||
|
gem 'rcov'
|
||||||
|
gem 'rspec', '>= 1.1.1'
|
||||||
|
|
||||||
|
COVERAGE_TARGETDIR = STATICWWWDIR + 'coverage'
|
||||||
|
|
||||||
|
RCOV_OPTS = ['--exclude', SPEC_EXCLUDES, '--xrefs', '--save']
|
||||||
|
|
||||||
|
### Task: coverage (via RCov)
|
||||||
|
### Task: spec
|
||||||
|
desc "Build test coverage reports"
|
||||||
|
Spec::Rake::SpecTask.new( :coverage ) do |task|
|
||||||
|
task.spec_files = SPEC_FILES
|
||||||
|
task.libs += [LIBDIR]
|
||||||
|
task.spec_opts = ['-f', 'p', '-b']
|
||||||
|
task.rcov_opts = RCOV_OPTS
|
||||||
|
task.rcov = true
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
task :rcov => [:coverage] do; end
|
||||||
|
|
||||||
|
### Other coverage tasks
|
||||||
|
namespace :coverage do
|
||||||
|
desc "Generate a detailed text coverage report"
|
||||||
|
Spec::Rake::SpecTask.new( :text ) do |task|
|
||||||
|
task.spec_files = SPEC_FILES
|
||||||
|
task.rcov_opts = RCOV_OPTS + ['--text-report']
|
||||||
|
task.rcov = true
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "Show differences in coverage from last run"
|
||||||
|
Spec::Rake::SpecTask.new( :diff ) do |task|
|
||||||
|
task.spec_files = SPEC_FILES
|
||||||
|
task.rcov_opts = ['--text-coverage-diff']
|
||||||
|
task.rcov = true
|
||||||
|
end
|
||||||
|
|
||||||
|
### Task: verify coverage
|
||||||
|
desc "Build coverage statistics"
|
||||||
|
VerifyTask.new( :verify => :rcov ) do |task|
|
||||||
|
task.threshold = 85.0
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "Run RCov in 'spec-only' mode to check coverage from specs"
|
||||||
|
Spec::Rake::SpecTask.new( :speconly ) do |task|
|
||||||
|
task.spec_files = SPEC_FILES
|
||||||
|
task.rcov_opts = ['--exclude', SPEC_EXCLUDES, '--text-report', '--save']
|
||||||
|
task.rcov = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
task :clobber_coverage do
|
||||||
|
rmtree( COVERAGE_TARGETDIR )
|
||||||
|
end
|
||||||
|
|
||||||
|
rescue LoadError => err
|
||||||
|
task :no_rcov do
|
||||||
|
$stderr.puts "Coverage tasks not defined: RSpec+RCov tasklib not available: %s" %
|
||||||
|
[ err.message ]
|
||||||
|
end
|
||||||
|
|
||||||
|
task :coverage => :no_rcov
|
||||||
|
task :clobber_coverage
|
||||||
|
task :rcov => :no_rcov
|
||||||
|
namespace :coverage do
|
||||||
|
task :text => :no_rcov
|
||||||
|
task :diff => :no_rcov
|
||||||
|
end
|
||||||
|
task :verify => :no_rcov
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
64
rake/verifytask.rb
Normal file
64
rake/verifytask.rb
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
#####################################################################
|
||||||
|
### S U B V E R S I O N T A S K S A N D H E L P E R S
|
||||||
|
#####################################################################
|
||||||
|
|
||||||
|
require 'rake/tasklib'
|
||||||
|
|
||||||
|
#
|
||||||
|
# Work around the inexplicable behaviour of the original RDoc::VerifyTask, which
|
||||||
|
# errors if your coverage isn't *exactly* the threshold.
|
||||||
|
#
|
||||||
|
|
||||||
|
# A task that can verify that the RCov coverage doesn't
|
||||||
|
# drop below a certain threshold. It should be run after
|
||||||
|
# running Spec::Rake::SpecTask.
|
||||||
|
class VerifyTask < Rake::TaskLib
|
||||||
|
|
||||||
|
COVERAGE_PERCENTAGE_PATTERN =
|
||||||
|
%r{<tt class='coverage_code'>(\d+\.\d+)%</tt>}
|
||||||
|
|
||||||
|
# Name of the task. Defaults to :verify_rcov
|
||||||
|
attr_accessor :name
|
||||||
|
|
||||||
|
# Path to the index.html file generated by RCov, which
|
||||||
|
# is the file containing the total coverage.
|
||||||
|
# Defaults to 'coverage/index.html'
|
||||||
|
attr_accessor :index_html
|
||||||
|
|
||||||
|
# Whether or not to output details. Defaults to true.
|
||||||
|
attr_accessor :verbose
|
||||||
|
|
||||||
|
# The threshold value (in percent) for coverage. If the
|
||||||
|
# actual coverage is not equal to this value, the task will raise an
|
||||||
|
# exception.
|
||||||
|
attr_accessor :threshold
|
||||||
|
|
||||||
|
def initialize( name=:verify )
|
||||||
|
@name = name
|
||||||
|
@index_html = 'coverage/index.html'
|
||||||
|
@verbose = true
|
||||||
|
yield self if block_given?
|
||||||
|
raise "Threshold must be set" if @threshold.nil?
|
||||||
|
define
|
||||||
|
end
|
||||||
|
|
||||||
|
def define
|
||||||
|
desc "Verify that rcov coverage is at least #{threshold}%"
|
||||||
|
|
||||||
|
task @name do
|
||||||
|
total_coverage = nil
|
||||||
|
if match = File.read( index_html ).match( COVERAGE_PERCENTAGE_PATTERN )
|
||||||
|
total_coverage = Float( match[1] )
|
||||||
|
else
|
||||||
|
raise "Couldn't find the coverage percentage in #{index_html}"
|
||||||
|
end
|
||||||
|
|
||||||
|
puts "Coverage: #{total_coverage}% (threshold: #{threshold}%)" if verbose
|
||||||
|
if total_coverage < threshold
|
||||||
|
raise "Coverage must be at least #{threshold}% but was #{total_coverage}%"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# vim: set nosta noet ts=4 sw=4:
|
||||||
33
spec/ezmlm/list_spec.rb
Normal file
33
spec/ezmlm/list_spec.rb
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
#!/usr/bin/env ruby
|
||||||
|
|
||||||
|
BEGIN {
|
||||||
|
require 'pathname'
|
||||||
|
basedir = Pathname.new( __FILE__ ).dirname.parent.parent
|
||||||
|
|
||||||
|
libdir = basedir + "lib"
|
||||||
|
|
||||||
|
$LOAD_PATH.unshift( libdir ) unless $LOAD_PATH.include?( libdir )
|
||||||
|
}
|
||||||
|
|
||||||
|
begin
|
||||||
|
require 'spec/runner'
|
||||||
|
require 'spec/lib/helpers'
|
||||||
|
require 'ezmlm/list'
|
||||||
|
rescue LoadError
|
||||||
|
unless Object.const_defined?( :Gem )
|
||||||
|
require 'rubygems'
|
||||||
|
retry
|
||||||
|
end
|
||||||
|
raise
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
describe Ezmlm::List do
|
||||||
|
include Ezmlm::SpecHelpers
|
||||||
|
|
||||||
|
|
||||||
|
it "is well-tested"
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
59
spec/ezmlm_spec.rb
Normal file
59
spec/ezmlm_spec.rb
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
#!/usr/bin/env ruby
|
||||||
|
|
||||||
|
BEGIN {
|
||||||
|
require 'pathname'
|
||||||
|
basedir = Pathname.new( __FILE__ ).dirname.parent
|
||||||
|
|
||||||
|
libdir = basedir + "lib"
|
||||||
|
|
||||||
|
$LOAD_PATH.unshift( libdir ) unless $LOAD_PATH.include?( libdir )
|
||||||
|
}
|
||||||
|
|
||||||
|
begin
|
||||||
|
require 'spec/runner'
|
||||||
|
require 'spec/lib/helpers'
|
||||||
|
require 'ezmlm'
|
||||||
|
rescue LoadError
|
||||||
|
unless Object.const_defined?( :Gem )
|
||||||
|
require 'rubygems'
|
||||||
|
retry
|
||||||
|
end
|
||||||
|
raise
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
describe Ezmlm do
|
||||||
|
include Ezmlm::SpecHelpers
|
||||||
|
|
||||||
|
LISTSDIR = '/tmp/lists'
|
||||||
|
|
||||||
|
it "can iterate over all mailing lists in a specified directory" do
|
||||||
|
file_entry = mock( "plain file" )
|
||||||
|
file_entry.should_receive( :directory? ).and_return( false )
|
||||||
|
|
||||||
|
nonexistant_mlentry = stub( "mailinglist path that doesn't exist", :exist? => false )
|
||||||
|
nonml_dir_entry = stub( "directory with no mailinglist file",
|
||||||
|
:directory? => true, :+ => nonexistant_mlentry )
|
||||||
|
|
||||||
|
existant_mlentry = stub( "mailinglist path that does exist", :exist? => true )
|
||||||
|
ml_dir_entry = stub( "directory with a mailinglist file", :directory? => true, :+ => existant_mlentry )
|
||||||
|
|
||||||
|
Pathname.should_receive( :glob ).with( an_instance_of(Pathname) ).
|
||||||
|
and_yield( file_entry ).
|
||||||
|
and_yield( nonml_dir_entry ).
|
||||||
|
and_yield( ml_dir_entry )
|
||||||
|
|
||||||
|
Ezmlm::List.should_receive( :new ).with( ml_dir_entry ).and_return( :listobject )
|
||||||
|
|
||||||
|
lists = []
|
||||||
|
Ezmlm.each_list( LISTSDIR ) do |list|
|
||||||
|
lists << list
|
||||||
|
end
|
||||||
|
|
||||||
|
lists.should have(1).member
|
||||||
|
lists.should include( :listobject )
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
157
spec/lib/helpers.rb
Normal file
157
spec/lib/helpers.rb
Normal file
|
|
@ -0,0 +1,157 @@
|
||||||
|
#!/usr/bin/ruby
|
||||||
|
|
||||||
|
begin
|
||||||
|
require 'ezmlm'
|
||||||
|
rescue LoadError
|
||||||
|
unless Object.const_defined?( :Gem )
|
||||||
|
require 'rubygems'
|
||||||
|
retry
|
||||||
|
end
|
||||||
|
raise
|
||||||
|
end
|
||||||
|
|
||||||
|
module Ezmlm::SpecHelpers
|
||||||
|
|
||||||
|
###############
|
||||||
|
module_function
|
||||||
|
###############
|
||||||
|
|
||||||
|
### Create a temporary working directory and return
|
||||||
|
### a Pathname object for it.
|
||||||
|
###
|
||||||
|
def make_tempdir
|
||||||
|
dirname = "%s.%d.%0.4f" % [
|
||||||
|
'ezmlm_spec',
|
||||||
|
Process.pid,
|
||||||
|
(Time.now.to_f % 3600),
|
||||||
|
]
|
||||||
|
tempdir = Pathname.new( Dir.tmpdir ) + dirname
|
||||||
|
tempdir.mkpath
|
||||||
|
|
||||||
|
return tempdir
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
# Override the badly-formatted output of the RSpec HTML formatter
|
||||||
|
require 'spec/runner/formatter/html_formatter'
|
||||||
|
|
||||||
|
class Spec::Runner::Formatter::HtmlFormatter
|
||||||
|
def example_failed( example, counter, failure )
|
||||||
|
failure_style = failure.pending_fixed? ? 'pending_fixed' : 'failed'
|
||||||
|
|
||||||
|
unless @header_red
|
||||||
|
@output.puts " <script type=\"text/javascript\">makeRed('rspec-header');</script>"
|
||||||
|
@header_red = true
|
||||||
|
end
|
||||||
|
|
||||||
|
unless @example_group_red
|
||||||
|
css_class = 'example_group_%d' % [current_example_group_number]
|
||||||
|
@output.puts " <script type=\"text/javascript\">makeRed('#{css_class}');</script>"
|
||||||
|
@example_group_red = true
|
||||||
|
end
|
||||||
|
|
||||||
|
move_progress()
|
||||||
|
|
||||||
|
@output.puts " <dd class=\"spec #{failure_style}\">",
|
||||||
|
" <span class=\"failed_spec_name\">#{h(example.description)}</span>",
|
||||||
|
" <div class=\"failure\" id=\"failure_#{counter}\">"
|
||||||
|
if failure.exception
|
||||||
|
backtrace = format_backtrace( failure.exception.backtrace )
|
||||||
|
message = failure.exception.message
|
||||||
|
|
||||||
|
@output.puts " <div class=\"message\"><code>#{h message}</code></div>",
|
||||||
|
" <div class=\"backtrace\"><pre>#{backtrace}</pre></div>"
|
||||||
|
end
|
||||||
|
|
||||||
|
if extra = extra_failure_content( failure )
|
||||||
|
@output.puts( extra )
|
||||||
|
end
|
||||||
|
|
||||||
|
@output.puts " </div>",
|
||||||
|
" </dd>"
|
||||||
|
@output.flush
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
alias_method :default_global_styles, :global_styles
|
||||||
|
|
||||||
|
def global_styles
|
||||||
|
css = default_global_styles()
|
||||||
|
css << %Q{
|
||||||
|
/* Stuff added by #{__FILE__} */
|
||||||
|
|
||||||
|
/* Overrides */
|
||||||
|
#rspec-header {
|
||||||
|
-webkit-box-shadow: #333 0 2px 5px;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example_group dt {
|
||||||
|
-webkit-box-shadow: #333 0 2px 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style for log output */
|
||||||
|
dd.log-message {
|
||||||
|
background: #eee;
|
||||||
|
padding: 0 2em;
|
||||||
|
margin: 0.2em 1em;
|
||||||
|
border-bottom: 1px dotted #999;
|
||||||
|
border-top: 1px dotted #999;
|
||||||
|
text-indent: -1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Parts of the message */
|
||||||
|
dd.log-message .log-time {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
dd.log-message .log-time:after {
|
||||||
|
content: ": ";
|
||||||
|
}
|
||||||
|
dd.log-message .log-level {
|
||||||
|
font-variant: small-caps;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
padding: 1px 2px;
|
||||||
|
}
|
||||||
|
dd.log-message .log-name {
|
||||||
|
font-size: 1.2em;
|
||||||
|
color: #1e51b2;
|
||||||
|
}
|
||||||
|
dd.log-message .log-name:before { content: "«"; }
|
||||||
|
dd.log-message .log-name:after { content: "»"; }
|
||||||
|
|
||||||
|
dd.log-message .log-message-text {
|
||||||
|
padding-left: 4px;
|
||||||
|
font-family: Monaco, "Andale Mono", "Vera Sans Mono", mono;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Distinguish levels */
|
||||||
|
dd.log-message.debug { color: #666; }
|
||||||
|
dd.log-message.info {}
|
||||||
|
|
||||||
|
dd.log-message.warn,
|
||||||
|
dd.log-message.error {
|
||||||
|
background: #ff9;
|
||||||
|
}
|
||||||
|
dd.log-message.error .log-level,
|
||||||
|
dd.log-message.error .log-message-text {
|
||||||
|
color: #900;
|
||||||
|
}
|
||||||
|
dd.log-message.fatal {
|
||||||
|
background: #900;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
dd.log-message.fatal .log-name {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return css
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue