# HG changeset patch # User Michael Granger # Date 1210184524 0 # Node ID 1d3cfd4837a8e108b577d6aab7a31cf218b46348 # Parent 1b096869b568810ce17777934f6ff7f30ad68ac9 Filled out the project, added Ezmlm module + spec. diff -r 1b096869b568 -r 1d3cfd4837a8 LICENSE --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/LICENSE Wed May 07 18:22:04 2008 +0000 @@ -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. diff -r 1b096869b568 -r 1d3cfd4837a8 README --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/README Wed May 07 18:22:04 2008 +0000 @@ -0,0 +1,45 @@ += ThingFish + +== Authors + +* Michael Granger +* Jeremiah Jordan + +== 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 + diff -r 1b096869b568 -r 1d3cfd4837a8 Rakefile --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Rakefile Wed May 07 18:22:04 2008 +0000 @@ -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 +# + +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 + diff -r 1b096869b568 -r 1d3cfd4837a8 lib/ezmlm.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/ezmlm.rb Wed May 07 18:22:04 2008 +0000 @@ -0,0 +1,61 @@ +#!/usr/bin/ruby +# +# A Ruby programmatic interface to the ezmlm-idx mailing list system +# +# == Version +# +# $Id$ +# +# == Authors +# +# * Michael Granger +# * Jeremiah Jordan +# +# :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: diff -r 1b096869b568 -r 1d3cfd4837a8 lib/ezmlm/list.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/ezmlm/list.rb Wed May 07 18:22:04 2008 +0000 @@ -0,0 +1,30 @@ +#!/usr/bin/ruby +# +# A Ruby interface to a single Ezmlm-idx mailing list directory +# +# == Version +# +# $Id$ +# +# == Authors +# +# * Michael Granger +# * Jeremiah Jordan +# +# :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: diff -r 1b096869b568 -r 1d3cfd4837a8 rake/helpers.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rake/helpers.rb Wed May 07 18:22:04 2008 +0000 @@ -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 and Zenin +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 msg 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 prompt_string 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 prompt_string via #prompt, +### substituting the given default 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 + + diff -r 1b096869b568 -r 1d3cfd4837a8 rake/packaging.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rake/packaging.rb Wed May 07 18:22:04 2008 +0000 @@ -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 + + + diff -r 1b096869b568 -r 1d3cfd4837a8 rake/rdoc.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rake/rdoc.rb Wed May 07 18:22:04 2008 +0000 @@ -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 + diff -r 1b096869b568 -r 1d3cfd4837a8 rake/style.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rake/style.rb Wed May 07 18:22:04 2008 +0000 @@ -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 + + diff -r 1b096869b568 -r 1d3cfd4837a8 rake/svn.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rake/svn.rb Wed May 07 18:22:04 2008 +0000 @@ -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 + diff -r 1b096869b568 -r 1d3cfd4837a8 rake/testing.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rake/testing.rb Wed May 07 18:22:04 2008 +0000 @@ -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 + + diff -r 1b096869b568 -r 1d3cfd4837a8 rake/verifytask.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rake/verifytask.rb Wed May 07 18:22:04 2008 +0000 @@ -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{(\d+\.\d+)%} + + # 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: diff -r 1b096869b568 -r 1d3cfd4837a8 spec/ezmlm/list_spec.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/spec/ezmlm/list_spec.rb Wed May 07 18:22:04 2008 +0000 @@ -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 + + diff -r 1b096869b568 -r 1d3cfd4837a8 spec/ezmlm_spec.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/spec/ezmlm_spec.rb Wed May 07 18:22:04 2008 +0000 @@ -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 + + diff -r 1b096869b568 -r 1d3cfd4837a8 spec/lib/helpers.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/spec/lib/helpers.rb Wed May 07 18:22:04 2008 +0000 @@ -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 " " + @header_red = true + end + + unless @example_group_red + css_class = 'example_group_%d' % [current_example_group_number] + @output.puts " " + @example_group_red = true + end + + move_progress() + + @output.puts "
", + " #{h(example.description)}", + "
" + if failure.exception + backtrace = format_backtrace( failure.exception.backtrace ) + message = failure.exception.message + + @output.puts "
#{h message}
", + "
#{backtrace}
" + end + + if extra = extra_failure_content( failure ) + @output.puts( extra ) + end + + @output.puts "
", + "
" + @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 + +