Module: Hoe::Deveiate

Defined in:
lib/hoe/deveiate.rb

Overview

A collection of Rake tasks and utility functions I use to maintain my Open Source projects.

Constant Summary collapse

VERSION =

Library version constant

'0.10.0'
REVISION =

Version-control revision constant

%q$Revision: a6ae8e318c90 $
TRAILING_WHITESPACE_RE =

Regexp to match trailing whitespace

/[ \t]+$/
SADFACE =

Emoji for style advisories

"\u{1f622}"
RVM_GEMSET =

The name of the RVM gemset

Pathname( '.gems' )

Instance Attribute Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#email_fromObject

Who to send announcement emails as



102
103
104
# File 'lib/hoe/deveiate.rb', line 102

def email_from
  @email_from
end

#email_toObject (readonly)

Where to send announcement emails



99
100
101
# File 'lib/hoe/deveiate.rb', line 99

def email_to
  @email_to
end

#quality_check_whitelistObject (readonly)

The Rake::FileList that contains files that shouldn't be considered when doing quality checks



106
107
108
# File 'lib/hoe/deveiate.rb', line 106

def quality_check_whitelist
  @quality_check_whitelist
end

Instance Method Details

#define_announce_tasksObject

Define tasks used to announce new releases



311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
# File 'lib/hoe/deveiate.rb', line 311

def define_announce_tasks

  # Avoid broken Hoe 3.0 task
  Rake::Task[:announce].clear if Rake::Task.task_defined?( :announce )
  Rake::Task[:send_email].clear if Rake::Task.task_defined?( :send_email )

  desc "Announce a new release"
  task :announce => :send_email

  desc "Send a release announcement to: %p" % [ @email_to ]
  task :send_email do
    mail = generate_mail()

    say "<%= color 'About to send this email:', :subheader %>"
    say( mail.to_s )

    smtp_host = @email_config['host'] || ask( "Email host: " )
    smtp_port = @email_config['port'] || 'smtp'
    smtp_port = Socket.getservbyname( smtp_port.to_s )
    smtp_user = @email_config['user']

    if agree( "\n<%= color 'Okay to send it?', :warning %> ")
      require 'socket'
      require 'net/smtp'
      require 'etc'

      username = smtp_user || ask( "Email username: " ) do |q|
        q.default = Etc.getlogin  # default to the current user
      end
      password = ask( "Email password for #{username}: " ) do |q|
        q.echo = color( '*', :yellow ) # Hide the password
      end

      say "Creating SMTP connection to #{smtp_host}:#{smtp_port}"
      smtp = Net::SMTP.new( smtp_host, smtp_port )
      smtp.set_debug_output( $stdout )
      smtp.esmtp = true

      ssl_context = OpenSSL::SSL::SSLContext.new
      smtp.enable_starttls( ssl_context )

      helo = Socket.gethostname
      smtp.start( helo, username, password, :plain ) do |smtp|
        mail.delivery_method( :smtp_connection, :connection => smtp )
        mail.deliver
      end


    else
      abort "Okay, aborting."
    end
  end

end

#define_deveiate_tasksObject

Add tasks



110
111
112
113
114
115
# File 'lib/hoe/deveiate.rb', line 110

def define_deveiate_tasks
  self.define_quality_tasks
  self.define_sanitycheck_tasks
  self.define_packaging_tasks
  self.define_announce_tasks
end

#define_packaging_tasksObject

Set up tasks for use in packaging.



183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
# File 'lib/hoe/deveiate.rb', line 183

def define_packaging_tasks

  ### Task: prerelease
  unless Rake::Task.task_defined?( :pre )
    desc "Append the package build number to package versions"
    task :pre do
      rev = get_numeric_rev()
      trace "Current rev is: %p" % [ rev ]
      $hoespec.spec.version.version << "pre#{rev}"
      Rake::Task[:gem].clear

      Gem::PackageTask.new( $hoespec.spec ) do |pkg|
        pkg.need_zip = true
        pkg.need_tar = true
      end
    end
  end

  namespace :deps do

    if RVM_GEMSET.exist?
      desc "Update the project's RVM gemset"
      task :gemset do
        deps = make_gemset_recommendations( $hoespec.spec )
        updates = deps.values.compact

        if !updates.empty?
          $stderr.puts "%d gems in the current gemset have newer matching versions:" %
             [ updates.length ]
          deps.each do |old, newer|
            next unless newer
            $stderr.puts "  #{old} -> #{newer}"
          end

          if ask( "Update? " )
            update_rvm_gemset( deps )
            run 'rvm', 'gemset', 'import', RVM_GEMSET.to_s
          end
        end
      end
    end

  end

  ### Make the ChangeLog update if the repo has changed since it was last built
  file '.hg/branch'
  file 'ChangeLog' => '.hg/branch' do |task|
    $stderr.puts "Updating the changelog..."
    content = make_changelog()
    File.open( task.name, 'w', 0644 ) do |fh|
      fh.print( content )
    end
  end

