Remove some unused variable definitions.
[cvs2svn.git] / cvs2svn_lib / property_setters.py
blob8e2eb2e4b5d2758af193a92db34d149b9b628206
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 configtext = open(configfilename).read()
241 if self.comment_re.search(configtext):
242 logger.warn(
243 '%s: Please be aware that a space followed by a\n'
244 'semicolon is sometimes treated as a comment in configuration\n'
245 'files. This pattern was seen in\n'
246 ' %s\n'
247 'Please make sure that you have not inadvertently commented\n'
248 'out part of an important line.'
249 % (warning_prefix, configfilename,)
252 config.readfp(StringIO(configtext), configfilename)
253 self.patterns = []
254 sections = config.sections()
255 sections.sort()
256 for section in sections:
257 if self.transform_case(section) == 'auto-props':
258 patterns = config.options(section)
259 patterns.sort()
260 for pattern in patterns:
261 value = config.get(section, pattern)
262 if value:
263 self._add_pattern(pattern, value)
265 def _add_pattern(self, pattern, props):
266 propdict = {}
267 if self.quoted_re.match(pattern):
268 logger.warn(
269 '%s: Quoting is not supported in auto-props; please verify rule\n'
270 'for %r. (Using pattern including quotation marks.)\n'
271 % (warning_prefix, pattern,)
273 for prop in props.split(';'):
274 prop = prop.strip()
275 m = self.property_unset_re.match(prop)
276 if m:
277 name = m.group('name')
278 logger.debug(
279 'auto-props: For %r, leaving %r unset.' % (pattern, name,)
281 propdict[name] = None
282 continue
284 m = self.property_set_re.match(prop)
285 if m:
286 name = m.group('name')
287 value = m.group('value')
288 if self.quoted_re.match(value):
289 logger.warn(
290 '%s: Quoting is not supported in auto-props; please verify\n'
291 'rule %r for pattern %r. (Using value\n'
292 'including quotation marks.)\n'
293 % (warning_prefix, prop, pattern,)
295 logger.debug(
296 'auto-props: For %r, setting %r to %r.' % (pattern, name, value,)
298 propdict[name] = value
299 continue
301 m = self.property_novalue_re.match(prop)
302 if m:
303 name = m.group('name')
304 logger.debug(
305 'auto-props: For %r, setting %r to the empty string'
306 % (pattern, name,)
308 propdict[name] = ''
309 continue
311 logger.warn(
312 '%s: in auto-props line for %r, value %r cannot be parsed (ignored)'
313 % (warning_prefix, pattern, prop,)
316 self.patterns.append(self.Pattern(self.transform_case(pattern), propdict))
318 def get_propdict(self, cvs_file):
319 basename = self.transform_case(cvs_file.rcs_basename)
320 propdict = {}
321 for pattern in self.patterns:
322 if pattern.match(basename):
323 for (key,value) in pattern.propdict.items():
324 if key in propdict:
325 if propdict[key] != value:
326 logger.warn(
327 "Contradictory values set for property '%s' for file %s."
328 % (key, cvs_file,))
329 else:
330 propdict[key] = value
332 return propdict
334 def set_properties(self, cvs_file):
335 propdict = self.get_propdict(cvs_file)
336 for (k,v) in propdict.items():
337 if k in cvs_file.properties:
338 if cvs_file.properties[k] != v:
339 logger.warn(
340 "Property '%s' already set to %r for file %s; "
341 "auto-props value (%r) ignored."
342 % (k, cvs_file.properties[k], cvs_file.cvs_path, v,)
344 else:
345 cvs_file.properties[k] = v
348 class CVSBinaryFileDefaultMimeTypeSetter(FilePropertySetter):
349 """If the file is binary and its svn:mime-type property is not yet
350 set, set it to 'application/octet-stream'."""
352 def set_properties(self, cvs_file):
353 if cvs_file.mode == 'b':
354 self.maybe_set_property(
355 cvs_file, 'svn:mime-type', 'application/octet-stream'
359 class EOLStyleFromMimeTypeSetter(FilePropertySetter):
360 """Set svn:eol-style based on svn:mime-type.
362 If svn:mime-type is known but svn:eol-style is not, then set
363 svn:eol-style based on svn:mime-type as follows: if svn:mime-type
364 starts with 'text/', then set svn:eol-style to native; otherwise,
365 force it to remain unset. See also issue #39."""
367 propname = 'svn:eol-style'
369 def set_properties(self, cvs_file):
370 if self.propname in cvs_file.properties:
371 return
373 mime_type = cvs_file.properties.get('svn:mime-type', None)
374 if mime_type:
375 if mime_type.startswith("text/"):
376 cvs_file.properties[self.propname] = 'native'
377 else:
378 cvs_file.properties[self.propname] = None
381 class DefaultEOLStyleSetter(FilePropertySetter):
382 """Set the eol-style if one has not already been set."""
384 valid_values = {
385 None : None,
386 # Also treat "binary" as None:
387 'binary' : None,
388 'native' : 'native',
389 'CRLF' : 'CRLF', 'LF' : 'LF', 'CR' : 'CR',
392 def __init__(self, value):
393 """Initialize with the specified default VALUE."""
395 try:
396 # Check that value is valid, and translate it to the proper case
397 self.value = self.valid_values[value]
398 except KeyError:
399 raise ValueError(
400 'Illegal value specified for the default EOL option: %r' % (value,)
403 def set_properties(self, cvs_file):
404 self.maybe_set_property(cvs_file, 'svn:eol-style', self.value)
407 class SVNBinaryFileKeywordsPropertySetter(FilePropertySetter):
408 """Turn off svn:keywords for files with binary svn:eol-style."""
410 propname = 'svn:keywords'
412 def set_properties(self, cvs_file):
413 if self.propname in cvs_file.properties:
414 return
416 if not cvs_file.properties.get('svn:eol-style'):
417 cvs_file.properties[self.propname] = None
420 class KeywordsPropertySetter(FilePropertySetter):
421 """If the svn:keywords property is not yet set, set it based on the
422 file's mode. See issue #2."""
424 def __init__(self, value):
425 """Use VALUE for the value of the svn:keywords property if it is
426 to be set."""
428 self.value = value
430 def set_properties(self, cvs_file):
431 if cvs_file.mode in [None, 'kv', 'kvl']:
432 self.maybe_set_property(cvs_file, 'svn:keywords', self.value)
435 class ConditionalPropertySetter(object):
436 """Delegate to the passed property setters when the passed predicate applies.
437 The predicate should be a function that takes a CVSFile or CVSRevision
438 argument and return True if the property setters should be applied."""
440 def __init__(self, predicate, *property_setters):
441 self.predicate = predicate
442 self.property_setters = property_setters
444 def set_properties(self, cvs_file_or_rev):
445 if self.predicate(cvs_file_or_rev):
446 for property_setter in self.property_setters:
447 property_setter.set_properties(cvs_file_or_rev)
450 class RevisionPropertySetter:
451 """Abstract class for objects that can set properties on a CVSRevision."""
453 def set_properties(self, cvs_rev):
454 """Set any properties that can be determined for CVS_REV.
456 CVS_REV is an instance of CVSRevision. This method should modify
457 CVS_REV.properties in place."""
459 raise NotImplementedError()
462 class CVSRevisionNumberSetter(RevisionPropertySetter):
463 """Store the CVS revision number to an SVN property."""
465 def __init__(self, propname='cvs2svn:cvs-rev'):
466 self.propname = propname
468 def set_properties(self, cvs_rev):
469 if self.propname in cvs_rev.properties:
470 return
472 cvs_rev.properties[self.propname] = cvs_rev.rev