Commits

Alessio Caiazza committed dc1ad0b

Full mercurial support also as gem source. Test suite for mercurial gem source.

It works as a git source but uses hg keyword, no submodules.

Comments (0)

Files changed (7)

lib/bundler/dsl.rb

       source Source::Git.new(_normalize_hash(options).merge("uri" => uri)), source_options, &blk
     end
 
+    def hg(uri, options = {}, source_options = {}, &blk)
+      unless block_given?
+        msg = "You can no longer specify a hg source by itself. Instead, \n" \
+              "either use the :hg option on a gem, or specify the gems that \n" \
+              "bundler should find in the hg source by passing a block to \n" \
+              "the hg method, like: \n\n" \
+              "  hg 'http://bitbucket.org/nolith/eusplazio' do\n" \
+              "    gem 'eusplazio'\n" \
+              "  end"
+        raise DeprecatedError, msg
+      end
+
+      source Source::Mercurial.new(_normalize_hash(options).merge("uri" => uri)), source_options, &blk
+    end
+
     def to_definition(lockfile, unlock)
       @sources << @rubygems_source
       @sources.uniq!
     def _normalize_options(name, version, opts)
       _normalize_hash(opts)
 
-      invalid_keys = opts.keys - %w(group groups git path name branch ref tag require submodules platform platforms)
+      invalid_keys = opts.keys - %w(group groups git path name branch ref tag require submodules platform platforms hg)
       if invalid_keys.any?
         plural = invalid_keys.size > 1
         message = "You passed #{invalid_keys.map{|k| ':'+k }.join(", ")} "
       end
 
       # Normalize git and path options
-      ["git", "path"].each do |type|
+      ["git", "path", "hg"].each do |type|
         if param = opts[type]
           if version.first && version.first =~ /^\s*=?\s*(\d[^\s]*)\s*$/
             options = opts.merge("name" => name, "version" => $1)

