Logo AND Algorithmique Numérique Distribuée

Public GIT Repository
Change the way supernovae files are generated so that out of source works
[simgrid.git] / buildtools / scripts / change-svn-wc-format.py
1 #!/usr/bin/env python
2 #
3 # change-svn-wc-format.py: Change the format of a Subversion working copy.
4 #
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
13 #
14 #      http://www.apache.org/licenses/LICENSE-2.0
15 #
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
21 #    under the License.
22 # ====================================================================
23
24 import sys
25 import os
26 import getopt
27 try:
28   my_getopt = getopt.gnu_getopt
29 except AttributeError:
30   my_getopt = getopt.getopt
31
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.
35
36 LATEST_FORMATS = { "1.4" : 8,
37                    "1.5" : 9,
38                    "1.6" : 10,
39                    # Do NOT add format 11 here.  See comment in must_retain_fields
40                    # for why.
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     stream.write("ERROR: %s\n\n" % error_msg)
53   stream.write("""\
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
62 """ % (progname, progname))
63   stream.flush()
64   sys.exit(error_msg and 1 or 0)
65
66 def get_adm_dir():
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>
70   for details."""
71   return "SVN_ASP_DOT_NET_HACK" in os.environ and "_svn" or ".svn"
72
73 class WCFormatConverter:
74   "Performs WC format conversions."
75   root_path = None
76   error_on_unrecognized = True
77   force = False
78   verbosity = 0
79
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."""
84
85     # Avoid iterating in unversioned directories.
86     if not (get_adm_dir() in paths):
87       del paths[:]
88       return
89
90     # Process the entries file for this versioned directory.
91     if self.verbosity:
92       print("Processing directory '%s'" % dirname)
93     entries = Entries(os.path.join(dirname, get_adm_dir(), "entries"))
94     entries_parsed = True
95     if self.verbosity:
96       print("Parsing file '%s'" % entries.path)
97     try:
98       entries.parse(self.verbosity)
99     except UnrecognizedWCFormatException, e:
100       if self.error_on_unrecognized:
101         raise
102       sys.stderr.write("%s, skipping\n" % e)
103       sys.stderr.flush()
104       entries_parsed = False
105
106     if entries_parsed:
107       format = Format(os.path.join(dirname, get_adm_dir(), "format"))
108       if self.verbosity:
109         print("Updating file '%s'" % format.path)
110       format.write_format(format_nbr, self.verbosity)
111     else:
112       if self.verbosity:
113         print("Skipping file '%s'" % format.path)
114
115     if self.verbosity:
116       print("Checking whether WC format can be converted")
117     try:
118       entries.assert_valid_format(format_nbr, self.verbosity)
119     except LossyConversionException, e:
120       # In --force mode, ignore complaints about lossy conversion.
121       if self.force:
122         print("WARNING: WC format conversion will be lossy. Dropping "\
123               "field(s) %s " % ", ".join(e.lossy_fields))
124       else:
125         raise
126
127     if self.verbosity:
128       print("Writing WC format")
129     entries.write_format(format_nbr)
130
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)
137
138 class Entries:
139   """Represents a .svn/entries file.
140
141   'The entries file' section in subversion/libsvn_wc/README is a
142   useful reference."""
143
144   # The name and index of each field composing an entry's record.
145   entry_fields = (
146     "name",
147     "kind",
148     "revision",
149     "url",
150     "repos",
151     "schedule",
152     "text-time",
153     "checksum",
154     "committed-date",
155     "committed-rev",
156     "last-author",
157     "has-props",
158     "has-prop-mods",
159     "cachable-props",
160     "present-props",
161     "conflict-old",
162     "conflict-new",
163     "conflict-wrk",
164     "prop-reject-file",
165     "copied",
166     "copyfrom-url",
167     "copyfrom-rev",
168     "deleted",
169     "absent",
170     "incomplete",
171     "uuid",
172     "lock-token",
173     "lock-owner",
174     "lock-comment",
175     "lock-creation-date",
176     "changelist",
177     "keep-local",
178     "working-size",
179     "depth",
180     "tree-conflicts",
181     "file-external",
182   )
183
184   # The format number.
185   format_nbr = -1
186
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
191
192   def __init__(self, path):
193     self.path = path
194     self.entries = []
195
196   def parse(self, verbosity=0):
197     """Parse the entries file.  Throw NotImplementedError if the WC
198     format is unrecognized."""
199
200     input = open(self.path, "r")
201
202     # Read WC format number from INPUT.  Validate that it
203     # is a supported format for conversion.
204     format_line = input.readline()
205     try:
206       self.format_nbr = int(format_line)
207       self.format_nbr_bytes = len(format_line.rstrip()) # remove '\n'
208     except ValueError:
209       self.format_nbr = -1
210       self.format_nbr_bytes = -1
211     if not self.format_nbr in LATEST_FORMATS.values():
212       raise UnrecognizedWCFormatException(self.format_nbr, self.path)
213
214     # Parse file into individual entries, to later inspect for
215     # non-convertable data.
216     entry = None
217     while True:
218       entry = self.parse_entry(input, verbosity)
219       if entry is None:
220         break
221       self.entries.append(entry)
222
223     input.close()
224
225   def assert_valid_format(self, format_nbr, verbosity=0):
226     if verbosity >= 2:
227       print("Validating format for entries file '%s'" % self.path)
228     for entry in self.entries:
229       if verbosity >= 3:
230         print("Validating format for entry '%s'" % entry.get_name())
231       try:
232         entry.assert_valid_format(format_nbr)
233       except LossyConversionException:
234         if verbosity >= 3:
235           sys.stderr.write("Offending entry:\n%s\n" % entry)
236           sys.stderr.flush()
237         raise
238
239   def parse_entry(self, input, verbosity=0):
240     "Read an individual entry from INPUT stream."
241     entry = None
242
243     while True:
244       line = input.readline()
245       if line in ("", "\x0c\n"):
246         # EOF or end of entry terminator encountered.
247         break
248
249       if entry is None:
250         entry = Entry()
251
252       # Retain the field value, ditching its field terminator ("\x0a").
253       entry.fields.append(line[:-1])
254
255     if entry is not None and verbosity >= 3:
256       sys.stdout.write(str(entry))
257       print("-" * 76)
258     return entry
259
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'
267
268     os.chmod(self.path, 0600)
269     output = open(self.path, "r+", 0)
270     output.write(format_string % format_nbr)
271     output.close()
272     os.chmod(self.path, 0400)
273
274 class Entry:
275   "Describes an entry in a WC."
276
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
283       9  : (34, 35),
284       10 : (),
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
288       # from the wc.
289       #
290       # Downgrading from the 1.7 SQLite-based format to format 10 is not
291       # implemented.
292       }
293
294   def __init__(self):
295     self.fields = []
296
297   def assert_valid_format(self, format_nbr):
298     "Assure that conversion will be non-lossy by examining fields."
299
300     # Check whether lossy conversion is being attempted.
301     lossy_fields = []
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])
305     if lossy_fields:
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)))
314
315   def get_name(self):
316     "Return the name of this entry."
317     return len(self.fields) > 0 and self.fields[0] or ""
318
319   def __str__(self):
320     "Return all fields from this entry as a multi-line string."
321     rep = ""
322     for i in range(0, len(self.fields)):
323       rep += "[%s] %s\n" % (Entries.entry_fields[i], self.fields[i])
324     return rep
325
326 class Format:
327   """Represents a .svn/format file."""
328
329   def __init__(self, path):
330     self.path = path
331
332   def write_format(self, format_nbr, verbosity=0):
333     format_string = '%d\n'
334     if os.path.exists(self.path):
335       if verbosity >= 1:
336         print("%s will be updated." % self.path)
337       os.chmod(self.path,0600)
338     else:
339       if verbosity >= 1:
340         print("%s does not exist, creating it." % self.path)
341     format = open(self.path, "w")
342     format.write(format_string % format_nbr)
343     format.close()
344     os.chmod(self.path, 0400)
345
346 class LocalException(Exception):
347   """Root of local exception class hierarchy."""
348   pass
349
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
354     self.str = str
355   def __str__(self):
356     return self.str
357
358 class UnrecognizedWCFormatException(LocalException):
359   def __init__(self, format, path):
360     self.format = format
361     self.path = path
362   def __str__(self):
363     return ("Unrecognized WC format %d in '%s'; "
364             "only formats 8, 9, and 10 can be supported") % (self.format, self.path)
365
366
367 def main():
368   try:
369     opts, args = my_getopt(sys.argv[1:], "vh?",
370                            ["debug", "force", "skip-unknown-format",
371                             "verbose", "help"])
372   except:
373     usage_and_exit("Unable to process arguments/options")
374
375   converter = WCFormatConverter()
376
377   # Process arguments.
378   if len(args) == 2:
379     converter.root_path = args[0]
380     svn_version = args[1]
381   else:
382     usage_and_exit()
383
384   # Process options.
385   debug = False
386   for opt, value in opts:
387     if opt in ("--help", "-h", "-?"):
388       usage_and_exit()
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":
396       debug = True
397     else:
398       usage_and_exit("Unknown option '%s'" % opt)
399
400   try:
401     new_format_nbr = LATEST_FORMATS[svn_version]
402   except KeyError:
403     usage_and_exit("Unsupported version number '%s'; "
404                    "only 1.4, 1.5, and 1.6 can be supported" % svn_version)
405
406   try:
407     converter.change_wc_format(new_format_nbr)
408   except LocalException, e:
409     if debug:
410       raise
411     sys.stderr.write("%s\n" % e)
412     sys.stderr.flush()
413     sys.exit(1)
414
415   print("Converted WC at '%s' into format %d for Subversion %s" % \
416         (converter.root_path, new_format_nbr, svn_version))
417
418 if __name__ == "__main__":
419   main()