]> thyrsus.com Git - rcs-fast-import.git/commitdiff
Implement preservation of committer and author metadata.
authorEric S. Raymond <esr@thyrsus.com>
Thu, 11 Nov 2010 16:45:35 +0000 (11:45 -0500)
committerEric S. Raymond <esr@thyrsus.com>
Thu, 11 Nov 2010 16:45:35 +0000 (11:45 -0500)
rcs-fast-import
rcs-fast-import.xml

index 74154fbff414f96c352076d5ba5fbed880b16a03..642532c8e59fb8b7a3810081d2d17c9ec878688d 100755 (executable)
@@ -10,7 +10,7 @@
 #   * Branch structure!
 #
 """
-Usage: rcs-fast-import [-v] [-u] [-l] [-d] [-n] [-V] 
+Usage: rcs-fast-import [-v] [-p] [-u] [-l] [-d] [-n] [-V] 
 
 Break the content of a git-fast-import stream on stdin into individual RCS
 master files in RCS subdirectories of the corresponding directory structure.
@@ -18,6 +18,10 @@ master files in RCS subdirectories of the corresponding directory structure.
 With the -v option, emit a twirling-baton prompt during stream read, and
 more progress messages during RCS writes.
 
+Normally, committer and author data are written into each comment as
+RFC-822 mail headers, followed by a blank line.  The -p option suppresses
+this, producing a plain RCS comment.
+
 With the -l option, check out and lock all resulting files at their latest
 revisions; with the -u option, check out but do not lock.
 
@@ -35,17 +39,18 @@ combination with -v.
 R file operations rename corresponding RCS master files; C file operations
 copy them.
 
-If D, R, or C operations are present, the conversion will not round-trip
-properly (that is, rcs-fast-export | rcs-fast-import won't be an exact
-copy) with differences near deleted, renamed, and copied files.
+If D, R, or C operations are present, the conversion will not
+round-trip properly (that is, the file structure after rcs-fast-export
+| rcs-fast-import won't be an exact copy) with differences near
+deleted, renamed, and copied files.
 
 Warning: All tag metadata other than the name will be lost.  The date
 imputed to commits is from the author metadata if that exists, otherwise
-from the committer metadata. Committer and author information is lost.
+from the committer metadata.
 """
 
 import sys, os, getopt, re, cStringIO, subprocess, shutil
-import time, calendar, copy, shlex, email.utils
+import time, calendar, copy, shlex, email.message, email.utils
 
 version="0.1"
 
@@ -158,6 +163,10 @@ class Attribution:
                 self.date = Date(person.strip().split("|")[2])
             except (ValueError, IndexError):
                 raise FatalException("malformed attribution %s" % person)
+    def email_out(self, msg, hdr):
+        "Update an RC822 message object with a representation of this."
+        msg[hdr] = self.name + " <" + self.email + ">"
+        msg[hdr + "-Date"] = self.date.rfc822()
 
 class Blob:
     "Represent a detached blob of data referenced by a mark."
@@ -176,6 +185,16 @@ class Tag:
         self.committish = committish
         self.tagger = tagger
         self.comment = content
+    def email_out(self, eventnum):
+        "Enable do_mailbox_out() to report these."
+        msg = email.message.Message()
+        msg.set_unixfrom(None)
+        msg["Event-Number"] = str(eventnum+1)
+        msg["Tag-Name"] = self.name
+        if self.tagger:
+            self.tagger.email_out(msg, "Tagger")
+        msg.set_payload(self.comment)
+        return str(msg)
 
 class Reset:
     "Represents a branch creation."
@@ -215,6 +234,30 @@ class Commit:
         self.fileops = []            # blob and file operation list
         self.properties = {}         # commit properties (extension)
         self.taglist = []            # Tags that point at this commit
+    def email_out(self):
+        "Enable do_mailbox_out() to report these."
+        msg = email.message.Message()
+        msg.set_unixfrom(None)
+        if self.authors:
+            self.authors[0].email_out(msg, "Author")
+            for (i, coauthor) in enumerate(self.authors[1:]):
+                coauthor.email_out(msg, "Author" + `2+i`)
+        self.committer.email_out(msg, "Committer")
+        empty_properties = []
+        propkeys = self.properties.keys()
+        propkeys.sort()
+        for name in propkeys:
+            value = self.properties[name]
+            if value in (True, False):
+                if value:
+                    empty_properties.append(name)
+            else:
+                hdr = "-".join(map(lambda s: s.capitalize(), name.split("-")))
+                msg["Property-" + hdr] = value
+        if empty_properties:
+            msg["Empty-Properties"] = ",".join(empty_properties)
+        msg.set_payload(self.comment)
+        return str(msg)
 
 class Passthrough:
     "Represents a passthrough line."
@@ -491,42 +534,14 @@ def complain(msg):
 def announce(msg):
     print "rcs-fast-import:", msg
 
