rake-deveiate 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,103 @@
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
3
+
4
+ require 'erb'
5
+
6
+ require 'rake/deveiate' unless defined?( Rake::DevEiate )
7
+
8
+
9
+ # Project-file generation tasks
10
+ module Rake::DevEiate::Generate
11
+
12
+
13
+ # Template files
14
+ README_TEMPLATE = 'README.erb'
15
+ HISTORY_TEMPLATE = 'History.erb'
16
+
17
+ # RVM metadata files
18
+ RUBY_VERSION_FILE = Rake::DevEiate::PROJECT_DIR + '.ruby-version'
19
+ GEMSET_FILE = Rake::DevEiate::PROJECT_DIR + '.ruby-gemset'
20
+
21
+ # Flags to use when opening a file for generation
22
+ FILE_CREATION_FLAGS = File::WRONLY | File::CREAT | File::EXCL
23
+
24
+
25
+ ### Define generation tasks.
26
+ def define_tasks
27
+ super if defined?( super )
28
+
29
+ file( self.readme_file.to_s )
30
+ file( self.history_file.to_s )
31
+ file( self.manifest_file.to_s )
32
+ file( RUBY_VERSION_FILE.to_s )
33
+ file( GEMSET_FILE.to_s )
34
+
35
+ task( self.readme_file, &method(:do_generate_readme_file) )
36
+ task( self.history_file, &method(:do_generate_history_file) )
37
+ task( self.manifest_file, &method(:do_generate_manifest_file) )
38
+ task( RUBY_VERSION_FILE, &method(:do_generate_ruby_version_file) )
39
+ task( GEMSET_FILE, &method(:do_generate_gemset_file) )
40
+
41
+ task :generate => [
42
+ self.readme_file,
43
+ self.history_file,
44
+ self.manifest_file,
45
+ RUBY_VERSION_FILE,
46
+ GEMSET_FILE,
47
+ ]
48
+ end
49
+
50
+
51
+
52
+ ### Generate a README file if one doesn't already exist. Error if one does.
53
+ def do_generate_readme_file( task, args )
54
+ self.generate_from_template( task.name, README_TEMPLATE )
55
+ end
56
+
57
+
58
+
59
+ ### Generate a History file if one doesn't already exist. Error if one does.
60
+ def do_generate_history_file( task, args )
61
+ self.generate_from_template( task.name, HISTORY_TEMPLATE )
62
+ end
63
+
64
+
65
+ ### Generate a manifest with a default set of files listed.
66
+ def do_generate_manifest_file( task, args )
67
+ self.prompt.ok "Generating #{task.name}..."
68
+ File.open( task.name, FILE_CREATION_FLAGS, 0644, encoding: 'utf-8' ) do |io|
69
+ io.puts( *self.project_files )
70
+ end
71
+ end
72
+
73
+
74
+ ### Generate a file that sets the project's working Ruby version.
75
+ def do_generate_ruby_version_file( task, args )
76
+ self.prompt.ok "Generating #{task.name}..."
77
+ File.open( task.name, FILE_CREATION_FLAGS, 0644, encoding: 'utf-8' ) do |io|
78
+ io.puts( RUBY_VERSION.sub(/\.\d+$/, '') )
79
+ end
80
+ end
81
+
82
+
83
+ ### Generate a file that sets the project's gemset
84
+ def do_generate_gemset_file( task, args )
85
+ self.prompt.ok "Generating #{task.name}..."
86
+ File.open( task.name, FILE_CREATION_FLAGS, 0644, encoding: 'utf-8' ) do |io|
87
+ io.puts( self.name )
88
+ end
89
+ end
90
+
91
+
92
+ ### Generate the given +filename+ from the template filed at +template_path+.
93
+ def generate_from_template( filename, template_path )
94
+ self.prompt.ok "Generating #{filename}..."
95
+ File.open( filename, FILE_CREATION_FLAGS, 0644, encoding: 'utf-8' ) do |io|
96
+ result = self.load_and_render_template( template_path )
97
+ io.print( result )
98
+ end
99
+ end
100
+
101
+ end # module Rake::DevEiate::Hg
102
+
103
+
@@ -0,0 +1,542 @@
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
3
+
4
+ require 'tempfile'
5
+ require 'shellwords'
6
+ require 'hglib'
7
+ require 'tty/editor'
8
+
9
+ require 'rake/deveiate' unless defined?( Rake::DevEiate )
10
+
11
+
12
+ # Version-control tasks
13
+ module Rake::DevEiate::Hg
14
+
15
+ # The name of the file to edit for the commit message
16
+ COMMIT_MSG_FILE = Pathname( 'commit-msg.txt' )
17
+
18
+ # The name of the ignore file
19
+ IGNORE_FILE = Rake::DevEiate::PROJECT_DIR + '.hgignore'
20
+
21
+ # The prefix to use for release version tags by default
22
+ DEFAULT_RELEASE_TAG_PREFIX = 'v'
23
+
24
+ # Colors for presenting file statuses
25
+ STATUS_COLORS = {
26
+ 'M' => [:blue], # modified
27
+ 'A' => [:bold, :green], # added
28
+ 'R' => [:bold, :black], # removed
29
+ 'C' => [:white], # clean
30
+ '!' => [:bold, :white, :on_red], # missing
31
+ '?' => [:yellow], # not tracked
32
+ 'I' => [:dim, :white], # ignored
33
+ }
34
+
35
+ # File indentation
36
+ FILE_INDENT = " • "
37
+
38
+
39
+ ### Set up defaults
40
+ def setup( _name, **options )
41
+ super if defined?( super )
42
+
43
+ @release_tag_prefix = options[:release_tag_prefix] || DEFAULT_RELEASE_TAG_PREFIX
44
+ @sign_tags = options[:sign_tags] || true
45
+ end
46
+
47
+
48
+ ##
49
+ # The prefix to use for version tags
50
+ attr_accessor :release_tag_prefix
51
+
52
+ ##
53
+ # Boolean: if true, sign tags after creating them
54
+ attr_accessor :sign_tags
55
+
56
+
57
+ ### Define version-control tasks
58
+ def define_tasks
59
+ super if defined?( super )
60
+
61
+ return unless File.directory?( '.hg' )
62
+
63
+ file COMMIT_MSG_FILE.to_s do |task|
64
+ edit_commit_log( task.name )
65
+ end
66
+
67
+ CLEAN.include( COMMIT_MSG_FILE.to_s )
68
+
69
+ namespace :hg do
70
+
71
+ desc "Prepare for a new release"
72
+ task( :prerelease, &method(:do_hg_prerelease) )
73
+
74
+ desc "Check for new files and offer to add/ignore/delete them."
75
+ task( :newfiles, &method(:do_hg_newfiles) )
76
+ task :add => :newfiles
77
+
78
+ desc "Pull and update from the default repo"
79
+ task( :pull, &method(:do_hg_pull) )
80
+
81
+ desc "Pull and update without confirmation"
82
+ task( :pull_without_confirmation, &method(:do_hg_pull_without_confirmation) )
83
+
84
+ desc "Update to tip"
85
+ task( :update, &method(:do_hg_update) )
86
+
87
+ desc "Clobber all changes (hg up -C)"
88
+ task( :update_and_clobber, &method(:do_hg_update_and_clobber) )
89
+
90
+ desc "Mercurial-specific pre-checkin hook"
91
+ task :precheckin
92
+
93
+ desc "Mercurial-specific pre-release hook"
94
+ task :prerelease => 'hg:check_history'
95
+
96
+ desc "Check the current code in if tests pass"
97
+ task( :checkin => [:pull, :newfiles, :precheckin, COMMIT_MSG_FILE.to_s], &method(:do_hg_checkin) )
98
+
99
+ desc "Mercurial-specific post-release hook"
100
+ task( :postrelease, &method(:do_hg_postrelease) )
101
+
102
+ desc "Push to the default origin repo (if there is one)"
103
+ task( :push, &method(:do_hg_push) )
104
+
105
+ desc "Push to the default repo without confirmation"
106
+ task :push_without_confirmation do |task, args|
107
+ self.hg.push
108
+ end
109
+
110
+ desc "Check the history file to ensure it contains an entry for each release tag"
111
+ task( :check_history, &method(:do_hg_check_history) )
112
+
113
+ desc "Generate and edit a new version entry in the history file"
114
+ task( :update_history, &method(:do_hg_update_history) )
115
+
116
+ task( :debug, &method(:do_hg_debug) )
117
+ end
118
+
119
+
120
+ # Hook some generic tasks to the mercurial-specific ones
121
+ task :ci => 'hg:checkin'
122
+
123
+ task :prerelease => 'hg:prerelease'
124
+ task :precheckin => 'hg:precheckin'
125
+ task :debug => 'hg:debug'
126
+ task :postrelease => 'hg:postrelease'
127
+
128
+ desc "Update the history file with the changes since the last version tag."
129
+ task :update_history => 'hg:update_history'
130
+
131
+ rescue ::Exception => err
132
+ $stderr.puts "%s while defining Mercurial tasks: %s" % [ err.class.name, err.message ]
133
+ raise
134
+ end
135
+
136
+
137
+ ### The body of the hg:prerelease task.
138
+ def do_hg_prerelease( task, args )
139
+ uncommitted_files = self.hg.status( n: true )
140
+ unless uncommitted_files.empty?
141
+ self.show_file_statuses( uncommitted_files )
142
+
143
+ fail unless self.prompt.yes?( "Release anyway?" ) do |q|
144
+ q.default( false )
145
+ end
146
+
147
+ self.prompt.warn "Okay, releasing with uncommitted versions."
148
+ end
149
+
150
+ pkg_version_tag = self.current_version_tag
151
+
152
+ # Look for a tag for the current release version, and if it exists abort
153
+ if self.hg.tags.find {|tag| tag.name == pkg_version_tag }
154
+ self.prompt.error "Version #{self.version} already has a tag."
155
+ fail
156
+ end
157
+
158
+ if self.sign_tags
159
+ message = "Signing %s" % [ pkg_version_tag ]
160
+ self.prompt.ok( message )
161
+ self.hg.sign( message: message )
162
+ end
163
+
164
+ # Tag the current rev
165
+ rev = self.hg.identify
166
+ self.prompt.ok "Tagging rev %s as %s" % [ rev, pkg_version_tag ]
167
+ self.hg.tag( pkg_version_tag )
168
+ end
169
+
170
+
171
+ ### The body of the hg:postrelease task.
172
+ def do_hg_postrelease( task, args )
173
+ if self.hg.status( 'checksum', unknown: true ).any?
174
+ self.prompt.say "Adding release artifacts..."
175
+ self.hg.add( 'checksum' )
176
+ self.hg.commit( 'checksum', message: "Adding release checksum." )
177
+ end
178
+
179
+ if self.prompt.yes?( "Move released changesets to public phase?" )
180
+ self.prompt.say "Publicising changesets..."
181
+ self.hg.phase( :public )
182
+ end
183
+
184
+ Rake::Take['hg:push'].invoke
185
+ end
186
+
187
+
188
+ ### The body of the hg:newfiles task.
189
+ def do_hg_newfiles( task, args )
190
+ self.prompt.say "Checking for new files..."
191
+
192
+ entries = self.hg.status( no_status: true, unknown: true )
193
+
194
+ unless entries.empty?
195
+ files_to_add = []
196
+ files_to_ignore = []
197
+ files_to_delete = []
198
+
199
+ entries.each do |entry|
200
+ description = " %s: %s" % [ entry.path, entry.status_description ]
201
+ action = self.prompt.select( description ) do |menu|
202
+ menu.choice "add", :a
203
+ menu.choice "ignore", :i
204
+ menu.choice "skip", :s
205
+ menu.choice "delete", :d
206
+ end
207
+
208
+ case action
209
+ when :a
210
+ files_to_add << entry.path
211
+ when :i
212
+ files_to_ignore << entry.path
213
+ when :d
214
+ files_to_delete << entry.path
215
+ end
216
+ end
217
+
218
+ unless files_to_add.empty?
219
+ self.hg.add( *files_to_add )
220
+ end
221
+
222
+ unless files_to_ignore.empty?
223
+ hg_ignore_files( *files_to_ignore )
224
+ end
225
+
226
+ unless files_to_delete.empty?
227
+ delete_extra_files( *files_to_delete )
228
+ end
229
+ end
230
+ end
231
+
232
+
233
+ ### The body of the hg:pull task.
234
+ def do_hg_pull( task, args )
235
+ paths = self.hg.paths
236
+ if origin_url = paths[:default]
237
+ if self.prompt.yes?( "Pull and update from '#{origin_url}'?" )
238
+ self.hg.pull_update
239
+ end
240
+ else
241
+ trace "Skipping pull: No 'default' path."
242
+ end
243
+ end
244
+
245
+
246
+ ### The body of the hg:pull_without_confirmation task.
247
+ def do_hg_pull_without_confirmation( task, args )
248
+ self.hg.pull
249
+ end
250
+
251
+
252
+ ### The body of the hg:update task.
253
+ def do_hg_update( task, args )
254
+ self.hg.pull_update
255
+ end
256
+
257
+
258
+ ### The body of the hg:update_and_clobber task.
259
+ def do_hg_update_and_clobber( task, args )
260
+ self.hg.update( clean: true )
261
+ end
262
+
263
+
264
+ ### The body of the checkin task.
265
+ def do_hg_checkin( task, args )
266
+ targets = args.extras
267
+ self.prompt.say( self.pastel.cyan( "---\n", COMMIT_MSG_FILE.read, "---\n" ) )
268
+ if self.prompt.yes?( "Continue with checkin?" )
269
+ self.hg.commit( *targets, logfile: COMMIT_MSG_FILE.to_s )
270
+ rm_f COMMIT_MSG_FILE
271
+ else
272
+ abort
273
+ end
274
+ Rake::Task[ 'hg:push' ].invoke
275
+ end
276
+
277
+
278
+ ### The body of the push task.
279
+ def do_hg_push( task, args )
280
+ paths = self.hg.paths
281
+ if origin_url = paths[:default]
282
+ if self.prompt.yes?( "Push to '#{origin_url}'?" ) {|q| q.default(false) }
283
+ self.hg.push
284
+ self.prompt.ok "Done."
285
+ else
286
+ abort
287
+ end
288
+ else
289
+ trace "Skipping push: No 'default' path."
290
+ end
291
+ end
292
+
293
+
294
+ ### Check the history file against the list of release tags in the working copy
295
+ ### and ensure there's an entry for each tag.
296
+ def do_hg_check_history( task, args )
297
+ unless self.history_file.readable?
298
+ self.prompt.error "History file is missing or unreadable."
299
+ abort
300
+ end
301
+
302
+ self.prompt.say "Checking history..."
303
+ missing_tags = self.get_unhistoried_version_tags
304
+
305
+ unless missing_tags.empty?
306
+ self.prompt.error "%s needs updating; missing entries for tags: %s" %
307
+ [ self.history_file, missing_tags.join(', ') ]
308
+ abort
309
+ end
310
+ end
311
+
312
+
313
+ ### Generate a new history file entry for the current version.
314
+ def do_hg_update_history( task, args ) # Needs refactoring
315
+ unless self.history_file.readable?
316
+ self.prompt.error "History file is missing or unreadable."
317
+ abort
318
+ end
319
+
320
+ version_tag = self.current_version_tag
321
+ previous_tag = self.previous_version_tag
322
+ self.prompt.say "Updating history for %s..." % [ version_tag ]
323
+
324
+ if self.get_history_file_versions.include?( version_tag )
325
+ self.log.ok "History file already includes a section for %s" % [ version_tag ]
326
+ abort
327
+ end
328
+
329
+ header, rest = self.history_file.read( encoding: 'utf-8' ).
330
+ split( /(?<=^---)/m, 2 )
331
+
332
+ self.trace "Rest is: %p" % [ rest ]
333
+ if !rest || rest.empty?
334
+ self.prompt.warn "History file needs a header with a `---` marker to support updating."
335
+ self.prompt.say "Adding an auto-generated one."
336
+ rest = header
337
+ header = self.load_and_render_template( 'History.erb', self.history_file )
338
+ end
339
+
340
+ header_char = self.header_char_for( self.history_file )
341
+ ext = self.history_file.extname
342
+ log_entries = if previous_tag
343
+ self.hg.log( rev: "#{previous_tag}~-2::" )
344
+ else
345
+ self.hg.log
346
+ end
347
+
348
+ Tempfile.create( ['History', ext], encoding: 'utf-8' ) do |tmp_copy|
349
+ tmp_copy.print( header )
350
+ tmp_copy.puts
351
+
352
+ tmp_copy.puts "%s %s [%s] %s" % [
353
+ header_char * 2,
354
+ version_tag,
355
+ Date.today.strftime( '%Y-%m-%d' ),
356
+ self.authors.first,
357
+ ]
358
+
359
+ tmp_copy.puts
360
+ log_entries.each do |entry|
361
+ tmp_copy.puts "- %s" % [ entry.summary ]
362
+ end
363
+ tmp_copy.puts
364
+ tmp_copy.puts
365
+
366
+ tmp_copy.print( rest )
367
+ tmp_copy.close
368
+
369
+ TTY::Editor.open( tmp_copy.path )
370
+
371
+ if File.size?( tmp_copy.path )
372
+ cp( tmp_copy.path, self.history_file )
373
+ else
374
+ self.prompt.error "Empty file: aborting."
375
+ end
376
+ end
377
+
378
+ end
379
+
380
+
381
+ ### Show debugging information.
382
+ def do_hg_debug( task, args )
383
+ self.prompt.say( "Hg Info", color: :bright_green )
384
+
385
+ self.prompt.say( "Mercurial version: " )
386
+ self.prompt.say( Hglib.version, color: :bold )
387
+ self.prompt.say( "Release tag prefix: " )
388
+ self.prompt.say( self.release_tag_prefix, color: :bold )
389
+
390
+ self.prompt.say( "Version tags:" )
391
+ self.get_version_tag_names.each do |tag|
392
+ self.prompt.say( '- ' )
393
+ self.prompt.say( tag, color: :bold )
394
+ end
395
+
396
+ self.prompt.say( "History file versions:" )
397
+ self.get_history_file_versions.each do |tag|
398
+ self.prompt.say( '- ' )
399
+ self.prompt.say( tag, color: :bold )
400
+ end
401
+
402
+ self.prompt.say( "Unhistoried version tags:" )
403
+ self.get_unhistoried_version_tags.each do |tag|
404
+ self.prompt.say( '- ' )
405
+ self.prompt.say( tag, color: :bold )
406
+ end
407
+
408
+ self.prompt.say( "\n" )
409
+ end
410
+
411
+ #
412
+ # utility methods
413
+ #
414
+
415
+ ### Return an Hglib::Repo for the directory rake was invoked in, creating it if
416
+ ### necessary.
417
+ def hg
418
+ @hg ||= Hglib.repo( Rake::DevEiate::PROJECT_DIR )
419
+ end
420
+
421
+
422
+ ### Given a +status_hash+ like that returned by Hglib::Repo.status, return a
423
+ ### string description of the files and their status.
424
+ def show_file_statuses( statuses )
425
+ lines = statuses.map do |entry|
426
+ status_color = STATUS_COLORS[ entry.status ]
427
+ " %s: %s" % [
428
+ self.pastel.white( entry.path.to_s ),
429
+ self.pastel.decorate( entry.status_description, *status_color ),
430
+ ]
431
+ end
432
+
433
+ self.prompt.say( self.pastel.headline "Uncommitted files:" )
434
+ self.prompt.say( lines.join("\n") )
435
+ end
436
+
437
+
438
+ ### Fetch the name of the current version's tag.
439
+ def current_version_tag
440
+ return [ self.release_tag_prefix, self.version ].join
441
+ end
442
+
443
+
444
+ ### Fetch the name of the tag for the previous version.
445
+ def previous_version_tag
446
+ return self.get_version_tag_names.first
447
+ end
448
+
449
+
450
+ ### Return a Regexp that matches the project's convention for versions.
451
+ def release_tag_pattern
452
+ prefix = self.release_tag_prefix
453
+ return /#{prefix}\d+(\.\d+)+/
454
+ end
455
+
456
+
457
+ ### Fetch the list of names of tags that match the versioning scheme of this
458
+ ### project.
459
+ def get_version_tag_names
460
+ tag_pattern = self.release_tag_pattern
461
+ return self.hg.tags.map( &:name ).grep( tag_pattern )
462
+ end
463
+
464
+
465
+ ### Fetch the list of the versions of releases that have entries in the history
466
+ ### file.
467
+ def get_history_file_versions
468
+ tag_pattern = self.release_tag_pattern
469
+
470
+ return IO.readlines( self.history_file ).grep( tag_pattern ).map do |line|
471
+ line[ /^(?:h\d\.|#+|=+)\s+(#{tag_pattern})\s+/, 1 ]
472
+ end.compact
473
+ end
474
+
475
+
476
+ ### Read the list of tags and return any that don't have a corresponding section
477
+ ### in the history file.
478
+ def get_unhistoried_version_tags( include_current_version: true )
479
+ release_tags = self.get_version_tag_names
480
+ release_tags.unshift( self.current_version_tag ) if include_current_version
481
+
482
+ self.get_history_file_versions.each do |tag|
483
+ release_tags.delete( tag )
484
+ end
485
+
486
+ return release_tags
487
+ end
488
+
489
+
490
+ ### Generate a commit log and invoke the user's editor on it.
491
+ def edit_commit_log( logfile )
492
+ diff = self.hg.diff
493
+
494
+ File.open( logfile, 'w' ) do |fh|
495
+ fh.print( diff )
496
+ end
497
+
498
+ TTY::Editor.open( logfile )
499
+ end
500
+
501
+
502
+ ### Add the list of +pathnames+ to the .hgignore list.
503
+ def hg_ignore_files( *pathnames )
504
+ patterns = pathnames.flatten.collect do |path|
505
+ '^' + Regexp.escape( path.to_s ) + '$'
506
+ end
507
+ self.trace "Ignoring %d files." % [ pathnames.length ]
508
+
509
+ IGNORE_FILE.open( File::CREAT|File::WRONLY|File::APPEND, 0644 ) do |fh|
510
+ fh.puts( patterns )
511
+ end
512
+ end
513
+
514
+
515
+ ### Delete the files in the given +filelist+ after confirming with the user.
516
+ def delete_extra_files( *filelist )
517
+ description = humanize_file_list( filelist, ' ' )
518
+ self.prompt.say "Files to delete:"
519
+ self.prompt.say( description )
520
+
521
+ if self.prompt.yes?( "Really delete them?" ) {|q| q.default(false) }
522
+ filelist.each do |f|
523
+ rm_rf( f, verbose: true )
524
+ end
525
+ end
526
+ end
527
+
528
+
529
+ ### Returns a human-scannable file list by joining and truncating the list if it's too long.
530
+ def humanize_file_list( list, indent=FILE_INDENT )
531
+ listtext = list[0..5].join( "\n#{indent}" )
532
+ if list.length > 5
533
+ listtext << " (and %d other/s)" % [ list.length - 5 ]
534
+ end
535
+
536
+ return listtext
537
+ end
538
+
539
+
540
+ end # module Rake::DevEiate::Hg
541
+
542
+
@@ -0,0 +1,42 @@
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
3
+
4
+ require 'rubygems/package_task'
5
+
6
+ require 'rake/deveiate' unless defined?( Rake::DevEiate )
7
+
8
+
9
+ # Packaging tasks and functions
10
+ module Rake::DevEiate::Packaging
11
+
12
+ ### Post-loading hook -- set up default attributes.
13
+ def setup( name, **options )
14
+ super if defined?( super )
15
+
16
+ gem_basename = "%s-%s" % [ name, self.version ]
17
+
18
+ @gem_filename = gem_basename + '.gem'
19
+ @gem_path = Rake::DevEiate::PKG_DIR + @gem_filename
20
+ end
21
+
22
+ ##
23
+ # The filename of the generated gemfile
24
+ attr_reader :gem_filename
25
+
26
+ ##
27
+ # The Pathname of the generated gemfile
28
+ attr_reader :gem_path
29
+
30
+
31
+ ### Set up packaging tasks.
32
+ def define_tasks
33
+ super if defined?( super )
34
+
35
+ spec = self.gemspec
36
+ Gem::PackageTask.new( spec ).define
37
+
38
+ task :release_gem => :gem
39
+ end
40
+
41
+ end # module Rake::DevEiate::Packaging
42
+