bookmarks.py
1059 lines
| 32.6 KiB
| text/x-python
|
PythonLexer
/ mercurial / bookmarks.py
|
r13350 | # Mercurial bookmark support code | ||
# | ||||
# Copyright 2008 David Soria Parra <dsp@php.net> | ||||
# | ||||
# This software may be used and distributed according to the terms of the | ||||
# GNU General Public License version 2 or any later version. | ||||
|
r25917 | from __future__ import absolute_import | ||
import errno | ||||
|
r35258 | import struct | ||
|
r25917 | |||
from .i18n import _ | ||||
from .node import ( | ||||
bin, | ||||
hex, | ||||
|
r32956 | short, | ||
|
r35258 | wdirid, | ||
|
r25917 | ) | ||
|
r43359 | from .pycompat import getattr | ||
|
r25917 | from . import ( | ||
encoding, | ||||
|
r29354 | error, | ||
r33147 | obsutil, | |||
|
r33092 | pycompat, | ||
|
r32955 | scmutil, | ||
|
r31052 | txnutil, | ||
|
r25917 | util, | ||
) | ||||
|
r13350 | |||
|
r33009 | # label constants | ||
# until 3.5, bookmarks.current was the advertised name, not | ||||
# bookmarks.active, so we must use both to avoid breaking old | ||||
# custom styles | ||||
|
r43347 | activebookmarklabel = b'bookmarks.active bookmarks.current' | ||
|
r33009 | |||
|
r43347 | BOOKMARKS_IN_STORE_REQUIREMENT = b'bookmarksinstore' | ||
|
r42512 | |||
|
r43346 | |||
|
r42512 | def bookmarksinstore(repo): | ||
return BOOKMARKS_IN_STORE_REQUIREMENT in repo.requirements | ||||
|
r43346 | |||
|
r42512 | def bookmarksvfs(repo): | ||
return repo.svfs if bookmarksinstore(repo) else repo.vfs | ||||
|
r43346 | |||
|
r27186 | def _getbkfile(repo): | ||
"""Hook so that extensions that mess with the store can hook bm storage. | ||||
For core, this just handles wether we should see pending | ||||
bookmarks or the committed ones. Other extensions (like share) | ||||
may need to tweak this behavior further. | ||||
""" | ||||
|
r43347 | fp, pending = txnutil.trypending( | ||
repo.root, bookmarksvfs(repo), b'bookmarks' | ||||
) | ||||
|
r31052 | return fp | ||
|
r27186 | |||
|
r43346 | |||
|
r37866 | class bmstore(object): | ||
|
r41674 | r"""Storage for bookmarks. | ||
|
r17922 | |||
|
r27698 | This object should do all bookmark-related reads and writes, so | ||
that it's fairly simple to replace the storage underlying | ||||
bookmarks without having to clone the logic surrounding | ||||
bookmarks. This type also should manage the active bookmark, if | ||||
any. | ||||
|
r17922 | |||
This particular bmstore implementation stores bookmarks as | ||||
{hash}\s{name}\n (the same format as localtags) in | ||||
.hg/bookmarks. The mapping is stored as {name: nodeid}. | ||||
""" | ||||
|
r13351 | |||
|
r17922 | def __init__(self, repo): | ||
self._repo = repo | ||||
|
r37866 | self._refmap = refmap = {} # refspec: node | ||
|
r37869 | self._nodemap = nodemap = {} # node: sorted([refspec, ...]) | ||
r32738 | self._clean = True | |||
self._aclean = True | ||||
r43944 | has_node = repo.changelog.index.has_node | |||
|
r43346 | tonode = bin # force local lookup | ||
|
r17922 | try: | ||
r32794 | with _getbkfile(repo) as bkfile: | |||
for line in bkfile: | ||||
line = line.strip() | ||||
if not line: | ||||
continue | ||||
try: | ||||
|
r43347 | sha, refspec = line.split(b' ', 1) | ||
r32794 | node = tonode(sha) | |||
r43944 | if has_node(node): | |||
r32794 | refspec = encoding.tolocal(refspec) | |||
|
r37866 | refmap[refspec] = node | ||
|
r37869 | nrefs = nodemap.get(node) | ||
if nrefs is None: | ||||
nodemap[node] = [refspec] | ||||
else: | ||||
nrefs.append(refspec) | ||||
if nrefs[-2] > refspec: | ||||
# bookmarks weren't sorted before 4.5 | ||||
nrefs.sort() | ||||
r32794 | except (TypeError, ValueError): | |||
# TypeError: | ||||
# - bin(...) | ||||
# ValueError: | ||||
# - node in nm, for non-20-bytes entry | ||||
# - split(...), for string without ' ' | ||||
|
r43347 | bookmarkspath = b'.hg/bookmarks' | ||
|
r42512 | if bookmarksinstore(repo): | ||
|
r43347 | bookmarkspath = b'.hg/store/bookmarks' | ||
|
r43346 | repo.ui.warn( | ||
|
r43347 | _(b'malformed line in %s: %r\n') | ||
|
r43346 | % (bookmarkspath, pycompat.bytestr(line)) | ||
) | ||||
|
r25660 | except IOError as inst: | ||
|
r17922 | if inst.errno != errno.ENOENT: | ||
raise | ||||
|
r27698 | self._active = _readactive(repo, self) | ||
@property | ||||
def active(self): | ||||
return self._active | ||||
@active.setter | ||||
def active(self, mark): | ||||
|
r37866 | if mark is not None and mark not in self._refmap: | ||
|
r43347 | raise AssertionError(b'bookmark %s does not exist!' % mark) | ||
|
r27698 | |||
self._active = mark | ||||
self._aclean = False | ||||
|
r27187 | |||
|
r37866 | def __len__(self): | ||
return len(self._refmap) | ||||
def __iter__(self): | ||||
return iter(self._refmap) | ||||
def iteritems(self): | ||||
|
r43376 | return pycompat.iteritems(self._refmap) | ||
|
r37866 | |||
def items(self): | ||||
return self._refmap.items() | ||||
# TODO: maybe rename to allnames()? | ||||
def keys(self): | ||||
return self._refmap.keys() | ||||
# TODO: maybe rename to allnodes()? but nodes would have to be deduplicated | ||||
|
r37869 | # could be self._nodemap.keys() | ||
|
r37866 | def values(self): | ||
return self._refmap.values() | ||||
def __contains__(self, mark): | ||||
return mark in self._refmap | ||||
def __getitem__(self, mark): | ||||
return self._refmap[mark] | ||||
def get(self, mark, default=None): | ||||
return self._refmap.get(mark, default) | ||||
|
r33517 | |||
|
r37868 | def _set(self, mark, node): | ||
|
r27187 | self._clean = False | ||
|
r37869 | if mark in self._refmap: | ||
self._del(mark) | ||||
|
r37868 | self._refmap[mark] = node | ||
|
r37869 | nrefs = self._nodemap.get(node) | ||
if nrefs is None: | ||||
self._nodemap[node] = [mark] | ||||
else: | ||||
nrefs.append(mark) | ||||
nrefs.sort() | ||||
|
r33518 | |||
|
r37868 | def _del(self, mark): | ||
|
r44852 | if mark not in self._refmap: | ||
return | ||||
|
r27187 | self._clean = False | ||
|
r37869 | node = self._refmap.pop(mark) | ||
nrefs = self._nodemap[node] | ||||
if len(nrefs) == 1: | ||||
assert nrefs[0] == mark | ||||
del self._nodemap[node] | ||||
else: | ||||
nrefs.remove(mark) | ||||
|
r35697 | |||
|
r37867 | def names(self, node): | ||
"""Return a sorted list of bookmarks pointing to the specified node""" | ||||
|
r37869 | return self._nodemap.get(node, []) | ||
|
r37867 | |||
|
r33480 | def applychanges(self, repo, tr, changes): | ||
"""Apply a list of changes to bookmarks | ||||
""" | ||||
|
r43347 | bmchanges = tr.changes.get(b'bookmarks') | ||
|
r33480 | for name, node in changes: | ||
|
r37866 | old = self._refmap.get(name) | ||
|
r33480 | if node is None: | ||
|
r33518 | self._del(name) | ||
|
r33480 | else: | ||
|
r33517 | self._set(name, node) | ||
|
r33516 | if bmchanges is not None: | ||
# if a previous value exist preserve the "initial" value | ||||
previous = bmchanges.get(name) | ||||
if previous is not None: | ||||
old = previous[0] | ||||
bmchanges[name] = (old, node) | ||||
|
r33515 | self._recordchange(tr) | ||
|
r33480 | |||
|
r33515 | def _recordchange(self, tr): | ||
|
r22665 | """record that bookmarks have been changed in a transaction | ||
The transaction is then responsible for updating the file content.""" | ||||
|
r43347 | location = b'' if bookmarksinstore(self._repo) else b'plain' | ||
|
r43346 | tr.addfilegenerator( | ||
|
r43347 | b'bookmarks', (b'bookmarks',), self._write, location=location | ||
|
r43346 | ) | ||
|
r43347 | tr.hookargs[b'bookmark_moved'] = b'1' | ||
|
r22665 | |||
|
r23469 | def _writerepo(self, repo): | ||
"""Factored out for extensibility""" | ||||
|
r27698 | rbm = repo._bookmarks | ||
|
r37866 | if rbm.active not in self._refmap: | ||
|
r27698 | rbm.active = None | ||
rbm._writeactive() | ||||
|
r17922 | |||
|
r42512 | if bookmarksinstore(repo): | ||
vfs = repo.svfs | ||||
lock = repo.lock() | ||||
else: | ||||
vfs = repo.vfs | ||||
lock = repo.wlock() | ||||
with lock: | ||||
|
r43347 | with vfs(b'bookmarks', b'w', atomictemp=True, checkambig=True) as f: | ||
|
r42502 | self._write(f) | ||
|
r17922 | |||
|
r27698 | def _writeactive(self): | ||
if self._aclean: | ||||
return | ||||
|
r27800 | with self._repo.wlock(): | ||
|
r27698 | if self._active is not None: | ||
|
r43346 | with self._repo.vfs( | ||
|
r43347 | b'bookmarks.current', b'w', atomictemp=True, checkambig=True | ||
|
r43346 | ) as f: | ||
|
r27698 | f.write(encoding.fromlocal(self._active)) | ||
else: | ||||
|
r43347 | self._repo.vfs.tryunlink(b'bookmarks.current') | ||
|
r27698 | self._aclean = True | ||
|
r22664 | def _write(self, fp): | ||
|
r43376 | for name, node in sorted(pycompat.iteritems(self._refmap)): | ||
|
r43347 | fp.write(b"%s %s\n" % (hex(node), encoding.fromlocal(name))) | ||
|
r27187 | self._clean = True | ||
|
r29066 | self._repo.invalidatevolatilesets() | ||
|
r22664 | |||
|
r28182 | def expandname(self, bname): | ||
|
r43347 | if bname == b'.': | ||
|
r29354 | if self.active: | ||
return self.active | ||||
else: | ||||
|
r43347 | raise error.RepoLookupError(_(b"no active bookmark")) | ||
|
r28182 | return bname | ||
|
r32956 | def checkconflict(self, mark, force=False, target=None): | ||
"""check repo for a potential clash of mark with an existing bookmark, | ||||
branch, or hash | ||||
If target is supplied, then check that we are moving the bookmark | ||||
forward. | ||||
If force is supplied, then forcibly move the bookmark to a new commit | ||||
regardless if it is a move forward. | ||||
|
r33513 | |||
If divergent bookmark are to be deleted, they will be returned as list. | ||||
|
r32956 | """ | ||
|
r43347 | cur = self._repo[b'.'].node() | ||
|
r37866 | if mark in self._refmap and not force: | ||
|
r32956 | if target: | ||
|
r37866 | if self._refmap[mark] == target and target == cur: | ||
|
r32956 | # re-activating a bookmark | ||
|
r33513 | return [] | ||
|
r32956 | rev = self._repo[target].rev() | ||
anc = self._repo.changelog.ancestors([rev]) | ||||
|
r43248 | bmctx = self._repo[self[mark]] | ||
|
r43346 | divs = [ | ||
self._refmap[b] | ||||
for b in self._refmap | ||||
|
r43347 | if b.split(b'@', 1)[0] == mark.split(b'@', 1)[0] | ||
|
r43346 | ] | ||
|
r32956 | |||
# allow resolving a single divergent bookmark even if moving | ||||
# the bookmark across branches when a revision is specified | ||||
# that contains a divergent bookmark | ||||
if bmctx.rev() not in anc and target in divs: | ||||
|
r33513 | return divergent2delete(self._repo, [target], mark) | ||
|
r32956 | |||
|
r43346 | deletefrom = [ | ||
b for b in divs if self._repo[b].rev() in anc or b == target | ||||
] | ||||
|
r33513 | delbms = divergent2delete(self._repo, deletefrom, mark) | ||
|
r32956 | if validdest(self._repo, bmctx, self._repo[target]): | ||
self._repo.ui.status( | ||||
|
r43347 | _(b"moving bookmark '%s' forward from %s\n") | ||
|
r43346 | % (mark, short(bmctx.node())) | ||
) | ||||
|
r33513 | return delbms | ||
|
r32956 | raise error.Abort( | ||
|
r43387 | _(b"bookmark '%s' already exists (use -f to force)") % mark | ||
|
r43346 | ) | ||
if ( | ||||
mark in self._repo.branchmap() | ||||
or mark == self._repo.dirstate.branch() | ||||
) and not force: | ||||
raise error.Abort( | ||||
|
r43347 | _(b"a bookmark cannot have the name of an existing branch") | ||
|
r43346 | ) | ||
|
r32956 | if len(mark) > 3 and not force: | ||
try: | ||||
|
r37415 | shadowhash = scmutil.isrevsymbol(self._repo, mark) | ||
|
r32956 | except error.LookupError: # ambiguous identifier | ||
shadowhash = False | ||||
if shadowhash: | ||||
self._repo.ui.warn( | ||||
|
r43346 | _( | ||
|
r43347 | b"bookmark %s matches a changeset hash\n" | ||
b"(did you leave a -r out of an 'hg bookmark' " | ||||
b"command?)\n" | ||||
|
r43346 | ) | ||
% mark | ||||
) | ||||
|
r33513 | return [] | ||
|
r32956 | |||
|
r43346 | |||
|
r27698 | def _readactive(repo, marks): | ||
|
r24946 | """ | ||
Get the active bookmark. We can have an active bookmark that updates | ||||
itself as we commit. This function returns the name of that bookmark. | ||||
It is stored in .hg/bookmarks.current | ||||
""" | ||||
|
r42503 | # No readline() in osutil.posixfile, reading everything is | ||
# cheap. | ||||
|
r43347 | content = repo.vfs.tryread(b'bookmarks.current') | ||
mark = encoding.tolocal((content.splitlines() or [b''])[0]) | ||||
if mark == b'' or mark not in marks: | ||||
|
r42503 | mark = None | ||
|
r13351 | return mark | ||
|
r43346 | |||
|
r24945 | def activate(repo, mark): | ||
""" | ||||
Set the given bookmark to be 'active', meaning that this bookmark will | ||||
follow new commits that are made. | ||||
|
r13350 | The name is recorded in .hg/bookmarks.current | ||
|
r24945 | """ | ||
|
r27698 | repo._bookmarks.active = mark | ||
repo._bookmarks._writeactive() | ||||
|
r13352 | |||
|
r43346 | |||
|
r24944 | def deactivate(repo): | ||
""" | ||||
|
r26781 | Unset the active bookmark in this repository. | ||
|
r24944 | """ | ||
|
r27698 | repo._bookmarks.active = None | ||
repo._bookmarks._writeactive() | ||||
|
r16191 | |||
|
r43346 | |||
|
r24986 | def isactivewdirparent(repo): | ||
""" | ||||
Tell whether the 'active' bookmark (the one that follows new commits) | ||||
points to one of the parents of the current working directory (wdir). | ||||
|
r18471 | |||
|
r24986 | While this is normally the case, it can on occasion be false; for example, | ||
immediately after a pull, the active bookmark can be moved to point | ||||
to a place different than the wdir. This is solved by running `hg update`. | ||||
""" | ||||
mark = repo._activebookmark | ||||
|
r18471 | marks = repo._bookmarks | ||
|
r24986 | parents = [p.node() for p in repo[None].parents()] | ||
|
r43346 | return mark in marks and marks[mark] in parents | ||
|
r18471 | |||
|
r33510 | def divergent2delete(repo, deletefrom, bm): | ||
"""find divergent versions of bm on nodes in deletefrom. | ||||
the list of bookmark to delete.""" | ||||
todelete = [] | ||||
|
r18513 | marks = repo._bookmarks | ||
|
r43347 | divergent = [ | ||
b for b in marks if b.split(b'@', 1)[0] == bm.split(b'@', 1)[0] | ||||
] | ||||
|
r18513 | for mark in divergent: | ||
|
r43347 | if mark == b'@' or b'@' not in mark: | ||
|
r21843 | # can't be divergent by definition | ||
continue | ||||
|
r18513 | if mark and marks[mark] in deletefrom: | ||
if mark != bm: | ||||
|
r33510 | todelete.append(mark) | ||
return todelete | ||||
|
r18513 | |||
|
r43346 | |||
|
r32381 | def headsforactive(repo): | ||
"""Given a repo with an active bookmark, return divergent bookmark nodes. | ||||
Args: | ||||
repo: A repository with an active bookmark. | ||||
Returns: | ||||
A list of binary node ids that is the full list of other | ||||
revisions with bookmarks divergent from the active bookmark. If | ||||
there were no divergent bookmarks, then this list will contain | ||||
only one entry. | ||||
""" | ||||
if not repo._activebookmark: | ||||
raise ValueError( | ||||
|
r43347 | b'headsforactive() only makes sense with an active bookmark' | ||
|
r43346 | ) | ||
|
r43347 | name = repo._activebookmark.split(b'@', 1)[0] | ||
|
r32381 | heads = [] | ||
|
r43376 | for mark, n in pycompat.iteritems(repo._bookmarks): | ||
|
r43347 | if mark.split(b'@', 1)[0] == name: | ||
|
r32381 | heads.append(n) | ||
return heads | ||||
|
r43346 | |||
|
r37393 | def calculateupdate(ui, repo): | ||
|
r37377 | '''Return a tuple (activemark, movemarkfrom) indicating the active bookmark | ||
and where to move the active bookmark from, if needed.''' | ||||
|
r37393 | checkout, movemarkfrom = None, None | ||
activemark = repo._activebookmark | ||||
if isactivewdirparent(repo): | ||||
|
r43347 | movemarkfrom = repo[b'.'].node() | ||
|
r37393 | elif activemark: | ||
|
r43347 | ui.status(_(b"updating to active bookmark %s\n") % activemark) | ||
|
r37393 | checkout = activemark | ||
|
r19523 | return (checkout, movemarkfrom) | ||
|
r43346 | |||
|
r13352 | def update(repo, parents, node): | ||
|
r19110 | deletefrom = parents | ||
|
r13352 | marks = repo._bookmarks | ||
|
r27698 | active = marks.active | ||
|
r25100 | if not active: | ||
|
r16706 | return False | ||
|
r33491 | bmchanges = [] | ||
|
r25100 | if marks[active] in parents: | ||
|
r18513 | new = repo[node] | ||
|
r43346 | divs = [ | ||
repo[marks[b]] | ||||
for b in marks | ||||
|
r43347 | if b.split(b'@', 1)[0] == active.split(b'@', 1)[0] | ||
|
r43346 | ] | ||
|
r19110 | anc = repo.changelog.ancestors([new.rev()]) | ||
deletefrom = [b.node() for b in divs if b.rev() in anc or b == new] | ||||
|
r43248 | if validdest(repo, repo[marks[active]], new): | ||
|
r33491 | bmchanges.append((active, new.node())) | ||
|
r18513 | |||
|
r33512 | for bm in divergent2delete(repo, deletefrom, active): | ||
bmchanges.append((bm, None)) | ||||
|
r18513 | |||
|
r33512 | if bmchanges: | ||
|
r43347 | with repo.lock(), repo.transaction(b'bookmark') as tr: | ||
|
r33491 | marks.applychanges(repo, tr, bmchanges) | ||
|
r33512 | return bool(bmchanges) | ||
|
r13353 | |||
|
r43346 | |||
|
r44853 | def isdivergent(b): | ||
return b'@' in b and not b.endswith(b'@') | ||||
|
r30481 | def listbinbookmarks(repo): | ||
# We may try to list bookmarks on a repo type that does not | ||||
# support it (e.g., statichttprepository). | ||||
marks = getattr(repo, '_bookmarks', {}) | ||||
hasnode = repo.changelog.hasnode | ||||
|
r43376 | for k, v in pycompat.iteritems(marks): | ||
|
r30481 | # don't expose local divergent bookmarks | ||
|
r44853 | if hasnode(v) and not isdivergent(k): | ||
|
r30481 | yield k, v | ||
|
r43346 | |||
|
r13353 | def listbookmarks(repo): | ||
d = {} | ||||
|
r30482 | for book, node in listbinbookmarks(repo): | ||
d[book] = hex(node) | ||||
|
r13353 | return d | ||
|
r43346 | |||
|
r13353 | def pushbookmark(repo, key, old, new): | ||
|
r44854 | if isdivergent(key): | ||
return False | ||||
|
r42512 | if bookmarksinstore(repo): | ||
wlock = util.nullcontextmanager() | ||||
else: | ||||
wlock = repo.wlock() | ||||
|
r43347 | with wlock, repo.lock(), repo.transaction(b'bookmarks') as tr: | ||
|
r13353 | marks = repo._bookmarks | ||
|
r43347 | existing = hex(marks.get(key, b'')) | ||
|
r22364 | if existing != old and existing != new: | ||
|
r13353 | return False | ||
|
r43347 | if new == b'': | ||
|
r33485 | changes = [(key, None)] | ||
|
r13353 | else: | ||
if new not in repo: | ||||
return False | ||||
|
r33485 | changes = [(key, repo[new].node())] | ||
marks.applychanges(repo, tr, changes) | ||||
|
r13353 | return True | ||
|
r13354 | |||
|
r43346 | |||
|
r30583 | def comparebookmarks(repo, srcmarks, dstmarks, targets=None): | ||
|
r20024 | '''Compare bookmarks between srcmarks and dstmarks | ||
This returns tuple "(addsrc, adddst, advsrc, advdst, diverge, | ||||
differ, invalid)", each are list of bookmarks below: | ||||
:addsrc: added on src side (removed on dst side, perhaps) | ||||
:adddst: added on dst side (removed on src side, perhaps) | ||||
:advsrc: advanced on src side | ||||
:advdst: advanced on dst side | ||||
:diverge: diverge | ||||
:differ: changed, but changeset referred on src is unknown on dst | ||||
:invalid: unknown on both side | ||||
|
r23081 | :same: same on both side | ||
|
r20024 | |||
Each elements of lists in result tuple is tuple "(bookmark name, | ||||
changeset ID on source side, changeset ID on destination | ||||
|
r43184 | side)". Each changeset ID is a binary node or None. | ||
|
r20024 | |||
Changeset IDs of tuples in "addsrc", "adddst", "differ" or | ||||
"invalid" list may be unknown for repo. | ||||
If "targets" is specified, only bookmarks listed in it are | ||||
examined. | ||||
''' | ||||
if targets: | ||||
bset = set(targets) | ||||
else: | ||||
srcmarkset = set(srcmarks) | ||||
dstmarkset = set(dstmarks) | ||||
|
r23081 | bset = srcmarkset | dstmarkset | ||
|
r20024 | |||
|
r23081 | results = ([], [], [], [], [], [], [], []) | ||
|
r20024 | addsrc = results[0].append | ||
adddst = results[1].append | ||||
advsrc = results[2].append | ||||
advdst = results[3].append | ||||
diverge = results[4].append | ||||
differ = results[5].append | ||||
invalid = results[6].append | ||||
|
r23081 | same = results[7].append | ||
|
r20024 | |||
for b in sorted(bset): | ||||
if b not in srcmarks: | ||||
if b in dstmarks: | ||||
|
r30583 | adddst((b, None, dstmarks[b])) | ||
|
r20024 | else: | ||
invalid((b, None, None)) | ||||
elif b not in dstmarks: | ||||
|
r30583 | addsrc((b, srcmarks[b], None)) | ||
|
r20024 | else: | ||
|
r30583 | scid = srcmarks[b] | ||
dcid = dstmarks[b] | ||||
|
r23081 | if scid == dcid: | ||
same((b, scid, dcid)) | ||||
elif scid in repo and dcid in repo: | ||||
|
r20024 | sctx = repo[scid] | ||
dctx = repo[dcid] | ||||
if sctx.rev() < dctx.rev(): | ||||
if validdest(repo, sctx, dctx): | ||||
advdst((b, scid, dcid)) | ||||
else: | ||||
diverge((b, scid, dcid)) | ||||
else: | ||||
if validdest(repo, dctx, sctx): | ||||
advsrc((b, scid, dcid)) | ||||
else: | ||||
diverge((b, scid, dcid)) | ||||
else: | ||||
# it is too expensive to examine in detail, in this case | ||||
differ((b, scid, dcid)) | ||||
return results | ||||
|
r43346 | |||
|
r24355 | def _diverge(ui, b, path, localmarks, remotenode): | ||
|
r24353 | '''Return appropriate diverged bookmark for specified ``path`` | ||
This returns None, if it is failed to assign any divergent | ||||
bookmark name. | ||||
|
r24355 | |||
This reuses already existing one with "@number" suffix, if it | ||||
refers ``remotenode``. | ||||
|
r24353 | ''' | ||
|
r43347 | if b == b'@': | ||
b = b'' | ||||
|
r20025 | # try to use an @pathalias suffix | ||
# if an @pathalias already exists, we overwrite (update) it | ||||
|
r43347 | if path.startswith(b"file:"): | ||
|
r22629 | path = util.url(path).path | ||
|
r43347 | for p, u in ui.configitems(b"paths"): | ||
if u.startswith(b"file:"): | ||||
|
r22629 | u = util.url(u).path | ||
|
r20025 | if path == u: | ||
|
r43347 | return b'%s@%s' % (b, p) | ||
|
r24354 | |||
# assign a unique "@number" suffix newly | ||||
for x in range(1, 100): | ||||
|
r43347 | n = b'%s@%d' % (b, x) | ||
|
r24355 | if n not in localmarks or localmarks[n] == remotenode: | ||
|
r24354 | return n | ||
return None | ||||
|
r20025 | |||
|
r43346 | |||
|
r30583 | def unhexlifybookmarks(marks): | ||
binremotemarks = {} | ||||
for name, node in marks.items(): | ||||
binremotemarks[name] = bin(node) | ||||
return binremotemarks | ||||
|
r43346 | |||
|
r43347 | _binaryentry = struct.Struct(b'>20sH') | ||
|
r35258 | |||
|
r43346 | |||
|
r35258 | def binaryencode(bookmarks): | ||
"""encode a '(bookmark, node)' iterable into a binary stream | ||||
the binary format is: | ||||
<node><bookmark-length><bookmark-name> | ||||
:node: is a 20 bytes binary node, | ||||
:bookmark-length: an unsigned short, | ||||
:bookmark-name: the name of the bookmark (of length <bookmark-length>) | ||||
wdirid (all bits set) will be used as a special value for "missing" | ||||
""" | ||||
binarydata = [] | ||||
for book, node in bookmarks: | ||||
|
r43346 | if not node: # None or '' | ||
|
r35258 | node = wdirid | ||
binarydata.append(_binaryentry.pack(node, len(book))) | ||||
binarydata.append(book) | ||||
|
r43347 | return b''.join(binarydata) | ||
|
r35258 | |||
|
r43346 | |||
|
r35258 | def binarydecode(stream): | ||
"""decode a binary stream into an '(bookmark, node)' iterable | ||||
the binary format is: | ||||
<node><bookmark-length><bookmark-name> | ||||
:node: is a 20 bytes binary node, | ||||
:bookmark-length: an unsigned short, | ||||
:bookmark-name: the name of the bookmark (of length <bookmark-length>)) | ||||
wdirid (all bits set) will be used as a special value for "missing" | ||||
""" | ||||
entrysize = _binaryentry.size | ||||
books = [] | ||||
while True: | ||||
entry = stream.read(entrysize) | ||||
if len(entry) < entrysize: | ||||
if entry: | ||||
|
r43347 | raise error.Abort(_(b'bad bookmark stream')) | ||
|
r35258 | break | ||
node, length = _binaryentry.unpack(entry) | ||||
bookmark = stream.read(length) | ||||
if len(bookmark) < length: | ||||
if entry: | ||||
|
r43347 | raise error.Abort(_(b'bad bookmark stream')) | ||
|
r35258 | if node == wdirid: | ||
node = None | ||||
books.append((bookmark, node)) | ||||
return books | ||||
|
r43346 | |||
|
r22666 | def updatefromremote(ui, repo, remotemarks, path, trfunc, explicit=()): | ||
|
r43347 | ui.debug(b"checking for updated bookmarks\n") | ||
|
r17922 | localmarks = repo._bookmarks | ||
|
r43346 | ( | ||
addsrc, | ||||
adddst, | ||||
advsrc, | ||||
advdst, | ||||
diverge, | ||||
differ, | ||||
invalid, | ||||
same, | ||||
|
r30583 | ) = comparebookmarks(repo, remotemarks, localmarks) | ||
|
r15614 | |||
|
r22644 | status = ui.status | ||
warn = ui.warn | ||||
|
r43347 | if ui.configbool(b'ui', b'quietbookmarkmove'): | ||
|
r22644 | status = warn = ui.debug | ||
|
r22659 | explicit = set(explicit) | ||
|
r20025 | changed = [] | ||
for b, scid, dcid in addsrc: | ||||
|
r43346 | if scid in repo: # add remote bookmarks for changes we already have | ||
changed.append( | ||||
|
r43347 | (b, scid, status, _(b"adding remote bookmark %s\n") % b) | ||
|
r43346 | ) | ||
|
r25564 | elif b in explicit: | ||
explicit.remove(b) | ||||
|
r43346 | ui.warn( | ||
|
r43347 | _(b"remote bookmark %s points to locally missing %s\n") | ||
|
r43346 | % (b, hex(scid)[:12]) | ||
) | ||||
|
r25564 | |||
|
r20025 | for b, scid, dcid in advsrc: | ||
|
r43347 | changed.append((b, scid, status, _(b"updating bookmark %s\n") % b)) | ||
|
r22659 | # remove normal movement from explicit set | ||
explicit.difference_update(d[0] for d in changed) | ||||
|
r20025 | for b, scid, dcid in diverge: | ||
|
r22659 | if b in explicit: | ||
explicit.discard(b) | ||||
|
r43347 | changed.append((b, scid, status, _(b"importing bookmark %s\n") % b)) | ||
|
r22659 | else: | ||
|
r30583 | db = _diverge(ui, b, path, localmarks, scid) | ||
|
r24353 | if db: | ||
|
r43346 | changed.append( | ||
( | ||||
db, | ||||
scid, | ||||
warn, | ||||
|
r43347 | _(b"divergent bookmark %s stored as %s\n") % (b, db), | ||
|
r43346 | ) | ||
) | ||||
|
r24353 | else: | ||
|
r43346 | warn( | ||
_( | ||||
|
r43347 | b"warning: failed to assign numbered name " | ||
b"to divergent bookmark %s\n" | ||||
|
r43346 | ) | ||
% b | ||||
) | ||||
|
r22659 | for b, scid, dcid in adddst + advdst: | ||
if b in explicit: | ||||
explicit.discard(b) | ||||
|
r43347 | changed.append((b, scid, status, _(b"importing bookmark %s\n") % b)) | ||
|
r25564 | for b, scid, dcid in differ: | ||
if b in explicit: | ||||
explicit.remove(b) | ||||
|
r43346 | ui.warn( | ||
|
r43347 | _(b"remote bookmark %s points to locally missing %s\n") | ||
|
r43346 | % (b, hex(scid)[:12]) | ||
) | ||||
|
r22659 | |||
|
r13646 | if changed: | ||
|
r22666 | tr = trfunc() | ||
|
r33484 | changes = [] | ||
|
r20025 | for b, node, writer, msg in sorted(changed): | ||
|
r33484 | changes.append((b, node)) | ||
|
r20025 | writer(msg) | ||
|
r33484 | localmarks.applychanges(repo, tr, changes) | ||
|
r13646 | |||
|
r43346 | |||
|
r37659 | def incoming(ui, repo, peer): | ||
|
r24397 | '''Show bookmarks incoming from other to repo | ||
''' | ||||
|
r43347 | ui.status(_(b"searching for changed bookmarks\n")) | ||
|
r24397 | |||
|
r37659 | with peer.commandexecutor() as e: | ||
|
r43346 | remotemarks = unhexlifybookmarks( | ||
|
r43347 | e.callcommand(b'listkeys', {b'namespace': b'bookmarks',}).result() | ||
|
r43346 | ) | ||
|
r37659 | |||
|
r30583 | r = comparebookmarks(repo, remotemarks, repo._bookmarks) | ||
|
r24397 | addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = r | ||
incomings = [] | ||||
if ui.debugflag: | ||||
getid = lambda id: id | ||||
else: | ||||
getid = lambda id: id[:12] | ||||
|
r24660 | if ui.verbose: | ||
|
r43346 | |||
|
r24660 | def add(b, id, st): | ||
|
r43347 | incomings.append(b" %-25s %s %s\n" % (b, getid(id), st)) | ||
|
r43346 | |||
|
r24660 | else: | ||
|
r43346 | |||
|
r24660 | def add(b, id, st): | ||
|
r43347 | incomings.append(b" %-25s %s\n" % (b, getid(id))) | ||
|
r43346 | |||
|
r24397 | for b, scid, dcid in addsrc: | ||
|
r24832 | # i18n: "added" refers to a bookmark | ||
|
r43347 | add(b, hex(scid), _(b'added')) | ||
|
r24657 | for b, scid, dcid in advsrc: | ||
|
r24832 | # i18n: "advanced" refers to a bookmark | ||
|
r43347 | add(b, hex(scid), _(b'advanced')) | ||
|
r24657 | for b, scid, dcid in diverge: | ||
|
r24832 | # i18n: "diverged" refers to a bookmark | ||
|
r43347 | add(b, hex(scid), _(b'diverged')) | ||
|
r24657 | for b, scid, dcid in differ: | ||
|
r24832 | # i18n: "changed" refers to a bookmark | ||
|
r43347 | add(b, hex(scid), _(b'changed')) | ||
|
r24397 | |||
if not incomings: | ||||
|
r43347 | ui.status(_(b"no changed bookmarks found\n")) | ||
|
r24397 | return 1 | ||
for s in sorted(incomings): | ||||
ui.write(s) | ||||
return 0 | ||||
|
r43346 | |||
|
r24398 | def outgoing(ui, repo, other): | ||
'''Show bookmarks outgoing from repo to other | ||||
''' | ||||
|
r43347 | ui.status(_(b"searching for changed bookmarks\n")) | ||
|
r24398 | |||
|
r43347 | remotemarks = unhexlifybookmarks(other.listkeys(b'bookmarks')) | ||
|
r30583 | r = comparebookmarks(repo, repo._bookmarks, remotemarks) | ||
|
r24398 | addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = r | ||
outgoings = [] | ||||
if ui.debugflag: | ||||
getid = lambda id: id | ||||
else: | ||||
getid = lambda id: id[:12] | ||||
|
r24661 | if ui.verbose: | ||
|
r43346 | |||
|
r24661 | def add(b, id, st): | ||
|
r43347 | outgoings.append(b" %-25s %s %s\n" % (b, getid(id), st)) | ||
|
r43346 | |||
|
r24661 | else: | ||
|
r43346 | |||
|
r24661 | def add(b, id, st): | ||
|
r43347 | outgoings.append(b" %-25s %s\n" % (b, getid(id))) | ||
|
r43346 | |||
|
r24398 | for b, scid, dcid in addsrc: | ||
|
r24832 | # i18n: "added refers to a bookmark | ||
|
r43347 | add(b, hex(scid), _(b'added')) | ||
|
r24658 | for b, scid, dcid in adddst: | ||
|
r24832 | # i18n: "deleted" refers to a bookmark | ||
|
r43347 | add(b, b' ' * 40, _(b'deleted')) | ||
|
r24658 | for b, scid, dcid in advsrc: | ||
|
r24832 | # i18n: "advanced" refers to a bookmark | ||
|
r43347 | add(b, hex(scid), _(b'advanced')) | ||
|
r24658 | for b, scid, dcid in diverge: | ||
|
r24832 | # i18n: "diverged" refers to a bookmark | ||
|
r43347 | add(b, hex(scid), _(b'diverged')) | ||
|
r24658 | for b, scid, dcid in differ: | ||
|
r24832 | # i18n: "changed" refers to a bookmark | ||
|
r43347 | add(b, hex(scid), _(b'changed')) | ||
|
r24398 | |||
if not outgoings: | ||||
|
r43347 | ui.status(_(b"no changed bookmarks found\n")) | ||
|
r24398 | return 1 | ||
for s in sorted(outgoings): | ||||
ui.write(s) | ||||
return 0 | ||||
|
r43346 | |||
|
r37659 | def summary(repo, peer): | ||
|
r24400 | '''Compare bookmarks between repo and other for "hg summary" output | ||
This returns "(# of incoming, # of outgoing)" tuple. | ||||
''' | ||||
|
r37659 | with peer.commandexecutor() as e: | ||
|
r43346 | remotemarks = unhexlifybookmarks( | ||
|
r43347 | e.callcommand(b'listkeys', {b'namespace': b'bookmarks',}).result() | ||
|
r43346 | ) | ||
|
r37659 | |||
|
r30583 | r = comparebookmarks(repo, remotemarks, repo._bookmarks) | ||
|
r24400 | addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = r | ||
return (len(addsrc), len(adddst)) | ||||
|
r43346 | |||
|
r17550 | def validdest(repo, old, new): | ||
"""Is the new bookmark destination a valid update from the old one""" | ||||
|
r18008 | repo = repo.unfiltered() | ||
|
r17551 | if old == new: | ||
# Old == new -> nothing to update. | ||||
|
r17625 | return False | ||
|
r17551 | elif not old: | ||
# old is nullrev, anything is valid. | ||||
# (new != nullrev has been excluded by the previous check) | ||||
|
r17625 | return True | ||
|
r17551 | elif repo.obsstore: | ||
r33147 | return new.node() in obsutil.foreground(repo, [old.node()]) | |||
|
r17551 | else: | ||
|
r24180 | # still an independent clause as it is lazier (and therefore faster) | ||
|
r38692 | return old.isancestorof(new) | ||
|
r32955 | |||
|
r43346 | |||
|
r32955 | def checkformat(repo, mark): | ||
"""return a valid version of a potential bookmark name | ||||
Raises an abort error if the bookmark name is not valid. | ||||
""" | ||||
mark = mark.strip() | ||||
if not mark: | ||||
|
r43346 | raise error.Abort( | ||
|
r43387 | _(b"bookmark names cannot consist entirely of whitespace") | ||
|
r43346 | ) | ||
|
r43347 | scmutil.checknewlabel(repo, mark, b'bookmark') | ||
|
r32955 | return mark | ||
|
r33005 | |||
|
r43346 | |||
|
r33005 | def delete(repo, tr, names): | ||
"""remove a mark from the bookmark store | ||||
Raises an abort error if mark does not exist. | ||||
""" | ||||
marks = repo._bookmarks | ||||
|
r33481 | changes = [] | ||
|
r33005 | for mark in names: | ||
if mark not in marks: | ||||
|
r43347 | raise error.Abort(_(b"bookmark '%s' does not exist") % mark) | ||
|
r33005 | if mark == repo._activebookmark: | ||
deactivate(repo) | ||||
|
r33481 | changes.append((mark, None)) | ||
marks.applychanges(repo, tr, changes) | ||||
|
r33006 | |||
|
r43346 | |||
|
r33006 | def rename(repo, tr, old, new, force=False, inactive=False): | ||
"""rename a bookmark from old to new | ||||
If force is specified, then the new name can overwrite an existing | ||||
bookmark. | ||||
If inactive is specified, then do not activate the new bookmark. | ||||
Raises an abort error if old is not in the bookmark store. | ||||
""" | ||||
marks = repo._bookmarks | ||||
mark = checkformat(repo, new) | ||||
if old not in marks: | ||||
|
r43347 | raise error.Abort(_(b"bookmark '%s' does not exist") % old) | ||
|
r33513 | changes = [] | ||
for bm in marks.checkconflict(mark, force): | ||||
changes.append((bm, None)) | ||||
changes.extend([(mark, marks[old]), (old, None)]) | ||||
|
r33482 | marks.applychanges(repo, tr, changes) | ||
|
r33006 | if repo._activebookmark == old and not inactive: | ||
activate(repo, mark) | ||||
|
r33007 | |||
|
r43346 | |||
|
r33007 | def addbookmarks(repo, tr, names, rev=None, force=False, inactive=False): | ||
"""add a list of bookmarks | ||||
If force is specified, then the new name can overwrite an existing | ||||
bookmark. | ||||
If inactive is specified, then do not activate any bookmark. Otherwise, the | ||||
first bookmark is activated. | ||||
Raises an abort error if old is not in the bookmark store. | ||||
""" | ||||
marks = repo._bookmarks | ||||
|
r43347 | cur = repo[b'.'].node() | ||
|
r33007 | newact = None | ||
|
r33483 | changes = [] | ||
|
r35665 | |||
# unhide revs if any | ||||
if rev: | ||||
|
r43347 | repo = scmutil.unhidehashlikerevs(repo, [rev], b'nowarn') | ||
|
r43998 | |||
|
r43999 | ctx = scmutil.revsingle(repo, rev, None) | ||
# bookmarking wdir means creating a bookmark on p1 and activating it | ||||
activatenew = not inactive and ctx.rev() is None | ||||
if ctx.node() is None: | ||||
ctx = ctx.p1() | ||||
|
r43998 | tgt = ctx.node() | ||
|
r43999 | assert tgt | ||
|
r35665 | |||
|
r33007 | for mark in names: | ||
mark = checkformat(repo, mark) | ||||
if newact is None: | ||||
newact = mark | ||||
if inactive and mark == repo._activebookmark: | ||||
deactivate(repo) | ||||
|
r43996 | continue | ||
|
r33513 | for bm in marks.checkconflict(mark, force, tgt): | ||
changes.append((bm, None)) | ||||
|
r33483 | changes.append((mark, tgt)) | ||
|
r35665 | |||
|
r43996 | # nothing changed but for the one deactivated above | ||
if not changes: | ||||
return | ||||
|
r43998 | if ctx.hidden(): | ||
repo.ui.warn(_(b"bookmarking hidden changeset %s\n") % ctx.hex()[:12]) | ||||
|
r35730 | |||
if ctx.obsolete(): | ||||
|
r43998 | msg = obsutil._getfilteredreason(repo, ctx.hex()[:12], ctx) | ||
|
r43347 | repo.ui.warn(b"(%s)\n" % msg) | ||
|
r35730 | |||
|
r33483 | marks.applychanges(repo, tr, changes) | ||
|
r43999 | if activatenew and cur == marks[newact]: | ||
|
r33007 | activate(repo, newact) | ||
elif cur != tgt and newact == repo._activebookmark: | ||||
deactivate(repo) | ||||
|
r33010 | |||
|
r43346 | |||
|
r39782 | def _printbookmarks(ui, repo, fm, bmarks): | ||
|
r33011 | """private method to print bookmarks | ||
Provides a way for extensions to control how bookmarks are printed (e.g. | ||||
prepend or postpend names) | ||||
""" | ||||
hexfn = fm.hexfunc | ||||
if len(bmarks) == 0 and fm.isplain(): | ||||
|
r43347 | ui.status(_(b"no bookmarks set\n")) | ||
|
r43376 | for bmark, (n, prefix, label) in sorted(pycompat.iteritems(bmarks)): | ||
|
r33011 | fm.startitem() | ||
|
r39660 | fm.context(repo=repo) | ||
|
r33011 | if not ui.quiet: | ||
|
r43347 | fm.plain(b' %s ' % prefix, label=label) | ||
fm.write(b'bookmark', b'%s', bmark, label=label) | ||||
pad = b" " * (25 - encoding.colwidth(bmark)) | ||||
|
r43346 | fm.condwrite( | ||
not ui.quiet, | ||||
|
r43347 | b'rev node', | ||
pad + b' %d:%s', | ||||
|
r43346 | repo.changelog.rev(n), | ||
hexfn(n), | ||||
label=label, | ||||
) | ||||
|
r33011 | fm.data(active=(activebookmarklabel in label)) | ||
|
r43347 | fm.plain(b'\n') | ||
|
r33011 | |||
|
r43346 | |||
|
r39789 | def printbookmarks(ui, repo, fm, names=None): | ||
|
r39782 | """print bookmarks by the given formatter | ||
|
r33010 | |||
Provides a way for extensions to control how bookmarks are printed. | ||||
""" | ||||
marks = repo._bookmarks | ||||
|
r33011 | bmarks = {} | ||
|
r43346 | for bmark in names or marks: | ||
|
r39789 | if bmark not in marks: | ||
|
r43347 | raise error.Abort(_(b"bookmark '%s' does not exist") % bmark) | ||
|
r33010 | active = repo._activebookmark | ||
if bmark == active: | ||||
|
r43347 | prefix, label = b'*', activebookmarklabel | ||
|
r33010 | else: | ||
|
r43347 | prefix, label = b' ', b'' | ||
|
r33010 | |||
|
r39789 | bmarks[bmark] = (marks[bmark], prefix, label) | ||
|
r39782 | _printbookmarks(ui, repo, fm, bmarks) | ||
|
r34709 | |||
|
r43346 | |||
|
r34709 | def preparehookargs(name, old, new): | ||
if new is None: | ||||
|
r43347 | new = b'' | ||
|
r34709 | if old is None: | ||
|
r43347 | old = b'' | ||
return {b'bookmark': name, b'node': hex(new), b'oldnode': hex(old)} | ||||