Mercurial > dotfiles
comparison unixSoft/bin/change-svn-wc-format.py @ 0:c30d68fbd368
Initial import from svn.
| author | Augie Fackler <durin42@gmail.com> |
|---|---|
| date | Wed, 26 Nov 2008 10:56:09 -0600 |
| parents | |
| children |
comparison
equal
deleted
inserted
replaced
| -1:000000000000 | 0:c30d68fbd368 |
|---|---|
| 1 #!/usr/bin/env python | |
| 2 # | |
| 3 # change-svn-wc-format.py: Change the format of a Subversion working copy. | |
| 4 # | |
| 5 # ==================================================================== | |
| 6 # Copyright (c) 2007-2008 CollabNet. All rights reserved. | |
| 7 # | |
| 8 # This software is licensed as described in the file COPYING, which | |
| 9 # you should have received as part of this distribution. The terms | |
| 10 # are also available at http://subversion.tigris.org/license-1.html. | |
| 11 # If newer versions of this license are posted there, you may use a | |
| 12 # newer version instead, at your option. | |
| 13 # | |
| 14 # This software consists of voluntary contributions made by many | |
| 15 # individuals. For exact contribution history, see the revision | |
| 16 # history and logs, available at http://subversion.tigris.org/. | |
| 17 # ==================================================================== | |
| 18 | |
| 19 import sys | |
| 20 import os | |
| 21 import getopt | |
| 22 try: | |
| 23 my_getopt = getopt.gnu_getopt | |
| 24 except AttributeError: | |
| 25 my_getopt = getopt.getopt | |
| 26 | |
| 27 # Pretend we have true booleans on older python versions | |
| 28 try: | |
| 29 True | |
| 30 except: | |
| 31 True = 1 | |
| 32 False = 0 | |
| 33 | |
| 34 ### The entries file parser in subversion/tests/cmdline/svntest/entry.py | |
| 35 ### handles the XML-based WC entries file format used by Subversion | |
| 36 ### 1.3 and lower. It could be rolled into this script. | |
| 37 | |
| 38 LATEST_FORMATS = { "1.4" : 8, | |
| 39 "1.5" : 9, | |
| 40 "1.6" : 10, | |
| 41 } | |
| 42 | |
| 43 def usage_and_exit(error_msg=None): | |
| 44 """Write usage information and exit. If ERROR_MSG is provide, that | |
| 45 error message is printed first (to stderr), the usage info goes to | |
| 46 stderr, and the script exits with a non-zero status. Otherwise, | |
| 47 usage info goes to stdout and the script exits with a zero status.""" | |
| 48 progname = os.path.basename(sys.argv[0]) | |
| 49 | |
| 50 stream = error_msg and sys.stderr or sys.stdout | |
| 51 if error_msg: | |
| 52 print >> stream, "ERROR: %s\n" % error_msg | |
| 53 print >> stream, """\ | |
| 54 usage: %s WC_PATH SVN_VERSION [--verbose] [--force] [--skip-unknown-format] | |
| 55 %s --help | |
| 56 | |
| 57 Change the format of a Subversion working copy to that of SVN_VERSION. | |
| 58 | |
| 59 --skip-unknown-format : skip directories with unknown working copy | |
| 60 format and continue the update | |
| 61 """ % (progname, progname) | |
| 62 sys.exit(error_msg and 1 or 0) | |
| 63 | |
| 64 def get_adm_dir(): | |
| 65 """Return the name of Subversion's administrative directory, | |
| 66 adjusted for the SVN_ASP_DOT_NET_HACK environment variable. See | |
| 67 <http://svn.collab.net/repos/svn/trunk/notes/asp-dot-net-hack.txt> | |
| 68 for details.""" | |
| 69 return "SVN_ASP_DOT_NET_HACK" in os.environ and "_svn" or ".svn" | |
| 70 | |
| 71 class WCFormatConverter: | |
| 72 "Performs WC format conversions." | |
| 73 root_path = None | |
| 74 error_on_unrecognized = True | |
| 75 force = False | |
| 76 verbosity = 0 | |
| 77 | |
| 78 def write_dir_format(self, format_nbr, dirname, paths): | |
| 79 """Attempt to write the WC format FORMAT_NBR to the entries file | |
| 80 for DIRNAME. Throws LossyConversionException when not in --force | |
| 81 mode, and unconvertable WC data is encountered.""" | |
| 82 | |
| 83 # Avoid iterating in unversioned directories. | |
| 84 if not (get_adm_dir() in paths): | |
| 85 del paths[:] | |
| 86 return | |
| 87 | |
| 88 # Process the entries file for this versioned directory. | |
| 89 if self.verbosity: | |
| 90 print "Processing directory '%s'" % dirname | |
| 91 entries = Entries(os.path.join(dirname, get_adm_dir(), "entries")) | |
| 92 | |
| 93 if self.verbosity: | |
| 94 print "Parsing file '%s'" % entries.path | |
| 95 try: | |
| 96 entries.parse(self.verbosity) | |
| 97 except UnrecognizedWCFormatException, e: | |
| 98 if self.error_on_unrecognized: | |
| 99 raise | |
| 100 print >>sys.stderr, "%s, skipping" % (e,) | |
| 101 | |
| 102 if self.verbosity: | |
| 103 print "Checking whether WC format can be converted" | |
| 104 try: | |
| 105 entries.assert_valid_format(format_nbr, self.verbosity) | |
| 106 except LossyConversionException, e: | |
| 107 # In --force mode, ignore complaints about lossy conversion. | |
| 108 if self.force: | |
| 109 print "WARNING: WC format conversion will be lossy. Dropping "\ | |
| 110 "field(s) %s " % ", ".join(e.lossy_fields) | |
| 111 else: | |
| 112 raise | |
| 113 | |
| 114 if self.verbosity: | |
| 115 print "Writing WC format" | |
| 116 entries.write_format(format_nbr) | |
| 117 | |
| 118 def change_wc_format(self, format_nbr): | |
| 119 """Walk all paths in a WC tree, and change their format to | |
| 120 FORMAT_NBR. Throw LossyConversionException or NotImplementedError | |
| 121 if the WC format should not be converted, or is unrecognized.""" | |
| 122 os.path.walk(self.root_path, self.write_dir_format, format_nbr) | |
| 123 | |
| 124 class Entries: | |
| 125 """Represents a .svn/entries file. | |
| 126 | |
| 127 'The entries file' section in subversion/libsvn_wc/README is a | |
| 128 useful reference.""" | |
| 129 | |
| 130 # The name and index of each field composing an entry's record. | |
| 131 entry_fields = ( | |
| 132 "name", | |
| 133 "kind", | |
| 134 "revision", | |
| 135 "url", | |
| 136 "repos", | |
| 137 "schedule", | |
| 138 "text-time", | |
| 139 "checksum", | |
| 140 "committed-date", | |
| 141 "committed-rev", | |
| 142 "last-author", | |
| 143 "has-props", | |
| 144 "has-prop-mods", | |
| 145 "cachable-props", | |
| 146 "present-props", | |
| 147 "conflict-old", | |
| 148 "conflict-new", | |
| 149 "conflict-wrk", | |
| 150 "prop-reject-file", | |
| 151 "copied", | |
| 152 "copyfrom-url", | |
| 153 "copyfrom-rev", | |
| 154 "deleted", | |
| 155 "absent", | |
| 156 "incomplete", | |
| 157 "uuid", | |
| 158 "lock-token", | |
| 159 "lock-owner", | |
| 160 "lock-comment", | |
| 161 "lock-creation-date", | |
| 162 "changelist", | |
| 163 "keep-local", | |
| 164 "working-size", | |
| 165 "depth", | |
| 166 "tree-conflicts", | |
| 167 "file-external", | |
| 168 ) | |
| 169 | |
| 170 # The format number. | |
| 171 format_nbr = -1 | |
| 172 | |
| 173 # How many bytes the format number takes in the file. (The format number | |
| 174 # may have leading zeroes after using this script to convert format 10 to | |
| 175 # format 9 -- which would write the format number as '09'.) | |
| 176 format_nbr_bytes = -1 | |
| 177 | |
| 178 def __init__(self, path): | |
| 179 self.path = path | |
| 180 self.entries = [] | |
| 181 | |
| 182 def parse(self, verbosity=0): | |
| 183 """Parse the entries file. Throw NotImplementedError if the WC | |
| 184 format is unrecognized.""" | |
| 185 | |
| 186 input = open(self.path, "r") | |
| 187 | |
| 188 # Read WC format number from INPUT. Validate that it | |
| 189 # is a supported format for conversion. | |
| 190 format_line = input.readline() | |
| 191 try: | |
| 192 self.format_nbr = int(format_line) | |
| 193 self.format_nbr_bytes = len(format_line.rstrip()) # remove '\n' | |
| 194 except ValueError: | |
| 195 self.format_nbr = -1 | |
| 196 self.format_nbr_bytes = -1 | |
| 197 if not self.format_nbr in LATEST_FORMATS.values(): | |
| 198 raise UnrecognizedWCFormatException(self.format_nbr, self.path) | |
| 199 | |
| 200 # Parse file into individual entries, to later inspect for | |
| 201 # non-convertable data. | |
| 202 entry = None | |
| 203 while True: | |
| 204 entry = self.parse_entry(input, verbosity) | |
| 205 if entry is None: | |
| 206 break | |
| 207 self.entries.append(entry) | |
| 208 | |
| 209 input.close() | |
| 210 | |
| 211 def assert_valid_format(self, format_nbr, verbosity=0): | |
| 212 if verbosity >= 2: | |
| 213 print "Validating format for entries file '%s'" % self.path | |
| 214 for entry in self.entries: | |
| 215 if verbosity >= 3: | |
| 216 print "Validating format for entry '%s'" % entry.get_name() | |
| 217 try: | |
| 218 entry.assert_valid_format(format_nbr) | |
| 219 except LossyConversionException: | |
| 220 if verbosity >= 3: | |
| 221 print >> sys.stderr, "Offending entry:" | |
| 222 print >> sys.stderr, str(entry) | |
| 223 raise | |
| 224 | |
| 225 def parse_entry(self, input, verbosity=0): | |
| 226 "Read an individual entry from INPUT stream." | |
| 227 entry = None | |
| 228 | |
| 229 while True: | |
| 230 line = input.readline() | |
| 231 if line in ("", "\x0c\n"): | |
| 232 # EOF or end of entry terminator encountered. | |
| 233 break | |
| 234 | |
| 235 if entry is None: | |
| 236 entry = Entry() | |
| 237 | |
| 238 # Retain the field value, ditching its field terminator ("\x0a"). | |
| 239 entry.fields.append(line[:-1]) | |
| 240 | |
| 241 if entry is not None and verbosity >= 3: | |
| 242 sys.stdout.write(str(entry)) | |
| 243 print "-" * 76 | |
| 244 return entry | |
| 245 | |
| 246 def write_format(self, format_nbr): | |
| 247 # Overwrite all bytes of the format number (which are the first bytes in | |
| 248 # the file). Overwrite format '10' by format '09', which will be converted | |
| 249 # to '9' by Subversion when it rewrites the file. (Subversion 1.4 and later | |
| 250 # ignore leading zeroes in the format number.) | |
| 251 assert len(str(format_nbr)) <= self.format_nbr_bytes | |
| 252 format_string = '%0' + str(self.format_nbr_bytes) + 'd' | |
| 253 | |
| 254 os.chmod(self.path, 0600) | |
| 255 output = open(self.path, "r+", 0) | |
| 256 output.write(format_string % format_nbr) | |
| 257 output.close() | |
| 258 os.chmod(self.path, 0400) | |
| 259 | |
| 260 class Entry: | |
| 261 "Describes an entry in a WC." | |
| 262 | |
| 263 # Maps format numbers to indices of fields within an entry's record that must | |
| 264 # be retained when downgrading to that format. | |
| 265 must_retain_fields = { | |
| 266 # Not in 1.4: changelist, keep-local, depth, tree-conflicts, file-externals | |
| 267 8 : (30, 31, 33, 34, 35), | |
| 268 # Not in 1.5: tree-conflicts, file-externals | |
| 269 9 : (34, 35), | |
| 270 } | |
| 271 | |
| 272 def __init__(self): | |
| 273 self.fields = [] | |
| 274 | |
| 275 def assert_valid_format(self, format_nbr): | |
| 276 "Assure that conversion will be non-lossy by examining fields." | |
| 277 | |
| 278 # Check whether lossy conversion is being attempted. | |
| 279 lossy_fields = [] | |
| 280 for field_index in self.must_retain_fields[format_nbr]: | |
| 281 if len(self.fields) - 1 >= field_index and self.fields[field_index]: | |
| 282 lossy_fields.append(Entries.entry_fields[field_index]) | |
| 283 if lossy_fields: | |
| 284 raise LossyConversionException(lossy_fields, | |
| 285 "Lossy WC format conversion requested for entry '%s'\n" | |
| 286 "Data for the following field(s) is unsupported by older versions " | |
| 287 "of\nSubversion, and is likely to be subsequently discarded, and/or " | |
| 288 "have\nunexpected side-effects: %s\n\n" | |
| 289 "WC format conversion was cancelled, use the --force option to " | |
| 290 "override\nthe default behavior." | |
| 291 % (self.get_name(), ", ".join(lossy_fields))) | |
| 292 | |
| 293 def get_name(self): | |
| 294 "Return the name of this entry." | |
| 295 return len(self.fields) > 0 and self.fields[0] or "" | |
| 296 | |
| 297 def __str__(self): | |
| 298 "Return all fields from this entry as a multi-line string." | |
| 299 rep = "" | |
| 300 for i in range(0, len(self.fields)): | |
| 301 rep += "[%s] %s\n" % (Entries.entry_fields[i], self.fields[i]) | |
| 302 return rep | |
| 303 | |
| 304 | |
| 305 class LocalException(Exception): | |
| 306 """Root of local exception class hierarchy.""" | |
| 307 pass | |
| 308 | |
| 309 class LossyConversionException(LocalException): | |
| 310 "Exception thrown when a lossy WC format conversion is requested." | |
| 311 def __init__(self, lossy_fields, str): | |
| 312 self.lossy_fields = lossy_fields | |
| 313 self.str = str | |
| 314 def __str__(self): | |
| 315 return self.str | |
| 316 | |
| 317 class UnrecognizedWCFormatException(LocalException): | |
| 318 def __init__(self, format, path): | |
| 319 self.format = format | |
| 320 self.path = path | |
| 321 def __str__(self): | |
| 322 return "Unrecognized WC format %d in '%s'" % (self.format, self.path) | |
| 323 | |
| 324 | |
| 325 def main(): | |
| 326 try: | |
| 327 opts, args = my_getopt(sys.argv[1:], "vh?", | |
| 328 ["debug", "force", "skip-unknown-format", | |
| 329 "verbose", "help"]) | |
| 330 except: | |
| 331 usage_and_exit("Unable to process arguments/options") | |
| 332 | |
| 333 converter = WCFormatConverter() | |
| 334 | |
| 335 # Process arguments. | |
| 336 if len(args) == 2: | |
| 337 converter.root_path = args[0] | |
| 338 svn_version = args[1] | |
| 339 else: | |
| 340 usage_and_exit() | |
| 341 | |
| 342 # Process options. | |
| 343 debug = False | |
| 344 for opt, value in opts: | |
| 345 if opt in ("--help", "-h", "-?"): | |
| 346 usage_and_exit() | |
| 347 elif opt == "--force": | |
| 348 converter.force = True | |
| 349 elif opt == "--skip-unknown-format": | |
| 350 converter.error_on_unrecognized = False | |
| 351 elif opt in ("--verbose", "-v"): | |
| 352 converter.verbosity += 1 | |
| 353 elif opt == "--debug": | |
| 354 debug = True | |
| 355 else: | |
| 356 usage_and_exit("Unknown option '%s'" % opt) | |
| 357 | |
| 358 try: | |
| 359 new_format_nbr = LATEST_FORMATS[svn_version] | |
| 360 except KeyError: | |
| 361 usage_and_exit("Unsupported version number '%s'" % svn_version) | |
| 362 | |
| 363 try: | |
| 364 converter.change_wc_format(new_format_nbr) | |
| 365 except LocalException, e: | |
| 366 if debug: | |
| 367 raise | |
| 368 print >> sys.stderr, str(e) | |
| 369 sys.exit(1) | |
| 370 | |
| 371 print "Converted WC at '%s' into format %d for Subversion %s" % \ | |
| 372 (converter.root_path, new_format_nbr, svn_version) | |
| 373 | |
| 374 if __name__ == "__main__": | |
| 375 main() |
