+++ /dev/null
-#!/usr/bin/env python
-"""
-TODO
-* Refactor commit coalescing
-* Add --strict-symbol-check to only coalesce commits if their symbol lists are equal
-* Further coalescing options? (e.g. small logfile differences)
-* Proper branching support in multi-file export
-* Optimize memory usage by discarding unneeded text
-"""
-
-import sys, os, stat
-import pprint # Functional equivalent of Ruby pp?
-
-def usage():
- print >>sys.stderr, """\
-rcs-fast-export [options] file [file ...]
-
-Fast-export the RCS history of one or more files. If a directory is specified,
-all RCS-tracked files in the directory and its descendants are exported.
-
-When importing single files, their pathname is discarded during import. When
-importing directories, only the specified directory component is discarded.
-
-When importing a single file, RCS commits are converted one by one. Otherwise,
-some heuristics is used to determine how to coalesce commits touching different
-files.
-
-Currently, commits are coalesced if they share the exact same log and if their
-date differs by no more than the user-specified fuzziness. Additionally, the
-symbols in one of the commit must be a subset of the symbols in the other
-commit, unless --no-symbol-check is specified or rcs.symbolCheck is set to
-false in the git configuration.
-
-Typical usage:
- git init && rcs-fast-export.rb . | git fast-import && git reset --hard
-
-Options:
- --help, -h, -? display this help text
- --authors-file, -A specify a file containing username = Full Name <email> mappings
- --rcs-commit-fuzz fuzziness in RCS commits to be considered a single one when
- importing multiple files
- (in seconds, defaults to 300, i.e. 5 minutes)
- --[no-]symbol-check [do not] check symbols when coalescing commits
- --[no-]tag-each-rev [do not] create a lightweight tag for each RCS revision when
- importing a single file
- --[no-]log-filename [do not] prepend the filename to the commit log when importing
- a single file
-
-Config options:
- rcs.authorsFile for --authors-file
- rcs.tagEachRev for --tag-each-rev
- rcs.logFilename for --log-filename
- rcs.commitFuzz for --rcs-commit-fuzz
- rcs.symbolCheck for --rcs-symbol-check
- rcs.tagFuzz for --rcs-tag-fuzz
-"""
-
-def not_found(arg):
- print >>sys.stderr,"Could not find %s" % arg
-
-def load_authors_file(fn):
- "Returns a dict that maps usernames to author names & emails."
- authdict = {}
- try:
- for line in open(fn):
- (uname, author) = map(lambda x: x.strip(), line.split("-"))
- if uname in hashdict:
- print >>sys.stderr, "Username %(uname)s redefined to %(author)s" % locals()
- authdict[uname] = author
- return authdict
- except OSError:
- not_found(fn)
-
-class FatalException:
- "Unrecoverable error."
- def __init__(self, msg):
- self.msg = msg
-
-class RCS:
- RCS.marks = {}
-
- @staticmethod
- def clean(arg):
- "Strip an optional final ;"
- if arg.endswith(";"):
- arg = arg[:-1]
- return arg
-
- @staticmethod
- def sanitize(arg):
- "Strip the first and last @, and de-double @@s"
- if type(arg) == type([]):
- if not arg[0].startswith("@@"):
- raise FatalException('malformed first line')
- if not (arg[-1][1] == "@" and arg[-1][-1] == "@"):
- raise FatalException('malformed last line')
- arg[0] = arg[0][1:]
- arg[-1] = arg[-1][:-1]
- arg = map(lambda s: s.replace("@@", "@"), arg)
- return arg
- elif type(arg) == type(""):
- if arg.endswith("@"):
- arg = arg[:-1]
- if arg.startswith("@"):
- arg = arg[1:]
- arg = arg.replace("@@", "@")
- return arg
- else:
- raise FatalException("unexpected argument type in sanitize")
-
- @staticmethod
- def at_clean(arg):
- "Clean and sanitize."
- return RCS.sanitize(RCS.clean(arg))
-
- @staticmethod
- def mark(key):
- "Return a sequence mark for an object, which may be a blob or a commit."
- if not key in RCS.marks:
- RCS.marks[key] = len(RCS.marks)+1
- return RCS.marks[key]
-
- @staticmethod
- def blob(file, rev):
- return RCS.mark([file, rev])
-
- @staticmethod
- def commit(commit):
- return RCS.mark(commit)
-
- class File:
- def __init__(master):
- self.fname = master
- self.head = None
- self.comment = None
- self.desc = []
- self.revision = {} #Hash.new { |h, r| h[r] = Revision.new(self, r) }
- executable = stat.S_IXUSER(os.stat(master)[stat.ST_MODE])
- self.mode = '755' if executable else '644'
-
- def has_revision(rev):
- return rev in revision and self.revision[rev].author is not None
-
- def export_commits(opts):
- counter = 0
- exported = []
- while self.revision:
- counter += 1
-
- # a string sort is a very good candidate for
- # export order, getting a miss only for
- # multi-digit revision components
- keys = self.revision.keys()
- keys.sort()
-
- print >>sys.stderr,"commit export loop %d" % counter
- if exported:
- print >>sys.stderr,"\t#%d commits exported so far: %s" % ", ".join(exported)
- print >>sys.stderr,"\t#%d to export: %s" % (len(keys), ",".join(keys))
- for key in keys:
- rev = self.revision[key]
- # the parent commit is rev.next if we're on the
- # master branch (rev.branch is None) or
- # rev.diff_base otherwise
- if rev.branch is None:
- parent = rev.next
- else:
- parent = rev.diff_base
- # A commit can only be exported if it has no
- # parent, or if the parent has been exported
- # already. Skip this commit otherwise
- if parent and parent not in exported:
- continue
-
- branch = rev.branch or 'master'
- author = opts.authors[rev.author] or (rev.author + " <empty>")
- date = `rev.date.tv_sec` + " +0000"
- log = ""
- if opts.log_filename:
- log += self.fname + ": "
- log += rev.log.to_s
-
- print "commit refs/heads/" + branch
- print "mark :" + RCS.commit(key)
- print "committer %s %s" % (author, date)
- print "data %d" % len(log)
- if log:
- print log
- if rev.branch_point:
- print "from :" + RCS.commit(parent)
- print "M %s :%s %s" % (RCS.blob(self.fname, key), fname)
- # TODO FIXME this *should* be safe, in
- # that it should not unduly move
- # branches back in time, but I'm not
- # 100% sure ...
- for sym in rev.branches:
- print "reset refs/heads/" + sym
- print "from :" + RCS.commit(key)
- for sym in rev.symbols:
- print "reset refs/tags/" + sym
- print "from :" + RCS.commit(key)
- if opts.tag_each_rev:
- print "reset refs/tags/" + key
- print "from :" + RCS.commit(key)
- exported.append(key)
- for k in exported: del self.revision[k]
-
- class Revision:
- def __init__(fileobj, rev):
- self.file = fileobj
- self.rev = rev
- self.author = None
- self.date = None
- self.state = None
- self.next = None
- self.branches = []
- self.branch = None
- self.branch_point = None
- self.diff_base = None
- self.log = []
- self.text = []
- self.symbols = set([])
- def __hash__(self):
- return hash(file, rev)
- #def date=(str)
- # @date = Time.rcs(str)
- #end
- def blob(self):
- str = "".join(self.text)
- return "blob\nmark :#%s\ndata %d\n%s\n" \
- (RCS.blob(self.file.fname, self.rev), len(str), str)
-
- @staticmethod
- def parse(rcsfile):
- rcs = RCS.File(rcsfile)
- status = ["basic"]
- rev = None
- lines = []
- difflines = []
- linebuffer = []
- with file(rcs.fname) as fp:
- while True:
- if linebuffer:
- line = linebuffer.pop()
- else:
- line = fp.readline()
- if not line:
- break
- if status[-1] == "basic":
- m = re.match(r"^([a-z]+)\s+([^;]*);?", line)
- if not m:
- raise FatalException("failed to find command")
- command = m.group(1)
- args = m.group(2)
- if not command:
- if debug:
- print >>sys.stderr,"Skipping empty command"
- continue
- if command == 'head':
- rcs.head = RCS.clean(args)
- continue
- if command == 'symbols':
- status.append('symbols')
- if args:
- linebuffer.append(args)
- continue
- if command == 'comment':
- rcs.comment = RCS.at_clean(args)
- continue
- m = re.match("^[0-9.]+$", command)
- if m:
- rev = command
- if rcs.has_revision(rev):
- status.append("revision_data")
- else:
- status.append("new_revision")
- continue
- if command == 'desc':
- status.append("desc")
- lines = []
- status.append("read_lines")
- continue
- if command in ('branch', 'access', 'locks', 'expand'):
- if debug:
- print >>sys.stderr,"Skipping unhandled command %s" % command
- status.append("skipping_lines")
- if args:
- linebuffer.apppend(args)
- continue
- raise FatalException("Unknown command %s" % command)
- elif status[-1] == "skipping_lines":
- if line.strip.endswith(";"):
- status.pop()
- continue
- elif status[-1] == "symbols":
- # we can have multiple symbols per line
- for pair in line.strip().split(";"):
- if not pair:
- status.pop()
- else:
- (sym, rev) = pair.strip().split(':')
- if rev:
- rcs.revision[rev].symbols.append(sym)
- else:
- status.pop()
- elif status[-1] == "desc":
- rcs.desc = []
- status.pop()
- elif status[-1] == "read_lines":
- # we sanitize lines as we read them
- # the first line must begin with a @, which we strip
- if not rcs.desc:
- if not line.startswith("@"):
- raise FatalException('malformed line')
- else:
- line = line[1:]
-
- # if the line ends with an ODD number of @, it's the
- # last line -- we work on actual_line so that content
- # such as @\n or @ work correctly (they would be
- # encoded respectively as ['@@@\n','@\n'] and
- # ['@@@@\n']
- ats = actual_line.chomp.match(/@+$/)
- if nomore = (ats && Regexp.last_match(0).length.odd?)
- actual_line.replace actual_line.chomp.sub(/@$/,'')
- end
- lines << actual_line.gsub('@@','@')
- if nomore
- status.pop
- redo
- end
- when :new_revision
- case line.chomp
- when /^date\s+(\S+);\s+author\s+(\S+);\s+state\s+(\S+);$/
- rcs.revision[rev].date = $1
- rcs.revision[rev].author = $2
- rcs.revision[rev].state = $3
- when 'branches'
- status.push :branches
- when /branches\s*;/
- next
- when /^next\s+(\S+)?;$/
- nxt = rcs.revision[rev].next = $1
- next unless nxt
- raise "multiple diff_bases for #{nxt}" unless rcs.revision[nxt].diff_base.None?
- rcs.revision[nxt].diff_base = rev
- rcs.revision[nxt].branch = rcs.revision[rev].branch
- else
- status.pop
- end
- when :branches
- candidate = line.split(';',2)
- branch = candidate.first.strip
- rcs.revision[rev].branches.push branch
- raise "multiple diff_bases for #{branch}" unless rcs.revision[branch].diff_base.None?
- rcs.revision[branch].diff_base = rev
- # we drop the last number from the branch name
- rcs.revision[branch].branch = branch.sub(/\.\d+$/,'.x')
- rcs.revision[branch].branch_point = rev
- status.pop if candidate.length > 1
- when :revision_data
- case line.chomp
- when 'log'
- status.push :log
- lines.clear
- status.push :read_lines
- when 'text'
- if rev == rcs.head
- status.push :head
- else
- status.push :diff
- end
- lines.clear
- status.push :read_lines
- else
- status.pop
- end
- when :log
- rcs.revision[rev].log.replace lines.dup
- status.pop
- when :head
- rcs.revision[rev].text.replace lines.dup
- print rcs.revision[rev].blob
- status.pop
- when :diff
- difflines.replace lines.dup
- difflines.pop if difflines.last.empty?
- base = rcs.revision[rev].diff_base
- unless rcs.revision[base].text
- pp rcs
- print rev, base
- raise 'no diff base!'
- end
- # deep copy
- buffer = []
- rcs.revision[base].text.each { |l| buffer << [l.dup] }
-
- adding = false
- index = None
- count = None
-
- while l = difflines.shift
- if adding
- raise 'negative index during insertion' if index < 0
- raise 'negative count during insertion' if count < 0
- adding << l
- count -= 1
- # collected all the lines, put the before
- unless count > 0
- unless buffer[index]
- buffer[index] = []
- end
- buffer[index].unshift(*adding)
- adding = false
- end
- next
- end
-
- l.chomp!
- raise 'malformed diff' unless l =~ /^([ad])(\d+) (\d+)$/
- diff_cmd = $1.intern
- index = $2.to_i
- count = $3.to_i
- case diff_cmd
- when :d
- # for deletion, index 1 is the first index, so the Ruby
- # index is one less than the diff one
- index -= 1
- # we replace them with empty string so that 'a' commands
- # referring to the same line work properly
- while count > 0
- buffer[index].clear
- index += 1
- count -= 1
- end
- when :a
- # addition will prepend the appropriate lines
- # to the given index, and in this case Ruby
- # and diff indices are the same
- adding = []
- end
- end
-
- # turn the buffer into an array of lines, deleting the empty ones
- buffer.delete_if { |l| l.empty? }
- buffer.flatten!
-
- rcs.revision[rev].text = buffer
- print rcs.revision[rev].blob
- status.pop
- else
- raise "Unknown status #{status.last}"
- end
- end
- end
-
- # clean up the symbols/branches: look for revisions that have
- # one or more symbols but no dates, and make them into
- # branches, pointing to the highest commit with that key
- branches = []
- keys = rcs.revision.keys
- rcs.revision.each do |key, rev|
- if rev.date.None? and not rev.symbols.empty?
- top = keys.select { |k| k.match(/^#{key}\./) }.sort.last
- tr = rcs.revision[top]
- raise "unhandled complex branch structure met: #{rev.inspect} refers #{tr.inspect}" if tr.date.None?
- tr.branches |= rev.symbols
- branches << key
- end
- end
- branches.each { |k| rcs.revision.delete k }
-
- return rcs
- end
-
- class Tree
- def initialize(commit)
- @commit = commit
- @files = Hash.new
- end
-
- def merge!(tree)
- testfiles = @files.dup
- tree.each { |rcs, rev| self.add(rcs, rev, testfiles) }
- # the next line is only reached if all the adds were
- # successful, so the merge is atomic
- @files.replace testfiles
- end
-
- def add(rcs, rev, file_list=@files)
- if file_list.key? rcs
- prev = file_list[rcs]
- if prev.log == rev.log
- str = "re-adding existing file #{rcs.fname} (old: #{prev.rev}, new: #{rev.rev})"
- else
- str = "re-adding existing file #{rcs.fname} (old: #{[prev.rev, prev.log.to_s].inspect}, new: #{[rev.rev, rev.log.to_s].inspect})"
- end
- if prev.text != rev.text
- raise str
- else
- @commit.warn_about str
- end
- end
- file_list[rcs] = rev
- end
-
- def each &block
- @files.each &block
- end
-
- def to_a
- files = []
- @files.map do |rcs, rev|
- files << "M #{rcs.mode} :#{RCS.blob rcs.fname, rev.rev} #{rcs.fname}"
- end
- files
- end
-
- def filenames
- @files.map { |rcs, rev| rcs.fname }
- end
-
- def to_s
- self.to_a.join("\n")
- end
- end
-
- class Commit
- attr_accessor :date, :log, :symbols, :author, :branch
- attr_accessor :tree
- def initialize(rcs, rev)
- raise NotImplementedError if rev.branch
- self.date = rev.date.dup
- self.log = rev.log.dup
- self.symbols = rev.symbols.dup
- self.author = rev.author
- self.branch = rev.branch
-
- self.tree = Tree.new self
- self.tree.add rcs, rev
- end
-
- def to_a
- [self.date, self.branch, self.symbols, self.author, self.log, self.tree.to_a]
- end
-
- def warn_about(str)
- warn str + " for commit on #{self.date}"
- end
-
- # Sort by date and then by number of symbols
- def <=>(other)
- ds = self.date <=> other.date
- if ds != 0
- return ds
- else
- return self.symbols.length <=> other.symbols.length
- end
- end
-
- def merge!(commit)
- self.tree.merge! commit.tree
- if commit.date > self.date
- warn_about "updating date to #{commit.date}"
- self.date = commit.date
- end
- self.symbols.merge commit.symbols
- end
-
- def export(opts={})
- xbranch = self.branch || 'master'
- xauthor = opts[:authors][self.author] || "#{self.author} <empty>"
- xlog = self.log.to_s
- numdate = self.date.tv_sec
- xdate = "#{numdate} +0000"
- key = numdate.to_s
-
- print "commit refs/heads/#{xbranch}"
- print "mark :#{RCS.commit key}"
- print "committer #{xauthor} #{xdate}"
- print "data #{xlog.length}"
- print xlog unless xlog.empty?
- # TODO branching support for multi-file export
- # print "from :#{RCS.commit from}" if self.branch_point
- print self.tree.to_s
-
- # TODO branching support for multi-file export
- # rev.branches.each do |sym|
- # print "reset refs/heads/#{sym}"
- # print "from :#{RCS.commit key}"
- # end
-
- self.symbols.each do |sym|
- print "reset refs/tags/#{sym}"
- print "from :#{RCS.commit key}"
- end
-
- end
- end
-end
-
-require 'getoptlong'
-
-opts = GetoptLong.new(
- # Authors file, like git-svn and git-cvsimport, more than one can be
- # specified
- ['--authors-file', '-A', GetoptLong::REQUIRED_ARGUMENT],
- # RCS file suffix, like RCS
- ['--rcs-suffixes', '-x', GetoptLong::REQUIRED_ARGUMENT],
- # Date fuzziness for commits to be considered the same (in seconds)
- ['--rcs-commit-fuzz', GetoptLong::REQUIRED_ARGUMENT],
- # check symbols when coalescing?
- ['--symbol-check', GetoptLong::NO_ARGUMENT],
- ['--no-symbol-check', GetoptLong::NO_ARGUMENT],
- # tag each revision?
- ['--tag-each-rev', GetoptLong::NO_ARGUMENT],
- ['--no-tag-each-rev', GetoptLong::NO_ARGUMENT],
- # prepend filenames to commit logs?
- ['--log-filename', GetoptLong::NO_ARGUMENT],
- ['--no-log-filename', GetoptLong::NO_ARGUMENT],
- ['--help', '-h', '-?', GetoptLong::NO_ARGUMENT]
-)
-
-# We read options in order, but they apply to all passed parameters.
-# TODO maybe they should only apply to the following, unless there's only one
-# file?
-opts.ordering = GetoptLong::RETURN_IN_ORDER
-
-file_list = []
-parse_options = {
- :authors => Hash.new,
- :commit_fuzz => 300,
- :tag_fuzz => -1,
-}
-
-# Read config options
-`git config --get-all rcs.authorsfile`.each_line do |fn|
- parse_options[:authors].merge! load_authors_file(fn.chomp)
-end
-
-parse_options[:tag_each_rev] = (
- `git config --bool rcs.tageachrev`.chomp == 'true'
-) ? true : false
-
-parse_options[:log_filename] = (
- `git config --bool rcs.logfilename`.chomp == 'true'
-) ? true : false
-
-fuzz = `git config --int rcs.commitFuzz`.chomp
-parse_options[:commit_fuzz] = fuzz.to_i unless fuzz.empty?
-
-fuzz = `git config --int rcs.tagFuzz`.chomp
-parse_options[:tag_fuzz] = fuzz.to_i unless fuzz.empty?
-
-parse_options[:symbol_check] = (
- `git config --bool rcs.symbolcheck`.chomp == 'false'
-) ? false : true
-
-opts.each do |opt, arg|
- case opt
- when '--authors-file'
- authors = load_authors_file(arg)
- redef = parse_options[:authors].keys & authors.keys
- print >>sys.stderr,"Authors file #{arg} redefines #{redef.join(', ')}" unless redef.empty?
- parse_options[:authors].merge!(authors)
- when '--rcs-suffixes'
- # TODO
- when '--rcs-commit-fuzz'
- parse_options[:commit_fuzz] = arg.to_i
- when '--rcs-tag-fuzz'
- parse_options[:tag_fuzz] = arg.to_i
- when '--symbol-check'
- parse_options[:symbol_check] = true
- when '--no-symbol-check'
- parse_options[:symbol_check] = false
- when '--tag-each-rev'
- parse_options[:tag_each_rev] = true
- when '--no-tag-each-rev'
- # this is the default, which is fine since the missing key
- # (default) returns None which is false in Ruby
- parse_options[:tag_each_rev] = false
- when '--log-filename'
- parse_options[:log_filename] = true
- when '--no-log-filename'
- # this is the default, which is fine since the missing key
- # (default) returns None which is false in Ruby
- parse_options[:log_filename] = false
- when ''
- file_list << arg
- when '--help'
- usage
- exit
- end
-end
-
-if parse_options[:tag_fuzz] < parse_options[:commit_fuzz]
- parse_options[:tag_fuzz] = parse_options[:commit_fuzz]
-end
-
-require 'etc'
-
-user = Etc.getlogin || ENV['USER']
-
-# steal username/email data from other init files that may contain the
-# information
-def steal_username
- [
- # the user's .hgrc file for a username field
- ['~/.hgrc', /^\s*username\s*=\s*(["'])?(.*)\1$/, 2],
- # the user's .(g)vimrc for a changelog_username setting
- ['~/.vimrc', /changelog_username\s*=\s*(["'])?(.*)\1$/, 2],
- ['~/.gvimrc', /changelog_username\s*=\s*(["'])?(.*)\1$/, 2],
- []
- ].each do |fn, rx, idx|
- file = File.expand_path fn
- if File.readable?(file) and File.read(file) =~ rx
- parse_options[:authors][user] = Regexp.last_match(idx).strip
- break
- end
- end
-end
-
-if user and not user.empty? and not parse_options[:authors].has_key?(user)
- name = ENV['GIT_AUTHOR_NAME'] || ''
- name.replace(`git config user.name`.chomp) if name.empty?
- name.replace(Etc.getpwnam(user).gecos) if name.empty?
-
- if name.empty?
- # couldn't find a name, try to steal data from other sources
- steal_username
- else
- # if we found a name, try to find an email too
- email = ENV['GIT_AUTHOR_EMAIL'] || ''
- email.replace(`git config user.email`.chomp) if email.empty?
-
- if email.empty?
- # couldn't find an email, try to steal data too
- steal_username
- else
- # we got both a name and email, fill the info
- parse_options[:authors][user] = "#{name} <#{email}>"
- end
- end
-end
-
-if file_list.empty?
- usage
- exit 1
-end
-
-SFX = ',v'
-
-status = 0
-
-rcs = []
-file_list.each do |arg|
- case ftype = File.ftype(arg)
- when 'file'
- if arg[-2,2] == SFX
- if File.exists? arg
- rcsfile = arg.dup
- else
- not_found "RCS file #{arg}"
- status |= 1
- end
- filename = File.basename(arg, SFX)
- else
- filename = File.basename(arg)
- path = File.dirname(arg)
- rcsfile = File.join(path, 'RCS', filename) + SFX
- unless File.exists? rcsfile
- rcsfile.replace File.join(path, filename) + SFX
- unless File.exists? rcsfile
- not_found "RCS file for #{filename} in #{path}"
- end
- end
- end
- rcs << RCS.parse(filename, rcsfile)
- when 'directory'
- pattern = File.join(arg, '**', '*' + SFX)
- Dir.glob(pattern).each do |rcsfile|
- filename = File.basename(rcsfile, SFX)
- path = File.dirname(rcsfile)
- path.sub!(/\/?RCS$/, '') # strip final /RCS if present
- path.sub!(/^#{Regexp.escape arg}\/?/, '') # strip initial dirname
- filename = File.join(path, filename) unless path.empty?
- begin
- rcs << RCS.parse(filename, rcsfile)
- rescue Exception => e
- print >>sys.stderr,"Failed to parse #{filename} @ #{rcsfile}:#{$.}"
- raise e
- end
- end
- else
- print >>sys.stderr,"Cannot handle #{arg} of #{ftype} type"
- status |= 1
- end
-end
-
-if rcs.length == 1
- rcs.first.export_commits(parse_options)
-else
- print >>sys.stderr,"Preparing commits"
-
- commits = []
-
- rcs.each do |r|
- r.revision.each do |k, rev|
- commits << RCS::Commit.new(r, rev)
- end
- end
-
- print >>sys.stderr,"Sorting by date"
-
- commits.sort!
-
- if $DEBUG
- print >>sys.stderr,"RAW commits (#{commits.length}):"
- commits.each do |c|
- PP.pp c.to_a, $stderr
- end
- else
- print >>sys.stderr,"#{commits.length} single-file commits"
- end
-
- print >>sys.stderr,"Coalescing [1] by date with fuzz #{parse_options[:commit_fuzz]}"
-
- commits.reverse_each do |c|
- commits.reverse_each do |k|
- break if k.date < c.date - parse_options[:commit_fuzz]
- next if k == c
- next if c.log != k.log or c.author != k.author or c.branch != k.branch
- next if k.date > c.date
- unless c.symbols.subset?(k.symbols) or k.symbols.subset?(c.symbols)
- if parse_options[:symbol_check]
- print >>sys.stderr,"Not coalescing #{c.log.inspect}\n\tfor (#{c.tree.filenames.join(', ')})\n\tand (#{k.tree.filenames.join(', ')})"
- print >>sys.stderr,"\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}"
- print >>sys.stderr,"\tretry with the --no-symbol-check option if you want to merge these commits anyway"
- next
- elsif $DEBUG
- print >>sys.stderr,"Coalescing #{c.log.inspect}\n\tfor (#{c.tree.filenames.join(', ')})\n\tand (#{k.tree.filenames.join(', ')})"
- print >>sys.stderr,"\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}"
- end
- end
- begin
- c.merge! k
- rescue RuntimeError => err
- fuzz = c.date - k.date
- print >>sys.stderr,"Fuzzy commit coalescing failed: #{err}"
- print >>sys.stderr,"\tretry with commit fuzz < #{fuzz} if you don't want to see this message"
- break
- end
- commits.delete k
- end
- end
-
- if $DEBUG
- print >>sys.stderr,"[1] commits (#{commits.length}):"
- commits.each do |c|
- PP.pp c.to_a, $stderr
- end
- else
- print >>sys.stderr,"#{commits.length} coalesced commits"
- end
-
- commits.each { |c| c.export(parse_options) }
-
-end
-
-exit status
-
-