0
|
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() |