=== modified file 'lib/command_line.rb'

=== modified file 'lib/source_control.rb'
--- lib/source_control.rb	2008-07-03 00:40:13 +0000
+++ lib/source_control.rb	2009-05-22 07:24:28 +0000
@@ -13,11 +13,13 @@
           case scm_options[:repository]
           when /^git:/ then SourceControl::Git
           when /^svn:/, /^svn\+ssh:/ then SourceControl::Subversion
+          when /^bzr:/, /^bzr\+ssh:/ then SourceControl::Bazaar
           else SourceControl::Subversion
           end
       else
         scm_type = "subversion" if scm_type == "svn"
         scm_type = "mercurial" if scm_type == "hg"
+        scm_type = "bazaar" if scm_type == "bzr"
         
         source_control_class_name = scm_type.to_s.camelize
         begin
@@ -39,12 +41,14 @@
       git = File.directory?(File.join(path, '.git'))
       svn = File.directory?(File.join(path, '.svn'))
       hg = File.directory?(File.join(path, '.hg'))
+      bzr = File.directory?(File.join(path, '.bzr'))
 
-      case [git, svn, hg]
-      when [true, false, false] then SourceControl::Git.new(:path => path)
-      when [false, true, false] then SourceControl::Subversion.new(:path => path)
-      when [false, false, true] then SourceControl::Mercurial.new(:path => path)
-      when [false, false, false] then raise "Could not detect the type of source control in #{path}"
+      case [git, svn, hg, bzr]
+      when [true, false, false, false] then SourceControl::Git.new(:path => path)
+      when [false, true, false, false] then SourceControl::Subversion.new(:path => path)
+      when [false, false, true, false] then SourceControl::Mercurial.new(:path => path)
+      when [false, false, false, true] then SourceControl::Bazaar.new(:path => path)
+      when [false, false, false, false] then raise "Could not detect the type of source control in #{path}"
       else raise "More than one type of source control was detected in #{path}"
       end
     end

=== modified file 'lib/source_control/abstract_adapter.rb'
--- lib/source_control/abstract_adapter.rb	2009-01-28 01:22:24 +0000
+++ lib/source_control/abstract_adapter.rb	2009-05-22 08:25:27 +0000
@@ -39,29 +39,30 @@
       if block_given?
         if options[:execute_in_project_directory]
           Dir.chdir(path) do
-            execute(command, &block)
+            execute(command, options, &block)
           end
         else
-          execute(command, &block)
+          execute(command, options, &block)
         end
       else
         error_log = File.expand_path(self.error_log)
         if options[:execute_in_project_directory]
           raise "path is nil" if path.nil?
           Dir.chdir(path) do
-            execute_with_error_log(command, error_log)
+            execute_with_error_log(command, error_log, options)
           end
         else
-          execute_with_error_log(command, error_log)
+          execute_with_error_log(command, error_log, options)
         end
       end
     end
 
-    def execute_with_error_log(command, error_log)
+    def execute_with_error_log(command, error_log, options = {})
+      options = {:stderr => error_log}.merge(options)
       FileUtils.rm_f(error_log)
       FileUtils.touch(error_log)
       stdout_output = nil
-      execute(command, :stderr => error_log) do |io|
+      execute(command, options) do |io|
         stdout_output = io.readlines
         File.open(error_log, "a") {|f| f << stdout_output}
       end

