rake-deveiate 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+