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