=== added directory 'lib/source_control/bazaar'
=== added file 'lib/source_control/bazaar.rb'
--- lib/source_control/bazaar.rb	1970-01-01 00:00:00 +0000
+++ lib/source_control/bazaar.rb	2009-05-22 08:18:03 +0000
@@ -0,0 +1,74 @@
+module SourceControl
+  class Bazaar < AbstractAdapter
+
+    attr_accessor :repository
+
+    def initialize(options = {})
+      options = options.dup
+      @path = options.delete(:path) || "."
+      @error_log = options.delete(:error_log)
+      @interactive = options.delete(:interactive)
+      @repository = options.delete(:repository)
+      raise "don't know how to handle '#{options.keys.first}'" if options.length > 0
+    end
+
+    def checkout(revision = nil, stdout = $stdout)
+      raise 'Repository location is not specified' unless @repository
+
+      raise "#{path} is not empty, cannot branch a project into it" unless (Dir.entries(path) - ['.', '..']).empty?
+      FileUtils.rm_rf(path)
+
+      args = [@repository, path]
+      args << ['-r', revision.number] if revision
+      bzr('branch', args, :execute_in_project_directory => false)
+    end
+
+    def latest_revision
+      bzr('pull') 
+      bzr_output = bzr('log', ['-v', '-r', '-1'])
+      Bazaar::LogParser.new.parse(bzr_output).first
+    end
+
+    def up_to_date?(reasons = [])
+      repository = bzr('info').join("\n").match(/parent branch:\s+(.*)/)[1].strip
+      bzr_local = bzr('revno').first.to_i
+      bzr_remote = bzr('revno', [repository]).first.to_i
+
+      if bzr_remote == bzr_local
+        return true
+      elsif bzr_local > bzr_remote
+        raise "Local repository is bigger that should be impossible"
+      else
+        bzr_output = bzr('missing', ['-v'], :exitstatus => 1)
+        _new_revisions = Bazaar::LogParser.new.parse(bzr_output)
+        reasons << _new_revisions
+        return false
+      end
+    end
+
+    def update(revision = nil)
+      if revision
+        bzr("revert", ['-r', revision.number])
+      else
+        bzr("pull")
+      end
+    end
+
+    def creates_ordered_build_labels?() true end
+
+    def clean_checkout(revision = nil, stdout = $stdout)
+      update(revision)
+      bzr("clean-tree")
+    end
+
+    protected
+
+    def bzr(operation, arguments = [], options = {}, &block)
+      command = ["bzr", operation] + arguments.compact
+      ## TODO: figure out how to handle the same thing with hg
+      ##      command << "--non-interactive" unless @interactive
+      execute_in_local_copy(command, options, &block)
+    end
+
+  end
+end

=== added file 'lib/source_control/bazaar/log_parser.rb'
--- lib/source_control/bazaar/log_parser.rb	1970-01-01 00:00:00 +0000
+++ lib/source_control/bazaar/log_parser.rb	2009-05-22 08:18:09 +0000
@@ -0,0 +1,62 @@
+require 'date'
+
+module SourceControl
+  class Bazaar
+    class LogParser
+
+      def parse(message)
+
+        revisions = []
+
+        entries = split_log(message)
+
+        entries.each do |entry|
+          rev_number = parse_for_rev_number(entry)
+          name = parse_for_name(entry)
+          date = parse_for_date(entry)
+          message, files = parse_for_message_and_files(entry)
+          change_set_entries = []
+          files.each do |file_name|
+            change_set_entries << ChangesetEntry.new("", file_name)
+          end
+          revisions << Revision.new(rev_number, name, date, message, change_set_entries)
+        end
+
+        revisions
+      end
+
+      def parse_for_name(message)
+        name_match = (message.match(/^committer:\s+(.*)\</) ||
+                     message.match(/^committer:\s+(.*)$/))
+        name_match[1].strip
+      end
+
+      def parse_for_date(message)
+        date_string = message.match(/^timestamp:\s+(.*)/)[1].strip
+        DateTime.parse(date_string)
+      end
+
+      def parse_for_message_and_files(message)
+        message, files_text = message.match(/^message:.(.*)^(modified|added|renamed|removed):(.*)/m)[1..2]
+        files = files_text.split(/\s+/)
+        %w{modified: added: renamed: removed:}.each do |header|
+          files.delete(header)
+        end
+        [message, files]
+      end
+
+      def parse_for_rev_number(message)
+        message.match(/^revno:\s+(\d+)/)[1]
+      end
+
+      def split_log(message)
+        message = message.join("\n") if message.is_a? Array
+        if message.match(/Branches are up to date/)
+          return []
+        end
+        message.split(/^\s+$/).delete_if {|t| t =~ /^\s+$/ }.map(&:strip)
+      end
+
+    end
+  end
+end

