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