common.py
496 lines
| 15.7 KiB
| text/x-python
|
PythonLexer
|
r8250 | # common.py - common code for the convert extension | ||
# | ||||
# Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others | ||||
# | ||||
# This software may be used and distributed according to the terms of the | ||||
|
r10263 | # GNU General Public License version 2 or any later version. | ||
|
r28410 | from __future__ import absolute_import | ||
|
r8250 | |||
|
r28410 | import base64 | ||
import datetime | ||||
import errno | ||||
import os | ||||
import re | ||||
import subprocess | ||||
|
r29205 | from mercurial.i18n import _ | ||
|
r28410 | from mercurial import ( | ||
|
r34024 | encoding, | ||
|
r28410 | error, | ||
phases, | ||||
util, | ||||
) | ||||
|
r5127 | |||
|
r29324 | pickle = util.pickle | ||
|
r15606 | propertycache = util.propertycache | ||
|
r5127 | def encodeargs(args): | ||
def encodearg(s): | ||||
lines = base64.encodestring(s) | ||||
lines = [l.splitlines()[0] for l in lines] | ||||
return ''.join(lines) | ||||
|
r5143 | |||
|
r5127 | s = pickle.dumps(args) | ||
return encodearg(s) | ||||
def decodeargs(s): | ||||
s = base64.decodestring(s) | ||||
return pickle.loads(s) | ||||
|
r4536 | |||
|
r10282 | class MissingTool(Exception): | ||
pass | ||||
|
r6332 | |||
def checktool(exe, name=None, abort=True): | ||||
|
r5497 | name = name or exe | ||
|
r14271 | if not util.findexe(exe): | ||
|
r24306 | if abort: | ||
|
r26587 | exc = error.Abort | ||
|
r24306 | else: | ||
exc = MissingTool | ||||
|
r6332 | raise exc(_('cannot find required "%s" tool') % name) | ||
|
r5497 | |||
|
r10282 | class NoRepo(Exception): | ||
pass | ||||
|
r4536 | |||
|
r5400 | SKIPREV = 'SKIP' | ||
|
r5374 | |||
|
r4536 | class commit(object): | ||
|
r5439 | def __init__(self, author, date, desc, parents, branch=None, rev=None, | ||
|
r30659 | extra=None, sortkey=None, saverev=True, phase=phases.draft, | ||
|
r28900 | optparents=None): | ||
|
r5984 | self.author = author or 'unknown' | ||
self.date = date or '0 0' | ||||
|
r5024 | self.desc = desc | ||
|
r28900 | self.parents = parents # will be converted and used as parents | ||
self.optparents = optparents or [] # will be used if already converted | ||||
|
r5012 | self.branch = branch | ||
self.rev = rev | ||||
|
r30659 | self.extra = extra or {} | ||
|
r8690 | self.sortkey = sortkey | ||
|
r25570 | self.saverev = saverev | ||
|
r25571 | self.phase = phase | ||
|
r4536 | |||
class converter_source(object): | ||||
"""Conversion source interface""" | ||||
|
r25748 | def __init__(self, ui, path=None, revs=None): | ||
|
r4536 | """Initialize conversion source (or raise NoRepo("message") | ||
exception if path is not a valid repository)""" | ||||
|
r4810 | self.ui = ui | ||
self.path = path | ||||
|
r25748 | self.revs = revs | ||
|
r4810 | |||
self.encoding = 'utf-8' | ||||
|
r4812 | |||
|
r20373 | def checkhexformat(self, revstr, mapname='splicemap'): | ||
|
r19120 | """ fails if revstr is not a 40 byte hex. mercurial and git both uses | ||
such format for their revision numbering | ||||
""" | ||||
|
r19122 | if not re.match(r'[0-9a-fA-F]{40,40}$', revstr): | ||
|
r26587 | raise error.Abort(_('%s entry %s is not a valid revision' | ||
|
r20373 | ' identifier') % (mapname, revstr)) | ||
|
r19120 | |||
|
r5356 | def before(self): | ||
pass | ||||
def after(self): | ||||
pass | ||||
|
r26035 | def targetfilebelongstosource(self, targetfilename): | ||
"""Returns true if the given targetfile belongs to the source repo. This | ||||
is useful when only a subdirectory of the target belongs to the source | ||||
repo.""" | ||||
# For normal full repo converts, this is always True. | ||||
return True | ||||
|
r5510 | def setrevmap(self, revmap): | ||
"""set the map of already-converted revisions""" | ||||
|
r4813 | pass | ||
|
r4536 | |||
def getheads(self): | ||||
"""Return a list of this repository's heads""" | ||||
|
r16687 | raise NotImplementedError | ||
|
r4536 | |||
def getfile(self, name, rev): | ||||
|
r11134 | """Return a pair (data, mode) where data is the file content | ||
as a string and mode one of '', 'x' or 'l'. rev is the | ||||
|
r22296 | identifier returned by a previous call to getchanges(). | ||
Data is None if file is missing/deleted in rev. | ||||
|
r7055 | """ | ||
|
r16687 | raise NotImplementedError | ||
|
r4536 | |||
|
r22300 | def getchanges(self, version, full): | ||
|
r24395 | """Returns a tuple of (files, copies, cleanp2). | ||
|
r7055 | |||
files is a sorted list of (filename, id) tuples for all files | ||||
|
r8444 | changed between version and its first parent returned by | ||
|
r22300 | getcommit(). If full, all files in that revision is returned. | ||
id is the source revision id of the file. | ||||
|
r4536 | |||
|
r5121 | copies is a dictionary of dest: source | ||
|
r24395 | |||
cleanp2 is the set of files filenames that are clean against p2. | ||||
(Files that are clean against p1 are already not in files (unless | ||||
full). This makes it possible to handle p2 clean files similarly.) | ||||
|
r5121 | """ | ||
|
r16687 | raise NotImplementedError | ||
|
r4536 | |||
def getcommit(self, version): | ||||
"""Return the commit object for version""" | ||||
|
r16687 | raise NotImplementedError | ||
|
r4536 | |||
|
r22411 | def numcommits(self): | ||
"""Return the number of commits in this source. | ||||
If unknown, return None. | ||||
""" | ||||
return None | ||||
|
r4536 | def gettags(self): | ||
|
r8887 | """Return the tags as a dictionary of name: revision | ||
Tag names must be UTF-8 strings. | ||||
""" | ||||
|
r16687 | raise NotImplementedError | ||
|
r4536 | |||
|
r4759 | def recode(self, s, encoding=None): | ||
if not encoding: | ||||
|
r4810 | encoding = self.encoding or 'utf-8' | ||
|
r4957 | |||
|
r5287 | if isinstance(s, unicode): | ||
return s.encode("utf-8") | ||||
|
r4759 | try: | ||
return s.decode(encoding).encode("utf-8") | ||||
|
r16688 | except UnicodeError: | ||
|
r4759 | try: | ||
return s.decode("latin-1").encode("utf-8") | ||||
|
r16688 | except UnicodeError: | ||
|
r4759 | return s.decode(encoding, "replace").encode("utf-8") | ||
|
r5377 | def getchangedfiles(self, rev, i): | ||
"""Return the files changed by rev compared to parent[i]. | ||||
|
r5760 | |||
|
r5377 | i is an index selecting one of the parents of rev. The return | ||
value should be the list of files that are different in rev and | ||||
this parent. | ||||
If rev has no parents, i is None. | ||||
|
r5760 | |||
|
r5377 | This function is only needed to support --filemap | ||
""" | ||||
|
r16687 | raise NotImplementedError | ||
|
r5377 | |||
|
r5554 | def converted(self, rev, sinkrev): | ||
'''Notify the source that a revision has been converted.''' | ||||
pass | ||||
|
r8691 | def hasnativeorder(self): | ||
"""Return true if this source has a meaningful, native revision | ||||
order. For instance, Mercurial revisions are store sequentially | ||||
while there is no such global ordering with Darcs. | ||||
""" | ||||
return False | ||||
|
r18819 | def hasnativeclose(self): | ||
"""Return true if this source has ability to close branch. | ||||
""" | ||||
return False | ||||
|
r8693 | def lookuprev(self, rev): | ||
"""If rev is a meaningful revision reference in source, return | ||||
the referenced identifier in the same format used by getcommit(). | ||||
return None otherwise. | ||||
""" | ||||
return None | ||||
|
r5554 | |||
|
r13744 | def getbookmarks(self): | ||
"""Return the bookmarks as a dictionary of name: revision | ||||
Bookmark names are to be UTF-8 strings. | ||||
""" | ||||
return {} | ||||
|
r20373 | def checkrevformat(self, revstr, mapname='splicemap'): | ||
|
r19120 | """revstr is a string that describes a revision in the given | ||
source control system. Return true if revstr has correct | ||||
format. | ||||
""" | ||||
return True | ||||
|
r4536 | class converter_sink(object): | ||
"""Conversion sink (target) interface""" | ||||
def __init__(self, ui, path): | ||||
"""Initialize conversion sink (or raise NoRepo("message") | ||||
|
r5441 | exception if path is not a valid repository) | ||
created is a list of paths to remove if a fatal error occurs | ||||
later""" | ||||
self.ui = ui | ||||
|
r5440 | self.path = path | ||
|
r5441 | self.created = [] | ||
|
r4536 | |||
|
r5011 | def revmapfile(self): | ||
|
r4536 | """Path to a file that will contain lines | ||
source_rev_id sink_rev_id | ||||
mapping equivalent revision identifiers for each system.""" | ||||
|
r16687 | raise NotImplementedError | ||
|
r4536 | |||
|
r4589 | def authorfile(self): | ||
"""Path to a file that will contain lines | ||||
srcauthor=dstauthor | ||||
mapping equivalent authors identifiers for each system.""" | ||||
|
r4590 | return None | ||
|
r4589 | |||
|
r24395 | def putcommit(self, files, copies, parents, commit, source, revmap, full, | ||
cleanp2): | ||||
|
r4536 | """Create a revision with all changed files listed in 'files' | ||
|
r8693 | and having listed parents. 'commit' is a commit object | ||
containing at a minimum the author, date, and message for this | ||||
changeset. 'files' is a list of (path, version) tuples, | ||||
'copies' is a dictionary mapping destinations to sources, | ||||
'source' is the source repository, and 'revmap' is a mapfile | ||||
|
r11134 | of source revisions to converted revisions. Only getfile() and | ||
|
r22300 | lookuprev() should be called on 'source'. 'full' means that 'files' | ||
is complete and all other files should be removed. | ||||
|
r24395 | 'cleanp2' is a set of the filenames that are unchanged from p2 | ||
(only in the common merge case where there two parents). | ||||
|
r6716 | |||
Note that the sink repository is not told to update itself to | ||||
a particular revision (or even what that revision would be) | ||||
before it receives the file data. | ||||
""" | ||||
|
r16687 | raise NotImplementedError | ||
|
r4536 | |||
def puttags(self, tags): | ||||
"""Put tags into sink. | ||||
|
r8887 | |||
tags: {tagname: sink_rev_id, ...} where tagname is an UTF-8 string. | ||||
|
r9431 | Return a pair (tag_revision, tag_parent_revision), or (None, None) | ||
if nothing was changed. | ||||
|
r8887 | """ | ||
|
r16687 | raise NotImplementedError | ||
|
r5127 | |||
|
r5934 | def setbranch(self, branch, pbranches): | ||
|
r6716 | """Set the current branch name. Called before the first putcommit | ||
|
r5173 | on the branch. | ||
branch: branch name for subsequent commits | ||||
|
r5934 | pbranches: (converted parent revision, parent branch) tuples""" | ||
|
r5173 | pass | ||
|
r5378 | |||
def setfilemapmode(self, active): | ||||
"""Tell the destination that we're using a filemap | ||||
Some converter_sources (svn in particular) can claim that a file | ||||
was changed in a revision, even if there was no change. This method | ||||
tells the destination that we're using a filemap and that it should | ||||
filter empty revisions. | ||||
""" | ||||
pass | ||||
|
r5510 | |||
|
r5512 | def before(self): | ||
pass | ||||
def after(self): | ||||
pass | ||||
|
r13744 | def putbookmarks(self, bookmarks): | ||
"""Put bookmarks into sink. | ||||
bookmarks: {bookmarkname: sink_rev_id, ...} | ||||
where bookmarkname is an UTF-8 string. | ||||
""" | ||||
pass | ||||
|
r5512 | |||
|
r21635 | def hascommitfrommap(self, rev): | ||
"""Return False if a rev mentioned in a filemap is known to not be | ||||
present.""" | ||||
raise NotImplementedError | ||||
|
r21634 | def hascommitforsplicemap(self, rev): | ||
"""This method is for the special needs for splicemap handling and not | ||||
for general use. Returns True if the sink contains rev, aborts on some | ||||
special cases.""" | ||||
|
r16687 | raise NotImplementedError | ||
|
r16106 | |||
|
r5512 | class commandline(object): | ||
def __init__(self, ui, command): | ||||
self.ui = ui | ||||
self.command = command | ||||
def prerun(self): | ||||
pass | ||||
def postrun(self): | ||||
pass | ||||
|
r17413 | def _cmdline(self, cmd, *args, **kwargs): | ||
|
r5512 | cmdline = [self.command, cmd] + list(args) | ||
for k, v in kwargs.iteritems(): | ||||
if len(k) == 1: | ||||
cmdline.append('-' + k) | ||||
else: | ||||
cmdline.append('--' + k.replace('_', '-')) | ||||
try: | ||||
if len(k) == 1: | ||||
cmdline.append('' + v) | ||||
else: | ||||
cmdline[-1] += '=' + v | ||||
except TypeError: | ||||
pass | ||||
cmdline = [util.shellquote(arg) for arg in cmdline] | ||||
|
r7611 | if not self.ui.debugflag: | ||
|
r17391 | cmdline += ['2>', os.devnull] | ||
|
r5529 | cmdline = ' '.join(cmdline) | ||
|
r5832 | return cmdline | ||
|
r5512 | |||
|
r5832 | def _run(self, cmd, *args, **kwargs): | ||
|
r17413 | def popen(cmdline): | ||
p = subprocess.Popen(cmdline, shell=True, bufsize=-1, | ||||
close_fds=util.closefds, | ||||
stdout=subprocess.PIPE) | ||||
return p | ||||
return self._dorun(popen, cmd, *args, **kwargs) | ||||
|
r13759 | |||
def _run2(self, cmd, *args, **kwargs): | ||||
|
r17413 | return self._dorun(util.popen2, cmd, *args, **kwargs) | ||
|
r13759 | |||
|
r28662 | def _run3(self, cmd, *args, **kwargs): | ||
return self._dorun(util.popen3, cmd, *args, **kwargs) | ||||
|
r17413 | def _dorun(self, openfunc, cmd, *args, **kwargs): | ||
cmdline = self._cmdline(cmd, *args, **kwargs) | ||||
|
r9467 | self.ui.debug('running: %s\n' % (cmdline,)) | ||
|
r5512 | self.prerun() | ||
try: | ||||
|
r13759 | return openfunc(cmdline) | ||
|
r5512 | finally: | ||
self.postrun() | ||||
def run(self, cmd, *args, **kwargs): | ||||
|
r17413 | p = self._run(cmd, *args, **kwargs) | ||
output = p.communicate()[0] | ||||
|
r5512 | self.ui.debug(output) | ||
|
r17413 | return output, p.returncode | ||
|
r5512 | |||
|
r6035 | def runlines(self, cmd, *args, **kwargs): | ||
|
r17413 | p = self._run(cmd, *args, **kwargs) | ||
output = p.stdout.readlines() | ||||
p.wait() | ||||
|
r6049 | self.ui.debug(''.join(output)) | ||
|
r17413 | return output, p.returncode | ||
|
r6035 | |||
|
r5512 | def checkexit(self, status, output=''): | ||
if status: | ||||
if output: | ||||
self.ui.warn(_('%s error:\n') % self.command) | ||||
self.ui.warn(output) | ||||
|
r14234 | msg = util.explainexit(status)[0] | ||
|
r26587 | raise error.Abort('%s %s' % (self.command, msg)) | ||
|
r5512 | |||
def run0(self, cmd, *args, **kwargs): | ||||
output, status = self.run(cmd, *args, **kwargs) | ||||
self.checkexit(status, output) | ||||
return output | ||||
|
r6035 | def runlines0(self, cmd, *args, **kwargs): | ||
output, status = self.runlines(cmd, *args, **kwargs) | ||||
|
r6049 | self.checkexit(status, ''.join(output)) | ||
|
r6035 | return output | ||
|
r15606 | @propertycache | ||
def argmax(self): | ||||
|
r5832 | # POSIX requires at least 4096 bytes for ARG_MAX | ||
|
r15606 | argmax = 4096 | ||
|
r5832 | try: | ||
|
r15606 | argmax = os.sysconf("SC_ARG_MAX") | ||
|
r16688 | except (AttributeError, ValueError): | ||
|
r5832 | pass | ||
# Windows shells impose their own limits on command line length, | ||||
# down to 2047 bytes for cmd.exe under Windows NT/2k and 2500 bytes | ||||
# for older 4nt.exe. See http://support.microsoft.com/kb/830473 for | ||||
# details about cmd.exe limitations. | ||||
# Since ARG_MAX is for command line _and_ environment, lower our limit | ||||
# (and make happy Windows shells while doing this). | ||||
|
r15791 | return argmax // 2 - 1 | ||
|
r5832 | |||
|
r17412 | def _limit_arglist(self, arglist, cmd, *args, **kwargs): | ||
cmdlen = len(self._cmdline(cmd, *args, **kwargs)) | ||||
|
r15606 | limit = self.argmax - cmdlen | ||
|
r5832 | bytes = 0 | ||
fl = [] | ||||
for fn in arglist: | ||||
b = len(fn) + 3 | ||||
if bytes + b < limit or len(fl) == 0: | ||||
fl.append(fn) | ||||
bytes += b | ||||
else: | ||||
yield fl | ||||
fl = [fn] | ||||
bytes = b | ||||
if fl: | ||||
yield fl | ||||
def xargs(self, arglist, cmd, *args, **kwargs): | ||||
|
r17412 | for l in self._limit_arglist(arglist, cmd, *args, **kwargs): | ||
|
r5832 | self.run0(cmd, *(list(args) + l), **kwargs) | ||
|
r5510 | |||
class mapfile(dict): | ||||
def __init__(self, ui, path): | ||||
super(mapfile, self).__init__() | ||||
self.ui = ui | ||||
self.path = path | ||||
self.fp = None | ||||
self.order = [] | ||||
self._read() | ||||
def _read(self): | ||||
|
r7774 | if not self.path: | ||
|
r5996 | return | ||
|
r5510 | try: | ||
fp = open(self.path, 'r') | ||||
|
r25660 | except IOError as err: | ||
|
r5510 | if err.errno != errno.ENOENT: | ||
raise | ||||
return | ||||
|
r30400 | for i, line in enumerate(util.iterfile(fp)): | ||
|
r16190 | line = line.splitlines()[0].rstrip() | ||
if not line: | ||||
# Ignore blank lines | ||||
continue | ||||
|
r8047 | try: | ||
|
r16190 | key, value = line.rsplit(' ', 1) | ||
|
r8047 | except ValueError: | ||
|
r26587 | raise error.Abort( | ||
|
r10282 | _('syntax error in %s(%d): key/value pair expected') | ||
% (self.path, i + 1)) | ||||
|
r5510 | if key not in self: | ||
self.order.append(key) | ||||
super(mapfile, self).__setitem__(key, value) | ||||
fp.close() | ||||
|
r5760 | |||
|
r5510 | def __setitem__(self, key, value): | ||
if self.fp is None: | ||||
try: | ||||
self.fp = open(self.path, 'a') | ||||
|
r25660 | except IOError as err: | ||
|
r34024 | raise error.Abort( | ||
_('could not open map file %r: %s') % | ||||
(self.path, encoding.strtolocal(err.strerror))) | ||||
|
r5510 | self.fp.write('%s %s\n' % (key, value)) | ||
self.fp.flush() | ||||
super(mapfile, self).__setitem__(key, value) | ||||
def close(self): | ||||
|
r5512 | if self.fp: | ||
self.fp.close() | ||||
self.fp = None | ||||
|
r16105 | |||
|
r17974 | def makedatetimestamp(t): | ||
"""Like util.makedate() but for time t instead of current time""" | ||||
delta = (datetime.datetime.utcfromtimestamp(t) - | ||||
datetime.datetime.fromtimestamp(t)) | ||||
tz = delta.days * 86400 + delta.seconds | ||||
return t, tz | ||||