##// END OF EJS Templates
hggettext: handle i18nfunctions declaration for docstrings translations
Patrick Mezard -
r12823:80deae3b stable
parent child Browse files
Show More
@@ -1,568 +1,571
1 1 # Mercurial extension to provide the 'hg bookmark' command
2 2 #
3 3 # Copyright 2008 David Soria Parra <dsp@php.net>
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 '''track a line of development with movable markers
9 9
10 10 Bookmarks are local movable markers to changesets. Every bookmark
11 11 points to a changeset identified by its hash. If you commit a
12 12 changeset that is based on a changeset that has a bookmark on it, the
13 13 bookmark shifts to the new changeset.
14 14
15 15 It is possible to use bookmark names in every revision lookup (e.g.
16 16 :hg:`merge`, :hg:`update`).
17 17
18 18 By default, when several bookmarks point to the same changeset, they
19 19 will all move forward together. It is possible to obtain a more
20 20 git-like experience by adding the following configuration option to
21 21 your configuration file::
22 22
23 23 [bookmarks]
24 24 track.current = True
25 25
26 26 This will cause Mercurial to track the bookmark that you are currently
27 27 using, and only update it. This is similar to git's approach to
28 28 branching.
29 29 '''
30 30
31 31 from mercurial.i18n import _
32 32 from mercurial.node import nullid, nullrev, bin, hex, short
33 33 from mercurial import util, commands, repair, extensions, pushkey, hg, url
34 34 from mercurial import revset
35 35 import os
36 36
37 37 def write(repo):
38 38 '''Write bookmarks
39 39
40 40 Write the given bookmark => hash dictionary to the .hg/bookmarks file
41 41 in a format equal to those of localtags.
42 42
43 43 We also store a backup of the previous state in undo.bookmarks that
44 44 can be copied back on rollback.
45 45 '''
46 46 refs = repo._bookmarks
47 47 if os.path.exists(repo.join('bookmarks')):
48 48 util.copyfile(repo.join('bookmarks'), repo.join('undo.bookmarks'))
49 49 if repo._bookmarkcurrent not in refs:
50 50 setcurrent(repo, None)
51 51 wlock = repo.wlock()
52 52 try:
53 53 file = repo.opener('bookmarks', 'w', atomictemp=True)
54 54 for refspec, node in refs.iteritems():
55 55 file.write("%s %s\n" % (hex(node), refspec))
56 56 file.rename()
57 57
58 58 # touch 00changelog.i so hgweb reloads bookmarks (no lock needed)
59 59 try:
60 60 os.utime(repo.sjoin('00changelog.i'), None)
61 61 except OSError:
62 62 pass
63 63
64 64 finally:
65 65 wlock.release()
66 66
67 67 def setcurrent(repo, mark):
68 68 '''Set the name of the bookmark that we are currently on
69 69
70 70 Set the name of the bookmark that we are on (hg update <bookmark>).
71 71 The name is recorded in .hg/bookmarks.current
72 72 '''
73 73 current = repo._bookmarkcurrent
74 74 if current == mark:
75 75 return
76 76
77 77 refs = repo._bookmarks
78 78
79 79 # do not update if we do update to a rev equal to the current bookmark
80 80 if (mark and mark not in refs and
81 81 current and refs[current] == repo.changectx('.').node()):
82 82 return
83 83 if mark not in refs:
84 84 mark = ''
85 85 wlock = repo.wlock()
86 86 try:
87 87 file = repo.opener('bookmarks.current', 'w', atomictemp=True)
88 88 file.write(mark)
89 89 file.rename()
90 90 finally:
91 91 wlock.release()
92 92 repo._bookmarkcurrent = mark
93 93
94 94 def bookmark(ui, repo, mark=None, rev=None, force=False, delete=False, rename=None):
95 95 '''track a line of development with movable markers
96 96
97 97 Bookmarks are pointers to certain commits that move when
98 98 committing. Bookmarks are local. They can be renamed, copied and
99 99 deleted. It is possible to use bookmark names in :hg:`merge` and
100 100 :hg:`update` to merge and update respectively to a given bookmark.
101 101
102 102 You can use :hg:`bookmark NAME` to set a bookmark on the working
103 103 directory's parent revision with the given name. If you specify
104 104 a revision using -r REV (where REV may be an existing bookmark),
105 105 the bookmark is assigned to that revision.
106 106
107 107 Bookmarks can be pushed and pulled between repositories (see :hg:`help
108 108 push` and :hg:`help pull`). This requires the bookmark extension to be
109 109 enabled for both the local and remote repositories.
110 110 '''
111 111 hexfn = ui.debugflag and hex or short
112 112 marks = repo._bookmarks
113 113 cur = repo.changectx('.').node()
114 114
115 115 if rename:
116 116 if rename not in marks:
117 117 raise util.Abort(_("a bookmark of this name does not exist"))
118 118 if mark in marks and not force:
119 119 raise util.Abort(_("a bookmark of the same name already exists"))
120 120 if mark is None:
121 121 raise util.Abort(_("new bookmark name required"))
122 122 marks[mark] = marks[rename]
123 123 del marks[rename]
124 124 if repo._bookmarkcurrent == rename:
125 125 setcurrent(repo, mark)
126 126 write(repo)
127 127 return
128 128
129 129 if delete:
130 130 if mark is None:
131 131 raise util.Abort(_("bookmark name required"))
132 132 if mark not in marks:
133 133 raise util.Abort(_("a bookmark of this name does not exist"))
134 134 if mark == repo._bookmarkcurrent:
135 135 setcurrent(repo, None)
136 136 del marks[mark]
137 137 write(repo)
138 138 return
139 139
140 140 if mark != None:
141 141 if "\n" in mark:
142 142 raise util.Abort(_("bookmark name cannot contain newlines"))
143 143 mark = mark.strip()
144 144 if not mark:
145 145 raise util.Abort(_("bookmark names cannot consist entirely of "
146 146 "whitespace"))
147 147 if mark in marks and not force:
148 148 raise util.Abort(_("a bookmark of the same name already exists"))
149 149 if ((mark in repo.branchtags() or mark == repo.dirstate.branch())
150 150 and not force):
151 151 raise util.Abort(
152 152 _("a bookmark cannot have the name of an existing branch"))
153 153 if rev:
154 154 marks[mark] = repo.lookup(rev)
155 155 else:
156 156 marks[mark] = repo.changectx('.').node()
157 157 setcurrent(repo, mark)
158 158 write(repo)
159 159 return
160 160
161 161 if mark is None:
162 162 if rev:
163 163 raise util.Abort(_("bookmark name required"))
164 164 if len(marks) == 0:
165 165 ui.status(_("no bookmarks set\n"))
166 166 else:
167 167 for bmark, n in marks.iteritems():
168 168 if ui.configbool('bookmarks', 'track.current'):
169 169 current = repo._bookmarkcurrent
170 170 if bmark == current and n == cur:
171 171 prefix, label = '*', 'bookmarks.current'
172 172 else:
173 173 prefix, label = ' ', ''
174 174 else:
175 175 if n == cur:
176 176 prefix, label = '*', 'bookmarks.current'
177 177 else:
178 178 prefix, label = ' ', ''
179 179
180 180 if ui.quiet:
181 181 ui.write("%s\n" % bmark, label=label)
182 182 else:
183 183 ui.write(" %s %-25s %d:%s\n" % (
184 184 prefix, bmark, repo.changelog.rev(n), hexfn(n)),
185 185 label=label)
186 186 return
187 187
188 188 def _revstostrip(changelog, node):
189 189 srev = changelog.rev(node)
190 190 tostrip = [srev]
191 191 saveheads = []
192 192 for r in xrange(srev, len(changelog)):
193 193 parents = changelog.parentrevs(r)
194 194 if parents[0] in tostrip or parents[1] in tostrip:
195 195 tostrip.append(r)
196 196 if parents[1] != nullrev:
197 197 for p in parents:
198 198 if p not in tostrip and p > srev:
199 199 saveheads.append(p)
200 200 return [r for r in tostrip if r not in saveheads]
201 201
202 202 def strip(oldstrip, ui, repo, node, backup="all"):
203 203 """Strip bookmarks if revisions are stripped using
204 204 the mercurial.strip method. This usually happens during
205 205 qpush and qpop"""
206 206 revisions = _revstostrip(repo.changelog, node)
207 207 marks = repo._bookmarks
208 208 update = []
209 209 for mark, n in marks.iteritems():
210 210 if repo.changelog.rev(n) in revisions:
211 211 update.append(mark)
212 212 oldstrip(ui, repo, node, backup)
213 213 if len(update) > 0:
214 214 for m in update:
215 215 marks[m] = repo.changectx('.').node()
216 216 write(repo)
217 217
218 218 def reposetup(ui, repo):
219 219 if not repo.local():
220 220 return
221 221
222 222 class bookmark_repo(repo.__class__):
223 223
224 224 @util.propertycache
225 225 def _bookmarks(self):
226 226 '''Parse .hg/bookmarks file and return a dictionary
227 227
228 228 Bookmarks are stored as {HASH}\\s{NAME}\\n (localtags format) values
229 229 in the .hg/bookmarks file.
230 230 Read the file and return a (name=>nodeid) dictionary
231 231 '''
232 232 try:
233 233 bookmarks = {}
234 234 for line in self.opener('bookmarks'):
235 235 sha, refspec = line.strip().split(' ', 1)
236 236 bookmarks[refspec] = self.changelog.lookup(sha)
237 237 except:
238 238 pass
239 239 return bookmarks
240 240
241 241 @util.propertycache
242 242 def _bookmarkcurrent(self):
243 243 '''Get the current bookmark
244 244
245 245 If we use gittishsh branches we have a current bookmark that
246 246 we are on. This function returns the name of the bookmark. It
247 247 is stored in .hg/bookmarks.current
248 248 '''
249 249 mark = None
250 250 if os.path.exists(self.join('bookmarks.current')):
251 251 file = self.opener('bookmarks.current')
252 252 # No readline() in posixfile_nt, reading everything is cheap
253 253 mark = (file.readlines() or [''])[0]
254 254 if mark == '':
255 255 mark = None
256 256 file.close()
257 257 return mark
258 258
259 259 def rollback(self, *args):
260 260 if os.path.exists(self.join('undo.bookmarks')):
261 261 util.rename(self.join('undo.bookmarks'), self.join('bookmarks'))
262 262 return super(bookmark_repo, self).rollback(*args)
263 263
264 264 def lookup(self, key):
265 265 if key in self._bookmarks:
266 266 key = self._bookmarks[key]
267 267 return super(bookmark_repo, self).lookup(key)
268 268
269 269 def _bookmarksupdate(self, parents, node):
270 270 marks = self._bookmarks
271 271 update = False
272 272 if ui.configbool('bookmarks', 'track.current'):
273 273 mark = self._bookmarkcurrent
274 274 if mark and marks[mark] in parents:
275 275 marks[mark] = node
276 276 update = True
277 277 else:
278 278 for mark, n in marks.items():
279 279 if n in parents:
280 280 marks[mark] = node
281 281 update = True
282 282 if update:
283 283 write(self)
284 284
285 285 def commitctx(self, ctx, error=False):
286 286 """Add a revision to the repository and
287 287 move the bookmark"""
288 288 wlock = self.wlock() # do both commit and bookmark with lock held
289 289 try:
290 290 node = super(bookmark_repo, self).commitctx(ctx, error)
291 291 if node is None:
292 292 return None
293 293 parents = self.changelog.parents(node)
294 294 if parents[1] == nullid:
295 295 parents = (parents[0],)
296 296
297 297 self._bookmarksupdate(parents, node)
298 298 return node
299 299 finally:
300 300 wlock.release()
301 301
302 302 def pull(self, remote, heads=None, force=False):
303 303 result = super(bookmark_repo, self).pull(remote, heads, force)
304 304
305 305 self.ui.debug("checking for updated bookmarks\n")
306 306 rb = remote.listkeys('bookmarks')
307 307 changed = False
308 308 for k in rb.keys():
309 309 if k in self._bookmarks:
310 310 nr, nl = rb[k], self._bookmarks[k]
311 311 if nr in self:
312 312 cr = self[nr]
313 313 cl = self[nl]
314 314 if cl.rev() >= cr.rev():
315 315 continue
316 316 if cr in cl.descendants():
317 317 self._bookmarks[k] = cr.node()
318 318 changed = True
319 319 self.ui.status(_("updating bookmark %s\n") % k)
320 320 else:
321 321 self.ui.warn(_("not updating divergent"
322 322 " bookmark %s\n") % k)
323 323 if changed:
324 324 write(repo)
325 325
326 326 return result
327 327
328 328 def push(self, remote, force=False, revs=None, newbranch=False):
329 329 result = super(bookmark_repo, self).push(remote, force, revs,
330 330 newbranch)
331 331
332 332 self.ui.debug("checking for updated bookmarks\n")
333 333 rb = remote.listkeys('bookmarks')
334 334 for k in rb.keys():
335 335 if k in self._bookmarks:
336 336 nr, nl = rb[k], self._bookmarks[k]
337 337 if nr in self:
338 338 cr = self[nr]
339 339 cl = self[nl]
340 340 if cl in cr.descendants():
341 341 r = remote.pushkey('bookmarks', k, nr, nl)
342 342 if r:
343 343 self.ui.status(_("updating bookmark %s\n") % k)
344 344 else:
345 345 self.ui.warn(_('updating bookmark %s'
346 346 ' failed!\n') % k)
347 347
348 348 return result
349 349
350 350 def addchangegroup(self, *args, **kwargs):
351 351 parents = self.dirstate.parents()
352 352
353 353 result = super(bookmark_repo, self).addchangegroup(*args, **kwargs)
354 354 if result > 1:
355 355 # We have more heads than before
356 356 return result
357 357 node = self.changelog.tip()
358 358
359 359 self._bookmarksupdate(parents, node)
360 360 return result
361 361
362 362 def _findtags(self):
363 363 """Merge bookmarks with normal tags"""
364 364 (tags, tagtypes) = super(bookmark_repo, self)._findtags()
365 365 tags.update(self._bookmarks)
366 366 return (tags, tagtypes)
367 367
368 368 if hasattr(repo, 'invalidate'):
369 369 def invalidate(self):
370 370 super(bookmark_repo, self).invalidate()
371 371 for attr in ('_bookmarks', '_bookmarkcurrent'):
372 372 if attr in self.__dict__:
373 373 delattr(self, attr)
374 374
375 375 repo.__class__ = bookmark_repo
376 376
377 377 def listbookmarks(repo):
378 378 # We may try to list bookmarks on a repo type that does not
379 379 # support it (e.g., statichttprepository).
380 380 if not hasattr(repo, '_bookmarks'):
381 381 return {}
382 382
383 383 d = {}
384 384 for k, v in repo._bookmarks.iteritems():
385 385 d[k] = hex(v)
386 386 return d
387 387
388 388 def pushbookmark(repo, key, old, new):
389 389 w = repo.wlock()
390 390 try:
391 391 marks = repo._bookmarks
392 392 if hex(marks.get(key, '')) != old:
393 393 return False
394 394 if new == '':
395 395 del marks[key]
396 396 else:
397 397 if new not in repo:
398 398 return False
399 399 marks[key] = repo[new].node()
400 400 write(repo)
401 401 return True
402 402 finally:
403 403 w.release()
404 404
405 405 def pull(oldpull, ui, repo, source="default", **opts):
406 406 # translate bookmark args to rev args for actual pull
407 407 if opts.get('bookmark'):
408 408 # this is an unpleasant hack as pull will do this internally
409 409 source, branches = hg.parseurl(ui.expandpath(source),
410 410 opts.get('branch'))
411 411 other = hg.repository(hg.remoteui(repo, opts), source)
412 412 rb = other.listkeys('bookmarks')
413 413
414 414 for b in opts['bookmark']:
415 415 if b not in rb:
416 416 raise util.Abort(_('remote bookmark %s not found!') % b)
417 417 opts.setdefault('rev', []).append(b)
418 418
419 419 result = oldpull(ui, repo, source, **opts)
420 420
421 421 # update specified bookmarks
422 422 if opts.get('bookmark'):
423 423 for b in opts['bookmark']:
424 424 # explicit pull overrides local bookmark if any
425 425 ui.status(_("importing bookmark %s\n") % b)
426 426 repo._bookmarks[b] = repo[rb[b]].node()
427 427 write(repo)
428 428
429 429 return result
430 430
431 431 def push(oldpush, ui, repo, dest=None, **opts):
432 432 dopush = True
433 433 if opts.get('bookmark'):
434 434 dopush = False
435 435 for b in opts['bookmark']:
436 436 if b in repo._bookmarks:
437 437 dopush = True
438 438 opts.setdefault('rev', []).append(b)
439 439
440 440 result = 0
441 441 if dopush:
442 442 result = oldpush(ui, repo, dest, **opts)
443 443
444 444 if opts.get('bookmark'):
445 445 # this is an unpleasant hack as push will do this internally
446 446 dest = ui.expandpath(dest or 'default-push', dest or 'default')
447 447 dest, branches = hg.parseurl(dest, opts.get('branch'))
448 448 other = hg.repository(hg.remoteui(repo, opts), dest)
449 449 rb = other.listkeys('bookmarks')
450 450 for b in opts['bookmark']:
451 451 # explicit push overrides remote bookmark if any
452 452 if b in repo._bookmarks:
453 453 ui.status(_("exporting bookmark %s\n") % b)
454 454 new = repo[b].hex()
455 455 elif b in rb:
456 456 ui.status(_("deleting remote bookmark %s\n") % b)
457 457 new = '' # delete
458 458 else:
459 459 ui.warn(_('bookmark %s does not exist on the local '
460 460 'or remote repository!\n') % b)
461 461 return 2
462 462 old = rb.get(b, '')
463 463 r = other.pushkey('bookmarks', b, old, new)
464 464 if not r:
465 465 ui.warn(_('updating bookmark %s failed!\n') % b)
466 466 if not result:
467 467 result = 2
468 468
469 469 return result
470 470
471 471 def diffbookmarks(ui, repo, remote):
472 472 ui.status(_("searching for changes\n"))
473 473
474 474 lmarks = repo.listkeys('bookmarks')
475 475 rmarks = remote.listkeys('bookmarks')
476 476
477 477 diff = sorted(set(rmarks) - set(lmarks))
478 478 for k in diff:
479 479 ui.write(" %-25s %s\n" % (k, rmarks[k][:12]))
480 480
481 481 if len(diff) <= 0:
482 482 ui.status(_("no changes found\n"))
483 483 return 1
484 484 return 0
485 485
486 486 def incoming(oldincoming, ui, repo, source="default", **opts):
487 487 if opts.get('bookmarks'):
488 488 source, branches = hg.parseurl(ui.expandpath(source), opts.get('branch'))
489 489 other = hg.repository(hg.remoteui(repo, opts), source)
490 490 ui.status(_('comparing with %s\n') % url.hidepassword(source))
491 491 return diffbookmarks(ui, repo, other)
492 492 else:
493 493 return oldincoming(ui, repo, source, **opts)
494 494
495 495 def outgoing(oldoutgoing, ui, repo, dest=None, **opts):
496 496 if opts.get('bookmarks'):
497 497 dest = ui.expandpath(dest or 'default-push', dest or 'default')
498 498 dest, branches = hg.parseurl(dest, opts.get('branch'))
499 499 other = hg.repository(hg.remoteui(repo, opts), dest)
500 500 ui.status(_('comparing with %s\n') % url.hidepassword(dest))
501 501 return diffbookmarks(ui, other, repo)
502 502 else:
503 503 return oldoutgoing(ui, repo, dest, **opts)
504 504
505 505 def uisetup(ui):
506 506 extensions.wrapfunction(repair, "strip", strip)
507 507 if ui.configbool('bookmarks', 'track.current'):
508 508 extensions.wrapcommand(commands.table, 'update', updatecurbookmark)
509 509
510 510 entry = extensions.wrapcommand(commands.table, 'pull', pull)
511 511 entry[1].append(('B', 'bookmark', [],
512 512 _("bookmark to import"),
513 513 _('BOOKMARK')))
514 514 entry = extensions.wrapcommand(commands.table, 'push', push)
515 515 entry[1].append(('B', 'bookmark', [],
516 516 _("bookmark to export"),
517 517 _('BOOKMARK')))
518 518 entry = extensions.wrapcommand(commands.table, 'incoming', incoming)
519 519 entry[1].append(('B', 'bookmarks', False,
520 520 _("compare bookmark")))
521 521 entry = extensions.wrapcommand(commands.table, 'outgoing', outgoing)
522 522 entry[1].append(('B', 'bookmarks', False,
523 523 _("compare bookmark")))
524 524
525 525 pushkey.register('bookmarks', pushbookmark, listbookmarks)
526 526
527 527 def updatecurbookmark(orig, ui, repo, *args, **opts):
528 528 '''Set the current bookmark
529 529
530 530 If the user updates to a bookmark we update the .hg/bookmarks.current
531 531 file.
532 532 '''
533 533 res = orig(ui, repo, *args, **opts)
534 534 rev = opts['rev']
535 535 if not rev and len(args) > 0:
536 536 rev = args[0]
537 537 setcurrent(repo, rev)
538 538 return res
539 539
540 540 def bmrevset(repo, subset, x):
541 541 """``bookmark([name])``
542 542 The named bookmark or all bookmarks.
543 543 """
544 544 args = revset.getargs(x, 0, 1, _('bookmark takes one or no arguments'))
545 545 if args:
546 546 bm = revset.getstring(args[0],
547 547 _('the argument to bookmark must be a string'))
548 548 bmrev = listbookmarks(repo).get(bm, None)
549 549 if bmrev:
550 550 bmrev = repo.changelog.rev(bin(bmrev))
551 551 return [r for r in subset if r == bmrev]
552 552 bms = set([repo.changelog.rev(bin(r)) for r in listbookmarks(repo).values()])
553 553 return [r for r in subset if r in bms]
554 554
555 555 def extsetup(ui):
556 556 revset.symbols['bookmark'] = bmrevset
557 557
558 558 cmdtable = {
559 559 "bookmarks":
560 560 (bookmark,
561 561 [('f', 'force', False, _('force')),
562 562 ('r', 'rev', '', _('revision'), _('REV')),
563 563 ('d', 'delete', False, _('delete a given bookmark')),
564 564 ('m', 'rename', '', _('rename a given bookmark'), _('NAME'))],
565 565 _('hg bookmarks [-f] [-d] [-m NAME] [-r REV] [NAME]')),
566 566 }
567 567
568 568 colortable = {'bookmarks.current': 'green'}
569
570 # tell hggettext to extract docstrings from these functions:
571 i18nfunctions = [bmrevset]
@@ -1,627 +1,630
1 1 # Patch transplanting extension for Mercurial
2 2 #
3 3 # Copyright 2006, 2007 Brendan Cully <brendan@kublai.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 '''command to transplant changesets from another branch
9 9
10 10 This extension allows you to transplant patches from another branch.
11 11
12 12 Transplanted patches are recorded in .hg/transplant/transplants, as a
13 13 map from a changeset hash to its hash in the source repository.
14 14 '''
15 15
16 16 from mercurial.i18n import _
17 17 import os, tempfile
18 18 from mercurial import bundlerepo, cmdutil, hg, merge, match
19 19 from mercurial import patch, revlog, util, error
20 20 from mercurial import revset
21 21
22 22 class transplantentry(object):
23 23 def __init__(self, lnode, rnode):
24 24 self.lnode = lnode
25 25 self.rnode = rnode
26 26
27 27 class transplants(object):
28 28 def __init__(self, path=None, transplantfile=None, opener=None):
29 29 self.path = path
30 30 self.transplantfile = transplantfile
31 31 self.opener = opener
32 32
33 33 if not opener:
34 34 self.opener = util.opener(self.path)
35 35 self.transplants = {}
36 36 self.dirty = False
37 37 self.read()
38 38
39 39 def read(self):
40 40 abspath = os.path.join(self.path, self.transplantfile)
41 41 if self.transplantfile and os.path.exists(abspath):
42 42 for line in self.opener(self.transplantfile).read().splitlines():
43 43 lnode, rnode = map(revlog.bin, line.split(':'))
44 44 list = self.transplants.setdefault(rnode, [])
45 45 list.append(transplantentry(lnode, rnode))
46 46
47 47 def write(self):
48 48 if self.dirty and self.transplantfile:
49 49 if not os.path.isdir(self.path):
50 50 os.mkdir(self.path)
51 51 fp = self.opener(self.transplantfile, 'w')
52 52 for list in self.transplants.itervalues():
53 53 for t in list:
54 54 l, r = map(revlog.hex, (t.lnode, t.rnode))
55 55 fp.write(l + ':' + r + '\n')
56 56 fp.close()
57 57 self.dirty = False
58 58
59 59 def get(self, rnode):
60 60 return self.transplants.get(rnode) or []
61 61
62 62 def set(self, lnode, rnode):
63 63 list = self.transplants.setdefault(rnode, [])
64 64 list.append(transplantentry(lnode, rnode))
65 65 self.dirty = True
66 66
67 67 def remove(self, transplant):
68 68 list = self.transplants.get(transplant.rnode)
69 69 if list:
70 70 del list[list.index(transplant)]
71 71 self.dirty = True
72 72
73 73 class transplanter(object):
74 74 def __init__(self, ui, repo):
75 75 self.ui = ui
76 76 self.path = repo.join('transplant')
77 77 self.opener = util.opener(self.path)
78 78 self.transplants = transplants(self.path, 'transplants',
79 79 opener=self.opener)
80 80
81 81 def applied(self, repo, node, parent):
82 82 '''returns True if a node is already an ancestor of parent
83 83 or has already been transplanted'''
84 84 if hasnode(repo, node):
85 85 if node in repo.changelog.reachable(parent, stop=node):
86 86 return True
87 87 for t in self.transplants.get(node):
88 88 # it might have been stripped
89 89 if not hasnode(repo, t.lnode):
90 90 self.transplants.remove(t)
91 91 return False
92 92 if t.lnode in repo.changelog.reachable(parent, stop=t.lnode):
93 93 return True
94 94 return False
95 95
96 96 def apply(self, repo, source, revmap, merges, opts={}):
97 97 '''apply the revisions in revmap one by one in revision order'''
98 98 revs = sorted(revmap)
99 99 p1, p2 = repo.dirstate.parents()
100 100 pulls = []
101 101 diffopts = patch.diffopts(self.ui, opts)
102 102 diffopts.git = True
103 103
104 104 lock = wlock = None
105 105 try:
106 106 wlock = repo.wlock()
107 107 lock = repo.lock()
108 108 for rev in revs:
109 109 node = revmap[rev]
110 110 revstr = '%s:%s' % (rev, revlog.short(node))
111 111
112 112 if self.applied(repo, node, p1):
113 113 self.ui.warn(_('skipping already applied revision %s\n') %
114 114 revstr)
115 115 continue
116 116
117 117 parents = source.changelog.parents(node)
118 118 if not opts.get('filter'):
119 119 # If the changeset parent is the same as the
120 120 # wdir's parent, just pull it.
121 121 if parents[0] == p1:
122 122 pulls.append(node)
123 123 p1 = node
124 124 continue
125 125 if pulls:
126 126 if source != repo:
127 127 repo.pull(source, heads=pulls)
128 128 merge.update(repo, pulls[-1], False, False, None)
129 129 p1, p2 = repo.dirstate.parents()
130 130 pulls = []
131 131
132 132 domerge = False
133 133 if node in merges:
134 134 # pulling all the merge revs at once would mean we
135 135 # couldn't transplant after the latest even if
136 136 # transplants before them fail.
137 137 domerge = True
138 138 if not hasnode(repo, node):
139 139 repo.pull(source, heads=[node])
140 140
141 141 if parents[1] != revlog.nullid:
142 142 self.ui.note(_('skipping merge changeset %s:%s\n')
143 143 % (rev, revlog.short(node)))
144 144 patchfile = None
145 145 else:
146 146 fd, patchfile = tempfile.mkstemp(prefix='hg-transplant-')
147 147 fp = os.fdopen(fd, 'w')
148 148 gen = patch.diff(source, parents[0], node, opts=diffopts)
149 149 for chunk in gen:
150 150 fp.write(chunk)
151 151 fp.close()
152 152
153 153 del revmap[rev]
154 154 if patchfile or domerge:
155 155 try:
156 156 n = self.applyone(repo, node,
157 157 source.changelog.read(node),
158 158 patchfile, merge=domerge,
159 159 log=opts.get('log'),
160 160 filter=opts.get('filter'))
161 161 if n and domerge:
162 162 self.ui.status(_('%s merged at %s\n') % (revstr,
163 163 revlog.short(n)))
164 164 elif n:
165 165 self.ui.status(_('%s transplanted to %s\n')
166 166 % (revlog.short(node),
167 167 revlog.short(n)))
168 168 finally:
169 169 if patchfile:
170 170 os.unlink(patchfile)
171 171 if pulls:
172 172 repo.pull(source, heads=pulls)
173 173 merge.update(repo, pulls[-1], False, False, None)
174 174 finally:
175 175 self.saveseries(revmap, merges)
176 176 self.transplants.write()
177 177 lock.release()
178 178 wlock.release()
179 179
180 180 def filter(self, filter, changelog, patchfile):
181 181 '''arbitrarily rewrite changeset before applying it'''
182 182
183 183 self.ui.status(_('filtering %s\n') % patchfile)
184 184 user, date, msg = (changelog[1], changelog[2], changelog[4])
185 185
186 186 fd, headerfile = tempfile.mkstemp(prefix='hg-transplant-')
187 187 fp = os.fdopen(fd, 'w')
188 188 fp.write("# HG changeset patch\n")
189 189 fp.write("# User %s\n" % user)
190 190 fp.write("# Date %d %d\n" % date)
191 191 fp.write(msg + '\n')
192 192 fp.close()
193 193
194 194 try:
195 195 util.system('%s %s %s' % (filter, util.shellquote(headerfile),
196 196 util.shellquote(patchfile)),
197 197 environ={'HGUSER': changelog[1]},
198 198 onerr=util.Abort, errprefix=_('filter failed'))
199 199 user, date, msg = self.parselog(file(headerfile))[1:4]
200 200 finally:
201 201 os.unlink(headerfile)
202 202
203 203 return (user, date, msg)
204 204
205 205 def applyone(self, repo, node, cl, patchfile, merge=False, log=False,
206 206 filter=None):
207 207 '''apply the patch in patchfile to the repository as a transplant'''
208 208 (manifest, user, (time, timezone), files, message) = cl[:5]
209 209 date = "%d %d" % (time, timezone)
210 210 extra = {'transplant_source': node}
211 211 if filter:
212 212 (user, date, message) = self.filter(filter, cl, patchfile)
213 213
214 214 if log:
215 215 # we don't translate messages inserted into commits
216 216 message += '\n(transplanted from %s)' % revlog.hex(node)
217 217
218 218 self.ui.status(_('applying %s\n') % revlog.short(node))
219 219 self.ui.note('%s %s\n%s\n' % (user, date, message))
220 220
221 221 if not patchfile and not merge:
222 222 raise util.Abort(_('can only omit patchfile if merging'))
223 223 if patchfile:
224 224 try:
225 225 files = {}
226 226 try:
227 227 patch.patch(patchfile, self.ui, cwd=repo.root,
228 228 files=files, eolmode=None)
229 229 if not files:
230 230 self.ui.warn(_('%s: empty changeset')
231 231 % revlog.hex(node))
232 232 return None
233 233 finally:
234 234 files = cmdutil.updatedir(self.ui, repo, files)
235 235 except Exception, inst:
236 236 seriespath = os.path.join(self.path, 'series')
237 237 if os.path.exists(seriespath):
238 238 os.unlink(seriespath)
239 239 p1 = repo.dirstate.parents()[0]
240 240 p2 = node
241 241 self.log(user, date, message, p1, p2, merge=merge)
242 242 self.ui.write(str(inst) + '\n')
243 243 raise util.Abort(_('fix up the merge and run '
244 244 'hg transplant --continue'))
245 245 else:
246 246 files = None
247 247 if merge:
248 248 p1, p2 = repo.dirstate.parents()
249 249 repo.dirstate.setparents(p1, node)
250 250 m = match.always(repo.root, '')
251 251 else:
252 252 m = match.exact(repo.root, '', files)
253 253
254 254 n = repo.commit(message, user, date, extra=extra, match=m)
255 255 if not n:
256 256 # Crash here to prevent an unclear crash later, in
257 257 # transplants.write(). This can happen if patch.patch()
258 258 # does nothing but claims success or if repo.status() fails
259 259 # to report changes done by patch.patch(). These both
260 260 # appear to be bugs in other parts of Mercurial, but dying
261 261 # here, as soon as we can detect the problem, is preferable
262 262 # to silently dropping changesets on the floor.
263 263 raise RuntimeError('nothing committed after transplant')
264 264 if not merge:
265 265 self.transplants.set(n, node)
266 266
267 267 return n
268 268
269 269 def resume(self, repo, source, opts=None):
270 270 '''recover last transaction and apply remaining changesets'''
271 271 if os.path.exists(os.path.join(self.path, 'journal')):
272 272 n, node = self.recover(repo)
273 273 self.ui.status(_('%s transplanted as %s\n') % (revlog.short(node),
274 274 revlog.short(n)))
275 275 seriespath = os.path.join(self.path, 'series')
276 276 if not os.path.exists(seriespath):
277 277 self.transplants.write()
278 278 return
279 279 nodes, merges = self.readseries()
280 280 revmap = {}
281 281 for n in nodes:
282 282 revmap[source.changelog.rev(n)] = n
283 283 os.unlink(seriespath)
284 284
285 285 self.apply(repo, source, revmap, merges, opts)
286 286
287 287 def recover(self, repo):
288 288 '''commit working directory using journal metadata'''
289 289 node, user, date, message, parents = self.readlog()
290 290 merge = len(parents) == 2
291 291
292 292 if not user or not date or not message or not parents[0]:
293 293 raise util.Abort(_('transplant log file is corrupt'))
294 294
295 295 extra = {'transplant_source': node}
296 296 wlock = repo.wlock()
297 297 try:
298 298 p1, p2 = repo.dirstate.parents()
299 299 if p1 != parents[0]:
300 300 raise util.Abort(
301 301 _('working dir not at transplant parent %s') %
302 302 revlog.hex(parents[0]))
303 303 if merge:
304 304 repo.dirstate.setparents(p1, parents[1])
305 305 n = repo.commit(message, user, date, extra=extra)
306 306 if not n:
307 307 raise util.Abort(_('commit failed'))
308 308 if not merge:
309 309 self.transplants.set(n, node)
310 310 self.unlog()
311 311
312 312 return n, node
313 313 finally:
314 314 wlock.release()
315 315
316 316 def readseries(self):
317 317 nodes = []
318 318 merges = []
319 319 cur = nodes
320 320 for line in self.opener('series').read().splitlines():
321 321 if line.startswith('# Merges'):
322 322 cur = merges
323 323 continue
324 324 cur.append(revlog.bin(line))
325 325
326 326 return (nodes, merges)
327 327
328 328 def saveseries(self, revmap, merges):
329 329 if not revmap:
330 330 return
331 331
332 332 if not os.path.isdir(self.path):
333 333 os.mkdir(self.path)
334 334 series = self.opener('series', 'w')
335 335 for rev in sorted(revmap):
336 336 series.write(revlog.hex(revmap[rev]) + '\n')
337 337 if merges:
338 338 series.write('# Merges\n')
339 339 for m in merges:
340 340 series.write(revlog.hex(m) + '\n')
341 341 series.close()
342 342
343 343 def parselog(self, fp):
344 344 parents = []
345 345 message = []
346 346 node = revlog.nullid
347 347 inmsg = False
348 348 for line in fp.read().splitlines():
349 349 if inmsg:
350 350 message.append(line)
351 351 elif line.startswith('# User '):
352 352 user = line[7:]
353 353 elif line.startswith('# Date '):
354 354 date = line[7:]
355 355 elif line.startswith('# Node ID '):
356 356 node = revlog.bin(line[10:])
357 357 elif line.startswith('# Parent '):
358 358 parents.append(revlog.bin(line[9:]))
359 359 elif not line.startswith('# '):
360 360 inmsg = True
361 361 message.append(line)
362 362 return (node, user, date, '\n'.join(message), parents)
363 363
364 364 def log(self, user, date, message, p1, p2, merge=False):
365 365 '''journal changelog metadata for later recover'''
366 366
367 367 if not os.path.isdir(self.path):
368 368 os.mkdir(self.path)
369 369 fp = self.opener('journal', 'w')
370 370 fp.write('# User %s\n' % user)
371 371 fp.write('# Date %s\n' % date)
372 372 fp.write('# Node ID %s\n' % revlog.hex(p2))
373 373 fp.write('# Parent ' + revlog.hex(p1) + '\n')
374 374 if merge:
375 375 fp.write('# Parent ' + revlog.hex(p2) + '\n')
376 376 fp.write(message.rstrip() + '\n')
377 377 fp.close()
378 378
379 379 def readlog(self):
380 380 return self.parselog(self.opener('journal'))
381 381
382 382 def unlog(self):
383 383 '''remove changelog journal'''
384 384 absdst = os.path.join(self.path, 'journal')
385 385 if os.path.exists(absdst):
386 386 os.unlink(absdst)
387 387
388 388 def transplantfilter(self, repo, source, root):
389 389 def matchfn(node):
390 390 if self.applied(repo, node, root):
391 391 return False
392 392 if source.changelog.parents(node)[1] != revlog.nullid:
393 393 return False
394 394 extra = source.changelog.read(node)[5]
395 395 cnode = extra.get('transplant_source')
396 396 if cnode and self.applied(repo, cnode, root):
397 397 return False
398 398 return True
399 399
400 400 return matchfn
401 401
402 402 def hasnode(repo, node):
403 403 try:
404 404 return repo.changelog.rev(node) != None
405 405 except error.RevlogError:
406 406 return False
407 407
408 408 def browserevs(ui, repo, nodes, opts):
409 409 '''interactively transplant changesets'''
410 410 def browsehelp(ui):
411 411 ui.write(_('y: transplant this changeset\n'
412 412 'n: skip this changeset\n'
413 413 'm: merge at this changeset\n'
414 414 'p: show patch\n'
415 415 'c: commit selected changesets\n'
416 416 'q: cancel transplant\n'
417 417 '?: show this help\n'))
418 418
419 419 displayer = cmdutil.show_changeset(ui, repo, opts)
420 420 transplants = []
421 421 merges = []
422 422 for node in nodes:
423 423 displayer.show(repo[node])
424 424 action = None
425 425 while not action:
426 426 action = ui.prompt(_('apply changeset? [ynmpcq?]:'))
427 427 if action == '?':
428 428 browsehelp(ui)
429 429 action = None
430 430 elif action == 'p':
431 431 parent = repo.changelog.parents(node)[0]
432 432 for chunk in patch.diff(repo, parent, node):
433 433 ui.write(chunk)
434 434 action = None
435 435 elif action not in ('y', 'n', 'm', 'c', 'q'):
436 436 ui.write(_('no such option\n'))
437 437 action = None
438 438 if action == 'y':
439 439 transplants.append(node)
440 440 elif action == 'm':
441 441 merges.append(node)
442 442 elif action == 'c':
443 443 break
444 444 elif action == 'q':
445 445 transplants = ()
446 446 merges = ()
447 447 break
448 448 displayer.close()
449 449 return (transplants, merges)
450 450
451 451 def transplant(ui, repo, *revs, **opts):
452 452 '''transplant changesets from another branch
453 453
454 454 Selected changesets will be applied on top of the current working
455 455 directory with the log of the original changeset. If --log is
456 456 specified, log messages will have a comment appended of the form::
457 457
458 458 (transplanted from CHANGESETHASH)
459 459
460 460 You can rewrite the changelog message with the --filter option.
461 461 Its argument will be invoked with the current changelog message as
462 462 $1 and the patch as $2.
463 463
464 464 If --source/-s is specified, selects changesets from the named
465 465 repository. If --branch/-b is specified, selects changesets from
466 466 the branch holding the named revision, up to that revision. If
467 467 --all/-a is specified, all changesets on the branch will be
468 468 transplanted, otherwise you will be prompted to select the
469 469 changesets you want.
470 470
471 471 :hg:`transplant --branch REVISION --all` will rebase the selected
472 472 branch (up to the named revision) onto your current working
473 473 directory.
474 474
475 475 You can optionally mark selected transplanted changesets as merge
476 476 changesets. You will not be prompted to transplant any ancestors
477 477 of a merged transplant, and you can merge descendants of them
478 478 normally instead of transplanting them.
479 479
480 480 If no merges or revisions are provided, :hg:`transplant` will
481 481 start an interactive changeset browser.
482 482
483 483 If a changeset application fails, you can fix the merge by hand
484 484 and then resume where you left off by calling :hg:`transplant
485 485 --continue/-c`.
486 486 '''
487 487 def incwalk(repo, incoming, branches, match=util.always):
488 488 if not branches:
489 489 branches = None
490 490 for node in repo.changelog.nodesbetween(incoming, branches)[0]:
491 491 if match(node):
492 492 yield node
493 493
494 494 def transplantwalk(repo, root, branches, match=util.always):
495 495 if not branches:
496 496 branches = repo.heads()
497 497 ancestors = []
498 498 for branch in branches:
499 499 ancestors.append(repo.changelog.ancestor(root, branch))
500 500 for node in repo.changelog.nodesbetween(ancestors, branches)[0]:
501 501 if match(node):
502 502 yield node
503 503
504 504 def checkopts(opts, revs):
505 505 if opts.get('continue'):
506 506 if opts.get('branch') or opts.get('all') or opts.get('merge'):
507 507 raise util.Abort(_('--continue is incompatible with '
508 508 'branch, all or merge'))
509 509 return
510 510 if not (opts.get('source') or revs or
511 511 opts.get('merge') or opts.get('branch')):
512 512 raise util.Abort(_('no source URL, branch tag or revision '
513 513 'list provided'))
514 514 if opts.get('all'):
515 515 if not opts.get('branch'):
516 516 raise util.Abort(_('--all requires a branch revision'))
517 517 if revs:
518 518 raise util.Abort(_('--all is incompatible with a '
519 519 'revision list'))
520 520
521 521 checkopts(opts, revs)
522 522
523 523 if not opts.get('log'):
524 524 opts['log'] = ui.config('transplant', 'log')
525 525 if not opts.get('filter'):
526 526 opts['filter'] = ui.config('transplant', 'filter')
527 527
528 528 tp = transplanter(ui, repo)
529 529
530 530 p1, p2 = repo.dirstate.parents()
531 531 if len(repo) > 0 and p1 == revlog.nullid:
532 532 raise util.Abort(_('no revision checked out'))
533 533 if not opts.get('continue'):
534 534 if p2 != revlog.nullid:
535 535 raise util.Abort(_('outstanding uncommitted merges'))
536 536 m, a, r, d = repo.status()[:4]
537 537 if m or a or r or d:
538 538 raise util.Abort(_('outstanding local changes'))
539 539
540 540 bundle = None
541 541 source = opts.get('source')
542 542 if source:
543 543 sourcerepo = ui.expandpath(source)
544 544 source = hg.repository(ui, sourcerepo)
545 545 source, incoming, bundle = bundlerepo.getremotechanges(ui, repo, source,
546 546 force=True)
547 547 else:
548 548 source = repo
549 549
550 550 try:
551 551 if opts.get('continue'):
552 552 tp.resume(repo, source, opts)
553 553 return
554 554
555 555 tf = tp.transplantfilter(repo, source, p1)
556 556 if opts.get('prune'):
557 557 prune = [source.lookup(r)
558 558 for r in cmdutil.revrange(source, opts.get('prune'))]
559 559 matchfn = lambda x: tf(x) and x not in prune
560 560 else:
561 561 matchfn = tf
562 562 branches = map(source.lookup, opts.get('branch', ()))
563 563 merges = map(source.lookup, opts.get('merge', ()))
564 564 revmap = {}
565 565 if revs:
566 566 for r in cmdutil.revrange(source, revs):
567 567 revmap[int(r)] = source.lookup(r)
568 568 elif opts.get('all') or not merges:
569 569 if source != repo:
570 570 alltransplants = incwalk(source, incoming, branches,
571 571 match=matchfn)
572 572 else:
573 573 alltransplants = transplantwalk(source, p1, branches,
574 574 match=matchfn)
575 575 if opts.get('all'):
576 576 revs = alltransplants
577 577 else:
578 578 revs, newmerges = browserevs(ui, source, alltransplants, opts)
579 579 merges.extend(newmerges)
580 580 for r in revs:
581 581 revmap[source.changelog.rev(r)] = r
582 582 for r in merges:
583 583 revmap[source.changelog.rev(r)] = r
584 584
585 585 tp.apply(repo, source, revmap, merges, opts)
586 586 finally:
587 587 if bundle:
588 588 source.close()
589 589 os.unlink(bundle)
590 590
591 591 def revsettransplanted(repo, subset, x):
592 592 """``transplanted(set)``
593 593 Transplanted changesets in set.
594 594 """
595 595 if x:
596 596 s = revset.getset(repo, subset, x)
597 597 else:
598 598 s = subset
599 599 cs = set()
600 600 for r in xrange(0, len(repo)):
601 601 if repo[r].extra().get('transplant_source'):
602 602 cs.add(r)
603 603 return [r for r in s if r in cs]
604 604
605 605 def extsetup(ui):
606 606 revset.symbols['transplanted'] = revsettransplanted
607 607
608 608 cmdtable = {
609 609 "transplant":
610 610 (transplant,
611 611 [('s', 'source', '',
612 612 _('pull patches from REPO'), _('REPO')),
613 613 ('b', 'branch', [],
614 614 _('pull patches from branch BRANCH'), _('BRANCH')),
615 615 ('a', 'all', None, _('pull all changesets up to BRANCH')),
616 616 ('p', 'prune', [],
617 617 _('skip over REV'), _('REV')),
618 618 ('m', 'merge', [],
619 619 _('merge at REV'), _('REV')),
620 620 ('', 'log', None, _('append transplant info to log message')),
621 621 ('c', 'continue', None, _('continue last transplant session '
622 622 'after repair')),
623 623 ('', 'filter', '',
624 624 _('filter changesets through command'), _('CMD'))],
625 625 _('hg transplant [-s REPO] [-b BRANCH [-a]] [-p REV] '
626 626 '[-m REV] [REV]...'))
627 627 }
628
629 # tell hggettext to extract docstrings from these functions:
630 i18nfunctions = [revsettransplanted]
@@ -1,131 +1,137
1 1 #!/usr/bin/env python
2 2 #
3 3 # hggettext - carefully extract docstrings for Mercurial
4 4 #
5 5 # Copyright 2009 Matt Mackall <mpm@selenic.com> and others
6 6 #
7 7 # This software may be used and distributed according to the terms of the
8 8 # GNU General Public License version 2 or any later version.
9 9
10 10 # The normalize function is taken from pygettext which is distributed
11 11 # with Python under the Python License, which is GPL compatible.
12 12
13 13 """Extract docstrings from Mercurial commands.
14 14
15 15 Compared to pygettext, this script knows about the cmdtable and table
16 16 dictionaries used by Mercurial, and will only extract docstrings from
17 17 functions mentioned therein.
18 18
19 19 Use xgettext like normal to extract strings marked as translatable and
20 20 join the message cataloges to get the final catalog.
21 21 """
22 22
23 23 import os, sys, inspect
24 24
25 25
26 26 def escape(s):
27 27 # The order is important, the backslash must be escaped first
28 28 # since the other replacements introduce new backslashes
29 29 # themselves.
30 30 s = s.replace('\\', '\\\\')
31 31 s = s.replace('\n', '\\n')
32 32 s = s.replace('\r', '\\r')
33 33 s = s.replace('\t', '\\t')
34 34 s = s.replace('"', '\\"')
35 35 return s
36 36
37 37
38 38 def normalize(s):
39 39 # This converts the various Python string types into a format that
40 40 # is appropriate for .po files, namely much closer to C style.
41 41 lines = s.split('\n')
42 42 if len(lines) == 1:
43 43 s = '"' + escape(s) + '"'
44 44 else:
45 45 if not lines[-1]:
46 46 del lines[-1]
47 47 lines[-1] = lines[-1] + '\n'
48 48 lines = map(escape, lines)
49 49 lineterm = '\\n"\n"'
50 50 s = '""\n"' + lineterm.join(lines) + '"'
51 51 return s
52 52
53 53
54 54 def poentry(path, lineno, s):
55 55 return ('#: %s:%d\n' % (path, lineno) +
56 56 'msgid %s\n' % normalize(s) +
57 57 'msgstr ""\n')
58 58
59 59
60 60 def offset(src, doc, name, default):
61 61 """Compute offset or issue a warning on stdout."""
62 62 # Backslashes in doc appear doubled in src.
63 63 end = src.find(doc.replace('\\', '\\\\'))
64 64 if end == -1:
65 65 # This can happen if the docstring contains unnecessary escape
66 66 # sequences such as \" in a triple-quoted string. The problem
67 67 # is that \" is turned into " and so doc wont appear in src.
68 68 sys.stderr.write("warning: unknown offset in %s, assuming %d lines\n"
69 69 % (name, default))
70 70 return default
71 71 else:
72 72 return src.count('\n', 0, end)
73 73
74 74
75 75 def importpath(path):
76 76 """Import a path like foo/bar/baz.py and return the baz module."""
77 77 if path.endswith('.py'):
78 78 path = path[:-3]
79 79 if path.endswith('/__init__'):
80 80 path = path[:-9]
81 81 path = path.replace('/', '.')
82 82 mod = __import__(path)
83 83 for comp in path.split('.')[1:]:
84 84 mod = getattr(mod, comp)
85 85 return mod
86 86
87 87
88 88 def docstrings(path):
89 89 """Extract docstrings from path.
90 90
91 91 This respects the Mercurial cmdtable/table convention and will
92 92 only extract docstrings from functions mentioned in these tables.
93 93 """
94 94 mod = importpath(path)
95 95 if mod.__doc__:
96 96 src = open(path).read()
97 97 lineno = 1 + offset(src, mod.__doc__, path, 7)
98 98 print poentry(path, lineno, mod.__doc__)
99 99
100 functions = list(getattr(mod, 'i18nfunctions', []))
101 functions = [(f, True) for f in functions]
102
100 103 cmdtable = getattr(mod, 'cmdtable', {})
101 104 if not cmdtable:
102 105 # Maybe we are processing mercurial.commands?
103 106 cmdtable = getattr(mod, 'table', {})
107 functions.extend((c[0], False) for c in cmdtable.itervalues())
104 108
105 for entry in cmdtable.itervalues():
106 func = entry[0]
109 for func, rstrip in functions:
107 110 if func.__doc__:
108 111 src = inspect.getsource(func)
109 112 name = "%s.%s" % (path, func.__name__)
110 113 lineno = func.func_code.co_firstlineno
111 lineno += offset(src, func.__doc__, name, 1)
112 print poentry(path, lineno, func.__doc__)
114 doc = func.__doc__
115 if rstrip:
116 doc = doc.rstrip()
117 lineno += offset(src, doc, name, 1)
118 print poentry(path, lineno, doc)
113 119
114 120
115 121 def rawtext(path):
116 122 src = open(path).read()
117 123 print poentry(path, 1, src)
118 124
119 125
120 126 if __name__ == "__main__":
121 127 # It is very important that we import the Mercurial modules from
122 128 # the source tree where hggettext is executed. Otherwise we might
123 129 # accidentally import and extract strings from a Mercurial
124 130 # installation mentioned in PYTHONPATH.
125 131 sys.path.insert(0, os.getcwd())
126 132 from mercurial import demandimport; demandimport.enable()
127 133 for path in sys.argv[1:]:
128 134 if path.endswith('.txt'):
129 135 rawtext(path)
130 136 else:
131 137 docstrings(path)
@@ -1,793 +1,796
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
10 10 import match as matchmod
11 11 from i18n import _
12 12
13 13 elements = {
14 14 "(": (20, ("group", 1, ")"), ("func", 1, ")")),
15 15 "-": (5, ("negate", 19), ("minus", 5)),
16 16 "::": (17, ("dagrangepre", 17), ("dagrange", 17),
17 17 ("dagrangepost", 17)),
18 18 "..": (17, ("dagrangepre", 17), ("dagrange", 17),
19 19 ("dagrangepost", 17)),
20 20 ":": (15, ("rangepre", 15), ("range", 15), ("rangepost", 15)),
21 21 "not": (10, ("not", 10)),
22 22 "!": (10, ("not", 10)),
23 23 "and": (5, None, ("and", 5)),
24 24 "&": (5, None, ("and", 5)),
25 25 "or": (4, None, ("or", 4)),
26 26 "|": (4, None, ("or", 4)),
27 27 "+": (4, None, ("or", 4)),
28 28 ",": (2, None, ("list", 2)),
29 29 ")": (0, None, None),
30 30 "symbol": (0, ("symbol",), None),
31 31 "string": (0, ("string",), None),
32 32 "end": (0, None, None),
33 33 }
34 34
35 35 keywords = set(['and', 'or', 'not'])
36 36
37 37 def tokenize(program):
38 38 pos, l = 0, len(program)
39 39 while pos < l:
40 40 c = program[pos]
41 41 if c.isspace(): # skip inter-token whitespace
42 42 pass
43 43 elif c == ':' and program[pos:pos + 2] == '::': # look ahead carefully
44 44 yield ('::', None, pos)
45 45 pos += 1 # skip ahead
46 46 elif c == '.' and program[pos:pos + 2] == '..': # look ahead carefully
47 47 yield ('..', None, pos)
48 48 pos += 1 # skip ahead
49 49 elif c in "():,-|&+!": # handle simple operators
50 50 yield (c, None, pos)
51 51 elif (c in '"\'' or c == 'r' and
52 52 program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings
53 53 if c == 'r':
54 54 pos += 1
55 55 c = program[pos]
56 56 decode = lambda x: x
57 57 else:
58 58 decode = lambda x: x.decode('string-escape')
59 59 pos += 1
60 60 s = pos
61 61 while pos < l: # find closing quote
62 62 d = program[pos]
63 63 if d == '\\': # skip over escaped characters
64 64 pos += 2
65 65 continue
66 66 if d == c:
67 67 yield ('string', decode(program[s:pos]), s)
68 68 break
69 69 pos += 1
70 70 else:
71 71 raise error.ParseError(_("unterminated string"), s)
72 72 elif c.isalnum() or c in '._' or ord(c) > 127: # gather up a symbol/keyword
73 73 s = pos
74 74 pos += 1
75 75 while pos < l: # find end of symbol
76 76 d = program[pos]
77 77 if not (d.isalnum() or d in "._" or ord(d) > 127):
78 78 break
79 79 if d == '.' and program[pos - 1] == '.': # special case for ..
80 80 pos -= 1
81 81 break
82 82 pos += 1
83 83 sym = program[s:pos]
84 84 if sym in keywords: # operator keywords
85 85 yield (sym, None, s)
86 86 else:
87 87 yield ('symbol', sym, s)
88 88 pos -= 1
89 89 else:
90 90 raise error.ParseError(_("syntax error"), pos)
91 91 pos += 1
92 92 yield ('end', None, pos)
93 93
94 94 # helpers
95 95
96 96 def getstring(x, err):
97 97 if x and (x[0] == 'string' or x[0] == 'symbol'):
98 98 return x[1]
99 99 raise error.ParseError(err)
100 100
101 101 def getlist(x):
102 102 if not x:
103 103 return []
104 104 if x[0] == 'list':
105 105 return getlist(x[1]) + [x[2]]
106 106 return [x]
107 107
108 108 def getargs(x, min, max, err):
109 109 l = getlist(x)
110 110 if len(l) < min or len(l) > max:
111 111 raise error.ParseError(err)
112 112 return l
113 113
114 114 def getset(repo, subset, x):
115 115 if not x:
116 116 raise error.ParseError(_("missing argument"))
117 117 return methods[x[0]](repo, subset, *x[1:])
118 118
119 119 # operator methods
120 120
121 121 def stringset(repo, subset, x):
122 122 x = repo[x].rev()
123 123 if x == -1 and len(subset) == len(repo):
124 124 return [-1]
125 125 if x in subset:
126 126 return [x]
127 127 return []
128 128
129 129 def symbolset(repo, subset, x):
130 130 if x in symbols:
131 131 raise error.ParseError(_("can't use %s here") % x)
132 132 return stringset(repo, subset, x)
133 133
134 134 def rangeset(repo, subset, x, y):
135 135 m = getset(repo, subset, x)
136 136 if not m:
137 137 m = getset(repo, range(len(repo)), x)
138 138
139 139 n = getset(repo, subset, y)
140 140 if not n:
141 141 n = getset(repo, range(len(repo)), y)
142 142
143 143 if not m or not n:
144 144 return []
145 145 m, n = m[0], n[-1]
146 146
147 147 if m < n:
148 148 r = range(m, n + 1)
149 149 else:
150 150 r = range(m, n - 1, -1)
151 151 s = set(subset)
152 152 return [x for x in r if x in s]
153 153
154 154 def andset(repo, subset, x, y):
155 155 return getset(repo, getset(repo, subset, x), y)
156 156
157 157 def orset(repo, subset, x, y):
158 158 s = set(getset(repo, subset, x))
159 159 s |= set(getset(repo, [r for r in subset if r not in s], y))
160 160 return [r for r in subset if r in s]
161 161
162 162 def notset(repo, subset, x):
163 163 s = set(getset(repo, subset, x))
164 164 return [r for r in subset if r not in s]
165 165
166 166 def listset(repo, subset, a, b):
167 167 raise error.ParseError(_("can't use a list in this context"))
168 168
169 169 def func(repo, subset, a, b):
170 170 if a[0] == 'symbol' and a[1] in symbols:
171 171 return symbols[a[1]](repo, subset, b)
172 172 raise error.ParseError(_("not a function: %s") % a[1])
173 173
174 174 # functions
175 175
176 176 def node(repo, subset, x):
177 177 """``id(string)``
178 178 Revision non-ambiguously specified by the given hex string prefix
179 179 """
180 180 # i18n: "id" is a keyword
181 181 l = getargs(x, 1, 1, _("id requires one argument"))
182 182 # i18n: "id" is a keyword
183 183 n = getstring(l[0], _("id requires a string"))
184 184 if len(n) == 40:
185 185 rn = repo[n].rev()
186 186 else:
187 187 rn = repo.changelog.rev(repo.changelog._partialmatch(n))
188 188 return [r for r in subset if r == rn]
189 189
190 190 def rev(repo, subset, x):
191 191 """``rev(number)``
192 192 Revision with the given numeric identifier.
193 193 """
194 194 # i18n: "rev" is a keyword
195 195 l = getargs(x, 1, 1, _("rev requires one argument"))
196 196 try:
197 197 # i18n: "rev" is a keyword
198 198 l = int(getstring(l[0], _("rev requires a number")))
199 199 except ValueError:
200 200 # i18n: "rev" is a keyword
201 201 raise error.ParseError(_("rev expects a number"))
202 202 return [r for r in subset if r == l]
203 203
204 204 def p1(repo, subset, x):
205 205 """``p1(set)``
206 206 First parent of changesets in set.
207 207 """
208 208 ps = set()
209 209 cl = repo.changelog
210 210 for r in getset(repo, range(len(repo)), x):
211 211 ps.add(cl.parentrevs(r)[0])
212 212 return [r for r in subset if r in ps]
213 213
214 214 def p2(repo, subset, x):
215 215 """``p2(set)``
216 216 Second parent of changesets in set.
217 217 """
218 218 ps = set()
219 219 cl = repo.changelog
220 220 for r in getset(repo, range(len(repo)), x):
221 221 ps.add(cl.parentrevs(r)[1])
222 222 return [r for r in subset if r in ps]
223 223
224 224 def parents(repo, subset, x):
225 225 """``parents(set)``
226 226 The set of all parents for all changesets in set.
227 227 """
228 228 ps = set()
229 229 cl = repo.changelog
230 230 for r in getset(repo, range(len(repo)), x):
231 231 ps.update(cl.parentrevs(r))
232 232 return [r for r in subset if r in ps]
233 233
234 234 def maxrev(repo, subset, x):
235 235 """``max(set)``
236 236 Changeset with highest revision number in set.
237 237 """
238 238 s = getset(repo, subset, x)
239 239 if s:
240 240 m = max(s)
241 241 if m in subset:
242 242 return [m]
243 243 return []
244 244
245 245 def minrev(repo, subset, x):
246 246 """``min(set)``
247 247 Changeset with lowest revision number in set.
248 248 """
249 249 s = getset(repo, subset, x)
250 250 if s:
251 251 m = min(s)
252 252 if m in subset:
253 253 return [m]
254 254 return []
255 255
256 256 def limit(repo, subset, x):
257 257 """``limit(set, n)``
258 258 First n members of set.
259 259 """
260 260 # i18n: "limit" is a keyword
261 261 l = getargs(x, 2, 2, _("limit requires two arguments"))
262 262 try:
263 263 # i18n: "limit" is a keyword
264 264 lim = int(getstring(l[1], _("limit requires a number")))
265 265 except ValueError:
266 266 # i18n: "limit" is a keyword
267 267 raise error.ParseError(_("limit expects a number"))
268 268 return getset(repo, subset, l[0])[:lim]
269 269
270 270 def children(repo, subset, x):
271 271 """``children(set)``
272 272 Child changesets of changesets in set.
273 273 """
274 274 cs = set()
275 275 cl = repo.changelog
276 276 s = set(getset(repo, range(len(repo)), x))
277 277 for r in xrange(0, len(repo)):
278 278 for p in cl.parentrevs(r):
279 279 if p in s:
280 280 cs.add(r)
281 281 return [r for r in subset if r in cs]
282 282
283 283 def branch(repo, subset, x):
284 284 """``branch(set)``
285 285 All changesets belonging to the branches of changesets in set.
286 286 """
287 287 s = getset(repo, range(len(repo)), x)
288 288 b = set()
289 289 for r in s:
290 290 b.add(repo[r].branch())
291 291 s = set(s)
292 292 return [r for r in subset if r in s or repo[r].branch() in b]
293 293
294 294 def ancestor(repo, subset, x):
295 295 """``ancestor(single, single)``
296 296 Greatest common ancestor of the two changesets.
297 297 """
298 298 # i18n: "ancestor" is a keyword
299 299 l = getargs(x, 2, 2, _("ancestor requires two arguments"))
300 300 r = range(len(repo))
301 301 a = getset(repo, r, l[0])
302 302 b = getset(repo, r, l[1])
303 303 if len(a) != 1 or len(b) != 1:
304 304 # i18n: "ancestor" is a keyword
305 305 raise error.ParseError(_("ancestor arguments must be single revisions"))
306 306 an = [repo[a[0]].ancestor(repo[b[0]]).rev()]
307 307
308 308 return [r for r in an if r in subset]
309 309
310 310 def ancestors(repo, subset, x):
311 311 """``ancestors(set)``
312 312 Changesets that are ancestors of a changeset in set.
313 313 """
314 314 args = getset(repo, range(len(repo)), x)
315 315 if not args:
316 316 return []
317 317 s = set(repo.changelog.ancestors(*args)) | set(args)
318 318 return [r for r in subset if r in s]
319 319
320 320 def descendants(repo, subset, x):
321 321 """``descendants(set)``
322 322 Changesets which are descendants of changesets in set.
323 323 """
324 324 args = getset(repo, range(len(repo)), x)
325 325 if not args:
326 326 return []
327 327 s = set(repo.changelog.descendants(*args)) | set(args)
328 328 return [r for r in subset if r in s]
329 329
330 330 def follow(repo, subset, x):
331 331 """``follow()``
332 332 An alias for ``::.`` (ancestors of the working copy's first parent).
333 333 """
334 334 # i18n: "follow" is a keyword
335 335 getargs(x, 0, 0, _("follow takes no arguments"))
336 336 p = repo['.'].rev()
337 337 s = set(repo.changelog.ancestors(p)) | set([p])
338 338 return [r for r in subset if r in s]
339 339
340 340 def date(repo, subset, x):
341 341 """``date(interval)``
342 342 Changesets within the interval, see :hg:`help dates`.
343 343 """
344 344 # i18n: "date" is a keyword
345 345 ds = getstring(x, _("date requires a string"))
346 346 dm = util.matchdate(ds)
347 347 return [r for r in subset if dm(repo[r].date()[0])]
348 348
349 349 def keyword(repo, subset, x):
350 350 """``keyword(string)``
351 351 Search commit message, user name, and names of changed files for
352 352 string.
353 353 """
354 354 # i18n: "keyword" is a keyword
355 355 kw = getstring(x, _("keyword requires a string")).lower()
356 356 l = []
357 357 for r in subset:
358 358 c = repo[r]
359 359 t = " ".join(c.files() + [c.user(), c.description()])
360 360 if kw in t.lower():
361 361 l.append(r)
362 362 return l
363 363
364 364 def grep(repo, subset, x):
365 365 """``grep(regex)``
366 366 Like ``keyword(string)`` but accepts a regex. Use ``grep(r'...')``
367 367 to ensure special escape characters are handled correctly.
368 368 """
369 369 try:
370 370 # i18n: "grep" is a keyword
371 371 gr = re.compile(getstring(x, _("grep requires a string")))
372 372 except re.error, e:
373 373 raise error.ParseError(_('invalid match pattern: %s') % e)
374 374 l = []
375 375 for r in subset:
376 376 c = repo[r]
377 377 for e in c.files() + [c.user(), c.description()]:
378 378 if gr.search(e):
379 379 l.append(r)
380 380 continue
381 381 return l
382 382
383 383 def author(repo, subset, x):
384 384 """``author(string)``
385 385 Alias for ``user(string)``.
386 386 """
387 387 # i18n: "author" is a keyword
388 388 n = getstring(x, _("author requires a string")).lower()
389 389 return [r for r in subset if n in repo[r].user().lower()]
390 390
391 391 def user(repo, subset, x):
392 392 """``user(string)``
393 393 User name is string.
394 394 """
395 395 return author(repo, subset, x)
396 396
397 397 def hasfile(repo, subset, x):
398 398 """``file(pattern)``
399 399 Changesets affecting files matched by pattern.
400 400 """
401 401 # i18n: "file" is a keyword
402 402 pat = getstring(x, _("file requires a pattern"))
403 403 m = matchmod.match(repo.root, repo.getcwd(), [pat])
404 404 s = []
405 405 for r in subset:
406 406 for f in repo[r].files():
407 407 if m(f):
408 408 s.append(r)
409 409 continue
410 410 return s
411 411
412 412 def contains(repo, subset, x):
413 413 """``contains(pattern)``
414 414 Revision contains pattern.
415 415 """
416 416 # i18n: "contains" is a keyword
417 417 pat = getstring(x, _("contains requires a pattern"))
418 418 m = matchmod.match(repo.root, repo.getcwd(), [pat])
419 419 s = []
420 420 if m.files() == [pat]:
421 421 for r in subset:
422 422 if pat in repo[r]:
423 423 s.append(r)
424 424 continue
425 425 else:
426 426 for r in subset:
427 427 for f in repo[r].manifest():
428 428 if m(f):
429 429 s.append(r)
430 430 continue
431 431 return s
432 432
433 433 def checkstatus(repo, subset, pat, field):
434 434 m = matchmod.match(repo.root, repo.getcwd(), [pat])
435 435 s = []
436 436 fast = (m.files() == [pat])
437 437 for r in subset:
438 438 c = repo[r]
439 439 if fast:
440 440 if pat not in c.files():
441 441 continue
442 442 else:
443 443 for f in c.files():
444 444 if m(f):
445 445 break
446 446 else:
447 447 continue
448 448 files = repo.status(c.p1().node(), c.node())[field]
449 449 if fast:
450 450 if pat in files:
451 451 s.append(r)
452 452 continue
453 453 else:
454 454 for f in files:
455 455 if m(f):
456 456 s.append(r)
457 457 continue
458 458 return s
459 459
460 460 def modifies(repo, subset, x):
461 461 """``modifies(pattern)``
462 462 Changesets modifying files matched by pattern.
463 463 """
464 464 # i18n: "modifies" is a keyword
465 465 pat = getstring(x, _("modifies requires a pattern"))
466 466 return checkstatus(repo, subset, pat, 0)
467 467
468 468 def adds(repo, subset, x):
469 469 """``adds(pattern)``
470 470 Changesets that add a file matching pattern.
471 471 """
472 472 # i18n: "adds" is a keyword
473 473 pat = getstring(x, _("adds requires a pattern"))
474 474 return checkstatus(repo, subset, pat, 1)
475 475
476 476 def removes(repo, subset, x):
477 477 """``removes(pattern)``
478 478 Changesets which remove files matching pattern.
479 479 """
480 480 # i18n: "removes" is a keyword
481 481 pat = getstring(x, _("removes requires a pattern"))
482 482 return checkstatus(repo, subset, pat, 2)
483 483
484 484 def merge(repo, subset, x):
485 485 """``merge()``
486 486 Changeset is a merge changeset.
487 487 """
488 488 # i18n: "merge" is a keyword
489 489 getargs(x, 0, 0, _("merge takes no arguments"))
490 490 cl = repo.changelog
491 491 return [r for r in subset if cl.parentrevs(r)[1] != -1]
492 492
493 493 def closed(repo, subset, x):
494 494 """``closed()``
495 495 Changeset is closed.
496 496 """
497 497 # i18n: "closed" is a keyword
498 498 getargs(x, 0, 0, _("closed takes no arguments"))
499 499 return [r for r in subset if repo[r].extra().get('close')]
500 500
501 501 def head(repo, subset, x):
502 502 """``head()``
503 503 Changeset is a named branch head.
504 504 """
505 505 # i18n: "head" is a keyword
506 506 getargs(x, 0, 0, _("head takes no arguments"))
507 507 hs = set()
508 508 for b, ls in repo.branchmap().iteritems():
509 509 hs.update(repo[h].rev() for h in ls)
510 510 return [r for r in subset if r in hs]
511 511
512 512 def reverse(repo, subset, x):
513 513 """``reverse(set)``
514 514 Reverse order of set.
515 515 """
516 516 l = getset(repo, subset, x)
517 517 l.reverse()
518 518 return l
519 519
520 520 def present(repo, subset, x):
521 521 """``present(set)``
522 522 An empty set, if any revision in set isn't found; otherwise,
523 523 all revisions in set.
524 524 """
525 525 try:
526 526 return getset(repo, subset, x)
527 527 except error.RepoLookupError:
528 528 return []
529 529
530 530 def sort(repo, subset, x):
531 531 """``sort(set[, [-]key...])``
532 532 Sort set by keys. The default sort order is ascending, specify a key
533 533 as ``-key`` to sort in descending order.
534 534
535 535 The keys can be:
536 536
537 537 - ``rev`` for the revision number,
538 538 - ``branch`` for the branch name,
539 539 - ``desc`` for the commit message (description),
540 540 - ``user`` for user name (``author`` can be used as an alias),
541 541 - ``date`` for the commit date
542 542 """
543 543 # i18n: "sort" is a keyword
544 544 l = getargs(x, 1, 2, _("sort requires one or two arguments"))
545 545 keys = "rev"
546 546 if len(l) == 2:
547 547 keys = getstring(l[1], _("sort spec must be a string"))
548 548
549 549 s = l[0]
550 550 keys = keys.split()
551 551 l = []
552 552 def invert(s):
553 553 return "".join(chr(255 - ord(c)) for c in s)
554 554 for r in getset(repo, subset, s):
555 555 c = repo[r]
556 556 e = []
557 557 for k in keys:
558 558 if k == 'rev':
559 559 e.append(r)
560 560 elif k == '-rev':
561 561 e.append(-r)
562 562 elif k == 'branch':
563 563 e.append(c.branch())
564 564 elif k == '-branch':
565 565 e.append(invert(c.branch()))
566 566 elif k == 'desc':
567 567 e.append(c.description())
568 568 elif k == '-desc':
569 569 e.append(invert(c.description()))
570 570 elif k in 'user author':
571 571 e.append(c.user())
572 572 elif k in '-user -author':
573 573 e.append(invert(c.user()))
574 574 elif k == 'date':
575 575 e.append(c.date()[0])
576 576 elif k == '-date':
577 577 e.append(-c.date()[0])
578 578 else:
579 579 raise error.ParseError(_("unknown sort key %r") % k)
580 580 e.append(r)
581 581 l.append(e)
582 582 l.sort()
583 583 return [e[-1] for e in l]
584 584
585 585 def getall(repo, subset, x):
586 586 """``all()``
587 587 All changesets, the same as ``0:tip``.
588 588 """
589 589 # i18n: "all" is a keyword
590 590 getargs(x, 0, 0, _("all takes no arguments"))
591 591 return subset
592 592
593 593 def heads(repo, subset, x):
594 594 """``heads(set)``
595 595 Members of set with no children in set.
596 596 """
597 597 s = getset(repo, subset, x)
598 598 ps = set(parents(repo, subset, x))
599 599 return [r for r in s if r not in ps]
600 600
601 601 def roots(repo, subset, x):
602 602 """``roots(set)``
603 603 Changesets with no parent changeset in set.
604 604 """
605 605 s = getset(repo, subset, x)
606 606 cs = set(children(repo, subset, x))
607 607 return [r for r in s if r not in cs]
608 608
609 609 def outgoing(repo, subset, x):
610 610 """``outgoing([path])``
611 611 Changesets not found in the specified destination repository, or the
612 612 default push location.
613 613 """
614 614 import hg # avoid start-up nasties
615 615 # i18n: "outgoing" is a keyword
616 616 l = getargs(x, 0, 1, _("outgoing requires a repository path"))
617 617 # i18n: "outgoing" is a keyword
618 618 dest = l and getstring(l[0], _("outgoing requires a repository path")) or ''
619 619 dest = repo.ui.expandpath(dest or 'default-push', dest or 'default')
620 620 dest, branches = hg.parseurl(dest)
621 621 revs, checkout = hg.addbranchrevs(repo, repo, branches, [])
622 622 if revs:
623 623 revs = [repo.lookup(rev) for rev in revs]
624 624 other = hg.repository(hg.remoteui(repo, {}), dest)
625 625 repo.ui.pushbuffer()
626 626 o = discovery.findoutgoing(repo, other)
627 627 repo.ui.popbuffer()
628 628 cl = repo.changelog
629 629 o = set([cl.rev(r) for r in repo.changelog.nodesbetween(o, revs)[0]])
630 630 return [r for r in subset if r in o]
631 631
632 632 def tag(repo, subset, x):
633 633 """``tag(name)``
634 634 The specified tag by name, or all tagged revisions if no name is given.
635 635 """
636 636 # i18n: "tag" is a keyword
637 637 args = getargs(x, 0, 1, _("tag takes one or no arguments"))
638 638 cl = repo.changelog
639 639 if args:
640 640 tn = getstring(args[0],
641 641 # i18n: "tag" is a keyword
642 642 _('the argument to tag must be a string'))
643 643 s = set([cl.rev(n) for t, n in repo.tagslist() if t == tn])
644 644 else:
645 645 s = set([cl.rev(n) for t, n in repo.tagslist() if t != 'tip'])
646 646 return [r for r in subset if r in s]
647 647
648 648 def tagged(repo, subset, x):
649 649 return tag(repo, subset, x)
650 650
651 651 symbols = {
652 652 "adds": adds,
653 653 "all": getall,
654 654 "ancestor": ancestor,
655 655 "ancestors": ancestors,
656 656 "author": author,
657 657 "branch": branch,
658 658 "children": children,
659 659 "closed": closed,
660 660 "contains": contains,
661 661 "date": date,
662 662 "descendants": descendants,
663 663 "file": hasfile,
664 664 "follow": follow,
665 665 "grep": grep,
666 666 "head": head,
667 667 "heads": heads,
668 668 "keyword": keyword,
669 669 "limit": limit,
670 670 "max": maxrev,
671 671 "min": minrev,
672 672 "merge": merge,
673 673 "modifies": modifies,
674 674 "id": node,
675 675 "outgoing": outgoing,
676 676 "p1": p1,
677 677 "p2": p2,
678 678 "parents": parents,
679 679 "present": present,
680 680 "removes": removes,
681 681 "reverse": reverse,
682 682 "rev": rev,
683 683 "roots": roots,
684 684 "sort": sort,
685 685 "tag": tag,
686 686 "tagged": tagged,
687 687 "user": user,
688 688 }
689 689
690 690 methods = {
691 691 "range": rangeset,
692 692 "string": stringset,
693 693 "symbol": symbolset,
694 694 "and": andset,
695 695 "or": orset,
696 696 "not": notset,
697 697 "list": listset,
698 698 "func": func,
699 699 }
700 700
701 701 def optimize(x, small):
702 702 if x == None:
703 703 return 0, x
704 704
705 705 smallbonus = 1
706 706 if small:
707 707 smallbonus = .5
708 708
709 709 op = x[0]
710 710 if op == 'minus':
711 711 return optimize(('and', x[1], ('not', x[2])), small)
712 712 elif op == 'dagrange':
713 713 return optimize(('and', ('func', ('symbol', 'descendants'), x[1]),
714 714 ('func', ('symbol', 'ancestors'), x[2])), small)
715 715 elif op == 'dagrangepre':
716 716 return optimize(('func', ('symbol', 'ancestors'), x[1]), small)
717 717 elif op == 'dagrangepost':
718 718 return optimize(('func', ('symbol', 'descendants'), x[1]), small)
719 719 elif op == 'rangepre':
720 720 return optimize(('range', ('string', '0'), x[1]), small)
721 721 elif op == 'rangepost':
722 722 return optimize(('range', x[1], ('string', 'tip')), small)
723 723 elif op == 'negate':
724 724 return optimize(('string',
725 725 '-' + getstring(x[1], _("can't negate that"))), small)
726 726 elif op in 'string symbol negate':
727 727 return smallbonus, x # single revisions are small
728 728 elif op == 'and' or op == 'dagrange':
729 729 wa, ta = optimize(x[1], True)
730 730 wb, tb = optimize(x[2], True)
731 731 w = min(wa, wb)
732 732 if wa > wb:
733 733 return w, (op, tb, ta)
734 734 return w, (op, ta, tb)
735 735 elif op == 'or':
736 736 wa, ta = optimize(x[1], False)
737 737 wb, tb = optimize(x[2], False)
738 738 if wb < wa:
739 739 wb, wa = wa, wb
740 740 return max(wa, wb), (op, ta, tb)
741 741 elif op == 'not':
742 742 o = optimize(x[1], not small)
743 743 return o[0], (op, o[1])
744 744 elif op == 'group':
745 745 return optimize(x[1], small)
746 746 elif op in 'range list':
747 747 wa, ta = optimize(x[1], small)
748 748 wb, tb = optimize(x[2], small)
749 749 return wa + wb, (op, ta, tb)
750 750 elif op == 'func':
751 751 f = getstring(x[1], _("not a symbol"))
752 752 wa, ta = optimize(x[2], small)
753 753 if f in "grep date user author keyword branch file outgoing":
754 754 w = 10 # slow
755 755 elif f in "modifies adds removes":
756 756 w = 30 # slower
757 757 elif f == "contains":
758 758 w = 100 # very slow
759 759 elif f == "ancestor":
760 760 w = 1 * smallbonus
761 761 elif f == "reverse limit":
762 762 w = 0
763 763 elif f in "sort":
764 764 w = 10 # assume most sorts look at changelog
765 765 else:
766 766 w = 1
767 767 return w + wa, (op, x[1], ta)
768 768 return 1, x
769 769
770 770 parse = parser.parser(tokenize, elements).parse
771 771
772 772 def match(spec):
773 773 if not spec:
774 774 raise error.ParseError(_("empty query"))
775 775 tree = parse(spec)
776 776 weight, tree = optimize(tree, True)
777 777 def mfunc(repo, subset):
778 778 return getset(repo, subset, tree)
779 779 return mfunc
780 780
781 781 def makedoc(topic, doc):
782 782 """Generate and include predicates help in revsets topic."""
783 783 predicates = []
784 784 for name in sorted(symbols):
785 785 text = symbols[name].__doc__
786 786 if not text:
787 787 continue
788 788 lines = text.splitlines()
789 789 lines[1:] = [(' ' + l.strip()) for l in lines[1:]]
790 790 predicates.append('\n'.join(lines))
791 791 predicates = '\n'.join(predicates)
792 792 doc = doc.replace('.. predicatesmarker', predicates)
793 793 return doc
794
795 # tell hggettext to extract docstrings from these functions:
796 i18nfunctions = symbols.values()
General Comments 0
You need to be logged in to leave comments. Login now