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."""
24 from cStringIO
import StringIO
26 from cvs2svn_lib
.common
import warning_prefix
27 from cvs2svn_lib
.log
import logger
34 def _preserve_case(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 def set_properties(self
, cvs_file
):
62 if cvs_file
.executable
:
63 self
.maybe_set_property(cvs_file
, 'svn:executable', '*')
66 class DescriptionPropertySetter(FilePropertySetter
):
67 """Set the cvs:description property based on cvs_file.description."""
69 def __init__(self
, propname
='cvs:description'):
70 self
.propname
= propname
72 def set_properties(self
, cvs_file
):
73 if cvs_file
.description
:
74 self
.maybe_set_property(cvs_file
, self
.propname
, cvs_file
.description
)
77 class CVSBinaryFileEOLStyleSetter(FilePropertySetter
):
78 """Set the eol-style to None for files with CVS mode '-kb'."""
80 def set_properties(self
, cvs_file
):
81 if cvs_file
.mode
== 'b':
82 self
.maybe_set_property(cvs_file
, 'svn:eol-style', None)
85 class MimeMapper(FilePropertySetter
):
86 """A class that provides mappings from file names to MIME types."""
88 propname
= 'svn:mime-type'
91 self
, mime_types_file
=None, mime_mappings
=None,
98 mime_types_file -- a path to a MIME types file on disk. Each
99 line of the file should contain the MIME type, then a
100 whitespace-separated list of file extensions; e.g., one line
101 might be 'text/plain txt c h cpp hpp'.
103 mime_mappings -- a dictionary mapping a file extension to a MIME
104 type; e.g., {'txt': 'text/plain', 'cpp': 'text/plain'}.
106 ignore_case -- True iff case should be ignored in filename
107 extensions. Setting this option to True can be useful if
108 your CVS repository was used on systems with
109 case-insensitive filenames, in which case you might have a
110 mix of uppercase and lowercase filenames."""
114 self
.transform_case
= _squash_case
116 self
.transform_case
= _preserve_case
118 if mime_types_file
is None and mime_mappings
is None:
119 logger
.error('Should specify MIME types file or dict.\n')
121 if mime_types_file
is not None:
122 for line
in file(mime_types_file
):
123 if line
.startswith("#"):
126 # format of a line is something like
128 extensions
= line
.split()
129 if len(extensions
) < 2:
131 type = extensions
.pop(0)
132 for ext
in extensions
:
133 ext
= self
.transform_case(ext
)
134 if ext
in self
.mappings
and self
.mappings
[ext
] != type:
136 "%s: ambiguous MIME mapping for *.%s (%s or %s)\n"
137 % (warning_prefix
, ext
, self
.mappings
[ext
], type)
139 self
.mappings
[ext
] = type
141 if mime_mappings
is not None:
142 for ext
, type in mime_mappings
.iteritems():
143 ext
= self
.transform_case(ext
)
144 if ext
in self
.mappings
and self
.mappings
[ext
] != type:
146 "%s: ambiguous MIME mapping for *.%s (%s or %s)\n"
147 % (warning_prefix
, ext
, self
.mappings
[ext
], type)
149 self
.mappings
[ext
] = type
151 def set_properties(self
, cvs_file
):
152 if self
.propname
in cvs_file
.properties
:
155 basename
, extension
= os
.path
.splitext(cvs_file
.rcs_basename
)
157 # Extension includes the dot, so strip it (will leave extension
158 # empty if filename ends with a dot, which is ok):
159 extension
= extension
[1:]
161 # If there is no extension (or the file ends with a period), use
162 # the base name for mapping. This allows us to set mappings for
163 # files such as README or Makefile:
167 extension
= self
.transform_case(extension
)
169 mime_type
= self
.mappings
.get(extension
, None)
170 if mime_type
is not None:
171 cvs_file
.properties
[self
.propname
] = mime_type
174 class AutoPropsPropertySetter(FilePropertySetter
):
175 """Set arbitrary svn properties based on an auto-props configuration.
177 This class supports case-sensitive or case-insensitive pattern
178 matching. The command-line default is case-insensitive behavior,
179 consistent with Subversion (see
180 http://subversion.tigris.org/issues/show_bug.cgi?id=2036).
182 As a special extension to Subversion's auto-props handling, if a
183 property name is preceded by a '!' then that property is forced to
186 If a property specified in auto-props has already been set to a
187 different value, print a warning and leave the old property value
190 Python's treatment of whitespaces in the ConfigParser module is
191 buggy and inconsistent. Usually spaces are preserved, but if there
192 is at least one semicolon in the value, and the *first* semicolon is
193 preceded by a space, then that is treated as the start of a comment
194 and the rest of the line is silently discarded."""
196 property_name_pattern
= r
'(?P<name>[^\!\=\s]+)'
197 property_unset_re
= re
.compile(
198 r
'^\!\s*' + property_name_pattern
+ r
'$'
200 property_set_re
= re
.compile(
201 r
'^' + property_name_pattern
+ r
'\s*\=\s*(?P<value>.*)$'
203 property_novalue_re
= re
.compile(
204 r
'^' + property_name_pattern
+ r
'$'
207 quoted_re
= re
.compile(
210 comment_re = re.compile(r'\s
;')
213 """Describes the properties to be set for files matching a pattern."""
215 def __init__(self, pattern, propdict):
216 # A glob-like pattern:
217 self.pattern = pattern
218 # A dictionary of properties that should be set:
219 self.propdict = propdict
221 def match(self, basename):
222 """Does the file with the specified basename match pattern?"""
224 return fnmatch.fnmatch(basename, self.pattern)
226 def __init__(self, configfilename, ignore_case=True):
227 config = ConfigParser.ConfigParser()
229 self.transform_case = _squash_case
231 config.optionxform = _preserve_case
232 self.transform_case = _preserve_case
234 configtext = open(configfilename).read()
235 if self.comment_re.search(configtext):
237 '%s: Please be aware that a space followed by a
\n'
238 'semicolon
is sometimes treated
as a comment
in configuration
\n'
239 'files
. This pattern was seen
in\n'
241 'Please make sure that you have
not inadvertently commented
\n'
242 'out part of an important line
.'
243 % (warning_prefix, configfilename,)
246 config.readfp(StringIO(configtext), configfilename)
248 sections = config.sections()
250 for section in sections:
251 if self.transform_case(section) == 'auto
-props
':
252 patterns = config.options(section)
254 for pattern in patterns:
255 value = config.get(section, pattern)
257 self._add_pattern(pattern, value)
259 def _add_pattern(self, pattern, props):
261 if self.quoted_re.match(pattern):
263 '%s: Quoting
is not supported
in auto
-props
; please verify rule
\n'
264 'for %r. (Using pattern including quotation marks
.)\n'
265 % (warning_prefix, pattern,)
267 for prop in props.split(';'):
269 m = self.property_unset_re.match(prop)
271 name = m.group('name
')
273 'auto
-props
: For
%r, leaving
%r unset
.' % (pattern, name,)
275 propdict[name] = None
278 m = self.property_set_re.match(prop)
280 name = m.group('name
')
281 value = m.group('value
')
282 if self.quoted_re.match(value):
284 '%s: Quoting
is not supported
in auto
-props
; please verify
\n'
285 'rule
%r for pattern
%r. (Using value
\n'
286 'including quotation marks
.)\n'
287 % (warning_prefix, prop, pattern,)
290 'auto
-props
: For
%r, setting
%r to
%r.' % (pattern, name, value,)
292 propdict[name] = value
295 m = self.property_novalue_re.match(prop)
297 name = m.group('name
')
299 'auto
-props
: For
%r, setting
%r to the empty string
'
306 '%s: in auto
-props line
for %r, value
%r cannot be
parsed (ignored
)'
307 % (warning_prefix, pattern, prop,)
310 self.patterns.append(self.Pattern(self.transform_case(pattern), propdict))
312 def get_propdict(self, cvs_file):
313 basename = self.transform_case(cvs_file.rcs_basename)
315 for pattern in self.patterns:
316 if pattern.match(basename):
317 for (key,value) in pattern.propdict.items():
319 if propdict[key] != value:
321 "Contradictory values set for property '%s' for file %s."
324 propdict[key] = value
328 def set_properties(self, cvs_file):
329 propdict = self.get_propdict(cvs_file)
330 for (k,v) in propdict.items():
331 if k in cvs_file.properties:
332 if cvs_file.properties[k] != v:
334 "Property '%s' already set to %r for file %s; "
335 "auto-props value (%r) ignored."
336 % (k, cvs_file.properties[k], cvs_file.cvs_path, v,)
339 cvs_file.properties[k] = v
342 class CVSBinaryFileDefaultMimeTypeSetter(FilePropertySetter):
343 """If the file is binary and its svn:mime-type property is not yet
344 set, set it to 'application
/octet
-stream
'."""
346 def set_properties(self, cvs_file):
347 if cvs_file.mode == 'b
':
348 self.maybe_set_property(
349 cvs_file, 'svn
:mime
-type', 'application
/octet
-stream
'
353 class EOLStyleFromMimeTypeSetter(FilePropertySetter):
354 """Set svn:eol-style based on svn:mime-type.
356 If svn:mime-type is known but svn:eol-style is not, then set
357 svn:eol-style based on svn:mime-type as follows: if svn:mime-type
358 starts with 'text
/', then set svn:eol-style to native; otherwise,
359 force it to remain unset. See also issue #39."""
361 propname = 'svn
:eol
-style
'
363 def set_properties(self, cvs_file):
364 if self.propname in cvs_file.properties:
367 mime_type = cvs_file.properties.get('svn
:mime
-type', None)
369 if mime_type.startswith("text/"):
370 cvs_file.properties[self.propname] = 'native
'
372 cvs_file.properties[self.propname] = None
375 class DefaultEOLStyleSetter(FilePropertySetter):
376 """Set the eol-style if one has not already been set."""
380 # Also treat "binary" as None:
383 'CRLF
' : 'CRLF
', 'LF
' : 'LF
', 'CR
' : 'CR
',
386 def __init__(self, value):
387 """Initialize with the specified default VALUE."""
390 # Check that value is valid, and translate it to the proper case
391 self.value = self.valid_values[value]
394 'Illegal value specified
for the default EOL option
: %r' % (value,)
397 def set_properties(self, cvs_file):
398 self.maybe_set_property(cvs_file, 'svn
:eol
-style
', self.value)
401 class SVNBinaryFileKeywordsPropertySetter(FilePropertySetter):
402 """Turn off svn:keywords for files with binary svn:eol-style."""
404 propname = 'svn
:keywords
'
406 def set_properties(self, cvs_file):
407 if self.propname in cvs_file.properties:
410 if not cvs_file.properties.get('svn
:eol
-style
'):
411 cvs_file.properties[self.propname] = None
414 class KeywordsPropertySetter(FilePropertySetter):
415 """If the svn:keywords property is not yet set, set it based on the
416 file's mode
. See issue
#2."""
418 def __init__(self
, value
):
419 """Use VALUE for the value of the svn:keywords property if it is
424 def set_properties(self
, cvs_file
):
425 if cvs_file
.mode
in [None, 'kv', 'kvl']:
426 self
.maybe_set_property(cvs_file
, 'svn:keywords', self
.value
)
429 class RevisionPropertySetter
:
430 """Abstract class for objects that can set properties on a CVSRevision."""
432 def set_properties(self
, cvs_rev
):
433 """Set any properties that can be determined for CVS_REV.
435 CVS_REV is an instance of CVSRevision. This method should modify
436 CVS_REV.properties in place."""
438 raise NotImplementedError()
441 class CVSRevisionNumberSetter(RevisionPropertySetter
):
442 """Store the CVS revision number to an SVN property."""
444 def __init__(self
, propname
='cvs2svn:cvs-rev'):
445 self
.propname
= propname
447 def set_properties(self
, cvs_rev
):
448 if self
.propname
in cvs_rev
.properties
:
451 cvs_rev
.properties
[self
.propname
] = cvs_rev
.rev