##// END OF EJS Templates
py3: drop unused aliases to array.array which are replaced with bytearray
Yuya Nishihara -
r31360:37acdf02 default
parent child Browse files
Show More
@@ -1,520 +1,518 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 array
11 import struct
10 import struct
12
11
13 from .node import (
12 from .node import (
14 bin,
13 bin,
15 hex,
14 hex,
16 nullid,
15 nullid,
17 nullrev,
16 nullrev,
18 )
17 )
19 from . import (
18 from . import (
20 encoding,
19 encoding,
21 error,
20 error,
22 scmutil,
21 scmutil,
23 util,
22 util,
24 )
23 )
25
24
26 array = array.array
27 calcsize = struct.calcsize
25 calcsize = struct.calcsize
28 pack = struct.pack
26 pack = struct.pack
29 unpack = struct.unpack
27 unpack = struct.unpack
30
28
31 def _filename(repo):
29 def _filename(repo):
32 """name of a branchcache file for a given repo or repoview"""
30 """name of a branchcache file for a given repo or repoview"""
33 filename = "cache/branch2"
31 filename = "cache/branch2"
34 if repo.filtername:
32 if repo.filtername:
35 filename = '%s-%s' % (filename, repo.filtername)
33 filename = '%s-%s' % (filename, repo.filtername)
36 return filename
34 return filename
37
35
38 def read(repo):
36 def read(repo):
39 try:
37 try:
40 f = repo.vfs(_filename(repo))
38 f = repo.vfs(_filename(repo))
41 lines = f.read().split('\n')
39 lines = f.read().split('\n')
42 f.close()
40 f.close()
43 except (IOError, OSError):
41 except (IOError, OSError):
44 return None
42 return None
45
43
46 try:
44 try:
47 cachekey = lines.pop(0).split(" ", 2)
45 cachekey = lines.pop(0).split(" ", 2)
48 last, lrev = cachekey[:2]
46 last, lrev = cachekey[:2]
49 last, lrev = bin(last), int(lrev)
47 last, lrev = bin(last), int(lrev)
50 filteredhash = None
48 filteredhash = None
51 if len(cachekey) > 2:
49 if len(cachekey) > 2:
52 filteredhash = bin(cachekey[2])
50 filteredhash = bin(cachekey[2])
53 partial = branchcache(tipnode=last, tiprev=lrev,
51 partial = branchcache(tipnode=last, tiprev=lrev,
54 filteredhash=filteredhash)
52 filteredhash=filteredhash)
55 if not partial.validfor(repo):
53 if not partial.validfor(repo):
56 # invalidate the cache
54 # invalidate the cache
57 raise ValueError('tip differs')
55 raise ValueError('tip differs')
58 cl = repo.changelog
56 cl = repo.changelog
59 for l in lines:
57 for l in lines:
60 if not l:
58 if not l:
61 continue
59 continue
62 node, state, label = l.split(" ", 2)
60 node, state, label = l.split(" ", 2)
63 if state not in 'oc':
61 if state not in 'oc':
64 raise ValueError('invalid branch state')
62 raise ValueError('invalid branch state')
65 label = encoding.tolocal(label.strip())
63 label = encoding.tolocal(label.strip())
66 node = bin(node)
64 node = bin(node)
67 if not cl.hasnode(node):
65 if not cl.hasnode(node):
68 raise ValueError('node %s does not exist' % hex(node))
66 raise ValueError('node %s does not exist' % hex(node))
69 partial.setdefault(label, []).append(node)
67 partial.setdefault(label, []).append(node)
70 if state == 'c':
68 if state == 'c':
71 partial._closednodes.add(node)
69 partial._closednodes.add(node)
72 except KeyboardInterrupt:
70 except KeyboardInterrupt:
73 raise
71 raise
74 except Exception as inst:
72 except Exception as inst:
75 if repo.ui.debugflag:
73 if repo.ui.debugflag:
76 msg = 'invalid branchheads cache'
74 msg = 'invalid branchheads cache'
77 if repo.filtername is not None:
75 if repo.filtername is not None:
78 msg += ' (%s)' % repo.filtername
76 msg += ' (%s)' % repo.filtername
79 msg += ': %s\n'
77 msg += ': %s\n'
80 repo.ui.debug(msg % inst)
78 repo.ui.debug(msg % inst)
81 partial = None
79 partial = None
82 return partial
80 return partial
83
81
84 ### Nearest subset relation
82 ### Nearest subset relation
85 # Nearest subset of filter X is a filter Y so that:
83 # Nearest subset of filter X is a filter Y so that:
86 # * Y is included in X,
84 # * Y is included in X,
87 # * X - Y is as small as possible.
85 # * X - Y is as small as possible.
88 # This create and ordering used for branchmap purpose.
86 # This create and ordering used for branchmap purpose.
89 # the ordering may be partial
87 # the ordering may be partial
90 subsettable = {None: 'visible',
88 subsettable = {None: 'visible',
91 'visible': 'served',
89 'visible': 'served',
92 'served': 'immutable',
90 'served': 'immutable',
93 'immutable': 'base'}
91 'immutable': 'base'}
94
92
95 def updatecache(repo):
93 def updatecache(repo):
96 cl = repo.changelog
94 cl = repo.changelog
97 filtername = repo.filtername
95 filtername = repo.filtername
98 partial = repo._branchcaches.get(filtername)
96 partial = repo._branchcaches.get(filtername)
99
97
100 revs = []
98 revs = []
101 if partial is None or not partial.validfor(repo):
99 if partial is None or not partial.validfor(repo):
102 partial = read(repo)
100 partial = read(repo)
103 if partial is None:
101 if partial is None:
104 subsetname = subsettable.get(filtername)
102 subsetname = subsettable.get(filtername)
105 if subsetname is None:
103 if subsetname is None:
106 partial = branchcache()
104 partial = branchcache()
107 else:
105 else:
108 subset = repo.filtered(subsetname)
106 subset = repo.filtered(subsetname)
109 partial = subset.branchmap().copy()
107 partial = subset.branchmap().copy()
110 extrarevs = subset.changelog.filteredrevs - cl.filteredrevs
108 extrarevs = subset.changelog.filteredrevs - cl.filteredrevs
111 revs.extend(r for r in extrarevs if r <= partial.tiprev)
109 revs.extend(r for r in extrarevs if r <= partial.tiprev)
112 revs.extend(cl.revs(start=partial.tiprev + 1))
110 revs.extend(cl.revs(start=partial.tiprev + 1))
113 if revs:
111 if revs:
114 partial.update(repo, revs)
112 partial.update(repo, revs)
115 partial.write(repo)
113 partial.write(repo)
116
114
117 assert partial.validfor(repo), filtername
115 assert partial.validfor(repo), filtername
118 repo._branchcaches[repo.filtername] = partial
116 repo._branchcaches[repo.filtername] = partial
119
117
120 def replacecache(repo, bm):
118 def replacecache(repo, bm):
121 """Replace the branchmap cache for a repo with a branch mapping.
119 """Replace the branchmap cache for a repo with a branch mapping.
122
120
123 This is likely only called during clone with a branch map from a remote.
121 This is likely only called during clone with a branch map from a remote.
124 """
122 """
125 rbheads = []
123 rbheads = []
126 closed = []
124 closed = []
127 for bheads in bm.itervalues():
125 for bheads in bm.itervalues():
128 rbheads.extend(bheads)
126 rbheads.extend(bheads)
129 for h in bheads:
127 for h in bheads:
130 r = repo.changelog.rev(h)
128 r = repo.changelog.rev(h)
131 b, c = repo.changelog.branchinfo(r)
129 b, c = repo.changelog.branchinfo(r)
132 if c:
130 if c:
133 closed.append(h)
131 closed.append(h)
134
132
135 if rbheads:
133 if rbheads:
136 rtiprev = max((int(repo.changelog.rev(node))
134 rtiprev = max((int(repo.changelog.rev(node))
137 for node in rbheads))
135 for node in rbheads))
138 cache = branchcache(bm,
136 cache = branchcache(bm,
139 repo[rtiprev].node(),
137 repo[rtiprev].node(),
140 rtiprev,
138 rtiprev,
141 closednodes=closed)
139 closednodes=closed)
142
140
143 # Try to stick it as low as possible
141 # Try to stick it as low as possible
144 # filter above served are unlikely to be fetch from a clone
142 # filter above served are unlikely to be fetch from a clone
145 for candidate in ('base', 'immutable', 'served'):
143 for candidate in ('base', 'immutable', 'served'):
146 rview = repo.filtered(candidate)
144 rview = repo.filtered(candidate)
147 if cache.validfor(rview):
145 if cache.validfor(rview):
148 repo._branchcaches[candidate] = cache
146 repo._branchcaches[candidate] = cache
149 cache.write(rview)
147 cache.write(rview)
150 break
148 break
151
149
152 class branchcache(dict):
150 class branchcache(dict):
153 """A dict like object that hold branches heads cache.
151 """A dict like object that hold branches heads cache.
154
152
155 This cache is used to avoid costly computations to determine all the
153 This cache is used to avoid costly computations to determine all the
156 branch heads of a repo.
154 branch heads of a repo.
157
155
158 The cache is serialized on disk in the following format:
156 The cache is serialized on disk in the following format:
159
157
160 <tip hex node> <tip rev number> [optional filtered repo hex hash]
158 <tip hex node> <tip rev number> [optional filtered repo hex hash]
161 <branch head hex node> <open/closed state> <branch name>
159 <branch head hex node> <open/closed state> <branch name>
162 <branch head hex node> <open/closed state> <branch name>
160 <branch head hex node> <open/closed state> <branch name>
163 ...
161 ...
164
162
165 The first line is used to check if the cache is still valid. If the
163 The first line is used to check if the cache is still valid. If the
166 branch cache is for a filtered repo view, an optional third hash is
164 branch cache is for a filtered repo view, an optional third hash is
167 included that hashes the hashes of all filtered revisions.
165 included that hashes the hashes of all filtered revisions.
168
166
169 The open/closed state is represented by a single letter 'o' or 'c'.
167 The open/closed state is represented by a single letter 'o' or 'c'.
170 This field can be used to avoid changelog reads when determining if a
168 This field can be used to avoid changelog reads when determining if a
171 branch head closes a branch or not.
169 branch head closes a branch or not.
172 """
170 """
173
171
174 def __init__(self, entries=(), tipnode=nullid, tiprev=nullrev,
172 def __init__(self, entries=(), tipnode=nullid, tiprev=nullrev,
175 filteredhash=None, closednodes=None):
173 filteredhash=None, closednodes=None):
176 super(branchcache, self).__init__(entries)
174 super(branchcache, self).__init__(entries)
177 self.tipnode = tipnode
175 self.tipnode = tipnode
178 self.tiprev = tiprev
176 self.tiprev = tiprev
179 self.filteredhash = filteredhash
177 self.filteredhash = filteredhash
180 # closednodes is a set of nodes that close their branch. If the branch
178 # closednodes is a set of nodes that close their branch. If the branch
181 # cache has been updated, it may contain nodes that are no longer
179 # cache has been updated, it may contain nodes that are no longer
182 # heads.
180 # heads.
183 if closednodes is None:
181 if closednodes is None:
184 self._closednodes = set()
182 self._closednodes = set()
185 else:
183 else:
186 self._closednodes = closednodes
184 self._closednodes = closednodes
187
185
188 def validfor(self, repo):
186 def validfor(self, repo):
189 """Is the cache content valid regarding a repo
187 """Is the cache content valid regarding a repo
190
188
191 - False when cached tipnode is unknown or if we detect a strip.
189 - False when cached tipnode is unknown or if we detect a strip.
192 - True when cache is up to date or a subset of current repo."""
190 - True when cache is up to date or a subset of current repo."""
193 try:
191 try:
194 return ((self.tipnode == repo.changelog.node(self.tiprev))
192 return ((self.tipnode == repo.changelog.node(self.tiprev))
195 and (self.filteredhash == \
193 and (self.filteredhash == \
196 scmutil.filteredhash(repo, self.tiprev)))
194 scmutil.filteredhash(repo, self.tiprev)))
197 except IndexError:
195 except IndexError:
198 return False
196 return False
199
197
200 def _branchtip(self, heads):
198 def _branchtip(self, heads):
201 '''Return tuple with last open head in heads and false,
199 '''Return tuple with last open head in heads and false,
202 otherwise return last closed head and true.'''
200 otherwise return last closed head and true.'''
203 tip = heads[-1]
201 tip = heads[-1]
204 closed = True
202 closed = True
205 for h in reversed(heads):
203 for h in reversed(heads):
206 if h not in self._closednodes:
204 if h not in self._closednodes:
207 tip = h
205 tip = h
208 closed = False
206 closed = False
209 break
207 break
210 return tip, closed
208 return tip, closed
211
209
212 def branchtip(self, branch):
210 def branchtip(self, branch):
213 '''Return the tipmost open head on branch head, otherwise return the
211 '''Return the tipmost open head on branch head, otherwise return the
214 tipmost closed head on branch.
212 tipmost closed head on branch.
215 Raise KeyError for unknown branch.'''
213 Raise KeyError for unknown branch.'''
216 return self._branchtip(self[branch])[0]
214 return self._branchtip(self[branch])[0]
217
215
218 def branchheads(self, branch, closed=False):
216 def branchheads(self, branch, closed=False):
219 heads = self[branch]
217 heads = self[branch]
220 if not closed:
218 if not closed:
221 heads = [h for h in heads if h not in self._closednodes]
219 heads = [h for h in heads if h not in self._closednodes]
222 return heads
220 return heads
223
221
224 def iterbranches(self):
222 def iterbranches(self):
225 for bn, heads in self.iteritems():
223 for bn, heads in self.iteritems():
226 yield (bn, heads) + self._branchtip(heads)
224 yield (bn, heads) + self._branchtip(heads)
227
225
228 def copy(self):
226 def copy(self):
229 """return an deep copy of the branchcache object"""
227 """return an deep copy of the branchcache object"""
230 return branchcache(self, self.tipnode, self.tiprev, self.filteredhash,
228 return branchcache(self, self.tipnode, self.tiprev, self.filteredhash,
231 self._closednodes)
229 self._closednodes)
232
230
233 def write(self, repo):
231 def write(self, repo):
234 try:
232 try:
235 f = repo.vfs(_filename(repo), "w", atomictemp=True)
233 f = repo.vfs(_filename(repo), "w", atomictemp=True)
236 cachekey = [hex(self.tipnode), '%d' % self.tiprev]
234 cachekey = [hex(self.tipnode), '%d' % self.tiprev]
237 if self.filteredhash is not None:
235 if self.filteredhash is not None:
238 cachekey.append(hex(self.filteredhash))
236 cachekey.append(hex(self.filteredhash))
239 f.write(" ".join(cachekey) + '\n')
237 f.write(" ".join(cachekey) + '\n')
240 nodecount = 0
238 nodecount = 0
241 for label, nodes in sorted(self.iteritems()):
239 for label, nodes in sorted(self.iteritems()):
242 for node in nodes:
240 for node in nodes:
243 nodecount += 1
241 nodecount += 1
244 if node in self._closednodes:
242 if node in self._closednodes:
245 state = 'c'
243 state = 'c'
246 else:
244 else:
247 state = 'o'
245 state = 'o'
248 f.write("%s %s %s\n" % (hex(node), state,
246 f.write("%s %s %s\n" % (hex(node), state,
249 encoding.fromlocal(label)))
247 encoding.fromlocal(label)))
250 f.close()
248 f.close()
251 repo.ui.log('branchcache',
249 repo.ui.log('branchcache',
252 'wrote %s branch cache with %d labels and %d nodes\n',
250 'wrote %s branch cache with %d labels and %d nodes\n',
253 repo.filtername, len(self), nodecount)
251 repo.filtername, len(self), nodecount)
254 except (IOError, OSError, error.Abort) as inst:
252 except (IOError, OSError, error.Abort) as inst:
255 repo.ui.debug("couldn't write branch cache: %s\n" % inst)
253 repo.ui.debug("couldn't write branch cache: %s\n" % inst)
256 # Abort may be raise by read only opener
254 # Abort may be raise by read only opener
257 pass
255 pass
258
256
259 def update(self, repo, revgen):
257 def update(self, repo, revgen):
260 """Given a branchhead cache, self, that may have extra nodes or be
258 """Given a branchhead cache, self, that may have extra nodes or be
261 missing heads, and a generator of nodes that are strictly a superset of
259 missing heads, and a generator of nodes that are strictly a superset of
262 heads missing, this function updates self to be correct.
260 heads missing, this function updates self to be correct.
263 """
261 """
264 starttime = util.timer()
262 starttime = util.timer()
265 cl = repo.changelog
263 cl = repo.changelog
266 # collect new branch entries
264 # collect new branch entries
267 newbranches = {}
265 newbranches = {}
268 getbranchinfo = repo.revbranchcache().branchinfo
266 getbranchinfo = repo.revbranchcache().branchinfo
269 for r in revgen:
267 for r in revgen:
270 branch, closesbranch = getbranchinfo(r)
268 branch, closesbranch = getbranchinfo(r)
271 newbranches.setdefault(branch, []).append(r)
269 newbranches.setdefault(branch, []).append(r)
272 if closesbranch:
270 if closesbranch:
273 self._closednodes.add(cl.node(r))
271 self._closednodes.add(cl.node(r))
274
272
275 # fetch current topological heads to speed up filtering
273 # fetch current topological heads to speed up filtering
276 topoheads = set(cl.headrevs())
274 topoheads = set(cl.headrevs())
277
275
278 # if older branchheads are reachable from new ones, they aren't
276 # if older branchheads are reachable from new ones, they aren't
279 # really branchheads. Note checking parents is insufficient:
277 # really branchheads. Note checking parents is insufficient:
280 # 1 (branch a) -> 2 (branch b) -> 3 (branch a)
278 # 1 (branch a) -> 2 (branch b) -> 3 (branch a)
281 for branch, newheadrevs in newbranches.iteritems():
279 for branch, newheadrevs in newbranches.iteritems():
282 bheads = self.setdefault(branch, [])
280 bheads = self.setdefault(branch, [])
283 bheadset = set(cl.rev(node) for node in bheads)
281 bheadset = set(cl.rev(node) for node in bheads)
284
282
285 # This have been tested True on all internal usage of this function.
283 # This have been tested True on all internal usage of this function.
286 # run it again in case of doubt
284 # run it again in case of doubt
287 # assert not (set(bheadrevs) & set(newheadrevs))
285 # assert not (set(bheadrevs) & set(newheadrevs))
288 newheadrevs.sort()
286 newheadrevs.sort()
289 bheadset.update(newheadrevs)
287 bheadset.update(newheadrevs)
290
288
291 # This prunes out two kinds of heads - heads that are superseded by
289 # This prunes out two kinds of heads - heads that are superseded by
292 # a head in newheadrevs, and newheadrevs that are not heads because
290 # a head in newheadrevs, and newheadrevs that are not heads because
293 # an existing head is their descendant.
291 # an existing head is their descendant.
294 uncertain = bheadset - topoheads
292 uncertain = bheadset - topoheads
295 if uncertain:
293 if uncertain:
296 floorrev = min(uncertain)
294 floorrev = min(uncertain)
297 ancestors = set(cl.ancestors(newheadrevs, floorrev))
295 ancestors = set(cl.ancestors(newheadrevs, floorrev))
298 bheadset -= ancestors
296 bheadset -= ancestors
299 bheadrevs = sorted(bheadset)
297 bheadrevs = sorted(bheadset)
300 self[branch] = [cl.node(rev) for rev in bheadrevs]
298 self[branch] = [cl.node(rev) for rev in bheadrevs]
301 tiprev = bheadrevs[-1]
299 tiprev = bheadrevs[-1]
302 if tiprev > self.tiprev:
300 if tiprev > self.tiprev:
303 self.tipnode = cl.node(tiprev)
301 self.tipnode = cl.node(tiprev)
304 self.tiprev = tiprev
302 self.tiprev = tiprev
305
303
306 if not self.validfor(repo):
304 if not self.validfor(repo):
307 # cache key are not valid anymore
305 # cache key are not valid anymore
308 self.tipnode = nullid
306 self.tipnode = nullid
309 self.tiprev = nullrev
307 self.tiprev = nullrev
310 for heads in self.values():
308 for heads in self.values():
311 tiprev = max(cl.rev(node) for node in heads)
309 tiprev = max(cl.rev(node) for node in heads)
312 if tiprev > self.tiprev:
310 if tiprev > self.tiprev:
313 self.tipnode = cl.node(tiprev)
311 self.tipnode = cl.node(tiprev)
314 self.tiprev = tiprev
312 self.tiprev = tiprev
315 self.filteredhash = scmutil.filteredhash(repo, self.tiprev)
313 self.filteredhash = scmutil.filteredhash(repo, self.tiprev)
316
314
317 duration = util.timer() - starttime
315 duration = util.timer() - starttime
318 repo.ui.log('branchcache', 'updated %s branch cache in %.4f seconds\n',
316 repo.ui.log('branchcache', 'updated %s branch cache in %.4f seconds\n',
319 repo.filtername, duration)
317 repo.filtername, duration)
320
318
321 # Revision branch info cache
319 # Revision branch info cache
322
320
323 _rbcversion = '-v1'
321 _rbcversion = '-v1'
324 _rbcnames = 'cache/rbc-names' + _rbcversion
322 _rbcnames = 'cache/rbc-names' + _rbcversion
325 _rbcrevs = 'cache/rbc-revs' + _rbcversion
323 _rbcrevs = 'cache/rbc-revs' + _rbcversion
326 # [4 byte hash prefix][4 byte branch name number with sign bit indicating open]
324 # [4 byte hash prefix][4 byte branch name number with sign bit indicating open]
327 _rbcrecfmt = '>4sI'
325 _rbcrecfmt = '>4sI'
328 _rbcrecsize = calcsize(_rbcrecfmt)
326 _rbcrecsize = calcsize(_rbcrecfmt)
329 _rbcnodelen = 4
327 _rbcnodelen = 4
330 _rbcbranchidxmask = 0x7fffffff
328 _rbcbranchidxmask = 0x7fffffff
331 _rbccloseflag = 0x80000000
329 _rbccloseflag = 0x80000000
332
330
333 class revbranchcache(object):
331 class revbranchcache(object):
334 """Persistent cache, mapping from revision number to branch name and close.
332 """Persistent cache, mapping from revision number to branch name and close.
335 This is a low level cache, independent of filtering.
333 This is a low level cache, independent of filtering.
336
334
337 Branch names are stored in rbc-names in internal encoding separated by 0.
335 Branch names are stored in rbc-names in internal encoding separated by 0.
338 rbc-names is append-only, and each branch name is only stored once and will
336 rbc-names is append-only, and each branch name is only stored once and will
339 thus have a unique index.
337 thus have a unique index.
340
338
341 The branch info for each revision is stored in rbc-revs as constant size
339 The branch info for each revision is stored in rbc-revs as constant size
342 records. The whole file is read into memory, but it is only 'parsed' on
340 records. The whole file is read into memory, but it is only 'parsed' on
343 demand. The file is usually append-only but will be truncated if repo
341 demand. The file is usually append-only but will be truncated if repo
344 modification is detected.
342 modification is detected.
345 The record for each revision contains the first 4 bytes of the
343 The record for each revision contains the first 4 bytes of the
346 corresponding node hash, and the record is only used if it still matches.
344 corresponding node hash, and the record is only used if it still matches.
347 Even a completely trashed rbc-revs fill thus still give the right result
345 Even a completely trashed rbc-revs fill thus still give the right result
348 while converging towards full recovery ... assuming no incorrectly matching
346 while converging towards full recovery ... assuming no incorrectly matching
349 node hashes.
347 node hashes.
350 The record also contains 4 bytes where 31 bits contains the index of the
348 The record also contains 4 bytes where 31 bits contains the index of the
351 branch and the last bit indicate that it is a branch close commit.
349 branch and the last bit indicate that it is a branch close commit.
352 The usage pattern for rbc-revs is thus somewhat similar to 00changelog.i
350 The usage pattern for rbc-revs is thus somewhat similar to 00changelog.i
353 and will grow with it but be 1/8th of its size.
351 and will grow with it but be 1/8th of its size.
354 """
352 """
355
353
356 def __init__(self, repo, readonly=True):
354 def __init__(self, repo, readonly=True):
357 assert repo.filtername is None
355 assert repo.filtername is None
358 self._repo = repo
356 self._repo = repo
359 self._names = [] # branch names in local encoding with static index
357 self._names = [] # branch names in local encoding with static index
360 self._rbcrevs = bytearray()
358 self._rbcrevs = bytearray()
361 self._rbcsnameslen = 0 # length of names read at _rbcsnameslen
359 self._rbcsnameslen = 0 # length of names read at _rbcsnameslen
362 try:
360 try:
363 bndata = repo.vfs.read(_rbcnames)
361 bndata = repo.vfs.read(_rbcnames)
364 self._rbcsnameslen = len(bndata) # for verification before writing
362 self._rbcsnameslen = len(bndata) # for verification before writing
365 self._names = [encoding.tolocal(bn) for bn in bndata.split('\0')]
363 self._names = [encoding.tolocal(bn) for bn in bndata.split('\0')]
366 except (IOError, OSError):
364 except (IOError, OSError):
367 if readonly:
365 if readonly:
368 # don't try to use cache - fall back to the slow path
366 # don't try to use cache - fall back to the slow path
369 self.branchinfo = self._branchinfo
367 self.branchinfo = self._branchinfo
370
368
371 if self._names:
369 if self._names:
372 try:
370 try:
373 data = repo.vfs.read(_rbcrevs)
371 data = repo.vfs.read(_rbcrevs)
374 self._rbcrevs[:] = data
372 self._rbcrevs[:] = data
375 except (IOError, OSError) as inst:
373 except (IOError, OSError) as inst:
376 repo.ui.debug("couldn't read revision branch cache: %s\n" %
374 repo.ui.debug("couldn't read revision branch cache: %s\n" %
377 inst)
375 inst)
378 # remember number of good records on disk
376 # remember number of good records on disk
379 self._rbcrevslen = min(len(self._rbcrevs) // _rbcrecsize,
377 self._rbcrevslen = min(len(self._rbcrevs) // _rbcrecsize,
380 len(repo.changelog))
378 len(repo.changelog))
381 if self._rbcrevslen == 0:
379 if self._rbcrevslen == 0:
382 self._names = []
380 self._names = []
383 self._rbcnamescount = len(self._names) # number of names read at
381 self._rbcnamescount = len(self._names) # number of names read at
384 # _rbcsnameslen
382 # _rbcsnameslen
385 self._namesreverse = dict((b, r) for r, b in enumerate(self._names))
383 self._namesreverse = dict((b, r) for r, b in enumerate(self._names))
386
384
387 def _clear(self):
385 def _clear(self):
388 self._rbcsnameslen = 0
386 self._rbcsnameslen = 0
389 del self._names[:]
387 del self._names[:]
390 self._rbcnamescount = 0
388 self._rbcnamescount = 0
391 self._namesreverse.clear()
389 self._namesreverse.clear()
392 self._rbcrevslen = len(self._repo.changelog)
390 self._rbcrevslen = len(self._repo.changelog)
393 self._rbcrevs = bytearray(self._rbcrevslen * _rbcrecsize)
391 self._rbcrevs = bytearray(self._rbcrevslen * _rbcrecsize)
394
392
395 def branchinfo(self, rev):
393 def branchinfo(self, rev):
396 """Return branch name and close flag for rev, using and updating
394 """Return branch name and close flag for rev, using and updating
397 persistent cache."""
395 persistent cache."""
398 changelog = self._repo.changelog
396 changelog = self._repo.changelog
399 rbcrevidx = rev * _rbcrecsize
397 rbcrevidx = rev * _rbcrecsize
400
398
401 # avoid negative index, changelog.read(nullrev) is fast without cache
399 # avoid negative index, changelog.read(nullrev) is fast without cache
402 if rev == nullrev:
400 if rev == nullrev:
403 return changelog.branchinfo(rev)
401 return changelog.branchinfo(rev)
404
402
405 # if requested rev isn't allocated, grow and cache the rev info
403 # if requested rev isn't allocated, grow and cache the rev info
406 if len(self._rbcrevs) < rbcrevidx + _rbcrecsize:
404 if len(self._rbcrevs) < rbcrevidx + _rbcrecsize:
407 return self._branchinfo(rev)
405 return self._branchinfo(rev)
408
406
409 # fast path: extract data from cache, use it if node is matching
407 # fast path: extract data from cache, use it if node is matching
410 reponode = changelog.node(rev)[:_rbcnodelen]
408 reponode = changelog.node(rev)[:_rbcnodelen]
411 cachenode, branchidx = unpack(
409 cachenode, branchidx = unpack(
412 _rbcrecfmt, util.buffer(self._rbcrevs, rbcrevidx, _rbcrecsize))
410 _rbcrecfmt, util.buffer(self._rbcrevs, rbcrevidx, _rbcrecsize))
413 close = bool(branchidx & _rbccloseflag)
411 close = bool(branchidx & _rbccloseflag)
414 if close:
412 if close:
415 branchidx &= _rbcbranchidxmask
413 branchidx &= _rbcbranchidxmask
416 if cachenode == '\0\0\0\0':
414 if cachenode == '\0\0\0\0':
417 pass
415 pass
418 elif cachenode == reponode:
416 elif cachenode == reponode:
419 try:
417 try:
420 return self._names[branchidx], close
418 return self._names[branchidx], close
421 except IndexError:
419 except IndexError:
422 # recover from invalid reference to unknown branch
420 # recover from invalid reference to unknown branch
423 self._repo.ui.debug("referenced branch names not found"
421 self._repo.ui.debug("referenced branch names not found"
424 " - rebuilding revision branch cache from scratch\n")
422 " - rebuilding revision branch cache from scratch\n")
425 self._clear()
423 self._clear()
426 else:
424 else:
427 # rev/node map has changed, invalidate the cache from here up
425 # rev/node map has changed, invalidate the cache from here up
428 self._repo.ui.debug("history modification detected - truncating "
426 self._repo.ui.debug("history modification detected - truncating "
429 "revision branch cache to revision %s\n" % rev)
427 "revision branch cache to revision %s\n" % rev)
430 truncate = rbcrevidx + _rbcrecsize
428 truncate = rbcrevidx + _rbcrecsize
431 del self._rbcrevs[truncate:]
429 del self._rbcrevs[truncate:]
432 self._rbcrevslen = min(self._rbcrevslen, truncate)
430 self._rbcrevslen = min(self._rbcrevslen, truncate)
433
431
434 # fall back to slow path and make sure it will be written to disk
432 # fall back to slow path and make sure it will be written to disk
435 return self._branchinfo(rev)
433 return self._branchinfo(rev)
436
434
437 def _branchinfo(self, rev):
435 def _branchinfo(self, rev):
438 """Retrieve branch info from changelog and update _rbcrevs"""
436 """Retrieve branch info from changelog and update _rbcrevs"""
439 changelog = self._repo.changelog
437 changelog = self._repo.changelog
440 b, close = changelog.branchinfo(rev)
438 b, close = changelog.branchinfo(rev)
441 if b in self._namesreverse:
439 if b in self._namesreverse:
442 branchidx = self._namesreverse[b]
440 branchidx = self._namesreverse[b]
443 else:
441 else:
444 branchidx = len(self._names)
442 branchidx = len(self._names)
445 self._names.append(b)
443 self._names.append(b)
446 self._namesreverse[b] = branchidx
444 self._namesreverse[b] = branchidx
447 reponode = changelog.node(rev)
445 reponode = changelog.node(rev)
448 if close:
446 if close:
449 branchidx |= _rbccloseflag
447 branchidx |= _rbccloseflag
450 self._setcachedata(rev, reponode, branchidx)
448 self._setcachedata(rev, reponode, branchidx)
451 return b, close
449 return b, close
452
450
453 def _setcachedata(self, rev, node, branchidx):
451 def _setcachedata(self, rev, node, branchidx):
454 """Writes the node's branch data to the in-memory cache data."""
452 """Writes the node's branch data to the in-memory cache data."""
455 rbcrevidx = rev * _rbcrecsize
453 rbcrevidx = rev * _rbcrecsize
456 rec = bytearray(pack(_rbcrecfmt, node, branchidx))
454 rec = bytearray(pack(_rbcrecfmt, node, branchidx))
457 if len(self._rbcrevs) < rbcrevidx + _rbcrecsize:
455 if len(self._rbcrevs) < rbcrevidx + _rbcrecsize:
458 self._rbcrevs.extend('\0' *
456 self._rbcrevs.extend('\0' *
459 (len(self._repo.changelog) * _rbcrecsize -
457 (len(self._repo.changelog) * _rbcrecsize -
460 len(self._rbcrevs)))
458 len(self._rbcrevs)))
461 self._rbcrevs[rbcrevidx:rbcrevidx + _rbcrecsize] = rec
459 self._rbcrevs[rbcrevidx:rbcrevidx + _rbcrecsize] = rec
462 self._rbcrevslen = min(self._rbcrevslen, rev)
460 self._rbcrevslen = min(self._rbcrevslen, rev)
463
461
464 tr = self._repo.currenttransaction()
462 tr = self._repo.currenttransaction()
465 if tr:
463 if tr:
466 tr.addfinalize('write-revbranchcache', self.write)
464 tr.addfinalize('write-revbranchcache', self.write)
467
465
468 def write(self, tr=None):
466 def write(self, tr=None):
469 """Save branch cache if it is dirty."""
467 """Save branch cache if it is dirty."""
470 repo = self._repo
468 repo = self._repo
471 wlock = None
469 wlock = None
472 step = ''
470 step = ''
473 try:
471 try:
474 if self._rbcnamescount < len(self._names):
472 if self._rbcnamescount < len(self._names):
475 step = ' names'
473 step = ' names'
476 wlock = repo.wlock(wait=False)
474 wlock = repo.wlock(wait=False)
477 if self._rbcnamescount != 0:
475 if self._rbcnamescount != 0:
478 f = repo.vfs.open(_rbcnames, 'ab')
476 f = repo.vfs.open(_rbcnames, 'ab')
479 if f.tell() == self._rbcsnameslen:
477 if f.tell() == self._rbcsnameslen:
480 f.write('\0')
478 f.write('\0')
481 else:
479 else:
482 f.close()
480 f.close()
483 repo.ui.debug("%s changed - rewriting it\n" % _rbcnames)
481 repo.ui.debug("%s changed - rewriting it\n" % _rbcnames)
484 self._rbcnamescount = 0
482 self._rbcnamescount = 0
485 self._rbcrevslen = 0
483 self._rbcrevslen = 0
486 if self._rbcnamescount == 0:
484 if self._rbcnamescount == 0:
487 # before rewriting names, make sure references are removed
485 # before rewriting names, make sure references are removed
488 repo.vfs.unlinkpath(_rbcrevs, ignoremissing=True)
486 repo.vfs.unlinkpath(_rbcrevs, ignoremissing=True)
489 f = repo.vfs.open(_rbcnames, 'wb')
487 f = repo.vfs.open(_rbcnames, 'wb')
490 f.write('\0'.join(encoding.fromlocal(b)
488 f.write('\0'.join(encoding.fromlocal(b)
491 for b in self._names[self._rbcnamescount:]))
489 for b in self._names[self._rbcnamescount:]))
492 self._rbcsnameslen = f.tell()
490 self._rbcsnameslen = f.tell()
493 f.close()
491 f.close()
494 self._rbcnamescount = len(self._names)
492 self._rbcnamescount = len(self._names)
495
493
496 start = self._rbcrevslen * _rbcrecsize
494 start = self._rbcrevslen * _rbcrecsize
497 if start != len(self._rbcrevs):
495 if start != len(self._rbcrevs):
498 step = ''
496 step = ''
499 if wlock is None:
497 if wlock is None:
500 wlock = repo.wlock(wait=False)
498 wlock = repo.wlock(wait=False)
501 revs = min(len(repo.changelog),
499 revs = min(len(repo.changelog),
502 len(self._rbcrevs) // _rbcrecsize)
500 len(self._rbcrevs) // _rbcrecsize)
503 f = repo.vfs.open(_rbcrevs, 'ab')
501 f = repo.vfs.open(_rbcrevs, 'ab')
504 if f.tell() != start:
502 if f.tell() != start:
505 repo.ui.debug("truncating %s to %s\n" % (_rbcrevs, start))
503 repo.ui.debug("truncating %s to %s\n" % (_rbcrevs, start))
506 f.seek(start)
504 f.seek(start)
507 if f.tell() != start:
505 if f.tell() != start:
508 start = 0
506 start = 0
509 f.seek(start)
507 f.seek(start)
510 f.truncate()
508 f.truncate()
511 end = revs * _rbcrecsize
509 end = revs * _rbcrecsize
512 f.write(self._rbcrevs[start:end])
510 f.write(self._rbcrevs[start:end])
513 f.close()
511 f.close()
514 self._rbcrevslen = revs
512 self._rbcrevslen = revs
515 except (IOError, OSError, error.Abort, error.LockError) as inst:
513 except (IOError, OSError, error.Abort, error.LockError) as inst:
516 repo.ui.debug("couldn't write revision branch cache%s: %s\n"
514 repo.ui.debug("couldn't write revision branch cache%s: %s\n"
517 % (step, inst))
515 % (step, inst))
518 finally:
516 finally:
519 if wlock is not None:
517 if wlock is not None:
520 wlock.release()
518 wlock.release()
@@ -1,568 +1,565 b''
1 # tags.py - read tag info from local repository
1 # tags.py - read tag info from local repository
2 #
2 #
3 # Copyright 2009 Matt Mackall <mpm@selenic.com>
3 # Copyright 2009 Matt Mackall <mpm@selenic.com>
4 # Copyright 2009 Greg Ward <greg@gerg.ca>
4 # Copyright 2009 Greg Ward <greg@gerg.ca>
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 # Currently this module only deals with reading and caching tags.
9 # Currently this module only deals with reading and caching tags.
10 # Eventually, it could take care of updating (adding/removing/moving)
10 # Eventually, it could take care of updating (adding/removing/moving)
11 # tags too.
11 # tags too.
12
12
13 from __future__ import absolute_import
13 from __future__ import absolute_import
14
14
15 import array
16 import errno
15 import errno
17
16
18 from .node import (
17 from .node import (
19 bin,
18 bin,
20 hex,
19 hex,
21 nullid,
20 nullid,
22 short,
21 short,
23 )
22 )
24 from . import (
23 from . import (
25 encoding,
24 encoding,
26 error,
25 error,
27 scmutil,
26 scmutil,
28 util,
27 util,
29 )
28 )
30
29
31 array = array.array
32
33 # Tags computation can be expensive and caches exist to make it fast in
30 # Tags computation can be expensive and caches exist to make it fast in
34 # the common case.
31 # the common case.
35 #
32 #
36 # The "hgtagsfnodes1" cache file caches the .hgtags filenode values for
33 # The "hgtagsfnodes1" cache file caches the .hgtags filenode values for
37 # each revision in the repository. The file is effectively an array of
34 # each revision in the repository. The file is effectively an array of
38 # fixed length records. Read the docs for "hgtagsfnodescache" for technical
35 # fixed length records. Read the docs for "hgtagsfnodescache" for technical
39 # details.
36 # details.
40 #
37 #
41 # The .hgtags filenode cache grows in proportion to the length of the
38 # The .hgtags filenode cache grows in proportion to the length of the
42 # changelog. The file is truncated when the # changelog is stripped.
39 # changelog. The file is truncated when the # changelog is stripped.
43 #
40 #
44 # The purpose of the filenode cache is to avoid the most expensive part
41 # The purpose of the filenode cache is to avoid the most expensive part
45 # of finding global tags, which is looking up the .hgtags filenode in the
42 # of finding global tags, which is looking up the .hgtags filenode in the
46 # manifest for each head. This can take dozens or over 100ms for
43 # manifest for each head. This can take dozens or over 100ms for
47 # repositories with very large manifests. Multiplied by dozens or even
44 # repositories with very large manifests. Multiplied by dozens or even
48 # hundreds of heads and there is a significant performance concern.
45 # hundreds of heads and there is a significant performance concern.
49 #
46 #
50 # There also exist a separate cache file for each repository filter.
47 # There also exist a separate cache file for each repository filter.
51 # These "tags-*" files store information about the history of tags.
48 # These "tags-*" files store information about the history of tags.
52 #
49 #
53 # The tags cache files consists of a cache validation line followed by
50 # The tags cache files consists of a cache validation line followed by
54 # a history of tags.
51 # a history of tags.
55 #
52 #
56 # The cache validation line has the format:
53 # The cache validation line has the format:
57 #
54 #
58 # <tiprev> <tipnode> [<filteredhash>]
55 # <tiprev> <tipnode> [<filteredhash>]
59 #
56 #
60 # <tiprev> is an integer revision and <tipnode> is a 40 character hex
57 # <tiprev> is an integer revision and <tipnode> is a 40 character hex
61 # node for that changeset. These redundantly identify the repository
58 # node for that changeset. These redundantly identify the repository
62 # tip from the time the cache was written. In addition, <filteredhash>,
59 # tip from the time the cache was written. In addition, <filteredhash>,
63 # if present, is a 40 character hex hash of the contents of the filtered
60 # if present, is a 40 character hex hash of the contents of the filtered
64 # revisions for this filter. If the set of filtered revs changes, the
61 # revisions for this filter. If the set of filtered revs changes, the
65 # hash will change and invalidate the cache.
62 # hash will change and invalidate the cache.
66 #
63 #
67 # The history part of the tags cache consists of lines of the form:
64 # The history part of the tags cache consists of lines of the form:
68 #
65 #
69 # <node> <tag>
66 # <node> <tag>
70 #
67 #
71 # (This format is identical to that of .hgtags files.)
68 # (This format is identical to that of .hgtags files.)
72 #
69 #
73 # <tag> is the tag name and <node> is the 40 character hex changeset
70 # <tag> is the tag name and <node> is the 40 character hex changeset
74 # the tag is associated with.
71 # the tag is associated with.
75 #
72 #
76 # Tags are written sorted by tag name.
73 # Tags are written sorted by tag name.
77 #
74 #
78 # Tags associated with multiple changesets have an entry for each changeset.
75 # Tags associated with multiple changesets have an entry for each changeset.
79 # The most recent changeset (in terms of revlog ordering for the head
76 # The most recent changeset (in terms of revlog ordering for the head
80 # setting it) for each tag is last.
77 # setting it) for each tag is last.
81
78
82 def findglobaltags(ui, repo, alltags, tagtypes):
79 def findglobaltags(ui, repo, alltags, tagtypes):
83 '''Find global tags in a repo.
80 '''Find global tags in a repo.
84
81
85 "alltags" maps tag name to (node, hist) 2-tuples.
82 "alltags" maps tag name to (node, hist) 2-tuples.
86
83
87 "tagtypes" maps tag name to tag type. Global tags always have the
84 "tagtypes" maps tag name to tag type. Global tags always have the
88 "global" tag type.
85 "global" tag type.
89
86
90 The "alltags" and "tagtypes" dicts are updated in place. Empty dicts
87 The "alltags" and "tagtypes" dicts are updated in place. Empty dicts
91 should be passed in.
88 should be passed in.
92
89
93 The tags cache is read and updated as a side-effect of calling.
90 The tags cache is read and updated as a side-effect of calling.
94 '''
91 '''
95 # This is so we can be lazy and assume alltags contains only global
92 # This is so we can be lazy and assume alltags contains only global
96 # tags when we pass it to _writetagcache().
93 # tags when we pass it to _writetagcache().
97 assert len(alltags) == len(tagtypes) == 0, \
94 assert len(alltags) == len(tagtypes) == 0, \
98 "findglobaltags() should be called first"
95 "findglobaltags() should be called first"
99
96
100 (heads, tagfnode, valid, cachetags, shouldwrite) = _readtagcache(ui, repo)
97 (heads, tagfnode, valid, cachetags, shouldwrite) = _readtagcache(ui, repo)
101 if cachetags is not None:
98 if cachetags is not None:
102 assert not shouldwrite
99 assert not shouldwrite
103 # XXX is this really 100% correct? are there oddball special
100 # XXX is this really 100% correct? are there oddball special
104 # cases where a global tag should outrank a local tag but won't,
101 # cases where a global tag should outrank a local tag but won't,
105 # because cachetags does not contain rank info?
102 # because cachetags does not contain rank info?
106 _updatetags(cachetags, 'global', alltags, tagtypes)
103 _updatetags(cachetags, 'global', alltags, tagtypes)
107 return
104 return
108
105
109 seen = set() # set of fnode
106 seen = set() # set of fnode
110 fctx = None
107 fctx = None
111 for head in reversed(heads): # oldest to newest
108 for head in reversed(heads): # oldest to newest
112 assert head in repo.changelog.nodemap, \
109 assert head in repo.changelog.nodemap, \
113 "tag cache returned bogus head %s" % short(head)
110 "tag cache returned bogus head %s" % short(head)
114
111
115 fnode = tagfnode.get(head)
112 fnode = tagfnode.get(head)
116 if fnode and fnode not in seen:
113 if fnode and fnode not in seen:
117 seen.add(fnode)
114 seen.add(fnode)
118 if not fctx:
115 if not fctx:
119 fctx = repo.filectx('.hgtags', fileid=fnode)
116 fctx = repo.filectx('.hgtags', fileid=fnode)
120 else:
117 else:
121 fctx = fctx.filectx(fnode)
118 fctx = fctx.filectx(fnode)
122
119
123 filetags = _readtags(ui, repo, fctx.data().splitlines(), fctx)
120 filetags = _readtags(ui, repo, fctx.data().splitlines(), fctx)
124 _updatetags(filetags, 'global', alltags, tagtypes)
121 _updatetags(filetags, 'global', alltags, tagtypes)
125
122
126 # and update the cache (if necessary)
123 # and update the cache (if necessary)
127 if shouldwrite:
124 if shouldwrite:
128 _writetagcache(ui, repo, valid, alltags)
125 _writetagcache(ui, repo, valid, alltags)
129
126
130 def readlocaltags(ui, repo, alltags, tagtypes):
127 def readlocaltags(ui, repo, alltags, tagtypes):
131 '''Read local tags in repo. Update alltags and tagtypes.'''
128 '''Read local tags in repo. Update alltags and tagtypes.'''
132 try:
129 try:
133 data = repo.vfs.read("localtags")
130 data = repo.vfs.read("localtags")
134 except IOError as inst:
131 except IOError as inst:
135 if inst.errno != errno.ENOENT:
132 if inst.errno != errno.ENOENT:
136 raise
133 raise
137 return
134 return
138
135
139 # localtags is in the local encoding; re-encode to UTF-8 on
136 # localtags is in the local encoding; re-encode to UTF-8 on
140 # input for consistency with the rest of this module.
137 # input for consistency with the rest of this module.
141 filetags = _readtags(
138 filetags = _readtags(
142 ui, repo, data.splitlines(), "localtags",
139 ui, repo, data.splitlines(), "localtags",
143 recode=encoding.fromlocal)
140 recode=encoding.fromlocal)
144
141
145 # remove tags pointing to invalid nodes
142 # remove tags pointing to invalid nodes
146 cl = repo.changelog
143 cl = repo.changelog
147 for t in filetags.keys():
144 for t in filetags.keys():
148 try:
145 try:
149 cl.rev(filetags[t][0])
146 cl.rev(filetags[t][0])
150 except (LookupError, ValueError):
147 except (LookupError, ValueError):
151 del filetags[t]
148 del filetags[t]
152
149
153 _updatetags(filetags, "local", alltags, tagtypes)
150 _updatetags(filetags, "local", alltags, tagtypes)
154
151
155 def _readtaghist(ui, repo, lines, fn, recode=None, calcnodelines=False):
152 def _readtaghist(ui, repo, lines, fn, recode=None, calcnodelines=False):
156 '''Read tag definitions from a file (or any source of lines).
153 '''Read tag definitions from a file (or any source of lines).
157
154
158 This function returns two sortdicts with similar information:
155 This function returns two sortdicts with similar information:
159
156
160 - the first dict, bintaghist, contains the tag information as expected by
157 - the first dict, bintaghist, contains the tag information as expected by
161 the _readtags function, i.e. a mapping from tag name to (node, hist):
158 the _readtags function, i.e. a mapping from tag name to (node, hist):
162 - node is the node id from the last line read for that name,
159 - node is the node id from the last line read for that name,
163 - hist is the list of node ids previously associated with it (in file
160 - hist is the list of node ids previously associated with it (in file
164 order). All node ids are binary, not hex.
161 order). All node ids are binary, not hex.
165
162
166 - the second dict, hextaglines, is a mapping from tag name to a list of
163 - the second dict, hextaglines, is a mapping from tag name to a list of
167 [hexnode, line number] pairs, ordered from the oldest to the newest node.
164 [hexnode, line number] pairs, ordered from the oldest to the newest node.
168
165
169 When calcnodelines is False the hextaglines dict is not calculated (an
166 When calcnodelines is False the hextaglines dict is not calculated (an
170 empty dict is returned). This is done to improve this function's
167 empty dict is returned). This is done to improve this function's
171 performance in cases where the line numbers are not needed.
168 performance in cases where the line numbers are not needed.
172 '''
169 '''
173
170
174 bintaghist = util.sortdict()
171 bintaghist = util.sortdict()
175 hextaglines = util.sortdict()
172 hextaglines = util.sortdict()
176 count = 0
173 count = 0
177
174
178 def dbg(msg):
175 def dbg(msg):
179 ui.debug("%s, line %s: %s\n" % (fn, count, msg))
176 ui.debug("%s, line %s: %s\n" % (fn, count, msg))
180
177
181 for nline, line in enumerate(lines):
178 for nline, line in enumerate(lines):
182 count += 1
179 count += 1
183 if not line:
180 if not line:
184 continue
181 continue
185 try:
182 try:
186 (nodehex, name) = line.split(" ", 1)
183 (nodehex, name) = line.split(" ", 1)
187 except ValueError:
184 except ValueError:
188 dbg("cannot parse entry")
185 dbg("cannot parse entry")
189 continue
186 continue
190 name = name.strip()
187 name = name.strip()
191 if recode:
188 if recode:
192 name = recode(name)
189 name = recode(name)
193 try:
190 try:
194 nodebin = bin(nodehex)
191 nodebin = bin(nodehex)
195 except TypeError:
192 except TypeError:
196 dbg("node '%s' is not well formed" % nodehex)
193 dbg("node '%s' is not well formed" % nodehex)
197 continue
194 continue
198
195
199 # update filetags
196 # update filetags
200 if calcnodelines:
197 if calcnodelines:
201 # map tag name to a list of line numbers
198 # map tag name to a list of line numbers
202 if name not in hextaglines:
199 if name not in hextaglines:
203 hextaglines[name] = []
200 hextaglines[name] = []
204 hextaglines[name].append([nodehex, nline])
201 hextaglines[name].append([nodehex, nline])
205 continue
202 continue
206 # map tag name to (node, hist)
203 # map tag name to (node, hist)
207 if name not in bintaghist:
204 if name not in bintaghist:
208 bintaghist[name] = []
205 bintaghist[name] = []
209 bintaghist[name].append(nodebin)
206 bintaghist[name].append(nodebin)
210 return bintaghist, hextaglines
207 return bintaghist, hextaglines
211
208
212 def _readtags(ui, repo, lines, fn, recode=None, calcnodelines=False):
209 def _readtags(ui, repo, lines, fn, recode=None, calcnodelines=False):
213 '''Read tag definitions from a file (or any source of lines).
210 '''Read tag definitions from a file (or any source of lines).
214
211
215 Returns a mapping from tag name to (node, hist).
212 Returns a mapping from tag name to (node, hist).
216
213
217 "node" is the node id from the last line read for that name. "hist"
214 "node" is the node id from the last line read for that name. "hist"
218 is the list of node ids previously associated with it (in file order).
215 is the list of node ids previously associated with it (in file order).
219 All node ids are binary, not hex.
216 All node ids are binary, not hex.
220 '''
217 '''
221 filetags, nodelines = _readtaghist(ui, repo, lines, fn, recode=recode,
218 filetags, nodelines = _readtaghist(ui, repo, lines, fn, recode=recode,
222 calcnodelines=calcnodelines)
219 calcnodelines=calcnodelines)
223 # util.sortdict().__setitem__ is much slower at replacing then inserting
220 # util.sortdict().__setitem__ is much slower at replacing then inserting
224 # new entries. The difference can matter if there are thousands of tags.
221 # new entries. The difference can matter if there are thousands of tags.
225 # Create a new sortdict to avoid the performance penalty.
222 # Create a new sortdict to avoid the performance penalty.
226 newtags = util.sortdict()
223 newtags = util.sortdict()
227 for tag, taghist in filetags.items():
224 for tag, taghist in filetags.items():
228 newtags[tag] = (taghist[-1], taghist[:-1])
225 newtags[tag] = (taghist[-1], taghist[:-1])
229 return newtags
226 return newtags
230
227
231 def _updatetags(filetags, tagtype, alltags, tagtypes):
228 def _updatetags(filetags, tagtype, alltags, tagtypes):
232 '''Incorporate the tag info read from one file into the two
229 '''Incorporate the tag info read from one file into the two
233 dictionaries, alltags and tagtypes, that contain all tag
230 dictionaries, alltags and tagtypes, that contain all tag
234 info (global across all heads plus local).'''
231 info (global across all heads plus local).'''
235
232
236 for name, nodehist in filetags.iteritems():
233 for name, nodehist in filetags.iteritems():
237 if name not in alltags:
234 if name not in alltags:
238 alltags[name] = nodehist
235 alltags[name] = nodehist
239 tagtypes[name] = tagtype
236 tagtypes[name] = tagtype
240 continue
237 continue
241
238
242 # we prefer alltags[name] if:
239 # we prefer alltags[name] if:
243 # it supersedes us OR
240 # it supersedes us OR
244 # mutual supersedes and it has a higher rank
241 # mutual supersedes and it has a higher rank
245 # otherwise we win because we're tip-most
242 # otherwise we win because we're tip-most
246 anode, ahist = nodehist
243 anode, ahist = nodehist
247 bnode, bhist = alltags[name]
244 bnode, bhist = alltags[name]
248 if (bnode != anode and anode in bhist and
245 if (bnode != anode and anode in bhist and
249 (bnode not in ahist or len(bhist) > len(ahist))):
246 (bnode not in ahist or len(bhist) > len(ahist))):
250 anode = bnode
247 anode = bnode
251 else:
248 else:
252 tagtypes[name] = tagtype
249 tagtypes[name] = tagtype
253 ahist.extend([n for n in bhist if n not in ahist])
250 ahist.extend([n for n in bhist if n not in ahist])
254 alltags[name] = anode, ahist
251 alltags[name] = anode, ahist
255
252
256 def _filename(repo):
253 def _filename(repo):
257 """name of a tagcache file for a given repo or repoview"""
254 """name of a tagcache file for a given repo or repoview"""
258 filename = 'cache/tags2'
255 filename = 'cache/tags2'
259 if repo.filtername:
256 if repo.filtername:
260 filename = '%s-%s' % (filename, repo.filtername)
257 filename = '%s-%s' % (filename, repo.filtername)
261 return filename
258 return filename
262
259
263 def _readtagcache(ui, repo):
260 def _readtagcache(ui, repo):
264 '''Read the tag cache.
261 '''Read the tag cache.
265
262
266 Returns a tuple (heads, fnodes, validinfo, cachetags, shouldwrite).
263 Returns a tuple (heads, fnodes, validinfo, cachetags, shouldwrite).
267
264
268 If the cache is completely up-to-date, "cachetags" is a dict of the
265 If the cache is completely up-to-date, "cachetags" is a dict of the
269 form returned by _readtags() and "heads", "fnodes", and "validinfo" are
266 form returned by _readtags() and "heads", "fnodes", and "validinfo" are
270 None and "shouldwrite" is False.
267 None and "shouldwrite" is False.
271
268
272 If the cache is not up to date, "cachetags" is None. "heads" is a list
269 If the cache is not up to date, "cachetags" is None. "heads" is a list
273 of all heads currently in the repository, ordered from tip to oldest.
270 of all heads currently in the repository, ordered from tip to oldest.
274 "validinfo" is a tuple describing cache validation info. This is used
271 "validinfo" is a tuple describing cache validation info. This is used
275 when writing the tags cache. "fnodes" is a mapping from head to .hgtags
272 when writing the tags cache. "fnodes" is a mapping from head to .hgtags
276 filenode. "shouldwrite" is True.
273 filenode. "shouldwrite" is True.
277
274
278 If the cache is not up to date, the caller is responsible for reading tag
275 If the cache is not up to date, the caller is responsible for reading tag
279 info from each returned head. (See findglobaltags().)
276 info from each returned head. (See findglobaltags().)
280 '''
277 '''
281 try:
278 try:
282 cachefile = repo.vfs(_filename(repo), 'r')
279 cachefile = repo.vfs(_filename(repo), 'r')
283 # force reading the file for static-http
280 # force reading the file for static-http
284 cachelines = iter(cachefile)
281 cachelines = iter(cachefile)
285 except IOError:
282 except IOError:
286 cachefile = None
283 cachefile = None
287
284
288 cacherev = None
285 cacherev = None
289 cachenode = None
286 cachenode = None
290 cachehash = None
287 cachehash = None
291 if cachefile:
288 if cachefile:
292 try:
289 try:
293 validline = next(cachelines)
290 validline = next(cachelines)
294 validline = validline.split()
291 validline = validline.split()
295 cacherev = int(validline[0])
292 cacherev = int(validline[0])
296 cachenode = bin(validline[1])
293 cachenode = bin(validline[1])
297 if len(validline) > 2:
294 if len(validline) > 2:
298 cachehash = bin(validline[2])
295 cachehash = bin(validline[2])
299 except Exception:
296 except Exception:
300 # corruption of the cache, just recompute it.
297 # corruption of the cache, just recompute it.
301 pass
298 pass
302
299
303 tipnode = repo.changelog.tip()
300 tipnode = repo.changelog.tip()
304 tiprev = len(repo.changelog) - 1
301 tiprev = len(repo.changelog) - 1
305
302
306 # Case 1 (common): tip is the same, so nothing has changed.
303 # Case 1 (common): tip is the same, so nothing has changed.
307 # (Unchanged tip trivially means no changesets have been added.
304 # (Unchanged tip trivially means no changesets have been added.
308 # But, thanks to localrepository.destroyed(), it also means none
305 # But, thanks to localrepository.destroyed(), it also means none
309 # have been destroyed by strip or rollback.)
306 # have been destroyed by strip or rollback.)
310 if (cacherev == tiprev
307 if (cacherev == tiprev
311 and cachenode == tipnode
308 and cachenode == tipnode
312 and cachehash == scmutil.filteredhash(repo, tiprev)):
309 and cachehash == scmutil.filteredhash(repo, tiprev)):
313 tags = _readtags(ui, repo, cachelines, cachefile.name)
310 tags = _readtags(ui, repo, cachelines, cachefile.name)
314 cachefile.close()
311 cachefile.close()
315 return (None, None, None, tags, False)
312 return (None, None, None, tags, False)
316 if cachefile:
313 if cachefile:
317 cachefile.close() # ignore rest of file
314 cachefile.close() # ignore rest of file
318
315
319 valid = (tiprev, tipnode, scmutil.filteredhash(repo, tiprev))
316 valid = (tiprev, tipnode, scmutil.filteredhash(repo, tiprev))
320
317
321 repoheads = repo.heads()
318 repoheads = repo.heads()
322 # Case 2 (uncommon): empty repo; get out quickly and don't bother
319 # Case 2 (uncommon): empty repo; get out quickly and don't bother
323 # writing an empty cache.
320 # writing an empty cache.
324 if repoheads == [nullid]:
321 if repoheads == [nullid]:
325 return ([], {}, valid, {}, False)
322 return ([], {}, valid, {}, False)
326
323
327 # Case 3 (uncommon): cache file missing or empty.
324 # Case 3 (uncommon): cache file missing or empty.
328
325
329 # Case 4 (uncommon): tip rev decreased. This should only happen
326 # Case 4 (uncommon): tip rev decreased. This should only happen
330 # when we're called from localrepository.destroyed(). Refresh the
327 # when we're called from localrepository.destroyed(). Refresh the
331 # cache so future invocations will not see disappeared heads in the
328 # cache so future invocations will not see disappeared heads in the
332 # cache.
329 # cache.
333
330
334 # Case 5 (common): tip has changed, so we've added/replaced heads.
331 # Case 5 (common): tip has changed, so we've added/replaced heads.
335
332
336 # As it happens, the code to handle cases 3, 4, 5 is the same.
333 # As it happens, the code to handle cases 3, 4, 5 is the same.
337
334
338 # N.B. in case 4 (nodes destroyed), "new head" really means "newly
335 # N.B. in case 4 (nodes destroyed), "new head" really means "newly
339 # exposed".
336 # exposed".
340 if not len(repo.file('.hgtags')):
337 if not len(repo.file('.hgtags')):
341 # No tags have ever been committed, so we can avoid a
338 # No tags have ever been committed, so we can avoid a
342 # potentially expensive search.
339 # potentially expensive search.
343 return ([], {}, valid, None, True)
340 return ([], {}, valid, None, True)
344
341
345 starttime = util.timer()
342 starttime = util.timer()
346
343
347 # Now we have to lookup the .hgtags filenode for every new head.
344 # Now we have to lookup the .hgtags filenode for every new head.
348 # This is the most expensive part of finding tags, so performance
345 # This is the most expensive part of finding tags, so performance
349 # depends primarily on the size of newheads. Worst case: no cache
346 # depends primarily on the size of newheads. Worst case: no cache
350 # file, so newheads == repoheads.
347 # file, so newheads == repoheads.
351 fnodescache = hgtagsfnodescache(repo.unfiltered())
348 fnodescache = hgtagsfnodescache(repo.unfiltered())
352 cachefnode = {}
349 cachefnode = {}
353 for head in reversed(repoheads):
350 for head in reversed(repoheads):
354 fnode = fnodescache.getfnode(head)
351 fnode = fnodescache.getfnode(head)
355 if fnode != nullid:
352 if fnode != nullid:
356 cachefnode[head] = fnode
353 cachefnode[head] = fnode
357
354
358 fnodescache.write()
355 fnodescache.write()
359
356
360 duration = util.timer() - starttime
357 duration = util.timer() - starttime
361 ui.log('tagscache',
358 ui.log('tagscache',
362 '%d/%d cache hits/lookups in %0.4f '
359 '%d/%d cache hits/lookups in %0.4f '
363 'seconds\n',
360 'seconds\n',
364 fnodescache.hitcount, fnodescache.lookupcount, duration)
361 fnodescache.hitcount, fnodescache.lookupcount, duration)
365
362
366 # Caller has to iterate over all heads, but can use the filenodes in
363 # Caller has to iterate over all heads, but can use the filenodes in
367 # cachefnode to get to each .hgtags revision quickly.
364 # cachefnode to get to each .hgtags revision quickly.
368 return (repoheads, cachefnode, valid, None, True)
365 return (repoheads, cachefnode, valid, None, True)
369
366
370 def _writetagcache(ui, repo, valid, cachetags):
367 def _writetagcache(ui, repo, valid, cachetags):
371 filename = _filename(repo)
368 filename = _filename(repo)
372 try:
369 try:
373 cachefile = repo.vfs(filename, 'w', atomictemp=True)
370 cachefile = repo.vfs(filename, 'w', atomictemp=True)
374 except (OSError, IOError):
371 except (OSError, IOError):
375 return
372 return
376
373
377 ui.log('tagscache', 'writing .hg/%s with %d tags\n',
374 ui.log('tagscache', 'writing .hg/%s with %d tags\n',
378 filename, len(cachetags))
375 filename, len(cachetags))
379
376
380 if valid[2]:
377 if valid[2]:
381 cachefile.write('%d %s %s\n' % (valid[0], hex(valid[1]), hex(valid[2])))
378 cachefile.write('%d %s %s\n' % (valid[0], hex(valid[1]), hex(valid[2])))
382 else:
379 else:
383 cachefile.write('%d %s\n' % (valid[0], hex(valid[1])))
380 cachefile.write('%d %s\n' % (valid[0], hex(valid[1])))
384
381
385 # Tag names in the cache are in UTF-8 -- which is the whole reason
382 # Tag names in the cache are in UTF-8 -- which is the whole reason
386 # we keep them in UTF-8 throughout this module. If we converted
383 # we keep them in UTF-8 throughout this module. If we converted
387 # them local encoding on input, we would lose info writing them to
384 # them local encoding on input, we would lose info writing them to
388 # the cache.
385 # the cache.
389 for (name, (node, hist)) in sorted(cachetags.iteritems()):
386 for (name, (node, hist)) in sorted(cachetags.iteritems()):
390 for n in hist:
387 for n in hist:
391 cachefile.write("%s %s\n" % (hex(n), name))
388 cachefile.write("%s %s\n" % (hex(n), name))
392 cachefile.write("%s %s\n" % (hex(node), name))
389 cachefile.write("%s %s\n" % (hex(node), name))
393
390
394 try:
391 try:
395 cachefile.close()
392 cachefile.close()
396 except (OSError, IOError):
393 except (OSError, IOError):
397 pass
394 pass
398
395
399 _fnodescachefile = 'cache/hgtagsfnodes1'
396 _fnodescachefile = 'cache/hgtagsfnodes1'
400 _fnodesrecsize = 4 + 20 # changeset fragment + filenode
397 _fnodesrecsize = 4 + 20 # changeset fragment + filenode
401 _fnodesmissingrec = '\xff' * 24
398 _fnodesmissingrec = '\xff' * 24
402
399
403 class hgtagsfnodescache(object):
400 class hgtagsfnodescache(object):
404 """Persistent cache mapping revisions to .hgtags filenodes.
401 """Persistent cache mapping revisions to .hgtags filenodes.
405
402
406 The cache is an array of records. Each item in the array corresponds to
403 The cache is an array of records. Each item in the array corresponds to
407 a changelog revision. Values in the array contain the first 4 bytes of
404 a changelog revision. Values in the array contain the first 4 bytes of
408 the node hash and the 20 bytes .hgtags filenode for that revision.
405 the node hash and the 20 bytes .hgtags filenode for that revision.
409
406
410 The first 4 bytes are present as a form of verification. Repository
407 The first 4 bytes are present as a form of verification. Repository
411 stripping and rewriting may change the node at a numeric revision in the
408 stripping and rewriting may change the node at a numeric revision in the
412 changelog. The changeset fragment serves as a verifier to detect
409 changelog. The changeset fragment serves as a verifier to detect
413 rewriting. This logic is shared with the rev branch cache (see
410 rewriting. This logic is shared with the rev branch cache (see
414 branchmap.py).
411 branchmap.py).
415
412
416 The instance holds in memory the full cache content but entries are
413 The instance holds in memory the full cache content but entries are
417 only parsed on read.
414 only parsed on read.
418
415
419 Instances behave like lists. ``c[i]`` works where i is a rev or
416 Instances behave like lists. ``c[i]`` works where i is a rev or
420 changeset node. Missing indexes are populated automatically on access.
417 changeset node. Missing indexes are populated automatically on access.
421 """
418 """
422 def __init__(self, repo):
419 def __init__(self, repo):
423 assert repo.filtername is None
420 assert repo.filtername is None
424
421
425 self._repo = repo
422 self._repo = repo
426
423
427 # Only for reporting purposes.
424 # Only for reporting purposes.
428 self.lookupcount = 0
425 self.lookupcount = 0
429 self.hitcount = 0
426 self.hitcount = 0
430
427
431
428
432 try:
429 try:
433 data = repo.vfs.read(_fnodescachefile)
430 data = repo.vfs.read(_fnodescachefile)
434 except (OSError, IOError):
431 except (OSError, IOError):
435 data = ""
432 data = ""
436 self._raw = bytearray(data)
433 self._raw = bytearray(data)
437
434
438 # The end state of self._raw is an array that is of the exact length
435 # The end state of self._raw is an array that is of the exact length
439 # required to hold a record for every revision in the repository.
436 # required to hold a record for every revision in the repository.
440 # We truncate or extend the array as necessary. self._dirtyoffset is
437 # We truncate or extend the array as necessary. self._dirtyoffset is
441 # defined to be the start offset at which we need to write the output
438 # defined to be the start offset at which we need to write the output
442 # file. This offset is also adjusted when new entries are calculated
439 # file. This offset is also adjusted when new entries are calculated
443 # for array members.
440 # for array members.
444 cllen = len(repo.changelog)
441 cllen = len(repo.changelog)
445 wantedlen = cllen * _fnodesrecsize
442 wantedlen = cllen * _fnodesrecsize
446 rawlen = len(self._raw)
443 rawlen = len(self._raw)
447
444
448 self._dirtyoffset = None
445 self._dirtyoffset = None
449
446
450 if rawlen < wantedlen:
447 if rawlen < wantedlen:
451 self._dirtyoffset = rawlen
448 self._dirtyoffset = rawlen
452 self._raw.extend('\xff' * (wantedlen - rawlen))
449 self._raw.extend('\xff' * (wantedlen - rawlen))
453 elif rawlen > wantedlen:
450 elif rawlen > wantedlen:
454 # There's no easy way to truncate array instances. This seems
451 # There's no easy way to truncate array instances. This seems
455 # slightly less evil than copying a potentially large array slice.
452 # slightly less evil than copying a potentially large array slice.
456 for i in range(rawlen - wantedlen):
453 for i in range(rawlen - wantedlen):
457 self._raw.pop()
454 self._raw.pop()
458 self._dirtyoffset = len(self._raw)
455 self._dirtyoffset = len(self._raw)
459
456
460 def getfnode(self, node, computemissing=True):
457 def getfnode(self, node, computemissing=True):
461 """Obtain the filenode of the .hgtags file at a specified revision.
458 """Obtain the filenode of the .hgtags file at a specified revision.
462
459
463 If the value is in the cache, the entry will be validated and returned.
460 If the value is in the cache, the entry will be validated and returned.
464 Otherwise, the filenode will be computed and returned unless
461 Otherwise, the filenode will be computed and returned unless
465 "computemissing" is False, in which case None will be returned without
462 "computemissing" is False, in which case None will be returned without
466 any potentially expensive computation being performed.
463 any potentially expensive computation being performed.
467
464
468 If an .hgtags does not exist at the specified revision, nullid is
465 If an .hgtags does not exist at the specified revision, nullid is
469 returned.
466 returned.
470 """
467 """
471 ctx = self._repo[node]
468 ctx = self._repo[node]
472 rev = ctx.rev()
469 rev = ctx.rev()
473
470
474 self.lookupcount += 1
471 self.lookupcount += 1
475
472
476 offset = rev * _fnodesrecsize
473 offset = rev * _fnodesrecsize
477 record = '%s' % self._raw[offset:offset + _fnodesrecsize]
474 record = '%s' % self._raw[offset:offset + _fnodesrecsize]
478 properprefix = node[0:4]
475 properprefix = node[0:4]
479
476
480 # Validate and return existing entry.
477 # Validate and return existing entry.
481 if record != _fnodesmissingrec:
478 if record != _fnodesmissingrec:
482 fileprefix = record[0:4]
479 fileprefix = record[0:4]
483
480
484 if fileprefix == properprefix:
481 if fileprefix == properprefix:
485 self.hitcount += 1
482 self.hitcount += 1
486 return record[4:]
483 return record[4:]
487
484
488 # Fall through.
485 # Fall through.
489
486
490 # If we get here, the entry is either missing or invalid.
487 # If we get here, the entry is either missing or invalid.
491
488
492 if not computemissing:
489 if not computemissing:
493 return None
490 return None
494
491
495 # Populate missing entry.
492 # Populate missing entry.
496 try:
493 try:
497 fnode = ctx.filenode('.hgtags')
494 fnode = ctx.filenode('.hgtags')
498 except error.LookupError:
495 except error.LookupError:
499 # No .hgtags file on this revision.
496 # No .hgtags file on this revision.
500 fnode = nullid
497 fnode = nullid
501
498
502 self._writeentry(offset, properprefix, fnode)
499 self._writeentry(offset, properprefix, fnode)
503 return fnode
500 return fnode
504
501
505 def setfnode(self, node, fnode):
502 def setfnode(self, node, fnode):
506 """Set the .hgtags filenode for a given changeset."""
503 """Set the .hgtags filenode for a given changeset."""
507 assert len(fnode) == 20
504 assert len(fnode) == 20
508 ctx = self._repo[node]
505 ctx = self._repo[node]
509
506
510 # Do a lookup first to avoid writing if nothing has changed.
507 # Do a lookup first to avoid writing if nothing has changed.
511 if self.getfnode(ctx.node(), computemissing=False) == fnode:
508 if self.getfnode(ctx.node(), computemissing=False) == fnode:
512 return
509 return
513
510
514 self._writeentry(ctx.rev() * _fnodesrecsize, node[0:4], fnode)
511 self._writeentry(ctx.rev() * _fnodesrecsize, node[0:4], fnode)
515
512
516 def _writeentry(self, offset, prefix, fnode):
513 def _writeentry(self, offset, prefix, fnode):
517 # Slices on array instances only accept other array.
514 # Slices on array instances only accept other array.
518 entry = bytearray(prefix + fnode)
515 entry = bytearray(prefix + fnode)
519 self._raw[offset:offset + _fnodesrecsize] = entry
516 self._raw[offset:offset + _fnodesrecsize] = entry
520 # self._dirtyoffset could be None.
517 # self._dirtyoffset could be None.
521 self._dirtyoffset = min(self._dirtyoffset, offset) or 0
518 self._dirtyoffset = min(self._dirtyoffset, offset) or 0
522
519
523 def write(self):
520 def write(self):
524 """Perform all necessary writes to cache file.
521 """Perform all necessary writes to cache file.
525
522
526 This may no-op if no writes are needed or if a write lock could
523 This may no-op if no writes are needed or if a write lock could
527 not be obtained.
524 not be obtained.
528 """
525 """
529 if self._dirtyoffset is None:
526 if self._dirtyoffset is None:
530 return
527 return
531
528
532 data = self._raw[self._dirtyoffset:]
529 data = self._raw[self._dirtyoffset:]
533 if not data:
530 if not data:
534 return
531 return
535
532
536 repo = self._repo
533 repo = self._repo
537
534
538 try:
535 try:
539 lock = repo.wlock(wait=False)
536 lock = repo.wlock(wait=False)
540 except error.LockError:
537 except error.LockError:
541 repo.ui.log('tagscache',
538 repo.ui.log('tagscache',
542 'not writing .hg/%s because lock cannot be acquired\n' %
539 'not writing .hg/%s because lock cannot be acquired\n' %
543 (_fnodescachefile))
540 (_fnodescachefile))
544 return
541 return
545
542
546 try:
543 try:
547 f = repo.vfs.open(_fnodescachefile, 'ab')
544 f = repo.vfs.open(_fnodescachefile, 'ab')
548 try:
545 try:
549 # if the file has been truncated
546 # if the file has been truncated
550 actualoffset = f.tell()
547 actualoffset = f.tell()
551 if actualoffset < self._dirtyoffset:
548 if actualoffset < self._dirtyoffset:
552 self._dirtyoffset = actualoffset
549 self._dirtyoffset = actualoffset
553 data = self._raw[self._dirtyoffset:]
550 data = self._raw[self._dirtyoffset:]
554 f.seek(self._dirtyoffset)
551 f.seek(self._dirtyoffset)
555 f.truncate()
552 f.truncate()
556 repo.ui.log('tagscache',
553 repo.ui.log('tagscache',
557 'writing %d bytes to %s\n' % (
554 'writing %d bytes to %s\n' % (
558 len(data), _fnodescachefile))
555 len(data), _fnodescachefile))
559 f.write(data)
556 f.write(data)
560 self._dirtyoffset = None
557 self._dirtyoffset = None
561 finally:
558 finally:
562 f.close()
559 f.close()
563 except (IOError, OSError) as inst:
560 except (IOError, OSError) as inst:
564 repo.ui.log('tagscache',
561 repo.ui.log('tagscache',
565 "couldn't write %s: %s\n" % (
562 "couldn't write %s: %s\n" % (
566 _fnodescachefile, inst))
563 _fnodescachefile, inst))
567 finally:
564 finally:
568 lock.release()
565 lock.release()
General Comments 0
You need to be logged in to leave comments. Login now