diff options
-rw-r--r-- | lib/chef/provider/git.rb | 33 | ||||
-rw-r--r-- | spec/data/git_bundles/example-repo.gitbundle | bin | 0 -> 1214 bytes | |||
-rw-r--r-- | spec/functional/resource/git_spec.rb | 229 | ||||
-rw-r--r-- | spec/unit/provider/git_spec.rb | 20 |
4 files changed, 273 insertions, 9 deletions
diff --git a/lib/chef/provider/git.rb b/lib/chef/provider/git.rb index cc524a2fcd..fcc7c81a99 100644 --- a/lib/chef/provider/git.rb +++ b/lib/chef/provider/git.rb @@ -218,12 +218,37 @@ class Chef def remote_resolve_reference Chef::Log.debug("#{@new_resource} resolving remote reference") - command = git('ls-remote', @new_resource.repository, @new_resource.revision) + # The sha pointed to by an annotated tag is identified by the + # '^{}' suffix appended to the tag. In order to resolve + # annotated tags, we have to search for "revision*" and + # post-process. Special handling for 'HEAD' to ignore a tag + # named 'HEAD'. + rev_pattern = case @new_resource.revision + when '', 'HEAD' + 'HEAD' + else + @new_resource.revision + '*' + end + command = git('ls-remote', @new_resource.repository, rev_pattern) @resolved_reference = shell_out!(command, run_options).stdout - if @resolved_reference =~ /^([0-9a-f]{40})\s+(\S+)/ - $1 + ref_lines = @resolved_reference.split("\n") + refs = ref_lines.map { |line| line.split("\t") } + # first try for ^{} indicating the commit pointed to by an + # annotated tag + tagged_commit = refs.find { |m| m[1].end_with?("#{@new_resource.revision}^{}") } + # It is possible for a user to create a tag named 'HEAD'. + # Using such a degenerate annotated tag would be very + # confusing. We avoid the issue by disallowing the use of + # annotated tags named 'HEAD'. + if tagged_commit && rev_pattern != 'HEAD' + tagged_commit[0] else - nil + found = refs.find { |m| m[1].end_with?(@new_resource.revision) } + if found + found[0] + else + nil + end end end diff --git a/spec/data/git_bundles/example-repo.gitbundle b/spec/data/git_bundles/example-repo.gitbundle Binary files differnew file mode 100644 index 0000000000..de08296dc3 --- /dev/null +++ b/spec/data/git_bundles/example-repo.gitbundle diff --git a/spec/functional/resource/git_spec.rb b/spec/functional/resource/git_spec.rb new file mode 100644 index 0000000000..d4578341cd --- /dev/null +++ b/spec/functional/resource/git_spec.rb @@ -0,0 +1,229 @@ +# +# Author:: Seth Falcon (<seth@opscode.com>) +# Copyright:: Copyright (c) 2013 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'spec_helper' +require 'chef/mixin/shell_out' +require 'tmpdir' + +# Deploy relies heavily on symlinks, so it doesn't work on windows. +describe Chef::Resource::Git do + include Chef::Mixin::ShellOut + let(:file_cache_path) { Dir.mktmpdir } + let(:deploy_directory) { Dir.mktmpdir } + + let(:node) do + Chef::Node.new.tap do |n| + n.name "rspec-test" + n.consume_external_attrs(@ohai.data, {}) + end + end + + let(:event_dispatch) { Chef::EventDispatch::Dispatcher.new } + let(:run_context) { Chef::RunContext.new(node, {}, event_dispatch) } + + # These tests use git's bundle feature, which is a way to export an entire + # git repo (or subset of commits) as a single file. + # + # Generally you can treat a git bundle as a regular git remote. + # + # See also: http://git-scm.com/2010/03/10/bundles.html + # + # Beware that git bundles don't behave exactly the same as real + # remotes. To get closer to real remotes, we'll create a local clone + # of the bundle to use as a remote for the tests. This at least + # gives the expected responses for ls-remote using git version + # 1.7.12.4 + let(:git_bundle_repo) { File.expand_path("git_bundles/example-repo.gitbundle", CHEF_SPEC_DATA) } + let(:origin_repo_dir) { Dir.mktmpdir } + let(:origin_repo) { "#{origin_repo_dir}/example" } + + # This is the fourth version + let(:v1_commit) { "bc5ec79931ae74089aeadca6edc173527613e6d9" } + let(:v1_tag) { "9b73fb5e316bfaff7b822b0ccb3e1e08f9885085" } + let(:rev_foo) { "ed181b3419b6f489bedab282348162a110d6d3a1" } + let(:rev_testing) { "972d153654503bccec29f630c5dd369854a561e8" } + let(:rev_head) { "d294fbfd05aa7709ad9a9b8ef6343b17d355bf5f"} + + before(:each) do + @old_file_cache_path = Chef::Config[:file_cache_path] + shell_out!("git clone #{git_bundle_repo} example", :cwd => origin_repo_dir) + Chef::Config[:file_cache_path] = file_cache_path + end + + after(:each) do + Chef::Config[:file_cache_path] = @old_file_cache_path + FileUtils.remove_entry_secure deploy_directory if File.exist?(deploy_directory) + FileUtils.remove_entry_secure file_cache_path + end + + after(:all) do + FileUtils.remove_entry_secure origin_repo_dir + end + + before(:all) do + @ohai = Ohai::System.new + @ohai.require_plugin("os") + end + + context "when deploying from an annotated tag" do + let(:basic_git_resource) do + Chef::Resource::Git.new(deploy_directory, run_context).tap do |r| + r.repository origin_repo + r.revision "v1.0.0" + end + end + + # We create a copy of the basic_git_resource so that we can run + # the resource again and verify that it doesn't update. + let(:copy_git_resource) do + Chef::Resource::Git.new(deploy_directory, run_context).tap do |r| + r.repository origin_repo + r.revision "v1.0.0" + end + end + + it "checks out the revision pointed to by the tag commit, not the tag commit itself" do + basic_git_resource.run_action(:sync) + head_rev = shell_out!('git rev-parse HEAD', :cwd => deploy_directory, :returns => [0]).stdout.strip + head_rev.should == v1_commit + # also verify the tag commit itself is what we expect as an extra sanity check + rev = shell_out!('git rev-parse v1.0.0', :cwd => deploy_directory, :returns => [0]).stdout.strip + rev.should == v1_tag + end + + it "doesn't update if up-to-date" do + # this used to fail because we didn't resolve the annotated tag + # properly to the pointed to commit. + basic_git_resource.run_action(:sync) + head_rev = shell_out!('git rev-parse HEAD', :cwd => deploy_directory, :returns => [0]).stdout.strip + head_rev.should == v1_commit + + copy_git_resource.run_action(:sync) + copy_git_resource.should_not be_updated + end + end + + context "when deploying from a SHA revision" do + let(:basic_git_resource) do + Chef::Resource::Git.new(deploy_directory, run_context).tap do |r| + r.repository git_bundle_repo + end + end + + # We create a copy of the basic_git_resource so that we can run + # the resource again and verify that it doesn't update. + let(:copy_git_resource) do + Chef::Resource::Git.new(deploy_directory, run_context).tap do |r| + r.repository origin_repo + end + end + + it "checks out the expected revision ed18" do + basic_git_resource.revision rev_foo + basic_git_resource.run_action(:sync) + head_rev = shell_out!('git rev-parse HEAD', :cwd => deploy_directory, :returns => [0]).stdout.strip + head_rev.should == rev_foo + end + + it "doesn't update if up-to-date" do + basic_git_resource.revision rev_foo + basic_git_resource.run_action(:sync) + head_rev = shell_out!('git rev-parse HEAD', :cwd => deploy_directory, :returns => [0]).stdout.strip + head_rev.should == rev_foo + + copy_git_resource.revision rev_foo + copy_git_resource.run_action(:sync) + copy_git_resource.should_not be_updated + end + + it "checks out the expected revision 972d" do + basic_git_resource.revision rev_testing + basic_git_resource.run_action(:sync) + head_rev = shell_out!('git rev-parse HEAD', :cwd => deploy_directory, :returns => [0]).stdout.strip + head_rev.should == rev_testing + end + end + + context "when deploying from a revision named 'HEAD'" do + let(:basic_git_resource) do + Chef::Resource::Git.new(deploy_directory, run_context).tap do |r| + r.repository origin_repo + r.revision 'HEAD' + end + end + + it "checks out the expected revision" do + basic_git_resource.run_action(:sync) + head_rev = shell_out!('git rev-parse HEAD', :cwd => deploy_directory, :returns => [0]).stdout.strip + head_rev.should == rev_head + end + end + + context "when deploying from the default revision" do + let(:basic_git_resource) do + Chef::Resource::Git.new(deploy_directory, run_context).tap do |r| + r.repository origin_repo + # use default + end + end + + it "checks out HEAD as the default revision" do + basic_git_resource.run_action(:sync) + head_rev = shell_out!('git rev-parse HEAD', :cwd => deploy_directory, :returns => [0]).stdout.strip + head_rev.should == rev_head + end + end + + context "when dealing with a repo with a degenerate tag named 'HEAD'" do + before do + shell_out!("git tag -m\"degenerate tag\" HEAD ed181b3419b6f489bedab282348162a110d6d3a1", + :cwd => origin_repo) + end + + let(:basic_git_resource) do + Chef::Resource::Git.new(deploy_directory, run_context).tap do |r| + r.repository origin_repo + r.revision 'HEAD' + end + end + + let(:git_resource_default_rev) do + Chef::Resource::Git.new(deploy_directory, run_context).tap do |r| + r.repository origin_repo + # use default of revision + end + end + + it "checks out the (master) HEAD revision and ignores the tag" do + basic_git_resource.run_action(:sync) + head_rev = shell_out!('git rev-parse HEAD', + :cwd => deploy_directory, + :returns => [0]).stdout.strip + head_rev.should == rev_head + end + + it "checks out the (master) HEAD revision when no revision is specified (ignores tag)" do + git_resource_default_rev.run_action(:sync) + head_rev = shell_out!('git rev-parse HEAD', + :cwd => deploy_directory, + :returns => [0]).stdout.strip + head_rev.should == rev_head + end + + end +end diff --git a/spec/unit/provider/git_spec.rb b/spec/unit/provider/git_spec.rb index 275b2907ec..c700aada22 100644 --- a/spec/unit/provider/git_spec.rb +++ b/spec/unit/provider/git_spec.rb @@ -91,11 +91,21 @@ describe Chef::Provider::Git do it "converts resource.revision from a tag to a SHA" do @resource.revision "v1.0" - @stdout = "503c22a5e41f5ae3193460cca044ed1435029f53\trefs/heads/0.8-alpha\n" - @provider.should_receive(:shell_out!).with(@git_ls_remote + "v1.0", {:log_tag=>"git[web2.0 app]", :log_level=>:debug}).and_return(mock("ShellOut result", :stdout => @stdout)) + @stdout = ("d03c22a5e41f5ae3193460cca044ed1435029f53\trefs/heads/0.8-alpha\n" + + "503c22a5e41f5ae3193460cca044ed1435029f53\trefs/heads/v1.0\n") + @provider.should_receive(:shell_out!).with(@git_ls_remote + "v1.0*", {:log_tag=>"git[web2.0 app]", :log_level=>:debug}).and_return(mock("ShellOut result", :stdout => @stdout)) @provider.target_revision.should eql("503c22a5e41f5ae3193460cca044ed1435029f53") end + it "converts resource.revision from an annotated tag to the tagged SHA (not SHA of tag)" do + @resource.revision "v1.0" + @stdout = ("d03c22a5e41f5ae3193460cca044ed1435029f53\trefs/heads/0.8-alpha\n" + + "503c22a5e41f5ae3193460cca044ed1435029f53\trefs/heads/v1.0\n" + + "663c22a5e41f5ae3193460cca044ed1435029f53\trefs/heads/v1.0^{}\n") + @provider.should_receive(:shell_out!).with(@git_ls_remote + "v1.0*", {:log_tag=>"git[web2.0 app]", :log_level=>:debug}).and_return(mock("ShellOut result", :stdout => @stdout)) + @provider.target_revision.should eql("663c22a5e41f5ae3193460cca044ed1435029f53") + end + it "raises an invalid remote reference error if you try to deploy from ``origin'' and assertions are run" do @resource.revision "origin/" @provider.action = :checkout @@ -119,9 +129,9 @@ describe Chef::Provider::Git do end it "does not raise an error when the revision is valid and assertions are run." do - @resource.revision "v1.0" + @resource.revision "0.8-alpha" @stdout = "503c22a5e41f5ae3193460cca044ed1435029f53\trefs/heads/0.8-alpha\n" - @provider.should_receive(:shell_out!).with(@git_ls_remote + "v1.0", {:log_tag=>"git[web2.0 app]", :log_level=>:debug}).and_return(mock("ShellOut result", :stdout => @stdout)) + @provider.should_receive(:shell_out!).with(@git_ls_remote + "0.8-alpha*", {:log_tag=>"git[web2.0 app]", :log_level=>:debug}).and_return(mock("ShellOut result", :stdout => @stdout)) @provider.action = :checkout ::File.stub!(:directory?).with("/my/deploy").and_return(true) @provider.define_resource_requirements @@ -146,7 +156,7 @@ b7d19519a1c15f1c1a324e2683bd728b6198ce5a\trefs/tags/0.7.8^{} ebc1b392fe7e8f0fbabc305c299b4d365d2b4d9b\trefs/tags/chef-server-package SHAS @resource.revision '' - @provider.should_receive(:shell_out!).with(@git_ls_remote, {:log_tag=>"git[web2.0 app]", :log_level=>:debug}).and_return(mock("ShellOut result", :stdout => @stdout)) + @provider.should_receive(:shell_out!).with(@git_ls_remote + "HEAD", {:log_tag=>"git[web2.0 app]", :log_level=>:debug}).and_return(mock("ShellOut result", :stdout => @stdout)) @provider.target_revision.should eql("28af684d8460ba4793eda3e7ac238c864a5d029a") end end |