<file_sep>\[**Note**: this file _should not_ be distributed with the prcs2hg package.\]

This repository contains the source code for prcs2hg, which is a command to convert a [PRCS][] project to [Mercurial][] revisions.
It would help you publish the revision history of an obsolete project whose changes were maintained with [PRCS][].

prcs2hg is free software: you can redistribute it and/or modify it under the conditions specified in each file.
The main part of prcs2hg is licensed with the [OSI-approved MIT License][].

For more information about prcs2hg, visit the prcs2hg project at <http://www.vx68k.org/prcs2hg>.

[PRCS]: <http://prcs.sourceforge.net/>
[Mercurial]: <http://mercurial.selenic.com/>
[OSI-approved MIT License]: <http://opensource.org/licenses/MIT>

## Third-party components

The file 'prcs/sexpdata.py' was copied from the [sexpdata][] package and
modified not to parse numbers so that we can handle version numbers correctly.

[sexpdata]: <https://github.com/tkf/sexpdata>
<file_sep>#!/usr/bin/env python
#
# Copyright (C) 2012-2015 <NAME>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
#

from distutils.core import setup

setup(name = 'prcs2hg',
      version = '2.0',
      author = '<NAME>',
      author_email = '<EMAIL>',
      url = 'http://www.vx68k.org/prcs2hg',
      packages = ['prcs2hg', 'prcslib'],
      scripts = ['scripts/prcs2hg'])
<file_sep>#
# Copyright (C) 2012-2014 <NAME>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
#

"""convert PRCS revisions to Mercurial changesets
"""

import sys
import re
import os
import hglib
from string import join
from prcslib import PrcsProject

class Converter(object):

    def __init__(self, name, verbose = False):
        """Construct a Converter object."""
        self.name = name
        self.verbose = verbose
        self.revisionmap = {}
        self.symlink_warned = {}

        self.prcs = PrcsProject(self.name)
        self.revisions = self.prcs.revisions()

        self.hgclient = hglib.open(".")

    def convert(self):
        """Convert all revisions in a project."""
        list = sorted(self.revisions, key = lambda id:
            self.revisions[id]["date"])

        for i in list:
            self.convertrevision(i)

    def convertrevision(self, version):
        version = str(version)

        if self.revisions[version].get("deleted"):
            sys.stderr.write("Ignored deleted version {0}\n".format(version))
            return

        if self.verbose:
            sys.stderr.write("Converting version {0}\n".format(version))

        descriptor = self.prcs.descriptor(version)
        parent = descriptor.parentversion()
        if parent is None:
            # It is a root revision.
            self.hgclient.update("null")
            parent_filemap = {}
        else:
            while self.revisions[str(parent)].get("deleted"):
                parent.minor -= 1
            parent = str(parent)

            if self.revisionmap.get(parent) is None:
                self.convertrevision(parent)
                # TODO: If the parent is not converted, do it here.
                sys.exit("Parent revision {0} not converted"
                    .format(parent))

            self.hgclient.update(self.revisionmap[parent])

            parent_filemap = self.revisions[parent].get("filemap")
            if parent_filemap is None:
                sys.exit("No parent filemap")
                parent_descriptor = self.prcs.descriptor(parent)
                parent_filemap = _makefilemap(parent_descriptor.files())

        # Handles merges.
        mergeparents = descriptor.mergeparents()
        if mergeparents:
            self._handlemerge(mergeparents)

        # Makes the working directory clean.
        for i in self.hgclient.status():
            if i[0] == "?":
                os.unlink(i[1])
        self.hgclient.revert([], "null", all = True)

        self.prcs.checkout(version)
        files = descriptor.files()
        filemap = _makefilemap(files)
        self.revisions[version]["filemap"] = filemap

        # Checks for files.
        addlist = []
        for name, file in files.iteritems():
            # We cannot include symbolic links in Mercurial repositories.
            if "symlink" in file:
                if not self.symlink_warned.get(name, False):
                    sys.stderr.write("{0}: warning: symbolic link\n"
                        .format(name))
                    self.symlink_warned[name] = True
            else:
                file_id = file.get("id")
                if file_id is None:
                    sys.exit("{0}: Missing file identity".format(name))

                parent_name = parent_filemap.get(file_id)
                if parent_name is not None and parent_name != name:
                    if self.verbose:
                        sys.stderr.write("{0}: renamed from {1}\n"
                            .format(name, parent_name))
                    self.hgclient.copy(parent_name, name, after = True)
                else:
                    addlist.append(name)

        if addlist:
            self.hgclient.add(addlist)

        # Sets the branch for the following commit.
        version = descriptor.version()
        branch = "default"
        if not re.match("[0-9]+$", version.major):
            branch = version.major
        self.hgclient.branch(branch, force = True)
        version = str(version)

        message = descriptor.message()
        if not message:
            message = "(empty commit message)"
        revision = self.hgclient.commit(message = message,
            date = self.revisions[version]["date"],
            user = self.revisions[version]["author"])

        self.revisionmap[version] = revision[1]
        # Keeps the revision identifier as a local tag for convenience.
        self.hgclient.tag([version], local = True, force = True)

    def _handlemerge(self, mergeparents):
        """Handle merges."""
        if len(mergeparents) > 1:
            sys.stderr.write("warning: multiple merge parents: {0}\n"
                .format(join(mergeparents, ", ")))
            sys.stderr.write("warning: picked {0} on record\n"
                .format(mergeparents[-1]))
        self.hgclient.merge(self.revisionmap[mergeparents[-1]],
            tool = "internal:local", cb = hglib.merge.handlers.noninteractive)

