Split up too-long line.
[cvs2svn.git] / cvs2svn_lib / property_setters.py
blob070f637d061679143f777f9f480fafd1e72e5c8f
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 ExecutablePropertySetter(FilePropertySetter):
51 """Set the svn:executable property based on cvs_file.executable."""
53 propname = 'svn:executable'
55 def set_properties(self, cvs_file):
56 if self.propname in cvs_file.properties:
57 return
59 if cvs_file.executable:
60 cvs_file.properties[self.propname] = '*'
63 class DescriptionPropertySetter(FilePropertySetter):
64 """Set the cvs:description property based on cvs_file.description."""
66 def __init__(self, propname='cvs:description'):
67 self.propname = propname
69 def set_properties(self, cvs_file):
70 if self.propname in cvs_file.properties:
71 return
73 if cvs_file.description:
74 cvs_file.properties[self.propname] = cvs_file.description
77 class CVSBinaryFileEOLStyleSetter(FilePropertySetter):
78 """Set the eol-style to None for files with CVS mode '-kb'."""
80 propname = 'svn:eol-style'
82 def set_properties(self, cvs_file):
83 if self.propname in cvs_file.properties:
84 return
86 if cvs_file.mode == 'b':
87 cvs_file.properties[self.propname] = None
90 class MimeMapper(FilePropertySetter):
91 """A class that provides mappings from file names to MIME types."""
93 propname = 'svn:mime-type'
95 def __init__(
96 self, mime_types_file=None, mime_mappings=None,
97 ignore_case=False
99 """Constructor.
101 Arguments:
103 mime_types_file -- a path to a MIME types file on disk. Each
104 line of the file should contain the MIME type, then a
105 whitespace-separated list of file extensions; e.g., one line
106 might be 'text/plain txt c h cpp hpp'.
108 mime_mappings -- a dictionary mapping a file extension to a MIME
109 type; e.g., {'txt': 'text/plain', 'cpp': 'text/plain'}.
111 ignore_case -- True iff case should be ignored in filename
112 extensions. Setting this option to True can be useful if
113 your CVS repository was used on systems with
114 case-insensitive filenames, in which case you might have a
115 mix of uppercase and lowercase filenames."""
117 self.mappings = { }
118 if ignore_case:
119 self.transform_case = _squash_case
120 else:
121 self.transform_case = _preserve_case
123 if mime_types_file is None and mime_mappings is None:
124 Log().error('Should specify MIME types file or dict.\n')
126 if mime_types_file is not None:
127 for line in file(mime_types_file):
128 if line.startswith("#"):
129 continue
131 # format of a line is something like
132 # text/plain c h cpp
133 extensions = line.split()
134 if len(extensions) < 2:
135 continue
136 type = extensions.pop(0)
137 for ext in extensions:
138 ext = self.transform_case(ext)
139 if ext in self.mappings and self.mappings[ext] != type:
140 Log().error(
141 "%s: ambiguous MIME mapping for *.%s (%s or %s)\n"
142 % (warning_prefix, ext, self.mappings[ext], type)
144 self.mappings[ext] = type
146 if mime_mappings is not None:
147 for ext, type in mime_mappings.iteritems():
148 ext = self.transform_case(ext)
149 if ext in self.mappings and self.mappings[ext] != type:
150 Log().error(
151 "%s: ambiguous MIME mapping for *.%s (%s or %s)\n"
152 % (warning_prefix, ext, self.mappings[ext], type)
154 self.mappings[ext] = type
156 def set_properties(self, cvs_file):
157 if self.propname in cvs_file.properties:
158 return
160 basename, extension = os.path.splitext(cvs_file.basename)
162 # Extension includes the dot, so strip it (will leave extension
163 # empty if filename ends with a dot, which is ok):
164 extension = extension[1:]
166 # If there is no extension (or the file ends with a period), use
167 # the base name for mapping. This allows us to set mappings for
168 # files such as README or Makefile:
169 if not extension:
170 extension = basename
172 extension = self.transform_case(extension)
174 mime_type = self.mappings.get(extension, None)
175 if mime_type is not None:
176 cvs_file.properties[self.propname] = mime_type
179 class AutoPropsPropertySetter(FilePropertySetter):
180 """Set arbitrary svn properties based on an auto-props configuration.
182 This class supports case-sensitive or case-insensitive pattern
183 matching. The command-line default is case-insensitive behavior,
184 consistent with Subversion (see
185 http://subversion.tigris.org/issues/show_bug.cgi?id=2036).
187 As a special extension to Subversion's auto-props handling, if a
188 property name is preceded by a '!' then that property is forced to
189 be left unset.
191 If a property specified in auto-props has already been set to a
192 different value, print a warning and leave the old property value
193 unchanged.
195 Python's treatment of whitespaces in the ConfigParser module is
196 buggy and inconsistent. Usually spaces are preserved, but if there
197 is at least one semicolon in the value, and the *first* semicolon is
198 preceded by a space, then that is treated as the start of a comment
199 and the rest of the line is silently discarded."""
201 property_name_pattern = r'(?P<name>[^\!\=\s]+)'
202 property_unset_re = re.compile(
203 r'^\!\s*' + property_name_pattern + r'$'
205 property_set_re = re.compile(
206 r'^' + property_name_pattern + r'\s*\=\s*(?P<value>.*)$'
208 property_novalue_re = re.compile(
209 r'^' + property_name_pattern + r'$'
212 quoted_re = re.compile(
213 r'^([\'\"]).*\1$'
215 comment_re = re.compile(r'\s;')
217 class Pattern:
218 """Describes the properties to be set for files matching a pattern."""
220 def __init__(self, pattern, propdict):
221 # A glob-like pattern:
222 self.pattern = pattern
223 # A dictionary of properties that should be set:
224 self.propdict = propdict
226 def match(self, basename):
227 """Does the file with the specified basename match pattern?"""
229 return fnmatch.fnmatch(basename, self.pattern)
231 def __init__(self, configfilename, ignore_case=True):
232 config = ConfigParser.ConfigParser()
233 if ignore_case:
234 self.transform_case = _squash_case
235 else:
236 config.optionxform = _preserve_case
237 self.transform_case = _preserve_case
239 configtext = open(configfilename).read()
240 if self.comment_re.search(configtext):
241 Log().warn(
242 '%s: Please be aware that a space followed by a\n'
243 'semicolon is sometimes treated as a comment in configuration\n'
244 'files. This pattern was seen in\n'
245 ' %s\n'
246 'Please make sure that you have not inadvertently commented\n'
247 'out part of an important line.'
248 % (warning_prefix, configfilename,)
251 config.readfp(StringIO(configtext), configfilename)
252 self.patterns = []
253 sections = config.sections()
254 sections.sort()
255 for section in sections:
256 if self.transform_case(section) == 'auto-props':
257 patterns = config.options(section)
258 patterns.sort()
259 for pattern in patterns:
260 value = config.get(section, pattern)
261 if value:
262 self._add_pattern(pattern, value)
264 def _add_pattern(self, pattern, props):
265 propdict = {}
266 if self.quoted_re.match(pattern):
267 Log().warn(
268 '%s: Quoting is not supported in auto-props; please verify rule\n'
269 'for %r. (Using pattern including quotation marks.)\n'
270 % (warning_prefix, pattern,)
272 for prop in props.split(';'):
273 prop = prop.strip()
274 m = self.property_unset_re.match(prop)
275 if m:
276 name = m.group('name')
277 Log().debug(
278 'auto-props: For %r, leaving %r unset.' % (pattern, name,)
280 propdict[name] = None
281 continue
283 m = self.property_set_re.match(prop)
284 if m:
285 name = m.group('name')
286 value = m.group('value')
287 if self.quoted_re.match(value):
288 Log().warn(
289 '%s: Quoting is not supported in auto-props; please verify\n'
290 'rule %r for pattern %r. (Using value\n'
291 'including quotation marks.)\n'
292 % (warning_prefix, prop, pattern,)
294 Log().debug(
295 'auto-props: For %r, setting %r to %r.' % (pattern, name, value,)
297 propdict[name] = value
298 continue
300 m = self.property_novalue_re.match(prop)
301 if m:
302 name = m.group('name')
303 Log().debug(
304 'auto-props: For %r, setting %r to the empty string'
305 % (pattern, name,)
307 propdict[name] = ''
308 continue
310 Log().warn(
311 '%s: in auto-props line for %r, value %r cannot be parsed (ignored)'
312 % (warning_prefix, pattern, prop,)
315 self.patterns.append(self.Pattern(self.transform_case(pattern), propdict))
317 def get_propdict(self, cvs_file):
318 basename = self.transform_case(cvs_file.basename)
319 propdict = {}
320 for pattern in self.patterns:
321 if pattern.match(basename):
322 for (key,value) in pattern.propdict.items():
323 if key in propdict:
324 if propdict[key] != value:
325 Log().warn(
326 "Contradictory values set for property '%s' for file %s."
327 % (key, cvs_file,))
328 else:
329 propdict[key] = value
331 return propdict
333 def set_properties(self, cvs_file):
334 propdict = self.get_propdict(cvs_file)
335 for (k,v) in propdict.items():
336 if k in cvs_file.properties:
337 if cvs_file.properties[k] != v:
338 Log().warn(
339 "Property '%s' already set to %r for file %s; "
340 "auto-props value (%r) ignored."
341 % (k, cvs_file.properties[k], cvs_file.cvs_path, v,)
343 else:
344 cvs_file.properties[k] = v
347 class CVSBinaryFileDefaultMimeTypeSetter(FilePropertySetter):
348 """If the file is binary and its svn:mime-type property is not yet
349 set, set it to 'application/octet-stream'."""
351 propname = 'svn:mime-type'
353 def set_properties(self, cvs_file):
354 if self.propname in cvs_file.properties:
355 return
357 if cvs_file.mode == 'b':
358 cvs_file.properties[self.propname] = '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 if cvs_file.properties.get('svn:mime-type', None) is not None:
376 if cvs_file.properties['svn:mime-type'].startswith("text/"):
377 cvs_file.properties[self.propname] = 'native'
378 else:
379 cvs_file.properties[self.propname] = None
382 class DefaultEOLStyleSetter(FilePropertySetter):
383 """Set the eol-style if one has not already been set."""
385 propname = 'svn:eol-style'
387 def __init__(self, value):
388 """Initialize with the specified default VALUE."""
390 self.value = value
392 def set_properties(self, cvs_file):
393 if self.propname in cvs_file.properties:
394 return
396 cvs_file.properties[self.propname] = self.value
399 class SVNBinaryFileKeywordsPropertySetter(FilePropertySetter):
400 """Turn off svn:keywords for files with binary svn:eol-style."""
402 propname = 'svn:keywords'
404 def set_properties(self, cvs_file):
405 if self.propname in cvs_file.properties:
406 return
408 if not cvs_file.properties.get('svn:eol-style'):
409 cvs_file.properties[self.propname] = None
412 class KeywordsPropertySetter(FilePropertySetter):
413 """If the svn:keywords property is not yet set, set it based on the
414 file's mode. See issue #2."""
416 propname = 'svn:keywords'
418 def __init__(self, value):
419 """Use VALUE for the value of the svn:keywords property if it is
420 to be set."""
422 self.value = value
424 def set_properties(self, cvs_file):
425 if self.propname in cvs_file.properties:
426 return
428 if cvs_file.mode in [None, 'kv', 'kvl']:
429 cvs_file.properties[self.propname] = self.value
432 class RevisionPropertySetter:
433 """Abstract class for objects that can set properties on a CVSRevision."""
435 def set_properties(self, cvs_rev):
436 """Set any properties that can be determined for CVS_REV.
438 CVS_REV is an instance of CVSRevision. This method should modify
439 CVS_REV.properties in place."""
441 raise NotImplementedError()
444 class CVSRevisionNumberSetter(RevisionPropertySetter):
445 """Store the CVS revision number to an SVN property."""
447 def __init__(self, propname='cvs2svn:cvs-rev'):
448 self.propname = propname
450 def set_properties(self, cvs_rev):
451 if self.propname in cvs_rev.properties:
452 return
454 cvs_rev.properties[self.propname] = cvs_rev.rev