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
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
:
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
:
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
:
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
:
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'
102 self
, mime_types_file
=None, mime_mappings
=None,
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."""
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("#"):
134 # format of a line is something like
136 extensions
= line
.split()
137 if len(extensions
) < 2:
139 type = extensions
.pop(0)
140 for ext
in extensions
:
143 if ext
in self
.mappings
and self
.mappings
[ext
] != type:
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():
154 if ext
in self
.mappings
and self
.mappings
[ext
] != type:
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
:
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:
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
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
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(
221 comment_re = re.compile(r'\s
;')
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()
240 self.transform_case = self.squash_case
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):
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'
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)
259 sections = config.sections()
261 for section in sections:
262 if self.transform_case(section) == 'auto
-props
':
263 patterns = config.options(section)
265 for pattern in patterns:
266 value = config.get(section, pattern)
268 self._add_pattern(pattern, value)
270 def squash_case(self, s):
273 def preserve_case(self, s):
276 def _add_pattern(self, pattern, props):
278 if self.quoted_re.match(pattern):
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(';'):
286 m = self.property_unset_re.match(prop)
288 name = m.group('name
')
290 'auto
-props
: For
%r, leaving
%r unset
.' % (pattern, name,)
292 propdict[name] = None
295 m = self.property_set_re.match(prop)
297 name = m.group('name
')
298 value = m.group('value
')
299 if self.quoted_re.match(value):
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,)
307 'auto
-props
: For
%r, setting
%r to
%r.' % (pattern, name, value,)
309 propdict[name] = value
312 m = self.property_novalue_re.match(prop)
314 name = m.group('name
')
316 'auto
-props
: For
%r, setting
%r to the empty string
'
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)
332 for pattern in self.patterns:
333 if pattern.match(basename):
334 for (key,value) in pattern.propdict.items():
336 if propdict[key] != value:
338 "Contradictory values set for property '%s' for file %s."
341 propdict[key] = value
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:
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,))
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:
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:
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
'
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."""
403 def set_properties(self, s_item):
404 if self.propname in s_item.svn_props:
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:
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
435 def set_properties(self
, s_item
):
436 if self
.propname
in s_item
.svn_props
:
439 if s_item
.cvs_rev
.cvs_file
.mode
in [None, 'kv', 'kvl']:
440 s_item
.svn_props
[self
.propname
] = self
.value