bookmarks.py
358 lines
| 13.5 KiB
| text/x-python
|
PythonLexer
/ hgext / bookmarks.py
|
r7239 | # Mercurial extension to provide the 'hg bookmark' command | ||
# | ||||
# Copyright 2008 David Soria Parra <dsp@php.net> | ||||
# | ||||
|
r8225 | # This software may be used and distributed according to the terms of the | ||
|
r10263 | # GNU General Public License version 2 or any later version. | ||
|
r7252 | |||
|
r8894 | '''track a line of development with movable markers | ||
|
r7239 | |||
|
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. | ||||
|
r7239 | |||
|
r11193 | It is possible to use bookmark names in every revision lookup (e.g. | ||
:hg:`merge`, :hg:`update`). | ||||
|
r7481 | |||
|
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 | ||||
|
r12083 | your configuration file:: | ||
|
r7481 | |||
|
r8892 | [bookmarks] | ||
track.current = True | ||||
|
r7481 | |||
|
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. | ||||
|
r7239 | ''' | ||
|
r7252 | |||
|
r7239 | from mercurial.i18n import _ | ||
|
r12714 | from mercurial.node import nullid, nullrev, bin, hex, short | ||
|
r11431 | from mercurial import util, commands, repair, extensions, pushkey, hg, url | ||
|
r13359 | from mercurial import encoding | ||
|
r13350 | from mercurial import bookmarks | ||
|
r7638 | import os | ||
|
r7239 | |||
|
r7255 | def bookmark(ui, repo, mark=None, rev=None, force=False, delete=False, rename=None): | ||
|
r8894 | '''track a line of development with movable markers | ||
|
r7250 | |||
|
r9251 | Bookmarks are pointers to certain commits that move when | ||
committing. Bookmarks are local. They can be renamed, copied and | ||||
|
r10973 | deleted. It is possible to use bookmark names in :hg:`merge` and | ||
:hg:`update` to merge and update respectively to a given bookmark. | ||||
|
r7250 | |||
|
r10973 | You can use :hg:`bookmark NAME` to set a bookmark on the working | ||
|
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. | ||||
|
r12772 | |||
Bookmarks can be pushed and pulled between repositories (see :hg:`help | ||||
push` and :hg:`help pull`). This requires the bookmark extension to be | ||||
enabled for both the local and remote repositories. | ||||
|
r7239 | ''' | ||
hexfn = ui.debugflag and hex or short | ||||
|
r10105 | marks = repo._bookmarks | ||
|
r7239 | cur = repo.changectx('.').node() | ||
|
r7255 | if rename: | ||
if rename not in marks: | ||||
|
r7251 | raise util.Abort(_("a bookmark of this name does not exist")) | ||
|
r7239 | if mark in marks and not force: | ||
raise util.Abort(_("a bookmark of the same name already exists")) | ||||
|
r7254 | if mark is None: | ||
raise util.Abort(_("new bookmark name required")) | ||||
|
r7255 | marks[mark] = marks[rename] | ||
del marks[rename] | ||||
|
r10107 | if repo._bookmarkcurrent == rename: | ||
|
r13350 | bookmarks.setcurrent(repo, mark) | ||
bookmarks.write(repo) | ||||
|
r7239 | return | ||
|
r7250 | |||
|
r7239 | if delete: | ||
|
r8527 | if mark is None: | ||
|
r7239 | raise util.Abort(_("bookmark name required")) | ||
if mark not in marks: | ||||
|
r7251 | raise util.Abort(_("a bookmark of this name does not exist")) | ||
|
r10107 | if mark == repo._bookmarkcurrent: | ||
|
r13350 | bookmarks.setcurrent(repo, None) | ||
|
r7239 | del marks[mark] | ||
|
r13350 | bookmarks.write(repo) | ||
|
r7239 | return | ||
|
r13031 | if mark is not None: | ||
|
r7259 | if "\n" in mark: | ||
raise util.Abort(_("bookmark name cannot contain newlines")) | ||||
|
r7260 | mark = mark.strip() | ||
|
r11704 | if not mark: | ||
raise util.Abort(_("bookmark names cannot consist entirely of " | ||||
"whitespace")) | ||||
|
r7239 | if mark in marks and not force: | ||
raise util.Abort(_("a bookmark of the same name already exists")) | ||||
|
r7250 | if ((mark in repo.branchtags() or mark == repo.dirstate.branch()) | ||
|
r7239 | and not force): | ||
|
r7252 | raise util.Abort( | ||
_("a bookmark cannot have the name of an existing branch")) | ||||
|
r7239 | if rev: | ||
marks[mark] = repo.lookup(rev) | ||||
else: | ||||
marks[mark] = repo.changectx('.').node() | ||||
|
r13350 | bookmarks.setcurrent(repo, mark) | ||
bookmarks.write(repo) | ||||
|
r7239 | return | ||
|
r8527 | if mark is None: | ||
|
r7258 | if rev: | ||
raise util.Abort(_("bookmark name required")) | ||||
|
r7239 | if len(marks) == 0: | ||
|
r10510 | ui.status(_("no bookmarks set\n")) | ||
|
r7239 | else: | ||
for bmark, n in marks.iteritems(): | ||||
|
r7481 | if ui.configbool('bookmarks', 'track.current'): | ||
|
r10107 | current = repo._bookmarkcurrent | ||
|
r10820 | if bmark == current and n == cur: | ||
prefix, label = '*', 'bookmarks.current' | ||||
else: | ||||
prefix, label = ' ', '' | ||||
|
r7481 | else: | ||
|
r10820 | if n == cur: | ||
prefix, label = '*', 'bookmarks.current' | ||||
else: | ||||
prefix, label = ' ', '' | ||||
|
r7481 | |||
|
r9459 | if ui.quiet: | ||
|
r10820 | ui.write("%s\n" % bmark, label=label) | ||
|
r9459 | else: | ||
ui.write(" %s %-25s %d:%s\n" % ( | ||||
|
r10820 | prefix, bmark, repo.changelog.rev(n), hexfn(n)), | ||
label=label) | ||||
|
r7239 | return | ||
def _revstostrip(changelog, node): | ||||
srev = changelog.rev(node) | ||||
tostrip = [srev] | ||||
saveheads = [] | ||||
|
r7283 | for r in xrange(srev, len(changelog)): | ||
|
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: | ||||
|
r7283 | if p not in tostrip and p > srev: | ||
|
r7239 | saveheads.append(p) | ||
return [r for r in tostrip if r not in saveheads] | ||||
|
r7638 | def strip(oldstrip, ui, repo, node, backup="all"): | ||
|
r7239 | """Strip bookmarks if revisions are stripped using | ||
the mercurial.strip method. This usually happens during | ||||
qpush and qpop""" | ||||
revisions = _revstostrip(repo.changelog, node) | ||||
|
r10105 | marks = repo._bookmarks | ||
|
r7239 | update = [] | ||
|
r7622 | for mark, n in marks.iteritems(): | ||
|
r7239 | if repo.changelog.rev(n) in revisions: | ||
update.append(mark) | ||||
|
r7280 | oldstrip(ui, repo, node, backup) | ||
|
r7239 | if len(update) > 0: | ||
for m in update: | ||||
marks[m] = repo.changectx('.').node() | ||||
|
r13350 | bookmarks.write(repo) | ||
|
r7239 | |||
def reposetup(ui, repo): | ||||
|
r9643 | if not repo.local(): | ||
|
r7239 | return | ||
class bookmark_repo(repo.__class__): | ||||
def lookup(self, key): | ||||
|
r12747 | if key in self._bookmarks: | ||
key = self._bookmarks[key] | ||||
|
r7239 | return super(bookmark_repo, self).lookup(key) | ||
|
r11373 | def pull(self, remote, heads=None, force=False): | ||
result = super(bookmark_repo, self).pull(remote, heads, force) | ||||
self.ui.debug("checking for updated bookmarks\n") | ||||
rb = remote.listkeys('bookmarks') | ||||
|
r11774 | changed = False | ||
|
r11373 | for k in rb.keys(): | ||
if k in self._bookmarks: | ||||
nr, nl = rb[k], self._bookmarks[k] | ||||
if nr in self: | ||||
cr = self[nr] | ||||
cl = self[nl] | ||||
if cl.rev() >= cr.rev(): | ||||
continue | ||||
if cr in cl.descendants(): | ||||
self._bookmarks[k] = cr.node() | ||||
|
r11774 | changed = True | ||
|
r11373 | self.ui.status(_("updating bookmark %s\n") % k) | ||
else: | ||||
self.ui.warn(_("not updating divergent" | ||||
" bookmark %s\n") % k) | ||||
|
r11774 | if changed: | ||
|
r13350 | bookmarks.write(repo) | ||
|
r11373 | |||
return result | ||||
|
r11374 | def push(self, remote, force=False, revs=None, newbranch=False): | ||
result = super(bookmark_repo, self).push(remote, force, revs, | ||||
newbranch) | ||||
self.ui.debug("checking for updated bookmarks\n") | ||||
rb = remote.listkeys('bookmarks') | ||||
for k in rb.keys(): | ||||
if k in self._bookmarks: | ||||
|
r13050 | nr, nl = rb[k], hex(self._bookmarks[k]) | ||
|
r11374 | if nr in self: | ||
cr = self[nr] | ||||
cl = self[nl] | ||||
if cl in cr.descendants(): | ||||
r = remote.pushkey('bookmarks', k, nr, nl) | ||||
if r: | ||||
self.ui.status(_("updating bookmark %s\n") % k) | ||||
else: | ||||
|
r11434 | self.ui.warn(_('updating bookmark %s' | ||
' failed!\n') % k) | ||||
|
r11374 | |||
return result | ||||
|
r11442 | def addchangegroup(self, *args, **kwargs): | ||
result = super(bookmark_repo, self).addchangegroup(*args, **kwargs) | ||||
|
r7239 | if result > 1: | ||
# We have more heads than before | ||||
return result | ||||
|
r8944 | node = self.changelog.tip() | ||
|
r13032 | parents = self.dirstate.parents() | ||
|
r13352 | bookmarks.update(self, parents, node) | ||
|
r7239 | return result | ||
repo.__class__ = bookmark_repo | ||||
|
r11378 | def pull(oldpull, ui, repo, source="default", **opts): | ||
# translate bookmark args to rev args for actual pull | ||||
if opts.get('bookmark'): | ||||
# this is an unpleasant hack as pull will do this internally | ||||
source, branches = hg.parseurl(ui.expandpath(source), | ||||
opts.get('branch')) | ||||
other = hg.repository(hg.remoteui(repo, opts), source) | ||||
rb = other.listkeys('bookmarks') | ||||
for b in opts['bookmark']: | ||||
if b not in rb: | ||||
raise util.Abort(_('remote bookmark %s not found!') % b) | ||||
opts.setdefault('rev', []).append(b) | ||||
result = oldpull(ui, repo, source, **opts) | ||||
# update specified bookmarks | ||||
if opts.get('bookmark'): | ||||
for b in opts['bookmark']: | ||||
# explicit pull overrides local bookmark if any | ||||
ui.status(_("importing bookmark %s\n") % b) | ||||
repo._bookmarks[b] = repo[rb[b]].node() | ||||
|
r13350 | bookmarks.write(repo) | ||
|
r11378 | |||
return result | ||||
|
r11379 | def push(oldpush, ui, repo, dest=None, **opts): | ||
dopush = True | ||||
if opts.get('bookmark'): | ||||
dopush = False | ||||
for b in opts['bookmark']: | ||||
if b in repo._bookmarks: | ||||
dopush = True | ||||
opts.setdefault('rev', []).append(b) | ||||
result = 0 | ||||
if dopush: | ||||
result = oldpush(ui, repo, dest, **opts) | ||||
if opts.get('bookmark'): | ||||
# this is an unpleasant hack as push will do this internally | ||||
dest = ui.expandpath(dest or 'default-push', dest or 'default') | ||||
dest, branches = hg.parseurl(dest, opts.get('branch')) | ||||
other = hg.repository(hg.remoteui(repo, opts), dest) | ||||
rb = other.listkeys('bookmarks') | ||||
for b in opts['bookmark']: | ||||
# explicit push overrides remote bookmark if any | ||||
if b in repo._bookmarks: | ||||
ui.status(_("exporting bookmark %s\n") % b) | ||||
new = repo[b].hex() | ||||
|
r11994 | elif b in rb: | ||
|
r11379 | ui.status(_("deleting remote bookmark %s\n") % b) | ||
new = '' # delete | ||||
|
r11994 | else: | ||
|
r12146 | ui.warn(_('bookmark %s does not exist on the local ' | ||
'or remote repository!\n') % b) | ||||
|
r11994 | return 2 | ||
|
r11379 | old = rb.get(b, '') | ||
r = other.pushkey('bookmarks', b, old, new) | ||||
if not r: | ||||
ui.warn(_('updating bookmark %s failed!\n') % b) | ||||
if not result: | ||||
result = 2 | ||||
return result | ||||
|
r11431 | def incoming(oldincoming, ui, repo, source="default", **opts): | ||
if opts.get('bookmarks'): | ||||
source, branches = hg.parseurl(ui.expandpath(source), opts.get('branch')) | ||||
other = hg.repository(hg.remoteui(repo, opts), source) | ||||
ui.status(_('comparing with %s\n') % url.hidepassword(source)) | ||||
|
r13354 | return bookmarks.diff(ui, repo, other) | ||
|
r11431 | else: | ||
|
r11444 | return oldincoming(ui, repo, source, **opts) | ||
|
r11431 | |||
|
r11443 | def outgoing(oldoutgoing, ui, repo, dest=None, **opts): | ||
|
r11431 | if opts.get('bookmarks'): | ||
|
r11443 | dest = ui.expandpath(dest or 'default-push', dest or 'default') | ||
dest, branches = hg.parseurl(dest, opts.get('branch')) | ||||
other = hg.repository(hg.remoteui(repo, opts), dest) | ||||
ui.status(_('comparing with %s\n') % url.hidepassword(dest)) | ||||
|
r13354 | return bookmarks.diff(ui, other, repo) | ||
|
r11431 | else: | ||
|
r11444 | return oldoutgoing(ui, repo, dest, **opts) | ||
|
r11431 | |||
|
r7638 | def uisetup(ui): | ||
extensions.wrapfunction(repair, "strip", strip) | ||||
if ui.configbool('bookmarks', 'track.current'): | ||||
extensions.wrapcommand(commands.table, 'update', updatecurbookmark) | ||||
|
r11378 | |||
entry = extensions.wrapcommand(commands.table, 'pull', pull) | ||||
entry[1].append(('B', 'bookmark', [], | ||||
|
r12302 | _("bookmark to import"), | ||
_('BOOKMARK'))) | ||||
|
r11379 | entry = extensions.wrapcommand(commands.table, 'push', push) | ||
entry[1].append(('B', 'bookmark', [], | ||||
|
r12302 | _("bookmark to export"), | ||
_('BOOKMARK'))) | ||||
|
r11431 | entry = extensions.wrapcommand(commands.table, 'incoming', incoming) | ||
entry[1].append(('B', 'bookmarks', False, | ||||
_("compare bookmark"))) | ||||
entry = extensions.wrapcommand(commands.table, 'outgoing', outgoing) | ||||
entry[1].append(('B', 'bookmarks', False, | ||||
_("compare bookmark"))) | ||||
|
r11378 | |||
|
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] | ||||
|
r13350 | bookmarks.setcurrent(repo, rev) | ||
|
r7481 | return res | ||
|
r7239 | cmdtable = { | ||
"bookmarks": | ||||
(bookmark, | ||||
[('f', 'force', False, _('force')), | ||||
|
r11321 | ('r', 'rev', '', _('revision'), _('REV')), | ||
|
r7239 | ('d', 'delete', False, _('delete a given bookmark')), | ||
|
r11321 | ('m', 'rename', '', _('rename a given bookmark'), _('NAME'))], | ||
|
r7818 | _('hg bookmarks [-f] [-d] [-m NAME] [-r REV] [NAME]')), | ||
|
r7239 | } | ||