##// END OF EJS Templates
tags: make argument 'tagtype' optional in '_updatetags'...
Pierre-Yves David -
r31708:d0e7c70f default
parent child Browse files
Show More
@@ -1,676 +1,683 b''
1 # tags.py - read tag info from local repository
1 # tags.py - read tag info from local repository
2 #
2 #
3 # Copyright 2009 Matt Mackall <mpm@selenic.com>
3 # Copyright 2009 Matt Mackall <mpm@selenic.com>
4 # Copyright 2009 Greg Ward <greg@gerg.ca>
4 # Copyright 2009 Greg Ward <greg@gerg.ca>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 # Currently this module only deals with reading and caching tags.
9 # Currently this module only deals with reading and caching tags.
10 # Eventually, it could take care of updating (adding/removing/moving)
10 # Eventually, it could take care of updating (adding/removing/moving)
11 # tags too.
11 # tags too.
12
12
13 from __future__ import absolute_import
13 from __future__ import absolute_import
14
14
15 import errno
15 import errno
16
16
17 from .node import (
17 from .node import (
18 bin,
18 bin,
19 hex,
19 hex,
20 nullid,
20 nullid,
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
31
32 # Tags computation can be expensive and caches exist to make it fast in
32 # Tags computation can be expensive and caches exist to make it fast in
33 # the common case.
33 # the common case.
34 #
34 #
35 # The "hgtagsfnodes1" cache file caches the .hgtags filenode values for
35 # The "hgtagsfnodes1" cache file caches the .hgtags filenode values for
36 # each revision in the repository. The file is effectively an array of
36 # each revision in the repository. The file is effectively an array of
37 # fixed length records. Read the docs for "hgtagsfnodescache" for technical
37 # fixed length records. Read the docs for "hgtagsfnodescache" for technical
38 # details.
38 # details.
39 #
39 #
40 # The .hgtags filenode cache grows in proportion to the length of the
40 # The .hgtags filenode cache grows in proportion to the length of the
41 # changelog. The file is truncated when the # changelog is stripped.
41 # changelog. The file is truncated when the # changelog is stripped.
42 #
42 #
43 # The purpose of the filenode cache is to avoid the most expensive part
43 # The purpose of the filenode cache is to avoid the most expensive part
44 # of finding global tags, which is looking up the .hgtags filenode in the
44 # of finding global tags, which is looking up the .hgtags filenode in the
45 # manifest for each head. This can take dozens or over 100ms for
45 # manifest for each head. This can take dozens or over 100ms for
46 # repositories with very large manifests. Multiplied by dozens or even
46 # repositories with very large manifests. Multiplied by dozens or even
47 # hundreds of heads and there is a significant performance concern.
47 # hundreds of heads and there is a significant performance concern.
48 #
48 #
49 # There also exist a separate cache file for each repository filter.
49 # There also exist a separate cache file for each repository filter.
50 # These "tags-*" files store information about the history of tags.
50 # These "tags-*" files store information about the history of tags.
51 #
51 #
52 # The tags cache files consists of a cache validation line followed by
52 # The tags cache files consists of a cache validation line followed by
53 # a history of tags.
53 # a history of tags.
54 #
54 #
55 # The cache validation line has the format:
55 # The cache validation line has the format:
56 #
56 #
57 # <tiprev> <tipnode> [<filteredhash>]
57 # <tiprev> <tipnode> [<filteredhash>]
58 #
58 #
59 # <tiprev> is an integer revision and <tipnode> is a 40 character hex
59 # <tiprev> is an integer revision and <tipnode> is a 40 character hex
60 # node for that changeset. These redundantly identify the repository
60 # node for that changeset. These redundantly identify the repository
61 # tip from the time the cache was written. In addition, <filteredhash>,
61 # tip from the time the cache was written. In addition, <filteredhash>,
62 # if present, is a 40 character hex hash of the contents of the filtered
62 # if present, is a 40 character hex hash of the contents of the filtered
63 # revisions for this filter. If the set of filtered revs changes, the
63 # revisions for this filter. If the set of filtered revs changes, the
64 # hash will change and invalidate the cache.
64 # hash will change and invalidate the cache.
65 #
65 #
66 # The history part of the tags cache consists of lines of the form:
66 # The history part of the tags cache consists of lines of the form:
67 #
67 #
68 # <node> <tag>
68 # <node> <tag>
69 #
69 #
70 # (This format is identical to that of .hgtags files.)
70 # (This format is identical to that of .hgtags files.)
71 #
71 #
72 # <tag> is the tag name and <node> is the 40 character hex changeset
72 # <tag> is the tag name and <node> is the 40 character hex changeset
73 # the tag is associated with.
73 # the tag is associated with.
74 #
74 #
75 # Tags are written sorted by tag name.
75 # Tags are written sorted by tag name.
76 #
76 #
77 # Tags associated with multiple changesets have an entry for each changeset.
77 # Tags associated with multiple changesets have an entry for each changeset.
78 # The most recent changeset (in terms of revlog ordering for the head
78 # The most recent changeset (in terms of revlog ordering for the head
79 # setting it) for each tag is last.
79 # setting it) for each tag is last.
80
80
81 def findglobaltags(ui, repo):
81 def findglobaltags(ui, repo):
82 '''Find global tags in a repo: return (alltags, tagtypes)
82 '''Find global tags in a repo: return (alltags, tagtypes)
83
83
84 "alltags" maps tag name to (node, hist) 2-tuples.
84 "alltags" maps tag name to (node, hist) 2-tuples.
85
85
86 "tagtypes" maps tag name to tag type. Global tags always have the
86 "tagtypes" maps tag name to tag type. Global tags always have the
87 "global" tag type.
87 "global" tag type.
88
88
89 The tags cache is read and updated as a side-effect of calling.
89 The tags cache is read and updated as a side-effect of calling.
90 '''
90 '''
91 alltags = {}
91 alltags = {}
92 tagtypes = {}
92 tagtypes = {}
93
93
94 (heads, tagfnode, valid, cachetags, shouldwrite) = _readtagcache(ui, repo)
94 (heads, tagfnode, valid, cachetags, shouldwrite) = _readtagcache(ui, repo)
95 if cachetags is not None:
95 if cachetags is not None:
96 assert not shouldwrite
96 assert not shouldwrite
97 # XXX is this really 100% correct? are there oddball special
97 # XXX is this really 100% correct? are there oddball special
98 # cases where a global tag should outrank a local tag but won't,
98 # cases where a global tag should outrank a local tag but won't,
99 # because cachetags does not contain rank info?
99 # because cachetags does not contain rank info?
100 _updatetags(cachetags, alltags, 'global', tagtypes)
100 _updatetags(cachetags, alltags, 'global', tagtypes)
101 return alltags, tagtypes
101 return alltags, tagtypes
102
102
103 seen = set() # set of fnode
103 seen = set() # set of fnode
104 fctx = None
104 fctx = None
105 for head in reversed(heads): # oldest to newest
105 for head in reversed(heads): # oldest to newest
106 assert head in repo.changelog.nodemap, \
106 assert head in repo.changelog.nodemap, \
107 "tag cache returned bogus head %s" % short(head)
107 "tag cache returned bogus head %s" % short(head)
108
108
109 fnode = tagfnode.get(head)
109 fnode = tagfnode.get(head)
110 if fnode and fnode not in seen:
110 if fnode and fnode not in seen:
111 seen.add(fnode)
111 seen.add(fnode)
112 if not fctx:
112 if not fctx:
113 fctx = repo.filectx('.hgtags', fileid=fnode)
113 fctx = repo.filectx('.hgtags', fileid=fnode)
114 else:
114 else:
115 fctx = fctx.filectx(fnode)
115 fctx = fctx.filectx(fnode)
116
116
117 filetags = _readtags(ui, repo, fctx.data().splitlines(), fctx)
117 filetags = _readtags(ui, repo, fctx.data().splitlines(), fctx)
118 _updatetags(filetags, alltags, 'global', tagtypes)
118 _updatetags(filetags, alltags, 'global', tagtypes)
119
119
120 # and update the cache (if necessary)
120 # and update the cache (if necessary)
121 if shouldwrite:
121 if shouldwrite:
122 _writetagcache(ui, repo, valid, alltags)
122 _writetagcache(ui, repo, valid, alltags)
123 return alltags, tagtypes
123 return alltags, tagtypes
124
124
125 def readlocaltags(ui, repo, alltags, tagtypes):
125 def readlocaltags(ui, repo, alltags, tagtypes):
126 '''Read local tags in repo. Update alltags and tagtypes.'''
126 '''Read local tags in repo. Update alltags and tagtypes.'''
127 try:
127 try:
128 data = repo.vfs.read("localtags")
128 data = repo.vfs.read("localtags")
129 except IOError as inst:
129 except IOError as inst:
130 if inst.errno != errno.ENOENT:
130 if inst.errno != errno.ENOENT:
131 raise
131 raise
132 return
132 return
133
133
134 # localtags is in the local encoding; re-encode to UTF-8 on
134 # localtags is in the local encoding; re-encode to UTF-8 on
135 # input for consistency with the rest of this module.
135 # input for consistency with the rest of this module.
136 filetags = _readtags(
136 filetags = _readtags(
137 ui, repo, data.splitlines(), "localtags",
137 ui, repo, data.splitlines(), "localtags",
138 recode=encoding.fromlocal)
138 recode=encoding.fromlocal)
139
139
140 # remove tags pointing to invalid nodes
140 # remove tags pointing to invalid nodes
141 cl = repo.changelog
141 cl = repo.changelog
142 for t in filetags.keys():
142 for t in filetags.keys():
143 try:
143 try:
144 cl.rev(filetags[t][0])
144 cl.rev(filetags[t][0])
145 except (LookupError, ValueError):
145 except (LookupError, ValueError):
146 del filetags[t]
146 del filetags[t]
147
147
148 _updatetags(filetags, alltags, 'local', tagtypes)
148 _updatetags(filetags, alltags, 'local', tagtypes)
149
149
150 def _readtaghist(ui, repo, lines, fn, recode=None, calcnodelines=False):
150 def _readtaghist(ui, repo, lines, fn, recode=None, calcnodelines=False):
151 '''Read tag definitions from a file (or any source of lines).
151 '''Read tag definitions from a file (or any source of lines).
152
152
153 This function returns two sortdicts with similar information:
153 This function returns two sortdicts with similar information:
154
154
155 - the first dict, bintaghist, contains the tag information as expected by
155 - the first dict, bintaghist, contains the tag information as expected by
156 the _readtags function, i.e. a mapping from tag name to (node, hist):
156 the _readtags function, i.e. a mapping from tag name to (node, hist):
157 - node is the node id from the last line read for that name,
157 - node is the node id from the last line read for that name,
158 - hist is the list of node ids previously associated with it (in file
158 - hist is the list of node ids previously associated with it (in file
159 order). All node ids are binary, not hex.
159 order). All node ids are binary, not hex.
160
160
161 - the second dict, hextaglines, is a mapping from tag name to a list of
161 - the second dict, hextaglines, is a mapping from tag name to a list of
162 [hexnode, line number] pairs, ordered from the oldest to the newest node.
162 [hexnode, line number] pairs, ordered from the oldest to the newest node.
163
163
164 When calcnodelines is False the hextaglines dict is not calculated (an
164 When calcnodelines is False the hextaglines dict is not calculated (an
165 empty dict is returned). This is done to improve this function's
165 empty dict is returned). This is done to improve this function's
166 performance in cases where the line numbers are not needed.
166 performance in cases where the line numbers are not needed.
167 '''
167 '''
168
168
169 bintaghist = util.sortdict()
169 bintaghist = util.sortdict()
170 hextaglines = util.sortdict()
170 hextaglines = util.sortdict()
171 count = 0
171 count = 0
172
172
173 def dbg(msg):
173 def dbg(msg):
174 ui.debug("%s, line %s: %s\n" % (fn, count, msg))
174 ui.debug("%s, line %s: %s\n" % (fn, count, msg))
175
175
176 for nline, line in enumerate(lines):
176 for nline, line in enumerate(lines):
177 count += 1
177 count += 1
178 if not line:
178 if not line:
179 continue
179 continue
180 try:
180 try:
181 (nodehex, name) = line.split(" ", 1)
181 (nodehex, name) = line.split(" ", 1)
182 except ValueError:
182 except ValueError:
183 dbg("cannot parse entry")
183 dbg("cannot parse entry")
184 continue
184 continue
185 name = name.strip()
185 name = name.strip()
186 if recode:
186 if recode:
187 name = recode(name)
187 name = recode(name)
188 try:
188 try:
189 nodebin = bin(nodehex)
189 nodebin = bin(nodehex)
190 except TypeError:
190 except TypeError:
191 dbg("node '%s' is not well formed" % nodehex)
191 dbg("node '%s' is not well formed" % nodehex)
192 continue
192 continue
193
193
194 # update filetags
194 # update filetags
195 if calcnodelines:
195 if calcnodelines:
196 # map tag name to a list of line numbers
196 # map tag name to a list of line numbers
197 if name not in hextaglines:
197 if name not in hextaglines:
198 hextaglines[name] = []
198 hextaglines[name] = []
199 hextaglines[name].append([nodehex, nline])
199 hextaglines[name].append([nodehex, nline])
200 continue
200 continue
201 # map tag name to (node, hist)
201 # map tag name to (node, hist)
202 if name not in bintaghist:
202 if name not in bintaghist:
203 bintaghist[name] = []
203 bintaghist[name] = []
204 bintaghist[name].append(nodebin)
204 bintaghist[name].append(nodebin)
205 return bintaghist, hextaglines
205 return bintaghist, hextaglines
206
206
207 def _readtags(ui, repo, lines, fn, recode=None, calcnodelines=False):
207 def _readtags(ui, repo, lines, fn, recode=None, calcnodelines=False):
208 '''Read tag definitions from a file (or any source of lines).
208 '''Read tag definitions from a file (or any source of lines).
209
209
210 Returns a mapping from tag name to (node, hist).
210 Returns a mapping from tag name to (node, hist).
211
211
212 "node" is the node id from the last line read for that name. "hist"
212 "node" is the node id from the last line read for that name. "hist"
213 is the list of node ids previously associated with it (in file order).
213 is the list of node ids previously associated with it (in file order).
214 All node ids are binary, not hex.
214 All node ids are binary, not hex.
215 '''
215 '''
216 filetags, nodelines = _readtaghist(ui, repo, lines, fn, recode=recode,
216 filetags, nodelines = _readtaghist(ui, repo, lines, fn, recode=recode,
217 calcnodelines=calcnodelines)
217 calcnodelines=calcnodelines)
218 # util.sortdict().__setitem__ is much slower at replacing then inserting
218 # util.sortdict().__setitem__ is much slower at replacing then inserting
219 # new entries. The difference can matter if there are thousands of tags.
219 # new entries. The difference can matter if there are thousands of tags.
220 # Create a new sortdict to avoid the performance penalty.
220 # Create a new sortdict to avoid the performance penalty.
221 newtags = util.sortdict()
221 newtags = util.sortdict()
222 for tag, taghist in filetags.items():
222 for tag, taghist in filetags.items():
223 newtags[tag] = (taghist[-1], taghist[:-1])
223 newtags[tag] = (taghist[-1], taghist[:-1])
224 return newtags
224 return newtags
225
225
226 def _updatetags(filetags, alltags, tagtype, tagtypes):
226 def _updatetags(filetags, alltags, tagtype=None, tagtypes=None):
227 '''Incorporate the tag info read from one file into the two
227 """Incorporate the tag info read from one file into dictionnaries
228 dictionaries, alltags and tagtypes, that contain all tag
228
229 info (global across all heads plus local).'''
229 The first one, 'alltags', is a "tagmaps" (see 'findglobaltags' for details).
230
231 The second one, 'tagtypes', is optional and will be updated to track the
232 "tagtype" of entries in the tagmaps. When set, the 'tagtype' argument also
233 needs to be set."""
234 if tagtype is None:
235 assert tagtypes is None
230
236
231 for name, nodehist in filetags.iteritems():
237 for name, nodehist in filetags.iteritems():
232 if name not in alltags:
238 if name not in alltags:
233 alltags[name] = nodehist
239 alltags[name] = nodehist
240 if tagtype is not None:
234 tagtypes[name] = tagtype
241 tagtypes[name] = tagtype
235 continue
242 continue
236
243
237 # we prefer alltags[name] if:
244 # we prefer alltags[name] if:
238 # it supersedes us OR
245 # it supersedes us OR
239 # mutual supersedes and it has a higher rank
246 # mutual supersedes and it has a higher rank
240 # otherwise we win because we're tip-most
247 # otherwise we win because we're tip-most
241 anode, ahist = nodehist
248 anode, ahist = nodehist
242 bnode, bhist = alltags[name]
249 bnode, bhist = alltags[name]
243 if (bnode != anode and anode in bhist and
250 if (bnode != anode and anode in bhist and
244 (bnode not in ahist or len(bhist) > len(ahist))):
251 (bnode not in ahist or len(bhist) > len(ahist))):
245 anode = bnode
252 anode = bnode
246 else:
253 elif tagtype is not None:
247 tagtypes[name] = tagtype
254 tagtypes[name] = tagtype
248 ahist.extend([n for n in bhist if n not in ahist])
255 ahist.extend([n for n in bhist if n not in ahist])
249 alltags[name] = anode, ahist
256 alltags[name] = anode, ahist
250
257
251 def _filename(repo):
258 def _filename(repo):
252 """name of a tagcache file for a given repo or repoview"""
259 """name of a tagcache file for a given repo or repoview"""
253 filename = 'cache/tags2'
260 filename = 'cache/tags2'
254 if repo.filtername:
261 if repo.filtername:
255 filename = '%s-%s' % (filename, repo.filtername)
262 filename = '%s-%s' % (filename, repo.filtername)
256 return filename
263 return filename
257
264
258 def _readtagcache(ui, repo):
265 def _readtagcache(ui, repo):
259 '''Read the tag cache.
266 '''Read the tag cache.
260
267
261 Returns a tuple (heads, fnodes, validinfo, cachetags, shouldwrite).
268 Returns a tuple (heads, fnodes, validinfo, cachetags, shouldwrite).
262
269
263 If the cache is completely up-to-date, "cachetags" is a dict of the
270 If the cache is completely up-to-date, "cachetags" is a dict of the
264 form returned by _readtags() and "heads", "fnodes", and "validinfo" are
271 form returned by _readtags() and "heads", "fnodes", and "validinfo" are
265 None and "shouldwrite" is False.
272 None and "shouldwrite" is False.
266
273
267 If the cache is not up to date, "cachetags" is None. "heads" is a list
274 If the cache is not up to date, "cachetags" is None. "heads" is a list
268 of all heads currently in the repository, ordered from tip to oldest.
275 of all heads currently in the repository, ordered from tip to oldest.
269 "validinfo" is a tuple describing cache validation info. This is used
276 "validinfo" is a tuple describing cache validation info. This is used
270 when writing the tags cache. "fnodes" is a mapping from head to .hgtags
277 when writing the tags cache. "fnodes" is a mapping from head to .hgtags
271 filenode. "shouldwrite" is True.
278 filenode. "shouldwrite" is True.
272
279
273 If the cache is not up to date, the caller is responsible for reading tag
280 If the cache is not up to date, the caller is responsible for reading tag
274 info from each returned head. (See findglobaltags().)
281 info from each returned head. (See findglobaltags().)
275 '''
282 '''
276 try:
283 try:
277 cachefile = repo.vfs(_filename(repo), 'r')
284 cachefile = repo.vfs(_filename(repo), 'r')
278 # force reading the file for static-http
285 # force reading the file for static-http
279 cachelines = iter(cachefile)
286 cachelines = iter(cachefile)
280 except IOError:
287 except IOError:
281 cachefile = None
288 cachefile = None
282
289
283 cacherev = None
290 cacherev = None
284 cachenode = None
291 cachenode = None
285 cachehash = None
292 cachehash = None
286 if cachefile:
293 if cachefile:
287 try:
294 try:
288 validline = next(cachelines)
295 validline = next(cachelines)
289 validline = validline.split()
296 validline = validline.split()
290 cacherev = int(validline[0])
297 cacherev = int(validline[0])
291 cachenode = bin(validline[1])
298 cachenode = bin(validline[1])
292 if len(validline) > 2:
299 if len(validline) > 2:
293 cachehash = bin(validline[2])
300 cachehash = bin(validline[2])
294 except Exception:
301 except Exception:
295 # corruption of the cache, just recompute it.
302 # corruption of the cache, just recompute it.
296 pass
303 pass
297
304
298 tipnode = repo.changelog.tip()
305 tipnode = repo.changelog.tip()
299 tiprev = len(repo.changelog) - 1
306 tiprev = len(repo.changelog) - 1
300
307
301 # Case 1 (common): tip is the same, so nothing has changed.
308 # Case 1 (common): tip is the same, so nothing has changed.
302 # (Unchanged tip trivially means no changesets have been added.
309 # (Unchanged tip trivially means no changesets have been added.
303 # But, thanks to localrepository.destroyed(), it also means none
310 # But, thanks to localrepository.destroyed(), it also means none
304 # have been destroyed by strip or rollback.)
311 # have been destroyed by strip or rollback.)
305 if (cacherev == tiprev
312 if (cacherev == tiprev
306 and cachenode == tipnode
313 and cachenode == tipnode
307 and cachehash == scmutil.filteredhash(repo, tiprev)):
314 and cachehash == scmutil.filteredhash(repo, tiprev)):
308 tags = _readtags(ui, repo, cachelines, cachefile.name)
315 tags = _readtags(ui, repo, cachelines, cachefile.name)
309 cachefile.close()
316 cachefile.close()
310 return (None, None, None, tags, False)
317 return (None, None, None, tags, False)
311 if cachefile:
318 if cachefile:
312 cachefile.close() # ignore rest of file
319 cachefile.close() # ignore rest of file
313
320
314 valid = (tiprev, tipnode, scmutil.filteredhash(repo, tiprev))
321 valid = (tiprev, tipnode, scmutil.filteredhash(repo, tiprev))
315
322
316 repoheads = repo.heads()
323 repoheads = repo.heads()
317 # Case 2 (uncommon): empty repo; get out quickly and don't bother
324 # Case 2 (uncommon): empty repo; get out quickly and don't bother
318 # writing an empty cache.
325 # writing an empty cache.
319 if repoheads == [nullid]:
326 if repoheads == [nullid]:
320 return ([], {}, valid, {}, False)
327 return ([], {}, valid, {}, False)
321
328
322 # Case 3 (uncommon): cache file missing or empty.
329 # Case 3 (uncommon): cache file missing or empty.
323
330
324 # Case 4 (uncommon): tip rev decreased. This should only happen
331 # Case 4 (uncommon): tip rev decreased. This should only happen
325 # when we're called from localrepository.destroyed(). Refresh the
332 # when we're called from localrepository.destroyed(). Refresh the
326 # cache so future invocations will not see disappeared heads in the
333 # cache so future invocations will not see disappeared heads in the
327 # cache.
334 # cache.
328
335
329 # Case 5 (common): tip has changed, so we've added/replaced heads.
336 # Case 5 (common): tip has changed, so we've added/replaced heads.
330
337
331 # As it happens, the code to handle cases 3, 4, 5 is the same.
338 # As it happens, the code to handle cases 3, 4, 5 is the same.
332
339
333 # N.B. in case 4 (nodes destroyed), "new head" really means "newly
340 # N.B. in case 4 (nodes destroyed), "new head" really means "newly
334 # exposed".
341 # exposed".
335 if not len(repo.file('.hgtags')):
342 if not len(repo.file('.hgtags')):
336 # No tags have ever been committed, so we can avoid a
343 # No tags have ever been committed, so we can avoid a
337 # potentially expensive search.
344 # potentially expensive search.
338 return ([], {}, valid, None, True)
345 return ([], {}, valid, None, True)
339
346
340
347
341 # Now we have to lookup the .hgtags filenode for every new head.
348 # Now we have to lookup the .hgtags filenode for every new head.
342 # This is the most expensive part of finding tags, so performance
349 # This is the most expensive part of finding tags, so performance
343 # depends primarily on the size of newheads. Worst case: no cache
350 # depends primarily on the size of newheads. Worst case: no cache
344 # file, so newheads == repoheads.
351 # file, so newheads == repoheads.
345 cachefnode = _getfnodes(ui, repo, repoheads)
352 cachefnode = _getfnodes(ui, repo, repoheads)
346
353
347 # Caller has to iterate over all heads, but can use the filenodes in
354 # Caller has to iterate over all heads, but can use the filenodes in
348 # cachefnode to get to each .hgtags revision quickly.
355 # cachefnode to get to each .hgtags revision quickly.
349 return (repoheads, cachefnode, valid, None, True)
356 return (repoheads, cachefnode, valid, None, True)
350
357
351 def _getfnodes(ui, repo, nodes):
358 def _getfnodes(ui, repo, nodes):
352 """return .hgtags fnodes for a list of changeset nodes
359 """return .hgtags fnodes for a list of changeset nodes
353
360
354 Return value is a {node: fnode} mapping. There will be no entry for nodes
361 Return value is a {node: fnode} mapping. There will be no entry for nodes
355 without a '.hgtags' file.
362 without a '.hgtags' file.
356 """
363 """
357 starttime = util.timer()
364 starttime = util.timer()
358 fnodescache = hgtagsfnodescache(repo.unfiltered())
365 fnodescache = hgtagsfnodescache(repo.unfiltered())
359 cachefnode = {}
366 cachefnode = {}
360 for head in reversed(nodes):
367 for head in reversed(nodes):
361 fnode = fnodescache.getfnode(head)
368 fnode = fnodescache.getfnode(head)
362 if fnode != nullid:
369 if fnode != nullid:
363 cachefnode[head] = fnode
370 cachefnode[head] = fnode
364
371
365 fnodescache.write()
372 fnodescache.write()
366
373
367 duration = util.timer() - starttime
374 duration = util.timer() - starttime
368 ui.log('tagscache',
375 ui.log('tagscache',
369 '%d/%d cache hits/lookups in %0.4f '
376 '%d/%d cache hits/lookups in %0.4f '
370 'seconds\n',
377 'seconds\n',
371 fnodescache.hitcount, fnodescache.lookupcount, duration)
378 fnodescache.hitcount, fnodescache.lookupcount, duration)
372 return cachefnode
379 return cachefnode
373
380
374 def _writetagcache(ui, repo, valid, cachetags):
381 def _writetagcache(ui, repo, valid, cachetags):
375 filename = _filename(repo)
382 filename = _filename(repo)
376 try:
383 try:
377 cachefile = repo.vfs(filename, 'w', atomictemp=True)
384 cachefile = repo.vfs(filename, 'w', atomictemp=True)
378 except (OSError, IOError):
385 except (OSError, IOError):
379 return
386 return
380
387
381 ui.log('tagscache', 'writing .hg/%s with %d tags\n',
388 ui.log('tagscache', 'writing .hg/%s with %d tags\n',
382 filename, len(cachetags))
389 filename, len(cachetags))
383
390
384 if valid[2]:
391 if valid[2]:
385 cachefile.write('%d %s %s\n' % (valid[0], hex(valid[1]), hex(valid[2])))
392 cachefile.write('%d %s %s\n' % (valid[0], hex(valid[1]), hex(valid[2])))
386 else:
393 else:
387 cachefile.write('%d %s\n' % (valid[0], hex(valid[1])))
394 cachefile.write('%d %s\n' % (valid[0], hex(valid[1])))
388
395
389 # Tag names in the cache are in UTF-8 -- which is the whole reason
396 # Tag names in the cache are in UTF-8 -- which is the whole reason
390 # we keep them in UTF-8 throughout this module. If we converted
397 # we keep them in UTF-8 throughout this module. If we converted
391 # them local encoding on input, we would lose info writing them to
398 # them local encoding on input, we would lose info writing them to
392 # the cache.
399 # the cache.
393 for (name, (node, hist)) in sorted(cachetags.iteritems()):
400 for (name, (node, hist)) in sorted(cachetags.iteritems()):
394 for n in hist:
401 for n in hist:
395 cachefile.write("%s %s\n" % (hex(n), name))
402 cachefile.write("%s %s\n" % (hex(n), name))
396 cachefile.write("%s %s\n" % (hex(node), name))
403 cachefile.write("%s %s\n" % (hex(node), name))
397
404
398 try:
405 try:
399 cachefile.close()
406 cachefile.close()
400 except (OSError, IOError):
407 except (OSError, IOError):
401 pass
408 pass
402
409
403 def tag(repo, names, node, message, local, user, date, editor=False):
410 def tag(repo, names, node, message, local, user, date, editor=False):
404 '''tag a revision with one or more symbolic names.
411 '''tag a revision with one or more symbolic names.
405
412
406 names is a list of strings or, when adding a single tag, names may be a
413 names is a list of strings or, when adding a single tag, names may be a
407 string.
414 string.
408
415
409 if local is True, the tags are stored in a per-repository file.
416 if local is True, the tags are stored in a per-repository file.
410 otherwise, they are stored in the .hgtags file, and a new
417 otherwise, they are stored in the .hgtags file, and a new
411 changeset is committed with the change.
418 changeset is committed with the change.
412
419
413 keyword arguments:
420 keyword arguments:
414
421
415 local: whether to store tags in non-version-controlled file
422 local: whether to store tags in non-version-controlled file
416 (default False)
423 (default False)
417
424
418 message: commit message to use if committing
425 message: commit message to use if committing
419
426
420 user: name of user to use if committing
427 user: name of user to use if committing
421
428
422 date: date tuple to use if committing'''
429 date: date tuple to use if committing'''
423
430
424 if not local:
431 if not local:
425 m = matchmod.exact(repo.root, '', ['.hgtags'])
432 m = matchmod.exact(repo.root, '', ['.hgtags'])
426 if any(repo.status(match=m, unknown=True, ignored=True)):
433 if any(repo.status(match=m, unknown=True, ignored=True)):
427 raise error.Abort(_('working copy of .hgtags is changed'),
434 raise error.Abort(_('working copy of .hgtags is changed'),
428 hint=_('please commit .hgtags manually'))
435 hint=_('please commit .hgtags manually'))
429
436
430 repo.tags() # instantiate the cache
437 repo.tags() # instantiate the cache
431 _tag(repo.unfiltered(), names, node, message, local, user, date,
438 _tag(repo.unfiltered(), names, node, message, local, user, date,
432 editor=editor)
439 editor=editor)
433
440
434 def _tag(repo, names, node, message, local, user, date, extra=None,
441 def _tag(repo, names, node, message, local, user, date, extra=None,
435 editor=False):
442 editor=False):
436 if isinstance(names, str):
443 if isinstance(names, str):
437 names = (names,)
444 names = (names,)
438
445
439 branches = repo.branchmap()
446 branches = repo.branchmap()
440 for name in names:
447 for name in names:
441 repo.hook('pretag', throw=True, node=hex(node), tag=name,
448 repo.hook('pretag', throw=True, node=hex(node), tag=name,
442 local=local)
449 local=local)
443 if name in branches:
450 if name in branches:
444 repo.ui.warn(_("warning: tag %s conflicts with existing"
451 repo.ui.warn(_("warning: tag %s conflicts with existing"
445 " branch name\n") % name)
452 " branch name\n") % name)
446
453
447 def writetags(fp, names, munge, prevtags):
454 def writetags(fp, names, munge, prevtags):
448 fp.seek(0, 2)
455 fp.seek(0, 2)
449 if prevtags and prevtags[-1] != '\n':
456 if prevtags and prevtags[-1] != '\n':
450 fp.write('\n')
457 fp.write('\n')
451 for name in names:
458 for name in names:
452 if munge:
459 if munge:
453 m = munge(name)
460 m = munge(name)
454 else:
461 else:
455 m = name
462 m = name
456
463
457 if (repo._tagscache.tagtypes and
464 if (repo._tagscache.tagtypes and
458 name in repo._tagscache.tagtypes):
465 name in repo._tagscache.tagtypes):
459 old = repo.tags().get(name, nullid)
466 old = repo.tags().get(name, nullid)
460 fp.write('%s %s\n' % (hex(old), m))
467 fp.write('%s %s\n' % (hex(old), m))
461 fp.write('%s %s\n' % (hex(node), m))
468 fp.write('%s %s\n' % (hex(node), m))
462 fp.close()
469 fp.close()
463
470
464 prevtags = ''
471 prevtags = ''
465 if local:
472 if local:
466 try:
473 try:
467 fp = repo.vfs('localtags', 'r+')
474 fp = repo.vfs('localtags', 'r+')
468 except IOError:
475 except IOError:
469 fp = repo.vfs('localtags', 'a')
476 fp = repo.vfs('localtags', 'a')
470 else:
477 else:
471 prevtags = fp.read()
478 prevtags = fp.read()
472
479
473 # local tags are stored in the current charset
480 # local tags are stored in the current charset
474 writetags(fp, names, None, prevtags)
481 writetags(fp, names, None, prevtags)
475 for name in names:
482 for name in names:
476 repo.hook('tag', node=hex(node), tag=name, local=local)
483 repo.hook('tag', node=hex(node), tag=name, local=local)
477 return
484 return
478
485
479 try:
486 try:
480 fp = repo.wvfs('.hgtags', 'rb+')
487 fp = repo.wvfs('.hgtags', 'rb+')
481 except IOError as e:
488 except IOError as e:
482 if e.errno != errno.ENOENT:
489 if e.errno != errno.ENOENT:
483 raise
490 raise
484 fp = repo.wvfs('.hgtags', 'ab')
491 fp = repo.wvfs('.hgtags', 'ab')
485 else:
492 else:
486 prevtags = fp.read()
493 prevtags = fp.read()
487
494
488 # committed tags are stored in UTF-8
495 # committed tags are stored in UTF-8
489 writetags(fp, names, encoding.fromlocal, prevtags)
496 writetags(fp, names, encoding.fromlocal, prevtags)
490
497
491 fp.close()
498 fp.close()
492
499
493 repo.invalidatecaches()
500 repo.invalidatecaches()
494
501
495 if '.hgtags' not in repo.dirstate:
502 if '.hgtags' not in repo.dirstate:
496 repo[None].add(['.hgtags'])
503 repo[None].add(['.hgtags'])
497
504
498 m = matchmod.exact(repo.root, '', ['.hgtags'])
505 m = matchmod.exact(repo.root, '', ['.hgtags'])
499 tagnode = repo.commit(message, user, date, extra=extra, match=m,
506 tagnode = repo.commit(message, user, date, extra=extra, match=m,
500 editor=editor)
507 editor=editor)
501
508
502 for name in names:
509 for name in names:
503 repo.hook('tag', node=hex(node), tag=name, local=local)
510 repo.hook('tag', node=hex(node), tag=name, local=local)
504
511
505 return tagnode
512 return tagnode
506
513
507 _fnodescachefile = 'cache/hgtagsfnodes1'
514 _fnodescachefile = 'cache/hgtagsfnodes1'
508 _fnodesrecsize = 4 + 20 # changeset fragment + filenode
515 _fnodesrecsize = 4 + 20 # changeset fragment + filenode
509 _fnodesmissingrec = '\xff' * 24
516 _fnodesmissingrec = '\xff' * 24
510
517
511 class hgtagsfnodescache(object):
518 class hgtagsfnodescache(object):
512 """Persistent cache mapping revisions to .hgtags filenodes.
519 """Persistent cache mapping revisions to .hgtags filenodes.
513
520
514 The cache is an array of records. Each item in the array corresponds to
521 The cache is an array of records. Each item in the array corresponds to
515 a changelog revision. Values in the array contain the first 4 bytes of
522 a changelog revision. Values in the array contain the first 4 bytes of
516 the node hash and the 20 bytes .hgtags filenode for that revision.
523 the node hash and the 20 bytes .hgtags filenode for that revision.
517
524
518 The first 4 bytes are present as a form of verification. Repository
525 The first 4 bytes are present as a form of verification. Repository
519 stripping and rewriting may change the node at a numeric revision in the
526 stripping and rewriting may change the node at a numeric revision in the
520 changelog. The changeset fragment serves as a verifier to detect
527 changelog. The changeset fragment serves as a verifier to detect
521 rewriting. This logic is shared with the rev branch cache (see
528 rewriting. This logic is shared with the rev branch cache (see
522 branchmap.py).
529 branchmap.py).
523
530
524 The instance holds in memory the full cache content but entries are
531 The instance holds in memory the full cache content but entries are
525 only parsed on read.
532 only parsed on read.
526
533
527 Instances behave like lists. ``c[i]`` works where i is a rev or
534 Instances behave like lists. ``c[i]`` works where i is a rev or
528 changeset node. Missing indexes are populated automatically on access.
535 changeset node. Missing indexes are populated automatically on access.
529 """
536 """
530 def __init__(self, repo):
537 def __init__(self, repo):
531 assert repo.filtername is None
538 assert repo.filtername is None
532
539
533 self._repo = repo
540 self._repo = repo
534
541
535 # Only for reporting purposes.
542 # Only for reporting purposes.
536 self.lookupcount = 0
543 self.lookupcount = 0
537 self.hitcount = 0
544 self.hitcount = 0
538
545
539
546
540 try:
547 try:
541 data = repo.vfs.read(_fnodescachefile)
548 data = repo.vfs.read(_fnodescachefile)
542 except (OSError, IOError):
549 except (OSError, IOError):
543 data = ""
550 data = ""
544 self._raw = bytearray(data)
551 self._raw = bytearray(data)
545
552
546 # The end state of self._raw is an array that is of the exact length
553 # The end state of self._raw is an array that is of the exact length
547 # required to hold a record for every revision in the repository.
554 # required to hold a record for every revision in the repository.
548 # We truncate or extend the array as necessary. self._dirtyoffset is
555 # We truncate or extend the array as necessary. self._dirtyoffset is
549 # defined to be the start offset at which we need to write the output
556 # defined to be the start offset at which we need to write the output
550 # file. This offset is also adjusted when new entries are calculated
557 # file. This offset is also adjusted when new entries are calculated
551 # for array members.
558 # for array members.
552 cllen = len(repo.changelog)
559 cllen = len(repo.changelog)
553 wantedlen = cllen * _fnodesrecsize
560 wantedlen = cllen * _fnodesrecsize
554 rawlen = len(self._raw)
561 rawlen = len(self._raw)
555
562
556 self._dirtyoffset = None
563 self._dirtyoffset = None
557
564
558 if rawlen < wantedlen:
565 if rawlen < wantedlen:
559 self._dirtyoffset = rawlen
566 self._dirtyoffset = rawlen
560 self._raw.extend('\xff' * (wantedlen - rawlen))
567 self._raw.extend('\xff' * (wantedlen - rawlen))
561 elif rawlen > wantedlen:
568 elif rawlen > wantedlen:
562 # There's no easy way to truncate array instances. This seems
569 # There's no easy way to truncate array instances. This seems
563 # slightly less evil than copying a potentially large array slice.
570 # slightly less evil than copying a potentially large array slice.
564 for i in range(rawlen - wantedlen):
571 for i in range(rawlen - wantedlen):
565 self._raw.pop()
572 self._raw.pop()
566 self._dirtyoffset = len(self._raw)
573 self._dirtyoffset = len(self._raw)
567
574
568 def getfnode(self, node, computemissing=True):
575 def getfnode(self, node, computemissing=True):
569 """Obtain the filenode of the .hgtags file at a specified revision.
576 """Obtain the filenode of the .hgtags file at a specified revision.
570
577
571 If the value is in the cache, the entry will be validated and returned.
578 If the value is in the cache, the entry will be validated and returned.
572 Otherwise, the filenode will be computed and returned unless
579 Otherwise, the filenode will be computed and returned unless
573 "computemissing" is False, in which case None will be returned without
580 "computemissing" is False, in which case None will be returned without
574 any potentially expensive computation being performed.
581 any potentially expensive computation being performed.
575
582
576 If an .hgtags does not exist at the specified revision, nullid is
583 If an .hgtags does not exist at the specified revision, nullid is
577 returned.
584 returned.
578 """
585 """
579 ctx = self._repo[node]
586 ctx = self._repo[node]
580 rev = ctx.rev()
587 rev = ctx.rev()
581
588
582 self.lookupcount += 1
589 self.lookupcount += 1
583
590
584 offset = rev * _fnodesrecsize
591 offset = rev * _fnodesrecsize
585 record = '%s' % self._raw[offset:offset + _fnodesrecsize]
592 record = '%s' % self._raw[offset:offset + _fnodesrecsize]
586 properprefix = node[0:4]
593 properprefix = node[0:4]
587
594
588 # Validate and return existing entry.
595 # Validate and return existing entry.
589 if record != _fnodesmissingrec:
596 if record != _fnodesmissingrec:
590 fileprefix = record[0:4]
597 fileprefix = record[0:4]
591
598
592 if fileprefix == properprefix:
599 if fileprefix == properprefix:
593 self.hitcount += 1
600 self.hitcount += 1
594 return record[4:]
601 return record[4:]
595
602
596 # Fall through.
603 # Fall through.
597
604
598 # If we get here, the entry is either missing or invalid.
605 # If we get here, the entry is either missing or invalid.
599
606
600 if not computemissing:
607 if not computemissing:
601 return None
608 return None
602
609
603 # Populate missing entry.
610 # Populate missing entry.
604 try:
611 try:
605 fnode = ctx.filenode('.hgtags')
612 fnode = ctx.filenode('.hgtags')
606 except error.LookupError:
613 except error.LookupError:
607 # No .hgtags file on this revision.
614 # No .hgtags file on this revision.
608 fnode = nullid
615 fnode = nullid
609
616
610 self._writeentry(offset, properprefix, fnode)
617 self._writeentry(offset, properprefix, fnode)
611 return fnode
618 return fnode
612
619
613 def setfnode(self, node, fnode):
620 def setfnode(self, node, fnode):
614 """Set the .hgtags filenode for a given changeset."""
621 """Set the .hgtags filenode for a given changeset."""
615 assert len(fnode) == 20
622 assert len(fnode) == 20
616 ctx = self._repo[node]
623 ctx = self._repo[node]
617
624
618 # Do a lookup first to avoid writing if nothing has changed.
625 # Do a lookup first to avoid writing if nothing has changed.
619 if self.getfnode(ctx.node(), computemissing=False) == fnode:
626 if self.getfnode(ctx.node(), computemissing=False) == fnode:
620 return
627 return
621
628
622 self._writeentry(ctx.rev() * _fnodesrecsize, node[0:4], fnode)
629 self._writeentry(ctx.rev() * _fnodesrecsize, node[0:4], fnode)
623
630
624 def _writeentry(self, offset, prefix, fnode):
631 def _writeentry(self, offset, prefix, fnode):
625 # Slices on array instances only accept other array.
632 # Slices on array instances only accept other array.
626 entry = bytearray(prefix + fnode)
633 entry = bytearray(prefix + fnode)
627 self._raw[offset:offset + _fnodesrecsize] = entry
634 self._raw[offset:offset + _fnodesrecsize] = entry
628 # self._dirtyoffset could be None.
635 # self._dirtyoffset could be None.
629 self._dirtyoffset = min(self._dirtyoffset, offset) or 0
636 self._dirtyoffset = min(self._dirtyoffset, offset) or 0
630
637
631 def write(self):
638 def write(self):
632 """Perform all necessary writes to cache file.
639 """Perform all necessary writes to cache file.
633
640
634 This may no-op if no writes are needed or if a write lock could
641 This may no-op if no writes are needed or if a write lock could
635 not be obtained.
642 not be obtained.
636 """
643 """
637 if self._dirtyoffset is None:
644 if self._dirtyoffset is None:
638 return
645 return
639
646
640 data = self._raw[self._dirtyoffset:]
647 data = self._raw[self._dirtyoffset:]
641 if not data:
648 if not data:
642 return
649 return
643
650
644 repo = self._repo
651 repo = self._repo
645
652
646 try:
653 try:
647 lock = repo.wlock(wait=False)
654 lock = repo.wlock(wait=False)
648 except error.LockError:
655 except error.LockError:
649 repo.ui.log('tagscache',
656 repo.ui.log('tagscache',
650 'not writing .hg/%s because lock cannot be acquired\n' %
657 'not writing .hg/%s because lock cannot be acquired\n' %
651 (_fnodescachefile))
658 (_fnodescachefile))
652 return
659 return
653
660
654 try:
661 try:
655 f = repo.vfs.open(_fnodescachefile, 'ab')
662 f = repo.vfs.open(_fnodescachefile, 'ab')
656 try:
663 try:
657 # if the file has been truncated
664 # if the file has been truncated
658 actualoffset = f.tell()
665 actualoffset = f.tell()
659 if actualoffset < self._dirtyoffset:
666 if actualoffset < self._dirtyoffset:
660 self._dirtyoffset = actualoffset
667 self._dirtyoffset = actualoffset
661 data = self._raw[self._dirtyoffset:]
668 data = self._raw[self._dirtyoffset:]
662 f.seek(self._dirtyoffset)
669 f.seek(self._dirtyoffset)
663 f.truncate()
670 f.truncate()
664 repo.ui.log('tagscache',
671 repo.ui.log('tagscache',
665 'writing %d bytes to %s\n' % (
672 'writing %d bytes to %s\n' % (
666 len(data), _fnodescachefile))
673 len(data), _fnodescachefile))
667 f.write(data)
674 f.write(data)
668 self._dirtyoffset = None
675 self._dirtyoffset = None
669 finally:
676 finally:
670 f.close()
677 f.close()
671 except (IOError, OSError) as inst:
678 except (IOError, OSError) as inst:
672 repo.ui.log('tagscache',
679 repo.ui.log('tagscache',
673 "couldn't write %s: %s\n" % (
680 "couldn't write %s: %s\n" % (
674 _fnodescachefile, inst))
681 _fnodescachefile, inst))
675 finally:
682 finally:
676 lock.release()
683 lock.release()
General Comments 0
You need to be logged in to leave comments. Login now