p4.py
336 lines
| 11.6 KiB
| text/x-python
|
PythonLexer
|
r7823 | # Perforce source for convert extension. | ||
# | ||||
# Copyright 2009, Frank Kingswood <frank@kingswood-consulting.co.uk> | ||||
# | ||||
|
r8225 | # This software may be used and distributed according to the terms of the | ||
|
r10263 | # GNU General Public License version 2 or any later version. | ||
|
r28371 | from __future__ import absolute_import | ||
|
r7823 | |||
|
r28371 | import marshal | ||
import re | ||||
|
r29205 | from mercurial.i18n import _ | ||
|
r28371 | from mercurial import ( | ||
error, | ||||
util, | ||||
) | ||||
|
r7823 | |||
|
r28371 | from . import common | ||
|
r7823 | |||
def loaditer(f): | ||||
"Yield the dictionary objects generated by p4" | ||||
try: | ||||
while True: | ||||
d = marshal.load(f) | ||||
if not d: | ||||
break | ||||
yield d | ||||
except EOFError: | ||||
pass | ||||
|
r25788 | def decodefilename(filename): | ||
"""Perforce escapes special characters @, #, *, or % | ||||
with %40, %23, %2A, or %25 respectively | ||||
>>> decodefilename('portable-net45%252Bnetcore45%252Bwp8%252BMonoAndroid') | ||||
'portable-net45%2Bnetcore45%2Bwp8%2BMonoAndroid' | ||||
>>> decodefilename('//Depot/Directory/%2525/%2523/%23%40.%2A') | ||||
'//Depot/Directory/%25/%23/#@.*' | ||||
""" | ||||
replacements = [('%2A', '*'), ('%23', '#'), ('%40', '@'), ('%25', '%')] | ||||
for k, v in replacements: | ||||
filename = filename.replace(k, v) | ||||
return filename | ||||
|
r28371 | class p4_source(common.converter_source): | ||
|
r25748 | def __init__(self, ui, path, revs=None): | ||
|
r25884 | # avoid import cycle | ||
|
r28371 | from . import convcmd | ||
|
r25884 | |||
|
r25748 | super(p4_source, self).__init__(ui, path, revs=revs) | ||
|
r7823 | |||
|
r8829 | if "/" in path and not path.startswith('//'): | ||
|
r28371 | raise common.NoRepo(_('%s does not look like a P4 repository') % | ||
path) | ||||
|
r7973 | |||
|
r28371 | common.checktool('p4', abort=False) | ||
|
r7823 | |||
|
r30601 | self.revmap = {} | ||
|
r7823 | self.p4changes = {} | ||
|
r30598 | self.heads = [] | ||
|
r7823 | self.changeset = {} | ||
self.files = {} | ||||
|
r25751 | self.copies = {} | ||
|
r25884 | self.encoding = self.ui.config('convert', 'p4.encoding', | ||
default=convcmd.orig_encoding) | ||||
|
r7823 | self.depotname = {} # mapping from local name to depot name | ||
|
r10282 | self.re_type = re.compile( | ||
"([a-z]+)?(text|binary|symlink|apple|resource|unicode|utf\d+)" | ||||
"(\+\w+)?$") | ||||
self.re_keywords = re.compile( | ||||
r"\$(Id|Header|Date|DateTime|Change|File|Revision|Author)" | ||||
r":[^$\n]*\$") | ||||
|
r8829 | self.re_keywords_old = re.compile("\$(Id|Header):[^$\n]*\$") | ||
|
r7823 | |||
|
r25748 | if revs and len(revs) > 1: | ||
|
r26587 | raise error.Abort(_("p4 source does not support specifying " | ||
|
r25748 | "multiple revisions")) | ||
|
r7823 | self._parse(ui, path) | ||
|
r30601 | def setrevmap(self, revmap): | ||
"""Sets the parsed revmap dictionary. | ||||
Revmap stores mappings from a source revision to a target revision. | ||||
It is set in convertcmd.convert and provided by the user as a file | ||||
on the commandline. | ||||
Revisions in the map are considered beeing present in the | ||||
repository and ignored during _parse(). This allows for incremental | ||||
imports if a revmap is provided. | ||||
""" | ||||
self.revmap = revmap | ||||
|
r7823 | def _parse_view(self, path): | ||
"Read changes affecting the path" | ||||
|
r11347 | cmd = 'p4 -G changes -s submitted %s' % util.shellquote(path) | ||
|
r9474 | stdout = util.popen(cmd, mode='rb') | ||
|
r30629 | p4changes = {} | ||
|
r7823 | for d in loaditer(stdout): | ||
c = d.get("change", None) | ||||
if c: | ||||
|
r30629 | p4changes[c] = True | ||
return p4changes | ||||
|
r7823 | |||
def _parse(self, ui, path): | ||||
"Prepare list of P4 filenames and revisions to import" | ||||
ui.status(_('reading p4 views\n')) | ||||
# read client spec or view | ||||
if "/" in path: | ||||
|
r30629 | self.p4changes.update(self._parse_view(path)) | ||
|
r7823 | if path.startswith("//") and path.endswith("/..."): | ||
views = {path[:-3]:""} | ||||
else: | ||||
views = {"//": ""} | ||||
else: | ||||
|
r11347 | cmd = 'p4 -G client -o %s' % util.shellquote(path) | ||
|
r9474 | clientspec = marshal.load(util.popen(cmd, mode='rb')) | ||
|
r7869 | |||
|
r7823 | views = {} | ||
for client in clientspec: | ||||
if client.startswith("View"): | ||||
sview, cview = clientspec[client].split() | ||||
|
r30629 | self.p4changes.update(self._parse_view(sview)) | ||
|
r7823 | if sview.endswith("...") and cview.endswith("..."): | ||
sview = sview[:-3] | ||||
cview = cview[:-3] | ||||
cview = cview[2:] | ||||
cview = cview[cview.find("/") + 1:] | ||||
views[sview] = cview | ||||
# list of changes that affect our source files | ||||
self.p4changes = self.p4changes.keys() | ||||
self.p4changes.sort(key=int) | ||||
# list with depot pathnames, longest first | ||||
vieworder = views.keys() | ||||
|
r9039 | vieworder.sort(key=len, reverse=True) | ||
|
r7823 | |||
# handle revision limiting | ||||
startrev = self.ui.config('convert', 'p4.startrev', default=0) | ||||
# now read the full changelists to get the list of file revisions | ||||
ui.status(_('collecting p4 changelists\n')) | ||||
lastid = None | ||||
for change in self.p4changes: | ||||
|
r30597 | if startrev and int(change) < int(startrev): | ||
continue | ||||
if self.revs and int(change) > int(self.revs[0]): | ||||
continue | ||||
|
r30601 | if change in self.revmap: | ||
# Ignore already present revisions, but set the parent pointer. | ||||
lastid = change | ||||
continue | ||||
|
r30597 | |||
|
r7823 | if lastid: | ||
parents = [lastid] | ||||
else: | ||||
parents = [] | ||||
|
r7869 | |||
|
r30603 | d = self._fetch_revision(change) | ||
c = self._construct_commit(d, parents) | ||||
shortdesc = c.desc.splitlines(True)[0].rstrip('\r\n') | ||||
t = '%s %s' % (c.rev, repr(shortdesc)[1:-1]) | ||||
ui.status(util.ellipsis(t, 80) + '\n') | ||||
|
r7823 | |||
files = [] | ||||
|
r25751 | copies = {} | ||
copiedfiles = [] | ||||
|
r30630 | localname = {} | ||
|
r7823 | i = 0 | ||
while ("depotFile%d" % i) in d and ("rev%d" % i) in d: | ||||
oldname = d["depotFile%d" % i] | ||||
filename = None | ||||
for v in vieworder: | ||||
|
r25776 | if oldname.lower().startswith(v.lower()): | ||
|
r25788 | filename = decodefilename(views[v] + oldname[len(v):]) | ||
|
r7823 | break | ||
if filename: | ||||
files.append((filename, d["rev%d" % i])) | ||||
self.depotname[filename] = oldname | ||||
|
r25751 | if (d.get("action%d" % i) == "move/add"): | ||
copiedfiles.append(filename) | ||||
|
r30630 | localname[oldname] = filename | ||
|
r7823 | i += 1 | ||
|
r25751 | |||
# Collect information about copied files | ||||
for filename in copiedfiles: | ||||
oldname = self.depotname[filename] | ||||
flcmd = 'p4 -G filelog %s' \ | ||||
% util.shellquote(oldname) | ||||
flstdout = util.popen(flcmd, mode='rb') | ||||
copiedfilename = None | ||||
for d in loaditer(flstdout): | ||||
copiedoldname = None | ||||
i = 0 | ||||
while ("change%d" % i) in d: | ||||
if (d["change%d" % i] == change and | ||||
d["action%d" % i] == "move/add"): | ||||
j = 0 | ||||
while ("file%d,%d" % (i, j)) in d: | ||||
if d["how%d,%d" % (i, j)] == "moved from": | ||||
copiedoldname = d["file%d,%d" % (i, j)] | ||||
break | ||||
j += 1 | ||||
i += 1 | ||||
|
r30630 | if copiedoldname and copiedoldname in localname: | ||
copiedfilename = localname[copiedoldname] | ||||
|
r25751 | break | ||
if copiedfilename: | ||||
copies[filename] = copiedfilename | ||||
else: | ||||
ui.warn(_("cannot find source for copied file: %s@%s\n") | ||||
% (filename, change)) | ||||
|
r7823 | self.changeset[change] = c | ||
self.files[change] = files | ||||
|
r25751 | self.copies[change] = copies | ||
|
r7823 | lastid = change | ||
|
r7869 | |||
|
r30602 | if lastid and len(self.changeset) > 0: | ||
|
r7823 | self.heads = [lastid] | ||
def getheads(self): | ||||
return self.heads | ||||
def getfile(self, name, rev): | ||||
|
r11348 | cmd = 'p4 -G print %s' \ | ||
% util.shellquote("%s#%s" % (self.depotname[name], rev)) | ||||
|
r7823 | |||
|
r25775 | lasterror = None | ||
while True: | ||||
stdout = util.popen(cmd, mode='rb') | ||||
mode = None | ||||
|
r25882 | contents = [] | ||
|
r25775 | keywords = None | ||
|
r7823 | |||
|
r25775 | for d in loaditer(stdout): | ||
code = d["code"] | ||||
data = d.get("data") | ||||
|
r8829 | |||
|
r25775 | if code == "error": | ||
# if this is the first time error happened | ||||
# re-attempt getting the file | ||||
if not lasterror: | ||||
lasterror = IOError(d["generic"], data) | ||||
# this will exit inner-most for-loop | ||||
break | ||||
else: | ||||
raise lasterror | ||||
|
r8843 | |||
|
r25775 | elif code == "stat": | ||
action = d.get("action") | ||||
if action in ["purge", "delete", "move/delete"]: | ||||
return None, None | ||||
p4type = self.re_type.match(d["type"]) | ||||
if p4type: | ||||
mode = "" | ||||
flags = ((p4type.group(1) or "") | ||||
+ (p4type.group(3) or "")) | ||||
if "x" in flags: | ||||
mode = "x" | ||||
if p4type.group(2) == "symlink": | ||||
mode = "l" | ||||
if "ko" in flags: | ||||
keywords = self.re_keywords_old | ||||
elif "k" in flags: | ||||
keywords = self.re_keywords | ||||
|
r8843 | |||
|
r25775 | elif code == "text" or code == "binary": | ||
|
r25882 | contents.append(data) | ||
|
r25775 | |||
lasterror = None | ||||
if not lasterror: | ||||
break | ||||
|
r7823 | |||
if mode is None: | ||||
|
r22296 | return None, None | ||
|
r7823 | |||
|
r25882 | contents = ''.join(contents) | ||
|
r8829 | if keywords: | ||
contents = keywords.sub("$\\1$", contents) | ||||
if mode == "l" and contents.endswith("\n"): | ||||
contents = contents[:-1] | ||||
|
r11134 | return contents, mode | ||
|
r7823 | |||
|
r22300 | def getchanges(self, rev, full): | ||
if full: | ||||
|
r26779 | raise error.Abort(_("convert from p4 does not support --full")) | ||
|
r25751 | return self.files[rev], self.copies[rev], set() | ||
|
r7823 | |||
|
r30603 | def _construct_commit(self, obj, parents=None): | ||
""" | ||||
Constructs a common.commit object from an unmarshalled | ||||
`p4 describe` output | ||||
""" | ||||
desc = self.recode(obj.get("desc", "")) | ||||
date = (int(obj["time"]), 0) # timezone not set | ||||
if parents is None: | ||||
parents = [] | ||||
return common.commit(author=self.recode(obj["user"]), | ||||
date=util.datestr(date, '%Y-%m-%d %H:%M:%S %1%2'), | ||||
parents=parents, desc=desc, branch=None, rev=obj['change'], | ||||
extra={"p4": obj['change'], "convert_revision": obj['change']}) | ||||
def _fetch_revision(self, rev): | ||||
"""Return an output of `p4 describe` including author, commit date as | ||||
a dictionary.""" | ||||
cmd = "p4 -G describe -s %s" % rev | ||||
stdout = util.popen(cmd, mode='rb') | ||||
return marshal.load(stdout) | ||||
|
r7823 | def getcommit(self, rev): | ||
|
r30604 | if rev in self.changeset: | ||
return self.changeset[rev] | ||||
elif rev in self.revmap: | ||||
d = self._fetch_revision(rev) | ||||
return self._construct_commit(d, parents=None) | ||||
raise error.Abort( | ||||
_("cannot find %s in the revmap or parsed changesets") % rev) | ||||
|
r7823 | |||
def gettags(self): | ||||
|
r30599 | return {} | ||
|
r7823 | |||
def getchangedfiles(self, rev, i): | ||||
|
r8209 | return sorted([x[0] for x in self.files[rev]]) | ||