diff --git a/.travis.yml b/.travis.yml index d75a189..be08357 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,28 +1,20 @@ -language: python +dist: xenial +language: minimal -install: - - if [ "$HG_VERSION" != "dev" ]; - then pip install -q Mercurial${HG_VERSION+==$HG_VERSION}; - else pip install -q http://selenic.com/repo/hg/archive/tip.tar.gz; - fi - - pip install -q dulwich hg-git==0.6.1 || true - -before_script: - - hg --version || true - - pip show hg-git dulwich +cache: + directories: + - $HOME/.cache/git-remote-hg script: - - make test + - ./tools/check-versions hg:$HG_VERSION matrix: include: - - env: HG_VERSION=2.9.1 - - env: HG_VERSION=2.8.2 - - env: HG_VERSION=2.7.2 - - env: HG_VERSION=3.0 - - env: HG_VERSION=3.5.2 - - env: HG_VERSION=3.6.3 - - env: HG_VERSION=3.7 - - env: HG_VERSION=dev - - python: 2.7 - - python: 2.6 + - env: + - env: HG_VERSION=@ + - env: HG_VERSION=5.0 + - env: HG_VERSION=4.9 + - env: HG_VERSION=4.8 + - env: HG_VERSION=4.7 + - env: HG_VERSION=4.6 + - env: HG_VERSION=4.5 diff --git a/README.asciidoc b/README.asciidoc index 502fead..215bb04 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -153,8 +153,6 @@ Limitations of the remote-helpers' framework apply. In particular, these commands don't work: * `git push origin :branch-to-delete` -* `git push origin old:new` (it will push 'old') (patches available) -* `git push --dry-run origin branch` (it will push) (patches available) **** Another limitation is that if `git log` reports a rename, this will not survive diff --git a/git-remote-hg b/git-remote-hg index 112777f..caf0d8a 100755 --- a/git-remote-hg +++ b/git-remote-hg @@ -73,7 +73,16 @@ def gitmode(flags): return 'l' in flags and '120000' or 'x' in flags and '100755' or '100644' def gittz(tz): - return '%+03d%02d' % (-tz / 3600, -tz % 3600 / 60) + sign = 1 if tz >= 0 else -1 + hours, minutes = divmod(abs(tz), 60 * 60) + return '%+03d%02d' % (-sign * hours, minutes / 60) + +def hgtz(tz): + tz = int(tz) + sign = 1 if tz >= 0 else -1 + hours, minutes = divmod(abs(tz), 100) + tz = hours * 60 * 60 + minutes * 60 + return -sign * tz def hgmode(mode): m = { '100755': 'x', '120000': 'l' } @@ -280,9 +289,7 @@ class Parser: if ex: user += ex - tz = int(tz) - tz = ((tz / 100) * 3600) + ((tz % 100) * 60) - return (user, int(date), -tz) + return (user, int(date), hgtz(tz)) def fix_file_path(path): def posix_path(path): diff --git a/test/bidi.t b/test/bidi.t index 3228ce3..5f80d09 100755 --- a/test/bidi.t +++ b/test/bidi.t @@ -243,4 +243,42 @@ test_expect_success 'hg tags' ' test_cmp expected actual ' +test_expect_success 'test timezones' ' + test_when_finished "rm -rf gitrepo* hgrepo*" && + + ( + git init -q gitrepo && + cd gitrepo && + + echo alpha > alpha && + git add alpha && + git commit -m "add alpha" --date="2007-01-01 00:00:00 +0000" && + + echo beta > beta && + git add beta && + git commit -m "add beta" --date="2007-01-01 00:00:00 +0100" && + + echo gamma > gamma && + git add gamma && + git commit -m "add gamma" --date="2007-01-01 00:00:00 -0100" && + + echo delta > delta && + git add delta && + git commit -m "add delta" --date="2007-01-01 00:00:00 +0130" && + + echo epsilon > epsilon && + git add epsilon && + git commit -m "add epsilon" --date="2007-01-01 00:00:00 -0130" + ) && + + hg_clone gitrepo hgrepo && + git_clone hgrepo gitrepo2 && + hg_clone gitrepo2 hgrepo2 && + + hg_log hgrepo > expected && + hg_log hgrepo2 > actual && + + test_cmp expected actual +' + test_done diff --git a/test/main.t b/test/main.t index 2531ea8..43577ca 100755 --- a/test/main.t +++ b/test/main.t @@ -1050,7 +1050,7 @@ test_expect_success 'push bookmark without changesets' ' check_bookmark hgrepo feature-a two ' -test_expect_success 'pull tags' ' +test_expect_unstable 'pull tags' ' test_when_finished "rm -rf hgrepo gitrepo" && ( @@ -1271,6 +1271,27 @@ test_expect_success 'push annotated tag' ' test_cmp expected actual ' +test_expect_success 'timezone issues with negative offsets' ' + test_when_finished "rm -rf hgrepo gitrepo1 gitrepo2" && + + hg init hgrepo && + + ( + git clone "hg::hgrepo" gitrepo1 && + cd gitrepo1 && + echo two >> content && + git add content && + git commit -m two --date="2016-09-26 00:00:00 -0230" && + git push + ) && + + git clone "hg::hgrepo" gitrepo2 && + + git --git-dir=gitrepo1/.git log -1 --format="%ai" > expected && + git --git-dir=gitrepo2/.git log -1 --format="%ai" > actual && + test_cmp expected actual +' + if test "$CAPABILITY_PUSH" != "t" then test_done diff --git a/tools/.gitignore b/tools/.gitignore new file mode 100644 index 0000000..e2b3ee7 --- /dev/null +++ b/tools/.gitignore @@ -0,0 +1 @@ +results.txt diff --git a/tools/check-versions b/tools/check-versions new file mode 100755 index 0000000..95b891b --- /dev/null +++ b/tools/check-versions @@ -0,0 +1,297 @@ +#!/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' + +$tests = %w[main.t bidi.t hg-git.t] +$workdir = "#{Dir.home}/.cache/git-remote-hg" +$builddir = "/tmp/git-remote-hg-build" +$testoutdir = "/tmp/git-remote-hg-tests" + +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[python2 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 diff --git a/tools/hg_setup_hack_2.4.patch b/tools/hg_setup_hack_2.4.patch new file mode 100644 index 0000000..3966daf --- /dev/null +++ b/tools/hg_setup_hack_2.4.patch @@ -0,0 +1,15 @@ +diff --git a/setup.py b/setup.py +--- a/setup.py ++++ b/setup.py +@@ -181,7 +181,10 @@ + # error 0xc0150004. See: http://bugs.python.org/issue3440 + env['SystemRoot'] = os.environ['SystemRoot'] + +-if os.path.isdir('.hg'): ++if os.path.exists('.hg_force_version'): ++ with open('.hg_force_version') as f: ++ version = f.read().rstrip('\n') ++elif os.path.isdir('.hg'): + cmd = [sys.executable, 'hg', 'log', '-r', '.', '--template', '{tags}\n'] + numerictags = [t for t in runhg(cmd, env).split() if t[0].isdigit()] + hgid = runhg([sys.executable, 'hg', 'id', '-i'], env).strip() diff --git a/tools/hggit_rename_fix_0.8.0.patch b/tools/hggit_rename_fix_0.8.0.patch new file mode 100644 index 0000000..71ca694 --- /dev/null +++ b/tools/hggit_rename_fix_0.8.0.patch @@ -0,0 +1,22 @@ +diff --git a/hggit/git_handler.py b/hggit/git_handler.py +--- a/hggit/git_handler.py ++++ b/hggit/git_handler.py +@@ -693,6 +693,8 @@ + def import_git_commit(self, commit): + self.ui.debug(_("importing: %s\n") % commit.id) + ++ extra_in_message = self.ui.configbool('git', 'debugextrainmessage', False) ++ + detect_renames = False + (strip_message, hg_renames, + hg_branch, extra) = git2hg.extract_hg_metadata( +@@ -703,7 +705,8 @@ + # renames detected from Git. This is because we export an extra + # 'HG:rename-source' Git parameter when this isn't set, which will + # break bidirectionality. +- extra['hg-git-rename-source'] = 'git' ++ if not extra_in_message: ++ extra['hg-git-rename-source'] = 'git' + else: + renames = hg_renames + diff --git a/tools/versions.txt b/tools/versions.txt new file mode 100644 index 0000000..d410ac9 --- /dev/null +++ b/tools/versions.txt @@ -0,0 +1,33 @@ +# vi: ft=ruby + +hg:2.4 hggit:0.4.0 dulwich:0.9.0 # 2013_02 +hg:2.5 hggit:0.4.0 dulwich:0.9.0 # 2013_02 +hg:2.6 hggit:0.4.0 dulwich:0.9.0 # 2013_02 +hg:2.7 hggit:0.4.0 dulwich:0.9.0 # 2013_02 +hg:2.8 hggit:0.4.0 dulwich:0.9.0 # 2013_02 +hg:2.9 hggit:0.4.0 dulwich:0.9.0 # 2013_02 + +hg:3.0 hggit:0.7.0 dulwich:0.10.0 # 2014_11 +hg:3.1 hggit:0.7.0 dulwich:0.10.0 # 2014_11 +hg:3.2 hggit:0.7.0 dulwich:0.10.0 # 2014_11 + +hg:3.3 hggit:0.8.4 dulwich:0.13.0 # 2016_01 +hg:3.4 hggit:0.8.4 dulwich:0.13.0 # 2016_01 +hg:3.5 hggit:0.8.4 dulwich:0.13.0 # 2016_01 +hg:3.6 hggit:0.8.4 dulwich:0.13.0 # 2016_01 +hg:3.7 hggit:0.8.4 dulwich:0.13.0 # 2016_01 +hg:3.8 hggit:0.8.4 dulwich:0.13.0 # 2016_01 +hg:3.9 hggit:0.8.4 dulwich:0.13.0 # 2016_01 + +hg:4.0 hggit:0.8.10 dulwich:0.18.0 # 2017_11 +hg:4.1 hggit:0.8.10 dulwich:0.18.0 # 2017_11 +hg:4.2 hggit:0.8.10 dulwich:0.18.0 # 2017_11 +hg:4.3 hggit:0.8.10 dulwich:0.18.0 # 2017_11 +hg:4.4 hggit:0.8.10 dulwich:0.18.0 # 2017_11 + +hg:4.5 hggit:0.8.11 dulwich:0.18.0 # 2018_02 +hg:4.6 hggit:0.8.12 dulwich:0.19.7 # 2018_10 +hg:4.7 hggit:0.8.12 dulwich:0.19.7 # 2018_10 +hg:4.8 hggit:@ dulwich:0.19.11 +hg:4.9 hggit:@ dulwich:0.19.11 +hg:5.0 hggit:@ dulwich:0.19.11