rake/svn.rb
changeset 9 143e61e24c08
parent 8 308f7dc97753
child 10 389e66b0d38e
equal deleted inserted replaced
8:308f7dc97753 9:143e61e24c08
     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