##// END OF EJS Templates
repoview: introduce a filter for serving hidden changesets...
marmoute -
r42295:ef0e3cc6 default
parent child Browse files
Show More
@@ -0,0 +1,97 b''
1 ========================================================
2 Test the ability to access a hidden revision on a server
3 ========================================================
4
5 #require serve
6
7 $ . $TESTDIR/testlib/obsmarker-common.sh
8 $ cat >> $HGRCPATH << EOF
9 > [phases]
10 > # public changeset are not obsolete
11 > publish=false
12 > [experimental]
13 > evolution=all
14 > [ui]
15 > logtemplate='{rev}:{node|short} {desc} [{phase}]\n'
16 > EOF
17
18 Setup a simple repository with some hidden revisions
19 ----------------------------------------------------
20
21 Testing the `served.hidden` view
22
23 $ hg init repo-with-hidden
24 $ cd repo-with-hidden
25
26 $ echo 0 > a
27 $ hg ci -qAm "c_Public"
28 $ hg phase --public
29 $ echo 1 > a
30 $ hg ci -m "c_Amend_Old"
31 $ echo 2 > a
32 $ hg ci -m "c_Amend_New" --amend
33 $ hg up ".^"
34 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
35 $ echo 3 > a
36 $ hg ci -m "c_Pruned"
37 created new head
38 $ hg debugobsolete --record-parents `getid 'desc("c_Pruned")'` -d '0 0'
39 obsoleted 1 changesets
40 $ hg up ".^"
41 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
42 $ echo 4 > a
43 $ hg ci -m "c_Secret" --secret
44 created new head
45 $ echo 5 > a
46 $ hg ci -m "c_Secret_Pruned" --secret
47 $ hg debugobsolete --record-parents `getid 'desc("c_Secret_Pruned")'` -d '0 0'
48 obsoleted 1 changesets
49 $ hg up null
50 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
51
52 $ hg log -G -T '{rev}:{node|short} {desc} [{phase}]\n' --hidden
53 x 5:8d28cbe335f3 c_Secret_Pruned [secret]
54 |
55 o 4:1c6afd79eb66 c_Secret [secret]
56 |
57 | x 3:5d1575e42c25 c_Pruned [draft]
58 |/
59 | o 2:c33affeb3f6b c_Amend_New [draft]
60 |/
61 | x 1:be215fbb8c50 c_Amend_Old [draft]
62 |/
63 o 0:5f354f46e585 c_Public [public]
64
65 $ hg debugobsolete
66 be215fbb8c5090028b00154c1fe877ad1b376c61 c33affeb3f6b4e9621d1839d6175ddc07708807c 0 (Thu Jan 01 00:00:00 1970 +0000) {'ef1': '9', 'operation': 'amend', 'user': 'test'}
67 5d1575e42c25b7f2db75cd4e0b881b1c35158fae 0 {5f354f46e5853535841ec7a128423e991ca4d59b} (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
68 8d28cbe335f311bc89332d7bbe8a07889b6914a0 0 {1c6afd79eb6663275bbe30097e162b1c24ced0f0} (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
69
70 $ cd ..
71
72 Test the feature
73 ================
74
75 Check that the `served.hidden` repoview
76 ---------------------------------------
77
78 $ hg -R repo-with-hidden serve -p $HGPORT -d --pid-file hg.pid --config web.view=served.hidden
79 $ cat hg.pid >> $DAEMON_PIDS
80
81 changesets in secret and higher phases are not visible through hgweb
82
83 $ hg -R repo-with-hidden log --template "revision: {rev}\\n" --rev "reverse(not secret())"
84 revision: 2
85 revision: 0
86 $ hg -R repo-with-hidden log --template "revision: {rev}\\n" --rev "reverse(not secret())" --hidden
87 revision: 3
88 revision: 2
89 revision: 1
90 revision: 0
91 $ get-with-headers.py localhost:$HGPORT 'log?style=raw' | grep revision:
92 revision: 3
93 revision: 2
94 revision: 1
95 revision: 0
96
97 $ killdaemons.py
@@ -1,668 +1,669 b''
1 1 # branchmap.py - logic to computes, maintain and stores branchmap for local repo
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 __future__ import absolute_import
9 9
10 10 import struct
11 11
12 12 from .node import (
13 13 bin,
14 14 hex,
15 15 nullid,
16 16 nullrev,
17 17 )
18 18 from . import (
19 19 encoding,
20 20 error,
21 21 pycompat,
22 22 scmutil,
23 23 util,
24 24 )
25 25 from .utils import (
26 26 stringutil,
27 27 )
28 28
29 29 calcsize = struct.calcsize
30 30 pack_into = struct.pack_into
31 31 unpack_from = struct.unpack_from
32 32
33 33
34 34 ### Nearest subset relation
35 35 # Nearest subset of filter X is a filter Y so that:
36 36 # * Y is included in X,
37 37 # * X - Y is as small as possible.
38 38 # This create and ordering used for branchmap purpose.
39 39 # the ordering may be partial
40 40 subsettable = {None: 'visible',
41 41 'visible-hidden': 'visible',
42 42 'visible': 'served',
43 'served.hidden': 'served',
43 44 'served': 'immutable',
44 45 'immutable': 'base'}
45 46
46 47
47 48 class BranchMapCache(object):
48 49 """mapping of filtered views of repo with their branchcache"""
49 50 def __init__(self):
50 51 self._per_filter = {}
51 52
52 53 def __getitem__(self, repo):
53 54 self.updatecache(repo)
54 55 return self._per_filter[repo.filtername]
55 56
56 57 def updatecache(self, repo):
57 58 """Update the cache for the given filtered view on a repository"""
58 59 # This can trigger updates for the caches for subsets of the filtered
59 60 # view, e.g. when there is no cache for this filtered view or the cache
60 61 # is stale.
61 62
62 63 cl = repo.changelog
63 64 filtername = repo.filtername
64 65 bcache = self._per_filter.get(filtername)
65 66 if bcache is None or not bcache.validfor(repo):
66 67 # cache object missing or cache object stale? Read from disk
67 68 bcache = branchcache.fromfile(repo)
68 69
69 70 revs = []
70 71 if bcache is None:
71 72 # no (fresh) cache available anymore, perhaps we can re-use
72 73 # the cache for a subset, then extend that to add info on missing
73 74 # revisions.
74 75 subsetname = subsettable.get(filtername)
75 76 if subsetname is not None:
76 77 subset = repo.filtered(subsetname)
77 78 bcache = self[subset].copy()
78 79 extrarevs = subset.changelog.filteredrevs - cl.filteredrevs
79 80 revs.extend(r for r in extrarevs if r <= bcache.tiprev)
80 81 else:
81 82 # nothing to fall back on, start empty.
82 83 bcache = branchcache()
83 84
84 85 revs.extend(cl.revs(start=bcache.tiprev + 1))
85 86 if revs:
86 87 bcache.update(repo, revs)
87 88
88 89 assert bcache.validfor(repo), filtername
89 90 self._per_filter[repo.filtername] = bcache
90 91
91 92 def replace(self, repo, remotebranchmap):
92 93 """Replace the branchmap cache for a repo with a branch mapping.
93 94
94 95 This is likely only called during clone with a branch map from a
95 96 remote.
96 97
97 98 """
98 99 cl = repo.changelog
99 100 clrev = cl.rev
100 101 clbranchinfo = cl.branchinfo
101 102 rbheads = []
102 103 closed = []
103 104 for bheads in remotebranchmap.itervalues():
104 105 rbheads += bheads
105 106 for h in bheads:
106 107 r = clrev(h)
107 108 b, c = clbranchinfo(r)
108 109 if c:
109 110 closed.append(h)
110 111
111 112 if rbheads:
112 113 rtiprev = max((int(clrev(node)) for node in rbheads))
113 114 cache = branchcache(
114 115 remotebranchmap, repo[rtiprev].node(), rtiprev,
115 116 closednodes=closed)
116 117
117 118 # Try to stick it as low as possible
118 119 # filter above served are unlikely to be fetch from a clone
119 120 for candidate in ('base', 'immutable', 'served'):
120 121 rview = repo.filtered(candidate)
121 122 if cache.validfor(rview):
122 123 self._per_filter[candidate] = cache
123 124 cache.write(rview)
124 125 return
125 126
126 127 def clear(self):
127 128 self._per_filter.clear()
128 129
129 130 def _unknownnode(node):
130 131 """ raises ValueError when branchcache found a node which does not exists
131 132 """
132 133 raise ValueError(r'node %s does not exist' % pycompat.sysstr(hex(node)))
133 134
134 135 class branchcache(object):
135 136 """A dict like object that hold branches heads cache.
136 137
137 138 This cache is used to avoid costly computations to determine all the
138 139 branch heads of a repo.
139 140
140 141 The cache is serialized on disk in the following format:
141 142
142 143 <tip hex node> <tip rev number> [optional filtered repo hex hash]
143 144 <branch head hex node> <open/closed state> <branch name>
144 145 <branch head hex node> <open/closed state> <branch name>
145 146 ...
146 147
147 148 The first line is used to check if the cache is still valid. If the
148 149 branch cache is for a filtered repo view, an optional third hash is
149 150 included that hashes the hashes of all filtered revisions.
150 151
151 152 The open/closed state is represented by a single letter 'o' or 'c'.
152 153 This field can be used to avoid changelog reads when determining if a
153 154 branch head closes a branch or not.
154 155 """
155 156
156 157 def __init__(self, entries=(), tipnode=nullid, tiprev=nullrev,
157 158 filteredhash=None, closednodes=None, hasnode=None):
158 159 """ hasnode is a function which can be used to verify whether changelog
159 160 has a given node or not. If it's not provided, we assume that every node
160 161 we have exists in changelog """
161 162 self.tipnode = tipnode
162 163 self.tiprev = tiprev
163 164 self.filteredhash = filteredhash
164 165 # closednodes is a set of nodes that close their branch. If the branch
165 166 # cache has been updated, it may contain nodes that are no longer
166 167 # heads.
167 168 if closednodes is None:
168 169 self._closednodes = set()
169 170 else:
170 171 self._closednodes = closednodes
171 172 self._entries = dict(entries)
172 173 # whether closed nodes are verified or not
173 174 self._closedverified = False
174 175 # branches for which nodes are verified
175 176 self._verifiedbranches = set()
176 177 self._hasnode = hasnode
177 178 if self._hasnode is None:
178 179 self._hasnode = lambda x: True
179 180
180 181 def _verifyclosed(self):
181 182 """ verify the closed nodes we have """
182 183 if self._closedverified:
183 184 return
184 185 for node in self._closednodes:
185 186 if not self._hasnode(node):
186 187 _unknownnode(node)
187 188
188 189 self._closedverified = True
189 190
190 191 def _verifybranch(self, branch):
191 192 """ verify head nodes for the given branch. If branch is None, verify
192 193 for all the branches """
193 194 if branch not in self._entries or branch in self._verifiedbranches:
194 195 return
195 196 for n in self._entries[branch]:
196 197 if not self._hasnode(n):
197 198 _unknownnode(n)
198 199
199 200 self._verifiedbranches.add(branch)
200 201
201 202 def _verifyall(self):
202 203 """ verifies nodes of all the branches """
203 204 for b in self._entries:
204 205 self._verifybranch(b)
205 206
206 207 def __iter__(self):
207 208 return iter(self._entries)
208 209
209 210 def __setitem__(self, key, value):
210 211 self._entries[key] = value
211 212
212 213 def __getitem__(self, key):
213 214 self._verifybranch(key)
214 215 return self._entries[key]
215 216
216 217 def __contains__(self, key):
217 218 self._verifybranch(key)
218 219 return key in self._entries
219 220
220 221 def iteritems(self):
221 222 self._verifyall()
222 223 return self._entries.iteritems()
223 224
224 225 def hasbranch(self, label):
225 226 """ checks whether a branch of this name exists or not """
226 227 self._verifybranch(label)
227 228 return label in self._entries
228 229
229 230 @classmethod
230 231 def fromfile(cls, repo):
231 232 f = None
232 233 try:
233 234 f = repo.cachevfs(cls._filename(repo))
234 235 lineiter = iter(f)
235 236 cachekey = next(lineiter).rstrip('\n').split(" ", 2)
236 237 last, lrev = cachekey[:2]
237 238 last, lrev = bin(last), int(lrev)
238 239 filteredhash = None
239 240 hasnode = repo.changelog.hasnode
240 241 if len(cachekey) > 2:
241 242 filteredhash = bin(cachekey[2])
242 243 bcache = cls(tipnode=last, tiprev=lrev, filteredhash=filteredhash,
243 244 hasnode=hasnode)
244 245 if not bcache.validfor(repo):
245 246 # invalidate the cache
246 247 raise ValueError(r'tip differs')
247 248 bcache.load(repo, lineiter)
248 249 except (IOError, OSError):
249 250 return None
250 251
251 252 except Exception as inst:
252 253 if repo.ui.debugflag:
253 254 msg = 'invalid branchheads cache'
254 255 if repo.filtername is not None:
255 256 msg += ' (%s)' % repo.filtername
256 257 msg += ': %s\n'
257 258 repo.ui.debug(msg % pycompat.bytestr(inst))
258 259 bcache = None
259 260
260 261 finally:
261 262 if f:
262 263 f.close()
263 264
264 265 return bcache
265 266
266 267 def load(self, repo, lineiter):
267 268 """ fully loads the branchcache by reading from the file using the line
268 269 iterator passed"""
269 270 for line in lineiter:
270 271 line = line.rstrip('\n')
271 272 if not line:
272 273 continue
273 274 node, state, label = line.split(" ", 2)
274 275 if state not in 'oc':
275 276 raise ValueError(r'invalid branch state')
276 277 label = encoding.tolocal(label.strip())
277 278 node = bin(node)
278 279 self._entries.setdefault(label, []).append(node)
279 280 if state == 'c':
280 281 self._closednodes.add(node)
281 282
282 283 @staticmethod
283 284 def _filename(repo):
284 285 """name of a branchcache file for a given repo or repoview"""
285 286 filename = "branch2"
286 287 if repo.filtername:
287 288 filename = '%s-%s' % (filename, repo.filtername)
288 289 return filename
289 290
290 291 def validfor(self, repo):
291 292 """Is the cache content valid regarding a repo
292 293
293 294 - False when cached tipnode is unknown or if we detect a strip.
294 295 - True when cache is up to date or a subset of current repo."""
295 296 try:
296 297 return ((self.tipnode == repo.changelog.node(self.tiprev))
297 298 and (self.filteredhash ==
298 299 scmutil.filteredhash(repo, self.tiprev)))
299 300 except IndexError:
300 301 return False
301 302
302 303 def _branchtip(self, heads):
303 304 '''Return tuple with last open head in heads and false,
304 305 otherwise return last closed head and true.'''
305 306 tip = heads[-1]
306 307 closed = True
307 308 for h in reversed(heads):
308 309 if h not in self._closednodes:
309 310 tip = h
310 311 closed = False
311 312 break
312 313 return tip, closed
313 314
314 315 def branchtip(self, branch):
315 316 '''Return the tipmost open head on branch head, otherwise return the
316 317 tipmost closed head on branch.
317 318 Raise KeyError for unknown branch.'''
318 319 return self._branchtip(self[branch])[0]
319 320
320 321 def iteropen(self, nodes):
321 322 return (n for n in nodes if n not in self._closednodes)
322 323
323 324 def branchheads(self, branch, closed=False):
324 325 self._verifybranch(branch)
325 326 heads = self._entries[branch]
326 327 if not closed:
327 328 heads = list(self.iteropen(heads))
328 329 return heads
329 330
330 331 def iterbranches(self):
331 332 for bn, heads in self.iteritems():
332 333 yield (bn, heads) + self._branchtip(heads)
333 334
334 335 def iterheads(self):
335 336 """ returns all the heads """
336 337 self._verifyall()
337 338 return self._entries.itervalues()
338 339
339 340 def copy(self):
340 341 """return an deep copy of the branchcache object"""
341 342 self._verifyall()
342 343 return type(self)(
343 344 self._entries, self.tipnode, self.tiprev, self.filteredhash,
344 345 self._closednodes)
345 346
346 347 def write(self, repo):
347 348 try:
348 349 f = repo.cachevfs(self._filename(repo), "w", atomictemp=True)
349 350 cachekey = [hex(self.tipnode), '%d' % self.tiprev]
350 351 if self.filteredhash is not None:
351 352 cachekey.append(hex(self.filteredhash))
352 353 f.write(" ".join(cachekey) + '\n')
353 354 nodecount = 0
354 355 for label, nodes in sorted(self.iteritems()):
355 356 label = encoding.fromlocal(label)
356 357 for node in nodes:
357 358 nodecount += 1
358 359 if node in self._closednodes:
359 360 state = 'c'
360 361 else:
361 362 state = 'o'
362 363 f.write("%s %s %s\n" % (hex(node), state, label))
363 364 f.close()
364 365 repo.ui.log('branchcache',
365 366 'wrote %s branch cache with %d labels and %d nodes\n',
366 367 repo.filtername, len(self._entries), nodecount)
367 368 except (IOError, OSError, error.Abort) as inst:
368 369 # Abort may be raised by read only opener, so log and continue
369 370 repo.ui.debug("couldn't write branch cache: %s\n" %
370 371 stringutil.forcebytestr(inst))
371 372
372 373 def update(self, repo, revgen):
373 374 """Given a branchhead cache, self, that may have extra nodes or be
374 375 missing heads, and a generator of nodes that are strictly a superset of
375 376 heads missing, this function updates self to be correct.
376 377 """
377 378 starttime = util.timer()
378 379 cl = repo.changelog
379 380 # collect new branch entries
380 381 newbranches = {}
381 382 getbranchinfo = repo.revbranchcache().branchinfo
382 383 for r in revgen:
383 384 branch, closesbranch = getbranchinfo(r)
384 385 newbranches.setdefault(branch, []).append(r)
385 386 if closesbranch:
386 387 self._closednodes.add(cl.node(r))
387 388
388 389 # fetch current topological heads to speed up filtering
389 390 topoheads = set(cl.headrevs())
390 391
391 392 # if older branchheads are reachable from new ones, they aren't
392 393 # really branchheads. Note checking parents is insufficient:
393 394 # 1 (branch a) -> 2 (branch b) -> 3 (branch a)
394 395 for branch, newheadrevs in newbranches.iteritems():
395 396 bheads = self._entries.setdefault(branch, [])
396 397 bheadset = set(cl.rev(node) for node in bheads)
397 398
398 399 # This have been tested True on all internal usage of this function.
399 400 # run it again in case of doubt
400 401 # assert not (set(bheadrevs) & set(newheadrevs))
401 402 bheadset.update(newheadrevs)
402 403
403 404 # This prunes out two kinds of heads - heads that are superseded by
404 405 # a head in newheadrevs, and newheadrevs that are not heads because
405 406 # an existing head is their descendant.
406 407 uncertain = bheadset - topoheads
407 408 if uncertain:
408 409 floorrev = min(uncertain)
409 410 ancestors = set(cl.ancestors(newheadrevs, floorrev))
410 411 bheadset -= ancestors
411 412 bheadrevs = sorted(bheadset)
412 413 self[branch] = [cl.node(rev) for rev in bheadrevs]
413 414 tiprev = bheadrevs[-1]
414 415 if tiprev > self.tiprev:
415 416 self.tipnode = cl.node(tiprev)
416 417 self.tiprev = tiprev
417 418
418 419 if not self.validfor(repo):
419 420 # cache key are not valid anymore
420 421 self.tipnode = nullid
421 422 self.tiprev = nullrev
422 423 for heads in self.iterheads():
423 424 tiprev = max(cl.rev(node) for node in heads)
424 425 if tiprev > self.tiprev:
425 426 self.tipnode = cl.node(tiprev)
426 427 self.tiprev = tiprev
427 428 self.filteredhash = scmutil.filteredhash(repo, self.tiprev)
428 429
429 430 duration = util.timer() - starttime
430 431 repo.ui.log('branchcache', 'updated %s branch cache in %.4f seconds\n',
431 432 repo.filtername or b'None', duration)
432 433
433 434 self.write(repo)
434 435
435 436
436 437 class remotebranchcache(branchcache):
437 438 """Branchmap info for a remote connection, should not write locally"""
438 439 def write(self, repo):
439 440 pass
440 441
441 442
442 443 # Revision branch info cache
443 444
444 445 _rbcversion = '-v1'
445 446 _rbcnames = 'rbc-names' + _rbcversion
446 447 _rbcrevs = 'rbc-revs' + _rbcversion
447 448 # [4 byte hash prefix][4 byte branch name number with sign bit indicating open]
448 449 _rbcrecfmt = '>4sI'
449 450 _rbcrecsize = calcsize(_rbcrecfmt)
450 451 _rbcnodelen = 4
451 452 _rbcbranchidxmask = 0x7fffffff
452 453 _rbccloseflag = 0x80000000
453 454
454 455 class revbranchcache(object):
455 456 """Persistent cache, mapping from revision number to branch name and close.
456 457 This is a low level cache, independent of filtering.
457 458
458 459 Branch names are stored in rbc-names in internal encoding separated by 0.
459 460 rbc-names is append-only, and each branch name is only stored once and will
460 461 thus have a unique index.
461 462
462 463 The branch info for each revision is stored in rbc-revs as constant size
463 464 records. The whole file is read into memory, but it is only 'parsed' on
464 465 demand. The file is usually append-only but will be truncated if repo
465 466 modification is detected.
466 467 The record for each revision contains the first 4 bytes of the
467 468 corresponding node hash, and the record is only used if it still matches.
468 469 Even a completely trashed rbc-revs fill thus still give the right result
469 470 while converging towards full recovery ... assuming no incorrectly matching
470 471 node hashes.
471 472 The record also contains 4 bytes where 31 bits contains the index of the
472 473 branch and the last bit indicate that it is a branch close commit.
473 474 The usage pattern for rbc-revs is thus somewhat similar to 00changelog.i
474 475 and will grow with it but be 1/8th of its size.
475 476 """
476 477
477 478 def __init__(self, repo, readonly=True):
478 479 assert repo.filtername is None
479 480 self._repo = repo
480 481 self._names = [] # branch names in local encoding with static index
481 482 self._rbcrevs = bytearray()
482 483 self._rbcsnameslen = 0 # length of names read at _rbcsnameslen
483 484 try:
484 485 bndata = repo.cachevfs.read(_rbcnames)
485 486 self._rbcsnameslen = len(bndata) # for verification before writing
486 487 if bndata:
487 488 self._names = [encoding.tolocal(bn)
488 489 for bn in bndata.split('\0')]
489 490 except (IOError, OSError):
490 491 if readonly:
491 492 # don't try to use cache - fall back to the slow path
492 493 self.branchinfo = self._branchinfo
493 494
494 495 if self._names:
495 496 try:
496 497 data = repo.cachevfs.read(_rbcrevs)
497 498 self._rbcrevs[:] = data
498 499 except (IOError, OSError) as inst:
499 500 repo.ui.debug("couldn't read revision branch cache: %s\n" %
500 501 stringutil.forcebytestr(inst))
501 502 # remember number of good records on disk
502 503 self._rbcrevslen = min(len(self._rbcrevs) // _rbcrecsize,
503 504 len(repo.changelog))
504 505 if self._rbcrevslen == 0:
505 506 self._names = []
506 507 self._rbcnamescount = len(self._names) # number of names read at
507 508 # _rbcsnameslen
508 509
509 510 def _clear(self):
510 511 self._rbcsnameslen = 0
511 512 del self._names[:]
512 513 self._rbcnamescount = 0
513 514 self._rbcrevslen = len(self._repo.changelog)
514 515 self._rbcrevs = bytearray(self._rbcrevslen * _rbcrecsize)
515 516 util.clearcachedproperty(self, '_namesreverse')
516 517
517 518 @util.propertycache
518 519 def _namesreverse(self):
519 520 return dict((b, r) for r, b in enumerate(self._names))
520 521
521 522 def branchinfo(self, rev):
522 523 """Return branch name and close flag for rev, using and updating
523 524 persistent cache."""
524 525 changelog = self._repo.changelog
525 526 rbcrevidx = rev * _rbcrecsize
526 527
527 528 # avoid negative index, changelog.read(nullrev) is fast without cache
528 529 if rev == nullrev:
529 530 return changelog.branchinfo(rev)
530 531
531 532 # if requested rev isn't allocated, grow and cache the rev info
532 533 if len(self._rbcrevs) < rbcrevidx + _rbcrecsize:
533 534 return self._branchinfo(rev)
534 535
535 536 # fast path: extract data from cache, use it if node is matching
536 537 reponode = changelog.node(rev)[:_rbcnodelen]
537 538 cachenode, branchidx = unpack_from(
538 539 _rbcrecfmt, util.buffer(self._rbcrevs), rbcrevidx)
539 540 close = bool(branchidx & _rbccloseflag)
540 541 if close:
541 542 branchidx &= _rbcbranchidxmask
542 543 if cachenode == '\0\0\0\0':
543 544 pass
544 545 elif cachenode == reponode:
545 546 try:
546 547 return self._names[branchidx], close
547 548 except IndexError:
548 549 # recover from invalid reference to unknown branch
549 550 self._repo.ui.debug("referenced branch names not found"
550 551 " - rebuilding revision branch cache from scratch\n")
551 552 self._clear()
552 553 else:
553 554 # rev/node map has changed, invalidate the cache from here up
554 555 self._repo.ui.debug("history modification detected - truncating "
555 556 "revision branch cache to revision %d\n" % rev)
556 557 truncate = rbcrevidx + _rbcrecsize
557 558 del self._rbcrevs[truncate:]
558 559 self._rbcrevslen = min(self._rbcrevslen, truncate)
559 560
560 561 # fall back to slow path and make sure it will be written to disk
561 562 return self._branchinfo(rev)
562 563
563 564 def _branchinfo(self, rev):
564 565 """Retrieve branch info from changelog and update _rbcrevs"""
565 566 changelog = self._repo.changelog
566 567 b, close = changelog.branchinfo(rev)
567 568 if b in self._namesreverse:
568 569 branchidx = self._namesreverse[b]
569 570 else:
570 571 branchidx = len(self._names)
571 572 self._names.append(b)
572 573 self._namesreverse[b] = branchidx
573 574 reponode = changelog.node(rev)
574 575 if close:
575 576 branchidx |= _rbccloseflag
576 577 self._setcachedata(rev, reponode, branchidx)
577 578 return b, close
578 579
579 580 def setdata(self, branch, rev, node, close):
580 581 """add new data information to the cache"""
581 582 if branch in self._namesreverse:
582 583 branchidx = self._namesreverse[branch]
583 584 else:
584 585 branchidx = len(self._names)
585 586 self._names.append(branch)
586 587 self._namesreverse[branch] = branchidx
587 588 if close:
588 589 branchidx |= _rbccloseflag
589 590 self._setcachedata(rev, node, branchidx)
590 591 # If no cache data were readable (non exists, bad permission, etc)
591 592 # the cache was bypassing itself by setting:
592 593 #
593 594 # self.branchinfo = self._branchinfo
594 595 #
595 596 # Since we now have data in the cache, we need to drop this bypassing.
596 597 if r'branchinfo' in vars(self):
597 598 del self.branchinfo
598 599
599 600 def _setcachedata(self, rev, node, branchidx):
600 601 """Writes the node's branch data to the in-memory cache data."""
601 602 if rev == nullrev:
602 603 return
603 604 rbcrevidx = rev * _rbcrecsize
604 605 if len(self._rbcrevs) < rbcrevidx + _rbcrecsize:
605 606 self._rbcrevs.extend('\0' *
606 607 (len(self._repo.changelog) * _rbcrecsize -
607 608 len(self._rbcrevs)))
608 609 pack_into(_rbcrecfmt, self._rbcrevs, rbcrevidx, node, branchidx)
609 610 self._rbcrevslen = min(self._rbcrevslen, rev)
610 611
611 612 tr = self._repo.currenttransaction()
612 613 if tr:
613 614 tr.addfinalize('write-revbranchcache', self.write)
614 615
615 616 def write(self, tr=None):
616 617 """Save branch cache if it is dirty."""
617 618 repo = self._repo
618 619 wlock = None
619 620 step = ''
620 621 try:
621 622 if self._rbcnamescount < len(self._names):
622 623 step = ' names'
623 624 wlock = repo.wlock(wait=False)
624 625 if self._rbcnamescount != 0:
625 626 f = repo.cachevfs.open(_rbcnames, 'ab')
626 627 if f.tell() == self._rbcsnameslen:
627 628 f.write('\0')
628 629 else:
629 630 f.close()
630 631 repo.ui.debug("%s changed - rewriting it\n" % _rbcnames)
631 632 self._rbcnamescount = 0
632 633 self._rbcrevslen = 0
633 634 if self._rbcnamescount == 0:
634 635 # before rewriting names, make sure references are removed
635 636 repo.cachevfs.unlinkpath(_rbcrevs, ignoremissing=True)
636 637 f = repo.cachevfs.open(_rbcnames, 'wb')
637 638 f.write('\0'.join(encoding.fromlocal(b)
638 639 for b in self._names[self._rbcnamescount:]))
639 640 self._rbcsnameslen = f.tell()
640 641 f.close()
641 642 self._rbcnamescount = len(self._names)
642 643
643 644 start = self._rbcrevslen * _rbcrecsize
644 645 if start != len(self._rbcrevs):
645 646 step = ''
646 647 if wlock is None:
647 648 wlock = repo.wlock(wait=False)
648 649 revs = min(len(repo.changelog),
649 650 len(self._rbcrevs) // _rbcrecsize)
650 651 f = repo.cachevfs.open(_rbcrevs, 'ab')
651 652 if f.tell() != start:
652 653 repo.ui.debug("truncating cache/%s to %d\n"
653 654 % (_rbcrevs, start))
654 655 f.seek(start)
655 656 if f.tell() != start:
656 657 start = 0
657 658 f.seek(start)
658 659 f.truncate()
659 660 end = revs * _rbcrecsize
660 661 f.write(self._rbcrevs[start:end])
661 662 f.close()
662 663 self._rbcrevslen = revs
663 664 except (IOError, OSError, error.Abort, error.LockError) as inst:
664 665 repo.ui.debug("couldn't write revision branch cache%s: %s\n"
665 666 % (step, stringutil.forcebytestr(inst)))
666 667 finally:
667 668 if wlock is not None:
668 669 wlock.release()
@@ -1,271 +1,280 b''
1 1 # repoview.py - Filtered view of a localrepo object
2 2 #
3 3 # Copyright 2012 Pierre-Yves David <pierre-yves.david@ens-lyon.org>
4 4 # Logilab SA <contact@logilab.fr>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 from __future__ import absolute_import
10 10
11 11 import copy
12 12 import weakref
13 13
14 14 from .node import nullrev
15 15 from . import (
16 16 obsolete,
17 17 phases,
18 18 pycompat,
19 19 tags as tagsmod,
20 20 )
21 21
22 22 def hideablerevs(repo):
23 23 """Revision candidates to be hidden
24 24
25 25 This is a standalone function to allow extensions to wrap it.
26 26
27 27 Because we use the set of immutable changesets as a fallback subset in
28 28 branchmap (see mercurial.branchmap.subsettable), you cannot set "public"
29 29 changesets as "hideable". Doing so would break multiple code assertions and
30 30 lead to crashes."""
31 31 obsoletes = obsolete.getrevs(repo, 'obsolete')
32 32 internals = repo._phasecache.getrevset(repo, phases.localhiddenphases)
33 33 internals = frozenset(internals)
34 34 return obsoletes | internals
35 35
36 36 def pinnedrevs(repo):
37 37 """revisions blocking hidden changesets from being filtered
38 38 """
39 39
40 40 cl = repo.changelog
41 41 pinned = set()
42 42 pinned.update([par.rev() for par in repo[None].parents()])
43 43 pinned.update([cl.rev(bm) for bm in repo._bookmarks.values()])
44 44
45 45 tags = {}
46 46 tagsmod.readlocaltags(repo.ui, repo, tags, {})
47 47 if tags:
48 48 rev, nodemap = cl.rev, cl.nodemap
49 49 pinned.update(rev(t[0]) for t in tags.values() if t[0] in nodemap)
50 50 return pinned
51 51
52 52
53 53 def _revealancestors(pfunc, hidden, revs):
54 54 """reveals contiguous chains of hidden ancestors of 'revs' by removing them
55 55 from 'hidden'
56 56
57 57 - pfunc(r): a funtion returning parent of 'r',
58 58 - hidden: the (preliminary) hidden revisions, to be updated
59 59 - revs: iterable of revnum,
60 60
61 61 (Ancestors are revealed exclusively, i.e. the elements in 'revs' are
62 62 *not* revealed)
63 63 """
64 64 stack = list(revs)
65 65 while stack:
66 66 for p in pfunc(stack.pop()):
67 67 if p != nullrev and p in hidden:
68 68 hidden.remove(p)
69 69 stack.append(p)
70 70
71 71 def computehidden(repo, visibilityexceptions=None):
72 72 """compute the set of hidden revision to filter
73 73
74 74 During most operation hidden should be filtered."""
75 75 assert not repo.changelog.filteredrevs
76 76
77 77 hidden = hideablerevs(repo)
78 78 if hidden:
79 79 hidden = set(hidden - pinnedrevs(repo))
80 80 if visibilityexceptions:
81 81 hidden -= visibilityexceptions
82 82 pfunc = repo.changelog.parentrevs
83 83 mutable = repo._phasecache.getrevset(repo, phases.mutablephases)
84 84
85 85 visible = mutable - hidden
86 86 _revealancestors(pfunc, hidden, visible)
87 87 return frozenset(hidden)
88 88
89 def computesecret(repo, visibilityexceptions=None):
90 """compute the set of revision that can never be exposed through hgweb
91
92 Changeset in the secret phase (or above) should stay unaccessible."""
93 assert not repo.changelog.filteredrevs
94 secrets = repo._phasecache.getrevset(repo, phases.remotehiddenphases)
95 return frozenset(secrets)
96
89 97 def computeunserved(repo, visibilityexceptions=None):
90 98 """compute the set of revision that should be filtered when used a server
91 99
92 100 Secret and hidden changeset should not pretend to be here."""
93 101 assert not repo.changelog.filteredrevs
94 102 # fast path in simple case to avoid impact of non optimised code
95 103 hiddens = filterrevs(repo, 'visible')
96 secrets = repo._phasecache.getrevset(repo, phases.remotehiddenphases)
104 secrets = filterrevs(repo, 'served.hidden')
97 105 if secrets:
98 return frozenset(hiddens | frozenset(secrets))
106 return frozenset(hiddens | secrets)
99 107 else:
100 108 return hiddens
101 109
102 110 def computemutable(repo, visibilityexceptions=None):
103 111 assert not repo.changelog.filteredrevs
104 112 # fast check to avoid revset call on huge repo
105 113 if any(repo._phasecache.phaseroots[1:]):
106 114 getphase = repo._phasecache.phase
107 115 maymutable = filterrevs(repo, 'base')
108 116 return frozenset(r for r in maymutable if getphase(repo, r))
109 117 return frozenset()
110 118
111 119 def computeimpactable(repo, visibilityexceptions=None):
112 120 """Everything impactable by mutable revision
113 121
114 122 The immutable filter still have some chance to get invalidated. This will
115 123 happen when:
116 124
117 125 - you garbage collect hidden changeset,
118 126 - public phase is moved backward,
119 127 - something is changed in the filtering (this could be fixed)
120 128
121 129 This filter out any mutable changeset and any public changeset that may be
122 130 impacted by something happening to a mutable revision.
123 131
124 132 This is achieved by filtered everything with a revision number egal or
125 133 higher than the first mutable changeset is filtered."""
126 134 assert not repo.changelog.filteredrevs
127 135 cl = repo.changelog
128 136 firstmutable = len(cl)
129 137 for roots in repo._phasecache.phaseroots[1:]:
130 138 if roots:
131 139 firstmutable = min(firstmutable, min(cl.rev(r) for r in roots))
132 140 # protect from nullrev root
133 141 firstmutable = max(0, firstmutable)
134 142 return frozenset(pycompat.xrange(firstmutable, len(cl)))
135 143
136 144 # function to compute filtered set
137 145 #
138 146 # When adding a new filter you MUST update the table at:
139 147 # mercurial.branchmap.subsettable
140 148 # Otherwise your filter will have to recompute all its branches cache
141 149 # from scratch (very slow).
142 150 filtertable = {'visible': computehidden,
143 151 'visible-hidden': computehidden,
152 'served.hidden': computesecret,
144 153 'served': computeunserved,
145 154 'immutable': computemutable,
146 155 'base': computeimpactable}
147 156
148 157 def filterrevs(repo, filtername, visibilityexceptions=None):
149 158 """returns set of filtered revision for this filter name
150 159
151 160 visibilityexceptions is a set of revs which must are exceptions for
152 161 hidden-state and must be visible. They are dynamic and hence we should not
153 162 cache it's result"""
154 163 if filtername not in repo.filteredrevcache:
155 164 func = filtertable[filtername]
156 165 if visibilityexceptions:
157 166 return func(repo.unfiltered, visibilityexceptions)
158 167 repo.filteredrevcache[filtername] = func(repo.unfiltered())
159 168 return repo.filteredrevcache[filtername]
160 169
161 170 class repoview(object):
162 171 """Provide a read/write view of a repo through a filtered changelog
163 172
164 173 This object is used to access a filtered version of a repository without
165 174 altering the original repository object itself. We can not alter the
166 175 original object for two main reasons:
167 176 - It prevents the use of a repo with multiple filters at the same time. In
168 177 particular when multiple threads are involved.
169 178 - It makes scope of the filtering harder to control.
170 179
171 180 This object behaves very closely to the original repository. All attribute
172 181 operations are done on the original repository:
173 182 - An access to `repoview.someattr` actually returns `repo.someattr`,
174 183 - A write to `repoview.someattr` actually sets value of `repo.someattr`,
175 184 - A deletion of `repoview.someattr` actually drops `someattr`
176 185 from `repo.__dict__`.
177 186
178 187 The only exception is the `changelog` property. It is overridden to return
179 188 a (surface) copy of `repo.changelog` with some revisions filtered. The
180 189 `filtername` attribute of the view control the revisions that need to be
181 190 filtered. (the fact the changelog is copied is an implementation detail).
182 191
183 192 Unlike attributes, this object intercepts all method calls. This means that
184 193 all methods are run on the `repoview` object with the filtered `changelog`
185 194 property. For this purpose the simple `repoview` class must be mixed with
186 195 the actual class of the repository. This ensures that the resulting
187 196 `repoview` object have the very same methods than the repo object. This
188 197 leads to the property below.
189 198
190 199 repoview.method() --> repo.__class__.method(repoview)
191 200
192 201 The inheritance has to be done dynamically because `repo` can be of any
193 202 subclasses of `localrepo`. Eg: `bundlerepo` or `statichttprepo`.
194 203 """
195 204
196 205 def __init__(self, repo, filtername, visibilityexceptions=None):
197 206 object.__setattr__(self, r'_unfilteredrepo', repo)
198 207 object.__setattr__(self, r'filtername', filtername)
199 208 object.__setattr__(self, r'_clcachekey', None)
200 209 object.__setattr__(self, r'_clcache', None)
201 210 # revs which are exceptions and must not be hidden
202 211 object.__setattr__(self, r'_visibilityexceptions',
203 212 visibilityexceptions)
204 213
205 214 # not a propertycache on purpose we shall implement a proper cache later
206 215 @property
207 216 def changelog(self):
208 217 """return a filtered version of the changeset
209 218
210 219 this changelog must not be used for writing"""
211 220 # some cache may be implemented later
212 221 unfi = self._unfilteredrepo
213 222 unfichangelog = unfi.changelog
214 223 # bypass call to changelog.method
215 224 unfiindex = unfichangelog.index
216 225 unfilen = len(unfiindex)
217 226 unfinode = unfiindex[unfilen - 1][7]
218 227
219 228 revs = filterrevs(unfi, self.filtername, self._visibilityexceptions)
220 229 cl = self._clcache
221 230 newkey = (unfilen, unfinode, hash(revs), unfichangelog._delayed)
222 231 # if cl.index is not unfiindex, unfi.changelog would be
223 232 # recreated, and our clcache refers to garbage object
224 233 if (cl is not None and
225 234 (cl.index is not unfiindex or newkey != self._clcachekey)):
226 235 cl = None
227 236 # could have been made None by the previous if
228 237 if cl is None:
229 238 cl = copy.copy(unfichangelog)
230 239 cl.filteredrevs = revs
231 240 object.__setattr__(self, r'_clcache', cl)
232 241 object.__setattr__(self, r'_clcachekey', newkey)
233 242 return cl
234 243
235 244 def unfiltered(self):
236 245 """Return an unfiltered version of a repo"""
237 246 return self._unfilteredrepo
238 247
239 248 def filtered(self, name, visibilityexceptions=None):
240 249 """Return a filtered version of a repository"""
241 250 if name == self.filtername and not visibilityexceptions:
242 251 return self
243 252 return self.unfiltered().filtered(name, visibilityexceptions)
244 253
245 254 def __repr__(self):
246 255 return r'<%s:%s %r>' % (self.__class__.__name__,
247 256 pycompat.sysstr(self.filtername),
248 257 self.unfiltered())
249 258
250 259 # everything access are forwarded to the proxied repo
251 260 def __getattr__(self, attr):
252 261 return getattr(self._unfilteredrepo, attr)
253 262
254 263 def __setattr__(self, attr, value):
255 264 return setattr(self._unfilteredrepo, attr, value)
256 265
257 266 def __delattr__(self, attr):
258 267 return delattr(self._unfilteredrepo, attr)
259 268
260 269 # Python <3.4 easily leaks types via __mro__. See
261 270 # https://bugs.python.org/issue17950. We cache dynamically created types
262 271 # so they won't be leaked on every invocation of repo.filtered().
263 272 _filteredrepotypes = weakref.WeakKeyDictionary()
264 273
265 274 def newtype(base):
266 275 """Create a new type with the repoview mixin and the given base class"""
267 276 if base not in _filteredrepotypes:
268 277 class filteredrepo(repoview, base):
269 278 pass
270 279 _filteredrepotypes[base] = filteredrepo
271 280 return _filteredrepotypes[base]
General Comments 0
You need to be logged in to leave comments. Login now