##// END OF EJS Templates
changelog: hexlify node when throwing a LookupError on a filtered node...
Augie Fackler -
r18587:cbf5f3eb default
parent child Browse files
Show More
@@ -1,349 +1,349 b''
1 1 # changelog.py - changelog class for mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from node import bin, hex, nullid
9 9 from i18n import _
10 10 import util, error, revlog, encoding
11 11
12 12 _defaultextra = {'branch': 'default'}
13 13
14 14 def _string_escape(text):
15 15 """
16 16 >>> d = {'nl': chr(10), 'bs': chr(92), 'cr': chr(13), 'nul': chr(0)}
17 17 >>> s = "ab%(nl)scd%(bs)s%(bs)sn%(nul)sab%(cr)scd%(bs)s%(nl)s" % d
18 18 >>> s
19 19 'ab\\ncd\\\\\\\\n\\x00ab\\rcd\\\\\\n'
20 20 >>> res = _string_escape(s)
21 21 >>> s == res.decode('string_escape')
22 22 True
23 23 """
24 24 # subset of the string_escape codec
25 25 text = text.replace('\\', '\\\\').replace('\n', '\\n').replace('\r', '\\r')
26 26 return text.replace('\0', '\\0')
27 27
28 28 def decodeextra(text):
29 29 """
30 30 >>> sorted(decodeextra(encodeextra({'foo': 'bar', 'baz': chr(0) + '2'})
31 31 ... ).iteritems())
32 32 [('baz', '\\x002'), ('branch', 'default'), ('foo', 'bar')]
33 33 >>> sorted(decodeextra(encodeextra({'foo': 'bar',
34 34 ... 'baz': chr(92) + chr(0) + '2'})
35 35 ... ).iteritems())
36 36 [('baz', '\\\\\\x002'), ('branch', 'default'), ('foo', 'bar')]
37 37 """
38 38 extra = _defaultextra.copy()
39 39 for l in text.split('\0'):
40 40 if l:
41 41 if '\\0' in l:
42 42 # fix up \0 without getting into trouble with \\0
43 43 l = l.replace('\\\\', '\\\\\n')
44 44 l = l.replace('\\0', '\0')
45 45 l = l.replace('\n', '')
46 46 k, v = l.decode('string_escape').split(':', 1)
47 47 extra[k] = v
48 48 return extra
49 49
50 50 def encodeextra(d):
51 51 # keys must be sorted to produce a deterministic changelog entry
52 52 items = [_string_escape('%s:%s' % (k, d[k])) for k in sorted(d)]
53 53 return "\0".join(items)
54 54
55 55 def stripdesc(desc):
56 56 """strip trailing whitespace and leading and trailing empty lines"""
57 57 return '\n'.join([l.rstrip() for l in desc.splitlines()]).strip('\n')
58 58
59 59 class appender(object):
60 60 '''the changelog index must be updated last on disk, so we use this class
61 61 to delay writes to it'''
62 62 def __init__(self, fp, buf):
63 63 self.data = buf
64 64 self.fp = fp
65 65 self.offset = fp.tell()
66 66 self.size = util.fstat(fp).st_size
67 67
68 68 def end(self):
69 69 return self.size + len("".join(self.data))
70 70 def tell(self):
71 71 return self.offset
72 72 def flush(self):
73 73 pass
74 74 def close(self):
75 75 self.fp.close()
76 76
77 77 def seek(self, offset, whence=0):
78 78 '''virtual file offset spans real file and data'''
79 79 if whence == 0:
80 80 self.offset = offset
81 81 elif whence == 1:
82 82 self.offset += offset
83 83 elif whence == 2:
84 84 self.offset = self.end() + offset
85 85 if self.offset < self.size:
86 86 self.fp.seek(self.offset)
87 87
88 88 def read(self, count=-1):
89 89 '''only trick here is reads that span real file and data'''
90 90 ret = ""
91 91 if self.offset < self.size:
92 92 s = self.fp.read(count)
93 93 ret = s
94 94 self.offset += len(s)
95 95 if count > 0:
96 96 count -= len(s)
97 97 if count != 0:
98 98 doff = self.offset - self.size
99 99 self.data.insert(0, "".join(self.data))
100 100 del self.data[1:]
101 101 s = self.data[0][doff:doff + count]
102 102 self.offset += len(s)
103 103 ret += s
104 104 return ret
105 105
106 106 def write(self, s):
107 107 self.data.append(str(s))
108 108 self.offset += len(s)
109 109
110 110 def delayopener(opener, target, divert, buf):
111 111 def o(name, mode='r'):
112 112 if name != target:
113 113 return opener(name, mode)
114 114 if divert:
115 115 return opener(name + ".a", mode.replace('a', 'w'))
116 116 # otherwise, divert to memory
117 117 return appender(opener(name, mode), buf)
118 118 return o
119 119
120 120 class changelog(revlog.revlog):
121 121 def __init__(self, opener):
122 122 revlog.revlog.__init__(self, opener, "00changelog.i")
123 123 if self._initempty:
124 124 # changelogs don't benefit from generaldelta
125 125 self.version &= ~revlog.REVLOGGENERALDELTA
126 126 self._generaldelta = False
127 127 self._realopener = opener
128 128 self._delayed = False
129 129 self._divert = False
130 130 self.filteredrevs = frozenset()
131 131
132 132 def tip(self):
133 133 """filtered version of revlog.tip"""
134 134 for i in xrange(len(self) -1, -2, -1):
135 135 if i not in self.filteredrevs:
136 136 return self.node(i)
137 137
138 138 def __iter__(self):
139 139 """filtered version of revlog.__iter__"""
140 140 if len(self.filteredrevs) == 0:
141 141 return revlog.revlog.__iter__(self)
142 142
143 143 def filterediter():
144 144 for i in xrange(len(self)):
145 145 if i not in self.filteredrevs:
146 146 yield i
147 147
148 148 return filterediter()
149 149
150 150 def revs(self, start=0, stop=None):
151 151 """filtered version of revlog.revs"""
152 152 for i in super(changelog, self).revs(start, stop):
153 153 if i not in self.filteredrevs:
154 154 yield i
155 155
156 156 @util.propertycache
157 157 def nodemap(self):
158 158 # XXX need filtering too
159 159 self.rev(self.node(0))
160 160 return self._nodecache
161 161
162 162 def hasnode(self, node):
163 163 """filtered version of revlog.hasnode"""
164 164 try:
165 165 i = self.rev(node)
166 166 return i not in self.filteredrevs
167 167 except KeyError:
168 168 return False
169 169
170 170 def headrevs(self):
171 171 if self.filteredrevs:
172 172 # XXX we should fix and use the C version
173 173 return self._headrevs()
174 174 return super(changelog, self).headrevs()
175 175
176 176 def strip(self, *args, **kwargs):
177 177 # XXX make something better than assert
178 178 # We can't expect proper strip behavior if we are filtered.
179 179 assert not self.filteredrevs
180 180 super(changelog, self).strip(*args, **kwargs)
181 181
182 182 def rev(self, node):
183 183 """filtered version of revlog.rev"""
184 184 r = super(changelog, self).rev(node)
185 185 if r in self.filteredrevs:
186 raise error.LookupError(node, self.indexfile, _('no node'))
186 raise error.LookupError(hex(node), self.indexfile, _('no node'))
187 187 return r
188 188
189 189 def node(self, rev):
190 190 """filtered version of revlog.node"""
191 191 if rev in self.filteredrevs:
192 192 raise IndexError(rev)
193 193 return super(changelog, self).node(rev)
194 194
195 195 def linkrev(self, rev):
196 196 """filtered version of revlog.linkrev"""
197 197 if rev in self.filteredrevs:
198 198 raise IndexError(rev)
199 199 return super(changelog, self).linkrev(rev)
200 200
201 201 def parentrevs(self, rev):
202 202 """filtered version of revlog.parentrevs"""
203 203 if rev in self.filteredrevs:
204 204 raise IndexError(rev)
205 205 return super(changelog, self).parentrevs(rev)
206 206
207 207 def flags(self, rev):
208 208 """filtered version of revlog.flags"""
209 209 if rev in self.filteredrevs:
210 210 raise IndexError(rev)
211 211 return super(changelog, self).flags(rev)
212 212
213 213 def delayupdate(self):
214 214 "delay visibility of index updates to other readers"
215 215 self._delayed = True
216 216 self._divert = (len(self) == 0)
217 217 self._delaybuf = []
218 218 self.opener = delayopener(self._realopener, self.indexfile,
219 219 self._divert, self._delaybuf)
220 220
221 221 def finalize(self, tr):
222 222 "finalize index updates"
223 223 self._delayed = False
224 224 self.opener = self._realopener
225 225 # move redirected index data back into place
226 226 if self._divert:
227 227 nfile = self.opener(self.indexfile + ".a")
228 228 n = nfile.name
229 229 nfile.close()
230 230 util.rename(n, n[:-2])
231 231 elif self._delaybuf:
232 232 fp = self.opener(self.indexfile, 'a')
233 233 fp.write("".join(self._delaybuf))
234 234 fp.close()
235 235 self._delaybuf = []
236 236 # split when we're done
237 237 self.checkinlinesize(tr)
238 238
239 239 def readpending(self, file):
240 240 r = revlog.revlog(self.opener, file)
241 241 self.index = r.index
242 242 self.nodemap = r.nodemap
243 243 self._nodecache = r._nodecache
244 244 self._chunkcache = r._chunkcache
245 245
246 246 def writepending(self):
247 247 "create a file containing the unfinalized state for pretxnchangegroup"
248 248 if self._delaybuf:
249 249 # make a temporary copy of the index
250 250 fp1 = self._realopener(self.indexfile)
251 251 fp2 = self._realopener(self.indexfile + ".a", "w")
252 252 fp2.write(fp1.read())
253 253 # add pending data
254 254 fp2.write("".join(self._delaybuf))
255 255 fp2.close()
256 256 # switch modes so finalize can simply rename
257 257 self._delaybuf = []
258 258 self._divert = True
259 259
260 260 if self._divert:
261 261 return True
262 262
263 263 return False
264 264
265 265 def checkinlinesize(self, tr, fp=None):
266 266 if not self._delayed:
267 267 revlog.revlog.checkinlinesize(self, tr, fp)
268 268
269 269 def read(self, node):
270 270 """
271 271 format used:
272 272 nodeid\n : manifest node in ascii
273 273 user\n : user, no \n or \r allowed
274 274 time tz extra\n : date (time is int or float, timezone is int)
275 275 : extra is metadata, encoded and separated by '\0'
276 276 : older versions ignore it
277 277 files\n\n : files modified by the cset, no \n or \r allowed
278 278 (.*) : comment (free text, ideally utf-8)
279 279
280 280 changelog v0 doesn't use extra
281 281 """
282 282 text = self.revision(node)
283 283 if not text:
284 284 return (nullid, "", (0, 0), [], "", _defaultextra)
285 285 last = text.index("\n\n")
286 286 desc = encoding.tolocal(text[last + 2:])
287 287 l = text[:last].split('\n')
288 288 manifest = bin(l[0])
289 289 user = encoding.tolocal(l[1])
290 290
291 291 tdata = l[2].split(' ', 2)
292 292 if len(tdata) != 3:
293 293 time = float(tdata[0])
294 294 try:
295 295 # various tools did silly things with the time zone field.
296 296 timezone = int(tdata[1])
297 297 except ValueError:
298 298 timezone = 0
299 299 extra = _defaultextra
300 300 else:
301 301 time, timezone = float(tdata[0]), int(tdata[1])
302 302 extra = decodeextra(tdata[2])
303 303
304 304 files = l[3:]
305 305 return (manifest, user, (time, timezone), files, desc, extra)
306 306
307 307 def add(self, manifest, files, desc, transaction, p1, p2,
308 308 user, date=None, extra=None):
309 309 # Convert to UTF-8 encoded bytestrings as the very first
310 310 # thing: calling any method on a localstr object will turn it
311 311 # into a str object and the cached UTF-8 string is thus lost.
312 312 user, desc = encoding.fromlocal(user), encoding.fromlocal(desc)
313 313
314 314 user = user.strip()
315 315 # An empty username or a username with a "\n" will make the
316 316 # revision text contain two "\n\n" sequences -> corrupt
317 317 # repository since read cannot unpack the revision.
318 318 if not user:
319 319 raise error.RevlogError(_("empty username"))
320 320 if "\n" in user:
321 321 raise error.RevlogError(_("username %s contains a newline")
322 322 % repr(user))
323 323
324 324 desc = stripdesc(desc)
325 325
326 326 if date:
327 327 parseddate = "%d %d" % util.parsedate(date)
328 328 else:
329 329 parseddate = "%d %d" % util.makedate()
330 330 if extra:
331 331 branch = extra.get("branch")
332 332 if branch in ("default", ""):
333 333 del extra["branch"]
334 334 elif branch in (".", "null", "tip"):
335 335 raise error.RevlogError(_('the name \'%s\' is reserved')
336 336 % branch)
337 337 if extra:
338 338 extra = encodeextra(extra)
339 339 parseddate = "%s %s" % (parseddate, extra)
340 340 l = [hex(manifest), user, parseddate] + sorted(files) + ["", desc]
341 341 text = "\n".join(l)
342 342 return self.addrevision(text, transaction, len(self), p1, p2)
343 343
344 344 def branch(self, rev):
345 345 """return the branch of a revision
346 346
347 347 This function exists because creating a changectx object
348 348 just to access this is costly."""
349 349 return encoding.tolocal(self.read(rev)[5].get("branch"))
General Comments 0
You need to be logged in to leave comments. Login now