end

#define_quality_tasksObject

Set up tasks for various code-quality checks.



119
120
121
122
123
124
125
126
127
128
# File 'lib/hoe/deveiate.rb', line 119

def define_quality_tasks
  self.define_whitespace_checker_tasks

  # Quality-check before checking in
  task 'hg:precheckin' => :quality_check
  task 'git:precheckin' => :quality_check

  desc "Run several quality-checks on the code"
  task :quality_check => [ :check_whitespace ]
end

#define_sanitycheck_tasksObject

Set up some sanity-checks as dependencies of higher-level tasks



170
171
172
173
174
175
176
177
178
179
# File 'lib/hoe/deveiate.rb', line 170

def define_sanitycheck_tasks

  task 'hg:precheckin' => [:spec] if File.directory?( 'spec' )
  task 'hg:prep_release' => [ :check_manifest, :check_history ]

  # Rebuild the ChangeLog immediately before release
  task :check_manifest => 'ChangeLog'
  task :prerelease => 'ChangeLog'

end

#define_whitespace_checker_tasksObject

Set up tasks that check for poor whitespace discipline



132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/hoe/deveiate.rb', line 132

def define_whitespace_checker_tasks

  desc "Check source code for inconsistent whitespace"
  task :check_whitespace => [
    :check_for_trailing_whitespace,
    :check_for_mixed_indentation,
  ]

  desc "Check source code for trailing whitespace"
  task :check_for_trailing_whitespace do
    lines = find_matching_source_lines do |line, _|
      line =~ TRAILING_WHITESPACE_RE
    end

    unless lines.empty?
      desc = "Found some trailing whitespace"
      describe_lines_that_need_fixing( desc, lines, TRAILING_WHITESPACE_RE )
      fail
    end
  end

  desc "Check source code for mixed indentation"
  task :check_for_mixed_indentation do
    lines = find_matching_source_lines do |line, _|
      line =~ /(?<!#)([ ]\t)/
    end

    unless lines.empty?
      desc = "Found mixed indentation"
      describe_lines_that_need_fixing( desc, lines, /[ ]\t/ )
      fail
    end
  end

end

#describe_lines_that_need_fixing(description, lines, re) ⇒ Object

Output a listing of the specified lines with the given description, highlighting the characters matched by the specified re.



394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
# File 'lib/hoe/deveiate.rb', line 394

def describe_lines_that_need_fixing( description, lines, re )
  say "\n"
  say SADFACE + "  " + color( "Oh noes! " + description, :header )

  grouped_lines = group_line_matches( lines )

  grouped_lines.each do |filename, linegroups|
    linegroups.each do |group, lines|
      if group.min == group.max
        say color("%s:%d" % [ filename, group.min ], :bold)
      else
        say color("%s:%d-%d" % [ filename, group.min, group.max ], :bold)
      end

      lines.each_with_index do |line, i|
        say "%s: %s" % [
          color( group.to_a[i].to_s, :dark, :white ),
          highlight_problems( line, re )
        ]
      end
      say "\n"
    end
  end
end

#find_matching_source_linesObject

Return tuples of the form:

[ <filename>, <line number>, <line> ]

for every line in the Gemspec's source files for which the block returns true.



373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
# File 'lib/hoe/deveiate.rb', line 373

def find_matching_source_lines
  matches = []

  source_files = $hoespec.spec.files.grep( /\.(h|c|rb)$/ )
  source_files -= self.quality_check_whitelist

  source_files.each do |filename|
    previous_line = nil

    IO.foreach( filename ).with_index do |line, i|
      matches << [filename, i + 1, line] if yield( line, previous_line )
      previous_line = line
    end
  end

  return matches
end

#generate_mailObject

Generate an announcement email.



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/hoe/deveiate.rb', line 68

def generate_mail
  $stderr.puts "Generating an announcement email."
  abort "no email config in your ~/.hoerc" unless defined?( @email_config )

    changes = self.changes
    subject = "#{self.name} #{self.version} Released"
    title   = "#{self.name} version #{self.version} has been released!"
    body    = "#{self.description}\n\nChanges:\n\n#{self.changes}"
    urls    = self.urls.map do |url|
    case url
    when Array
      "* <#{url[1].strip}> (#{url[0]})"
    when String
      "* <#{url.strip}>"
    else
      "* %p" % [ url ]
    end
  end

  $stderr.puts "  returning a new Mail::Message."
  mail         = Mail.new
  mail.from    = @email_config['from'] || "%s <%s>" % self.developer.first
  mail.to      = @email_to.join(", ")
  mail.subject = "[ANN] #{subject}"
  mail.body    = [ title, urls.join($/), body ].join( $/ * 2 )

  return mail
end

#group_line_matches(tuples) ⇒ Object

Return a Hash, keyed by filename, whose values are tuples of Ranges and lines extracted from the given [filename, linenumber, line] tuples.



422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
# File 'lib/hoe/deveiate.rb', line 422

def group_line_matches( tuples )
  by_file = tuples.group_by {|tuple| tuple.first }

  return by_file.each_with_object({}) do |(filename, lines), hash|
    last_linenum = 0
    linegroups = lines.slice_before do |filename, linenum|
      gap = linenum > last_linenum + 1
      last_linenum = linenum
      gap
    end

    hash[ filename ] = linegroups.map do |group|
      rng = group.first[1] .. group.last[1]
      grouplines = group.transpose.last
      [ rng, grouplines ]
    end
  end
end

#highlight_problems(line, re) ⇒ Object

Transform invisibles in the specified line into visible analogues.



443
444
445
446
447
# File 'lib/hoe/deveiate.rb', line 443

def highlight_problems( line, re )
  line \
    .gsub( re )    { color $&, :on_red } \
    .gsub( /\t+/ ) { color "\u{21e5}   " * $&.length, :dark, :white }
end

#initialize_deveiateObject

Set up defaults



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/hoe/deveiate.rb', line 37

def initialize_deveiate
  $hoespec = self
  abort "requires the hoe-mercurial plugin" unless Hoe.plugins.include?( :mercurial )

  minor_version = VERSION[ /^\d+\.\d+/ ]
  self.extra_dev_deps << ['hoe-deveiate', "~> #{minor_version}"] unless
    self.name == 'hoe-deveiate'

  @email_to ||= []
  @email_from = nil unless defined?( @email_from )
  self.hg_sign_tags = true
  self.check_history_on_release = true

    with_config do |config, _|
    self.spec_extras[:signing_key] = config['signing_key_file'] or
      abort "no signing key ('signing_key_file') configured."
    @email_config = config['email']
    @email_to = if @email_config
        Array( @email_config['to'] )
      else
        ['nobody@nowhere']
      end
    end

  @quality_check_whitelist = Rake::FileList.new

  $stderr.puts "Done initializing hoe-deveiate" if Rake.application.options.trace
end

#make_gemset_recommendations(gemspec) ⇒ Object

Return a Hash of Gem::Dependency objects, the keys of which are dependencies in the current gemspec, and the values are which are either nil if the current gemset contains the latest version of the gem which matches the dependency, or the newer version if there is a newer one.



283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
# File 'lib/hoe/deveiate.rb', line 283

def make_gemset_recommendations( gemspec )
  recommendations = {}
  fetcher = Gem::SpecFetcher.fetcher

  gemspec.dependencies.each do |dep|
    newer_dep = nil

    if (( mspec = dep.matching_specs.last ))
      newer_dep = Gem::Dependency.new( dep.name, dep.requirement, "> #{mspec.version}" )
    else
      newer_dep = Gem::Dependency.new( dep.name, dep.requirement )
    end

    remotes, _ = fetcher.search_for_dependency( newer_dep )
    remotes.map! {|gem, _| gem.version }

    if remotes.empty?
      recommendations[ dep ] = nil
    else
      recommendations[ dep ] = remotes.last
    end
  end

  return recommendations
end

Print out the list of dependency calls that should be included in the Hoespec in the Rakefile.



272
273
274
275
276
# File 'lib/hoe/deveiate.rb', line 272

def print_hoespec_dependencies( deps )
  deps.each_key do |dep|
    $stderr.puts "self.dependency '%s', '%s'" % [ dep.name, dep.version.to_s ]
  end
end

#update_rvm_gemset(deps) ⇒ Object

Update the contents of .rvm.gems to include the latest gems.



241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
# File 'lib/hoe/deveiate.rb', line 241

def update_rvm_gemset( deps )
  tmp = Tempfile.new( 'gemset' )
  deps.keys.each {|dep| deps[dep.name] = deps.delete(dep) }

  RVM_GEMSET.each_line do |line|
    if line =~ /^\s*(#|$)/
      tmp.print( line )
    else
      gem, version = line.split( /\s+/, 2 )

      if (( newer = deps.delete(gem) ))
        tmp.puts( gem + ' -v' + newer.to_s )
      else
        tmp.print( line )
      end
    end
  end

  deps.each do |gem, newer|
    next unless newer
    tmp.puts( gem + ' -v' + newer.to_s )
  end

  tmp.close

  FileUtils.cp( tmp.path, RVM_GEMSET, :verbose => true )
end