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.6.0'
REVISION =

Version-control revision constant

%q$Revision: e52ebafe9f20 $
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( '.rvm.gems' )

Instance Attribute Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#email_fromObject

Who to send announcement emails as



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

def email_from
  @email_from
end

#email_toObject (readonly)

Where to send announcement emails



97
98
99
# File 'lib/hoe/deveiate.rb', line 97

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



104
105
106
# File 'lib/hoe/deveiate.rb', line 104

def quality_check_whitelist
  @quality_check_whitelist
end

Instance Method Details

#define_announce_tasksObject

Define tasks used to announce new releases



309
310
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 309

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

			# Don't verify the server cert, as my server's cert is self-signed
			ssl_context = OpenSSL::SSL::SSLContext.new
			ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
			smtp.enable_ssl( 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



108
109
110
111
112
113
# File 'lib/hoe/deveiate.rb', line 108

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.



181
182
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
# File 'lib/hoe/deveiate.rb', line 181

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.



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

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



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

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



130
131
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
# File 'lib/hoe/deveiate.rb', line 130

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.



66
67
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
# File 'lib/hoe/deveiate.rb', line 66

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
# 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|
		linegroups = lines.slice_before( [0] ) do |line, last_linenum|
			rval = line[1] > last_linenum.first.succ
			last_linenum[0] = line[1]
			rval
		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.



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

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



35
36
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
# File 'lib/hoe/deveiate.rb', line 35

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.



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

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.



270
271
272
273
274
# File 'lib/hoe/deveiate.rb', line 270

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.



239
240
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
# File 'lib/hoe/deveiate.rb', line 239

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