* www/faq.html: Update link to SourceForge info about converting CVS -> SVN.
[cvs2svn.git] / cvs2svn_lib / property_setters.py
blob7cf379eba81e74c40aa4647864d4766992edacb7
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 CVSBinaryFileEOLStyleSetter(SVNPropertySetter):
69 """Set the eol-style to None for files with CVS mode '-kb'."""
71 propname = 'svn:eol-style'
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.mode == 'b':
78 s_item.svn_props[self.propname] = None
81 class MimeMapper(SVNPropertySetter):
82 """A class that provides mappings from file names to MIME types."""
84 propname = 'svn:mime-type'
86 def __init__(self, mime_types_file):
87 self.mappings = { }
89 for line in file(mime_types_file):
90 if line.startswith("#"):
91 continue
93 # format of a line is something like
94 # text/plain c h cpp
95 extensions = line.split()
96 if len(extensions) < 2:
97 continue
98 type = extensions.pop(0)
99 for ext in extensions:
100 if ext in self.mappings and self.mappings[ext] != type:
101 Log().error(
102 "%s: ambiguous MIME mapping for *.%s (%s or %s)\n"
103 % (warning_prefix, ext, self.mappings[ext], type)
105 self.mappings[ext] = type
107 def set_properties(self, s_item):
108 if self.propname in s_item.svn_props:
109 return
111 basename, extension = os.path.splitext(s_item.cvs_rev.cvs_file.basename)
113 # Extension includes the dot, so strip it (will leave extension
114 # empty if filename ends with a dot, which is ok):
115 extension = extension[1:]
117 # If there is no extension (or the file ends with a period), use
118 # the base name for mapping. This allows us to set mappings for
119 # files such as README or Makefile:
120 if not extension:
121 extension = basename
123 mime_type = self.mappings.get(extension, None)
124 if mime_type is not None:
125 s_item.svn_props[self.propname] = mime_type
128 class AutoPropsPropertySetter(SVNPropertySetter):
129 """Set arbitrary svn properties based on an auto-props configuration.
131 This class supports case-sensitive or case-insensitive pattern
132 matching. The command-line default is case-insensitive behavior,
133 consistent with Subversion (see
134 http://subversion.tigris.org/issues/show_bug.cgi?id=2036).
136 As a special extension to Subversion's auto-props handling, if a
137 property name is preceded by a '!' then that property is forced to
138 be left unset.
140 If a property specified in auto-props has already been set to a
141 different value, print a warning and leave the old property value
142 unchanged.
144 Python's treatment of whitespaces in the ConfigParser module is
145 buggy and inconsistent. Usually spaces are preserved, but if there
146 is at least one semicolon in the value, and the *first* semicolon is
147 preceded by a space, then that is treated as the start of a comment
148 and the rest of the line is silently discarded."""
150 property_name_pattern = r'(?P<name>[^\!\=\s]+)'
151 property_unset_re = re.compile(
152 r'^\!\s*' + property_name_pattern + r'$'
154 property_set_re = re.compile(
155 r'^' + property_name_pattern + r'\s*\=\s*(?P<value>.*)$'
157 property_novalue_re = re.compile(
158 r'^' + property_name_pattern + r'$'
161 quoted_re = re.compile(
162 r'^([\'\"]).*\1$'
164 comment_re = re.compile(r'\s;')
166 class Pattern:
167 """Describes the properties to be set for files matching a pattern."""
169 def __init__(self, pattern, propdict):
170 # A glob-like pattern:
171 self.pattern = pattern
172 # A dictionary of properties that should be set:
173 self.propdict = propdict
175 def match(self, basename):
176 """Does the file with the specified basename match pattern?"""
178 return fnmatch.fnmatch(basename, self.pattern)
180 def __init__(self, configfilename, ignore_case=True):
181 config = ConfigParser.ConfigParser()
182 if ignore_case:
183 self.transform_case = self.squash_case
184 else:
185 config.optionxform = self.preserve_case
186 self.transform_case = self.preserve_case
188 configtext = open(configfilename).read()
189 if self.comment_re.search(configtext):
190 Log().warn(
191 '%s: Please be aware that a space followed by a\n'
192 'semicolon is sometimes treated as a comment in configuration\n'
193 'files. This pattern was seen in\n'
194 ' %s\n'
195 'Please make sure that you have not inadvertently commented\n'
196 'out part of an important line.'
197 % (warning_prefix, configfilename,)
200 config.readfp(StringIO(configtext), configfilename)
201 self.patterns = []
202 sections = config.sections()
203 sections.sort()
204 for section in sections:
205 if self.transform_case(section) == 'auto-props':
206 patterns = config.options(section)
207 patterns.sort()
208 for pattern in patterns:
209 value = config.get(section, pattern)
210 if value:
211 self._add_pattern(pattern, value)
213 def squash_case(self, s):
214 return s.lower()
216 def preserve_case(self, s):
217 return s
219 def _add_pattern(self, pattern, props):
220 propdict = {}
221 if self.quoted_re.match(pattern):
222 Log().warn(
223 '%s: Quoting is not supported in auto-props; please verify rule\n'
224 'for %r. (Using pattern including quotation marks.)\n'
225 % (warning_prefix, pattern,)
227 for prop in props.split(';'):
228 prop = prop.strip()
229 m = self.property_unset_re.match(prop)
230 if m:
231 name = m.group('name')
232 Log().debug(
233 'auto-props: For %r, leaving %r unset.' % (pattern, name,)
235 propdict[name] = None
236 continue
238 m = self.property_set_re.match(prop)
239 if m:
240 name = m.group('name')
241 value = m.group('value')
242 if self.quoted_re.match(value):
243 Log().warn(
244 '%s: Quoting is not supported in auto-props; please verify\n'
245 'rule %r for pattern %r. (Using value\n'
246 'including quotation marks.)\n'
247 % (warning_prefix, prop, pattern,)
249 Log().debug(
250 'auto-props: For %r, setting %r to %r.' % (pattern, name, value,)
252 propdict[name] = value
253 continue
255 m = self.property_novalue_re.match(prop)
256 if m:
257 name = m.group('name')
258 Log().debug(
259 'auto-props: For %r, setting %r to the empty string'
260 % (pattern, name,)
262 propdict[name] = ''
263 continue
265 Log().warn(
266 '%s: in auto-props line for %r, value %r cannot be parsed (ignored)'
267 % (warning_prefix, pattern, prop,)
270 self.patterns.append(self.Pattern(self.transform_case(pattern), propdict))
272 def get_propdict(self, cvs_file):
273 basename = self.transform_case(cvs_file.basename)
274 propdict = {}
275 for pattern in self.patterns:
276 if pattern.match(basename):
277 for (key,value) in pattern.propdict.items():
278 if key in propdict:
279 if propdict[key] != value:
280 Log().warn(
281 "Contradictory values set for property '%s' for file %s."
282 % (key, cvs_file,))
283 else:
284 propdict[key] = value
286 return propdict
288 def set_properties(self, s_item):
289 propdict = self.get_propdict(s_item.cvs_rev.cvs_file)
290 for (k,v) in propdict.items():
291 if k in s_item.svn_props:
292 if s_item.svn_props[k] != v:
293 Log().warn(
294 "Property '%s' already set to %r for file %s; "
295 "auto-props value (%r) ignored."
296 % (k, s_item.svn_props[k], s_item.cvs_rev.cvs_path, v,))
297 else:
298 s_item.svn_props[k] = v
301 class CVSBinaryFileDefaultMimeTypeSetter(SVNPropertySetter):
302 """If the file is binary and its svn:mime-type property is not yet
303 set, set it to 'application/octet-stream'."""
305 propname = 'svn:mime-type'
307 def set_properties(self, s_item):
308 if self.propname in s_item.svn_props:
309 return
311 if s_item.cvs_rev.cvs_file.mode == 'b':
312 s_item.svn_props[self.propname] = 'application/octet-stream'
315 class EOLStyleFromMimeTypeSetter(SVNPropertySetter):
316 """Set svn:eol-style based on svn:mime-type.
318 If svn:mime-type is known but svn:eol-style is not, then set
319 svn:eol-style based on svn:mime-type as follows: if svn:mime-type
320 starts with 'text/', then set svn:eol-style to native; otherwise,
321 force it to remain unset. See also issue #39."""
323 propname = 'svn:eol-style'
325 def set_properties(self, s_item):
326 if self.propname in s_item.svn_props:
327 return
329 if s_item.svn_props.get('svn:mime-type', None) is not None:
330 if s_item.svn_props['svn:mime-type'].startswith("text/"):
331 s_item.svn_props[self.propname] = 'native'
332 else:
333 s_item.svn_props[self.propname] = None
336 class DefaultEOLStyleSetter(SVNPropertySetter):
337 """Set the eol-style if one has not already been set."""
339 propname = 'svn:eol-style'
341 def __init__(self, value):
342 """Initialize with the specified default VALUE."""
344 self.value = value
346 def set_properties(self, s_item):
347 if self.propname in s_item.svn_props:
348 return
350 s_item.svn_props[self.propname] = self.value
353 class SVNBinaryFileKeywordsPropertySetter(SVNPropertySetter):
354 """Turn off svn:keywords for files with binary svn:eol-style."""
356 propname = 'svn:keywords'
358 def set_properties(self, s_item):
359 if self.propname in s_item.svn_props:
360 return
362 if not s_item.svn_props.get('svn:eol-style'):
363 s_item.svn_props[self.propname] = None
366 class KeywordsPropertySetter(SVNPropertySetter):
367 """If the svn:keywords property is not yet set, set it based on the
368 file's mode. See issue #2."""
370 propname = 'svn:keywords'
372 def __init__(self, value):
373 """Use VALUE for the value of the svn:keywords property if it is
374 to be set."""
376 self.value = value
378 def set_properties(self, s_item):
379 if self.propname in s_item.svn_props:
380 return
382 if s_item.cvs_rev.cvs_file.mode in [None, 'kv', 'kvl']:
383 s_item.svn_props[self.propname] = self.value