##// END OF EJS Templates
tags-cache: directly operate on rev-num warming hgtagsfnodescache...
marmoute -
r52491:2664cacd default
parent child Browse files
Show More
@@ -1,955 +1,966 b''
1 # tags.py - read tag info from local repository
1 # tags.py - read tag info from local repository
2 #
2 #
3 # Copyright 2009 Olivia Mackall <olivia@selenic.com>
3 # Copyright 2009 Olivia Mackall <olivia@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
13
14 import binascii
14 import binascii
15 import io
15 import io
16
16
17 from .node import (
17 from .node import (
18 bin,
18 bin,
19 hex,
19 hex,
20 nullrev,
20 nullrev,
21 short,
21 short,
22 )
22 )
23 from .i18n import _
23 from .i18n import _
24 from . import (
24 from . import (
25 encoding,
25 encoding,
26 error,
26 error,
27 match as matchmod,
27 match as matchmod,
28 scmutil,
28 scmutil,
29 util,
29 util,
30 )
30 )
31 from .utils import stringutil
31 from .utils import stringutil
32
32
33 # Tags computation can be expensive and caches exist to make it fast in
33 # Tags computation can be expensive and caches exist to make it fast in
34 # the common case.
34 # the common case.
35 #
35 #
36 # The "hgtagsfnodes1" cache file caches the .hgtags filenode values for
36 # The "hgtagsfnodes1" cache file caches the .hgtags filenode values for
37 # each revision in the repository. The file is effectively an array of
37 # each revision in the repository. The file is effectively an array of
38 # fixed length records. Read the docs for "hgtagsfnodescache" for technical
38 # fixed length records. Read the docs for "hgtagsfnodescache" for technical
39 # details.
39 # details.
40 #
40 #
41 # The .hgtags filenode cache grows in proportion to the length of the
41 # The .hgtags filenode cache grows in proportion to the length of the
42 # changelog. The file is truncated when the # changelog is stripped.
42 # changelog. The file is truncated when the # changelog is stripped.
43 #
43 #
44 # The purpose of the filenode cache is to avoid the most expensive part
44 # 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
45 # 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
46 # manifest for each head. This can take dozens or over 100ms for
47 # repositories with very large manifests. Multiplied by dozens or even
47 # repositories with very large manifests. Multiplied by dozens or even
48 # hundreds of heads and there is a significant performance concern.
48 # hundreds of heads and there is a significant performance concern.
49 #
49 #
50 # There also exist a separate cache file for each repository filter.
50 # There also exist a separate cache file for each repository filter.
51 # These "tags-*" files store information about the history of tags.
51 # These "tags-*" files store information about the history of tags.
52 #
52 #
53 # The tags cache files consists of a cache validation line followed by
53 # The tags cache files consists of a cache validation line followed by
54 # a history of tags.
54 # a history of tags.
55 #
55 #
56 # The cache validation line has the format:
56 # The cache validation line has the format:
57 #
57 #
58 # <tiprev> <tipnode> [<filteredhash>]
58 # <tiprev> <tipnode> [<filteredhash>]
59 #
59 #
60 # <tiprev> is an integer revision and <tipnode> is a 40 character hex
60 # <tiprev> is an integer revision and <tipnode> is a 40 character hex
61 # node for that changeset. These redundantly identify the repository
61 # node for that changeset. These redundantly identify the repository
62 # tip from the time the cache was written. In addition, <filteredhash>,
62 # 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
63 # 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
64 # revisions for this filter. If the set of filtered revs changes, the
65 # hash will change and invalidate the cache.
65 # hash will change and invalidate the cache.
66 #
66 #
67 # The history part of the tags cache consists of lines of the form:
67 # The history part of the tags cache consists of lines of the form:
68 #
68 #
69 # <node> <tag>
69 # <node> <tag>
70 #
70 #
71 # (This format is identical to that of .hgtags files.)
71 # (This format is identical to that of .hgtags files.)
72 #
72 #
73 # <tag> is the tag name and <node> is the 40 character hex changeset
73 # <tag> is the tag name and <node> is the 40 character hex changeset
74 # the tag is associated with.
74 # the tag is associated with.
75 #
75 #
76 # Tags are written sorted by tag name.
76 # Tags are written sorted by tag name.
77 #
77 #
78 # Tags associated with multiple changesets have an entry for each changeset.
78 # Tags associated with multiple changesets have an entry for each changeset.
79 # The most recent changeset (in terms of revlog ordering for the head
79 # The most recent changeset (in terms of revlog ordering for the head
80 # setting it) for each tag is last.
80 # setting it) for each tag is last.
81
81
82
82
83 def warm_cache(repo):
83 def warm_cache(repo):
84 """ensure the cache is properly filled"""
84 """ensure the cache is properly filled"""
85 unfi = repo.unfiltered()
85 unfi = repo.unfiltered()
86 tonode = unfi.changelog.node
86 _getfnodes(repo.ui, repo, revs=unfi.changelog.revs())
87 nodes = [tonode(r) for r in unfi.changelog.revs()]
88 _getfnodes(repo.ui, repo, nodes)
89
87
90
88
91 def fnoderevs(ui, repo, revs):
89 def fnoderevs(ui, repo, revs):
92 """return the list of '.hgtags' fnodes used in a set revisions
90 """return the list of '.hgtags' fnodes used in a set revisions
93
91
94 This is returned as list of unique fnodes. We use a list instead of a set
92 This is returned as list of unique fnodes. We use a list instead of a set
95 because order matters when it comes to tags."""
93 because order matters when it comes to tags."""
96 unfi = repo.unfiltered()
94 unfi = repo.unfiltered()
97 tonode = unfi.changelog.node
95 tonode = unfi.changelog.node
98 nodes = [tonode(r) for r in revs]
96 nodes = [tonode(r) for r in revs]
99 fnodes = _getfnodes(ui, repo, nodes)
97 fnodes = _getfnodes(ui, repo, nodes)
100 fnodes = _filterfnodes(fnodes, nodes)
98 fnodes = _filterfnodes(fnodes, nodes)
101 return fnodes
99 return fnodes
102
100
103
101
104 def _nulltonone(repo, value):
102 def _nulltonone(repo, value):
105 """convert nullid to None
103 """convert nullid to None
106
104
107 For tag value, nullid means "deleted". This small utility function helps
105 For tag value, nullid means "deleted". This small utility function helps
108 translating that to None."""
106 translating that to None."""
109 if value == repo.nullid:
107 if value == repo.nullid:
110 return None
108 return None
111 return value
109 return value
112
110
113
111
114 def difftags(ui, repo, oldfnodes, newfnodes):
112 def difftags(ui, repo, oldfnodes, newfnodes):
115 """list differences between tags expressed in two set of file-nodes
113 """list differences between tags expressed in two set of file-nodes
116
114
117 The list contains entries in the form: (tagname, oldvalue, new value).
115 The list contains entries in the form: (tagname, oldvalue, new value).
118 None is used to expressed missing value:
116 None is used to expressed missing value:
119 ('foo', None, 'abcd') is a new tag,
117 ('foo', None, 'abcd') is a new tag,
120 ('bar', 'ef01', None) is a deletion,
118 ('bar', 'ef01', None) is a deletion,
121 ('baz', 'abcd', 'ef01') is a tag movement.
119 ('baz', 'abcd', 'ef01') is a tag movement.
122 """
120 """
123 if oldfnodes == newfnodes:
121 if oldfnodes == newfnodes:
124 return []
122 return []
125 oldtags = _tagsfromfnodes(ui, repo, oldfnodes)
123 oldtags = _tagsfromfnodes(ui, repo, oldfnodes)
126 newtags = _tagsfromfnodes(ui, repo, newfnodes)
124 newtags = _tagsfromfnodes(ui, repo, newfnodes)
127
125
128 # list of (tag, old, new): None means missing
126 # list of (tag, old, new): None means missing
129 entries = []
127 entries = []
130 for tag, (new, __) in newtags.items():
128 for tag, (new, __) in newtags.items():
131 new = _nulltonone(repo, new)
129 new = _nulltonone(repo, new)
132 old, __ = oldtags.pop(tag, (None, None))
130 old, __ = oldtags.pop(tag, (None, None))
133 old = _nulltonone(repo, old)
131 old = _nulltonone(repo, old)
134 if old != new:
132 if old != new:
135 entries.append((tag, old, new))
133 entries.append((tag, old, new))
136 # handle deleted tags
134 # handle deleted tags
137 for tag, (old, __) in oldtags.items():
135 for tag, (old, __) in oldtags.items():
138 old = _nulltonone(repo, old)
136 old = _nulltonone(repo, old)
139 if old is not None:
137 if old is not None:
140 entries.append((tag, old, None))
138 entries.append((tag, old, None))
141 entries.sort()
139 entries.sort()
142 return entries
140 return entries
143
141
144
142
145 def writediff(fp, difflist):
143 def writediff(fp, difflist):
146 """write tags diff information to a file.
144 """write tags diff information to a file.
147
145
148 Data are stored with a line based format:
146 Data are stored with a line based format:
149
147
150 <action> <hex-node> <tag-name>\n
148 <action> <hex-node> <tag-name>\n
151
149
152 Action are defined as follow:
150 Action are defined as follow:
153 -R tag is removed,
151 -R tag is removed,
154 +A tag is added,
152 +A tag is added,
155 -M tag is moved (old value),
153 -M tag is moved (old value),
156 +M tag is moved (new value),
154 +M tag is moved (new value),
157
155
158 Example:
156 Example:
159
157
160 +A 875517b4806a848f942811a315a5bce30804ae85 t5
158 +A 875517b4806a848f942811a315a5bce30804ae85 t5
161
159
162 See documentation of difftags output for details about the input.
160 See documentation of difftags output for details about the input.
163 """
161 """
164 add = b'+A %s %s\n'
162 add = b'+A %s %s\n'
165 remove = b'-R %s %s\n'
163 remove = b'-R %s %s\n'
166 updateold = b'-M %s %s\n'
164 updateold = b'-M %s %s\n'
167 updatenew = b'+M %s %s\n'
165 updatenew = b'+M %s %s\n'
168 for tag, old, new in difflist:
166 for tag, old, new in difflist:
169 # translate to hex
167 # translate to hex
170 if old is not None:
168 if old is not None:
171 old = hex(old)
169 old = hex(old)
172 if new is not None:
170 if new is not None:
173 new = hex(new)
171 new = hex(new)
174 # write to file
172 # write to file
175 if old is None:
173 if old is None:
176 fp.write(add % (new, tag))
174 fp.write(add % (new, tag))
177 elif new is None:
175 elif new is None:
178 fp.write(remove % (old, tag))
176 fp.write(remove % (old, tag))
179 else:
177 else:
180 fp.write(updateold % (old, tag))
178 fp.write(updateold % (old, tag))
181 fp.write(updatenew % (new, tag))
179 fp.write(updatenew % (new, tag))
182
180
183
181
184 def findglobaltags(ui, repo):
182 def findglobaltags(ui, repo):
185 """Find global tags in a repo: return a tagsmap
183 """Find global tags in a repo: return a tagsmap
186
184
187 tagsmap: tag name to (node, hist) 2-tuples.
185 tagsmap: tag name to (node, hist) 2-tuples.
188
186
189 The tags cache is read and updated as a side-effect of calling.
187 The tags cache is read and updated as a side-effect of calling.
190 """
188 """
191 (heads, tagfnode, valid, cachetags, shouldwrite) = _readtagcache(ui, repo)
189 (heads, tagfnode, valid, cachetags, shouldwrite) = _readtagcache(ui, repo)
192 if cachetags is not None:
190 if cachetags is not None:
193 assert not shouldwrite
191 assert not shouldwrite
194 # XXX is this really 100% correct? are there oddball special
192 # XXX is this really 100% correct? are there oddball special
195 # cases where a global tag should outrank a local tag but won't,
193 # cases where a global tag should outrank a local tag but won't,
196 # because cachetags does not contain rank info?
194 # because cachetags does not contain rank info?
197 alltags = {}
195 alltags = {}
198 _updatetags(cachetags, alltags)
196 _updatetags(cachetags, alltags)
199 return alltags
197 return alltags
200
198
201 has_node = repo.changelog.index.has_node
199 has_node = repo.changelog.index.has_node
202 for head in reversed(heads): # oldest to newest
200 for head in reversed(heads): # oldest to newest
203 assert has_node(head), b"tag cache returned bogus head %s" % short(head)
201 assert has_node(head), b"tag cache returned bogus head %s" % short(head)
204 fnodes = _filterfnodes(tagfnode, reversed(heads))
202 fnodes = _filterfnodes(tagfnode, reversed(heads))
205 alltags = _tagsfromfnodes(ui, repo, fnodes)
203 alltags = _tagsfromfnodes(ui, repo, fnodes)
206
204
207 # and update the cache (if necessary)
205 # and update the cache (if necessary)
208 if shouldwrite:
206 if shouldwrite:
209 _writetagcache(ui, repo, valid, alltags)
207 _writetagcache(ui, repo, valid, alltags)
210 return alltags
208 return alltags
211
209
212
210
213 def _filterfnodes(tagfnode, nodes):
211 def _filterfnodes(tagfnode, nodes):
214 """return a list of unique fnodes
212 """return a list of unique fnodes
215
213
216 The order of this list matches the order of "nodes". Preserving this order
214 The order of this list matches the order of "nodes". Preserving this order
217 is important as reading tags in different order provides different
215 is important as reading tags in different order provides different
218 results."""
216 results."""
219 seen = set() # set of fnode
217 seen = set() # set of fnode
220 fnodes = []
218 fnodes = []
221 for no in nodes: # oldest to newest
219 for no in nodes: # oldest to newest
222 fnode = tagfnode.get(no)
220 fnode = tagfnode.get(no)
223 if fnode and fnode not in seen:
221 if fnode and fnode not in seen:
224 seen.add(fnode)
222 seen.add(fnode)
225 fnodes.append(fnode)
223 fnodes.append(fnode)
226 return fnodes
224 return fnodes
227
225
228
226
229 def _tagsfromfnodes(ui, repo, fnodes):
227 def _tagsfromfnodes(ui, repo, fnodes):
230 """return a tagsmap from a list of file-node
228 """return a tagsmap from a list of file-node
231
229
232 tagsmap: tag name to (node, hist) 2-tuples.
230 tagsmap: tag name to (node, hist) 2-tuples.
233
231
234 The order of the list matters."""
232 The order of the list matters."""
235 alltags = {}
233 alltags = {}
236 fctx = None
234 fctx = None
237 for fnode in fnodes:
235 for fnode in fnodes:
238 if fctx is None:
236 if fctx is None:
239 fctx = repo.filectx(b'.hgtags', fileid=fnode)
237 fctx = repo.filectx(b'.hgtags', fileid=fnode)
240 else:
238 else:
241 fctx = fctx.filectx(fnode)
239 fctx = fctx.filectx(fnode)
242 filetags = _readtags(ui, repo, fctx.data().splitlines(), fctx)
240 filetags = _readtags(ui, repo, fctx.data().splitlines(), fctx)
243 _updatetags(filetags, alltags)
241 _updatetags(filetags, alltags)
244 return alltags
242 return alltags
245
243
246
244
247 def readlocaltags(ui, repo, alltags, tagtypes):
245 def readlocaltags(ui, repo, alltags, tagtypes):
248 '''Read local tags in repo. Update alltags and tagtypes.'''
246 '''Read local tags in repo. Update alltags and tagtypes.'''
249 try:
247 try:
250 data = repo.vfs.read(b"localtags")
248 data = repo.vfs.read(b"localtags")
251 except FileNotFoundError:
249 except FileNotFoundError:
252 return
250 return
253
251
254 # localtags is in the local encoding; re-encode to UTF-8 on
252 # localtags is in the local encoding; re-encode to UTF-8 on
255 # input for consistency with the rest of this module.
253 # input for consistency with the rest of this module.
256 filetags = _readtags(
254 filetags = _readtags(
257 ui, repo, data.splitlines(), b"localtags", recode=encoding.fromlocal
255 ui, repo, data.splitlines(), b"localtags", recode=encoding.fromlocal
258 )
256 )
259
257
260 # remove tags pointing to invalid nodes
258 # remove tags pointing to invalid nodes
261 cl = repo.changelog
259 cl = repo.changelog
262 for t in list(filetags):
260 for t in list(filetags):
263 try:
261 try:
264 cl.rev(filetags[t][0])
262 cl.rev(filetags[t][0])
265 except (LookupError, ValueError):
263 except (LookupError, ValueError):
266 del filetags[t]
264 del filetags[t]
267
265
268 _updatetags(filetags, alltags, b'local', tagtypes)
266 _updatetags(filetags, alltags, b'local', tagtypes)
269
267
270
268
271 def _readtaghist(ui, repo, lines, fn, recode=None, calcnodelines=False):
269 def _readtaghist(ui, repo, lines, fn, recode=None, calcnodelines=False):
272 """Read tag definitions from a file (or any source of lines).
270 """Read tag definitions from a file (or any source of lines).
273
271
274 This function returns two sortdicts with similar information:
272 This function returns two sortdicts with similar information:
275
273
276 - the first dict, bintaghist, contains the tag information as expected by
274 - the first dict, bintaghist, contains the tag information as expected by
277 the _readtags function, i.e. a mapping from tag name to (node, hist):
275 the _readtags function, i.e. a mapping from tag name to (node, hist):
278 - node is the node id from the last line read for that name,
276 - node is the node id from the last line read for that name,
279 - hist is the list of node ids previously associated with it (in file
277 - hist is the list of node ids previously associated with it (in file
280 order). All node ids are binary, not hex.
278 order). All node ids are binary, not hex.
281
279
282 - the second dict, hextaglines, is a mapping from tag name to a list of
280 - the second dict, hextaglines, is a mapping from tag name to a list of
283 [hexnode, line number] pairs, ordered from the oldest to the newest node.
281 [hexnode, line number] pairs, ordered from the oldest to the newest node.
284
282
285 When calcnodelines is False the hextaglines dict is not calculated (an
283 When calcnodelines is False the hextaglines dict is not calculated (an
286 empty dict is returned). This is done to improve this function's
284 empty dict is returned). This is done to improve this function's
287 performance in cases where the line numbers are not needed.
285 performance in cases where the line numbers are not needed.
288 """
286 """
289
287
290 bintaghist = util.sortdict()
288 bintaghist = util.sortdict()
291 hextaglines = util.sortdict()
289 hextaglines = util.sortdict()
292 count = 0
290 count = 0
293
291
294 def dbg(msg):
292 def dbg(msg):
295 ui.debug(b"%s, line %d: %s\n" % (fn, count, msg))
293 ui.debug(b"%s, line %d: %s\n" % (fn, count, msg))
296
294
297 for nline, line in enumerate(lines):
295 for nline, line in enumerate(lines):
298 count += 1
296 count += 1
299 if not line:
297 if not line:
300 continue
298 continue
301 try:
299 try:
302 (nodehex, name) = line.split(b" ", 1)
300 (nodehex, name) = line.split(b" ", 1)
303 except ValueError:
301 except ValueError:
304 dbg(b"cannot parse entry")
302 dbg(b"cannot parse entry")
305 continue
303 continue
306 name = name.strip()
304 name = name.strip()
307 if recode:
305 if recode:
308 name = recode(name)
306 name = recode(name)
309 try:
307 try:
310 nodebin = bin(nodehex)
308 nodebin = bin(nodehex)
311 except binascii.Error:
309 except binascii.Error:
312 dbg(b"node '%s' is not well formed" % nodehex)
310 dbg(b"node '%s' is not well formed" % nodehex)
313 continue
311 continue
314
312
315 # update filetags
313 # update filetags
316 if calcnodelines:
314 if calcnodelines:
317 # map tag name to a list of line numbers
315 # map tag name to a list of line numbers
318 if name not in hextaglines:
316 if name not in hextaglines:
319 hextaglines[name] = []
317 hextaglines[name] = []
320 hextaglines[name].append([nodehex, nline])
318 hextaglines[name].append([nodehex, nline])
321 continue
319 continue
322 # map tag name to (node, hist)
320 # map tag name to (node, hist)
323 if name not in bintaghist:
321 if name not in bintaghist:
324 bintaghist[name] = []
322 bintaghist[name] = []
325 bintaghist[name].append(nodebin)
323 bintaghist[name].append(nodebin)
326 return bintaghist, hextaglines
324 return bintaghist, hextaglines
327
325
328
326
329 def _readtags(ui, repo, lines, fn, recode=None, calcnodelines=False):
327 def _readtags(ui, repo, lines, fn, recode=None, calcnodelines=False):
330 """Read tag definitions from a file (or any source of lines).
328 """Read tag definitions from a file (or any source of lines).
331
329
332 Returns a mapping from tag name to (node, hist).
330 Returns a mapping from tag name to (node, hist).
333
331
334 "node" is the node id from the last line read for that name. "hist"
332 "node" is the node id from the last line read for that name. "hist"
335 is the list of node ids previously associated with it (in file order).
333 is the list of node ids previously associated with it (in file order).
336 All node ids are binary, not hex.
334 All node ids are binary, not hex.
337 """
335 """
338 filetags, nodelines = _readtaghist(
336 filetags, nodelines = _readtaghist(
339 ui, repo, lines, fn, recode=recode, calcnodelines=calcnodelines
337 ui, repo, lines, fn, recode=recode, calcnodelines=calcnodelines
340 )
338 )
341 # util.sortdict().__setitem__ is much slower at replacing then inserting
339 # util.sortdict().__setitem__ is much slower at replacing then inserting
342 # new entries. The difference can matter if there are thousands of tags.
340 # new entries. The difference can matter if there are thousands of tags.
343 # Create a new sortdict to avoid the performance penalty.
341 # Create a new sortdict to avoid the performance penalty.
344 newtags = util.sortdict()
342 newtags = util.sortdict()
345 for tag, taghist in filetags.items():
343 for tag, taghist in filetags.items():
346 newtags[tag] = (taghist[-1], taghist[:-1])
344 newtags[tag] = (taghist[-1], taghist[:-1])
347 return newtags
345 return newtags
348
346
349
347
350 def _updatetags(filetags, alltags, tagtype=None, tagtypes=None):
348 def _updatetags(filetags, alltags, tagtype=None, tagtypes=None):
351 """Incorporate the tag info read from one file into dictionnaries
349 """Incorporate the tag info read from one file into dictionnaries
352
350
353 The first one, 'alltags', is a "tagmaps" (see 'findglobaltags' for details).
351 The first one, 'alltags', is a "tagmaps" (see 'findglobaltags' for details).
354
352
355 The second one, 'tagtypes', is optional and will be updated to track the
353 The second one, 'tagtypes', is optional and will be updated to track the
356 "tagtype" of entries in the tagmaps. When set, the 'tagtype' argument also
354 "tagtype" of entries in the tagmaps. When set, the 'tagtype' argument also
357 needs to be set."""
355 needs to be set."""
358 if tagtype is None:
356 if tagtype is None:
359 assert tagtypes is None
357 assert tagtypes is None
360
358
361 for name, nodehist in filetags.items():
359 for name, nodehist in filetags.items():
362 if name not in alltags:
360 if name not in alltags:
363 alltags[name] = nodehist
361 alltags[name] = nodehist
364 if tagtype is not None:
362 if tagtype is not None:
365 tagtypes[name] = tagtype
363 tagtypes[name] = tagtype
366 continue
364 continue
367
365
368 # we prefer alltags[name] if:
366 # we prefer alltags[name] if:
369 # it supersedes us OR
367 # it supersedes us OR
370 # mutual supersedes and it has a higher rank
368 # mutual supersedes and it has a higher rank
371 # otherwise we win because we're tip-most
369 # otherwise we win because we're tip-most
372 anode, ahist = nodehist
370 anode, ahist = nodehist
373 bnode, bhist = alltags[name]
371 bnode, bhist = alltags[name]
374 if (
372 if (
375 bnode != anode
373 bnode != anode
376 and anode in bhist
374 and anode in bhist
377 and (bnode not in ahist or len(bhist) > len(ahist))
375 and (bnode not in ahist or len(bhist) > len(ahist))
378 ):
376 ):
379 anode = bnode
377 anode = bnode
380 elif tagtype is not None:
378 elif tagtype is not None:
381 tagtypes[name] = tagtype
379 tagtypes[name] = tagtype
382 ahist.extend([n for n in bhist if n not in ahist])
380 ahist.extend([n for n in bhist if n not in ahist])
383 alltags[name] = anode, ahist
381 alltags[name] = anode, ahist
384
382
385
383
386 def _filename(repo):
384 def _filename(repo):
387 """name of a tagcache file for a given repo or repoview"""
385 """name of a tagcache file for a given repo or repoview"""
388 filename = b'tags2'
386 filename = b'tags2'
389 if repo.filtername:
387 if repo.filtername:
390 filename = b'%s-%s' % (filename, repo.filtername)
388 filename = b'%s-%s' % (filename, repo.filtername)
391 return filename
389 return filename
392
390
393
391
394 def _readtagcache(ui, repo):
392 def _readtagcache(ui, repo):
395 """Read the tag cache.
393 """Read the tag cache.
396
394
397 Returns a tuple (heads, fnodes, validinfo, cachetags, shouldwrite).
395 Returns a tuple (heads, fnodes, validinfo, cachetags, shouldwrite).
398
396
399 If the cache is completely up-to-date, "cachetags" is a dict of the
397 If the cache is completely up-to-date, "cachetags" is a dict of the
400 form returned by _readtags() and "heads", "fnodes", and "validinfo" are
398 form returned by _readtags() and "heads", "fnodes", and "validinfo" are
401 None and "shouldwrite" is False.
399 None and "shouldwrite" is False.
402
400
403 If the cache is not up to date, "cachetags" is None. "heads" is a list
401 If the cache is not up to date, "cachetags" is None. "heads" is a list
404 of all heads currently in the repository, ordered from tip to oldest.
402 of all heads currently in the repository, ordered from tip to oldest.
405 "validinfo" is a tuple describing cache validation info. This is used
403 "validinfo" is a tuple describing cache validation info. This is used
406 when writing the tags cache. "fnodes" is a mapping from head to .hgtags
404 when writing the tags cache. "fnodes" is a mapping from head to .hgtags
407 filenode. "shouldwrite" is True.
405 filenode. "shouldwrite" is True.
408
406
409 If the cache is not up to date, the caller is responsible for reading tag
407 If the cache is not up to date, the caller is responsible for reading tag
410 info from each returned head. (See findglobaltags().)
408 info from each returned head. (See findglobaltags().)
411 """
409 """
412 try:
410 try:
413 cachefile = repo.cachevfs(_filename(repo), b'r')
411 cachefile = repo.cachevfs(_filename(repo), b'r')
414 # force reading the file for static-http
412 # force reading the file for static-http
415 cachelines = iter(cachefile)
413 cachelines = iter(cachefile)
416 except IOError:
414 except IOError:
417 cachefile = None
415 cachefile = None
418
416
419 cacherev = None
417 cacherev = None
420 cachenode = None
418 cachenode = None
421 cachehash = None
419 cachehash = None
422 if cachefile:
420 if cachefile:
423 try:
421 try:
424 validline = next(cachelines)
422 validline = next(cachelines)
425 validline = validline.split()
423 validline = validline.split()
426 cacherev = int(validline[0])
424 cacherev = int(validline[0])
427 cachenode = bin(validline[1])
425 cachenode = bin(validline[1])
428 if len(validline) > 2:
426 if len(validline) > 2:
429 cachehash = bin(validline[2])
427 cachehash = bin(validline[2])
430 except Exception:
428 except Exception:
431 # corruption of the cache, just recompute it.
429 # corruption of the cache, just recompute it.
432 pass
430 pass
433
431
434 tipnode = repo.changelog.tip()
432 tipnode = repo.changelog.tip()
435 tiprev = len(repo.changelog) - 1
433 tiprev = len(repo.changelog) - 1
436
434
437 # Case 1 (common): tip is the same, so nothing has changed.
435 # Case 1 (common): tip is the same, so nothing has changed.
438 # (Unchanged tip trivially means no changesets have been added.
436 # (Unchanged tip trivially means no changesets have been added.
439 # But, thanks to localrepository.destroyed(), it also means none
437 # But, thanks to localrepository.destroyed(), it also means none
440 # have been destroyed by strip or rollback.)
438 # have been destroyed by strip or rollback.)
441 if (
439 if (
442 cacherev == tiprev
440 cacherev == tiprev
443 and cachenode == tipnode
441 and cachenode == tipnode
444 and cachehash
442 and cachehash
445 == scmutil.combined_filtered_and_obsolete_hash(
443 == scmutil.combined_filtered_and_obsolete_hash(
446 repo,
444 repo,
447 tiprev,
445 tiprev,
448 )
446 )
449 ):
447 ):
450 tags = _readtags(ui, repo, cachelines, cachefile.name)
448 tags = _readtags(ui, repo, cachelines, cachefile.name)
451 cachefile.close()
449 cachefile.close()
452 return (None, None, None, tags, False)
450 return (None, None, None, tags, False)
453 if cachefile:
451 if cachefile:
454 cachefile.close() # ignore rest of file
452 cachefile.close() # ignore rest of file
455
453
456 valid = (
454 valid = (
457 tiprev,
455 tiprev,
458 tipnode,
456 tipnode,
459 scmutil.combined_filtered_and_obsolete_hash(
457 scmutil.combined_filtered_and_obsolete_hash(
460 repo,
458 repo,
461 tiprev,
459 tiprev,
462 ),
460 ),
463 )
461 )
464
462
465 repoheads = repo.heads()
463 repoheads = repo.heads()
466 # Case 2 (uncommon): empty repo; get out quickly and don't bother
464 # Case 2 (uncommon): empty repo; get out quickly and don't bother
467 # writing an empty cache.
465 # writing an empty cache.
468 if repoheads == [repo.nullid]:
466 if repoheads == [repo.nullid]:
469 return ([], {}, valid, {}, False)
467 return ([], {}, valid, {}, False)
470
468
471 # Case 3 (uncommon): cache file missing or empty.
469 # Case 3 (uncommon): cache file missing or empty.
472
470
473 # Case 4 (uncommon): tip rev decreased. This should only happen
471 # Case 4 (uncommon): tip rev decreased. This should only happen
474 # when we're called from localrepository.destroyed(). Refresh the
472 # when we're called from localrepository.destroyed(). Refresh the
475 # cache so future invocations will not see disappeared heads in the
473 # cache so future invocations will not see disappeared heads in the
476 # cache.
474 # cache.
477
475
478 # Case 5 (common): tip has changed, so we've added/replaced heads.
476 # Case 5 (common): tip has changed, so we've added/replaced heads.
479
477
480 # As it happens, the code to handle cases 3, 4, 5 is the same.
478 # As it happens, the code to handle cases 3, 4, 5 is the same.
481
479
482 # N.B. in case 4 (nodes destroyed), "new head" really means "newly
480 # N.B. in case 4 (nodes destroyed), "new head" really means "newly
483 # exposed".
481 # exposed".
484 if not len(repo.file(b'.hgtags')):
482 if not len(repo.file(b'.hgtags')):
485 # No tags have ever been committed, so we can avoid a
483 # No tags have ever been committed, so we can avoid a
486 # potentially expensive search.
484 # potentially expensive search.
487 return ([], {}, valid, None, True)
485 return ([], {}, valid, None, True)
488
486
489 # Now we have to lookup the .hgtags filenode for every new head.
487 # Now we have to lookup the .hgtags filenode for every new head.
490 # This is the most expensive part of finding tags, so performance
488 # This is the most expensive part of finding tags, so performance
491 # depends primarily on the size of newheads. Worst case: no cache
489 # depends primarily on the size of newheads. Worst case: no cache
492 # file, so newheads == repoheads.
490 # file, so newheads == repoheads.
493 # Reversed order helps the cache ('repoheads' is in descending order)
491 # Reversed order helps the cache ('repoheads' is in descending order)
494 cachefnode = _getfnodes(ui, repo, reversed(repoheads))
492 cachefnode = _getfnodes(ui, repo, reversed(repoheads))
495
493
496 # Caller has to iterate over all heads, but can use the filenodes in
494 # Caller has to iterate over all heads, but can use the filenodes in
497 # cachefnode to get to each .hgtags revision quickly.
495 # cachefnode to get to each .hgtags revision quickly.
498 return (repoheads, cachefnode, valid, None, True)
496 return (repoheads, cachefnode, valid, None, True)
499
497
500
498
501 def _getfnodes(ui, repo, nodes):
499 def _getfnodes(ui, repo, nodes=None, revs=None):
502 """return .hgtags fnodes for a list of changeset nodes
500 """return .hgtags fnodes for a list of changeset nodes
503
501
504 Return value is a {node: fnode} mapping. There will be no entry for nodes
502 Return value is a {node: fnode} mapping. There will be no entry for nodes
505 without a '.hgtags' file.
503 without a '.hgtags' file.
506 """
504 """
507 starttime = util.timer()
505 starttime = util.timer()
508 fnodescache = hgtagsfnodescache(repo.unfiltered())
506 fnodescache = hgtagsfnodescache(repo.unfiltered())
509 cachefnode = {}
507 cachefnode = {}
510 validated_fnodes = set()
508 validated_fnodes = set()
511 unknown_entries = set()
509 unknown_entries = set()
512
510
511 if nodes is None and revs is None:
512 raise error.ProgrammingError("need to specify either nodes or revs")
513 elif nodes is not None and revs is None:
514 to_rev = repo.changelog.index.rev
515 nodes_revs = ((n, to_rev(n)) for n in nodes)
516 elif nodes is None and revs is not None:
517 to_node = repo.changelog.node
518 nodes_revs = ((to_node(r), r) for r in revs)
519 else:
520 msg = "need to specify only one of nodes or revs"
521 raise error.ProgrammingError(msg)
522
513 flog = None
523 flog = None
514 for node in nodes:
524 for node, rev in nodes_revs:
515 fnode = fnodescache.getfnode(node)
525 fnode = fnodescache.getfnode(node=node, rev=rev)
516 if fnode != repo.nullid:
526 if fnode != repo.nullid:
517 if fnode not in validated_fnodes:
527 if fnode not in validated_fnodes:
518 if flog is None:
528 if flog is None:
519 flog = repo.file(b'.hgtags')
529 flog = repo.file(b'.hgtags')
520 if flog.hasnode(fnode):
530 if flog.hasnode(fnode):
521 validated_fnodes.add(fnode)
531 validated_fnodes.add(fnode)
522 else:
532 else:
523 unknown_entries.add(node)
533 unknown_entries.add(node)
524 cachefnode[node] = fnode
534 cachefnode[node] = fnode
525
535
526 if unknown_entries:
536 if unknown_entries:
527 fixed_nodemap = fnodescache.refresh_invalid_nodes(unknown_entries)
537 fixed_nodemap = fnodescache.refresh_invalid_nodes(unknown_entries)
528 for node, fnode in fixed_nodemap.items():
538 for node, fnode in fixed_nodemap.items():
529 if fnode != repo.nullid:
539 if fnode != repo.nullid:
530 cachefnode[node] = fnode
540 cachefnode[node] = fnode
531
541
532 fnodescache.write()
542 fnodescache.write()
533
543
534 duration = util.timer() - starttime
544 duration = util.timer() - starttime
535 ui.log(
545 ui.log(
536 b'tagscache',
546 b'tagscache',
537 b'%d/%d cache hits/lookups in %0.4f seconds\n',
547 b'%d/%d cache hits/lookups in %0.4f seconds\n',
538 fnodescache.hitcount,
548 fnodescache.hitcount,
539 fnodescache.lookupcount,
549 fnodescache.lookupcount,
540 duration,
550 duration,
541 )
551 )
542 return cachefnode
552 return cachefnode
543
553
544
554
545 def _writetagcache(ui, repo, valid, cachetags):
555 def _writetagcache(ui, repo, valid, cachetags):
546 filename = _filename(repo)
556 filename = _filename(repo)
547 try:
557 try:
548 cachefile = repo.cachevfs(filename, b'w', atomictemp=True)
558 cachefile = repo.cachevfs(filename, b'w', atomictemp=True)
549 except (OSError, IOError):
559 except (OSError, IOError):
550 return
560 return
551
561
552 ui.log(
562 ui.log(
553 b'tagscache',
563 b'tagscache',
554 b'writing .hg/cache/%s with %d tags\n',
564 b'writing .hg/cache/%s with %d tags\n',
555 filename,
565 filename,
556 len(cachetags),
566 len(cachetags),
557 )
567 )
558
568
559 if valid[2]:
569 if valid[2]:
560 cachefile.write(
570 cachefile.write(
561 b'%d %s %s\n' % (valid[0], hex(valid[1]), hex(valid[2]))
571 b'%d %s %s\n' % (valid[0], hex(valid[1]), hex(valid[2]))
562 )
572 )
563 else:
573 else:
564 cachefile.write(b'%d %s\n' % (valid[0], hex(valid[1])))
574 cachefile.write(b'%d %s\n' % (valid[0], hex(valid[1])))
565
575
566 # Tag names in the cache are in UTF-8 -- which is the whole reason
576 # Tag names in the cache are in UTF-8 -- which is the whole reason
567 # we keep them in UTF-8 throughout this module. If we converted
577 # we keep them in UTF-8 throughout this module. If we converted
568 # them local encoding on input, we would lose info writing them to
578 # them local encoding on input, we would lose info writing them to
569 # the cache.
579 # the cache.
570 for (name, (node, hist)) in sorted(cachetags.items()):
580 for (name, (node, hist)) in sorted(cachetags.items()):
571 for n in hist:
581 for n in hist:
572 cachefile.write(b"%s %s\n" % (hex(n), name))
582 cachefile.write(b"%s %s\n" % (hex(n), name))
573 cachefile.write(b"%s %s\n" % (hex(node), name))
583 cachefile.write(b"%s %s\n" % (hex(node), name))
574
584
575 try:
585 try:
576 cachefile.close()
586 cachefile.close()
577 except (OSError, IOError):
587 except (OSError, IOError):
578 pass
588 pass
579
589
580
590
581 def tag(repo, names, node, message, local, user, date, editor=False):
591 def tag(repo, names, node, message, local, user, date, editor=False):
582 """tag a revision with one or more symbolic names.
592 """tag a revision with one or more symbolic names.
583
593
584 names is a list of strings or, when adding a single tag, names may be a
594 names is a list of strings or, when adding a single tag, names may be a
585 string.
595 string.
586
596
587 if local is True, the tags are stored in a per-repository file.
597 if local is True, the tags are stored in a per-repository file.
588 otherwise, they are stored in the .hgtags file, and a new
598 otherwise, they are stored in the .hgtags file, and a new
589 changeset is committed with the change.
599 changeset is committed with the change.
590
600
591 keyword arguments:
601 keyword arguments:
592
602
593 local: whether to store tags in non-version-controlled file
603 local: whether to store tags in non-version-controlled file
594 (default False)
604 (default False)
595
605
596 message: commit message to use if committing
606 message: commit message to use if committing
597
607
598 user: name of user to use if committing
608 user: name of user to use if committing
599
609
600 date: date tuple to use if committing"""
610 date: date tuple to use if committing"""
601
611
602 if not local:
612 if not local:
603 m = matchmod.exact([b'.hgtags'])
613 m = matchmod.exact([b'.hgtags'])
604 st = repo.status(match=m, unknown=True, ignored=True)
614 st = repo.status(match=m, unknown=True, ignored=True)
605 if any(
615 if any(
606 (
616 (
607 st.modified,
617 st.modified,
608 st.added,
618 st.added,
609 st.removed,
619 st.removed,
610 st.deleted,
620 st.deleted,
611 st.unknown,
621 st.unknown,
612 st.ignored,
622 st.ignored,
613 )
623 )
614 ):
624 ):
615 raise error.Abort(
625 raise error.Abort(
616 _(b'working copy of .hgtags is changed'),
626 _(b'working copy of .hgtags is changed'),
617 hint=_(b'please commit .hgtags manually'),
627 hint=_(b'please commit .hgtags manually'),
618 )
628 )
619
629
620 with repo.wlock():
630 with repo.wlock():
621 repo.tags() # instantiate the cache
631 repo.tags() # instantiate the cache
622 _tag(repo, names, node, message, local, user, date, editor=editor)
632 _tag(repo, names, node, message, local, user, date, editor=editor)
623
633
624
634
625 def _tag(
635 def _tag(
626 repo, names, node, message, local, user, date, extra=None, editor=False
636 repo, names, node, message, local, user, date, extra=None, editor=False
627 ):
637 ):
628 if isinstance(names, bytes):
638 if isinstance(names, bytes):
629 names = (names,)
639 names = (names,)
630
640
631 branches = repo.branchmap()
641 branches = repo.branchmap()
632 for name in names:
642 for name in names:
633 repo.hook(b'pretag', throw=True, node=hex(node), tag=name, local=local)
643 repo.hook(b'pretag', throw=True, node=hex(node), tag=name, local=local)
634 if name in branches:
644 if name in branches:
635 repo.ui.warn(
645 repo.ui.warn(
636 _(b"warning: tag %s conflicts with existing branch name\n")
646 _(b"warning: tag %s conflicts with existing branch name\n")
637 % name
647 % name
638 )
648 )
639
649
640 def writetags(fp, names, munge, prevtags):
650 def writetags(fp, names, munge, prevtags):
641 fp.seek(0, io.SEEK_END)
651 fp.seek(0, io.SEEK_END)
642 if prevtags and not prevtags.endswith(b'\n'):
652 if prevtags and not prevtags.endswith(b'\n'):
643 fp.write(b'\n')
653 fp.write(b'\n')
644 for name in names:
654 for name in names:
645 if munge:
655 if munge:
646 m = munge(name)
656 m = munge(name)
647 else:
657 else:
648 m = name
658 m = name
649
659
650 if repo._tagscache.tagtypes and name in repo._tagscache.tagtypes:
660 if repo._tagscache.tagtypes and name in repo._tagscache.tagtypes:
651 old = repo.tags().get(name, repo.nullid)
661 old = repo.tags().get(name, repo.nullid)
652 fp.write(b'%s %s\n' % (hex(old), m))
662 fp.write(b'%s %s\n' % (hex(old), m))
653 fp.write(b'%s %s\n' % (hex(node), m))
663 fp.write(b'%s %s\n' % (hex(node), m))
654 fp.close()
664 fp.close()
655
665
656 prevtags = b''
666 prevtags = b''
657 if local:
667 if local:
658 try:
668 try:
659 fp = repo.vfs(b'localtags', b'r+')
669 fp = repo.vfs(b'localtags', b'r+')
660 except IOError:
670 except IOError:
661 fp = repo.vfs(b'localtags', b'a')
671 fp = repo.vfs(b'localtags', b'a')
662 else:
672 else:
663 prevtags = fp.read()
673 prevtags = fp.read()
664
674
665 # local tags are stored in the current charset
675 # local tags are stored in the current charset
666 writetags(fp, names, None, prevtags)
676 writetags(fp, names, None, prevtags)
667 for name in names:
677 for name in names:
668 repo.hook(b'tag', node=hex(node), tag=name, local=local)
678 repo.hook(b'tag', node=hex(node), tag=name, local=local)
669 return
679 return
670
680
671 try:
681 try:
672 fp = repo.wvfs(b'.hgtags', b'rb+')
682 fp = repo.wvfs(b'.hgtags', b'rb+')
673 except FileNotFoundError:
683 except FileNotFoundError:
674 fp = repo.wvfs(b'.hgtags', b'ab')
684 fp = repo.wvfs(b'.hgtags', b'ab')
675 else:
685 else:
676 prevtags = fp.read()
686 prevtags = fp.read()
677
687
678 # committed tags are stored in UTF-8
688 # committed tags are stored in UTF-8
679 writetags(fp, names, encoding.fromlocal, prevtags)
689 writetags(fp, names, encoding.fromlocal, prevtags)
680
690
681 fp.close()
691 fp.close()
682
692
683 repo.invalidatecaches()
693 repo.invalidatecaches()
684
694
685 with repo.dirstate.changing_files(repo):
695 with repo.dirstate.changing_files(repo):
686 if b'.hgtags' not in repo.dirstate:
696 if b'.hgtags' not in repo.dirstate:
687 repo[None].add([b'.hgtags'])
697 repo[None].add([b'.hgtags'])
688
698
689 m = matchmod.exact([b'.hgtags'])
699 m = matchmod.exact([b'.hgtags'])
690 tagnode = repo.commit(
700 tagnode = repo.commit(
691 message, user, date, extra=extra, match=m, editor=editor
701 message, user, date, extra=extra, match=m, editor=editor
692 )
702 )
693
703
694 for name in names:
704 for name in names:
695 repo.hook(b'tag', node=hex(node), tag=name, local=local)
705 repo.hook(b'tag', node=hex(node), tag=name, local=local)
696
706
697 return tagnode
707 return tagnode
698
708
699
709
700 _fnodescachefile = b'hgtagsfnodes1'
710 _fnodescachefile = b'hgtagsfnodes1'
701 _fnodesrecsize = 4 + 20 # changeset fragment + filenode
711 _fnodesrecsize = 4 + 20 # changeset fragment + filenode
702 _fnodesmissingrec = b'\xff' * 24
712 _fnodesmissingrec = b'\xff' * 24
703
713
704
714
705 class hgtagsfnodescache:
715 class hgtagsfnodescache:
706 """Persistent cache mapping revisions to .hgtags filenodes.
716 """Persistent cache mapping revisions to .hgtags filenodes.
707
717
708 The cache is an array of records. Each item in the array corresponds to
718 The cache is an array of records. Each item in the array corresponds to
709 a changelog revision. Values in the array contain the first 4 bytes of
719 a changelog revision. Values in the array contain the first 4 bytes of
710 the node hash and the 20 bytes .hgtags filenode for that revision.
720 the node hash and the 20 bytes .hgtags filenode for that revision.
711
721
712 The first 4 bytes are present as a form of verification. Repository
722 The first 4 bytes are present as a form of verification. Repository
713 stripping and rewriting may change the node at a numeric revision in the
723 stripping and rewriting may change the node at a numeric revision in the
714 changelog. The changeset fragment serves as a verifier to detect
724 changelog. The changeset fragment serves as a verifier to detect
715 rewriting. This logic is shared with the rev branch cache (see
725 rewriting. This logic is shared with the rev branch cache (see
716 branchmap.py).
726 branchmap.py).
717
727
718 The instance holds in memory the full cache content but entries are
728 The instance holds in memory the full cache content but entries are
719 only parsed on read.
729 only parsed on read.
720
730
721 Instances behave like lists. ``c[i]`` works where i is a rev or
731 Instances behave like lists. ``c[i]`` works where i is a rev or
722 changeset node. Missing indexes are populated automatically on access.
732 changeset node. Missing indexes are populated automatically on access.
723 """
733 """
724
734
725 def __init__(self, repo):
735 def __init__(self, repo):
726 assert repo.filtername is None
736 assert repo.filtername is None
727
737
728 self._repo = repo
738 self._repo = repo
729
739
730 # Only for reporting purposes.
740 # Only for reporting purposes.
731 self.lookupcount = 0
741 self.lookupcount = 0
732 self.hitcount = 0
742 self.hitcount = 0
733
743
734 try:
744 try:
735 data = repo.cachevfs.read(_fnodescachefile)
745 data = repo.cachevfs.read(_fnodescachefile)
736 except (OSError, IOError):
746 except (OSError, IOError):
737 data = b""
747 data = b""
738 self._raw = bytearray(data)
748 self._raw = bytearray(data)
739
749
740 # The end state of self._raw is an array that is of the exact length
750 # The end state of self._raw is an array that is of the exact length
741 # required to hold a record for every revision in the repository.
751 # required to hold a record for every revision in the repository.
742 # We truncate or extend the array as necessary. self._dirtyoffset is
752 # We truncate or extend the array as necessary. self._dirtyoffset is
743 # defined to be the start offset at which we need to write the output
753 # defined to be the start offset at which we need to write the output
744 # file. This offset is also adjusted when new entries are calculated
754 # file. This offset is also adjusted when new entries are calculated
745 # for array members.
755 # for array members.
746 cllen = len(repo.changelog)
756 cllen = len(repo.changelog)
747 wantedlen = cllen * _fnodesrecsize
757 wantedlen = cllen * _fnodesrecsize
748 rawlen = len(self._raw)
758 rawlen = len(self._raw)
749
759
750 self._dirtyoffset = None
760 self._dirtyoffset = None
751
761
752 rawlentokeep = min(
762 rawlentokeep = min(
753 wantedlen, (rawlen // _fnodesrecsize) * _fnodesrecsize
763 wantedlen, (rawlen // _fnodesrecsize) * _fnodesrecsize
754 )
764 )
755 if rawlen > rawlentokeep:
765 if rawlen > rawlentokeep:
756 # There's no easy way to truncate array instances. This seems
766 # There's no easy way to truncate array instances. This seems
757 # slightly less evil than copying a potentially large array slice.
767 # slightly less evil than copying a potentially large array slice.
758 for i in range(rawlen - rawlentokeep):
768 for i in range(rawlen - rawlentokeep):
759 self._raw.pop()
769 self._raw.pop()
760 rawlen = len(self._raw)
770 rawlen = len(self._raw)
761 self._dirtyoffset = rawlen
771 self._dirtyoffset = rawlen
762 if rawlen < wantedlen:
772 if rawlen < wantedlen:
763 if self._dirtyoffset is None:
773 if self._dirtyoffset is None:
764 self._dirtyoffset = rawlen
774 self._dirtyoffset = rawlen
765 # TODO: zero fill entire record, because it's invalid not missing?
775 # TODO: zero fill entire record, because it's invalid not missing?
766 self._raw.extend(b'\xff' * (wantedlen - rawlen))
776 self._raw.extend(b'\xff' * (wantedlen - rawlen))
767
777
768 def getfnode(self, node, computemissing=True):
778 def getfnode(self, node, computemissing=True, rev=None):
769 """Obtain the filenode of the .hgtags file at a specified revision.
779 """Obtain the filenode of the .hgtags file at a specified revision.
770
780
771 If the value is in the cache, the entry will be validated and returned.
781 If the value is in the cache, the entry will be validated and returned.
772 Otherwise, the filenode will be computed and returned unless
782 Otherwise, the filenode will be computed and returned unless
773 "computemissing" is False. In that case, None will be returned if
783 "computemissing" is False. In that case, None will be returned if
774 the entry is missing or False if the entry is invalid without
784 the entry is missing or False if the entry is invalid without
775 any potentially expensive computation being performed.
785 any potentially expensive computation being performed.
776
786
777 If an .hgtags does not exist at the specified revision, nullid is
787 If an .hgtags does not exist at the specified revision, nullid is
778 returned.
788 returned.
779 """
789 """
780 if node == self._repo.nullid:
790 if node == self._repo.nullid:
781 return node
791 return node
782
792
783 rev = self._repo.changelog.rev(node)
793 if rev is None:
794 rev = self._repo.changelog.rev(node)
784
795
785 self.lookupcount += 1
796 self.lookupcount += 1
786
797
787 offset = rev * _fnodesrecsize
798 offset = rev * _fnodesrecsize
788 record = b'%s' % self._raw[offset : offset + _fnodesrecsize]
799 record = b'%s' % self._raw[offset : offset + _fnodesrecsize]
789 properprefix = node[0:4]
800 properprefix = node[0:4]
790
801
791 # Validate and return existing entry.
802 # Validate and return existing entry.
792 if record != _fnodesmissingrec and len(record) == _fnodesrecsize:
803 if record != _fnodesmissingrec and len(record) == _fnodesrecsize:
793 fileprefix = record[0:4]
804 fileprefix = record[0:4]
794
805
795 if fileprefix == properprefix:
806 if fileprefix == properprefix:
796 self.hitcount += 1
807 self.hitcount += 1
797 return record[4:]
808 return record[4:]
798
809
799 # Fall through.
810 # Fall through.
800
811
801 # If we get here, the entry is either missing or invalid.
812 # If we get here, the entry is either missing or invalid.
802
813
803 if not computemissing:
814 if not computemissing:
804 if record != _fnodesmissingrec:
815 if record != _fnodesmissingrec:
805 return False
816 return False
806 return None
817 return None
807
818
808 fnode = self._computefnode(node)
819 fnode = self._computefnode(node)
809 self._writeentry(offset, properprefix, fnode)
820 self._writeentry(offset, properprefix, fnode)
810 return fnode
821 return fnode
811
822
812 def _computefnode(self, node):
823 def _computefnode(self, node):
813 """Finds the tag filenode for a node which is missing or invalid
824 """Finds the tag filenode for a node which is missing or invalid
814 in cache"""
825 in cache"""
815 ctx = self._repo[node]
826 ctx = self._repo[node]
816 rev = ctx.rev()
827 rev = ctx.rev()
817 fnode = None
828 fnode = None
818 cl = self._repo.changelog
829 cl = self._repo.changelog
819 p1rev, p2rev = cl._uncheckedparentrevs(rev)
830 p1rev, p2rev = cl._uncheckedparentrevs(rev)
820 p1node = cl.node(p1rev)
831 p1node = cl.node(p1rev)
821 p1fnode = self.getfnode(p1node, computemissing=False)
832 p1fnode = self.getfnode(p1node, computemissing=False)
822 if p2rev != nullrev:
833 if p2rev != nullrev:
823 # There is some no-merge changeset where p1 is null and p2 is set
834 # There is some no-merge changeset where p1 is null and p2 is set
824 # Processing them as merge is just slower, but still gives a good
835 # Processing them as merge is just slower, but still gives a good
825 # result.
836 # result.
826 p2node = cl.node(p2rev)
837 p2node = cl.node(p2rev)
827 p2fnode = self.getfnode(p2node, computemissing=False)
838 p2fnode = self.getfnode(p2node, computemissing=False)
828 if p1fnode != p2fnode:
839 if p1fnode != p2fnode:
829 # we cannot rely on readfast because we don't know against what
840 # we cannot rely on readfast because we don't know against what
830 # parent the readfast delta is computed
841 # parent the readfast delta is computed
831 p1fnode = None
842 p1fnode = None
832 if p1fnode:
843 if p1fnode:
833 mctx = ctx.manifestctx()
844 mctx = ctx.manifestctx()
834 fnode = mctx.readfast().get(b'.hgtags')
845 fnode = mctx.readfast().get(b'.hgtags')
835 if fnode is None:
846 if fnode is None:
836 fnode = p1fnode
847 fnode = p1fnode
837 if fnode is None:
848 if fnode is None:
838 # Populate missing entry.
849 # Populate missing entry.
839 try:
850 try:
840 fnode = ctx.filenode(b'.hgtags')
851 fnode = ctx.filenode(b'.hgtags')
841 except error.LookupError:
852 except error.LookupError:
842 # No .hgtags file on this revision.
853 # No .hgtags file on this revision.
843 fnode = self._repo.nullid
854 fnode = self._repo.nullid
844 return fnode
855 return fnode
845
856
846 def setfnode(self, node, fnode):
857 def setfnode(self, node, fnode):
847 """Set the .hgtags filenode for a given changeset."""
858 """Set the .hgtags filenode for a given changeset."""
848 assert len(fnode) == 20
859 assert len(fnode) == 20
849 ctx = self._repo[node]
860 ctx = self._repo[node]
850
861
851 # Do a lookup first to avoid writing if nothing has changed.
862 # Do a lookup first to avoid writing if nothing has changed.
852 if self.getfnode(ctx.node(), computemissing=False) == fnode:
863 if self.getfnode(ctx.node(), computemissing=False) == fnode:
853 return
864 return
854
865
855 self._writeentry(ctx.rev() * _fnodesrecsize, node[0:4], fnode)
866 self._writeentry(ctx.rev() * _fnodesrecsize, node[0:4], fnode)
856
867
857 def refresh_invalid_nodes(self, nodes):
868 def refresh_invalid_nodes(self, nodes):
858 """recomputes file nodes for a given set of nodes which has unknown
869 """recomputes file nodes for a given set of nodes which has unknown
859 filenodes for them in the cache
870 filenodes for them in the cache
860 Also updates the in-memory cache with the correct filenode.
871 Also updates the in-memory cache with the correct filenode.
861 Caller needs to take care about calling `.write()` so that updates are
872 Caller needs to take care about calling `.write()` so that updates are
862 persisted.
873 persisted.
863 Returns a map {node: recomputed fnode}
874 Returns a map {node: recomputed fnode}
864 """
875 """
865 fixed_nodemap = {}
876 fixed_nodemap = {}
866 for node in nodes:
877 for node in nodes:
867 fnode = self._computefnode(node)
878 fnode = self._computefnode(node)
868 fixed_nodemap[node] = fnode
879 fixed_nodemap[node] = fnode
869 self.setfnode(node, fnode)
880 self.setfnode(node, fnode)
870 return fixed_nodemap
881 return fixed_nodemap
871
882
872 def _writeentry(self, offset, prefix, fnode):
883 def _writeentry(self, offset, prefix, fnode):
873 # Slices on array instances only accept other array.
884 # Slices on array instances only accept other array.
874 entry = bytearray(prefix + fnode)
885 entry = bytearray(prefix + fnode)
875 self._raw[offset : offset + _fnodesrecsize] = entry
886 self._raw[offset : offset + _fnodesrecsize] = entry
876 # self._dirtyoffset could be None.
887 # self._dirtyoffset could be None.
877 self._dirtyoffset = min(self._dirtyoffset or 0, offset or 0)
888 self._dirtyoffset = min(self._dirtyoffset or 0, offset or 0)
878
889
879 def write(self):
890 def write(self):
880 """Perform all necessary writes to cache file.
891 """Perform all necessary writes to cache file.
881
892
882 This may no-op if no writes are needed or if a write lock could
893 This may no-op if no writes are needed or if a write lock could
883 not be obtained.
894 not be obtained.
884 """
895 """
885 if self._dirtyoffset is None:
896 if self._dirtyoffset is None:
886 return
897 return
887
898
888 data = self._raw[self._dirtyoffset :]
899 data = self._raw[self._dirtyoffset :]
889 if not data:
900 if not data:
890 return
901 return
891
902
892 repo = self._repo
903 repo = self._repo
893
904
894 try:
905 try:
895 lock = repo.lock(wait=False)
906 lock = repo.lock(wait=False)
896 except error.LockError:
907 except error.LockError:
897 repo.ui.log(
908 repo.ui.log(
898 b'tagscache',
909 b'tagscache',
899 b'not writing .hg/cache/%s because '
910 b'not writing .hg/cache/%s because '
900 b'lock cannot be acquired\n' % _fnodescachefile,
911 b'lock cannot be acquired\n' % _fnodescachefile,
901 )
912 )
902 return
913 return
903
914
904 try:
915 try:
905 f = repo.cachevfs.open(_fnodescachefile, b'ab')
916 f = repo.cachevfs.open(_fnodescachefile, b'ab')
906 try:
917 try:
907 # if the file has been truncated
918 # if the file has been truncated
908 actualoffset = f.tell()
919 actualoffset = f.tell()
909 if actualoffset < self._dirtyoffset:
920 if actualoffset < self._dirtyoffset:
910 self._dirtyoffset = actualoffset
921 self._dirtyoffset = actualoffset
911 data = self._raw[self._dirtyoffset :]
922 data = self._raw[self._dirtyoffset :]
912 f.seek(self._dirtyoffset)
923 f.seek(self._dirtyoffset)
913 f.truncate()
924 f.truncate()
914 repo.ui.log(
925 repo.ui.log(
915 b'tagscache',
926 b'tagscache',
916 b'writing %d bytes to cache/%s\n'
927 b'writing %d bytes to cache/%s\n'
917 % (len(data), _fnodescachefile),
928 % (len(data), _fnodescachefile),
918 )
929 )
919 f.write(data)
930 f.write(data)
920 self._dirtyoffset = None
931 self._dirtyoffset = None
921 finally:
932 finally:
922 f.close()
933 f.close()
923 except (IOError, OSError) as inst:
934 except (IOError, OSError) as inst:
924 repo.ui.log(
935 repo.ui.log(
925 b'tagscache',
936 b'tagscache',
926 b"couldn't write cache/%s: %s\n"
937 b"couldn't write cache/%s: %s\n"
927 % (_fnodescachefile, stringutil.forcebytestr(inst)),
938 % (_fnodescachefile, stringutil.forcebytestr(inst)),
928 )
939 )
929 finally:
940 finally:
930 lock.release()
941 lock.release()
931
942
932
943
933 def clear_cache_on_disk(repo):
944 def clear_cache_on_disk(repo):
934 """function used by the perf extension to "tags" cache"""
945 """function used by the perf extension to "tags" cache"""
935 repo.cachevfs.tryunlink(_filename(repo))
946 repo.cachevfs.tryunlink(_filename(repo))
936
947
937
948
938 # a small attribute to help `hg perf::tags` to detect a fixed version.
949 # a small attribute to help `hg perf::tags` to detect a fixed version.
939 clear_cache_fnodes_is_working = True
950 clear_cache_fnodes_is_working = True
940
951
941
952
942 def clear_cache_fnodes(repo):
953 def clear_cache_fnodes(repo):
943 """function used by the perf extension to clear "file node cache"""
954 """function used by the perf extension to clear "file node cache"""
944 repo.cachevfs.tryunlink(_fnodescachefile)
955 repo.cachevfs.tryunlink(_fnodescachefile)
945
956
946
957
947 def forget_fnodes(repo, revs):
958 def forget_fnodes(repo, revs):
948 """function used by the perf extension to prune some entries from the fnodes
959 """function used by the perf extension to prune some entries from the fnodes
949 cache"""
960 cache"""
950 missing_1 = b'\xff' * 4
961 missing_1 = b'\xff' * 4
951 missing_2 = b'\xff' * 20
962 missing_2 = b'\xff' * 20
952 cache = hgtagsfnodescache(repo.unfiltered())
963 cache = hgtagsfnodescache(repo.unfiltered())
953 for r in revs:
964 for r in revs:
954 cache._writeentry(r * _fnodesrecsize, missing_1, missing_2)
965 cache._writeentry(r * _fnodesrecsize, missing_1, missing_2)
955 cache.write()
966 cache.write()
General Comments 0
You need to be logged in to leave comments. Login now