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 def cvs_file_is_binary(cvs_file
):
39 return cvs_file
.mode
== 'b'
42 class FilePropertySetter(object):
43 """Abstract class for objects that set properties on a CVSFile."""
45 def maybe_set_property(self
, cvs_file
, name
, value
):
46 """Set a property on CVS_FILE if it does not already have a value.
48 This method is here for the convenience of derived classes."""
50 if name
not in cvs_file
.properties
:
51 cvs_file
.properties
[name
] = value
53 def set_properties(self
, cvs_file
):
54 """Set any properties needed for CVS_FILE.
56 CVS_FILE is an instance of CVSFile. This method should modify
57 CVS_FILE.properties in place."""
59 raise NotImplementedError()
62 class ExecutablePropertySetter(FilePropertySetter
):
63 """Set the svn:executable property based on cvs_file.executable."""
65 def set_properties(self
, cvs_file
):
66 if cvs_file
.executable
:
67 self
.maybe_set_property(cvs_file
, 'svn:executable', '*')
70 class DescriptionPropertySetter(FilePropertySetter
):
71 """Set the cvs:description property based on cvs_file.description."""
73 def __init__(self
, propname
='cvs:description'):
74 self
.propname
= propname
76 def set_properties(self
, cvs_file
):
77 if cvs_file
.description
:
78 self
.maybe_set_property(cvs_file
, self
.propname
, cvs_file
.description
)
81 class CVSBinaryFileEOLStyleSetter(FilePropertySetter
):
82 """Set the eol-style to None for files with CVS mode '-kb'."""
84 def set_properties(self
, cvs_file
):
85 if cvs_file
.mode
== 'b':
86 self
.maybe_set_property(cvs_file
, 'svn:eol-style', None)
89 class MimeMapper(FilePropertySetter
):
90 """A class that provides mappings from file names to MIME types."""
92 propname
= 'svn:mime-type'
95 self
, mime_types_file
=None, mime_mappings
=None,
102 mime_types_file -- a path to a MIME types file on disk. Each
103 line of the file should contain the MIME type, then a
104 whitespace-separated list of file extensions; e.g., one line
105 might be 'text/plain txt c h cpp hpp'. (See
106 http://en.wikipedia.org/wiki/Mime.types for information
107 about mime.types files):
109 mime_mappings -- a dictionary mapping a file extension to a MIME
110 type; e.g., {'txt': 'text/plain', 'cpp': 'text/plain'}.
112 ignore_case -- True iff case should be ignored in filename
113 extensions. Setting this option to True can be useful if
114 your CVS repository was used on systems with
115 case-insensitive filenames, in which case you might have a
116 mix of uppercase and lowercase filenames."""
120 self
.transform_case
= _squash_case
122 self
.transform_case
= _preserve_case
124 if mime_types_file
is None and mime_mappings
is None:
125 logger
.error('Should specify MIME types file or dict.\n')
127 if mime_types_file
is not None:
128 for line
in file(mime_types_file
):
129 if line
.startswith("#"):
132 # format of a line is something like
134 extensions
= line
.split()
135 if len(extensions
) < 2:
137 type = extensions
.pop(0)
138 for ext
in extensions
:
139 ext
= self
.transform_case(ext
)
140 if ext
in self
.mappings
and self
.mappings
[ext
] != type:
142 "%s: ambiguous MIME mapping for *.%s (%s or %s)\n"
143 % (warning_prefix
, ext
, self
.mappings
[ext
], type)
145 self
.mappings
[ext
] = type
147 if mime_mappings
is not None:
148 for ext
, type in mime_mappings
.iteritems():
149 ext
= self
.transform_case(ext
)
150 if ext
in self
.mappings
and self
.mappings
[ext
] != type:
152 "%s: ambiguous MIME mapping for *.%s (%s or %s)\n"
153 % (warning_prefix
, ext
, self
.mappings
[ext
], type)
155 self
.mappings
[ext
] = type
157 def set_properties(self
, cvs_file
):
158 if self
.propname
in cvs_file
.properties
:
161 basename
, extension
= os
.path
.splitext(cvs_file
.rcs_basename
)
163 # Extension includes the dot, so strip it (will leave extension
164 # empty if filename ends with a dot, which is ok):
165 extension
= extension
[1:]
167 # If there is no extension (or the file ends with a period), use
168 # the base name for mapping. This allows us to set mappings for
169 # files such as README or Makefile:
173 extension
= self
.transform_case(extension
)
175 mime_type
= self
.mappings
.get(extension
, None)
176 if mime_type
is not None:
177 cvs_file
.properties
[self
.propname
] = mime_type
180 class AutoPropsPropertySetter(FilePropertySetter
):
181 """Set arbitrary svn properties based on an auto-props configuration.
183 This class supports case-sensitive or case-insensitive pattern
184 matching. The command-line default is case-insensitive behavior,
185 consistent with Subversion (see
186 http://subversion.tigris.org/issues/show_bug.cgi?id=2036).
188 As a special extension to Subversion's auto-props handling, if a
189 property name is preceded by a '!' then that property is forced to
192 If a property specified in auto-props has already been set to a
193 different value, print a warning and leave the old property value
196 Python's treatment of whitespaces in the ConfigParser module is
197 buggy and inconsistent. Usually spaces are preserved, but if there
198 is at least one semicolon in the value, and the *first* semicolon is
199 preceded by a space, then that is treated as the start of a comment
200 and the rest of the line is silently discarded."""
202 property_name_pattern
= r
'(?P<name>[^\!\=\s]+)'
203 property_unset_re
= re
.compile(
204 r
'^\!\s*' + property_name_pattern
+ r
'$'
206 property_set_re
= re
.compile(
207 r
'^' + property_name_pattern
+ r
'\s*\=\s*(?P<value>.*)$'
209 property_novalue_re
= re
.compile(
210 r
'^' + property_name_pattern
+ r
'$'
213 quoted_re
= re
.compile(
216 comment_re = re.compile(r'\s
;')
219 """Describes the properties to be set for files matching a pattern."""
221 def __init__(self, pattern, propdict):
222 # A glob-like pattern:
223 self.pattern = pattern
224 # A dictionary of properties that should be set:
225 self.propdict = propdict
227 def match(self, basename):
228 """Does the file with the specified basename match pattern?"""
230 return fnmatch.fnmatch(basename, self.pattern)
232 def __init__(self, configfilename, ignore_case=True):
233 config = ConfigParser.ConfigParser()
235 self.transform_case = _squash_case
237 config.optionxform = _preserve_case
238 self.transform_case = _preserve_case
240 f = open(configfilename)
241 configtext = f.read()
243 if self.comment_re.search(configtext):
245 '%s: Please be aware that a space followed by a
\n'
246 'semicolon
is sometimes treated
as a comment
in configuration
\n'
247 'files
. This pattern was seen
in\n'
249 'Please make sure that you have
not inadvertently commented
\n'
250 'out part of an important line
.'
251 % (warning_prefix, configfilename,)
254 config.readfp(StringIO(configtext), configfilename)
256 sections = config.sections()
258 for section in sections:
259 if self.transform_case(section) == 'auto
-props
':
260 patterns = config.options(section)
262 for pattern in patterns:
263 value = config.get(section, pattern)
265 self._add_pattern(pattern, value)
267 def _add_pattern(self, pattern, props):
269 if self.quoted_re.match(pattern):
271 '%s: Quoting
is not supported
in auto
-props
; please verify rule
\n'
272 'for %r. (Using pattern including quotation marks
.)\n'
273 % (warning_prefix, pattern,)
275 for prop in props.split(';'):
277 m = self.property_unset_re.match(prop)
279 name = m.group('name
')
281 'auto
-props
: For
%r, leaving
%r unset
.' % (pattern, name,)
283 propdict[name] = None
286 m = self.property_set_re.match(prop)
288 name = m.group('name
')
289 value = m.group('value
')
290 if self.quoted_re.match(value):
292 '%s: Quoting
is not supported
in auto
-props
; please verify
\n'
293 'rule
%r for pattern
%r. (Using value
\n'
294 'including quotation marks
.)\n'
295 % (warning_prefix, prop, pattern,)
298 'auto
-props
: For
%r, setting
%r to
%r.' % (pattern, name, value,)
300 propdict[name] = value
303 m = self.property_novalue_re.match(prop)
305 name = m.group('name
')
307 'auto
-props
: For
%r, setting
%r to the empty string
'
314 '%s: in auto
-props line
for %r, value
%r cannot be
parsed (ignored
)'
315 % (warning_prefix, pattern, prop,)
318 self.patterns.append(self.Pattern(self.transform_case(pattern), propdict))
320 def get_propdict(self, cvs_file):
321 basename = self.transform_case(cvs_file.rcs_basename)
323 for pattern in self.patterns:
324 if pattern.match(basename):
325 for (key,value) in pattern.propdict.items():
327 if propdict[key] != value:
329 "Contradictory values set for property '%s' for file %s."
332 propdict[key] = value
336 def set_properties(self, cvs_file):
337 propdict = self.get_propdict(cvs_file)
338 for (k,v) in propdict.items():
339 if k in cvs_file.properties:
340 if cvs_file.properties[k] != v:
342 "Property '%s' already set to %r for file %s; "
343 "auto-props value (%r) ignored."
344 % (k, cvs_file.properties[k], cvs_file.cvs_path, v,)
347 cvs_file.properties[k] = v
350 class CVSBinaryFileDefaultMimeTypeSetter(FilePropertySetter):
351 """If the file is binary and its svn:mime-type property is not yet
352 set, set it to 'application
/octet
-stream
'."""
354 def set_properties(self, cvs_file):
355 if cvs_file.mode == 'b
':
356 self.maybe_set_property(
357 cvs_file, 'svn
:mime
-type', '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:
375 mime_type = cvs_file.properties.get('svn
:mime
-type', None)
377 if mime_type.startswith("text/"):
378 cvs_file.properties[self.propname] = 'native
'
380 cvs_file.properties[self.propname] = None
383 class DefaultEOLStyleSetter(FilePropertySetter):
384 """Set the eol-style if one has not already been set."""
388 # Also treat "binary" as None:
391 'CRLF
' : 'CRLF
', 'LF
' : 'LF
', 'CR
' : 'CR
',
394 def __init__(self, value):
395 """Initialize with the specified default VALUE."""
398 # Check that value is valid, and translate it to the proper case
399 self.value = self.valid_values[value]
402 'Illegal value specified
for the default EOL option
: %r' % (value,)
405 def set_properties(self, cvs_file):
406 self.maybe_set_property(cvs_file, 'svn
:eol
-style
', self.value)
409 class SVNBinaryFileKeywordsPropertySetter(FilePropertySetter):
410 """Turn off svn:keywords for files with binary svn:eol-style."""
412 propname = 'svn
:keywords
'
414 def set_properties(self, cvs_file):
415 if self.propname in cvs_file.properties:
418 if not cvs_file.properties.get('svn
:eol
-style
'):
419 cvs_file.properties[self.propname] = None
422 class KeywordsPropertySetter(FilePropertySetter):
423 """If the svn:keywords property is not yet set, set it based on the
424 file's mode
. See issue
#2."""
426 def __init__(self
, value
):
427 """Use VALUE for the value of the svn:keywords property if it is
432 def set_properties(self
, cvs_file
):
433 if cvs_file
.mode
in [None, 'kv', 'kvl']:
434 self
.maybe_set_property(cvs_file
, 'svn:keywords', self
.value
)
437 class ConditionalPropertySetter(object):
438 """Delegate to the passed property setters when the passed predicate applies.
439 The predicate should be a function that takes a CVSFile or CVSRevision
440 argument and return True if the property setters should be applied."""
442 def __init__(self
, predicate
, *property_setters
):
443 self
.predicate
= predicate
444 self
.property_setters
= property_setters
446 def set_properties(self
, cvs_file_or_rev
):
447 if self
.predicate(cvs_file_or_rev
):
448 for property_setter
in self
.property_setters
:
449 property_setter
.set_properties(cvs_file_or_rev
)
452 class RevisionPropertySetter
:
453 """Abstract class for objects that can set properties on a CVSRevision."""
455 def set_properties(self
, cvs_rev
):
456 """Set any properties that can be determined for CVS_REV.
458 CVS_REV is an instance of CVSRevision. This method should modify
459 CVS_REV.properties in place."""
461 raise NotImplementedError()
464 class CVSRevisionNumberSetter(RevisionPropertySetter
):
465 """Store the CVS revision number to an SVN property."""
467 def __init__(self
, propname
='cvs2svn:cvs-rev'):
468 self
.propname
= propname
470 def set_properties(self
, cvs_rev
):
471 if self
.propname
in cvs_rev
.properties
:
474 cvs_rev
.properties
[self
.propname
] = cvs_rev
.rev