You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@buildr.apache.org by bo...@apache.org on 2009/03/02 21:12:01 UTC

svn commit: r749429 - in /buildr/trunk: CHANGELOG lib/buildr/core/build.rb spec/core/build_spec.rb

Author: boisvert
Date: Mon Mar  2 20:12:00 2009
New Revision: 749429

URL: http://svn.apache.org/viewvc?rev=749429&view=rev
Log:
BUILDR-222 Support Git as a version control system
BUILDR-223 Release Task: customizable commit message


Modified:
    buildr/trunk/CHANGELOG
    buildr/trunk/lib/buildr/core/build.rb
    buildr/trunk/spec/core/build_spec.rb

Modified: buildr/trunk/CHANGELOG
URL: http://svn.apache.org/viewvc/buildr/trunk/CHANGELOG?rev=749429&r1=749428&r2=749429&view=diff
==============================================================================
--- buildr/trunk/CHANGELOG (original)
+++ buildr/trunk/CHANGELOG Mon Mar  2 20:12:00 2009
@@ -7,6 +7,8 @@
 * Added:  BUILDR-242: Include Scala-Tools Repository by Default.  
 * Added:  BUILDR-93: Add specs for ScalaCheck integration
 * Added:  BUILDR-94: Add specs for Scala Specs integration
+* Added:  BUILDR-222: Support Git as a version control system
+* Added   BUILDR-223 Release Task: customizable commit message
 * Added:  Info message "Packaging filename.ext" now displayed for packaging tasks
 * Change: require 'buildr/scala' is now official required to use Scala features
 * Change: Introduced new options from Rake 0.8.3: -I (libdir), -R (rakelib),

Modified: buildr/trunk/lib/buildr/core/build.rb
URL: http://svn.apache.org/viewvc/buildr/trunk/lib/buildr/core/build.rb?rev=749429&r1=749428&r2=749429&view=diff
==============================================================================
--- buildr/trunk/lib/buildr/core/build.rb (original)
+++ buildr/trunk/lib/buildr/core/build.rb Mon Mar  2 20:12:00 2009
@@ -106,14 +106,123 @@
 
   end
 
+  module Git 
+    class << self
+      # :call-seq:
+      #   execute(*args)
+      #   execute('tag', 'FOO', :nofail)
+      #
+      # Executes the VCS command and returns the output. A :RuntimeError is thrown if the exit statu
+      # non-zero.
+      # This behavior might be changed by providing the symbol :nofail has the last argument.
+      def execute(*args)
+        cmd = 'git ' + args.map { |arg| arg[' '] ? %Q{"#{arg}"} : arg }.join(' ')
+        trace cmd
+        `#{cmd}`.tap {
+            fail "GIT command failed with status #{$?.exitstatus}" unless $?.exitstatus == 0
+      }
+      end
+      alias :git :execute
+
+      # Execute 'git status' and return the output lines in an array.
+      def uncommitted_files
+        s = status
+        if s =~ /^nothing to commit \(working directory clean\)$/
+          []
+        else
+          s.split "\n"
+        end
+      end
 
-  class Svn
+      # Commit the given file with a message.
+      # The file has to be known to Git meaning that it has either to have been already committed in the past
+      # or freshly added to the index. Otherwise it will fail.
+      def commit(file, message)
+        git 'commit', '-m', message, file
+      end
+
+      # Update the remote refs using local refs
+      #
+      # By default, the "remote" destination of the push is the the remote repo linked to the current branch.
+      # The default remote branch is the current local branch.
+      def push(remote_repo = remote, remote_branch = current_branch)
+        git 'push', remote, current_branch
+      end
+
+      # Execute git status and return the output.
+      # This method won't fail if the exit status is non-zero.
+      def status
+        cmd = "git status"
+        trace cmd
+        `#{cmd}`
+      end
 
+      protected 
+
+      # Return true if at least one remote repository is defined. False otherwise.
+      def remotes_defined?
+          !git('remote').empty?
+      end
+
+      # Return the name of the remote repository whose branch the current local branch tracks,
+      # or nil if none.
+      def remote(branch = current_branch)
+        match = git('config', '-l').split("\n").find {|l| l.match "^branch\.#{branch}\.remote=.*$"}
+        match.split('=').last if match
+      end
+
+      # Return true if the given local branch tracks a remote branch.
+      def has_remote?(branch = current_branch)
+        remote(branch) != nil
+      end
+
+      def has_no_remote?(branch = current_branch)
+        !has_remote? branch
+      end
+
+      # Return the name of the current branch
+      def current_branch
+        git('branch').split("\n").find {|l| l =~ /^\* .*$/}[2..-1]
+      end
+    end
+  end #of Git
+
+  module Svn
     class << self
