import os import errno from mercurial import bookmarks from mercurial import commands from mercurial import encoding from mercurial import error from mercurial import exchange from mercurial import extensions from mercurial import hg from mercurial import localrepo from mercurial import namespaces from mercurial import obsolete from mercurial import repoview from mercurial import revset from mercurial import scmutil from mercurial import templatekw from mercurial import url from mercurial import util from mercurial.i18n import _ from mercurial.node import hex, short from hgext import schemes _remotenames = { "bookmarks": {}, "branches": {}, } def expush(orig, repo, remote, *args, **kwargs): res = orig(repo, remote, *args, **kwargs) lock = repo.lock() try: pullremotenames(repo, remote) finally: lock.release() return res def expull(orig, repo, remote, *args, **kwargs): res = orig(repo, remote, *args, **kwargs) pullremotenames(repo, remote) writedistance(repo) return res def pullremotenames(repo, remote): lock = repo.lock() try: path = activepath(repo.ui, remote) if path: # 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) ui.debug('remotenames: skipped syncing local bookmarks\n') 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): 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() return (srcpeer, dstpeer) def excommit(orig, repo, *args, **opts): res = orig(repo, *args, **opts) writedistance(repo) return res def exupdate(orig, repo, *args, **opts): res = orig(repo, *args, **opts) writedistance(repo) return res def exsetcurrent(orig, repo, mark): res = orig(repo, mark) writedistance(repo) return res def reposetup(ui, repo): if not repo.local(): return 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): 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) 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! 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 renames = getrenames(ui) realpath = renames.get(realpath, realpath) return realpath # 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 = '' 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) 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. ''' 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. """ 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)) 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)