Add ConditionalPropertySetter
[cvs2svn.git] / cvs2svn_lib / property_setters.py
blob686452dbb3d53fecba7150c07bd650433f60fe64
1 # (Be in -*- python -*- mode.)
3 # ====================================================================
4 # Copyright (c) 2000-2008 CollabNet. All rights reserved.
6 # This software is licensed as described in the file COPYING, which
7 # you should have received as part of this distribution. The terms
8 # are also available at http://subversion.tigris.org/license-1.html.
9 # If newer versions of this license are posted there, you may use a
10 # newer version instead, at your option.
12 # This software consists of voluntary contributions made by many
13 # individuals. For exact contribution history, see the revision
14 # history and logs, available at http://cvs2svn.tigris.org/.
15 # ====================================================================
17 """This module contains classes to set Subversion properties on files."""
20 import os
21 import re
22 import fnmatch
23 import ConfigParser
24 from cStringIO import StringIO
26 from cvs2svn_lib.common import warning_prefix
27 from cvs2svn_lib.log import logger
30 def _squash_case(s):
31 return s.lower()
34 def _preserve_case(s):
35 return s
38 class FilePropertySetter(object):
39 """Abstract class for objects that set properties on a CVSFile."""
41 def maybe_set_property(self, cvs_file, name, value):
42 """Set a property on CVS_FILE if it does not already have a value.
44 This method is here for the convenience of derived classes."""
46 if name not in cvs_file.properties:
47 cvs_file.properties[name] = value
49 def set_properties(self, cvs_file):
50 """Set any properties needed for CVS_FILE.
52 CVS_FILE is an instance of CVSFile. This method should modify
53 CVS_FILE.properties in place."""
55 raise NotImplementedError()
58 class ExecutablePropertySetter(FilePropertySetter):
59 """Set the svn:executable property based on cvs_file.executable."""
61 def set_properties(self, cvs_file):
62 if cvs_file.executable:
63 self.maybe_set_property(cvs_file, 'svn:executable', '*')
66 class DescriptionPropertySetter(FilePropertySetter):
67 """Set the cvs:description property based on cvs_file.description."""
69 def __init__(self, propname='cvs:description'):
70 self.propname = propname
72 def set_properties(self, cvs_file):
73 if cvs_file.description:
74 self.maybe_set_property(cvs_file, self.propname, cvs_file.description)
77 class CVSBinaryFileEOLStyleSetter(FilePropertySetter):
78 """Set the eol-style to None for files with CVS mode '-kb'."""
80 def set_properties(self, cvs_file):
81 if cvs_file.mode == 'b':
82 self.maybe_set_property(cvs_file, 'svn:eol-style', None)
85 class MimeMapper(FilePropertySetter):
86 """A class that provides mappings from file names to MIME types."""
88 propname = 'svn:mime-type'
90 def __init__(
91 self, mime_types_file=None, mime_mappings=None,
92 ignore_case=False
94 """Constructor.
96 Arguments:
98 mime_types_file -- a path to a MIME types file on disk. Each
99 line of the file should contain the MIME type, then a
100 whitespace-separated list of file extensions; e.g., one line
101 might be 'text/plain txt c h cpp hpp'. (See
102 http://en.wikipedia.org/wiki/Mime.types for information
103 about mime.types files):
105 mime_mappings -- a dictionary mapping a file extension to a MIME
106 type; e.g., {'txt': 'text/plain', 'cpp': 'text/plain'}.
108 ignore_case -- True iff case should be ignored in filename
109 extensions. Setting this option to True can be useful if
110 your CVS repository was used on systems with
111 case-insensitive filenames, in which case you might have a
112 mix of uppercase and lowercase filenames."""
114 self.mappings = { }
115 if ignore_case:
116 self.transform_case = _squash_case
117 else:
118 self.transform_case = _preserve_case
120 if mime_types_file is None and mime_mappings is None:
121 logger.error('Should specify MIME types file or dict.\n')
123 if mime_types_file is not None:
124 for line in file(mime_types_file):
125 if line.startswith("#"):
126 continue
128 # format of a line is something like
129 # text/plain c h cpp
130 extensions = line.split()
131 if len(extensions) < 2:
132 continue
133 type = extensions.pop(0)
134 for ext in extensions:
135 ext = self.transform_case(ext)
136 if ext in self.mappings and self.mappings[ext] != type:
137 logger.error(
138 "%s: ambiguous MIME mapping for *.%s (%s or %s)\n"
139 % (warning_prefix, ext, self.mappings[ext], type)
141 self.mappings[ext] = type
143 if mime_mappings is not None:
144 for ext, type in mime_mappings.iteritems():
145 ext = self.transform_case(ext)
146 if ext in self.mappings and self.mappings[ext] != type:
147 logger.error(
148 "%s: ambiguous MIME mapping for *.%s (%s or %s)\n"
149 % (warning_prefix, ext, self.mappings[ext], type)
151 self.mappings[ext] = type
153 def set_properties(self, cvs_file):
154 if self.propname in cvs_file.properties:
155 return
157 basename, extension = os.path.splitext(cvs_file.rcs_basename)
159 # Extension includes the dot, so strip it (will leave extension
160 # empty if filename ends with a dot, which is ok):
161 extension = extension[1:]
163 # If there is no extension (or the file ends with a period), use
164 # the base name for mapping. This allows us to set mappings for
165 # files such as README or Makefile:
166 if not extension:
167 extension = basename
169 extension = self.transform_case(extension)
171 mime_type = self.mappings.get(extension, None)
172 if mime_type is not None:
173 cvs_file.properties[self.propname] = mime_type
176 class AutoPropsPropertySetter(FilePropertySetter):
177 """Set arbitrary svn properties based on an auto-props configuration.
179 This class supports case-sensitive or case-insensitive pattern
180 matching. The command-line default is case-insensitive behavior,
181 consistent with Subversion (see
182 http://subversion.tigris.org/issues/show_bug.cgi?id=2036).
184 As a special extension to Subversion's auto-props handling, if a
185 property name is preceded by a '!' then that property is forced to
186 be left unset.
188 If a property specified in auto-props has already been set to a
189 different value, print a warning and leave the old property value
190 unchanged.
192 Python's treatment of whitespaces in the ConfigParser module is
193 buggy and inconsistent. Usually spaces are preserved, but if there
194 is at least one semicolon in the value, and the *first* semicolon is
195 preceded by a space, then that is treated as the start of a comment
196 and the rest of the line is silently discarded."""
198 property_name_pattern = r'(?P<name>[^\!\=\s]+)'
199 property_unset_re = re.compile(
200 r'^\!\s*' + property_name_pattern + r'$'
202 property_set_re = re.compile(
203 r'^' + property_name_pattern + r'\s*\=\s*(?P<value>.*)$'
205 property_novalue_re = re.compile(
206 r'^' + property_name_pattern + r'$'
209 quoted_re = re.compile(
210 r'^([\'\"]).*\1$'
212 comment_re = re.compile(r'\s;')
214 class Pattern:
215 """Describes the properties to be set for files matching a pattern."""
217 def __init__(self, pattern, propdict):
218 # A glob-like pattern:
219 self.pattern = pattern
220 # A dictionary of properties that should be set:
221 self.propdict = propdict
223 def match(self, basename):
224 """Does the file with the specified basename match pattern?"""
226 return fnmatch.fnmatch(basename, self.pattern)
228 def __init__(self, configfilename, ignore_case=True):
229 config = ConfigParser.ConfigParser()
230 if ignore_case:
231 self.transform_case = _squash_case
232 else:
233 config.optionxform = _preserve_case
234 self.transform_case = _preserve_case
236 configtext = open(configfilename).read()
237 if self.comment_re.search(configtext):
238 logger.warn(
239 '%s: Please be aware that a space followed by a\n'
240 'semicolon is sometimes treated as a comment in configuration\n'
241 'files. This pattern was seen in\n'
242 ' %s\n'
243 'Please make sure that you have not inadvertently commented\n'
244 'out part of an important line.'
245 % (warning_prefix, configfilename,)
248 config.readfp(StringIO(configtext), configfilename)
249 self.patterns = []
250 sections = config.sections()
251 sections.sort()
252 for section in sections:
253 if self.transform_case(section) == 'auto-props':
254 patterns = config.options(section)
255 patterns.sort()
256 for pattern in patterns:
257 value = config.get(section, pattern)
258 if value:
259 self._add_pattern(pattern, value)
261 def _add_pattern(self, pattern, props):
262 propdict = {}
263 if self.quoted_re.match(pattern):
264 logger.warn(
265 '%s: Quoting is not supported in auto-props; please verify rule\n'
266 'for %r. (Using pattern including quotation marks.)\n'
267 % (warning_prefix, pattern,)
269 for prop in props.split(';'):
270 prop = prop.strip()
271 m = self.property_unset_re.match(prop)
272 if m:
273 name = m.group('name')
274 logger.debug(
275 'auto-props: For %r, leaving %r unset.' % (pattern, name,)
277 propdict[name] = None
278 continue
280 m = self.property_set_re.match(prop)
281 if m:
282 name = m.group('name')
283 value = m.group('value')
284 if self.quoted_re.match(value):
285 logger.warn(
286 '%s: Quoting is not supported in auto-props; please verify\n'
287 'rule %r for pattern %r. (Using value\n'
288 'including quotation marks.)\n'
289 % (warning_prefix, prop, pattern,)
291 logger.debug(
292 'auto-props: For %r, setting %r to %r.' % (pattern, name, value,)
294 propdict[name] = value
295 continue
297 m = self.property_novalue_re.match(prop)
298 if m:
299 name = m.group('name')
300 logger.debug(
301 'auto-props: For %r, setting %r to the empty string'
302 % (pattern, name,)
304 propdict[name] = ''
305 continue
307 logger.warn(
308 '%s: in auto-props line for %r, value %r cannot be parsed (ignored)'
309 % (warning_prefix, pattern, prop,)
312 self.patterns.append(self.Pattern(self.transform_case(pattern), propdict))
314 def get_propdict(self, cvs_file):
315 basename = self.transform_case(cvs_file.rcs_basename)
316 propdict = {}
317 for pattern in self.patterns:
318 if pattern.match(basename):
319 for (key,value) in pattern.propdict.items():
320 if key in propdict:
321 if propdict[key] != value:
322 logger.warn(
323 "Contradictory values set for property '%s' for file %s."
324 % (key, cvs_file,))
325 else:
326 propdict[key] = value
328 return propdict
330 def set_properties(self, cvs_file):
331 propdict = self.get_propdict(cvs_file)
332 for (k,v) in propdict.items():
333 if k in cvs_file.properties:
334 if cvs_file.properties[k] != v:
335 logger.warn(
336 "Property '%s' already set to %r for file %s; "
337 "auto-props value (%r) ignored."
338 % (k, cvs_file.properties[k], cvs_file.cvs_path, v,)
340 else:
341 cvs_file.properties[k] = v
344 class CVSBinaryFileDefaultMimeTypeSetter(FilePropertySetter):
345 """If the file is binary and its svn:mime-type property is not yet
346 set, set it to 'application/octet-stream'."""
348 def set_properties(self, cvs_file):
349 if cvs_file.mode == 'b':
350 self.maybe_set_property(
351 cvs_file, 'svn:mime-type', 'application/octet-stream'
355 class EOLStyleFromMimeTypeSetter(FilePropertySetter):
356 """Set svn:eol-style based on svn:mime-type.
358 If svn:mime-type is known but svn:eol-style is not, then set
359 svn:eol-style based on svn:mime-type as follows: if svn:mime-type
360 starts with 'text/', then set svn:eol-style to native; otherwise,
361 force it to remain unset. See also issue #39."""
363 propname = 'svn:eol-style'
365 def set_properties(self, cvs_file):
366 if self.propname in cvs_file.properties:
367 return
369 mime_type = cvs_file.properties.get('svn:mime-type', None)
370 if mime_type:
371 if mime_type.startswith("text/"):
372 cvs_file.properties[self.propname] = 'native'
373 else:
374 cvs_file.properties[self.propname] = None
377 class DefaultEOLStyleSetter(FilePropertySetter):
378 """Set the eol-style if one has not already been set."""
380 valid_values = {
381 None : None,
382 # Also treat "binary" as None:
383 'binary' : None,
384 'native' : 'native',
385 'CRLF' : 'CRLF', 'LF' : 'LF', 'CR' : 'CR',
388 def __init__(self, value):
389 """Initialize with the specified default VALUE."""
391 try:
392 # Check that value is valid, and translate it to the proper case
393 self.value = self.valid_values[value]
394 except KeyError:
395 raise ValueError(
396 'Illegal value specified for the default EOL option: %r' % (value,)
399 def set_properties(self, cvs_file):
400 self.maybe_set_property(cvs_file, 'svn:eol-style', self.value)
403 class SVNBinaryFileKeywordsPropertySetter(FilePropertySetter):
404 """Turn off svn:keywords for files with binary svn:eol-style."""
406 propname = 'svn:keywords'
408 def set_properties(self, cvs_file):
409 if self.propname in cvs_file.properties:
410 return
412 if not cvs_file.properties.get('svn:eol-style'):
413 cvs_file.properties[self.propname] = None
416 class KeywordsPropertySetter(FilePropertySetter):
417 """If the svn:keywords property is not yet set, set it based on the
418 file's mode. See issue #2."""
420 def __init__(self, value):
421 """Use VALUE for the value of the svn:keywords property if it is
422 to be set."""
424 self.value = value
426 def set_properties(self, cvs_file):
427 if cvs_file.mode in [None, 'kv', 'kvl']:
428 self.maybe_set_property(cvs_file, 'svn:keywords', self.value)
431 class ConditionalPropertySetter(object):
432 """Delegate to the passed property setters when the passed predicate applies.
433 The predicate should be a function that takes a CVSFile or CVSRevision
434 argument and return True if the property setters should be applied."""
436 def __init__(self, predicate, *property_setters):
437 self.predicate = predicate
438 self.property_setters = property_setters
440 def set_properties(self, cvs_file_or_rev):
441 if self.predicate(cvs_file_or_rev):
442 for property_setter in self.property_setters:
443 property_setter.set_properties(cvs_file_or_rev)
446 class RevisionPropertySetter:
447 """Abstract class for objects that can set properties on a CVSRevision."""
449 def set_properties(self, cvs_rev):
450 """Set any properties that can be determined for CVS_REV.
452 CVS_REV is an instance of CVSRevision. This method should modify
453 CVS_REV.properties in place."""
455 raise NotImplementedError()
458 class CVSRevisionNumberSetter(RevisionPropertySetter):
459 """Store the CVS revision number to an SVN property."""
461 def __init__(self, propname='cvs2svn:cvs-rev'):
462 self.propname = propname
464 def set_properties(self, cvs_rev):
465 if self.propname in cvs_rev.properties:
466 return
468 cvs_rev.properties[self.propname] = cvs_rev.rev