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 --ignore ignore the specified files (shell pattern)
108 --rcs-commit-fuzz fuzziness in RCS commits to be considered a single one when
109 importing multiple files
110 (in seconds, defaults to 300, i.e. 5 minutes)
111 --[no-]warn-missing-authors
112 [do not] warn about usernames missing from the map file
113 --[no-]symbol-check [do not] check symbols when coalescing commits
114 --[no-]tag-each-rev [do not] create a lightweight tag for each RCS revision when
115 importing a single file
116 --[no-]log-filename [do not] prepend the filename to the commit log when importing
118 --skip-branches when exporting multiple files with a branched history, export
119 the main branch only instead of aborting due to the lack of
120 support for branched multi-file history export
125 rcs.authorsFile for --authors-file
126 rcs.tagEachRev for --tag-each-rev
127 rcs.logFilename for --log-filename
128 rcs.commitFuzz for --rcs-commit-fuzz
129 rcs.warnMissingAuthors for --warn-missing-authors
130 rcs.symbolCheck for --rcs-symbol-check
131 rcs.tagFuzz for --rcs-tag-fuzz
142 warning "Could not find #{arg}"
145 # returns a hash that maps usernames to author names & emails
146 def load_authors_file(fn)
149 File.open(File.expand_path(fn)) do |io|
150 io.each_line do |line|
151 uname, author = line.split('=', 2)
154 warning "Username #{uname} redefined to #{author}" if hash.has_key? uname
164 def username_to_author(name, opts)
166 raise "no authors map defined" unless map and Hash === map
168 # if name is not found in map, provide a default one, optionally giving a warning (once)
170 warning "no author found for #{name}" if opts[:warn_missing_authors]
171 map[name] = "#{name} <empty>"
176 # display a message about a (recoverable) error
177 def alert(msg, action)
178 STDERR.puts "ERROR:\t#{msg}"
179 STDERR.puts "\t#{action}"
184 fields = string.split('.')
185 raise ArgumentError, "wrong number of fields for RCS date #{string}" unless fields.length == 6
186 # in Ruby 1.9, '99' is interpreted as year 99, not year 1999
187 if fields.first.length < 3
188 fields.first.insert 0, '19'
195 # strip an optional final ;
200 # strip the first and last @, and de-double @@s
201 def RCS.sanitize(arg)
205 raise 'malformed first line' unless ret.first[0,1] == '@'
206 raise 'malformed last line' unless ret.last[-1,1] == '@'
207 ret.first.sub!(/^@/,'')
208 ret.last.sub!(/@$/,'')
209 ret.map { |l| l.gsub('@@','@') }
211 arg.chomp('@').sub(/^@/,'').gsub('@@','@')
218 def RCS.at_clean(arg)
219 RCS.sanitize RCS.clean(arg)
227 @@marks[key] = @@marks.length + 1
231 def RCS.blob(file, rev)
232 RCS.mark([file, rev])
235 def RCS.commit(commit)
240 attr_accessor :head, :comment, :desc, :revision, :fname, :mode
241 def initialize(fname, executable)
246 @revision = Hash.new { |h, r| h[r] = Revision.new(self, r) }
247 @mode = executable ? '755' : '644'
250 def has_revision?(rev)
251 @revision.has_key?(rev) and not @revision[rev].author.nil?
254 def export_commits(opts={})
257 until @revision.empty?
260 # a string sort is a very good candidate for
261 # export order, getting a miss only for
262 # multi-digit revision components
263 keys = @revision.keys.sort
265 warning "commit export loop ##{counter}"
266 warning "\t#{exported.length} commits exported so far: #{exported.join(', ')}" unless exported.empty?
267 warning "\t#{keys.size} to export: #{keys.join(', ')}"
271 # the parent commit is rev.next if we're on the
272 # master branch (rev.branch is nil) or
273 # rev.diff_base otherwise
274 from = rev.branch.nil? ? rev.next : rev.diff_base
275 # A commit can only be exported if it has no
276 # parent, or if the parent has been exported
277 # already. Skip this commit otherwise
278 if from and not exported.include? from
282 branch = rev.branch || 'master'
283 author = username_to_author(rev.author, opts)
284 date = "#{rev.date.tv_sec} +0000"
286 if opts[:log_filename]
287 log << @fname << ": "
291 puts "commit refs/heads/#{branch}"
292 puts "mark :#{RCS.commit key}"
293 puts "committer #{author} #{date}"
294 puts "data #{log.length}"
295 puts log unless log.empty?
296 puts "from :#{RCS.commit from}" if from
297 puts "M #{@mode} :#{RCS.blob @fname, key} #{@fname}"
299 # TODO FIXME this *should* be safe, in
300 # that it should not unduly move
301 # branches back in time, but I'm not
303 rev.branches.each do |sym|
304 puts "reset refs/heads/#{sym}"
305 puts "from :#{RCS.commit key}"
307 rev.symbols.each do |sym|
308 puts "reset refs/tags/#{sym}"
309 puts "from :#{RCS.commit key}"
311 if opts[:tag_each_rev]
312 puts "reset refs/tags/#{key}"
313 puts "from :#{RCS.commit key}"
318 exported.each { |k| @revision.delete(k) }
324 attr_accessor :rev, :author, :state, :next
325 attr_accessor :branches, :log, :text, :symbols
326 attr_accessor :branch, :diff_base, :branch_point
328 def initialize(file, rev)
345 @date = Time.rcs(str)
350 ret = "blob\nmark :#{RCS.blob @file.fname, @rev}\ndata #{str.length}\n#{str}\n"
355 # TODO: what if a revision does not end with newline?
356 def RCS.expand_keywords(rcsfile, revision)
357 ret = ::File.read("|co -q -p#{revision} #{rcsfile}")
359 ret.each_line do |line|
365 def RCS.parse(fname, rcsfile, opts={})
366 rcs = RCS::File.new(fname, ::File.executable?(rcsfile))
368 ::File.open(rcsfile, 'r:ASCII-8BIT') do |file|
373 file.each_line do |line|
376 command, args = line.split($;,2)
377 next if command.empty?
379 if command.chomp!(';')
380 warning "Skipping empty command #{command.inspect}" if $DEBUG
386 rcs.head = RCS.clean(args.chomp)
392 rcs.comment = RCS.at_clean(args.chomp)
395 if rcs.has_revision?(rev)
396 status.push :revision_data
398 status.push :new_revision
403 status.push :read_lines
404 when 'branch', 'access', 'locks', 'expand'
405 warning "Skipping unhandled command #{command.inspect}" if $DEBUG
406 status.push :skipping_lines
410 raise "Unknown command #{command.inspect}"
413 status.pop if line.strip.chomp!(';')
415 # we can have multiple symbols per line
416 pairs = line.strip.split($;)
418 sym, rev = pair.strip.split(':',2);
420 status.pop if rev.chomp!(';')
421 rcs.revision[rev].symbols << sym
427 rcs.desc.replace lines.dup
430 # we sanitize lines as we read them
432 actual_line = line.dup
434 # the first line must begin with a @, which we strip
436 ats = line.match(/^@+/)
437 raise 'malformed line' unless ats
438 actual_line.replace line.sub(/^@/,'')
441 # if the line ends with an ODD number of @, it's the
442 # last line -- we work on actual_line so that content
443 # such as @\n or @ work correctly (they would be
444 # encoded respectively as ['@@@\n','@\n'] and
446 ats = actual_line.chomp.match(/@+$/)
447 if nomore = (ats && Regexp.last_match(0).length.odd?)
448 actual_line.replace actual_line.chomp.sub(/@$/,'')
450 lines << actual_line.gsub('@@','@')
457 when /^date\s+(\S+);\s+author\s+(\S+);\s+state\s+(\S+);$/
458 rcs.revision[rev].date = $1
459 rcs.revision[rev].author = $2
460 rcs.revision[rev].state = $3
463 when /^branches(?:\s+|$)/
464 status.push :branches
466 line = line.sub(/^branches\s+/,'')
469 when /^next\s+(\S+)?;$/
470 nxt = rcs.revision[rev].next = $1
472 raise "multiple diff_bases for #{nxt}" unless rcs.revision[nxt].diff_base.nil?
473 rcs.revision[nxt].diff_base = rev
474 rcs.revision[nxt].branch = rcs.revision[rev].branch
479 candidate = line.split(';',2)
480 candidate.first.strip.split.each do |branch|
481 raise "multiple diff_bases for #{branch}" unless rcs.revision[branch].diff_base.nil?
482 rcs.revision[branch].diff_base = rev
483 # we drop the last number from the branch name
484 rcs.revision[branch].branch = branch.sub(/\.\d+$/,'.x')
485 rcs.revision[branch].branch_point = rev
487 status.pop if candidate.length > 1
493 status.push :read_lines
501 status.push :read_lines
506 rcs.revision[rev].log.replace lines.dup
509 rcs.revision[rev].text.replace lines.dup
510 if opts[:expand_keywords]
511 rcs.revision[rev].text.replace RCS.expand_keywords(rcsfile, rev)
513 puts rcs.revision[rev].blob
516 difflines.replace lines.dup
517 difflines.pop if difflines.last.empty?
518 if difflines.first.chomp.empty?
519 alert "malformed diff: empty initial line @ #{rcsfile}:#{file.lineno-difflines.length-1}", "skipping"
521 end unless difflines.empty?
522 base = rcs.revision[rev].diff_base
523 unless rcs.revision[base].text
526 raise 'no diff base!'
530 rcs.revision[base].text.each { |l| buffer << [l.dup] }
536 while l = difflines.shift
538 raise 'negative index during insertion' if index < 0
539 raise 'negative count during insertion' if count < 0
542 # collected all the lines, put the before
547 buffer[index].unshift(*adding)
554 raise "malformed diff @ #{rcsfile}:#{file.lineno-difflines.length-1} `#{l}`" unless l =~ /^([ad])(\d+) (\d+)$/
560 # for deletion, index 1 is the first index, so the Ruby
561 # index is one less than the diff one
563 # we replace them with empty string so that 'a' commands
564 # referring to the same line work properly
571 # addition will prepend the appropriate lines
572 # to the given index, and in this case Ruby
573 # and diff indices are the same
578 # turn the buffer into an array of lines, deleting the empty ones
579 buffer.delete_if { |l| l.empty? }
582 rcs.revision[rev].text = buffer
583 if opts[:expand_keywords]
584 rcs.revision[rev].text.replace RCS.expand_keywords(rcsfile, rev)
586 puts rcs.revision[rev].blob
589 raise "Unknown status #{status.last}"
594 # clean up the symbols/branches: look for revisions that have
595 # one or more symbols but no dates, and make them into
596 # branches, pointing to the highest commit with that key
598 keys = rcs.revision.keys
599 rcs.revision.each do |key, rev|
600 if rev.date.nil? and not rev.symbols.empty?
601 top = keys.select { |k| k.match(/^#{key}\./) }.sort.last
602 tr = rcs.revision[top]
603 raise "unhandled complex branch structure met: #{rev.inspect} refers #{tr.inspect}" if tr.date.nil?
604 tr.branches |= rev.symbols
608 branches.each { |k| rcs.revision.delete k }
614 def initialize(commit)
620 testfiles = @files.dup
621 tree.each { |rcs, rev| self.add(rcs, rev, testfiles) }
622 # the next line is only reached if all the adds were
623 # successful, so the merge is atomic
624 @files.replace testfiles
627 def add(rcs, rev, file_list=@files)
628 if file_list.key? rcs
629 prev = file_list[rcs]
630 if prev.log == rev.log
631 str = "re-adding existing file #{rcs.fname} (old: #{prev.rev}, new: #{rev.rev})"
633 str = "re-adding existing file #{rcs.fname} (old: #{[prev.rev, prev.log.to_s].inspect}, new: #{[rev.rev, rev.log.to_s].inspect})"
635 if prev.text != rev.text
638 @commit.warn_about str
650 @files.map do |rcs, rev|
651 if rev.state.downcase == "dead"
652 files << "D #{rcs.fname}"
654 files << "M #{rcs.mode} :#{RCS.blob rcs.fname, rev.rev} #{rcs.fname}"
661 @files.map { |rcs, rev| rcs.fname }
670 attr_accessor :date, :log, :symbols, :author, :branch
672 attr_accessor :min_date, :max_date
673 def initialize(rcs, rev)
674 raise NoBranchSupport if rev.branch
675 self.date = rev.date.dup
676 self.min_date = self.max_date = self.date
677 self.log = rev.log.dup
678 self.symbols = rev.symbols.dup
679 self.author = rev.author
680 self.branch = rev.branch
682 self.tree = Tree.new self
683 self.tree.add rcs, rev
687 [self.min_date, self.date, self.max_date, self.branch, self.symbols, self.author, self.log, self.tree.to_a]
691 warn str + " for commit on #{self.date}"
694 # Sort by date and then by number of symbols
696 ds = self.date <=> other.date
700 return self.symbols.length <=> other.symbols.length
705 self.tree.merge! commit.tree
706 if commit.max_date > self.max_date
707 self.max_date = commit.max_date
709 if commit.min_date < self.min_date
710 self.min_date = commit.min_date
712 self.symbols.merge commit.symbols
716 xbranch = self.branch || 'master'
717 xauthor = username_to_author(self.author, opts)
719 numdate = self.date.tv_sec
720 xdate = "#{numdate} +0000"
723 puts "commit refs/heads/#{xbranch}"
724 puts "mark :#{RCS.commit key}"
725 puts "committer #{xauthor} #{xdate}"
726 puts "data #{xlog.length}"
727 puts xlog unless xlog.empty?
728 # TODO branching support for multi-file export
729 # puts "from :#{RCS.commit from}" if self.branch_point
732 # TODO branching support for multi-file export
733 # rev.branches.each do |sym|
734 # puts "reset refs/heads/#{sym}"
735 # puts "from :#{RCS.commit key}"
738 self.symbols.each do |sym|
739 puts "reset refs/tags/#{sym}"
740 puts "from :#{RCS.commit key}"
749 opts = GetoptLong.new(
750 # Authors file, like git-svn and git-cvsimport, more than one can be
752 ['--authors-file', '-A', GetoptLong::REQUIRED_ARGUMENT],
753 # Use "co" to obtain the actual revision with keywords expanded.
754 ['--expand-keywords', GetoptLong::NO_ARGUMENT],
755 # RCS file suffix, like RCS
756 ['--rcs-suffixes', '-x', GetoptLong::REQUIRED_ARGUMENT],
757 # Shell pattern to identify files to be ignored
758 ['--ignore', GetoptLong::REQUIRED_ARGUMENT],
759 # Date fuzziness for commits to be considered the same (in seconds)
760 ['--rcs-commit-fuzz', GetoptLong::REQUIRED_ARGUMENT],
761 # warn about usernames missing in authors file map?
762 ['--warn-missing-authors', GetoptLong::NO_ARGUMENT],
763 ['--no-warn-missing-authors', GetoptLong::NO_ARGUMENT],
764 # check symbols when coalescing?
765 ['--symbol-check', GetoptLong::NO_ARGUMENT],
766 ['--no-symbol-check', GetoptLong::NO_ARGUMENT],
768 ['--tag-each-rev', GetoptLong::NO_ARGUMENT],
769 ['--no-tag-each-rev', GetoptLong::NO_ARGUMENT],
770 # prepend filenames to commit logs?
771 ['--log-filename', GetoptLong::NO_ARGUMENT],
772 ['--no-log-filename', GetoptLong::NO_ARGUMENT],
773 # skip branches when exporting a whole tree?
774 ['--skip-branches', GetoptLong::NO_ARGUMENT],
775 # show current version
776 ['--version', '-v', GetoptLong::NO_ARGUMENT],
778 ['--help', '-h', '-?', GetoptLong::NO_ARGUMENT]
781 # We read options in order, but they apply to all passed parameters.
782 # TODO maybe they should only apply to the following, unless there's only one
784 opts.ordering = GetoptLong::RETURN_IN_ORDER
788 :authors => Hash.new,
789 :ignore => Array.new,
794 # Read config options
795 `git config --get-all rcs.authorsfile`.each_line do |fn|
796 parse_options[:authors].merge! load_authors_file(fn.chomp)
799 parse_options[:tag_each_rev] = (
800 `git config --bool rcs.tageachrev`.chomp == 'true'
803 parse_options[:log_filename] = (
804 `git config --bool rcs.logfilename`.chomp == 'true'
807 fuzz = `git config --int rcs.commitFuzz`.chomp
808 parse_options[:commit_fuzz] = fuzz.to_i unless fuzz.empty?
810 fuzz = `git config --int rcs.tagFuzz`.chomp
811 parse_options[:tag_fuzz] = fuzz.to_i unless fuzz.empty?
813 parse_options[:symbol_check] = (
814 `git config --bool rcs.symbolcheck`.chomp == 'false'
817 parse_options[:warn_missing_authors] = (
818 `git config --bool rcs.warnmissingauthors`.chomp == 'false'
821 opts.each do |opt, arg|
823 when '--authors-file'
824 authors = load_authors_file(arg)
825 redef = parse_options[:authors].keys & authors.keys
826 warning "Authors file #{arg} redefines #{redef.join(', ')}" unless redef.empty?
827 parse_options[:authors].merge!(authors)
828 when '--expand-keywords'
829 parse_options[:expand_keywords] = true
830 when '--rcs-suffixes'
833 parse_options[:ignore] << arg
834 when '--rcs-commit-fuzz'
835 parse_options[:commit_fuzz] = arg.to_i
836 when '--rcs-tag-fuzz'
837 parse_options[:tag_fuzz] = arg.to_i
838 when '--symbol-check'
839 parse_options[:symbol_check] = true
840 when '--no-symbol-check'
841 parse_options[:symbol_check] = false
842 when '--tag-each-rev'
843 parse_options[:tag_each_rev] = true
844 when '--no-tag-each-rev'
845 # this is the default, which is fine since the missing key
846 # (default) returns nil which is false in Ruby
847 parse_options[:tag_each_rev] = false
848 when '--log-filename'
849 parse_options[:log_filename] = true
850 when '--no-log-filename'
851 # this is the default, which is fine since the missing key
852 # (default) returns nil which is false in Ruby
853 parse_options[:log_filename] = false
854 when '--skip-branches'
855 parse_options[:skip_branches] = true
867 if parse_options[:tag_fuzz] < parse_options[:commit_fuzz]
868 parse_options[:tag_fuzz] = parse_options[:commit_fuzz]
873 user = Etc.getlogin || ENV['USER']
875 # steal username/email data from other init files that may contain the
879 # the user's .hgrc file for a username field
880 ['~/.hgrc', /^\s*username\s*=\s*(["'])?(.*)\1$/, 2],
881 # the user's .(g)vimrc for a changelog_username setting
882 ['~/.vimrc', /changelog_username\s*=\s*(["'])?(.*)\1$/, 2],
883 ['~/.gvimrc', /changelog_username\s*=\s*(["'])?(.*)\1$/, 2],
884 ].each do |fn, rx, idx|
885 file = File.expand_path fn
886 if File.readable?(file) and File.read(file) =~ rx
887 parse_options[:authors][user] = Regexp.last_match(idx).strip
893 if user and not user.empty? and not parse_options[:authors].has_key?(user)
894 name = ENV['GIT_AUTHOR_NAME'] || ''
895 name.replace(`git config user.name`.chomp) if name.empty?
896 name.replace(Etc.getpwnam(user).gecos) if name.empty?
899 # couldn't find a name, try to steal data from other sources
902 # if we found a name, try to find an email too
903 email = ENV['GIT_AUTHOR_EMAIL'] || ''
904 email.replace(`git config user.email`.chomp) if email.empty?
907 # couldn't find an email, try to steal data too
910 # we got both a name and email, fill the info
911 parse_options[:authors][user] = "#{name} <#{email}>"
926 file_list.each do |arg|
927 case ftype = File.ftype(arg)
933 not_found "RCS file #{arg}"
936 filename = File.basename(arg, SFX)
938 filename = File.basename(arg)
939 path = File.dirname(arg)
940 rcsfile = File.join(path, 'RCS', filename) + SFX
941 unless File.exists? rcsfile
942 rcsfile.replace File.join(path, filename) + SFX
943 unless File.exists? rcsfile
944 not_found "RCS file for #{filename} in #{path}"
948 rcs << RCS.parse(filename, rcsfile, parse_options)
950 argdirname = arg.chomp(File::SEPARATOR)
951 pattern = File.join(argdirname, '**', '*' + SFX)
952 Dir.glob(pattern, File::FNM_DOTMATCH).each do |rcsfile|
953 filename = File.basename(rcsfile, SFX)
954 path = File.dirname(rcsfile)
955 # strip trailing "/RCS" if present, or "RCS" if that's
957 path.sub!(/(^|#{File::SEPARATOR})RCS$/, '')
958 # strip off the portion of the path specified
959 # on the command line from the front of the path
960 # (or delete the path completely if it is the same
961 # as the specified directory)
962 path.sub!(/^#{Regexp.escape argdirname}(#{File::SEPARATOR}|$)/, '')
963 filename = File.join(path, filename) unless path.empty?
965 # skip file if it's to be ignored
966 unless parse_options[:ignore].empty?
968 parse_options[:ignore].each do |pat|
969 if File.fnmatch?(pat, filename, File::FNM_PATHNAME)
979 rcs << RCS.parse(filename, rcsfile, parse_options)
980 rescue Exception => e
981 warning "Failed to parse #{filename} @ #{rcsfile}:#{$.}"
986 warning "Cannot handle #{arg} of #{ftype} type"
992 rcs.first.export_commits(parse_options)
994 warning "Preparing commits"
999 r.revision.each do |k, rev|
1001 commits << RCS::Commit.new(r, rev)
1002 rescue NoBranchSupport
1003 if parse_options[:skip_branches]
1004 warning "Skipping revision #{rev.rev} for #{r.fname} (branch)"
1011 warning "Sorting by date"
1016 warning "RAW commits (#{commits.length}):"
1018 PP.pp c.to_a, $stderr
1021 warning "#{commits.length} single-file commits"
1024 warning "Coalescing [1] by date with fuzz #{parse_options[:commit_fuzz]}"
1026 thisindex = commits.size
1027 commits.reverse_each do |c|
1028 nextindex = thisindex
1031 cfiles = Set.new c.tree.filenames
1036 # test for mergeable commits by looking at following commits
1037 while nextindex < commits.size
1038 k = commits[nextindex]
1041 # commits are date-sorted, so we know we can quit early if we are too far
1042 # for coalescing to work
1043 break if k.min_date > c.max_date + parse_options[:commit_fuzz]
1047 kfiles = Set.new k.tree.filenames
1049 if c.log != k.log or c.author != k.author or c.branch != k.branch
1053 unless c.symbols.subset?(k.symbols) or k.symbols.subset?(c.symbols)
1054 cflist = cfiles.to_a.join(', ')
1055 kflist = kfiles.to_a.join(', ')
1056 if parse_options[:symbol_check]
1057 warning "Not coalescing #{c.log.inspect}\n\tfor (#{cflist})\n\tand (#{kflist})"
1058 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}"
1059 warning "\tretry with the --no-symbol-check option if you want to merge these commits anyway"
1062 warning "Coalescing #{c.log.inspect}\n\tfor (#{cflist})\n\tand (#{kflist})"
1063 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}"
1067 # keep track of filenames touched by commits we are not merging with,
1068 # since we don't want to merge with commits that touch them, to preserve
1069 # the monotonicity of history for each file
1070 # TODO we could forward-merge with them, unless some of our files were
1073 # if the candidate touches any file already in the commit,
1074 # we can stop looking forward
1075 break unless cfiles.intersection(kfiles).empty?
1080 # the candidate has the same log, author, branch and appropriate symbols
1081 # does it touch anything in ofiles?
1082 unless ofiles.intersection(kfiles).empty?
1084 cflist = cfiles.to_a.join(', ')
1085 kflist = kfiles.to_a.join(', ')
1086 oflist = ofiles.to_a.join(', ')
1087 warning "Not coalescing #{c.log.inspect}\n\tfor (#{cflist})\n\tand (#{kflist})"
1088 warning "\tbecause the latter intersects #{oflist} in #{(ofiles & kfiles).to_a.inspect}"
1096 mergeable.each do |k|
1099 rescue RuntimeError => err
1100 fuzz = c.date - k.date
1101 warning "Fuzzy commit coalescing failed: #{err}"
1102 warning "\tretry with commit fuzz < #{fuzz} if you don't want to see this message"
1110 warning "[1] commits (#{commits.length}):"
1112 PP.pp c.to_a, $stderr
1115 warning "#{commits.length} coalesced commits"
1118 commits.each { |c| c.export(parse_options) }