|
|
# Perforce source for convert extension.
|
|
|
#
|
|
|
# Copyright 2009, Frank Kingswood <frank@kingswood-consulting.co.uk>
|
|
|
#
|
|
|
# This software may be used and distributed according to the terms of the
|
|
|
# GNU General Public License version 2 or any later version.
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
import marshal
|
|
|
import re
|
|
|
|
|
|
from mercurial.i18n import _
|
|
|
from mercurial import (
|
|
|
error,
|
|
|
util,
|
|
|
)
|
|
|
from mercurial.utils import (
|
|
|
dateutil,
|
|
|
procutil,
|
|
|
stringutil,
|
|
|
)
|
|
|
|
|
|
from . import common
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
def decodefilename(filename):
|
|
|
"""Perforce escapes special characters @, #, *, or %
|
|
|
with %40, %23, %2A, or %25 respectively
|
|
|
|
|
|
>>> decodefilename(b'portable-net45%252Bnetcore45%252Bwp8%252BMonoAndroid')
|
|
|
'portable-net45%2Bnetcore45%2Bwp8%2BMonoAndroid'
|
|
|
>>> decodefilename(b'//Depot/Directory/%2525/%2523/%23%40.%2A')
|
|
|
'//Depot/Directory/%25/%23/#@.*'
|
|
|
"""
|
|
|
replacements = [
|
|
|
(b'%2A', b'*'),
|
|
|
(b'%23', b'#'),
|
|
|
(b'%40', b'@'),
|
|
|
(b'%25', b'%'),
|
|
|
]
|
|
|
for k, v in replacements:
|
|
|
filename = filename.replace(k, v)
|
|
|
return filename
|
|
|
|
|
|
|
|
|
class p4_source(common.converter_source):
|
|
|
def __init__(self, ui, repotype, path, revs=None):
|
|
|
# avoid import cycle
|
|
|
from . import convcmd
|
|
|
|
|
|
super(p4_source, self).__init__(ui, repotype, path, revs=revs)
|
|
|
|
|
|
if b"/" in path and not path.startswith(b'//'):
|
|
|
raise common.NoRepo(
|
|
|
_(b'%s does not look like a P4 repository') % path
|
|
|
)
|
|
|
|
|
|
common.checktool(b'p4', abort=False)
|
|
|
|
|
|
self.revmap = {}
|
|
|
self.encoding = self.ui.config(
|
|
|
b'convert', b'p4.encoding', convcmd.orig_encoding
|
|
|
)
|
|
|
self.re_type = re.compile(
|
|
|
br"([a-z]+)?(text|binary|symlink|apple|resource|unicode|utf\d+)"
|
|
|
br"(\+\w+)?$"
|
|
|
)
|
|
|
self.re_keywords = re.compile(
|
|
|
br"\$(Id|Header|Date|DateTime|Change|File|Revision|Author)"
|
|
|
br":[^$\n]*\$"
|
|
|
)
|
|
|
self.re_keywords_old = re.compile(br"\$(Id|Header):[^$\n]*\$")
|
|
|
|
|
|
if revs and len(revs) > 1:
|
|
|
raise error.Abort(
|
|
|
_(
|
|
|
b"p4 source does not support specifying "
|
|
|
b"multiple revisions"
|
|
|
)
|
|
|
)
|
|
|
|
|
|
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
|
|
|
|
|
|
def _parse_view(self, path):
|
|
|
"""Read changes affecting the path"""
|
|
|
cmd = b'p4 -G changes -s submitted %s' % procutil.shellquote(path)
|
|
|
stdout = procutil.popen(cmd, mode=b'rb')
|
|
|
p4changes = {}
|
|
|
for d in loaditer(stdout):
|
|
|
c = d.get(b"change", None)
|
|
|
if c:
|
|
|
p4changes[c] = True
|
|
|
return p4changes
|
|
|
|
|
|
def _parse(self, ui, path):
|
|
|
"""Prepare list of P4 filenames and revisions to import"""
|
|
|
p4changes = {}
|
|
|
changeset = {}
|
|
|
files_map = {}
|
|
|
copies_map = {}
|
|
|
localname = {}
|
|
|
depotname = {}
|
|
|
heads = []
|
|
|
|
|
|
ui.status(_(b'reading p4 views\n'))
|
|
|
|
|
|
# read client spec or view
|
|
|
if b"/" in path:
|
|
|
p4changes.update(self._parse_view(path))
|
|
|
if path.startswith(b"//") and path.endswith(b"/..."):
|
|
|
views = {path[:-3]: b""}
|
|
|
else:
|
|
|
views = {b"//": b""}
|
|
|
else:
|
|
|
cmd = b'p4 -G client -o %s' % procutil.shellquote(path)
|
|
|
clientspec = marshal.load(procutil.popen(cmd, mode=b'rb'))
|
|
|
|
|
|
views = {}
|
|
|
for client in clientspec:
|
|
|
if client.startswith(b"View"):
|
|
|
sview, cview = clientspec[client].split()
|
|
|
p4changes.update(self._parse_view(sview))
|
|
|
if sview.endswith(b"...") and cview.endswith(b"..."):
|
|
|
sview = sview[:-3]
|
|
|
cview = cview[:-3]
|
|
|
cview = cview[2:]
|
|
|
cview = cview[cview.find(b"/") + 1 :]
|
|
|
views[sview] = cview
|
|
|
|
|
|
# list of changes that affect our source files
|
|
|
p4changes = sorted(p4changes.keys(), key=int)
|
|
|
|
|
|
# list with depot pathnames, longest first
|
|
|
vieworder = sorted(views.keys(), key=len, reverse=True)
|
|
|
|
|
|
# handle revision limiting
|
|
|
startrev = self.ui.config(b'convert', b'p4.startrev')
|
|
|
|
|
|
# now read the full changelists to get the list of file revisions
|
|
|
ui.status(_(b'collecting p4 changelists\n'))
|
|
|
lastid = None
|
|
|
for change in p4changes:
|
|
|
if startrev and int(change) < int(startrev):
|
|
|
continue
|
|
|
if self.revs and int(change) > int(self.revs[0]):
|
|
|
continue
|
|
|
if change in self.revmap:
|
|
|
# Ignore already present revisions, but set the parent pointer.
|
|
|
lastid = change
|
|
|
continue
|
|
|
|
|
|
if lastid:
|
|
|
parents = [lastid]
|
|
|
else:
|
|
|
parents = []
|
|
|
|
|
|
d = self._fetch_revision(change)
|
|
|
c = self._construct_commit(d, parents)
|
|
|
|
|
|
descarr = c.desc.splitlines(True)
|
|
|
if len(descarr) > 0:
|
|
|
shortdesc = descarr[0].rstrip(b'\r\n')
|
|
|
else:
|
|
|
shortdesc = b'**empty changelist description**'
|
|
|
|
|
|
t = b'%s %s' % (c.rev, shortdesc)
|
|
|
ui.status(stringutil.ellipsis(t, 80) + b'\n')
|
|
|
|
|
|
files = []
|
|
|
copies = {}
|
|
|
copiedfiles = []
|
|
|
i = 0
|
|
|
while (b"depotFile%d" % i) in d and (b"rev%d" % i) in d:
|
|
|
oldname = d[b"depotFile%d" % i]
|
|
|
filename = None
|
|
|
for v in vieworder:
|
|
|
if oldname.lower().startswith(v.lower()):
|
|
|
filename = decodefilename(views[v] + oldname[len(v) :])
|
|
|
break
|
|
|
if filename:
|
|
|
files.append((filename, d[b"rev%d" % i]))
|
|
|
depotname[filename] = oldname
|
|
|
if d.get(b"action%d" % i) == b"move/add":
|
|
|
copiedfiles.append(filename)
|
|
|
localname[oldname] = filename
|
|
|
i += 1
|
|
|
|
|
|
# Collect information about copied files
|
|
|
for filename in copiedfiles:
|
|
|
oldname = depotname[filename]
|
|
|
|
|
|
flcmd = b'p4 -G filelog %s' % procutil.shellquote(oldname)
|
|
|
flstdout = procutil.popen(flcmd, mode=b'rb')
|
|
|
|
|
|
copiedfilename = None
|
|
|
for d in loaditer(flstdout):
|
|
|
copiedoldname = None
|
|
|
|
|
|
i = 0
|
|
|
while (b"change%d" % i) in d:
|
|
|
if (
|
|
|
d[b"change%d" % i] == change
|
|
|
and d[b"action%d" % i] == b"move/add"
|
|
|
):
|
|
|
j = 0
|
|
|
while (b"file%d,%d" % (i, j)) in d:
|
|
|
if d[b"how%d,%d" % (i, j)] == b"moved from":
|
|
|
copiedoldname = d[b"file%d,%d" % (i, j)]
|
|
|
break
|
|
|
j += 1
|
|
|
i += 1
|
|
|
|
|
|
if copiedoldname and copiedoldname in localname:
|
|
|
copiedfilename = localname[copiedoldname]
|
|
|
break
|
|
|
|
|
|
if copiedfilename:
|
|
|
copies[filename] = copiedfilename
|
|
|
else:
|
|
|
ui.warn(
|
|
|
_(b"cannot find source for copied file: %s@%s\n")
|
|
|
% (filename, change)
|
|
|
)
|
|
|
|
|
|
changeset[change] = c
|
|
|
files_map[change] = files
|
|
|
copies_map[change] = copies
|
|
|
lastid = change
|
|
|
|
|
|
if lastid and len(changeset) > 0:
|
|
|
heads = [lastid]
|
|
|
|
|
|
return {
|
|
|
b'changeset': changeset,
|
|
|
b'files': files_map,
|
|
|
b'copies': copies_map,
|
|
|
b'heads': heads,
|
|
|
b'depotname': depotname,
|
|
|
}
|
|
|
|
|
|
@util.propertycache
|
|
|
def _parse_once(self):
|
|
|
return self._parse(self.ui, self.path)
|
|
|
|
|
|
@util.propertycache
|
|
|
def copies(self):
|
|
|
return self._parse_once[b'copies']
|
|
|
|
|
|
@util.propertycache
|
|
|
def files(self):
|
|
|
return self._parse_once[b'files']
|
|
|
|
|
|
@util.propertycache
|
|
|
def changeset(self):
|
|
|
return self._parse_once[b'changeset']
|
|
|
|
|
|
@util.propertycache
|
|
|
def heads(self):
|
|
|
return self._parse_once[b'heads']
|
|
|
|
|
|
@util.propertycache
|
|
|
def depotname(self):
|
|
|
return self._parse_once[b'depotname']
|
|
|
|
|
|
def getheads(self):
|
|
|
return self.heads
|
|
|
|
|
|
def getfile(self, name, rev):
|
|
|
cmd = b'p4 -G print %s' % procutil.shellquote(
|
|
|
b"%s#%s" % (self.depotname[name], rev)
|
|
|
)
|
|
|
|
|
|
lasterror = None
|
|
|
while True:
|
|
|
stdout = procutil.popen(cmd, mode=b'rb')
|
|
|
|
|
|
mode = None
|
|
|
contents = []
|
|
|
keywords = None
|
|
|
|
|
|
for d in loaditer(stdout):
|
|
|
code = d[b"code"]
|
|
|
data = d.get(b"data")
|
|
|
|
|
|
if code == b"error":
|
|
|
# if this is the first time error happened
|
|
|
# re-attempt getting the file
|
|
|
if not lasterror:
|
|
|
lasterror = IOError(d[b"generic"], data)
|
|
|
# this will exit inner-most for-loop
|
|
|
break
|
|
|
else:
|
|
|
raise lasterror
|
|
|
|
|
|
elif code == b"stat":
|
|
|
action = d.get(b"action")
|
|
|
if action in [b"purge", b"delete", b"move/delete"]:
|
|
|
return None, None
|
|
|
p4type = self.re_type.match(d[b"type"])
|
|
|
if p4type:
|
|
|
mode = b""
|
|
|
flags = (p4type.group(1) or b"") + (
|
|
|
p4type.group(3) or b""
|
|
|
)
|
|
|
if b"x" in flags:
|
|
|
mode = b"x"
|
|
|
if p4type.group(2) == b"symlink":
|
|
|
mode = b"l"
|
|
|
if b"ko" in flags:
|
|
|
keywords = self.re_keywords_old
|
|
|
elif b"k" in flags:
|
|
|
keywords = self.re_keywords
|
|
|
|
|
|
elif code == b"text" or code == b"binary":
|
|
|
contents.append(data)
|
|
|
|
|
|
lasterror = None
|
|
|
|
|
|
if not lasterror:
|
|
|
break
|
|
|
|
|
|
if mode is None:
|
|
|
return None, None
|
|
|
|
|
|
contents = b''.join(contents)
|
|
|
|
|
|
if keywords:
|
|
|
contents = keywords.sub(b"$\\1$", contents)
|
|
|
if mode == b"l" and contents.endswith(b"\n"):
|
|
|
contents = contents[:-1]
|
|
|
|
|
|
return contents, mode
|
|
|
|
|
|
def getchanges(self, rev, full):
|
|
|
if full:
|
|
|
raise error.Abort(_(b"convert from p4 does not support --full"))
|
|
|
return self.files[rev], self.copies[rev], set()
|
|
|
|
|
|
def _construct_commit(self, obj, parents=None):
|
|
|
"""
|
|
|
Constructs a common.commit object from an unmarshalled
|
|
|
`p4 describe` output
|
|
|
"""
|
|
|
desc = self.recode(obj.get(b"desc", b""))
|
|
|
date = (int(obj[b"time"]), 0) # timezone not set
|
|
|
if parents is None:
|
|
|
parents = []
|
|
|
|
|
|
return common.commit(
|
|
|
author=self.recode(obj[b"user"]),
|
|
|
date=dateutil.datestr(date, b'%Y-%m-%d %H:%M:%S %1%2'),
|
|
|
parents=parents,
|
|
|
desc=desc,
|
|
|
branch=None,
|
|
|
rev=obj[b'change'],
|
|
|
extra={b"p4": obj[b'change'], b"convert_revision": obj[b'change']},
|
|
|
)
|
|
|
|
|
|
def _fetch_revision(self, rev):
|
|
|
"""Return an output of `p4 describe` including author, commit date as
|
|
|
a dictionary."""
|
|
|
cmd = b"p4 -G describe -s %s" % rev
|
|
|
stdout = procutil.popen(cmd, mode=b'rb')
|
|
|
return marshal.load(stdout)
|
|
|
|
|
|
def getcommit(self, rev):
|
|
|
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(
|
|
|
_(b"cannot find %s in the revmap or parsed changesets") % rev
|
|
|
)
|
|
|
|
|
|
def gettags(self):
|
|
|
return {}
|
|
|
|
|
|
def getchangedfiles(self, rev, i):
|
|
|
return sorted([x[0] for x in self.files[rev]])
|
|
|
|