PageRenderTime 63ms CodeModel.GetById 29ms RepoModel.GetById 0ms app.codeStats 0ms

/hgapi/hgapi.py

https://bitbucket.org/magnacarta/hgapi
Python | 921 lines | 901 code | 19 blank | 1 comment | 11 complexity | 9c26237c8223d55e7f94f7400d4d7411 MD5 | raw file
  1. # -*- coding: utf-8 -*-
  2. from __future__ import print_function, unicode_literals, with_statement
  3. import sys
  4. from subprocess import Popen, STDOUT, PIPE
  5. from datetime import datetime
  6. try:
  7. from ConfigParser import ConfigParser, NoOptionError
  8. except: #python 3
  9. from configparser import ConfigParser, NoOptionError
  10. import re
  11. import os
  12. import shutil
  13. try:
  14. from urllib import unquote
  15. except: #python 3
  16. from urllib.parse import unquote
  17. try:
  18. import json #for reading logs
  19. except:
  20. import simplejson as json
  21. from revision import Revision
  22. from status import Status, ResolveState
  23. PLATFORM_WINDOWS = 'windows'
  24. PLATFORM_LINUX = 'linux'
  25. PLATFORM_MAC = 'mac'
  26. def _get_platform():
  27. os_name = sys.registry['os.name']
  28. if os_name.startswith( 'Windows' ):
  29. return PLATFORM_WINDOWS
  30. elif os_name.startswith( 'Linux' ):
  31. return PLATFORM_LINUX
  32. elif os_name.startswith( 'Mac' ):
  33. return PLATFORM_MAC
  34. else:
  35. raise ValueError, 'Unrecognized os.name \'{0}\''.format(os_name)
  36. def __platform_ssh_cmd(username, ssh_key_path, disable_host_key_checking):
  37. platform = _get_platform()
  38. if platform == PLATFORM_WINDOWS:
  39. return 'TortoisePLink.exe -ssh -l {0} -i "{1}"'.format(username, ssh_key_path)
  40. elif platform == PLATFORM_LINUX:
  41. insecure_option = ' -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no' if disable_host_key_checking else ''
  42. return 'ssh -l {0} -i "{1}"{2}'.format(username, ssh_key_path, insecure_option)
  43. elif platform == PLATFORM_MAC:
  44. insecure_option = ' -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no' if disable_host_key_checking else ''
  45. return 'ssh -l {0} -i "{1}"{2}'.format(username, ssh_key_path, insecure_option)
  46. else:
  47. raise ValueError, 'Unreckognized platform \'{0}\''.format(platform)
  48. __hg_path = 'hg'
  49. def get_hg_path():
  50. return __hg_path
  51. def set_hg_path(p):
  52. global __hg_path
  53. __hg_path = p
  54. def _hg_env():
  55. env = dict(os.environ)
  56. env[str('LANG')] = str('en_US.UTF-8')
  57. return env
  58. def _hg_config_options(username, ssh_key_path, disable_host_key_checking):
  59. if username is not None and ssh_key_path is not None:
  60. cmd = __platform_ssh_cmd(username, ssh_key_path, disable_host_key_checking)
  61. return ['--config', 'ui.ssh={0}'.format(cmd)]
  62. else:
  63. return []
  64. MERGETOOL_INTERNAL_DUMP = 'internal:dump'
  65. MERGETOOL_INTERNAL_FAIL = 'internal:fail'
  66. MERGETOOL_INTERNAL_LOCAL = 'internal:local'
  67. MERGETOOL_INTERNAL_MERGE = 'internal:merge'
  68. MERGETOOL_INTERNAL_OTHER = 'internal:other'
  69. MERGETOOL_INTERNAL_PROMPT = 'internal:prompt'
  70. class HGBaseError (Exception):
  71. pass
  72. class HGError (HGBaseError):
  73. pass
  74. class HGCannotLaunchError (HGBaseError):
  75. pass
  76. class HGExtensionDisabledError (HGBaseError):
  77. pass
  78. class HGPushNothingToPushError (HGBaseError):
  79. pass
  80. class HGRemoveWarning (HGBaseError):
  81. pass
  82. class HGMoveError (HGBaseError):
  83. pass
  84. class HGCopyError (HGBaseError):
  85. pass
  86. class HGUnresolvedFiles (HGBaseError):
  87. pass
  88. class HGHeadsNoHeads (HGBaseError):
  89. pass
  90. class HGResolveFailed (HGBaseError):
  91. pass
  92. class HGCommitNoChanges (HGBaseError):
  93. pass
  94. class HGRebaseNothingToRebase (HGBaseError):
  95. pass
  96. class HGCloneRepoNotFound (HGBaseError):
  97. pass
  98. class HGRepoUnrelated (HGBaseError):
  99. pass
  100. class _ReturnCodeHandler (object):
  101. def __init__(self):
  102. self.__exc_type_map = {}
  103. def map_returncode_to_exception(self, returncode, exc_type):
  104. x = _ReturnCodeHandler()
  105. x.__exc_type_map.update(self.__exc_type_map)
  106. x.__exc_type_map[returncode] = exc_type
  107. return x
  108. def _handle_return_code(self, cmd, err, out, returncode):
  109. exc_type = self.__exc_type_map.get(returncode, HGError)
  110. raise exc_type("Error running %s:\n\tErr: %s\n\tOut: %s\n\tExit: %s"
  111. % (' '.join(cmd),err,out,returncode))
  112. _default_return_code_handler = _ReturnCodeHandler()
  113. def _hg_cmd(return_code_handler, username, ssh_key_path, disable_host_key_checking, *args):
  114. """Run a hg command in path and return the result.
  115. Throws on error."""
  116. cmd = [get_hg_path(), "--encoding", "UTF-8"] + _hg_config_options(username, ssh_key_path, disable_host_key_checking) + list(args)
  117. proc = Popen(cmd, stdout=PIPE, stderr=PIPE, env=_hg_env())
  118. out, err = [x.decode("utf-8") for x in proc.communicate()]
  119. if proc.returncode:
  120. return_code_handler._handle_return_code(cmd, err, out, proc.returncode)
  121. return out
  122. class Repo(object):
  123. """
  124. A representation of a Mercurial repository
  125. """
  126. __user_cfg_mod_date = None
  127. def __init__(self, path, user=None, ssh_key_path=None, disable_host_key_checking=False, on_filesystem_modified=None):
  128. """Create a Repo object from the repository at path"""
  129. # Call hg_version() to check that it is installed and that it works
  130. hg_version()
  131. self.path = path
  132. self.__cfg_date = None
  133. self.__cfg = None
  134. self.user = user
  135. self.ssh_key_path = ssh_key_path
  136. self.disable_host_key_checking = disable_host_key_checking
  137. self.__on_filesystem_modified = on_filesystem_modified
  138. # Call hg_status to check that the repo is valid
  139. self.hg_status()
  140. self.__extensions = set()
  141. self.__refresh_extensions()
  142. self.__revisions_by_index = []
  143. def __getitem__(self, rev=slice(0, 'tip')):
  144. """Get a Revision object for the revision identifed by rev
  145. rev can be a range (6c31a9f7be7ac58686f0610dd3c4ba375db2472c:tip)
  146. a single changeset id
  147. or it can be left blank to indicate the entire history
  148. """
  149. if isinstance(rev, slice):
  150. return self.revisions(":".join([str(x)for x in (rev.start, rev.stop)]))
  151. return self.revision(rev)
  152. def __hg_command(self, return_code_handler, args, stdout_listener=None):
  153. """Run a hg command in path and return the result.
  154. Throws on error."""
  155. assert return_code_handler is None or isinstance(return_code_handler, _ReturnCodeHandler)
  156. cmd = [get_hg_path(), "--cwd", self.path, "--encoding", "UTF-8"] + list(args)
  157. if stdout_listener is None:
  158. proc = Popen(cmd, stdout=PIPE, stderr=PIPE, env=_hg_env())
  159. out, err = [x.decode("utf-8") for x in proc.communicate()]
  160. else:
  161. proc = Popen(cmd, stdout=PIPE, stderr=PIPE, env=_hg_env(), bufsize=-1)
  162. out_lines = []
  163. while proc.poll() is None:
  164. x = proc.stdout.readline().decode('utf-8')
  165. out_lines.append(x)
  166. stdout_listener(x)
  167. out = ''.join(out_lines)
  168. err = proc.stderr.read().decode('utf-8')
  169. if proc.returncode:
  170. if return_code_handler is not None:
  171. return_code_handler._handle_return_code(cmd, err, out, proc.returncode)
  172. else:
  173. _default_return_code_handler._handle_return_code(cmd, err, out, proc.returncode)
  174. raise HGError("Error running %s:\n\tErr: %s\n\tOut: %s\n\tExit: %s"
  175. % (' '.join(cmd),err,out,proc.returncode))
  176. return out
  177. def hg_command(self, return_code_handler, *args):
  178. """Run a hg command in path and return the result.
  179. Throws on error."""
  180. return self.__hg_command(return_code_handler, args)
  181. def hg_remote_command(self, return_code_handler, *args):
  182. """Run a hg command in path and return the result.
  183. Throws on error.
  184. Adds SSH key path"""
  185. return self.hg_command(return_code_handler, *(_hg_config_options(self.user, self.ssh_key_path, self.disable_host_key_checking) + list(args)))
  186. def hg_remote_command_with_stdout_listener(self, return_code_handler, stdout_listener, *args):
  187. """Run a hg command in path and return the result.
  188. Throws on error.
  189. Adds SSH key path"""
  190. return self.__hg_command(return_code_handler, _hg_config_options(self.user, self.ssh_key_path, self.disable_host_key_checking) + list(args), stdout_listener)
  191. def read_repo_config(self):
  192. """Read the repo configuration and return a ConfigParser object"""
  193. config = ConfigParser()
  194. config_path = os.path.join(self.path, '.hg', 'hgrc')
  195. if os.path.exists(config_path):
  196. config.read(config_path)
  197. return config
  198. def write_repo_config(self, config):
  199. """Write the repo configuration in the form of a ConfigParser object"""
  200. with open(os.path.join(self.path, '.hg', 'hgrc'), 'w') as f:
  201. config.write(f)
  202. self.__cfg = None
  203. def is_extension_enabled(self, extension_name):
  204. """Determine if a named HG extension is enabled"""
  205. self.__refresh_extensions()
  206. return extension_name in self.__extensions
  207. def enable_extension(self, extension_name):
  208. """Enable a named HG extension"""
  209. config = self.read_repo_config()
  210. if not config.has_section('extensions'):
  211. config.add_section('extensions')
  212. if not config.has_option('extensions', extension_name):
  213. config.set('extensions', extension_name, '')
  214. self.write_repo_config(config)
  215. @staticmethod
  216. def read_user_config():
  217. """Read the user HG configuration, returns a ConfigParser object"""
  218. config = ConfigParser()
  219. config_path = os.path.expanduser(os.path.join('~', '.hgrc'))
  220. if os.path.exists(config_path):
  221. config.read(config_path)
  222. return config
  223. @staticmethod
  224. def write_user_config(config):
  225. """Write the user HG configuration, in the form of a ConfigParser"""
  226. with open(os.path.expanduser(os.path.join('~', '.hgrc')), 'w') as f:
  227. config.write(f)
  228. Repo.__user_cfg_mod_date = datetime.now()
  229. def __refresh_extensions(self):
  230. cfg = self.__refresh_config()
  231. self.__extensions = set(cfg.get('extensions', []))
  232. def hg_id(self):
  233. """Get the output of the hg id command"""
  234. res = self.hg_command(None, "id", "-i")
  235. return res.strip("\n +")
  236. def hg_rev(self):
  237. """Get the revision number of the current revision"""
  238. res = self.hg_command(None, "id", "-n")
  239. str_rev = res.strip("\n +")
  240. return int(str_rev)
  241. def hg_node(self, rev_id=None):
  242. """Get the full node id of a revision
  243. rev_id - a string identifying the revision. If None, will use the current working directory
  244. """
  245. if rev_id is None:
  246. rev_id = self.hg_id()
  247. res = self.hg_command(None, "log", "-r", rev_id, "--template", "{node}")
  248. return res.strip()
  249. def hg_log(self, rev_identifier=None, limit=None, template=None, filename=None, **kwargs):
  250. """Get repositiory log."""
  251. cmds = ["log"]
  252. if rev_identifier: cmds += ['-r', str(rev_identifier)]
  253. if limit: cmds += ['-l', str(limit)]
  254. if template: cmds += ['--template', str(template)]
  255. if kwargs:
  256. for key in kwargs:
  257. cmds += [key, kwargs[key]]
  258. if filename:
  259. cmds.append(filename)
  260. return self.hg_command(None, *cmds)
  261. def hg_status(self, filenames=None):
  262. """Get repository status.
  263. Returns a Status object. A status object has five attributes, each of which contains a set of filenames.
  264. added,
  265. modified,
  266. removed,
  267. untracked,
  268. missing
  269. Example - added one.txt, modified a_folder/two.txt and three.txt::
  270. Status(added={'one.txt'}, modified={'a_folder/two.txt', 'three.txt'})
  271. """
  272. if filenames is None:
  273. filenames = []
  274. cmds = ['status'] + filenames
  275. out = self.hg_command(None, *cmds).strip()
  276. #default empty set
  277. status = Status()
  278. if not out: return status
  279. lines = out.split("\n")
  280. status_split = re.compile("^(.) (.*)$")
  281. for change, path in [status_split.match(x).groups() for x in lines]:
  282. getattr(status, self._status_codes[change]).add(path)
  283. return status
  284. _status_codes = {'A': 'added', 'M': 'modified', 'R': 'removed', '!': 'missing', '?': 'untracked'}
  285. rev_log_tpl = '\{"node":"{node}","rev":"{rev}","author":"{author|urlescape}","branch":"{branches}","parents":"{parents}","date":"{date|isodate}","tags":"{tags}","desc":"{desc|urlescape}\"}\n'
  286. @staticmethod
  287. def __revision_from_json(json_rev):
  288. """Create a Revision object from a JSON representation"""
  289. j = json.loads(json_rev)
  290. j = {key : unquote(value) for key, value in j.items()}
  291. rev = int(j['rev'])
  292. branch = j['branch']
  293. branch = branch if branch else 'default'
  294. jparents = j['parents']
  295. if not jparents:
  296. parents = [rev-1]
  297. else:
  298. parents = [int(p.split(':')[0]) for p in jparents.split()]
  299. return Revision(j['node'], rev, j['author'], branch, parents, j['date'], j['tags'], j['desc'])
  300. @staticmethod
  301. def __revision_from_log(log):
  302. log = log.strip()
  303. if len(log) > 0:
  304. return Repo.__revision_from_json(log)
  305. else:
  306. return None
  307. @staticmethod
  308. def __revisions_from_log(log):
  309. lines = log.split('\n')[:-1]
  310. lines = [line.strip() for line in lines]
  311. return [Repo.__revision_from_json(line) for line in lines if len(line) > 0]
  312. def revision(self, rev_identifier):
  313. """Get the identified revision as a Revision object"""
  314. out = self.hg_log(rev_identifier=str(rev_identifier), template=self.rev_log_tpl)
  315. return Repo.__revision_from_log(out)
  316. def revisions(self, rev_identifier):
  317. """Returns a list of Revision objects for the given identifier"""
  318. out = self.hg_log(rev_identifier=str(rev_identifier), template=self.rev_log_tpl)
  319. return Repo.__revisions_from_log(out)
  320. def revisions_for(self, filename, rev_identifier=None):
  321. """Returns a list of Revision objects for the given identifier"""
  322. out = self.hg_log(rev_identifier=str(rev_identifier) if rev_identifier is not None else None, template=self.rev_log_tpl, filename=filename)
  323. return Repo.__revisions_from_log(out)
  324. def hg_paths(self):
  325. """Returns aliases for remote repositories"""
  326. out = self.hg_command(None, 'paths')
  327. lines = [l.strip() for l in out.split('\n')]
  328. pairs = [l.split('=') for l in lines if l != '']
  329. return {a.strip() : b.strip() for a, b in pairs}
  330. def hg_path(self, name):
  331. """Returns the alias for the given name"""
  332. out = self.hg_command(None, 'paths', name)
  333. out = out.strip()
  334. return out if out != '' else None
  335. _heads_handler = _ReturnCodeHandler().map_returncode_to_exception(1, HGHeadsNoHeads)
  336. def hg_heads(self):
  337. """Gets a list with the node id's of all open heads"""
  338. res = self.hg_command(self._heads_handler, "heads","--template", "{node}\n")
  339. return [head for head in res.split("\n") if head]
  340. def hg_add(self, filepath):
  341. """Add a file to the repo"""
  342. self.hg_command(None, "add", filepath)
  343. _remove_handler = _ReturnCodeHandler().map_returncode_to_exception(1, HGRemoveWarning)
  344. def hg_remove(self, filepath):
  345. """Remove a file from the repo"""
  346. self.hg_command(self._remove_handler, "remove", filepath)
  347. _move_handler = _ReturnCodeHandler().map_returncode_to_exception(1, HGMoveError)
  348. def hg_move(self, srcpath, destpath):
  349. """Move/rename a file in the repo"""
  350. self.hg_command(self._move_handler, "move", srcpath, destpath)
  351. _copy_handler = _ReturnCodeHandler().map_returncode_to_exception(1, HGCopyError)
  352. def hg_copy(self, srcpath, destpath):
  353. """Copy a file in the repo"""
  354. self.hg_command(self._copy_handler, "copy", srcpath, destpath)
  355. _commit_handler = _ReturnCodeHandler().map_returncode_to_exception(1, HGCommitNoChanges)
  356. def hg_commit(self, message, user=None, files=[], close_branch=False):
  357. """Commit changes to the repository."""
  358. userspec = "-u" + user if user else "-u" + self.user if self.user else ""
  359. close = "--close-branch" if close_branch else ""
  360. args = [close, userspec] + files
  361. # don't send a "" arg for userspec or close, which HG will
  362. # consider the files arg, committing all files instead of what
  363. # was passed in files kwarg
  364. args = [arg for arg in args if arg]
  365. self.hg_command(self._commit_handler, "commit", "-m", message, *args)
  366. def hg_revert(self, all=False, *files):
  367. """Revert repository"""
  368. if all:
  369. cmd = ["revert", "--all"]
  370. else:
  371. cmd = ["revert"] + list(files)
  372. self.hg_command(None, *cmd)
  373. self._notify_filesystem_modified()
  374. _unresolved_handler = _ReturnCodeHandler().map_returncode_to_exception(1, HGUnresolvedFiles)
  375. def hg_update(self, reference, clean=False):
  376. """Update to the revision indetified by reference"""
  377. cmd = ["update"]
  378. if reference is not None:
  379. cmd.append(str(reference))
  380. if clean: cmd.append("--clean")
  381. try:
  382. self.hg_command(self._unresolved_handler, *cmd)
  383. finally:
  384. self._notify_filesystem_modified()
  385. def hg_merge(self, reference=None, tool=None):
  386. """Merge reference to current"""
  387. cmd = ['merge']
  388. if reference is not None:
  389. cmd.append('-r')
  390. cmd.append(reference)
  391. if tool is not None:
  392. cmd.append('--tool')
  393. cmd.append(tool)
  394. try:
  395. self.hg_command(self._unresolved_handler, *cmd)
  396. finally:
  397. self._notify_filesystem_modified()
  398. _resolve_handler = _ReturnCodeHandler().map_returncode_to_exception(1, HGResolveFailed)
  399. def hg_resolve_remerge(self, tool=None, files=None):
  400. cmd = ['resolve']
  401. if tool is not None:
  402. cmd.append('--tool')
  403. cmd.append(tool)
  404. if files is None:
  405. cmd.append('--all')
  406. else:
  407. cmd.extend(files)
  408. try:
  409. self.hg_command(self._resolve_handler, *cmd)
  410. finally:
  411. self._notify_filesystem_modified()
  412. def hg_resolve_mark_as_resolved(self, files=None):
  413. cmd = ['resolve', '-m']
  414. if files is not None:
  415. cmd.extend(files)
  416. self.hg_command(self._resolve_handler, *cmd)
  417. def hg_resolve_mark_as_unresolved(self, files=None):
  418. cmd = ['resolve', '-u']
  419. if files is not None:
  420. cmd.extend(files)
  421. self.hg_command(self._resolve_handler, *cmd)
  422. def hg_resolve_list(self):
  423. cmd = ['resolve', '-l']
  424. resolve_result = self.hg_command(self._resolve_handler, *cmd)
  425. unresolved_list = resolve_result.strip().split("\n")
  426. # Create the resolve state
  427. state = ResolveState()
  428. # Fill it in
  429. for u in unresolved_list:
  430. u = u.strip()
  431. if u != '':
  432. code, name = u.split(' ')
  433. if code == 'R':
  434. state.resolved.add(name)
  435. elif code == 'U':
  436. state.unresolved.add(name)
  437. else:
  438. raise ValueError, 'Unknown resolve code \'{0}\''.format(code)
  439. return state
  440. def hg_merge_custom(self, reference=None):
  441. """Merge reference to current, with custom conflict resolution
  442. Returns a CustomMergeState that describes files that are in an unresolved state, allowing the application
  443. to handle them.
  444. Uses the HG 'internal:dump' merging tool, causing the base and derived versions of the file to be written,
  445. where the application can access them.
  446. """
  447. try:
  448. self.hg_merge(reference, MERGETOOL_INTERNAL_DUMP)
  449. except HGUnresolvedFiles:
  450. # We have unresolved files
  451. pass
  452. finally:
  453. self._notify_filesystem_modified()
  454. return self.hg_resolve_list()
  455. def hg_resolve_custom_take_local(self, file):
  456. self.__hg_resolve_custom_take(file, '.local')
  457. def hg_resolve_custom_take_other(self, file):
  458. self.__hg_resolve_custom_take(file, '.other')
  459. def __hg_resolve_custom_take(self, file, suffix):
  460. path = os.path.join(self.path, file)
  461. merge_path = path + suffix
  462. if not os.path.exists(path):
  463. raise IOError, 'File \'{0}\' does not exist'.format(path)
  464. if not os.path.exists(merge_path):
  465. raise IOError, 'Merge file \'{0}\' does not exist'.format(merge_path)
  466. shutil.copyfile(path, merge_path)
  467. self.hg_resolve_mark_as_resolved([file])
  468. def remove_merge_files(self, files):
  469. """Remove files resulting from merging
  470. files - a file name or a collection of filenames
  471. For each file in the input list, the existence of .base, .local, .other and .orig files it tested.
  472. If they exist, they are deleted
  473. """
  474. if isinstance(files, str) or isinstance(files, unicode):
  475. files = [files]
  476. removed = False
  477. for file in files:
  478. merge_file_paths = [os.path.join(self.path, file + suffix) for suffix in ['.base', '.local', '.other', '.orig']]
  479. for m in merge_file_paths:
  480. if os.path.exists(m):
  481. os.remove(m)
  482. removed = True
  483. if removed:
  484. self._notify_filesystem_modified()
  485. _pull_handler = _ReturnCodeHandler().map_returncode_to_exception(1, HGUnresolvedFiles).map_returncode_to_exception(255, HGRepoUnrelated)
  486. def hg_pull(self, progress_listener=None):
  487. def _stdout_listener(line):
  488. line = line.strip()
  489. if line != '' and line != '(run \'hg update\' to get a working copy)':
  490. progress_listener(line)
  491. cmd = ['pull']
  492. if progress_listener:
  493. cmd.append('-v')
  494. return self.hg_remote_command_with_stdout_listener(self._pull_handler, _stdout_listener if progress_listener is not None else None, *cmd)
  495. _push_handler = _ReturnCodeHandler().map_returncode_to_exception(1, HGPushNothingToPushError).map_returncode_to_exception(255, HGRepoUnrelated)
  496. def hg_push(self, force=False, progress_listener=None):
  497. def _stdout_listener(line):
  498. line = line.strip()
  499. if line != '':
  500. progress_listener(line)
  501. cmd = ['push']
  502. if force:
  503. cmd.append('--force')
  504. if progress_listener:
  505. cmd.append('-v')
  506. return self.hg_remote_command_with_stdout_listener(self._push_handler, _stdout_listener if progress_listener is not None else None, *cmd)
  507. def get_branches(self, active_only=False, show_closed=False):
  508. """ Returns a list of branches from the repo, including versions """
  509. cmd = ['branches']
  510. if active_only:
  511. cmd.append('--active')
  512. if show_closed:
  513. cmd.append('--closed')
  514. branches = self.hg_command(None, *cmd)
  515. branch_list = branches.strip().split("\n")
  516. values = []
  517. for branch in branch_list:
  518. b = branch.partition(" ")
  519. if not b:
  520. continue
  521. name = b[0].strip()
  522. version = b[-1].strip()
  523. values.append({'name':name, 'version':version})
  524. return values
  525. def get_branch_names(self, active_only=False, show_closed=False):
  526. return [branch['name'] for branch in self.get_branches(active_only=active_only, show_closed=show_closed)]
  527. def hg_branch(self, branch_name=None):
  528. """ Creates a branch of branch_name isn't None
  529. If not, returns the current branch name.
  530. """
  531. args = []
  532. if branch_name:
  533. args.append(branch_name)
  534. branch = self.hg_command(None, "branch", *args)
  535. return branch.strip()
  536. ARCHIVETYPE_FILES = 'files'
  537. ARCHIVETYPE_TAR = 'tar'
  538. ARCHIVETYPE_TAR_BZIP2 = 'tbz2'
  539. ARCHIVETYPE_TAR_GZ = 'tgz'
  540. ARCHIVETYPE_UNCOMPRESSED_ZIP = 'uzip'
  541. ARCHIVETYPE_ZIP = 'zip'
  542. def hg_archive(self, dest, revision=None, archive_type=None, prefix=None):
  543. """Create an archive of the repository
  544. dest - the folder into which the archive is to be placed
  545. revision (optional) - the revision to use
  546. archive_type - the type of archive
  547. use: ARCHIVETYPE_FILES, ARCHIVETYPE_TAR, ARCHIVETYPE_TAR_BZIP2, ARCHIVETYPE_TAR_GZ, ARCHIVETYPE_UNCOMPRESSED_ZIP, ARCHIVETYPE_ZIP
  548. prefix - the directory prefix
  549. returns - hg's standard output
  550. For more information, at the command line, invoke:
  551. > hg help archive
  552. """
  553. cmd = ['archive']
  554. if revision is not None:
  555. cmd.extend(['--rev', str(revision)])
  556. if archive_type is not None:
  557. cmd.extend(['--type', str(archive_type)])
  558. if prefix is not None:
  559. cmd.extend(['--prefix', str(prefix)])
  560. cmd.append(str(dest))
  561. return self.hg_command(None, *cmd)
  562. _rebase_handler = _ReturnCodeHandler().map_returncode_to_exception(1, HGRebaseNothingToRebase)
  563. def hg_rebase(self, source, destination):
  564. if not self.is_extension_enabled('rebase'):
  565. raise HGExtensionDisabledError, 'rebase extension is disabled'
  566. cmd = ['rebase', '--source', str(source), '--dest', str(destination)]
  567. return self.hg_command(self._rebase_handler, *cmd)
  568. def enable_rebase(self):
  569. self.enable_extension('rebase')
  570. def enable_progress(self, delay=None):
  571. self.enable_extension('progress')
  572. config = self.read_repo_config()
  573. if not config.has_section('progress'):
  574. config.add_section('progress')
  575. config.set('progress', 'delay', str(delay))
  576. self.write_repo_config(config)
  577. def read_config(self):
  578. """Read the configuration as seen with 'hg showconfig'
  579. Is called by __init__ - only needs to be called explicitly
  580. to reflect changes made since instantiation"""
  581. # Not technically a remote command, but use hg_remote_command so that the SSH key path config option is present
  582. res = self.hg_remote_command(None, "showconfig")
  583. cfg = {}
  584. for row in res.split("\n"):
  585. section, ign, value = row.partition("=")
  586. main, ign, sub = section.partition(".")
  587. sect_cfg = cfg.setdefault(main, {})
  588. sect_cfg[sub] = value.strip()
  589. self.__cfg = cfg
  590. self.__cfg_date = datetime.now()
  591. return cfg
  592. def __refresh_config(self):
  593. if self.__cfg is None or \
  594. (self.__cfg_date is not None and \
  595. Repo.__user_cfg_mod_date is not None and \
  596. self.__cfg_date < Repo.__user_cfg_mod_date):
  597. self.read_config()
  598. return self.__cfg
  599. def config(self, section, key):
  600. """Return the value of a configuration variable"""
  601. cfg = self.__refresh_config()
  602. return cfg.get(section, {}).get(key, None)
  603. def configbool(self, section, key):
  604. """Return a config value as a boolean value.
  605. Empty values, the string 'false' (any capitalization),
  606. and '0' are considered False, anything else True"""
  607. cfg = self.__refresh_config()
  608. value = cfg.get(section, {}).get(key, None)
  609. if not value:
  610. return False
  611. if (value == "0"
  612. or value.upper() == "FALSE"
  613. or value.upper() == "None"):
  614. return False
  615. return True
  616. def configlist(self, section, key):
  617. """Return a config value as a list; will try to create a list
  618. delimited by commas, or whitespace if no commas are present"""
  619. cfg = self.__refresh_config()
  620. value = cfg.get(section, {}).get(key, None)
  621. if not value:
  622. return []
  623. if value.count(","):
  624. return value.split(",")
  625. else:
  626. return value.split()
  627. def _notify_filesystem_modified(self):
  628. if self.__on_filesystem_modified is not None:
  629. self.__on_filesystem_modified()
  630. @staticmethod
  631. def hg_init(path, user=None, ssh_key_path=None, disable_host_key_checking=False, on_filesystem_modified=None):
  632. """Initialize a new repo"""
  633. # Call hg_version() to check that it is installed and that it works
  634. hg_version()
  635. _hg_cmd(_default_return_code_handler, user, None, disable_host_key_checking, 'init', path)
  636. repo = Repo(path, user, ssh_key_path=ssh_key_path, disable_host_key_checking=disable_host_key_checking, on_filesystem_modified=on_filesystem_modified)
  637. return repo
  638. _clone_handler = _ReturnCodeHandler().map_returncode_to_exception(255, HGCloneRepoNotFound)
  639. @staticmethod
  640. def hg_clone(path, remote_uri, user=None, revision=None, ssh_key_path=None, disable_host_key_checking=False, on_filesystem_modified=None, ok_if_local_dir_exists=False):
  641. """Clone an existing repo"""
  642. # Call hg_version() to check that it is installed and that it works
  643. hg_version()
  644. if os.path.exists(path):
  645. if os.path.isdir(path):
  646. if not ok_if_local_dir_exists:
  647. raise HGError, 'Local directory \'{0}\' already exists'.format(path)
  648. else:
  649. raise HGError, 'Cannot clone into \'{0}\'; it is not a directory'.format(path)
  650. else:
  651. os.makedirs(path)
  652. cmd = ['clone']
  653. if revision is not None:
  654. cmd.extend(['-r', revision])
  655. cmd.extend([remote_uri, path])
  656. _hg_cmd(Repo._clone_handler, user, ssh_key_path, disable_host_key_checking, *cmd)
  657. repo = Repo(path, user, ssh_key_path=ssh_key_path, disable_host_key_checking=disable_host_key_checking, on_filesystem_modified=on_filesystem_modified)
  658. return repo
  659. def hg_version():
  660. """Return version number of mercurial"""
  661. try:
  662. proc = Popen([get_hg_path(), "version"], stdout=PIPE, stderr=PIPE, env=_hg_env())
  663. except:
  664. raise HGCannotLaunchError, 'Cannot launch hg executable'
  665. out, err = [x.decode("utf-8") for x in proc.communicate()]
  666. if proc.returncode:
  667. raise HGCannotLaunchError, 'Cannot get hg version'
  668. match = re.search('\s(([\w\.\-]+?)(\+[0-9]+)?)\)$', out.split("\n")[0])
  669. return match.group(1)
  670. def hg_check():
  671. try:
  672. hg_version()
  673. except HGCannotLaunchError:
  674. return False
  675. else:
  676. return True