##// END OF EJS Templates
Fix and unify transplant and bookmarks revsets doc registration
Patrick Mezard -
r12822:f13acb96 stable
parent child Browse files
Show More
@@ -1,570 +1,568 b''
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 """``bookmark([name])``
542 The named bookmark or all bookmarks.
543 """
541 544 args = revset.getargs(x, 0, 1, _('bookmark takes one or no arguments'))
542 545 if args:
543 546 bm = revset.getstring(args[0],
544 547 _('the argument to bookmark must be a string'))
545 548 bmrev = listbookmarks(repo).get(bm, None)
546 549 if bmrev:
547 550 bmrev = repo.changelog.rev(bin(bmrev))
548 551 return [r for r in subset if r == bmrev]
549 552 bms = set([repo.changelog.rev(bin(r)) for r in listbookmarks(repo).values()])
550 553 return [r for r in subset if r in bms]
554
555 def extsetup(ui):
551 556 revset.symbols['bookmark'] = bmrevset
552 557
553 def revsetdoc():
554 doc = help.loaddoc('revsets')()
555 doc += _('\nAdded by the bookmarks extension:\n\n'
556 '``bookmark([name])``\n'
557 ' The named bookmark or all bookmarks.\n')
558 return doc
559
560 558 cmdtable = {
561 559 "bookmarks":
562 560 (bookmark,
563 561 [('f', 'force', False, _('force')),
564 562 ('r', 'rev', '', _('revision'), _('REV')),
565 563 ('d', 'delete', False, _('delete a given bookmark')),
566 564 ('m', 'rename', '', _('rename a given bookmark'), _('NAME'))],
567 565 _('hg bookmarks [-f] [-d] [-m NAME] [-r REV] [NAME]')),
568 566 }
569 567
570 568 colortable = {'bookmarks.current': 'green'}
@@ -1,634 +1,627 b''
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 from mercurial import revset, help
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 """``transplanted(set)``
593 Transplanted changesets in set.
594 """
592 595 if x:
593 596 s = revset.getset(repo, subset, x)
594 597 else:
595 598 s = subset
596 599 cs = set()
597 600 for r in xrange(0, len(repo)):
598 601 if repo[r].extra().get('transplant_source'):
599 602 cs.add(r)
600 603 return [r for r in s if r in cs]
601 604
602 def revsetdoc():
603 doc = help.loaddoc('revsets')()
604 doc += _('\nAdded by the transplant extension:\n\n'
605 '``transplanted(set)``\n'
606 ' Transplanted changesets in set.\n')
607 return doc
608
609 def uisetup(ui):
610 'Add the transplanted revset predicate'
611 for i in (i for i, x in enumerate(help.helptable) if x[0] == ['revsets']):
612 help.helptable[i] = (['revsets'], _("Specifying Revision Sets"), revsetdoc)
605 def extsetup(ui):
613 606 revset.symbols['transplanted'] = revsettransplanted
614 607
615 608 cmdtable = {
616 609 "transplant":
617 610 (transplant,
618 611 [('s', 'source', '',
619 612 _('pull patches from REPO'), _('REPO')),
620 613 ('b', 'branch', [],
621 614 _('pull patches from branch BRANCH'), _('BRANCH')),
622 615 ('a', 'all', None, _('pull all changesets up to BRANCH')),
623 616 ('p', 'prune', [],
624 617 _('skip over REV'), _('REV')),
625 618 ('m', 'merge', [],
626 619 _('merge at REV'), _('REV')),
627 620 ('', 'log', None, _('append transplant info to log message')),
628 621 ('c', 'continue', None, _('continue last transplant session '
629 622 'after repair')),
630 623 ('', 'filter', '',
631 624 _('filter changesets through command'), _('CMD'))],
632 625 _('hg transplant [-s REPO] [-b BRANCH [-a]] [-p REV] '
633 626 '[-m REV] [REV]...'))
634 627 }
@@ -1,211 +1,213 b''
1 1 $ echo "[extensions]" >> $HGRCPATH
2 2 $ echo "bookmarks=" >> $HGRCPATH
3 3
4 4 $ hg init
5 5
6 6 no bookmarks
7 7
8 8 $ hg bookmarks
9 9 no bookmarks set
10 10
11 11 bookmark rev -1
12 12
13 13 $ hg bookmark X
14 14
15 15 list bookmarks
16 16
17 17 $ hg bookmarks
18 18 * X -1:000000000000
19 19
20 20 list bookmarks with color
21 21
22 22 $ hg --config extensions.color= --config color.mode=ansi \
23 23 > bookmarks --color=always
24 24  * X -1:000000000000
25 25
26 26 $ echo a > a
27 27 $ hg add a
28 28 $ hg commit -m 0
29 29
30 30 bookmark X moved to rev 0
31 31
32 32 $ hg bookmarks
33 33 * X 0:f7b1eb17ad24
34 34
35 35 look up bookmark
36 36
37 37 $ hg log -r X
38 38 changeset: 0:f7b1eb17ad24
39 39 tag: X
40 40 tag: tip
41 41 user: test
42 42 date: Thu Jan 01 00:00:00 1970 +0000
43 43 summary: 0
44 44
45 45
46 46 second bookmark for rev 0
47 47
48 48 $ hg bookmark X2
49 49
50 50 bookmark rev -1 again
51 51
52 52 $ hg bookmark -r null Y
53 53
54 54 list bookmarks
55 55
56 56 $ hg bookmarks
57 57 * X2 0:f7b1eb17ad24
58 58 * X 0:f7b1eb17ad24
59 59 Y -1:000000000000
60 60
61 61 $ echo b > b
62 62 $ hg add b
63 63 $ hg commit -m 1
64 64
65 65 bookmarks revset
66 66
67 67 $ hg log -r 'bookmark()'
68 68 changeset: 1:925d80f479bb
69 69 tag: X
70 70 tag: X2
71 71 tag: tip
72 72 user: test
73 73 date: Thu Jan 01 00:00:00 1970 +0000
74 74 summary: 1
75 75
76 76 $ hg log -r 'bookmark(Y)'
77 77 $ hg log -r 'bookmark(X2)'
78 78 changeset: 1:925d80f479bb
79 79 tag: X
80 80 tag: X2
81 81 tag: tip
82 82 user: test
83 83 date: Thu Jan 01 00:00:00 1970 +0000
84 84 summary: 1
85 85
86 $ hg help revsets | grep 'bookmark('
87 "bookmark([name])"
86 88
87 89 bookmarks X and X2 moved to rev 1, Y at rev -1
88 90
89 91 $ hg bookmarks
90 92 * X2 1:925d80f479bb
91 93 * X 1:925d80f479bb
92 94 Y -1:000000000000
93 95
94 96 bookmark rev 0 again
95 97
96 98 $ hg bookmark -r 0 Z
97 99
98 100 $ echo c > c
99 101 $ hg add c
100 102 $ hg commit -m 2
101 103
102 104 bookmarks X and X2 moved to rev 2, Y at rev -1, Z at rev 0
103 105
104 106 $ hg bookmarks
105 107 * X2 2:0316ce92851d
106 108 * X 2:0316ce92851d
107 109 Z 0:f7b1eb17ad24
108 110 Y -1:000000000000
109 111
110 112 rename nonexistent bookmark
111 113
112 114 $ hg bookmark -m A B
113 115 abort: a bookmark of this name does not exist
114 116 [255]
115 117
116 118 rename to existent bookmark
117 119
118 120 $ hg bookmark -m X Y
119 121 abort: a bookmark of the same name already exists
120 122 [255]
121 123
122 124 force rename to existent bookmark
123 125
124 126 $ hg bookmark -f -m X Y
125 127
126 128 list bookmarks
127 129
128 130 $ hg bookmark
129 131 * X2 2:0316ce92851d
130 132 * Y 2:0316ce92851d
131 133 Z 0:f7b1eb17ad24
132 134
133 135 rename without new name
134 136
135 137 $ hg bookmark -m Y
136 138 abort: new bookmark name required
137 139 [255]
138 140
139 141 delete without name
140 142
141 143 $ hg bookmark -d
142 144 abort: bookmark name required
143 145 [255]
144 146
145 147 delete nonexistent bookmark
146 148
147 149 $ hg bookmark -d A
148 150 abort: a bookmark of this name does not exist
149 151 [255]
150 152
151 153 bookmark name with spaces should be stripped
152 154
153 155 $ hg bookmark ' x y '
154 156
155 157 list bookmarks
156 158
157 159 $ hg bookmarks
158 160 * X2 2:0316ce92851d
159 161 * Y 2:0316ce92851d
160 162 Z 0:f7b1eb17ad24
161 163 * x y 2:0316ce92851d
162 164
163 165 look up stripped bookmark name
164 166
165 167 $ hg log -r '"x y"'
166 168 changeset: 2:0316ce92851d
167 169 tag: X2
168 170 tag: Y
169 171 tag: tip
170 172 tag: x y
171 173 user: test
172 174 date: Thu Jan 01 00:00:00 1970 +0000
173 175 summary: 2
174 176
175 177
176 178 reject bookmark name with newline
177 179
178 180 $ hg bookmark '
179 181 > '
180 182 abort: bookmark name cannot contain newlines
181 183 [255]
182 184
183 185 bookmark with existing name
184 186
185 187 $ hg bookmark Z
186 188 abort: a bookmark of the same name already exists
187 189 [255]
188 190
189 191 force bookmark with existing name
190 192
191 193 $ hg bookmark -f Z
192 194
193 195 list bookmarks
194 196
195 197 $ hg bookmark
196 198 * X2 2:0316ce92851d
197 199 * Y 2:0316ce92851d
198 200 * Z 2:0316ce92851d
199 201 * x y 2:0316ce92851d
200 202
201 203 revision but no bookmark name
202 204
203 205 $ hg bookmark -r .
204 206 abort: bookmark name required
205 207 [255]
206 208
207 209 bookmark name with whitespace only
208 210
209 211 $ hg bookmark ' '
210 212 abort: bookmark names cannot consist entirely of whitespace
211 213 [255]
General Comments 0
You need to be logged in to leave comments. Login now