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 # ====================================================================
23 my_getopt
= getopt
.gnu_getopt
24 except AttributeError:
25 my_getopt
= getopt
.getopt
27 # Pretend we have true booleans on older python versions
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,
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
50 print >> stream
, "ERROR: %s\n" % error_msg
52 usage: %s WC_PATH SVN_VERSION [--verbose] [--force] [--skip-unknown-format]
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)
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>
67 return os
.environ
.has_key("SVN_ASP_DOT_NET_HACK") and "_svn" or ".svn"
69 class WCFormatConverter
:
70 "Performs WC format conversions."
72 error_on_unrecognized
= True
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
:
87 # Process the entries file for this versioned directory.
88 if path
== get_adm_dir():
90 print "Processing directory '%s'" % dirname
91 entries
= Entries(os
.path
.join(dirname
, path
, "entries"))
94 print "Parsing file '%s'" % entries
.path
96 entries
.parse(self
.verbosity
)
97 except UnrecognizedWCFormatException
, e
:
98 if self
.error_on_unrecognized
:
100 print >>sys
.stderr
, "%s, skipping" % (e
,)
103 print "Checking whether WC format can be converted"
105 entries
.assert_valid_format(format_nbr
, self
.verbosity
)
106 except LossyConversionException
, e
:
107 # In --force mode, ignore complaints about lossy conversion.
109 print "WARNING: WC format conversion will be lossy. Dropping "\
110 "field(s) %s " % ", ".join(e
.lossy_fields
)
115 print "Writing WC format"
116 entries
.write_format(format_nbr
)
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
)
126 """Represents a .svn/entries file.
128 'The entries file' section in subversion/libsvn_wc/README is a
131 # The name and index of each field composing an entry's record.
162 "lock-creation-date",
169 def __init__(self
, path
):
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()
183 format_nbr
= int(format_line
)
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.
193 entry
= self
.parse_entry(input, verbosity
)
196 self
.entries
.append(entry
)
200 def assert_valid_format(self
, format_nbr
, verbosity
=0):
202 print "Validating format for entries file '%s'" % self
.path
203 for entry
in self
.entries
:
205 print "Validating format for entry '%s'" % entry
.get_name()
207 entry
.assert_valid_format(format_nbr
)
208 except LossyConversionException
:
210 print >> sys
.stderr
, "Offending entry:"
211 print >> sys
.stderr
, str(entry
)
214 def parse_entry(self
, input, verbosity
=0):
215 "Read an individual entry from INPUT stream."
219 line
= input.readline()
220 if line
in ("", "\x0c\n"):
221 # EOF or end of entry terminator encountered.
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
))
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
)
240 os
.chmod(self
.path
, 0400)
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)
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.
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
])
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
)))
271 "Return the name of this entry."
272 return len(self
.fields
) > 0 and self
.fields
[0] or ""
275 "Return all fields from this entry as a multi-line string."
277 for i
in range(0, len(self
.fields
)):
278 rep
+= "[%s] %s\n" % (Entries
.entry_fields
[i
], self
.fields
[i
])
282 class LocalException(Exception):
283 """Root of local exception class hierarchy."""
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
294 class UnrecognizedWCFormatException(LocalException
):
295 def __init__(self
, format
, path
):
299 return "Unrecognized WC format %d in '%s'" % (self
.format
, self
.path
)
304 opts
, args
= my_getopt(sys
.argv
[1:], "vh?",
305 ["debug", "force", "skip-unknown-format",
308 usage_and_exit("Unable to process arguments/options")
310 converter
= WCFormatConverter()
314 converter
.root_path
= args
[0]
315 svn_version
= args
[1]
321 for opt
, value
in opts
:
322 if opt
in ("--help", "-h", "-?"):
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":
333 usage_and_exit("Unknown option '%s'" % opt
)
336 new_format_nbr
= LATEST_FORMATS
[svn_version
]
338 usage_and_exit("Unsupported version number '%s'" % svn_version
)
341 converter
.change_wc_format(new_format_nbr
)
342 except LocalException
, e
:
345 print >> sys
.stderr
, str(e
)
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__":