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