4 RCS fast export: run the script with the `--help` option for further
7 No installation needed: you can run it from anywhere, including the git
8 checkout directory. For extra comfort, symlink it to some directory in
9 your PATH. I myself have this symlink:
11 ~/bin/rcs-fast-export -> ~/src/rcs-fast-export/rcs-fast-export.rb
13 allowing me to run `rcs-fast-export` from anywhere.
18 * Refactor commit coalescing
19 * Add --strict-symbol-check to only coalesce commits if their symbol lists are equal
20 * Add support for commitid for coalescing commits
21 * Further coalescing options? (e.g. small logfile differences)
22 * Proper branching support in multi-file export
23 * Optimize memory usage by discarding unneeded text
24 * Provide an option that marks a file as deleted based on symbolic revisions
30 class NoBranchSupport < NotImplementedError ; end
32 # Integer#odd? was introduced in Ruby 1.8.7, backport it to
34 unless 2.respond_to? :odd?
42 # Set standard output to binary mode: git fast-import doesn't like Windows
43 # line-endings, and this ensures that the line termination will be a simple 0x0a
44 # on Windows too (it expands to 0x0D 0x0A otherwise).
48 RCS fast-export version: set to `git` in the repository, but can be overridden
49 by packagers, e.g. based on the latest tag, git description, custom packager
52 When the version is set to `git`, we make a little effort to find more information
53 about which commit we are at.
59 if RFE_VERSION == "git"
60 nolinkfile = File.readlink(__FILE__) rescue __FILE__
61 Dir.chdir File.expand_path File.dirname nolinkfile
63 if File.exists? '.git' ; begin
64 git_out = `git log -1 --pretty="%h %H%n%ai" | git name-rev --stdin`.split("\n")
65 hash=git_out.first.split.first
66 branch=git_out.first.split('(').last.chomp(')')
67 date=git_out.last.split.first
68 changed=`git diff --no-ext-diff --quiet --exit-code`
69 branch << "*" unless $?.success?
70 info=" [#{branch}] #{hash} (#{date})"
75 STDERR.puts "#{$0}: RCS fast-export, #{RFE_VERSION} version#{info}"
77 STDERR.puts "#{$0}: RCS fast-export, version #{RFE_VERSION}"
84 #{$0} [options] file [file ...]
86 Fast-export the RCS history of one or more files. If a directory is specified,
87 all RCS-tracked files in the directory and its descendants are exported.
89 When importing single files, their pathname is discarded during import. When
90 importing directories, only the specified directory component is discarded.
92 When importing a single file, RCS commits are converted one by one. Otherwise,
93 some heuristics is used to determine how to coalesce commits touching different
96 Currently, commits are coalesced if they share the exact same log and if their
97 date differs by no more than the user-specified fuzziness. Additionally, the
98 symbols in one of the commit must be a subset of the symbols in the other
99 commit, unless --no-symbol-check is specified or rcs.symbolCheck is set to
100 false in the git configuration.
103 git init && rcs-fast-export.rb . | git fast-import && git reset
106 --help, -h, -? display this help text
107 --authors-file, -A specify a file containing username = Full Name <email> mappings
108 --[no-]author-is-committer
109 use the author name and date as committer identity
110 --ignore ignore the specified files (shell pattern)
111 --log-encoding specify the encoding of log messages, for transcoding to UTF-8
112 --rcs-commit-fuzz fuzziness in RCS commits to be considered a single one when
113 importing multiple files
114 (in seconds, defaults to 300, i.e. 5 minutes)
115 --[no-]warn-missing-authors
116 [do not] warn about usernames missing from the map file
117 --[no-]symbol-check [do not] check symbols when coalescing commits
118 --[no-]tag-each-rev [do not] create a lightweight tag for each RCS revision when
119 importing a single file
120 --[no-]log-filename [do not] prepend the filename to the commit log when importing
122 --skip-branches when exporting multiple files with a branched history, export
123 the main branch only instead of aborting due to the lack of
124 support for branched multi-file history export
129 rcs.authorsFile for --authors-file
130 rcs.authorIsCommitter for --author-is-committer
131 rcs.tagEachRev for --tag-each-rev
132 rcs.logFilename for --log-filename
133 rcs.commitFuzz for --rcs-commit-fuzz
134 rcs.warnMissingAuthors for --warn-missing-authors
135 rcs.symbolCheck for --rcs-symbol-check
136 rcs.tagFuzz for --rcs-tag-fuzz
147 warning "Could not find #{arg}"
150 def emit_committer(opts, author, date)
151 if opts[:author_is_committer]
152 committer = "#{author} #{date}"
154 committer = `git var GIT_COMMITTER_IDENT`.chomp
156 puts "committer #{committer}"
159 # returns a hash that maps usernames to author names & emails
160 def load_authors_file(fn)
163 File.open(File.expand_path(fn)) do |io|
164 io.each_line do |line|
165 uname, author = line.split('=', 2)
168 warning "Username #{uname} redefined to #{author}" if hash.has_key? uname
178 def username_to_author(name, opts)
180 raise "no authors map defined" unless map and Hash === map
182 # if name is not found in map, provide a default one, optionally giving a warning (once)
184 warning "no author found for #{name}" if opts[:warn_missing_authors]
185 map[name] = "#{name} <empty>"
190 # display a message about a (recoverable) error
191 def alert(msg, action)
192 STDERR.puts "ERROR:\t#{msg}"
193 STDERR.puts "\t#{action}"
198 fields = string.split('.')
199 raise ArgumentError, "wrong number of fields for RCS date #{string}" unless fields.length == 6
200 # in Ruby 1.9, '99' is interpreted as year 99, not year 1999
201 if fields.first.length < 3
202 fields.first.insert 0, '19'
209 # strip an optional final ;
214 # strip the first and last @, and de-double @@s
215 def RCS.sanitize(arg)
219 raise 'malformed first line' unless ret.first[0,1] == '@'
220 raise 'malformed last line' unless ret.last[-1,1] == '@'
221 ret.first.sub!(/^@/,'')
222 ret.last.sub!(/@$/,'')
223 ret.map { |l| l.gsub('@@','@') }
225 arg.chomp('@').sub(/^@/,'').gsub('@@','@')
232 def RCS.at_clean(arg)
233 RCS.sanitize RCS.clean(arg)
241 @@marks[key] = @@marks.length + 1
245 def RCS.blob(file, rev)
246 RCS.mark([file, rev])
249 def RCS.commit(commit)
254 attr_accessor :head, :comment, :desc, :revision, :fname, :mode
255 def initialize(fname, executable)
260 @revision = Hash.new { |h, r| h[r] = Revision.new(self, r) }
261 @mode = executable ? '755' : '644'
264 def has_revision?(rev)
265 @revision.has_key?(rev) and not @revision[rev].author.nil?
268 def export_commits(opts={})
271 log_enc = opts[:log_encoding]
272 until @revision.empty?
275 # a string sort is a very good candidate for
276 # export order, getting a miss only for
277 # multi-digit revision components
278 keys = @revision.keys.sort
280 warning "commit export loop ##{counter}"
281 warning "\t#{exported.length} commits exported so far: #{exported.join(', ')}" unless exported.empty?
282 warning "\t#{keys.size} to export: #{keys.join(', ')}"
286 # the parent commit is rev.next if we're on the
287 # master branch (rev.branch is nil) or
288 # rev.diff_base otherwise
289 from = rev.branch.nil? ? rev.next : rev.diff_base
290 # A commit can only be exported if it has no
291 # parent, or if the parent has been exported
292 # already. Skip this commit otherwise
293 if from and not exported.include? from
297 branch = rev.branch || 'master'
298 author = username_to_author(rev.author, opts)
299 date = "#{rev.date.tv_sec} +0000"
301 if opts[:log_filename]
302 log << @fname << ": "
305 # git fast-import expects logs to be in UTF-8, so if a different log encoding
306 # is specified for the log we transcode from whatever was specified to UTF-8.
307 # we then mark the string as ASCII-8BIT (as everything else) so that string
308 # lengths are computed in bytes
309 log << rev.log.join.encode('UTF-8', log_enc).force_encoding('ASCII-8BIT')
314 puts "commit refs/heads/#{branch}"
315 puts "mark :#{RCS.commit key}"
316 puts "author #{author} #{date}"
317 emit_committer(opts, author, date)
318 puts "data #{log.length}"
319 puts log unless log.empty?
320 puts "from :#{RCS.commit from}" if from
321 puts "M #{@mode} :#{RCS.blob @fname, key} #{@fname}"
323 # TODO FIXME this *should* be safe, in
324 # that it should not unduly move
325 # branches back in time, but I'm not
327 rev.branches.each do |sym|
328 puts "reset refs/heads/#{sym}"
329 puts "from :#{RCS.commit key}"
331 rev.symbols.each do |sym|
332 puts "reset refs/tags/#{sym}"
333 puts "from :#{RCS.commit key}"
335 if opts[:tag_each_rev]
336 puts "reset refs/tags/#{key}"
337 puts "from :#{RCS.commit key}"
342 exported.each { |k| @revision.delete(k) }
348 attr_accessor :rev, :author, :state, :next
349 attr_accessor :branches, :log, :text, :symbols
350 attr_accessor :branch, :diff_base, :branch_point
352 def initialize(file, rev)
369 @date = Time.rcs(str)
374 ret = "blob\nmark :#{RCS.blob @file.fname, @rev}\ndata #{str.length}\n#{str}\n"
379 # TODO: what if a revision does not end with newline?
380 # TODO this should be done internally, not piping out to RCS
381 def RCS.expand_keywords(rcsfile, revision)
382 ret = ::File.read("|co -q -p#{revision} #{rcsfile}")
384 ret.each_line do |line|
390 def RCS.parse(fname, rcsfile, opts={})
391 rcs = RCS::File.new(fname, ::File.executable?(rcsfile))
393 ::File.open(rcsfile, 'r:ASCII-8BIT') do |file|
398 file.each_line do |line|
401 command, args = line.split($;,2)
402 next if command.empty?
404 if command.chomp!(';')
405 warning "Skipping empty command #{command.inspect}" if $DEBUG
411 rcs.head = RCS.clean(args.chomp)
417 rcs.comment = RCS.at_clean(args.chomp)
420 if rcs.has_revision?(rev)
421 status.push :revision_data
423 status.push :new_revision
428 status.push :read_lines
429 when 'branch', 'access', 'locks', 'expand'
430 warning "Skipping unhandled command #{command.inspect}" if $DEBUG
431 status.push :skipping_lines
435 raise "Unknown command #{command.inspect}"
438 status.pop if line.strip.chomp!(';')
440 # we can have multiple symbols per line
441 pairs = line.strip.split($;)
443 sym, rev = pair.strip.split(':',2);
445 status.pop if rev.chomp!(';')
446 rcs.revision[rev].symbols << sym
452 rcs.desc.replace lines.dup
455 # we sanitize lines as we read them
457 actual_line = line.dup
459 # the first line must begin with a @, which we strip
461 ats = line.match(/^@+/)
462 raise 'malformed line' unless ats
463 actual_line.replace line.sub(/^@/,'')
466 # if the line ends with an ODD number of @, it's the
467 # last line -- we work on actual_line so that content
468 # such as @\n or @ work correctly (they would be
469 # encoded respectively as ['@@@\n','@\n'] and
471 ats = actual_line.chomp.match(/@+$/)
472 if nomore = (ats && Regexp.last_match(0).length.odd?)
473 actual_line.replace actual_line.chomp.sub(/@$/,'')
475 lines << actual_line.gsub('@@','@')
482 when /^date\s+(\S+);\s+author\s+(\S+);\s+state\s+(\S+);$/
483 rcs.revision[rev].date = $1
484 rcs.revision[rev].author = $2
485 rcs.revision[rev].state = $3
488 when /^branches(?:\s+|$)/
489 status.push :branches
491 line = line.sub(/^branches\s+/,'')
494 when /^next\s+(\S+)?;$/
495 nxt = rcs.revision[rev].next = $1
497 raise "multiple diff_bases for #{nxt}" unless rcs.revision[nxt].diff_base.nil?
498 rcs.revision[nxt].diff_base = rev
499 rcs.revision[nxt].branch = rcs.revision[rev].branch
504 candidate = line.split(';',2)
505 candidate.first.strip.split.each do |branch|
506 raise "multiple diff_bases for #{branch}" unless rcs.revision[branch].diff_base.nil?
507 rcs.revision[branch].diff_base = rev
508 # we drop the last number from the branch name
509 rcs.revision[branch].branch = branch.sub(/\.\d+$/,'.x')
510 rcs.revision[branch].branch_point = rev
512 status.pop if candidate.length > 1
518 status.push :read_lines
526 status.push :read_lines
531 rcs.revision[rev].log.replace lines.dup
534 if opts[:expand_keywords]
535 rcs.revision[rev].text.replace RCS.expand_keywords(rcsfile, rev)
537 rcs.revision[rev].text.replace lines.dup
539 puts rcs.revision[rev].blob
542 if opts[:expand_keywords]
543 rcs.revision[rev].text.replace RCS.expand_keywords(rcsfile, rev)
545 difflines.replace lines.dup
546 difflines.pop if difflines.last.empty?
547 if difflines.first.chomp.empty?
548 alert "malformed diff: empty initial line @ #{rcsfile}:#{file.lineno-difflines.length-1}", "skipping"
550 end unless difflines.empty?
551 base = rcs.revision[rev].diff_base
552 unless rcs.revision[base].text
555 raise 'no diff base!'
559 rcs.revision[base].text.each { |l| buffer << [l.dup] }
565 while l = difflines.shift
567 raise 'negative index during insertion' if index < 0
568 raise 'negative count during insertion' if count < 0
571 # collected all the lines, put the before
576 buffer[index].unshift(*adding)
583 raise "malformed diff @ #{rcsfile}:#{file.lineno-difflines.length-1} `#{l}`" unless l =~ /^([ad])(\d+) (\d+)$/
589 # for deletion, index 1 is the first index, so the Ruby
590 # index is one less than the diff one
592 # we replace them with empty string so that 'a' commands
593 # referring to the same line work properly
600 # addition will prepend the appropriate lines
601 # to the given index, and in this case Ruby
602 # and diff indices are the same
607 # turn the buffer into an array of lines, deleting the empty ones
608 buffer.delete_if { |l| l.empty? }
611 rcs.revision[rev].text = buffer
613 puts rcs.revision[rev].blob
616 raise "Unknown status #{status.last}"
621 # clean up the symbols/branches: look for revisions that have
622 # one or more symbols but no dates, and make them into
623 # branches, pointing to the highest commit with that key
625 keys = rcs.revision.keys
626 rcs.revision.each do |key, rev|
627 if rev.date.nil? and not rev.symbols.empty?
628 top = keys.select { |k| k.match(/^#{key}\./) }.sort.last
629 tr = rcs.revision[top]
630 raise "unhandled complex branch structure met: #{rev.inspect} refers #{tr.inspect}" if tr.date.nil?
631 tr.branches |= rev.symbols
635 branches.each { |k| rcs.revision.delete k }
641 def initialize(commit)
647 testfiles = @files.dup
648 tree.each { |rcs, rev| self.add(rcs, rev, testfiles) }
649 # the next line is only reached if all the adds were
650 # successful, so the merge is atomic
651 @files.replace testfiles
654 def add(rcs, rev, file_list=@files)
655 if file_list.key? rcs
656 prev = file_list[rcs]
657 if prev.log == rev.log
658 str = "re-adding existing file #{rcs.fname} (old: #{prev.rev}, new: #{rev.rev})"
660 str = "re-adding existing file #{rcs.fname} (old: #{[prev.rev, prev.log.to_s].inspect}, new: #{[rev.rev, rev.log.to_s].inspect})"
662 if prev.text != rev.text
665 @commit.warn_about str
677 @files.map do |rcs, rev|
678 if rev.state.downcase == "dead"
679 files << "D #{rcs.fname}"
681 files << "M #{rcs.mode} :#{RCS.blob rcs.fname, rev.rev} #{rcs.fname}"
688 @files.map { |rcs, rev| rcs.fname }
697 attr_accessor :date, :log, :symbols, :author, :branch
699 attr_accessor :min_date, :max_date
700 def initialize(rcs, rev)
701 raise NoBranchSupport if rev.branch
702 self.date = rev.date.dup
703 self.min_date = self.max_date = self.date
704 self.log = rev.log.dup
705 self.symbols = rev.symbols.dup
706 self.author = rev.author
707 self.branch = rev.branch
709 self.tree = Tree.new self
710 self.tree.add rcs, rev
714 [self.min_date, self.date, self.max_date, self.branch, self.symbols, self.author, self.log, self.tree.to_a]
718 warn str + " for commit on #{self.date}"
721 # Sort by date and then by number of symbols
723 ds = self.date <=> other.date
727 return self.symbols.length <=> other.symbols.length
732 self.tree.merge! commit.tree
733 if commit.max_date > self.max_date
734 self.max_date = commit.max_date
736 if commit.min_date < self.min_date
737 self.min_date = commit.min_date
739 self.symbols.merge commit.symbols
743 xbranch = self.branch || 'master'
744 xauthor = username_to_author(self.author, opts)
746 numdate = self.date.tv_sec
747 xdate = "#{numdate} +0000"
750 puts "commit refs/heads/#{xbranch}"
751 puts "mark :#{RCS.commit key}"
752 puts "author #{xauthor} #{xdate}"
753 emit_committer(opts, xauthor, xdate)
754 puts "data #{xlog.length}"
755 puts xlog unless xlog.empty?
756 # TODO branching support for multi-file export
757 # puts "from :#{RCS.commit from}" if self.branch_point
760 # TODO branching support for multi-file export
761 # rev.branches.each do |sym|
762 # puts "reset refs/heads/#{sym}"
763 # puts "from :#{RCS.commit key}"
766 self.symbols.each do |sym|
767 puts "reset refs/tags/#{sym}"
768 puts "from :#{RCS.commit key}"
777 opts = GetoptLong.new(
778 # Authors file, like git-svn and git-cvsimport, more than one can be
780 ['--authors-file', '-A', GetoptLong::REQUIRED_ARGUMENT],
781 # Use author identity as committer identity?
782 ['--author-is-committer', GetoptLong::NO_ARGUMENT],
783 ['--no-author-is-committer', GetoptLong::NO_ARGUMENT],
784 # Use "co" to obtain the actual revision with keywords expanded.
785 ['--expand-keywords', GetoptLong::NO_ARGUMENT],
786 # RCS file suffix, like RCS
787 ['--rcs-suffixes', '-x', GetoptLong::REQUIRED_ARGUMENT],
788 # Shell pattern to identify files to be ignored
789 ['--ignore', GetoptLong::REQUIRED_ARGUMENT],
790 # Encoding of log messages in the RCS files
791 ['--log-encoding', GetoptLong::REQUIRED_ARGUMENT],
792 # Date fuzziness for commits to be considered the same (in seconds)
793 ['--rcs-commit-fuzz', GetoptLong::REQUIRED_ARGUMENT],
794 # warn about usernames missing in authors file map?
795 ['--warn-missing-authors', GetoptLong::NO_ARGUMENT],
796 ['--no-warn-missing-authors', GetoptLong::NO_ARGUMENT],
797 # check symbols when coalescing?
798 ['--symbol-check', GetoptLong::NO_ARGUMENT],
799 ['--no-symbol-check', GetoptLong::NO_ARGUMENT],
801 ['--tag-each-rev', GetoptLong::NO_ARGUMENT],
802 ['--no-tag-each-rev', GetoptLong::NO_ARGUMENT],
803 # prepend filenames to commit logs?
804 ['--log-filename', GetoptLong::NO_ARGUMENT],
805 ['--no-log-filename', GetoptLong::NO_ARGUMENT],
806 # skip branches when exporting a whole tree?
807 ['--skip-branches', GetoptLong::NO_ARGUMENT],
808 # show current version
809 ['--version', '-v', GetoptLong::NO_ARGUMENT],
811 ['--help', '-h', '-?', GetoptLong::NO_ARGUMENT]
814 # We read options in order, but they apply to all passed parameters.
815 # TODO maybe they should only apply to the following, unless there's only one
817 opts.ordering = GetoptLong::RETURN_IN_ORDER
821 :authors => Hash.new,
822 :ignore => Array.new,
827 # Read config options
828 `git config --get-all rcs.authorsfile`.each_line do |fn|
829 parse_options[:authors].merge! load_authors_file(fn.chomp)
832 parse_options[:author_is_committer] = (
833 `git config --bool rcs.authoriscommitter`.chomp == 'false'
836 parse_options[:tag_each_rev] = (
837 `git config --bool rcs.tageachrev`.chomp == 'true'
840 parse_options[:log_filename] = (
841 `git config --bool rcs.logfilename`.chomp == 'true'
844 fuzz = `git config --int rcs.commitFuzz`.chomp
845 parse_options[:commit_fuzz] = fuzz.to_i unless fuzz.empty?
847 fuzz = `git config --int rcs.tagFuzz`.chomp
848 parse_options[:tag_fuzz] = fuzz.to_i unless fuzz.empty?
850 parse_options[:symbol_check] = (
851 `git config --bool rcs.symbolcheck`.chomp == 'false'
854 parse_options[:warn_missing_authors] = (
855 `git config --bool rcs.warnmissingauthors`.chomp == 'false'
858 opts.each do |opt, arg|
860 when '--authors-file'
861 authors = load_authors_file(arg)
862 redef = parse_options[:authors].keys & authors.keys
863 warning "Authors file #{arg} redefines #{redef.join(', ')}" unless redef.empty?
864 parse_options[:authors].merge!(authors)
865 when '--author-is-committer'
866 parse_options[:author_is_committer] = true
867 when '--no-author-is-committer'
868 parse_options[:author_is_committer] = false
869 when '--expand-keywords'
870 parse_options[:expand_keywords] = true
871 when '--rcs-suffixes'
874 parse_options[:ignore] << arg
875 when '--log-encoding'
876 parse_options[:log_encoding] = Encoding.find(arg)
877 when '--rcs-commit-fuzz'
878 parse_options[:commit_fuzz] = arg.to_i
879 when '--rcs-tag-fuzz'
880 parse_options[:tag_fuzz] = arg.to_i
881 when '--symbol-check'
882 parse_options[:symbol_check] = true
883 when '--no-symbol-check'
884 parse_options[:symbol_check] = false
885 when '--tag-each-rev'
886 parse_options[:tag_each_rev] = true
887 when '--no-tag-each-rev'
888 # this is the default, which is fine since the missing key
889 # (default) returns nil which is false in Ruby
890 parse_options[:tag_each_rev] = false
891 when '--log-filename'
892 parse_options[:log_filename] = true
893 when '--no-log-filename'
894 # this is the default, which is fine since the missing key
895 # (default) returns nil which is false in Ruby
896 parse_options[:log_filename] = false
897 when '--skip-branches'
898 parse_options[:skip_branches] = true
910 if parse_options[:tag_fuzz] < parse_options[:commit_fuzz]
911 parse_options[:tag_fuzz] = parse_options[:commit_fuzz]
916 user = Etc.getlogin || ENV['USER']
918 # steal username/email data from other init files that may contain the
922 # the user's .hgrc file for a username field
923 ['~/.hgrc', /^\s*username\s*=\s*(["'])?(.*)\1$/, 2],
924 # the user's .(g)vimrc for a changelog_username setting
925 ['~/.vimrc', /changelog_username\s*=\s*(["'])?(.*)\1$/, 2],
926 ['~/.gvimrc', /changelog_username\s*=\s*(["'])?(.*)\1$/, 2],
927 ].each do |fn, rx, idx|
928 file = File.expand_path fn
929 if File.readable?(file) and File.read(file) =~ rx
930 parse_options[:authors][user] = Regexp.last_match(idx).strip
936 if user and not user.empty? and not parse_options[:authors].has_key?(user)
937 name = ENV['GIT_AUTHOR_NAME'] || ''
938 name.replace(`git config user.name`.chomp) if name.empty?
939 name.replace(Etc.getpwnam(user).gecos) if name.empty?
942 # couldn't find a name, try to steal data from other sources
945 # if we found a name, try to find an email too
946 email = ENV['GIT_AUTHOR_EMAIL'] || ''
947 email.replace(`git config user.email`.chomp) if email.empty?
950 # couldn't find an email, try to steal data too
953 # we got both a name and email, fill the info
954 parse_options[:authors][user] = "#{name} <#{email}>"
969 file_list.each do |arg|
970 case ftype = File.ftype(arg)
976 not_found "RCS file #{arg}"
979 filename = File.basename(arg, SFX)
981 filename = File.basename(arg)
982 path = File.dirname(arg)
983 rcsfile = File.join(path, 'RCS', filename) + SFX
984 unless File.exists? rcsfile
985 rcsfile.replace File.join(path, filename) + SFX
986 unless File.exists? rcsfile
987 not_found "RCS file for #{filename} in #{path}"
991 rcs << RCS.parse(filename, rcsfile, parse_options)
993 argdirname = arg.chomp(File::SEPARATOR)
994 pattern = File.join(argdirname, '**', '*' + SFX)
995 Dir.glob(pattern, File::FNM_DOTMATCH).each do |rcsfile|
996 filename = File.basename(rcsfile, SFX)
997 path = File.dirname(rcsfile)
998 # strip trailing "/RCS" if present, or "RCS" if that's
1000 path.sub!(/(^|#{File::SEPARATOR})RCS$/, '')
1001 # strip off the portion of the path specified
1002 # on the command line from the front of the path
1003 # (or delete the path completely if it is the same
1004 # as the specified directory)
1005 path.sub!(/^#{Regexp.escape argdirname}(#{File::SEPARATOR}|$)/, '')
1006 filename = File.join(path, filename) unless path.empty?
1008 # skip file if it's to be ignored
1009 unless parse_options[:ignore].empty?
1011 parse_options[:ignore].each do |pat|
1012 if File.fnmatch?(pat, filename, File::FNM_PATHNAME)
1022 rcs << RCS.parse(filename, rcsfile, parse_options)
1023 rescue Exception => e
1024 warning "Failed to parse #{filename} @ #{rcsfile}:#{$.}"
1029 warning "Cannot handle #{arg} of #{ftype} type"
1035 rcs.first.export_commits(parse_options)
1037 warning "Preparing commits"
1042 r.revision.each do |k, rev|
1044 commits << RCS::Commit.new(r, rev)
1045 rescue NoBranchSupport
1046 if parse_options[:skip_branches]
1047 warning "Skipping revision #{rev.rev} for #{r.fname} (branch)"
1054 warning "Sorting by date"
1059 warning "RAW commits (#{commits.length}):"
1061 PP.pp c.to_a, $stderr
1064 warning "#{commits.length} single-file commits"
1067 warning "Coalescing [1] by date with fuzz #{parse_options[:commit_fuzz]}"
1069 thisindex = commits.size
1070 commits.reverse_each do |c|
1071 nextindex = thisindex
1074 cfiles = Set.new c.tree.filenames
1079 # test for mergeable commits by looking at following commits
1080 while nextindex < commits.size
1081 k = commits[nextindex]
1084 # commits are date-sorted, so we know we can quit early if we are too far
1085 # for coalescing to work
1086 break if k.min_date > c.max_date + parse_options[:commit_fuzz]
1090 kfiles = Set.new k.tree.filenames
1092 if c.log != k.log or c.author != k.author or c.branch != k.branch
1096 unless c.symbols.subset?(k.symbols) or k.symbols.subset?(c.symbols)
1097 cflist = cfiles.to_a.join(', ')
1098 kflist = kfiles.to_a.join(', ')
1099 if parse_options[:symbol_check]
1100 warning "Not coalescing #{c.log.inspect}\n\tfor (#{cflist})\n\tand (#{kflist})"
1101 warning "\tbecause their symbols disagree:\n\t#{c.symbols.to_a.inspect} and #{k.symbols.to_a.inspect} disagree on #{(c.symbols ^ k.symbols).to_a.inspect}"
1102 warning "\tretry with the --no-symbol-check option if you want to merge these commits anyway"
1105 warning "Coalescing #{c.log.inspect}\n\tfor (#{cflist})\n\tand (#{kflist})"
1106 warning "\twith disagreeing symbols:\n\t#{c.symbols.to_a.inspect} and #{k.symbols.to_a.inspect} disagree on #{(c.symbols ^ k.symbols).to_a.inspect}"
1110 # keep track of filenames touched by commits we are not merging with,
1111 # since we don't want to merge with commits that touch them, to preserve
1112 # the monotonicity of history for each file
1113 # TODO we could forward-merge with them, unless some of our files were
1116 # if the candidate touches any file already in the commit,
1117 # we can stop looking forward
1118 break unless cfiles.intersection(kfiles).empty?
1123 # the candidate has the same log, author, branch and appropriate symbols
1124 # does it touch anything in ofiles?
1125 unless ofiles.intersection(kfiles).empty?
1127 cflist = cfiles.to_a.join(', ')
1128 kflist = kfiles.to_a.join(', ')
1129 oflist = ofiles.to_a.join(', ')
1130 warning "Not coalescing #{c.log.inspect}\n\tfor (#{cflist})\n\tand (#{kflist})"
1131 warning "\tbecause the latter intersects #{oflist} in #{(ofiles & kfiles).to_a.inspect}"
1139 mergeable.each do |k|
1142 rescue RuntimeError => err
1143 fuzz = c.date - k.date
1144 warning "Fuzzy commit coalescing failed: #{err}"
1145 warning "\tretry with commit fuzz < #{fuzz} if you don't want to see this message"
1153 warning "[1] commits (#{commits.length}):"
1155 PP.pp c.to_a, $stderr
1158 warning "#{commits.length} coalesced commits"
1161 commits.each { |c| c.export(parse_options) }