Change the property name for CVS descriptions to "cvs:description".
[cvs2svn.git] / cvs2svn_lib / property_setters.py
blob4bc79090bbeb1a0360e9f63de096b86564fe9594
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 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 """Set the cvs2svn:cvs-rev property to the CVS revision number."""
45 propname = 'cvs2svn:cvs-rev'
47 def set_properties(self, s_item):
48 if self.propname in s_item.svn_props:
49 return
51 s_item.svn_props[self.propname] = s_item.cvs_rev.rev
52 s_item.svn_props_changed = True
55 class ExecutablePropertySetter(SVNPropertySetter):
56 """Set the svn:executable property based on cvs_rev.cvs_file.executable."""
58 propname = 'svn:executable'
60 def set_properties(self, s_item):
61 if self.propname in s_item.svn_props:
62 return
64 if s_item.cvs_rev.cvs_file.executable:
65 s_item.svn_props[self.propname] = '*'
68 class DescriptionPropertySetter(SVNPropertySetter):
69 """Set the svn:description property based on cvs_rev.cvs_file.description."""
71 propname = 'cvs:description'
73 def set_properties(self, s_item):
74 if self.propname in s_item.svn_props:
75 return
77 if s_item.cvs_rev.cvs_file.description:
78 s_item.svn_props[self.propname] = s_item.cvs_rev.cvs_file.description
81 class CVSBinaryFileEOLStyleSetter(SVNPropertySetter):
82 """Set the eol-style to None for files with CVS mode '-kb'."""
84 propname = 'svn:eol-style'
86 def set_properties(self, s_item):
87 if self.propname in s_item.svn_props:
88 return
90 if s_item.cvs_rev.cvs_file.mode == 'b':
91 s_item.svn_props[self.propname] = None
94 class MimeMapper(SVNPropertySetter):
95 """A class that provides mappings from file names to MIME types."""
97 propname = 'svn:mime-type'
99 def __init__(self, mime_types_file):
100 self.mappings = { }
102 for line in file(mime_types_file):
103 if line.startswith("#"):
104 continue
106 # format of a line is something like
107 # text/plain c h cpp
108 extensions = line.split()
109 if len(extensions) < 2:
110 continue
111 type = extensions.pop(0)
112 for ext in extensions:
113 if ext in self.mappings and self.mappings[ext] != type:
114 Log().error(
115 "%s: ambiguous MIME mapping for *.%s (%s or %s)\n"
116 % (warning_prefix, ext, self.mappings[ext], type)
118 self.mappings[ext] = type
120 def set_properties(self, s_item):
121 if self.propname in s_item.svn_props:
122 return
124 basename, extension = os.path.splitext(s_item.cvs_rev.cvs_file.basename)
126 # Extension includes the dot, so strip it (will leave extension
127 # empty if filename ends with a dot, which is ok):
128 extension = extension[1:]
130 # If there is no extension (or the file ends with a period), use
131 # the base name for mapping. This allows us to set mappings for
132 # files such as README or Makefile:
133 if not extension:
134 extension = basename
136 mime_type = self.mappings.get(extension, None)
137 if mime_type is not None:
138 s_item.svn_props[self.propname] = mime_type
141 class AutoPropsPropertySetter(SVNPropertySetter):
142 """Set arbitrary svn properties based on an auto-props configuration.
144 This class supports case-sensitive or case-insensitive pattern
145 matching. The command-line default is case-insensitive behavior,
146 consistent with Subversion (see
147 http://subversion.tigris.org/issues/show_bug.cgi?id=2036).
149 As a special extension to Subversion's auto-props handling, if a
150 property name is preceded by a '!' then that property is forced to
151 be left unset.
153 If a property specified in auto-props has already been set to a
154 different value, print a warning and leave the old property value
155 unchanged.
157 Python's treatment of whitespaces in the ConfigParser module is
158 buggy and inconsistent. Usually spaces are preserved, but if there
159 is at least one semicolon in the value, and the *first* semicolon is
160 preceded by a space, then that is treated as the start of a comment
161 and the rest of the line is silently discarded."""
163 property_name_pattern = r'(?P<name>[^\!\=\s]+)'
164 property_unset_re = re.compile(
165 r'^\!\s*' + property_name_pattern + r'$'
167 property_set_re = re.compile(
168 r'^' + property_name_pattern + r'\s*\=\s*(?P<value>.*)$'
170 property_novalue_re = re.compile(
171 r'^' + property_name_pattern + r'$'
174 quoted_re = re.compile(
175 r'^([\'\"]).*\1$'
177 comment_re = re.compile(r'\s;')
179 class Pattern:
180 """Describes the properties to be set for files matching a pattern."""
182 def __init__(self, pattern, propdict):
183 # A glob-like pattern:
184 self.pattern = pattern
185 # A dictionary of properties that should be set:
186 self.propdict = propdict
188 def match(self, basename):
189 """Does the file with the specified basename match pattern?"""
191 return fnmatch.fnmatch(basename, self.pattern)
193 def __init__(self, configfilename, ignore_case=True):
194 config = ConfigParser.ConfigParser()
195 if ignore_case:
196 self.transform_case = self.squash_case
197 else:
198 config.optionxform = self.preserve_case
199 self.transform_case = self.preserve_case
201 configtext = open(configfilename).read()
202 if self.comment_re.search(configtext):
203 Log().warn(
204 '%s: Please be aware that a space followed by a\n'
205 'semicolon is sometimes treated as a comment in configuration\n'
206 'files. This pattern was seen in\n'
207 ' %s\n'
208 'Please make sure that you have not inadvertently commented\n'
209 'out part of an important line.'
210 % (warning_prefix, configfilename,)
213 config.readfp(StringIO(configtext), configfilename)
214 self.patterns = []
215 sections = config.sections()
216 sections.sort()
217 for section in sections:
218 if self.transform_case(section) == 'auto-props':
219 patterns = config.options(section)
220 patterns.sort()
221 for pattern in patterns:
222 value = config.get(section, pattern)
223 if value:
224 self._add_pattern(pattern, value)
226 def squash_case(self, s):
227 return s.lower()
229 def preserve_case(self, s):
230 return s
232 def _add_pattern(self, pattern, props):
233 propdict = {}
234 if self.quoted_re.match(pattern):
235 Log().warn(
236 '%s: Quoting is not supported in auto-props; please verify rule\n'
237 'for %r. (Using pattern including quotation marks.)\n'
238 % (warning_prefix, pattern,)
240 for prop in props.split(';'):
241 prop = prop.strip()
242 m = self.property_unset_re.match(prop)
243 if m:
244 name = m.group('name')
245 Log().debug(
246 'auto-props: For %r, leaving %r unset.' % (pattern, name,)
248 propdict[name] = None
249 continue
251 m = self.property_set_re.match(prop)
252 if m:
253 name = m.group('name')
254 value = m.group('value')
255 if self.quoted_re.match(value):
256 Log().warn(
257 '%s: Quoting is not supported in auto-props; please verify\n'
258 'rule %r for pattern %r. (Using value\n'
259 'including quotation marks.)\n'
260 % (warning_prefix, prop, pattern,)
262 Log().debug(
263 'auto-props: For %r, setting %r to %r.' % (pattern, name, value,)
265 propdict[name] = value
266 continue
268 m = self.property_novalue_re.match(prop)
269 if m:
270 name = m.group('name')
271 Log().debug(
272 'auto-props: For %r, setting %r to the empty string'
273 % (pattern, name,)
275 propdict[name] = ''
276 continue
278 Log().warn(
279 '%s: in auto-props line for %r, value %r cannot be parsed (ignored)'
280 % (warning_prefix, pattern, prop,)
283 self.patterns.append(self.Pattern(self.transform_case(pattern), propdict))
285 def get_propdict(self, cvs_file):
286 basename = self.transform_case(cvs_file.basename)
287 propdict = {}
288 for pattern in self.patterns:
289 if pattern.match(basename):
290 for (key,value) in pattern.propdict.items():
291 if key in propdict:
292 if propdict[key] != value:
293 Log().warn(
294 "Contradictory values set for property '%s' for file %s."
295 % (key, cvs_file,))
296 else:
297 propdict[key] = value
299 return propdict
301 def set_properties(self, s_item):
302 propdict = self.get_propdict(s_item.cvs_rev.cvs_file)
303 for (k,v) in propdict.items():
304 if k in s_item.svn_props:
305 if s_item.svn_props[k] != v:
306 Log().warn(
307 "Property '%s' already set to %r for file %s; "
308 "auto-props value (%r) ignored."
309 % (k, s_item.svn_props[k], s_item.cvs_rev.cvs_path, v,))
310 else:
311 s_item.svn_props[k] = v
314 class CVSBinaryFileDefaultMimeTypeSetter(SVNPropertySetter):
315 """If the file is binary and its svn:mime-type property is not yet
316 set, set it to 'application/octet-stream'."""
318 propname = 'svn:mime-type'
320 def set_properties(self, s_item):
321 if self.propname in s_item.svn_props:
322 return
324 if s_item.cvs_rev.cvs_file.mode == 'b':
325 s_item.svn_props[self.propname] = 'application/octet-stream'
328 class EOLStyleFromMimeTypeSetter(SVNPropertySetter):
329 """Set svn:eol-style based on svn:mime-type.
331 If svn:mime-type is known but svn:eol-style is not, then set
332 svn:eol-style based on svn:mime-type as follows: if svn:mime-type
333 starts with 'text/', then set svn:eol-style to native; otherwise,
334 force it to remain unset. See also issue #39."""
336 propname = 'svn:eol-style'
338 def set_properties(self, s_item):
339 if self.propname in s_item.svn_props:
340 return
342 if s_item.svn_props.get('svn:mime-type', None) is not None:
343 if s_item.svn_props['svn:mime-type'].startswith("text/"):
344 s_item.svn_props[self.propname] = 'native'
345 else:
346 s_item.svn_props[self.propname] = None
349 class DefaultEOLStyleSetter(SVNPropertySetter):
350 """Set the eol-style if one has not already been set."""
352 propname = 'svn:eol-style'
354 def __init__(self, value):
355 """Initialize with the specified default VALUE."""
357 self.value = value
359 def set_properties(self, s_item):
360 if self.propname in s_item.svn_props:
361 return
363 s_item.svn_props[self.propname] = self.value
366 class SVNBinaryFileKeywordsPropertySetter(SVNPropertySetter):
367 """Turn off svn:keywords for files with binary svn:eol-style."""
369 propname = 'svn:keywords'
371 def set_properties(self, s_item):
372 if self.propname in s_item.svn_props:
373 return
375 if not s_item.svn_props.get('svn:eol-style'):
376 s_item.svn_props[self.propname] = None
379 class KeywordsPropertySetter(SVNPropertySetter):
380 """If the svn:keywords property is not yet set, set it based on the
381 file's mode. See issue #2."""
383 propname = 'svn:keywords'
385 def __init__(self, value):
386 """Use VALUE for the value of the svn:keywords property if it is
387 to be set."""
389 self.value = value
391 def set_properties(self, s_item):
392 if self.propname in s_item.svn_props:
393 return
395 if s_item.cvs_rev.cvs_file.mode in [None, 'kv', 'kvl']:
396 s_item.svn_props[self.propname] = self.value