#!/usr/bin/env ruby

#
# Copyright (c) 2019 Felipe Contreras
#
# This script runs the tests for all versions of the components:
#   hg, hggit and dulwich
#
# You can run it without arguments, in which case it reads the file
# 'versions.txt' and executes all those checks.
#
# Or you can pass the versions to check manually, like:
#
#   ./check-versions hg:4.7 hggit:0.8.12 dulwich:0.19.7
#
# Or you can pass just the hg version, the other versions are fetched from
# 'versions.txt':
#
#   ./check-versions hg:5.0
#

require 'fileutils'
require 'tmpdir'

$tests = %w[main.t bidi.t hg-git.t]
$workdir = "#{Dir.home}/.cache/git-remote-hg"
$builddir = Dir.mktmpdir("git-remote-hg-build-")
$testoutdir = Dir.mktmpdir("git-remote-hg-tests-")

at_exit {
  FileUtils.remove_entry($builddir)
  FileUtils.remove_entry($testoutdir)
}

QUIET, LOW, HIGH = (1..3).to_a
$verbosity = LOW

# Util {{{1

def section(text)
  puts [nil, text, '=' * text.size]
end

def title(text)
  puts [nil, text, '-' * text.size] unless $verbosity < HIGH
end

def run_cmd(cmd, fatal: true)
  puts cmd.join(' ') unless $verbosity < HIGH
  result = system(*cmd)
  unless result or not fatal
    STDERR.puts "Failed to run command '%s'" % cmd.join(' ')
    exit -1
  end
  result
end

def check_version(a, b)
  return true if a == '@'
  a = a.split('.').map(&:to_i)
  b = b.split('.').map(&:to_i)
  (a <=> b) >= 0
end

# Component {{{1

class Component

  attr_reader :id

  def initialize(id, url, kind: nil, **args)
    @id = id
    @url = url
    @kind = kind || (url.start_with?('git') ? :git : :hg)
    @tool = @kind.to_s
    @checkout_fix = args[:checkout_fix]
    @version_format = args[:version_format]
  end

  def dir
    "#{$workdir}/#{@id}"
  end

  def get_version(version)
    return @kind == :hg ? 'tip' : '@' if version == '@'
    @version_format ? @version_format % version : version
  end

  def clone
    run_cmd [@tool, 'clone', '-q', @url, dir]
  end

  def checkout(version)
    Dir.chdir(dir) do
      case @kind
      when :hg
        cmd = %w[update --clean]
      when :git
        cmd = %w[reset --hard]
      else
        cmd = %w[checkout]
      end
      run_cmd [@tool] + cmd + ['-q', get_version(version)]
      @checkout_fix.call(version) if @checkout_fix
    end
  end

  def build
    Dir.chdir(dir) do
      targets = %w[build_py build_ext].map { |e| [e, '--build-lib', "#{$builddir}/python"] }
      run_cmd %w[python setup.py --quiet] + targets.flatten
    end
  end

end

# Functions {{{1

def setup
  dirs = %w[bin python]
  FileUtils.mkdir_p(dirs.map { |e| "#{$builddir}/#{e}" })
  FileUtils.mkdir_p($workdir)

  $components.each do |id, component|
    next if File.exists?(component.dir)

    if $verbosity < HIGH
      puts "Cloning #{component.id}"
    else
      title "Cloning #{component.id}"
    end
    component.clone
  end
end

def test_env(paths: nil)
  old = ENV.to_h
  paths.each do |id, path|
    name = id.to_s
    ENV[name] = "#{path}:#{ENV[name]}"
  end
  r = yield
  ENV.replace(old)
  return r
end

def run_tests(tests)
  title "Running tests"

  Dir.chdir("#{__dir__}/../test") do
    case $verbosity
    when QUIET
      tests_opt = tests.join(' ')
      cmd = "prove -q #{tests_opt} :: -i"
    when LOW
      tests_opt = "T='%s'" % tests.join(' ')
      cmd = "make -j1 #{tests_opt}"
    else
      tests_opt = "T='%s'" % tests.join(' ')
      cmd = "TEST_OPTS='-v -i' make -j1 #{tests_opt}"
    end
    system(cmd)
  end
end

def versions_to_s(versions)
  versions.map { |k,v| "#{k}:#{v}" }.join(' ')
end

def versions_from_args(args)
  args.map { |e| k, v = e.split(':'); [k.to_sym, v] }.to_h
end

def versions_from_s(str)
  versions_from_args(str.split(' '))
end

def check(versions)
  section versions_to_s(versions)

  versions.each do |id, version|
    component = $components[id]
    next unless component

    title "Checking out #{component.id} #{version}"
    component.checkout(version)

    title "Building #{component.id}"
    component.build
  end

  paths = {
    PATH: "#{$builddir}/bin",
    PYTHONPATH: "#{$builddir}/python",
  }

  test_env(paths: paths) do
    ENV['SHARNESS_TEST_OUTPUT_DIRECTORY'] = $testoutdir
    run_tests($tests)
  end
end

# Add components {{{1

$components = {}

def add_component(id, url, **args)
  $components[id] = Component.new(id, url, **args)
end

hg_checkout_fix = lambda do |version|
  FileUtils.cp('hg', "#{$builddir}/bin/")

  return if check_version(version, '4.3')

  if run_cmd %W[hg import -q --no-commit #{__dir__}/hg_setup_hack_2.4.patch], fatal: false
    File.write('.hg_force_version', "%s\n" % version)
  else
    File.write('mercurial/__version__.py', "version = \"%s\"\n" % version)
  end
end

add_component(:hg, 'https://www.mercurial-scm.org/repo/hg', checkout_fix: hg_checkout_fix)

hggit_checkout_fix = lambda do |version|
  return unless check_version(version, '0.8.0')

  run_cmd %W[hg import -q --no-commit #{__dir__}/hggit_rename_fix_0.8.0.patch], fatal: false
end

add_component(:hggit, 'https://bitbucket.org/durin42/hg-git', checkout_fix: hggit_checkout_fix)

add_component(:dulwich, 'https://github.com/dulwich/dulwich.git', version_format: 'dulwich-%s', kind: :git)

def load_checks(file)
  file.each do |e|
    e.chomp!
    next if e.empty? or e.start_with?('#')
    content, comment = e.split(' # ')
    versions = versions_from_s(content)
    $checks << versions
  end
end

def store_results(file)
  $results.each do |versions, result|
    content = versions_to_s(versions)
    comment = result ? 'OK' : 'FAIL'
    file.puts '%s # %s' % [content, comment]
  end
end

# Main {{{1

setup

$checks = []
$results = []

$versions = versions_from_args(ARGV)

File.open("#{__dir__}/versions.txt") do |f|
  load_checks(f)
end

if $versions.size == 1 and $versions.key?(:hg)
  # mode 1
  $verbosity = LOW

  if ['@', nil].include?($versions[:hg])
    versions = $checks.last
    versions[:hg] = $versions[:hg] if $versions[:hg]
  else
    versions = $checks.find { |e| e[:hg] == $versions[:hg] }
    exit 1 unless versions
  end

  exit check(versions) ? 0 : 1
elsif not $versions.empty?
  # mode 2
  $verbosity = HIGH

  exit check(versions) ? 0 : 1
else
  # mode 3
  $verbosity = QUIET

  at_exit do
    File.open("#{__dir__}/results.txt", 'w') do |f|
      store_results(f)
    end
  end

  failures = 0

  $checks.each do |versions|
    result = check(versions)
    failures += 1 unless result
    $results << [versions, result]
  end

  exit 1 unless failures == 0
end
