-#!/usr/bin/ruby
-
-=begin
+#!/usr/bin/env python
+"""
TODO
- * Refactor commit coalescing
- * Add --strict-symbol-check to only coalesce commits if their symbol lists are equal
- * Further coalescing options? (e.g. small logfile differences)
- * Proper branching support in multi-file export
- * Optimize memory usage by discarding unneeded text
-=end
-
-require 'pp'
-require 'set'
-
-# Integer#odd? was introduced in Ruby 1.8.7, backport it to
-# older versions
-unless 2.respond_to? :odd?
- class Integer
- def odd?
- self % 2 == 1
- end
- end
-end
+* Refactor commit coalescing
+* Add --strict-symbol-check to only coalesce commits if their symbol lists are equal
+* Further coalescing options? (e.g. small logfile differences)
+* Proper branching support in multi-file export
+* Optimize memory usage by discarding unneeded text
+"""
-def usage
- STDERR.puts <<EOM
-#{$0} [options] file [file ...]
+import sys
+import pprint # Functional equivalent of Ruby pp?
+
+def usage():
+
+ print >>sys.stderr, """\
+rcs-fast-export [options] file [file ...]
Fast-export the RCS history of one or more files. If a directory is specified,
all RCS-tracked files in the directory and its descendants are exported.
rcs.commitFuzz for --rcs-commit-fuzz
rcs.symbolCheck for --rcs-symbol-check
rcs.tagFuzz for --rcs-tag-fuzz
-
-EOM
-end
-
-def not_found(arg)
- STDERR.puts "Could not find #{arg}"
-end
-
-# returns a hash that maps usernames to author names & emails
-def load_authors_file(fn)
- hash = {}
- begin
- File.open(File.expand_path(fn)) do |io|
- io.each_line do |line|
- uname, author = line.split('=', 2)
- uname.strip!
- author.strip!
- STDERR.puts "Username #{uname} redefined to #{author}" if hash.has_key? uname
- hash[uname] = author
- end
- end
- rescue
- not_found(fn)
- end
- return hash
-end
-
-class Time
- def Time.rcs(string)
- fields = string.split('.')
- raise ArgumentError, "wrong number of fields for RCS date #{string}" unless fields.length == 6
- Time.utc(*fields)
- end
-end
-
-module RCS
- # strip an optional final ;
- def RCS.clean(arg)
- arg.chomp(';')
- end
-
- # strip the first and last @, and de-double @@s
- def RCS.sanitize(arg)
- case arg
- when Array
- ret = arg.dup
- raise 'malformed first line' unless ret.first[0,1] == '@'
- raise 'malformed last line' unless ret.last[-1,1] == '@'
- ret.first.sub!(/^@/,'')
- ret.last.sub!(/@$/,'')
- ret.map { |l| l.gsub('@@','@') }
- when String
- arg.chomp('@').sub(/^@/,'').gsub('@@','@')
- else
- raise
- end
- end
-
- # clean and sanitize
- def RCS.at_clean(arg)
- RCS.sanitize RCS.clean(arg)
- end
-
- def RCS.mark(key)
- @@marks ||= {}
- if @@marks.key? key
- @@marks[key]
- else
- @@marks[key] = @@marks.length + 1
- end
- end
-
- def RCS.blob(file, rev)
- RCS.mark([file, rev])
- end
-
- def RCS.commit(commit)
- RCS.mark(commit)
- end
-
- class File
- attr_accessor :head, :comment, :desc, :revision, :fname, :mode
- def initialize(fname, executable)
- @fname = fname.dup
- @head = nil
- @comment = nil
- @desc = []
- @revision = Hash.new { |h, r| h[r] = Revision.new(self, r) }
- @mode = executable ? '755' : '644'
- end
-
- def has_revision?(rev)
- @revision.has_key?(rev) and not @revision[rev].author.nil?
- end
-
- def export_commits(opts={})
- counter = 0
- exported = []
- until @revision.empty?
- counter += 1
-
- # a string sort is a very good candidate for
- # export order, getting a miss only for
- # multi-digit revision components
- keys = @revision.keys.sort
-
- STDERR.puts "commit export loop ##{counter}"
- STDERR.puts "\t#{exported.length} commits exported so far: #{exported.join(', ')}" unless exported.empty?
- STDERR.puts "\t#{keys.size} to export: #{keys.join(', ')}"
-
- keys.each do |key|
- rev = @revision[key]
- # the parent commit is rev.next if we're on the
- # master branch (rev.branch is nil) or
- # rev.diff_base otherwise
- from = rev.branch.nil? ? rev.next : rev.diff_base
- # A commit can only be exported if it has no
- # parent, or if the parent has been exported
- # already. Skip this commit otherwise
- if from and not exported.include? from
- next
- end
-
- branch = rev.branch || 'master'
- author = opts[:authors][rev.author] || "#{rev.author} <empty>"
- date = "#{rev.date.tv_sec} +0000"
- log = String.new
- if opts[:log_filename]
- log << @fname << ": "
- end
- log << rev.log.to_s
-
- puts "commit refs/heads/#{branch}"
- puts "mark :#{RCS.commit key}"
- puts "committer #{author} #{date}"
- puts "data #{log.length}"
- puts log unless log.empty?
- puts "from :#{RCS.commit from}" if rev.branch_point
- puts "M #{@mode} :#{RCS.blob @fname, key} #{@fname}"
-
- # TODO FIXME this *should* be safe, in
- # that it should not unduly move
- # branches back in time, but I'm not
- # 100% sure ...
- rev.branches.each do |sym|
- puts "reset refs/heads/#{sym}"
- puts "from :#{RCS.commit key}"
- end
- rev.symbols.each do |sym|
- puts "reset refs/tags/#{sym}"
- puts "from :#{RCS.commit key}"
- end
- if opts[:tag_each_rev]
- puts "reset refs/tags/#{key}"
- puts "from :#{RCS.commit key}"
- end
-
- exported.push key
- end
- exported.each { |k| @revision.delete(k) }
- end
- end
- end
+"""
+
+def not_found(arg):
+ print >>sys.stderr,"Could not find %s" % arg
+
+def load_authors_file(fn):
+ "Returns a dict that maps usernames to author names & emails."
+ authdict = {}
+ try:
+ for line in open(fn):
+ (uname, author) = map(lambda x: x.strip(), line.split("-"))
+ if uname in hashdict:
+ print >>sys.stderr, "Username %(uname)s redefined to %(author)s" % locals()
+ authdict[uname] = author
+ return authdict
+ except OSError:
+ not_found(fn)
+
+class FatalException:
+ "Unrecoverable error."
+ def __init__(self, msg):
+ self.msg = msg
+
+class RCS:
+ @staticmethod
+ def clean(arg):
+ # strip an optional final ;
+ if arg.endswith(";"):
+ arg = arg[:-1]
+ return arg
+
+ @staticmethod
+ def sanitize(arg):
+ # strip the first and last @, and de-double @@s
+ if type(arg) == type([]):
+ if not arg[0].startswith("@@"):
+ raise FatalException('malformed first line')
+ if not (arg[-1][1] == "@" and arg[-1][-1] == "@"):
+ raise FatalException('malformed last line')
+ arg[0] = arg[0][1:]
+ arg[-1] = arg[-1][:-1]
+ arg = map(lambda s: s.replace("@@", "@"), arg)
+ return arg
+ elif type(arg) == type(""):
+ if arg.endswith("@"):
+ arg = arg[:-1]
+ if arg.startswith("@"):
+ arg = arg[1:]
+ arg = arg.replace("@@", "@")
+ return arg
+ else:
+ raise FatalException("unexpected argument type in sanitize")
+
+ @staticmethod
+ def at_clean(arg):
+ # clean and sanitize
+ return RCS.sanitize(RCS.clean(arg))
+
+ def RCS.mark(key)
+ @@marks ||= {}
+ if @@marks.key? key
+ @@marks[key]
+ else
+ @@marks[key] = @@marks.length + 1
+ end
+ end
+
+ def RCS.blob(file, rev)
+ RCS.mark([file, rev])
+ end
+
+ def RCS.commit(commit)
+ RCS.mark(commit)
+ end
+
+ class File
+ attr_accessor :head, :comment, :desc, :revision, :fname, :mode
+ def initialize(fname, executable)
+ @fname = fname.dup
+ @head = nil
+ @comment = nil
+ @desc = []
+ @revision = Hash.new { |h, r| h[r] = Revision.new(self, r) }
+ @mode = executable ? '755' : '644'
+ end
+
+ def has_revision?(rev)
+ @revision.has_key?(rev) and not @revision[rev].author.nil?
+ end
+
+ def export_commits(opts={})
+ counter = 0
+ exported = []
+ until @revision.empty?
+ counter += 1
+
+ # a string sort is a very good candidate for
+ # export order, getting a miss only for
+ # multi-digit revision components
+ keys = @revision.keys.sort
+
+ STDERR.puts "commit export loop ##{counter}"
+ STDERR.puts "\t#{exported.length} commits exported so far: #{exported.join(', ')}" unless exported.empty?
+ STDERR.puts "\t#{keys.size} to export: #{keys.join(', ')}"
+
+ keys.each do |key|
+ rev = @revision[key]
+ # the parent commit is rev.next if we're on the
+ # master branch (rev.branch is nil) or
+ # rev.diff_base otherwise
+ from = rev.branch.nil? ? rev.next : rev.diff_base
+ # A commit can only be exported if it has no
+ # parent, or if the parent has been exported
+ # already. Skip this commit otherwise
+ if from and not exported.include? from
+ next
+ end
+
+ branch = rev.branch || 'master'
+ author = opts[:authors][rev.author] || "#{rev.author} <empty>"
+ date = "#{rev.date.tv_sec} +0000"
+ log = String.new
+ if opts[:log_filename]
+ log << @fname << ": "
+ end
+ log << rev.log.to_s
+
+ puts "commit refs/heads/#{branch}"
+ puts "mark :#{RCS.commit key}"
+ puts "committer #{author} #{date}"
+ puts "data #{log.length}"
+ puts log unless log.empty?
+ puts "from :#{RCS.commit from}" if rev.branch_point
+ puts "M #{@mode} :#{RCS.blob @fname, key} #{@fname}"
+
+ # TODO FIXME this *should* be safe, in
+ # that it should not unduly move
+ # branches back in time, but I'm not
+ # 100% sure ...
+ rev.branches.each do |sym|
+ puts "reset refs/heads/#{sym}"
+ puts "from :#{RCS.commit key}"
+ end
+ rev.symbols.each do |sym|
+ puts "reset refs/tags/#{sym}"
+ puts "from :#{RCS.commit key}"
+ end
+ if opts[:tag_each_rev]
+ puts "reset refs/tags/#{key}"
+ puts "from :#{RCS.commit key}"
+ end
+
+ exported.push key
+ end
+ exported.each { |k| @revision.delete(k) }
+ end
+ end
+ end
class Revision
attr_accessor :rev, :author, :state, :next
end
exit status
+
+
+