implement better AGPL compliance
authorLunar <lunar@anargeek.net>
Wed, 13 Mar 2013 11:39:58 +0000 (12:39 +0100)
committerLunar <lunar@anargeek.net>
Thu, 14 Mar 2013 09:16:59 +0000 (10:16 +0100)
We now detect if Coquelicot is running from a serviceable Git clone.
When it is the case, we offer to retrieve source using Git as we did
previously.

If there is a Git repository which is not usable, a warning is sent to the
logs.

In case source can't be provided by Git, we offer a link to an on-the-fly
created Gem that can be downloaded and unpacked. The version number of
the running software is mangled to add the server hostname and a date.

lib/coquelicot/app.rb
lib/coquelicot/helpers.rb
spec/coquelicot/app_spec.rb
views/layout.haml

index 3487459..ed230ae 100644 (file)
@@ -26,6 +26,7 @@ require 'moneta'
 require 'unicorn/launcher'
 require 'rainbows'
 require 'optparse'
+require 'rubygems/package'
 
 module Coquelicot
   class << self
@@ -281,6 +282,33 @@ module Coquelicot
       haml :about_your_data
     end
 
+    get '/source' do
+      Gem::DefaultUserInteraction.ui = Gem::SilentUI.new
+
+      spec = Gem::loaded_specs['coquelicot'].clone
+      Dir.chdir(spec.full_gem_path) do
+        spec.version = gem_version
+        spec.mark_version
+        spec.validate
+        Tempfile.open('coquelicot-gem') do |gem_file|
+          Gem::Package.open(gem_file, 'w', nil) do |pkg|
+            pkg.metadata = spec.to_yaml
+            spec.files.each do |file|
+              next if File.directory?(file)
+              stat = File.stat(file)
+              mode = stat.mode & 0777
+              size = stat.size
+              pkg.add_file_simple(file, mode, size) do |tar_io|
+                tar_io.write(open(file, "rb") { |f| f.read })
+              end
+            end
+          end
+          send_file gem_file.path, :filename => spec.file_name
+          #gem_file.unlink
+        end
+      end
+    end
+
     get '/random_pass' do
       "#{Coquelicot.gen_random_pass}"
     end
index e621db2..b2acd7a 100644 (file)
 
 module Coquelicot
   module Helpers
-    def clone_url
-      settings.respond_to?(:clone_url) ? settings.clone_url : uri('coquelicot.git')
+    def can_provide_git_repository?
+      return @@can_provide_git_repository if defined?(@@can_provide_git_repository)
+
+      # Test if `git update-server-info` was executed in the local repository
+      @@can_provide_git_repository =
+         File.readable?(File.expand_path('coquelicot.git/info/refs', settings.public_folder)) &&
+         File.readable?(File.expand_path('coquelicot.git/objects/info/packs', settings.public_folder))
+
+      if File.readable?(File.expand_path('coquelicot.git', settings.public_folder)) &&
+         !@@can_provide_git_repository
+        logger.warn <<-MESSAGE.gsub(/\n */m, ' ').strip
+          Unable to provide access to local Git repository. Please ensure that
+          you have run `git update-server-info` in the Coquelicot directory,
+          and that the symlink `public/coquelicot.git` is properly set.
+        MESSAGE
+      end
+      @@can_provide_git_repository
+    end
+
+    def gem_hostname
+      # We need to mangle the hostname to fits Gem::Version constraints
+      @@hostname ||= Socket.gethostname.gsub(/[^0-9a-zA-Z]/, '')
+    end
+
+    def gem_version
+      spec = Gem::loaded_specs['coquelicot']
+      current_version = spec.version.to_s.gsub(/\.[0-9a-zA-Z]+\.[0-9]{8}/, '')
+      Gem::Version.new("#{current_version}.#{gem_hostname}.#{Date.today.strftime('%Y%m%d')}")
+    end
+
+    def clone_command
+      if can_provide_git_repository?
+        "git clone #{uri('coquelicot.git')}"
+      else
+        "curl -OJ #{uri('source')} && gem unpack coqueliot-#{gem_version}.gem"
+      end
     end
 
     def authenticate(params)
index 4df6df6..417c19b 100644 (file)
@@ -19,6 +19,7 @@ require 'spec_helper'
 require 'coquelicot/jyraphe_migrator'
 require 'capybara/dsl'
 require 'tempfile'
+require 'timecop'
 
 describe Coquelicot::Application do
   include Rack::Test::Methods
@@ -191,6 +192,54 @@ describe Coquelicot::Application do
         end
       end
     end