-class popen_or_die:
-    "Read or write from a subordinate process."
-    def __init__(self, cmd, legend="", mode="r"):
-        assert mode in ("r", "w")
-        self.cmd = cmd
-        self.legend = legend
-        self.mode = mode
-        if self.legend:
-            self.legend = " "  + self.legend
-    def __enter__(self):
-        if verbose >= DEBUG_COMMANDS:
-            if self.mode == "r":
-                announce("reading from '%s'%s" % (self.cmd, self.legend))
-            else:
-                announce("writing to '%s'%s" % (self.cmd, self.legend))
-        if dryrun:
-            self.fp = file("/dev/null", self.mode)
-            return self.fp
-        try:
-            self.fp = os.popen(self.cmd, self.mode)
-            return self.fp
-        except OSError, e:
-            raise FatalException("execution of %s%s failed: %s" \
-                                 % (self.cmd, self.legend, e))
-    def __exit__(self, type, value, traceback):
-        if self.fp.close() is not None:
-            raise FatalException("%s%s returned error." \
-                                 % (self.cmd, self.legend))
-        return False
-
 def do_or_die(cmd, legend=""):
     "Either execute a command or raise a fatal exception."
     if legend:
         legend = " "  + legend
     if verbose >= DEBUG_COMMANDS:
         announce("executing '%s'%s" % (cmd, legend))
+    if not verbose:
+        cmd += ">/dev/null 2>&1" 
     if dryrun:
         return
     try:
@@ -564,7 +579,7 @@ class RCS:
                 for name in files:
                     res.append(os.path.join(root, name))
         return res
-    def commit(self, event, committables):
+    def commit(self, event, committables, roundtrip):
         "Perform RCS file checkins and modifications,"
         for (path, mode, copyfile) in committables:
             if mode == "160000":
@@ -585,13 +600,17 @@ class RCS:
                     except OSError:
                         raise FatalException("refusing to step on %s." % path)
                 responsible = event.authors[0] or event.committer
-                command = "ci -q -l"
+                if roundtrip:
+                    comment = event.email_out()
+                else:
+                    comment = event.comment
+                command = "ci -l"
                 # If we were going to try to match committer or author
                 # email addresses to local names, this would be the place
                 # to do it.  Not a whole lot of point in trying, though.
                 #command += " -w%s" % responsible.email
                 command += " -d'%s'" % responsible.date.rfc822()
-                command += ' -m"%s"' % event.comment.replace('"', '\\"')
+                command += ' -m"%s"' % comment.replace('"', '\\"')
                 command += " " + path + " " + RCS.master(path)
                 do_or_die(command, "checkin")
                 if verbose >= DEBUG_SHUFFLE:
@@ -621,15 +640,18 @@ class RCS:
                 do_or_die("co -u %s" % (" ".join(RCS.rcsfiles()),))
 
 if __name__ == '__main__':
-    (options, arguments) = getopt.getopt(sys.argv[1:], "dlnuvV?")
+    (options, arguments) = getopt.getopt(sys.argv[1:], "dlnpuvV?")
     do_unlocked_checkouts = False
     checkout_mode = None
+    roundtrip = True
     for (switch, val) in options:
        if (switch == '-?'):
            print __doc__
             raise SystemExit, 0
         elif switch == '-d':
             do_deletes = True
+        elif switch == '-p':
+            roundtrip = False
         elif switch in ('-l', '-u'):
             checkout_mode = switch
         elif (switch == '-n'):
@@ -672,7 +694,7 @@ if __name__ == '__main__':
                             vcs.copy(ops.source, op.target)
                     else:
                         raise FatalException("unknown op type %s" % op.op)
-                vcs.commit(event, committables)
+                vcs.commit(event, committables, roundtrip)
             vcs.checkout(checkout_mode)
         raise SystemExit, 0
     except FatalException, e:
index 9bd47297349cac9d968b261fa2199f74ff3419d7..a9d5184188296f8347cef0a5b487b1834baaa6dd 100644 (file)
@@ -23,6 +23,7 @@
   <arg choice='opt'>-u</arg>
   <arg choice='opt'>-l</arg>
   <arg choice='opt'>-d</arg>
+  <arg choice='opt'>-p</arg>
   <arg choice='opt'>-n</arg>
   <arg choice='opt'>-V</arg>
 </cmdsynopsis>
@@ -38,6 +39,10 @@ corresponding directory structure.</para>
 will terminate with error rather than stepping on files that already
 exist in the current directory.</para>
 
+<para>Normally, committer and author data are written into each comment as
+RFC-822 mail headers, followed by a blank line.  The -p option suppresses
+this, producing a plain RCS comment.</para>
+
 <para>With the -v option, emit a twirling-baton prompt during stream read, and
 more progress messages during RCS writes.</para>
 
@@ -65,7 +70,7 @@ differences near deleted, renamed, and copied files.</para>
 
 <para>Warning: All tag metadata other than the name will be lost.  The date
 imputed to commits is from the author metadata if that exists, otherwise
-from the committer metadata. Committer and author information is lost.</para>
+from the committer metadata.</para>
 </refsect1>
 
 <refsect1 id='returns'><title>ERROR RETURNS</title>