remotenames.py
352 lines
| 12.0 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. | ||||
|
r36079 | """ showing remotebookmarks and remotebranches in UI | ||
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 ( | ||
logexchange, | ||||
|
r36079 | namespaces, | ||
|
r36974 | pycompat, | ||
|
r36079 | registrar, | ||
|
r36167 | revsetlang, | ||
smartset, | ||||
|
r36939 | templateutil, | ||
|
r36077 | ) | ||
|
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])]) | ||||
|
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 | ||||
|
r36079 | def reposetup(ui, repo): | ||
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 | |||
def _revsetutil(repo, subset, x, rtypes): | ||||
"""utility function to return a set of revs based on the rtypes""" | ||||
revs = set() | ||||
cl = repo.changelog | ||||
for rtype in rtypes: | ||||
if rtype in repo.names: | ||||
ns = repo.names[rtype] | ||||
for name in ns.listnames(repo): | ||||
revs.update(ns.nodes(repo, name)) | ||||
results = (cl.rev(n) for n in revs if cl.hasnode(n)) | ||||
return subset & smartset.baseset(sorted(results)) | ||||
@revsetpredicate('remotenames()') | ||||
def remotenamesrevset(repo, subset, x): | ||||
"""All changesets which have a remotename on them.""" | ||||
revsetlang.getargs(x, 0, 0, _("remotenames takes no arguments")) | ||||
return _revsetutil(repo, subset, x, ('remotebookmarks', 'remotebranches')) | ||||
@revsetpredicate('remotebranches()') | ||||
def remotebranchesrevset(repo, subset, x): | ||||
"""All changesets which are branch heads on remotes.""" | ||||
revsetlang.getargs(x, 0, 0, _("remotebranches takes no arguments")) | ||||
return _revsetutil(repo, subset, x, ('remotebranches',)) | ||||
@revsetpredicate('remotebookmarks()') | ||||
def remotebmarksrevset(repo, subset, x): | ||||
"""All changesets which have bookmarks on remotes.""" | ||||
revsetlang.getargs(x, 0, 0, _("remotebookmarks takes no arguments")) | ||||
return _revsetutil(repo, subset, x, ('remotebookmarks',)) | ||||