remotenames.py
437 lines
| 14.4 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, | ||
extensions, | ||||
|
r36077 | logexchange, | ||
|
r36079 | namespaces, | ||
|
r36974 | pycompat, | ||
|
r36079 | registrar, | ||
|
r36167 | revsetlang, | ||
smartset, | ||||
|
r36939 | templateutil, | ||
|
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])]) | ||||
|
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 | |||
|
r40095 | def _revsetutil(repo, subset, x, rtypes, args): | ||
|
r36167 | """utility function to return a set of revs based on the rtypes""" | ||
revs = set() | ||||
cl = repo.changelog | ||||
|
r40095 | literals, matchers = args | ||
# whether arguments were passed or not | ||||
argspassed = bool(literals or matchers) | ||||
|
r36167 | for rtype in rtypes: | ||
if rtype in repo.names: | ||||
ns = repo.names[rtype] | ||||
for name in ns.listnames(repo): | ||||
|
r40095 | if argspassed: | ||
if name in literals: | ||||
revs.update(ns.nodes(repo, name)) | ||||
continue | ||||
for matcher in matchers: | ||||
if matcher(name): | ||||
revs.update(ns.nodes(repo, name)) | ||||
break | ||||
else: | ||||
revs.update(ns.nodes(repo, name)) | ||||
|
r36167 | |||
results = (cl.rev(n) for n in revs if cl.hasnode(n)) | ||||
return subset & smartset.baseset(sorted(results)) | ||||
|
r40095 | def _parseargs(x): | ||
"""parses the argument passed in revsets | ||||
returns (literals, matchers) where, | ||||
literals is a set of literals passed by user | ||||
matchers is a list of matcher objects for patterns passed by user | ||||
""" | ||||
# set of paths passed as literals | ||||
literals = set() | ||||
# list of matcher to match the patterns passed as names | ||||
matchers = [] | ||||
if not x: | ||||
return literals, matchers | ||||
args = set() | ||||
lx = revsetlang.getlist(x) | ||||
err = _('the argument must be a string') | ||||
for entry in lx: | ||||
args.add(revsetlang.getstring(entry, err)) | ||||
for p in args: | ||||
kind, pattern, matcher = stringutil.stringmatcher(p) | ||||
if kind == 'literal': | ||||
literals.add(pattern) | ||||
else: | ||||
matchers.append(matcher) | ||||
return literals, matchers | ||||
@revsetpredicate('remotenames([name, ...])') | ||||
|
r36167 | def remotenamesrevset(repo, subset, x): | ||
|
r40095 | """All changesets which have a remotename on them. If paths are specified, | ||
remotenames of those remote paths are only considered. | ||||
Pattern matching is supported for `name`. See :hg:`help revisions.patterns`. | ||||
""" | ||||
|
r36167 | |||
|
r40095 | args = _parseargs(x) | ||
return _revsetutil(repo, subset, x, ('remotebookmarks', 'remotebranches'), | ||||
args) | ||||
@revsetpredicate('remotebranches([name, ...])') | ||||
|
r36167 | def remotebranchesrevset(repo, subset, x): | ||
|
r40095 | """All changesets which are branch heads on remotes. If paths are specified, | ||
only those remotes paths are considered. | ||||
Pattern matching is supported for `name`. See :hg:`help revisions.patterns`. | ||||
""" | ||||
args = _parseargs(x) | ||||
return _revsetutil(repo, subset, x, ('remotebranches',), args) | ||||
|
r36167 | |||
|
r40095 | @revsetpredicate('remotebookmarks([name, ...])') | ||
|
r36167 | def remotebmarksrevset(repo, subset, x): | ||
|
r40095 | """All changesets which have bookmarks on remotes. If paths are specified, | ||
only those remote paths are considered. | ||||
Pattern matching is supported for `name`. See :hg:`help revisions.patterns`. | ||||
""" | ||||
args = _parseargs(x) | ||||
return _revsetutil(repo, subset, x, ('remotebookmarks',), args) | ||||