bookmarks.py
343 lines
| 11.8 KiB
| text/x-python
|
PythonLexer
/ hgext / bookmarks.py
David Soria Parra
|
r7239 | # Mercurial extension to provide the 'hg bookmark' command | ||
# | ||||
# Copyright 2008 David Soria Parra <dsp@php.net> | ||||
# | ||||
Martin Geisler
|
r8225 | # This software may be used and distributed according to the terms of the | ||
Matt Mackall
|
r10263 | # GNU General Public License version 2 or any later version. | ||
Joel Rosdahl
|
r7252 | |||
Cédric Duval
|
r8894 | '''track a line of development with movable markers | ||
David Soria Parra
|
r7239 | |||
Martin Geisler
|
r9251 | Bookmarks are local movable markers to changesets. Every bookmark | ||
points to a changeset identified by its hash. If you commit a | ||||
changeset that is based on a changeset that has a bookmark on it, the | ||||
bookmark shifts to the new changeset. | ||||
David Soria Parra
|
r7239 | |||
Martin Geisler
|
r9251 | It is possible to use bookmark names in every revision lookup (e.g. hg | ||
merge, hg update). | ||||
David Soria Parra
|
r7481 | |||
Martin Geisler
|
r9251 | By default, when several bookmarks point to the same changeset, they | ||
will all move forward together. It is possible to obtain a more | ||||
git-like experience by adding the following configuration option to | ||||
your .hgrc:: | ||||
David Soria Parra
|
r7481 | |||
Cédric Duval
|
r8892 | [bookmarks] | ||
track.current = True | ||||
David Soria Parra
|
r7481 | |||
Martin Geisler
|
r9251 | This will cause Mercurial to track the bookmark that you are currently | ||
using, and only update it. This is similar to git's approach to | ||||
branching. | ||||
David Soria Parra
|
r7239 | ''' | ||
Joel Rosdahl
|
r7252 | |||
David Soria Parra
|
r7239 | from mercurial.i18n import _ | ||
Matt Mackall
|
r7638 | from mercurial.node import nullid, nullrev, hex, short | ||
Brodie Rao
|
r10463 | from mercurial import util, commands, repair, extensions | ||
Matt Mackall
|
r7638 | import os | ||
David Soria Parra
|
r7239 | |||
Nicolas Dumazet
|
r10106 | def write(repo): | ||
David Soria Parra
|
r7239 | '''Write bookmarks | ||
Joel Rosdahl
|
r7250 | |||
David Soria Parra
|
r7239 | 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. | ||||
''' | ||||
Nicolas Dumazet
|
r10106 | refs = repo._bookmarks | ||
Joel Rosdahl
|
r7253 | if os.path.exists(repo.join('bookmarks')): | ||
util.copyfile(repo.join('bookmarks'), repo.join('undo.bookmarks')) | ||||
Nicolas Dumazet
|
r10107 | if repo._bookmarkcurrent not in refs: | ||
David Soria Parra
|
r7481 | setcurrent(repo, None) | ||
Matt Mackall
|
r8862 | wlock = repo.wlock() | ||
try: | ||||
file = repo.opener('bookmarks', 'w', atomictemp=True) | ||||
for refspec, node in refs.iteritems(): | ||||
file.write("%s %s\n" % (hex(node), refspec)) | ||||
file.rename() | ||||
finally: | ||||
wlock.release() | ||||
David Soria Parra
|
r7239 | |||
David Soria Parra
|
r7481 | 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>). | ||||
Wagner Bruna
|
r8087 | The name is recorded in .hg/bookmarks.current | ||
David Soria Parra
|
r7481 | ''' | ||
Nicolas Dumazet
|
r10107 | current = repo._bookmarkcurrent | ||
if current == mark: | ||||
David Soria Parra
|
r7481 | return | ||
David Soria Parra
|
r7484 | |||
Nicolas Dumazet
|
r10105 | refs = repo._bookmarks | ||
David Soria Parra
|
r7484 | |||
Benoit Boissinot
|
r7491 | # do not update if we do update to a rev equal to the current bookmark | ||
Alex Unden
|
r7817 | if (mark and mark not in refs and | ||
Nicolas Dumazet
|
r10107 | current and refs[current] == repo.changectx('.').node()): | ||
David Soria Parra
|
r7484 | return | ||
David Soria Parra
|
r7481 | if mark not in refs: | ||
mark = '' | ||||
Matt Mackall
|
r8862 | wlock = repo.wlock() | ||
try: | ||||
file = repo.opener('bookmarks.current', 'w', atomictemp=True) | ||||
file.write(mark) | ||||
file.rename() | ||||
finally: | ||||
wlock.release() | ||||
David Soria Parra
|
r7481 | repo._bookmarkcurrent = mark | ||
Joel Rosdahl
|
r7255 | def bookmark(ui, repo, mark=None, rev=None, force=False, delete=False, rename=None): | ||
Cédric Duval
|
r8894 | '''track a line of development with movable markers | ||
Joel Rosdahl
|
r7250 | |||
Martin Geisler
|
r9251 | Bookmarks are pointers to certain commits that move when | ||
committing. Bookmarks are local. They can be renamed, copied and | ||||
Martin Geisler
|
r10973 | deleted. It is possible to use bookmark names in :hg:`merge` and | ||
:hg:`update` to merge and update respectively to a given bookmark. | ||||
Joel Rosdahl
|
r7250 | |||
Martin Geisler
|
r10973 | You can use :hg:`bookmark NAME` to set a bookmark on the working | ||
Martin Geisler
|
r9251 | directory's parent revision with the given name. If you specify | ||
a revision using -r REV (where REV may be an existing bookmark), | ||||
the bookmark is assigned to that revision. | ||||
David Soria Parra
|
r7239 | ''' | ||
hexfn = ui.debugflag and hex or short | ||||
Nicolas Dumazet
|
r10105 | marks = repo._bookmarks | ||
David Soria Parra
|
r7239 | cur = repo.changectx('.').node() | ||
Joel Rosdahl
|
r7255 | if rename: | ||
if rename not in marks: | ||||
Joel Rosdahl
|
r7251 | raise util.Abort(_("a bookmark of this name does not exist")) | ||
David Soria Parra
|
r7239 | if mark in marks and not force: | ||
raise util.Abort(_("a bookmark of the same name already exists")) | ||||
Joel Rosdahl
|
r7254 | if mark is None: | ||
raise util.Abort(_("new bookmark name required")) | ||||
Joel Rosdahl
|
r7255 | marks[mark] = marks[rename] | ||
del marks[rename] | ||||
Nicolas Dumazet
|
r10107 | if repo._bookmarkcurrent == rename: | ||
David Soria Parra
|
r7550 | setcurrent(repo, mark) | ||
Nicolas Dumazet
|
r10106 | write(repo) | ||
David Soria Parra
|
r7239 | return | ||
Joel Rosdahl
|
r7250 | |||
David Soria Parra
|
r7239 | if delete: | ||
Martin Geisler
|
r8527 | if mark is None: | ||
David Soria Parra
|
r7239 | raise util.Abort(_("bookmark name required")) | ||
if mark not in marks: | ||||
Joel Rosdahl
|
r7251 | raise util.Abort(_("a bookmark of this name does not exist")) | ||
Nicolas Dumazet
|
r10107 | if mark == repo._bookmarkcurrent: | ||
Alex Unden
|
r7817 | setcurrent(repo, None) | ||
David Soria Parra
|
r7239 | del marks[mark] | ||
Nicolas Dumazet
|
r10106 | write(repo) | ||
David Soria Parra
|
r7239 | return | ||
if mark != None: | ||||
Joel Rosdahl
|
r7259 | if "\n" in mark: | ||
raise util.Abort(_("bookmark name cannot contain newlines")) | ||||
Joel Rosdahl
|
r7260 | mark = mark.strip() | ||
David Soria Parra
|
r7239 | if mark in marks and not force: | ||
raise util.Abort(_("a bookmark of the same name already exists")) | ||||
Joel Rosdahl
|
r7250 | if ((mark in repo.branchtags() or mark == repo.dirstate.branch()) | ||
David Soria Parra
|
r7239 | and not force): | ||
Joel Rosdahl
|
r7252 | raise util.Abort( | ||
_("a bookmark cannot have the name of an existing branch")) | ||||
David Soria Parra
|
r7239 | if rev: | ||
marks[mark] = repo.lookup(rev) | ||||
else: | ||||
marks[mark] = repo.changectx('.').node() | ||||
David Soria Parra
|
r7816 | setcurrent(repo, mark) | ||
Nicolas Dumazet
|
r10106 | write(repo) | ||
David Soria Parra
|
r7239 | return | ||
Martin Geisler
|
r8527 | if mark is None: | ||
Joel Rosdahl
|
r7258 | if rev: | ||
raise util.Abort(_("bookmark name required")) | ||||
David Soria Parra
|
r7239 | if len(marks) == 0: | ||
Benoit Boissinot
|
r10510 | ui.status(_("no bookmarks set\n")) | ||
David Soria Parra
|
r7239 | else: | ||
for bmark, n in marks.iteritems(): | ||||
David Soria Parra
|
r7481 | if ui.configbool('bookmarks', 'track.current'): | ||
Nicolas Dumazet
|
r10107 | current = repo._bookmarkcurrent | ||
Brodie Rao
|
r10820 | if bmark == current and n == cur: | ||
prefix, label = '*', 'bookmarks.current' | ||||
else: | ||||
prefix, label = ' ', '' | ||||
David Soria Parra
|
r7481 | else: | ||
Brodie Rao
|
r10820 | if n == cur: | ||
prefix, label = '*', 'bookmarks.current' | ||||
else: | ||||
prefix, label = ' ', '' | ||||
David Soria Parra
|
r7481 | |||
Steve Losh
|
r9459 | if ui.quiet: | ||
Brodie Rao
|
r10820 | ui.write("%s\n" % bmark, label=label) | ||
Steve Losh
|
r9459 | else: | ||
ui.write(" %s %-25s %d:%s\n" % ( | ||||
Brodie Rao
|
r10820 | prefix, bmark, repo.changelog.rev(n), hexfn(n)), | ||
label=label) | ||||
David Soria Parra
|
r7239 | return | ||
def _revstostrip(changelog, node): | ||||
srev = changelog.rev(node) | ||||
tostrip = [srev] | ||||
saveheads = [] | ||||
Benoit Boissinot
|
r7283 | for r in xrange(srev, len(changelog)): | ||
David Soria Parra
|
r7239 | parents = changelog.parentrevs(r) | ||
if parents[0] in tostrip or parents[1] in tostrip: | ||||
tostrip.append(r) | ||||
if parents[1] != nullrev: | ||||
for p in parents: | ||||
Benoit Boissinot
|
r7283 | if p not in tostrip and p > srev: | ||
David Soria Parra
|
r7239 | saveheads.append(p) | ||
return [r for r in tostrip if r not in saveheads] | ||||
Matt Mackall
|
r7638 | def strip(oldstrip, ui, repo, node, backup="all"): | ||
David Soria Parra
|
r7239 | """Strip bookmarks if revisions are stripped using | ||
the mercurial.strip method. This usually happens during | ||||
qpush and qpop""" | ||||
revisions = _revstostrip(repo.changelog, node) | ||||
Nicolas Dumazet
|
r10105 | marks = repo._bookmarks | ||
David Soria Parra
|
r7239 | update = [] | ||
Dirkjan Ochtman
|
r7622 | for mark, n in marks.iteritems(): | ||
David Soria Parra
|
r7239 | if repo.changelog.rev(n) in revisions: | ||
update.append(mark) | ||||
Benoit Boissinot
|
r7280 | oldstrip(ui, repo, node, backup) | ||
David Soria Parra
|
r7239 | if len(update) > 0: | ||
for m in update: | ||||
marks[m] = repo.changectx('.').node() | ||||
Nicolas Dumazet
|
r10106 | write(repo) | ||
David Soria Parra
|
r7239 | |||
def reposetup(ui, repo): | ||||
Alexander Solovyov
|
r9643 | if not repo.local(): | ||
David Soria Parra
|
r7239 | return | ||
class bookmark_repo(repo.__class__): | ||||
Nicolas Dumazet
|
r10105 | |||
@util.propertycache | ||||
def _bookmarks(self): | ||||
Nicolas Dumazet
|
r10109 | '''Parse .hg/bookmarks file and return a dictionary | ||
Bookmarks are stored as {HASH}\\s{NAME}\\n (localtags format) values | ||||
in the .hg/bookmarks file. They are read returned as a dictionary | ||||
with name => hash values. | ||||
''' | ||||
try: | ||||
bookmarks = {} | ||||
for line in self.opener('bookmarks'): | ||||
sha, refspec = line.strip().split(' ', 1) | ||||
bookmarks[refspec] = super(bookmark_repo, self).lookup(sha) | ||||
except: | ||||
pass | ||||
return bookmarks | ||||
David Soria Parra
|
r7239 | |||
Nicolas Dumazet
|
r10107 | @util.propertycache | ||
def _bookmarkcurrent(self): | ||||
Nicolas Dumazet
|
r10109 | '''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 | ||||
if os.path.exists(self.join('bookmarks.current')): | ||||
file = self.opener('bookmarks.current') | ||||
# No readline() in posixfile_nt, reading everything is cheap | ||||
mark = (file.readlines() or [''])[0] | ||||
if mark == '': | ||||
mark = None | ||||
file.close() | ||||
return mark | ||||
Nicolas Dumazet
|
r10107 | |||
Steve Borho
|
r10882 | def rollback(self, *args): | ||
David Soria Parra
|
r7239 | if os.path.exists(self.join('undo.bookmarks')): | ||
util.rename(self.join('undo.bookmarks'), self.join('bookmarks')) | ||||
Steve Borho
|
r10882 | return super(bookmark_repo, self).rollback(*args) | ||
David Soria Parra
|
r7239 | |||
def lookup(self, key): | ||||
if key in self._bookmarks: | ||||
key = self._bookmarks[key] | ||||
return super(bookmark_repo, self).lookup(key) | ||||
Nicolas Dumazet
|
r10108 | def _bookmarksupdate(self, parents, node): | ||
Nicolas Dumazet
|
r10105 | marks = self._bookmarks | ||
David Soria Parra
|
r7239 | update = False | ||
David Soria Parra
|
r9236 | if ui.configbool('bookmarks', 'track.current'): | ||
Nicolas Dumazet
|
r10107 | mark = self._bookmarkcurrent | ||
David Soria Parra
|
r9236 | if mark and marks[mark] in parents: | ||
David Soria Parra
|
r7239 | marks[mark] = node | ||
update = True | ||||
David Soria Parra
|
r9236 | else: | ||
for mark, n in marks.items(): | ||||
if n in parents: | ||||
marks[mark] = node | ||||
update = True | ||||
David Soria Parra
|
r7239 | if update: | ||
Nicolas Dumazet
|
r10106 | write(self) | ||
Nicolas Dumazet
|
r10108 | |||
David Soria Parra
|
r9235 | def commitctx(self, ctx, error=False): | ||
David Soria Parra
|
r7239 | """Add a revision to the repository and | ||
move the bookmark""" | ||||
Matt Mackall
|
r8862 | wlock = self.wlock() # do both commit and bookmark with lock held | ||
try: | ||||
David Soria Parra
|
r9235 | node = super(bookmark_repo, self).commitctx(ctx, error) | ||
Matt Mackall
|
r8862 | if node is None: | ||
return None | ||||
Isaac Jurado
|
r8944 | parents = self.changelog.parents(node) | ||
Matt Mackall
|
r8862 | if parents[1] == nullid: | ||
parents = (parents[0],) | ||||
Nicolas Dumazet
|
r10108 | |||
self._bookmarksupdate(parents, node) | ||||
Matt Mackall
|
r8862 | return node | ||
finally: | ||||
wlock.release() | ||||
David Soria Parra
|
r7239 | |||
def addchangegroup(self, source, srctype, url, emptyok=False): | ||||
Isaac Jurado
|
r8944 | parents = self.dirstate.parents() | ||
David Soria Parra
|
r7239 | |||
Joel Rosdahl
|
r7252 | result = super(bookmark_repo, self).addchangegroup( | ||
source, srctype, url, emptyok) | ||||
David Soria Parra
|
r7239 | if result > 1: | ||
# We have more heads than before | ||||
return result | ||||
Isaac Jurado
|
r8944 | node = self.changelog.tip() | ||
Nicolas Dumazet
|
r10108 | |||
self._bookmarksupdate(parents, node) | ||||
David Soria Parra
|
r7239 | return result | ||
Greg Ward
|
r9145 | def _findtags(self): | ||
Martin Geisler
|
r7480 | """Merge bookmarks with normal tags""" | ||
Greg Ward
|
r9145 | (tags, tagtypes) = super(bookmark_repo, self)._findtags() | ||
Nicolas Dumazet
|
r10105 | tags.update(self._bookmarks) | ||
Greg Ward
|
r9145 | return (tags, tagtypes) | ||
Martin Geisler
|
r7480 | |||
Paul Molodowitch
|
r10597 | if hasattr(repo, 'invalidate'): | ||
def invalidate(self): | ||||
super(bookmark_repo, self).invalidate() | ||||
for attr in ('_bookmarks', '_bookmarkcurrent'): | ||||
if attr in self.__dict__: | ||||
Isaac Jurado
|
r10956 | delattr(self, attr) | ||
Paul Molodowitch
|
r10597 | |||
David Soria Parra
|
r7239 | repo.__class__ = bookmark_repo | ||
Matt Mackall
|
r7638 | def uisetup(ui): | ||
extensions.wrapfunction(repair, "strip", strip) | ||||
if ui.configbool('bookmarks', 'track.current'): | ||||
extensions.wrapcommand(commands.table, 'update', updatecurbookmark) | ||||
David Soria Parra
|
r7481 | def updatecurbookmark(orig, ui, repo, *args, **opts): | ||
'''Set the current bookmark | ||||
If the user updates to a bookmark we update the .hg/bookmarks.current | ||||
file. | ||||
''' | ||||
res = orig(ui, repo, *args, **opts) | ||||
rev = opts['rev'] | ||||
if not rev and len(args) > 0: | ||||
rev = args[0] | ||||
setcurrent(repo, rev) | ||||
return res | ||||
David Soria Parra
|
r7239 | cmdtable = { | ||
"bookmarks": | ||||
(bookmark, | ||||
[('f', 'force', False, _('force')), | ||||
('r', 'rev', '', _('revision')), | ||||
('d', 'delete', False, _('delete a given bookmark')), | ||||
Joel Rosdahl
|
r7255 | ('m', 'rename', '', _('rename a given bookmark'))], | ||
Benoit Allard
|
r7818 | _('hg bookmarks [-f] [-d] [-m NAME] [-r REV] [NAME]')), | ||
David Soria Parra
|
r7239 | } | ||
Brodie Rao
|
r10826 | |||
colortable = {'bookmarks.current': 'green'} | ||||