Newer
Older
# -*- coding: utf-8 -*-
#
# path pattern: define global path aliases, cloneto command
#
# Copyright (c) 2015-2016 Marcin Kasperski <Marcin.Kasperski@mekk.waw.pl>
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# 3. The name of the author may not be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
# See README.txt for more details.
"""Define [paths] once and reuse over many repositories.
path_pattern
------------
This extension frees you from defining and maintaining [paths]
individually for every repository. Instead, you may define general
patterns of how to resolve paths.
For example, write::
[path_pattern]
abc.local = ~/abcdevel/{repo}
abc.remote = ssh://johny@devel.abc.com/sources/{repo}
dev.local = ~/sources/{repo}
dev.remote = https://tim@devel-department.local/{repo}
and use ``hg push abc`` in any repo kept below ``~/abcdevel`` or ``hg
pull dev`` in any repo below ``~/sources``.
cloneto
-------
The extension defines also ``cloneto`` helper, which clones current
repo to address specified by given path. It works especially well
together with patterns. With the example above:
cd ~/sources/libs
hg init xyz
cd xyz
hg cloneto dev
Marcin Kasperski
committed
More information
----------------
For more information, see path_pattern README or
http://bitbucket.org/Mekk/mercurial-path_pattern/
"""
from mercurial import commands, util
from mercurial.i18n import _
import os
import sys
def import_meu():
"""Importing mercurial_extension_utils so it can be found also outside
Python PATH (support for TortoiseHG/Win and similar setups)"""
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
try:
import mercurial_extension_utils
except ImportError:
my_dir = os.path.dirname(__file__)
sys.path.extend([
# In the same dir (manual or site-packages after pip)
my_dir,
# Developer clone
os.path.join(os.path.dirname(my_dir), "extension_utils"),
# Side clone
os.path.join(os.path.dirname(my_dir), "mercurial-extension_utils"),
])
try:
import mercurial_extension_utils
except ImportError:
raise util.Abort(_("""Can not import mercurial_extension_utils.
Please install this module in Python path.
See Installation chapter in https://bitbucket.org/Mekk/mercurial-dynamic_username/ for details
(and for info about TortoiseHG on Windows, or other bundled Python)."""))
return mercurial_extension_utils
meu = import_meu()
# pylint:disable=fixme,line-too-long,invalid-name
# (invalid-name because of ui and cmdtable)
############################################################
# Utility classes and functions
############################################################
class PatternPair(object):
"""
Represents individual path pattern - pair of local (like
"~/sources/{path}") and remote (like "ssh://some/where/{path}")
"""
re_dotted_alias = re.compile(r'^ ([^\.]+) \. .*$', re.VERBOSE)
Marcin Kasperski
committed
def __init__(self, prefix):
self.prefix = prefix
match = self.re_dotted_alias.search(prefix)
if match:
self.alias = match.group(1)
else:
self.alias = prefix
self.local = None
self.remote = None
self.enforce = False
def lookup_remote(self, local_directory, ui):
"""
Checks whether local directory matches, if so, returns
matching remote, if not, returns None.
"""
value = None
if self.local and self.remote:
match = self.local.search(local_directory)
if match is not None:
value = self.remote.fill(**match)
if not value:
ui.warn(_("path_pattern: Invalid pattern - markers mismatch between %s.local and %s.remote\n")
Marcin Kasperski
committed
% (self.prefix, self.prefix))
return value
def learn_local(self, path_text, ui):
"""Parse and save local path"""
self.local = meu.DirectoryPattern(path_text, ui)
def learn_remote(self, path_text, ui):
"""Parse and save remote path"""
self.remote = meu.TextFiller(path_text, ui)
# TODO: validate whether path_text looks like path
def validate(self, ui):
"""Checks whether both sides are defined and valid. Warns about problems"""
for name, obj in [("local", self.local),
("remote", self.remote)]:
if not obj:
ui.warn(
Marcin Kasperski
committed
_("path_pattern: Incomplete pattern - missing %s.%s\n") % (self.prefix, name))
return False
if not obj.is_valid():
ui.warn(
_("path_pattern: Invalid pattern %s.%s - bad syntax\n") % (self.prefix, name))
return False
return True
def describe(self):
return _("%s%s\n local: %s\n remote: %s") % (
self.alias,
Marcin Kasperski
committed
self.enforce and " [*]" or "",
self.local.pattern_text, self.remote.fill_text)
_re_lead = re.compile(r'^([^{(]*)')
_re_fill = re.compile(r'(?: { [^}]* } | \( [^)]* \) )', re.VERBOSE)
def calc_score(self):
"""Calculates ”strength” of local part of the pattern, used to prioritize
patterns."""
match = self._re_lead.search(self.local.pattern_text)
lead_items = match.group(1).replace('\\', '/').split('/')
text_without_fillers = self._re_fill.sub('', self.local.pattern_text)
return (2 if self.enforce else 0, len(lead_items), len(text_without_fillers))
class PathPatterns(object):
"""
Loads and parses pattern definitions
"""
def __init__(self, ui):
rgxp_prefix = r'^(\w+(?:\.[\w0-9-]+)?)\.'
Marcin Kasperski
committed
self.patterns = {} # prefix → PatternPair
# Read «sth».local
for prefix, value in meu.rgxp_config_items(
ui, "path_pattern", re.compile(rgxp_prefix + "local")):
Marcin Kasperski
committed
if prefix not in self.patterns:
self.patterns[prefix] = PatternPair(prefix)
ui.debug(_("path_pattern: Parsing local side of path pattern %s\n") % prefix)
Marcin Kasperski
committed
self.patterns[prefix].learn_local(value, ui)
# Read «sth».remote
for prefix, value in meu.rgxp_config_items(
ui, "path_pattern", re.compile(rgxp_prefix + "remote")):
Marcin Kasperski
committed
if prefix not in self.patterns:
self.patterns[prefix] = PatternPair(prefix)
ui.debug(_("path_pattern: Parsing remote side of path pattern %s\n") % prefix)
Marcin Kasperski
committed
self.patterns[prefix].learn_remote(value, ui)
# Read «sth».enforce
for prefix, value in meu.rgxp_configbool_items(
ui, "path_pattern", re.compile(rgxp_prefix + "enforce")):
Marcin Kasperski
committed
if prefix in self.patterns:
self.patterns[prefix].enforce = value
# Read «sth».alias
for prefix, value in meu.rgxp_config_items(
ui, "path_pattern", re.compile(rgxp_prefix + "alias")):
Marcin Kasperski
committed
if prefix in self.patterns:
self.patterns[prefix].alias = value
# Check for incomplete and invalid items
Marcin Kasperski
committed
for prefix in self.patterns.keys()[:]:
if not self.patterns[prefix].validate(ui):
del self.patterns[prefix]
def generate_paths(self, ui, repo):
"""
Updates ui config with new path's for given repo, generated from patterns
"""
HGRC_PATH_SCORE = (1,)
# Support for priority management. This is a mapping
# alias → (name, score)
# where score is a tuple reflecting the priority (first item is
# 0 for normal patterns, 1 for per-repo paths, 2 for enforced patterns,
# see calc_score)
known_aliases = {}
# Reading known paths to avoid overwriting them (unless enforced).
for key, value in ui.configitems("paths"):
known_aliases[key] = (".hg/hgrc in " + repo.root, HGRC_PATH_SCORE)
# Actually applying new patterns (with respect to prioritization)
Marcin Kasperski
committed
for prefix, pattern_pair in self.patterns.iteritems():
expanded = pattern_pair.lookup_remote(repo.root, ui)
if expanded:
Marcin Kasperski
committed
path_alias = pattern_pair.alias
score = pattern_pair.calc_score()
if path_alias in known_aliases:
prev_name, prev_score = known_aliases[path_alias]
if score < prev_score:
ui.debug(_("path_pattern: NOT defining path %s as %s (pattern %s is less important than %s)\n") % (path_alias, expanded, prefix, prev_name))
continue
else:
ui.debug(_("path_pattern: Defining path %s as %s (using pattern %s, overriding %s applied earlier as new pattern is stronger\n") % (path_alias, expanded, prefix, prev_name))
else:
Marcin Kasperski
committed
ui.debug(_("path_pattern: Defining path %s as %s (using pattern %s)\n") % (path_alias, expanded, prefix))
known_aliases[path_alias] = (prefix, score)
ui.setconfig("paths", path_alias, expanded)
def print_patterns(self, ui, list_repos=False):
"""
Prints pattern information to standard output
"""
if self.patterns:
Marcin Kasperski
committed
has_enforced = any(p.enforce for p in self.patterns.values())
ui.status(_("Defined path patterns:\n%s\n%s") % (
"\n".join(
self.patterns[pname].describe()
for pname in sorted(self.patterns.keys())),
_("[*]-marked patterns are enforced over .hg/hgrc\n") if has_enforced else "",
Marcin Kasperski
committed
))
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
else:
ui.status(_("No path patterns defined. Add [path_pattern] section to ~/.hgrc\n"))
############################################################
# Mercurial extension hooks
############################################################
patterns = None
# def uisetup(ui):
# Not used, better to load patterns later, config can be updated by plugins
def extsetup(ui):
"""Setup extension: load patterns definitions from config"""
global patterns # pylint:disable=global-statement
patterns = PathPatterns(ui)
def reposetup(ui, repo):
"""Setup repo: add pattern-based paths to repository config"""
# Checking whether this is local repository, for other types extension
# is pointless. Unfortunately we can't test repo type, as some extensions
# change it (for example hgext.git.hgrepo.hgrepo happens to me…)
if not hasattr(repo, 'root'):
return
patterns.generate_paths(ui, repo)
############################################################
# Commands
############################################################
cmdtable = {}
command = meu.command(cmdtable)
@command("list_path_patterns",
[
# ('r', 'list-repos', None, 'List repositories matching the pattern'),
],
_("list_path_patterns"),
norepo=True)
def cmd_list_path_patterns(ui, **opts):
"""
List all active path patterns.
"""
patterns.print_patterns(ui)
@command("cloneto",
[],
_("cloneto ALIAS"))
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
def cmd_cloneto(ui, repo, path_alias, **opts):
"""
Clone current repository to (usually remote) url
pointed by already defined path alias::
hg cloneto somealias
is equivalent to::
hg clone . <aliaspath>
where <aliaspath> is whatever somealias expands to
according to ``hg paths``.
Command most useful together with path_pattern.
"""
known_paths = []
for key, value in ui.configitems("paths"):
if key == path_alias:
ui.status(_("Cloning current repository to %s (resolved from: %s)\n" % (value, path_alias)))
return commands.clone(ui, source=repo, dest=value)
else:
known_paths.append(key)
# Failing helpfully
if known_paths:
raise util.Abort(_("Unknown alias: %s. Defined path aliases: %s") % (
path_alias, ", ".join(known_paths)))
else:
raise util.Abort(_("Uknown alias: %s. No paths defined, consider creating some paths or path_patterns") % path_alias)
############################################################
# Extension setup
############################################################
testedwith = '2.7 2.9 3.0 3.3 3.6 3.7'
buglink = 'https://bitbucket.org/Mekk/mercurial-path_pattern/issues'