Show More
@@ -0,0 +1,339 b'' | |||||
|
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, incorporated herein by reference. | |||
|
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 | import os | |||
|
14 | from node import nullid, bin, hex, short | |||
|
15 | from i18n import _ | |||
|
16 | import encoding | |||
|
17 | import error | |||
|
18 | ||||
|
19 | def _debugalways(ui, *msg): | |||
|
20 | ui.write(*msg) | |||
|
21 | ||||
|
22 | def _debugconditional(ui, *msg): | |||
|
23 | ui.debug(*msg) | |||
|
24 | ||||
|
25 | def _debugnever(ui, *msg): | |||
|
26 | pass | |||
|
27 | ||||
|
28 | _debug = _debugalways | |||
|
29 | _debug = _debugnever | |||
|
30 | ||||
|
31 | def findglobaltags1(ui, repo, alltags, tagtypes): | |||
|
32 | '''Find global tags in repo by reading .hgtags from every head that | |||
|
33 | has a distinct version of it. Updates the dicts alltags, tagtypes | |||
|
34 | in place: alltags maps tag name to (node, hist) pair (see _readtags() | |||
|
35 | below), and tagtypes maps tag name to tag type ('global' in this | |||
|
36 | case).''' | |||
|
37 | ||||
|
38 | seen = set() | |||
|
39 | fctx = None | |||
|
40 | ctxs = [] # list of filectx | |||
|
41 | for node in repo.heads(): | |||
|
42 | try: | |||
|
43 | fnode = repo[node].filenode('.hgtags') | |||
|
44 | except error.LookupError: | |||
|
45 | continue | |||
|
46 | if fnode not in seen: | |||
|
47 | seen.add(fnode) | |||
|
48 | if not fctx: | |||
|
49 | fctx = repo.filectx('.hgtags', fileid=fnode) | |||
|
50 | else: | |||
|
51 | fctx = fctx.filectx(fnode) | |||
|
52 | ctxs.append(fctx) | |||
|
53 | ||||
|
54 | # read the tags file from each head, ending with the tip | |||
|
55 | for fctx in reversed(ctxs): | |||
|
56 | filetags = _readtags( | |||
|
57 | ui, repo, fctx.data().splitlines(), fctx) | |||
|
58 | _updatetags(filetags, "global", alltags, tagtypes) | |||
|
59 | ||||
|
60 | def findglobaltags2(ui, repo, alltags, tagtypes): | |||
|
61 | '''Same as findglobaltags1(), but with caching.''' | |||
|
62 | # This is so we can be lazy and assume alltags contains only global | |||
|
63 | # tags when we pass it to _writetagcache(). | |||
|
64 | assert len(alltags) == len(tagtypes) == 0, \ | |||
|
65 | "findglobaltags() should be called first" | |||
|
66 | ||||
|
67 | (heads, tagfnode, cachetags, shouldwrite) = _readtagcache(ui, repo) | |||
|
68 | if cachetags is not None: | |||
|
69 | assert not shouldwrite | |||
|
70 | # XXX is this really 100% correct? are there oddball special | |||
|
71 | # cases where a global tag should outrank a local tag but won't, | |||
|
72 | # because cachetags does not contain rank info? | |||
|
73 | _updatetags(cachetags, 'global', alltags, tagtypes) | |||
|
74 | return | |||
|
75 | ||||
|
76 | _debug(ui, "reading tags from %d head(s): %s\n" | |||
|
77 | % (len(heads), map(short, reversed(heads)))) | |||
|
78 | seen = set() # set of fnode | |||
|
79 | fctx = None | |||
|
80 | for head in reversed(heads): # oldest to newest | |||
|
81 | assert head in repo.changelog.nodemap, \ | |||
|
82 | "tag cache returned bogus head %s" % short(head) | |||
|
83 | ||||
|
84 | fnode = tagfnode.get(head) | |||
|
85 | if fnode and fnode not in seen: | |||
|
86 | seen.add(fnode) | |||
|
87 | if not fctx: | |||
|
88 | fctx = repo.filectx('.hgtags', fileid=fnode) | |||
|
89 | else: | |||
|
90 | fctx = fctx.filectx(fnode) | |||
|
91 | ||||
|
92 | filetags = _readtags(ui, repo, fctx.data().splitlines(), fctx) | |||
|
93 | _updatetags(filetags, 'global', alltags, tagtypes) | |||
|
94 | ||||
|
95 | # and update the cache (if necessary) | |||
|
96 | if shouldwrite: | |||
|
97 | _writetagcache(ui, repo, heads, tagfnode, alltags) | |||
|
98 | ||||
|
99 | # Set this to findglobaltags1 to disable tag caching. | |||
|
100 | findglobaltags = findglobaltags2 | |||
|
101 | ||||
|
102 | def readlocaltags(ui, repo, alltags, tagtypes): | |||
|
103 | '''Read local tags in repo. Update alltags and tagtypes.''' | |||
|
104 | try: | |||
|
105 | # localtags is in the local encoding; re-encode to UTF-8 on | |||
|
106 | # input for consistency with the rest of this module. | |||
|
107 | data = repo.opener("localtags").read() | |||
|
108 | filetags = _readtags( | |||
|
109 | ui, repo, data.splitlines(), "localtags", | |||
|
110 | recode=encoding.fromlocal) | |||
|
111 | _updatetags(filetags, "local", alltags, tagtypes) | |||
|
112 | except IOError: | |||
|
113 | pass | |||
|
114 | ||||
|
115 | def _readtags(ui, repo, lines, fn, recode=None): | |||
|
116 | '''Read tag definitions from a file (or any source of lines). | |||
|
117 | Return a mapping from tag name to (node, hist): node is the node id | |||
|
118 | from the last line read for that name, and hist is the list of node | |||
|
119 | ids previously associated with it (in file order). All node ids are | |||
|
120 | binary, not hex.''' | |||
|
121 | ||||
|
122 | filetags = {} # map tag name to (node, hist) | |||
|
123 | count = 0 | |||
|
124 | ||||
|
125 | def warn(msg): | |||
|
126 | ui.warn(_("%s, line %s: %s\n") % (fn, count, msg)) | |||
|
127 | ||||
|
128 | for line in lines: | |||
|
129 | count += 1 | |||
|
130 | if not line: | |||
|
131 | continue | |||
|
132 | try: | |||
|
133 | (nodehex, name) = line.split(" ", 1) | |||
|
134 | except ValueError: | |||
|
135 | warn(_("cannot parse entry")) | |||
|
136 | continue | |||
|
137 | name = name.strip() | |||
|
138 | if recode: | |||
|
139 | name = recode(name) | |||
|
140 | try: | |||
|
141 | nodebin = bin(nodehex) | |||
|
142 | except TypeError: | |||
|
143 | warn(_("node '%s' is not well formed") % nodehex) | |||
|
144 | continue | |||
|
145 | if nodebin not in repo.changelog.nodemap: | |||
|
146 | # silently ignore as pull -r might cause this | |||
|
147 | continue | |||
|
148 | ||||
|
149 | # update filetags | |||
|
150 | hist = [] | |||
|
151 | if name in filetags: | |||
|
152 | n, hist = filetags[name] | |||
|
153 | hist.append(n) | |||
|
154 | filetags[name] = (nodebin, hist) | |||
|
155 | return filetags | |||
|
156 | ||||
|
157 | def _updatetags(filetags, tagtype, alltags, tagtypes): | |||
|
158 | '''Incorporate the tag info read from one file into the two | |||
|
159 | dictionaries, alltags and tagtypes, that contain all tag | |||
|
160 | info (global across all heads plus local).''' | |||
|
161 | ||||
|
162 | for name, nodehist in filetags.iteritems(): | |||
|
163 | if name not in alltags: | |||
|
164 | alltags[name] = nodehist | |||
|
165 | tagtypes[name] = tagtype | |||
|
166 | continue | |||
|
167 | ||||
|
168 | # we prefer alltags[name] if: | |||
|
169 | # it supercedes us OR | |||
|
170 | # mutual supercedes and it has a higher rank | |||
|
171 | # otherwise we win because we're tip-most | |||
|
172 | anode, ahist = nodehist | |||
|
173 | bnode, bhist = alltags[name] | |||
|
174 | if (bnode != anode and anode in bhist and | |||
|
175 | (bnode not in ahist or len(bhist) > len(ahist))): | |||
|
176 | anode = bnode | |||
|
177 | ahist.extend([n for n in bhist if n not in ahist]) | |||
|
178 | alltags[name] = anode, ahist | |||
|
179 | tagtypes[name] = tagtype | |||
|
180 | ||||
|
181 | ||||
|
182 | # The tag cache only stores info about heads, not the tag contents | |||
|
183 | # from each head. I.e. it doesn't try to squeeze out the maximum | |||
|
184 | # performance, but is simpler has a better chance of actually | |||
|
185 | # working correctly. And this gives the biggest performance win: it | |||
|
186 | # avoids looking up .hgtags in the manifest for every head, and it | |||
|
187 | # can avoid calling heads() at all if there have been no changes to | |||
|
188 | # the repo. | |||
|
189 | ||||
|
190 | def _readtagcache(ui, repo): | |||
|
191 | '''Read the tag cache and return a tuple (heads, fnodes, cachetags, | |||
|
192 | shouldwrite). If the cache is completely up-to-date, cachetags is a | |||
|
193 | dict of the form returned by _readtags(); otherwise, it is None and | |||
|
194 | heads and fnodes are set. In that case, heads is the list of all | |||
|
195 | heads currently in the repository (ordered from tip to oldest) and | |||
|
196 | fnodes is a mapping from head to .hgtags filenode. If those two are | |||
|
197 | set, caller is responsible for reading tag info from each head.''' | |||
|
198 | ||||
|
199 | try: | |||
|
200 | cachefile = repo.opener('tags.cache', 'r') | |||
|
201 | _debug(ui, 'reading tag cache from %s\n' % cachefile.name) | |||
|
202 | except IOError: | |||
|
203 | cachefile = None | |||
|
204 | ||||
|
205 | # The cache file consists of lines like | |||
|
206 | # <headrev> <headnode> [<tagnode>] | |||
|
207 | # where <headrev> and <headnode> redundantly identify a repository | |||
|
208 | # head from the time the cache was written, and <tagnode> is the | |||
|
209 | # filenode of .hgtags on that head. Heads with no .hgtags file will | |||
|
210 | # have no <tagnode>. The cache is ordered from tip to oldest (which | |||
|
211 | # is part of why <headrev> is there: a quick visual check is all | |||
|
212 | # that's required to ensure correct order). | |||
|
213 | # | |||
|
214 | # This information is enough to let us avoid the most expensive part | |||
|
215 | # of finding global tags, which is looking up <tagnode> in the | |||
|
216 | # manifest for each head. | |||
|
217 | cacherevs = [] # list of headrev | |||
|
218 | cacheheads = [] # list of headnode | |||
|
219 | cachefnode = {} # map headnode to filenode | |||
|
220 | if cachefile: | |||
|
221 | for line in cachefile: | |||
|
222 | if line == "\n": | |||
|
223 | break | |||
|
224 | line = line.rstrip().split() | |||
|
225 | cacherevs.append(int(line[0])) | |||
|
226 | headnode = bin(line[1]) | |||
|
227 | cacheheads.append(headnode) | |||
|
228 | if len(line) == 3: | |||
|
229 | fnode = bin(line[2]) | |||
|
230 | cachefnode[headnode] = fnode | |||
|
231 | ||||
|
232 | tipnode = repo.changelog.tip() | |||
|
233 | tiprev = len(repo.changelog) - 1 | |||
|
234 | ||||
|
235 | # Case 1 (common): tip is the same, so nothing has changed. | |||
|
236 | # (Unchanged tip trivially means no changesets have been added. | |||
|
237 | # But, thanks to localrepository.destroyed(), it also means none | |||
|
238 | # have been destroyed by strip or rollback.) | |||
|
239 | if cacheheads and cacheheads[0] == tipnode and cacherevs[0] == tiprev: | |||
|
240 | _debug(ui, "tag cache: tip unchanged\n") | |||
|
241 | tags = _readtags(ui, repo, cachefile, cachefile.name) | |||
|
242 | cachefile.close() | |||
|
243 | return (None, None, tags, False) | |||
|
244 | if cachefile: | |||
|
245 | cachefile.close() # ignore rest of file | |||
|
246 | ||||
|
247 | repoheads = repo.heads() | |||
|
248 | ||||
|
249 | # Case 2 (uncommon): empty repo; get out quickly and don't bother | |||
|
250 | # writing an empty cache. | |||
|
251 | if repoheads == [nullid]: | |||
|
252 | return ([], {}, {}, False) | |||
|
253 | ||||
|
254 | # Case 3 (uncommon): cache file missing or empty. | |||
|
255 | if not cacheheads: | |||
|
256 | _debug(ui, 'tag cache: cache file missing or empty\n') | |||
|
257 | ||||
|
258 | # Case 4 (uncommon): tip rev decreased. This should only happen | |||
|
259 | # when we're called from localrepository.destroyed(). Refresh the | |||
|
260 | # cache so future invocations will not see disappeared heads in the | |||
|
261 | # cache. | |||
|
262 | elif cacheheads and tiprev < cacherevs[0]: | |||
|
263 | _debug(ui, | |||
|
264 | 'tag cache: tip rev decremented (from %d to %d), ' | |||
|
265 | 'so we must be destroying nodes\n' | |||
|
266 | % (cacherevs[0], tiprev)) | |||
|
267 | ||||
|
268 | # Case 5 (common): tip has changed, so we've added/replaced heads. | |||
|
269 | else: | |||
|
270 | _debug(ui, | |||
|
271 | 'tag cache: tip has changed (%d:%s); must find new heads\n' | |||
|
272 | % (tiprev, short(tipnode))) | |||
|
273 | ||||
|
274 | # Luckily, the code to handle cases 3, 4, 5 is the same. So the | |||
|
275 | # above if/elif/else can disappear once we're confident this thing | |||
|
276 | # actually works and we don't need the debug output. | |||
|
277 | ||||
|
278 | # N.B. in case 4 (nodes destroyed), "new head" really means "newly | |||
|
279 | # exposed". | |||
|
280 | newheads = [head | |||
|
281 | for head in repoheads | |||
|
282 | if head not in set(cacheheads)] | |||
|
283 | _debug(ui, 'tag cache: found %d head(s) not in cache: %s\n' | |||
|
284 | % (len(newheads), map(short, newheads))) | |||
|
285 | ||||
|
286 | # Now we have to lookup the .hgtags filenode for every new head. | |||
|
287 | # This is the most expensive part of finding tags, so performance | |||
|
288 | # depends primarily on the size of newheads. Worst case: no cache | |||
|
289 | # file, so newheads == repoheads. | |||
|
290 | for head in newheads: | |||
|
291 | cctx = repo[head] | |||
|
292 | try: | |||
|
293 | fnode = cctx.filenode('.hgtags') | |||
|
294 | cachefnode[head] = fnode | |||
|
295 | except error.LookupError: | |||
|
296 | # no .hgtags file on this head | |||
|
297 | pass | |||
|
298 | ||||
|
299 | # Caller has to iterate over all heads, but can use the filenodes in | |||
|
300 | # cachefnode to get to each .hgtags revision quickly. | |||
|
301 | return (repoheads, cachefnode, None, True) | |||
|
302 | ||||
|
303 | def _writetagcache(ui, repo, heads, tagfnode, cachetags): | |||
|
304 | ||||
|
305 | cachefile = repo.opener('tags.cache', 'w', atomictemp=True) | |||
|
306 | _debug(ui, 'writing cache file %s\n' % cachefile.name) | |||
|
307 | ||||
|
308 | realheads = repo.heads() # for sanity checks below | |||
|
309 | for head in heads: | |||
|
310 | # temporary sanity checks; these can probably be removed | |||
|
311 | # once this code has been in crew for a few weeks | |||
|
312 | assert head in repo.changelog.nodemap, \ | |||
|
313 | 'trying to write non-existent node %s to tag cache' % short(head) | |||
|
314 | assert head in realheads, \ | |||
|
315 | 'trying to write non-head %s to tag cache' % short(head) | |||
|
316 | assert head != nullid, \ | |||
|
317 | 'trying to write nullid to tag cache' | |||
|
318 | ||||
|
319 | # This can't fail because of the first assert above. When/if we | |||
|
320 | # remove that assert, we might want to catch LookupError here | |||
|
321 | # and downgrade it to a warning. | |||
|
322 | rev = repo.changelog.rev(head) | |||
|
323 | ||||
|
324 | fnode = tagfnode.get(head) | |||
|
325 | if fnode: | |||
|
326 | cachefile.write('%d %s %s\n' % (rev, hex(head), hex(fnode))) | |||
|
327 | else: | |||
|
328 | cachefile.write('%d %s\n' % (rev, hex(head))) | |||
|
329 | ||||
|
330 | # Tag names in the cache are in UTF-8 -- which is the whole reason | |||
|
331 | # we keep them in UTF-8 throughout this module. If we converted | |||
|
332 | # them local encoding on input, we would lose info writing them to | |||
|
333 | # the cache. | |||
|
334 | cachefile.write('\n') | |||
|
335 | for (name, (node, hist)) in cachetags.iteritems(): | |||
|
336 | cachefile.write("%s %s\n" % (hex(node), name)) | |||
|
337 | ||||
|
338 | cachefile.rename() | |||
|
339 | cachefile.close() |
@@ -51,7 +51,7 b' def perftags(ui, repo):' | |||||
51 | def t(): |
|
51 | def t(): | |
52 | repo.changelog = mercurial.changelog.changelog(repo.sopener) |
|
52 | repo.changelog = mercurial.changelog.changelog(repo.sopener) | |
53 | repo.manifest = mercurial.manifest.manifest(repo.sopener) |
|
53 | repo.manifest = mercurial.manifest.manifest(repo.sopener) | |
54 |
repo.tags |
|
54 | repo._tags = None | |
55 | return len(repo.tags()) |
|
55 | return len(repo.tags()) | |
56 | timer(t) |
|
56 | timer(t) | |
57 |
|
57 |
@@ -293,14 +293,11 b' def reposetup(ui, repo):' | |||||
293 | write(self, marks) |
|
293 | write(self, marks) | |
294 | return result |
|
294 | return result | |
295 |
|
295 | |||
296 | def tags(self): |
|
296 | def _findtags(self): | |
297 | """Merge bookmarks with normal tags""" |
|
297 | """Merge bookmarks with normal tags""" | |
298 | if self.tagscache: |
|
298 | (tags, tagtypes) = super(bookmark_repo, self)._findtags() | |
299 | return self.tagscache |
|
299 | tags.update(parse(self)) | |
300 |
|
300 | return (tags, tagtypes) | ||
301 | tagscache = super(bookmark_repo, self).tags() |
|
|||
302 | tagscache.update(parse(self)) |
|
|||
303 | return tagscache |
|
|||
304 |
|
301 | |||
305 | repo.__class__ = bookmark_repo |
|
302 | repo.__class__ = bookmark_repo | |
306 |
|
303 |
@@ -2415,34 +2415,33 b' def reposetup(ui, repo):' | |||||
2415 | raise util.Abort(_('source has mq patches applied')) |
|
2415 | raise util.Abort(_('source has mq patches applied')) | |
2416 | return super(mqrepo, self).push(remote, force, revs) |
|
2416 | return super(mqrepo, self).push(remote, force, revs) | |
2417 |
|
2417 | |||
2418 | def tags(self): |
|
2418 | def _findtags(self): | |
2419 | if self.tagscache: |
|
2419 | '''augment tags from base class with patch tags''' | |
2420 |
|
|
2420 | result = super(mqrepo, self)._findtags() | |
2421 |
|
||||
2422 | tagscache = super(mqrepo, self).tags() |
|
|||
2423 |
|
2421 | |||
2424 | q = self.mq |
|
2422 | q = self.mq | |
2425 | if not q.applied: |
|
2423 | if not q.applied: | |
2426 |
return t |
|
2424 | return result | |
2427 |
|
2425 | |||
2428 | mqtags = [(bin(patch.rev), patch.name) for patch in q.applied] |
|
2426 | mqtags = [(bin(patch.rev), patch.name) for patch in q.applied] | |
2429 |
|
2427 | |||
2430 | if mqtags[-1][0] not in self.changelog.nodemap: |
|
2428 | if mqtags[-1][0] not in self.changelog.nodemap: | |
2431 | self.ui.warn(_('mq status file refers to unknown node %s\n') |
|
2429 | self.ui.warn(_('mq status file refers to unknown node %s\n') | |
2432 | % short(mqtags[-1][0])) |
|
2430 | % short(mqtags[-1][0])) | |
2433 |
return t |
|
2431 | return result | |
2434 |
|
2432 | |||
2435 | mqtags.append((mqtags[-1][0], 'qtip')) |
|
2433 | mqtags.append((mqtags[-1][0], 'qtip')) | |
2436 | mqtags.append((mqtags[0][0], 'qbase')) |
|
2434 | mqtags.append((mqtags[0][0], 'qbase')) | |
2437 | mqtags.append((self.changelog.parents(mqtags[0][0])[0], 'qparent')) |
|
2435 | mqtags.append((self.changelog.parents(mqtags[0][0])[0], 'qparent')) | |
|
2436 | tags = result[0] | |||
2438 | for patch in mqtags: |
|
2437 | for patch in mqtags: | |
2439 |
if patch[1] in tags |
|
2438 | if patch[1] in tags: | |
2440 | self.ui.warn(_('Tag %s overrides mq patch of the same name\n') |
|
2439 | self.ui.warn(_('Tag %s overrides mq patch of the same name\n') | |
2441 | % patch[1]) |
|
2440 | % patch[1]) | |
2442 | else: |
|
2441 | else: | |
2443 |
tags |
|
2442 | tags[patch[1]] = patch[0] | |
2444 |
|
2443 | |||
2445 |
return t |
|
2444 | return result | |
2446 |
|
2445 | |||
2447 | def _branchtags(self, partial, lrev): |
|
2446 | def _branchtags(self, partial, lrev): | |
2448 | q = self.mq |
|
2447 | q = self.mq |
@@ -49,6 +49,9 b' def decode(arg):' | |||||
49 | return tuple(map(decode, arg)) |
|
49 | return tuple(map(decode, arg)) | |
50 | elif isinstance(arg, list): |
|
50 | elif isinstance(arg, list): | |
51 | return map(decode, arg) |
|
51 | return map(decode, arg) | |
|
52 | elif isinstance(arg, dict): | |||
|
53 | for k, v in arg.items(): | |||
|
54 | arg[k] = decode(v) | |||
52 | return arg |
|
55 | return arg | |
53 |
|
56 | |||
54 | def encode(arg): |
|
57 | def encode(arg): | |
@@ -58,29 +61,50 b' def encode(arg):' | |||||
58 | return tuple(map(encode, arg)) |
|
61 | return tuple(map(encode, arg)) | |
59 | elif isinstance(arg, list): |
|
62 | elif isinstance(arg, list): | |
60 | return map(encode, arg) |
|
63 | return map(encode, arg) | |
|
64 | elif isinstance(arg, dict): | |||
|
65 | for k, v in arg.items(): | |||
|
66 | arg[k] = encode(v) | |||
61 | return arg |
|
67 | return arg | |
62 |
|
68 | |||
63 | def wrapper(func, args): |
|
69 | def appendsep(s): | |
|
70 | # ensure the path ends with os.sep, appending it if necessary. | |||
|
71 | try: | |||
|
72 | us = decode(s) | |||
|
73 | except UnicodeError: | |||
|
74 | us = s | |||
|
75 | if us and us[-1] not in ':/\\': | |||
|
76 | s += os.sep | |||
|
77 | return s | |||
|
78 | ||||
|
79 | def wrapper(func, args, kwds): | |||
64 | # check argument is unicode, then call original |
|
80 | # check argument is unicode, then call original | |
65 | for arg in args: |
|
81 | for arg in args: | |
66 | if isinstance(arg, unicode): |
|
82 | if isinstance(arg, unicode): | |
67 | return func(*args) |
|
83 | return func(*args, **kwds) | |
68 |
|
84 | |||
69 | try: |
|
85 | try: | |
70 | # convert arguments to unicode, call func, then convert back |
|
86 | # convert arguments to unicode, call func, then convert back | |
71 | return encode(func(*decode(args))) |
|
87 | return encode(func(*decode(args), **decode(kwds))) | |
72 | except UnicodeError: |
|
88 | except UnicodeError: | |
73 | # If not encoded with encoding.encoding, report it then |
|
89 | raise util.Abort(_("[win32mbcs] filename conversion failed with" | |
74 | # continue with calling original function. |
|
|||
75 | raise util.Abort(_("[win32mbcs] filename conversion fail with" |
|
|||
76 | " %s encoding\n") % (encoding.encoding)) |
|
90 | " %s encoding\n") % (encoding.encoding)) | |
77 |
|
91 | |||
78 | def wrapname(name): |
|
92 | def wrapperforlistdir(func, args, kwds): | |
|
93 | # Ensure 'path' argument ends with os.sep to avoids | |||
|
94 | # misinterpreting last 0x5c of MBCS 2nd byte as path separator. | |||
|
95 | if args: | |||
|
96 | args = list(args) | |||
|
97 | args[0] = appendsep(args[0]) | |||
|
98 | if kwds.has_key('path'): | |||
|
99 | kwds['path'] = appendsep(kwds['path']) | |||
|
100 | return func(*args, **kwds) | |||
|
101 | ||||
|
102 | def wrapname(name, wrapper): | |||
79 | module, name = name.rsplit('.', 1) |
|
103 | module, name = name.rsplit('.', 1) | |
80 | module = sys.modules[module] |
|
104 | module = sys.modules[module] | |
81 | func = getattr(module, name) |
|
105 | func = getattr(module, name) | |
82 | def f(*args): |
|
106 | def f(*args, **kwds): | |
83 | return wrapper(func, args) |
|
107 | return wrapper(func, args, kwds) | |
84 | try: |
|
108 | try: | |
85 | f.__name__ = func.__name__ # fail with python23 |
|
109 | f.__name__ = func.__name__ # fail with python23 | |
86 | except Exception: |
|
110 | except Exception: | |
@@ -110,7 +134,8 b' def reposetup(ui, repo):' | |||||
110 | # fake is only for relevant environment. |
|
134 | # fake is only for relevant environment. | |
111 | if encoding.encoding.lower() in problematic_encodings.split(): |
|
135 | if encoding.encoding.lower() in problematic_encodings.split(): | |
112 | for f in funcs.split(): |
|
136 | for f in funcs.split(): | |
113 | wrapname(f) |
|
137 | wrapname(f, wrapper) | |
|
138 | wrapname("mercurial.osutil.listdir", wrapperforlistdir) | |||
114 | ui.debug(_("[win32mbcs] activated with encoding: %s\n") |
|
139 | ui.debug(_("[win32mbcs] activated with encoding: %s\n") | |
115 | % encoding.encoding) |
|
140 | % encoding.encoding) | |
116 |
|
141 |
@@ -13,6 +13,7 b' import lock, transaction, store, encodin' | |||||
13 | import util, extensions, hook, error |
|
13 | import util, extensions, hook, error | |
14 | import match as match_ |
|
14 | import match as match_ | |
15 | import merge as merge_ |
|
15 | import merge as merge_ | |
|
16 | import tags as tags_ | |||
16 | from lock import release |
|
17 | from lock import release | |
17 | import weakref, stat, errno, os, time, inspect |
|
18 | import weakref, stat, errno, os, time, inspect | |
18 | propertycache = util.propertycache |
|
19 | propertycache = util.propertycache | |
@@ -89,8 +90,14 b' class localrepository(repo.repository):' | |||||
89 | self.sjoin = self.store.join |
|
90 | self.sjoin = self.store.join | |
90 | self.opener.createmode = self.store.createmode |
|
91 | self.opener.createmode = self.store.createmode | |
91 |
|
92 | |||
92 | self.tagscache = None |
|
93 | # These two define the set of tags for this repository. _tags | |
93 | self._tagstypecache = None |
|
94 | # maps tag name to node; _tagtypes maps tag name to 'global' or | |
|
95 | # 'local'. (Global tags are defined by .hgtags across all | |||
|
96 | # heads, and local tags are defined in .hg/localtags.) They | |||
|
97 | # constitute the in-memory cache of tags. | |||
|
98 | self._tags = None | |||
|
99 | self._tagtypes = None | |||
|
100 | ||||
94 | self.branchcache = None |
|
101 | self.branchcache = None | |
95 | self._ubranchcache = None # UTF-8 version of branchcache |
|
102 | self._ubranchcache = None # UTF-8 version of branchcache | |
96 | self._branchcachetip = None |
|
103 | self._branchcachetip = None | |
@@ -160,8 +167,8 b' class localrepository(repo.repository):' | |||||
160 | fp.write('\n') |
|
167 | fp.write('\n') | |
161 | for name in names: |
|
168 | for name in names: | |
162 | m = munge and munge(name) or name |
|
169 | m = munge and munge(name) or name | |
163 |
if self._tag |
|
170 | if self._tagtypes and name in self._tagtypes: | |
164 |
old = self.tags |
|
171 | old = self._tags.get(name, nullid) | |
165 | fp.write('%s %s\n' % (hex(old), m)) |
|
172 | fp.write('%s %s\n' % (hex(old), m)) | |
166 | fp.write('%s %s\n' % (hex(node), m)) |
|
173 | fp.write('%s %s\n' % (hex(node), m)) | |
167 | fp.close() |
|
174 | fp.close() | |
@@ -233,100 +240,43 b' class localrepository(repo.repository):' | |||||
233 |
|
240 | |||
234 | def tags(self): |
|
241 | def tags(self): | |
235 | '''return a mapping of tag to node''' |
|
242 | '''return a mapping of tag to node''' | |
236 |
if self. |
|
243 | if self._tags is None: | |
237 | return self.tagscache |
|
244 | (self._tags, self._tagtypes) = self._findtags() | |
|
245 | ||||
|
246 | return self._tags | |||
238 |
|
247 | |||
239 | globaltags = {} |
|
248 | def _findtags(self): | |
|
249 | '''Do the hard work of finding tags. Return a pair of dicts | |||
|
250 | (tags, tagtypes) where tags maps tag name to node, and tagtypes | |||
|
251 | maps tag name to a string like \'global\' or \'local\'. | |||
|
252 | Subclasses or extensions are free to add their own tags, but | |||
|
253 | should be aware that the returned dicts will be retained for the | |||
|
254 | duration of the localrepo object.''' | |||
|
255 | ||||
|
256 | # XXX what tagtype should subclasses/extensions use? Currently | |||
|
257 | # mq and bookmarks add tags, but do not set the tagtype at all. | |||
|
258 | # Should each extension invent its own tag type? Should there | |||
|
259 | # be one tagtype for all such "virtual" tags? Or is the status | |||
|
260 | # quo fine? | |||
|
261 | ||||
|
262 | alltags = {} # map tag name to (node, hist) | |||
240 | tagtypes = {} |
|
263 | tagtypes = {} | |
241 |
|
264 | |||
242 | def readtags(lines, fn, tagtype): |
|
265 | tags_.findglobaltags(self.ui, self, alltags, tagtypes) | |
243 | filetags = {} |
|
266 | tags_.readlocaltags(self.ui, self, alltags, tagtypes) | |
244 | count = 0 |
|
|||
245 |
|
||||
246 | def warn(msg): |
|
|||
247 | self.ui.warn(_("%s, line %s: %s\n") % (fn, count, msg)) |
|
|||
248 |
|
||||
249 | for l in lines: |
|
|||
250 | count += 1 |
|
|||
251 | if not l: |
|
|||
252 | continue |
|
|||
253 | s = l.split(" ", 1) |
|
|||
254 | if len(s) != 2: |
|
|||
255 | warn(_("cannot parse entry")) |
|
|||
256 | continue |
|
|||
257 | node, key = s |
|
|||
258 | key = encoding.tolocal(key.strip()) # stored in UTF-8 |
|
|||
259 | try: |
|
|||
260 | bin_n = bin(node) |
|
|||
261 | except TypeError: |
|
|||
262 | warn(_("node '%s' is not well formed") % node) |
|
|||
263 | continue |
|
|||
264 | if bin_n not in self.changelog.nodemap: |
|
|||
265 | # silently ignore as pull -r might cause this |
|
|||
266 | continue |
|
|||
267 |
|
||||
268 | h = [] |
|
|||
269 | if key in filetags: |
|
|||
270 | n, h = filetags[key] |
|
|||
271 | h.append(n) |
|
|||
272 | filetags[key] = (bin_n, h) |
|
|||
273 |
|
||||
274 | for k, nh in filetags.iteritems(): |
|
|||
275 | if k not in globaltags: |
|
|||
276 | globaltags[k] = nh |
|
|||
277 | tagtypes[k] = tagtype |
|
|||
278 | continue |
|
|||
279 |
|
267 | |||
280 | # we prefer the global tag if: |
|
268 | # Build the return dicts. Have to re-encode tag names because | |
281 | # it supercedes us OR |
|
269 | # the tags module always uses UTF-8 (in order not to lose info | |
282 | # mutual supercedes and it has a higher rank |
|
270 | # writing to the cache), but the rest of Mercurial wants them in | |
283 | # otherwise we win because we're tip-most |
|
271 | # local encoding. | |
284 | an, ah = nh |
|
272 | tags = {} | |
285 | bn, bh = globaltags[k] |
|
273 | for (name, (node, hist)) in alltags.iteritems(): | |
286 | if (bn != an and an in bh and |
|
274 | if node != nullid: | |
287 | (bn not in ah or len(bh) > len(ah))): |
|
275 | tags[encoding.tolocal(name)] = node | |
288 | an = bn |
|
276 | tags['tip'] = self.changelog.tip() | |
289 | ah.extend([n for n in bh if n not in ah]) |
|
277 | tagtypes = dict([(encoding.tolocal(name), value) | |
290 | globaltags[k] = an, ah |
|
278 | for (name, value) in tagtypes.iteritems()]) | |
291 | tagtypes[k] = tagtype |
|
279 | return (tags, tagtypes) | |
292 |
|
||||
293 | seen = set() |
|
|||
294 | f = None |
|
|||
295 | ctxs = [] |
|
|||
296 | for node in self.heads(): |
|
|||
297 | try: |
|
|||
298 | fnode = self[node].filenode('.hgtags') |
|
|||
299 | except error.LookupError: |
|
|||
300 | continue |
|
|||
301 | if fnode not in seen: |
|
|||
302 | seen.add(fnode) |
|
|||
303 | if not f: |
|
|||
304 | f = self.filectx('.hgtags', fileid=fnode) |
|
|||
305 | else: |
|
|||
306 | f = f.filectx(fnode) |
|
|||
307 | ctxs.append(f) |
|
|||
308 |
|
||||
309 | # read the tags file from each head, ending with the tip |
|
|||
310 | for f in reversed(ctxs): |
|
|||
311 | readtags(f.data().splitlines(), f, "global") |
|
|||
312 |
|
||||
313 | try: |
|
|||
314 | data = encoding.fromlocal(self.opener("localtags").read()) |
|
|||
315 | # localtags are stored in the local character set |
|
|||
316 | # while the internal tag table is stored in UTF-8 |
|
|||
317 | readtags(data.splitlines(), "localtags", "local") |
|
|||
318 | except IOError: |
|
|||
319 | pass |
|
|||
320 |
|
||||
321 | self.tagscache = {} |
|
|||
322 | self._tagstypecache = {} |
|
|||
323 | for k, nh in globaltags.iteritems(): |
|
|||
324 | n = nh[0] |
|
|||
325 | if n != nullid: |
|
|||
326 | self.tagscache[k] = n |
|
|||
327 | self._tagstypecache[k] = tagtypes[k] |
|
|||
328 | self.tagscache['tip'] = self.changelog.tip() |
|
|||
329 | return self.tagscache |
|
|||
330 |
|
280 | |||
331 | def tagtype(self, tagname): |
|
281 | def tagtype(self, tagname): | |
332 | ''' |
|
282 | ''' | |
@@ -339,7 +289,7 b' class localrepository(repo.repository):' | |||||
339 |
|
289 | |||
340 | self.tags() |
|
290 | self.tags() | |
341 |
|
291 | |||
342 |
return self._tag |
|
292 | return self._tagtypes.get(tagname) | |
343 |
|
293 | |||
344 | def tagslist(self): |
|
294 | def tagslist(self): | |
345 | '''return a list of tags ordered by revision''' |
|
295 | '''return a list of tags ordered by revision''' | |
@@ -668,6 +618,7 b' class localrepository(repo.repository):' | |||||
668 | % encoding.tolocal(self.dirstate.branch())) |
|
618 | % encoding.tolocal(self.dirstate.branch())) | |
669 | self.invalidate() |
|
619 | self.invalidate() | |
670 | self.dirstate.invalidate() |
|
620 | self.dirstate.invalidate() | |
|
621 | self.destroyed() | |||
671 | else: |
|
622 | else: | |
672 | self.ui.warn(_("no rollback information available\n")) |
|
623 | self.ui.warn(_("no rollback information available\n")) | |
673 | finally: |
|
624 | finally: | |
@@ -677,8 +628,8 b' class localrepository(repo.repository):' | |||||
677 | for a in "changelog manifest".split(): |
|
628 | for a in "changelog manifest".split(): | |
678 | if a in self.__dict__: |
|
629 | if a in self.__dict__: | |
679 | delattr(self, a) |
|
630 | delattr(self, a) | |
680 |
self.tags |
|
631 | self._tags = None | |
681 |
self._tag |
|
632 | self._tagtypes = None | |
682 | self.nodetagscache = None |
|
633 | self.nodetagscache = None | |
683 | self.branchcache = None |
|
634 | self.branchcache = None | |
684 | self._ubranchcache = None |
|
635 | self._ubranchcache = None | |
@@ -966,6 +917,25 b' class localrepository(repo.repository):' | |||||
966 | del tr |
|
917 | del tr | |
967 | lock.release() |
|
918 | lock.release() | |
968 |
|
919 | |||
|
920 | def destroyed(self): | |||
|
921 | '''Inform the repository that nodes have been destroyed. | |||
|
922 | Intended for use by strip and rollback, so there's a common | |||
|
923 | place for anything that has to be done after destroying history.''' | |||
|
924 | # XXX it might be nice if we could take the list of destroyed | |||
|
925 | # nodes, but I don't see an easy way for rollback() to do that | |||
|
926 | ||||
|
927 | # Ensure the persistent tag cache is updated. Doing it now | |||
|
928 | # means that the tag cache only has to worry about destroyed | |||
|
929 | # heads immediately after a strip/rollback. That in turn | |||
|
930 | # guarantees that "cachetip == currenttip" (comparing both rev | |||
|
931 | # and node) always means no nodes have been added or destroyed. | |||
|
932 | ||||
|
933 | # XXX this is suboptimal when qrefresh'ing: we strip the current | |||
|
934 | # head, refresh the tag cache, then immediately add a new head. | |||
|
935 | # But I think doing it this way is necessary for the "instant | |||
|
936 | # tag cache retrieval" case to work. | |||
|
937 | tags_.findglobaltags(self.ui, self, {}, {}) | |||
|
938 | ||||
969 | def walk(self, match, node=None): |
|
939 | def walk(self, match, node=None): | |
970 | ''' |
|
940 | ''' | |
971 | walk recursively through the directory tree or a given |
|
941 | walk recursively through the directory tree or a given |
@@ -142,3 +142,4 b' def strip(ui, repo, node, backup="all"):' | |||||
142 | if backup != "strip": |
|
142 | if backup != "strip": | |
143 | os.unlink(chgrpfile) |
|
143 | os.unlink(chgrpfile) | |
144 |
|
144 | |||
|
145 | repo.destroyed() |
@@ -114,7 +114,7 b' class statichttprepository(localrepo.loc' | |||||
114 |
|
114 | |||
115 | self.manifest = manifest.manifest(self.sopener) |
|
115 | self.manifest = manifest.manifest(self.sopener) | |
116 | self.changelog = changelog.changelog(self.sopener) |
|
116 | self.changelog = changelog.changelog(self.sopener) | |
117 |
self.tags |
|
117 | self._tags = None | |
118 | self.nodetagscache = None |
|
118 | self.nodetagscache = None | |
119 | self.encodepats = None |
|
119 | self.encodepats = None | |
120 | self.decodepats = None |
|
120 | self.decodepats = None |
@@ -284,16 +284,17 b' class fncachestore(basicstore):' | |||||
284 | self.pathjoiner = pathjoiner |
|
284 | self.pathjoiner = pathjoiner | |
285 | self.path = self.pathjoiner(path, 'store') |
|
285 | self.path = self.pathjoiner(path, 'store') | |
286 | self.createmode = _calcmode(self.path) |
|
286 | self.createmode = _calcmode(self.path) | |
287 |
|
|
287 | op = opener(self.path) | |
288 |
|
|
288 | op.createmode = self.createmode | |
289 |
|
|
289 | fnc = fncache(op) | |
|
290 | self.fncache = fnc | |||
290 |
|
291 | |||
291 | def fncacheopener(path, mode='r', *args, **kw): |
|
292 | def fncacheopener(path, mode='r', *args, **kw): | |
292 | if (mode not in ('r', 'rb') |
|
293 | if (mode not in ('r', 'rb') | |
293 | and path.startswith('data/') |
|
294 | and path.startswith('data/') | |
294 |
and path not in |
|
295 | and path not in fnc): | |
295 |
|
|
296 | fnc.add(path) | |
296 |
return |
|
297 | return op(hybridencode(path), mode, *args, **kw) | |
297 | self.opener = fncacheopener |
|
298 | self.opener = fncacheopener | |
298 |
|
299 | |||
299 | def join(self, f): |
|
300 | def join(self, f): |
@@ -349,3 +349,33 b' class ui(object):' | |||||
349 | self.config("ui", "editor") or |
|
349 | self.config("ui", "editor") or | |
350 | os.environ.get("VISUAL") or |
|
350 | os.environ.get("VISUAL") or | |
351 | os.environ.get("EDITOR", "vi")) |
|
351 | os.environ.get("EDITOR", "vi")) | |
|
352 | ||||
|
353 | def progress(self, topic, pos, item="", unit="", total=None): | |||
|
354 | '''show a progress message | |||
|
355 | ||||
|
356 | With stock hg, this is simply a debug message that is hidden | |||
|
357 | by default, but with extensions or GUI tools it may be | |||
|
358 | visible. 'topic' is the current operation, 'item' is a | |||
|
359 | non-numeric marker of the current position (ie the currently | |||
|
360 | in-process file), 'pos' is the current numeric position (ie | |||
|
361 | revision, bytes, etc.), units is a corresponding unit label, | |||
|
362 | and total is the highest expected pos. | |||
|
363 | ||||
|
364 | Multiple nested topics may be active at a time. All topics | |||
|
365 | should be marked closed by setting pos to None at termination. | |||
|
366 | ''' | |||
|
367 | ||||
|
368 | if pos == None or not self.debugflag: | |||
|
369 | return | |||
|
370 | ||||
|
371 | if units: | |||
|
372 | units = ' ' + units | |||
|
373 | if item: | |||
|
374 | item = ' ' + item | |||
|
375 | ||||
|
376 | if total: | |||
|
377 | pct = 100.0 * pos / total | |||
|
378 | ui.debug('%s:%s %s/%s%s (%4.2g%%)\n' | |||
|
379 | % (topic, item, pos, total, units, pct)) | |||
|
380 | else: | |||
|
381 | ui.debug('%s:%s %s%s\n' % (topic, item, pos, units)) |
@@ -107,9 +107,18 b' echo % qpop' | |||||
107 | hg qpop |
|
107 | hg qpop | |
108 | checkundo qpop |
|
108 | checkundo qpop | |
109 |
|
109 | |||
110 | echo % qpush |
|
110 | echo % qpush with dump of tag cache | |
111 |
|
111 | |||
|
112 | # Dump the tag cache to ensure that it has exactly one head after qpush. | |||
|
113 | rm -f .hg/tags.cache | |||
|
114 | hg tags > /dev/null | |||
|
115 | echo ".hg/tags.cache (pre qpush):" | |||
|
116 | sed 's/ [0-9a-f]*//' .hg/tags.cache | |||
112 | hg qpush |
|
117 | hg qpush | |
|
118 | hg tags > /dev/null | |||
|
119 | echo ".hg/tags.cache (post qpush):" | |||
|
120 | sed 's/ [0-9a-f]*//' .hg/tags.cache | |||
|
121 | ||||
113 | checkundo qpush |
|
122 | checkundo qpush | |
114 |
|
123 | |||
115 | cd .. |
|
124 | cd .. |
@@ -110,9 +110,15 b' working dir diff:' | |||||
110 | % qpop |
|
110 | % qpop | |
111 | popping test.patch |
|
111 | popping test.patch | |
112 | patch queue now empty |
|
112 | patch queue now empty | |
113 | % qpush |
|
113 | % qpush with dump of tag cache | |
|
114 | .hg/tags.cache (pre qpush): | |||
|
115 | 1 | |||
|
116 | ||||
114 | applying test.patch |
|
117 | applying test.patch | |
115 | now at: test.patch |
|
118 | now at: test.patch | |
|
119 | .hg/tags.cache (post qpush): | |||
|
120 | 2 | |||
|
121 | ||||
116 | % pop/push outside repo |
|
122 | % pop/push outside repo | |
117 | popping test.patch |
|
123 | popping test.patch | |
118 | patch queue now empty |
|
124 | patch queue now empty |
@@ -1,24 +1,46 b'' | |||||
1 | #!/bin/sh |
|
1 | #!/bin/sh | |
2 |
|
2 | |||
|
3 | cacheexists() { | |||
|
4 | [ -f .hg/tags.cache ] && echo "tag cache exists" || echo "no tag cache" | |||
|
5 | } | |||
|
6 | ||||
|
7 | # XXX need to test that the tag cache works when we strip an old head | |||
|
8 | # and add a new one rooted off non-tip: i.e. node and rev of tip are the | |||
|
9 | # same, but stuff has changed behind tip. | |||
|
10 | ||||
|
11 | echo "% setup" | |||
3 | mkdir t |
|
12 | mkdir t | |
4 | cd t |
|
13 | cd t | |
5 | hg init |
|
14 | hg init | |
|
15 | cacheexists | |||
6 | hg id |
|
16 | hg id | |
|
17 | cacheexists | |||
7 | echo a > a |
|
18 | echo a > a | |
8 | hg add a |
|
19 | hg add a | |
9 |
hg commit -m "test" |
|
20 | hg commit -m "test" | |
10 | hg co |
|
21 | hg co | |
11 | hg identify |
|
22 | hg identify | |
12 | T=`hg tip --debug | head -n 1 | cut -d : -f 3` |
|
23 | cacheexists | |
|
24 | ||||
|
25 | echo "% create local tag with long name" | |||
|
26 | T=`hg identify --debug --id` | |||
13 | hg tag -l "This is a local tag with a really long name!" |
|
27 | hg tag -l "This is a local tag with a really long name!" | |
14 | hg tags |
|
28 | hg tags | |
15 | rm .hg/localtags |
|
29 | rm .hg/localtags | |
|
30 | ||||
|
31 | echo "% create a tag behind hg's back" | |||
16 | echo "$T first" > .hgtags |
|
32 | echo "$T first" > .hgtags | |
17 | cat .hgtags |
|
33 | cat .hgtags | |
18 | hg add .hgtags |
|
34 | hg add .hgtags | |
19 |
hg commit -m "add tags" |
|
35 | hg commit -m "add tags" | |
20 | hg tags |
|
36 | hg tags | |
21 | hg identify |
|
37 | hg identify | |
|
38 | ||||
|
39 | # repeat with cold tag cache | |||
|
40 | rm -f .hg/tags.cache | |||
|
41 | hg identify | |||
|
42 | ||||
|
43 | echo "% create a branch" | |||
22 | echo bb > a |
|
44 | echo bb > a | |
23 | hg status |
|
45 | hg status | |
24 | hg identify |
|
46 | hg identify | |
@@ -28,89 +50,126 b' hg -v id' | |||||
28 | hg status |
|
50 | hg status | |
29 | echo 1 > b |
|
51 | echo 1 > b | |
30 | hg add b |
|
52 | hg add b | |
31 |
hg commit -m "branch" |
|
53 | hg commit -m "branch" | |
32 | hg id |
|
54 | hg id | |
|
55 | ||||
|
56 | echo "% merge the two heads" | |||
33 | hg merge 1 |
|
57 | hg merge 1 | |
34 | hg id |
|
58 | hg id | |
35 | hg status |
|
59 | hg status | |
36 |
|
60 | |||
37 |
hg commit -m "merge" |
|
61 | hg commit -m "merge" | |
38 |
|
62 | |||
39 |
|
|
63 | echo "% create fake head, make sure tag not visible afterwards" | |
40 | cp .hgtags tags |
|
64 | cp .hgtags tags | |
41 | hg tag -d "1000000 0" last |
|
65 | hg tag last | |
42 | hg rm .hgtags |
|
66 | hg rm .hgtags | |
43 |
hg commit -m "remove" |
|
67 | hg commit -m "remove" | |
44 |
|
68 | |||
45 | mv tags .hgtags |
|
69 | mv tags .hgtags | |
46 | hg add .hgtags |
|
70 | hg add .hgtags | |
47 |
hg commit -m "readd" |
|
71 | hg commit -m "readd" | |
48 |
|
72 | |||
49 | hg tags |
|
73 | hg tags | |
50 |
|
74 | |||
51 |
|
|
75 | echo "% add invalid tags" | |
52 | echo "spam" >> .hgtags |
|
76 | echo "spam" >> .hgtags | |
53 | echo >> .hgtags |
|
77 | echo >> .hgtags | |
54 | echo "foo bar" >> .hgtags |
|
78 | echo "foo bar" >> .hgtags | |
55 | echo "$T invalid" | sed "s/..../a5a5/" >> .hg/localtags |
|
79 | echo "$T invalid" | sed "s/..../a5a5/" >> .hg/localtags | |
56 | hg commit -m "tags" -d "1000000 0" |
|
80 | echo "committing .hgtags:" | |
|
81 | cat .hgtags | |||
|
82 | hg commit -m "tags" | |||
57 |
|
83 | |||
58 |
|
|
84 | echo "% report tag parse error on other head" | |
59 | hg up 3 |
|
85 | hg up 3 | |
60 | echo 'x y' >> .hgtags |
|
86 | echo 'x y' >> .hgtags | |
61 |
hg commit -m "head" |
|
87 | hg commit -m "head" | |
62 |
|
88 | |||
63 | hg tags |
|
89 | hg tags | |
64 | hg tip |
|
90 | hg tip | |
65 |
|
91 | |||
66 |
|
|
92 | echo "% test tag precedence rules" | |
67 | cd .. |
|
93 | cd .. | |
68 | hg init t2 |
|
94 | hg init t2 | |
69 | cd t2 |
|
95 | cd t2 | |
70 | echo foo > foo |
|
96 | echo foo > foo | |
71 | hg add foo |
|
97 | hg add foo | |
72 |
hg ci -m 'add foo' |
|
98 | hg ci -m 'add foo' # rev 0 | |
73 |
hg tag |
|
99 | hg tag bar # rev 1 | |
74 | echo >> foo |
|
100 | echo >> foo | |
75 |
hg ci -m 'change foo 1' |
|
101 | hg ci -m 'change foo 1' # rev 2 | |
76 | hg up -C 1 |
|
102 | hg up -C 1 | |
77 |
hg tag -r 1 - |
|
103 | hg tag -r 1 -f bar # rev 3 | |
78 | hg up -C 1 |
|
104 | hg up -C 1 | |
79 | echo >> foo |
|
105 | echo >> foo | |
80 |
hg ci -m 'change foo 2' |
|
106 | hg ci -m 'change foo 2' # rev 4 | |
|
107 | hg tags | |||
|
108 | hg tags # repeat in case of cache effects | |||
|
109 | ||||
|
110 | dumptags() { | |||
|
111 | rev=$1 | |||
|
112 | echo "rev $rev: .hgtags:" | |||
|
113 | hg cat -r$rev .hgtags | |||
|
114 | } | |||
|
115 | ||||
|
116 | echo "% detailed dump of tag info" | |||
|
117 | echo "heads:" | |||
|
118 | hg heads -q # expect 4, 3, 2 | |||
|
119 | dumptags 2 | |||
|
120 | dumptags 3 | |||
|
121 | dumptags 4 | |||
|
122 | echo ".hg/tags.cache:" | |||
|
123 | [ -f .hg/tags.cache ] && cat .hg/tags.cache || echo "no such file" | |||
|
124 | ||||
|
125 | echo "% test tag removal" | |||
|
126 | hg tag --remove bar # rev 5 | |||
|
127 | hg tip -vp | |||
|
128 | hg tags | |||
|
129 | hg tags # again, try to expose cache bugs | |||
|
130 | ||||
|
131 | echo '% remove nonexistent tag' | |||
|
132 | hg tag --remove foobar | |||
|
133 | hg tip | |||
|
134 | ||||
|
135 | echo "% rollback undoes tag operation" | |||
|
136 | hg rollback # destroy rev 5 (restore bar) | |||
|
137 | hg tags | |||
81 | hg tags |
|
138 | hg tags | |
82 |
|
139 | |||
83 |
|
|
140 | echo "% test tag rank" | |
84 | hg tag --remove -d '1000000 0' bar |
|
|||
85 | hg tip |
|
|||
86 | hg tags |
|
|||
87 |
|
||||
88 | echo '% remove nonexistent tag' |
|
|||
89 | hg tag --remove -d '1000000 0' foobar |
|
|||
90 | hg tip |
|
|||
91 |
|
||||
92 | # test tag rank |
|
|||
93 | cd .. |
|
141 | cd .. | |
94 | hg init t3 |
|
142 | hg init t3 | |
95 | cd t3 |
|
143 | cd t3 | |
96 | echo foo > foo |
|
144 | echo foo > foo | |
97 | hg add foo |
|
145 | hg add foo | |
98 |
hg ci -m 'add foo' |
|
146 | hg ci -m 'add foo' # rev 0 | |
99 |
hg tag - |
|
147 | hg tag -f bar # rev 1 bar -> 0 | |
100 |
hg tag - |
|
148 | hg tag -f bar # rev 2 bar -> 1 | |
101 |
hg tag - |
|
149 | hg tag -fr 0 bar # rev 3 bar -> 0 | |
102 |
hg tag - |
|
150 | hg tag -fr 1 bar # rev 4 bar -> 1 | |
103 |
hg tag - |
|
151 | hg tag -fr 0 bar # rev 5 bar -> 0 | |
104 | hg tags |
|
152 | hg tags | |
105 | hg co 3 |
|
153 | hg co 3 | |
106 | echo barbar > foo |
|
154 | echo barbar > foo | |
107 |
hg ci -m 'change foo' |
|
155 | hg ci -m 'change foo' # rev 6 | |
|
156 | hg tags | |||
|
157 | ||||
|
158 | echo "% don't allow moving tag without -f" | |||
|
159 | hg tag -r 3 bar | |||
108 | hg tags |
|
160 | hg tags | |
109 |
|
161 | |||
110 | hg tag -d '1000000 0' -r 3 bar # should complain |
|
162 | echo "% strip 1: expose an old head" | |
111 | hg tags |
|
163 | hg --config extensions.mq= strip 5 > /dev/null 2>&1 | |
|
164 | hg tags # partly stale cache | |||
|
165 | hg tags # up-to-date cache | |||
|
166 | echo "% strip 2: destroy whole branch, no old head exposed" | |||
|
167 | hg --config extensions.mq= strip 4 > /dev/null 2>&1 | |||
|
168 | hg tags # partly stale | |||
|
169 | rm -f .hg/tags.cache | |||
|
170 | hg tags # cold cache | |||
112 |
|
171 | |||
113 |
|
|
172 | echo "% test tag rank with 3 heads" | |
114 | cd .. |
|
173 | cd .. | |
115 | hg init t4 |
|
174 | hg init t4 | |
116 | cd t4 |
|
175 | cd t4 | |
@@ -124,10 +183,11 b' hg tag -fr 2 bar # rev' | |||||
124 | hg tags |
|
183 | hg tags | |
125 | hg up -qC 0 |
|
184 | hg up -qC 0 | |
126 | hg tag -m 'retag rev 0' -fr 0 bar # rev 4 bar -> 0, but bar stays at 2 |
|
185 | hg tag -m 'retag rev 0' -fr 0 bar # rev 4 bar -> 0, but bar stays at 2 | |
127 | echo % bar should still point to rev 2 |
|
186 | echo "% bar should still point to rev 2" | |
128 | hg tags |
|
187 | hg tags | |
129 |
|
188 | |||
130 |
|
189 | |||
|
190 | echo "% remove local as global and global as local" | |||
131 | # test that removing global/local tags does not get confused when trying |
|
191 | # test that removing global/local tags does not get confused when trying | |
132 | # to remove a tag of type X which actually only exists as a type Y |
|
192 | # to remove a tag of type X which actually only exists as a type Y | |
133 | cd .. |
|
193 | cd .. |
@@ -1,78 +1,147 b'' | |||||
|
1 | % setup | |||
|
2 | no tag cache | |||
1 | 000000000000 tip |
|
3 | 000000000000 tip | |
|
4 | no tag cache | |||
2 | 0 files updated, 0 files merged, 0 files removed, 0 files unresolved |
|
5 | 0 files updated, 0 files merged, 0 files removed, 0 files unresolved | |
3 | 0acdaf898367 tip |
|
6 | acb14030fe0a tip | |
4 | tip 0:0acdaf898367 |
|
7 | tag cache exists | |
5 | This is a local tag with a really long name! 0:0acdaf898367 |
|
8 | % create local tag with long name | |
6 | 0acdaf8983679e0aac16e811534eb49d7ee1f2b4 first |
|
9 | tip 0:acb14030fe0a | |
7 | tip 1:8a3ca90d111d |
|
10 | This is a local tag with a really long name! 0:acb14030fe0a | |
8 | first 0:0acdaf898367 |
|
11 | % create a tag behind hg's back | |
9 | 8a3ca90d111d tip |
|
12 | acb14030fe0a21b60322c440ad2d20cf7685a376 first | |
|
13 | tip 1:b9154636be93 | |||
|
14 | first 0:acb14030fe0a | |||
|
15 | b9154636be93 tip | |||
|
16 | b9154636be93 tip | |||
|
17 | % create a branch | |||
10 | M a |
|
18 | M a | |
11 | 8a3ca90d111d+ tip |
|
19 | b9154636be93+ tip | |
12 | 0 files updated, 0 files merged, 1 files removed, 0 files unresolved |
|
20 | 0 files updated, 0 files merged, 1 files removed, 0 files unresolved | |
13 | 0acdaf898367+ first |
|
21 | acb14030fe0a+ first | |
14 | 0acdaf898367+ first |
|
22 | acb14030fe0a+ first | |
15 | M a |
|
23 | M a | |
16 | created new head |
|
24 | created new head | |
17 | 8216907a933d tip |
|
25 | c8edf04160c7 tip | |
|
26 | % merge the two heads | |||
18 | 1 files updated, 0 files merged, 0 files removed, 0 files unresolved |
|
27 | 1 files updated, 0 files merged, 0 files removed, 0 files unresolved | |
19 | (branch merge, don't forget to commit) |
|
28 | (branch merge, don't forget to commit) | |
20 | 8216907a933d+8a3ca90d111d+ tip |
|
29 | c8edf04160c7+b9154636be93+ tip | |
21 | M .hgtags |
|
30 | M .hgtags | |
22 | tip 6:e2174d339386 |
|
31 | % create fake head, make sure tag not visible afterwards | |
23 |
|
|
32 | tip 6:35ff301afafe | |
|
33 | first 0:acb14030fe0a | |||
|
34 | % add invalid tags | |||
|
35 | committing .hgtags: | |||
|
36 | acb14030fe0a21b60322c440ad2d20cf7685a376 first | |||
|
37 | spam | |||
|
38 | ||||
|
39 | foo bar | |||
|
40 | % report tag parse error on other head | |||
24 | 1 files updated, 0 files merged, 0 files removed, 0 files unresolved |
|
41 | 1 files updated, 0 files merged, 0 files removed, 0 files unresolved | |
25 | created new head |
|
42 | created new head | |
26 |
.hgtags@ |
|
43 | .hgtags@75d9f02dfe28, line 2: cannot parse entry | |
27 |
.hgtags@ |
|
44 | .hgtags@75d9f02dfe28, line 4: node 'foo' is not well formed | |
28 |
.hgtags@ |
|
45 | .hgtags@c4be69a18c11, line 2: node 'x' is not well formed | |
29 |
tip 8: |
|
46 | tip 8:c4be69a18c11 | |
30 |
first 0: |
|
47 | first 0:acb14030fe0a | |
31 |
changeset: 8: |
|
48 | changeset: 8:c4be69a18c11 | |
32 | .hgtags@c071f74ab5eb, line 2: cannot parse entry |
|
|||
33 | .hgtags@c071f74ab5eb, line 4: node 'foo' is not well formed |
|
|||
34 | .hgtags@4ca6f1b1a68c, line 2: node 'x' is not well formed |
|
|||
35 | tag: tip |
|
49 | tag: tip | |
36 |
parent: 3: |
|
50 | parent: 3:ac5e980c4dc0 | |
37 | user: test |
|
51 | user: test | |
38 |
date: |
|
52 | date: Thu Jan 01 00:00:00 1970 +0000 | |
39 | summary: head |
|
53 | summary: head | |
40 |
|
54 | |||
|
55 | % test tag precedence rules | |||
41 | 1 files updated, 0 files merged, 0 files removed, 0 files unresolved |
|
56 | 1 files updated, 0 files merged, 0 files removed, 0 files unresolved | |
42 | 1 files updated, 0 files merged, 0 files removed, 0 files unresolved |
|
57 | 1 files updated, 0 files merged, 0 files removed, 0 files unresolved | |
43 | created new head |
|
58 | created new head | |
44 |
tip 4: |
|
59 | tip 4:0c192d7d5e6b | |
45 |
bar 1: |
|
60 | bar 1:78391a272241 | |
46 | changeset: 5:1f98c77278de |
|
61 | tip 4:0c192d7d5e6b | |
|
62 | bar 1:78391a272241 | |||
|
63 | % detailed dump of tag info | |||
|
64 | heads: | |||
|
65 | 4:0c192d7d5e6b | |||
|
66 | 3:6fa450212aeb | |||
|
67 | 2:7a94127795a3 | |||
|
68 | rev 2: .hgtags: | |||
|
69 | bbd179dfa0a71671c253b3ae0aa1513b60d199fa bar | |||
|
70 | rev 3: .hgtags: | |||
|
71 | bbd179dfa0a71671c253b3ae0aa1513b60d199fa bar | |||
|
72 | bbd179dfa0a71671c253b3ae0aa1513b60d199fa bar | |||
|
73 | 78391a272241d70354aa14c874552cad6b51bb42 bar | |||
|
74 | rev 4: .hgtags: | |||
|
75 | bbd179dfa0a71671c253b3ae0aa1513b60d199fa bar | |||
|
76 | .hg/tags.cache: | |||
|
77 | 4 0c192d7d5e6b78a714de54a2e9627952a877e25a 0c04f2a8af31de17fab7422878ee5a2dadbc943d | |||
|
78 | 3 6fa450212aeb2a21ed616a54aea39a4a27894cd7 7d3b718c964ef37b89e550ebdafd5789e76ce1b0 | |||
|
79 | 2 7a94127795a33c10a370c93f731fd9fea0b79af6 0c04f2a8af31de17fab7422878ee5a2dadbc943d | |||
|
80 | ||||
|
81 | 78391a272241d70354aa14c874552cad6b51bb42 bar | |||
|
82 | % test tag removal | |||
|
83 | changeset: 5:5f6e8655b1c7 | |||
47 | tag: tip |
|
84 | tag: tip | |
48 | user: test |
|
85 | user: test | |
49 |
date: |
|
86 | date: Thu Jan 01 00:00:00 1970 +0000 | |
50 | summary: Removed tag bar |
|
87 | files: .hgtags | |
|
88 | description: | |||
|
89 | Removed tag bar | |||
|
90 | ||||
51 |
|
91 | |||
52 | tip 5:1f98c77278de |
|
92 | diff -r 0c192d7d5e6b -r 5f6e8655b1c7 .hgtags | |
|
93 | --- a/.hgtags Thu Jan 01 00:00:00 1970 +0000 | |||
|
94 | +++ b/.hgtags Thu Jan 01 00:00:00 1970 +0000 | |||
|
95 | @@ -1,1 +1,3 @@ | |||
|
96 | bbd179dfa0a71671c253b3ae0aa1513b60d199fa bar | |||
|
97 | +78391a272241d70354aa14c874552cad6b51bb42 bar | |||
|
98 | +0000000000000000000000000000000000000000 bar | |||
|
99 | ||||
|
100 | tip 5:5f6e8655b1c7 | |||
|
101 | tip 5:5f6e8655b1c7 | |||
53 | % remove nonexistent tag |
|
102 | % remove nonexistent tag | |
54 | abort: tag 'foobar' does not exist |
|
103 | abort: tag 'foobar' does not exist | |
55 |
changeset: 5: |
|
104 | changeset: 5:5f6e8655b1c7 | |
56 | tag: tip |
|
105 | tag: tip | |
57 | user: test |
|
106 | user: test | |
58 |
date: |
|
107 | date: Thu Jan 01 00:00:00 1970 +0000 | |
59 | summary: Removed tag bar |
|
108 | summary: Removed tag bar | |
60 |
|
109 | |||
61 | tip 5:e86d7ed95fd3 |
|
110 | % rollback undoes tag operation | |
62 | bar 0:b409d9da318e |
|
111 | rolling back last transaction | |
|
112 | tip 4:0c192d7d5e6b | |||
|
113 | bar 1:78391a272241 | |||
|
114 | tip 4:0c192d7d5e6b | |||
|
115 | bar 1:78391a272241 | |||
|
116 | % test tag rank | |||
|
117 | tip 5:85f05169d91d | |||
|
118 | bar 0:bbd179dfa0a7 | |||
63 | 1 files updated, 0 files merged, 0 files removed, 0 files unresolved |
|
119 | 1 files updated, 0 files merged, 0 files removed, 0 files unresolved | |
64 | created new head |
|
120 | created new head | |
65 |
tip 6: |
|
121 | tip 6:735c3ca72986 | |
66 |
bar 0:b |
|
122 | bar 0:bbd179dfa0a7 | |
|
123 | % don't allow moving tag without -f | |||
67 | abort: tag 'bar' already exists (use -f to force) |
|
124 | abort: tag 'bar' already exists (use -f to force) | |
68 |
tip 6: |
|
125 | tip 6:735c3ca72986 | |
69 |
bar 0:b |
|
126 | bar 0:bbd179dfa0a7 | |
|
127 | % strip 1: expose an old head | |||
|
128 | tip 5:735c3ca72986 | |||
|
129 | bar 1:78391a272241 | |||
|
130 | tip 5:735c3ca72986 | |||
|
131 | bar 1:78391a272241 | |||
|
132 | % strip 2: destroy whole branch, no old head exposed | |||
|
133 | tip 4:735c3ca72986 | |||
|
134 | bar 0:bbd179dfa0a7 | |||
|
135 | tip 4:735c3ca72986 | |||
|
136 | bar 0:bbd179dfa0a7 | |||
|
137 | % test tag rank with 3 heads | |||
70 | adding foo |
|
138 | adding foo | |
71 | tip 3:197c21bbbf2c |
|
139 | tip 3:197c21bbbf2c | |
72 | bar 2:6fa450212aeb |
|
140 | bar 2:6fa450212aeb | |
73 | % bar should still point to rev 2 |
|
141 | % bar should still point to rev 2 | |
74 | tip 4:3b4b14ed0202 |
|
142 | tip 4:3b4b14ed0202 | |
75 | bar 2:6fa450212aeb |
|
143 | bar 2:6fa450212aeb | |
|
144 | % remove local as global and global as local | |||
76 | adding foo |
|
145 | adding foo | |
77 | abort: tag 'localtag' is not a global tag |
|
146 | abort: tag 'localtag' is not a global tag | |
78 | abort: tag 'globaltag' is not a local tag |
|
147 | abort: tag 'globaltag' is not a local tag |
General Comments 0
You need to be logged in to leave comments.
Login now