remotenames.py
445 lines
| 13.7 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 | |||
|
r36167 | from mercurial.i18n import _ | ||
|
r43346 | 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 | ) | ||
|
r43346 | from mercurial.utils import stringutil | ||
|
r40095 | |||
|
r36974 | if pycompat.ispy3: | ||
import collections.abc | ||||
|
r43346 | |||
|
r36974 | mutablemapping = collections.abc.MutableMapping | ||
else: | ||||
import collections | ||||
|
r43346 | |||
|
r36974 | 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. | ||||
|
r43347 | testedwith = b'ships-with-hg-core' | ||
|
r36077 | |||
|
r36079 | configtable = {} | ||
configitem = registrar.configitem(configtable) | ||||
|
r36080 | templatekeyword = registrar.templatekeyword() | ||
|
r36167 | revsetpredicate = registrar.revsetpredicate() | ||
|
r36079 | |||
|
r43346 | configitem( | ||
|
r46554 | b'remotenames', | ||
b'bookmarks', | ||||
default=True, | ||||
|
r36079 | ) | ||
|
r43346 | configitem( | ||
|
r46554 | b'remotenames', | ||
b'branches', | ||||
default=True, | ||||
|
r36079 | ) | ||
|
r43346 | configitem( | ||
|
r46554 | b'remotenames', | ||
b'hoistedpeer', | ||||
default=b'default', | ||||
|
r37107 | ) | ||
|
r36079 | |||
|
r43346 | |||
|
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). | ||||
""" | ||||
|
r43346 | |||
|
r36078 | def __init__(self, kind, repo): | ||
self.cache = {} | ||||
self.potentialentries = {} | ||||
|
r43346 | self._kind = kind # bookmarks or branches | ||
|
r36078 | self._repo = repo | ||
self.loaded = False | ||||
def _load(self): | ||||
|
r47856 | """Read the remotenames file, store entries matching selected kind""" | ||
|
r36078 | self.loaded = True | ||
repo = self._repo | ||||
|
r43346 | for node, rpath, rname in logexchange.readremotenamefile( | ||
repo, self._kind | ||||
): | ||||
|
r43347 | name = rpath + b'/' + rname | ||
|
r36078 | self.potentialentries[name] = (node, rpath, name) | ||
def _resolvedata(self, potentialentry): | ||||
|
r47856 | """Check that the node for potentialentry exists and return it""" | ||
|
r36078 | 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 | ||||
|
r43347 | if self._kind == b'branches' and repo[binnode].closesbranch(): | ||
|
r36078 | 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): | ||||
|
r47856 | """Get a list of bookmark or branch names""" | ||
|
r36078 | if not self.loaded: | ||
self._load() | ||||
return self.potentialentries.keys() | ||||
def iteritems(self): | ||||
|
r47856 | """Iterate over (name, node) tuples""" | ||
|
r36078 | |||
if not self.loaded: | ||||
self._load() | ||||
|
r49768 | for k, vtup in self.potentialentries.items(): | ||
|
r36078 | yield (k, [bin(vtup[0])]) | ||
|
r42809 | items = iteritems | ||
|
r43346 | |||
|
r49801 | class remotenames: | ||
|
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): | ||||
|
r47856 | """Clear all remote names state""" | ||
|
r43347 | self.bookmarks = lazyremotenamedict(b"bookmarks", self._repo) | ||
self.branches = lazyremotenamedict(b"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 = {} | ||||
|
r49768 | for name, node in bmarktonodes.items(): | ||
|
r36077 | 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 = {} | ||||
|
r49768 | for name, nodes in branchtonodes.items(): | ||
|
r36077 | 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 = {} | ||||
|
r43347 | hoist += b'/' | ||
|
r49768 | for name, node in marktonodes.items(): | ||
|
r37107 | if name.startswith(hoist): | ||
|
r43346 | name = name[len(hoist) :] | ||
|
r37107 | self._hoisttonodes[name] = node | ||
return self._hoisttonodes | ||||
def nodetohoists(self, hoist): | ||||
if not self._nodetohoists: | ||||
marktonodes = self.bmarktonodes() | ||||
self._nodetohoists = {} | ||||
|
r43347 | hoist += b'/' | ||
|
r49768 | for name, node in marktonodes.items(): | ||
|
r37107 | if name.startswith(hoist): | ||
|
r43346 | name = name[len(hoist) :] | ||
|
r37107 | self._nodetohoists.setdefault(node[0], []).append(name) | ||
return self._nodetohoists | ||||
|
r43346 | |||
|
r39782 | def wrapprintbookmarks(orig, ui, repo, fm, bmarks): | ||
|
r43347 | if b'remotebookmarks' not in repo.names: | ||
|
r37108 | return | ||
|
r43347 | ns = repo.names[b'remotebookmarks'] | ||
|
r37108 | |||
for name in ns.listnames(repo): | ||||
nodes = ns.nodes(repo, name) | ||||
if not nodes: | ||||
continue | ||||
node = nodes[0] | ||||
|
r43347 | bmarks[name] = (node, b' ', b'') | ||
|
r37108 | |||
|
r39782 | return orig(ui, repo, fm, bmarks) | ||
|
r37108 | |||
|
r43346 | |||
|
r37108 | def extsetup(ui): | ||
|
r43347 | extensions.wrapfunction(bookmarks, b'_printbookmarks', wrapprintbookmarks) | ||
|
r37108 | |||
|
r43346 | |||
|
r36079 | def reposetup(ui, repo): | ||
|
r38003 | |||
# set the config option to store remotenames | ||||
|
r43347 | repo.ui.setconfig(b'experimental', b'remotenames', True, b'remotenames-ext') | ||
|
r38003 | |||
|
r36079 | if not repo.local(): | ||
return | ||||
repo._remotenames = remotenames(repo) | ||||
ns = namespaces.namespace | ||||
|
r43347 | if ui.configbool(b'remotenames', b'bookmarks'): | ||
|
r36079 | remotebookmarkns = ns( | ||
|
r43347 | b'remotebookmarks', | ||
templatename=b'remotebookmarks', | ||||
colorname=b'remotebookmark', | ||||
logfmt=b'remote bookmark: %s\n', | ||||
|
r36079 | listnames=lambda repo: repo._remotenames.bmarktonodes().keys(), | ||
|
r43346 | namemap=lambda repo, name: repo._remotenames.bmarktonodes().get( | ||
name, [] | ||||
), | ||||
nodemap=lambda repo, node: repo._remotenames.nodetobmarks().get( | ||||
node, [] | ||||
), | ||||
) | ||||
|
r36079 | repo.names.addnamespace(remotebookmarkns) | ||
|
r37107 | # hoisting only works if there are remote bookmarks | ||
|
r43347 | hoist = ui.config(b'remotenames', b'hoistedpeer') | ||
|
r37107 | if hoist: | ||
hoistednamens = ns( | ||||
|
r43347 | b'hoistednames', | ||
templatename=b'hoistednames', | ||||
colorname=b'hoistedname', | ||||
logfmt=b'hoisted name: %s\n', | ||||
|
r43346 | 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, []), | ||||
) | ||||
|
r37107 | repo.names.addnamespace(hoistednamens) | ||
|
r43347 | if ui.configbool(b'remotenames', b'branches'): | ||
|
r36079 | remotebranchns = ns( | ||
|
r43347 | b'remotebranches', | ||
templatename=b'remotebranches', | ||||
colorname=b'remotebranch', | ||||
logfmt=b'remote branch: %s\n', | ||||
|
r43346 | 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, [] | ||||
), | ||||
) | ||||
|
r36079 | repo.names.addnamespace(remotebranchns) | ||
|
r36080 | |||
|
r43346 | |||
|
r43347 | @templatekeyword(b'remotenames', requires={b'repo', b'ctx'}) | ||
|
r36609 | def remotenameskw(context, mapping): | ||
|
r36458 | """List of strings. Remote names associated with the changeset.""" | ||
|
r43347 | repo = context.resource(mapping, b'repo') | ||
ctx = context.resource(mapping, b'ctx') | ||||
|
r36080 | |||
remotenames = [] | ||||
|
r43347 | if b'remotebookmarks' in repo.names: | ||
remotenames = repo.names[b'remotebookmarks'].names(repo, ctx.node()) | ||||
|
r36080 | |||
|
r43347 | if b'remotebranches' in repo.names: | ||
remotenames += repo.names[b'remotebranches'].names(repo, ctx.node()) | ||||
|
r36080 | |||
|
r43346 | return templateutil.compatlist( | ||
|
r43347 | context, mapping, b'remotename', remotenames, plural=b'remotenames' | ||
|
r43346 | ) | ||
|
r36080 | |||
|
r43347 | @templatekeyword(b'remotebookmarks', requires={b'repo', b'ctx'}) | ||
|
r36609 | def remotebookmarkskw(context, mapping): | ||
|
r36458 | """List of strings. Remote bookmarks associated with the changeset.""" | ||
|
r43347 | repo = context.resource(mapping, b'repo') | ||
ctx = context.resource(mapping, b'ctx') | ||||
|
r36080 | |||
remotebmarks = [] | ||||
|
r43347 | if b'remotebookmarks' in repo.names: | ||
remotebmarks = repo.names[b'remotebookmarks'].names(repo, ctx.node()) | ||||
|
r36080 | |||
|
r43346 | return templateutil.compatlist( | ||
context, | ||||
mapping, | ||||
|
r43347 | b'remotebookmark', | ||
|
r43346 | remotebmarks, | ||
|
r43347 | plural=b'remotebookmarks', | ||
|
r43346 | ) | ||
|
r36080 | |||
|
r43347 | @templatekeyword(b'remotebranches', requires={b'repo', b'ctx'}) | ||
|
r36609 | def remotebrancheskw(context, mapping): | ||
|
r36458 | """List of strings. Remote branches associated with the changeset.""" | ||
|
r43347 | repo = context.resource(mapping, b'repo') | ||
ctx = context.resource(mapping, b'ctx') | ||||
|
r36080 | |||
remotebranches = [] | ||||
|
r43347 | if b'remotebranches' in repo.names: | ||
remotebranches = repo.names[b'remotebranches'].names(repo, ctx.node()) | ||||
|
r36080 | |||
|
r43346 | return templateutil.compatlist( | ||
context, | ||||
mapping, | ||||
|
r43347 | b'remotebranch', | ||
|
r43346 | remotebranches, | ||
|
r43347 | plural=b'remotebranches', | ||
|
r43346 | ) | ||
|
r36167 | |||
|
r40101 | def _revsetutil(repo, subset, x, rtypes): | ||
|
r36167 | """utility function to return a set of revs based on the rtypes""" | ||
|
r43347 | args = revsetlang.getargs(x, 0, 1, _(b'only one argument accepted')) | ||
|
r40101 | if args: | ||
kind, pattern, matcher = stringutil.stringmatcher( | ||||
|
r43347 | revsetlang.getstring(args[0], _(b'argument must be a string')) | ||
|
r43346 | ) | ||
|
r40101 | 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)) | ||
|
r43347 | if kind == b'literal' and not nodes: | ||
|
r43346 | raise error.RepoLookupError( | ||
|
r43347 | _(b"remote name '%s' does not exist") % pattern | ||
|
r43346 | ) | ||
|
r36167 | |||
|
r40103 | revs = (cl.rev(n) for n in nodes if cl.hasnode(n)) | ||
|
r40104 | return subset & smartset.baseset(revs) | ||
|
r36167 | |||
|
r43346 | |||
|
r43347 | @revsetpredicate(b'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`. | ||||
""" | ||||
|
r43347 | return _revsetutil(repo, subset, x, (b'remotebookmarks', b'remotebranches')) | ||
|
r40095 | |||
|
r43346 | |||
|
r43347 | @revsetpredicate(b'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`. | ||||
""" | ||||
|
r43347 | return _revsetutil(repo, subset, x, (b'remotebranches',)) | ||
|
r36167 | |||
|
r43346 | |||
|
r43347 | @revsetpredicate(b'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`. | ||||
""" | ||||
|
r43347 | return _revsetutil(repo, subset, x, (b'remotebookmarks',)) | ||