lib/bundler/lockfile_parser.rb

   private
 
     TYPES = {
+      "HG"   => Bundler::Source::Mercurial,
       "GIT"  => Bundler::Source::Git,
       "GEM"  => Bundler::Source::Rubygems,
       "PATH" => Bundler::Source::Path
 
     def parse_source(line)
       case line
-      when "GIT", "GEM", "PATH"
+      when "HG", "GIT", "GEM", "PATH"
         @current_source = nil
         @opts, @type = {}, line
       when "  specs:"

lib/bundler/mercurial.rb

+module Bundler
+  module Source
+    class Mercurial < Path
+      attr_reader :uri, :ref, :options
+
+      def initialize(options)
+        super
+
+        # stringify options that could be set as symbols
+        %w(ref branch tag revision).each{|k| options[k] = options[k].to_s if options[k] }
+
+        @uri        = options["uri"]
+        @ref        = options["ref"] || options["branch"] || options["tag"] || 'default'
+        @revision   = options["revision"]
+        @update     = false
+      end
+
+      def self.from_lock(options)
+        new(options.merge("uri" => options.delete("remote")))
+      end
+
+      def to_lock
+        out = "HG\n"
+        out << "  remote: #{@uri}\n"
+        out << "  revision: #{revision}\n"
+        %w(ref branch tag).each do |opt|
+          out << "  #{opt}: #{options[opt]}\n" if options[opt]
+        end
+        out << "  glob: #{@glob}\n" unless @glob == DEFAULT_GLOB
+        out << "  specs:\n"
+      end
+
+      def eql?(o)
+        Mercurial === o      &&
+        uri == o.uri         &&
+        ref == o.ref         &&
+        name == o.name       &&
+        version == o.version
+      end
+
+      alias == eql?
+
+      def to_s
+        sref = options["ref"] ? shortref_for_display(options["ref"]) : ref
+        "#{uri} (at #{sref})"
+      end
+
+      def name
+        File.basename(@uri)
+      end
+
+      def path
+        @install_path ||= begin
+          hg_scope = "#{base_name}-#{shortref_for_path(revision)}"
+
+          if Bundler.requires_sudo?
+            Bundler.user_bundle_path.join(Bundler.ruby_scope).join(hg_scope)
+          else
+            Bundler.install_path.join(hg_scope)
+          end
+        end
+      end
+
+      def unlock!
+        @revision = nil
+      end
+
+      # TODO: actually cache hg specs
+      def specs(*)
+        if allow_hg_ops? && !@update
+          # Start by making sure the hg cache is up to date
+          cache
+          checkout
+          @update = true
+        end
+        local_specs
+      end
+
+      def install(spec)
+        Bundler.ui.info "Using #{spec.name} (#{spec.version}) from #{to_s} "
+
+        unless @installed
+          Bundler.ui.debug "  * Checking out revision: #{ref}"
+          checkout if allow_hg_ops?
+          @installed = true
+        end
+        generate_bin(spec)
+      end
+
+      def load_spec_files
+        super
+      rescue PathError, GitError
+        raise GitError, "#{to_s} is not checked out. Please run `bundle install`"
+      end
+
+    private
+
+      def hg(command)
+        if allow_hg_ops?
+          Bundler.ui.debug("Executing hg #{command}")
+          out = %x{hg #{command}}
+          Bundler.ui.debug("Output #{out}")
+
+          if $?.exitstatus != 0
+            raise GitError, "An error has occurred in hg when running `hg #{command}`. Cannot complete bundling."
+          end
+          out
+        else
+          raise GitError, "Bundler is trying to run a `hg #{command}` at runtime. You probably need to run `bundle install`. However, " \
+                          "this error message could probably be more useful. Please submit a ticket at http://github.com/carlhuda/bundler/issues " \
+                          "with steps to reproduce as well as the following\n\nCALLER: #{caller.join("\n")}"
+        end
+      end
+
+      def base_name
+        File.basename(uri) #this should be enough with hg path style
+      end
+
+      def shortref_for_display(ref)
+        ref[0..6]
+      end
+
+      def shortref_for_path(ref)
+        ref[0..11]
+      end
+
+      def uri_hash
+        if uri =~ %r{^\w+://(\w+@)?}
+          # Downcase the domain component of the URI
+          # and strip off a trailing slash, if one is present
+          input = URI.parse(uri).normalize.to_s.sub(%r{/$},'')
+        else
+          # If there is no URI scheme, assume it is an ssh/git URI
+          input = uri
+        end
+        Digest::SHA1.hexdigest(input)
+      end
+
+      def cache_path
+        @cache_path ||= begin
+          hg_scope = "#{base_name}-#{uri_hash}"
+
+          if Bundler.requires_sudo?
+            Bundler.user_bundle_path.join("cache/hg", hg_scope)
+          else
+            Bundler.cache.join("hg", hg_scope)
+          end
+        end
+      end
+
+      def cache
+        if cached?
+          return if has_revision_cached?
+          Bundler.ui.info "Updating #{uri}"
+          in_cache do
+            hg %|pull -q "#{uri}"|
+          end
+        else
+          Bundler.ui.info "Fetching #{uri}"
+          FileUtils.mkdir_p(cache_path.dirname)
+          hg %|clone --noupdate "#{uri}" "#{cache_path}"|
+        end
+      end
+
+      def checkout
+        unless File.exist?(path.join(".hg"))
+          FileUtils.mkdir_p(path.dirname)
+          FileUtils.rm_rf(path)
+          hg %|clone --noupdate "#{cache_path}" "#{path}"|
+        end
+        Dir.chdir(path) do
+          hg %|pull "#{cache_path}"|
+          hg "update -C #{revision}"
+        end
+      end
+
+      def has_revision_cached?
+        return unless @revision
+        in_cache do
+          if hg(%|-q log --style=compact|).split("\n").map() { |line| line[-12..13]}.include? @revision
+            return true 
+          end
+        end
+        false
+      rescue GitError
+        false
+      end
+
+      def allow_hg_ops?
+        @allow_remote || @allow_cached
+      end
+
+      def revision
+        @revision ||= begin
+          if allow_hg_ops?
+            in_cache do
+              hg("log -r #{ref} --style=default") =~ /\b\d+:(\w{12})$/
+              $1
+            end
+          else
+            raise GitError, "The git source #{uri} is not yet checked out. Please run `bundle install` before trying to start your application"
+          end
+        end
+      end
+
+      def cached?
+        cache_path.exist?
+      end
+
+      def in_cache(&blk)
+        cache unless cached?
+        Dir.chdir(cache_path, &blk)
+      end
+
+    end
+  end
+end

lib/bundler/source.rb

 
 module Bundler
   module Source
+    autoload :Mercurial, 'bundler/mercurial'
     # TODO: Refactor this class
     class Rubygems
       attr_reader :remotes

spec/install/mercurial_spec.rb

+require "spec_helper"
+
+describe "bundle install with hg sources" do
+  describe "when floating on master" do
+    before :each do
+      build_hg "foo" do |s|
+        s.executables = "foobar"
+      end
+
+      install_gemfile <<-G
+        source "file://#{gem_repo1}"
+        hg "#{lib_path('foo-1.0')}" do
+          gem 'foo'
+        end
+      G
+    end
+
+    it "fetches gems" do
+      should_be_installed("foo 1.0")
+
+      run <<-RUBY
+        require 'foo'
+        puts "WIN" unless defined?(FOO_PREV_REF)
+      RUBY
+
+      out.should == "WIN"
+    end
+
+    it "caches the hg repo" do
+      Dir["#{default_bundle_path}/cache/bundler/hg/foo-1.0-*"].should have(1).item
+    end
+
+    it "does not update the hg source implicitly" do
+      update_hg "foo"
+
+      in_app_root2 do
+        install_gemfile bundled_app2("Gemfile"), <<-G
+          hg "#{lib_path('foo-1.0')}" do
+            gem 'foo'
+          end
+        G
+      end
+
+      in_app_root do
+        run <<-RUBY
+          require 'foo'
+          puts "fail" if defined?(FOO_PREV_REF)
+        RUBY
+
+        out.should be_empty
+      end
+    end
+
+    it "setups executables" do
+      pending_jruby_shebang_fix
+      bundle "exec foobar"
+      out.should == "1.0"
+    end
+
+    it "complains if pinned specs don't exist in the hg repo" do
+#      build_hg "foo"
+
+      install_gemfile <<-G
+        gem "foo", "1.1", :hg => "#{lib_path('foo-1.0')}"
+      G
+
+      out.should include("Source contains 'foo' at: 1.0")
+    end
+
+    it "still works after moving the application directory" do
+      bundle "install --path vendor/bundle"
+      FileUtils.mv bundled_app, tmp('bundled_app.bck')
+
+      Dir.chdir tmp('bundled_app.bck')
+      should_be_installed "foo 1.0"
+    end
+
+    it "can still install after moving the application directory" do
+      bundle "install --path vendor/bundle"
+      FileUtils.mv bundled_app, tmp('bundled_app.bck')
+
+      update_hg "foo", "1.1", :path => lib_path("foo-1.0")
+
+      Dir.chdir tmp('bundled_app.bck')
+      gemfile tmp('bundled_app.bck/Gemfile'), <<-G
+        source "file://#{gem_repo1}"
+        hg "#{lib_path('foo-1.0')}" do
+          gem 'foo'
+        end
+
+        gem "rack", "1.0"
+      G
+
+      bundle "update foo"
+
+      should_be_installed "foo 1.1", "rack 1.0"
+    end
+
+  end
+
+  describe "with an empty hg block" do
+    before do
+      build_hg "foo"
+      gemfile <<-G
+        source "file://#{gem_repo1}"
+        gem "rack"
+
+        hg "#{lib_path("foo-1.0")}" do
+          # this page left intentionally blank
+        end
+      G
+    end
+
+    it "does not explode" do
+      bundle "install"
+      should_be_installed "rack 1.0"
+    end
+  end
+
+  describe "when specifying a revision" do
+    before(:each) do
+      build_hg "foo"
+      @revision = revision_for(lib_path("foo-1.0"))
+      update_hg "foo"
+    end
+
+    it "works" do
+      install_gemfile <<-G
+        hg "#{lib_path('foo-1.0')}", :ref => "#{@revision}" do
+          gem "foo"
+        end
+      G
+
+      run <<-RUBY
+        require 'foo'
+        puts "WIN" unless defined?(FOO_PREV_REF)
+      RUBY
+
+      out.should == "WIN"
+    end
+
+    it "works when the revision is a symbol" do
+      install_gemfile <<-G
+        hg "#{lib_path('foo-1.0')}", :ref => #{@revision.to_sym.inspect} do
+          gem "foo"
+        end
+      G
+      check err.should == ""
+
+      run <<-RUBY
+        require 'foo'
+        puts "WIN" unless defined?(FOO_PREV_REF)
+      RUBY
+
+      out.should == "WIN"
+    end
+  end
+
+  describe "specified inline" do
+    # TODO: Figure out how to write this test so that it is not flaky depending
+    #       on the current network situation.
+    # it "supports private git URLs" do
+    #   gemfile <<-G
+    #     gem "thingy", :git => "git@notthere.fallingsnow.net:somebody/thingy.git"
+    #   G
+    #
+    #   bundle :install, :expect_err => true
+    #
+    #   # p out
+    #   # p err
+    #   puts err unless err.empty? # This spec fails randomly every so often
+    #   err.should include("notthere.fallingsnow.net")
+    #   err.should include("ssh")
+    # end
+
+    it "installs from hg even if a newer gem is available elsewhere" do
+      build_hg "rack", "0.8"
+
+      install_gemfile <<-G
+        source "file://#{gem_repo1}"
+        gem "rack", :hg => "#{lib_path('rack-0.8')}"
+      G
+
+      should_be_installed "rack 0.8"
+    end
+
+    it "installs dependencies from hg even if a newer gem is available elsewhere" do
+      system_gems "rack-1.0.0"
+
+      build_lib "rack", "1.0", :path => lib_path('nested/bar') do |s|
+        s.write "lib/rack.rb", "puts 'WIN OVERRIDE'"
+      end
+
+      build_hg "foo", :path => lib_path('nested') do |s|
+        s.add_dependency "rack", "= 1.0"
+      end
+
+      install_gemfile <<-G
+        source "file://#{gem_repo1}"
+        gem "foo", :hg => "#{lib_path('nested')}"
+      G
+
+      run "require 'rack'"
+      out.should == 'WIN OVERRIDE'
+    end
+
+    it "correctly unlocks when changing to a hg source" do
+      install_gemfile <<-G
+        source "file://#{gem_repo1}"
+        gem "rack", "0.9.1"
+      G
+
+      build_hg "rack", :path => lib_path("rack")
+
+      install_gemfile <<-G
+        source "file://#{gem_repo1}"
+        gem "rack", "1.0.0", :hg => "#{lib_path('rack')}"
+      G
+
+      should_be_installed "rack 1.0.0"
+    end
+
+    it "correctly unlocks when changing to a hg source without versions" do
+      install_gemfile <<-G
+        source "file://#{gem_repo1}"
+        gem "rack"
+      G
+
+      build_hg "rack", "1.2", :path => lib_path("rack")
+
+      install_gemfile <<-G
+        source "file://#{gem_repo1}"
+        gem "rack", :hg => "#{lib_path('rack')}"
+      G
+
+      should_be_installed "rack 1.2"
+    end
+  end
+
+  describe "block syntax" do
+    it "pulls all gems from a hg block" do
+      build_lib "omg", :path => lib_path('hi2u/omg')
+      build_lib "hi2u", :path => lib_path('hi2u')
+
+      install_gemfile <<-G
+        path "#{lib_path('hi2u')}" do
+          gem "omg"
+          gem "hi2u"
+        end
+      G
+
+      should_be_installed "omg 1.0", "hi2u 1.0"
+    end
+  end
+
+  it "uses a ref if specified" do
+    build_hg "foo"
+    @revision = revision_for(lib_path("foo-1.0"))
+    update_hg "foo"
+
+    install_gemfile <<-G
+      gem "foo", :hg => "#{lib_path('foo-1.0')}", :ref => "#{@revision}"
+    G
+
+    run <<-RUBY
+      require 'foo'
+      puts "WIN" unless defined?(FOO_PREV_REF)
+    RUBY
+
+    out.should == "WIN"
+  end
+
+  it "correctly handles cases with invalid gemspecs" do
+    build_hg "foo" do |s|
+      s.summary = nil
+    end
+
+    install_gemfile <<-G
+      source "file://#{gem_repo1}"
+      gem "foo", :hg => "#{lib_path('foo-1.0')}"
+      gem "rails", "2.3.2"
+    G
+
+    should_be_installed "foo 1.0"
+    should_be_installed "rails 2.3.2"
+  end
+
+  it "runs the gemspec in the context of its parent directory" do
+    build_lib "bar", :path => lib_path("foo/bar"), :gemspec => false do |s|
+      s.write lib_path("foo/bar/lib/version.rb"), %{BAR_VERSION = '1.0'}
+      s.write "bar.gemspec", <<-G
+        $:.unshift Dir.pwd # For 1.9
+        require 'lib/version'
+        Gem::Specification.new do |s|
+          s.name        = 'bar'
+          s.version     = BAR_VERSION
+          s.summary     = 'Bar'
+          s.files       = Dir["lib/**/*.rb"]
+        end
+      G
+    end
+
+    build_hg "foo", :path => lib_path("foo") do |s|
+      s.write "bin/foo", ""
+    end
+
+    install_gemfile <<-G
+      source "file://#{gem_repo1}"
+      gem "bar", :hg => "#{lib_path("foo")}"
+      gem "rails", "2.3.2"
+    G
+
+    should_be_installed "bar 1.0"
+    should_be_installed "rails 2.3.2"
+  end
+
+  it "installs from hg even if a rubygems gem is present" do
+    build_gem "foo", "1.0", :path => lib_path('fake_foo'), :to_system => true do |s|
+      s.write "lib/foo.rb", "raise 'FAIL'"
+    end
+
+    build_hg "foo", "1.0"
+
+    install_gemfile <<-G
+      gem "foo", "1.0", :hg => "#{lib_path('foo-1.0')}"
+    G
+
+    should_be_installed "foo 1.0"
+  end
+
+  it "fakes the gem out if there is no gemspec" do
+    build_hg "foo", :gemspec => false
+
+    install_gemfile <<-G
+      source "file://#{gem_repo1}"
+      gem "foo", "1.0", :hg => "#{lib_path('foo-1.0')}"
+      gem "rails", "2.3.2"
+    G
+
+    should_be_installed("foo 1.0")
+    should_be_installed("rails 2.3.2")
+  end
+
+  it "catches hg errors and spits out useful output" do
+    gemfile <<-G
+      gem "foo", "1.0", :hg => "omgomg"
+    G
+
+    oldEnv = ENV['LC_MESSAGES']
+    ENV['LC_MESSAGES'] = 'C' #ensure English output
+    bundle :install, :expect_err => true
+
+    out.should include("An error has occurred in hg")
+    err.should include("abort", "omgomg", "not found!")
+    ENV['LC_MESSAGES'] = oldEnv
+  end
+
+  it "works when the gem path has spaces in it" do
+    build_hg "foo", :path => lib_path('foo space-1.0')
+
+    install_gemfile <<-G
+      gem "foo", :hg => "#{lib_path('foo space-1.0')}"
+    G
+
+    should_be_installed "foo 1.0"
+  end
+
+  it "handles repos that have been force-pushed" do
+    build_hg "forced", "1.0"
+
+    install_gemfile <<-G
+      hg "#{lib_path('forced-1.0')}" do
+        gem 'forced'
+      end
+    G
+    should_be_installed "forced 1.0"
+
+    update_hg "forced" do |s|
+      s.write "lib/forced.rb", "FORCED = '1.1'"
+    end
+
+    bundle "update"
+    should_be_installed "forced 1.1"
+
+    Dir.chdir(lib_path('forced-1.0')) do
+      `hg backout tip -m "backout"`
+    end
+
+    bundle "update"
+    should_be_installed "forced 1.0"
+  end
+
+
+  it "handles implicit updates when modifying the source info" do
+    hg = build_hg "foo"
+
+    install_gemfile <<-G
+      hg "#{lib_path('foo-1.0')}" do
+        gem "foo"
+      end
+    G
+
+    update_hg "foo"
+    update_hg "foo"
+
+    install_gemfile <<-G
+      hg "#{lib_path('foo-1.0')}", :ref => "#{hg.ref_for('-2')}" do
+        gem "foo"
+      end
+    G
+
+    run <<-RUBY
+      require 'foo'
+      puts "WIN" if FOO_PREV_REF == '#{hg.ref_for("-3")}'
+    RUBY
+
+    out.should == "WIN"
+  end
+
+  it "does not to a remote fetch if the revision is cached locally" do
+    build_hg "foo"
+
+    install_gemfile <<-G
+      gem "foo", :hg => "#{lib_path('foo-1.0')}"
+    G
+
+    FileUtils.rm_rf(lib_path('foo-1.0'))
+
+    bundle "install"
+    out.should_not =~ /updating/i
+  end
+
+  it "doesn't blow up if bundle install is run twice in a row" do
+    build_hg "foo"
+
+    gemfile <<-G
+      gem "foo", :hg => "#{lib_path('foo-1.0')}"
+    G
+
+    bundle "install"
+    bundle "install", :exitstatus => true
+    exitstatus.should == 0
+  end
+
+  describe "switching sources" do
+    it "doesn't explode when switching Path to hg sources" do
+      build_gem "foo", "1.0", :to_system => true do |s|
+        s.write "lib/foo.rb", "raise 'fail'"
+      end
+      build_lib "foo", "1.0", :path => lib_path('bar/foo')
+      build_hg "bar", "1.0", :path => lib_path('bar') do |s|
+        s.add_dependency 'foo'
+      end
+
+      install_gemfile <<-G
+        source "file://#{gem_repo1}"
+        gem "bar", :path => "#{lib_path('bar')}"
+      G
+
+      install_gemfile <<-G
+        source "file://#{gem_repo1}"
+        gem "bar", :hg => "#{lib_path('bar')}"
+      G
+
+      should_be_installed "foo 1.0", "bar 1.0"
+    end
+
+    it "doesn't explode when switching Gem to hg source" do
+      install_gemfile <<-G
+        source "file://#{gem_repo1}"
+        gem "rack-obama"
+        gem "rack", "1.0.0"
+      G
+
+      build_hg "rack", "1.0" do |s|
+        s.write "lib/new_file.rb", "puts 'USING HG'"
+      end
+
+      install_gemfile <<-G
+        source "file://#{gem_repo1}"
+        gem "rack-obama"
+        gem "rack", "1.0.0", :hg => "#{lib_path("rack-1.0")}"
+      G
+
+      run "require 'new_file'"
+      out.should == "USING HG"
+    end
+  end
+
+  describe "bundle install after the remote has been updated" do
+    it "installs" do
+      build_hg "valim"
+
+      install_gemfile <<-G
+        gem "valim", :hg => "file://#{lib_path("valim-1.0")}"
+      G
+
+      old_revision = revision_for(lib_path("valim-1.0"))
+      update_hg "valim"
+      new_revision = revision_for(lib_path("valim-1.0"))
+
+      lockfile = File.read(bundled_app("Gemfile.lock"))
+      File.open(bundled_app("Gemfile.lock"), "w") do |file|
+        file.puts lockfile.gsub(/revision: #{old_revision}/, "revision: #{new_revision}")
+      end
+
+      bundle "install"
+
+      run <<-R
+        require "valim"
+        puts VALIM_PREV_REF
+      R
+
+      out.should == old_revision
+    end
+  end
+
+  describe "bundle install --deployment with hg sources" do
+    it "works" do
+      build_hg "valim", :path => lib_path('valim')
+
+      install_gemfile <<-G
+        source "file://#{gem_repo1}"
+
+        gem "valim", "= 1.0", :hg => "#{lib_path('valim')}"
+      G
+
+      simulate_new_machine
+
+      bundle "install --deployment", :exitstatus => true
+      exitstatus.should == 0
+    end
+  end
+end

spec/support/builders.rb

       build_with(GitUpdater, name, args, &block)
     end
 
+    def build_hg(name, *args, &block)
+      opts = args.last.is_a?(Hash) ? args.last : {}
+      builder = opts[:bare] ? HgBareBuilder : HgBuilder
+      spec = build_with(builder, name, args, &block)
+      HgReader.new(opts[:path] || lib_path(spec.full_name))
+    end
+
+    def update_hg(name, *args, &block)
+      build_with(HgUpdater, name, args, &block)
+    end
+
   private
 
     def build_with(builder, name, args, &blk)
         @context.gem_repo1('gems')
       end
     end
+
+    class HgBareBuilder < LibBuilder
+      def _build(options)
+        path = options[:path] || _default_path
+        super(options.merge(:path => path))
+        Dir.chdir(path) do
+          `hg init .`
+          open('.hg/hgrc','a') { |f| f << "[ui]\nusername= Me The Tester <me@tester.org>\n"}
+        end
+      end
+    end
+
+    class HgBuilder < HgBareBuilder
+      def _build(options)
+        path = options[:path] || _default_path
+        super(options.merge(:path => path))
+        Dir.chdir(path) do
+          `hg add *`
+          `hg commit -m 'OMG INITIAL COMMIT'`
+        end
+      end
+    end
+
+    class HgUpdater < LibBuilder
+      WINDOWS = RbConfig::CONFIG["host_os"] =~ %r!(msdos|mswin|djgpp|mingw)!
+      NULL    = WINDOWS ? "NUL" : "/dev/null"
+
+      def silently(str)
+        `#{str} 2>#{NULL}`
+      end
+
+      def _build(options)
+        libpath = options[:path] || _default_path
+
+        Dir.chdir(libpath) do
+          silently "hg update -C default"
+
+          if branch = options[:branch]
+            raise "You can't specify `default` as the branch" if branch == "default"
+
+            if `hg branches | grep #{branch}`.empty?
+              silently("hg branch #{branch}")
+              silently("hg commit -m 'branching'")
+            end
+
+            silently("hg update -C #{branch}")
+          elsif tag = options[:tag]
+            `hg tag #{tag}`
+          elsif options[:remote]
+            open('.hg/hgrc','a') {|f| f << "[paths]\ndefault = #{options[:remote]}\n"}
+          elsif options[:push]
+            silently("hg push #{options[:push]}")
+          end
+
+          `hg log -r tip --style=default` =~ /\b\d+:(\w{12})$/
+          current_ref = $1
+          _default_files.keys.each do |path|
+            _default_files[path] << "\n#{Builders.constantize(name)}_PREV_REF = '#{current_ref}'"
+          end
+          super(options.merge(:path => libpath))
+          `hg addremove`
+          `hg commit -m "BUMP"`
+        end
+      end
+    end
+
+    class HgReader
+      attr_reader :path
+
+      def initialize(path)
+        @path = path
+      end
+
+      def ref_for(ref, len = nil)
+        Dir.chdir(@path) do
+          `hg log -r #{ref} --style=default` =~ /\b\d+:(\w{12})$/
+          ref = $1
+          ref = ref[0..len] if len
+          ref
+        end
+      end
+    end
+
   end
 end

spec/support/helpers.rb

     end
 
     def revision_for(path)
-      Dir.chdir(path) { `git rev-parse HEAD`.strip }
+      Dir.chdir(path) do
+        if File.exist? '.git'
+          `git rev-parse HEAD`.strip
+        else
+          `hg log -r tip --style=default` =~ /\b\d+:(\w{12})$/
+          $1
+        end
+      end
     end
   end
 end