io-reactor 0.05 → 1.0.4

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,468 @@
1
+ #
2
+ # Subversion Rake Tasks
3
+ # $Id: svn.rb 33 2008-08-14 05:39:37Z deveiant $
4
+ #
5
+ # Authors:
6
+ # * Michael Granger <ged@FaerieMUD.org>
7
+ #
8
+
9
+
10
+ require 'pp'
11
+ require 'yaml'
12
+ require 'date'
13
+
14
+ # Strftime format for tags/releases
15
+ TAG_TIMESTAMP_FORMAT = '%Y%m%d-%H%M%S'
16
+ TAG_TIMESTAMP_PATTERN = /\d{4}\d{2}\d{2}-\d{6}/
17
+
18
+ RELEASE_VERSION_PATTERN = /\d+\.\d+\.\d+/
19
+
20
+ DEFAULT_EDITOR = 'vi'
21
+ DEFAULT_KEYWORDS = %w[Date Rev Author URL Id]
22
+ KEYWORDED_FILEDIRS = %w[applets spec bin etc ext experiments examples lib misc docs]
23
+ KEYWORDED_FILEPATTERN = /
24
+ ^(?:
25
+ (?:meta)?rakefile.* # Rakefiles
26
+ |
27
+ .*\.(?:rb|c|h|js|html|css|template|erb) # Source file extensions
28
+ |
29
+ readme|install|todo
30
+ )$/ix
31
+
32
+ COMMIT_MSG_FILE = 'commit-msg.txt'
33
+
34
+ SVN_TRUNK_DIR = 'trunk' unless defined?( SVN_TRUNK_DIR )
35
+ SVN_RELEASES_DIR = 'branches' unless defined?( SVN_RELEASES_DIR )
36
+ SVN_BRANCHES_DIR = 'branches' unless defined?( SVN_BRANCHES_DIR )
37
+ SVN_TAGS_DIR = 'tags' unless defined?( SVN_TAGS_DIR )
38
+
39
+ FILE_INDENT = " " * 12
40
+ LOG_INDENT = " " * 3
41
+
42
+
43
+
44
+ ###
45
+ ### Subversion-specific Helpers
46
+ ###
47
+
48
+ ### Return a new tag for the given time
49
+ def make_new_tag( time=Time.now )
50
+ return time.strftime( TAG_TIMESTAMP_FORMAT )
51
+ end
52
+
53
+
54
+ ### Get the subversion information for the current working directory as
55
+ ### a hash.
56
+ def get_svn_info( dir='.' )
57
+ return {} unless File.directory?( File.join(dir, '.svn') )
58
+ info = IO.read( '|-' ) or exec 'svn', 'info', dir
59
+ return YAML.load( info ) # 'svn info' outputs valid YAML! Yay!
60
+ end
61
+
62
+
63
+ ### Get a list of the objects registered with subversion under the specified directory and
64
+ ### return them as an Array of Pathame objects.
65
+ def get_svn_filelist( dir='.' )
66
+ list = IO.read( '|-' ) or exec 'svn', 'st', '-v', '--ignore-externals', dir
67
+
68
+ # Split into lines, filter out the unknowns, and grab the filenames as Pathnames
69
+ # :FIXME: This will break if we ever put in a file with spaces in its name. This
70
+ # will likely be the least of our worries if we do so, however, so it's not worth
71
+ # the additional complexity to make it handle that case. If we do need that, there's
72
+ # always the --xml output for 'svn st'...
73
+ return list.split( $/ ).
74
+ reject {|line| line =~ /^\?/ }.
75
+ collect {|fn| Pathname(fn[/\S+$/]) }
76
+ end
77
+
78
+ ### Return the URL to the repository root for the specified +dir+.
79
+ def get_svn_repo_root( dir='.' )
80
+ info = get_svn_info( dir )
81
+ return info['Repository Root']
82
+ end
83
+
84
+
85
+ ### Return the Subversion URL to the given +dir+.
86
+ def get_svn_url( dir='.' )
87
+ info = get_svn_info( dir )
88
+ return info['URL']
89
+ end
90
+
91
+
92
+ ### Return the path of the specified +dir+ under the svn root of the
93
+ ### checkout.
94
+ def get_svn_path( dir='.' )
95
+ root = get_svn_repo_root( dir )
96
+ url = get_svn_url( dir )
97
+
98
+ return url.sub( root + '/', '' )
99
+ end
100
+
101
+
102
+ ### Return the keywords for the specified array of +files+ as a Hash keyed by filename.
103
+ def get_svn_keyword_map( files )
104
+ cmd = ['svn', 'pg', 'svn:keywords', *files]
105
+
106
+ # trace "Executing: svn pg svn:keywords " + files.join(' ')
107
+ output = IO.read( '|-' ) or exec( 'svn', 'pg', 'svn:keywords', *files )
108
+
109
+ kwmap = {}
110
+ output.split( "\n" ).each do |line|
111
+ next if line !~ /\s+-\s+/
112
+ path, keywords = line.split( /\s+-\s+/, 2 )
113
+ kwmap[ path ] = keywords.split
114
+ end
115
+
116
+ return kwmap
117
+ end
118
+
119
+
120
+ ### Return the latest revision number of the specified +dir+ as an Integer.
121
+ def get_svn_rev( dir='.' )
122
+ info = get_svn_info( dir )
123
+ return info['Revision']
124
+ end
125
+
126
+
127
+ ### Return the latest revision number of the specified +dir+ as an Integer.
128
+ def get_last_changed_rev( dir='.' )
129
+ info = get_svn_info( dir )
130
+ return info['Last Changed Rev']
131
+ end
132
+
133
+
134
+ ### Return a list of the entries at the specified Subversion url. If
135
+ ### no +url+ is specified, it will default to the list in the URL
136
+ ### corresponding to the current working directory.
137
+ def svn_ls( url=nil )
138
+ url ||= get_svn_url()
139
+ list = IO.read( '|-' ) or exec 'svn', 'ls', url
140
+
141
+ trace 'svn ls of %s: %p' % [url, list] if $trace
142
+
143
+ return [] if list.nil? || list.empty?
144
+ return list.split( $INPUT_RECORD_SEPARATOR )
145
+ end
146
+
147
+
148
+ ### Return the URL of the latest timestamp in the tags directory.
149
+ def get_latest_svn_timestamp_tag
150
+ rooturl = get_svn_repo_root()
151
+ tagsurl = rooturl + "/#{SVN_TAGS_DIR}"
152
+
153
+ tags = svn_ls( tagsurl ).grep( TAG_TIMESTAMP_PATTERN ).sort
154
+ return nil if tags.nil? || tags.empty?
155
+ return tagsurl + '/' + tags.last
156
+ end
157
+
158
+
159
+ ### Get a subversion diff of the specified targets and return it. If no targets are
160
+ ### specified, the current directory will be diffed instead.
161
+ def get_svn_diff( *targets )
162
+ targets << BASEDIR if targets.empty?
163
+ trace "Getting svn diff for targets: %p" % [targets]
164
+ log = IO.read( '|-' ) or exec 'svn', 'diff', *(targets.flatten)
165
+
166
+ return log
167
+ end
168
+
169
+
170
+ ### Return the URL of the latest timestamp in the tags directory.
171
+ def get_latest_release_tag
172
+ rooturl = get_svn_repo_root()
173
+ releaseurl = rooturl + "/#{SVN_RELEASES_DIR}"
174
+
175
+ tags = svn_ls( releaseurl ).grep( RELEASE_VERSION_PATTERN ).sort_by do |tag|
176
+ tag[RELEASE_VERSION_PATTERN].split('.').collect {|i| Integer(i) }
177
+ end
178
+ return nil if tags.empty?
179
+
180
+ return releaseurl + '/' + tags.last
181
+ end
182
+
183
+
184
+ ### Extract a diff from the specified subversion working +dir+ and return it.
185
+ def make_svn_commit_log( dir='.' )
186
+ diff = IO.read( '|-' ) or exec 'svn', 'diff'
187
+ fail "No differences." if diff.empty?
188
+
189
+ return diff
190
+ end
191
+
192
+
193
+ ### Extract the svn log from the specified subversion working +dir+,
194
+ ### starting from rev +start+ and ending with rev +finish+, and return it.
195
+ def make_svn_log( dir='.', start='PREV', finish='HEAD' )
196
+ trace "svn log -r#{start}:#{finish} #{dir}"
197
+ log = IO.read( '|-' ) or exec 'svn', 'log', "-r#{start}:#{finish}", dir
198
+ fail "No log between #{start} and #{finish}." if log.empty?
199
+
200
+ return log
201
+ end
202
+
203
+
204
+ ### Extract the verbose XML svn log from the specified subversion working +dir+,
205
+ ### starting from rev +start+ and ending with rev +finish+, and return it.
206
+ def make_xml_svn_log( dir='.', start='PREV', finish='HEAD' )
207
+ trace "svn log --xml --verbose -r#{start}:#{finish} #{dir}"
208
+ log = IO.read( '|-' ) or exec 'svn', 'log', '--verbose', '--xml', "-r#{start}:#{finish}", dir
209
+ fail "No log between #{start} and #{finish}." if log.empty?
210
+
211
+ return log
212
+ end
213
+
214
+
215
+ ### Create a changelog from the subversion log of the specified +dir+ and return it.
216
+ def make_svn_changelog( dir='.' )
217
+ require 'xml/libxml'
218
+
219
+ changelog = ''
220
+ path_prefix = '/' + get_svn_path( dir ) + '/'
221
+
222
+ xmllog = make_xml_svn_log( dir, 0 )
223
+
224
+ parser = XML::Parser.string( xmllog )
225
+ root = parser.parse.root
226
+ root.find( '//log/logentry' ).to_a.reverse.each do |entry|
227
+ trace "Making a changelog entry for r%s" % [ entry['revision'] ]
228
+
229
+ added = []
230
+ deleted = []
231
+ changed = []
232
+
233
+ entry.find( 'paths/path').each do |path|
234
+ pathname = path.content
235
+ pathname.sub!( path_prefix , '' ) if pathname.count('/') > 1
236
+
237
+ case path['action']
238
+ when 'A', 'R'
239
+ if path['copyfrom-path']
240
+ verb = path['action'] == 'A' ? 'renamed' : 'copied'
241
+ added << "%s\n#{FILE_INDENT}-> #{verb} from %s@r%s" % [
242
+ pathname,
243
+ path['copyfrom-path'],
244
+ path['copyfrom-rev'],
245
+ ]
246
+ else
247
+ added << "%s (new)" % [ pathname ]
248
+ end
249
+
250
+ when 'M'
251
+ changed << pathname
252
+
253
+ when 'D'
254
+ deleted << pathname
255
+
256
+ else
257
+ log "Unknown action %p in rev %d" % [ path['action'], entry['revision'] ]
258
+ end
259
+
260
+ end
261
+
262
+ date = Time.parse( entry.find_first('date').content )
263
+
264
+ # cvs2svn doesn't set 'author'
265
+ author = 'unknown'
266
+ if entry.find_first( 'author' )
267
+ author = entry.find_first( 'author' ).content
268
+ end
269
+
270
+ msg = entry.find_first( 'msg' ).content
271
+ rev = entry['revision']
272
+
273
+ changelog << "-- #{date.rfc2822} by #{author} (r#{rev}) -----\n"
274
+ changelog << " Added: " << humanize_file_list(added) << "\n" unless added.empty?
275
+ changelog << " Changed: " << humanize_file_list(changed) << "\n" unless changed.empty?
276
+ changelog << " Deleted: " << humanize_file_list(deleted) << "\n" unless deleted.empty?
277
+ changelog << "\n"
278
+
279
+ indent = msg[/^(\s*)/] + LOG_INDENT
280
+
281
+ changelog << indent << msg.strip.gsub(/\n\s*/m, "\n#{indent}")
282
+ changelog << "\n\n\n"
283
+ end
284
+
285
+ return changelog
286
+ end
287
+
288
+
289
+ ### Returns a human-scannable file list by joining and truncating the list if it's too long.
290
+ def humanize_file_list( list )
291
+ listtext = list[0..5].join( "\n#{FILE_INDENT}" )
292
+ if list.length > 5
293
+ listtext << " (and %d other/s)" % [ list.length - 5 ]
294
+ end
295
+
296
+ return listtext
297
+ end
298
+
299
+
300
+
301
+ ###
302
+ ### Tasks
303
+ ###
304
+
305
+ desc "Subversion tasks"
306
+ namespace :svn do
307
+
308
+ desc "Copy the HEAD revision of the current #{SVN_TRUNK_DIR}/ to #{SVN_TAGS_DIR}/ with a " +
309
+ "current timestamp."
310
+ task :tag do
311
+ svninfo = get_svn_info()
312
+ tag = make_new_tag()
313
+ svntrunk = svninfo['Repository Root'] + "/#{SVN_TRUNK_DIR}"
314
+ svntagdir = svninfo['Repository Root'] + "/#{SVN_TAGS_DIR}"
315
+ svntag = svntagdir + '/' + tag
316
+
317
+ desc = "Tagging trunk as #{svntag}"
318
+ ask_for_confirmation( desc ) do
319
+ msg = prompt_with_default( "Commit log: ", "Tagging for code push" )
320
+ run 'svn', 'cp', '-m', msg, svntrunk, svntag
321
+ end
322
+ end
323
+
324
+
325
+ desc "Copy the most recent tag to #{SVN_RELEASES_DIR}/#{PKG_VERSION}"
326
+ task :release do
327
+ last_tag = get_latest_svn_timestamp_tag()
328
+ svninfo = get_svn_info()
329
+ svnroot = svninfo['Repository Root']
330
+ svntrunk = svnroot + "/#{SVN_TRUNK_DIR}"
331
+ svnrel = svnroot + "/#{SVN_RELEASES_DIR}"
332
+ release = PKG_VERSION
333
+ svnrelease = svnrel + '/' + release
334
+
335
+ topdirs = svn_ls( svnroot ).collect {|dir| dir.chomp('/') }
336
+ unless topdirs.include?( SVN_RELEASES_DIR )
337
+ trace "Top directories (%p) does not include %p" %
338
+ [ topdirs, SVN_RELEASES_DIR ]
339
+ log "Releases path #{svnrel} does not exist."
340
+ ask_for_confirmation( "To continue I'll need to create it." ) do
341
+ run 'svn', 'mkdir', svnrel, '-m', 'Creating releases/ directory'
342
+ end
343
+ else
344
+ trace "Found release dir #{SVN_RELEASES_DIR} in the top directories %p" %
345
+ [ topdirs ]
346
+ end
347
+
348
+ releases = svn_ls( svnrel ).collect {|name| name.sub(%r{/$}, '') }
349
+ trace "Releases: %p" % [releases]
350
+ if releases.include?( release )
351
+ error "Version #{release} already has a branch (#{svnrelease}). Did you mean " +
352
+ "to increment the version in #{VERSION_FILE}?"
353
+ fail
354
+ else
355
+ trace "No #{release} version currently exists"
356
+ end
357
+
358
+ desc = "Tagging trunk as #{svnrelease}..."
359
+ ask_for_confirmation( desc ) do
360
+ msg = prompt_with_default( "Commit log: ", "Branching for release" )
361
+ run 'svn', 'cp', '-m', msg, svntrunk, svnrelease
362
+ end
363
+ end
364
+
365
+ ### Task for debugging the #get_target_args helper
366
+ task :show_targets do
367
+ $stdout.puts "Targets from ARGV (%p): %p" % [ARGV, get_target_args()]
368
+ end
369
+
370
+
371
+ desc "Generate a commit log"
372
+ task :commitlog => [COMMIT_MSG_FILE]
373
+
374
+ desc "Show the (pre-edited) commit log for the current directory"
375
+ task :show_commitlog do
376
+ puts make_svn_commit_log()
377
+ end
378
+
379
+
380
+ file COMMIT_MSG_FILE do
381
+ diff = make_svn_commit_log()
382
+
383
+ File.open( COMMIT_MSG_FILE, File::WRONLY|File::EXCL|File::CREAT ) do |fh|
384
+ fh.print( diff )
385
+ end
386
+
387
+ editor = ENV['EDITOR'] || ENV['VISUAL'] || DEFAULT_EDITOR
388
+ system editor, COMMIT_MSG_FILE
389
+ unless $?.success?
390
+ fail "Editor exited uncleanly."
391
+ end
392
+ end
393
+
394
+
395
+ desc "Update from Subversion"
396
+ task :update do
397
+ run 'svn', 'up', '--ignore-externals'
398
+ end
399
+
400
+
401
+ desc "Check in all the changes in your current working copy"
402
+ task :checkin => ['svn:update', 'test', 'svn:fix_keywords', COMMIT_MSG_FILE] do
403
+ targets = get_target_args()
404
+ $deferr.puts '---', File.read( COMMIT_MSG_FILE ), '---'
405
+ ask_for_confirmation( "Continue with checkin?" ) do
406
+ run 'svn', 'ci', '-F', COMMIT_MSG_FILE, targets
407
+ rm_f COMMIT_MSG_FILE
408
+ end
409
+ end
410
+ task :commit => :checkin
411
+ task :ci => :checkin
412
+
413
+
414
+ task :clean do
415
+ rm_f COMMIT_MSG_FILE
416
+ end
417
+
418
+
419
+ desc "Check and fix any missing keywords for any files in the project which need them"
420
+ task :fix_keywords do
421
+ log "Checking subversion keywords..."
422
+ paths = get_svn_filelist( BASEDIR ).
423
+ select {|path| path.file? && path.to_s =~ KEYWORDED_FILEPATTERN }
424
+
425
+ trace "Looking at %d paths for keywords:\n %p" % [paths.length, paths]
426
+ kwmap = get_svn_keyword_map( paths )
427
+
428
+ buf = ''
429
+ PP.pp( kwmap, buf, 132 )
430
+ trace "keyword map is: %s" % [buf]
431
+
432
+ files_needing_fixups = paths.find_all do |path|
433
+ (kwmap[path.to_s] & DEFAULT_KEYWORDS) != DEFAULT_KEYWORDS
434
+ end
435
+
436
+ unless files_needing_fixups.empty?
437
+ $deferr.puts "Files needing keyword fixes: ",
438
+ files_needing_fixups.collect {|f|
439
+ " %s: %s" % [f, kwmap[f] ? kwmap[f].join(' ') : "(no keywords)"]
440
+ }
441
+ ask_for_confirmation( "Will add default keywords to these files." ) do
442
+ run 'svn', 'ps', 'svn:keywords', DEFAULT_KEYWORDS.join(' '), *files_needing_fixups
443
+ end
444
+ else
445
+ log "Keywords are all up to date."
446
+ end
447
+ end
448
+
449
+
450
+ task :debug_helpers do
451
+ methods = [
452
+ :make_new_tag,
453
+ :get_svn_info,
454
+ :get_svn_repo_root,
455
+ :get_svn_url,
456
+ :get_svn_path,
457
+ :svn_ls,
458
+ :get_latest_svn_timestamp_tag,
459
+ ]
460
+ maxlen = methods.collect {|sym| sym.to_s.length }.max
461
+
462
+ methods.each do |meth|
463
+ res = send( meth )
464
+ puts "%*s => %p" % [ maxlen, colorize(meth.to_s, :cyan), res ]
465
+ end
466
+ end
467
+ end
468
+