=== added file 'lib/source_control/bazaar/revision.rb'
--- lib/source_control/bazaar/revision.rb	1970-01-01 00:00:00 +0000
+++ lib/source_control/bazaar/revision.rb	2009-05-22 06:52:58 +0000
@@ -0,0 +1,36 @@
+module SourceControl
+  class Bazaar
+
+  # FIXME: Mercurial revision is almost same as Subversion revision; and Git is not much different. Remove redundancy.
+  class Revision < AbstractRevision
+
+      attr_reader :number, :author, :time, :message, :changeset
+
+      def initialize(number, author = nil, time = nil, message = nil, changeset = nil)
+        @number = number
+        @author, @time, @message, @changeset = author, time, message, changeset
+      end
+
+      def to_s
+        <<-EOL
+Revision #{number} committed by #{author} on #{time.strftime('%Y-%m-%d %H:%M:%S') if time}
+#{message}
+#{changeset ? changeset.collect { |entry| entry.to_s }.join("\n") : nil}
+        EOL
+      end
+
+      def <=>(other)
+        @number <=> other.number
+      end
+
+      def ==(other)
+        @number == other.number
+      end
+
+      def to_i
+        @number.to_i
+      end
+
+    end
+  end
+end

=== added directory 'test/unit/source_control/bazaar'
=== added file 'test/unit/source_control/bazaar/log_parser_test.rb'
--- test/unit/source_control/bazaar/log_parser_test.rb	1970-01-01 00:00:00 +0000
+++ test/unit/source_control/bazaar/log_parser_test.rb	2009-05-22 05:38:38 +0000
@@ -0,0 +1,134 @@
+require File.dirname(__FILE__) + '/../../../test_helper'
+require "date"
+
+module SourceControl
+  class Mercurial::LogParserTest < Test::Unit::TestCase
+
+    @@example_log_output_single = <<END
+changeset:   5:c3f57b2b476c
+tag:         tip
+user:        Marcus Ahnve <marcus@re-mind.se>
+date:        Mon Apr 02 16:14:59 2007 +0200
+files:       app/models/mercurial.rb test/unit/mercurial_test.rb
+description:
+moved mercurial back to plugin
+END
+
+    @@example_log_output_double = <<END
+changeset:   4:865db6698186
+user:        Marcus Ahnve <marcus@re-mind.se>
+date:        Mon Apr 02 15:42:14 2007 +0200
+files:       app/models/mercurial.rb app/models/project.rb
+description:
+moved mercurial to model
+
+
+changeset:   3:e3f2e642cb26
+user:        Marcus Ahnve <marcus@re-mind.se>
+date:        Sun Apr 01 17:22:59 2007 +0200
+files:       vcs.txt
+description:
+mailing list instruction
+END
+
+    def setup
+      @parser = Mercurial::LogParser.new
+    end
+
+    def test_should_parse_single_changeset
+      expected = [
+          Mercurial::Revision.new('865db',
+             "Marcus Ahnve",
+             DateTime.parse("Mon Apr 02 15:42:14 2007 +0200"),
+             "moved mercurial to model",
+             [ChangesetEntry.new("", "app/models/mercurial.rb"),
+              ChangesetEntry.new("", "app/models/project.rb")]),
+          Mercurial::Revision.new('e3f2e',
+              'Marcus Ahnve',
+              'Sun Apr 01 17:22:59 2007 +0200',
+              'mailing list instruction',
+              [ChangesetEntry.new("", "vcs.txt")])]
+
+      assert_equal expected, @parser.parse(@@example_log_output_double.split("\n"))
+    end
+
+    def test_should_parse_single_changeset
+      expected = [Mercurial::Revision.new('c3f57',
+                     "Marcus Ahnve",
+                     DateTime.parse("Mon Apr 02 16:14:59 2007 +0200"),
+                     "moved mercurial back to plugin",
+                     [ChangesetEntry.new("", "app/models/mercurial.rb"),
+                     ChangesetEntry.new("", "test/unit/mercurial_test.rb")])]
+      assert_equal expected, @parser.parse(@@example_log_output_single.split("\n"))
+    end
+
+    def test_should_parse_a_merge_changeset_with_no_files
+      merge_changeset = <<END
+changeset:   173:6cb3d52e03c6
+tag:         tip
+parent:      171:c2a67e37d51f
+parent:      172:9be005a8f694
+user:        Alex Verkhovsky <alexey.verkhovsky@gmail.com>
+date:        Fri May 09 11:03:54 2008 -0600
+description:
+Fixing config/database.yml to not use the same database name for all databases
+END
+      expected = [Mercurial::Revision.new('6cb3d',
+             "Alex Verkhovsky",
+             DateTime.parse("Fri May 09 11:03:54 2008 -0600"),
+             "Fixing config/database.yml to not use the same database name for all databases",
+             [])]
+      assert_equal expected, @parser.parse(merge_changeset.split("\n"))
+    end
+
+    def test_parse_name
+      assert_equal("Marcus Ahnve", @parser.parse_for_name(@@example_log_output_single))
+    end
+
+    def test_parse_name_when_name_is_not_set
+      input = "user:        joepoon@joe-poons-computer.local"
+      assert_equal "joepoon@joe-poons-computer.local", @parser.parse_for_name(input)
+    end
+
+    def test_parse_date
+      assert_equal(DateTime.parse("Mon Apr 02 16:14:59 2007 +0200"), @parser.parse_for_date(@@example_log_output_single))
+    end
+
+    def test_message
+      assert_equal("moved mercurial back to plugin", @parser.parse_for_message(@@example_log_output_single))
+    end
+
+    def test_parse_rev_number
+      assert_equal('c3f57', @parser.parse_for_rev_number(@@example_log_output_single))
+    end
+
+    def test_parse_files
+      expected = ["app/models/mercurial.rb","test/unit/mercurial_test.rb"]
+      assert_equal(expected, @parser.parse_for_files(@@example_log_output_single))
+    end
+
+    def test_split_multiple_log_entries
+            entry1 =<<END
+changeset:   4:865db6698186
+user:        Marcus Ahnve <marcus@re-mind.se>
+date:        Mon Apr 02 15:42:14 2007 +0200
+files:       app/models/mercurial.rb app/models/project.rb
+description:
+moved mercurial to model
+END
+            entry2 =<<END
+changeset:   3:e3f2e642cb26
+user:        Marcus Ahnve <marcus@re-mind.se>
+date:        Sun Apr 01 17:22:59 2007 +0200
+files:       vcs.txt
+description:
+mailing list instruction
+END
+            expected=[entry1.strip!,entry2.strip!]
+            assert_equal(expected, @parser.split_log(@@example_log_output_double))
+    end
+
+  end
+
+end
+

