import os
import logging
import subprocess
from ConfigParser import ConfigParser
from ConfigParser import NoOptionError
from ConfigParser import NoSectionError
from zc.buildout import UserError
from .base import BaseRepo
from .base import SUBPROCESS_ENV
from .base import update_check_call
from ..utils import check_output
logger = logging.getLogger(__name__)
[docs]class HgRepo(BaseRepo):
vcs_control_dir = '.hg'
vcs_official_name = 'Mercurial'
[docs] def update_hgrc_paths(self):
"""Update hgrc paths section if needed.
Old paths are kept in renamed form: buildout_save_%d."""
parser = ConfigParser()
hgrc_path = os.path.join(self.target_dir, '.hg', 'hgrc')
parser.read(hgrc_path) # does not fail if file does not exist
previous = None
try:
previous = parser.get('paths', 'default')
except NoOptionError:
logger.info("No 'default' value for [paths] in %s, will set one",
hgrc_path)
except NoSectionError:
logger.info("Creating [paths] section in %s", hgrc_path)
parser.add_section('paths')
if previous == self.url:
return
if previous is not None:
count = 1
while True:
save = 'buildout_save_%d' % count
try:
parser.get('paths', save)
except NoOptionError:
break
count += 1
parser.set('paths', save, previous)
logger.info("Change of origin URL, saving previous value as %r in "
"[paths] section of %s", save, hgrc_path)
parser.set('paths', 'default', self.url)
f = open(hgrc_path, 'w')
parser.write(f)
f.close()
[docs] def uncommitted_changes(self):
"""True if we have uncommitted changes."""
return bool(check_output(['hg', '--cwd', self.target_dir, 'status'],
env=SUBPROCESS_ENV))
[docs] def parents(self, pip_compatible=False):
"""Return full hash of parent nodes.
:param pip_compatible: ignored, all Hg revspecs are pip compatible
"""
return check_output(['hg', '--cwd', self.target_dir, 'parents',
'--template={node}'],
env=SUBPROCESS_ENV).split()
[docs] def have_fixed_revision(self, revstr):
"""True if revstr is a fixed revision that we already have.
Check is done for known tags (except tip) and known nodes identified
by a long enough (12 char) prefix of their hexadecimal hash.
Summary of collision cases for hg up:
- a revision number has precedence over a tag identically named
- a tag that is a strict prefix of a full hexadecimal node hash wins
over that node.
- a full hexadecimal node hash wins over a tag that would be
identically named (someone would need to be really disturbed to do
that in real life).
- a hexadecimal node that coincides with a decimal revision number is
not something I can test :-)
In theory, a 12 char hexadecimal node hash could be shadowed by an
incoming tag. But also, any tag could be overridden. These are
considered to be fixed anyway for convenience in sensible use-cases.
People having CI robots involving tags that do get overridden by a
third party upstream should complain to upstream for utterly bad
practices.
"""
revstr = revstr.strip()
if revstr == 'tip' or not revstr:
return False
try:
out = check_output(['hg', '--cwd', self.target_dir, 'log',
'-r', revstr,
'--template={node}\n{tags}\n{rev}'],
env=SUBPROCESS_ENV)
except subprocess.CalledProcessError:
return False
node, tags, rev = out.split(os.linesep)
if node.startswith(revstr):
# if too short, can be superseded by an incoming tag
if len(revstr) >= 12:
logger.info("[hg] Found requested revision %r in %s",
revstr, self.target_dir)
return True
if revstr == rev:
logger.warn("[hg] In repo %s, you should not pinpoint revision "
"by a local revision number such as %r",
self.target_dir, revstr)
# but indeed, nothing can change it (unless one day a node has
# exactly that hash code, chances are...)
return True
if revstr in tags.split():
logger.info("[hg] In repo %s, found tag %r as %s",
self.target_dir, revstr, node)
return True
return False
[docs] def clean(self):
if not os.path.isdir(self.target_dir):
return
try:
subprocess.check_call(['hg', 'purge', '--cwd', self.target_dir])
except subprocess.CalledProcessError as exc:
if exc.returncode == 255:
# fallback to default implementation
logger.warn("The 'purge' Mercurial extension is not "
"activated. Do 'hg help purge' for more details "
"Falling back to default cleaning implementation")
super(HgRepo, self).clean()
[docs] def get_update(self, revision):
"""Ensure that target_dir is a clone of url at specified revision.
If target_dir already exists, does a simple pull.
Offline-mode: no clone nor pull, but update.
"""
target_dir = self.target_dir
url = self.url
offline = self.offline
if not os.path.exists(target_dir):
# TODO case of local url ?
if offline:
raise UserError(
"hg repository %r does not exist; "
"cannot clone it from %r (offline mode)" % (target_dir,
url))
logger.info("Cloning %s ...", url)
clone_cmd = ['hg', 'clone']
if revision:
clone_cmd.extend(['-r', revision])
clone_cmd.extend([url, target_dir])
subprocess.check_call(clone_cmd, env=SUBPROCESS_ENV)
else:
self.update_hgrc_paths()
# TODO what if remote repo is actually local fs ?
if self.have_fixed_revision(revision):
self._update(revision)
return
if not offline:
self._pull()
self._update(revision)
def _pull(self):
logger.info("Pull for hg repo %r ...", self.target_dir)
subprocess.check_call(['hg', '--cwd', self.target_dir, 'pull'],
env=SUBPROCESS_ENV)
def _update(self, revision):
target_dir = self.target_dir
logger.info("Updating %s to revision %s", target_dir, revision)
up_cmd = ['hg', '--cwd', target_dir, 'up']
if revision:
up_cmd.extend(['-r', revision])
update_check_call(up_cmd, env=SUBPROCESS_ENV)
[docs] def archive(self, target_path):
subprocess.check_call(['hg', '--cwd', self.target_dir,
'archive', target_path])