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