=== added file 'test/unit/source_control/bazaar_test.rb'
--- test/unit/source_control/bazaar_test.rb	1970-01-01 00:00:00 +0000
+++ test/unit/source_control/bazaar_test.rb	2009-05-22 11:25:01 +0000
@@ -0,0 +1,59 @@
+require File.dirname(__FILE__) + '/../../test_helper'
+
+module SourceControl
+  class BazaarTest < Test::Unit::TestCase
+
+    include FileSandbox
+
+    def setup
+      @bazaar = Bazaar.new
+    end
+
+    def test_update
+      in_sandbox do
+        revision = Bazaar::Revision.new('5') 
+        @bazaar.expects(:bzr).with("revert", ['-r', '5'])
+        @bazaar.update(revision)
+      end
+    end
+
+    def test_latest_revision
+      in_sandbox do
+        parser = mock('parser')
+        parser.expects(:parse).with("log_result").returns(["foo"])
+        Bazaar::LogParser.expects(:new).returns(parser)
+
+        @bazaar.expects(:bzr).with("pull")
+        @bazaar.expects(:bzr).with("log", ['-v', '-r', '-1']).returns("log_result")
+        assert_equal("foo", @bazaar.latest_revision)
+      end
+    end
+
+    def test_update
+      in_sandbox do
+        stub_revision = stub('revision', :number => '12345')
+        @bazaar.expects(:bzr).with('revert', ['-r', '12345'])
+        assert_nothing_raised { @bazaar.update(stub_revision) }
+      end
+    end
+
+    def test_update_with_no_revision_specified
+      in_sandbox do
+        @bazaar.expects(:bzr).with('pull')
+        assert_nothing_raised { @bazaar.update }
+      end
+    end
+
+    def test_checkout
+      bazaar_with_checkout_data = Bazaar.new(:repository => '/tmp/bzr_repo') 
+      in_sandbox do
+        bazaar_with_checkout_data.expects(:bzr).with(
+            'branch', ['/tmp/bzr_repo', '.'], :execute_in_project_directory => false)
+        assert_nothing_raised { bazaar_with_checkout_data.checkout }
+      end
+    end
+
+    # TODO tests for other public methods of this class
+
+  end
+end

