PageRenderTime 43ms CodeModel.GetById 11ms RepoModel.GetById 0ms app.codeStats 0ms

/hgext/convert/common.py

https://bitbucket.org/mirror/mercurial/
Python | 450 lines | 441 code | 3 blank | 6 comment | 1 complexity | 645356421d4136c054d154005ff5d4a6 MD5 | raw file
Possible License(s): GPL-2.0
  1. # common.py - common code for the convert extension
  2. #
  3. # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
  4. #
  5. # This software may be used and distributed according to the terms of the
  6. # GNU General Public License version 2 or any later version.
  7. import base64, errno, subprocess, os, datetime, re
  8. import cPickle as pickle
  9. from mercurial import util
  10. from mercurial.i18n import _
  11. propertycache = util.propertycache
  12. def encodeargs(args):
  13. def encodearg(s):
  14. lines = base64.encodestring(s)
  15. lines = [l.splitlines()[0] for l in lines]
  16. return ''.join(lines)
  17. s = pickle.dumps(args)
  18. return encodearg(s)
  19. def decodeargs(s):
  20. s = base64.decodestring(s)
  21. return pickle.loads(s)
  22. class MissingTool(Exception):
  23. pass
  24. def checktool(exe, name=None, abort=True):
  25. name = name or exe
  26. if not util.findexe(exe):
  27. exc = abort and util.Abort or MissingTool
  28. raise exc(_('cannot find required "%s" tool') % name)
  29. class NoRepo(Exception):
  30. pass
  31. SKIPREV = 'SKIP'
  32. class commit(object):
  33. def __init__(self, author, date, desc, parents, branch=None, rev=None,
  34. extra={}, sortkey=None):
  35. self.author = author or 'unknown'
  36. self.date = date or '0 0'
  37. self.desc = desc
  38. self.parents = parents
  39. self.branch = branch
  40. self.rev = rev
  41. self.extra = extra
  42. self.sortkey = sortkey
  43. class converter_source(object):
  44. """Conversion source interface"""
  45. def __init__(self, ui, path=None, rev=None):
  46. """Initialize conversion source (or raise NoRepo("message")
  47. exception if path is not a valid repository)"""
  48. self.ui = ui
  49. self.path = path
  50. self.rev = rev
  51. self.encoding = 'utf-8'
  52. def checkhexformat(self, revstr, mapname='splicemap'):
  53. """ fails if revstr is not a 40 byte hex. mercurial and git both uses
  54. such format for their revision numbering
  55. """
  56. if not re.match(r'[0-9a-fA-F]{40,40}$', revstr):
  57. raise util.Abort(_('%s entry %s is not a valid revision'
  58. ' identifier') % (mapname, revstr))
  59. def before(self):
  60. pass
  61. def after(self):
  62. pass
  63. def setrevmap(self, revmap):
  64. """set the map of already-converted revisions"""
  65. pass
  66. def getheads(self):
  67. """Return a list of this repository's heads"""
  68. raise NotImplementedError
  69. def getfile(self, name, rev):
  70. """Return a pair (data, mode) where data is the file content
  71. as a string and mode one of '', 'x' or 'l'. rev is the
  72. identifier returned by a previous call to getchanges(). Raise
  73. IOError to indicate that name was deleted in rev.
  74. """
  75. raise NotImplementedError
  76. def getchanges(self, version):
  77. """Returns a tuple of (files, copies).
  78. files is a sorted list of (filename, id) tuples for all files
  79. changed between version and its first parent returned by
  80. getcommit(). id is the source revision id of the file.
  81. copies is a dictionary of dest: source
  82. """
  83. raise NotImplementedError
  84. def getcommit(self, version):
  85. """Return the commit object for version"""
  86. raise NotImplementedError
  87. def gettags(self):
  88. """Return the tags as a dictionary of name: revision
  89. Tag names must be UTF-8 strings.
  90. """
  91. raise NotImplementedError
  92. def recode(self, s, encoding=None):
  93. if not encoding:
  94. encoding = self.encoding or 'utf-8'
  95. if isinstance(s, unicode):
  96. return s.encode("utf-8")
  97. try:
  98. return s.decode(encoding).encode("utf-8")
  99. except UnicodeError:
  100. try:
  101. return s.decode("latin-1").encode("utf-8")
  102. except UnicodeError:
  103. return s.decode(encoding, "replace").encode("utf-8")
  104. def getchangedfiles(self, rev, i):
  105. """Return the files changed by rev compared to parent[i].
  106. i is an index selecting one of the parents of rev. The return
  107. value should be the list of files that are different in rev and
  108. this parent.
  109. If rev has no parents, i is None.
  110. This function is only needed to support --filemap
  111. """
  112. raise NotImplementedError
  113. def converted(self, rev, sinkrev):
  114. '''Notify the source that a revision has been converted.'''
  115. pass
  116. def hasnativeorder(self):
  117. """Return true if this source has a meaningful, native revision
  118. order. For instance, Mercurial revisions are store sequentially
  119. while there is no such global ordering with Darcs.
  120. """
  121. return False
  122. def hasnativeclose(self):
  123. """Return true if this source has ability to close branch.
  124. """
  125. return False
  126. def lookuprev(self, rev):
  127. """If rev is a meaningful revision reference in source, return
  128. the referenced identifier in the same format used by getcommit().
  129. return None otherwise.
  130. """
  131. return None
  132. def getbookmarks(self):
  133. """Return the bookmarks as a dictionary of name: revision
  134. Bookmark names are to be UTF-8 strings.
  135. """
  136. return {}
  137. def checkrevformat(self, revstr, mapname='splicemap'):
  138. """revstr is a string that describes a revision in the given
  139. source control system. Return true if revstr has correct
  140. format.
  141. """
  142. return True
  143. class converter_sink(object):
  144. """Conversion sink (target) interface"""
  145. def __init__(self, ui, path):
  146. """Initialize conversion sink (or raise NoRepo("message")
  147. exception if path is not a valid repository)
  148. created is a list of paths to remove if a fatal error occurs
  149. later"""
  150. self.ui = ui
  151. self.path = path
  152. self.created = []
  153. def revmapfile(self):
  154. """Path to a file that will contain lines
  155. source_rev_id sink_rev_id
  156. mapping equivalent revision identifiers for each system."""
  157. raise NotImplementedError
  158. def authorfile(self):
  159. """Path to a file that will contain lines
  160. srcauthor=dstauthor
  161. mapping equivalent authors identifiers for each system."""
  162. return None
  163. def putcommit(self, files, copies, parents, commit, source, revmap):
  164. """Create a revision with all changed files listed in 'files'
  165. and having listed parents. 'commit' is a commit object
  166. containing at a minimum the author, date, and message for this
  167. changeset. 'files' is a list of (path, version) tuples,
  168. 'copies' is a dictionary mapping destinations to sources,
  169. 'source' is the source repository, and 'revmap' is a mapfile
  170. of source revisions to converted revisions. Only getfile() and
  171. lookuprev() should be called on 'source'.
  172. Note that the sink repository is not told to update itself to
  173. a particular revision (or even what that revision would be)
  174. before it receives the file data.
  175. """
  176. raise NotImplementedError
  177. def puttags(self, tags):
  178. """Put tags into sink.
  179. tags: {tagname: sink_rev_id, ...} where tagname is an UTF-8 string.
  180. Return a pair (tag_revision, tag_parent_revision), or (None, None)
  181. if nothing was changed.
  182. """
  183. raise NotImplementedError
  184. def setbranch(self, branch, pbranches):
  185. """Set the current branch name. Called before the first putcommit
  186. on the branch.
  187. branch: branch name for subsequent commits
  188. pbranches: (converted parent revision, parent branch) tuples"""
  189. pass
  190. def setfilemapmode(self, active):
  191. """Tell the destination that we're using a filemap
  192. Some converter_sources (svn in particular) can claim that a file
  193. was changed in a revision, even if there was no change. This method
  194. tells the destination that we're using a filemap and that it should
  195. filter empty revisions.
  196. """
  197. pass
  198. def before(self):
  199. pass
  200. def after(self):
  201. pass
  202. def putbookmarks(self, bookmarks):
  203. """Put bookmarks into sink.
  204. bookmarks: {bookmarkname: sink_rev_id, ...}
  205. where bookmarkname is an UTF-8 string.
  206. """
  207. pass
  208. def hascommitfrommap(self, rev):
  209. """Return False if a rev mentioned in a filemap is known to not be
  210. present."""
  211. raise NotImplementedError
  212. def hascommitforsplicemap(self, rev):
  213. """This method is for the special needs for splicemap handling and not
  214. for general use. Returns True if the sink contains rev, aborts on some
  215. special cases."""
  216. raise NotImplementedError
  217. class commandline(object):
  218. def __init__(self, ui, command):
  219. self.ui = ui
  220. self.command = command
  221. def prerun(self):
  222. pass
  223. def postrun(self):
  224. pass
  225. def _cmdline(self, cmd, *args, **kwargs):
  226. cmdline = [self.command, cmd] + list(args)
  227. for k, v in kwargs.iteritems():
  228. if len(k) == 1:
  229. cmdline.append('-' + k)
  230. else:
  231. cmdline.append('--' + k.replace('_', '-'))
  232. try:
  233. if len(k) == 1:
  234. cmdline.append('' + v)
  235. else:
  236. cmdline[-1] += '=' + v
  237. except TypeError:
  238. pass
  239. cmdline = [util.shellquote(arg) for arg in cmdline]
  240. if not self.ui.debugflag:
  241. cmdline += ['2>', os.devnull]
  242. cmdline = ' '.join(cmdline)
  243. return cmdline
  244. def _run(self, cmd, *args, **kwargs):
  245. def popen(cmdline):
  246. p = subprocess.Popen(cmdline, shell=True, bufsize=-1,
  247. close_fds=util.closefds,
  248. stdout=subprocess.PIPE)
  249. return p
  250. return self._dorun(popen, cmd, *args, **kwargs)
  251. def _run2(self, cmd, *args, **kwargs):
  252. return self._dorun(util.popen2, cmd, *args, **kwargs)
  253. def _dorun(self, openfunc, cmd, *args, **kwargs):
  254. cmdline = self._cmdline(cmd, *args, **kwargs)
  255. self.ui.debug('running: %s\n' % (cmdline,))
  256. self.prerun()
  257. try:
  258. return openfunc(cmdline)
  259. finally:
  260. self.postrun()
  261. def run(self, cmd, *args, **kwargs):
  262. p = self._run(cmd, *args, **kwargs)
  263. output = p.communicate()[0]
  264. self.ui.debug(output)
  265. return output, p.returncode
  266. def runlines(self, cmd, *args, **kwargs):
  267. p = self._run(cmd, *args, **kwargs)
  268. output = p.stdout.readlines()
  269. p.wait()
  270. self.ui.debug(''.join(output))
  271. return output, p.returncode
  272. def checkexit(self, status, output=''):
  273. if status:
  274. if output:
  275. self.ui.warn(_('%s error:\n') % self.command)
  276. self.ui.warn(output)
  277. msg = util.explainexit(status)[0]
  278. raise util.Abort('%s %s' % (self.command, msg))
  279. def run0(self, cmd, *args, **kwargs):
  280. output, status = self.run(cmd, *args, **kwargs)
  281. self.checkexit(status, output)
  282. return output
  283. def runlines0(self, cmd, *args, **kwargs):
  284. output, status = self.runlines(cmd, *args, **kwargs)
  285. self.checkexit(status, ''.join(output))
  286. return output
  287. @propertycache
  288. def argmax(self):
  289. # POSIX requires at least 4096 bytes for ARG_MAX
  290. argmax = 4096
  291. try:
  292. argmax = os.sysconf("SC_ARG_MAX")
  293. except (AttributeError, ValueError):
  294. pass
  295. # Windows shells impose their own limits on command line length,
  296. # down to 2047 bytes for cmd.exe under Windows NT/2k and 2500 bytes
  297. # for older 4nt.exe. See http://support.microsoft.com/kb/830473 for
  298. # details about cmd.exe limitations.
  299. # Since ARG_MAX is for command line _and_ environment, lower our limit
  300. # (and make happy Windows shells while doing this).
  301. return argmax // 2 - 1
  302. def _limit_arglist(self, arglist, cmd, *args, **kwargs):
  303. cmdlen = len(self._cmdline(cmd, *args, **kwargs))
  304. limit = self.argmax - cmdlen
  305. bytes = 0
  306. fl = []
  307. for fn in arglist:
  308. b = len(fn) + 3
  309. if bytes + b < limit or len(fl) == 0:
  310. fl.append(fn)
  311. bytes += b
  312. else:
  313. yield fl
  314. fl = [fn]
  315. bytes = b
  316. if fl:
  317. yield fl
  318. def xargs(self, arglist, cmd, *args, **kwargs):
  319. for l in self._limit_arglist(arglist, cmd, *args, **kwargs):
  320. self.run0(cmd, *(list(args) + l), **kwargs)
  321. class mapfile(dict):
  322. def __init__(self, ui, path):
  323. super(mapfile, self).__init__()
  324. self.ui = ui
  325. self.path = path
  326. self.fp = None
  327. self.order = []
  328. self._read()
  329. def _read(self):
  330. if not self.path:
  331. return
  332. try:
  333. fp = open(self.path, 'r')
  334. except IOError, err:
  335. if err.errno != errno.ENOENT:
  336. raise
  337. return
  338. for i, line in enumerate(fp):
  339. line = line.splitlines()[0].rstrip()
  340. if not line:
  341. # Ignore blank lines
  342. continue
  343. try:
  344. key, value = line.rsplit(' ', 1)
  345. except ValueError:
  346. raise util.Abort(
  347. _('syntax error in %s(%d): key/value pair expected')
  348. % (self.path, i + 1))
  349. if key not in self:
  350. self.order.append(key)
  351. super(mapfile, self).__setitem__(key, value)
  352. fp.close()
  353. def __setitem__(self, key, value):
  354. if self.fp is None:
  355. try:
  356. self.fp = open(self.path, 'a')
  357. except IOError, err:
  358. raise util.Abort(_('could not open map file %r: %s') %
  359. (self.path, err.strerror))
  360. self.fp.write('%s %s\n' % (key, value))
  361. self.fp.flush()
  362. super(mapfile, self).__setitem__(key, value)
  363. def close(self):
  364. if self.fp:
  365. self.fp.close()
  366. self.fp = None
  367. def makedatetimestamp(t):
  368. """Like util.makedate() but for time t instead of current time"""
  369. delta = (datetime.datetime.utcfromtimestamp(t) -
  370. datetime.datetime.fromtimestamp(t))
  371. tz = delta.days * 86400 + delta.seconds
  372. return t, tz