bookmarks.py
434 lines
| 13.9 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. | ||||
from mercurial.i18n import _ | ||||
|
r20025 | from mercurial.node import hex, bin | ||
|
r17916 | from mercurial import encoding, error, util, obsolete | ||
|
r19896 | import errno | ||
|
r13350 | |||
|
r17922 | class bmstore(dict): | ||
"""Storage for bookmarks. | ||||
This object should do all bookmark reads and writes, so that it's | ||||
fairly simple to replace the storage underlying bookmarks without | ||||
having to clone the logic surrounding bookmarks. | ||||
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}. | ||||
This class does NOT handle the "current" bookmark state at this | ||||
time. | ||||
""" | ||||
|
r13351 | |||
|
r17922 | def __init__(self, repo): | ||
dict.__init__(self) | ||||
self._repo = repo | ||||
try: | ||||
for line in repo.vfs('bookmarks'): | ||||
line = line.strip() | ||||
if not line: | ||||
continue | ||||
if ' ' not in line: | ||||
repo.ui.warn(_('malformed line in .hg/bookmarks: %r\n') | ||||
% line) | ||||
continue | ||||
sha, refspec = line.split(' ', 1) | ||||
refspec = encoding.tolocal(refspec) | ||||
try: | ||||
self[refspec] = repo.changelog.lookup(sha) | ||||
except LookupError: | ||||
pass | ||||
except IOError, inst: | ||||
if inst.errno != errno.ENOENT: | ||||
raise | ||||
def write(self): | ||||
'''Write bookmarks | ||||
Write the given bookmark => hash dictionary to the .hg/bookmarks file | ||||
in a format equal to those of localtags. | ||||
We also store a backup of the previous state in undo.bookmarks that | ||||
can be copied back on rollback. | ||||
''' | ||||
repo = self._repo | ||||
if repo._bookmarkcurrent not in self: | ||||
|
r20098 | unsetcurrent(repo) | ||
|
r17922 | |||
wlock = repo.wlock() | ||||
try: | ||||
file = repo.vfs('bookmarks', 'w', atomictemp=True) | ||||
for name, node in self.iteritems(): | ||||
file.write("%s %s\n" % (hex(node), encoding.fromlocal(name))) | ||||
file.close() | ||||
# touch 00changelog.i so hgweb reloads bookmarks (no lock needed) | ||||
|
r14027 | try: | ||
|
r19896 | repo.svfs.utime('00changelog.i', None) | ||
|
r17922 | except OSError: | ||
|
r14027 | pass | ||
|
r17922 | |||
finally: | ||||
wlock.release() | ||||
|
r13351 | |||
def readcurrent(repo): | ||||
'''Get the current bookmark | ||||
|
r18043 | If we use gittish branches we have a current bookmark that | ||
|
r13351 | we are on. This function returns the name of the bookmark. It | ||
is stored in .hg/bookmarks.current | ||||
''' | ||||
mark = None | ||||
|
r14027 | try: | ||
|
r13351 | file = repo.opener('bookmarks.current') | ||
|
r14027 | except IOError, inst: | ||
if inst.errno != errno.ENOENT: | ||||
raise | ||||
return None | ||||
try: | ||||
|
r17425 | # No readline() in osutil.posixfile, reading everything is cheap | ||
|
r13381 | mark = encoding.tolocal((file.readlines() or [''])[0]) | ||
|
r13627 | if mark == '' or mark not in repo._bookmarks: | ||
|
r13351 | mark = None | ||
|
r14027 | finally: | ||
|
r13351 | file.close() | ||
return mark | ||||
|
r13350 | def setcurrent(repo, mark): | ||
'''Set the name of the bookmark that we are currently on | ||||
Set the name of the bookmark that we are on (hg update <bookmark>). | ||||
The name is recorded in .hg/bookmarks.current | ||||
''' | ||||
|
r20100 | if mark not in repo._bookmarks: | ||
raise AssertionError('bookmark %s does not exist!' % mark) | ||||
|
r13350 | current = repo._bookmarkcurrent | ||
if current == mark: | ||||
return | ||||
|
r15908 | wlock = repo.wlock() | ||
|
r13350 | try: | ||
file = repo.opener('bookmarks.current', 'w', atomictemp=True) | ||||
|
r14559 | file.write(encoding.fromlocal(mark)) | ||
|
r15057 | file.close() | ||
|
r13350 | finally: | ||
|
r15908 | wlock.release() | ||
|
r13350 | repo._bookmarkcurrent = mark | ||
|
r13352 | |||
|
r16191 | def unsetcurrent(repo): | ||
wlock = repo.wlock() | ||||
try: | ||||
|
r16194 | try: | ||
|
r19895 | repo.vfs.unlink('bookmarks.current') | ||
|
r16194 | repo._bookmarkcurrent = None | ||
except OSError, inst: | ||||
if inst.errno != errno.ENOENT: | ||||
raise | ||||
|
r16191 | finally: | ||
wlock.release() | ||||
|
r18471 | def iscurrent(repo, mark=None, parents=None): | ||
'''Tell whether the current bookmark is also active | ||||
I.e., the bookmark listed in .hg/bookmarks.current also points to a | ||||
parent of the working directory. | ||||
''' | ||||
if not mark: | ||||
mark = repo._bookmarkcurrent | ||||
if not parents: | ||||
parents = [p.node() for p in repo[None].parents()] | ||||
marks = repo._bookmarks | ||||
return (mark in marks and marks[mark] in parents) | ||||
|
r13663 | def updatecurrentbookmark(repo, oldnode, curbranch): | ||
try: | ||||
|
r16719 | return update(repo, oldnode, repo.branchtip(curbranch)) | ||
except error.RepoLookupError: | ||||
|
r13663 | if curbranch == "default": # no default branch! | ||
|
r15621 | return update(repo, oldnode, repo.lookup("tip")) | ||
|
r13663 | else: | ||
raise util.Abort(_("branch %s not found") % curbranch) | ||||
|
r18513 | def deletedivergent(repo, deletefrom, bm): | ||
'''Delete divergent versions of bm on nodes in deletefrom. | ||||
Return True if at least one bookmark was deleted, False otherwise.''' | ||||
deleted = False | ||||
marks = repo._bookmarks | ||||
divergent = [b for b in marks if b.split('@', 1)[0] == bm.split('@', 1)[0]] | ||||
for mark in divergent: | ||||
|
r21843 | if mark == '@' or '@' not in mark: | ||
# can't be divergent by definition | ||||
continue | ||||
|
r18513 | if mark and marks[mark] in deletefrom: | ||
if mark != bm: | ||||
del marks[mark] | ||||
deleted = True | ||||
return deleted | ||||
|
r19523 | def calculateupdate(ui, repo, checkout): | ||
'''Return a tuple (targetrev, movemarkfrom) indicating the rev to | ||||
check out and where to move the active bookmark from, if needed.''' | ||||
movemarkfrom = None | ||||
if checkout is None: | ||||
curmark = repo._bookmarkcurrent | ||||
if iscurrent(repo): | ||||
movemarkfrom = repo['.'].node() | ||||
elif curmark: | ||||
ui.status(_("updating to active bookmark %s\n") % curmark) | ||||
checkout = curmark | ||||
return (checkout, movemarkfrom) | ||||
|
r13352 | def update(repo, parents, node): | ||
|
r19110 | deletefrom = parents | ||
|
r13352 | marks = repo._bookmarks | ||
update = False | ||||
|
r16706 | cur = repo._bookmarkcurrent | ||
if not cur: | ||||
return False | ||||
|
r18513 | if marks[cur] in parents: | ||
new = repo[node] | ||||
|
r19110 | divs = [repo[b] for b in marks | ||
if b.split('@', 1)[0] == cur.split('@', 1)[0]] | ||||
anc = repo.changelog.ancestors([new.rev()]) | ||||
deletefrom = [b.node() for b in divs if b.rev() in anc or b == new] | ||||
|
r20281 | if validdest(repo, repo[marks[cur]], new): | ||
|
r18513 | marks[cur] = new.node() | ||
update = True | ||||
|
r19110 | if deletedivergent(repo, deletefrom, cur): | ||
|
r18513 | update = True | ||
|
r13352 | if update: | ||
|
r17922 | marks.write() | ||
|
r15621 | return update | ||
|
r13353 | |||
def listbookmarks(repo): | ||||
# We may try to list bookmarks on a repo type that does not | ||||
# support it (e.g., statichttprepository). | ||||
|
r14946 | marks = getattr(repo, '_bookmarks', {}) | ||
|
r13353 | |||
d = {} | ||||
|
r18496 | hasnode = repo.changelog.hasnode | ||
|
r14946 | for k, v in marks.iteritems(): | ||
|
r15613 | # don't expose local divergent bookmarks | ||
|
r18496 | if hasnode(v) and ('@' not in k or k.endswith('@')): | ||
|
r15613 | d[k] = hex(v) | ||
|
r13353 | return d | ||
def pushbookmark(repo, key, old, new): | ||||
|
r15908 | w = repo.wlock() | ||
|
r13353 | try: | ||
marks = repo._bookmarks | ||||
|
r22364 | existing = hex(marks.get(key, '')) | ||
if existing != old and existing != new: | ||||
|
r13353 | return False | ||
if new == '': | ||||
del marks[key] | ||||
else: | ||||
if new not in repo: | ||||
return False | ||||
marks[key] = repo[new].node() | ||||
|
r17922 | marks.write() | ||
|
r13353 | return True | ||
finally: | ||||
|
r15908 | w.release() | ||
|
r13354 | |||
|
r20024 | def compare(repo, srcmarks, dstmarks, | ||
srchex=None, dsthex=None, targets=None): | ||||
'''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 | ||||
Each elements of lists in result tuple is tuple "(bookmark name, | ||||
changeset ID on source side, changeset ID on destination | ||||
side)". Each changeset IDs are 40 hexadecimal digit string or | ||||
None. | ||||
Changeset IDs of tuples in "addsrc", "adddst", "differ" or | ||||
"invalid" list may be unknown for repo. | ||||
This function expects that "srcmarks" and "dstmarks" return | ||||
changeset ID in 40 hexadecimal digit string for specified | ||||
bookmark. If not so (e.g. bmstore "repo._bookmarks" returning | ||||
binary value), "srchex" or "dsthex" should be specified to convert | ||||
into such form. | ||||
If "targets" is specified, only bookmarks listed in it are | ||||
examined. | ||||
''' | ||||
if not srchex: | ||||
srchex = lambda x: x | ||||
if not dsthex: | ||||
dsthex = lambda x: x | ||||
if targets: | ||||
bset = set(targets) | ||||
else: | ||||
srcmarkset = set(srcmarks) | ||||
dstmarkset = set(dstmarks) | ||||
bset = srcmarkset ^ dstmarkset | ||||
for b in srcmarkset & dstmarkset: | ||||
if srchex(srcmarks[b]) != dsthex(dstmarks[b]): | ||||
bset.add(b) | ||||
results = ([], [], [], [], [], [], []) | ||||
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 | ||||
for b in sorted(bset): | ||||
if b not in srcmarks: | ||||
if b in dstmarks: | ||||
adddst((b, None, dsthex(dstmarks[b]))) | ||||
else: | ||||
invalid((b, None, None)) | ||||
elif b not in dstmarks: | ||||
addsrc((b, srchex(srcmarks[b]), None)) | ||||
else: | ||||
scid = srchex(srcmarks[b]) | ||||
dcid = dsthex(dstmarks[b]) | ||||
if scid in repo and dcid in repo: | ||||
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 | ||||
|
r20025 | def _diverge(ui, b, path, localmarks): | ||
if b == '@': | ||||
b = '' | ||||
# find a unique @ suffix | ||||
for x in range(1, 100): | ||||
n = '%s@%d' % (b, x) | ||||
if n not in localmarks: | ||||
break | ||||
# try to use an @pathalias suffix | ||||
# if an @pathalias already exists, we overwrite (update) it | ||||
|
r22629 | if path.startswith("file:"): | ||
path = util.url(path).path | ||||
|
r20025 | for p, u in ui.configitems("paths"): | ||
|
r22629 | if u.startswith("file:"): | ||
u = util.url(u).path | ||||
|
r20025 | if path == u: | ||
n = '%s@%s' % (b, p) | ||||
return n | ||||
|
r18851 | def updatefromremote(ui, repo, remotemarks, path): | ||
|
r13646 | ui.debug("checking for updated bookmarks\n") | ||
|
r17922 | localmarks = repo._bookmarks | ||
|
r20025 | (addsrc, adddst, advsrc, advdst, diverge, differ, invalid | ||
) = compare(repo, remotemarks, localmarks, dsthex=hex) | ||||
|
r15614 | |||
|
r20025 | changed = [] | ||
for b, scid, dcid in addsrc: | ||||
if scid in repo: # add remote bookmarks for changes we already have | ||||
changed.append((b, bin(scid), ui.status, | ||||
_("adding remote bookmark %s\n") % (b))) | ||||
for b, scid, dcid in advsrc: | ||||
changed.append((b, bin(scid), ui.status, | ||||
_("updating bookmark %s\n") % (b))) | ||||
for b, scid, dcid in diverge: | ||||
db = _diverge(ui, b, path, localmarks) | ||||
changed.append((db, bin(scid), ui.warn, | ||||
_("divergent bookmark %s stored as %s\n") % (b, db))) | ||||
|
r13646 | if changed: | ||
|
r20025 | for b, node, writer, msg in sorted(changed): | ||
localmarks[b] = node | ||||
writer(msg) | ||||
|
r17922 | localmarks.write() | ||
|
r13646 | |||
|
r20026 | def pushtoremote(ui, repo, remote, targets): | ||
(addsrc, adddst, advsrc, advdst, diverge, differ, invalid | ||||
) = compare(repo, repo._bookmarks, remote.listkeys('bookmarks'), | ||||
srchex=hex, targets=targets) | ||||
if invalid: | ||||
b, scid, dcid = invalid[0] | ||||
ui.warn(_('bookmark %s does not exist on the local ' | ||||
'or remote repository!\n') % b) | ||||
return 2 | ||||
def push(b, old, new): | ||||
r = remote.pushkey('bookmarks', b, old, new) | ||||
if not r: | ||||
ui.warn(_('updating bookmark %s failed!\n') % b) | ||||
return 1 | ||||
return 0 | ||||
failed = 0 | ||||
for b, scid, dcid in sorted(addsrc + advsrc + advdst + diverge + differ): | ||||
ui.status(_("exporting bookmark %s\n") % b) | ||||
if dcid is None: | ||||
dcid = '' | ||||
failed += push(b, dcid, scid) | ||||
for b, scid, dcid in adddst: | ||||
# treat as "deleted locally" | ||||
ui.status(_("deleting remote bookmark %s\n") % b) | ||||
failed += push(b, dcid, '') | ||||
if failed: | ||||
return 1 | ||||
|
r17667 | def diff(ui, dst, src): | ||
|
r13354 | ui.status(_("searching for changed bookmarks\n")) | ||
|
r17667 | smarks = src.listkeys('bookmarks') | ||
dmarks = dst.listkeys('bookmarks') | ||||
|
r13354 | |||
|
r17667 | diff = sorted(set(smarks) - set(dmarks)) | ||
|
r13354 | for k in diff: | ||
|
r17667 | mark = ui.debugflag and smarks[k] or smarks[k][:12] | ||
|
r15984 | ui.write(" %-25s %s\n" % (k, mark)) | ||
|
r13354 | |||
if len(diff) <= 0: | ||||
ui.status(_("no changed bookmarks found\n")) | ||||
return 1 | ||||
return 0 | ||||
|
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: | ||
|
r18984 | return new.node() in obsolete.foreground(repo, [old.node()]) | ||
|
r17551 | else: | ||
|
r19951 | # still an independent clause as it is lazyer (and therefore faster) | ||
|
r17627 | return old.descendant(new) | ||