bookmarks.py
284 lines
| 8.8 KiB
| text/x-python
|
PythonLexer
/ mercurial / bookmarks.py
Matt Mackall
|
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 _ | ||||
Alexander Solovyov
|
r14064 | from mercurial.node import hex | ||
Pierre-Yves David
|
r17551 | from mercurial import encoding, error, util, obsolete, phases | ||
Benoit Boissinot
|
r14027 | import errno, os | ||
Matt Mackall
|
r13350 | |||
David Soria Parra
|
r13425 | def valid(mark): | ||
for c in (':', '\0', '\n', '\r'): | ||||
if c in mark: | ||||
return False | ||||
return True | ||||
Matt Mackall
|
r13351 | def read(repo): | ||
'''Parse .hg/bookmarks file and return a dictionary | ||||
Bookmarks are stored as {HASH}\\s{NAME}\\n (localtags format) values | ||||
in the .hg/bookmarks file. | ||||
Read the file and return a (name=>nodeid) dictionary | ||||
''' | ||||
Benoit Boissinot
|
r14027 | bookmarks = {} | ||
Matt Mackall
|
r13351 | try: | ||
for line in repo.opener('bookmarks'): | ||||
Pierre-Yves David
|
r14845 | line = line.strip() | ||
Matt Mackall
|
r14848 | if not line: | ||
continue | ||||
Pierre-Yves David
|
r14845 | if ' ' not in line: | ||
Matt Mackall
|
r14848 | repo.ui.warn(_('malformed line in .hg/bookmarks: %r\n') % line) | ||
Pierre-Yves David
|
r14845 | continue | ||
Matt Mackall
|
r14847 | sha, refspec = line.split(' ', 1) | ||
Matt Mackall
|
r13351 | refspec = encoding.tolocal(refspec) | ||
Benoit Boissinot
|
r14027 | try: | ||
bookmarks[refspec] = repo.changelog.lookup(sha) | ||||
Matt Mackall
|
r16573 | except LookupError: | ||
Benoit Boissinot
|
r14027 | pass | ||
except IOError, inst: | ||||
if inst.errno != errno.ENOENT: | ||||
raise | ||||
Matt Mackall
|
r13351 | return bookmarks | ||
def readcurrent(repo): | ||||
'''Get the current bookmark | ||||
If we use gittishsh branches we have a current bookmark that | ||||
we are on. This function returns the name of the bookmark. It | ||||
is stored in .hg/bookmarks.current | ||||
''' | ||||
mark = None | ||||
Benoit Boissinot
|
r14027 | try: | ||
Matt Mackall
|
r13351 | file = repo.opener('bookmarks.current') | ||
Benoit Boissinot
|
r14027 | except IOError, inst: | ||
if inst.errno != errno.ENOENT: | ||||
raise | ||||
return None | ||||
try: | ||||
Mads Kiilerich
|
r17425 | # No readline() in osutil.posixfile, reading everything is cheap | ||
David Soria Parra
|
r13381 | mark = encoding.tolocal((file.readlines() or [''])[0]) | ||
Benoit Boissinot
|
r13627 | if mark == '' or mark not in repo._bookmarks: | ||
Matt Mackall
|
r13351 | mark = None | ||
Benoit Boissinot
|
r14027 | finally: | ||
Matt Mackall
|
r13351 | file.close() | ||
return mark | ||||
Matt Mackall
|
r13350 | def write(repo): | ||
'''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. | ||||
''' | ||||
refs = repo._bookmarks | ||||
if repo._bookmarkcurrent not in refs: | ||||
setcurrent(repo, None) | ||||
David Soria Parra
|
r13425 | for mark in refs.keys(): | ||
if not valid(mark): | ||||
raise util.Abort(_("bookmark '%s' contains illegal " | ||||
"character" % mark)) | ||||
Matt Mackall
|
r15908 | wlock = repo.wlock() | ||
Matt Mackall
|
r13350 | try: | ||
David Soria Parra
|
r13425 | |||
Matt Mackall
|
r13350 | file = repo.opener('bookmarks', 'w', atomictemp=True) | ||
for refspec, node in refs.iteritems(): | ||||
file.write("%s %s\n" % (hex(node), encoding.fromlocal(refspec))) | ||||
Greg Ward
|
r15057 | file.close() | ||
Matt Mackall
|
r13350 | |||
# touch 00changelog.i so hgweb reloads bookmarks (no lock needed) | ||||
try: | ||||
os.utime(repo.sjoin('00changelog.i'), None) | ||||
except OSError: | ||||
pass | ||||
finally: | ||||
Matt Mackall
|
r15908 | wlock.release() | ||
Matt Mackall
|
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 | ||||
''' | ||||
current = repo._bookmarkcurrent | ||||
if current == mark: | ||||
return | ||||
David Soria Parra
|
r13647 | if mark not in repo._bookmarks: | ||
Matt Mackall
|
r13350 | mark = '' | ||
David Soria Parra
|
r13425 | if not valid(mark): | ||
raise util.Abort(_("bookmark '%s' contains illegal " | ||||
"character" % mark)) | ||||
Matt Mackall
|
r15908 | wlock = repo.wlock() | ||
Matt Mackall
|
r13350 | try: | ||
file = repo.opener('bookmarks.current', 'w', atomictemp=True) | ||||
LUO Zheng
|
r14559 | file.write(encoding.fromlocal(mark)) | ||
Greg Ward
|
r15057 | file.close() | ||
Matt Mackall
|
r13350 | finally: | ||
Matt Mackall
|
r15908 | wlock.release() | ||
Matt Mackall
|
r13350 | repo._bookmarkcurrent = mark | ||
Matt Mackall
|
r13352 | |||
Idan Kamara
|
r16191 | def unsetcurrent(repo): | ||
wlock = repo.wlock() | ||||
try: | ||||
Gilles Moris
|
r16194 | try: | ||
util.unlink(repo.join('bookmarks.current')) | ||||
repo._bookmarkcurrent = None | ||||
except OSError, inst: | ||||
if inst.errno != errno.ENOENT: | ||||
raise | ||||
Idan Kamara
|
r16191 | finally: | ||
wlock.release() | ||||
David Soria Parra
|
r13663 | def updatecurrentbookmark(repo, oldnode, curbranch): | ||
try: | ||||
Brodie Rao
|
r16719 | return update(repo, oldnode, repo.branchtip(curbranch)) | ||
except error.RepoLookupError: | ||||
David Soria Parra
|
r13663 | if curbranch == "default": # no default branch! | ||
Kevin Bullock
|
r15621 | return update(repo, oldnode, repo.lookup("tip")) | ||
David Soria Parra
|
r13663 | else: | ||
raise util.Abort(_("branch %s not found") % curbranch) | ||||
Matt Mackall
|
r13352 | def update(repo, parents, node): | ||
marks = repo._bookmarks | ||||
update = False | ||||
David Soria Parra
|
r16706 | cur = repo._bookmarkcurrent | ||
if not cur: | ||||
return False | ||||
toupdate = [b for b in marks if b.split('@', 1)[0] == cur.split('@', 1)[0]] | ||||
for mark in toupdate: | ||||
if mark and marks[mark] in parents: | ||||
old = repo[marks[mark]] | ||||
new = repo[node] | ||||
if new in old.descendants() and mark == cur: | ||||
marks[cur] = new.node() | ||||
update = True | ||||
if mark != cur: | ||||
del marks[mark] | ||||
Matt Mackall
|
r13352 | if update: | ||
Augie Fackler
|
r15237 | repo._writebookmarks(marks) | ||
Kevin Bullock
|
r15621 | return update | ||
Matt Mackall
|
r13353 | |||
def listbookmarks(repo): | ||||
# We may try to list bookmarks on a repo type that does not | ||||
# support it (e.g., statichttprepository). | ||||
Augie Fackler
|
r14946 | marks = getattr(repo, '_bookmarks', {}) | ||
Matt Mackall
|
r13353 | |||
d = {} | ||||
Augie Fackler
|
r14946 | for k, v in marks.iteritems(): | ||
Matt Mackall
|
r15613 | # don't expose local divergent bookmarks | ||
Kevin Bullock
|
r16276 | if '@' not in k or k.endswith('@'): | ||
Matt Mackall
|
r15613 | d[k] = hex(v) | ||
Matt Mackall
|
r13353 | return d | ||
def pushbookmark(repo, key, old, new): | ||||
Matt Mackall
|
r15908 | w = repo.wlock() | ||
Matt Mackall
|
r13353 | try: | ||
marks = repo._bookmarks | ||||
if hex(marks.get(key, '')) != old: | ||||
return False | ||||
if new == '': | ||||
del marks[key] | ||||
else: | ||||
if new not in repo: | ||||
return False | ||||
marks[key] = repo[new].node() | ||||
write(repo) | ||||
return True | ||||
finally: | ||||
Matt Mackall
|
r15908 | w.release() | ||
Matt Mackall
|
r13354 | |||
Matt Mackall
|
r15614 | def updatefromremote(ui, repo, remote, path): | ||
David Soria Parra
|
r13646 | ui.debug("checking for updated bookmarks\n") | ||
rb = remote.listkeys('bookmarks') | ||||
changed = False | ||||
for k in rb.keys(): | ||||
if k in repo._bookmarks: | ||||
nr, nl = rb[k], repo._bookmarks[k] | ||||
if nr in repo: | ||||
cr = repo[nr] | ||||
cl = repo[nl] | ||||
if cl.rev() >= cr.rev(): | ||||
continue | ||||
Pierre-Yves David
|
r17550 | if validdest(repo, cl, cr): | ||
David Soria Parra
|
r13646 | repo._bookmarks[k] = cr.node() | ||
changed = True | ||||
ui.status(_("updating bookmark %s\n") % k) | ||||
else: | ||||
Matt Mackall
|
r15614 | # find a unique @ suffix | ||
Matt Mackall
|
r15613 | for x in range(1, 100): | ||
n = '%s@%d' % (k, x) | ||||
if n not in repo._bookmarks: | ||||
break | ||||
Matt Mackall
|
r15614 | # try to use an @pathalias suffix | ||
# if an @pathalias already exists, we overwrite (update) it | ||||
for p, u in ui.configitems("paths"): | ||||
if path == u: | ||||
n = '%s@%s' % (k, p) | ||||
Matt Mackall
|
r15613 | repo._bookmarks[n] = cr.node() | ||
changed = True | ||||
ui.warn(_("divergent bookmark %s stored as %s\n") % (k, n)) | ||||
Levi Bard
|
r16697 | elif rb[k] in repo: | ||
# add remote bookmarks for changes we already have | ||||
repo._bookmarks[k] = repo[rb[k]].node() | ||||
changed = True | ||||
ui.status(_("adding remote bookmark %s\n") % k) | ||||
Matt Mackall
|
r15613 | |||
David Soria Parra
|
r13646 | if changed: | ||
write(repo) | ||||
Matt Mackall
|
r13354 | def diff(ui, repo, remote): | ||
ui.status(_("searching for changed bookmarks\n")) | ||||
lmarks = repo.listkeys('bookmarks') | ||||
rmarks = remote.listkeys('bookmarks') | ||||
diff = sorted(set(rmarks) - set(lmarks)) | ||||
for k in diff: | ||||
David Soria Parra
|
r15984 | mark = ui.debugflag and rmarks[k] or rmarks[k][:12] | ||
ui.write(" %-25s %s\n" % (k, mark)) | ||||
Matt Mackall
|
r13354 | |||
if len(diff) <= 0: | ||||
ui.status(_("no changed bookmarks found\n")) | ||||
return 1 | ||||
return 0 | ||||
Pierre-Yves David
|
r17550 | |||
def validdest(repo, old, new): | ||||
"""Is the new bookmark destination a valid update from the old one""" | ||||
Pierre-Yves David
|
r17551 | if old == new: | ||
# Old == new -> nothing to update. | ||||
validdests = () | ||||
elif not old: | ||||
# old is nullrev, anything is valid. | ||||
# (new != nullrev has been excluded by the previous check) | ||||
validdests = (new,) | ||||
elif repo.obsstore: | ||||
# We only need this complicated logic if there is obsolescence | ||||
# XXX will probably deserve an optimised rset. | ||||
validdests = set([old]) | ||||
plen = -1 | ||||
# compute the whole set of successors or descendants | ||||
while len(validdests) != plen: | ||||
plen = len(validdests) | ||||
succs = set(c.node() for c in validdests) | ||||
for c in validdests: | ||||
if c.phase() > phases.public: | ||||
# obsolescence marker does not apply to public changeset | ||||
succs.update(obsolete.anysuccessors(repo.obsstore, | ||||
c.node())) | ||||
validdests = set(repo.set('%ln::', succs)) | ||||
validdests.remove(old) | ||||
else: | ||||
validdests = old.descendants() | ||||
return new in validdests | ||||