4 RCS fast export: run the script with the `--usage` 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}"
82 #{$0} [options] file [file ...]
84 Fast-export the RCS history of one or more files. If a directory is specified,
85 all RCS-tracked files in the directory and its descendants are exported.
87 When importing single files, their pathname is discarded during import. When
88 importing directories, only the specified directory component is discarded.
90 When importing a single file, RCS commits are converted one by one. Otherwise,
91 some heuristics is used to determine how to coalesce commits touching different
94 Currently, commits are coalesced if they share the exact same log and if their
95 date differs by no more than the user-specified fuzziness. Additionally, the
96 symbols in one of the commit must be a subset of the symbols in the other
97 commit, unless --no-symbol-check is specified or rcs.symbolCheck is set to
98 false in the git configuration.
101 git init && rcs-fast-export.rb . | git fast-import && git reset
104 --help, -h, -? display this help text
105 --authors-file, -A specify a file containing username = Full Name <email> mappings
106 --rcs-commit-fuzz fuzziness in RCS commits to be considered a single one when
107 importing multiple files
108 (in seconds, defaults to 300, i.e. 5 minutes)
109 --[no-]symbol-check [do not] check symbols when coalescing commits
110 --[no-]tag-each-rev [do not] create a lightweight tag for each RCS revision when
111 importing a single file
112 --[no-]log-filename [do not] prepend the filename to the commit log when importing
114 --skip-branches when exporting multiple files with a branched history, export
115 the main branch only instead of aborting due to the lack of
116 support for branched multi-file history export
121 rcs.authorsFile for --authors-file
122 rcs.tagEachRev for --tag-each-rev
123 rcs.logFilename for --log-filename
124 rcs.commitFuzz for --rcs-commit-fuzz
125 rcs.symbolCheck for --rcs-symbol-check
126 rcs.tagFuzz for --rcs-tag-fuzz
132 STDERR.puts "Could not find #{arg}"
135 # returns a hash that maps usernames to author names & emails
136 def load_authors_file(fn)
139 File.open(File.expand_path(fn)) do |io|
140 io.each_line do |line|
141 uname, author = line.split('=', 2)
144 STDERR.puts "Username #{uname} redefined to #{author}" if hash.has_key? uname
154 # display a message about a (recoverable) error
155 def alert(msg, action)
156 STDERR.puts "ERROR:\t#{msg}"
157 STDERR.puts "\t#{action}"
162 fields = string.split('.')
163 raise ArgumentError, "wrong number of fields for RCS date #{string}" unless fields.length == 6
169 # strip an optional final ;
174 # strip the first and last @, and de-double @@s
175 def RCS.sanitize(arg)
179 raise 'malformed first line' unless ret.first[0,1] == '@'
180 raise 'malformed last line' unless ret.last[-1,1] == '@'
181 ret.first.sub!(/^@/,'')
182 ret.last.sub!(/@$/,'')
183 ret.map { |l| l.gsub('@@','@') }
185 arg.chomp('@').sub(/^@/,'').gsub('@@','@')
192 def RCS.at_clean(arg)
193 RCS.sanitize RCS.clean(arg)
201 @@marks[key] = @@marks.length + 1
205 def RCS.blob(file, rev)
206 RCS.mark([file, rev])
209 def RCS.commit(commit)
214 attr_accessor :head, :comment, :desc, :revision, :fname, :mode
215 def initialize(fname, executable)
220 @revision = Hash.new { |h, r| h[r] = Revision.new(self, r) }
221 @mode = executable ? '755' : '644'
224 def has_revision?(rev)
225 @revision.has_key?(rev) and not @revision[rev].author.nil?
228 def export_commits(opts={})
231 until @revision.empty?
234 # a string sort is a very good candidate for
235 # export order, getting a miss only for
236 # multi-digit revision components
237 keys = @revision.keys.sort
239 STDERR.puts "commit export loop ##{counter}"
240 STDERR.puts "\t#{exported.length} commits exported so far: #{exported.join(', ')}" unless exported.empty?
241 STDERR.puts "\t#{keys.size} to export: #{keys.join(', ')}"
245 # the parent commit is rev.next if we're on the
246 # master branch (rev.branch is nil) or
247 # rev.diff_base otherwise
248 from = rev.branch.nil? ? rev.next : rev.diff_base
249 # A commit can only be exported if it has no
250 # parent, or if the parent has been exported
251 # already. Skip this commit otherwise
252 if from and not exported.include? from
256 branch = rev.branch || 'master'
257 author = opts[:authors][rev.author] || "#{rev.author} <empty>"
258 date = "#{rev.date.tv_sec} +0000"
260 if opts[:log_filename]
261 log << @fname << ": "
265 puts "commit refs/heads/#{branch}"
266 puts "mark :#{RCS.commit key}"
267 puts "committer #{author} #{date}"
268 puts "data #{log.length}"
269 puts log unless log.empty?
270 puts "from :#{RCS.commit from}" if from
271 puts "M #{@mode} :#{RCS.blob @fname, key} #{@fname}"
273 # TODO FIXME this *should* be safe, in
274 # that it should not unduly move
275 # branches back in time, but I'm not
277 rev.branches.each do |sym|
278 puts "reset refs/heads/#{sym}"
279 puts "from :#{RCS.commit key}"
281 rev.symbols.each do |sym|
282 puts "reset refs/tags/#{sym}"
283 puts "from :#{RCS.commit key}"
285 if opts[:tag_each_rev]
286 puts "reset refs/tags/#{key}"
287 puts "from :#{RCS.commit key}"
292 exported.each { |k| @revision.delete(k) }
298 attr_accessor :rev, :author, :state, :next
299 attr_accessor :branches, :log, :text, :symbols
300 attr_accessor :branch, :diff_base, :branch_point
302 def initialize(file, rev)
319 @date = Time.rcs(str)
324 ret = "blob\nmark :#{RCS.blob @file.fname, @rev}\ndata #{str.length}\n#{str}\n"
329 def RCS.parse(fname, rcsfile)
330 rcs = RCS::File.new(fname, ::File.executable?(rcsfile))
332 ::File.open(rcsfile, 'r:ASCII-8BIT') do |file|
337 file.each_line do |line|
340 command, args = line.split($;,2)
341 next if command.empty?
343 if command.chomp!(';')
344 STDERR.puts "Skipping empty command #{command.inspect}" if $DEBUG
350 rcs.head = RCS.clean(args.chomp)
356 rcs.comment = RCS.at_clean(args.chomp)
359 if rcs.has_revision?(rev)
360 status.push :revision_data
362 status.push :new_revision
367 status.push :read_lines
368 when 'branch', 'access', 'locks', 'expand'
369 STDERR.puts "Skipping unhandled command #{command.inspect}" if $DEBUG
370 status.push :skipping_lines
374 raise "Unknown command #{command.inspect}"
377 status.pop if line.strip.chomp!(';')
379 # we can have multiple symbols per line
380 pairs = line.strip.split($;)
382 sym, rev = pair.strip.split(':',2);
384 status.pop if rev.chomp!(';')
385 rcs.revision[rev].symbols << sym
391 rcs.desc.replace lines.dup
394 # we sanitize lines as we read them
396 actual_line = line.dup
398 # the first line must begin with a @, which we strip
400 ats = line.match(/^@+/)
401 raise 'malformed line' unless ats
402 actual_line.replace line.sub(/^@/,'')
405 # if the line ends with an ODD number of @, it's the
406 # last line -- we work on actual_line so that content
407 # such as @\n or @ work correctly (they would be
408 # encoded respectively as ['@@@\n','@\n'] and
410 ats = actual_line.chomp.match(/@+$/)
411 if nomore = (ats && Regexp.last_match(0).length.odd?)
412 actual_line.replace actual_line.chomp.sub(/@$/,'')
414 lines << actual_line.gsub('@@','@')
421 when /^date\s+(\S+);\s+author\s+(\S+);\s+state\s+(\S+);$/
422 rcs.revision[rev].date = $1
423 rcs.revision[rev].author = $2
424 rcs.revision[rev].state = $3
427 when /^branches(?:\s+|$)/
428 status.push :branches
430 line = line.sub(/^branches\s+/,'')
433 when /^next\s+(\S+)?;$/
434 nxt = rcs.revision[rev].next = $1
436 raise "multiple diff_bases for #{nxt}" unless rcs.revision[nxt].diff_base.nil?
437 rcs.revision[nxt].diff_base = rev
438 rcs.revision[nxt].branch = rcs.revision[rev].branch
443 candidate = line.split(';',2)
444 candidate.first.strip.split.each do |branch|
445 raise "multiple diff_bases for #{branch}" unless rcs.revision[branch].diff_base.nil?
446 rcs.revision[branch].diff_base = rev
447 # we drop the last number from the branch name
448 rcs.revision[branch].branch = branch.sub(/\.\d+$/,'.x')
449 rcs.revision[branch].branch_point = rev
451 status.pop if candidate.length > 1
457 status.push :read_lines
465 status.push :read_lines
470 rcs.revision[rev].log.replace lines.dup
473 rcs.revision[rev].text.replace lines.dup
474 puts rcs.revision[rev].blob
477 difflines.replace lines.dup
478 difflines.pop if difflines.last.empty?
479 if difflines.first.chomp.empty?
480 alert "malformed diff: empty initial line @ #{rcsfile}:#{file.lineno-difflines.length-1}", "skipping"
483 base = rcs.revision[rev].diff_base
484 unless rcs.revision[base].text
487 raise 'no diff base!'
491 rcs.revision[base].text.each { |l| buffer << [l.dup] }
497 while l = difflines.shift
499 raise 'negative index during insertion' if index < 0
500 raise 'negative count during insertion' if count < 0
503 # collected all the lines, put the before
508 buffer[index].unshift(*adding)
515 raise "malformed diff @ #{rcsfile}:#{file.lineno-difflines.length-1} `#{l}`" unless l =~ /^([ad])(\d+) (\d+)$/
521 # for deletion, index 1 is the first index, so the Ruby
522 # index is one less than the diff one
524 # we replace them with empty string so that 'a' commands
525 # referring to the same line work properly
532 # addition will prepend the appropriate lines
533 # to the given index, and in this case Ruby
534 # and diff indices are the same
539 # turn the buffer into an array of lines, deleting the empty ones
540 buffer.delete_if { |l| l.empty? }
543 rcs.revision[rev].text = buffer
544 puts rcs.revision[rev].blob
547 raise "Unknown status #{status.last}"
552 # clean up the symbols/branches: look for revisions that have
553 # one or more symbols but no dates, and make them into
554 # branches, pointing to the highest commit with that key
556 keys = rcs.revision.keys
557 rcs.revision.each do |key, rev|
558 if rev.date.nil? and not rev.symbols.empty?
559 top = keys.select { |k| k.match(/^#{key}\./) }.sort.last
560 tr = rcs.revision[top]
561 raise "unhandled complex branch structure met: #{rev.inspect} refers #{tr.inspect}" if tr.date.nil?
562 tr.branches |= rev.symbols
566 branches.each { |k| rcs.revision.delete k }
572 def initialize(commit)
578 testfiles = @files.dup
579 tree.each { |rcs, rev| self.add(rcs, rev, testfiles) }
580 # the next line is only reached if all the adds were
581 # successful, so the merge is atomic
582 @files.replace testfiles
585 def add(rcs, rev, file_list=@files)
586 if file_list.key? rcs
587 prev = file_list[rcs]
588 if prev.log == rev.log
589 str = "re-adding existing file #{rcs.fname} (old: #{prev.rev}, new: #{rev.rev})"
591 str = "re-adding existing file #{rcs.fname} (old: #{[prev.rev, prev.log.to_s].inspect}, new: #{[rev.rev, rev.log.to_s].inspect})"
593 if prev.text != rev.text
596 @commit.warn_about str
608 @files.map do |rcs, rev|
609 if rev.state.downcase == "dead"
610 files << "D #{rcs.fname}"
612 files << "M #{rcs.mode} :#{RCS.blob rcs.fname, rev.rev} #{rcs.fname}"
619 @files.map { |rcs, rev| rcs.fname }
628 attr_accessor :date, :log, :symbols, :author, :branch
630 attr_accessor :min_date, :max_date
631 def initialize(rcs, rev)
632 raise NoBranchSupport if rev.branch
633 self.date = rev.date.dup
634 self.min_date = self.max_date = self.date
635 self.log = rev.log.dup
636 self.symbols = rev.symbols.dup
637 self.author = rev.author
638 self.branch = rev.branch
640 self.tree = Tree.new self
641 self.tree.add rcs, rev
645 [self.min_date, self.date, self.max_date, self.branch, self.symbols, self.author, self.log, self.tree.to_a]
649 warn str + " for commit on #{self.date}"
652 # Sort by date and then by number of symbols
654 ds = self.date <=> other.date
658 return self.symbols.length <=> other.symbols.length
663 self.tree.merge! commit.tree
664 if commit.max_date > self.max_date
665 self.max_date = commit.max_date
667 if commit.min_date < self.min_date
668 self.min_date = commit.min_date
670 self.symbols.merge commit.symbols
674 xbranch = self.branch || 'master'
675 xauthor = opts[:authors][self.author] || "#{self.author} <empty>"
677 numdate = self.date.tv_sec
678 xdate = "#{numdate} +0000"
681 puts "commit refs/heads/#{xbranch}"
682 puts "mark :#{RCS.commit key}"
683 puts "committer #{xauthor} #{xdate}"
684 puts "data #{xlog.length}"
685 puts xlog unless xlog.empty?
686 # TODO branching support for multi-file export
687 # puts "from :#{RCS.commit from}" if self.branch_point
690 # TODO branching support for multi-file export
691 # rev.branches.each do |sym|
692 # puts "reset refs/heads/#{sym}"
693 # puts "from :#{RCS.commit key}"
696 self.symbols.each do |sym|
697 puts "reset refs/tags/#{sym}"
698 puts "from :#{RCS.commit key}"
707 opts = GetoptLong.new(
708 # Authors file, like git-svn and git-cvsimport, more than one can be
710 ['--authors-file', '-A', GetoptLong::REQUIRED_ARGUMENT],
711 # RCS file suffix, like RCS
712 ['--rcs-suffixes', '-x', GetoptLong::REQUIRED_ARGUMENT],
713 # Date fuzziness for commits to be considered the same (in seconds)
714 ['--rcs-commit-fuzz', GetoptLong::REQUIRED_ARGUMENT],
715 # check symbols when coalescing?
716 ['--symbol-check', GetoptLong::NO_ARGUMENT],
717 ['--no-symbol-check', GetoptLong::NO_ARGUMENT],
719 ['--tag-each-rev', GetoptLong::NO_ARGUMENT],
720 ['--no-tag-each-rev', GetoptLong::NO_ARGUMENT],
721 # prepend filenames to commit logs?
722 ['--log-filename', GetoptLong::NO_ARGUMENT],
723 ['--no-log-filename', GetoptLong::NO_ARGUMENT],
724 # skip branches when exporting a whole tree?
725 ['--skip-branches', GetoptLong::NO_ARGUMENT],
726 # show current version
727 ['--version', '-v', GetoptLong::NO_ARGUMENT],
729 ['--help', '-h', '-?', GetoptLong::NO_ARGUMENT]
732 # We read options in order, but they apply to all passed parameters.
733 # TODO maybe they should only apply to the following, unless there's only one
735 opts.ordering = GetoptLong::RETURN_IN_ORDER
739 :authors => Hash.new,
744 # Read config options
745 `git config --get-all rcs.authorsfile`.each_line do |fn|
746 parse_options[:authors].merge! load_authors_file(fn.chomp)
749 parse_options[:tag_each_rev] = (
750 `git config --bool rcs.tageachrev`.chomp == 'true'
753 parse_options[:log_filename] = (
754 `git config --bool rcs.logfilename`.chomp == 'true'
757 fuzz = `git config --int rcs.commitFuzz`.chomp
758 parse_options[:commit_fuzz] = fuzz.to_i unless fuzz.empty?
760 fuzz = `git config --int rcs.tagFuzz`.chomp
761 parse_options[:tag_fuzz] = fuzz.to_i unless fuzz.empty?
763 parse_options[:symbol_check] = (
764 `git config --bool rcs.symbolcheck`.chomp == 'false'
767 opts.each do |opt, arg|
769 when '--authors-file'
770 authors = load_authors_file(arg)
771 redef = parse_options[:authors].keys & authors.keys
772 STDERR.puts "Authors file #{arg} redefines #{redef.join(', ')}" unless redef.empty?
773 parse_options[:authors].merge!(authors)
774 when '--rcs-suffixes'
776 when '--rcs-commit-fuzz'
777 parse_options[:commit_fuzz] = arg.to_i
778 when '--rcs-tag-fuzz'
779 parse_options[:tag_fuzz] = arg.to_i
780 when '--symbol-check'
781 parse_options[:symbol_check] = true
782 when '--no-symbol-check'
783 parse_options[:symbol_check] = false
784 when '--tag-each-rev'
785 parse_options[:tag_each_rev] = true
786 when '--no-tag-each-rev'
787 # this is the default, which is fine since the missing key
788 # (default) returns nil which is false in Ruby
789 parse_options[:tag_each_rev] = false
790 when '--log-filename'
791 parse_options[:log_filename] = true
792 when '--no-log-filename'
793 # this is the default, which is fine since the missing key
794 # (default) returns nil which is false in Ruby
795 parse_options[:log_filename] = false
796 when '--skip-branches'
797 parse_options[:skip_branches] = true
809 if parse_options[:tag_fuzz] < parse_options[:commit_fuzz]
810 parse_options[:tag_fuzz] = parse_options[:commit_fuzz]
815 user = Etc.getlogin || ENV['USER']
817 # steal username/email data from other init files that may contain the
821 # the user's .hgrc file for a username field
822 ['~/.hgrc', /^\s*username\s*=\s*(["'])?(.*)\1$/, 2],
823 # the user's .(g)vimrc for a changelog_username setting
824 ['~/.vimrc', /changelog_username\s*=\s*(["'])?(.*)\1$/, 2],
825 ['~/.gvimrc', /changelog_username\s*=\s*(["'])?(.*)\1$/, 2],
826 ].each do |fn, rx, idx|
827 file = File.expand_path fn
828 if File.readable?(file) and File.read(file) =~ rx
829 parse_options[:authors][user] = Regexp.last_match(idx).strip
835 if user and not user.empty? and not parse_options[:authors].has_key?(user)
836 name = ENV['GIT_AUTHOR_NAME'] || ''
837 name.replace(`git config user.name`.chomp) if name.empty?
838 name.replace(Etc.getpwnam(user).gecos) if name.empty?
841 # couldn't find a name, try to steal data from other sources
844 # if we found a name, try to find an email too
845 email = ENV['GIT_AUTHOR_EMAIL'] || ''
846 email.replace(`git config user.email`.chomp) if email.empty?
849 # couldn't find an email, try to steal data too
852 # we got both a name and email, fill the info
853 parse_options[:authors][user] = "#{name} <#{email}>"
868 file_list.each do |arg|
869 case ftype = File.ftype(arg)
875 not_found "RCS file #{arg}"
878 filename = File.basename(arg, SFX)
880 filename = File.basename(arg)
881 path = File.dirname(arg)
882 rcsfile = File.join(path, 'RCS', filename) + SFX
883 unless File.exists? rcsfile
884 rcsfile.replace File.join(path, filename) + SFX
885 unless File.exists? rcsfile
886 not_found "RCS file for #{filename} in #{path}"
890 rcs << RCS.parse(filename, rcsfile)
892 argdirname = arg.chomp(File::SEPARATOR)
893 pattern = File.join(argdirname, '**', '*' + SFX)
894 Dir.glob(pattern).each do |rcsfile|
895 filename = File.basename(rcsfile, SFX)
896 path = File.dirname(rcsfile)
897 # strip trailing "/RCS" if present, or "RCS" if that's
899 path.sub!(/(^|#{File::SEPARATOR})RCS$/, '')
900 # strip off the portion of the path sepecified
901 # on the command line from the front of the path
902 # (or delete the path completely if it is the same
903 # as the specified directory)
904 path.sub!(/^#{Regexp.escape argdirname}(#{File::SEPARATOR}|$)/, '')
905 filename = File.join(path, filename) unless path.empty?
907 rcs << RCS.parse(filename, rcsfile)
908 rescue Exception => e
909 STDERR.puts "Failed to parse #{filename} @ #{rcsfile}:#{$.}"
914 STDERR.puts "Cannot handle #{arg} of #{ftype} type"
920 rcs.first.export_commits(parse_options)
922 STDERR.puts "Preparing commits"
927 r.revision.each do |k, rev|
929 commits << RCS::Commit.new(r, rev)
930 rescue NoBranchSupport
931 if parse_options[:skip_branches]
932 STDERR.puts "Skipping revision #{rev.rev} for #{r.fname} (branch)"
939 STDERR.puts "Sorting by date"
944 STDERR.puts "RAW commits (#{commits.length}):"
946 PP.pp c.to_a, $stderr
949 STDERR.puts "#{commits.length} single-file commits"
952 STDERR.puts "Coalescing [1] by date with fuzz #{parse_options[:commit_fuzz]}"
954 thisindex = commits.size
955 commits.reverse_each do |c|
956 nextindex = thisindex
959 cfiles = Set.new c.tree.filenames
964 # test for mergeable commits by looking at following commits
965 while nextindex < commits.size
966 k = commits[nextindex]
969 # commits are date-sorted, so we know we can quit early if we are too far
970 # for coalescing to work
971 break if k.min_date > c.max_date + parse_options[:commit_fuzz]
975 kfiles = Set.new k.tree.filenames
977 if c.log != k.log or c.author != k.author or c.branch != k.branch
981 unless c.symbols.subset?(k.symbols) or k.symbols.subset?(c.symbols)
982 cflist = cfiles.to_a.join(', ')
983 kflist = kfiles.to_a.join(', ')
984 if parse_options[:symbol_check]
985 STDERR.puts "Not coalescing #{c.log.inspect}\n\tfor (#{cflist})\n\tand (#{kflist})"
986 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}"
987 STDERR.puts "\tretry with the --no-symbol-check option if you want to merge these commits anyway"
990 STDERR.puts "Coalescing #{c.log.inspect}\n\tfor (#{cflist})\n\tand (#{kflist})"
991 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}"
995 # keep track of filenames touched by commits we are not merging with,
996 # since we don't want to merge with commits that touch them, to preserve
997 # the monotonicity of history for each file
998 # TODO we could forward-merge with them, unless some of our files were
1001 # if the candidate touches any file already in the commit,
1002 # we can stop looking forward
1003 break unless cfiles.intersection(kfiles).empty?
1008 # the candidate has the same log, author, branch and appropriate symbols
1009 # does it touch anything in ofiles?
1010 unless ofiles.intersection(kfiles).empty?
1012 cflist = cfiles.to_a.join(', ')
1013 kflist = kfiles.to_a.join(', ')
1014 oflist = ofiles.to_a.join(', ')
1015 STDERR.puts "Not coalescing #{c.log.inspect}\n\tfor (#{cflist})\n\tand (#{kflist})"
1016 STDERR.puts "\tbecause the latter intersects #{oflist} in #{(ofiles & kfiles).to_a.inspect}"
1024 mergeable.each do |k|
1027 rescue RuntimeError => err
1028 fuzz = c.date - k.date
1029 STDERR.puts "Fuzzy commit coalescing failed: #{err}"
1030 STDERR.puts "\tretry with commit fuzz < #{fuzz} if you don't want to see this message"
1038 STDERR.puts "[1] commits (#{commits.length}):"
1040 PP.pp c.to_a, $stderr
1043 STDERR.puts "#{commits.length} coalesced commits"
1046 commits.each { |c| c.export(parse_options) }