Move properties code into subvertpy.
[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 import bisect
17 from bzrlib import urlutils
18
19
20 class InvalidExternalsDescription(Exception):
21     _fmt = """Unable to parse externals description."""
22
23
24 def is_valid_property_name(prop):
25     if not prop[0].isalnum() and not prop[0] in ":_":
26         return False
27     for c in prop[1:]:
28         if not c.isalnum() and not c in "-:._":
29             return False
30     return True
31
32
33 def time_to_cstring(timestamp):
34     import time
35     tm_usec = timestamp % 1000000
36     (tm_year, tm_mon, tm_mday, tm_hour, tm_min, 
37             tm_sec, tm_wday, tm_yday, tm_isdst) = time.gmtime(timestamp / 1000000)
38     return "%04d-%02d-%02dT%02d:%02d:%02d.%06dZ" % (tm_year, tm_mon, tm_mday, tm_hour, tm_min, tm_sec, tm_usec)
39
40
41 def time_from_cstring(text):
42     import time
43     (basestr, usecstr) = text.split(".", 1)
44     assert usecstr[-1] == "Z"
45     tm_usec = int(usecstr[:-1])
46     tm = time.strptime(basestr, "%Y-%m-%dT%H:%M:%S")
47     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)
48
49
50 def parse_externals_description(base_url, val):
51     """Parse an svn:externals property value.
52
53     :param base_url: URL on which the property is set. Used for 
54         relative externals.
55
56     :returns: dictionary with local names as keys, (revnum, url)
57               as value. revnum is the revision number and is 
58               set to None if not applicable.
59     """
60     def is_url(u):
61         return ("://" in u)
62     ret = {}
63     for l in val.splitlines():
64         if l == "" or l[0] == "#":
65             continue
66         pts = l.rsplit(None, 3) 
67         if len(pts) == 4:
68             if pts[0] == "-r": # -r X URL DIR
69                 revno = int(pts[1])
70                 path = pts[3]
71                 relurl = pts[2]
72             elif pts[1] == "-r": # DIR -r X URL
73                 revno = int(pts[2])
74                 path = pts[0]
75                 relurl = pts[3]
76             else:
77                 raise InvalidExternalsDescription()
78         elif len(pts) == 3:
79             if pts[1].startswith("-r"): # DIR -rX URL
80                 revno = int(pts[1][2:])
81                 path = pts[0]
82                 relurl = pts[2]
83             elif pts[0].startswith("-r"): # -rX URL DIR
84                 revno = int(pts[0][2:])
85                 path = pts[2]
86                 relurl = pts[1]
87             else:
88                 raise InvalidExternalsDescription()
89         elif len(pts) == 2:
90             if not is_url(pts[0]):
91                 relurl = pts[1]
92                 path = pts[0]
93             else:
94                 relurl = pts[0]
95                 path = pts[1]
96             revno = None
97         else:
98             raise InvalidExternalsDescription()
99         if relurl.startswith("//"):
100             raise NotImplementedError("Relative to the scheme externals not yet supported")
101         if relurl.startswith("^/"):
102             raise NotImplementedError("Relative to the repository root externals not yet supported")
103         ret[path] = (revno, "%s/%s" % (base_url, relurl))
104     return ret
105
106
107 def parse_mergeinfo_property(text):
108     ret = {}
109     for l in text.splitlines():
110         (path, ranges) = l.rsplit(":", 1)
111         assert path.startswith("/")
112         ret[path] = []
113         for range in ranges.split(","):
114             if range[-1] == "*":
115                 inheritable = False
116                 range = range[:-1]
117             else:
118                 inheritable = True
119             try:
120                 (start, end) = range.split("-", 1)
121                 ret[path].append((int(start), int(end), inheritable))
122             except ValueError:
123                 ret[path].append((int(range), int(range), inheritable))
124
125     return ret
126
127
128 def generate_mergeinfo_property(merges):
129     def formatrange((start, end, inheritable)):
130         suffix = ""
131         if not inheritable:
132             suffix = "*"
133         if start == end:
134             return "%d%s" % (start, suffix)
135         else:
136             return "%d-%d%s" % (start, end, suffix)
137     text = ""
138     for (path, ranges) in merges.items():
139         assert path.startswith("/")
140         text += "%s:%s\n" % (path, ",".join(map(formatrange, ranges)))
141     return text
142
143
144 def range_includes_revnum(ranges, revnum):
145     i = bisect.bisect(ranges, (revnum, revnum, True))
146     if i == 0:
147         return False
148     (start, end, inheritable) = ranges[i-1]
149     return (start <= revnum <= end)
150
151
152 def range_add_revnum(ranges, revnum, inheritable=True):
153     # TODO: Deal with inheritable
154     item = (revnum, revnum, inheritable)
155     if len(ranges) == 0:
156         ranges.append(item)
157         return ranges
158     i = bisect.bisect(ranges, item)
159     if i > 0:
160         (start, end, inh) = ranges[i-1]
161         if (start <= revnum <= end):
162             # already there
163             return ranges
164         if end == revnum-1:
165             # Extend previous range
166             ranges[i-1] = (start, end+1, inh)
167             return ranges
168     if i < len(ranges):
169         (start, end, inh) = ranges[i]
170         if start-1 == revnum:
171             # Extend next range
172             ranges[i] = (start-1, end, inh)
173             return ranges
174     ranges.insert(i, item)
175     return ranges
176
177
178 def mergeinfo_includes_revision(merges, path, revnum):
179     assert path.startswith("/")
180     try:
181         ranges = merges[path]
182     except KeyError:
183         return False
184
185     return range_includes_revnum(ranges, revnum)
186
187
188 def mergeinfo_add_revision(mergeinfo, path, revnum):
189     assert path.startswith("/")
190     mergeinfo[path] = range_add_revnum(mergeinfo.get(path, []), revnum)
191     return mergeinfo
192
193
194 PROP_EXECUTABLE = 'svn:executable'
195 PROP_EXECUTABLE_VALUE = '*'
196 PROP_EXTERNALS = 'svn:externals'
197 PROP_IGNORE = 'svn:ignore'
198 PROP_KEYWORDS = 'svn:keywords'
199 PROP_MIME_TYPE = 'svn:mime-type'
200 PROP_MERGEINFO = 'svn:mergeinfo'
201 PROP_NEEDS_LOCK = 'svn:needs-lock'
202 PROP_NEEDS_LOCK_VALUE = '*'
203 PROP_PREFIX = 'svn:'
204 PROP_SPECIAL = 'svn:special'
205 PROP_SPECIAL_VALUE = '*'
206 PROP_WC_PREFIX = 'svn:wc:'
207 PROP_ENTRY_PREFIX = 'svn:entry'
208 PROP_ENTRY_COMMITTED_DATE = 'svn:entry:committed-date'
209 PROP_ENTRY_COMMITTED_REV = 'svn:entry:committed-rev'
210 PROP_ENTRY_LAST_AUTHOR = 'svn:entry:last-author'
211 PROP_ENTRY_LOCK_TOKEN = 'svn:entry:lock-token'
212 PROP_ENTRY_UUID = 'svn:entry:uuid'
213
214 PROP_REVISION_LOG = "svn:log"
215 PROP_REVISION_AUTHOR = "svn:author"
216 PROP_REVISION_DATE = "svn:date"
217
218 def diff(current, previous):
219     ret = {}
220     for key, val in current.items():
221         if previous.get(key) != val:
222             ret[key] = val
223     return ret