+    context 'when a local Git repository is usable' do
+      before(:each) do
+        # Might be pretty brittle… but will do for now
+        Coquelicot::Helpers.module_eval('remove_class_variable :@@can_provide_git_repository if defined? @@can_provide_git_repository')
+        File.stub(:readable?).and_return(true)
+      end
+      it 'should offer a "git clone" to the local URI' do
+        visit '/'
+        find('#footer').should have_content('git clone http://www.example.com/coquelicot.git')
+      end
+    end
+    context 'when a local Git repository is not usable' do
+      before(:each) do
+        # Might be pretty brittle… but will do for now
+        Coquelicot::Helpers.module_eval('remove_class_variable :@@can_provide_git_repository')
+        File.stub(:readable?) do |p|
+          p.end_with?('.git')
+        end
+      end
+      it 'should offer a link to retrieve the source' do
+        visit '/'
+        find('#footer').text.should =~ /curl.*gem unpack.*\.gem$/
+      end
+      it 'should log a warning' do
+        logger = double('Logger')
+        logger.should_receive(:warn).with(/Unable to provide access to local Git repository/)
+        app.any_instance.stub(:logger).and_return(logger)
+        visit '/'
+      end
+      it 'should log a warning only on the first request' do
+        logger = double('Logger')
+        logger.should_receive(:warn).once
+        app.any_instance.stub(:logger).and_return(logger)
+        visit '/'
+        visit '/'
+      end
+    end
+    context 'when there is no local Git repository' do
+      before(:each) do
+        # Might be pretty brittle… but will do for now
+        Coquelicot::Helpers.module_eval('remove_class_variable :@@can_provide_git_repository')
+        File.stub(:readable?).and_return(false)
+      end
+      it 'should offer a link to retrieve the source' do
+        visit '/'
+        find('#footer').text.should =~ /curl.*gem unpack.*\.gem$/
+      end
+    end
   end
 
   describe 'get /README' do
@@ -222,6 +271,51 @@ describe Coquelicot::Application do
     end
   end
 
+  describe 'get /source' do
+    context 'when the server hostname is one-cool-hostname' do
+      before(:each) do
+        Coquelicot::Helpers.module_eval('remove_class_variable :@@hostname if defined? @@hostname')
+        Socket.stub(:gethostname).and_return('one-cool-hostname')
+        visit '/source'
+      end
+      it 'should send a file to be saved' do
+        page.response_headers['Content-Type'].should == 'application/octet-stream'
+        page.response_headers['Content-Disposition'].should =~ /^attachment;/
+      end
+      it 'should send a file with a proposed name correct for coquelicot gem' do
+        page.response_headers['Content-Disposition'].should =~ /filename="coquelicot-.*\.gem"/
+      end
+      context 'the downloaded gem' do
+        around(:each) do |example|
+          Gem::Package.open(StringIO.new(page.driver.response.body)) do |gem|
+            @gem = gem
+            example.run
+          end
+        end
+        it 'should be named "coquelicot"' do
+          @gem.metadata.name.should == 'coquelicot'
+        end
+        it "should have a version containing 'onecoolhostname' for the hostname" do
+          @gem.metadata.version.to_s.should =~ /\.onecoolhostname\./
+        end
+        it "should have a version containing today's date" do
+          Timecop.freeze(Time.now) do
+            date_str = Date.today.strftime('%Y%m%d')
+            @gem.metadata.version.to_s.should =~ /\.#{date_str}$/
+          end
+        end
+        it 'should at least contain this spec file' do
+          this_file = __FILE__.gsub(/^.*\/spec/, 'spec')
+          content = nil
+          @gem.each do |file|
+            content = file.read if file.full_name.end_with?(this_file)
+          end
+          content.should == File.open(__FILE__, 'rb').read
+        end
+      end
+    end
+  end
+
   describe 'post /authenticate' do
     context 'when given a request with too much input' do
       before do
index f7f2539..88271e2 100644 (file)
     #container
       = yield
     #footer
-      %a{ :href => 'about-your-data' }= 'About your data…'
-      = '—'
-      %a{ :href => 'README' }= 'Coquelicot'
-      %span= '© 2010-2013 potager.org'
-      %span
+      %div
+        %a{ :href => 'about-your-data' }= 'About your data…'
         = '—'
+        %a{ :href => 'README' }= 'Coquelicot'
+        = '© 2010-2013 potager.org —'
         %a{ :href => 'http://www.gnu.org/licenses/agpl.txt' }= 'AGPLv3'
-        = '—'
-      %span
-        %code= "git clone #{clone_url}"
+      %div
+        %code= "#{clone_command}"