cvs2git: Make the --blobfile argument optional.
[cvs2svn.git] / cvs2svn_lib / property_setters.py
blob79d06b3648600fbb41d3c81fb81fc47351600b69
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 def cvs_file_is_binary(cvs_file):
39 return cvs_file.mode == 'b'
42 class FilePropertySetter(object):
43 """Abstract class for objects that set properties on a CVSFile."""
45 def maybe_set_property(self, cvs_file, name, value):
46 """Set a property on CVS_FILE if it does not already have a value.
48 This method is here for the convenience of derived classes."""
50 if name not in cvs_file.properties:
51 cvs_file.properties[name] = value
53 def set_properties(self, cvs_file):
54 """Set any properties needed for CVS_FILE.
56 CVS_FILE is an instance of CVSFile. This method should modify
57 CVS_FILE.properties in place."""
59 raise NotImplementedError()
62 class ExecutablePropertySetter(FilePropertySetter):
63 """Set the svn:executable property based on cvs_file.executable."""
65 def set_properties(self, cvs_file):
66 if cvs_file.executable:
67 self.maybe_set_property(cvs_file, 'svn:executable', '*')
70 class DescriptionPropertySetter(FilePropertySetter):
71 """Set the cvs:description property based on cvs_file.description."""
73 def __init__(self, propname='cvs:description'):
74 self.propname = propname
76 def set_properties(self, cvs_file):
77 if cvs_file.description:
78 self.maybe_set_property(cvs_file, self.propname, cvs_file.description)
81 class CVSBinaryFileEOLStyleSetter(FilePropertySetter):
82 """Set the eol-style to None for files with CVS mode '-kb'."""
84 def set_properties(self, cvs_file):
85 if cvs_file.mode == 'b':
86 self.maybe_set_property(cvs_file, 'svn:eol-style', None)
89 class MimeMapper(FilePropertySetter):
90 """A class that provides mappings from file names to MIME types."""
92 propname = 'svn:mime-type'
94 def __init__(
95 self, mime_types_file=None, mime_mappings=None,
96 ignore_case=False
98 """Constructor.
100 Arguments:
102 mime_types_file -- a path to a MIME types file on disk. Each
103 line of the file should contain the MIME type, then a
104 whitespace-separated list of file extensions; e.g., one line
105 might be 'text/plain txt c h cpp hpp'. (See
106 http://en.wikipedia.org/wiki/Mime.types for information
107 about mime.types files):
109 mime_mappings -- a dictionary mapping a file extension to a MIME
110 type; e.g., {'txt': 'text/plain', 'cpp': 'text/plain'}.
112 ignore_case -- True iff case should be ignored in filename
113 extensions. Setting this option to True can be useful if
114 your CVS repository was used on systems with
115 case-insensitive filenames, in which case you might have a
116 mix of uppercase and lowercase filenames."""
118 self.mappings = { }
119 if ignore_case:
120 self.transform_case = _squash_case
121 else:
122 self.transform_case = _preserve_case
124 if mime_types_file is None and mime_mappings is None:
125 logger.error('Should specify MIME types file or dict.\n')
127 if mime_types_file is not None:
128 for line in file(mime_types_file):
129 if line.startswith("#"):
130 continue
132 # format of a line is something like
133 # text/plain c h cpp
134 extensions = line.split()
135 if len(extensions) < 2:
136 continue
137 type = extensions.pop(0)
138 for ext in extensions:
139 ext = self.transform_case(ext)
140 if ext in self.mappings and self.mappings[ext] != type:
141 logger.error(
142 "%s: ambiguous MIME mapping for *.%s (%s or %s)\n"
143 % (warning_prefix, ext, self.mappings[ext], type)
145 self.mappings[ext] = type
147 if mime_mappings is not None:
148 for ext, type in mime_mappings.iteritems():
149 ext = self.transform_case(ext)
150 if ext in self.mappings and self.mappings[ext] != type:
151 logger.error(
152 "%s: ambiguous MIME mapping for *.%s (%s or %s)\n"
153 % (warning_prefix, ext, self.mappings[ext], type)
155 self.mappings[ext] = type
157 def set_properties(self, cvs_file):
158 if self.propname in cvs_file.properties:
159 return
161 basename, extension = os.path.splitext(cvs_file.rcs_basename)
163 # Extension includes the dot, so strip it (will leave extension
164 # empty if filename ends with a dot, which is ok):
165 extension = extension[1:]
167 # If there is no extension (or the file ends with a period), use
168 # the base name for mapping. This allows us to set mappings for
169 # files such as README or Makefile:
170 if not extension:
171 extension = basename
173 extension = self.transform_case(extension)
175 mime_type = self.mappings.get(extension, None)
176 if mime_type is not None:
177 cvs_file.properties[self.propname] = mime_type
180 class AutoPropsPropertySetter(FilePropertySetter):
181 """Set arbitrary svn properties based on an auto-props configuration.
183 This class supports case-sensitive or case-insensitive pattern
184 matching. The command-line default is case-insensitive behavior,
185 consistent with Subversion (see
186 http://subversion.tigris.org/issues/show_bug.cgi?id=2036).
188 As a special extension to Subversion's auto-props handling, if a
189 property name is preceded by a '!' then that property is forced to
190 be left unset.
192 If a property specified in auto-props has already been set to a
193 different value, print a warning and leave the old property value
194 unchanged.
196 Python's treatment of whitespaces in the ConfigParser module is
197 buggy and inconsistent. Usually spaces are preserved, but if there
198 is at least one semicolon in the value, and the *first* semicolon is
199 preceded by a space, then that is treated as the start of a comment
200 and the rest of the line is silently discarded."""
202 property_name_pattern = r'(?P<name>[^\!\=\s]+)'
203 property_unset_re = re.compile(
204 r'^\!\s*' + property_name_pattern + r'$'
206 property_set_re = re.compile(
207 r'^' + property_name_pattern + r'\s*\=\s*(?P<value>.*)$'
209 property_novalue_re = re.compile(
210 r'^' + property_name_pattern + r'$'
213 quoted_re = re.compile(
214 r'^([\'\"]).*\1$'
216 comment_re = re.compile(r'\s;')
218 class Pattern:
219 """Describes the properties to be set for files matching a pattern."""
221 def __init__(self, pattern, propdict):
222 # A glob-like pattern:
223 self.pattern = pattern
224 # A dictionary of properties that should be set:
225 self.propdict = propdict
227 def match(self, basename):
228 """Does the file with the specified basename match pattern?"""
230 return fnmatch.fnmatch(basename, self.pattern)
232 def __init__(self, configfilename, ignore_case=True):
233 config = ConfigParser.ConfigParser()
234 if ignore_case:
235 self.transform_case = _squash_case
236 else:
237 config.optionxform = _preserve_case
238 self.transform_case = _preserve_case
240 f = open(configfilename)
241 configtext = f.read()
242 f.close()
243 if self.comment_re.search(configtext):
244 logger.warn(
245 '%s: Please be aware that a space followed by a\n'
246 'semicolon is sometimes treated as a comment in configuration\n'
247 'files. This pattern was seen in\n'
248 ' %s\n'
249 'Please make sure that you have not inadvertently commented\n'
250 'out part of an important line.'
251 % (warning_prefix, configfilename,)
254 config.readfp(StringIO(configtext), configfilename)
255 self.patterns = []
256 sections = config.sections()
257 sections.sort()
258 for section in sections:
259 if self.transform_case(section) == 'auto-props':
260 patterns = config.options(section)
261 patterns.sort()
262 for pattern in patterns:
263 value = config.get(section, pattern)
264 if value:
265 self._add_pattern(pattern, value)
267 def _add_pattern(self, pattern, props):
268 propdict = {}
269 if self.quoted_re.match(pattern):
270 logger.warn(
271 '%s: Quoting is not supported in auto-props; please verify rule\n'
272 'for %r. (Using pattern including quotation marks.)\n'
273 % (warning_prefix, pattern,)
275 for prop in props.split(';'):
276 prop = prop.strip()
277 m = self.property_unset_re.match(prop)
278 if m:
279 name = m.group('name')
280 logger.debug(
281 'auto-props: For %r, leaving %r unset.' % (pattern, name,)
283 propdict[name] = None
284 continue
286 m = self.property_set_re.match(prop)
287 if m:
288 name = m.group('name')
289 value = m.group('value')
290 if self.quoted_re.match(value):
291 logger.warn(
292 '%s: Quoting is not supported in auto-props; please verify\n'
293 'rule %r for pattern %r. (Using value\n'
294 'including quotation marks.)\n'
295 % (warning_prefix, prop, pattern,)
297 logger.debug(
298 'auto-props: For %r, setting %r to %r.' % (pattern, name, value,)
300 propdict[name] = value
301 continue
303 m = self.property_novalue_re.match(prop)
304 if m:
305 name = m.group('name')
306 logger.debug(
307 'auto-props: For %r, setting %r to the empty string'
308 % (pattern, name,)
310 propdict[name] = ''
311 continue
313 logger.warn(
314 '%s: in auto-props line for %r, value %r cannot be parsed (ignored)'
315 % (warning_prefix, pattern, prop,)
318 self.patterns.append(self.Pattern(self.transform_case(pattern), propdict))
320 def get_propdict(self, cvs_file):
321 basename = self.transform_case(cvs_file.rcs_basename)
322 propdict = {}
323 for pattern in self.patterns:
324 if pattern.match(basename):
325 for (key,value) in pattern.propdict.items():
326 if key in propdict:
327 if propdict[key] != value:
328 logger.warn(
329 "Contradictory values set for property '%s' for file %s."
330 % (key, cvs_file,))
331 else:
332 propdict[key] = value
334 return propdict
336 def set_properties(self, cvs_file):
337 propdict = self.get_propdict(cvs_file)
338 for (k,v) in propdict.items():
339 if k in cvs_file.properties:
340 if cvs_file.properties[k] != v:
341 logger.warn(
342 "Property '%s' already set to %r for file %s; "
343 "auto-props value (%r) ignored."
344 % (k, cvs_file.properties[k], cvs_file.cvs_path, v,)
346 else:
347 cvs_file.properties[k] = v
350 class CVSBinaryFileDefaultMimeTypeSetter(FilePropertySetter):
351 """If the file is binary and its svn:mime-type property is not yet
352 set, set it to 'application/octet-stream'."""
354 def set_properties(self, cvs_file):
355 if cvs_file.mode == 'b':
356 self.maybe_set_property(
357 cvs_file, 'svn:mime-type', 'application/octet-stream'
361 class EOLStyleFromMimeTypeSetter(FilePropertySetter):
362 """Set svn:eol-style based on svn:mime-type.
364 If svn:mime-type is known but svn:eol-style is not, then set
365 svn:eol-style based on svn:mime-type as follows: if svn:mime-type
366 starts with 'text/', then set svn:eol-style to native; otherwise,
367 force it to remain unset. See also issue #39."""
369 propname = 'svn:eol-style'
371 def set_properties(self, cvs_file):
372 if self.propname in cvs_file.properties:
373 return
375 mime_type = cvs_file.properties.get('svn:mime-type', None)
376 if mime_type:
377 if mime_type.startswith("text/"):
378 cvs_file.properties[self.propname] = 'native'
379 else:
380 cvs_file.properties[self.propname] = None
383 class DefaultEOLStyleSetter(FilePropertySetter):
384 """Set the eol-style if one has not already been set."""
386 valid_values = {
387 None : None,
388 # Also treat "binary" as None:
389 'binary' : None,
390 'native' : 'native',
391 'CRLF' : 'CRLF', 'LF' : 'LF', 'CR' : 'CR',
394 def __init__(self, value):
395 """Initialize with the specified default VALUE."""
397 try:
398 # Check that value is valid, and translate it to the proper case
399 self.value = self.valid_values[value]
400 except KeyError:
401 raise ValueError(
402 'Illegal value specified for the default EOL option: %r' % (value,)
405 def set_properties(self, cvs_file):
406 self.maybe_set_property(cvs_file, 'svn:eol-style', self.value)
409 class SVNBinaryFileKeywordsPropertySetter(FilePropertySetter):
410 """Turn off svn:keywords for files with binary svn:eol-style."""
412 propname = 'svn:keywords'
414 def set_properties(self, cvs_file):
415 if self.propname in cvs_file.properties:
416 return
418 if not cvs_file.properties.get('svn:eol-style'):
419 cvs_file.properties[self.propname] = None
422 class KeywordsPropertySetter(FilePropertySetter):
423 """If the svn:keywords property is not yet set, set it based on the
424 file's mode. See issue #2."""
426 def __init__(self, value):
427 """Use VALUE for the value of the svn:keywords property if it is
428 to be set."""
430 self.value = value
432 def set_properties(self, cvs_file):
433 if cvs_file.mode in [None, 'kv', 'kvl']:
434 self.maybe_set_property(cvs_file, 'svn:keywords', self.value)
437 class ConditionalPropertySetter(object):
438 """Delegate to the passed property setters when the passed predicate applies.
439 The predicate should be a function that takes a CVSFile or CVSRevision
440 argument and return True if the property setters should be applied."""
442 def __init__(self, predicate, *property_setters):
443 self.predicate = predicate
444 self.property_setters = property_setters
446 def set_properties(self, cvs_file_or_rev):
447 if self.predicate(cvs_file_or_rev):
448 for property_setter in self.property_setters:
449 property_setter.set_properties(cvs_file_or_rev)
452 class RevisionPropertySetter:
453 """Abstract class for objects that can set properties on a CVSRevision."""
455 def set_properties(self, cvs_rev):
456 """Set any properties that can be determined for CVS_REV.
458 CVS_REV is an instance of CVSRevision. This method should modify
459 CVS_REV.properties in place."""
461 raise NotImplementedError()
464 class CVSRevisionNumberSetter(RevisionPropertySetter):
465 """Store the CVS revision number to an SVN property."""
467 def __init__(self, propname='cvs2svn:cvs-rev'):
468 self.propname = propname
470 def set_properties(self, cvs_rev):
471 if self.propname in cvs_rev.properties:
472 return
474 cvs_rev.properties[self.propname] = cvs_rev.rev