+      # :call-seq:
+      #   execute(*args)
+      #   execute('tag', 'FOO', :nofail)
+      #
+      # Executes the VCS command and returns the output. A :RuntimeError is thrown if the exit statu
+      # non-zero.
+      # This behavior might be changed by providing the symbol :nofail has the last argument.
+      def execute(*args)
+        cmd = 'svn ' + args.map { |arg| arg[' '] ? %Q{"#{arg}"} : arg }.join(' ')
+        trace cmd
+        `#{cmd}`.tap {
+            fail "SVN command failed with status #{$?.exitstatus}" unless $?.exitstatus == 0
+      }
+      end
+      alias :svn :execute
+
+      def tag(tag_name)
+        url = tag_url repo_url, tag_name
+        remove url, 'Removing old copy' rescue nil
+        copy Dir.pwd, url, "Release #{tag_name}"
+      end
+
+      # Status check reveals modified files, but also SVN externals which we can safely ignore.
+      def uncommitted_files
+        svn('status', '--ignore-externals').reject { |line| line =~ /^X\s/ }
+      end
+
       def commit(file, message)
         svn 'commit', '-m', message, file
       end
       
+
+      protected
+
       def copy(dir, url, message)
         svn 'copy', dir, url, '-m', message
       end
@@ -126,24 +235,24 @@
       def remove(url, message)
         svn 'remove', url, '-m', message
       end
-      
-      # Status check reveals modified files, but also SVN externals which we can safely ignore.
-      def uncommitted_files
-        svn('status', '--ignore-externals').reject { |line| line =~ /^X\s/ }
-      end
-      
+
       # :call-seq:
-      #   svn(*args)
+      #   tag_url(svn_url, version) => tag_url
       #
-      # Executes SVN command and returns the output.
-      def svn(*args)
-        cmd = 'svn ' + args.map { |arg| arg[' '] ? %Q{"#{arg}"} : arg }.join(' ')
-        trace cmd
-        `#{cmd}`.tap { fail 'SVN command failed' unless $?.exitstatus == 0 }
+      # Returns the SVN url for the tag.
+      # Can tag from the trunk or from branches.
+      # Can handle the two standard repository layouts.
+      #   - http://my.repo/foo/trunk => http://my.repo/foo/tags/1.0.0
+      #   - http://my.repo/trunk/foo => http://my.repo/tags/foo/1.0.0
+      def tag_url(svn_url, tag)
+        trunk_or_branches = Regexp.union(%r{^(.*)/trunk(.*)$}, %r{^(.*)/branches(.*)/([^/]*)$})
+        match = trunk_or_branches.match(svn_url)
+        prefix = match[1] || match[3]
+        suffix = match[2] || match[4]
+        prefix + '/tags' + suffix + '/' + tag
       end
     end
-  end
-  
+  end # of Svn class
   
   class Release
 
@@ -157,6 +266,27 @@
       #   Release.tag_name = lambda { |ver| "foo-#{ver}" }
       attr_accessor :tag_name
 
+      # Use this to specify a different commit message to commit the buildfile with the next version in source control.
+      # You can set the commit message or a proc that will be called with the next version number,
+      # for example:
+      #   Release.commit_message = lambda { |ver| "Changed version number to #{ver}" }
+      attr_accessor :commit_message
+
+      # :call-seq:
+      #     add(MyReleaseClass)
+      #
+      # Add a Release implementation to the list of available Release classes.
+      def add(release)
+        @list ||= []
+        @list |= [release]
+      end
+      alias :<< :add
+
+      # The list of supported Release implementations
+      def list
+        @list ||= []
+      end
+
       # :call-seq:
       #   make()
       #
@@ -168,12 +298,12 @@
           options << '--environment' << Buildr.environment unless Buildr.environment.to_s.empty?
           buildr %w{clean upload}, options
         end
-        tag_release
-        commit_new_snapshot
+        tag_release resolve_tag
+        update_version_to_next
       end
-
+      
       # :call-seq:
-      #   extract_version() => this_version
+      #   extract_version() => this_versin
       #
       # Extract the current version number from the buildfile.
       # Raise an error if not found.
@@ -184,36 +314,7 @@
         fail 'Looking for THIS_VERSION = "..." in your Buildfile, none found'
       end
       
-      # :call-seq:
-      #   tag_url(svn_url, version) => tag_url
-      #
-      # Returns the SVN url for the tag.
-      # Can tag from the trunk or from branches.
-      # Can handle the two standard repository layouts.
-      #   - http://my.repo/foo/trunk => http://my.repo/foo/tags/1.0.0
-      #   - http://my.repo/trunk/foo => http://my.repo/tags/foo/1.0.0
-      def tag_url(svn_url, version)
-        trunk_or_branches = Regexp.union(%r{^(.*)/trunk(.*)$}, %r{^(.*)/branches(.*)/([^/]*)$})
-        match = trunk_or_branches.match(svn_url)
-        prefix = match[1] || match[3]
-        suffix = match[2] || match[4]
-        tag = tag_name || version
-        tag = tag.call(version) if Proc === tag
-        prefix + '/tags' + suffix + '/' + tag
-      end
-      
-      # :call-seq:
-      #   check()
-      #
-      # Check that we don't have any local changes in the working copy. Fails if it finds anything
-      # in the working copy that is not checked into source control.
-      def check
-        fail "SVN URL must contain 'trunk' or 'branches/...'" unless Svn.repo_url =~ /(trunk)|(branches.*)$/
-        fail "Uncommitted SVN files violate the First Principle Of Release!\n#{Svn.uncommitted_files}" unless Svn.uncommitted_files.empty?
-      end
-
     protected
