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)