Add a convenience method FilePropertySetter.maybe_set_property().
[cvs2svn.git] / cvs2svn_lib / property_setters.py
blob11cfe738fa580fcc3f9d18ab4937a497c4e083a3
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 propname = 'svn:executable'
63 def set_properties(self, cvs_file):
64 if self.propname in cvs_file.properties:
65 return
67 if cvs_file.executable:
68 cvs_file.properties[self.propname] = '*'
71 class DescriptionPropertySetter(FilePropertySetter):
72 """Set the cvs:description property based on cvs_file.description."""
74 def __init__(self, propname='cvs:description'):
75 self.propname = propname
77 def set_properties(self, cvs_file):
78 if self.propname in cvs_file.properties:
79 return
81 if cvs_file.description:
82 cvs_file.properties[self.propname] = cvs_file.description
85 class CVSBinaryFileEOLStyleSetter(FilePropertySetter):
86 """Set the eol-style to None for files with CVS mode '-kb'."""
88 propname = 'svn:eol-style'
90 def set_properties(self, cvs_file):
91 if self.propname in cvs_file.properties:
92 return
94 if cvs_file.mode == 'b':
95 cvs_file.properties[self.propname] = None
98 class MimeMapper(FilePropertySetter):
99 """A class that provides mappings from file names to MIME types."""
101 propname = 'svn:mime-type'
103 def __init__(
104 self, mime_types_file=None, mime_mappings=None,
105 ignore_case=False
107 """Constructor.
109 Arguments:
111 mime_types_file -- a path to a MIME types file on disk. Each
112 line of the file should contain the MIME type, then a
113 whitespace-separated list of file extensions; e.g., one line
114 might be 'text/plain txt c h cpp hpp'.
116 mime_mappings -- a dictionary mapping a file extension to a MIME
117 type; e.g., {'txt': 'text/plain', 'cpp': 'text/plain'}.
119 ignore_case -- True iff case should be ignored in filename
120 extensions. Setting this option to True can be useful if
121 your CVS repository was used on systems with
122 case-insensitive filenames, in which case you might have a
123 mix of uppercase and lowercase filenames."""
125 self.mappings = { }
126 if ignore_case:
127 self.transform_case = _squash_case
128 else:
129 self.transform_case = _preserve_case
131 if mime_types_file is None and mime_mappings is None:
132 logger.error('Should specify MIME types file or dict.\n')
134 if mime_types_file is not None:
135 for line in file(mime_types_file):
136 if line.startswith("#"):
137 continue
139 # format of a line is something like
140 # text/plain c h cpp
141 extensions = line.split()
142 if len(extensions) < 2:
143 continue
144 type = extensions.pop(0)
145 for ext in extensions:
146 ext = self.transform_case(ext)
147 if ext in self.mappings and self.mappings[ext] != type:
148 logger.error(
149 "%s: ambiguous MIME mapping for *.%s (%s or %s)\n"
150 % (warning_prefix, ext, self.mappings[ext], type)
152 self.mappings[ext] = type
154 if mime_mappings is not None:
155 for ext, type in mime_mappings.iteritems():
156 ext = self.transform_case(ext)
157 if ext in self.mappings and self.mappings[ext] != type:
158 logger.error(
159 "%s: ambiguous MIME mapping for *.%s (%s or %s)\n"
160 % (warning_prefix, ext, self.mappings[ext], type)
162 self.mappings[ext] = type
164 def set_properties(self, cvs_file):
165 if self.propname in cvs_file.properties:
166 return
168 basename, extension = os.path.splitext(cvs_file.rcs_basename)
170 # Extension includes the dot, so strip it (will leave extension
171 # empty if filename ends with a dot, which is ok):
172 extension = extension[1:]
174 # If there is no extension (or the file ends with a period), use
175 # the base name for mapping. This allows us to set mappings for
176 # files such as README or Makefile:
177 if not extension:
178 extension = basename
180 extension = self.transform_case(extension)
182 mime_type = self.mappings.get(extension, None)
183 if mime_type is not None:
184 cvs_file.properties[self.propname] = mime_type
187 class AutoPropsPropertySetter(FilePropertySetter):
188 """Set arbitrary svn properties based on an auto-props configuration.
190 This class supports case-sensitive or case-insensitive pattern
191 matching. The command-line default is case-insensitive behavior,
192 consistent with Subversion (see
193 http://subversion.tigris.org/issues/show_bug.cgi?id=2036).
195 As a special extension to Subversion's auto-props handling, if a
196 property name is preceded by a '!' then that property is forced to
197 be left unset.
199 If a property specified in auto-props has already been set to a
200 different value, print a warning and leave the old property value
201 unchanged.
203 Python's treatment of whitespaces in the ConfigParser module is
204 buggy and inconsistent. Usually spaces are preserved, but if there
205 is at least one semicolon in the value, and the *first* semicolon is
206 preceded by a space, then that is treated as the start of a comment
207 and the rest of the line is silently discarded."""
209 property_name_pattern = r'(?P<name>[^\!\=\s]+)'
210 property_unset_re = re.compile(
211 r'^\!\s*' + property_name_pattern + r'$'
213 property_set_re = re.compile(
214 r'^' + property_name_pattern + r'\s*\=\s*(?P<value>.*)$'
216 property_novalue_re = re.compile(
217 r'^' + property_name_pattern + r'$'
220 quoted_re = re.compile(
221 r'^([\'\"]).*\1$'
223 comment_re = re.compile(r'\s;')
225 class Pattern:
226 """Describes the properties to be set for files matching a pattern."""
228 def __init__(self, pattern, propdict):
229 # A glob-like pattern:
230 self.pattern = pattern
231 # A dictionary of properties that should be set:
232 self.propdict = propdict
234 def match(self, basename):
235 """Does the file with the specified basename match pattern?"""
237 return fnmatch.fnmatch(basename, self.pattern)
239 def __init__(self, configfilename, ignore_case=True):
240 config = ConfigParser.ConfigParser()
241 if ignore_case:
242 self.transform_case = _squash_case
243 else:
244 config.optionxform = _preserve_case
245 self.transform_case = _preserve_case
247 configtext = open(configfilename).read()
248 if self.comment_re.search(configtext):
249 logger.warn(
250 '%s: Please be aware that a space followed by a\n'
251 'semicolon is sometimes treated as a comment in configuration\n'
252 'files. This pattern was seen in\n'
253 ' %s\n'
254 'Please make sure that you have not inadvertently commented\n'
255 'out part of an important line.'
256 % (warning_prefix, configfilename,)
259 config.readfp(StringIO(configtext), configfilename)
260 self.patterns = []
261 sections = config.sections()
262 sections.sort()
263 for section in sections:
264 if self.transform_case(section) == 'auto-props':
265 patterns = config.options(section)
266 patterns.sort()
267 for pattern in patterns:
268 value = config.get(section, pattern)
269 if value:
270 self._add_pattern(pattern, value)
272 def _add_pattern(self, pattern, props):
273 propdict = {}
274 if self.quoted_re.match(pattern):
275 logger.warn(
276 '%s: Quoting is not supported in auto-props; please verify rule\n'
277 'for %r. (Using pattern including quotation marks.)\n'
278 % (warning_prefix, pattern,)
280 for prop in props.split(';'):
281 prop = prop.strip()
282 m = self.property_unset_re.match(prop)
283 if m:
284 name = m.group('name')
285 logger.debug(
286 'auto-props: For %r, leaving %r unset.' % (pattern, name,)
288 propdict[name] = None
289 continue
291 m = self.property_set_re.match(prop)
292 if m:
293 name = m.group('name')
294 value = m.group('value')
295 if self.quoted_re.match(value):
296 logger.warn(
297 '%s: Quoting is not supported in auto-props; please verify\n'
298 'rule %r for pattern %r. (Using value\n'
299 'including quotation marks.)\n'
300 % (warning_prefix, prop, pattern,)
302 logger.debug(
303 'auto-props: For %r, setting %r to %r.' % (pattern, name, value,)
305 propdict[name] = value
306 continue
308 m = self.property_novalue_re.match(prop)
309 if m:
310 name = m.group('name')
311 logger.debug(
312 'auto-props: For %r, setting %r to the empty string'
313 % (pattern, name,)
315 propdict[name] = ''
316 continue
318 logger.warn(
319 '%s: in auto-props line for %r, value %r cannot be parsed (ignored)'
320 % (warning_prefix, pattern, prop,)
323 self.patterns.append(self.Pattern(self.transform_case(pattern), propdict))
325 def get_propdict(self, cvs_file):
326 basename = self.transform_case(cvs_file.rcs_basename)
327 propdict = {}
328 for pattern in self.patterns:
329 if pattern.match(basename):
330 for (key,value) in pattern.propdict.items():
331 if key in propdict:
332 if propdict[key] != value:
333 logger.warn(
334 "Contradictory values set for property '%s' for file %s."
335 % (key, cvs_file,))
336 else:
337 propdict[key] = value
339 return propdict
341 def set_properties(self, cvs_file):
342 propdict = self.get_propdict(cvs_file)
343 for (k,v) in propdict.items():
344 if k in cvs_file.properties:
345 if cvs_file.properties[k] != v:
346 logger.warn(
347 "Property '%s' already set to %r for file %s; "
348 "auto-props value (%r) ignored."
349 % (k, cvs_file.properties[k], cvs_file.cvs_path, v,)
351 else:
352 cvs_file.properties[k] = v
355 class CVSBinaryFileDefaultMimeTypeSetter(FilePropertySetter):
356 """If the file is binary and its svn:mime-type property is not yet
357 set, set it to 'application/octet-stream'."""
359 propname = 'svn:mime-type'
361 def set_properties(self, cvs_file):
362 if self.propname in cvs_file.properties:
363 return
365 if cvs_file.mode == 'b':
366 cvs_file.properties[self.propname] = 'application/octet-stream'
369 class EOLStyleFromMimeTypeSetter(FilePropertySetter):
370 """Set svn:eol-style based on svn:mime-type.
372 If svn:mime-type is known but svn:eol-style is not, then set
373 svn:eol-style based on svn:mime-type as follows: if svn:mime-type
374 starts with 'text/', then set svn:eol-style to native; otherwise,
375 force it to remain unset. See also issue #39."""
377 propname = 'svn:eol-style'
379 def set_properties(self, cvs_file):
380 if self.propname in cvs_file.properties:
381 return
383 if cvs_file.properties.get('svn:mime-type', None) is not None:
384 if cvs_file.properties['svn:mime-type'].startswith("text/"):
385 cvs_file.properties[self.propname] = 'native'
386 else:
387 cvs_file.properties[self.propname] = None
390 class DefaultEOLStyleSetter(FilePropertySetter):
391 """Set the eol-style if one has not already been set."""
393 propname = 'svn:eol-style'
395 def __init__(self, value):
396 """Initialize with the specified default VALUE."""
398 self.value = value
400 def set_properties(self, cvs_file):
401 if self.propname in cvs_file.properties:
402 return
404 cvs_file.properties[self.propname] = 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 propname = 'svn:keywords'
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 self.propname in cvs_file.properties:
434 return
436 if cvs_file.mode in [None, 'kv', 'kvl']:
437 cvs_file.properties[self.propname] = self.value
440 class RevisionPropertySetter:
441 """Abstract class for objects that can set properties on a CVSRevision."""
443 def set_properties(self, cvs_rev):
444 """Set any properties that can be determined for CVS_REV.
446 CVS_REV is an instance of CVSRevision. This method should modify
447 CVS_REV.properties in place."""
449 raise NotImplementedError()
452 class CVSRevisionNumberSetter(RevisionPropertySetter):
453 """Store the CVS revision number to an SVN property."""
455 def __init__(self, propname='cvs2svn:cvs-rev'):
456 self.propname = propname
458 def set_properties(self, cvs_rev):
459 if self.propname in cvs_rev.properties:
460 return
462 cvs_rev.properties[self.propname] = cvs_rev.rev