##// END OF EJS Templates
bundlerepo: improve performance for bundle() revset expression...
Mads Kiilerich -
r18411:8b0f0dd5 default
parent child Browse files
Show More
@@ -1,389 +1,389
1 1 # bundlerepo.py - repository class for viewing uncompressed bundles
2 2 #
3 3 # Copyright 2006, 2007 Benoit Boissinot <bboissin@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 """Repository class for viewing uncompressed bundles.
9 9
10 10 This provides a read-only repository interface to bundles as if they
11 11 were part of the actual repository.
12 12 """
13 13
14 14 from node import nullid
15 15 from i18n import _
16 16 import os, tempfile, shutil
17 17 import changegroup, util, mdiff, discovery, cmdutil, scmutil
18 18 import localrepo, changelog, manifest, filelog, revlog, error
19 19
20 20 class bundlerevlog(revlog.revlog):
21 21 def __init__(self, opener, indexfile, bundle, linkmapper):
22 22 # How it works:
23 23 # To retrieve a revision, we need to know the offset of the revision in
24 24 # the bundle (an unbundle object). We store this offset in the index
25 25 # (start).
26 26 #
27 27 # basemap is indexed with revisions coming from the bundle, and it
28 # maps to the corresponding node that is the base of the corresponding
29 # delta.
28 # maps to the node that is the base of the corresponding delta.
30 29 #
31 30 # To differentiate a rev in the bundle from a rev in the revlog, we
32 31 # check revision against basemap.
33 32 opener = scmutil.readonlyvfs(opener)
34 33 revlog.revlog.__init__(self, opener, indexfile)
35 34 self.bundle = bundle
36 35 self.basemap = {}
37 36 n = len(self)
38 37 self.disktiprev = n - 1
39 38 chain = None
40 self.bundlenodes = []
39 self.bundlerevs = set() # used by 'bundle()' revset expression
41 40 while True:
42 41 chunkdata = bundle.deltachunk(chain)
43 42 if not chunkdata:
44 43 break
45 44 node = chunkdata['node']
46 45 p1 = chunkdata['p1']
47 46 p2 = chunkdata['p2']
48 47 cs = chunkdata['cs']
49 48 deltabase = chunkdata['deltabase']
50 49 delta = chunkdata['delta']
51 50
52 51 size = len(delta)
53 52 start = bundle.tell() - size
54 53
55 54 link = linkmapper(cs)
56 self.bundlenodes.append(node)
57 55 if node in self.nodemap:
58 56 # this can happen if two branches make the same change
59 57 chain = node
58 self.bundlerevs.add(self.nodemap[node])
60 59 continue
61 60
62 61 for p in (p1, p2):
63 62 if p not in self.nodemap:
64 63 raise error.LookupError(p, self.indexfile,
65 64 _("unknown parent"))
66 65 # start, size, full unc. size, base (unused), link, p1, p2, node
67 66 e = (revlog.offset_type(start, 0), size, -1, -1, link,
68 67 self.rev(p1), self.rev(p2), node)
69 68 self.basemap[n] = deltabase
70 69 self.index.insert(-1, e)
71 70 self.nodemap[node] = n
71 self.bundlerevs.add(n)
72 72 chain = node
73 73 n += 1
74 74
75 75 def inbundle(self, rev):
76 76 """is rev from the bundle"""
77 77 if rev < 0:
78 78 return False
79 79 return rev in self.basemap
80 80 def bundlebase(self, rev):
81 81 return self.basemap[rev]
82 82 def _chunk(self, rev):
83 83 # Warning: in case of bundle, the diff is against bundlebase,
84 84 # not against rev - 1
85 85 # XXX: could use some caching
86 86 if not self.inbundle(rev):
87 87 return revlog.revlog._chunk(self, rev)
88 88 self.bundle.seek(self.start(rev))
89 89 return self.bundle.read(self.length(rev))
90 90
91 91 def revdiff(self, rev1, rev2):
92 92 """return or calculate a delta between two revisions"""
93 93 if self.inbundle(rev1) and self.inbundle(rev2):
94 94 # hot path for bundle
95 95 revb = self.rev(self.bundlebase(rev2))
96 96 if revb == rev1:
97 97 return self._chunk(rev2)
98 98 elif not self.inbundle(rev1) and not self.inbundle(rev2):
99 99 return revlog.revlog.revdiff(self, rev1, rev2)
100 100
101 101 return mdiff.textdiff(self.revision(self.node(rev1)),
102 102 self.revision(self.node(rev2)))
103 103
104 104 def revision(self, nodeorrev):
105 105 """return an uncompressed revision of a given node or revision
106 106 number.
107 107 """
108 108 if isinstance(nodeorrev, int):
109 109 rev = nodeorrev
110 110 node = self.node(rev)
111 111 else:
112 112 node = nodeorrev
113 113 rev = self.rev(node)
114 114
115 115 if node == nullid:
116 116 return ""
117 117
118 118 text = None
119 119 chain = []
120 120 iter_node = node
121 121 # reconstruct the revision if it is from a changegroup
122 122 while self.inbundle(rev):
123 123 if self._cache and self._cache[0] == iter_node:
124 124 text = self._cache[2]
125 125 break
126 126 chain.append(rev)
127 127 iter_node = self.bundlebase(rev)
128 128 rev = self.rev(iter_node)
129 129 if text is None:
130 130 text = revlog.revlog.revision(self, iter_node)
131 131
132 132 while chain:
133 133 delta = self._chunk(chain.pop())
134 134 text = mdiff.patches(text, [delta])
135 135
136 136 p1, p2 = self.parents(node)
137 137 if node != revlog.hash(text, p1, p2):
138 138 raise error.RevlogError(_("integrity check failed on %s:%d")
139 139 % (self.datafile, self.rev(node)))
140 140
141 141 self._cache = (node, self.rev(node), text)
142 142 return text
143 143
144 144 def addrevision(self, text, transaction, link, p1=None, p2=None, d=None):
145 145 raise NotImplementedError
146 146 def addgroup(self, revs, linkmapper, transaction):
147 147 raise NotImplementedError
148 148 def strip(self, rev, minlink):
149 149 raise NotImplementedError
150 150 def checksize(self):
151 151 raise NotImplementedError
152 152
153 153 class bundlechangelog(bundlerevlog, changelog.changelog):
154 154 def __init__(self, opener, bundle):
155 155 changelog.changelog.__init__(self, opener)
156 156 linkmapper = lambda x: x
157 157 bundlerevlog.__init__(self, opener, self.indexfile, bundle,
158 158 linkmapper)
159 159
160 160 class bundlemanifest(bundlerevlog, manifest.manifest):
161 161 def __init__(self, opener, bundle, linkmapper):
162 162 manifest.manifest.__init__(self, opener)
163 163 bundlerevlog.__init__(self, opener, self.indexfile, bundle,
164 164 linkmapper)
165 165
166 166 class bundlefilelog(bundlerevlog, filelog.filelog):
167 167 def __init__(self, opener, path, bundle, linkmapper, repo):
168 168 filelog.filelog.__init__(self, opener, path)
169 169 bundlerevlog.__init__(self, opener, self.indexfile, bundle,
170 170 linkmapper)
171 171 self._repo = repo
172 172
173 173 def _file(self, f):
174 174 self._repo.file(f)
175 175
176 176 class bundlepeer(localrepo.localpeer):
177 177 def canpush(self):
178 178 return False
179 179
180 180 class bundlerepository(localrepo.localrepository):
181 181 def __init__(self, ui, path, bundlename):
182 182 self._tempparent = None
183 183 try:
184 184 localrepo.localrepository.__init__(self, ui, path)
185 185 except error.RepoError:
186 186 self._tempparent = tempfile.mkdtemp()
187 187 localrepo.instance(ui, self._tempparent, 1)
188 188 localrepo.localrepository.__init__(self, ui, self._tempparent)
189 189 self.ui.setconfig('phases', 'publish', False)
190 190
191 191 if path:
192 192 self._url = 'bundle:' + util.expandpath(path) + '+' + bundlename
193 193 else:
194 194 self._url = 'bundle:' + bundlename
195 195
196 196 self.tempfile = None
197 197 f = util.posixfile(bundlename, "rb")
198 198 self.bundle = changegroup.readbundle(f, bundlename)
199 199 if self.bundle.compressed():
200 200 fdtemp, temp = tempfile.mkstemp(prefix="hg-bundle-",
201 201 suffix=".hg10un", dir=self.path)
202 202 self.tempfile = temp
203 203 fptemp = os.fdopen(fdtemp, 'wb')
204 204
205 205 try:
206 206 fptemp.write("HG10UN")
207 207 while True:
208 208 chunk = self.bundle.read(2**18)
209 209 if not chunk:
210 210 break
211 211 fptemp.write(chunk)
212 212 finally:
213 213 fptemp.close()
214 214
215 215 f = util.posixfile(self.tempfile, "rb")
216 216 self.bundle = changegroup.readbundle(f, bundlename)
217 217
218 218 # dict with the mapping 'filename' -> position in the bundle
219 219 self.bundlefilespos = {}
220 220
221 221 @localrepo.unfilteredpropertycache
222 222 def changelog(self):
223 223 # consume the header if it exists
224 224 self.bundle.changelogheader()
225 225 c = bundlechangelog(self.sopener, self.bundle)
226 226 self.manstart = self.bundle.tell()
227 227 return c
228 228
229 229 @localrepo.unfilteredpropertycache
230 230 def manifest(self):
231 231 self.bundle.seek(self.manstart)
232 232 # consume the header if it exists
233 233 self.bundle.manifestheader()
234 234 m = bundlemanifest(self.sopener, self.bundle, self.changelog.rev)
235 235 self.filestart = self.bundle.tell()
236 236 return m
237 237
238 238 @localrepo.unfilteredpropertycache
239 239 def manstart(self):
240 240 self.changelog
241 241 return self.manstart
242 242
243 243 @localrepo.unfilteredpropertycache
244 244 def filestart(self):
245 245 self.manifest
246 246 return self.filestart
247 247
248 248 def url(self):
249 249 return self._url
250 250
251 251 def file(self, f):
252 252 if not self.bundlefilespos:
253 253 self.bundle.seek(self.filestart)
254 254 while True:
255 255 chunkdata = self.bundle.filelogheader()
256 256 if not chunkdata:
257 257 break
258 258 fname = chunkdata['filename']
259 259 self.bundlefilespos[fname] = self.bundle.tell()
260 260 while True:
261 261 c = self.bundle.deltachunk(None)
262 262 if not c:
263 263 break
264 264
265 265 if f[0] == '/':
266 266 f = f[1:]
267 267 if f in self.bundlefilespos:
268 268 self.bundle.seek(self.bundlefilespos[f])
269 269 return bundlefilelog(self.sopener, f, self.bundle,
270 270 self.changelog.rev, self)
271 271 else:
272 272 return filelog.filelog(self.sopener, f)
273 273
274 274 def close(self):
275 275 """Close assigned bundle file immediately."""
276 276 self.bundle.close()
277 277 if self.tempfile is not None:
278 278 os.unlink(self.tempfile)
279 279 if self._tempparent:
280 280 shutil.rmtree(self._tempparent, True)
281 281
282 282 def cancopy(self):
283 283 return False
284 284
285 285 def peer(self):
286 286 return bundlepeer(self)
287 287
288 288 def getcwd(self):
289 289 return os.getcwd() # always outside the repo
290 290
291 291
292 292 def instance(ui, path, create):
293 293 if create:
294 294 raise util.Abort(_('cannot create new bundle repository'))
295 295 parentpath = ui.config("bundle", "mainreporoot", "")
296 296 if not parentpath:
297 297 # try to find the correct path to the working directory repo
298 298 parentpath = cmdutil.findrepo(os.getcwd())
299 299 if parentpath is None:
300 300 parentpath = ''
301 301 if parentpath:
302 302 # Try to make the full path relative so we get a nice, short URL.
303 303 # In particular, we don't want temp dir names in test outputs.
304 304 cwd = os.getcwd()
305 305 if parentpath == cwd:
306 306 parentpath = ''
307 307 else:
308 308 cwd = os.path.join(cwd,'')
309 309 if parentpath.startswith(cwd):
310 310 parentpath = parentpath[len(cwd):]
311 311 u = util.url(path)
312 312 path = u.localpath()
313 313 if u.scheme == 'bundle':
314 314 s = path.split("+", 1)
315 315 if len(s) == 1:
316 316 repopath, bundlename = parentpath, s[0]
317 317 else:
318 318 repopath, bundlename = s
319 319 else:
320 320 repopath, bundlename = parentpath, path
321 321 return bundlerepository(ui, repopath, bundlename)
322 322
323 323 def getremotechanges(ui, repo, other, onlyheads=None, bundlename=None,
324 324 force=False):
325 325 '''obtains a bundle of changes incoming from other
326 326
327 327 "onlyheads" restricts the returned changes to those reachable from the
328 328 specified heads.
329 329 "bundlename", if given, stores the bundle to this file path permanently;
330 330 otherwise it's stored to a temp file and gets deleted again when you call
331 331 the returned "cleanupfn".
332 332 "force" indicates whether to proceed on unrelated repos.
333 333
334 334 Returns a tuple (local, csets, cleanupfn):
335 335
336 336 "local" is a local repo from which to obtain the actual incoming
337 337 changesets; it is a bundlerepo for the obtained bundle when the
338 338 original "other" is remote.
339 339 "csets" lists the incoming changeset node ids.
340 340 "cleanupfn" must be called without arguments when you're done processing
341 341 the changes; it closes both the original "other" and the one returned
342 342 here.
343 343 '''
344 344 tmp = discovery.findcommonincoming(repo, other, heads=onlyheads,
345 345 force=force)
346 346 common, incoming, rheads = tmp
347 347 if not incoming:
348 348 try:
349 349 if bundlename:
350 350 os.unlink(bundlename)
351 351 except OSError:
352 352 pass
353 353 return repo, [], other.close
354 354
355 355 bundle = None
356 356 bundlerepo = None
357 357 localrepo = other.local()
358 358 if bundlename or not localrepo:
359 359 # create a bundle (uncompressed if other repo is not local)
360 360
361 361 if other.capable('getbundle'):
362 362 cg = other.getbundle('incoming', common=common, heads=rheads)
363 363 elif onlyheads is None and not other.capable('changegroupsubset'):
364 364 # compat with older servers when pulling all remote heads
365 365 cg = other.changegroup(incoming, "incoming")
366 366 rheads = None
367 367 else:
368 368 cg = other.changegroupsubset(incoming, rheads, 'incoming')
369 369 bundletype = localrepo and "HG10BZ" or "HG10UN"
370 370 fname = bundle = changegroup.writebundle(cg, bundlename, bundletype)
371 371 # keep written bundle?
372 372 if bundlename:
373 373 bundle = None
374 374 if not localrepo:
375 375 # use the created uncompressed bundlerepo
376 376 localrepo = bundlerepo = bundlerepository(ui, repo.root, fname)
377 377 # this repo contains local and other now, so filter out local again
378 378 common = repo.heads()
379 379
380 380 csets = localrepo.changelog.findmissing(common, rheads)
381 381
382 382 def cleanup():
383 383 if bundlerepo:
384 384 bundlerepo.close()
385 385 if bundle:
386 386 os.unlink(bundle)
387 387 other.close()
388 388
389 389 return (localrepo, csets, cleanup)
@@ -1,1937 +1,1936
1 1 # revset.py - revision set queries for mercurial
2 2 #
3 3 # Copyright 2010 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 import re
9 9 import parser, util, error, discovery, hbisect, phases
10 10 import node
11 11 import bookmarks as bookmarksmod
12 12 import match as matchmod
13 13 from i18n import _
14 14 import encoding
15 15 import obsolete as obsmod
16 16 import repoview
17 17
18 18 def _revancestors(repo, revs, followfirst):
19 19 """Like revlog.ancestors(), but supports followfirst."""
20 20 cut = followfirst and 1 or None
21 21 cl = repo.changelog
22 22 visit = util.deque(revs)
23 23 seen = set([node.nullrev])
24 24 while visit:
25 25 for parent in cl.parentrevs(visit.popleft())[:cut]:
26 26 if parent not in seen:
27 27 visit.append(parent)
28 28 seen.add(parent)
29 29 yield parent
30 30
31 31 def _revdescendants(repo, revs, followfirst):
32 32 """Like revlog.descendants() but supports followfirst."""
33 33 cut = followfirst and 1 or None
34 34 cl = repo.changelog
35 35 first = min(revs)
36 36 nullrev = node.nullrev
37 37 if first == nullrev:
38 38 # Are there nodes with a null first parent and a non-null
39 39 # second one? Maybe. Do we care? Probably not.
40 40 for i in cl:
41 41 yield i
42 42 return
43 43
44 44 seen = set(revs)
45 45 for i in cl.revs(first + 1):
46 46 for x in cl.parentrevs(i)[:cut]:
47 47 if x != nullrev and x in seen:
48 48 seen.add(i)
49 49 yield i
50 50 break
51 51
52 52 def _revsbetween(repo, roots, heads):
53 53 """Return all paths between roots and heads, inclusive of both endpoint
54 54 sets."""
55 55 if not roots:
56 56 return []
57 57 parentrevs = repo.changelog.parentrevs
58 58 visit = heads[:]
59 59 reachable = set()
60 60 seen = {}
61 61 minroot = min(roots)
62 62 roots = set(roots)
63 63 # open-code the post-order traversal due to the tiny size of
64 64 # sys.getrecursionlimit()
65 65 while visit:
66 66 rev = visit.pop()
67 67 if rev in roots:
68 68 reachable.add(rev)
69 69 parents = parentrevs(rev)
70 70 seen[rev] = parents
71 71 for parent in parents:
72 72 if parent >= minroot and parent not in seen:
73 73 visit.append(parent)
74 74 if not reachable:
75 75 return []
76 76 for rev in sorted(seen):
77 77 for parent in seen[rev]:
78 78 if parent in reachable:
79 79 reachable.add(rev)
80 80 return sorted(reachable)
81 81
82 82 elements = {
83 83 "(": (20, ("group", 1, ")"), ("func", 1, ")")),
84 84 "~": (18, None, ("ancestor", 18)),
85 85 "^": (18, None, ("parent", 18), ("parentpost", 18)),
86 86 "-": (5, ("negate", 19), ("minus", 5)),
87 87 "::": (17, ("dagrangepre", 17), ("dagrange", 17),
88 88 ("dagrangepost", 17)),
89 89 "..": (17, ("dagrangepre", 17), ("dagrange", 17),
90 90 ("dagrangepost", 17)),
91 91 ":": (15, ("rangepre", 15), ("range", 15), ("rangepost", 15)),
92 92 "not": (10, ("not", 10)),
93 93 "!": (10, ("not", 10)),
94 94 "and": (5, None, ("and", 5)),
95 95 "&": (5, None, ("and", 5)),
96 96 "or": (4, None, ("or", 4)),
97 97 "|": (4, None, ("or", 4)),
98 98 "+": (4, None, ("or", 4)),
99 99 ",": (2, None, ("list", 2)),
100 100 ")": (0, None, None),
101 101 "symbol": (0, ("symbol",), None),
102 102 "string": (0, ("string",), None),
103 103 "end": (0, None, None),
104 104 }
105 105
106 106 keywords = set(['and', 'or', 'not'])
107 107
108 108 def tokenize(program):
109 109 '''
110 110 Parse a revset statement into a stream of tokens
111 111
112 112 Check that @ is a valid unquoted token character (issue3686):
113 113 >>> list(tokenize("@::"))
114 114 [('symbol', '@', 0), ('::', None, 1), ('end', None, 3)]
115 115
116 116 '''
117 117
118 118 pos, l = 0, len(program)
119 119 while pos < l:
120 120 c = program[pos]
121 121 if c.isspace(): # skip inter-token whitespace
122 122 pass
123 123 elif c == ':' and program[pos:pos + 2] == '::': # look ahead carefully
124 124 yield ('::', None, pos)
125 125 pos += 1 # skip ahead
126 126 elif c == '.' and program[pos:pos + 2] == '..': # look ahead carefully
127 127 yield ('..', None, pos)
128 128 pos += 1 # skip ahead
129 129 elif c in "():,-|&+!~^": # handle simple operators
130 130 yield (c, None, pos)
131 131 elif (c in '"\'' or c == 'r' and
132 132 program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings
133 133 if c == 'r':
134 134 pos += 1
135 135 c = program[pos]
136 136 decode = lambda x: x
137 137 else:
138 138 decode = lambda x: x.decode('string-escape')
139 139 pos += 1
140 140 s = pos
141 141 while pos < l: # find closing quote
142 142 d = program[pos]
143 143 if d == '\\': # skip over escaped characters
144 144 pos += 2
145 145 continue
146 146 if d == c:
147 147 yield ('string', decode(program[s:pos]), s)
148 148 break
149 149 pos += 1
150 150 else:
151 151 raise error.ParseError(_("unterminated string"), s)
152 152 # gather up a symbol/keyword
153 153 elif c.isalnum() or c in '._@' or ord(c) > 127:
154 154 s = pos
155 155 pos += 1
156 156 while pos < l: # find end of symbol
157 157 d = program[pos]
158 158 if not (d.isalnum() or d in "._/@" or ord(d) > 127):
159 159 break
160 160 if d == '.' and program[pos - 1] == '.': # special case for ..
161 161 pos -= 1
162 162 break
163 163 pos += 1
164 164 sym = program[s:pos]
165 165 if sym in keywords: # operator keywords
166 166 yield (sym, None, s)
167 167 else:
168 168 yield ('symbol', sym, s)
169 169 pos -= 1
170 170 else:
171 171 raise error.ParseError(_("syntax error"), pos)
172 172 pos += 1
173 173 yield ('end', None, pos)
174 174
175 175 # helpers
176 176
177 177 def getstring(x, err):
178 178 if x and (x[0] == 'string' or x[0] == 'symbol'):
179 179 return x[1]
180 180 raise error.ParseError(err)
181 181
182 182 def getlist(x):
183 183 if not x:
184 184 return []
185 185 if x[0] == 'list':
186 186 return getlist(x[1]) + [x[2]]
187 187 return [x]
188 188
189 189 def getargs(x, min, max, err):
190 190 l = getlist(x)
191 191 if len(l) < min or (max >= 0 and len(l) > max):
192 192 raise error.ParseError(err)
193 193 return l
194 194
195 195 def getset(repo, subset, x):
196 196 if not x:
197 197 raise error.ParseError(_("missing argument"))
198 198 return methods[x[0]](repo, subset, *x[1:])
199 199
200 200 def _getrevsource(repo, r):
201 201 extra = repo[r].extra()
202 202 for label in ('source', 'transplant_source', 'rebase_source'):
203 203 if label in extra:
204 204 try:
205 205 return repo[extra[label]].rev()
206 206 except error.RepoLookupError:
207 207 pass
208 208 return None
209 209
210 210 # operator methods
211 211
212 212 def stringset(repo, subset, x):
213 213 x = repo[x].rev()
214 214 if x == -1 and len(subset) == len(repo):
215 215 return [-1]
216 216 if len(subset) == len(repo) or x in subset:
217 217 return [x]
218 218 return []
219 219
220 220 def symbolset(repo, subset, x):
221 221 if x in symbols:
222 222 raise error.ParseError(_("can't use %s here") % x)
223 223 return stringset(repo, subset, x)
224 224
225 225 def rangeset(repo, subset, x, y):
226 226 m = getset(repo, subset, x)
227 227 if not m:
228 228 m = getset(repo, list(repo), x)
229 229
230 230 n = getset(repo, subset, y)
231 231 if not n:
232 232 n = getset(repo, list(repo), y)
233 233
234 234 if not m or not n:
235 235 return []
236 236 m, n = m[0], n[-1]
237 237
238 238 if m < n:
239 239 r = range(m, n + 1)
240 240 else:
241 241 r = range(m, n - 1, -1)
242 242 s = set(subset)
243 243 return [x for x in r if x in s]
244 244
245 245 def dagrange(repo, subset, x, y):
246 246 if subset:
247 247 r = list(repo)
248 248 xs = _revsbetween(repo, getset(repo, r, x), getset(repo, r, y))
249 249 s = set(subset)
250 250 return [r for r in xs if r in s]
251 251 return []
252 252
253 253 def andset(repo, subset, x, y):
254 254 return getset(repo, getset(repo, subset, x), y)
255 255
256 256 def orset(repo, subset, x, y):
257 257 xl = getset(repo, subset, x)
258 258 s = set(xl)
259 259 yl = getset(repo, [r for r in subset if r not in s], y)
260 260 return xl + yl
261 261
262 262 def notset(repo, subset, x):
263 263 s = set(getset(repo, subset, x))
264 264 return [r for r in subset if r not in s]
265 265
266 266 def listset(repo, subset, a, b):
267 267 raise error.ParseError(_("can't use a list in this context"))
268 268
269 269 def func(repo, subset, a, b):
270 270 if a[0] == 'symbol' and a[1] in symbols:
271 271 return symbols[a[1]](repo, subset, b)
272 272 raise error.ParseError(_("not a function: %s") % a[1])
273 273
274 274 # functions
275 275
276 276 def adds(repo, subset, x):
277 277 """``adds(pattern)``
278 278 Changesets that add a file matching pattern.
279 279 """
280 280 # i18n: "adds" is a keyword
281 281 pat = getstring(x, _("adds requires a pattern"))
282 282 return checkstatus(repo, subset, pat, 1)
283 283
284 284 def ancestor(repo, subset, x):
285 285 """``ancestor(single, single)``
286 286 Greatest common ancestor of the two changesets.
287 287 """
288 288 # i18n: "ancestor" is a keyword
289 289 l = getargs(x, 2, 2, _("ancestor requires two arguments"))
290 290 r = list(repo)
291 291 a = getset(repo, r, l[0])
292 292 b = getset(repo, r, l[1])
293 293 if len(a) != 1 or len(b) != 1:
294 294 # i18n: "ancestor" is a keyword
295 295 raise error.ParseError(_("ancestor arguments must be single revisions"))
296 296 an = [repo[a[0]].ancestor(repo[b[0]]).rev()]
297 297
298 298 return [r for r in an if r in subset]
299 299
300 300 def _ancestors(repo, subset, x, followfirst=False):
301 301 args = getset(repo, list(repo), x)
302 302 if not args:
303 303 return []
304 304 s = set(_revancestors(repo, args, followfirst)) | set(args)
305 305 return [r for r in subset if r in s]
306 306
307 307 def ancestors(repo, subset, x):
308 308 """``ancestors(set)``
309 309 Changesets that are ancestors of a changeset in set.
310 310 """
311 311 return _ancestors(repo, subset, x)
312 312
313 313 def _firstancestors(repo, subset, x):
314 314 # ``_firstancestors(set)``
315 315 # Like ``ancestors(set)`` but follows only the first parents.
316 316 return _ancestors(repo, subset, x, followfirst=True)
317 317
318 318 def ancestorspec(repo, subset, x, n):
319 319 """``set~n``
320 320 Changesets that are the Nth ancestor (first parents only) of a changeset
321 321 in set.
322 322 """
323 323 try:
324 324 n = int(n[1])
325 325 except (TypeError, ValueError):
326 326 raise error.ParseError(_("~ expects a number"))
327 327 ps = set()
328 328 cl = repo.changelog
329 329 for r in getset(repo, subset, x):
330 330 for i in range(n):
331 331 r = cl.parentrevs(r)[0]
332 332 ps.add(r)
333 333 return [r for r in subset if r in ps]
334 334
335 335 def author(repo, subset, x):
336 336 """``author(string)``
337 337 Alias for ``user(string)``.
338 338 """
339 339 # i18n: "author" is a keyword
340 340 n = encoding.lower(getstring(x, _("author requires a string")))
341 341 kind, pattern, matcher = _substringmatcher(n)
342 342 return [r for r in subset if matcher(encoding.lower(repo[r].user()))]
343 343
344 344 def bisect(repo, subset, x):
345 345 """``bisect(string)``
346 346 Changesets marked in the specified bisect status:
347 347
348 348 - ``good``, ``bad``, ``skip``: csets explicitly marked as good/bad/skip
349 349 - ``goods``, ``bads`` : csets topologically good/bad
350 350 - ``range`` : csets taking part in the bisection
351 351 - ``pruned`` : csets that are goods, bads or skipped
352 352 - ``untested`` : csets whose fate is yet unknown
353 353 - ``ignored`` : csets ignored due to DAG topology
354 354 - ``current`` : the cset currently being bisected
355 355 """
356 356 # i18n: "bisect" is a keyword
357 357 status = getstring(x, _("bisect requires a string")).lower()
358 358 state = set(hbisect.get(repo, status))
359 359 return [r for r in subset if r in state]
360 360
361 361 # Backward-compatibility
362 362 # - no help entry so that we do not advertise it any more
363 363 def bisected(repo, subset, x):
364 364 return bisect(repo, subset, x)
365 365
366 366 def bookmark(repo, subset, x):
367 367 """``bookmark([name])``
368 368 The named bookmark or all bookmarks.
369 369
370 370 If `name` starts with `re:`, the remainder of the name is treated as
371 371 a regular expression. To match a bookmark that actually starts with `re:`,
372 372 use the prefix `literal:`.
373 373 """
374 374 # i18n: "bookmark" is a keyword
375 375 args = getargs(x, 0, 1, _('bookmark takes one or no arguments'))
376 376 if args:
377 377 bm = getstring(args[0],
378 378 # i18n: "bookmark" is a keyword
379 379 _('the argument to bookmark must be a string'))
380 380 kind, pattern, matcher = _stringmatcher(bm)
381 381 if kind == 'literal':
382 382 bmrev = bookmarksmod.listbookmarks(repo).get(bm, None)
383 383 if not bmrev:
384 384 raise util.Abort(_("bookmark '%s' does not exist") % bm)
385 385 bmrev = repo[bmrev].rev()
386 386 return [r for r in subset if r == bmrev]
387 387 else:
388 388 matchrevs = set()
389 389 for name, bmrev in bookmarksmod.listbookmarks(repo).iteritems():
390 390 if matcher(name):
391 391 matchrevs.add(bmrev)
392 392 if not matchrevs:
393 393 raise util.Abort(_("no bookmarks exist that match '%s'")
394 394 % pattern)
395 395 bmrevs = set()
396 396 for bmrev in matchrevs:
397 397 bmrevs.add(repo[bmrev].rev())
398 398 return [r for r in subset if r in bmrevs]
399 399
400 400 bms = set([repo[r].rev()
401 401 for r in bookmarksmod.listbookmarks(repo).values()])
402 402 return [r for r in subset if r in bms]
403 403
404 404 def branch(repo, subset, x):
405 405 """``branch(string or set)``
406 406 All changesets belonging to the given branch or the branches of the given
407 407 changesets.
408 408
409 409 If `string` starts with `re:`, the remainder of the name is treated as
410 410 a regular expression. To match a branch that actually starts with `re:`,
411 411 use the prefix `literal:`.
412 412 """
413 413 try:
414 414 b = getstring(x, '')
415 415 except error.ParseError:
416 416 # not a string, but another revspec, e.g. tip()
417 417 pass
418 418 else:
419 419 kind, pattern, matcher = _stringmatcher(b)
420 420 if kind == 'literal':
421 421 # note: falls through to the revspec case if no branch with
422 422 # this name exists
423 423 if pattern in repo.branchmap():
424 424 return [r for r in subset if matcher(repo[r].branch())]
425 425 else:
426 426 return [r for r in subset if matcher(repo[r].branch())]
427 427
428 428 s = getset(repo, list(repo), x)
429 429 b = set()
430 430 for r in s:
431 431 b.add(repo[r].branch())
432 432 s = set(s)
433 433 return [r for r in subset if r in s or repo[r].branch() in b]
434 434
435 435 def bumped(repo, subset, x):
436 436 """``bumped()``
437 437 Mutable changesets marked as successors of public changesets.
438 438
439 439 Only non-public and non-obsolete changesets can be `bumped`.
440 440 """
441 441 # i18n: "bumped" is a keyword
442 442 getargs(x, 0, 0, _("bumped takes no arguments"))
443 443 bumped = obsmod.getrevs(repo, 'bumped')
444 444 return [r for r in subset if r in bumped]
445 445
446 446 def bundle(repo, subset, x):
447 447 """``bundle()``
448 448 Changesets in the bundle.
449 449
450 450 Bundle must be specified by the -R option."""
451 451
452 452 try:
453 bundlenodes = repo.changelog.bundlenodes
453 bundlerevs = repo.changelog.bundlerevs
454 454 except AttributeError:
455 455 raise util.Abort(_("no bundle provided - specify with -R"))
456 revs = set(repo[n].rev() for n in bundlenodes)
457 return [r for r in subset if r in revs]
456 return [r for r in subset if r in bundlerevs]
458 457
459 458 def checkstatus(repo, subset, pat, field):
460 459 m = None
461 460 s = []
462 461 hasset = matchmod.patkind(pat) == 'set'
463 462 fname = None
464 463 for r in subset:
465 464 c = repo[r]
466 465 if not m or hasset:
467 466 m = matchmod.match(repo.root, repo.getcwd(), [pat], ctx=c)
468 467 if not m.anypats() and len(m.files()) == 1:
469 468 fname = m.files()[0]
470 469 if fname is not None:
471 470 if fname not in c.files():
472 471 continue
473 472 else:
474 473 for f in c.files():
475 474 if m(f):
476 475 break
477 476 else:
478 477 continue
479 478 files = repo.status(c.p1().node(), c.node())[field]
480 479 if fname is not None:
481 480 if fname in files:
482 481 s.append(r)
483 482 else:
484 483 for f in files:
485 484 if m(f):
486 485 s.append(r)
487 486 break
488 487 return s
489 488
490 489 def _children(repo, narrow, parentset):
491 490 cs = set()
492 491 if not parentset:
493 492 return cs
494 493 pr = repo.changelog.parentrevs
495 494 minrev = min(parentset)
496 495 for r in narrow:
497 496 if r <= minrev:
498 497 continue
499 498 for p in pr(r):
500 499 if p in parentset:
501 500 cs.add(r)
502 501 return cs
503 502
504 503 def children(repo, subset, x):
505 504 """``children(set)``
506 505 Child changesets of changesets in set.
507 506 """
508 507 s = set(getset(repo, list(repo), x))
509 508 cs = _children(repo, subset, s)
510 509 return [r for r in subset if r in cs]
511 510
512 511 def closed(repo, subset, x):
513 512 """``closed()``
514 513 Changeset is closed.
515 514 """
516 515 # i18n: "closed" is a keyword
517 516 getargs(x, 0, 0, _("closed takes no arguments"))
518 517 return [r for r in subset if repo[r].closesbranch()]
519 518
520 519 def contains(repo, subset, x):
521 520 """``contains(pattern)``
522 521 Revision contains a file matching pattern. See :hg:`help patterns`
523 522 for information about file patterns.
524 523 """
525 524 # i18n: "contains" is a keyword
526 525 pat = getstring(x, _("contains requires a pattern"))
527 526 m = None
528 527 s = []
529 528 if not matchmod.patkind(pat):
530 529 for r in subset:
531 530 if pat in repo[r]:
532 531 s.append(r)
533 532 else:
534 533 for r in subset:
535 534 c = repo[r]
536 535 if not m or matchmod.patkind(pat) == 'set':
537 536 m = matchmod.match(repo.root, repo.getcwd(), [pat], ctx=c)
538 537 for f in c.manifest():
539 538 if m(f):
540 539 s.append(r)
541 540 break
542 541 return s
543 542
544 543 def converted(repo, subset, x):
545 544 """``converted([id])``
546 545 Changesets converted from the given identifier in the old repository if
547 546 present, or all converted changesets if no identifier is specified.
548 547 """
549 548
550 549 # There is exactly no chance of resolving the revision, so do a simple
551 550 # string compare and hope for the best
552 551
553 552 rev = None
554 553 # i18n: "converted" is a keyword
555 554 l = getargs(x, 0, 1, _('converted takes one or no arguments'))
556 555 if l:
557 556 # i18n: "converted" is a keyword
558 557 rev = getstring(l[0], _('converted requires a revision'))
559 558
560 559 def _matchvalue(r):
561 560 source = repo[r].extra().get('convert_revision', None)
562 561 return source is not None and (rev is None or source.startswith(rev))
563 562
564 563 return [r for r in subset if _matchvalue(r)]
565 564
566 565 def date(repo, subset, x):
567 566 """``date(interval)``
568 567 Changesets within the interval, see :hg:`help dates`.
569 568 """
570 569 # i18n: "date" is a keyword
571 570 ds = getstring(x, _("date requires a string"))
572 571 dm = util.matchdate(ds)
573 572 return [r for r in subset if dm(repo[r].date()[0])]
574 573
575 574 def desc(repo, subset, x):
576 575 """``desc(string)``
577 576 Search commit message for string. The match is case-insensitive.
578 577 """
579 578 # i18n: "desc" is a keyword
580 579 ds = encoding.lower(getstring(x, _("desc requires a string")))
581 580 l = []
582 581 for r in subset:
583 582 c = repo[r]
584 583 if ds in encoding.lower(c.description()):
585 584 l.append(r)
586 585 return l
587 586
588 587 def _descendants(repo, subset, x, followfirst=False):
589 588 args = getset(repo, list(repo), x)
590 589 if not args:
591 590 return []
592 591 s = set(_revdescendants(repo, args, followfirst)) | set(args)
593 592 return [r for r in subset if r in s]
594 593
595 594 def descendants(repo, subset, x):
596 595 """``descendants(set)``
597 596 Changesets which are descendants of changesets in set.
598 597 """
599 598 return _descendants(repo, subset, x)
600 599
601 600 def _firstdescendants(repo, subset, x):
602 601 # ``_firstdescendants(set)``
603 602 # Like ``descendants(set)`` but follows only the first parents.
604 603 return _descendants(repo, subset, x, followfirst=True)
605 604
606 605 def destination(repo, subset, x):
607 606 """``destination([set])``
608 607 Changesets that were created by a graft, transplant or rebase operation,
609 608 with the given revisions specified as the source. Omitting the optional set
610 609 is the same as passing all().
611 610 """
612 611 if x is not None:
613 612 args = set(getset(repo, list(repo), x))
614 613 else:
615 614 args = set(getall(repo, list(repo), x))
616 615
617 616 dests = set()
618 617
619 618 # subset contains all of the possible destinations that can be returned, so
620 619 # iterate over them and see if their source(s) were provided in the args.
621 620 # Even if the immediate src of r is not in the args, src's source (or
622 621 # further back) may be. Scanning back further than the immediate src allows
623 622 # transitive transplants and rebases to yield the same results as transitive
624 623 # grafts.
625 624 for r in subset:
626 625 src = _getrevsource(repo, r)
627 626 lineage = None
628 627
629 628 while src is not None:
630 629 if lineage is None:
631 630 lineage = list()
632 631
633 632 lineage.append(r)
634 633
635 634 # The visited lineage is a match if the current source is in the arg
636 635 # set. Since every candidate dest is visited by way of iterating
637 636 # subset, any dests further back in the lineage will be tested by a
638 637 # different iteration over subset. Likewise, if the src was already
639 638 # selected, the current lineage can be selected without going back
640 639 # further.
641 640 if src in args or src in dests:
642 641 dests.update(lineage)
643 642 break
644 643
645 644 r = src
646 645 src = _getrevsource(repo, r)
647 646
648 647 return [r for r in subset if r in dests]
649 648
650 649 def divergent(repo, subset, x):
651 650 """``divergent()``
652 651 Final successors of changesets with an alternative set of final successors.
653 652 """
654 653 # i18n: "divergent" is a keyword
655 654 getargs(x, 0, 0, _("divergent takes no arguments"))
656 655 divergent = obsmod.getrevs(repo, 'divergent')
657 656 return [r for r in subset if r in divergent]
658 657
659 658 def draft(repo, subset, x):
660 659 """``draft()``
661 660 Changeset in draft phase."""
662 661 # i18n: "draft" is a keyword
663 662 getargs(x, 0, 0, _("draft takes no arguments"))
664 663 pc = repo._phasecache
665 664 return [r for r in subset if pc.phase(repo, r) == phases.draft]
666 665
667 666 def extinct(repo, subset, x):
668 667 """``extinct()``
669 668 Obsolete changesets with obsolete descendants only.
670 669 """
671 670 # i18n: "extinct" is a keyword
672 671 getargs(x, 0, 0, _("extinct takes no arguments"))
673 672 extincts = obsmod.getrevs(repo, 'extinct')
674 673 return [r for r in subset if r in extincts]
675 674
676 675 def extra(repo, subset, x):
677 676 """``extra(label, [value])``
678 677 Changesets with the given label in the extra metadata, with the given
679 678 optional value.
680 679
681 680 If `value` starts with `re:`, the remainder of the value is treated as
682 681 a regular expression. To match a value that actually starts with `re:`,
683 682 use the prefix `literal:`.
684 683 """
685 684
686 685 # i18n: "extra" is a keyword
687 686 l = getargs(x, 1, 2, _('extra takes at least 1 and at most 2 arguments'))
688 687 # i18n: "extra" is a keyword
689 688 label = getstring(l[0], _('first argument to extra must be a string'))
690 689 value = None
691 690
692 691 if len(l) > 1:
693 692 # i18n: "extra" is a keyword
694 693 value = getstring(l[1], _('second argument to extra must be a string'))
695 694 kind, value, matcher = _stringmatcher(value)
696 695
697 696 def _matchvalue(r):
698 697 extra = repo[r].extra()
699 698 return label in extra and (value is None or matcher(extra[label]))
700 699
701 700 return [r for r in subset if _matchvalue(r)]
702 701
703 702 def filelog(repo, subset, x):
704 703 """``filelog(pattern)``
705 704 Changesets connected to the specified filelog.
706 705
707 706 For performance reasons, ``filelog()`` does not show every changeset
708 707 that affects the requested file(s). See :hg:`help log` for details. For
709 708 a slower, more accurate result, use ``file()``.
710 709 """
711 710
712 711 # i18n: "filelog" is a keyword
713 712 pat = getstring(x, _("filelog requires a pattern"))
714 713 m = matchmod.match(repo.root, repo.getcwd(), [pat], default='relpath',
715 714 ctx=repo[None])
716 715 s = set()
717 716
718 717 if not matchmod.patkind(pat):
719 718 for f in m.files():
720 719 fl = repo.file(f)
721 720 for fr in fl:
722 721 s.add(fl.linkrev(fr))
723 722 else:
724 723 for f in repo[None]:
725 724 if m(f):
726 725 fl = repo.file(f)
727 726 for fr in fl:
728 727 s.add(fl.linkrev(fr))
729 728
730 729 return [r for r in subset if r in s]
731 730
732 731 def first(repo, subset, x):
733 732 """``first(set, [n])``
734 733 An alias for limit().
735 734 """
736 735 return limit(repo, subset, x)
737 736
738 737 def _follow(repo, subset, x, name, followfirst=False):
739 738 l = getargs(x, 0, 1, _("%s takes no arguments or a filename") % name)
740 739 c = repo['.']
741 740 if l:
742 741 x = getstring(l[0], _("%s expected a filename") % name)
743 742 if x in c:
744 743 cx = c[x]
745 744 s = set(ctx.rev() for ctx in cx.ancestors(followfirst=followfirst))
746 745 # include the revision responsible for the most recent version
747 746 s.add(cx.linkrev())
748 747 else:
749 748 return []
750 749 else:
751 750 s = set(_revancestors(repo, [c.rev()], followfirst)) | set([c.rev()])
752 751
753 752 return [r for r in subset if r in s]
754 753
755 754 def follow(repo, subset, x):
756 755 """``follow([file])``
757 756 An alias for ``::.`` (ancestors of the working copy's first parent).
758 757 If a filename is specified, the history of the given file is followed,
759 758 including copies.
760 759 """
761 760 return _follow(repo, subset, x, 'follow')
762 761
763 762 def _followfirst(repo, subset, x):
764 763 # ``followfirst([file])``
765 764 # Like ``follow([file])`` but follows only the first parent of
766 765 # every revision or file revision.
767 766 return _follow(repo, subset, x, '_followfirst', followfirst=True)
768 767
769 768 def getall(repo, subset, x):
770 769 """``all()``
771 770 All changesets, the same as ``0:tip``.
772 771 """
773 772 # i18n: "all" is a keyword
774 773 getargs(x, 0, 0, _("all takes no arguments"))
775 774 return subset
776 775
777 776 def grep(repo, subset, x):
778 777 """``grep(regex)``
779 778 Like ``keyword(string)`` but accepts a regex. Use ``grep(r'...')``
780 779 to ensure special escape characters are handled correctly. Unlike
781 780 ``keyword(string)``, the match is case-sensitive.
782 781 """
783 782 try:
784 783 # i18n: "grep" is a keyword
785 784 gr = re.compile(getstring(x, _("grep requires a string")))
786 785 except re.error, e:
787 786 raise error.ParseError(_('invalid match pattern: %s') % e)
788 787 l = []
789 788 for r in subset:
790 789 c = repo[r]
791 790 for e in c.files() + [c.user(), c.description()]:
792 791 if gr.search(e):
793 792 l.append(r)
794 793 break
795 794 return l
796 795
797 796 def _matchfiles(repo, subset, x):
798 797 # _matchfiles takes a revset list of prefixed arguments:
799 798 #
800 799 # [p:foo, i:bar, x:baz]
801 800 #
802 801 # builds a match object from them and filters subset. Allowed
803 802 # prefixes are 'p:' for regular patterns, 'i:' for include
804 803 # patterns and 'x:' for exclude patterns. Use 'r:' prefix to pass
805 804 # a revision identifier, or the empty string to reference the
806 805 # working directory, from which the match object is
807 806 # initialized. Use 'd:' to set the default matching mode, default
808 807 # to 'glob'. At most one 'r:' and 'd:' argument can be passed.
809 808
810 809 # i18n: "_matchfiles" is a keyword
811 810 l = getargs(x, 1, -1, _("_matchfiles requires at least one argument"))
812 811 pats, inc, exc = [], [], []
813 812 hasset = False
814 813 rev, default = None, None
815 814 for arg in l:
816 815 # i18n: "_matchfiles" is a keyword
817 816 s = getstring(arg, _("_matchfiles requires string arguments"))
818 817 prefix, value = s[:2], s[2:]
819 818 if prefix == 'p:':
820 819 pats.append(value)
821 820 elif prefix == 'i:':
822 821 inc.append(value)
823 822 elif prefix == 'x:':
824 823 exc.append(value)
825 824 elif prefix == 'r:':
826 825 if rev is not None:
827 826 # i18n: "_matchfiles" is a keyword
828 827 raise error.ParseError(_('_matchfiles expected at most one '
829 828 'revision'))
830 829 rev = value
831 830 elif prefix == 'd:':
832 831 if default is not None:
833 832 # i18n: "_matchfiles" is a keyword
834 833 raise error.ParseError(_('_matchfiles expected at most one '
835 834 'default mode'))
836 835 default = value
837 836 else:
838 837 # i18n: "_matchfiles" is a keyword
839 838 raise error.ParseError(_('invalid _matchfiles prefix: %s') % prefix)
840 839 if not hasset and matchmod.patkind(value) == 'set':
841 840 hasset = True
842 841 if not default:
843 842 default = 'glob'
844 843 m = None
845 844 s = []
846 845 for r in subset:
847 846 c = repo[r]
848 847 if not m or (hasset and rev is None):
849 848 ctx = c
850 849 if rev is not None:
851 850 ctx = repo[rev or None]
852 851 m = matchmod.match(repo.root, repo.getcwd(), pats, include=inc,
853 852 exclude=exc, ctx=ctx, default=default)
854 853 for f in c.files():
855 854 if m(f):
856 855 s.append(r)
857 856 break
858 857 return s
859 858
860 859 def hasfile(repo, subset, x):
861 860 """``file(pattern)``
862 861 Changesets affecting files matched by pattern.
863 862
864 863 For a faster but less accurate result, consider using ``filelog()``
865 864 instead.
866 865 """
867 866 # i18n: "file" is a keyword
868 867 pat = getstring(x, _("file requires a pattern"))
869 868 return _matchfiles(repo, subset, ('string', 'p:' + pat))
870 869
871 870 def head(repo, subset, x):
872 871 """``head()``
873 872 Changeset is a named branch head.
874 873 """
875 874 # i18n: "head" is a keyword
876 875 getargs(x, 0, 0, _("head takes no arguments"))
877 876 hs = set()
878 877 for b, ls in repo.branchmap().iteritems():
879 878 hs.update(repo[h].rev() for h in ls)
880 879 return [r for r in subset if r in hs]
881 880
882 881 def heads(repo, subset, x):
883 882 """``heads(set)``
884 883 Members of set with no children in set.
885 884 """
886 885 s = getset(repo, subset, x)
887 886 ps = set(parents(repo, subset, x))
888 887 return [r for r in s if r not in ps]
889 888
890 889 def hidden(repo, subset, x):
891 890 """``hidden()``
892 891 Hidden changesets.
893 892 """
894 893 # i18n: "hidden" is a keyword
895 894 getargs(x, 0, 0, _("hidden takes no arguments"))
896 895 hiddenrevs = repoview.filterrevs(repo, 'visible')
897 896 return [r for r in subset if r in hiddenrevs]
898 897
899 898 def keyword(repo, subset, x):
900 899 """``keyword(string)``
901 900 Search commit message, user name, and names of changed files for
902 901 string. The match is case-insensitive.
903 902 """
904 903 # i18n: "keyword" is a keyword
905 904 kw = encoding.lower(getstring(x, _("keyword requires a string")))
906 905 l = []
907 906 for r in subset:
908 907 c = repo[r]
909 908 t = " ".join(c.files() + [c.user(), c.description()])
910 909 if kw in encoding.lower(t):
911 910 l.append(r)
912 911 return l
913 912
914 913 def limit(repo, subset, x):
915 914 """``limit(set, [n])``
916 915 First n members of set, defaulting to 1.
917 916 """
918 917 # i18n: "limit" is a keyword
919 918 l = getargs(x, 1, 2, _("limit requires one or two arguments"))
920 919 try:
921 920 lim = 1
922 921 if len(l) == 2:
923 922 # i18n: "limit" is a keyword
924 923 lim = int(getstring(l[1], _("limit requires a number")))
925 924 except (TypeError, ValueError):
926 925 # i18n: "limit" is a keyword
927 926 raise error.ParseError(_("limit expects a number"))
928 927 ss = set(subset)
929 928 os = getset(repo, list(repo), l[0])[:lim]
930 929 return [r for r in os if r in ss]
931 930
932 931 def last(repo, subset, x):
933 932 """``last(set, [n])``
934 933 Last n members of set, defaulting to 1.
935 934 """
936 935 # i18n: "last" is a keyword
937 936 l = getargs(x, 1, 2, _("last requires one or two arguments"))
938 937 try:
939 938 lim = 1
940 939 if len(l) == 2:
941 940 # i18n: "last" is a keyword
942 941 lim = int(getstring(l[1], _("last requires a number")))
943 942 except (TypeError, ValueError):
944 943 # i18n: "last" is a keyword
945 944 raise error.ParseError(_("last expects a number"))
946 945 ss = set(subset)
947 946 os = getset(repo, list(repo), l[0])[-lim:]
948 947 return [r for r in os if r in ss]
949 948
950 949 def maxrev(repo, subset, x):
951 950 """``max(set)``
952 951 Changeset with highest revision number in set.
953 952 """
954 953 os = getset(repo, list(repo), x)
955 954 if os:
956 955 m = max(os)
957 956 if m in subset:
958 957 return [m]
959 958 return []
960 959
961 960 def merge(repo, subset, x):
962 961 """``merge()``
963 962 Changeset is a merge changeset.
964 963 """
965 964 # i18n: "merge" is a keyword
966 965 getargs(x, 0, 0, _("merge takes no arguments"))
967 966 cl = repo.changelog
968 967 return [r for r in subset if cl.parentrevs(r)[1] != -1]
969 968
970 969 def branchpoint(repo, subset, x):
971 970 """``branchpoint()``
972 971 Changesets with more than one child.
973 972 """
974 973 # i18n: "branchpoint" is a keyword
975 974 getargs(x, 0, 0, _("branchpoint takes no arguments"))
976 975 cl = repo.changelog
977 976 if not subset:
978 977 return []
979 978 baserev = min(subset)
980 979 parentscount = [0]*(len(repo) - baserev)
981 980 for r in cl.revs(start=baserev + 1):
982 981 for p in cl.parentrevs(r):
983 982 if p >= baserev:
984 983 parentscount[p - baserev] += 1
985 984 return [r for r in subset if (parentscount[r - baserev] > 1)]
986 985
987 986 def minrev(repo, subset, x):
988 987 """``min(set)``
989 988 Changeset with lowest revision number in set.
990 989 """
991 990 os = getset(repo, list(repo), x)
992 991 if os:
993 992 m = min(os)
994 993 if m in subset:
995 994 return [m]
996 995 return []
997 996
998 997 def modifies(repo, subset, x):
999 998 """``modifies(pattern)``
1000 999 Changesets modifying files matched by pattern.
1001 1000 """
1002 1001 # i18n: "modifies" is a keyword
1003 1002 pat = getstring(x, _("modifies requires a pattern"))
1004 1003 return checkstatus(repo, subset, pat, 0)
1005 1004
1006 1005 def node_(repo, subset, x):
1007 1006 """``id(string)``
1008 1007 Revision non-ambiguously specified by the given hex string prefix.
1009 1008 """
1010 1009 # i18n: "id" is a keyword
1011 1010 l = getargs(x, 1, 1, _("id requires one argument"))
1012 1011 # i18n: "id" is a keyword
1013 1012 n = getstring(l[0], _("id requires a string"))
1014 1013 if len(n) == 40:
1015 1014 rn = repo[n].rev()
1016 1015 else:
1017 1016 rn = None
1018 1017 pm = repo.changelog._partialmatch(n)
1019 1018 if pm is not None:
1020 1019 rn = repo.changelog.rev(pm)
1021 1020
1022 1021 return [r for r in subset if r == rn]
1023 1022
1024 1023 def obsolete(repo, subset, x):
1025 1024 """``obsolete()``
1026 1025 Mutable changeset with a newer version."""
1027 1026 # i18n: "obsolete" is a keyword
1028 1027 getargs(x, 0, 0, _("obsolete takes no arguments"))
1029 1028 obsoletes = obsmod.getrevs(repo, 'obsolete')
1030 1029 return [r for r in subset if r in obsoletes]
1031 1030
1032 1031 def origin(repo, subset, x):
1033 1032 """``origin([set])``
1034 1033 Changesets that were specified as a source for the grafts, transplants or
1035 1034 rebases that created the given revisions. Omitting the optional set is the
1036 1035 same as passing all(). If a changeset created by these operations is itself
1037 1036 specified as a source for one of these operations, only the source changeset
1038 1037 for the first operation is selected.
1039 1038 """
1040 1039 if x is not None:
1041 1040 args = set(getset(repo, list(repo), x))
1042 1041 else:
1043 1042 args = set(getall(repo, list(repo), x))
1044 1043
1045 1044 def _firstsrc(rev):
1046 1045 src = _getrevsource(repo, rev)
1047 1046 if src is None:
1048 1047 return None
1049 1048
1050 1049 while True:
1051 1050 prev = _getrevsource(repo, src)
1052 1051
1053 1052 if prev is None:
1054 1053 return src
1055 1054 src = prev
1056 1055
1057 1056 o = set([_firstsrc(r) for r in args])
1058 1057 return [r for r in subset if r in o]
1059 1058
1060 1059 def outgoing(repo, subset, x):
1061 1060 """``outgoing([path])``
1062 1061 Changesets not found in the specified destination repository, or the
1063 1062 default push location.
1064 1063 """
1065 1064 import hg # avoid start-up nasties
1066 1065 # i18n: "outgoing" is a keyword
1067 1066 l = getargs(x, 0, 1, _("outgoing takes one or no arguments"))
1068 1067 # i18n: "outgoing" is a keyword
1069 1068 dest = l and getstring(l[0], _("outgoing requires a repository path")) or ''
1070 1069 dest = repo.ui.expandpath(dest or 'default-push', dest or 'default')
1071 1070 dest, branches = hg.parseurl(dest)
1072 1071 revs, checkout = hg.addbranchrevs(repo, repo, branches, [])
1073 1072 if revs:
1074 1073 revs = [repo.lookup(rev) for rev in revs]
1075 1074 other = hg.peer(repo, {}, dest)
1076 1075 repo.ui.pushbuffer()
1077 1076 outgoing = discovery.findcommonoutgoing(repo, other, onlyheads=revs)
1078 1077 repo.ui.popbuffer()
1079 1078 cl = repo.changelog
1080 1079 o = set([cl.rev(r) for r in outgoing.missing])
1081 1080 return [r for r in subset if r in o]
1082 1081
1083 1082 def p1(repo, subset, x):
1084 1083 """``p1([set])``
1085 1084 First parent of changesets in set, or the working directory.
1086 1085 """
1087 1086 if x is None:
1088 1087 p = repo[x].p1().rev()
1089 1088 return [r for r in subset if r == p]
1090 1089
1091 1090 ps = set()
1092 1091 cl = repo.changelog
1093 1092 for r in getset(repo, list(repo), x):
1094 1093 ps.add(cl.parentrevs(r)[0])
1095 1094 return [r for r in subset if r in ps]
1096 1095
1097 1096 def p2(repo, subset, x):
1098 1097 """``p2([set])``
1099 1098 Second parent of changesets in set, or the working directory.
1100 1099 """
1101 1100 if x is None:
1102 1101 ps = repo[x].parents()
1103 1102 try:
1104 1103 p = ps[1].rev()
1105 1104 return [r for r in subset if r == p]
1106 1105 except IndexError:
1107 1106 return []
1108 1107
1109 1108 ps = set()
1110 1109 cl = repo.changelog
1111 1110 for r in getset(repo, list(repo), x):
1112 1111 ps.add(cl.parentrevs(r)[1])
1113 1112 return [r for r in subset if r in ps]
1114 1113
1115 1114 def parents(repo, subset, x):
1116 1115 """``parents([set])``
1117 1116 The set of all parents for all changesets in set, or the working directory.
1118 1117 """
1119 1118 if x is None:
1120 1119 ps = tuple(p.rev() for p in repo[x].parents())
1121 1120 return [r for r in subset if r in ps]
1122 1121
1123 1122 ps = set()
1124 1123 cl = repo.changelog
1125 1124 for r in getset(repo, list(repo), x):
1126 1125 ps.update(cl.parentrevs(r))
1127 1126 return [r for r in subset if r in ps]
1128 1127
1129 1128 def parentspec(repo, subset, x, n):
1130 1129 """``set^0``
1131 1130 The set.
1132 1131 ``set^1`` (or ``set^``), ``set^2``
1133 1132 First or second parent, respectively, of all changesets in set.
1134 1133 """
1135 1134 try:
1136 1135 n = int(n[1])
1137 1136 if n not in (0, 1, 2):
1138 1137 raise ValueError
1139 1138 except (TypeError, ValueError):
1140 1139 raise error.ParseError(_("^ expects a number 0, 1, or 2"))
1141 1140 ps = set()
1142 1141 cl = repo.changelog
1143 1142 for r in getset(repo, subset, x):
1144 1143 if n == 0:
1145 1144 ps.add(r)
1146 1145 elif n == 1:
1147 1146 ps.add(cl.parentrevs(r)[0])
1148 1147 elif n == 2:
1149 1148 parents = cl.parentrevs(r)
1150 1149 if len(parents) > 1:
1151 1150 ps.add(parents[1])
1152 1151 return [r for r in subset if r in ps]
1153 1152
1154 1153 def present(repo, subset, x):
1155 1154 """``present(set)``
1156 1155 An empty set, if any revision in set isn't found; otherwise,
1157 1156 all revisions in set.
1158 1157
1159 1158 If any of specified revisions is not present in the local repository,
1160 1159 the query is normally aborted. But this predicate allows the query
1161 1160 to continue even in such cases.
1162 1161 """
1163 1162 try:
1164 1163 return getset(repo, subset, x)
1165 1164 except error.RepoLookupError:
1166 1165 return []
1167 1166
1168 1167 def public(repo, subset, x):
1169 1168 """``public()``
1170 1169 Changeset in public phase."""
1171 1170 # i18n: "public" is a keyword
1172 1171 getargs(x, 0, 0, _("public takes no arguments"))
1173 1172 pc = repo._phasecache
1174 1173 return [r for r in subset if pc.phase(repo, r) == phases.public]
1175 1174
1176 1175 def remote(repo, subset, x):
1177 1176 """``remote([id [,path]])``
1178 1177 Local revision that corresponds to the given identifier in a
1179 1178 remote repository, if present. Here, the '.' identifier is a
1180 1179 synonym for the current local branch.
1181 1180 """
1182 1181
1183 1182 import hg # avoid start-up nasties
1184 1183 # i18n: "remote" is a keyword
1185 1184 l = getargs(x, 0, 2, _("remote takes one, two or no arguments"))
1186 1185
1187 1186 q = '.'
1188 1187 if len(l) > 0:
1189 1188 # i18n: "remote" is a keyword
1190 1189 q = getstring(l[0], _("remote requires a string id"))
1191 1190 if q == '.':
1192 1191 q = repo['.'].branch()
1193 1192
1194 1193 dest = ''
1195 1194 if len(l) > 1:
1196 1195 # i18n: "remote" is a keyword
1197 1196 dest = getstring(l[1], _("remote requires a repository path"))
1198 1197 dest = repo.ui.expandpath(dest or 'default')
1199 1198 dest, branches = hg.parseurl(dest)
1200 1199 revs, checkout = hg.addbranchrevs(repo, repo, branches, [])
1201 1200 if revs:
1202 1201 revs = [repo.lookup(rev) for rev in revs]
1203 1202 other = hg.peer(repo, {}, dest)
1204 1203 n = other.lookup(q)
1205 1204 if n in repo:
1206 1205 r = repo[n].rev()
1207 1206 if r in subset:
1208 1207 return [r]
1209 1208 return []
1210 1209
1211 1210 def removes(repo, subset, x):
1212 1211 """``removes(pattern)``
1213 1212 Changesets which remove files matching pattern.
1214 1213 """
1215 1214 # i18n: "removes" is a keyword
1216 1215 pat = getstring(x, _("removes requires a pattern"))
1217 1216 return checkstatus(repo, subset, pat, 2)
1218 1217
1219 1218 def rev(repo, subset, x):
1220 1219 """``rev(number)``
1221 1220 Revision with the given numeric identifier.
1222 1221 """
1223 1222 # i18n: "rev" is a keyword
1224 1223 l = getargs(x, 1, 1, _("rev requires one argument"))
1225 1224 try:
1226 1225 # i18n: "rev" is a keyword
1227 1226 l = int(getstring(l[0], _("rev requires a number")))
1228 1227 except (TypeError, ValueError):
1229 1228 # i18n: "rev" is a keyword
1230 1229 raise error.ParseError(_("rev expects a number"))
1231 1230 return [r for r in subset if r == l]
1232 1231
1233 1232 def matching(repo, subset, x):
1234 1233 """``matching(revision [, field])``
1235 1234 Changesets in which a given set of fields match the set of fields in the
1236 1235 selected revision or set.
1237 1236
1238 1237 To match more than one field pass the list of fields to match separated
1239 1238 by spaces (e.g. ``author description``).
1240 1239
1241 1240 Valid fields are most regular revision fields and some special fields.
1242 1241
1243 1242 Regular revision fields are ``description``, ``author``, ``branch``,
1244 1243 ``date``, ``files``, ``phase``, ``parents``, ``substate``, ``user``
1245 1244 and ``diff``.
1246 1245 Note that ``author`` and ``user`` are synonyms. ``diff`` refers to the
1247 1246 contents of the revision. Two revisions matching their ``diff`` will
1248 1247 also match their ``files``.
1249 1248
1250 1249 Special fields are ``summary`` and ``metadata``:
1251 1250 ``summary`` matches the first line of the description.
1252 1251 ``metadata`` is equivalent to matching ``description user date``
1253 1252 (i.e. it matches the main metadata fields).
1254 1253
1255 1254 ``metadata`` is the default field which is used when no fields are
1256 1255 specified. You can match more than one field at a time.
1257 1256 """
1258 1257 # i18n: "matching" is a keyword
1259 1258 l = getargs(x, 1, 2, _("matching takes 1 or 2 arguments"))
1260 1259
1261 1260 revs = getset(repo, repo.changelog, l[0])
1262 1261
1263 1262 fieldlist = ['metadata']
1264 1263 if len(l) > 1:
1265 1264 fieldlist = getstring(l[1],
1266 1265 # i18n: "matching" is a keyword
1267 1266 _("matching requires a string "
1268 1267 "as its second argument")).split()
1269 1268
1270 1269 # Make sure that there are no repeated fields,
1271 1270 # expand the 'special' 'metadata' field type
1272 1271 # and check the 'files' whenever we check the 'diff'
1273 1272 fields = []
1274 1273 for field in fieldlist:
1275 1274 if field == 'metadata':
1276 1275 fields += ['user', 'description', 'date']
1277 1276 elif field == 'diff':
1278 1277 # a revision matching the diff must also match the files
1279 1278 # since matching the diff is very costly, make sure to
1280 1279 # also match the files first
1281 1280 fields += ['files', 'diff']
1282 1281 else:
1283 1282 if field == 'author':
1284 1283 field = 'user'
1285 1284 fields.append(field)
1286 1285 fields = set(fields)
1287 1286 if 'summary' in fields and 'description' in fields:
1288 1287 # If a revision matches its description it also matches its summary
1289 1288 fields.discard('summary')
1290 1289
1291 1290 # We may want to match more than one field
1292 1291 # Not all fields take the same amount of time to be matched
1293 1292 # Sort the selected fields in order of increasing matching cost
1294 1293 fieldorder = ['phase', 'parents', 'user', 'date', 'branch', 'summary',
1295 1294 'files', 'description', 'substate', 'diff']
1296 1295 def fieldkeyfunc(f):
1297 1296 try:
1298 1297 return fieldorder.index(f)
1299 1298 except ValueError:
1300 1299 # assume an unknown field is very costly
1301 1300 return len(fieldorder)
1302 1301 fields = list(fields)
1303 1302 fields.sort(key=fieldkeyfunc)
1304 1303
1305 1304 # Each field will be matched with its own "getfield" function
1306 1305 # which will be added to the getfieldfuncs array of functions
1307 1306 getfieldfuncs = []
1308 1307 _funcs = {
1309 1308 'user': lambda r: repo[r].user(),
1310 1309 'branch': lambda r: repo[r].branch(),
1311 1310 'date': lambda r: repo[r].date(),
1312 1311 'description': lambda r: repo[r].description(),
1313 1312 'files': lambda r: repo[r].files(),
1314 1313 'parents': lambda r: repo[r].parents(),
1315 1314 'phase': lambda r: repo[r].phase(),
1316 1315 'substate': lambda r: repo[r].substate,
1317 1316 'summary': lambda r: repo[r].description().splitlines()[0],
1318 1317 'diff': lambda r: list(repo[r].diff(git=True),)
1319 1318 }
1320 1319 for info in fields:
1321 1320 getfield = _funcs.get(info, None)
1322 1321 if getfield is None:
1323 1322 raise error.ParseError(
1324 1323 # i18n: "matching" is a keyword
1325 1324 _("unexpected field name passed to matching: %s") % info)
1326 1325 getfieldfuncs.append(getfield)
1327 1326 # convert the getfield array of functions into a "getinfo" function
1328 1327 # which returns an array of field values (or a single value if there
1329 1328 # is only one field to match)
1330 1329 getinfo = lambda r: [f(r) for f in getfieldfuncs]
1331 1330
1332 1331 matches = set()
1333 1332 for rev in revs:
1334 1333 target = getinfo(rev)
1335 1334 for r in subset:
1336 1335 match = True
1337 1336 for n, f in enumerate(getfieldfuncs):
1338 1337 if target[n] != f(r):
1339 1338 match = False
1340 1339 break
1341 1340 if match:
1342 1341 matches.add(r)
1343 1342 return [r for r in subset if r in matches]
1344 1343
1345 1344 def reverse(repo, subset, x):
1346 1345 """``reverse(set)``
1347 1346 Reverse order of set.
1348 1347 """
1349 1348 l = getset(repo, subset, x)
1350 1349 if not isinstance(l, list):
1351 1350 l = list(l)
1352 1351 l.reverse()
1353 1352 return l
1354 1353
1355 1354 def roots(repo, subset, x):
1356 1355 """``roots(set)``
1357 1356 Changesets in set with no parent changeset in set.
1358 1357 """
1359 1358 s = set(getset(repo, repo.changelog, x))
1360 1359 subset = [r for r in subset if r in s]
1361 1360 cs = _children(repo, subset, s)
1362 1361 return [r for r in subset if r not in cs]
1363 1362
1364 1363 def secret(repo, subset, x):
1365 1364 """``secret()``
1366 1365 Changeset in secret phase."""
1367 1366 # i18n: "secret" is a keyword
1368 1367 getargs(x, 0, 0, _("secret takes no arguments"))
1369 1368 pc = repo._phasecache
1370 1369 return [r for r in subset if pc.phase(repo, r) == phases.secret]
1371 1370
1372 1371 def sort(repo, subset, x):
1373 1372 """``sort(set[, [-]key...])``
1374 1373 Sort set by keys. The default sort order is ascending, specify a key
1375 1374 as ``-key`` to sort in descending order.
1376 1375
1377 1376 The keys can be:
1378 1377
1379 1378 - ``rev`` for the revision number,
1380 1379 - ``branch`` for the branch name,
1381 1380 - ``desc`` for the commit message (description),
1382 1381 - ``user`` for user name (``author`` can be used as an alias),
1383 1382 - ``date`` for the commit date
1384 1383 """
1385 1384 # i18n: "sort" is a keyword
1386 1385 l = getargs(x, 1, 2, _("sort requires one or two arguments"))
1387 1386 keys = "rev"
1388 1387 if len(l) == 2:
1389 1388 # i18n: "sort" is a keyword
1390 1389 keys = getstring(l[1], _("sort spec must be a string"))
1391 1390
1392 1391 s = l[0]
1393 1392 keys = keys.split()
1394 1393 l = []
1395 1394 def invert(s):
1396 1395 return "".join(chr(255 - ord(c)) for c in s)
1397 1396 for r in getset(repo, subset, s):
1398 1397 c = repo[r]
1399 1398 e = []
1400 1399 for k in keys:
1401 1400 if k == 'rev':
1402 1401 e.append(r)
1403 1402 elif k == '-rev':
1404 1403 e.append(-r)
1405 1404 elif k == 'branch':
1406 1405 e.append(c.branch())
1407 1406 elif k == '-branch':
1408 1407 e.append(invert(c.branch()))
1409 1408 elif k == 'desc':
1410 1409 e.append(c.description())
1411 1410 elif k == '-desc':
1412 1411 e.append(invert(c.description()))
1413 1412 elif k in 'user author':
1414 1413 e.append(c.user())
1415 1414 elif k in '-user -author':
1416 1415 e.append(invert(c.user()))
1417 1416 elif k == 'date':
1418 1417 e.append(c.date()[0])
1419 1418 elif k == '-date':
1420 1419 e.append(-c.date()[0])
1421 1420 else:
1422 1421 raise error.ParseError(_("unknown sort key %r") % k)
1423 1422 e.append(r)
1424 1423 l.append(e)
1425 1424 l.sort()
1426 1425 return [e[-1] for e in l]
1427 1426
1428 1427 def _stringmatcher(pattern):
1429 1428 """
1430 1429 accepts a string, possibly starting with 're:' or 'literal:' prefix.
1431 1430 returns the matcher name, pattern, and matcher function.
1432 1431 missing or unknown prefixes are treated as literal matches.
1433 1432
1434 1433 helper for tests:
1435 1434 >>> def test(pattern, *tests):
1436 1435 ... kind, pattern, matcher = _stringmatcher(pattern)
1437 1436 ... return (kind, pattern, [bool(matcher(t)) for t in tests])
1438 1437
1439 1438 exact matching (no prefix):
1440 1439 >>> test('abcdefg', 'abc', 'def', 'abcdefg')
1441 1440 ('literal', 'abcdefg', [False, False, True])
1442 1441
1443 1442 regex matching ('re:' prefix)
1444 1443 >>> test('re:a.+b', 'nomatch', 'fooadef', 'fooadefbar')
1445 1444 ('re', 'a.+b', [False, False, True])
1446 1445
1447 1446 force exact matches ('literal:' prefix)
1448 1447 >>> test('literal:re:foobar', 'foobar', 're:foobar')
1449 1448 ('literal', 're:foobar', [False, True])
1450 1449
1451 1450 unknown prefixes are ignored and treated as literals
1452 1451 >>> test('foo:bar', 'foo', 'bar', 'foo:bar')
1453 1452 ('literal', 'foo:bar', [False, False, True])
1454 1453 """
1455 1454 if pattern.startswith('re:'):
1456 1455 pattern = pattern[3:]
1457 1456 try:
1458 1457 regex = re.compile(pattern)
1459 1458 except re.error, e:
1460 1459 raise error.ParseError(_('invalid regular expression: %s')
1461 1460 % e)
1462 1461 return 're', pattern, regex.search
1463 1462 elif pattern.startswith('literal:'):
1464 1463 pattern = pattern[8:]
1465 1464 return 'literal', pattern, pattern.__eq__
1466 1465
1467 1466 def _substringmatcher(pattern):
1468 1467 kind, pattern, matcher = _stringmatcher(pattern)
1469 1468 if kind == 'literal':
1470 1469 matcher = lambda s: pattern in s
1471 1470 return kind, pattern, matcher
1472 1471
1473 1472 def tag(repo, subset, x):
1474 1473 """``tag([name])``
1475 1474 The specified tag by name, or all tagged revisions if no name is given.
1476 1475 """
1477 1476 # i18n: "tag" is a keyword
1478 1477 args = getargs(x, 0, 1, _("tag takes one or no arguments"))
1479 1478 cl = repo.changelog
1480 1479 if args:
1481 1480 pattern = getstring(args[0],
1482 1481 # i18n: "tag" is a keyword
1483 1482 _('the argument to tag must be a string'))
1484 1483 kind, pattern, matcher = _stringmatcher(pattern)
1485 1484 if kind == 'literal':
1486 1485 # avoid resolving all tags
1487 1486 tn = repo._tagscache.tags.get(pattern, None)
1488 1487 if tn is None:
1489 1488 raise util.Abort(_("tag '%s' does not exist") % pattern)
1490 1489 s = set([repo[tn].rev()])
1491 1490 else:
1492 1491 s = set([cl.rev(n) for t, n in repo.tagslist() if matcher(t)])
1493 1492 if not s:
1494 1493 raise util.Abort(_("no tags exist that match '%s'") % pattern)
1495 1494 else:
1496 1495 s = set([cl.rev(n) for t, n in repo.tagslist() if t != 'tip'])
1497 1496 return [r for r in subset if r in s]
1498 1497
1499 1498 def tagged(repo, subset, x):
1500 1499 return tag(repo, subset, x)
1501 1500
1502 1501 def unstable(repo, subset, x):
1503 1502 """``unstable()``
1504 1503 Non-obsolete changesets with obsolete ancestors.
1505 1504 """
1506 1505 # i18n: "unstable" is a keyword
1507 1506 getargs(x, 0, 0, _("unstable takes no arguments"))
1508 1507 unstables = obsmod.getrevs(repo, 'unstable')
1509 1508 return [r for r in subset if r in unstables]
1510 1509
1511 1510
1512 1511 def user(repo, subset, x):
1513 1512 """``user(string)``
1514 1513 User name contains string. The match is case-insensitive.
1515 1514
1516 1515 If `string` starts with `re:`, the remainder of the string is treated as
1517 1516 a regular expression. To match a user that actually contains `re:`, use
1518 1517 the prefix `literal:`.
1519 1518 """
1520 1519 return author(repo, subset, x)
1521 1520
1522 1521 # for internal use
1523 1522 def _list(repo, subset, x):
1524 1523 s = getstring(x, "internal error")
1525 1524 if not s:
1526 1525 return []
1527 1526 if not isinstance(subset, set):
1528 1527 subset = set(subset)
1529 1528 ls = [repo[r].rev() for r in s.split('\0')]
1530 1529 return [r for r in ls if r in subset]
1531 1530
1532 1531 symbols = {
1533 1532 "adds": adds,
1534 1533 "all": getall,
1535 1534 "ancestor": ancestor,
1536 1535 "ancestors": ancestors,
1537 1536 "_firstancestors": _firstancestors,
1538 1537 "author": author,
1539 1538 "bisect": bisect,
1540 1539 "bisected": bisected,
1541 1540 "bookmark": bookmark,
1542 1541 "branch": branch,
1543 1542 "branchpoint": branchpoint,
1544 1543 "bumped": bumped,
1545 1544 "bundle": bundle,
1546 1545 "children": children,
1547 1546 "closed": closed,
1548 1547 "contains": contains,
1549 1548 "converted": converted,
1550 1549 "date": date,
1551 1550 "desc": desc,
1552 1551 "descendants": descendants,
1553 1552 "_firstdescendants": _firstdescendants,
1554 1553 "destination": destination,
1555 1554 "divergent": divergent,
1556 1555 "draft": draft,
1557 1556 "extinct": extinct,
1558 1557 "extra": extra,
1559 1558 "file": hasfile,
1560 1559 "filelog": filelog,
1561 1560 "first": first,
1562 1561 "follow": follow,
1563 1562 "_followfirst": _followfirst,
1564 1563 "grep": grep,
1565 1564 "head": head,
1566 1565 "heads": heads,
1567 1566 "hidden": hidden,
1568 1567 "id": node_,
1569 1568 "keyword": keyword,
1570 1569 "last": last,
1571 1570 "limit": limit,
1572 1571 "_matchfiles": _matchfiles,
1573 1572 "max": maxrev,
1574 1573 "merge": merge,
1575 1574 "min": minrev,
1576 1575 "modifies": modifies,
1577 1576 "obsolete": obsolete,
1578 1577 "origin": origin,
1579 1578 "outgoing": outgoing,
1580 1579 "p1": p1,
1581 1580 "p2": p2,
1582 1581 "parents": parents,
1583 1582 "present": present,
1584 1583 "public": public,
1585 1584 "remote": remote,
1586 1585 "removes": removes,
1587 1586 "rev": rev,
1588 1587 "reverse": reverse,
1589 1588 "roots": roots,
1590 1589 "sort": sort,
1591 1590 "secret": secret,
1592 1591 "matching": matching,
1593 1592 "tag": tag,
1594 1593 "tagged": tagged,
1595 1594 "user": user,
1596 1595 "unstable": unstable,
1597 1596 "_list": _list,
1598 1597 }
1599 1598
1600 1599 methods = {
1601 1600 "range": rangeset,
1602 1601 "dagrange": dagrange,
1603 1602 "string": stringset,
1604 1603 "symbol": symbolset,
1605 1604 "and": andset,
1606 1605 "or": orset,
1607 1606 "not": notset,
1608 1607 "list": listset,
1609 1608 "func": func,
1610 1609 "ancestor": ancestorspec,
1611 1610 "parent": parentspec,
1612 1611 "parentpost": p1,
1613 1612 }
1614 1613
1615 1614 def optimize(x, small):
1616 1615 if x is None:
1617 1616 return 0, x
1618 1617
1619 1618 smallbonus = 1
1620 1619 if small:
1621 1620 smallbonus = .5
1622 1621
1623 1622 op = x[0]
1624 1623 if op == 'minus':
1625 1624 return optimize(('and', x[1], ('not', x[2])), small)
1626 1625 elif op == 'dagrangepre':
1627 1626 return optimize(('func', ('symbol', 'ancestors'), x[1]), small)
1628 1627 elif op == 'dagrangepost':
1629 1628 return optimize(('func', ('symbol', 'descendants'), x[1]), small)
1630 1629 elif op == 'rangepre':
1631 1630 return optimize(('range', ('string', '0'), x[1]), small)
1632 1631 elif op == 'rangepost':
1633 1632 return optimize(('range', x[1], ('string', 'tip')), small)
1634 1633 elif op == 'negate':
1635 1634 return optimize(('string',
1636 1635 '-' + getstring(x[1], _("can't negate that"))), small)
1637 1636 elif op in 'string symbol negate':
1638 1637 return smallbonus, x # single revisions are small
1639 1638 elif op == 'and':
1640 1639 wa, ta = optimize(x[1], True)
1641 1640 wb, tb = optimize(x[2], True)
1642 1641 w = min(wa, wb)
1643 1642 if wa > wb:
1644 1643 return w, (op, tb, ta)
1645 1644 return w, (op, ta, tb)
1646 1645 elif op == 'or':
1647 1646 wa, ta = optimize(x[1], False)
1648 1647 wb, tb = optimize(x[2], False)
1649 1648 if wb < wa:
1650 1649 wb, wa = wa, wb
1651 1650 return max(wa, wb), (op, ta, tb)
1652 1651 elif op == 'not':
1653 1652 o = optimize(x[1], not small)
1654 1653 return o[0], (op, o[1])
1655 1654 elif op == 'parentpost':
1656 1655 o = optimize(x[1], small)
1657 1656 return o[0], (op, o[1])
1658 1657 elif op == 'group':
1659 1658 return optimize(x[1], small)
1660 1659 elif op in 'dagrange range list parent ancestorspec':
1661 1660 if op == 'parent':
1662 1661 # x^:y means (x^) : y, not x ^ (:y)
1663 1662 post = ('parentpost', x[1])
1664 1663 if x[2][0] == 'dagrangepre':
1665 1664 return optimize(('dagrange', post, x[2][1]), small)
1666 1665 elif x[2][0] == 'rangepre':
1667 1666 return optimize(('range', post, x[2][1]), small)
1668 1667
1669 1668 wa, ta = optimize(x[1], small)
1670 1669 wb, tb = optimize(x[2], small)
1671 1670 return wa + wb, (op, ta, tb)
1672 1671 elif op == 'func':
1673 1672 f = getstring(x[1], _("not a symbol"))
1674 1673 wa, ta = optimize(x[2], small)
1675 1674 if f in ("author branch closed date desc file grep keyword "
1676 1675 "outgoing user"):
1677 1676 w = 10 # slow
1678 1677 elif f in "modifies adds removes":
1679 1678 w = 30 # slower
1680 1679 elif f == "contains":
1681 1680 w = 100 # very slow
1682 1681 elif f == "ancestor":
1683 1682 w = 1 * smallbonus
1684 1683 elif f in "reverse limit first":
1685 1684 w = 0
1686 1685 elif f in "sort":
1687 1686 w = 10 # assume most sorts look at changelog
1688 1687 else:
1689 1688 w = 1
1690 1689 return w + wa, (op, x[1], ta)
1691 1690 return 1, x
1692 1691
1693 1692 _aliasarg = ('func', ('symbol', '_aliasarg'))
1694 1693 def _getaliasarg(tree):
1695 1694 """If tree matches ('func', ('symbol', '_aliasarg'), ('string', X))
1696 1695 return X, None otherwise.
1697 1696 """
1698 1697 if (len(tree) == 3 and tree[:2] == _aliasarg
1699 1698 and tree[2][0] == 'string'):
1700 1699 return tree[2][1]
1701 1700 return None
1702 1701
1703 1702 def _checkaliasarg(tree, known=None):
1704 1703 """Check tree contains no _aliasarg construct or only ones which
1705 1704 value is in known. Used to avoid alias placeholders injection.
1706 1705 """
1707 1706 if isinstance(tree, tuple):
1708 1707 arg = _getaliasarg(tree)
1709 1708 if arg is not None and (not known or arg not in known):
1710 1709 raise error.ParseError(_("not a function: %s") % '_aliasarg')
1711 1710 for t in tree:
1712 1711 _checkaliasarg(t, known)
1713 1712
1714 1713 class revsetalias(object):
1715 1714 funcre = re.compile('^([^(]+)\(([^)]+)\)$')
1716 1715 args = None
1717 1716
1718 1717 def __init__(self, name, value):
1719 1718 '''Aliases like:
1720 1719
1721 1720 h = heads(default)
1722 1721 b($1) = ancestors($1) - ancestors(default)
1723 1722 '''
1724 1723 m = self.funcre.search(name)
1725 1724 if m:
1726 1725 self.name = m.group(1)
1727 1726 self.tree = ('func', ('symbol', m.group(1)))
1728 1727 self.args = [x.strip() for x in m.group(2).split(',')]
1729 1728 for arg in self.args:
1730 1729 # _aliasarg() is an unknown symbol only used separate
1731 1730 # alias argument placeholders from regular strings.
1732 1731 value = value.replace(arg, '_aliasarg(%r)' % (arg,))
1733 1732 else:
1734 1733 self.name = name
1735 1734 self.tree = ('symbol', name)
1736 1735
1737 1736 self.replacement, pos = parse(value)
1738 1737 if pos != len(value):
1739 1738 raise error.ParseError(_('invalid token'), pos)
1740 1739 # Check for placeholder injection
1741 1740 _checkaliasarg(self.replacement, self.args)
1742 1741
1743 1742 def _getalias(aliases, tree):
1744 1743 """If tree looks like an unexpanded alias, return it. Return None
1745 1744 otherwise.
1746 1745 """
1747 1746 if isinstance(tree, tuple) and tree:
1748 1747 if tree[0] == 'symbol' and len(tree) == 2:
1749 1748 name = tree[1]
1750 1749 alias = aliases.get(name)
1751 1750 if alias and alias.args is None and alias.tree == tree:
1752 1751 return alias
1753 1752 if tree[0] == 'func' and len(tree) > 1:
1754 1753 if tree[1][0] == 'symbol' and len(tree[1]) == 2:
1755 1754 name = tree[1][1]
1756 1755 alias = aliases.get(name)
1757 1756 if alias and alias.args is not None and alias.tree == tree[:2]:
1758 1757 return alias
1759 1758 return None
1760 1759
1761 1760 def _expandargs(tree, args):
1762 1761 """Replace _aliasarg instances with the substitution value of the
1763 1762 same name in args, recursively.
1764 1763 """
1765 1764 if not tree or not isinstance(tree, tuple):
1766 1765 return tree
1767 1766 arg = _getaliasarg(tree)
1768 1767 if arg is not None:
1769 1768 return args[arg]
1770 1769 return tuple(_expandargs(t, args) for t in tree)
1771 1770
1772 1771 def _expandaliases(aliases, tree, expanding, cache):
1773 1772 """Expand aliases in tree, recursively.
1774 1773
1775 1774 'aliases' is a dictionary mapping user defined aliases to
1776 1775 revsetalias objects.
1777 1776 """
1778 1777 if not isinstance(tree, tuple):
1779 1778 # Do not expand raw strings
1780 1779 return tree
1781 1780 alias = _getalias(aliases, tree)
1782 1781 if alias is not None:
1783 1782 if alias in expanding:
1784 1783 raise error.ParseError(_('infinite expansion of revset alias "%s" '
1785 1784 'detected') % alias.name)
1786 1785 expanding.append(alias)
1787 1786 if alias.name not in cache:
1788 1787 cache[alias.name] = _expandaliases(aliases, alias.replacement,
1789 1788 expanding, cache)
1790 1789 result = cache[alias.name]
1791 1790 expanding.pop()
1792 1791 if alias.args is not None:
1793 1792 l = getlist(tree[2])
1794 1793 if len(l) != len(alias.args):
1795 1794 raise error.ParseError(
1796 1795 _('invalid number of arguments: %s') % len(l))
1797 1796 l = [_expandaliases(aliases, a, [], cache) for a in l]
1798 1797 result = _expandargs(result, dict(zip(alias.args, l)))
1799 1798 else:
1800 1799 result = tuple(_expandaliases(aliases, t, expanding, cache)
1801 1800 for t in tree)
1802 1801 return result
1803 1802
1804 1803 def findaliases(ui, tree):
1805 1804 _checkaliasarg(tree)
1806 1805 aliases = {}
1807 1806 for k, v in ui.configitems('revsetalias'):
1808 1807 alias = revsetalias(k, v)
1809 1808 aliases[alias.name] = alias
1810 1809 return _expandaliases(aliases, tree, [], {})
1811 1810
1812 1811 parse = parser.parser(tokenize, elements).parse
1813 1812
1814 1813 def match(ui, spec):
1815 1814 if not spec:
1816 1815 raise error.ParseError(_("empty query"))
1817 1816 tree, pos = parse(spec)
1818 1817 if (pos != len(spec)):
1819 1818 raise error.ParseError(_("invalid token"), pos)
1820 1819 if ui:
1821 1820 tree = findaliases(ui, tree)
1822 1821 weight, tree = optimize(tree, True)
1823 1822 def mfunc(repo, subset):
1824 1823 return getset(repo, subset, tree)
1825 1824 return mfunc
1826 1825
1827 1826 def formatspec(expr, *args):
1828 1827 '''
1829 1828 This is a convenience function for using revsets internally, and
1830 1829 escapes arguments appropriately. Aliases are intentionally ignored
1831 1830 so that intended expression behavior isn't accidentally subverted.
1832 1831
1833 1832 Supported arguments:
1834 1833
1835 1834 %r = revset expression, parenthesized
1836 1835 %d = int(arg), no quoting
1837 1836 %s = string(arg), escaped and single-quoted
1838 1837 %b = arg.branch(), escaped and single-quoted
1839 1838 %n = hex(arg), single-quoted
1840 1839 %% = a literal '%'
1841 1840
1842 1841 Prefixing the type with 'l' specifies a parenthesized list of that type.
1843 1842
1844 1843 >>> formatspec('%r:: and %lr', '10 or 11', ("this()", "that()"))
1845 1844 '(10 or 11):: and ((this()) or (that()))'
1846 1845 >>> formatspec('%d:: and not %d::', 10, 20)
1847 1846 '10:: and not 20::'
1848 1847 >>> formatspec('%ld or %ld', [], [1])
1849 1848 "_list('') or 1"
1850 1849 >>> formatspec('keyword(%s)', 'foo\\xe9')
1851 1850 "keyword('foo\\\\xe9')"
1852 1851 >>> b = lambda: 'default'
1853 1852 >>> b.branch = b
1854 1853 >>> formatspec('branch(%b)', b)
1855 1854 "branch('default')"
1856 1855 >>> formatspec('root(%ls)', ['a', 'b', 'c', 'd'])
1857 1856 "root(_list('a\\x00b\\x00c\\x00d'))"
1858 1857 '''
1859 1858
1860 1859 def quote(s):
1861 1860 return repr(str(s))
1862 1861
1863 1862 def argtype(c, arg):
1864 1863 if c == 'd':
1865 1864 return str(int(arg))
1866 1865 elif c == 's':
1867 1866 return quote(arg)
1868 1867 elif c == 'r':
1869 1868 parse(arg) # make sure syntax errors are confined
1870 1869 return '(%s)' % arg
1871 1870 elif c == 'n':
1872 1871 return quote(node.hex(arg))
1873 1872 elif c == 'b':
1874 1873 return quote(arg.branch())
1875 1874
1876 1875 def listexp(s, t):
1877 1876 l = len(s)
1878 1877 if l == 0:
1879 1878 return "_list('')"
1880 1879 elif l == 1:
1881 1880 return argtype(t, s[0])
1882 1881 elif t == 'd':
1883 1882 return "_list('%s')" % "\0".join(str(int(a)) for a in s)
1884 1883 elif t == 's':
1885 1884 return "_list('%s')" % "\0".join(s)
1886 1885 elif t == 'n':
1887 1886 return "_list('%s')" % "\0".join(node.hex(a) for a in s)
1888 1887 elif t == 'b':
1889 1888 return "_list('%s')" % "\0".join(a.branch() for a in s)
1890 1889
1891 1890 m = l // 2
1892 1891 return '(%s or %s)' % (listexp(s[:m], t), listexp(s[m:], t))
1893 1892
1894 1893 ret = ''
1895 1894 pos = 0
1896 1895 arg = 0
1897 1896 while pos < len(expr):
1898 1897 c = expr[pos]
1899 1898 if c == '%':
1900 1899 pos += 1
1901 1900 d = expr[pos]
1902 1901 if d == '%':
1903 1902 ret += d
1904 1903 elif d in 'dsnbr':
1905 1904 ret += argtype(d, args[arg])
1906 1905 arg += 1
1907 1906 elif d == 'l':
1908 1907 # a list of some type
1909 1908 pos += 1
1910 1909 d = expr[pos]
1911 1910 ret += listexp(list(args[arg]), d)
1912 1911 arg += 1
1913 1912 else:
1914 1913 raise util.Abort('unexpected revspec format character %s' % d)
1915 1914 else:
1916 1915 ret += c
1917 1916 pos += 1
1918 1917
1919 1918 return ret
1920 1919
1921 1920 def prettyformat(tree):
1922 1921 def _prettyformat(tree, level, lines):
1923 1922 if not isinstance(tree, tuple) or tree[0] in ('string', 'symbol'):
1924 1923 lines.append((level, str(tree)))
1925 1924 else:
1926 1925 lines.append((level, '(%s' % tree[0]))
1927 1926 for s in tree[1:]:
1928 1927 _prettyformat(s, level + 1, lines)
1929 1928 lines[-1:] = [(lines[-1][0], lines[-1][1] + ')')]
1930 1929
1931 1930 lines = []
1932 1931 _prettyformat(tree, 0, lines)
1933 1932 output = '\n'.join((' '*l + s) for l, s in lines)
1934 1933 return output
1935 1934
1936 1935 # tell hggettext to extract docstrings from these functions:
1937 1936 i18nfunctions = symbols.values()
General Comments 0
You need to be logged in to leave comments. Login now