Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
michael@0 | 1 | """Functions for interacting with hg""" |
michael@0 | 2 | import os |
michael@0 | 3 | import re |
michael@0 | 4 | import subprocess |
michael@0 | 5 | from urlparse import urlsplit |
michael@0 | 6 | from ConfigParser import RawConfigParser |
michael@0 | 7 | |
michael@0 | 8 | from util.commands import run_cmd, get_output, remove_path |
michael@0 | 9 | from util.retry import retry |
michael@0 | 10 | |
michael@0 | 11 | import logging |
michael@0 | 12 | log = logging.getLogger(__name__) |
michael@0 | 13 | |
michael@0 | 14 | |
michael@0 | 15 | class DefaultShareBase: |
michael@0 | 16 | pass |
michael@0 | 17 | DefaultShareBase = DefaultShareBase() |
michael@0 | 18 | |
michael@0 | 19 | |
michael@0 | 20 | class HgUtilError(Exception): |
michael@0 | 21 | pass |
michael@0 | 22 | |
michael@0 | 23 | |
michael@0 | 24 | def _make_absolute(repo): |
michael@0 | 25 | if repo.startswith("file://"): |
michael@0 | 26 | path = repo[len("file://"):] |
michael@0 | 27 | repo = "file://%s" % os.path.abspath(path) |
michael@0 | 28 | elif "://" not in repo: |
michael@0 | 29 | repo = os.path.abspath(repo) |
michael@0 | 30 | return repo |
michael@0 | 31 | |
michael@0 | 32 | |
michael@0 | 33 | def make_hg_url(hgHost, repoPath, protocol='https', revision=None, |
michael@0 | 34 | filename=None): |
michael@0 | 35 | """construct a valid hg url from a base hg url (hg.mozilla.org), |
michael@0 | 36 | repoPath, revision and possible filename""" |
michael@0 | 37 | base = '%s://%s' % (protocol, hgHost) |
michael@0 | 38 | repo = '/'.join(p.strip('/') for p in [base, repoPath]) |
michael@0 | 39 | if not filename: |
michael@0 | 40 | if not revision: |
michael@0 | 41 | return repo |
michael@0 | 42 | else: |
michael@0 | 43 | return '/'.join([p.strip('/') for p in [repo, 'rev', revision]]) |
michael@0 | 44 | else: |
michael@0 | 45 | assert revision |
michael@0 | 46 | return '/'.join([p.strip('/') for p in [repo, 'raw-file', revision, filename]]) |
michael@0 | 47 | |
michael@0 | 48 | |
michael@0 | 49 | def get_repo_name(repo): |
michael@0 | 50 | return repo.rstrip('/').split('/')[-1] |
michael@0 | 51 | |
michael@0 | 52 | |
michael@0 | 53 | def get_repo_path(repo): |
michael@0 | 54 | repo = _make_absolute(repo) |
michael@0 | 55 | if repo.startswith("/"): |
michael@0 | 56 | return repo.lstrip("/") |
michael@0 | 57 | else: |
michael@0 | 58 | return urlsplit(repo).path.lstrip("/") |
michael@0 | 59 | |
michael@0 | 60 | |
michael@0 | 61 | def get_revision(path): |
michael@0 | 62 | """Returns which revision directory `path` currently has checked out.""" |
michael@0 | 63 | return get_output(['hg', 'parent', '--template', '{node|short}'], cwd=path) |
michael@0 | 64 | |
michael@0 | 65 | |
michael@0 | 66 | def get_branch(path): |
michael@0 | 67 | return get_output(['hg', 'branch'], cwd=path).strip() |
michael@0 | 68 | |
michael@0 | 69 | |
michael@0 | 70 | def get_branches(path): |
michael@0 | 71 | branches = [] |
michael@0 | 72 | for line in get_output(['hg', 'branches', '-c'], cwd=path).splitlines(): |
michael@0 | 73 | branches.append(line.split()[0]) |
michael@0 | 74 | return branches |
michael@0 | 75 | |
michael@0 | 76 | |
michael@0 | 77 | def is_hg_cset(rev): |
michael@0 | 78 | """Retruns True if passed revision represents a valid HG revision |
michael@0 | 79 | (long or short(er) 40 bit hex)""" |
michael@0 | 80 | try: |
michael@0 | 81 | int(rev, 16) |
michael@0 | 82 | return True |
michael@0 | 83 | except (TypeError, ValueError): |
michael@0 | 84 | return False |
michael@0 | 85 | |
michael@0 | 86 | |
michael@0 | 87 | def hg_ver(): |
michael@0 | 88 | """Returns the current version of hg, as a tuple of |
michael@0 | 89 | (major, minor, build)""" |
michael@0 | 90 | ver_string = get_output(['hg', '-q', 'version']) |
michael@0 | 91 | match = re.search("\(version ([0-9.]+)\)", ver_string) |
michael@0 | 92 | if match: |
michael@0 | 93 | bits = match.group(1).split(".") |
michael@0 | 94 | if len(bits) < 3: |
michael@0 | 95 | bits += (0,) |
michael@0 | 96 | ver = tuple(int(b) for b in bits) |
michael@0 | 97 | else: |
michael@0 | 98 | ver = (0, 0, 0) |
michael@0 | 99 | log.debug("Running hg version %s", ver) |
michael@0 | 100 | return ver |
michael@0 | 101 | |
michael@0 | 102 | |
michael@0 | 103 | def purge(dest): |
michael@0 | 104 | """Purge the repository of all untracked and ignored files.""" |
michael@0 | 105 | try: |
michael@0 | 106 | run_cmd(['hg', '--config', 'extensions.purge=', 'purge', |
michael@0 | 107 | '-a', '--all', dest], cwd=dest) |
michael@0 | 108 | except subprocess.CalledProcessError, e: |
michael@0 | 109 | log.debug('purge failed: %s' % e) |
michael@0 | 110 | raise |
michael@0 | 111 | |
michael@0 | 112 | |
michael@0 | 113 | def update(dest, branch=None, revision=None): |
michael@0 | 114 | """Updates working copy `dest` to `branch` or `revision`. If neither is |
michael@0 | 115 | set then the working copy will be updated to the latest revision on the |
michael@0 | 116 | current branch. Local changes will be discarded.""" |
michael@0 | 117 | # If we have a revision, switch to that |
michael@0 | 118 | if revision is not None: |
michael@0 | 119 | cmd = ['hg', 'update', '-C', '-r', revision] |
michael@0 | 120 | run_cmd(cmd, cwd=dest) |
michael@0 | 121 | else: |
michael@0 | 122 | # Check & switch branch |
michael@0 | 123 | local_branch = get_output(['hg', 'branch'], cwd=dest).strip() |
michael@0 | 124 | |
michael@0 | 125 | cmd = ['hg', 'update', '-C'] |
michael@0 | 126 | |
michael@0 | 127 | # If this is different, checkout the other branch |
michael@0 | 128 | if branch and branch != local_branch: |
michael@0 | 129 | cmd.append(branch) |
michael@0 | 130 | |
michael@0 | 131 | run_cmd(cmd, cwd=dest) |
michael@0 | 132 | return get_revision(dest) |
michael@0 | 133 | |
michael@0 | 134 | |
michael@0 | 135 | def clone(repo, dest, branch=None, revision=None, update_dest=True, |
michael@0 | 136 | clone_by_rev=False, mirrors=None, bundles=None): |
michael@0 | 137 | """Clones hg repo and places it at `dest`, replacing whatever else is |
michael@0 | 138 | there. The working copy will be empty. |
michael@0 | 139 | |
michael@0 | 140 | If `revision` is set, only the specified revision and its ancestors will |
michael@0 | 141 | be cloned. |
michael@0 | 142 | |
michael@0 | 143 | If `update_dest` is set, then `dest` will be updated to `revision` if |
michael@0 | 144 | set, otherwise to `branch`, otherwise to the head of default. |
michael@0 | 145 | |
michael@0 | 146 | If `mirrors` is set, will try and clone from the mirrors before |
michael@0 | 147 | cloning from `repo`. |
michael@0 | 148 | |
michael@0 | 149 | If `bundles` is set, will try and download the bundle first and |
michael@0 | 150 | unbundle it. If successful, will pull in new revisions from mirrors or |
michael@0 | 151 | the master repo. If unbundling fails, will fall back to doing a regular |
michael@0 | 152 | clone from mirrors or the master repo. |
michael@0 | 153 | |
michael@0 | 154 | Regardless of how the repository ends up being cloned, the 'default' path |
michael@0 | 155 | will point to `repo`. |
michael@0 | 156 | """ |
michael@0 | 157 | if os.path.exists(dest): |
michael@0 | 158 | remove_path(dest) |
michael@0 | 159 | |
michael@0 | 160 | if bundles: |
michael@0 | 161 | log.info("Attempting to initialize clone with bundles") |
michael@0 | 162 | for bundle in bundles: |
michael@0 | 163 | if os.path.exists(dest): |
michael@0 | 164 | remove_path(dest) |
michael@0 | 165 | init(dest) |
michael@0 | 166 | log.info("Trying to use bundle %s", bundle) |
michael@0 | 167 | try: |
michael@0 | 168 | if not unbundle(bundle, dest): |
michael@0 | 169 | remove_path(dest) |
michael@0 | 170 | continue |
michael@0 | 171 | adjust_paths(dest, default=repo) |
michael@0 | 172 | # Now pull / update |
michael@0 | 173 | return pull(repo, dest, update_dest=update_dest, |
michael@0 | 174 | mirrors=mirrors, revision=revision, branch=branch) |
michael@0 | 175 | except Exception: |
michael@0 | 176 | remove_path(dest) |
michael@0 | 177 | log.exception("Problem unbundling/pulling from %s", bundle) |
michael@0 | 178 | continue |
michael@0 | 179 | else: |
michael@0 | 180 | log.info("Using bundles failed; falling back to clone") |
michael@0 | 181 | |
michael@0 | 182 | if mirrors: |
michael@0 | 183 | log.info("Attempting to clone from mirrors") |
michael@0 | 184 | for mirror in mirrors: |
michael@0 | 185 | log.info("Cloning from %s", mirror) |
michael@0 | 186 | try: |
michael@0 | 187 | retval = clone(mirror, dest, branch, revision, |
michael@0 | 188 | update_dest=update_dest, clone_by_rev=clone_by_rev) |
michael@0 | 189 | adjust_paths(dest, default=repo) |
michael@0 | 190 | return retval |
michael@0 | 191 | except: |
michael@0 | 192 | log.exception("Problem cloning from mirror %s", mirror) |
michael@0 | 193 | continue |
michael@0 | 194 | else: |
michael@0 | 195 | log.info("Pulling from mirrors failed; falling back to %s", repo) |
michael@0 | 196 | # We may have a partial repo here; mercurial() copes with that |
michael@0 | 197 | # We need to make sure our paths are correct though |
michael@0 | 198 | if os.path.exists(os.path.join(dest, '.hg')): |
michael@0 | 199 | adjust_paths(dest, default=repo) |
michael@0 | 200 | return mercurial(repo, dest, branch, revision, autoPurge=True, |
michael@0 | 201 | update_dest=update_dest, clone_by_rev=clone_by_rev) |
michael@0 | 202 | |
michael@0 | 203 | cmd = ['hg', 'clone'] |
michael@0 | 204 | if not update_dest: |
michael@0 | 205 | cmd.append('-U') |
michael@0 | 206 | |
michael@0 | 207 | if clone_by_rev: |
michael@0 | 208 | if revision: |
michael@0 | 209 | cmd.extend(['-r', revision]) |
michael@0 | 210 | elif branch: |
michael@0 | 211 | # hg >= 1.6 supports -b branch for cloning |
michael@0 | 212 | ver = hg_ver() |
michael@0 | 213 | if ver >= (1, 6, 0): |
michael@0 | 214 | cmd.extend(['-b', branch]) |
michael@0 | 215 | |
michael@0 | 216 | cmd.extend([repo, dest]) |
michael@0 | 217 | run_cmd(cmd) |
michael@0 | 218 | |
michael@0 | 219 | if update_dest: |
michael@0 | 220 | return update(dest, branch, revision) |
michael@0 | 221 | |
michael@0 | 222 | |
michael@0 | 223 | def common_args(revision=None, branch=None, ssh_username=None, ssh_key=None): |
michael@0 | 224 | """Fill in common hg arguments, encapsulating logic checks that depend on |
michael@0 | 225 | mercurial versions and provided arguments""" |
michael@0 | 226 | args = [] |
michael@0 | 227 | if ssh_username or ssh_key: |
michael@0 | 228 | opt = ['-e', 'ssh'] |
michael@0 | 229 | if ssh_username: |
michael@0 | 230 | opt[1] += ' -l %s' % ssh_username |
michael@0 | 231 | if ssh_key: |
michael@0 | 232 | opt[1] += ' -i %s' % ssh_key |
michael@0 | 233 | args.extend(opt) |
michael@0 | 234 | if revision: |
michael@0 | 235 | args.extend(['-r', revision]) |
michael@0 | 236 | elif branch: |
michael@0 | 237 | if hg_ver() >= (1, 6, 0): |
michael@0 | 238 | args.extend(['-b', branch]) |
michael@0 | 239 | return args |
michael@0 | 240 | |
michael@0 | 241 | |
michael@0 | 242 | def pull(repo, dest, update_dest=True, mirrors=None, **kwargs): |
michael@0 | 243 | """Pulls changes from hg repo and places it in `dest`. |
michael@0 | 244 | |
michael@0 | 245 | If `update_dest` is set, then `dest` will be updated to `revision` if |
michael@0 | 246 | set, otherwise to `branch`, otherwise to the head of default. |
michael@0 | 247 | |
michael@0 | 248 | If `mirrors` is set, will try and pull from the mirrors first before |
michael@0 | 249 | `repo`.""" |
michael@0 | 250 | |
michael@0 | 251 | if mirrors: |
michael@0 | 252 | for mirror in mirrors: |
michael@0 | 253 | try: |
michael@0 | 254 | return pull(mirror, dest, update_dest=update_dest, **kwargs) |
michael@0 | 255 | except: |
michael@0 | 256 | log.exception("Problem pulling from mirror %s", mirror) |
michael@0 | 257 | continue |
michael@0 | 258 | else: |
michael@0 | 259 | log.info("Pulling from mirrors failed; falling back to %s", repo) |
michael@0 | 260 | |
michael@0 | 261 | # Convert repo to an absolute path if it's a local repository |
michael@0 | 262 | repo = _make_absolute(repo) |
michael@0 | 263 | cmd = ['hg', 'pull'] |
michael@0 | 264 | # Don't pass -r to "hg pull", except when it's a valid HG revision. |
michael@0 | 265 | # Pulling using tag names is dangerous: it uses the local .hgtags, so if |
michael@0 | 266 | # the tag has moved on the remote side you won't pull the new revision the |
michael@0 | 267 | # remote tag refers to. |
michael@0 | 268 | pull_kwargs = kwargs.copy() |
michael@0 | 269 | if 'revision' in pull_kwargs and \ |
michael@0 | 270 | not is_hg_cset(pull_kwargs['revision']): |
michael@0 | 271 | del pull_kwargs['revision'] |
michael@0 | 272 | |
michael@0 | 273 | cmd.extend(common_args(**pull_kwargs)) |
michael@0 | 274 | |
michael@0 | 275 | cmd.append(repo) |
michael@0 | 276 | run_cmd(cmd, cwd=dest) |
michael@0 | 277 | |
michael@0 | 278 | if update_dest: |
michael@0 | 279 | branch = None |
michael@0 | 280 | if 'branch' in kwargs and kwargs['branch']: |
michael@0 | 281 | branch = kwargs['branch'] |
michael@0 | 282 | revision = None |
michael@0 | 283 | if 'revision' in kwargs and kwargs['revision']: |
michael@0 | 284 | revision = kwargs['revision'] |
michael@0 | 285 | return update(dest, branch=branch, revision=revision) |
michael@0 | 286 | |
michael@0 | 287 | # Defines the places of attributes in the tuples returned by `out' |
michael@0 | 288 | REVISION, BRANCH = 0, 1 |
michael@0 | 289 | |
michael@0 | 290 | |
michael@0 | 291 | def out(src, remote, **kwargs): |
michael@0 | 292 | """Check for outgoing changesets present in a repo""" |
michael@0 | 293 | cmd = ['hg', '-q', 'out', '--template', '{node} {branches}\n'] |
michael@0 | 294 | cmd.extend(common_args(**kwargs)) |
michael@0 | 295 | cmd.append(remote) |
michael@0 | 296 | if os.path.exists(src): |
michael@0 | 297 | try: |
michael@0 | 298 | revs = [] |
michael@0 | 299 | for line in get_output(cmd, cwd=src).rstrip().split("\n"): |
michael@0 | 300 | try: |
michael@0 | 301 | rev, branch = line.split() |
michael@0 | 302 | # Mercurial displays no branch at all if the revision is on |
michael@0 | 303 | # "default" |
michael@0 | 304 | except ValueError: |
michael@0 | 305 | rev = line.rstrip() |
michael@0 | 306 | branch = "default" |
michael@0 | 307 | revs.append((rev, branch)) |
michael@0 | 308 | return revs |
michael@0 | 309 | except subprocess.CalledProcessError, inst: |
michael@0 | 310 | # In some situations, some versions of Mercurial return "1" |
michael@0 | 311 | # if no changes are found, so we need to ignore this return code |
michael@0 | 312 | if inst.returncode == 1: |
michael@0 | 313 | return [] |
michael@0 | 314 | raise |
michael@0 | 315 | |
michael@0 | 316 | |
michael@0 | 317 | def push(src, remote, push_new_branches=True, force=False, **kwargs): |
michael@0 | 318 | cmd = ['hg', 'push'] |
michael@0 | 319 | cmd.extend(common_args(**kwargs)) |
michael@0 | 320 | if force: |
michael@0 | 321 | cmd.append('-f') |
michael@0 | 322 | if push_new_branches: |
michael@0 | 323 | cmd.append('--new-branch') |
michael@0 | 324 | cmd.append(remote) |
michael@0 | 325 | run_cmd(cmd, cwd=src) |
michael@0 | 326 | |
michael@0 | 327 | |
michael@0 | 328 | def mercurial(repo, dest, branch=None, revision=None, update_dest=True, |
michael@0 | 329 | shareBase=DefaultShareBase, allowUnsharedLocalClones=False, |
michael@0 | 330 | clone_by_rev=False, mirrors=None, bundles=None, autoPurge=False): |
michael@0 | 331 | """Makes sure that `dest` is has `revision` or `branch` checked out from |
michael@0 | 332 | `repo`. |
michael@0 | 333 | |
michael@0 | 334 | Do what it takes to make that happen, including possibly clobbering |
michael@0 | 335 | dest. |
michael@0 | 336 | |
michael@0 | 337 | If allowUnsharedLocalClones is True and we're trying to use the share |
michael@0 | 338 | extension but fail, then we will be able to clone from the shared repo to |
michael@0 | 339 | our destination. If this is False, the default, then if we don't have the |
michael@0 | 340 | share extension we will just clone from the remote repository. |
michael@0 | 341 | |
michael@0 | 342 | If `clone_by_rev` is True, use 'hg clone -r <rev>' instead of 'hg clone'. |
michael@0 | 343 | This is slower, but useful when cloning repos with lots of heads. |
michael@0 | 344 | |
michael@0 | 345 | If `mirrors` is set, will try and use the mirrors before `repo`. |
michael@0 | 346 | |
michael@0 | 347 | If `bundles` is set, will try and download the bundle first and |
michael@0 | 348 | unbundle it instead of doing a full clone. If successful, will pull in |
michael@0 | 349 | new revisions from mirrors or the master repo. If unbundling fails, will |
michael@0 | 350 | fall back to doing a regular clone from mirrors or the master repo. |
michael@0 | 351 | """ |
michael@0 | 352 | dest = os.path.abspath(dest) |
michael@0 | 353 | if shareBase is DefaultShareBase: |
michael@0 | 354 | shareBase = os.environ.get("HG_SHARE_BASE_DIR", None) |
michael@0 | 355 | |
michael@0 | 356 | log.info("Reporting hg version in use") |
michael@0 | 357 | cmd = ['hg', '-q', 'version'] |
michael@0 | 358 | run_cmd(cmd, cwd='.') |
michael@0 | 359 | |
michael@0 | 360 | if shareBase: |
michael@0 | 361 | # Check that 'hg share' works |
michael@0 | 362 | try: |
michael@0 | 363 | log.info("Checking if share extension works") |
michael@0 | 364 | output = get_output(['hg', 'help', 'share'], dont_log=True) |
michael@0 | 365 | if 'no commands defined' in output: |
michael@0 | 366 | # Share extension is enabled, but not functional |
michael@0 | 367 | log.info("Disabling sharing since share extension doesn't seem to work (1)") |
michael@0 | 368 | shareBase = None |
michael@0 | 369 | elif 'unknown command' in output: |
michael@0 | 370 | # Share extension is disabled |
michael@0 | 371 | log.info("Disabling sharing since share extension doesn't seem to work (2)") |
michael@0 | 372 | shareBase = None |
michael@0 | 373 | except subprocess.CalledProcessError: |
michael@0 | 374 | # The command failed, so disable sharing |
michael@0 | 375 | log.info("Disabling sharing since share extension doesn't seem to work (3)") |
michael@0 | 376 | shareBase = None |
michael@0 | 377 | |
michael@0 | 378 | # Check that our default path is correct |
michael@0 | 379 | if os.path.exists(os.path.join(dest, '.hg')): |
michael@0 | 380 | hgpath = path(dest, "default") |
michael@0 | 381 | |
michael@0 | 382 | # Make sure that our default path is correct |
michael@0 | 383 | if hgpath != _make_absolute(repo): |
michael@0 | 384 | log.info("hg path isn't correct (%s should be %s); clobbering", |
michael@0 | 385 | hgpath, _make_absolute(repo)) |
michael@0 | 386 | remove_path(dest) |
michael@0 | 387 | |
michael@0 | 388 | # If the working directory already exists and isn't using share we update |
michael@0 | 389 | # the working directory directly from the repo, ignoring the sharing |
michael@0 | 390 | # settings |
michael@0 | 391 | if os.path.exists(dest): |
michael@0 | 392 | if not os.path.exists(os.path.join(dest, ".hg")): |
michael@0 | 393 | log.warning("%s doesn't appear to be a valid hg directory; clobbering", dest) |
michael@0 | 394 | remove_path(dest) |
michael@0 | 395 | elif not os.path.exists(os.path.join(dest, ".hg", "sharedpath")): |
michael@0 | 396 | try: |
michael@0 | 397 | if autoPurge: |
michael@0 | 398 | purge(dest) |
michael@0 | 399 | return pull(repo, dest, update_dest=update_dest, branch=branch, |
michael@0 | 400 | revision=revision, |
michael@0 | 401 | mirrors=mirrors) |
michael@0 | 402 | except subprocess.CalledProcessError: |
michael@0 | 403 | log.warning("Error pulling changes into %s from %s; clobbering", dest, repo) |
michael@0 | 404 | log.debug("Exception:", exc_info=True) |
michael@0 | 405 | remove_path(dest) |
michael@0 | 406 | |
michael@0 | 407 | # If that fails for any reason, and sharing is requested, we'll try to |
michael@0 | 408 | # update the shared repository, and then update the working directory from |
michael@0 | 409 | # that. |
michael@0 | 410 | if shareBase: |
michael@0 | 411 | sharedRepo = os.path.join(shareBase, get_repo_path(repo)) |
michael@0 | 412 | dest_sharedPath = os.path.join(dest, '.hg', 'sharedpath') |
michael@0 | 413 | |
michael@0 | 414 | if os.path.exists(sharedRepo): |
michael@0 | 415 | hgpath = path(sharedRepo, "default") |
michael@0 | 416 | |
michael@0 | 417 | # Make sure that our default path is correct |
michael@0 | 418 | if hgpath != _make_absolute(repo): |
michael@0 | 419 | log.info("hg path isn't correct (%s should be %s); clobbering", |
michael@0 | 420 | hgpath, _make_absolute(repo)) |
michael@0 | 421 | # we need to clobber both the shared checkout and the dest, |
michael@0 | 422 | # since hgrc needs to be in both places |
michael@0 | 423 | remove_path(sharedRepo) |
michael@0 | 424 | remove_path(dest) |
michael@0 | 425 | |
michael@0 | 426 | if os.path.exists(dest_sharedPath): |
michael@0 | 427 | # Make sure that the sharedpath points to sharedRepo |
michael@0 | 428 | dest_sharedPath_data = os.path.normpath( |
michael@0 | 429 | open(dest_sharedPath).read()) |
michael@0 | 430 | norm_sharedRepo = os.path.normpath(os.path.join(sharedRepo, '.hg')) |
michael@0 | 431 | if dest_sharedPath_data != norm_sharedRepo: |
michael@0 | 432 | # Clobber! |
michael@0 | 433 | log.info("We're currently shared from %s, but are being requested to pull from %s (%s); clobbering", |
michael@0 | 434 | dest_sharedPath_data, repo, norm_sharedRepo) |
michael@0 | 435 | remove_path(dest) |
michael@0 | 436 | |
michael@0 | 437 | try: |
michael@0 | 438 | log.info("Updating shared repo") |
michael@0 | 439 | mercurial(repo, sharedRepo, branch=branch, revision=revision, |
michael@0 | 440 | update_dest=False, shareBase=None, clone_by_rev=clone_by_rev, |
michael@0 | 441 | mirrors=mirrors, bundles=bundles, autoPurge=False) |
michael@0 | 442 | if os.path.exists(dest): |
michael@0 | 443 | if autoPurge: |
michael@0 | 444 | purge(dest) |
michael@0 | 445 | return update(dest, branch=branch, revision=revision) |
michael@0 | 446 | |
michael@0 | 447 | try: |
michael@0 | 448 | log.info("Trying to share %s to %s", sharedRepo, dest) |
michael@0 | 449 | return share(sharedRepo, dest, branch=branch, revision=revision) |
michael@0 | 450 | except subprocess.CalledProcessError: |
michael@0 | 451 | if not allowUnsharedLocalClones: |
michael@0 | 452 | # Re-raise the exception so it gets caught below. |
michael@0 | 453 | # We'll then clobber dest, and clone from original repo |
michael@0 | 454 | raise |
michael@0 | 455 | |
michael@0 | 456 | log.warning("Error calling hg share from %s to %s;" |
michael@0 | 457 | "falling back to normal clone from shared repo", |
michael@0 | 458 | sharedRepo, dest) |
michael@0 | 459 | # Do a full local clone first, and then update to the |
michael@0 | 460 | # revision we want |
michael@0 | 461 | # This lets us use hardlinks for the local clone if the OS |
michael@0 | 462 | # supports it |
michael@0 | 463 | clone(sharedRepo, dest, update_dest=False, |
michael@0 | 464 | mirrors=mirrors, bundles=bundles) |
michael@0 | 465 | return update(dest, branch=branch, revision=revision) |
michael@0 | 466 | except subprocess.CalledProcessError: |
michael@0 | 467 | log.warning( |
michael@0 | 468 | "Error updating %s from sharedRepo (%s): ", dest, sharedRepo) |
michael@0 | 469 | log.debug("Exception:", exc_info=True) |
michael@0 | 470 | remove_path(dest) |
michael@0 | 471 | # end if shareBase |
michael@0 | 472 | |
michael@0 | 473 | if not os.path.exists(os.path.dirname(dest)): |
michael@0 | 474 | os.makedirs(os.path.dirname(dest)) |
michael@0 | 475 | |
michael@0 | 476 | # Share isn't available or has failed, clone directly from the source |
michael@0 | 477 | return clone(repo, dest, branch, revision, |
michael@0 | 478 | update_dest=update_dest, mirrors=mirrors, |
michael@0 | 479 | bundles=bundles, clone_by_rev=clone_by_rev) |
michael@0 | 480 | |
michael@0 | 481 | |
michael@0 | 482 | def apply_and_push(localrepo, remote, changer, max_attempts=10, |
michael@0 | 483 | ssh_username=None, ssh_key=None, force=False): |
michael@0 | 484 | """This function calls `changer' to make changes to the repo, and tries |
michael@0 | 485 | its hardest to get them to the origin repo. `changer' must be a |
michael@0 | 486 | callable object that receives two arguments: the directory of the local |
michael@0 | 487 | repository, and the attempt number. This function will push ALL |
michael@0 | 488 | changesets missing from remote.""" |
michael@0 | 489 | assert callable(changer) |
michael@0 | 490 | branch = get_branch(localrepo) |
michael@0 | 491 | changer(localrepo, 1) |
michael@0 | 492 | for n in range(1, max_attempts + 1): |
michael@0 | 493 | new_revs = [] |
michael@0 | 494 | try: |
michael@0 | 495 | new_revs = out(src=localrepo, remote=remote, |
michael@0 | 496 | ssh_username=ssh_username, |
michael@0 | 497 | ssh_key=ssh_key) |
michael@0 | 498 | if len(new_revs) < 1: |
michael@0 | 499 | raise HgUtilError("No revs to push") |
michael@0 | 500 | push(src=localrepo, remote=remote, ssh_username=ssh_username, |
michael@0 | 501 | ssh_key=ssh_key, force=force) |
michael@0 | 502 | return |
michael@0 | 503 | except subprocess.CalledProcessError, e: |
michael@0 | 504 | log.debug("Hit error when trying to push: %s" % str(e)) |
michael@0 | 505 | if n == max_attempts: |
michael@0 | 506 | log.debug("Tried %d times, giving up" % max_attempts) |
michael@0 | 507 | for r in reversed(new_revs): |
michael@0 | 508 | run_cmd(['hg', '--config', 'extensions.mq=', 'strip', '-n', |
michael@0 | 509 | r[REVISION]], cwd=localrepo) |
michael@0 | 510 | raise HgUtilError("Failed to push") |
michael@0 | 511 | pull(remote, localrepo, update_dest=False, |
michael@0 | 512 | ssh_username=ssh_username, ssh_key=ssh_key) |
michael@0 | 513 | # After we successfully rebase or strip away heads the push is |
michael@0 | 514 | # is attempted again at the start of the loop |
michael@0 | 515 | try: |
michael@0 | 516 | run_cmd(['hg', '--config', 'ui.merge=internal:merge', |
michael@0 | 517 | 'rebase'], cwd=localrepo) |
michael@0 | 518 | except subprocess.CalledProcessError, e: |
michael@0 | 519 | log.debug("Failed to rebase: %s" % str(e)) |
michael@0 | 520 | update(localrepo, branch=branch) |
michael@0 | 521 | for r in reversed(new_revs): |
michael@0 | 522 | run_cmd(['hg', '--config', 'extensions.mq=', 'strip', '-n', |
michael@0 | 523 | r[REVISION]], cwd=localrepo) |
michael@0 | 524 | changer(localrepo, n + 1) |
michael@0 | 525 | |
michael@0 | 526 | |
michael@0 | 527 | def share(source, dest, branch=None, revision=None): |
michael@0 | 528 | """Creates a new working directory in "dest" that shares history with |
michael@0 | 529 | "source" using Mercurial's share extension""" |
michael@0 | 530 | run_cmd(['hg', 'share', '-U', source, dest]) |
michael@0 | 531 | return update(dest, branch=branch, revision=revision) |
michael@0 | 532 | |
michael@0 | 533 | |
michael@0 | 534 | def cleanOutgoingRevs(reponame, remote, username, sshKey): |
michael@0 | 535 | outgoingRevs = retry(out, kwargs=dict(src=reponame, remote=remote, |
michael@0 | 536 | ssh_username=username, |
michael@0 | 537 | ssh_key=sshKey)) |
michael@0 | 538 | for r in reversed(outgoingRevs): |
michael@0 | 539 | run_cmd(['hg', '--config', 'extensions.mq=', 'strip', '-n', |
michael@0 | 540 | r[REVISION]], cwd=reponame) |
michael@0 | 541 | |
michael@0 | 542 | |
michael@0 | 543 | def path(src, name='default'): |
michael@0 | 544 | """Returns the remote path associated with "name" """ |
michael@0 | 545 | try: |
michael@0 | 546 | return get_output(['hg', 'path', name], cwd=src).strip() |
michael@0 | 547 | except subprocess.CalledProcessError: |
michael@0 | 548 | return None |
michael@0 | 549 | |
michael@0 | 550 | |
michael@0 | 551 | def init(dest): |
michael@0 | 552 | """Initializes an empty repo in `dest`""" |
michael@0 | 553 | run_cmd(['hg', 'init', dest]) |
michael@0 | 554 | |
michael@0 | 555 | |
michael@0 | 556 | def unbundle(bundle, dest): |
michael@0 | 557 | """Unbundles the bundle located at `bundle` into `dest`. |
michael@0 | 558 | |
michael@0 | 559 | `bundle` can be a local file or remote url.""" |
michael@0 | 560 | try: |
michael@0 | 561 | get_output(['hg', 'unbundle', bundle], cwd=dest, include_stderr=True) |
michael@0 | 562 | return True |
michael@0 | 563 | except subprocess.CalledProcessError: |
michael@0 | 564 | return False |
michael@0 | 565 | |
michael@0 | 566 | |
michael@0 | 567 | def adjust_paths(dest, **paths): |
michael@0 | 568 | """Adjusts paths in `dest`/.hg/hgrc so that names in `paths` are set to |
michael@0 | 569 | paths[name]. |
michael@0 | 570 | |
michael@0 | 571 | Note that any comments in the hgrc will be lost if changes are made to the |
michael@0 | 572 | file.""" |
michael@0 | 573 | hgrc = os.path.join(dest, '.hg', 'hgrc') |
michael@0 | 574 | config = RawConfigParser() |
michael@0 | 575 | config.read(hgrc) |
michael@0 | 576 | |
michael@0 | 577 | if not config.has_section('paths'): |
michael@0 | 578 | config.add_section('paths') |
michael@0 | 579 | |
michael@0 | 580 | changed = False |
michael@0 | 581 | for path_name, path_value in paths.items(): |
michael@0 | 582 | if (not config.has_option('paths', path_name) or |
michael@0 | 583 | config.get('paths', path_name) != path_value): |
michael@0 | 584 | changed = True |
michael@0 | 585 | config.set('paths', path_name, path_value) |
michael@0 | 586 | |
michael@0 | 587 | if changed: |
michael@0 | 588 | config.write(open(hgrc, 'w')) |
michael@0 | 589 | |
michael@0 | 590 | |
michael@0 | 591 | def commit(dest, msg, user=None): |
michael@0 | 592 | cmd = ['hg', 'commit', '-m', msg] |
michael@0 | 593 | if user: |
michael@0 | 594 | cmd.extend(['-u', user]) |
michael@0 | 595 | run_cmd(cmd, cwd=dest) |
michael@0 | 596 | return get_revision(dest) |
michael@0 | 597 | |
michael@0 | 598 | |
michael@0 | 599 | def tag(dest, tags, user=None, msg=None, rev=None, force=None): |
michael@0 | 600 | cmd = ['hg', 'tag'] |
michael@0 | 601 | if user: |
michael@0 | 602 | cmd.extend(['-u', user]) |
michael@0 | 603 | if msg: |
michael@0 | 604 | cmd.extend(['-m', msg]) |
michael@0 | 605 | if rev: |
michael@0 | 606 | cmd.extend(['-r', rev]) |
michael@0 | 607 | if force: |
michael@0 | 608 | cmd.append('-f') |
michael@0 | 609 | cmd.extend(tags) |
michael@0 | 610 | run_cmd(cmd, cwd=dest) |
michael@0 | 611 | return get_revision(dest) |