1 # Copyright (C) 2005-2007 Jelmer Vernooij <jelmer@samba.org>
3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation; either version 3 of the License, or
6 # (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 """Handling of Subversion properties."""
18 __author__ = "Jelmer Vernooij <jelmer@samba.org>"
19 __docformat__ = "restructuredText"
24 class InvalidExternalsDescription(Exception):
25 _fmt = """Unable to parse externals description."""
28 def is_valid_property_name(prop):
29 """Check the validity of a property name.
31 :param prop: Property name
32 :return: Whether prop is a valid property name
34 if not prop[0].isalnum() and not prop[0] in ":_":
37 if not c.isalnum() and not c in "-:._":
42 def time_to_cstring(timestamp):
44 tm_usec = timestamp % 1000000
45 (tm_year, tm_mon, tm_mday, tm_hour, tm_min,
46 tm_sec, tm_wday, tm_yday, tm_isdst) = time.gmtime(timestamp / 1000000)
47 return "%04d-%02d-%02dT%02d:%02d:%02d.%06dZ" % (tm_year, tm_mon, tm_mday, tm_hour, tm_min, tm_sec, tm_usec)
50 def time_from_cstring(text):
52 (basestr, usecstr) = text.split(".", 1)
53 assert usecstr[-1] == "Z"
54 tm_usec = int(usecstr[:-1])
55 tm = time.strptime(basestr, "%Y-%m-%dT%H:%M:%S")
56 return (long(time.mktime((tm[0], tm[1], tm[2], tm[3], tm[4], tm[5], tm[6], tm[7], -1)) - time.timezone) * 1000000 + tm_usec)
59 def parse_externals_description(base_url, val):
60 """Parse an svn:externals property value.
62 :param base_url: URL on which the property is set. Used for
65 :returns: dictionary with local names as keys, (revnum, url)
66 as value. revnum is the revision number and is
67 set to None if not applicable.
72 for l in val.splitlines():
73 if l == "" or l[0] == "#":
75 pts = l.rsplit(None, 3)
77 if pts[0] == "-r": # -r X URL DIR
81 elif pts[1] == "-r": # DIR -r X URL
86 raise InvalidExternalsDescription()
88 if pts[1].startswith("-r"): # DIR -rX URL
89 revno = int(pts[1][2:])
92 elif pts[0].startswith("-r"): # -rX URL DIR
93 revno = int(pts[0][2:])
97 raise InvalidExternalsDescription()
99 if not is_url(pts[0]):
107 raise InvalidExternalsDescription()
108 if relurl.startswith("//"):
109 raise NotImplementedError("Relative to the scheme externals not yet supported")
110 if relurl.startswith("^/"):
111 raise NotImplementedError("Relative to the repository root externals not yet supported")
112 ret[path] = (revno, urllib.basejoin(base_url, relurl))
116 def parse_mergeinfo_property(text):
117 """Parse a mergeinfo property.
119 :param text: Property contents
122 for l in text.splitlines():
123 (path, ranges) = l.rsplit(":", 1)
124 assert path.startswith("/")
126 for range in ranges.split(","):
133 (start, end) = range.split("-", 1)
134 ret[path].append((int(start), int(end), inheritable))
136 ret[path].append((int(range), int(range), inheritable))
141 def generate_mergeinfo_property(merges):
142 def formatrange((start, end, inheritable)):
147 return "%d%s" % (start, suffix)
149 return "%d-%d%s" % (start, end, suffix)
151 for (path, ranges) in merges.items():
152 assert path.startswith("/")
153 text += "%s:%s\n" % (path, ",".join(map(formatrange, ranges)))
157 def range_includes_revnum(ranges, revnum):
158 i = bisect.bisect(ranges, (revnum, revnum, True))
161 (start, end, inheritable) = ranges[i-1]
162 return (start <= revnum <= end)
165 def range_add_revnum(ranges, revnum, inheritable=True):
166 # TODO: Deal with inheritable
167 item = (revnum, revnum, inheritable)
171 i = bisect.bisect(ranges, item)
173 (start, end, inh) = ranges[i-1]
174 if (start <= revnum <= end):
178 # Extend previous range
179 ranges[i-1] = (start, end+1, inh)
182 (start, end, inh) = ranges[i]
183 if start-1 == revnum:
185 ranges[i] = (start-1, end, inh)
187 ranges.insert(i, item)
191 def mergeinfo_includes_revision(merges, path, revnum):
192 assert path.startswith("/")
194 ranges = merges[path]
198 return range_includes_revnum(ranges, revnum)
201 def mergeinfo_add_revision(mergeinfo, path, revnum):
202 assert path.startswith("/")
203 mergeinfo[path] = range_add_revnum(mergeinfo.get(path, []), revnum)
207 PROP_EXECUTABLE = 'svn:executable'
208 PROP_EXECUTABLE_VALUE = '*'
209 PROP_EXTERNALS = 'svn:externals'
210 PROP_IGNORE = 'svn:ignore'
211 PROP_KEYWORDS = 'svn:keywords'
212 PROP_MIME_TYPE = 'svn:mime-type'
213 PROP_MERGEINFO = 'svn:mergeinfo'
214 PROP_NEEDS_LOCK = 'svn:needs-lock'
215 PROP_NEEDS_LOCK_VALUE = '*'
217 PROP_SPECIAL = 'svn:special'
218 PROP_SPECIAL_VALUE = '*'
219 PROP_WC_PREFIX = 'svn:wc:'
220 PROP_ENTRY_PREFIX = 'svn:entry'
221 PROP_ENTRY_COMMITTED_DATE = 'svn:entry:committed-date'
222 PROP_ENTRY_COMMITTED_REV = 'svn:entry:committed-rev'
223 PROP_ENTRY_LAST_AUTHOR = 'svn:entry:last-author'
224 PROP_ENTRY_LOCK_TOKEN = 'svn:entry:lock-token'
225 PROP_ENTRY_UUID = 'svn:entry:uuid'
227 PROP_REVISION_LOG = "svn:log"
228 PROP_REVISION_AUTHOR = "svn:author"
229 PROP_REVISION_DATE = "svn:date"
231 def diff(current, previous):
232 """Find the differences between two property dictionaries.
234 :param current: Dictionary with current (new) properties
235 :param previous: Dictionary with previous (old) properties
236 :return: Dictionary that contains an entry for
237 each property that was changed. Value is a tuple
238 with the old and the new property value.
241 for key, val in current.items():
242 if previous.get(key) != val: