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
32 class NoBranchSupport < NotImplementedError ; end
34 # Integer#odd? was introduced in Ruby 1.8.7, backport it to
36 unless 2.respond_to? :odd?
44 # Set standard output to binary mode: git fast-import doesn't like Windows
45 # line-endings, and this ensures that the line termination will be a simple 0x0a
46 # on Windows too (it expands to 0x0D 0x0A otherwise).
50 RCS fast-export version: set to `git` in the repository, but can be overridden
51 by packagers, e.g. based on the latest tag, git description, custom packager
54 When the version is set to `git`, we make a little effort to find more information
55 about which commit we are at.
61 if RFE_VERSION == "git"
62 nolinkfile = File.readlink(__FILE__) rescue __FILE__
63 Dir.chdir File.expand_path File.dirname nolinkfile
65 if File.exists? '.git' ; begin
66 git_out = `git log -1 --pretty="%h %H%n%ai" | git name-rev --stdin`.split("\n")
67 hash=git_out.first.split.first
68 branch=git_out.first.split('(').last.chomp(')')
69 date=git_out.last.split.first
70 changed=`git diff --no-ext-diff --quiet --exit-code`
71 branch << "*" unless $?.success?
72 info=" [#{branch}] #{hash} (#{date})"
77 STDERR.puts "#{$0}: RCS fast-export, #{RFE_VERSION} version#{info}"
79 STDERR.puts "#{$0}: RCS fast-export, version #{RFE_VERSION}"
86 #{$0} [options] file [file ...]
88 Fast-export the RCS history of one or more files. If a directory is specified,
89 all RCS-tracked files in the directory and its descendants are exported.
91 When importing single files, their pathname is discarded during import. When
92 importing directories, only the specified directory component is discarded.
94 When importing a single file, RCS commits are converted one by one. Otherwise,
95 some heuristics is used to determine how to coalesce commits touching different
98 Currently, commits are coalesced if they share the exact same log and if their
99 date differs by no more than the user-specified fuzziness. Additionally, the
100 symbols in one of the commit must be a subset of the symbols in the other
101 commit, unless --no-symbol-check is specified or rcs.symbolCheck is set to
102 false in the git configuration.
105 git init && rcs-fast-export.rb . | git fast-import && git reset
108 --help, -h, -? display this help text
109 --authors-file, -A specify a file containing username = Full Name <email> mappings
110 --[no-]author-is-committer
111 use the author name and date as committer identity
112 --ignore ignore the specified files (shell pattern)
113 --log-encoding specify the encoding of log messages, for transcoding to UTF-8
114 --rcs-commit-fuzz fuzziness in RCS commits to be considered a single one when
115 importing multiple files
116 (in seconds, defaults to 300, i.e. 5 minutes)
117 --[no-]warn-missing-authors
118 [do not] warn about usernames missing from the map file
119 --[no-]symbol-check [do not] check symbols when coalescing commits
120 --[no-]tag-each-rev [do not] create a lightweight tag for each RCS revision when
121 importing a single file
122 --[no-]log-filename [do not] prepend the filename to the commit log when importing
124 --skip-branches when exporting multiple files with a branched history, export
125 the main branch only instead of aborting due to the lack of
126 support for branched multi-file history export
131 rcs.authorsFile for --authors-file
132 rcs.authorIsCommitter for --author-is-committer
133 rcs.tagEachRev for --tag-each-rev
134 rcs.logFilename for --log-filename
135 rcs.commitFuzz for --rcs-commit-fuzz
136 rcs.warnMissingAuthors for --warn-missing-authors
137 rcs.symbolCheck for --rcs-symbol-check
138 rcs.tagFuzz for --rcs-tag-fuzz
149 warning "Could not find #{arg}"
152 def emit_committer(opts, author, date)
153 if opts[:author_is_committer]
154 committer = "#{author} #{date}"
156 committer = `git var GIT_COMMITTER_IDENT`.chomp
158 puts "committer #{committer}"
161 # returns a hash that maps usernames to author names & emails
162 def load_authors_file(fn)
165 File.open(File.expand_path(fn)) do |io|
166 io.each_line do |line|
167 uname, author = line.split('=', 2)
170 warning "Username #{uname} redefined to #{author}" if hash.has_key? uname
180 def username_to_author(name, opts)
182 raise "no authors map defined" unless map and Hash === map
184 # if name is not found in map, provide a default one, optionally giving a warning (once)
186 warning "no author found for #{name}" if opts[:warn_missing_authors]
187 map[name] = "#{name} <empty>"
192 # display a message about a (recoverable) error
193 def alert(msg, action)
194 STDERR.puts "ERROR:\t#{msg}"
195 STDERR.puts "\t#{action}"
200 fields = string.split('.')
201 raise ArgumentError, "wrong number of fields for RCS date #{string}" unless fields.length == 6
202 # in Ruby 1.9, '99' is interpreted as year 99, not year 1999
203 if fields.first.length < 3
204 fields.first.insert 0, '19'
211 # strip an optional final ;
216 # strip the first and last @, and de-double @@s
217 def RCS.sanitize(arg)
221 raise 'malformed first line' unless ret.first[0,1] == '@'
222 raise 'malformed last line' unless ret.last[-1,1] == '@'
223 ret.first.sub!(/^@/,'')
224 ret.last.sub!(/@$/,'')
225 ret.map { |l| l.gsub('@@','@') }
227 arg.chomp('@').sub(/^@/,'').gsub('@@','@')
234 def RCS.at_clean(arg)
235 RCS.sanitize RCS.clean(arg)
243 @@marks[key] = @@marks.length + 1
247 def RCS.blob(file, rev)
248 RCS.mark([file, rev])
251 def RCS.commit(commit)
256 attr_accessor :head, :comment, :desc, :revision, :fname, :mode
257 def initialize(fname, executable)
262 @revision = Hash.new { |h, r| h[r] = Revision.new(self, r) }
263 @mode = executable ? '755' : '644'
266 def has_revision?(rev)
267 @revision.has_key?(rev) and not @revision[rev].author.nil?
270 def export_commits(opts={})
273 log_enc = opts[:log_encoding]
274 until @revision.empty?
277 # a string sort is a very good candidate for
278 # export order, getting a miss only for
279 # multi-digit revision components
280 keys = @revision.keys.sort
282 warning "commit export loop ##{counter}"
283 warning "\t#{exported.length} commits exported so far: #{exported.join(', ')}" unless exported.empty?
284 warning "\t#{keys.size} to export: #{keys.join(', ')}"
288 # the parent commit is rev.next if we're on the
289 # master branch (rev.branch is nil) or
290 # rev.diff_base otherwise
291 from = rev.branch.nil? ? rev.next : rev.diff_base
292 # A commit can only be exported if it has no
293 # parent, or if the parent has been exported
294 # already. Skip this commit otherwise
295 if from and not exported.include? from
299 branch = rev.branch || 'master'
300 author = username_to_author(rev.author, opts)
301 date = "#{rev.date.tv_sec} +0000"
303 if opts[:log_filename]
304 log << @fname << ": "
307 # git fast-import expects logs to be in UTF-8, so if a different log encoding
308 # is specified for the log we transcode from whatever was specified to UTF-8.
309 # we then mark the string as ASCII-8BIT (as everything else) so that string
310 # lengths are computed in bytes
311 log << rev.log.join.encode('UTF-8', log_enc).force_encoding('ASCII-8BIT')
316 puts "commit refs/heads/#{branch}"
317 puts "mark :#{RCS.commit key}"
318 puts "author #{author} #{date}"
319 emit_committer(opts, author, date)
320 puts "data #{log.length}"
321 puts log unless log.empty?
322 puts "from :#{RCS.commit from}" if from
323 puts "M #{@mode} :#{RCS.blob @fname, key} #{@fname}"
325 # TODO FIXME this *should* be safe, in
326 # that it should not unduly move
327 # branches back in time, but I'm not
329 rev.branches.each do |sym|
330 puts "reset refs/heads/#{sym}"
331 puts "from :#{RCS.commit key}"
333 rev.symbols.each do |sym|
334 puts "reset refs/tags/#{sym}"
335 puts "from :#{RCS.commit key}"
337 if opts[:tag_each_rev]
338 puts "reset refs/tags/#{key}"
339 puts "from :#{RCS.commit key}"
344 exported.each { |k| @revision.delete(k) }
350 attr_accessor :rev, :author, :state, :next
351 attr_accessor :branches, :log, :text, :symbols
352 attr_accessor :branch, :diff_base, :branch_point
354 def initialize(file, rev)
371 @date = Time.rcs(str)
376 ret = "blob\nmark :#{RCS.blob @file.fname, @rev}\ndata #{str.length}\n#{str}\n"
381 # TODO: what if a revision does not end with newline?
382 # TODO this should be done internally, not piping out to RCS
383 def RCS.expand_keywords(rcsfile, revision)
384 ret = ::File.read("|co -q -p#{revision} #{Shellwords.escape rcsfile}")
386 ret.each_line do |line|
392 def RCS.parse(fname, rcsfile, opts={})
393 rcs = RCS::File.new(fname, ::File.executable?(rcsfile))
395 ::File.open(rcsfile, 'rb') do |file|
400 file.each_line do |line|
403 command, args = line.split($;,2)
404 next if command.empty?
406 if command.chomp!(';')
407 warning "Skipping empty command #{command.inspect}" if $DEBUG
413 rcs.head = RCS.clean(args.chomp)
419 rcs.comment = RCS.at_clean(args.chomp)
422 if rcs.has_revision?(rev)
423 status.push :revision_data
425 status.push :new_revision
430 status.push :read_lines
431 when 'branch', 'access', 'locks', 'expand'
432 warning "Skipping unhandled command #{command.inspect}" if $DEBUG
433 status.push :skipping_lines
437 raise "Unknown command #{command.inspect}"
440 status.pop if line.strip.chomp!(';')
442 # we can have multiple symbols per line
443 pairs = line.strip.split($;)
445 sym, rev = pair.strip.split(':',2);
447 status.pop if rev.chomp!(';')
448 rcs.revision[rev].symbols << sym
454 rcs.desc.replace lines.dup
457 # we sanitize lines as we read them
459 actual_line = line.dup
461 # the first line must begin with a @, which we strip
463 ats = line.match(/^@+/)
464 raise 'malformed line' unless ats
465 actual_line.replace line.sub(/^@/,'')
468 # if the line ends with an ODD number of @, it's the
469 # last line -- we work on actual_line so that content
470 # such as @\n or @ work correctly (they would be
471 # encoded respectively as ['@@@\n','@\n'] and
473 ats = actual_line.chomp.match(/@+$/)
474 if nomore = (ats && Regexp.last_match(0).length.odd?)
475 actual_line.replace actual_line.chomp.sub(/@$/,'')
477 lines << actual_line.gsub('@@','@')
484 when /^date\s+(\S+);\s+author\s+(\S+);\s+state\s+(\S+);$/
485 rcs.revision[rev].date = $1
486 rcs.revision[rev].author = $2
487 rcs.revision[rev].state = $3
490 when /^branches(?:\s+|$)/
491 status.push :branches
493 line = line.sub(/^branches\s+/,'')
496 when /^next\s+(\S+)?;$/
497 nxt = rcs.revision[rev].next = $1
499 raise "multiple diff_bases for #{nxt}" unless rcs.revision[nxt].diff_base.nil?
500 rcs.revision[nxt].diff_base = rev
501 rcs.revision[nxt].branch = rcs.revision[rev].branch
506 candidate = line.split(';',2)
507 candidate.first.strip.split.each do |branch|
508 raise "multiple diff_bases for #{branch}" unless rcs.revision[branch].diff_base.nil?
509 rcs.revision[branch].diff_base = rev
510 # we drop the last number from the branch name
511 rcs.revision[branch].branch = branch.sub(/\.\d+$/,'.x')
512 rcs.revision[branch].branch_point = rev
514 status.pop if candidate.length > 1
520 status.push :read_lines
528 status.push :read_lines
533 rcs.revision[rev].log.replace lines.dup
536 if opts[:expand_keywords]
537 rcs.revision[rev].text.replace RCS.expand_keywords(rcsfile, rev)
539 rcs.revision[rev].text.replace lines.dup
541 puts rcs.revision[rev].blob
544 if opts[:expand_keywords]
545 rcs.revision[rev].text.replace RCS.expand_keywords(rcsfile, rev)
547 difflines.replace lines.dup
548 difflines.pop if difflines.last.empty?
549 if difflines.first.chomp.empty?
550 alert "malformed diff: empty initial line @ #{rcsfile}:#{file.lineno-difflines.length-1}", "skipping"
552 end unless difflines.empty?
553 base = rcs.revision[rev].diff_base
554 unless rcs.revision[base].text
557 raise 'no diff base!'
561 rcs.revision[base].text.each { |l| buffer << [l.dup] }
567 while l = difflines.shift
569 raise 'negative index during insertion' if index < 0
570 raise 'negative count during insertion' if count < 0
573 # collected all the lines, put the before
578 buffer[index].unshift(*adding)
585 raise "malformed diff @ #{rcsfile}:#{file.lineno-difflines.length-1} `#{l}`" unless l =~ /^([ad])(\d+) (\d+)$/
591 # for deletion, index 1 is the first index, so the Ruby
592 # index is one less than the diff one
594 # we replace them with empty string so that 'a' commands
595 # referring to the same line work properly
602 # addition will prepend the appropriate lines
603 # to the given index, and in this case Ruby
604 # and diff indices are the same
609 # turn the buffer into an array of lines, deleting the empty ones
610 buffer.delete_if { |l| l.empty? }
613 rcs.revision[rev].text = buffer
615 puts rcs.revision[rev].blob
618 raise "Unknown status #{status.last}"
623 # clean up the symbols/branches: look for revisions that have
624 # one or more symbols but no dates, and make them into
625 # branches, pointing to the highest commit with that key
627 keys = rcs.revision.keys
628 rcs.revision.each do |key, rev|
629 if rev.date.nil? and not rev.symbols.empty?
630 top = keys.select { |k| k.match(/^#{key}\./) }.sort.last
631 tr = rcs.revision[top]
632 raise "unhandled complex branch structure met: #{rev.inspect} refers #{tr.inspect}" if tr.date.nil?
633 tr.branches |= rev.symbols
637 branches.each { |k| rcs.revision.delete k }
643 def initialize(commit)
649 testfiles = @files.dup
650 tree.each { |rcs, rev| self.add(rcs, rev, testfiles) }
651 # the next line is only reached if all the adds were
652 # successful, so the merge is atomic
653 @files.replace testfiles
656 def add(rcs, rev, file_list=@files)
657 if file_list.key? rcs
658 prev = file_list[rcs]
659 if prev.log == rev.log
660 str = "re-adding existing file #{rcs.fname} (old: #{prev.rev}, new: #{rev.rev})"
662 str = "re-adding existing file #{rcs.fname} (old: #{[prev.rev, prev.log.to_s].inspect}, new: #{[rev.rev, rev.log.to_s].inspect})"
664 if prev.text != rev.text
667 @commit.warn_about str
679 @files.map do |rcs, rev|
680 if rev.state.downcase == "dead"
681 files << "D #{rcs.fname}"
683 files << "M #{rcs.mode} :#{RCS.blob rcs.fname, rev.rev} #{rcs.fname}"
690 @files.map { |rcs, rev| rcs.fname }
699 attr_accessor :date, :log, :symbols, :author, :branch
701 attr_accessor :min_date, :max_date
702 def initialize(rcs, rev)
703 raise NoBranchSupport if rev.branch
704 self.date = rev.date.dup
705 self.min_date = self.max_date = self.date
706 self.log = rev.log.dup
707 self.symbols = rev.symbols.dup
708 self.author = rev.author
709 self.branch = rev.branch
711 self.tree = Tree.new self
712 self.tree.add rcs, rev
716 [self.min_date, self.date, self.max_date, self.branch, self.symbols, self.author, self.log, self.tree.to_a]
720 warn str + " for commit on #{self.date}"
723 # Sort by date and then by number of symbols
725 ds = self.date <=> other.date
729 return self.symbols.length <=> other.symbols.length
734 self.tree.merge! commit.tree
735 if commit.max_date > self.max_date
736 self.max_date = commit.max_date
738 if commit.min_date < self.min_date
739 self.min_date = commit.min_date
741 self.symbols.merge commit.symbols
745 xbranch = self.branch || 'master'
746 xauthor = username_to_author(self.author, opts)
748 numdate = self.date.tv_sec
749 xdate = "#{numdate} +0000"
752 puts "commit refs/heads/#{xbranch}"
753 puts "mark :#{RCS.commit key}"
754 puts "author #{xauthor} #{xdate}"
755 emit_committer(opts, xauthor, xdate)
756 puts "data #{xlog.length}"
757 puts xlog unless xlog.empty?
758 # TODO branching support for multi-file export
759 # puts "from :#{RCS.commit from}" if self.branch_point
762 # TODO branching support for multi-file export
763 # rev.branches.each do |sym|
764 # puts "reset refs/heads/#{sym}"
765 # puts "from :#{RCS.commit key}"
768 self.symbols.each do |sym|
769 puts "reset refs/tags/#{sym}"
770 puts "from :#{RCS.commit key}"
779 opts = GetoptLong.new(
780 # Authors file, like git-svn and git-cvsimport, more than one can be
782 ['--authors-file', '-A', GetoptLong::REQUIRED_ARGUMENT],
783 # Use author identity as committer identity?
784 ['--author-is-committer', GetoptLong::NO_ARGUMENT],
785 ['--no-author-is-committer', GetoptLong::NO_ARGUMENT],
786 # Use "co" to obtain the actual revision with keywords expanded.
787 ['--expand-keywords', GetoptLong::NO_ARGUMENT],
788 # RCS file suffix, like RCS
789 ['--rcs-suffixes', '-x', GetoptLong::REQUIRED_ARGUMENT],
790 # Shell pattern to identify files to be ignored
791 ['--ignore', GetoptLong::REQUIRED_ARGUMENT],
792 # Encoding of log messages in the RCS files
793 ['--log-encoding', GetoptLong::REQUIRED_ARGUMENT],
794 # Date fuzziness for commits to be considered the same (in seconds)
795 ['--rcs-commit-fuzz', GetoptLong::REQUIRED_ARGUMENT],
796 # warn about usernames missing in authors file map?
797 ['--warn-missing-authors', GetoptLong::NO_ARGUMENT],
798 ['--no-warn-missing-authors', GetoptLong::NO_ARGUMENT],
799 # check symbols when coalescing?
800 ['--symbol-check', GetoptLong::NO_ARGUMENT],
801 ['--no-symbol-check', GetoptLong::NO_ARGUMENT],
803 ['--tag-each-rev', GetoptLong::NO_ARGUMENT],
804 ['--no-tag-each-rev', GetoptLong::NO_ARGUMENT],
805 # prepend filenames to commit logs?
806 ['--log-filename', GetoptLong::NO_ARGUMENT],
807 ['--no-log-filename', GetoptLong::NO_ARGUMENT],
808 # skip branches when exporting a whole tree?
809 ['--skip-branches', GetoptLong::NO_ARGUMENT],
810 # show current version
811 ['--version', '-v', GetoptLong::NO_ARGUMENT],
813 ['--help', '-h', '-?', GetoptLong::NO_ARGUMENT]
816 # We read options in order, but they apply to all passed parameters.
817 # TODO maybe they should only apply to the following, unless there's only one
819 opts.ordering = GetoptLong::RETURN_IN_ORDER
823 :authors => Hash.new,
824 :ignore => Array.new,
829 # Read config options
830 `git config --get-all rcs.authorsfile`.each_line do |fn|
831 parse_options[:authors].merge! load_authors_file(fn.chomp)
834 parse_options[:author_is_committer] = (
835 `git config --bool rcs.authoriscommitter`.chomp == 'false'
838 parse_options[:tag_each_rev] = (
839 `git config --bool rcs.tageachrev`.chomp == 'true'
842 parse_options[:log_filename] = (
843 `git config --bool rcs.logfilename`.chomp == 'true'
846 fuzz = `git config --int rcs.commitFuzz`.chomp
847 parse_options[:commit_fuzz] = fuzz.to_i unless fuzz.empty?
849 fuzz = `git config --int rcs.tagFuzz`.chomp
850 parse_options[:tag_fuzz] = fuzz.to_i unless fuzz.empty?
852 parse_options[:symbol_check] = (
853 `git config --bool rcs.symbolcheck`.chomp == 'false'
856 parse_options[:warn_missing_authors] = (
857 `git config --bool rcs.warnmissingauthors`.chomp == 'false'
860 opts.each do |opt, arg|
862 when '--authors-file'
863 authors = load_authors_file(arg)
864 redef = parse_options[:authors].keys & authors.keys
865 warning "Authors file #{arg} redefines #{redef.join(', ')}" unless redef.empty?
866 parse_options[:authors].merge!(authors)
867 when '--author-is-committer'
868 parse_options[:author_is_committer] = true
869 when '--no-author-is-committer'
870 parse_options[:author_is_committer] = false
871 when '--expand-keywords'
872 parse_options[:expand_keywords] = true
873 when '--rcs-suffixes'
876 parse_options[:ignore] << arg
877 when '--log-encoding'
878 parse_options[:log_encoding] = Encoding.find(arg)
879 when '--rcs-commit-fuzz'
880 parse_options[:commit_fuzz] = arg.to_i
881 when '--rcs-tag-fuzz'
882 parse_options[:tag_fuzz] = arg.to_i
883 when '--symbol-check'
884 parse_options[:symbol_check] = true
885 when '--no-symbol-check'
886 parse_options[:symbol_check] = false
887 when '--tag-each-rev'
888 parse_options[:tag_each_rev] = true
889 when '--no-tag-each-rev'
890 # this is the default, which is fine since the missing key
891 # (default) returns nil which is false in Ruby
892 parse_options[:tag_each_rev] = false
893 when '--log-filename'
894 parse_options[:log_filename] = true
895 when '--no-log-filename'
896 # this is the default, which is fine since the missing key
897 # (default) returns nil which is false in Ruby
898 parse_options[:log_filename] = false
899 when '--skip-branches'
900 parse_options[:skip_branches] = true
912 if parse_options[:tag_fuzz] < parse_options[:commit_fuzz]
913 parse_options[:tag_fuzz] = parse_options[:commit_fuzz]
918 user = Etc.getlogin || ENV['USER']
920 # steal username/email data from other init files that may contain the
924 # the user's .hgrc file for a username field
925 ['~/.hgrc', /^\s*username\s*=\s*(["'])?(.*)\1$/, 2],
926 # the user's .(g)vimrc for a changelog_username setting
927 ['~/.vimrc', /changelog_username\s*=\s*(["'])?(.*)\1$/, 2],
928 ['~/.gvimrc', /changelog_username\s*=\s*(["'])?(.*)\1$/, 2],
929 ].each do |fn, rx, idx|
930 file = File.expand_path fn
931 if File.readable?(file) and File.read(file) =~ rx
932 parse_options[:authors][user] = Regexp.last_match(idx).strip
938 if user and not user.empty? and not parse_options[:authors].has_key?(user)
939 name = ENV['GIT_AUTHOR_NAME'] || ''
940 name.replace(`git config user.name`.chomp) if name.empty?
941 name.replace(Etc.getpwnam(user).gecos) if name.empty?
944 # couldn't find a name, try to steal data from other sources
947 # if we found a name, try to find an email too
948 email = ENV['GIT_AUTHOR_EMAIL'] || ''
949 email.replace(`git config user.email`.chomp) if email.empty?
952 # couldn't find an email, try to steal data too
955 # we got both a name and email, fill the info
956 parse_options[:authors][user] = "#{name} <#{email}>"
971 file_list.each do |arg|
972 case ftype = File.ftype(arg)
978 not_found "RCS file #{arg}"
981 filename = File.basename(arg, SFX)
983 filename = File.basename(arg)
984 path = File.dirname(arg)
985 rcsfile = File.join(path, 'RCS', filename) + SFX
986 unless File.exists? rcsfile
987 rcsfile.replace File.join(path, filename) + SFX
988 unless File.exists? rcsfile
989 not_found "RCS file for #{filename} in #{path}"
993 rcs << RCS.parse(filename, rcsfile, parse_options)
995 argdirname = arg.chomp(File::SEPARATOR)
996 pattern = File.join(argdirname, '**', '*' + SFX)
997 Dir.glob(pattern, File::FNM_DOTMATCH).each do |rcsfile|
998 filename = File.basename(rcsfile, SFX)
999 path = File.dirname(rcsfile)
1000 # strip trailing "/RCS" if present, or "RCS" if that's
1002 path.sub!(/(^|#{File::SEPARATOR})RCS$/, '')
1003 # strip off the portion of the path specified
1004 # on the command line from the front of the path
1005 # (or delete the path completely if it is the same
1006 # as the specified directory)
1007 path.sub!(/^#{Regexp.escape argdirname}(#{File::SEPARATOR}|$)/, '')
1008 filename = File.join(path, filename) unless path.empty?
1010 # skip file if it's to be ignored
1011 unless parse_options[:ignore].empty?
1013 parse_options[:ignore].each do |pat|
1014 if File.fnmatch?(pat, filename, File::FNM_PATHNAME)
1024 rcs << RCS.parse(filename, rcsfile, parse_options)
1025 rescue Exception => e
1026 warning "Failed to parse #{filename} @ #{rcsfile}:#{$.}"
1031 warning "Cannot handle #{arg} of #{ftype} type"
1037 rcs.first.export_commits(parse_options)
1039 warning "Preparing commits"
1044 r.revision.each do |k, rev|
1046 commits << RCS::Commit.new(r, rev)
1047 rescue NoBranchSupport
1048 if parse_options[:skip_branches]
1049 warning "Skipping revision #{rev.rev} for #{r.fname} (branch)"
1056 warning "Sorting by date"
1061 warning "RAW commits (#{commits.length}):"
1063 PP.pp c.to_a, $stderr
1066 warning "#{commits.length} single-file commits"
1069 warning "Coalescing [1] by date with fuzz #{parse_options[:commit_fuzz]}"
1071 thisindex = commits.size
1072 commits.reverse_each do |c|
1073 nextindex = thisindex
1076 cfiles = Set.new c.tree.filenames
1081 # test for mergeable commits by looking at following commits
1082 while nextindex < commits.size
1083 k = commits[nextindex]
1086 # commits are date-sorted, so we know we can quit early if we are too far
1087 # for coalescing to work
1088 break if k.min_date > c.max_date + parse_options[:commit_fuzz]
1092 kfiles = Set.new k.tree.filenames
1094 if c.log != k.log or c.author != k.author or c.branch != k.branch
1098 unless c.symbols.subset?(k.symbols) or k.symbols.subset?(c.symbols)
1099 cflist = cfiles.to_a.join(', ')
1100 kflist = kfiles.to_a.join(', ')
1101 if parse_options[:symbol_check]
1102 warning "Not coalescing #{c.log.inspect}\n\tfor (#{cflist})\n\tand (#{kflist})"
1103 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}"
1104 warning "\tretry with the --no-symbol-check option if you want to merge these commits anyway"
1107 warning "Coalescing #{c.log.inspect}\n\tfor (#{cflist})\n\tand (#{kflist})"
1108 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}"
1112 # keep track of filenames touched by commits we are not merging with,
1113 # since we don't want to merge with commits that touch them, to preserve
1114 # the monotonicity of history for each file
1115 # TODO we could forward-merge with them, unless some of our files were
1118 # if the candidate touches any file already in the commit,
1119 # we can stop looking forward
1120 break unless cfiles.intersection(kfiles).empty?
1125 # the candidate has the same log, author, branch and appropriate symbols
1126 # does it touch anything in ofiles?
1127 unless ofiles.intersection(kfiles).empty?
1129 cflist = cfiles.to_a.join(', ')
1130 kflist = kfiles.to_a.join(', ')
1131 oflist = ofiles.to_a.join(', ')
1132 warning "Not coalescing #{c.log.inspect}\n\tfor (#{cflist})\n\tand (#{kflist})"
1133 warning "\tbecause the latter intersects #{oflist} in #{(ofiles & kfiles).to_a.inspect}"
1141 mergeable.each do |k|
1144 rescue RuntimeError => err
1145 fuzz = c.date - k.date
1146 warning "Fuzzy commit coalescing failed: #{err}"
1147 warning "\tretry with commit fuzz < #{fuzz} if you don't want to see this message"
1155 warning "[1] commits (#{commits.length}):"
1157 PP.pp c.to_a, $stderr
1160 warning "#{commits.length} coalesced commits"
1163 commits.each { |c| c.export(parse_options) }