#!/usr/bin/env ruby

require 'date'

class Commit

  attr_reader :id, :parents, :msg

  def initialize(data)
    @parents = []
    msg = nil

    data.each_line do |l|
      l.encode!('utf-8', 'iso-8859-1') if not l.valid_encoding?

      if not msg
        case l
        when /^commit (.+)$/
          @id = $1
        when /^tree (.+)$/
          @tree = $1
        when /^author (.+) <(.+)> (.+)$/
          @author = [$1, $2]
          @author_date = DateTime.strptime($3, '%s %z')
        when /^committer (.+) <(.+)> (.+)$/
          @committer = [$1, $2]
          @committer_date = DateTime.strptime($3, '%s %z')
        when /^parents (.+)$/
          @parents = $1.split(" ")
        when /^$/
          msg = ""
        end
      else
        msg << l
      end
    end

    @msg = msg
  end

  def export()
    ENV['GIT_AUTHOR_NAME'] = @author[0]
    ENV['GIT_AUTHOR_EMAIL'] = @author[1]
    ENV['GIT_AUTHOR_DATE'] = @author_date.strftime('%s %z')
    ENV['GIT_COMMITTER_NAME'] = @committer[0]
    ENV['GIT_COMMITTER_EMAIL'] = @committer[1]
    ENV['GIT_COMMITTER_DATE'] = @committer_date.strftime('%s %z')

    new_id = nil

    # skip empty
    if @parents.size == 1
      old_tree = `git rev-parse #{@parents.first}^{tree}`.chomp
      if old_tree == @tree
        $commit_map[@id] = @parents.first
        return
      end
    end

    parents = @parents.map { |e| ['-p', $commit_map[e]] }.flatten
    IO.popen(['git', 'commit-tree', @tree] + parents, "w+") do |pipe|
      pipe.write(@msg)
      pipe.close_write
      new_id = pipe.read().chomp()
    end

    $commit_map[@id] = new_id
  end

end

$commit_map = {}

stack = []
heads = {}

IO.popen(%w[git for-each-ref --format=%(refname):%(objectname)]).each do |l|
  ref, id = l.chomp.split(':')
  heads[ref] = id
end

format = [
  'commit %H',
  'tree %T',
  'author %an <%ae> %ad',
  'committer %cn <%ce> %cd',
  'parents %P',
  '', '%B' ].join('%n')
args = %w[--reverse --topo-order --parents --simplify-merges --all]
command = %W[git log -z -s --date=raw --format=format:#{format}] + args
IO.popen(command).each("\0") do |data|
  c = Commit.new(data.chomp("\0"))
  stack << c
end

def show(str)
  $stdout.print(str) if $stdout.isatty
end

count = 0
total = stack.size

until stack.empty? do
  c = stack.shift

  next if $commit_map[c.id]

  c.export
  show "\rRewrite #{c.id} (#{count += 1}/#{total})"
end

show "\n"

heads.each do |ref, id|
  new_id = $commit_map[id]
  next if not id or not new_id or id == new_id
  system('git', 'update-ref', ref, new_id, id)
  puts '%s %s -> %s' % [ref, id, new_id]
end
