hg.py 22 KB

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