build/util/hg.py

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/build/util/hg.py	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,611 @@
     1.4 +"""Functions for interacting with hg"""
     1.5 +import os
     1.6 +import re
     1.7 +import subprocess
     1.8 +from urlparse import urlsplit
     1.9 +from ConfigParser import RawConfigParser
    1.10 +
    1.11 +from util.commands import run_cmd, get_output, remove_path
    1.12 +from util.retry import retry
    1.13 +
    1.14 +import logging
    1.15 +log = logging.getLogger(__name__)
    1.16 +
    1.17 +
    1.18 +class DefaultShareBase:
    1.19 +    pass
    1.20 +DefaultShareBase = DefaultShareBase()
    1.21 +
    1.22 +
    1.23 +class HgUtilError(Exception):
    1.24 +    pass
    1.25 +
    1.26 +
    1.27 +def _make_absolute(repo):
    1.28 +    if repo.startswith("file://"):
    1.29 +        path = repo[len("file://"):]
    1.30 +        repo = "file://%s" % os.path.abspath(path)
    1.31 +    elif "://" not in repo:
    1.32 +        repo = os.path.abspath(repo)
    1.33 +    return repo
    1.34 +
    1.35 +
    1.36 +def make_hg_url(hgHost, repoPath, protocol='https', revision=None,
    1.37 +                filename=None):
    1.38 +    """construct a valid hg url from a base hg url (hg.mozilla.org),
    1.39 +    repoPath, revision and possible filename"""
    1.40 +    base = '%s://%s' % (protocol, hgHost)
    1.41 +    repo = '/'.join(p.strip('/') for p in [base, repoPath])
    1.42 +    if not filename:
    1.43 +        if not revision:
    1.44 +            return repo
    1.45 +        else:
    1.46 +            return '/'.join([p.strip('/') for p in [repo, 'rev', revision]])
    1.47 +    else:
    1.48 +        assert revision
    1.49 +        return '/'.join([p.strip('/') for p in [repo, 'raw-file', revision, filename]])
    1.50 +
    1.51 +
    1.52 +def get_repo_name(repo):
    1.53 +    return repo.rstrip('/').split('/')[-1]
    1.54 +
    1.55 +
    1.56 +def get_repo_path(repo):
    1.57 +    repo = _make_absolute(repo)
    1.58 +    if repo.startswith("/"):
    1.59 +        return repo.lstrip("/")
    1.60 +    else:
    1.61 +        return urlsplit(repo).path.lstrip("/")
    1.62 +
    1.63 +
    1.64 +def get_revision(path):
    1.65 +    """Returns which revision directory `path` currently has checked out."""
    1.66 +    return get_output(['hg', 'parent', '--template', '{node|short}'], cwd=path)
    1.67 +
    1.68 +
    1.69 +def get_branch(path):
    1.70 +    return get_output(['hg', 'branch'], cwd=path).strip()
    1.71 +
    1.72 +
    1.73 +def get_branches(path):
    1.74 +    branches = []
    1.75 +    for line in get_output(['hg', 'branches', '-c'], cwd=path).splitlines():
    1.76 +        branches.append(line.split()[0])
    1.77 +    return branches
    1.78 +
    1.79 +
    1.80 +def is_hg_cset(rev):
    1.81 +    """Retruns True if passed revision represents a valid HG revision
    1.82 +    (long or short(er) 40 bit hex)"""
    1.83 +    try:
    1.84 +        int(rev, 16)
    1.85 +        return True
    1.86 +    except (TypeError, ValueError):
    1.87 +        return False
    1.88 +
    1.89 +
    1.90 +def hg_ver():
    1.91 +    """Returns the current version of hg, as a tuple of
    1.92 +    (major, minor, build)"""
    1.93 +    ver_string = get_output(['hg', '-q', 'version'])
    1.94 +    match = re.search("\(version ([0-9.]+)\)", ver_string)
    1.95 +    if match:
    1.96 +        bits = match.group(1).split(".")
    1.97 +        if len(bits) < 3:
    1.98 +            bits += (0,)
    1.99 +        ver = tuple(int(b) for b in bits)
   1.100 +    else:
   1.101 +        ver = (0, 0, 0)
   1.102 +    log.debug("Running hg version %s", ver)
   1.103 +    return ver
   1.104 +
   1.105 +
   1.106 +def purge(dest):
   1.107 +    """Purge the repository of all untracked and ignored files."""
   1.108 +    try:
   1.109 +        run_cmd(['hg', '--config', 'extensions.purge=', 'purge',
   1.110 +                 '-a', '--all', dest], cwd=dest)
   1.111 +    except subprocess.CalledProcessError, e:
   1.112 +        log.debug('purge failed: %s' % e)
   1.113 +        raise
   1.114 +
   1.115 +
   1.116 +def update(dest, branch=None, revision=None):
   1.117 +    """Updates working copy `dest` to `branch` or `revision`.  If neither is
   1.118 +    set then the working copy will be updated to the latest revision on the
   1.119 +    current branch.  Local changes will be discarded."""
   1.120 +    # If we have a revision, switch to that
   1.121 +    if revision is not None:
   1.122 +        cmd = ['hg', 'update', '-C', '-r', revision]
   1.123 +        run_cmd(cmd, cwd=dest)
   1.124 +    else:
   1.125 +        # Check & switch branch
   1.126 +        local_branch = get_output(['hg', 'branch'], cwd=dest).strip()
   1.127 +
   1.128 +        cmd = ['hg', 'update', '-C']
   1.129 +
   1.130 +        # If this is different, checkout the other branch
   1.131 +        if branch and branch != local_branch:
   1.132 +            cmd.append(branch)
   1.133 +
   1.134 +        run_cmd(cmd, cwd=dest)
   1.135 +    return get_revision(dest)
   1.136 +
   1.137 +
   1.138 +def clone(repo, dest, branch=None, revision=None, update_dest=True,
   1.139 +          clone_by_rev=False, mirrors=None, bundles=None):
   1.140 +    """Clones hg repo and places it at `dest`, replacing whatever else is
   1.141 +    there.  The working copy will be empty.
   1.142 +
   1.143 +    If `revision` is set, only the specified revision and its ancestors will
   1.144 +    be cloned.
   1.145 +
   1.146 +    If `update_dest` is set, then `dest` will be updated to `revision` if
   1.147 +    set, otherwise to `branch`, otherwise to the head of default.
   1.148 +
   1.149 +    If `mirrors` is set, will try and clone from the mirrors before
   1.150 +    cloning from `repo`.
   1.151 +
   1.152 +    If `bundles` is set, will try and download the bundle first and
   1.153 +    unbundle it. If successful, will pull in new revisions from mirrors or
   1.154 +    the master repo. If unbundling fails, will fall back to doing a regular
   1.155 +    clone from mirrors or the master repo.
   1.156 +
   1.157 +    Regardless of how the repository ends up being cloned, the 'default' path
   1.158 +    will point to `repo`.
   1.159 +    """
   1.160 +    if os.path.exists(dest):
   1.161 +        remove_path(dest)
   1.162 +
   1.163 +    if bundles:
   1.164 +        log.info("Attempting to initialize clone with bundles")
   1.165 +        for bundle in bundles:
   1.166 +            if os.path.exists(dest):
   1.167 +                remove_path(dest)
   1.168 +            init(dest)
   1.169 +            log.info("Trying to use bundle %s", bundle)
   1.170 +            try:
   1.171 +                if not unbundle(bundle, dest):
   1.172 +                    remove_path(dest)
   1.173 +                    continue
   1.174 +                adjust_paths(dest, default=repo)
   1.175 +                # Now pull / update
   1.176 +                return pull(repo, dest, update_dest=update_dest,
   1.177 +                            mirrors=mirrors, revision=revision, branch=branch)
   1.178 +            except Exception:
   1.179 +                remove_path(dest)
   1.180 +                log.exception("Problem unbundling/pulling from %s", bundle)
   1.181 +                continue
   1.182 +        else:
   1.183 +            log.info("Using bundles failed; falling back to clone")
   1.184 +
   1.185 +    if mirrors:
   1.186 +        log.info("Attempting to clone from mirrors")
   1.187 +        for mirror in mirrors:
   1.188 +            log.info("Cloning from %s", mirror)
   1.189 +            try:
   1.190 +                retval = clone(mirror, dest, branch, revision,
   1.191 +                               update_dest=update_dest, clone_by_rev=clone_by_rev)
   1.192 +                adjust_paths(dest, default=repo)
   1.193 +                return retval
   1.194 +            except:
   1.195 +                log.exception("Problem cloning from mirror %s", mirror)
   1.196 +                continue
   1.197 +        else:
   1.198 +            log.info("Pulling from mirrors failed; falling back to %s", repo)
   1.199 +            # We may have a partial repo here; mercurial() copes with that
   1.200 +            # We need to make sure our paths are correct though
   1.201 +            if os.path.exists(os.path.join(dest, '.hg')):
   1.202 +                adjust_paths(dest, default=repo)
   1.203 +            return mercurial(repo, dest, branch, revision, autoPurge=True,
   1.204 +                             update_dest=update_dest, clone_by_rev=clone_by_rev)
   1.205 +
   1.206 +    cmd = ['hg', 'clone']
   1.207 +    if not update_dest:
   1.208 +        cmd.append('-U')
   1.209 +
   1.210 +    if clone_by_rev:
   1.211 +        if revision:
   1.212 +            cmd.extend(['-r', revision])
   1.213 +        elif branch:
   1.214 +            # hg >= 1.6 supports -b branch for cloning
   1.215 +            ver = hg_ver()
   1.216 +            if ver >= (1, 6, 0):
   1.217 +                cmd.extend(['-b', branch])
   1.218 +
   1.219 +    cmd.extend([repo, dest])
   1.220 +    run_cmd(cmd)
   1.221 +
   1.222 +    if update_dest:
   1.223 +        return update(dest, branch, revision)
   1.224 +
   1.225 +
   1.226 +def common_args(revision=None, branch=None, ssh_username=None, ssh_key=None):
   1.227 +    """Fill in common hg arguments, encapsulating logic checks that depend on
   1.228 +       mercurial versions and provided arguments"""
   1.229 +    args = []
   1.230 +    if ssh_username or ssh_key:
   1.231 +        opt = ['-e', 'ssh']
   1.232 +        if ssh_username:
   1.233 +            opt[1] += ' -l %s' % ssh_username
   1.234 +        if ssh_key:
   1.235 +            opt[1] += ' -i %s' % ssh_key
   1.236 +        args.extend(opt)
   1.237 +    if revision:
   1.238 +        args.extend(['-r', revision])
   1.239 +    elif branch:
   1.240 +        if hg_ver() >= (1, 6, 0):
   1.241 +            args.extend(['-b', branch])
   1.242 +    return args
   1.243 +
   1.244 +
   1.245 +def pull(repo, dest, update_dest=True, mirrors=None, **kwargs):
   1.246 +    """Pulls changes from hg repo and places it in `dest`.
   1.247 +
   1.248 +    If `update_dest` is set, then `dest` will be updated to `revision` if
   1.249 +    set, otherwise to `branch`, otherwise to the head of default.
   1.250 +
   1.251 +    If `mirrors` is set, will try and pull from the mirrors first before
   1.252 +    `repo`."""
   1.253 +
   1.254 +    if mirrors:
   1.255 +        for mirror in mirrors:
   1.256 +            try:
   1.257 +                return pull(mirror, dest, update_dest=update_dest, **kwargs)
   1.258 +            except:
   1.259 +                log.exception("Problem pulling from mirror %s", mirror)
   1.260 +                continue
   1.261 +        else:
   1.262 +            log.info("Pulling from mirrors failed; falling back to %s", repo)
   1.263 +
   1.264 +    # Convert repo to an absolute path if it's a local repository
   1.265 +    repo = _make_absolute(repo)
   1.266 +    cmd = ['hg', 'pull']
   1.267 +    # Don't pass -r to "hg pull", except when it's a valid HG revision.
   1.268 +    # Pulling using tag names is dangerous: it uses the local .hgtags, so if
   1.269 +    # the tag has moved on the remote side you won't pull the new revision the
   1.270 +    # remote tag refers to.
   1.271 +    pull_kwargs = kwargs.copy()
   1.272 +    if 'revision' in pull_kwargs and \
   1.273 +       not is_hg_cset(pull_kwargs['revision']):
   1.274 +        del pull_kwargs['revision']
   1.275 +
   1.276 +    cmd.extend(common_args(**pull_kwargs))
   1.277 +
   1.278 +    cmd.append(repo)
   1.279 +    run_cmd(cmd, cwd=dest)
   1.280 +
   1.281 +    if update_dest:
   1.282 +        branch = None
   1.283 +        if 'branch' in kwargs and kwargs['branch']:
   1.284 +            branch = kwargs['branch']
   1.285 +        revision = None
   1.286 +        if 'revision' in kwargs and kwargs['revision']:
   1.287 +            revision = kwargs['revision']
   1.288 +        return update(dest, branch=branch, revision=revision)
   1.289 +
   1.290 +# Defines the places of attributes in the tuples returned by `out'
   1.291 +REVISION, BRANCH = 0, 1
   1.292 +
   1.293 +
   1.294 +def out(src, remote, **kwargs):
   1.295 +    """Check for outgoing changesets present in a repo"""
   1.296 +    cmd = ['hg', '-q', 'out', '--template', '{node} {branches}\n']
   1.297 +    cmd.extend(common_args(**kwargs))
   1.298 +    cmd.append(remote)
   1.299 +    if os.path.exists(src):
   1.300 +        try:
   1.301 +            revs = []
   1.302 +            for line in get_output(cmd, cwd=src).rstrip().split("\n"):
   1.303 +                try:
   1.304 +                    rev, branch = line.split()
   1.305 +                # Mercurial displays no branch at all if the revision is on
   1.306 +                # "default"
   1.307 +                except ValueError:
   1.308 +                    rev = line.rstrip()
   1.309 +                    branch = "default"
   1.310 +                revs.append((rev, branch))
   1.311 +            return revs
   1.312 +        except subprocess.CalledProcessError, inst:
   1.313 +            # In some situations, some versions of Mercurial return "1"
   1.314 +            # if no changes are found, so we need to ignore this return code
   1.315 +            if inst.returncode == 1:
   1.316 +                return []
   1.317 +            raise
   1.318 +
   1.319 +
   1.320 +def push(src, remote, push_new_branches=True, force=False, **kwargs):
   1.321 +    cmd = ['hg', 'push']
   1.322 +    cmd.extend(common_args(**kwargs))
   1.323 +    if force:
   1.324 +        cmd.append('-f')
   1.325 +    if push_new_branches:
   1.326 +        cmd.append('--new-branch')
   1.327 +    cmd.append(remote)
   1.328 +    run_cmd(cmd, cwd=src)
   1.329 +
   1.330 +
   1.331 +def mercurial(repo, dest, branch=None, revision=None, update_dest=True,
   1.332 +              shareBase=DefaultShareBase, allowUnsharedLocalClones=False,
   1.333 +              clone_by_rev=False, mirrors=None, bundles=None, autoPurge=False):
   1.334 +    """Makes sure that `dest` is has `revision` or `branch` checked out from
   1.335 +    `repo`.
   1.336 +
   1.337 +    Do what it takes to make that happen, including possibly clobbering
   1.338 +    dest.
   1.339 +
   1.340 +    If allowUnsharedLocalClones is True and we're trying to use the share
   1.341 +    extension but fail, then we will be able to clone from the shared repo to
   1.342 +    our destination.  If this is False, the default, then if we don't have the
   1.343 +    share extension we will just clone from the remote repository.
   1.344 +
   1.345 +    If `clone_by_rev` is True, use 'hg clone -r <rev>' instead of 'hg clone'.
   1.346 +    This is slower, but useful when cloning repos with lots of heads.
   1.347 +
   1.348 +    If `mirrors` is set, will try and use the mirrors before `repo`.
   1.349 +
   1.350 +    If `bundles` is set, will try and download the bundle first and
   1.351 +    unbundle it instead of doing a full clone. If successful, will pull in
   1.352 +    new revisions from mirrors or the master repo. If unbundling fails, will
   1.353 +    fall back to doing a regular clone from mirrors or the master repo.
   1.354 +    """
   1.355 +    dest = os.path.abspath(dest)
   1.356 +    if shareBase is DefaultShareBase:
   1.357 +        shareBase = os.environ.get("HG_SHARE_BASE_DIR", None)
   1.358 +
   1.359 +    log.info("Reporting hg version in use")
   1.360 +    cmd = ['hg', '-q', 'version']
   1.361 +    run_cmd(cmd, cwd='.')
   1.362 +
   1.363 +    if shareBase:
   1.364 +        # Check that 'hg share' works
   1.365 +        try:
   1.366 +            log.info("Checking if share extension works")
   1.367 +            output = get_output(['hg', 'help', 'share'], dont_log=True)
   1.368 +            if 'no commands defined' in output:
   1.369 +                # Share extension is enabled, but not functional
   1.370 +                log.info("Disabling sharing since share extension doesn't seem to work (1)")
   1.371 +                shareBase = None
   1.372 +            elif 'unknown command' in output:
   1.373 +                # Share extension is disabled
   1.374 +                log.info("Disabling sharing since share extension doesn't seem to work (2)")
   1.375 +                shareBase = None
   1.376 +        except subprocess.CalledProcessError:
   1.377 +            # The command failed, so disable sharing
   1.378 +            log.info("Disabling sharing since share extension doesn't seem to work (3)")
   1.379 +            shareBase = None
   1.380 +
   1.381 +    # Check that our default path is correct
   1.382 +    if os.path.exists(os.path.join(dest, '.hg')):
   1.383 +        hgpath = path(dest, "default")
   1.384 +
   1.385 +        # Make sure that our default path is correct
   1.386 +        if hgpath != _make_absolute(repo):
   1.387 +            log.info("hg path isn't correct (%s should be %s); clobbering",
   1.388 +                     hgpath, _make_absolute(repo))
   1.389 +            remove_path(dest)
   1.390 +
   1.391 +    # If the working directory already exists and isn't using share we update
   1.392 +    # the working directory directly from the repo, ignoring the sharing
   1.393 +    # settings
   1.394 +    if os.path.exists(dest):
   1.395 +        if not os.path.exists(os.path.join(dest, ".hg")):
   1.396 +            log.warning("%s doesn't appear to be a valid hg directory; clobbering", dest)
   1.397 +            remove_path(dest)
   1.398 +        elif not os.path.exists(os.path.join(dest, ".hg", "sharedpath")):
   1.399 +            try:
   1.400 +                if autoPurge:
   1.401 +                    purge(dest)
   1.402 +                return pull(repo, dest, update_dest=update_dest, branch=branch,
   1.403 +                            revision=revision,
   1.404 +                            mirrors=mirrors)
   1.405 +            except subprocess.CalledProcessError:
   1.406 +                log.warning("Error pulling changes into %s from %s; clobbering", dest, repo)
   1.407 +                log.debug("Exception:", exc_info=True)
   1.408 +                remove_path(dest)
   1.409 +
   1.410 +    # If that fails for any reason, and sharing is requested, we'll try to
   1.411 +    # update the shared repository, and then update the working directory from
   1.412 +    # that.
   1.413 +    if shareBase:
   1.414 +        sharedRepo = os.path.join(shareBase, get_repo_path(repo))
   1.415 +        dest_sharedPath = os.path.join(dest, '.hg', 'sharedpath')
   1.416 +
   1.417 +        if os.path.exists(sharedRepo):
   1.418 +            hgpath = path(sharedRepo, "default")
   1.419 +
   1.420 +            # Make sure that our default path is correct
   1.421 +            if hgpath != _make_absolute(repo):
   1.422 +                log.info("hg path isn't correct (%s should be %s); clobbering",
   1.423 +                         hgpath, _make_absolute(repo))
   1.424 +                # we need to clobber both the shared checkout and the dest,
   1.425 +                # since hgrc needs to be in both places
   1.426 +                remove_path(sharedRepo)
   1.427 +                remove_path(dest)
   1.428 +
   1.429 +        if os.path.exists(dest_sharedPath):
   1.430 +            # Make sure that the sharedpath points to sharedRepo
   1.431 +            dest_sharedPath_data = os.path.normpath(
   1.432 +                open(dest_sharedPath).read())
   1.433 +            norm_sharedRepo = os.path.normpath(os.path.join(sharedRepo, '.hg'))
   1.434 +            if dest_sharedPath_data != norm_sharedRepo:
   1.435 +                # Clobber!
   1.436 +                log.info("We're currently shared from %s, but are being requested to pull from %s (%s); clobbering",
   1.437 +                         dest_sharedPath_data, repo, norm_sharedRepo)
   1.438 +                remove_path(dest)
   1.439 +
   1.440 +        try:
   1.441 +            log.info("Updating shared repo")
   1.442 +            mercurial(repo, sharedRepo, branch=branch, revision=revision,
   1.443 +                      update_dest=False, shareBase=None, clone_by_rev=clone_by_rev,
   1.444 +                      mirrors=mirrors, bundles=bundles, autoPurge=False)
   1.445 +            if os.path.exists(dest):
   1.446 +                if autoPurge:
   1.447 +                    purge(dest)
   1.448 +                return update(dest, branch=branch, revision=revision)
   1.449 +
   1.450 +            try:
   1.451 +                log.info("Trying to share %s to %s", sharedRepo, dest)
   1.452 +                return share(sharedRepo, dest, branch=branch, revision=revision)
   1.453 +            except subprocess.CalledProcessError:
   1.454 +                if not allowUnsharedLocalClones:
   1.455 +                    # Re-raise the exception so it gets caught below.
   1.456 +                    # We'll then clobber dest, and clone from original repo
   1.457 +                    raise
   1.458 +
   1.459 +                log.warning("Error calling hg share from %s to %s;"
   1.460 +                            "falling back to normal clone from shared repo",
   1.461 +                            sharedRepo, dest)
   1.462 +                # Do a full local clone first, and then update to the
   1.463 +                # revision we want
   1.464 +                # This lets us use hardlinks for the local clone if the OS
   1.465 +                # supports it
   1.466 +                clone(sharedRepo, dest, update_dest=False,
   1.467 +                      mirrors=mirrors, bundles=bundles)
   1.468 +                return update(dest, branch=branch, revision=revision)
   1.469 +        except subprocess.CalledProcessError:
   1.470 +            log.warning(
   1.471 +                "Error updating %s from sharedRepo (%s): ", dest, sharedRepo)
   1.472 +            log.debug("Exception:", exc_info=True)
   1.473 +            remove_path(dest)
   1.474 +    # end if shareBase
   1.475 +
   1.476 +    if not os.path.exists(os.path.dirname(dest)):
   1.477 +        os.makedirs(os.path.dirname(dest))
   1.478 +
   1.479 +    # Share isn't available or has failed, clone directly from the source
   1.480 +    return clone(repo, dest, branch, revision,
   1.481 +                 update_dest=update_dest, mirrors=mirrors,
   1.482 +                 bundles=bundles, clone_by_rev=clone_by_rev)
   1.483 +
   1.484 +
   1.485 +def apply_and_push(localrepo, remote, changer, max_attempts=10,
   1.486 +                   ssh_username=None, ssh_key=None, force=False):
   1.487 +    """This function calls `changer' to make changes to the repo, and tries
   1.488 +       its hardest to get them to the origin repo. `changer' must be a
   1.489 +       callable object that receives two arguments: the directory of the local
   1.490 +       repository, and the attempt number. This function will push ALL
   1.491 +       changesets missing from remote."""
   1.492 +    assert callable(changer)
   1.493 +    branch = get_branch(localrepo)
   1.494 +    changer(localrepo, 1)
   1.495 +    for n in range(1, max_attempts + 1):
   1.496 +        new_revs = []
   1.497 +        try:
   1.498 +            new_revs = out(src=localrepo, remote=remote,
   1.499 +                           ssh_username=ssh_username,
   1.500 +                           ssh_key=ssh_key)
   1.501 +            if len(new_revs) < 1:
   1.502 +                raise HgUtilError("No revs to push")
   1.503 +            push(src=localrepo, remote=remote, ssh_username=ssh_username,
   1.504 +                 ssh_key=ssh_key, force=force)
   1.505 +            return
   1.506 +        except subprocess.CalledProcessError, e:
   1.507 +            log.debug("Hit error when trying to push: %s" % str(e))
   1.508 +            if n == max_attempts:
   1.509 +                log.debug("Tried %d times, giving up" % max_attempts)
   1.510 +                for r in reversed(new_revs):
   1.511 +                    run_cmd(['hg', '--config', 'extensions.mq=', 'strip', '-n',
   1.512 +                             r[REVISION]], cwd=localrepo)
   1.513 +                raise HgUtilError("Failed to push")
   1.514 +            pull(remote, localrepo, update_dest=False,
   1.515 +                 ssh_username=ssh_username, ssh_key=ssh_key)
   1.516 +            # After we successfully rebase or strip away heads the push is
   1.517 +            # is attempted again at the start of the loop
   1.518 +            try:
   1.519 +                run_cmd(['hg', '--config', 'ui.merge=internal:merge',
   1.520 +                         'rebase'], cwd=localrepo)
   1.521 +            except subprocess.CalledProcessError, e:
   1.522 +                log.debug("Failed to rebase: %s" % str(e))
   1.523 +                update(localrepo, branch=branch)
   1.524 +                for r in reversed(new_revs):
   1.525 +                    run_cmd(['hg', '--config', 'extensions.mq=', 'strip', '-n',
   1.526 +                             r[REVISION]], cwd=localrepo)
   1.527 +                changer(localrepo, n + 1)
   1.528 +
   1.529 +
   1.530 +def share(source, dest, branch=None, revision=None):
   1.531 +    """Creates a new working directory in "dest" that shares history with
   1.532 +       "source" using Mercurial's share extension"""
   1.533 +    run_cmd(['hg', 'share', '-U', source, dest])
   1.534 +    return update(dest, branch=branch, revision=revision)
   1.535 +
   1.536 +
   1.537 +def cleanOutgoingRevs(reponame, remote, username, sshKey):
   1.538 +    outgoingRevs = retry(out, kwargs=dict(src=reponame, remote=remote,
   1.539 +                                          ssh_username=username,
   1.540 +                                          ssh_key=sshKey))
   1.541 +    for r in reversed(outgoingRevs):
   1.542 +        run_cmd(['hg', '--config', 'extensions.mq=', 'strip', '-n',
   1.543 +                 r[REVISION]], cwd=reponame)
   1.544 +
   1.545 +
   1.546 +def path(src, name='default'):
   1.547 +    """Returns the remote path associated with "name" """
   1.548 +    try:
   1.549 +        return get_output(['hg', 'path', name], cwd=src).strip()
   1.550 +    except subprocess.CalledProcessError:
   1.551 +        return None
   1.552 +
   1.553 +
   1.554 +def init(dest):
   1.555 +    """Initializes an empty repo in `dest`"""
   1.556 +    run_cmd(['hg', 'init', dest])
   1.557 +
   1.558 +
   1.559 +def unbundle(bundle, dest):
   1.560 +    """Unbundles the bundle located at `bundle` into `dest`.
   1.561 +
   1.562 +    `bundle` can be a local file or remote url."""
   1.563 +    try:
   1.564 +        get_output(['hg', 'unbundle', bundle], cwd=dest, include_stderr=True)
   1.565 +        return True
   1.566 +    except subprocess.CalledProcessError:
   1.567 +        return False
   1.568 +
   1.569 +
   1.570 +def adjust_paths(dest, **paths):
   1.571 +    """Adjusts paths in `dest`/.hg/hgrc so that names in `paths` are set to
   1.572 +    paths[name].
   1.573 +
   1.574 +    Note that any comments in the hgrc will be lost if changes are made to the
   1.575 +    file."""
   1.576 +    hgrc = os.path.join(dest, '.hg', 'hgrc')
   1.577 +    config = RawConfigParser()
   1.578 +    config.read(hgrc)
   1.579 +
   1.580 +    if not config.has_section('paths'):
   1.581 +        config.add_section('paths')
   1.582 +
   1.583 +    changed = False
   1.584 +    for path_name, path_value in paths.items():
   1.585 +        if (not config.has_option('paths', path_name) or
   1.586 +                config.get('paths', path_name) != path_value):
   1.587 +            changed = True
   1.588 +            config.set('paths', path_name, path_value)
   1.589 +
   1.590 +    if changed:
   1.591 +        config.write(open(hgrc, 'w'))
   1.592 +
   1.593 +
   1.594 +def commit(dest, msg, user=None):
   1.595 +    cmd = ['hg', 'commit', '-m', msg]
   1.596 +    if user:
   1.597 +        cmd.extend(['-u', user])
   1.598 +    run_cmd(cmd, cwd=dest)
   1.599 +    return get_revision(dest)
   1.600 +
   1.601 +
   1.602 +def tag(dest, tags, user=None, msg=None, rev=None, force=None):
   1.603 +    cmd = ['hg', 'tag']
   1.604 +    if user:
   1.605 +        cmd.extend(['-u', user])
   1.606 +    if msg:
   1.607 +        cmd.extend(['-m', msg])
   1.608 +    if rev:
   1.609 +        cmd.extend(['-r', rev])
   1.610 +    if force:
   1.611 +        cmd.append('-f')
   1.612 +    cmd.extend(tags)
   1.613 +    run_cmd(cmd, cwd=dest)
   1.614 +    return get_revision(dest)

mercurial