Change EOLStyleFromMimeTypeSetter into a FilePropertySetter.
[cvs2svn.git] / cvs2svn_lib / property_setters.py
blob22ee85c4ab27c5d5f1c4ad72451547ee25ed3614
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 Log
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 set_properties(self, cvs_file):
42 """Set any properties needed for CVS_FILE.
44 CVS_FILE is an instance of CVSFile. This method should modify
45 CVS_FILE.properties in place."""
47 raise NotImplementedError()
50 class RevisionPropertySetter:
51 """Abstract class for objects that can set properties on a SVNCommitItem."""
53 def set_properties(self, s_item):
54 """Set any properties that can be determined for S_ITEM.
56 S_ITEM is an instance of SVNCommitItem. This method should modify
57 S_ITEM.svn_props in place."""
59 raise NotImplementedError
62 class CVSRevisionNumberSetter(RevisionPropertySetter):
63 """Store the CVS revision number to an SVN property."""
65 def __init__(self, propname='cvs2svn:cvs-rev'):
66 self.propname = propname
68 def set_properties(self, s_item):
69 if self.propname in s_item.svn_props:
70 return
72 s_item.svn_props[self.propname] = s_item.cvs_rev.rev
73 s_item.svn_props_changed = True
76 class ExecutablePropertySetter(RevisionPropertySetter):
77 """Set the svn:executable property based on cvs_rev.cvs_file.executable."""
79 propname = 'svn:executable'
81 def set_properties(self, s_item):
82 if self.propname in s_item.svn_props:
83 return
85 if s_item.cvs_rev.cvs_file.executable:
86 s_item.svn_props[self.propname] = '*'
89 class DescriptionPropertySetter(RevisionPropertySetter):
90 """Set the cvs:description property based on cvs_rev.cvs_file.description."""
92 def __init__(self, propname='cvs:description'):
93 self.propname = propname
95 def set_properties(self, s_item):
96 if self.propname in s_item.svn_props:
97 return
99 if s_item.cvs_rev.cvs_file.description:
100 s_item.svn_props[self.propname] = s_item.cvs_rev.cvs_file.description
103 class CVSBinaryFileEOLStyleSetter(FilePropertySetter):
104 """Set the eol-style to None for files with CVS mode '-kb'."""
106 propname = 'svn:eol-style'
108 def set_properties(self, cvs_file):
109 if self.propname in cvs_file.properties:
110 return
112 if cvs_file.mode == 'b':
113 cvs_file.properties[self.propname] = None
116 class MimeMapper(FilePropertySetter):
117 """A class that provides mappings from file names to MIME types."""
119 propname = 'svn:mime-type'
121 def __init__(
122 self, mime_types_file=None, mime_mappings=None,
123 ignore_case=False
125 """Constructor.
127 Arguments:
129 mime_types_file -- a path to a MIME types file on disk. Each
130 line of the file should contain the MIME type, then a
131 whitespace-separated list of file extensions; e.g., one line
132 might be 'text/plain txt c h cpp hpp'.
134 mime_mappings -- a dictionary mapping a file extension to a MIME
135 type; e.g., {'txt': 'text/plain', 'cpp': 'text/plain'}.
137 ignore_case -- True iff case should be ignored in filename
138 extensions. Setting this option to True can be useful if
139 your CVS repository was used on systems with
140 case-insensitive filenames, in which case you might have a
141 mix of uppercase and lowercase filenames."""
143 self.mappings = { }
144 if ignore_case:
145 self.transform_case = _squash_case
146 else:
147 self.transform_case = _preserve_case
149 if mime_types_file is None and mime_mappings is None:
150 Log().error('Should specify MIME types file or dict.\n')
152 if mime_types_file is not None:
153 for line in file(mime_types_file):
154 if line.startswith("#"):
155 continue
157 # format of a line is something like
158 # text/plain c h cpp
159 extensions = line.split()
160 if len(extensions) < 2:
161 continue
162 type = extensions.pop(0)
163 for ext in extensions:
164 ext = self.transform_case(ext)
165 if ext in self.mappings and self.mappings[ext] != type:
166 Log().error(
167 "%s: ambiguous MIME mapping for *.%s (%s or %s)\n"
168 % (warning_prefix, ext, self.mappings[ext], type)
170 self.mappings[ext] = type
172 if mime_mappings is not None:
173 for ext, type in mime_mappings.iteritems():
174 ext = self.transform_case(ext)
175 if ext in self.mappings and self.mappings[ext] != type:
176 Log().error(
177 "%s: ambiguous MIME mapping for *.%s (%s or %s)\n"
178 % (warning_prefix, ext, self.mappings[ext], type)
180 self.mappings[ext] = type
182 def set_properties(self, cvs_file):
183 if self.propname in cvs_file.properties:
184 return
186 basename, extension = os.path.splitext(cvs_file.basename)
188 # Extension includes the dot, so strip it (will leave extension
189 # empty if filename ends with a dot, which is ok):
190 extension = extension[1:]
192 # If there is no extension (or the file ends with a period), use
193 # the base name for mapping. This allows us to set mappings for
194 # files such as README or Makefile:
195 if not extension:
196 extension = basename
198 extension = self.transform_case(extension)
200 mime_type = self.mappings.get(extension, None)
201 if mime_type is not None:
202 cvs_file.properties[self.propname] = mime_type
205 class AutoPropsPropertySetter(FilePropertySetter):
206 """Set arbitrary svn properties based on an auto-props configuration.
208 This class supports case-sensitive or case-insensitive pattern
209 matching. The command-line default is case-insensitive behavior,
210 consistent with Subversion (see
211 http://subversion.tigris.org/issues/show_bug.cgi?id=2036).
213 As a special extension to Subversion's auto-props handling, if a
214 property name is preceded by a '!' then that property is forced to
215 be left unset.
217 If a property specified in auto-props has already been set to a
218 different value, print a warning and leave the old property value
219 unchanged.
221 Python's treatment of whitespaces in the ConfigParser module is
222 buggy and inconsistent. Usually spaces are preserved, but if there
223 is at least one semicolon in the value, and the *first* semicolon is
224 preceded by a space, then that is treated as the start of a comment
225 and the rest of the line is silently discarded."""
227 property_name_pattern = r'(?P<name>[^\!\=\s]+)'
228 property_unset_re = re.compile(
229 r'^\!\s*' + property_name_pattern + r'$'
231 property_set_re = re.compile(
232 r'^' + property_name_pattern + r'\s*\=\s*(?P<value>.*)$'
234 property_novalue_re = re.compile(
235 r'^' + property_name_pattern + r'$'
238 quoted_re = re.compile(
239 r'^([\'\"]).*\1$'
241 comment_re = re.compile(r'\s;')
243 class Pattern:
244 """Describes the properties to be set for files matching a pattern."""
246 def __init__(self, pattern, propdict):
247 # A glob-like pattern:
248 self.pattern = pattern
249 # A dictionary of properties that should be set:
250 self.propdict = propdict
252 def match(self, basename):
253 """Does the file with the specified basename match pattern?"""
255 return fnmatch.fnmatch(basename, self.pattern)
257 def __init__(self, configfilename, ignore_case=True):
258 config = ConfigParser.ConfigParser()
259 if ignore_case:
260 self.transform_case = _squash_case
261 else:
262 config.optionxform = _preserve_case
263 self.transform_case = _preserve_case
265 configtext = open(configfilename).read()
266 if self.comment_re.search(configtext):
267 Log().warn(
268 '%s: Please be aware that a space followed by a\n'
269 'semicolon is sometimes treated as a comment in configuration\n'
270 'files. This pattern was seen in\n'
271 ' %s\n'
272 'Please make sure that you have not inadvertently commented\n'
273 'out part of an important line.'
274 % (warning_prefix, configfilename,)
277 config.readfp(StringIO(configtext), configfilename)
278 self.patterns = []
279 sections = config.sections()
280 sections.sort()
281 for section in sections:
282 if self.transform_case(section) == 'auto-props':
283 patterns = config.options(section)
284 patterns.sort()
285 for pattern in patterns:
286 value = config.get(section, pattern)
287 if value:
288 self._add_pattern(pattern, value)
290 def _add_pattern(self, pattern, props):
291 propdict = {}
292 if self.quoted_re.match(pattern):
293 Log().warn(
294 '%s: Quoting is not supported in auto-props; please verify rule\n'
295 'for %r. (Using pattern including quotation marks.)\n'
296 % (warning_prefix, pattern,)
298 for prop in props.split(';'):
299 prop = prop.strip()
300 m = self.property_unset_re.match(prop)
301 if m:
302 name = m.group('name')
303 Log().debug(
304 'auto-props: For %r, leaving %r unset.' % (pattern, name,)
306 propdict[name] = None
307 continue
309 m = self.property_set_re.match(prop)
310 if m:
311 name = m.group('name')
312 value = m.group('value')
313 if self.quoted_re.match(value):
314 Log().warn(
315 '%s: Quoting is not supported in auto-props; please verify\n'
316 'rule %r for pattern %r. (Using value\n'
317 'including quotation marks.)\n'
318 % (warning_prefix, prop, pattern,)
320 Log().debug(
321 'auto-props: For %r, setting %r to %r.' % (pattern, name, value,)
323 propdict[name] = value
324 continue
326 m = self.property_novalue_re.match(prop)
327 if m:
328 name = m.group('name')
329 Log().debug(
330 'auto-props: For %r, setting %r to the empty string'
331 % (pattern, name,)
333 propdict[name] = ''
334 continue
336 Log().warn(
337 '%s: in auto-props line for %r, value %r cannot be parsed (ignored)'
338 % (warning_prefix, pattern, prop,)
341 self.patterns.append(self.Pattern(self.transform_case(pattern), propdict))
343 def get_propdict(self, cvs_file):
344 basename = self.transform_case(cvs_file.basename)
345 propdict = {}
346 for pattern in self.patterns:
347 if pattern.match(basename):
348 for (key,value) in pattern.propdict.items():
349 if key in propdict:
350 if propdict[key] != value:
351 Log().warn(
352 "Contradictory values set for property '%s' for file %s."
353 % (key, cvs_file,))
354 else:
355 propdict[key] = value
357 return propdict
359 def set_properties(self, cvs_file):
360 propdict = self.get_propdict(cvs_file)
361 for (k,v) in propdict.items():
362 if k in cvs_file.properties:
363 if cvs_file.properties[k] != v:
364 Log().warn(
365 "Property '%s' already set to %r for file %s; "
366 "auto-props value (%r) ignored."
367 % (k, cvs_file.properties[k], cvs_file.cvs_path, v,)
369 else:
370 cvs_file.properties[k] = v
373 class CVSBinaryFileDefaultMimeTypeSetter(FilePropertySetter):
374 """If the file is binary and its svn:mime-type property is not yet
375 set, set it to 'application/octet-stream'."""
377 propname = 'svn:mime-type'
379 def set_properties(self, cvs_file):
380 if self.propname in cvs_file.properties:
381 return
383 if cvs_file.mode == 'b':
384 cvs_file.properties[self.propname] = 'application/octet-stream'
387 class EOLStyleFromMimeTypeSetter(FilePropertySetter):
388 """Set svn:eol-style based on svn:mime-type.
390 If svn:mime-type is known but svn:eol-style is not, then set
391 svn:eol-style based on svn:mime-type as follows: if svn:mime-type
392 starts with 'text/', then set svn:eol-style to native; otherwise,
393 force it to remain unset. See also issue #39."""
395 propname = 'svn:eol-style'
397 def set_properties(self, cvs_file):
398 if self.propname in cvs_file.properties:
399 return
401 if cvs_file.properties.get('svn:mime-type', None) is not None:
402 if cvs_file.properties['svn:mime-type'].startswith("text/"):
403 cvs_file.properties[self.propname] = 'native'
404 else:
405 cvs_file.properties[self.propname] = None
408 class DefaultEOLStyleSetter(RevisionPropertySetter):
409 """Set the eol-style if one has not already been set."""
411 propname = 'svn:eol-style'
413 def __init__(self, value):
414 """Initialize with the specified default VALUE."""
416 self.value = value
418 def set_properties(self, s_item):
419 if self.propname in s_item.svn_props:
420 return
422 s_item.svn_props[self.propname] = self.value
425 class SVNBinaryFileKeywordsPropertySetter(RevisionPropertySetter):
426 """Turn off svn:keywords for files with binary svn:eol-style."""
428 propname = 'svn:keywords'
430 def set_properties(self, s_item):
431 if self.propname in s_item.svn_props:
432 return
434 if not s_item.svn_props.get('svn:eol-style'):
435 s_item.svn_props[self.propname] = None
438 class KeywordsPropertySetter(RevisionPropertySetter):
439 """If the svn:keywords property is not yet set, set it based on the
440 file's mode. See issue #2."""
442 propname = 'svn:keywords'
444 def __init__(self, value):
445 """Use VALUE for the value of the svn:keywords property if it is
446 to be set."""
448 self.value = value
450 def set_properties(self, s_item):
451 if self.propname in s_item.svn_props:
452 return
454 if s_item.cvs_rev.cvs_file.mode in [None, 'kv', 'kvl']:
455 s_item.svn_props[self.propname] = self.value