Reorganize the output to "svnserve --help".
[svn.git] / tools / client-side / change-svn-wc-format.py
blob0871a675192da6545800b7a37fd61a2ff3a61a5f
1 #!/usr/bin/env python
3 # change-svn-wc-format.py: Change the format of a Subversion working copy.
5 # ====================================================================
6 # Copyright (c) 2007 CollabNet. All rights reserved.
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.
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 # ====================================================================
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
27 # Pretend we have true booleans on older python versions
28 try:
29 True
30 except:
31 True = 1
32 False = 0
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.
38 LATEST_FORMATS = { "1.4" : 8,
39 "1.5" : 9 }
41 def usage_and_exit(error_msg=None):
42 """Write usage information and exit. If ERROR_MSG is provide, that
43 error message is printed first (to stderr), the usage info goes to
44 stderr, and the script exits with a non-zero status. Otherwise,
45 usage info goes to stdout and the script exits with a zero status."""
46 progname = os.path.basename(sys.argv[0])
48 stream = error_msg and sys.stderr or sys.stdout
49 if error_msg:
50 print >> stream, "ERROR: %s\n" % error_msg
51 print >> stream, """\
52 usage: %s WC_PATH SVN_VERSION [--verbose] [--force] [--skip-unknown-format]
53 %s --help
55 Change the format of a Subversion working copy to that of SVN_VERSION.
57 --skip-unknown-format : skip directories with unknown working copy
58 format and continue the update
59 """ % (progname, progname)
60 sys.exit(error_msg and 1 or 0)
62 def get_adm_dir():
63 """Return the name of Subversion's administrative directory,
64 adjusted for the SVN_ASP_DOT_NET_HACK environment variable. See
65 <http://svn.collab.net/repos/svn/trunk/notes/asp-dot-net-hack.txt>
66 for details."""
67 return os.environ.has_key("SVN_ASP_DOT_NET_HACK") and "_svn" or ".svn"
69 class WCFormatConverter:
70 "Performs WC format conversions."
71 root_path = None
72 error_on_unrecognized = True
73 force = False
74 verbosity = 0
76 def write_dir_format(self, format_nbr, dirname, paths):
77 """Attempt to write the WC format FORMAT_NBR to the entries file
78 for DIRNAME. Throws LossyConversionException when not in --force
79 mode, and unconvertable WC data is encountered."""
81 # Avoid iterating in unversioned directories.
82 if not get_adm_dir() in paths:
83 del paths[:]
84 return
86 for path in paths:
87 # Process the entries file for this versioned directory.
88 if path == get_adm_dir():
89 if self.verbosity:
90 print "Processing directory '%s'" % dirname
91 entries = Entries(os.path.join(dirname, path, "entries"))
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,)
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
114 if self.verbosity:
115 print "Writing WC format"
116 entries.write_format(format_nbr)
117 break
119 def change_wc_format(self, format_nbr):
120 """Walk all paths in a WC tree, and change their format to
121 FORMAT_NBR. Throw LossyConversionException or NotImplementedError
122 if the WC format should not be converted, or is unrecognized."""
123 os.path.walk(self.root_path, self.write_dir_format, format_nbr)
125 class Entries:
126 """Represents a .svn/entries file.
128 'The entries file' section in subversion/libsvn_wc/README is a
129 useful reference."""
131 # The name and index of each field composing an entry's record.
132 entry_fields = (
133 "name",
134 "kind",
135 "revision",
136 "url",
137 "repos",
138 "schedule",
139 "text-time",
140 "checksum",
141 "committed-date",
142 "committed-rev",
143 "last-author",
144 "has-props",
145 "has-prop-mods",
146 "cachable-props",
147 "present-props",
148 "conflict-old",
149 "conflict-new",
150 "conflict-wrk",
151 "prop-reject-file",
152 "copied",
153 "copyfrom-url",
154 "copyfrom-rev",
155 "deleted",
156 "absent",
157 "incomplete",
158 "uuid",
159 "lock-token",
160 "lock-owner",
161 "lock-comment",
162 "lock-creation-date",
163 "changelist",
164 "keep-local",
165 "working-size",
166 "depth",
169 def __init__(self, path):
170 self.path = path
171 self.entries = []
173 def parse(self, verbosity=0):
174 """Parse the entries file. Throw NotImplementedError if the WC
175 format is unrecognized."""
177 input = open(self.path, "r")
179 # Read and discard WC format number from INPUT. Validate that it
180 # is a supported format for conversion.
181 format_line = input.readline()
182 try:
183 format_nbr = int(format_line)
184 except ValueError:
185 format_nbr = -1
186 if not format_nbr in LATEST_FORMATS.values():
187 raise UnrecognizedWCFormatException(format_nbr, self.path)
189 # Parse file into individual entries, to later inspect for
190 # non-convertable data.
191 entry = None
192 while True:
193 entry = self.parse_entry(input, verbosity)
194 if entry is None:
195 break
196 self.entries.append(entry)
198 input.close()
200 def assert_valid_format(self, format_nbr, verbosity=0):
201 if verbosity >= 2:
202 print "Validating format for entries file '%s'" % self.path
203 for entry in self.entries:
204 if verbosity >= 3:
205 print "Validating format for entry '%s'" % entry.get_name()
206 try:
207 entry.assert_valid_format(format_nbr)
208 except LossyConversionException:
209 if verbosity >= 3:
210 print >> sys.stderr, "Offending entry:"
211 print >> sys.stderr, str(entry)
212 raise
214 def parse_entry(self, input, verbosity=0):
215 "Read an individual entry from INPUT stream."
216 entry = None
218 while True:
219 line = input.readline()
220 if line in ("", "\x0c\n"):
221 # EOF or end of entry terminator encountered.
222 break
224 if entry is None:
225 entry = Entry()
227 # Retain the field value, ditching its field terminator ("\x0a").
228 entry.fields.append(line[:-1])
230 if entry is not None and verbosity >= 3:
231 sys.stdout.write(str(entry))
232 print "-" * 76
233 return entry
235 def write_format(self, format_nbr):
236 os.chmod(self.path, 0600)
237 output = open(self.path, "r+", 0)
238 output.write("%d" % format_nbr)
239 output.close()
240 os.chmod(self.path, 0400)
242 class Entry:
243 "Describes an entry in a WC."
245 # The list of field indices within an entry's record which must be
246 # retained for 1.5 -> 1.4 migration (changelist, keep-local, and depth).
247 must_retain_fields = (30, 31, 33)
249 def __init__(self):
250 self.fields = []
252 def assert_valid_format(self, format_nbr):
253 "Assure that conversion will be non-lossy by examining fields."
255 # Check whether lossy conversion is being attempted.
256 lossy_fields = []
257 for field_index in self.must_retain_fields:
258 if len(self.fields) - 1 >= field_index and self.fields[field_index]:
259 lossy_fields.append(Entries.entry_fields[field_index])
260 if lossy_fields:
261 raise LossyConversionException(lossy_fields,
262 "Lossy WC format conversion requested for entry '%s'\n"
263 "Data for the following field(s) is unsupported by older versions "
264 "of\nSubversion, and is likely to be subsequently discarded, and/or "
265 "have\nunexpected side-effects: %s\n\n"
266 "WC format conversion was cancelled, use the --force option to "
267 "override\nthe default behavior."
268 % (self.get_name(), ", ".join(lossy_fields)))
270 def get_name(self):
271 "Return the name of this entry."
272 return len(self.fields) > 0 and self.fields[0] or ""
274 def __str__(self):
275 "Return all fields from this entry as a multi-line string."
276 rep = ""
277 for i in range(0, len(self.fields)):
278 rep += "[%s] %s\n" % (Entries.entry_fields[i], self.fields[i])
279 return rep
282 class LocalException(Exception):
283 """Root of local exception class hierarchy."""
284 pass
286 class LossyConversionException(LocalException):
287 "Exception thrown when a lossy WC format conversion is requested."
288 def __init__(self, lossy_fields, str):
289 self.lossy_fields = lossy_fields
290 self.str = str
291 def __str__(self):
292 return self.str
294 class UnrecognizedWCFormatException(LocalException):
295 def __init__(self, format, path):
296 self.format = format
297 self.path = path
298 def __str__(self):
299 return "Unrecognized WC format %d in '%s'" % (self.format, self.path)
302 def main():
303 try:
304 opts, args = my_getopt(sys.argv[1:], "vh?",
305 ["debug", "force", "skip-unknown-format",
306 "verbose", "help"])
307 except:
308 usage_and_exit("Unable to process arguments/options")
310 converter = WCFormatConverter()
312 # Process arguments.
313 if len(args) == 2:
314 converter.root_path = args[0]
315 svn_version = args[1]
316 else:
317 usage_and_exit()
319 # Process options.
320 debug = False
321 for opt, value in opts:
322 if opt in ("--help", "-h", "-?"):
323 usage_and_exit()
324 elif opt == "--force":
325 converter.force = True
326 elif opt == "--skip-unknown-format":
327 converter.error_on_unrecognized = False
328 elif opt in ("--verbose", "-v"):
329 converter.verbosity += 1
330 elif opt == "--debug":
331 debug = True
332 else:
333 usage_and_exit("Unknown option '%s'" % opt)
335 try:
336 new_format_nbr = LATEST_FORMATS[svn_version]
337 except KeyError:
338 usage_and_exit("Unsupported version number '%s'" % svn_version)
340 try:
341 converter.change_wc_format(new_format_nbr)
342 except LocalException, e:
343 if debug:
344 raise
345 print >> sys.stderr, str(e)
346 sys.exit(1)
348 print "Converted WC at '%s' into format %d for Subversion %s" % \
349 (converter.root_path, new_format_nbr, svn_version)
351 if __name__ == "__main__":
352 main()