def _makefilemap(files):
    filemap = {}
    for name, file in files.iteritems():
        id = file.get("id")
        if id is not None:
            if filemap.get(id) is not None:
                sys.stderr.write(
                    "warning: Duplicate file identifier in a revision\n")
            filemap[id] = name
    return filemap

def convert(name, verbose = False):
    """convert revisions."""
    converter = Converter(name, verbose = verbose)
    converter.convert()
<file_sep>#
# prcslib/__init__.py - command interface package for PRCS
# Copyright (C) 2012-2015 <NAME>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
#

"""provide command line interface to PRCS
"""

import sys
import re
import os
from datetime import datetime
from email.utils import parsedate
from subprocess import Popen, PIPE
import prcslib.sexpdata as sexpdata

# Regular expression pattern for splitting versions.
_VERSION_RE = re.compile(r"^(.*)\.(\d+)$")

class PrcsError(Exception):
    """Base exception for this module."""
    pass

class PrcsCommandError(PrcsError):
    """Error from the PRCS command."""

    def __init__(self, error_message):
        """Construct a command error with an error message from PRCS."""
        super(PrcsCommandError, self).__init__(self)
        self.error_message = error_message

class PrcsVersion(object):
    """Version identifier of PRCS."""

    def __init__(self, major, minor=None):
        if minor is None:
            m = _VERSION_RE.match(major)
            major, minor = m.groups()

        self.major = major
        self.minor = int(minor)

    def __str__(self):
        return self.major + "." + str(self.minor)

class PrcsProject(object):

    def __init__(self, name):
        """construct a Project object."""
        self.name = name
        self.info_re = re.compile(
                "^([^ ]+) ([^ ]+) (.+) by ([^ ]+)( \*DELETED\*|)")

    def revisions(self):
        out, err = self._run_prcs(["info", "-f", self.name]);

        revisions = {}
        if (not err):
            # We use iteration over lines so that we can detect parse errors.
            for line in out.splitlines():
                m = self.info_re.search(line)
                if (m):
                    # The prcs info command always returns the local time.
                    date = parsedate(m.group(3))
                    revisions[m.group(2)] = {
                        "project": m.group(1),
                        "id": m.group(2),
                        "date": datetime(*date[0:6]),
                        "author": m.group(4),
                        "deleted": bool(m.group(5))
                    }
        else:
            raise PrcsCommandError(err)
        return revisions

    def descriptor(self, id = None):
        return PrcsDescriptor(self, id)

    def checkout(self, revision = None, files = []):
        args = ["checkout", "-fqu"]
        if not files:
            args.append("-P")
        if revision is not None:
            args.extend(["-r", revision])
        args.append(self.name)
        args.extend(files)
        out, err = self._run_prcs(args)
        if err:
            sys.stderr.write(err)

    def _run_prcs(self, args, input = None):
        """run a PRCS subprocess."""
        prcs = Popen(["prcs"] + args, stdin = PIPE, stdout = PIPE,
                stderr = PIPE)
        return prcs.communicate(input)

class PrcsDescriptor(object):

    def __init__(self, project, id = None):
        prj_name = project.name + ".prj"
        project.checkout(id, [prj_name])
        self.properties = _readdescriptor(prj_name)
        os.unlink(prj_name)

    def version(self):
        """Return the version in this descriptor."""
        v = self.properties["Project-Version"]
        return PrcsVersion(v[1].value(), v[2].value())

    def parentversion(self):
        """Return the major and minor parent versions."""
        v = self.properties["Parent-Version"]
        major = v[1].value()
        minor = v[2].value()
        if v[0].value() == "-*-" and major == "-*-" and minor == "-*-":
            return None
        return PrcsVersion(major, minor)

    def mergeparents(self):
        """Return the list of merge parents."""
        p = []
        for i in self.properties["Merge-Parents"]:
            if i[1].value() == "complete":
                p.append(i[0].value())
        return p

    def message(self):
        """Return the log message."""
        return self.properties["Version-Log"][0]

    def files(self):
        """Return the file information as a dictionary."""
        files = {}
        for i in self.properties["Files"]:
            name = i[0].value()
            symlink = False
            for j in i[2:]:
                if j.value() == ":symlink":
                    symlink = True
            if symlink:
                files[name] = {
                    "symlink": i[1][0].value(),
                }
            else:
                files[name] = {
                    "id": i[1][0].value(),
                    "revision": i[1][1].value(),
                    "mode": int(i[1][2].value(), 8),
                }
        return files

def _readdescriptor(name):
    with open(name, "r") as f:
        string = f.read()

    descriptor = {}
    # Encloses the project descriptor in a single list.
    for i in sexpdata.loads("(\n" + string + "\n)"):
        if isinstance(i, list) and isinstance(i[0], sexpdata.Symbol):
            descriptor[i[0].value()] = i[1:]
    return descriptor
