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
29 class NoBranchSupport < NotImplementedError ; end
31 # Integer#odd? was introduced in Ruby 1.8.7, backport it to
33 unless 2.respond_to? :odd?
41 # Set standard output to binary mode: git fast-import doesn't like Windows
42 # line-endings, and this ensures that the line termination will be a simple 0x0a
43 # on Windows too (it expands to 0x0D 0x0A otherwise).
47 RCS fast-export version: set to `git` in the repository, but can be overridden
48 by packagers, e.g. based on the latest tag, git description, custom packager
51 When the version is set to `git`, we make a little effort to find more information
52 about which commit we are at.
58 if RFE_VERSION == "git"
59 nolinkfile = File.readlink(__FILE__) rescue __FILE__
60 Dir.chdir File.expand_path File.dirname nolinkfile
62 if File.exists? '.git' ; begin
63 git_out = `git log -1 --pretty="%h %H%n%ai" | git name-rev --stdin`.split("\n")
64 hash=git_out.first.split.first
65 branch=git_out.first.split('(').last.chomp(')')
66 date=git_out.last.split.first
67 changed=`git diff --no-ext-diff --quiet --exit-code`
68 branch << "*" unless $?.success?
69 info=" [#{branch}] #{hash} (#{date})"
74 STDERR.puts "#{$0}: RCS fast-export, #{RFE_VERSION} version#{info}"
76 STDERR.puts "#{$0}: RCS fast-export, version #{RFE_VERSION}"
83 #{$0} [options] file [file ...]
85 Fast-export the RCS history of one or more files. If a directory is specified,
86 all RCS-tracked files in the directory and its descendants are exported.
88 When importing single files, their pathname is discarded during import. When
89 importing directories, only the specified directory component is discarded.
91 When importing a single file, RCS commits are converted one by one. Otherwise,
92 some heuristics is used to determine how to coalesce commits touching different
95 Currently, commits are coalesced if they share the exact same log and if their
96 date differs by no more than the user-specified fuzziness. Additionally, the
97 symbols in one of the commit must be a subset of the symbols in the other
98 commit, unless --no-symbol-check is specified or rcs.symbolCheck is set to
99 false in the git configuration.
102 git init && rcs-fast-export.rb . | git fast-import && git reset
105 --help, -h, -? display this help text
106 --authors-file, -A specify a file containing username = Full Name <email> mappings
107 --rcs-commit-fuzz fuzziness in RCS commits to be considered a single one when
108 importing multiple files
109 (in seconds, defaults to 300, i.e. 5 minutes)
110 --[no-]symbol-check [do not] check symbols when coalescing commits
111 --[no-]tag-each-rev [do not] create a lightweight tag for each RCS revision when
112 importing a single file
113 --[no-]log-filename [do not] prepend the filename to the commit log when importing
115 --skip-branches when exporting multiple files with a branched history, export
116 the main branch only instead of aborting due to the lack of
117 support for branched multi-file history export
122 rcs.authorsFile for --authors-file
123 rcs.tagEachRev for --tag-each-rev
124 rcs.logFilename for --log-filename
125 rcs.commitFuzz for --rcs-commit-fuzz
126 rcs.symbolCheck for --rcs-symbol-check
127 rcs.tagFuzz for --rcs-tag-fuzz
134 STDERR.puts "Could not find #{arg}"
137 # returns a hash that maps usernames to author names & emails
138 def load_authors_file(fn)
141 File.open(File.expand_path(fn)) do |io|
142 io.each_line do |line|
143 uname, author = line.split('=', 2)
147 STDERR.puts "Username #{uname} redefined to #{author}" if hash.has_key? uname
157 # display a message about a (recoverable) error
158 def alert(msg, action)
159 STDERR.puts "ERROR:\t#{msg}"
160 STDERR.puts "\t#{action}"
165 fields = string.split('.')
166 raise ArgumentError, "wrong number of fields for RCS date #{string}" unless fields.length == 6
167 # in Ruby 1.9, '99' is interpreted as year 99, not year 1999
168 if fields.first.length < 3
169 fields.first.insert 0, '19'
176 # strip an optional final ;
181 # strip the first and last @, and de-double @@s
182 def RCS.sanitize(arg)
186 raise 'malformed first line' unless ret.first[0,1] == '@'
187 raise 'malformed last line' unless ret.last[-1,1] == '@'
188 ret.first.sub!(/^@/,'')
189 ret.last.sub!(/@$/,'')
190 ret.map { |l| l.gsub('@@','@') }
192 arg.chomp('@').sub(/^@/,'').gsub('@@','@')
199 def RCS.at_clean(arg)
200 RCS.sanitize RCS.clean(arg)
208 @@marks[key] = @@marks.length + 1
212 def RCS.blob(file, rev)
213 RCS.mark([file, rev])
216 def RCS.commit(commit)
221 attr_accessor :head, :comment, :desc, :revision, :fname, :mode
222 def initialize(fname, executable)
227 @revision = Hash.new { |h, r| h[r] = Revision.new(self, r) }
228 @mode = executable ? '755' : '644'
231 def has_revision?(rev)
232 @revision.has_key?(rev) and not @revision[rev].author.nil?
235 def export_commits(opts={})
238 until @revision.empty?
241 # a string sort is a very good candidate for
242 # export order, getting a miss only for
243 # multi-digit revision components
244 keys = @revision.keys.sort
247 STDERR.puts "commit export loop ##{counter}"
248 STDERR.puts "\t#{exported.length} commits exported so far: #{exported.join(', ')}" unless exported.empty?
249 STDERR.puts "\t#{keys.size} to export: #{keys.join(', ')}"
253 # the parent commit is rev.next if we're on the
254 # master branch (rev.branch is nil) or
255 # rev.diff_base otherwise
256 from = rev.branch.nil? ? rev.next : rev.diff_base
257 # A commit can only be exported if it has no
258 # parent, or if the parent has been exported
259 # already. Skip this commit otherwise
260 if from and not exported.include? from
264 branch = rev.branch || 'master'
265 author = opts[:authors][rev.author] || "#{rev.author} <empty>"
266 date = "#{rev.date.tv_sec} +0000"
268 if opts[:log_filename]
269 log << @fname << ": "
273 puts "commit refs/heads/#{branch}"
274 puts "mark :#{RCS.commit key}"
275 puts "committer #{author} #{date}"
276 puts "data #{log.length}"
277 puts log unless log.empty?
278 puts "from :#{RCS.commit from}" if from
279 puts "M #{@mode} :#{RCS.blob @fname, key} #{@fname}"
281 # TODO FIXME this *should* be safe, in
282 # that it should not unduly move
283 # branches back in time, but I'm not
285 rev.branches.each do |sym|
286 puts "reset refs/heads/#{sym}"
287 puts "from :#{RCS.commit key}"
289 rev.symbols.each do |sym|
290 puts "reset refs/tags/#{sym}"
291 puts "from :#{RCS.commit key}"
293 if opts[:tag_each_rev]
294 puts "reset refs/tags/#{key}"
295 puts "from :#{RCS.commit key}"
300 exported.each { |k| @revision.delete(k) }
306 attr_accessor :rev, :author, :state, :next
307 attr_accessor :branches, :log, :text, :symbols
308 attr_accessor :branch, :diff_base, :branch_point
310 def initialize(file, rev)
327 @date = Time.rcs(str)
332 ret = "blob\nmark :#{RCS.blob @file.fname, @rev}\ndata #{str.length}\n#{str}\n"
337 def RCS.parse(fname, rcsfile)
338 rcs = RCS::File.new(fname, ::File.executable?(rcsfile))
340 ::File.open(rcsfile, 'r:ASCII-8BIT') do |file|
345 file.each_line do |line|
348 command, args = line.split($;,2)
349 next if command.empty?
351 if command.chomp!(';')
353 STDERR.puts "Skipping empty command #{command.inspect}" if $DEBUG
359 rcs.head = RCS.clean(args.chomp)
365 rcs.comment = RCS.at_clean(args.chomp)
368 if rcs.has_revision?(rev)
369 status.push :revision_data
371 status.push :new_revision
376 status.push :read_lines
377 when 'branch', 'access', 'locks', 'expand'
379 STDERR.puts "Skipping unhandled command #{command.inspect}" if $DEBUG
380 status.push :skipping_lines
384 raise "Unknown command #{command.inspect}"
387 status.pop if line.strip.chomp!(';')
389 # we can have multiple symbols per line
390 pairs = line.strip.split($;)
392 sym, rev = pair.strip.split(':',2);
394 status.pop if rev.chomp!(';')
395 rcs.revision[rev].symbols << sym
401 rcs.desc.replace lines.dup
404 # we sanitize lines as we read them
406 actual_line = line.dup
408 # the first line must begin with a @, which we strip
410 ats = line.match(/^@+/)
411 raise 'malformed line' unless ats
412 actual_line.replace line.sub(/^@/,'')
415 # if the line ends with an ODD number of @, it's the
416 # last line -- we work on actual_line so that content
417 # such as @\n or @ work correctly (they would be
418 # encoded respectively as ['@@@\n','@\n'] and
420 ats = actual_line.chomp.match(/@+$/)
421 if nomore = (ats && Regexp.last_match(0).length.odd?)
422 actual_line.replace actual_line.chomp.sub(/@$/,'')
424 lines << actual_line.gsub('@@','@')
431 when /^date\s+(\S+);\s+author\s+(\S+);\s+state\s+(\S+);$/
432 rcs.revision[rev].date = $1
433 rcs.revision[rev].author = $2
434 rcs.revision[rev].state = $3
437 when /^branches(?:\s+|$)/
438 status.push :branches
440 line = line.sub(/^branches\s+/,'')
443 when /^next\s+(\S+)?;$/
444 nxt = rcs.revision[rev].next = $1
446 raise "multiple diff_bases for #{nxt}" unless rcs.revision[nxt].diff_base.nil?
447 rcs.revision[nxt].diff_base = rev
448 rcs.revision[nxt].branch = rcs.revision[rev].branch
453 candidate = line.split(';',2)
454 candidate.first.strip.split.each do |branch|
455 raise "multiple diff_bases for #{branch}" unless rcs.revision[branch].diff_base.nil?
456 rcs.revision[branch].diff_base = rev
457 # we drop the last number from the branch name
458 rcs.revision[branch].branch = branch.sub(/\.\d+$/,'.x')
459 rcs.revision[branch].branch_point = rev
461 status.pop if candidate.length > 1
467 status.push :read_lines
475 status.push :read_lines
480 rcs.revision[rev].log.replace lines.dup
483 rcs.revision[rev].text.replace lines.dup
484 puts rcs.revision[rev].blob
487 difflines.replace lines.dup
488 difflines.pop if difflines.last.empty?
489 if difflines.first.chomp.empty?
490 alert "malformed diff: empty initial line @ #{rcsfile}:#{file.lineno-difflines.length-1}", "skipping"
492 end unless difflines.empty?
493 base = rcs.revision[rev].diff_base
494 unless rcs.revision[base].text
497 raise 'no diff base!'
501 rcs.revision[base].text.each { |l| buffer << [l.dup] }
507 while l = difflines.shift
509 raise 'negative index during insertion' if index < 0
510 raise 'negative count during insertion' if count < 0
513 # collected all the lines, put the before
518 buffer[index].unshift(*adding)
525 raise "malformed diff @ #{rcsfile}:#{file.lineno-difflines.length-1} `#{l}`" unless l =~ /^([ad])(\d+) (\d+)$/
531 # for deletion, index 1 is the first index, so the Ruby
532 # index is one less than the diff one
534 # we replace them with empty string so that 'a' commands
535 # referring to the same line work properly
542 # addition will prepend the appropriate lines
543 # to the given index, and in this case Ruby
544 # and diff indices are the same
549 # turn the buffer into an array of lines, deleting the empty ones
550 buffer.delete_if { |l| l.empty? }
553 rcs.revision[rev].text = buffer
554 puts rcs.revision[rev].blob
557 raise "Unknown status #{status.last}"
562 # clean up the symbols/branches: look for revisions that have
563 # one or more symbols but no dates, and make them into
564 # branches, pointing to the highest commit with that key
566 keys = rcs.revision.keys
567 rcs.revision.each do |key, rev|
568 if rev.date.nil? and not rev.symbols.empty?
569 top = keys.select { |k| k.match(/^#{key}\./) }.sort.last
570 tr = rcs.revision[top]
571 raise "unhandled complex branch structure met: #{rev.inspect} refers #{tr.inspect}" if tr.date.nil?
572 tr.branches |= rev.symbols
576 branches.each { |k| rcs.revision.delete k }
582 def initialize(commit)
588 testfiles = @files.dup
589 tree.each { |rcs, rev| self.add(rcs, rev, testfiles) }
590 # the next line is only reached if all the adds were
591 # successful, so the merge is atomic
592 @files.replace testfiles
595 def add(rcs, rev, file_list=@files)
596 if file_list.key? rcs
597 prev = file_list[rcs]
598 if prev.log == rev.log
599 str = "re-adding existing file #{rcs.fname} (old: #{prev.rev}, new: #{rev.rev})"
601 str = "re-adding existing file #{rcs.fname} (old: #{[prev.rev, prev.log.to_s].inspect}, new: #{[rev.rev, rev.log.to_s].inspect})"
603 if prev.text != rev.text
606 @commit.warn_about str
618 @files.map do |rcs, rev|
619 if rev.state.downcase == "dead"
620 files << "D #{rcs.fname}"
622 files << "M #{rcs.mode} :#{RCS.blob rcs.fname, rev.rev} #{rcs.fname}"
629 @files.map { |rcs, rev| rcs.fname }
638 attr_accessor :date, :log, :symbols, :author, :branch
640 attr_accessor :min_date, :max_date
641 def initialize(rcs, rev)
642 raise NoBranchSupport if rev.branch
643 self.date = rev.date.dup
644 self.min_date = self.max_date = self.date
645 self.log = rev.log.dup
646 self.symbols = rev.symbols.dup
647 self.author = rev.author
648 self.branch = rev.branch
650 self.tree = Tree.new self
651 self.tree.add rcs, rev
655 [self.min_date, self.date, self.max_date, self.branch, self.symbols, self.author, self.log, self.tree.to_a]
659 warn str + " for commit on #{self.date}"
662 # Sort by date and then by number of symbols
664 ds = self.date <=> other.date
668 return self.symbols.length <=> other.symbols.length
673 self.tree.merge! commit.tree
674 if commit.max_date > self.max_date
675 self.max_date = commit.max_date
677 if commit.min_date < self.min_date
678 self.min_date = commit.min_date
680 self.symbols.merge commit.symbols
684 xbranch = self.branch || 'master'
685 xauthor = opts[:authors][self.author] || "#{self.author} <empty>"
687 numdate = self.date.tv_sec
688 xdate = "#{numdate} +0000"
691 puts "commit refs/heads/#{xbranch}"
692 puts "mark :#{RCS.commit key}"
693 puts "committer #{xauthor} #{xdate}"
694 puts "data #{xlog.length}"
695 puts xlog unless xlog.empty?
696 # TODO branching support for multi-file export
697 # puts "from :#{RCS.commit from}" if self.branch_point
700 # TODO branching support for multi-file export
701 # rev.branches.each do |sym|
702 # puts "reset refs/heads/#{sym}"
703 # puts "from :#{RCS.commit key}"
706 self.symbols.each do |sym|
707 puts "reset refs/tags/#{sym}"
708 puts "from :#{RCS.commit key}"
717 opts = GetoptLong.new(
718 # Authors file, like git-svn and git-cvsimport, more than one can be
720 ['--authors-file', '-A', GetoptLong::REQUIRED_ARGUMENT],
721 # RCS file suffix, like RCS
722 ['--rcs-suffixes', '-x', GetoptLong::REQUIRED_ARGUMENT],
723 # Date fuzziness for commits to be considered the same (in seconds)
724 ['--rcs-commit-fuzz', GetoptLong::REQUIRED_ARGUMENT],
725 # check symbols when coalescing?
726 ['--symbol-check', GetoptLong::NO_ARGUMENT],
727 ['--no-symbol-check', GetoptLong::NO_ARGUMENT],
729 ['--tag-each-rev', GetoptLong::NO_ARGUMENT],
730 ['--no-tag-each-rev', GetoptLong::NO_ARGUMENT],
731 # prepend filenames to commit logs?
732 ['--log-filename', GetoptLong::NO_ARGUMENT],
733 ['--no-log-filename', GetoptLong::NO_ARGUMENT],
734 # skip branches when exporting a whole tree?
735 ['--skip-branches', GetoptLong::NO_ARGUMENT],
736 # show current version
737 ['--version', '-v', GetoptLong::NO_ARGUMENT],
739 ['--help', '-h', '-?', GetoptLong::NO_ARGUMENT]
742 # We read options in order, but they apply to all passed parameters.
743 # TODO maybe they should only apply to the following, unless there's only one
745 opts.ordering = GetoptLong::RETURN_IN_ORDER
749 :authors => Hash.new,
754 # Read config options
755 `git config --get-all rcs.authorsfile`.each_line do |fn|
756 parse_options[:authors].merge! load_authors_file(fn.chomp)
759 parse_options[:tag_each_rev] = (
760 `git config --bool rcs.tageachrev`.chomp == 'true'
763 parse_options[:log_filename] = (
764 `git config --bool rcs.logfilename`.chomp == 'true'
767 fuzz = `git config --int rcs.commitFuzz`.chomp
768 parse_options[:commit_fuzz] = fuzz.to_i unless fuzz.empty?
770 fuzz = `git config --int rcs.tagFuzz`.chomp
771 parse_options[:tag_fuzz] = fuzz.to_i unless fuzz.empty?
773 parse_options[:symbol_check] = (
774 `git config --bool rcs.symbolcheck`.chomp == 'false'
777 opts.each do |opt, arg|
779 when '--authors-file'
780 authors = load_authors_file(arg)
781 redef = parse_options[:authors].keys & authors.keys
783 STDERR.puts "Authors file #{arg} redefines #{redef.join(', ')}" unless redef.empty?
784 parse_options[:authors].merge!(authors)
785 when '--rcs-suffixes'
787 when '--rcs-commit-fuzz'
788 parse_options[:commit_fuzz] = arg.to_i
789 when '--rcs-tag-fuzz'
790 parse_options[:tag_fuzz] = arg.to_i
791 when '--symbol-check'
792 parse_options[:symbol_check] = true
793 when '--no-symbol-check'
794 parse_options[:symbol_check] = false
795 when '--tag-each-rev'
796 parse_options[:tag_each_rev] = true
797 when '--no-tag-each-rev'
798 # this is the default, which is fine since the missing key
799 # (default) returns nil which is false in Ruby
800 parse_options[:tag_each_rev] = false
801 when '--log-filename'
802 parse_options[:log_filename] = true
803 when '--no-log-filename'
804 # this is the default, which is fine since the missing key
805 # (default) returns nil which is false in Ruby
806 parse_options[:log_filename] = false
807 when '--skip-branches'
808 parse_options[:skip_branches] = true
820 if parse_options[:tag_fuzz] < parse_options[:commit_fuzz]
821 parse_options[:tag_fuzz] = parse_options[:commit_fuzz]
826 user = Etc.getlogin || ENV['USER']
828 # steal username/email data from other init files that may contain the
832 # the user's .hgrc file for a username field
833 ['~/.hgrc', /^\s*username\s*=\s*(["'])?(.*)\1$/, 2],
834 # the user's .(g)vimrc for a changelog_username setting
835 ['~/.vimrc', /changelog_username\s*=\s*(["'])?(.*)\1$/, 2],
836 ['~/.gvimrc', /changelog_username\s*=\s*(["'])?(.*)\1$/, 2],
837 ].each do |fn, rx, idx|
838 file = File.expand_path fn
839 if File.readable?(file) and File.read(file) =~ rx
840 parse_options[:authors][user] = Regexp.last_match(idx).strip
846 if user and not user.empty? and not parse_options[:authors].has_key?(user)
847 name = ENV['GIT_AUTHOR_NAME'] || ''
848 name.replace(`git config user.name`.chomp) if name.empty?
849 name.replace(Etc.getpwnam(user).gecos) if name.empty?
852 # couldn't find a name, try to steal data from other sources
855 # if we found a name, try to find an email too
856 email = ENV['GIT_AUTHOR_EMAIL'] || ''
857 email.replace(`git config user.email`.chomp) if email.empty?
860 # couldn't find an email, try to steal data too
863 # we got both a name and email, fill the info
864 parse_options[:authors][user] = "#{name} <#{email}>"
879 file_list.each do |arg|
880 case ftype = File.ftype(arg)
886 not_found "RCS file #{arg}"
889 filename = File.basename(arg, SFX)
891 filename = File.basename(arg)
892 path = File.dirname(arg)
893 rcsfile = File.join(path, 'RCS', filename) + SFX
894 unless File.exists? rcsfile
895 rcsfile.replace File.join(path, filename) + SFX
896 unless File.exists? rcsfile
897 not_found "RCS file for #{filename} in #{path}"
901 rcs << RCS.parse(filename, rcsfile)
903 argdirname = arg.chomp(File::SEPARATOR)
904 pattern = File.join(argdirname, '**', '*' + SFX)
905 Dir.glob(pattern, File::FNM_DOTMATCH).each do |rcsfile|
906 filename = File.basename(rcsfile, SFX)
907 path = File.dirname(rcsfile)
908 # strip trailing "/RCS" if present, or "RCS" if that's
910 path.sub!(/(^|#{File::SEPARATOR})RCS$/, '')
911 # strip off the portion of the path specified
912 # on the command line from the front of the path
913 # (or delete the path completely if it is the same
914 # as the specified directory)
915 path.sub!(/^#{Regexp.escape argdirname}(#{File::SEPARATOR}|$)/, '')
916 filename = File.join(path, filename) unless path.empty?
918 rcs << RCS.parse(filename, rcsfile)
919 rescue Exception => e
921 STDERR.puts "Failed to parse #{filename} @ #{rcsfile}:#{$.}"
927 STDERR.puts "Cannot handle #{arg} of #{ftype} type"
933 rcs.first.export_commits(parse_options)
936 STDERR.puts "Preparing commits"
941 r.revision.each do |k, rev|
943 commits << RCS::Commit.new(r, rev)
944 rescue NoBranchSupport
945 if parse_options[:skip_branches]
947 STDERR.puts "Skipping revision #{rev.rev} for #{r.fname} (branch)"
955 STDERR.puts "Sorting by date"
961 STDERR.puts "RAW commits (#{commits.length}):"
963 PP.pp c.to_a, $stderr
967 STDERR.puts "#{commits.length} single-file commits"
971 STDERR.puts "Coalescing [1] by date with fuzz #{parse_options[:commit_fuzz]}"
973 thisindex = commits.size
974 commits.reverse_each do |c|
975 nextindex = thisindex
978 cfiles = Set.new c.tree.filenames
983 # test for mergeable commits by looking at following commits
984 while nextindex < commits.size
985 k = commits[nextindex]
988 # commits are date-sorted, so we know we can quit early if we are too far
989 # for coalescing to work
990 break if k.min_date > c.max_date + parse_options[:commit_fuzz]
994 kfiles = Set.new k.tree.filenames
996 if c.log != k.log or c.author != k.author or c.branch != k.branch
1000 unless c.symbols.subset?(k.symbols) or k.symbols.subset?(c.symbols)
1001 cflist = cfiles.to_a.join(', ')
1002 kflist = kfiles.to_a.join(', ')
1003 if parse_options[:symbol_check]
1005 STDERR.puts "Not coalescing #{c.log.inspect}\n\tfor (#{cflist})\n\tand (#{kflist})"
1006 STDERR.puts "\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}"
1007 STDERR.puts "\tretry with the --no-symbol-check option if you want to merge these commits anyway"
1011 STDERR.puts "Coalescing #{c.log.inspect}\n\tfor (#{cflist})\n\tand (#{kflist})"
1012 STDERR.puts "\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}"
1016 # keep track of filenames touched by commits we are not merging with,
1017 # since we don't want to merge with commits that touch them, to preserve
1018 # the monotonicity of history for each file
1019 # TODO we could forward-merge with them, unless some of our files were
1022 # if the candidate touches any file already in the commit,
1023 # we can stop looking forward
1024 break unless cfiles.intersection(kfiles).empty?
1029 # the candidate has the same log, author, branch and appropriate symbols
1030 # does it touch anything in ofiles?
1031 unless ofiles.intersection(kfiles).empty?
1033 cflist = cfiles.to_a.join(', ')
1034 kflist = kfiles.to_a.join(', ')
1035 oflist = ofiles.to_a.join(', ')
1037 STDERR.puts "Not coalescing #{c.log.inspect}\n\tfor (#{cflist})\n\tand (#{kflist})"
1038 STDERR.puts "\tbecause the latter intersects #{oflist} in #{(ofiles & kfiles).to_a.inspect}"
1046 mergeable.each do |k|
1049 rescue RuntimeError => err
1050 fuzz = c.date - k.date
1052 STDERR.puts "Fuzzy commit coalescing failed: #{err}"
1053 STDERR.puts "\tretry with commit fuzz < #{fuzz} if you don't want to see this message"
1062 STDERR.puts "[1] commits (#{commits.length}):"
1064 PP.pp c.to_a, $stderr
1068 STDERR.puts "#{commits.length} coalesced commits"
1071 commits.each { |c| c.export(parse_options) }