=== modified file 'test/unit/source_control_test.rb'
--- test/unit/source_control_test.rb	2008-05-08 02:12:43 +0000
+++ test/unit/source_control_test.rb	2009-05-22 08:31:09 +0000
@@ -102,6 +102,7 @@
       File.expects(:directory?).with(File.join('./Proj1/work', '.git')).returns(true)
       File.expects(:directory?).with(File.join('./Proj1/work', '.svn')).returns(false)
       File.expects(:directory?).with(File.join('./Proj1/work', '.hg')).returns(false)
+      File.expects(:directory?).with(File.join('./Proj1/work', '.bzr')).returns(false)
       SourceControl::Git.expects(:new).with(:path => './Proj1/work').returns(:git_instance)
 
       assert_equal :git_instance, SourceControl.detect('./Proj1/work')
@@ -113,6 +114,7 @@
       File.expects(:directory?).with(File.join('./Proj1/work', '.git')).returns(false)
       File.expects(:directory?).with(File.join('./Proj1/work', '.svn')).returns(false)
       File.expects(:directory?).with(File.join('./Proj1/work', '.hg')).returns(true)
+      File.expects(:directory?).with(File.join('./Proj1/work', '.bzr')).returns(false)
       SourceControl::Mercurial.expects(:new).with(:path => './Proj1/work').returns(:hg_instance)
 
       assert_equal :hg_instance, SourceControl.detect('./Proj1/work')
@@ -124,17 +126,32 @@
       File.expects(:directory?).with(File.join('./Proj1/work', '.git')).returns(false)
       File.expects(:directory?).with(File.join('./Proj1/work', '.svn')).returns(true)
       File.expects(:directory?).with(File.join('./Proj1/work', '.hg')).returns(false)
+      File.expects(:directory?).with(File.join('./Proj1/work', '.bzr')).returns(false)
       SourceControl::Subversion.expects(:new).with(:path => './Proj1/work').returns(:svn_instance)
 
       assert_equal :svn_instance, SourceControl.detect('./Proj1/work')
     end
   end
 
+  def test_detect_should_identify_bazaar_repository_by_presence_of_dotbzr_directory
+    in_sandbox do
+      File.expects(:directory?).with(File.join('./Proj1/work', '.git')).returns(false)
+      File.expects(:directory?).with(File.join('./Proj1/work', '.svn')).returns(false)
+      File.expects(:directory?).with(File.join('./Proj1/work', '.hg')).returns(false)
+      File.expects(:directory?).with(File.join('./Proj1/work', '.bzr')).returns(true)
+      SourceControl::Bazaar.expects(:new).with(:path => './Proj1/work').returns(:bzr_instance)
+
+      assert_equal :bzr_instance, SourceControl.detect('./Proj1/work')
+    end
+  end
+
+
   def test_detect_should_blow_up_if_there_is_neither_subversion_nor_git
     in_sandbox do
       File.expects(:directory?).with(File.join('./Proj1/work', '.git')).returns(false)
       File.expects(:directory?).with(File.join('./Proj1/work', '.svn')).returns(false)
       File.expects(:directory?).with(File.join('./Proj1/work', '.hg')).returns(false)
+      File.expects(:directory?).with(File.join('./Proj1/work', '.bzr')).returns(false)
 
       assert_raises RuntimeError, "Could not detect the type of source control in ./Proj1/work" do
         SourceControl.detect('./Proj1/work')
@@ -147,6 +164,7 @@
       File.expects(:directory?).with(File.join('./Proj1/work', '.git')).returns(true)
       File.expects(:directory?).with(File.join('./Proj1/work', '.svn')).returns(true)
       File.expects(:directory?).with(File.join('./Proj1/work', '.hg')).returns(false)
+      File.expects(:directory?).with(File.join('./Proj1/work', '.bzr')).returns(false)
 
       assert_raises RuntimeError, "More than one type of source control was detected in ./Proj1/work" do
         SourceControl.detect('./Proj1/work')


