1# Mercurial bookmark support code 2# 3# Copyright 2008 David Soria Parra <dsp@php.net> 4# 5# This software may be used and distributed according to the terms of the 6# GNU General Public License version 2 or any later version. 7 8from __future__ import absolute_import 9 10import errno 11import struct 12 13from .i18n import _ 14from .node import ( 15 bin, 16 hex, 17 short, 18) 19from .pycompat import getattr 20from . import ( 21 encoding, 22 error, 23 obsutil, 24 pycompat, 25 scmutil, 26 txnutil, 27 util, 28) 29from .utils import ( 30 urlutil, 31) 32 33# label constants 34# until 3.5, bookmarks.current was the advertised name, not 35# bookmarks.active, so we must use both to avoid breaking old 36# custom styles 37activebookmarklabel = b'bookmarks.active bookmarks.current' 38 39BOOKMARKS_IN_STORE_REQUIREMENT = b'bookmarksinstore' 40 41 42def bookmarksinstore(repo): 43 return BOOKMARKS_IN_STORE_REQUIREMENT in repo.requirements 44 45 46def bookmarksvfs(repo): 47 return repo.svfs if bookmarksinstore(repo) else repo.vfs 48 49 50def _getbkfile(repo): 51 """Hook so that extensions that mess with the store can hook bm storage. 52 53 For core, this just handles wether we should see pending 54 bookmarks or the committed ones. Other extensions (like share) 55 may need to tweak this behavior further. 56 """ 57 fp, pending = txnutil.trypending( 58 repo.root, bookmarksvfs(repo), b'bookmarks' 59 ) 60 return fp 61 62 63class bmstore(object): 64 r"""Storage for bookmarks. 65 66 This object should do all bookmark-related reads and writes, so 67 that it's fairly simple to replace the storage underlying 68 bookmarks without having to clone the logic surrounding 69 bookmarks. This type also should manage the active bookmark, if 70 any. 71 72 This particular bmstore implementation stores bookmarks as 73 {hash}\s{name}\n (the same format as localtags) in 74 .hg/bookmarks. The mapping is stored as {name: nodeid}. 75 """ 76 77 def __init__(self, repo): 78 self._repo = repo 79 self._refmap = refmap = {} # refspec: node 80 self._nodemap = nodemap = {} # node: sorted([refspec, ...]) 81 self._clean = True 82 self._aclean = True 83 has_node = repo.changelog.index.has_node 84 tonode = bin # force local lookup 85 try: 86 with _getbkfile(repo) as bkfile: 87 for line in bkfile: 88 line = line.strip() 89 if not line: 90 continue 91 try: 92 sha, refspec = line.split(b' ', 1) 93 node = tonode(sha) 94 if has_node(node): 95 refspec = encoding.tolocal(refspec) 96 refmap[refspec] = node 97 nrefs = nodemap.get(node) 98 if nrefs is None: 99 nodemap[node] = [refspec] 100 else: 101 nrefs.append(refspec) 102 if nrefs[-2] > refspec: 103 # bookmarks weren't sorted before 4.5 104 nrefs.sort() 105 except (TypeError, ValueError): 106 # TypeError: 107 # - bin(...) 108 # ValueError: 109 # - node in nm, for non-20-bytes entry 110 # - split(...), for string without ' ' 111 bookmarkspath = b'.hg/bookmarks' 112 if bookmarksinstore(repo): 113 bookmarkspath = b'.hg/store/bookmarks' 114 repo.ui.warn( 115 _(b'malformed line in %s: %r\n') 116 % (bookmarkspath, pycompat.bytestr(line)) 117 ) 118 except IOError as inst: 119 if inst.errno != errno.ENOENT: 120 raise 121 self._active = _readactive(repo, self) 122 123 @property 124 def active(self): 125 return self._active 126 127 @active.setter 128 def active(self, mark): 129 if mark is not None and mark not in self._refmap: 130 raise AssertionError(b'bookmark %s does not exist!' % mark) 131 132 self._active = mark 133 self._aclean = False 134 135 def __len__(self): 136 return len(self._refmap) 137 138 def __iter__(self): 139 return iter(self._refmap) 140 141 def iteritems(self): 142 return pycompat.iteritems(self._refmap) 143 144 def items(self): 145 return self._refmap.items() 146 147 # TODO: maybe rename to allnames()? 148 def keys(self): 149 return self._refmap.keys() 150 151 # TODO: maybe rename to allnodes()? but nodes would have to be deduplicated 152 # could be self._nodemap.keys() 153 def values(self): 154 return self._refmap.values() 155 156 def __contains__(self, mark): 157 return mark in self._refmap 158 159 def __getitem__(self, mark): 160 return self._refmap[mark] 161 162 def get(self, mark, default=None): 163 return self._refmap.get(mark, default) 164 165 def _set(self, mark, node): 166 self._clean = False 167 if mark in self._refmap: 168 self._del(mark) 169 self._refmap[mark] = node 170 nrefs = self._nodemap.get(node) 171 if nrefs is None: 172 self._nodemap[node] = [mark] 173 else: 174 nrefs.append(mark) 175 nrefs.sort() 176 177 def _del(self, mark): 178 if mark not in self._refmap: 179 return 180 self._clean = False 181 node = self._refmap.pop(mark) 182 nrefs = self._nodemap[node] 183 if len(nrefs) == 1: 184 assert nrefs[0] == mark 185 del self._nodemap[node] 186 else: 187 nrefs.remove(mark) 188 189 def names(self, node): 190 """Return a sorted list of bookmarks pointing to the specified node""" 191 return self._nodemap.get(node, []) 192 193 def applychanges(self, repo, tr, changes): 194 """Apply a list of changes to bookmarks""" 195 bmchanges = tr.changes.get(b'bookmarks') 196 for name, node in changes: 197 old = self._refmap.get(name) 198 if node is None: 199 self._del(name) 200 else: 201 self._set(name, node) 202 if bmchanges is not None: 203 # if a previous value exist preserve the "initial" value 204 previous = bmchanges.get(name) 205 if previous is not None: 206 old = previous[0] 207 bmchanges[name] = (old, node) 208 self._recordchange(tr) 209 210 def _recordchange(self, tr): 211 """record that bookmarks have been changed in a transaction 212 213 The transaction is then responsible for updating the file content.""" 214 location = b'' if bookmarksinstore(self._repo) else b'plain' 215 tr.addfilegenerator( 216 b'bookmarks', (b'bookmarks',), self._write, location=location 217 ) 218 tr.hookargs[b'bookmark_moved'] = b'1' 219 220 def _writerepo(self, repo): 221 """Factored out for extensibility""" 222 rbm = repo._bookmarks 223 if rbm.active not in self._refmap: 224 rbm.active = None 225 rbm._writeactive() 226 227 if bookmarksinstore(repo): 228 vfs = repo.svfs 229 lock = repo.lock() 230 else: 231 vfs = repo.vfs 232 lock = repo.wlock() 233 with lock: 234 with vfs(b'bookmarks', b'w', atomictemp=True, checkambig=True) as f: 235 self._write(f) 236 237 def _writeactive(self): 238 if self._aclean: 239 return 240 with self._repo.wlock(): 241 if self._active is not None: 242 with self._repo.vfs( 243 b'bookmarks.current', b'w', atomictemp=True, checkambig=True 244 ) as f: 245 f.write(encoding.fromlocal(self._active)) 246 else: 247 self._repo.vfs.tryunlink(b'bookmarks.current') 248 self._aclean = True 249 250 def _write(self, fp): 251 for name, node in sorted(pycompat.iteritems(self._refmap)): 252 fp.write(b"%s %s\n" % (hex(node), encoding.fromlocal(name))) 253 self._clean = True 254 self._repo.invalidatevolatilesets() 255 256 def expandname(self, bname): 257 if bname == b'.': 258 if self.active: 259 return self.active 260 else: 261 raise error.RepoLookupError(_(b"no active bookmark")) 262 return bname 263 264 def checkconflict(self, mark, force=False, target=None): 265 """check repo for a potential clash of mark with an existing bookmark, 266 branch, or hash 267 268 If target is supplied, then check that we are moving the bookmark 269 forward. 270 271 If force is supplied, then forcibly move the bookmark to a new commit 272 regardless if it is a move forward. 273 274 If divergent bookmark are to be deleted, they will be returned as list. 275 """ 276 cur = self._repo[b'.'].node() 277 if mark in self._refmap and not force: 278 if target: 279 if self._refmap[mark] == target and target == cur: 280 # re-activating a bookmark 281 return [] 282 rev = self._repo[target].rev() 283 anc = self._repo.changelog.ancestors([rev]) 284 bmctx = self._repo[self[mark]] 285 divs = [ 286 self._refmap[b] 287 for b in self._refmap 288 if b.split(b'@', 1)[0] == mark.split(b'@', 1)[0] 289 ] 290 291 # allow resolving a single divergent bookmark even if moving 292 # the bookmark across branches when a revision is specified 293 # that contains a divergent bookmark 294 if bmctx.rev() not in anc and target in divs: 295 return divergent2delete(self._repo, [target], mark) 296 297 deletefrom = [ 298 b for b in divs if self._repo[b].rev() in anc or b == target 299 ] 300 delbms = divergent2delete(self._repo, deletefrom, mark) 301 if validdest(self._repo, bmctx, self._repo[target]): 302 self._repo.ui.status( 303 _(b"moving bookmark '%s' forward from %s\n") 304 % (mark, short(bmctx.node())) 305 ) 306 return delbms 307 raise error.Abort( 308 _(b"bookmark '%s' already exists (use -f to force)") % mark 309 ) 310 if ( 311 mark in self._repo.branchmap() 312 or mark == self._repo.dirstate.branch() 313 ) and not force: 314 raise error.Abort( 315 _(b"a bookmark cannot have the name of an existing branch") 316 ) 317 if len(mark) > 3 and not force: 318 try: 319 shadowhash = scmutil.isrevsymbol(self._repo, mark) 320 except error.LookupError: # ambiguous identifier 321 shadowhash = False 322 if shadowhash: 323 self._repo.ui.warn( 324 _( 325 b"bookmark %s matches a changeset hash\n" 326 b"(did you leave a -r out of an 'hg bookmark' " 327 b"command?)\n" 328 ) 329 % mark 330 ) 331 return [] 332 333 334def _readactive(repo, marks): 335 """ 336 Get the active bookmark. We can have an active bookmark that updates 337 itself as we commit. This function returns the name of that bookmark. 338 It is stored in .hg/bookmarks.current 339 """ 340 # No readline() in osutil.posixfile, reading everything is 341 # cheap. 342 content = repo.vfs.tryread(b'bookmarks.current') 343 mark = encoding.tolocal((content.splitlines() or [b''])[0]) 344 if mark == b'' or mark not in marks: 345 mark = None 346 return mark 347 348 349def activate(repo, mark): 350 """ 351 Set the given bookmark to be 'active', meaning that this bookmark will 352 follow new commits that are made. 353 The name is recorded in .hg/bookmarks.current 354 """ 355 repo._bookmarks.active = mark 356 repo._bookmarks._writeactive() 357 358 359def deactivate(repo): 360 """ 361 Unset the active bookmark in this repository. 362 """ 363 repo._bookmarks.active = None 364 repo._bookmarks._writeactive() 365 366 367def isactivewdirparent(repo): 368 """ 369 Tell whether the 'active' bookmark (the one that follows new commits) 370 points to one of the parents of the current working directory (wdir). 371 372 While this is normally the case, it can on occasion be false; for example, 373 immediately after a pull, the active bookmark can be moved to point 374 to a place different than the wdir. This is solved by running `hg update`. 375 """ 376 mark = repo._activebookmark 377 marks = repo._bookmarks 378 parents = [p.node() for p in repo[None].parents()] 379 return mark in marks and marks[mark] in parents 380 381 382def divergent2delete(repo, deletefrom, bm): 383 """find divergent versions of bm on nodes in deletefrom. 384 385 the list of bookmark to delete.""" 386 todelete = [] 387 marks = repo._bookmarks 388 divergent = [ 389 b for b in marks if b.split(b'@', 1)[0] == bm.split(b'@', 1)[0] 390 ] 391 for mark in divergent: 392 if mark == b'@' or b'@' not in mark: 393 # can't be divergent by definition 394 continue 395 if mark and marks[mark] in deletefrom: 396 if mark != bm: 397 todelete.append(mark) 398 return todelete 399 400 401def headsforactive(repo): 402 """Given a repo with an active bookmark, return divergent bookmark nodes. 403 404 Args: 405 repo: A repository with an active bookmark. 406 407 Returns: 408 A list of binary node ids that is the full list of other 409 revisions with bookmarks divergent from the active bookmark. If 410 there were no divergent bookmarks, then this list will contain 411 only one entry. 412 """ 413 if not repo._activebookmark: 414 raise ValueError( 415 b'headsforactive() only makes sense with an active bookmark' 416 ) 417 name = repo._activebookmark.split(b'@', 1)[0] 418 heads = [] 419 for mark, n in pycompat.iteritems(repo._bookmarks): 420 if mark.split(b'@', 1)[0] == name: 421 heads.append(n) 422 return heads 423 424 425def calculateupdate(ui, repo): 426 """Return a tuple (activemark, movemarkfrom) indicating the active bookmark 427 and where to move the active bookmark from, if needed.""" 428 checkout, movemarkfrom = None, None 429 activemark = repo._activebookmark 430 if isactivewdirparent(repo): 431 movemarkfrom = repo[b'.'].node() 432 elif activemark: 433 ui.status(_(b"updating to active bookmark %s\n") % activemark) 434 checkout = activemark 435 return (checkout, movemarkfrom) 436 437 438def update(repo, parents, node): 439 deletefrom = parents 440 marks = repo._bookmarks 441 active = marks.active 442 if not active: 443 return False 444 445 bmchanges = [] 446 if marks[active] in parents: 447 new = repo[node] 448 divs = [ 449 repo[marks[b]] 450 for b in marks 451 if b.split(b'@', 1)[0] == active.split(b'@', 1)[0] 452 ] 453 anc = repo.changelog.ancestors([new.rev()]) 454 deletefrom = [b.node() for b in divs if b.rev() in anc or b == new] 455 if validdest(repo, repo[marks[active]], new): 456 bmchanges.append((active, new.node())) 457 458 for bm in divergent2delete(repo, deletefrom, active): 459 bmchanges.append((bm, None)) 460 461 if bmchanges: 462 with repo.lock(), repo.transaction(b'bookmark') as tr: 463 marks.applychanges(repo, tr, bmchanges) 464 return bool(bmchanges) 465 466 467def isdivergent(b): 468 return b'@' in b and not b.endswith(b'@') 469 470 471def listbinbookmarks(repo): 472 # We may try to list bookmarks on a repo type that does not 473 # support it (e.g., statichttprepository). 474 marks = getattr(repo, '_bookmarks', {}) 475 476 hasnode = repo.changelog.hasnode 477 for k, v in pycompat.iteritems(marks): 478 # don't expose local divergent bookmarks 479 if hasnode(v) and not isdivergent(k): 480 yield k, v 481 482 483def listbookmarks(repo): 484 d = {} 485 for book, node in listbinbookmarks(repo): 486 d[book] = hex(node) 487 return d 488 489 490def pushbookmark(repo, key, old, new): 491 if isdivergent(key): 492 return False 493 if bookmarksinstore(repo): 494 wlock = util.nullcontextmanager() 495 else: 496 wlock = repo.wlock() 497 with wlock, repo.lock(), repo.transaction(b'bookmarks') as tr: 498 marks = repo._bookmarks 499 existing = hex(marks.get(key, b'')) 500 if existing != old and existing != new: 501 return False 502 if new == b'': 503 changes = [(key, None)] 504 else: 505 if new not in repo: 506 return False 507 changes = [(key, repo[new].node())] 508 marks.applychanges(repo, tr, changes) 509 return True 510 511 512def comparebookmarks(repo, srcmarks, dstmarks, targets=None): 513 """Compare bookmarks between srcmarks and dstmarks 514 515 This returns tuple "(addsrc, adddst, advsrc, advdst, diverge, 516 differ, invalid)", each are list of bookmarks below: 517 518 :addsrc: added on src side (removed on dst side, perhaps) 519 :adddst: added on dst side (removed on src side, perhaps) 520 :advsrc: advanced on src side 521 :advdst: advanced on dst side 522 :diverge: diverge 523 :differ: changed, but changeset referred on src is unknown on dst 524 :invalid: unknown on both side 525 :same: same on both side 526 527 Each elements of lists in result tuple is tuple "(bookmark name, 528 changeset ID on source side, changeset ID on destination 529 side)". Each changeset ID is a binary node or None. 530 531 Changeset IDs of tuples in "addsrc", "adddst", "differ" or 532 "invalid" list may be unknown for repo. 533 534 If "targets" is specified, only bookmarks listed in it are 535 examined. 536 """ 537 538 if targets: 539 bset = set(targets) 540 else: 541 srcmarkset = set(srcmarks) 542 dstmarkset = set(dstmarks) 543 bset = srcmarkset | dstmarkset 544 545 results = ([], [], [], [], [], [], [], []) 546 addsrc = results[0].append 547 adddst = results[1].append 548 advsrc = results[2].append 549 advdst = results[3].append 550 diverge = results[4].append 551 differ = results[5].append 552 invalid = results[6].append 553 same = results[7].append 554 555 for b in sorted(bset): 556 if b not in srcmarks: 557 if b in dstmarks: 558 adddst((b, None, dstmarks[b])) 559 else: 560 invalid((b, None, None)) 561 elif b not in dstmarks: 562 addsrc((b, srcmarks[b], None)) 563 else: 564 scid = srcmarks[b] 565 dcid = dstmarks[b] 566 if scid == dcid: 567 same((b, scid, dcid)) 568 elif scid in repo and dcid in repo: 569 sctx = repo[scid] 570 dctx = repo[dcid] 571 if sctx.rev() < dctx.rev(): 572 if validdest(repo, sctx, dctx): 573 advdst((b, scid, dcid)) 574 else: 575 diverge((b, scid, dcid)) 576 else: 577 if validdest(repo, dctx, sctx): 578 advsrc((b, scid, dcid)) 579 else: 580 diverge((b, scid, dcid)) 581 else: 582 # it is too expensive to examine in detail, in this case 583 differ((b, scid, dcid)) 584 585 return results 586 587 588def _diverge(ui, b, path, localmarks, remotenode): 589 """Return appropriate diverged bookmark for specified ``path`` 590 591 This returns None, if it is failed to assign any divergent 592 bookmark name. 593 594 This reuses already existing one with "@number" suffix, if it 595 refers ``remotenode``. 596 """ 597 if b == b'@': 598 b = b'' 599 # try to use an @pathalias suffix 600 # if an @pathalias already exists, we overwrite (update) it 601 if path.startswith(b"file:"): 602 path = urlutil.url(path).path 603 for name, p in urlutil.list_paths(ui): 604 loc = p.rawloc 605 if loc.startswith(b"file:"): 606 loc = urlutil.url(loc).path 607 if path == loc: 608 return b'%s@%s' % (b, name) 609 610 # assign a unique "@number" suffix newly 611 for x in range(1, 100): 612 n = b'%s@%d' % (b, x) 613 if n not in localmarks or localmarks[n] == remotenode: 614 return n 615 616 return None 617 618 619def unhexlifybookmarks(marks): 620 binremotemarks = {} 621 for name, node in marks.items(): 622 binremotemarks[name] = bin(node) 623 return binremotemarks 624 625 626_binaryentry = struct.Struct(b'>20sH') 627 628 629def binaryencode(repo, bookmarks): 630 """encode a '(bookmark, node)' iterable into a binary stream 631 632 the binary format is: 633 634 <node><bookmark-length><bookmark-name> 635 636 :node: is a 20 bytes binary node, 637 :bookmark-length: an unsigned short, 638 :bookmark-name: the name of the bookmark (of length <bookmark-length>) 639 640 wdirid (all bits set) will be used as a special value for "missing" 641 """ 642 binarydata = [] 643 for book, node in bookmarks: 644 if not node: # None or '' 645 node = repo.nodeconstants.wdirid 646 binarydata.append(_binaryentry.pack(node, len(book))) 647 binarydata.append(book) 648 return b''.join(binarydata) 649 650 651def binarydecode(repo, stream): 652 """decode a binary stream into an '(bookmark, node)' iterable 653 654 the binary format is: 655 656 <node><bookmark-length><bookmark-name> 657 658 :node: is a 20 bytes binary node, 659 :bookmark-length: an unsigned short, 660 :bookmark-name: the name of the bookmark (of length <bookmark-length>)) 661 662 wdirid (all bits set) will be used as a special value for "missing" 663 """ 664 entrysize = _binaryentry.size 665 books = [] 666 while True: 667 entry = stream.read(entrysize) 668 if len(entry) < entrysize: 669 if entry: 670 raise error.Abort(_(b'bad bookmark stream')) 671 break 672 node, length = _binaryentry.unpack(entry) 673 bookmark = stream.read(length) 674 if len(bookmark) < length: 675 if entry: 676 raise error.Abort(_(b'bad bookmark stream')) 677 if node == repo.nodeconstants.wdirid: 678 node = None 679 books.append((bookmark, node)) 680 return books 681 682 683def mirroring_remote(ui, repo, remotemarks): 684 """computes the bookmark changes that set the local bookmarks to 685 remotemarks""" 686 changed = [] 687 localmarks = repo._bookmarks 688 for (b, id) in pycompat.iteritems(remotemarks): 689 if id != localmarks.get(b, None) and id in repo: 690 changed.append((b, id, ui.debug, _(b"updating bookmark %s\n") % b)) 691 for b in localmarks: 692 if b not in remotemarks: 693 changed.append( 694 (b, None, ui.debug, _(b"removing bookmark %s\n") % b) 695 ) 696 return changed 697 698 699def merging_from_remote(ui, repo, remotemarks, path, explicit=()): 700 """computes the bookmark changes that merge remote bookmarks into the 701 local bookmarks, based on comparebookmarks""" 702 localmarks = repo._bookmarks 703 ( 704 addsrc, 705 adddst, 706 advsrc, 707 advdst, 708 diverge, 709 differ, 710 invalid, 711 same, 712 ) = comparebookmarks(repo, remotemarks, localmarks) 713 714 status = ui.status 715 warn = ui.warn 716 if ui.configbool(b'ui', b'quietbookmarkmove'): 717 status = warn = ui.debug 718 719 explicit = set(explicit) 720 changed = [] 721 for b, scid, dcid in addsrc: 722 if scid in repo: # add remote bookmarks for changes we already have 723 changed.append( 724 (b, scid, status, _(b"adding remote bookmark %s\n") % b) 725 ) 726 elif b in explicit: 727 explicit.remove(b) 728 ui.warn( 729 _(b"remote bookmark %s points to locally missing %s\n") 730 % (b, hex(scid)[:12]) 731 ) 732 733 for b, scid, dcid in advsrc: 734 changed.append((b, scid, status, _(b"updating bookmark %s\n") % b)) 735 # remove normal movement from explicit set 736 explicit.difference_update(d[0] for d in changed) 737 738 for b, scid, dcid in diverge: 739 if b in explicit: 740 explicit.discard(b) 741 changed.append((b, scid, status, _(b"importing bookmark %s\n") % b)) 742 else: 743 db = _diverge(ui, b, path, localmarks, scid) 744 if db: 745 changed.append( 746 ( 747 db, 748 scid, 749 warn, 750 _(b"divergent bookmark %s stored as %s\n") % (b, db), 751 ) 752 ) 753 else: 754 warn( 755 _( 756 b"warning: failed to assign numbered name " 757 b"to divergent bookmark %s\n" 758 ) 759 % b 760 ) 761 for b, scid, dcid in adddst + advdst: 762 if b in explicit: 763 explicit.discard(b) 764 changed.append((b, scid, status, _(b"importing bookmark %s\n") % b)) 765 for b, scid, dcid in differ: 766 if b in explicit: 767 explicit.remove(b) 768 ui.warn( 769 _(b"remote bookmark %s points to locally missing %s\n") 770 % (b, hex(scid)[:12]) 771 ) 772 return changed 773 774 775def updatefromremote( 776 ui, repo, remotemarks, path, trfunc, explicit=(), mode=None 777): 778 if mode == b'ignore': 779 # This should move to an higher level to avoid fetching bookmark at all 780 return 781 ui.debug(b"checking for updated bookmarks\n") 782 if mode == b'mirror': 783 changed = mirroring_remote(ui, repo, remotemarks) 784 else: 785 changed = merging_from_remote(ui, repo, remotemarks, path, explicit) 786 787 if changed: 788 tr = trfunc() 789 changes = [] 790 key = lambda t: (t[0], t[1] or b'') 791 for b, node, writer, msg in sorted(changed, key=key): 792 changes.append((b, node)) 793 writer(msg) 794 repo._bookmarks.applychanges(repo, tr, changes) 795 796 797def incoming(ui, repo, peer, mode=None): 798 """Show bookmarks incoming from other to repo""" 799 if mode == b'ignore': 800 ui.status(_(b"bookmarks exchange disabled with this path\n")) 801 return 0 802 ui.status(_(b"searching for changed bookmarks\n")) 803 804 with peer.commandexecutor() as e: 805 remotemarks = unhexlifybookmarks( 806 e.callcommand( 807 b'listkeys', 808 { 809 b'namespace': b'bookmarks', 810 }, 811 ).result() 812 ) 813 814 incomings = [] 815 if ui.debugflag: 816 getid = lambda id: id 817 else: 818 getid = lambda id: id[:12] 819 if ui.verbose: 820 821 def add(b, id, st): 822 incomings.append(b" %-25s %s %s\n" % (b, getid(id), st)) 823 824 else: 825 826 def add(b, id, st): 827 incomings.append(b" %-25s %s\n" % (b, getid(id))) 828 829 if mode == b'mirror': 830 localmarks = repo._bookmarks 831 allmarks = set(remotemarks.keys()) | set(localmarks.keys()) 832 for b in sorted(allmarks): 833 loc = localmarks.get(b) 834 rem = remotemarks.get(b) 835 if loc == rem: 836 continue 837 elif loc is None: 838 add(b, hex(rem), _(b'added')) 839 elif rem is None: 840 add(b, hex(repo.nullid), _(b'removed')) 841 else: 842 add(b, hex(rem), _(b'changed')) 843 else: 844 r = comparebookmarks(repo, remotemarks, repo._bookmarks) 845 addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = r 846 847 for b, scid, dcid in addsrc: 848 # i18n: "added" refers to a bookmark 849 add(b, hex(scid), _(b'added')) 850 for b, scid, dcid in advsrc: 851 # i18n: "advanced" refers to a bookmark 852 add(b, hex(scid), _(b'advanced')) 853 for b, scid, dcid in diverge: 854 # i18n: "diverged" refers to a bookmark 855 add(b, hex(scid), _(b'diverged')) 856 for b, scid, dcid in differ: 857 # i18n: "changed" refers to a bookmark 858 add(b, hex(scid), _(b'changed')) 859 860 if not incomings: 861 ui.status(_(b"no changed bookmarks found\n")) 862 return 1 863 864 for s in sorted(incomings): 865 ui.write(s) 866 867 return 0 868 869 870def outgoing(ui, repo, other): 871 """Show bookmarks outgoing from repo to other""" 872 ui.status(_(b"searching for changed bookmarks\n")) 873 874 remotemarks = unhexlifybookmarks(other.listkeys(b'bookmarks')) 875 r = comparebookmarks(repo, repo._bookmarks, remotemarks) 876 addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = r 877 878 outgoings = [] 879 if ui.debugflag: 880 getid = lambda id: id 881 else: 882 getid = lambda id: id[:12] 883 if ui.verbose: 884 885 def add(b, id, st): 886 outgoings.append(b" %-25s %s %s\n" % (b, getid(id), st)) 887 888 else: 889 890 def add(b, id, st): 891 outgoings.append(b" %-25s %s\n" % (b, getid(id))) 892 893 for b, scid, dcid in addsrc: 894 # i18n: "added refers to a bookmark 895 add(b, hex(scid), _(b'added')) 896 for b, scid, dcid in adddst: 897 # i18n: "deleted" refers to a bookmark 898 add(b, b' ' * 40, _(b'deleted')) 899 for b, scid, dcid in advsrc: 900 # i18n: "advanced" refers to a bookmark 901 add(b, hex(scid), _(b'advanced')) 902 for b, scid, dcid in diverge: 903 # i18n: "diverged" refers to a bookmark 904 add(b, hex(scid), _(b'diverged')) 905 for b, scid, dcid in differ: 906 # i18n: "changed" refers to a bookmark 907 add(b, hex(scid), _(b'changed')) 908 909 if not outgoings: 910 ui.status(_(b"no changed bookmarks found\n")) 911 return 1 912 913 for s in sorted(outgoings): 914 ui.write(s) 915 916 return 0 917 918 919def summary(repo, peer): 920 """Compare bookmarks between repo and other for "hg summary" output 921 922 This returns "(# of incoming, # of outgoing)" tuple. 923 """ 924 with peer.commandexecutor() as e: 925 remotemarks = unhexlifybookmarks( 926 e.callcommand( 927 b'listkeys', 928 { 929 b'namespace': b'bookmarks', 930 }, 931 ).result() 932 ) 933 934 r = comparebookmarks(repo, remotemarks, repo._bookmarks) 935 addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = r 936 return (len(addsrc), len(adddst)) 937 938 939def validdest(repo, old, new): 940 """Is the new bookmark destination a valid update from the old one""" 941 repo = repo.unfiltered() 942 if old == new: 943 # Old == new -> nothing to update. 944 return False 945 elif not old: 946 # old is nullrev, anything is valid. 947 # (new != nullrev has been excluded by the previous check) 948 return True 949 elif repo.obsstore: 950 return new.node() in obsutil.foreground(repo, [old.node()]) 951 else: 952 # still an independent clause as it is lazier (and therefore faster) 953 return old.isancestorof(new) 954 955 956def checkformat(repo, mark): 957 """return a valid version of a potential bookmark name 958 959 Raises an abort error if the bookmark name is not valid. 960 """ 961 mark = mark.strip() 962 if not mark: 963 raise error.InputError( 964 _(b"bookmark names cannot consist entirely of whitespace") 965 ) 966 scmutil.checknewlabel(repo, mark, b'bookmark') 967 return mark 968 969 970def delete(repo, tr, names): 971 """remove a mark from the bookmark store 972 973 Raises an abort error if mark does not exist. 974 """ 975 marks = repo._bookmarks 976 changes = [] 977 for mark in names: 978 if mark not in marks: 979 raise error.InputError(_(b"bookmark '%s' does not exist") % mark) 980 if mark == repo._activebookmark: 981 deactivate(repo) 982 changes.append((mark, None)) 983 marks.applychanges(repo, tr, changes) 984 985 986def rename(repo, tr, old, new, force=False, inactive=False): 987 """rename a bookmark from old to new 988 989 If force is specified, then the new name can overwrite an existing 990 bookmark. 991 992 If inactive is specified, then do not activate the new bookmark. 993 994 Raises an abort error if old is not in the bookmark store. 995 """ 996 marks = repo._bookmarks 997 mark = checkformat(repo, new) 998 if old not in marks: 999 raise error.InputError(_(b"bookmark '%s' does not exist") % old) 1000 changes = [] 1001 for bm in marks.checkconflict(mark, force): 1002 changes.append((bm, None)) 1003 changes.extend([(mark, marks[old]), (old, None)]) 1004 marks.applychanges(repo, tr, changes) 1005 if repo._activebookmark == old and not inactive: 1006 activate(repo, mark) 1007 1008 1009def addbookmarks(repo, tr, names, rev=None, force=False, inactive=False): 1010 """add a list of bookmarks 1011 1012 If force is specified, then the new name can overwrite an existing 1013 bookmark. 1014 1015 If inactive is specified, then do not activate any bookmark. Otherwise, the 1016 first bookmark is activated. 1017 1018 Raises an abort error if old is not in the bookmark store. 1019 """ 1020 marks = repo._bookmarks 1021 cur = repo[b'.'].node() 1022 newact = None 1023 changes = [] 1024 1025 # unhide revs if any 1026 if rev: 1027 repo = scmutil.unhidehashlikerevs(repo, [rev], b'nowarn') 1028 1029 ctx = scmutil.revsingle(repo, rev, None) 1030 # bookmarking wdir means creating a bookmark on p1 and activating it 1031 activatenew = not inactive and ctx.rev() is None 1032 if ctx.node() is None: 1033 ctx = ctx.p1() 1034 tgt = ctx.node() 1035 assert tgt 1036 1037 for mark in names: 1038 mark = checkformat(repo, mark) 1039 if newact is None: 1040 newact = mark 1041 if inactive and mark == repo._activebookmark: 1042 deactivate(repo) 1043 continue 1044 for bm in marks.checkconflict(mark, force, tgt): 1045 changes.append((bm, None)) 1046 changes.append((mark, tgt)) 1047 1048 # nothing changed but for the one deactivated above 1049 if not changes: 1050 return 1051 1052 if ctx.hidden(): 1053 repo.ui.warn(_(b"bookmarking hidden changeset %s\n") % ctx.hex()[:12]) 1054 1055 if ctx.obsolete(): 1056 msg = obsutil._getfilteredreason(repo, ctx.hex()[:12], ctx) 1057 repo.ui.warn(b"(%s)\n" % msg) 1058 1059 marks.applychanges(repo, tr, changes) 1060 if activatenew and cur == marks[newact]: 1061 activate(repo, newact) 1062 elif cur != tgt and newact == repo._activebookmark: 1063 deactivate(repo) 1064 1065 1066def _printbookmarks(ui, repo, fm, bmarks): 1067 """private method to print bookmarks 1068 1069 Provides a way for extensions to control how bookmarks are printed (e.g. 1070 prepend or postpend names) 1071 """ 1072 hexfn = fm.hexfunc 1073 if len(bmarks) == 0 and fm.isplain(): 1074 ui.status(_(b"no bookmarks set\n")) 1075 for bmark, (n, prefix, label) in sorted(pycompat.iteritems(bmarks)): 1076 fm.startitem() 1077 fm.context(repo=repo) 1078 if not ui.quiet: 1079 fm.plain(b' %s ' % prefix, label=label) 1080 fm.write(b'bookmark', b'%s', bmark, label=label) 1081 pad = b" " * (25 - encoding.colwidth(bmark)) 1082 fm.condwrite( 1083 not ui.quiet, 1084 b'rev node', 1085 pad + b' %d:%s', 1086 repo.changelog.rev(n), 1087 hexfn(n), 1088 label=label, 1089 ) 1090 fm.data(active=(activebookmarklabel in label)) 1091 fm.plain(b'\n') 1092 1093 1094def printbookmarks(ui, repo, fm, names=None): 1095 """print bookmarks by the given formatter 1096 1097 Provides a way for extensions to control how bookmarks are printed. 1098 """ 1099 marks = repo._bookmarks 1100 bmarks = {} 1101 for bmark in names or marks: 1102 if bmark not in marks: 1103 raise error.InputError(_(b"bookmark '%s' does not exist") % bmark) 1104 active = repo._activebookmark 1105 if bmark == active: 1106 prefix, label = b'*', activebookmarklabel 1107 else: 1108 prefix, label = b' ', b'' 1109 1110 bmarks[bmark] = (marks[bmark], prefix, label) 1111 _printbookmarks(ui, repo, fm, bmarks) 1112 1113 1114def preparehookargs(name, old, new): 1115 if new is None: 1116 new = b'' 1117 if old is None: 1118 old = b'' 1119 return {b'bookmark': name, b'node': hex(new), b'oldnode': hex(old)} 1120