sapling/remotenames.py

767 lines
25 KiB
Python
Raw Normal View History

2010-01-04 07:37:45 +03:00
import os
import errno
2010-01-04 07:37:45 +03:00
2015-02-10 22:43:07 +03:00
from mercurial import bookmarks
from mercurial import commands
from mercurial import encoding
2015-01-06 22:18:07 +03:00
from mercurial import error
from mercurial import exchange
from mercurial import extensions
2010-01-04 07:37:45 +03:00
from mercurial import hg
2015-02-10 20:18:13 +03:00
from mercurial import localrepo
2015-01-06 22:18:07 +03:00
from mercurial import namespaces
from mercurial import obsolete
from mercurial import repoview
2014-03-18 22:46:10 +04:00
from mercurial import revset
from mercurial import scmutil
2014-03-18 22:46:10 +04:00
from mercurial import templatekw
2015-01-06 22:18:07 +03:00
from mercurial import url
from mercurial import util
from mercurial.i18n import _
from mercurial.node import hex, short
2010-01-11 02:24:02 +03:00
from hgext import schemes
2010-01-04 07:37:45 +03:00
_remotenames = {
"bookmarks": {},
"branches": {},
}
def expush(orig, repo, remote, *args, **kwargs):
res = orig(repo, remote, *args, **kwargs)
lock = repo.lock()
try:
2015-03-03 10:56:57 +03:00
pullremotenames(repo, remote)
finally:
lock.release()
return res
def expull(orig, repo, remote, *args, **kwargs):
res = orig(repo, remote, *args, **kwargs)
pullremotenames(repo, remote)
2015-02-10 07:14:55 +03:00
writedistance(repo)
return res
def pullremotenames(repo, remote):
lock = repo.lock()
try:
path = activepath(repo.ui, remote)
if path:
2015-03-03 10:56:57 +03:00
# on a push, we don't want to keep obsolete heads since
# they won't show up as heads on the next pull, so we
# remove them here otherwise we would require the user
# to issue a pull to refresh .hg/remotenames
bmap = {}
repo = repo.unfiltered()
for branch, nodes in remote.branchmap().iteritems():
bmap[branch] = [n for n in nodes if not repo[n].obsolete()]
saveremotenames(repo, path, bmap, remote.listkeys('bookmarks'))
finally:
lock.release()
def blockerhook(orig, repo, *args, **kwargs):
blockers = orig(repo)
unblock = util.safehasattr(repo, '_unblockhiddenremotenames')
if not unblock:
return blockers
# add remotenames to blockers by looping over all names in our own cache
cl = repo.changelog
for remotename in _remotenames.keys():
rname = 'remote' + remotename
try:
ns = repo.names[rname]
except KeyError:
continue
for name in ns.listnames(repo):
blockers.update(cl.rev(node) for node in ns.nodes(repo, name))
return blockers
def exupdatefromremote(orig, ui, repo, remotemarks, path, trfunc, explicit=()):
if ui.configbool('remotenames', 'syncbookmarks', False):
return orig(ui, repo, remotemarks, path, trfunc, explicit)
2015-02-12 09:31:14 +03:00
ui.debug('remotenames: skipped syncing local bookmarks\n')
2015-01-29 04:45:48 +03:00
def exclone(orig, ui, *args, **opts):
"""
We may not want local bookmarks on clone... but we always want remotenames!
"""
srcpeer, dstpeer = orig(ui, *args, **opts)
pullremotenames(dstpeer.local(), srcpeer)
if not ui.configbool('remotenames', 'syncbookmarks', False):
2015-02-12 09:31:14 +03:00
ui.debug('remotenames: removing cloned bookmarks\n')
repo = dstpeer.local()
wlock = repo.wlock()
try:
try:
repo.vfs.unlink('bookmarks')
except OSError, inst:
if inst.errno != errno.ENOENT:
raise
finally:
wlock.release()
2015-01-29 04:45:48 +03:00
return (srcpeer, dstpeer)
2015-02-10 20:18:13 +03:00
def excommit(orig, repo, *args, **opts):
res = orig(repo, *args, **opts)
writedistance(repo)
return res
2015-02-10 07:16:15 +03:00
def exupdate(orig, repo, *args, **opts):
res = orig(repo, *args, **opts)
writedistance(repo)
return res
2015-02-10 07:38:30 +03:00
def exsetcurrent(orig, repo, mark):
res = orig(repo, mark)
writedistance(repo)
return res
2010-01-04 07:37:45 +03:00
def reposetup(ui, repo):
if not repo.local():
return
2010-01-04 07:37:45 +03:00
hoist = ui.config('remotenames', 'hoist')
if hoist:
hoist += '/'
loadremotenames(repo)
# cache this so we don't iterate over new values
items = list(repo.names.iteritems())
for nsname, ns in items:
d = _remotenames.get(nsname)
if not d:
continue
rname = 'remote' + nsname
rtmpl = 'remote' + ns.templatename
if nsname == 'bookmarks' and hoist:
def names(rp, d=d):
l = d.keys()
for name in l:
if name.startswith(hoist):
l.append(name[len(hoist):])
return l
def namemap(rp, name, d=d):
if name in d:
return d[name]
return d.get(hoist + name)
# we don't hoist nodemap because we don't want hoisted names
# to show up in logs, which is the primary use case here
else:
names = lambda rp, d=d: d.keys()
namemap = lambda rp, name, d=d: d.get(name)
nodemap = lambda rp, node, d=d: [name for name, n in d.iteritems()
for n2 in n if n2 == node]
n = namespaces.namespace(rname, templatename=rtmpl,
logname=ns.templatename, colorname=rtmpl,
listnames=names, namemap=namemap,
nodemap=nodemap)
repo.names.addnamespace(n)
def extsetup(ui):
extensions.wrapfunction(exchange, 'push', expush)
extensions.wrapfunction(exchange, 'pull', expull)
extensions.wrapfunction(repoview, '_getdynamicblockers', blockerhook)
extensions.wrapfunction(bookmarks, 'updatefromremote', exupdatefromremote)
extensions.wrapfunction(bookmarks, 'setcurrent', exsetcurrent)
extensions.wrapfunction(hg, 'clone', exclone)
extensions.wrapfunction(hg, 'updaterepo', exupdate)
extensions.wrapfunction(localrepo.localrepository, 'commit', excommit)
entry = extensions.wrapcommand(commands.table, 'bookmarks', exbookmarks)
entry[1].append(('a', 'all', None, 'show both remote and local bookmarks'))
entry[1].append(('', 'remote', None, 'show only remote bookmarks'))
entry = extensions.wrapcommand(commands.table, 'branches', exbranches)
entry[1].append(('a', 'all', None, 'show both remote and local branches'))
entry[1].append(('', 'remote', None, 'show only remote branches'))
entry = extensions.wrapcommand(commands.table, 'log', exlog)
entry[1].append(('', 'remote', None, 'show remote names even if hidden'))
entry = extensions.wrapcommand(commands.table, 'push', expushcmd)
entry[1].append(('t', 'to', '', 'push revs to this bookmark', 'BOOKMARK'))
exchange.pushdiscoverymapping['bookmarks'] = expushdiscoverybookmarks
templatekw.keywords['remotenames'] = remotenameskw
def exlog(orig, ui, repo, *args, **opts):
# hack for logging that turns on the dynamic blockerhook
if opts.get('remote'):
repo.__setattr__('_unblockhiddenremotenames', True)
res = orig(ui, repo, *args, **opts)
if opts.get('remote'):
repo.__setattr__('_unblockhiddenremotenames', False)
return res
_pushto = False
def expushdiscoverybookmarks(pushop):
2015-02-12 06:56:36 +03:00
repo = pushop.repo.unfiltered()
remotemarks = pushop.remote.listkeys('bookmarks')
force = pushop.force
if not _pushto:
ret = exchange._pushdiscoverybookmarks(pushop)
if not (repo.ui.configbool('remotenames', 'pushanonheads')
or force):
# check to make sure we don't push an anonymous head
if pushop.revs:
revs = set(pushop.revs)
else:
revs = set(repo.lookup(r) for r in repo.revs('head()'))
revs -= set(pushop.remoteheads)
# find heads that don't have a bookmark going with them
for bookmark in pushop.bookmarks:
rev = repo.lookup(bookmark)
if rev in revs:
revs.remove(rev)
# remove heads that already have a remote bookmark
for bookmark, node in remotemarks.iteritems():
rev = repo.lookup(node)
if rev in revs:
revs.remove(rev)
# remove heads that already advance bookmarks (old mercurial
# behavior)
for bookmark, old, new in pushop.outbookmarks:
rev = repo.lookup(new)
if rev in revs:
revs.remove(rev)
revs = [short(r) for r in revs
if not repo[r].obsolete()
and not repo[r].closesbranch()]
if revs:
msg = _("push would create new anonymous heads (%s)")
hint = _("use --force to override this warning")
raise util.Abort(msg % ', '.join(revs), hint=hint)
return ret
bookmark = pushop.bookmarks[0]
rev = pushop.revs[0]
# allow new bookmark only if force is True
old = ''
if bookmark in remotemarks:
old = remotemarks[bookmark]
elif not force:
msg = _('not creating new bookmark')
hint = _('use --force to create a new bookmark')
raise util.Abort(msg, hint=hint)
# allow non-ff only if force is True
if not force and old != '':
if old not in repo:
msg = _('remote bookmark revision is not in local repo')
hint = _('pull and merge or rebase or use --force')
raise util.Abort(msg, hint=hint)
foreground = obsolete.foreground(repo, [repo.lookup(old)])
if repo[rev].node() not in foreground:
msg = _('pushed rev is not in the foreground of remote bookmark')
hint = _('use --force flag to complete non-fast-forward update')
raise util.Abort(msg, hint=hint)
2015-02-12 10:41:42 +03:00
if repo[old] == repo[rev]:
repo.ui.warn(_('remote bookmark already points at pushed rev\n'))
return
pushop.outbookmarks.append((bookmark, old, hex(rev)))
def expushcmd(orig, ui, repo, dest=None, **opts):
to = opts.get('to')
if not to:
if ui.configbool('remotenames', 'forceto', False):
msg = _('must specify --to when pushing')
hint = _('see configuration option %s') % 'remotenames.forceto'
raise util.Abort(msg, hint=hint)
return orig(ui, repo, dest, **opts)
if opts.get('bookmark'):
msg = _('do not specify --to/-t and --bookmark/-B at the same time')
raise util.Abort(msg)
if opts.get('branch'):
msg = _('do not specify --to/-t and --branch/-b at the same time')
raise util.Abort(msg)
revs = opts.get('rev')
if revs:
revs = [repo.lookup(r) for r in scmutil.revrange(repo, revs)]
else:
revs = [repo.lookup('.')]
if len(revs) != 1:
msg = _('--to requires exactly one rev to push')
hint = _('use --rev BOOKMARK or omit --rev for current commit (.)')
raise util.Abort(msg, hint=hint)
rev = revs[0]
# needed for discovery method
global _pushto
_pushto = True
# big can o' copypasta from exchange.push
dest = ui.expandpath(dest or 'default-push', dest or 'default')
dest, branches = hg.parseurl(dest, opts.get('branch'))
try:
other = hg.peer(repo, opts, dest)
except error.RepoError:
if dest == "default-push":
hint = _('see the "path" section in "hg help config"')
raise util.Abort(_("default repository not configured!"),
hint=hint)
else:
raise
# all checks pass, go for it!
2015-02-12 09:31:27 +03:00
ui.status(_('pushing rev %s to destination %s bookmark %s\n') % (
short(rev), dest, to))
# TODO: subrepo stuff
pushop = exchange.push(repo, other, opts.get('force'), revs=revs,
bookmarks=(to,))
result = not pushop.cgresult
if pushop.bkresult is not None:
if pushop.bkresult == 2:
result = 2
elif not result and pushop.bkresult:
result = 2
_pushto = False
return result
def exbranches(orig, ui, repo, *args, **opts):
if not opts.get('remote'):
orig(ui, repo, *args, **opts)
if opts.get('all') or opts.get('remote'):
# exit early if namespace doesn't even exist
namespace = 'remotebranches'
if namespace not in repo.names:
return
ns = repo.names[namespace]
label = 'log.' + ns.colorname
fm = ui.formatter('branches', opts)
# it seems overkill to hide displaying hidden remote branches
repo = repo.unfiltered()
# create a sorted by descending rev list
revs = set()
for name in ns.listnames(repo):
for n in ns.nodes(repo, name):
revs.add(repo.changelog.rev(n))
for r in sorted(revs, reverse=True):
ctx = repo[r]
for name in ns.names(repo, ctx.node()):
fm.startitem()
padsize = max(31 - len(str(r)) - encoding.colwidth(name), 0)
tmplabel = label
if ctx.obsolete():
tmplabel = tmplabel + ' changeset.obsolete'
fm.write(ns.colorname, '%s', name, label=label)
fmt = ' ' * padsize + ' %d:%s'
fm.condwrite(not ui.quiet, 'rev node', fmt, r,
fm.hexfunc(ctx.node()), label=tmplabel)
fm.plain('\n')
fm.end()
def exbookmarks(orig, ui, repo, *args, **opts):
"""Bookmark output is sorted by bookmark name.
This has the side benefit of grouping all remote bookmarks by remote name.
"""
if not opts.get('remote'):
orig(ui, repo, *args, **opts)
if opts.get('all') or opts.get('remote'):
n = 'remotebookmarks'
if n not in repo.names:
return
ns = repo.names[n]
color = ns.colorname
label = 'log.' + color
fm = ui.formatter('bookmarks', opts)
# it seems overkill to hide displaying hidden remote bookmarks
repo = repo.unfiltered()
for name in sorted(ns.listnames(repo)):
node = ns.nodes(repo, name)[0]
ctx = repo[node]
fm.startitem()
if not ui.quiet:
fm.plain(' ')
padsize = max(25 - encoding.colwidth(name), 0)
fmt = ' ' * padsize + ' %d:%s'
tmplabel = label
if ctx.obsolete():
tmplabel = tmplabel + ' changeset.obsolete'
fm.write(color, '%s', name, label=label)
fm.condwrite(not ui.quiet, 'rev node', fmt, ctx.rev(),
fm.hexfunc(node), label=tmplabel)
fm.plain('\n')
def activepath(ui, remote):
realpath = ''
local = None
try:
local = remote.local()
except AttributeError:
pass
# determine the remote path from the repo, if possible; else just
# use the string given to us
rpath = remote
if local:
rpath = getattr(remote, 'root', None)
if rpath is None:
# Maybe a localpeer? (hg@1ac628cd7113, 2.3)
rpath = getattr(getattr(remote, '_repo', None),
'root', None)
elif not isinstance(remote, str):
try:
rpath = remote._url
except AttributeError:
rpath = remote.url
for path, uri in ui.configitems('paths'):
uri = ui.expandpath(expandscheme(ui, uri))
if local:
uri = os.path.realpath(uri)
else:
if uri.startswith('http'):
try:
uri = url.url(uri).authinfo()[0]
except AttributeError:
try:
uri = util.url(uri).authinfo()[0]
except AttributeError:
uri = url.getauthinfo(uri)[0]
uri = uri.rstrip('/')
rpath = rpath.rstrip('/')
if uri == rpath:
realpath = path
# prefer a non-default name to default
if path != 'default' and path != 'default-push':
break
2015-03-03 10:22:51 +03:00
renames = getrenames(ui)
realpath = renames.get(realpath, realpath)
return realpath
2015-03-03 10:22:51 +03:00
# memoization
_renames = None
def getrenames(ui):
global _renames
if _renames is None:
_renames = {}
for k, v in ui.configitems('remotenames'):
if k.startswith('rename.'):
_renames[k[7:]] = v
return _renames
def expandscheme(ui, uri):
'''For a given uri, expand the scheme for it'''
urischemes = [s for s in schemes.schemes.iterkeys()
if uri.startswith('%s://' % s)]
for s in urischemes:
# TODO: refactor schemes so we don't
# duplicate this logic
ui.note('performing schemes expansion with '
'scheme %s\n' % s)
scheme = hg.schemes[s]
parts = uri.split('://', 1)[1].split('/', scheme.parts)
if len(parts) > scheme.parts:
tail = parts[-1]
parts = parts[:-1]
else:
tail = ''
2014-12-16 10:45:59 +03:00
ctx = dict((str(i + 1), v) for i, v in enumerate(parts))
uri = ''.join(scheme.templater.process(scheme.url, ctx)) + tail
return uri
def splitremotename(remote):
name = ''
if '/' in remote:
remote, name = remote.split('/', 1)
return remote, name
def joinremotename(remote, ref):
if ref:
remote += '/' + ref
return remote
def readremotenames(repo):
rfile = repo.join('remotenames')
# exit early if there is nothing to do
if not os.path.exists(rfile):
return
# needed to heuristically determine if a file is in the old format
branches = repo.names['branches'].listnames(repo)
bookmarks = repo.names['bookmarks'].listnames(repo)
f = open(rfile)
for line in f:
nametype = None
line = line.strip()
if not line:
continue
nametype = None
remote, rname = None, None
node, name = line.split(' ', 1)
# check for nametype being written into the file format
if ' ' in name:
nametype, name = name.split(' ', 1)
remote, rname = splitremotename(name)
# skip old data that didn't write the name (only wrote the alias)
if not rname:
continue
# old format didn't save the nametype, so check for the name in
# branches and bookmarks
if nametype is None:
if rname in branches:
nametype = 'branches'
elif rname in bookmarks:
nametype = 'bookmarks'
yield node, nametype, remote, rname
f.close()
def loadremotenames(repo):
alias_default = repo.ui.configbool('remotenames', 'alias.default')
for node, nametype, remote, rname in readremotenames(repo):
# handle alias_default here
if remote != "default" and rname == "default" and alias_default:
name = remote
else:
name = joinremotename(remote, rname)
# if the node doesn't exist, skip it
try:
ctx = repo[node]
except error.RepoLookupError:
continue
# only mark as remote if the head changeset isn't marked closed
if not ctx.extra().get('close'):
nodes = _remotenames[nametype].get(name, [])
nodes.append(ctx.node())
_remotenames[nametype][name] = nodes
def transition(repo, ui):
"""
Help with transitioning to using a remotenames workflow.
Allows deleting matching local bookmarks defined in a config file:
[remotenames]
transitionbookmarks = master, stable
"""
transmarks = ui.configlist('remotenames', 'transitionbookmarks')
localmarks = repo._bookmarks
for mark in transmarks:
if mark in localmarks:
del localmarks[mark]
localmarks.write()
def saveremotenames(repo, remote, branches, bookmarks):
if not repo.vfs.exists('remotenames'):
transition(repo, repo.ui)
# read in all data first before opening file to write
olddata = set(readremotenames(repo))
bfile = repo.join('remotenames')
f = open(bfile, 'w')
# only update the given 'remote', so iterate over old data and re-save it
for node, nametype, oldremote, rname in olddata:
if oldremote != remote:
n = joinremotename(oldremote, rname)
f.write('%s %s %s\n' % (node, nametype, n))
for branch, nodes in branches.iteritems():
for n in nodes:
rname = joinremotename(remote, branch)
f.write('%s branches %s\n' % (hex(n), rname))
for bookmark, n in bookmarks.iteritems():
f.write('%s bookmarks %s\n' % (n, joinremotename(remote, bookmark)))
f.close()
def distancefromremote(repo, remote="default"):
"""returns the signed distance between the current node and remote"""
b = repo._bookmarkcurrent
# if no bookmark is active, fallback to the branchname
if not b:
b = repo.lookupbranch('.')
# get the non-default name
paths = dict(repo.ui.configitems('paths'))
rpath = paths.get(remote)
if remote == 'default':
for path, uri in paths.iteritems():
if path != 'default' and path != 'default-push' and rpath == uri:
remote = path
# if we couldn't find anything for remote then return
if not rpath:
return 0
remoteb = joinremotename(remote, b)
if b == 'default' and repo.ui.configbool('remotenames', 'alias.default'):
remoteb = remote
distance = 0
if remoteb in repo:
rev1 = repo[remoteb].rev()
rev2 = repo['.'].rev()
sign = 1
if rev2 < rev1:
sign = -1
rev1, rev2 = rev2, rev1
nodes = repo.revs('%s::%s' % (rev1, rev2))
distance = sign * (len(nodes) - 1)
return distance
def writedistance(repo, remote="default"):
distance = distancefromremote(repo, remote)
sign = '+'
if distance < 0:
sign = '-'
wlock = repo.wlock()
try:
try:
fp = repo.vfs('remotedistance', 'w', atomictemp=True)
fp.write('%s %s' % (sign, abs(distance)))
fp.close()
except OSError, inst:
if inst.errno != errno.ENOENT:
raise
finally:
wlock.release()
#########
# revsets
#########
def upstream_revs(filt, repo, subset, x):
upstream_tips = set()
for remotename in _remotenames.keys():
rname = 'remote' + remotename
try:
ns = repo.names[rname]
except KeyError:
continue
for name in ns.listnames(repo):
if filt(splitremotename(name)[0]):
upstream_tips.update(ns.nodes(repo, name))
if not upstream_tips:
return revset.baseset([])
tipancestors = repo.revs('::%ln', upstream_tips)
2014-12-16 10:45:59 +03:00
return revset.filteredset(subset, lambda n: n in tipancestors)
def upstream(repo, subset, x):
'''``upstream()``
Select changesets in an upstream repository according to remotenames.
'''
repo = repo.unfiltered()
upstream_names = repo.ui.configlist('remotenames', 'upstream')
# override default args from hgrc with args passed in on the command line
if x:
upstream_names = [revset.getstring(symbol,
"remote path must be a string")
for symbol in revset.getlist(x)]
filt = lambda x: True
default_path = dict(repo.ui.configitems('paths')).get('default')
if not upstream_names and default_path:
default_path = expandscheme(repo.ui, default_path)
upstream_names = [activepath(repo.ui, default_path)]
if upstream_names:
filt = lambda name: name in upstream_names
return upstream_revs(filt, repo, subset, x)
def pushed(repo, subset, x):
'''``pushed()``
Select changesets in any remote repository according to remotenames.
'''
2014-12-16 10:45:59 +03:00
revset.getargs(x, 0, 0, "pushed takes no arguments")
return upstream_revs(lambda x: True, repo, subset, x)
def remotenamesrevset(repo, subset, x):
"""``remotenames()``
All remote branches heads.
"""
2014-12-16 10:45:59 +03:00
revset.getargs(x, 0, 0, "remotenames takes no arguments")
remoterevs = set()
cl = repo.changelog
for remotename in _remotenames.keys():
rname = 'remote' + remotename
try:
ns = repo.names[rname]
except KeyError:
continue
for name in ns.listnames(repo):
remoterevs.update(ns.nodes(repo, name))
return revset.baseset(sorted(cl.rev(n) for n in remoterevs))
2014-03-18 22:46:10 +04:00
revset.symbols.update({'upstream': upstream,
'pushed': pushed,
'remotenames': remotenamesrevset})
###########
# templates
###########
def remotenameskw(**args):
""":remotenames: List of strings. List of remote names associated with the
changeset. If remotenames.suppressbranches is True then branch names will
be hidden if there is a bookmark at the same changeset.
"""
repo, ctx = args['repo'], args['ctx']
remotenames = []
if 'remotebookmarks' in repo.names:
remotenames = repo.names['remotebookmarks'].names(repo, ctx.node())
suppress = repo.ui.configbool('remotenames', 'suppressbranches', False)
if (not remotenames or not suppress) and 'remotebranches' in repo.names:
remotenames += repo.names['remotebranches'].names(repo, ctx.node())
return templatekw.showlist('remotename', remotenames,
plural='remotenames', **args)