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 Log
34 def _preserve_case(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
:
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
:
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
:
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'
96 self
, mime_types_file
=None, mime_mappings
=None,
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."""
119 self
.transform_case
= _squash_case
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("#"):
131 # format of a line is something like
133 extensions
= line
.split()
134 if len(extensions
) < 2:
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:
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:
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
:
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:
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
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
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(
215 comment_re = re.compile(r'\s
;')
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()
234 self.transform_case = _squash_case
236 config.optionxform = _preserve_case
237 self.transform_case = _preserve_case
239 configtext = open(configfilename).read()
240 if self.comment_re.search(configtext):
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'
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)
253 sections = config.sections()
255 for section in sections:
256 if self.transform_case(section) == 'auto
-props
':
257 patterns = config.options(section)
259 for pattern in patterns:
260 value = config.get(section, pattern)
262 self._add_pattern(pattern, value)
264 def _add_pattern(self, pattern, props):
266 if self.quoted_re.match(pattern):
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(';'):
274 m = self.property_unset_re.match(prop)
276 name = m.group('name
')
278 'auto
-props
: For
%r, leaving
%r unset
.' % (pattern, name,)
280 propdict[name] = None
283 m = self.property_set_re.match(prop)
285 name = m.group('name
')
286 value = m.group('value
')
287 if self.quoted_re.match(value):
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,)
295 'auto
-props
: For
%r, setting
%r to
%r.' % (pattern, name, value,)
297 propdict[name] = value
300 m = self.property_novalue_re.match(prop)
302 name = m.group('name
')
304 'auto
-props
: For
%r, setting
%r to the empty string
'
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)
320 for pattern in self.patterns:
321 if pattern.match(basename):
322 for (key,value) in pattern.propdict.items():
324 if propdict[key] != value:
326 "Contradictory values set for property '%s' for file %s."
329 propdict[key] = value
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:
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,)
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:
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:
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
'
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."""
392 def set_properties(self, cvs_file):
393 if self.propname in cvs_file.properties:
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:
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
424 def set_properties(self
, cvs_file
):
425 if self
.propname
in cvs_file
.properties
:
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
:
454 cvs_rev
.properties
[self
.propname
] = cvs_rev
.rev