io-reactor 0.05 → 1.0.4

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