##// END OF EJS Templates
tags: loosen IOError filtering when reading localtags
Idan Kamara -
r14038:0e6f622f default
parent child Browse files
Show More
@@ -1,286 +1,289 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 node import nullid, bin, hex, short
13 from node import nullid, bin, hex, short
14 from i18n import _
14 from i18n import _
15 import os.path
16 import encoding
15 import encoding
17 import error
16 import error
17 import errno
18
18
19 def findglobaltags(ui, repo, alltags, tagtypes):
19 def findglobaltags(ui, repo, alltags, tagtypes):
20 '''Find global tags in repo by reading .hgtags from every head that
20 '''Find global tags in repo by reading .hgtags from every head that
21 has a distinct version of it, using a cache to avoid excess work.
21 has a distinct version of it, using a cache to avoid excess work.
22 Updates the dicts alltags, tagtypes in place: alltags maps tag name
22 Updates the dicts alltags, tagtypes in place: alltags maps tag name
23 to (node, hist) pair (see _readtags() below), and tagtypes maps tag
23 to (node, hist) pair (see _readtags() below), and tagtypes maps tag
24 name to tag type ("global" in this case).'''
24 name to tag type ("global" in this case).'''
25 # This is so we can be lazy and assume alltags contains only global
25 # This is so we can be lazy and assume alltags contains only global
26 # tags when we pass it to _writetagcache().
26 # tags when we pass it to _writetagcache().
27 assert len(alltags) == len(tagtypes) == 0, \
27 assert len(alltags) == len(tagtypes) == 0, \
28 "findglobaltags() should be called first"
28 "findglobaltags() should be called first"
29
29
30 (heads, tagfnode, cachetags, shouldwrite) = _readtagcache(ui, repo)
30 (heads, tagfnode, cachetags, shouldwrite) = _readtagcache(ui, repo)
31 if cachetags is not None:
31 if cachetags is not None:
32 assert not shouldwrite
32 assert not shouldwrite
33 # XXX is this really 100% correct? are there oddball special
33 # XXX is this really 100% correct? are there oddball special
34 # cases where a global tag should outrank a local tag but won't,
34 # cases where a global tag should outrank a local tag but won't,
35 # because cachetags does not contain rank info?
35 # because cachetags does not contain rank info?
36 _updatetags(cachetags, 'global', alltags, tagtypes)
36 _updatetags(cachetags, 'global', alltags, tagtypes)
37 return
37 return
38
38
39 seen = set() # set of fnode
39 seen = set() # set of fnode
40 fctx = None
40 fctx = None
41 for head in reversed(heads): # oldest to newest
41 for head in reversed(heads): # oldest to newest
42 assert head in repo.changelog.nodemap, \
42 assert head in repo.changelog.nodemap, \
43 "tag cache returned bogus head %s" % short(head)
43 "tag cache returned bogus head %s" % short(head)
44
44
45 fnode = tagfnode.get(head)
45 fnode = tagfnode.get(head)
46 if fnode and fnode not in seen:
46 if fnode and fnode not in seen:
47 seen.add(fnode)
47 seen.add(fnode)
48 if not fctx:
48 if not fctx:
49 fctx = repo.filectx('.hgtags', fileid=fnode)
49 fctx = repo.filectx('.hgtags', fileid=fnode)
50 else:
50 else:
51 fctx = fctx.filectx(fnode)
51 fctx = fctx.filectx(fnode)
52
52
53 filetags = _readtags(ui, repo, fctx.data().splitlines(), fctx)
53 filetags = _readtags(ui, repo, fctx.data().splitlines(), fctx)
54 _updatetags(filetags, 'global', alltags, tagtypes)
54 _updatetags(filetags, 'global', alltags, tagtypes)
55
55
56 # and update the cache (if necessary)
56 # and update the cache (if necessary)
57 if shouldwrite:
57 if shouldwrite:
58 _writetagcache(ui, repo, heads, tagfnode, alltags)
58 _writetagcache(ui, repo, heads, tagfnode, alltags)
59
59
60 def readlocaltags(ui, repo, alltags, tagtypes):
60 def readlocaltags(ui, repo, alltags, tagtypes):
61 '''Read local tags in repo. Update alltags and tagtypes.'''
61 '''Read local tags in repo. Update alltags and tagtypes.'''
62 try:
62 try:
63 # localtags is in the local encoding; re-encode to UTF-8 on
64 # input for consistency with the rest of this module.
65 data = repo.opener("localtags").read()
63 data = repo.opener("localtags").read()
66 filetags = _readtags(
64 except IOError, inst:
67 ui, repo, data.splitlines(), "localtags",
65 if inst.errno != errno.ENOENT:
68 recode=encoding.fromlocal)
66 raise
69 _updatetags(filetags, "local", alltags, tagtypes)
67 return
70 except IOError:
68
71 pass
69 # localtags is in the local encoding; re-encode to UTF-8 on
70 # input for consistency with the rest of this module.
71 filetags = _readtags(
72 ui, repo, data.splitlines(), "localtags",
73 recode=encoding.fromlocal)
74 _updatetags(filetags, "local", alltags, tagtypes)
72
75
73 def _readtags(ui, repo, lines, fn, recode=None):
76 def _readtags(ui, repo, lines, fn, recode=None):
74 '''Read tag definitions from a file (or any source of lines).
77 '''Read tag definitions from a file (or any source of lines).
75 Return a mapping from tag name to (node, hist): node is the node id
78 Return a mapping from tag name to (node, hist): node is the node id
76 from the last line read for that name, and hist is the list of node
79 from the last line read for that name, and hist is the list of node
77 ids previously associated with it (in file order). All node ids are
80 ids previously associated with it (in file order). All node ids are
78 binary, not hex.'''
81 binary, not hex.'''
79
82
80 filetags = {} # map tag name to (node, hist)
83 filetags = {} # map tag name to (node, hist)
81 count = 0
84 count = 0
82
85
83 def warn(msg):
86 def warn(msg):
84 ui.warn(_("%s, line %s: %s\n") % (fn, count, msg))
87 ui.warn(_("%s, line %s: %s\n") % (fn, count, msg))
85
88
86 for line in lines:
89 for line in lines:
87 count += 1
90 count += 1
88 if not line:
91 if not line:
89 continue
92 continue
90 try:
93 try:
91 (nodehex, name) = line.split(" ", 1)
94 (nodehex, name) = line.split(" ", 1)
92 except ValueError:
95 except ValueError:
93 warn(_("cannot parse entry"))
96 warn(_("cannot parse entry"))
94 continue
97 continue
95 name = name.strip()
98 name = name.strip()
96 if recode:
99 if recode:
97 name = recode(name)
100 name = recode(name)
98 try:
101 try:
99 nodebin = bin(nodehex)
102 nodebin = bin(nodehex)
100 except TypeError:
103 except TypeError:
101 warn(_("node '%s' is not well formed") % nodehex)
104 warn(_("node '%s' is not well formed") % nodehex)
102 continue
105 continue
103
106
104 # update filetags
107 # update filetags
105 hist = []
108 hist = []
106 if name in filetags:
109 if name in filetags:
107 n, hist = filetags[name]
110 n, hist = filetags[name]
108 hist.append(n)
111 hist.append(n)
109 filetags[name] = (nodebin, hist)
112 filetags[name] = (nodebin, hist)
110 return filetags
113 return filetags
111
114
112 def _updatetags(filetags, tagtype, alltags, tagtypes):
115 def _updatetags(filetags, tagtype, alltags, tagtypes):
113 '''Incorporate the tag info read from one file into the two
116 '''Incorporate the tag info read from one file into the two
114 dictionaries, alltags and tagtypes, that contain all tag
117 dictionaries, alltags and tagtypes, that contain all tag
115 info (global across all heads plus local).'''
118 info (global across all heads plus local).'''
116
119
117 for name, nodehist in filetags.iteritems():
120 for name, nodehist in filetags.iteritems():
118 if name not in alltags:
121 if name not in alltags:
119 alltags[name] = nodehist
122 alltags[name] = nodehist
120 tagtypes[name] = tagtype
123 tagtypes[name] = tagtype
121 continue
124 continue
122
125
123 # we prefer alltags[name] if:
126 # we prefer alltags[name] if:
124 # it supercedes us OR
127 # it supercedes us OR
125 # mutual supercedes and it has a higher rank
128 # mutual supercedes and it has a higher rank
126 # otherwise we win because we're tip-most
129 # otherwise we win because we're tip-most
127 anode, ahist = nodehist
130 anode, ahist = nodehist
128 bnode, bhist = alltags[name]
131 bnode, bhist = alltags[name]
129 if (bnode != anode and anode in bhist and
132 if (bnode != anode and anode in bhist and
130 (bnode not in ahist or len(bhist) > len(ahist))):
133 (bnode not in ahist or len(bhist) > len(ahist))):
131 anode = bnode
134 anode = bnode
132 ahist.extend([n for n in bhist if n not in ahist])
135 ahist.extend([n for n in bhist if n not in ahist])
133 alltags[name] = anode, ahist
136 alltags[name] = anode, ahist
134 tagtypes[name] = tagtype
137 tagtypes[name] = tagtype
135
138
136
139
137 # The tag cache only stores info about heads, not the tag contents
140 # The tag cache only stores info about heads, not the tag contents
138 # from each head. I.e. it doesn't try to squeeze out the maximum
141 # from each head. I.e. it doesn't try to squeeze out the maximum
139 # performance, but is simpler has a better chance of actually
142 # performance, but is simpler has a better chance of actually
140 # working correctly. And this gives the biggest performance win: it
143 # working correctly. And this gives the biggest performance win: it
141 # avoids looking up .hgtags in the manifest for every head, and it
144 # avoids looking up .hgtags in the manifest for every head, and it
142 # can avoid calling heads() at all if there have been no changes to
145 # can avoid calling heads() at all if there have been no changes to
143 # the repo.
146 # the repo.
144
147
145 def _readtagcache(ui, repo):
148 def _readtagcache(ui, repo):
146 '''Read the tag cache and return a tuple (heads, fnodes, cachetags,
149 '''Read the tag cache and return a tuple (heads, fnodes, cachetags,
147 shouldwrite). If the cache is completely up-to-date, cachetags is a
150 shouldwrite). If the cache is completely up-to-date, cachetags is a
148 dict of the form returned by _readtags(); otherwise, it is None and
151 dict of the form returned by _readtags(); otherwise, it is None and
149 heads and fnodes are set. In that case, heads is the list of all
152 heads and fnodes are set. In that case, heads is the list of all
150 heads currently in the repository (ordered from tip to oldest) and
153 heads currently in the repository (ordered from tip to oldest) and
151 fnodes is a mapping from head to .hgtags filenode. If those two are
154 fnodes is a mapping from head to .hgtags filenode. If those two are
152 set, caller is responsible for reading tag info from each head.'''
155 set, caller is responsible for reading tag info from each head.'''
153
156
154 try:
157 try:
155 cachefile = repo.opener('cache/tags', 'r')
158 cachefile = repo.opener('cache/tags', 'r')
156 # force reading the file for static-http
159 # force reading the file for static-http
157 cachelines = iter(cachefile)
160 cachelines = iter(cachefile)
158 except IOError:
161 except IOError:
159 cachefile = None
162 cachefile = None
160
163
161 # The cache file consists of lines like
164 # The cache file consists of lines like
162 # <headrev> <headnode> [<tagnode>]
165 # <headrev> <headnode> [<tagnode>]
163 # where <headrev> and <headnode> redundantly identify a repository
166 # where <headrev> and <headnode> redundantly identify a repository
164 # head from the time the cache was written, and <tagnode> is the
167 # head from the time the cache was written, and <tagnode> is the
165 # filenode of .hgtags on that head. Heads with no .hgtags file will
168 # filenode of .hgtags on that head. Heads with no .hgtags file will
166 # have no <tagnode>. The cache is ordered from tip to oldest (which
169 # have no <tagnode>. The cache is ordered from tip to oldest (which
167 # is part of why <headrev> is there: a quick visual check is all
170 # is part of why <headrev> is there: a quick visual check is all
168 # that's required to ensure correct order).
171 # that's required to ensure correct order).
169 #
172 #
170 # This information is enough to let us avoid the most expensive part
173 # This information is enough to let us avoid the most expensive part
171 # of finding global tags, which is looking up <tagnode> in the
174 # of finding global tags, which is looking up <tagnode> in the
172 # manifest for each head.
175 # manifest for each head.
173 cacherevs = [] # list of headrev
176 cacherevs = [] # list of headrev
174 cacheheads = [] # list of headnode
177 cacheheads = [] # list of headnode
175 cachefnode = {} # map headnode to filenode
178 cachefnode = {} # map headnode to filenode
176 if cachefile:
179 if cachefile:
177 try:
180 try:
178 for line in cachelines:
181 for line in cachelines:
179 if line == "\n":
182 if line == "\n":
180 break
183 break
181 line = line.rstrip().split()
184 line = line.rstrip().split()
182 cacherevs.append(int(line[0]))
185 cacherevs.append(int(line[0]))
183 headnode = bin(line[1])
186 headnode = bin(line[1])
184 cacheheads.append(headnode)
187 cacheheads.append(headnode)
185 if len(line) == 3:
188 if len(line) == 3:
186 fnode = bin(line[2])
189 fnode = bin(line[2])
187 cachefnode[headnode] = fnode
190 cachefnode[headnode] = fnode
188 except Exception:
191 except Exception:
189 # corruption of the tags cache, just recompute it
192 # corruption of the tags cache, just recompute it
190 ui.warn(_('.hg/cache/tags is corrupt, rebuilding it\n'))
193 ui.warn(_('.hg/cache/tags is corrupt, rebuilding it\n'))
191 cacheheads = []
194 cacheheads = []
192 cacherevs = []
195 cacherevs = []
193 cachefnode = {}
196 cachefnode = {}
194
197
195 tipnode = repo.changelog.tip()
198 tipnode = repo.changelog.tip()
196 tiprev = len(repo.changelog) - 1
199 tiprev = len(repo.changelog) - 1
197
200
198 # Case 1 (common): tip is the same, so nothing has changed.
201 # Case 1 (common): tip is the same, so nothing has changed.
199 # (Unchanged tip trivially means no changesets have been added.
202 # (Unchanged tip trivially means no changesets have been added.
200 # But, thanks to localrepository.destroyed(), it also means none
203 # But, thanks to localrepository.destroyed(), it also means none
201 # have been destroyed by strip or rollback.)
204 # have been destroyed by strip or rollback.)
202 if cacheheads and cacheheads[0] == tipnode and cacherevs[0] == tiprev:
205 if cacheheads and cacheheads[0] == tipnode and cacherevs[0] == tiprev:
203 tags = _readtags(ui, repo, cachelines, cachefile.name)
206 tags = _readtags(ui, repo, cachelines, cachefile.name)
204 cachefile.close()
207 cachefile.close()
205 return (None, None, tags, False)
208 return (None, None, tags, False)
206 if cachefile:
209 if cachefile:
207 cachefile.close() # ignore rest of file
210 cachefile.close() # ignore rest of file
208
211
209 repoheads = repo.heads()
212 repoheads = repo.heads()
210 # Case 2 (uncommon): empty repo; get out quickly and don't bother
213 # Case 2 (uncommon): empty repo; get out quickly and don't bother
211 # writing an empty cache.
214 # writing an empty cache.
212 if repoheads == [nullid]:
215 if repoheads == [nullid]:
213 return ([], {}, {}, False)
216 return ([], {}, {}, False)
214
217
215 # Case 3 (uncommon): cache file missing or empty.
218 # Case 3 (uncommon): cache file missing or empty.
216
219
217 # Case 4 (uncommon): tip rev decreased. This should only happen
220 # Case 4 (uncommon): tip rev decreased. This should only happen
218 # when we're called from localrepository.destroyed(). Refresh the
221 # when we're called from localrepository.destroyed(). Refresh the
219 # cache so future invocations will not see disappeared heads in the
222 # cache so future invocations will not see disappeared heads in the
220 # cache.
223 # cache.
221
224
222 # Case 5 (common): tip has changed, so we've added/replaced heads.
225 # Case 5 (common): tip has changed, so we've added/replaced heads.
223
226
224 # As it happens, the code to handle cases 3, 4, 5 is the same.
227 # As it happens, the code to handle cases 3, 4, 5 is the same.
225
228
226 # N.B. in case 4 (nodes destroyed), "new head" really means "newly
229 # N.B. in case 4 (nodes destroyed), "new head" really means "newly
227 # exposed".
230 # exposed".
228 newheads = [head
231 newheads = [head
229 for head in repoheads
232 for head in repoheads
230 if head not in set(cacheheads)]
233 if head not in set(cacheheads)]
231
234
232 # Now we have to lookup the .hgtags filenode for every new head.
235 # Now we have to lookup the .hgtags filenode for every new head.
233 # This is the most expensive part of finding tags, so performance
236 # This is the most expensive part of finding tags, so performance
234 # depends primarily on the size of newheads. Worst case: no cache
237 # depends primarily on the size of newheads. Worst case: no cache
235 # file, so newheads == repoheads.
238 # file, so newheads == repoheads.
236 for head in newheads:
239 for head in newheads:
237 cctx = repo[head]
240 cctx = repo[head]
238 try:
241 try:
239 fnode = cctx.filenode('.hgtags')
242 fnode = cctx.filenode('.hgtags')
240 cachefnode[head] = fnode
243 cachefnode[head] = fnode
241 except error.LookupError:
244 except error.LookupError:
242 # no .hgtags file on this head
245 # no .hgtags file on this head
243 pass
246 pass
244
247
245 # Caller has to iterate over all heads, but can use the filenodes in
248 # Caller has to iterate over all heads, but can use the filenodes in
246 # cachefnode to get to each .hgtags revision quickly.
249 # cachefnode to get to each .hgtags revision quickly.
247 return (repoheads, cachefnode, None, True)
250 return (repoheads, cachefnode, None, True)
248
251
249 def _writetagcache(ui, repo, heads, tagfnode, cachetags):
252 def _writetagcache(ui, repo, heads, tagfnode, cachetags):
250
253
251 try:
254 try:
252 cachefile = repo.opener('cache/tags', 'w', atomictemp=True)
255 cachefile = repo.opener('cache/tags', 'w', atomictemp=True)
253 except (OSError, IOError):
256 except (OSError, IOError):
254 return
257 return
255
258
256 realheads = repo.heads() # for sanity checks below
259 realheads = repo.heads() # for sanity checks below
257 for head in heads:
260 for head in heads:
258 # temporary sanity checks; these can probably be removed
261 # temporary sanity checks; these can probably be removed
259 # once this code has been in crew for a few weeks
262 # once this code has been in crew for a few weeks
260 assert head in repo.changelog.nodemap, \
263 assert head in repo.changelog.nodemap, \
261 'trying to write non-existent node %s to tag cache' % short(head)
264 'trying to write non-existent node %s to tag cache' % short(head)
262 assert head in realheads, \
265 assert head in realheads, \
263 'trying to write non-head %s to tag cache' % short(head)
266 'trying to write non-head %s to tag cache' % short(head)
264 assert head != nullid, \
267 assert head != nullid, \
265 'trying to write nullid to tag cache'
268 'trying to write nullid to tag cache'
266
269
267 # This can't fail because of the first assert above. When/if we
270 # This can't fail because of the first assert above. When/if we
268 # remove that assert, we might want to catch LookupError here
271 # remove that assert, we might want to catch LookupError here
269 # and downgrade it to a warning.
272 # and downgrade it to a warning.
270 rev = repo.changelog.rev(head)
273 rev = repo.changelog.rev(head)
271
274
272 fnode = tagfnode.get(head)
275 fnode = tagfnode.get(head)
273 if fnode:
276 if fnode:
274 cachefile.write('%d %s %s\n' % (rev, hex(head), hex(fnode)))
277 cachefile.write('%d %s %s\n' % (rev, hex(head), hex(fnode)))
275 else:
278 else:
276 cachefile.write('%d %s\n' % (rev, hex(head)))
279 cachefile.write('%d %s\n' % (rev, hex(head)))
277
280
278 # Tag names in the cache are in UTF-8 -- which is the whole reason
281 # Tag names in the cache are in UTF-8 -- which is the whole reason
279 # we keep them in UTF-8 throughout this module. If we converted
282 # we keep them in UTF-8 throughout this module. If we converted
280 # them local encoding on input, we would lose info writing them to
283 # them local encoding on input, we would lose info writing them to
281 # the cache.
284 # the cache.
282 cachefile.write('\n')
285 cachefile.write('\n')
283 for (name, (node, hist)) in cachetags.iteritems():
286 for (name, (node, hist)) in cachetags.iteritems():
284 cachefile.write("%s %s\n" % (hex(node), name))
287 cachefile.write("%s %s\n" % (hex(node), name))
285
288
286 cachefile.rename()
289 cachefile.rename()
General Comments 0
You need to be logged in to leave comments. Login now