view hgext/convert/p4.py @ 19159:1b329f8c7b24 stable

windows: check target type before actual unlinking to follow POSIX semantics Creation and writing into target file via vfs (a.k.a opener) is done after "unlink()" target file, if it exists. For example, it is assumed that the revision X consists of file 'A', and the revision Y consists of file 'A/B'. Merging revision X into Y tries to "unlink()" on directory 'A' of 'A/B', before creation of file 'A'. On POSIX environment, directories should be removed by "rmdir(2)", and "unlink(2)" on directories fails. "unlink()" of Mercurial (and Python) uses "unlink(2)" directly, so unlinking in the merge case above would fail. In the other hand, on Windows environment, "unlink()" of Mercurial tries to rename before actual unlinking, to follow POSIX semantics: already opened file can be unlinked safely. This causes unexpected success in unlinking in the merge case above, even though directory 'A' is renamed to another. This confuses users. This patch checks whether target is directory or not before renaming, and raises IOError(errno.EPERM) if so, to follow POSIX semantics.
author FUJIWARA Katsunori <foozy@lares.dti.ne.jp>
date Tue, 07 May 2013 05:04:11 +0900
parents 363e808de349
children c27a37678508
line wrap: on
line source

# 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 mercurial import util
from mercurial.i18n import _

from common import commit, converter_source, checktool, NoRepo
import marshal
import re

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

class p4_source(converter_source):
    def __init__(self, ui, path, rev=None):
        super(p4_source, self).__init__(ui, path, rev=rev)

        if "/" in path and not path.startswith('//'):
            raise NoRepo(_('%s does not look like a P4 repository') % path)

        checktool('p4', abort=False)

        self.p4changes = {}
        self.heads = {}
        self.changeset = {}
        self.files = {}
        self.tags = {}
        self.lastbranch = {}
        self.parent = {}
        self.encoding = "latin_1"
        self.depotname = {}           # mapping from local name to depot name
        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]*\$")
        self.re_keywords_old = re.compile("\$(Id|Header):[^$\n]*\$")

        self._parse(ui, path)

    def _parse_view(self, path):
        "Read changes affecting the path"
        cmd = 'p4 -G changes -s submitted %s' % util.shellquote(path)
        stdout = util.popen(cmd, mode='rb')
        for d in loaditer(stdout):
            c = d.get("change", None)
            if c:
                self.p4changes[c] = True

    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:
            self._parse_view(path)
            if path.startswith("//") and path.endswith("/..."):
                views = {path[:-3]:""}
            else:
                views = {"//": ""}
        else:
            cmd = 'p4 -G client -o %s' % util.shellquote(path)
            clientspec = marshal.load(util.popen(cmd, mode='rb'))

            views = {}
            for client in clientspec:
                if client.startswith("View"):
                    sview, cview = clientspec[client].split()
                    self._parse_view(sview)
                    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()
        vieworder.sort(key=len, reverse=True)

        # handle revision limiting
        startrev = self.ui.config('convert', 'p4.startrev', default=0)
        self.p4changes = [x for x in self.p4changes
                          if ((not startrev or int(x) >= int(startrev)) and
                              (not self.rev or int(x) <= int(self.rev)))]

        # 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:
            cmd = "p4 -G describe -s %s" % change
            stdout = util.popen(cmd, mode='rb')
            d = marshal.load(stdout)
            desc = self.recode(d["desc"])
            shortdesc = desc.split("\n", 1)[0]
            t = '%s %s' % (d["change"], repr(shortdesc)[1:-1])
            ui.status(util.ellipsis(t, 80) + '\n')

            if lastid:
                parents = [lastid]
            else:
                parents = []

            date = (int(d["time"]), 0)     # timezone not set
            c = commit(author=self.recode(d["user"]),
                       date=util.datestr(date, '%Y-%m-%d %H:%M:%S %1%2'),
                       parents=parents, desc=desc, branch='',
                       extra={"p4": change})

            files = []
            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:
                    if oldname.startswith(v):
                        filename = views[v] + oldname[len(v):]
                        break
                if filename:
                    files.append((filename, d["rev%d" % i]))
                    self.depotname[filename] = oldname
                i += 1
            self.changeset[change] = c
            self.files[change] = files
            lastid = change

        if lastid:
            self.heads = [lastid]

    def getheads(self):
        return self.heads

    def getfile(self, name, rev):
        cmd = 'p4 -G print %s' \
            % util.shellquote("%s#%s" % (self.depotname[name], rev))
        stdout = util.popen(cmd, mode='rb')

        mode = None
        contents = ""
        keywords = None

        for d in loaditer(stdout):
            code = d["code"]
            data = d.get("data")

            if code == "error":
                raise IOError(d["generic"], data)

            elif code == "stat":
                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

            elif code == "text" or code == "binary":
                contents += data

        if mode is None:
            raise IOError(0, "bad stat")

        if keywords:
            contents = keywords.sub("$\\1$", contents)
        if mode == "l" and contents.endswith("\n"):
            contents = contents[:-1]

        return contents, mode

    def getchanges(self, rev):
        return self.files[rev], {}

    def getcommit(self, rev):
        return self.changeset[rev]

    def gettags(self):
        return self.tags

    def getchangedfiles(self, rev, i):
        return sorted([x[0] for x in self.files[rev]])