-
       # :call-seq:
       #   buildr(tasks, options)
       #
@@ -270,35 +371,104 @@
         buildfile.gsub(THIS_VERSION_PATTERN) { |ver| ver.sub(/(["']).*\1/, %Q{"#{new_version}"}) }
       end
 
-      # :call-seq:
-      #   tag_release()
-      #
-      # Tags the current working copy with the release version number.
-      def tag_release
+      # Return the name of the tag to tag the release with.
+      def resolve_tag
         version = extract_version
-        info "Tagging release #{version}"
-        url = tag_url Svn.repo_url, version
-        Svn.remove url, 'Removing old copy' rescue nil
-        Svn.copy Dir.pwd, url, "Release #{version}"
+        tag = tag_name || version
+        tag = tag.call(version) if Proc === tag
+        tag
       end
 
-      # :call-seq:
-      #   commit_new_snapshot()
-      #
-      # Last, we commit what we currently have in the working copy with an upgraded version number.
-      def commit_new_snapshot
+      # Move the version to next and save the updated buildfile
+      def update_buildfile
         buildfile = change_version { |version| version[-1] = (version[-1].to_i + 1).to_s + '-SNAPSHOT' }
         File.open(Buildr.application.buildfile.to_s, 'w') { |file| file.write buildfile }
-        Svn.commit Buildr.application.buildfile.to_s, "Changed version number to #{extract_version}"
+      end
+
+      # Return the message to use to cimmit the buildfile with the next version
+      def message
+        version = extract_version
+        msg = commit_message || "Changed version number to #{version}"
+        msg = msg.call(version) if Proc === msg
+        msg
+      end
+
+      def update_version_to_next
+        update_buildfile
+      end
+    end
+  end
+
+  class GitRelease < Release
+    class << self
+      def applies_to?(directory = '.')
+        File.exist? File.join(directory, '.git/config')
+      end
+
+      # Fails if one of theses 2 conditions are not met:
+      #    1. the repository is clean: no content staged or unstaged
+      #    2. some remote repositories are defined but the current branch does not track any
+      def check
+        fail "Uncommitted files violate the First Principle Of Release!\n"+Git.uncommitted_files.join("\n") unless Git.uncommitted_files.empty?
+        fail "You are releasing from a local branch that does not track a remote!" if Git.remotes_defined? && Git.has_no_remote?
+      end
+
+      # Add a tag reference in .git/refs/tags and push it to the remote if any.
+      # If a tag with the same name already exists it will get deleted (in both local and remote repositories).
+      def tag_release(tag)
+        info "Committing buildfile with version number #{extract_version}"
+        Git.commit File.basename(Buildr.application.buildfile.to_s), message
+        Git.push if Git.has_remote?
+        info "Tagging release #{tag}"
+        Git.git 'tag', '-d', tag rescue nil
+        Git.git 'push', Git.remote, ":refs/tags/#{tag}" rescue nil if Git.has_remote? 
+        Git.git 'tag', '-a', tag, '-m', "[buildr] Cutting release #{tag}"
+        Git.git 'push', Git.remote, 'tag', tag if Git.has_remote?
+      end
+
+      def update_version_to_next
+        super
         info "Current version is now #{extract_version}"
+        Git.commit File.basename(Buildr.application.buildfile.to_s), message
+        Git.push if Git.has_remote?
       end
     end
   end
+  # add GitRelease to the list of Release implementations
+  Release.add GitRelease
+  
+  class SvnRelease < Release
+    class << self
+      def applies_to?(directory = '.')
+        File.exist? File.join(directory, '.svn')
+      end
+    
+      def check
+        fail "Uncommitted files violate the First Principle Of Release!\n"+Svn.uncommitted_files.join("\n") unless Svn.uncommitted_files.empty?
+        fail "SVN URL must contain 'trunk' or 'branches/...'" unless Svn.repo_url =~ /(trunk)|(branches.*)$/
+      end
+
+      def tag_release(tag)
+        info "Tagging release #{tag}"
+        Svn.tag tag
+      end
 
+      def update_version_to_next
+        super
+        info "Current version is now #{extract_version}"
+        Svn.commit Buildr.application.buildfile.to_s, message
+      end
+    end
+  end
   
+  # add SvnRelease to the list of Release implementations
+  Release.add SvnRelease
+
   desc 'Make a release'
   task 'release' do |task|
-    Release.make
+    release = Release.list.detect { |impl| impl.applies_to? }
+    fail 'Unable to detect the Version Control System.' unless release
+    release.make
   end
 
 end

Modified: buildr/trunk/spec/core/build_spec.rb
URL: http://svn.apache.org/viewvc/buildr/trunk/spec/core/build_spec.rb?rev=749429&r1=749428&r2=749429&view=diff
==============================================================================
--- buildr/trunk/spec/core/build_spec.rb (original)
+++ buildr/trunk/spec/core/build_spec.rb Mon Mar  2 20:12:00 2009
@@ -200,224 +200,451 @@
   end
 end
 
+describe Buildr::Git do
+  describe '#uncommitted_files' do
+    it 'shoud return an empty array on a clean repository' do
+      cmd_output = <<EOF
+# On branch master
+nothing to commit (working directory clean)
+EOF
+      Git.stub!(:status).and_return(cmd_output)
+      Git.uncommitted_files.should be_empty
+    end
 
-describe Buildr::Release do
-  
+    it 'should reject a dirty repository' do
+      cmd_output = <<EOF
+# On branch master
+# Untracked files:
+#   (use "git add <file>..." to include in what will be committed)
+#
+#       foo.temp
+EOF
+      Git.stub!(:status).and_return(cmd_output)
+      Git.uncommitted_files.should == cmd_output.split("\n")
+    end
+  end
+
+  describe '#remotes_defined?' do
+    it 'should return false if no remote repositories defined' do
+      Git.stub!(:git).with('remote').and_return('')
+      Git.send(:remotes_defined?).should be_false
+    end
+
+    it 'should return true if at least one remote repository is defined' do
+      Git.stub!(:git).with('remote').and_return("origin\n")
+      Git.send(:remotes_defined?).should be_true
+    end
+  end
+
+  describe '#remote' do
+    it 'shoud return the name of the corresponding remote' do
+      Git.stub!(:git).with('config', '-l').and_return(<<EOF)
+alias.oneline=log --pretty=oneline
+core.repositoryformatversion=0
+core.filemode=true
+core.bare=false
+core.logallrefupdates=true
+remote.origin.url=git://github.com/vic/buildr.git
+remote.origin.fetch=+refs/heads/*:refs/remotes/origin/*
+branch.master.remote=origin
+branch.master.merge=refs/heads/master
+EOF
+      Git.send(:remote, 'master').should == 'origin'
+    end
+
+    it 'should return nil if no remote for the given branch' do
+      Git.stub!(:git).with('config', '-l').and_return(<<EOF)
+remote.origin.url=git://github.com/vic/buildr.git
+remote.origin.fetch=+refs/heads/*:refs/remotes/origin/*
+EOF
+      Git.send(:remote, 'master').should == nil
+    end
+  end
+
+  describe '#current_branch' do
+    it 'should return the current branch' do
+      Git.stub!(:git).with('branch').and_return("  master\n* a-clever-idea\n  ze-great-idea")
+      Git.send(:current_branch).should == 'a-clever-idea'
+    end
+  end
+
+end # of Git
+
+describe Buildr::Svn do
+  describe '#tag' do
+    it 'should remove any existing tag with the same name' do
+      Svn.stub!(:repo_url).and_return('http://my.repo.org/foo/trunk')
+      Svn.stub!(:copy)
+      Svn.should_receive(:remove).with('http://my.repo.org/foo/tags/1.0.0', 'Removing old copy')
+
+      Svn.tag '1.0.0'
+    end
+
+    it 'should do an svn copy with the release version' do
+      Svn.stub!(:repo_url).and_return('http://my.repo.org/foo/trunk')
+      Svn.stub!(:remove)
+      Svn.should_receive(:copy).with(Dir.pwd, 'http://my.repo.org/foo/tags/1.0.0', 'Release 1.0.0')
+
+      Svn.tag '1.0.0'
+    end
+  end
+
+  # Reference: http://svnbook.red-bean.com/en/1.4/svn.reposadmin.planning.html#svn.reposadmin.projects.chooselayout
+  describe '#tag_url' do
+    it 'should accept to tag foo/trunk' do
+      Svn.send(:tag_url, 'http://my.repo.org/foo/trunk', '1.0.0').should == 'http://my.repo.org/foo/tags/1.0.0'
+    end
+
+    it 'should accept to tag foo/branches/1.0' do
+      Svn.send(:tag_url,'http://my.repo.org/foo/branches/1.0', '1.0.1').should == 'http://my.repo.org/foo/tags/1.0.1'
+    end
+
+    it 'should accept to tag trunk/foo' do
+      Svn.send(:tag_url,'http://my.repo.org/trunk/foo', '1.0.0').should == 'http://my.repo.org/tags/foo/1.0.0'
+    end
+
+    it 'should accept to tag branches/foo/1.0' do
+      Svn.send(:tag_url,'http://my.repo.org/branches/foo/1.0', '1.0.0').should == 'http://my.repo.org/tags/foo/1.0.0'
+    end
+
+    describe '#repo_url' do
+      it 'should extract the SVN URL from svn info' do
+        Svn.stub!(:svn).and_return(<<EOF)
+Path: .
+URL: http://my.repo.org/foo/trunk
+Repository Root: http://my.repo.org
+Repository UUID: 12345678-9abc-def0-1234-56789abcdef0
+Revision: 112
+Node Kind: directory
+Schedule: normal
+Last Changed Author: Lacton
+Last Changed Rev: 110
+Last Changed Date: 2008-08-19 12:00:00 +0200 (Tue, 19 Aug 2008)
+EOF
+        Svn.send(:repo_url).should == 'http://my.repo.org/foo/trunk'
+      end
+    end
+
+
+  end
+
+end # of Buildr::Svn
+
+describe 'a release process', :shared=>true do 
   describe '#make' do
     before do
       write 'buildfile', "VERSION_NUMBER = '1.0.0-SNAPSHOT'"
       # Prevent a real call to a spawned buildr process.
-      Release.stub!(:buildr)
-      Svn.stub!(:repo_url).and_return('http://my.repo.org/foo/trunk')
-      Svn.stub!(:uncommitted_files).and_return('')
-      Svn.stub!(:remove)
-      Svn.stub!(:copy)
-      Svn.stub!(:commit)
+      @release.stub!(:buildr)
+      @release.stub!(:check)
     end
     
     it 'should tag a release with the release version' do
-      Svn.should_receive(:copy).with(Dir.pwd, 'http://my.repo.org/foo/tags/1.0.0', 'Release 1.0.0').and_return {
-        file('buildfile').should contain('VERSION_NUMBER = "1.0.0"')
+      @release.stub!(:update_version_to_next)
+      @release.should_receive(:tag_release).with('1.0.0')
+      @release.make
+    end
+
+    it 'should not alter the buildfile before tagging' do
+      @release.stub!(:update_version_to_next)
+      @release.should_receive(:tag_release).with('1.0.0').and_return {
+         file('buildfile').should contain('VERSION_NUMBER = "1.0.0"')
       }
-      Release.make
+      @release.make
     end
 
     it 'should update the buildfile with the next version number' do
-      Release.make
+      @release.stub!(:tag_release)
+      @release.make
       file('buildfile').should contain('VERSION_NUMBER = "1.0.1-SNAPSHOT"')
     end
 
     it 'should commit the updated buildfile' do
-      Svn.should_receive(:commit).with(File.expand_path('buildfile'), 'Changed version number to 1.0.1-SNAPSHOT').and_return {
-        file('buildfile').should contain('VERSION_NUMBER = "1.0.1-SNAPSHOT"')
-      }
-      Release.make
+      @release.stub!(:tag_release)
+      @release.make
+      file('buildfile').should contain('VERSION_NUMBER = "1.0.1-SNAPSHOT"')
     end    
   end
 
-
-  describe '#check' do
+  describe '#resolve_tag' do
     before do
-      Svn.stub!(:uncommitted_files).and_return('')
+      @release.stub!(:extract_version).and_return('1.0.0')
     end
 
-    it 'should accept to release from the trunk' do
-      Svn.stub!(:repo_url).and_return('http://my.repo.org/foo/trunk')
-      lambda { Release.check }.should_not raise_error
+    it 'should return tag specified by tag_name' do
+      @release.tag_name  = 'first'
+      @release.send(:resolve_tag).should == 'first'
     end
-
-    it 'should accept to release from a branch' do
-      Svn.stub!(:repo_url).and_return('http://my.repo.org/foo/branches/1.0')
-      lambda { Release.check }.should_not raise_error
-    end
-
-    it 'should reject releasing from a tag' do
-      Svn.stub!(:repo_url).and_return('http://my.repo.org/foo/tags/1.0.0')
-      lambda { Release.check }.should raise_error(RuntimeError, "SVN URL must contain 'trunk' or 'branches/...'")
-    end
-
-    it 'should reject a non standard repository layout' do
-      Svn.stub!(:repo_url).and_return('http://my.repo.org/foo/bar')
-      lambda { Release.check }.should raise_error(RuntimeError, "SVN URL must contain 'trunk' or 'branches/...'")
+    
+    it 'should use tag returned by tag_name if tag_name is a proc' do
+      @release.tag_name  = lambda { |version| "buildr-#{version}" }
+      @release.send(:resolve_tag).should == 'buildr-1.0.0'
     end
+    after { @release.tag_name = nil }
+  end
 
-    it 'should reject an uncommitted file' do
-      Svn.stub!(:repo_url).and_return('http://my.repo.org/foo/trunk')
-      Svn.stub!(:uncommitted_files).and_return('M      foo.rb')
-      lambda { Release.check }.should raise_error(RuntimeError,
-        "Uncommitted SVN files violate the First Principle Of Release!\n" +
-        "M      foo.rb")
+  describe '#tag_release' do
+    it 'should inform the user' do
+      @release.stub!(:extract_version).and_return('1.0.0')
+      lambda { @release.tag_release('1.0.0') }.should show_info('Tagging release 1.0.0')
     end
   end
-  
-  
+
   describe '#extract_version' do
     it 'should extract VERSION_NUMBER with single quotes' do
       write 'buildfile', "VERSION_NUMBER = '1.0.0-SNAPSHOT'"
-      Release.extract_version.should == '1.0.0-SNAPSHOT'
+      @release.extract_version.should == '1.0.0-SNAPSHOT'
     end
 
     it 'should extract VERSION_NUMBER with double quotes' do
       write 'buildfile', %{VERSION_NUMBER = "1.0.1-SNAPSHOT"}
-      Release.extract_version.should == '1.0.1-SNAPSHOT'
+      @release.extract_version.should == '1.0.1-SNAPSHOT'
     end
 
     it 'should extract VERSION_NUMBER without any spaces' do
       write 'buildfile', "VERSION_NUMBER='1.0.2-SNAPSHOT'"
-      Release.extract_version.should == '1.0.2-SNAPSHOT'
+      @release.extract_version.should == '1.0.2-SNAPSHOT'
     end
 
     it 'should extract THIS_VERSION as an alternative to VERSION_NUMBER' do
       write 'buildfile', "THIS_VERSION = '1.0.3-SNAPSHOT'"
-      Release.extract_version.should == '1.0.3-SNAPSHOT'
+      @release.extract_version.should == '1.0.3-SNAPSHOT'
     end
 
     it 'should complain if no current version number' do
       write 'buildfile', 'define foo'
-      lambda { Release.extract_version }.should raise_error('Looking for THIS_VERSION = "..." in your Buildfile, none found')
+      lambda { @release.extract_version }.should raise_error('Looking for THIS_VERSION = "..." in your Buildfile, none found')
     end
   end
 
 
-  # Reference: http://svnbook.red-bean.com/en/1.4/svn.reposadmin.planning.html#svn.reposadmin.projects.chooselayout
-  describe '#tag url' do
-    it 'should accept to tag foo/trunk' do
-      Release.tag_url('http://my.repo.org/foo/trunk', '1.0.0').should == 'http://my.repo.org/foo/tags/1.0.0'
+  describe '#with_release_candidate_version' do
+    before do
+      Buildr.application.stub!(:buildfile).and_return(file('buildfile'))
+      write 'buildfile', "THIS_VERSION = '1.1.0-SNAPSHOT'"
     end
 
-    it 'should accept to tag foo/branches/1.0' do
-      Release.tag_url('http://my.repo.org/foo/branches/1.0', '1.0.1').should == 'http://my.repo.org/foo/tags/1.0.1'
+    it 'should yield the name of the release candidate buildfile' do
+      @release.send :with_release_candidate_version do |new_filename|
+        File.read(new_filename).should == %{THIS_VERSION = "1.1.0"}
+      end
     end
 
-    it 'should accept to tag trunk/foo' do
-      Release.tag_url('http://my.repo.org/trunk/foo', '1.0.0').should == 'http://my.repo.org/tags/foo/1.0.0'
+    it 'should yield a name different from the original buildfile' do
+      @release.send :with_release_candidate_version do |new_filename|
+        new_filename.should_not point_to_path('buildfile')
+      end
     end
+  end
 
-    it 'should accept to tag branches/foo/1.0' do
-      Release.tag_url('http://my.repo.org/branches/foo/1.0', '1.0.0').should == 'http://my.repo.org/tags/foo/1.0.0'
+
+  describe '#update_version_to_next' do
+    before do
+      write 'buildfile', 'THIS_VERSION = "1.0.0"'
     end
-    
-    it 'should use tag specified by tag_name' do
-      Release.tag_name  = 'first'
-      Release.tag_url('http://my.repo.org/foo/trunk', '1.0.0').should == 'http://my.repo.org/foo/tags/first'
+
+    it 'should update the buildfile with a new version number' do
+      @release.send :update_version_to_next
+      file('buildfile').should contain('THIS_VERSION = "1.0.1-SNAPSHOT"')
     end
-    
-    it 'should use tag returned by tag_name if tag_name is a proc' do
-      Release.tag_name  = lambda { |version| "buildr-#{version}" }
-      Release.tag_url('http://my.repo.org/foo/trunk', '1.0.0').should == 'http://my.repo.org/foo/tags/buildr-1.0.0'
+
+    it 'should commit the new buildfile on the trunk' do
+      @release.should_receive(:message).and_return('Changed version number to 1.0.1-SNAPSHOT')
+      @release.update_version_to_next
+    end
+
+    it 'should use the commit message specified by commit_message' do
+      @release.commit_message  = 'Here is my custom message'
+      @release.should_receive(:message).and_return('Here is my custom message')
+      @release.update_version_to_next
     end
     
-    after { Release.tag_name = nil }
-  end
+    it 'should use the commit message returned by commit_message if commit_message is a proc' do
+      @release.commit_message  = lambda { |new_version| 
+        new_version.should == '1.0.1-SNAPSHOT'
+        "increment version number to #{new_version}"
+      }
+      @release.should_receive(:message).and_return('increment version number to 1.0.1-SNAPSHOT')
+      @release.update_version_to_next
+    end
 
 
-  describe '#with_release_candidate_version' do
-    before do
-      Buildr.application.stub!(:buildfile).and_return(file('buildfile'))
-      write 'buildfile', "THIS_VERSION = '1.1.0-SNAPSHOT'"
+    it 'should inform the user of the new version' do
+      lambda { @release.update_version_to_next }.should show_info('Current version is now 1.0.1-SNAPSHOT')
     end
+  end
+  
+end # of Release
 
-    it 'should yield the name of the release candidate buildfile' do
-      Release.send :with_release_candidate_version do |new_filename|
-        File.read(new_filename).should == %{THIS_VERSION = "1.1.0"}
-      end
+
+
+describe Buildr::GitRelease do
+  before do
+    @release = nil
+  end
+  describe '#applies_to?' do
+    it 'should reject a non-git repo' do
+      GitRelease.applies_to?.should be_false
     end
 
-    it 'should yield a name different from the original buildfile' do
-      Release.send :with_release_candidate_version do |new_filename|
-        new_filename.should_not point_to_path('buildfile')
-      end
+    it 'should accept a git repo' do
+      FileUtils.mkdir '.git'
+      FileUtils.touch File.join('.git', 'config')
+      GitRelease.applies_to?.should be_true 
     end
   end
 
+  describe '#release_check' do
+    it 'shoud accept a clean repository' do
+      cmd_output = <<EOF
+# On branch master
+nothing to commit (working directory clean)
+EOF
+        Git.stub!(:status).and_return(cmd_output)
+        Git.stub!(:remotes_defined?).and_return(false)
+        lambda{ GitRelease.check }.should_not raise_error
+    end
+
+    it 'should reject a dirty repository' do
+      cmd_output = <<EOF
+# On branch master
+# Untracked files:
+#   (use "git add <file>..." to include in what will be committed)
+#
+#       foo.temp
+EOF
+      Git.stub!(:uncommitted_files).and_return(cmd_output.split("\n"))
+      lambda { GitRelease.check }.should raise_error(RuntimeError)
+    end
+
+
+    it 'should pass if no remote repositories are defined' do
+      Git.stub!(:uncommitted_files).and_return([])
+      Git.stub!(:remotes_defined?).and_return(false)
+
+      lambda{ GitRelease.check }.should_not raise_error
+    end
+
+    it 'should fail if the current branch does not track a remote repo but at least one remote repo is defined' do
+      Git.stub!(:uncommitted_files).and_return([])
+      Git.stub!(:remotes_defined?).and_return(true)
+      Git.stub!(:has_no_remote?).and_return(true)
+
+      lambda{ GitRelease.check }.should raise_error(RuntimeError,
+        "You are releasing from a local branch that does not track a remote!")
+    end
+  end
 
   describe '#tag_release' do
     before do
-      write 'buildfile', "THIS_VERSION = '1.0.1'"
-      Svn.stub!(:repo_url).and_return('http://my.repo.org/foo/trunk')
-      Svn.stub!(:copy)
-      Svn.stub!(:remove)
+      GitRelease.stub!(:extract_version).and_return('1.0.1')
+      Git.stub!(:git).with('tag', '-a', 'TEST_TAG', '-m', '[buildr] Cutting release TEST_TAG')
+      Git.stub!(:git).with('push', 'origin', 'tag', 'TEST_TAG')
+      Git.stub!(:commit)
+      Git.stub!(:push)
+      Git.stub!(:remote).and_return('origin')
+      Git.stub!(:git).with('branch').and_return("* master\n")
+      GitRelease.stub!(:resolve_tag).and_return('TEST_TAG')
     end
 
-    it 'should tag the working copy' do
-      Svn.should_receive(:copy).with(Dir.pwd, 'http://my.repo.org/foo/tags/1.0.1', 'Release 1.0.1')
-      Release.send :tag_release
+    it 'should delete any existing tag with the same name' do
+      Git.should_receive(:git).with('tag', '-d', 'TEST_TAG')
+      Git.should_receive(:git).with('push', 'origin', ':refs/tags/TEST_TAG')
+
+      GitRelease.tag_release 'TEST_TAG'
     end
 
-    it 'should remove the tag if it already exists' do
-      Svn.should_receive(:remove).with('http://my.repo.org/foo/tags/1.0.1', 'Removing old copy')
-      Release.send :tag_release
+    it 'should commit the buildfile before tagging' do
+      Git.should_receive(:commit).with(File.basename(Buildr.application.buildfile.to_s), "Changed version number to 1.0.1")
+
+      GitRelease.tag_release 'TEST_TAG'
     end
 
-    it 'should accept that the tag does not exist' do
-      Svn.stub!(:remove).and_raise(RuntimeError)
-      Release.send :tag_release
+    it 'should push the tag if a remote is tracked' do
+      Git.should_receive(:git).with('tag', '-d', 'TEST_TAG')
+      Git.should_receive(:git).with('push', 'origin', ':refs/tags/TEST_TAG')
+      Git.should_receive(:git).with('tag', '-a', 'TEST_TAG', '-m', '[buildr] Cutting release TEST_TAG')
+      Git.should_receive(:git).with('push', 'origin', 'tag',  'TEST_TAG')
+
+      GitRelease.tag_release 'TEST_TAG'
     end
 
-    it 'should inform the user' do
-      lambda { Release.send :tag_release }.should show_info('Tagging release 1.0.1')
+    it 'should NOT push the tag if no remote is tracked' do
+      Git.stub!(:remote)
+      Git.should_not_receive(:git).with('push', 'origin', 'tag',  'TEST_TAG')
+
+      GitRelease.tag_release 'TEST_TAG'
     end
   end
+end
 
 
-  describe '#commit_new_snapshot' do
+describe Buildr::SvnRelease do
+  describe '#applies_to?' do
+    it 'should reject a non-git repo' do
+      SvnRelease.applies_to?.should be_false
+    end
+
+    it 'should accept a git repo' do
+      FileUtils.touch '.svn'
+      SvnRelease.applies_to?.should be_true 
+    end
+  end
+
+  describe '#check' do
     before do
-      write 'buildfile', 'THIS_VERSION = "1.0.0"'
-      Svn.stub!(:commit)
+      Svn.stub!(:uncommitted_files).and_return('')
     end
 
-    it 'should update the buildfile with a new version number' do
-      Release.send :commit_new_snapshot
-      file('buildfile').should contain('THIS_VERSION = "1.0.1-SNAPSHOT"')
+    it 'should accept to release from the trunk' do
+      Svn.stub!(:repo_url).and_return('http://my.repo.org/foo/trunk')
+      lambda { SvnRelease.check }.should_not raise_error
     end
 
-    it 'should commit the new buildfile on the trunk' do
-      Svn.should_receive(:commit).with(File.expand_path('buildfile'), 'Changed version number to 1.0.1-SNAPSHOT')
-      Release.send :commit_new_snapshot
+    it 'should accept to release from a branch' do
+      Svn.stub!(:repo_url).and_return('http://my.repo.org/foo/branches/1.0')
+      lambda { SvnRelease.check }.should_not raise_error
     end
 
-    it 'should inform the user of the new version' do
-      lambda { Release.send :commit_new_snapshot }.should show_info('Current version is now 1.0.1-SNAPSHOT')
+    it 'should reject releasing from a tag' do
+      Svn.stub!(:repo_url).and_return('http://my.repo.org/foo/tags/1.0.0')
+      lambda { SvnRelease.check }.should raise_error(RuntimeError, "SVN URL must contain 'trunk' or 'branches/...'")
+    end
+
+    it 'should reject a non standard repository layout' do
+      Svn.stub!(:repo_url).and_return('http://my.repo.org/foo/bar')
+      lambda { SvnRelease.check }.should raise_error(RuntimeError, "SVN URL must contain 'trunk' or 'branches/...'")
+    end
+
+    it 'should reject an uncommitted file' do
+      Svn.stub!(:repo_url).and_return('http://my.repo.org/foo/trunk')
+      Svn.stub!(:uncommitted_files).and_return(['M      foo.rb'])
+      lambda { SvnRelease.check }.should raise_error(RuntimeError,
+        "Uncommitted files violate the First Principle Of Release!\n" +
+        "M      foo.rb")
     end
   end
-  
+
 end
 
+describe Buildr::SvnRelease do
+  before do
+    @release = SvnRelease
+    Svn.stub!(:execute)
+    Svn.stub!(:tag)
+    Svn.stub!(:commit)
+    Svn.stub!(:uncommitted_files)
+  end
+  it_should_behave_like 'a release process'
+end
 
-describe Buildr::Svn, '#repo_url' do
-  it 'should extract the SVN URL from svn info' do
-    Svn.stub!(:svn, 'info').and_return(<<EOF)
-Path: .
-URL: http://my.repo.org/foo/trunk
-Repository Root: http://my.repo.org
-Repository UUID: 12345678-9abc-def0-1234-56789abcdef0
-Revision: 112
-Node Kind: directory
-Schedule: normal
-Last Changed Author: Lacton
-Last Changed Rev: 110
-Last Changed Date: 2008-08-19 12:00:00 +0200 (Tue, 19 Aug 2008)
-EOF
-    Svn.repo_url.should == 'http://my.repo.org/foo/trunk'
+describe Buildr::GitRelease do
+  before do
+    @release = GitRelease
+    Git.stub!(:git)
+    Git.stub!(:commit)
+    Git.stub!(:remote)
+    Git.stub!(:has_remote?)
   end
-end
\ No newline at end of file
+  it_should_behave_like 'a release process'
+end