Handle --default-eol=binary as per documentation.
[cvs2svn.git] / cvs2svn_lib / property_setters.py
blobd0ac13a48c4d390d3cab88b7f630bc866d88b49b
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'.
103 mime_mappings -- a dictionary mapping a file extension to a MIME
104 type; e.g., {'txt': 'text/plain', 'cpp': 'text/plain'}.
106 ignore_case -- True iff case should be ignored in filename
107 extensions. Setting this option to True can be useful if
108 your CVS repository was used on systems with
109 case-insensitive filenames, in which case you might have a
110 mix of uppercase and lowercase filenames."""
112 self.mappings = { }
113 if ignore_case:
114 self.transform_case = _squash_case
115 else:
116 self.transform_case = _preserve_case
118 if mime_types_file is None and mime_mappings is None:
119 logger.error('Should specify MIME types file or dict.\n')
121 if mime_types_file is not None:
122 for line in file(mime_types_file):
123 if line.startswith("#"):
124 continue
126 # format of a line is something like
127 # text/plain c h cpp
128 extensions = line.split()
129 if len(extensions) < 2:
130 continue
131 type = extensions.pop(0)
132 for ext in extensions:
133 ext = self.transform_case(ext)
134 if ext in self.mappings and self.mappings[ext] != type:
135 logger.error(
136 "%s: ambiguous MIME mapping for *.%s (%s or %s)\n"
137 % (warning_prefix, ext, self.mappings[ext], type)
139 self.mappings[ext] = type
141 if mime_mappings is not None:
142 for ext, type in mime_mappings.iteritems():
143 ext = self.transform_case(ext)
144 if ext in self.mappings and self.mappings[ext] != type:
145 logger.error(
146 "%s: ambiguous MIME mapping for *.%s (%s or %s)\n"
147 % (warning_prefix, ext, self.mappings[ext], type)
149 self.mappings[ext] = type
151 def set_properties(self, cvs_file):
152 if self.propname in cvs_file.properties:
153 return
155 basename, extension = os.path.splitext(cvs_file.rcs_basename)
157 # Extension includes the dot, so strip it (will leave extension
158 # empty if filename ends with a dot, which is ok):
159 extension = extension[1:]
161 # If there is no extension (or the file ends with a period), use
162 # the base name for mapping. This allows us to set mappings for
163 # files such as README or Makefile:
164 if not extension:
165 extension = basename
167 extension = self.transform_case(extension)
169 mime_type = self.mappings.get(extension, None)
170 if mime_type is not None:
171 cvs_file.properties[self.propname] = mime_type
174 class AutoPropsPropertySetter(FilePropertySetter):
175 """Set arbitrary svn properties based on an auto-props configuration.
177 This class supports case-sensitive or case-insensitive pattern
178 matching. The command-line default is case-insensitive behavior,
179 consistent with Subversion (see
180 http://subversion.tigris.org/issues/show_bug.cgi?id=2036).
182 As a special extension to Subversion's auto-props handling, if a
183 property name is preceded by a '!' then that property is forced to
184 be left unset.
186 If a property specified in auto-props has already been set to a
187 different value, print a warning and leave the old property value
188 unchanged.
190 Python's treatment of whitespaces in the ConfigParser module is
191 buggy and inconsistent. Usually spaces are preserved, but if there
192 is at least one semicolon in the value, and the *first* semicolon is
193 preceded by a space, then that is treated as the start of a comment
194 and the rest of the line is silently discarded."""
196 property_name_pattern = r'(?P<name>[^\!\=\s]+)'
197 property_unset_re = re.compile(
198 r'^\!\s*' + property_name_pattern + r'$'
200 property_set_re = re.compile(
201 r'^' + property_name_pattern + r'\s*\=\s*(?P<value>.*)$'
203 property_novalue_re = re.compile(
204 r'^' + property_name_pattern + r'$'
207 quoted_re = re.compile(
208 r'^([\'\"]).*\1$'
210 comment_re = re.compile(r'\s;')
212 class Pattern:
213 """Describes the properties to be set for files matching a pattern."""
215 def __init__(self, pattern, propdict):
216 # A glob-like pattern:
217 self.pattern = pattern
218 # A dictionary of properties that should be set:
219 self.propdict = propdict
221 def match(self, basename):
222 """Does the file with the specified basename match pattern?"""
224 return fnmatch.fnmatch(basename, self.pattern)
226 def __init__(self, configfilename, ignore_case=True):
227 config = ConfigParser.ConfigParser()
228 if ignore_case:
229 self.transform_case = _squash_case
230 else:
231 config.optionxform = _preserve_case
232 self.transform_case = _preserve_case
234 configtext = open(configfilename).read()
235 if self.comment_re.search(configtext):
236 logger.warn(
237 '%s: Please be aware that a space followed by a\n'
238 'semicolon is sometimes treated as a comment in configuration\n'
239 'files. This pattern was seen in\n'
240 ' %s\n'
241 'Please make sure that you have not inadvertently commented\n'
242 'out part of an important line.'
243 % (warning_prefix, configfilename,)
246 config.readfp(StringIO(configtext), configfilename)
247 self.patterns = []
248 sections = config.sections()
249 sections.sort()
250 for section in sections:
251 if self.transform_case(section) == 'auto-props':
252 patterns = config.options(section)
253 patterns.sort()
254 for pattern in patterns:
255 value = config.get(section, pattern)
256 if value:
257 self._add_pattern(pattern, value)
259 def _add_pattern(self, pattern, props):
260 propdict = {}
261 if self.quoted_re.match(pattern):
262 logger.warn(
263 '%s: Quoting is not supported in auto-props; please verify rule\n'
264 'for %r. (Using pattern including quotation marks.)\n'
265 % (warning_prefix, pattern,)
267 for prop in props.split(';'):
268 prop = prop.strip()
269 m = self.property_unset_re.match(prop)
270 if m:
271 name = m.group('name')
272 logger.debug(
273 'auto-props: For %r, leaving %r unset.' % (pattern, name,)
275 propdict[name] = None
276 continue
278 m = self.property_set_re.match(prop)
279 if m:
280 name = m.group('name')
281 value = m.group('value')
282 if self.quoted_re.match(value):
283 logger.warn(
284 '%s: Quoting is not supported in auto-props; please verify\n'
285 'rule %r for pattern %r. (Using value\n'
286 'including quotation marks.)\n'
287 % (warning_prefix, prop, pattern,)
289 logger.debug(
290 'auto-props: For %r, setting %r to %r.' % (pattern, name, value,)
292 propdict[name] = value
293 continue
295 m = self.property_novalue_re.match(prop)
296 if m:
297 name = m.group('name')
298 logger.debug(
299 'auto-props: For %r, setting %r to the empty string'
300 % (pattern, name,)
302 propdict[name] = ''
303 continue
305 logger.warn(
306 '%s: in auto-props line for %r, value %r cannot be parsed (ignored)'
307 % (warning_prefix, pattern, prop,)
310 self.patterns.append(self.Pattern(self.transform_case(pattern), propdict))
312 def get_propdict(self, cvs_file):
313 basename = self.transform_case(cvs_file.rcs_basename)
314 propdict = {}
315 for pattern in self.patterns:
316 if pattern.match(basename):
317 for (key,value) in pattern.propdict.items():
318 if key in propdict:
319 if propdict[key] != value:
320 logger.warn(
321 "Contradictory values set for property '%s' for file %s."
322 % (key, cvs_file,))
323 else:
324 propdict[key] = value
326 return propdict
328 def set_properties(self, cvs_file):
329 propdict = self.get_propdict(cvs_file)
330 for (k,v) in propdict.items():
331 if k in cvs_file.properties:
332 if cvs_file.properties[k] != v:
333 logger.warn(
334 "Property '%s' already set to %r for file %s; "
335 "auto-props value (%r) ignored."
336 % (k, cvs_file.properties[k], cvs_file.cvs_path, v,)
338 else:
339 cvs_file.properties[k] = v
342 class CVSBinaryFileDefaultMimeTypeSetter(FilePropertySetter):
343 """If the file is binary and its svn:mime-type property is not yet
344 set, set it to 'application/octet-stream'."""
346 def set_properties(self, cvs_file):
347 if cvs_file.mode == 'b':
348 self.maybe_set_property(
349 cvs_file, 'svn:mime-type', 'application/octet-stream'
353 class EOLStyleFromMimeTypeSetter(FilePropertySetter):
354 """Set svn:eol-style based on svn:mime-type.
356 If svn:mime-type is known but svn:eol-style is not, then set
357 svn:eol-style based on svn:mime-type as follows: if svn:mime-type
358 starts with 'text/', then set svn:eol-style to native; otherwise,
359 force it to remain unset. See also issue #39."""
361 propname = 'svn:eol-style'
363 def set_properties(self, cvs_file):
364 if self.propname in cvs_file.properties:
365 return
367 mime_type = cvs_file.properties.get('svn:mime-type', None)
368 if mime_type:
369 if mime_type.startswith("text/"):
370 cvs_file.properties[self.propname] = 'native'
371 else:
372 cvs_file.properties[self.propname] = None
375 class DefaultEOLStyleSetter(FilePropertySetter):
376 """Set the eol-style if one has not already been set."""
378 valid_values = {
379 None : None,
380 # Also treat "binary" as None:
381 'binary' : None,
382 'native' : 'native',
383 'CRLF' : 'CRLF', 'LF' : 'LF', 'CR' : 'CR',
386 def __init__(self, value):
387 """Initialize with the specified default VALUE."""
389 try:
390 # Check that value is valid, and translate it to the proper case
391 self.value = self.valid_values[value]
392 except KeyError:
393 raise ValueError(
394 'Illegal value specified for the default EOL option: %r' % (value,)
397 def set_properties(self, cvs_file):
398 self.maybe_set_property(cvs_file, 'svn:eol-style', self.value)
401 class SVNBinaryFileKeywordsPropertySetter(FilePropertySetter):
402 """Turn off svn:keywords for files with binary svn:eol-style."""
404 propname = 'svn:keywords'
406 def set_properties(self, cvs_file):
407 if self.propname in cvs_file.properties:
408 return
410 if not cvs_file.properties.get('svn:eol-style'):
411 cvs_file.properties[self.propname] = None
414 class KeywordsPropertySetter(FilePropertySetter):
415 """If the svn:keywords property is not yet set, set it based on the
416 file's mode. See issue #2."""
418 def __init__(self, value):
419 """Use VALUE for the value of the svn:keywords property if it is
420 to be set."""
422 self.value = value
424 def set_properties(self, cvs_file):
425 if cvs_file.mode in [None, 'kv', 'kvl']:
426 self.maybe_set_property(cvs_file, 'svn:keywords', self.value)
429 class RevisionPropertySetter:
430 """Abstract class for objects that can set properties on a CVSRevision."""
432 def set_properties(self, cvs_rev):
433 """Set any properties that can be determined for CVS_REV.
435 CVS_REV is an instance of CVSRevision. This method should modify
436 CVS_REV.properties in place."""
438 raise NotImplementedError()
441 class CVSRevisionNumberSetter(RevisionPropertySetter):
442 """Store the CVS revision number to an SVN property."""
444 def __init__(self, propname='cvs2svn:cvs-rev'):
445 self.propname = propname
447 def set_properties(self, cvs_rev):
448 if self.propname in cvs_rev.properties:
449 return
451 cvs_rev.properties[self.propname] = cvs_rev.rev