Add a way to specify the MimeMapper mappings to its constructor directly.
[cvs2svn.git] / cvs2svn_lib / property_setters.py
blobb852c400e64ae82cd10b9c657bc51712798af34b
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 class SVNPropertySetter:
31 """Abstract class for objects that can set properties on a SVNCommitItem."""
33 def set_properties(self, s_item):
34 """Set any properties that can be determined for S_ITEM.
36 S_ITEM is an instance of SVNCommitItem. This method should modify
37 S_ITEM.svn_props in place."""
39 raise NotImplementedError
42 class CVSRevisionNumberSetter(SVNPropertySetter):
43 """Store the CVS revision number to an SVN property."""
45 def __init__(self, propname='cvs2svn:cvs-rev'):
46 self.propname = propname
48 def set_properties(self, s_item):
49 if self.propname in s_item.svn_props:
50 return
52 s_item.svn_props[self.propname] = s_item.cvs_rev.rev
53 s_item.svn_props_changed = True
56 class ExecutablePropertySetter(SVNPropertySetter):
57 """Set the svn:executable property based on cvs_rev.cvs_file.executable."""
59 propname = 'svn:executable'
61 def set_properties(self, s_item):
62 if self.propname in s_item.svn_props:
63 return
65 if s_item.cvs_rev.cvs_file.executable:
66 s_item.svn_props[self.propname] = '*'
69 class DescriptionPropertySetter(SVNPropertySetter):
70 """Set the cvs:description property based on cvs_rev.cvs_file.description."""
72 def __init__(self, propname='cvs:description'):
73 self.propname = propname
75 def set_properties(self, s_item):
76 if self.propname in s_item.svn_props:
77 return
79 if s_item.cvs_rev.cvs_file.description:
80 s_item.svn_props[self.propname] = s_item.cvs_rev.cvs_file.description
83 class CVSBinaryFileEOLStyleSetter(SVNPropertySetter):
84 """Set the eol-style to None for files with CVS mode '-kb'."""
86 propname = 'svn:eol-style'
88 def set_properties(self, s_item):
89 if self.propname in s_item.svn_props:
90 return
92 if s_item.cvs_rev.cvs_file.mode == 'b':
93 s_item.svn_props[self.propname] = None
96 class MimeMapper(SVNPropertySetter):
97 """A class that provides mappings from file names to MIME types."""
99 propname = 'svn:mime-type'
101 def __init__(
102 self, mime_types_file=None, mime_mappings=None,
103 ignore_case=False
105 """Constructor.
107 Arguments:
109 mime_types_file -- a path to a MIME types file on disk. Each
110 line of the file should contain the MIME type, then a
111 whitespace-separated list of file extensions; e.g., one line
112 might be 'text/plain txt c h cpp hpp'.
114 mime_mappings -- a dictionary mapping a file extension to a MIME
115 type; e.g., {'txt': 'text/plain', 'cpp': 'text/plain'}.
117 ignore_case -- True iff case should be ignored in filename
118 extensions. Setting this option to True can be useful if
119 your CVS repository was used on systems with
120 case-insensitive filenames, in which case you might have a
121 mix of uppercase and lowercase filenames."""
123 self.mappings = { }
124 self.ignore_case = ignore_case
126 if mime_types_file is None and mime_mappings is None:
127 Log().error('Should specify MIME types file or dict.\n')
129 if mime_types_file is not None:
130 for line in file(mime_types_file):
131 if line.startswith("#"):
132 continue
134 # format of a line is something like
135 # text/plain c h cpp
136 extensions = line.split()
137 if len(extensions) < 2:
138 continue
139 type = extensions.pop(0)
140 for ext in extensions:
141 if ignore_case:
142 ext = ext.lower()
143 if ext in self.mappings and self.mappings[ext] != type:
144 Log().error(
145 "%s: ambiguous MIME mapping for *.%s (%s or %s)\n"
146 % (warning_prefix, ext, self.mappings[ext], type)
148 self.mappings[ext] = type
150 if mime_mappings is not None:
151 for ext, type in mime_mappings.iteritems():
152 if ignore_case:
153 ext = ext.lower()
154 if ext in self.mappings and self.mappings[ext] != type:
155 Log().error(
156 "%s: ambiguous MIME mapping for *.%s (%s or %s)\n"
157 % (warning_prefix, ext, self.mappings[ext], type)
159 self.mappings[ext] = type
161 def set_properties(self, s_item):
162 if self.propname in s_item.svn_props:
163 return
165 basename, extension = os.path.splitext(s_item.cvs_rev.cvs_file.basename)
167 # Extension includes the dot, so strip it (will leave extension
168 # empty if filename ends with a dot, which is ok):
169 extension = extension[1:]
171 # If there is no extension (or the file ends with a period), use
172 # the base name for mapping. This allows us to set mappings for
173 # files such as README or Makefile:
174 if not extension:
175 extension = basename
177 if self.ignore_case:
178 extension = extension.lower()
180 mime_type = self.mappings.get(extension, None)
181 if mime_type is not None:
182 s_item.svn_props[self.propname] = mime_type
185 class AutoPropsPropertySetter(SVNPropertySetter):
186 """Set arbitrary svn properties based on an auto-props configuration.
188 This class supports case-sensitive or case-insensitive pattern
189 matching. The command-line default is case-insensitive behavior,
190 consistent with Subversion (see
191 http://subversion.tigris.org/issues/show_bug.cgi?id=2036).
193 As a special extension to Subversion's auto-props handling, if a
194 property name is preceded by a '!' then that property is forced to
195 be left unset.
197 If a property specified in auto-props has already been set to a
198 different value, print a warning and leave the old property value
199 unchanged.
201 Python's treatment of whitespaces in the ConfigParser module is
202 buggy and inconsistent. Usually spaces are preserved, but if there
203 is at least one semicolon in the value, and the *first* semicolon is
204 preceded by a space, then that is treated as the start of a comment
205 and the rest of the line is silently discarded."""
207 property_name_pattern = r'(?P<name>[^\!\=\s]+)'
208 property_unset_re = re.compile(
209 r'^\!\s*' + property_name_pattern + r'$'
211 property_set_re = re.compile(
212 r'^' + property_name_pattern + r'\s*\=\s*(?P<value>.*)$'
214 property_novalue_re = re.compile(
215 r'^' + property_name_pattern + r'$'
218 quoted_re = re.compile(
219 r'^([\'\"]).*\1$'
221 comment_re = re.compile(r'\s;')
223 class Pattern:
224 """Describes the properties to be set for files matching a pattern."""
226 def __init__(self, pattern, propdict):
227 # A glob-like pattern:
228 self.pattern = pattern
229 # A dictionary of properties that should be set:
230 self.propdict = propdict
232 def match(self, basename):
233 """Does the file with the specified basename match pattern?"""
235 return fnmatch.fnmatch(basename, self.pattern)
237 def __init__(self, configfilename, ignore_case=True):
238 config = ConfigParser.ConfigParser()
239 if ignore_case:
240 self.transform_case = self.squash_case
241 else:
242 config.optionxform = self.preserve_case
243 self.transform_case = self.preserve_case
245 configtext = open(configfilename).read()
246 if self.comment_re.search(configtext):
247 Log().warn(
248 '%s: Please be aware that a space followed by a\n'
249 'semicolon is sometimes treated as a comment in configuration\n'
250 'files. This pattern was seen in\n'
251 ' %s\n'
252 'Please make sure that you have not inadvertently commented\n'
253 'out part of an important line.'
254 % (warning_prefix, configfilename,)
257 config.readfp(StringIO(configtext), configfilename)
258 self.patterns = []
259 sections = config.sections()
260 sections.sort()
261 for section in sections:
262 if self.transform_case(section) == 'auto-props':
263 patterns = config.options(section)
264 patterns.sort()
265 for pattern in patterns:
266 value = config.get(section, pattern)
267 if value:
268 self._add_pattern(pattern, value)
270 def squash_case(self, s):
271 return s.lower()
273 def preserve_case(self, s):
274 return s
276 def _add_pattern(self, pattern, props):
277 propdict = {}
278 if self.quoted_re.match(pattern):
279 Log().warn(
280 '%s: Quoting is not supported in auto-props; please verify rule\n'
281 'for %r. (Using pattern including quotation marks.)\n'
282 % (warning_prefix, pattern,)
284 for prop in props.split(';'):
285 prop = prop.strip()
286 m = self.property_unset_re.match(prop)
287 if m:
288 name = m.group('name')
289 Log().debug(
290 'auto-props: For %r, leaving %r unset.' % (pattern, name,)
292 propdict[name] = None
293 continue
295 m = self.property_set_re.match(prop)
296 if m:
297 name = m.group('name')
298 value = m.group('value')
299 if self.quoted_re.match(value):
300 Log().warn(
301 '%s: Quoting is not supported in auto-props; please verify\n'
302 'rule %r for pattern %r. (Using value\n'
303 'including quotation marks.)\n'
304 % (warning_prefix, prop, pattern,)
306 Log().debug(
307 'auto-props: For %r, setting %r to %r.' % (pattern, name, value,)
309 propdict[name] = value
310 continue
312 m = self.property_novalue_re.match(prop)
313 if m:
314 name = m.group('name')
315 Log().debug(
316 'auto-props: For %r, setting %r to the empty string'
317 % (pattern, name,)
319 propdict[name] = ''
320 continue
322 Log().warn(
323 '%s: in auto-props line for %r, value %r cannot be parsed (ignored)'
324 % (warning_prefix, pattern, prop,)
327 self.patterns.append(self.Pattern(self.transform_case(pattern), propdict))
329 def get_propdict(self, cvs_file):
330 basename = self.transform_case(cvs_file.basename)
331 propdict = {}
332 for pattern in self.patterns:
333 if pattern.match(basename):
334 for (key,value) in pattern.propdict.items():
335 if key in propdict:
336 if propdict[key] != value:
337 Log().warn(
338 "Contradictory values set for property '%s' for file %s."
339 % (key, cvs_file,))
340 else:
341 propdict[key] = value
343 return propdict
345 def set_properties(self, s_item):
346 propdict = self.get_propdict(s_item.cvs_rev.cvs_file)
347 for (k,v) in propdict.items():
348 if k in s_item.svn_props:
349 if s_item.svn_props[k] != v:
350 Log().warn(
351 "Property '%s' already set to %r for file %s; "
352 "auto-props value (%r) ignored."
353 % (k, s_item.svn_props[k], s_item.cvs_rev.cvs_path, v,))
354 else:
355 s_item.svn_props[k] = v
358 class CVSBinaryFileDefaultMimeTypeSetter(SVNPropertySetter):
359 """If the file is binary and its svn:mime-type property is not yet
360 set, set it to 'application/octet-stream'."""
362 propname = 'svn:mime-type'
364 def set_properties(self, s_item):
365 if self.propname in s_item.svn_props:
366 return
368 if s_item.cvs_rev.cvs_file.mode == 'b':
369 s_item.svn_props[self.propname] = 'application/octet-stream'
372 class EOLStyleFromMimeTypeSetter(SVNPropertySetter):
373 """Set svn:eol-style based on svn:mime-type.
375 If svn:mime-type is known but svn:eol-style is not, then set
376 svn:eol-style based on svn:mime-type as follows: if svn:mime-type
377 starts with 'text/', then set svn:eol-style to native; otherwise,
378 force it to remain unset. See also issue #39."""
380 propname = 'svn:eol-style'
382 def set_properties(self, s_item):
383 if self.propname in s_item.svn_props:
384 return
386 if s_item.svn_props.get('svn:mime-type', None) is not None:
387 if s_item.svn_props['svn:mime-type'].startswith("text/"):
388 s_item.svn_props[self.propname] = 'native'
389 else:
390 s_item.svn_props[self.propname] = None
393 class DefaultEOLStyleSetter(SVNPropertySetter):
394 """Set the eol-style if one has not already been set."""
396 propname = 'svn:eol-style'
398 def __init__(self, value):
399 """Initialize with the specified default VALUE."""
401 self.value = value
403 def set_properties(self, s_item):
404 if self.propname in s_item.svn_props:
405 return
407 s_item.svn_props[self.propname] = self.value
410 class SVNBinaryFileKeywordsPropertySetter(SVNPropertySetter):
411 """Turn off svn:keywords for files with binary svn:eol-style."""
413 propname = 'svn:keywords'
415 def set_properties(self, s_item):
416 if self.propname in s_item.svn_props:
417 return
419 if not s_item.svn_props.get('svn:eol-style'):
420 s_item.svn_props[self.propname] = None
423 class KeywordsPropertySetter(SVNPropertySetter):
424 """If the svn:keywords property is not yet set, set it based on the
425 file's mode. See issue #2."""
427 propname = 'svn:keywords'
429 def __init__(self, value):
430 """Use VALUE for the value of the svn:keywords property if it is
431 to be set."""
433 self.value = value
435 def set_properties(self, s_item):
436 if self.propname in s_item.svn_props:
437 return
439 if s_item.cvs_rev.cvs_file.mode in [None, 'kv', 'kvl']:
440 s_item.svn_props[self.propname] = self.value