Add some documentation.
[jelmer/subvertpy.git] / subvertpy / properties.py
1 # Copyright (C) 2005-2007 Jelmer Vernooij <jelmer@samba.org>
2  
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.
7
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.
12
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/>.
15
16 """Handling of Subversion properties."""
17
18 __author__ = "Jelmer Vernooij <jelmer@samba.org>"
19 __docformat__ = "restructuredText"
20
21 import bisect, urllib
22
23
24 class InvalidExternalsDescription(Exception):
25     _fmt = """Unable to parse externals description."""
26
27
28 def is_valid_property_name(prop):
29     """Check the validity of a property name.
30
31     :param prop: Property name
32     :return: Whether prop is a valid property name
33     """
34     if not prop[0].isalnum() and not prop[0] in ":_":
35         return False
36     for c in prop[1:]:
37         if not c.isalnum() and not c in "-:._":
38             return False
39     return True
40
41
42 def time_to_cstring(timestamp):
43     import time
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)
48
49
50 def time_from_cstring(text):
51     import time
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)
57
58
59 def parse_externals_description(base_url, val):
60     """Parse an svn:externals property value.
61
62     :param base_url: URL on which the property is set. Used for 
63         relative externals.
64
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.
68     """
69     def is_url(u):
70         return ("://" in u)
71     ret = {}
72     for l in val.splitlines():
73         if l == "" or l[0] == "#":
74             continue
75         pts = l.rsplit(None, 3) 
76         if len(pts) == 4:
77             if pts[0] == "-r": # -r X URL DIR
78                 revno = int(pts[1])
79                 path = pts[3]
80                 relurl = pts[2]
81             elif pts[1] == "-r": # DIR -r X URL
82                 revno = int(pts[2])
83                 path = pts[0]
84                 relurl = pts[3]
85             else:
86                 raise InvalidExternalsDescription()
87         elif len(pts) == 3:
88             if pts[1].startswith("-r"): # DIR -rX URL
89                 revno = int(pts[1][2:])
90                 path = pts[0]
91                 relurl = pts[2]
92             elif pts[0].startswith("-r"): # -rX URL DIR
93                 revno = int(pts[0][2:])
94                 path = pts[2]
95                 relurl = pts[1]
96             else:
97                 raise InvalidExternalsDescription()
98         elif len(pts) == 2:
99             if not is_url(pts[0]):
100                 relurl = pts[1]
101                 path = pts[0]
102             else:
103                 relurl = pts[0]
104                 path = pts[1]
105             revno = None
106         else:
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))
113     return ret
114
115
116 def parse_mergeinfo_property(text):
117     """Parse a mergeinfo property.
118
119     :param text: Property contents
120     """
121     ret = {}
122     for l in text.splitlines():
123         (path, ranges) = l.rsplit(":", 1)
124         assert path.startswith("/")
125         ret[path] = []
126         for range in ranges.split(","):
127             if range[-1] == "*":
128                 inheritable = False
129                 range = range[:-1]
130             else:
131                 inheritable = True
132             try:
133                 (start, end) = range.split("-", 1)
134                 ret[path].append((int(start), int(end), inheritable))
135             except ValueError:
136                 ret[path].append((int(range), int(range), inheritable))
137
138     return ret
139
140
141 def generate_mergeinfo_property(merges):
142     def formatrange((start, end, inheritable)):
143         suffix = ""
144         if not inheritable:
145             suffix = "*"
146         if start == end:
147             return "%d%s" % (start, suffix)
148         else:
149             return "%d-%d%s" % (start, end, suffix)
150     text = ""
151     for (path, ranges) in merges.items():
152         assert path.startswith("/")
153         text += "%s:%s\n" % (path, ",".join(map(formatrange, ranges)))
154     return text
155
156
157 def range_includes_revnum(ranges, revnum):
158     i = bisect.bisect(ranges, (revnum, revnum, True))
159     if i == 0:
160         return False
161     (start, end, inheritable) = ranges[i-1]
162     return (start <= revnum <= end)
163
164
165 def range_add_revnum(ranges, revnum, inheritable=True):
166     # TODO: Deal with inheritable
167     item = (revnum, revnum, inheritable)
168     if len(ranges) == 0:
169         ranges.append(item)
170         return ranges
171     i = bisect.bisect(ranges, item)
172     if i > 0:
173         (start, end, inh) = ranges[i-1]
174         if (start <= revnum <= end):
175             # already there
176             return ranges
177         if end == revnum-1:
178             # Extend previous range
179             ranges[i-1] = (start, end+1, inh)
180             return ranges
181     if i < len(ranges):
182         (start, end, inh) = ranges[i]
183         if start-1 == revnum:
184             # Extend next range
185             ranges[i] = (start-1, end, inh)
186             return ranges
187     ranges.insert(i, item)
188     return ranges
189
190
191 def mergeinfo_includes_revision(merges, path, revnum):
192     assert path.startswith("/")
193     try:
194         ranges = merges[path]
195     except KeyError:
196         return False
197
198     return range_includes_revnum(ranges, revnum)
199
200
201 def mergeinfo_add_revision(mergeinfo, path, revnum):
202     assert path.startswith("/")
203     mergeinfo[path] = range_add_revnum(mergeinfo.get(path, []), revnum)
204     return mergeinfo
205
206
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 = '*'
216 PROP_PREFIX = 'svn:'
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'
226
227 PROP_REVISION_LOG = "svn:log"
228 PROP_REVISION_AUTHOR = "svn:author"
229 PROP_REVISION_DATE = "svn:date"
230
231 def diff(current, previous):
232     """Find the differences between two property dictionaries.
233
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.
239     """
240     ret = {}
241     for key, val in current.items():
242         if previous.get(key) != val:
243             ret[key] = val
244     return ret