3 # change-svn-wc-format.py: Change the format of a Subversion working copy.
5 # ====================================================================
6 # Licensed to the Subversion Corporation (SVN Corp.) under one
7 # or more contributor license agreements. See the NOTICE file
8 # distributed with this work for additional information
9 # regarding copyright ownership. The SVN Corp. licenses this file
10 # to you under the Apache License, Version 2.0 (the
11 # "License"); you may not use this file except in compliance
12 # with the License. You may obtain a copy of the License at
14 # http://www.apache.org/licenses/LICENSE-2.0
16 # Unless required by applicable law or agreed to in writing,
17 # software distributed under the License is distributed on an
18 # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
19 # KIND, either express or implied. See the License for the
20 # specific language governing permissions and limitations
22 # ====================================================================
28 my_getopt = getopt.gnu_getopt
29 except AttributeError:
30 my_getopt = getopt.getopt
32 ### The entries file parser in subversion/tests/cmdline/svntest/entry.py
33 ### handles the XML-based WC entries file format used by Subversion
34 ### 1.3 and lower. It could be rolled into this script.
36 LATEST_FORMATS = { "1.4" : 8,
39 # Do NOT add format 11 here. See comment in must_retain_fields
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])
50 stream = error_msg and sys.stderr or sys.stdout
52 stream.write("ERROR: %s\n\n" % error_msg)
54 usage: %s WC_PATH SVN_VERSION [--verbose] [--force] [--skip-unknown-format]
57 Change the format of a Subversion working copy to that of SVN_VERSION.
59 --skip-unknown-format : skip directories with unknown working copy
60 format and continue the update
62 """ % (progname, progname))
64 sys.exit(error_msg and 1 or 0)
67 """Return the name of Subversion's administrative directory,
68 adjusted for the SVN_ASP_DOT_NET_HACK environment variable. See
69 <http://svn.collab.net/repos/svn/trunk/notes/asp-dot-net-hack.txt>
71 return "SVN_ASP_DOT_NET_HACK" in os.environ and "_svn" or ".svn"
73 class WCFormatConverter:
74 "Performs WC format conversions."
76 error_on_unrecognized = True
80 def write_dir_format(self, format_nbr, dirname, paths):
81 """Attempt to write the WC format FORMAT_NBR to the entries file
82 for DIRNAME. Throws LossyConversionException when not in --force
83 mode, and unconvertable WC data is encountered."""
85 # Avoid iterating in unversioned directories.
86 if not (get_adm_dir() in paths):
90 # Process the entries file for this versioned directory.
92 print("Processing directory '%s'" % dirname)
93 entries = Entries(os.path.join(dirname, get_adm_dir(), "entries"))
96 print("Parsing file '%s'" % entries.path)
98 entries.parse(self.verbosity)
99 except UnrecognizedWCFormatException, e:
100 if self.error_on_unrecognized:
102 sys.stderr.write("%s, skipping\n" % e)
104 entries_parsed = False
107 format = Format(os.path.join(dirname, get_adm_dir(), "format"))
109 print("Updating file '%s'" % format.path)
110 format.write_format(format_nbr, self.verbosity)
113 print("Skipping file '%s'" % format.path)
116 print("Checking whether WC format can be converted")
118 entries.assert_valid_format(format_nbr, self.verbosity)
119 except LossyConversionException, e:
120 # In --force mode, ignore complaints about lossy conversion.
122 print("WARNING: WC format conversion will be lossy. Dropping "\
123 "field(s) %s " % ", ".join(e.lossy_fields))
128 print("Writing WC format")
129 entries.write_format(format_nbr)
131 def change_wc_format(self, format_nbr):
132 """Walk all paths in a WC tree, and change their format to
133 FORMAT_NBR. Throw LossyConversionException or NotImplementedError
134 if the WC format should not be converted, or is unrecognized."""
135 for dirpath, dirs, files in os.walk(self.root_path):
136 self.write_dir_format(format_nbr, dirpath, dirs + files)
139 """Represents a .svn/entries file.
141 'The entries file' section in subversion/libsvn_wc/README is a
144 # The name and index of each field composing an entry's record.
175 "lock-creation-date",
187 # How many bytes the format number takes in the file. (The format number
188 # may have leading zeroes after using this script to convert format 10 to
189 # format 9 -- which would write the format number as '09'.)
190 format_nbr_bytes = -1
192 def __init__(self, path):
196 def parse(self, verbosity=0):
197 """Parse the entries file. Throw NotImplementedError if the WC
198 format is unrecognized."""
200 input = open(self.path, "r")
202 # Read WC format number from INPUT. Validate that it
203 # is a supported format for conversion.
204 format_line = input.readline()
206 self.format_nbr = int(format_line)
207 self.format_nbr_bytes = len(format_line.rstrip()) # remove '\n'
210 self.format_nbr_bytes = -1
211 if not self.format_nbr in LATEST_FORMATS.values():
212 raise UnrecognizedWCFormatException(self.format_nbr, self.path)
214 # Parse file into individual entries, to later inspect for
215 # non-convertable data.
218 entry = self.parse_entry(input, verbosity)
221 self.entries.append(entry)
225 def assert_valid_format(self, format_nbr, verbosity=0):
227 print("Validating format for entries file '%s'" % self.path)
228 for entry in self.entries:
230 print("Validating format for entry '%s'" % entry.get_name())
232 entry.assert_valid_format(format_nbr)
233 except LossyConversionException:
235 sys.stderr.write("Offending entry:\n%s\n" % entry)
239 def parse_entry(self, input, verbosity=0):
240 "Read an individual entry from INPUT stream."
244 line = input.readline()
245 if line in ("", "\x0c\n"):
246 # EOF or end of entry terminator encountered.
252 # Retain the field value, ditching its field terminator ("\x0a").
253 entry.fields.append(line[:-1])
255 if entry is not None and verbosity >= 3:
256 sys.stdout.write(str(entry))
260 def write_format(self, format_nbr):
261 # Overwrite all bytes of the format number (which are the first bytes in
262 # the file). Overwrite format '10' by format '09', which will be converted
263 # to '9' by Subversion when it rewrites the file. (Subversion 1.4 and later
264 # ignore leading zeroes in the format number.)
265 assert len(str(format_nbr)) <= self.format_nbr_bytes
266 format_string = '%0' + str(self.format_nbr_bytes) + 'd'
268 os.chmod(self.path, 0600)
269 output = open(self.path, "r+", 0)
270 output.write(format_string % format_nbr)
272 os.chmod(self.path, 0400)
275 "Describes an entry in a WC."
277 # Maps format numbers to indices of fields within an entry's record that must
278 # be retained when downgrading to that format.
279 must_retain_fields = {
280 # Not in 1.4: changelist, keep-local, depth, tree-conflicts, file-externals
281 8 : (30, 31, 33, 34, 35),
282 # Not in 1.5: tree-conflicts, file-externals
285 # Downgrading from format 11 (1.7-dev) to format 10 is not possible,
286 # because 11 does not use has-props and cachable-props (but 10 does).
287 # Naively downgrading in that situation causes properties to disappear
290 # Downgrading from the 1.7 SQLite-based format to format 10 is not
297 def assert_valid_format(self, format_nbr):
298 "Assure that conversion will be non-lossy by examining fields."
300 # Check whether lossy conversion is being attempted.
302 for field_index in self.must_retain_fields[format_nbr]:
303 if len(self.fields) - 1 >= field_index and self.fields[field_index]:
304 lossy_fields.append(Entries.entry_fields[field_index])
306 raise LossyConversionException(lossy_fields,
307 "Lossy WC format conversion requested for entry '%s'\n"
308 "Data for the following field(s) is unsupported by older versions "
309 "of\nSubversion, and is likely to be subsequently discarded, and/or "
310 "have\nunexpected side-effects: %s\n\n"
311 "WC format conversion was cancelled, use the --force option to "
312 "override\nthe default behavior."
313 % (self.get_name(), ", ".join(lossy_fields)))
316 "Return the name of this entry."
317 return len(self.fields) > 0 and self.fields[0] or ""
320 "Return all fields from this entry as a multi-line string."
322 for i in range(0, len(self.fields)):
323 rep += "[%s] %s\n" % (Entries.entry_fields[i], self.fields[i])
327 """Represents a .svn/format file."""
329 def __init__(self, path):
332 def write_format(self, format_nbr, verbosity=0):
333 format_string = '%d\n'
334 if os.path.exists(self.path):
336 print("%s will be updated." % self.path)
337 os.chmod(self.path,0600)
340 print("%s does not exist, creating it." % self.path)
341 format = open(self.path, "w")
342 format.write(format_string % format_nbr)
344 os.chmod(self.path, 0400)
346 class LocalException(Exception):
347 """Root of local exception class hierarchy."""
350 class LossyConversionException(LocalException):
351 "Exception thrown when a lossy WC format conversion is requested."
352 def __init__(self, lossy_fields, str):
353 self.lossy_fields = lossy_fields
358 class UnrecognizedWCFormatException(LocalException):
359 def __init__(self, format, path):
363 return ("Unrecognized WC format %d in '%s'; "
364 "only formats 8, 9, and 10 can be supported") % (self.format, self.path)
369 opts, args = my_getopt(sys.argv[1:], "vh?",
370 ["debug", "force", "skip-unknown-format",
373 usage_and_exit("Unable to process arguments/options")
375 converter = WCFormatConverter()
379 converter.root_path = args[0]
380 svn_version = args[1]
386 for opt, value in opts:
387 if opt in ("--help", "-h", "-?"):
389 elif opt == "--force":
390 converter.force = True
391 elif opt == "--skip-unknown-format":
392 converter.error_on_unrecognized = False
393 elif opt in ("--verbose", "-v"):
394 converter.verbosity += 1
395 elif opt == "--debug":
398 usage_and_exit("Unknown option '%s'" % opt)
401 new_format_nbr = LATEST_FORMATS[svn_version]
403 usage_and_exit("Unsupported version number '%s'; "
404 "only 1.4, 1.5, and 1.6 can be supported" % svn_version)
407 converter.change_wc_format(new_format_nbr)
408 except LocalException, e:
411 sys.stderr.write("%s\n" % e)
415 print("Converted WC at '%s' into format %d for Subversion %s" % \
416 (converter.root_path, new_format_nbr, svn_version))
418 if __name__ == "__main__":