remotenames.py
405 lines
| 13.6 KiB
| text/x-python
|
PythonLexer
/ hgext / remotenames.py
|
r36077 | # remotenames.py - extension to display remotenames | ||
# | ||||
# Copyright 2017 Augie Fackler <raf@durin42.com> | ||||
# Copyright 2017 Sean Farley <sean@farley.io> | ||||
# | ||||
# This software may be used and distributed according to the terms of the | ||||
# GNU General Public License version 2 or any later version. | ||||
|
r37835 | """ showing remotebookmarks and remotebranches in UI (EXPERIMENTAL) | ||
|
r36079 | |||
By default both remotebookmarks and remotebranches are turned on. Config knob to | ||||
control the individually are as follows. | ||||
Config options to tweak the default behaviour: | ||||
remotenames.bookmarks | ||||
|
r37107 | Boolean value to enable or disable showing of remotebookmarks (default: True) | ||
|
r36079 | |||
remotenames.branches | ||||
|
r37107 | Boolean value to enable or disable showing of remotebranches (default: True) | ||
remotenames.hoistedpeer | ||||
Name of the peer whose remotebookmarks should be hoisted into the top-level | ||||
namespace (default: 'default') | ||||
|
r36079 | """ | ||
|
r36077 | |||
from __future__ import absolute_import | ||||
|
r36167 | from mercurial.i18n import _ | ||
|
r36078 | from mercurial.node import ( | ||
bin, | ||||
) | ||||
|
r36077 | from mercurial import ( | ||
|
r37108 | bookmarks, | ||
|
r40105 | error, | ||
|
r37108 | extensions, | ||
|
r36077 | logexchange, | ||
|
r36079 | namespaces, | ||
|
r36974 | pycompat, | ||
|
r36079 | registrar, | ||
|
r36167 | revsetlang, | ||
smartset, | ||||
|
r36939 | templateutil, | ||
|
r40102 | util, | ||
|
r36077 | ) | ||
|
r40095 | from mercurial.utils import ( | ||
stringutil, | ||||
) | ||||
|
r36974 | if pycompat.ispy3: | ||
import collections.abc | ||||
mutablemapping = collections.abc.MutableMapping | ||||
else: | ||||
import collections | ||||
mutablemapping = collections.MutableMapping | ||||
|
r36077 | # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for | ||
# extensions which SHIP WITH MERCURIAL. Non-mainline extensions should | ||||
# be specifying the version(s) of Mercurial they are tested with, or | ||||
# leave the attribute unspecified. | ||||
testedwith = 'ships-with-hg-core' | ||||
|
r36079 | configtable = {} | ||
configitem = registrar.configitem(configtable) | ||||
|
r36080 | templatekeyword = registrar.templatekeyword() | ||
|
r36167 | revsetpredicate = registrar.revsetpredicate() | ||
|
r36079 | |||
configitem('remotenames', 'bookmarks', | ||||
default=True, | ||||
) | ||||
configitem('remotenames', 'branches', | ||||
default=True, | ||||
) | ||||
|
r37107 | configitem('remotenames', 'hoistedpeer', | ||
default='default', | ||||
) | ||||
|
r36079 | |||
|
r36974 | class lazyremotenamedict(mutablemapping): | ||
|
r36078 | """ | ||
Read-only dict-like Class to lazily resolve remotename entries | ||||
We are doing that because remotenames startup was slow. | ||||
We lazily read the remotenames file once to figure out the potential entries | ||||
and store them in self.potentialentries. Then when asked to resolve an | ||||
entry, if it is not in self.potentialentries, then it isn't there, if it | ||||
is in self.potentialentries we resolve it and store the result in | ||||
self.cache. We cannot be lazy is when asked all the entries (keys). | ||||
""" | ||||
def __init__(self, kind, repo): | ||||
self.cache = {} | ||||
self.potentialentries = {} | ||||
self._kind = kind # bookmarks or branches | ||||
self._repo = repo | ||||
self.loaded = False | ||||
def _load(self): | ||||
""" Read the remotenames file, store entries matching selected kind """ | ||||
self.loaded = True | ||||
repo = self._repo | ||||
for node, rpath, rname in logexchange.readremotenamefile(repo, | ||||
self._kind): | ||||
name = rpath + '/' + rname | ||||
self.potentialentries[name] = (node, rpath, name) | ||||
def _resolvedata(self, potentialentry): | ||||
""" Check that the node for potentialentry exists and return it """ | ||||
if not potentialentry in self.potentialentries: | ||||
return None | ||||
node, remote, name = self.potentialentries[potentialentry] | ||||
repo = self._repo | ||||
binnode = bin(node) | ||||
# if the node doesn't exist, skip it | ||||
try: | ||||
repo.changelog.rev(binnode) | ||||
except LookupError: | ||||
return None | ||||
# Skip closed branches | ||||
if (self._kind == 'branches' and repo[binnode].closesbranch()): | ||||
return None | ||||
return [binnode] | ||||
def __getitem__(self, key): | ||||
if not self.loaded: | ||||
self._load() | ||||
val = self._fetchandcache(key) | ||||
if val is not None: | ||||
return val | ||||
else: | ||||
raise KeyError() | ||||
|
r36266 | def __iter__(self): | ||
return iter(self.potentialentries) | ||||
def __len__(self): | ||||
return len(self.potentialentries) | ||||
def __setitem__(self): | ||||
raise NotImplementedError | ||||
def __delitem__(self): | ||||
raise NotImplementedError | ||||
|
r36078 | def _fetchandcache(self, key): | ||
if key in self.cache: | ||||
return self.cache[key] | ||||
val = self._resolvedata(key) | ||||
if val is not None: | ||||
self.cache[key] = val | ||||
return val | ||||
else: | ||||
return None | ||||
def keys(self): | ||||
""" Get a list of bookmark or branch names """ | ||||
if not self.loaded: | ||||
self._load() | ||||
return self.potentialentries.keys() | ||||
def iteritems(self): | ||||
""" Iterate over (name, node) tuples """ | ||||
if not self.loaded: | ||||
self._load() | ||||
for k, vtup in self.potentialentries.iteritems(): | ||||
yield (k, [bin(vtup[0])]) | ||||
|
r42806 | items = iteritems | ||
|
r36481 | class remotenames(object): | ||
|
r36077 | """ | ||
This class encapsulates all the remotenames state. It also contains | ||||
|
r36078 | methods to access that state in convenient ways. Remotenames are lazy | ||
loaded. Whenever client code needs to ensure the freshest copy of | ||||
remotenames, use the `clearnames` method to force an eventual load. | ||||
|
r36077 | """ | ||
def __init__(self, repo, *args): | ||||
self._repo = repo | ||||
|
r36078 | self.clearnames() | ||
|
r36077 | |||
def clearnames(self): | ||||
""" Clear all remote names state """ | ||||
|
r36481 | self.bookmarks = lazyremotenamedict("bookmarks", self._repo) | ||
self.branches = lazyremotenamedict("branches", self._repo) | ||||
|
r36077 | self._invalidatecache() | ||
def _invalidatecache(self): | ||||
self._nodetobmarks = None | ||||
self._nodetobranch = None | ||||
|
r37107 | self._hoisttonodes = None | ||
self._nodetohoists = None | ||||
|
r36077 | |||
def bmarktonodes(self): | ||||
|
r36481 | return self.bookmarks | ||
|
r36077 | |||
def nodetobmarks(self): | ||||
if not self._nodetobmarks: | ||||
bmarktonodes = self.bmarktonodes() | ||||
self._nodetobmarks = {} | ||||
for name, node in bmarktonodes.iteritems(): | ||||
self._nodetobmarks.setdefault(node[0], []).append(name) | ||||
return self._nodetobmarks | ||||
def branchtonodes(self): | ||||
|
r36481 | return self.branches | ||
|
r36077 | |||
def nodetobranch(self): | ||||
if not self._nodetobranch: | ||||
branchtonodes = self.branchtonodes() | ||||
self._nodetobranch = {} | ||||
for name, nodes in branchtonodes.iteritems(): | ||||
for node in nodes: | ||||
self._nodetobranch.setdefault(node, []).append(name) | ||||
return self._nodetobranch | ||||
|
r36079 | |||
|
r37107 | def hoisttonodes(self, hoist): | ||
if not self._hoisttonodes: | ||||
marktonodes = self.bmarktonodes() | ||||
self._hoisttonodes = {} | ||||
hoist += '/' | ||||
for name, node in marktonodes.iteritems(): | ||||
if name.startswith(hoist): | ||||
name = name[len(hoist):] | ||||
self._hoisttonodes[name] = node | ||||
return self._hoisttonodes | ||||
def nodetohoists(self, hoist): | ||||
if not self._nodetohoists: | ||||
marktonodes = self.bmarktonodes() | ||||
self._nodetohoists = {} | ||||
hoist += '/' | ||||
for name, node in marktonodes.iteritems(): | ||||
if name.startswith(hoist): | ||||
name = name[len(hoist):] | ||||
self._nodetohoists.setdefault(node[0], []).append(name) | ||||
return self._nodetohoists | ||||
|
r39782 | def wrapprintbookmarks(orig, ui, repo, fm, bmarks): | ||
|
r37108 | if 'remotebookmarks' not in repo.names: | ||
return | ||||
ns = repo.names['remotebookmarks'] | ||||
for name in ns.listnames(repo): | ||||
nodes = ns.nodes(repo, name) | ||||
if not nodes: | ||||
continue | ||||
node = nodes[0] | ||||
bmarks[name] = (node, ' ', '') | ||||
|
r39782 | return orig(ui, repo, fm, bmarks) | ||
|
r37108 | |||
def extsetup(ui): | ||||
extensions.wrapfunction(bookmarks, '_printbookmarks', wrapprintbookmarks) | ||||
|
r36079 | def reposetup(ui, repo): | ||
|
r38003 | |||
# set the config option to store remotenames | ||||
repo.ui.setconfig('experimental', 'remotenames', True, 'remotenames-ext') | ||||
|
r36079 | if not repo.local(): | ||
return | ||||
repo._remotenames = remotenames(repo) | ||||
ns = namespaces.namespace | ||||
if ui.configbool('remotenames', 'bookmarks'): | ||||
remotebookmarkns = ns( | ||||
'remotebookmarks', | ||||
templatename='remotebookmarks', | ||||
colorname='remotebookmark', | ||||
|
r36278 | logfmt='remote bookmark: %s\n', | ||
|
r36079 | listnames=lambda repo: repo._remotenames.bmarktonodes().keys(), | ||
namemap=lambda repo, name: | ||||
repo._remotenames.bmarktonodes().get(name, []), | ||||
nodemap=lambda repo, node: | ||||
repo._remotenames.nodetobmarks().get(node, [])) | ||||
repo.names.addnamespace(remotebookmarkns) | ||||
|
r37107 | # hoisting only works if there are remote bookmarks | ||
hoist = ui.config('remotenames', 'hoistedpeer') | ||||
if hoist: | ||||
hoistednamens = ns( | ||||
'hoistednames', | ||||
templatename='hoistednames', | ||||
colorname='hoistedname', | ||||
logfmt='hoisted name: %s\n', | ||||
listnames = lambda repo: | ||||
repo._remotenames.hoisttonodes(hoist).keys(), | ||||
namemap = lambda repo, name: | ||||
repo._remotenames.hoisttonodes(hoist).get(name, []), | ||||
nodemap = lambda repo, node: | ||||
repo._remotenames.nodetohoists(hoist).get(node, [])) | ||||
repo.names.addnamespace(hoistednamens) | ||||
|
r36079 | if ui.configbool('remotenames', 'branches'): | ||
remotebranchns = ns( | ||||
'remotebranches', | ||||
templatename='remotebranches', | ||||
colorname='remotebranch', | ||||
|
r36278 | logfmt='remote branch: %s\n', | ||
|
r36079 | listnames = lambda repo: repo._remotenames.branchtonodes().keys(), | ||
namemap = lambda repo, name: | ||||
repo._remotenames.branchtonodes().get(name, []), | ||||
nodemap = lambda repo, node: | ||||
repo._remotenames.nodetobranch().get(node, [])) | ||||
repo.names.addnamespace(remotebranchns) | ||||
|
r36080 | |||
|
r37086 | @templatekeyword('remotenames', requires={'repo', 'ctx'}) | ||
|
r36609 | def remotenameskw(context, mapping): | ||
|
r36458 | """List of strings. Remote names associated with the changeset.""" | ||
|
r36609 | repo = context.resource(mapping, 'repo') | ||
ctx = context.resource(mapping, 'ctx') | ||||
|
r36080 | |||
remotenames = [] | ||||
if 'remotebookmarks' in repo.names: | ||||
remotenames = repo.names['remotebookmarks'].names(repo, ctx.node()) | ||||
if 'remotebranches' in repo.names: | ||||
remotenames += repo.names['remotebranches'].names(repo, ctx.node()) | ||||
|
r36939 | return templateutil.compatlist(context, mapping, 'remotename', remotenames, | ||
plural='remotenames') | ||||
|
r36080 | |||
|
r37086 | @templatekeyword('remotebookmarks', requires={'repo', 'ctx'}) | ||
|
r36609 | def remotebookmarkskw(context, mapping): | ||
|
r36458 | """List of strings. Remote bookmarks associated with the changeset.""" | ||
|
r36609 | repo = context.resource(mapping, 'repo') | ||
ctx = context.resource(mapping, 'ctx') | ||||
|
r36080 | |||
remotebmarks = [] | ||||
if 'remotebookmarks' in repo.names: | ||||
remotebmarks = repo.names['remotebookmarks'].names(repo, ctx.node()) | ||||
|
r36939 | return templateutil.compatlist(context, mapping, 'remotebookmark', | ||
remotebmarks, plural='remotebookmarks') | ||||
|
r36080 | |||
|
r37086 | @templatekeyword('remotebranches', requires={'repo', 'ctx'}) | ||
|
r36609 | def remotebrancheskw(context, mapping): | ||
|
r36458 | """List of strings. Remote branches associated with the changeset.""" | ||
|
r36609 | repo = context.resource(mapping, 'repo') | ||
ctx = context.resource(mapping, 'ctx') | ||||
|
r36080 | |||
remotebranches = [] | ||||
if 'remotebranches' in repo.names: | ||||
remotebranches = repo.names['remotebranches'].names(repo, ctx.node()) | ||||
|
r36939 | return templateutil.compatlist(context, mapping, 'remotebranch', | ||
remotebranches, plural='remotebranches') | ||||
|
r36167 | |||
|
r40101 | def _revsetutil(repo, subset, x, rtypes): | ||
|
r36167 | """utility function to return a set of revs based on the rtypes""" | ||
|
r40101 | args = revsetlang.getargs(x, 0, 1, _('only one argument accepted')) | ||
if args: | ||||
kind, pattern, matcher = stringutil.stringmatcher( | ||||
revsetlang.getstring(args[0], _('argument must be a string'))) | ||||
else: | ||||
|
r40105 | kind = pattern = None | ||
|
r40102 | matcher = util.always | ||
|
r36167 | |||
|
r40103 | nodes = set() | ||
|
r36167 | cl = repo.changelog | ||
for rtype in rtypes: | ||||
if rtype in repo.names: | ||||
ns = repo.names[rtype] | ||||
for name in ns.listnames(repo): | ||||
|
r40096 | if not matcher(name): | ||
continue | ||||
|
r40103 | nodes.update(ns.nodes(repo, name)) | ||
|
r40105 | if kind == 'literal' and not nodes: | ||
raise error.RepoLookupError(_("remote name '%s' does not exist") | ||||
% pattern) | ||||
|
r36167 | |||
|
r40103 | revs = (cl.rev(n) for n in nodes if cl.hasnode(n)) | ||
|
r40104 | return subset & smartset.baseset(revs) | ||
|
r36167 | |||
|
r40096 | @revsetpredicate('remotenames([name])') | ||
|
r36167 | def remotenamesrevset(repo, subset, x): | ||
|
r40096 | """All changesets which have a remotename on them. If `name` is | ||
specified, only remotenames of matching remote paths are considered. | ||||
|
r40095 | |||
Pattern matching is supported for `name`. See :hg:`help revisions.patterns`. | ||||
""" | ||||
|
r40101 | return _revsetutil(repo, subset, x, ('remotebookmarks', 'remotebranches')) | ||
|
r40095 | |||
|
r40096 | @revsetpredicate('remotebranches([name])') | ||
|
r36167 | def remotebranchesrevset(repo, subset, x): | ||
|
r40096 | """All changesets which are branch heads on remotes. If `name` is | ||
specified, only remotenames of matching remote paths are considered. | ||||
|
r40095 | |||
Pattern matching is supported for `name`. See :hg:`help revisions.patterns`. | ||||
""" | ||||
|
r40101 | return _revsetutil(repo, subset, x, ('remotebranches',)) | ||
|
r36167 | |||
|
r40096 | @revsetpredicate('remotebookmarks([name])') | ||
|
r36167 | def remotebmarksrevset(repo, subset, x): | ||
|
r40096 | """All changesets which have bookmarks on remotes. If `name` is | ||
specified, only remotenames of matching remote paths are considered. | ||||
|
r40095 | |||
Pattern matching is supported for `name`. See :hg:`help revisions.patterns`. | ||||
""" | ||||
|
r40101 | return _revsetutil(repo, subset, x, ('remotebookmarks',)) | ||