|
1 ##################################################################### |
|
2 ### S U B V E R S I O N T A S K S A N D H E L P E R S |
|
3 ##################################################################### |
|
4 |
|
5 require 'pp' |
|
6 require 'yaml' |
|
7 require 'English' |
|
8 |
|
9 # Strftime format for tags/releases |
|
10 TAG_TIMESTAMP_FORMAT = '%Y%m%d-%H%M%S' |
|
11 TAG_TIMESTAMP_PATTERN = /\d{4}\d{2}\d{2}-\d{6}/ |
|
12 |
|
13 RELEASE_VERSION_PATTERN = /\d+\.\d+\.\d+/ |
|
14 |
|
15 DEFAULT_EDITOR = 'vi' |
|
16 DEFAULT_KEYWORDS = %w[Date Rev Author URL Id] |
|
17 KEYWORDED_FILEDIRS = %w[applets bin etc lib misc] |
|
18 KEYWORDED_FILEPATTERN = /^(?:Rakefile|.*\.(?:rb|js|html|template))$/i |
|
19 |
|
20 COMMIT_MSG_FILE = 'commit-msg.txt' |
|
21 |
|
22 ### |
|
23 ### Subversion-specific Helpers |
|
24 ### |
|
25 |
|
26 ### Return a new tag for the given time |
|
27 def make_new_tag( time=Time.now ) |
|
28 return time.strftime( TAG_TIMESTAMP_FORMAT ) |
|
29 end |
|
30 |
|
31 |
|
32 ### Get the subversion information for the current working directory as |
|
33 ### a hash. |
|
34 def get_svn_info( dir='.' ) |
|
35 info = IO.read( '|-' ) or exec 'svn', 'info', dir |
|
36 return YAML.load( info ) # 'svn info' outputs valid YAML! Yay! |
|
37 end |
|
38 |
|
39 |
|
40 ### Get a list of the objects registered with subversion under the specified directory and |
|
41 ### return them as an Array of Pathame objects. |
|
42 def get_svn_filelist( dir='.' ) |
|
43 list = IO.read( '|-' ) or exec 'svn', 'st', '-v', '--ignore-externals', dir |
|
44 |
|
45 # Split into lines, filter out the unknowns, and grab the filenames as Pathnames |
|
46 # :FIXME: This will break if we ever put in a file with spaces in its name. This |
|
47 # will likely be the least of our worries if we do so, however, so it's not worth |
|
48 # the additional complexity to make it handle that case. If we do need that, there's |
|
49 # always the --xml output for 'svn st'... |
|
50 return list.split( $/ ). |
|
51 reject {|line| line =~ /^\?/ }. |
|
52 collect {|fn| Pathname(fn[/\S+$/]) } |
|
53 end |
|
54 |
|
55 ### Return the URL to the repository root for the specified +dir+. |
|
56 def get_svn_repo_root( dir='.' ) |
|
57 info = get_svn_info( dir ) |
|
58 return info['URL'].sub( %r{/trunk$}, '' ) |
|
59 end |
|
60 |
|
61 |
|
62 ### Return the Subversion URL to the given +dir+. |
|
63 def get_svn_url( dir='.' ) |
|
64 info = get_svn_info( dir ) |
|
65 return info['URL'] |
|
66 end |
|
67 |
|
68 |
|
69 ### Return the path of the specified +dir+ under the svn root of the |
|
70 ### checkout. |
|
71 def get_svn_path( dir='.' ) |
|
72 root = get_svn_repo_root( dir ) |
|
73 url = get_svn_url( dir ) |
|
74 |
|
75 return url.sub( root + '/', '' ) |
|
76 end |
|
77 |
|
78 |
|
79 ### Return the keywords for the specified array of +files+ as a Hash keyed by filename. |
|
80 def get_svn_keyword_map( files ) |
|
81 cmd = ['svn', 'pg', 'svn:keywords', *files] |
|
82 |
|
83 # trace "Executing: svn pg svn:keywords " + files.join(' ') |
|
84 output = IO.read( '|-' ) or exec( 'svn', 'pg', 'svn:keywords', *files ) |
|
85 |
|
86 kwmap = {} |
|
87 output.split( "\n" ).each do |line| |
|
88 next if line !~ /\s+-\s+/ |
|
89 path, keywords = line.split( /\s+-\s+/, 2 ) |
|
90 kwmap[ path ] = keywords.split |
|
91 end |
|
92 |
|
93 return kwmap |
|
94 end |
|
95 |
|
96 |
|
97 ### Return the latest revision number of the specified +dir+ as an Integer. |
|
98 def get_svn_rev( dir='.' ) |
|
99 info = get_svn_info( dir ) |
|
100 return info['Revision'] |
|
101 end |
|
102 |
|
103 |
|
104 ### Return a list of the entries at the specified Subversion url. If |
|
105 ### no +url+ is specified, it will default to the list in the URL |
|
106 ### corresponding to the current working directory. |
|
107 def svn_ls( url=nil ) |
|
108 url ||= get_svn_url() |
|
109 list = IO.read( '|-' ) or exec 'svn', 'ls', url |
|
110 |
|
111 trace 'svn ls of %s: %p' % [url, list] if $trace |
|
112 |
|
113 return [] if list.nil? || list.empty? |
|
114 return list.split( $INPUT_RECORD_SEPARATOR ) |
|
115 end |
|
116 |
|
117 |
|
118 ### Return the URL of the latest timestamp in the tags directory. |
|
119 def get_latest_svn_timestamp_tag |
|
120 rooturl = get_svn_repo_root() |
|
121 tagsurl = rooturl + '/tags' |
|
122 |
|
123 tags = svn_ls( tagsurl ).grep( TAG_TIMESTAMP_PATTERN ).sort |
|
124 return nil if tags.nil? || tags.empty? |
|
125 return tagsurl + '/' + tags.last |
|
126 end |
|
127 |
|
128 |
|
129 ### Get a subversion diff of the specified targets and return it. If no targets are |
|
130 ### specified, the current directory will be diffed instead. |
|
131 def get_svn_diff( *targets ) |
|
132 targets << BASEDIR if targets.empty? |
|
133 trace "Getting svn diff for targets: %p" % [targets] |
|
134 log = IO.read( '|-' ) or exec 'svn', 'diff', *(targets.flatten) |
|
135 |
|
136 return log |
|
137 end |
|
138 |
|
139 |
|
140 ### Return the URL of the latest timestamp in the tags directory. |
|
141 def get_latest_release_tag |
|
142 rooturl = get_svn_repo_root() |
|
143 releaseurl = rooturl + '/releases' |
|
144 |
|
145 tags = svn_ls( releaseurl ).grep( RELEASE_VERSION_PATTERN ).sort_by do |tag| |
|
146 tag.split('.').collect {|i| Integer(i) } |
|
147 end |
|
148 return nil if tags.empty? |
|
149 |
|
150 return releaseurl + '/' + tags.last |
|
151 end |
|
152 |
|
153 |
|
154 ### Extract a diff from the specified subversion working +dir+, rewrite its |
|
155 ### file lines as Trac links, and return it. |
|
156 def make_svn_commit_log( dir='.' ) |
|
157 editor_prog = ENV['EDITOR'] || ENV['VISUAL'] || DEFAULT_EDITOR |
|
158 |
|
159 diff = IO.read( '|-' ) or exec 'svn', 'diff' |
|
160 fail "No differences." if diff.empty? |
|
161 |
|
162 return diff |
|
163 end |
|
164 |
|
165 |
|
166 |
|
167 ### |
|
168 ### Tasks |
|
169 ### |
|
170 |
|
171 desc "Subversion tasks" |
|
172 namespace :svn do |
|
173 |
|
174 desc "Copy the HEAD revision of the current trunk/ to tags/ with a " + |
|
175 "current timestamp." |
|
176 task :tag do |
|
177 svninfo = get_svn_info() |
|
178 tag = make_new_tag() |
|
179 svntrunk = svninfo['URL'] |
|
180 svntagdir = svninfo['URL'].sub( %r{trunk$}, 'tags' ) |
|
181 svntag = svntagdir + '/' + tag |
|
182 |
|
183 desc = "Tagging trunk as #{svntag}" |
|
184 ask_for_confirmation( desc ) do |
|
185 msg = prompt_with_default( "Commit log: ", "Tagging for code push" ) |
|
186 run 'svn', 'cp', '-m', msg, svntrunk, svntag |
|
187 end |
|
188 end |
|
189 |
|
190 |
|
191 desc "Copy the most recent tag to releases/#{PKG_VERSION}" |
|
192 task :release do |
|
193 last_tag = get_latest_svn_timestamp_tag() |
|
194 svninfo = get_svn_info() |
|
195 release = PKG_VERSION |
|
196 svnrel = svninfo['URL'] + '/releases' |
|
197 svnrelease = svnrel + '/' + release |
|
198 |
|
199 if last_tag.nil? |
|
200 error "There are no tags in the repository" |
|
201 fail |
|
202 end |
|
203 |
|
204 releases = svn_ls( svnrel ) |
|
205 trace "Releases: %p" % [releases] |
|
206 if releases.include?( release ) |
|
207 error "Version #{release} already has a branch (#{svnrelease}). Did you mean" + |
|
208 "to increment the version in #{PKG_VERSION_FROM}?" |
|
209 fail |
|
210 else |
|
211 trace "No #{svnrel} version currently exists" |
|
212 end |
|
213 |
|
214 desc = "Release tag\n #{last_tag}\nto\n #{svnrelease}" |
|
215 ask_for_confirmation( desc ) do |
|
216 msg = prompt_with_default( "Commit log: ", "Branching for release" ) |
|
217 run 'svn', 'cp', '-m', msg, last_tag, svnrelease |
|
218 end |
|
219 end |
|
220 |
|
221 ### Task for debugging the #get_target_args helper |
|
222 task :show_targets do |
|
223 $stdout.puts "Targets from ARGV (%p): %p" % [ARGV, get_target_args()] |
|
224 end |
|
225 |
|
226 |
|
227 desc "Generate a commit log" |
|
228 task :commitlog => [COMMIT_MSG_FILE] |
|
229 |
|
230 desc "Show the (pre-edited) commit log for the current directory" |
|
231 task :show_commitlog => [COMMIT_MSG_FILE] do |
|
232 ask_for_confirmation( "Confirm? " ) do |
|
233 args = get_target_args() |
|
234 puts get_svn_diff( *args ) |
|
235 end |
|
236 end |
|
237 |
|
238 |
|
239 file COMMIT_MSG_FILE do |
|
240 args = get_target_args() |
|
241 diff = get_svn_diff( *args ) |
|
242 |
|
243 File.open( COMMIT_MSG_FILE, File::WRONLY|File::EXCL|File::CREAT ) do |fh| |
|
244 fh.print( diff ) |
|
245 end |
|
246 |
|
247 editor = ENV['EDITOR'] || ENV['VISUAL'] || DEFAULT_EDITOR |
|
248 system editor, COMMIT_MSG_FILE |
|
249 unless $?.success? |
|
250 fail "Editor exited uncleanly." |
|
251 end |
|
252 end |
|
253 |
|
254 |
|
255 desc "Update from Subversion" |
|
256 task :update do |
|
257 run 'svn', 'up', '--ignore-externals' |
|
258 end |
|
259 |
|
260 |
|
261 desc "Check in all the changes in your current working copy" |
|
262 task :checkin => ['svn:update', 'coverage:verify', 'svn:fix_keywords', COMMIT_MSG_FILE] do |
|
263 targets = get_target_args() |
|
264 $deferr.puts '---', File.read( COMMIT_MSG_FILE ), '---' |
|
265 ask_for_confirmation( "Continue with checkin?" ) do |
|
266 run 'svn', 'ci', '-F', COMMIT_MSG_FILE, targets |
|
267 rm_f COMMIT_MSG_FILE |
|
268 end |
|
269 end |
|
270 task :commit => :checkin |
|
271 task :ci => :checkin |
|
272 |
|
273 |
|
274 task :clean do |
|
275 rm_f COMMIT_MSG_FILE |
|
276 end |
|
277 |
|
278 |
|
279 desc "Check and fix any missing keywords for any files in the project which need them" |
|
280 task :fix_keywords do |
|
281 log "Checking subversion keywords..." |
|
282 paths = get_svn_filelist( BASEDIR ). |
|
283 select {|path| path.file? && path.to_s =~ KEYWORDED_FILEPATTERN } |
|
284 |
|
285 trace "Looking at %d paths for keywords:\n %p" % [paths.length, paths] |
|
286 kwmap = get_svn_keyword_map( paths ) |
|
287 |
|
288 buf = '' |
|
289 PP.pp( kwmap, buf, 132 ) |
|
290 trace "keyword map is: %s" % [buf] |
|
291 |
|
292 files_needing_fixups = paths.find_all do |path| |
|
293 (kwmap[path.to_s] & DEFAULT_KEYWORDS) != DEFAULT_KEYWORDS |
|
294 end |
|
295 |
|
296 unless files_needing_fixups.empty? |
|
297 $deferr.puts "Files needing keyword fixes: ", |
|
298 files_needing_fixups.collect {|f| |
|
299 " %s: %s" % [f, kwmap[f] ? kwmap[f].join(' ') : "(no keywords)"] |
|
300 } |
|
301 ask_for_confirmation( "Will add default keywords to these files." ) do |
|
302 run 'svn', 'ps', 'svn:keywords', DEFAULT_KEYWORDS.join(' '), *files_needing_fixups |
|
303 end |
|
304 else |
|
305 log "Keywords are all up to date." |
|
306 end |
|
307 end |
|
308 |
|
309 |
|
310 task :debug_helpers do |
|
311 methods = [ |
|
312 :make_new_tag, |
|
313 :get_svn_info, |
|
314 :get_svn_repo_root, |
|
315 :get_svn_url, |
|
316 :get_svn_path, |
|
317 :svn_ls, |
|
318 :get_latest_svn_timestamp_tag, |
|
319 ] |
|
320 maxlen = methods.collect {|sym| sym.to_s.length }.max |
|
321 |
|
322 methods.each do |meth| |
|
323 res = send( meth ) |
|
324 puts "%*s => %p" % [ maxlen, colorize(meth.to_s, :cyan), res ] |
|
325 end |
|
326 end |
|
327 end |
|
328 |