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