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