<?xml version="1.0" encoding="UTF-8"?>
<commit>
  <added type="array">
    <added>
      <filename>app/models/subversion_info_parser.rb</filename>
    </added>
    <added>
      <filename>app/models/subversion_propget_parser.rb</filename>
    </added>
    <added>
      <filename>app/models/subversion_update_parser.rb</filename>
    </added>
    <added>
      <filename>lib/file_sandbox.rb</filename>
    </added>
    <added>
      <filename>lib/redcloth.rb</filename>
    </added>
    <added>
      <filename>test/unit/subversion_info_parser_test.rb</filename>
    </added>
    <added>
      <filename>test/unit/subversion_propget_parser_test.rb</filename>
    </added>
    <added>
      <filename>test/unit/subversion_update_parser_test.rb</filename>
    </added>
  </added>
  <modified type="array">
    <modified>
      <diff>@@ -1,4 +1,5 @@
 TRUNK
+  - [patch] subversion external support
 ------------------------------------------------------------------------------------------------------------------------
 * 1.2.1
   - [bugfix] fixed some windows specific things to do with running a server</diff>
      <filename>CHANGELOG</filename>
    </modified>
    <modified>
      <diff>@@ -21,6 +21,10 @@ Revision #{number} committed by #{committed_by} on #{time.strftime('%Y-%m-%d %H:
     @number &lt;=&gt; other.number
   end
 
+  def eql?(other)
+    @number == other.number
+  end
+
   def to_i
     @number
   end</diff>
      <filename>app/models/revision.rb</filename>
    </modified>
    <modified>
      <diff>@@ -3,11 +3,12 @@ require 'builder_error'
 class Subversion
   include CommandLine
 
-  attr_accessor :url, :username, :password
+  attr_accessor :url, :username, :password, :check_externals
 
   def initialize(options = {})
-    @url, @username, @password, @interactive = 
+    @url, @username, @password, @interactive =
           options.delete(:url), options.delete(:username), options.delete(:password), options.delete(:interactive)
+    @check_externals = true
     raise &quot;don't know how to handle '#{options.keys.first}'&quot; if options.length &gt; 0
   end
   
@@ -42,31 +43,54 @@ class Subversion
 
   def latest_revision(project)
     svn_output = execute_in_local_copy(project, log('HEAD', last_locally_known_revision(project)))
-    SubversionLogParser.new.parse_log(svn_output).first
+    SubversionLogParser.new.parse(svn_output).first
   end
 
   def revisions_since(project, revision_number)
-    svn_output = execute_in_local_copy(project, log('HEAD', revision_number))
-    new_revisions = SubversionLogParser.new.parse_log(svn_output).reverse
-    new_revisions.delete_if { |r| r.number == revision_number }
-    new_revisions
+    new_revisions = revisions_since_for_url(project, revision_number)
+
+    if @check_externals
+      externals(project).each do |directory, svn_external_path|
+        new_revisions += revisions_since_for_url(project, revision_number, svn_external_path)
+      end
+    end
+
+    new_revisions = new_revisions.sort_by {|rev| rev.number}
+
+    #uniq doesn't work on arrays of revisions for some reason
+    final_revisions = []
+    new_revisions.each do |rev|
+      final_revisions &lt;&lt; rev unless rev.number == revision_number || final_revisions.include?(rev)
+    end
+    final_revisions
+  end
+
+  def revisions_since_for_url(project, revision_number, url = @url)
+    svn_output = execute_in_local_copy(project, log('HEAD', revision_number, url))
+    log_parser = SubversionLogParser.new
+    log_parser.parse(svn_output)
   end
 
   def update(project, revision = nil)
     revision_number = revision ? revision_number(revision) : 'HEAD'
     svn_output = execute_in_local_copy(project, svn('update', &quot;--revision&quot;, revision_number))
-    SubversionLogParser.new.parse_update(svn_output)
+    SubversionUpdateParser.new.parse(svn_output)
+  end
+
+  def externals(project)
+    svn_output = execute_in_local_copy(project, svn('propget', '-R', 'svn:externals'))
+    SubversionPropgetParser.new.parse(svn_output)
   end
   
   private
   
-  def log(from, to)
-    svn('log', &quot;--revision&quot;, &quot;#{from}:#{to}&quot;, '--verbose', '--xml', @url)
+  def log(from, to, url = @url)
+    svn('log', &quot;--revision&quot;, &quot;#{from}:#{to}&quot;, '--verbose', '--xml', url)
   end
   
   def info(project)
     svn_output = execute_in_local_copy(project, svn('info', &quot;--xml&quot;))
-    SubversionLogParser.new.parse_info(svn_output)
+    SubversionInfoParser.new.parse(svn_output)
   end
 
   def svn(operation, *options)</diff>
      <filename>app/models/subversion.rb</filename>
    </modified>
    <modified>
      <diff>@@ -3,30 +3,12 @@ require 'xml_simple'
 
 class SubversionLogParser
 
-  def parse_log(lines)
+  def parse(lines)
     return [] if lines.empty?
     entries = XmlSimple.xml_in(lines.join, 'ForceArray' =&gt; ['logentry','path'])['logentry'] || []
     entries.map {|entry| parse_revision(entry) }
   end
 
-  UPDATE_PATTERN = /^(...)  (\S.*)$/
-  def parse_update(lines)
-    lines[0..-2].collect do |line|
-      match = UPDATE_PATTERN.match(line)
-      if match
-        operation, file = match[1..2]
-        ChangesetEntry.new(operation, file)
-      else
-        nil
-      end
-    end.compact
-  end
-
-  def parse_info(xml)
-    info = XmlSimple.xml_in(xml.to_s, 'ForceArray' =&gt; false)['entry']
-    Subversion::Info.new(info['revision'].to_i, info['commit']['revision'].to_i, info['commit']['author'])
-  end
-
   private
   
   def parse_revision(hash)</diff>
      <filename>app/models/subversion_log_parser.rb</filename>
    </modified>
    <modified>
      <diff>@@ -45,7 +45,6 @@ Rails::Initializer.run do |config|
 
   # Add additional load paths for your own custom dirs
   config.load_paths &lt;&lt; &quot;#{RAILS_ROOT}/builder_plugins/installed&quot;
-  config.load_paths &lt;&lt; &quot;vendor/redcloth&quot;
 
   # Use the database for sessions instead of the file system
   # (create the session table with 'rake db:sessions:create')</diff>
      <filename>config/environment.rb</filename>
    </modified>
    <modified>
      <diff>@@ -15,8 +15,6 @@ require 'breakpoint'
 require 'mocha'
 require 'stubba'
 
-require 'rubygems'
-gem 'file_sandbox', '&gt;= 0.3'
 require 'file_sandbox'
 
 ActionMailer::Base.delivery_method = :test</diff>
      <filename>test/test_helper.rb</filename>
    </modified>
    <modified>
      <diff>@@ -61,20 +61,6 @@ LOG_ENTRY_WITH_MULTIPLE_ENTRIES = &lt;&lt;EOF
 &lt;/log&gt;
 EOF
 
-UPDATE_OUTPUT = &lt;&lt;EOF
-A    failing_project
-D    failing_project\\Rakefile
-U*   failing_project\\failing_test.rb
-G    failing_project\\revision_label.txt
-C B  passing_project\\revision_label.txt
-?    foo.txt
-
-Fetching external item into 'vendor\rails'
-Updated external to revision 5875.
-
-Updated to revision 46.
-EOF
-
   def test_can_parse_LOG_WITH_NO_OPTIONAL_VALUES
     expected_result = [Revision.new(359, nil, nil, nil, [])]
                                   
@@ -121,18 +107,6 @@ EOF
     parse_log(LOG_ENTRY_WITH_MULTIPLE_ENTRIES)
   end
 
-  def test_can_parse_UPDATE_OUTPUT
-    expected_result = [
-      ChangesetEntry.new('A  ', 'failing_project'),
-      ChangesetEntry.new('D  ', 'failing_project\Rakefile'),
-      ChangesetEntry.new('U* ', 'failing_project\\failing_test.rb'),
-      ChangesetEntry.new('G  ', 'failing_project\\revision_label.txt'),
-      ChangesetEntry.new('C B', 'passing_project\\revision_label.txt'),
-      ChangesetEntry.new('?  ', 'foo.txt')]
-
-    assert_equal expected_result, parse_update(UPDATE_OUTPUT)
-  end
-
   def test_revision_and_changeset_should_know_how_to_convert_to_string
     expected_result = &lt;&lt;-EOL
 Revision 359 committed by aslak on #{DateTime.parse(&quot;2006-05-22 13:23:29 -0600&quot;).strftime('%Y-%m-%d %H:%M:%S')}
@@ -142,164 +116,8 @@ versioning
     assert_equal expected_result, parse_log(SIMPLE_LOG_ENTRY)[0].to_s
   end
 
-INFO_XML_OUTPUT = &lt;&lt;-EOF
-&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
-&lt;info&gt;
-&lt;entry
-   kind=&quot;dir&quot;
-   path=&quot;cruisecontrolrb&quot;
-   revision=&quot;328&quot;&gt;
-&lt;url&gt;svn://rubyforge.org/var/svn/cruisecontrolrb&lt;/url&gt;
-&lt;repository&gt;
-&lt;root&gt;svn://rubyforge.org/var/svn/cruisecontrolrb&lt;/root&gt;
-&lt;uuid&gt;c04ce798-636b-4ca8-9149-0f9336831111&lt;/uuid&gt;
-&lt;/repository&gt;
-&lt;commit
-   revision=&quot;328&quot;&gt;
-&lt;author&gt;stellsmi&lt;/author&gt;
-&lt;date&gt;2007-03-08T02:00:09.035499Z&lt;/date&gt;
-&lt;/commit&gt;
-&lt;/entry&gt;
-&lt;/info&gt;
-EOF
-
-INFO_XML_OUTPUT_WITH_WORKING_COPY = &lt;&lt;-EOF
-&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
-&lt;info&gt;
-&lt;entry
-   kind=&quot;file&quot;
-   path=&quot;README&quot;
-   revision=&quot;328&quot;&gt;
-&lt;url&gt;svn://rubyforge.org/var/svn/cruisecontrolrb/README&lt;/url&gt;
-&lt;repository&gt;
-&lt;root&gt;svn://rubyforge.org/var/svn/cruisecontrolrb&lt;/root&gt;
-&lt;uuid&gt;c04ce798-636b-4ca8-9149-0f9336831111&lt;/uuid&gt;
-&lt;/repository&gt;
-&lt;wc-info&gt;
-&lt;schedule&gt;add&lt;/schedule&gt;
-&lt;copy-from-url&gt;svn://rubyforge.org/var/svn/cruisecontrolrb/README&lt;/copy-from-url&gt;
-&lt;copy-from-rev&gt;99&lt;/copy-from-rev&gt;
-&lt;text-updated&gt;2007-03-10T03:35:34.000000Z&lt;/text-updated&gt;
-&lt;prop-updated&gt;2007-03-10T03:35:34.000000Z&lt;/prop-updated&gt;
-&lt;checksum&gt;d41d8cd98f00b204e9800998ecf8427e&lt;/checksum&gt;
-&lt;/wc-info&gt;
-&lt;commit
-   revision=&quot;328&quot;&gt;
-&lt;author&gt;stellsmi&lt;/author&gt;
-&lt;date&gt;2007-03-08T02:00:09.035499Z&lt;/date&gt;
-&lt;/commit&gt;
-&lt;/entry&gt;
-&lt;/info&gt;
-EOF
-
-INFO_XML_OUTPUT_WITH_LOCK = &lt;&lt;-EOF
-&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
-&lt;info&gt;
-&lt;entry
-   kind=&quot;dir&quot;
-   path=&quot;cruisecontrolrb&quot;
-   revision=&quot;328&quot;&gt;
-&lt;url&gt;svn://rubyforge.org/var/svn/cruisecontrolrb&lt;/url&gt;
-&lt;repository&gt;
-&lt;root&gt;svn://rubyforge.org/var/svn/cruisecontrolrb&lt;/root&gt;
-&lt;uuid&gt;c04ce798-636b-4ca8-9149-0f9336831111&lt;/uuid&gt;
-&lt;/repository&gt;
-&lt;commit
-   revision=&quot;328&quot;&gt;
-&lt;author&gt;stellsmi&lt;/author&gt;
-&lt;date&gt;2007-03-08T02:00:09.035499Z&lt;/date&gt;
-&lt;/commit&gt;
-&lt;lock&gt;
-&lt;token&gt;opaquelocktoken:fc2b4dee-98f9-0310-abf3-653ff3226e6b&lt;/token&gt;
-&lt;owner&gt;dtsato&lt;/owner&gt;
-&lt;comment&gt;Dummy comment&lt;/comment&gt;
-&lt;created&gt;2007-03-08T16:29:18.035499Z&lt;/created&gt;
-&lt;expires&gt;2007-03-09T16:29:18.035499Z&lt;/expires&gt;
-&lt;/lock&gt;
-&lt;/entry&gt;
-&lt;/info&gt;
-EOF
-
-INFO_XML_OUTPUT_WITH_CONFLICT = &lt;&lt;-EOF
-&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
-&lt;info&gt;
-&lt;entry
-   kind=&quot;file&quot;
-   path=&quot;README&quot;
-   revision=&quot;328&quot;&gt;
-&lt;url&gt;svn://rubyforge.org/var/svn/cruisecontrolrb/README&lt;/url&gt;
-&lt;repository&gt;
-&lt;root&gt;svn://rubyforge.org/var/svn/cruisecontrolrb&lt;/root&gt;
-&lt;uuid&gt;c04ce798-636b-4ca8-9149-0f9336831111&lt;/uuid&gt;
-&lt;/repository&gt;
-&lt;wc-info&gt;
-&lt;schedule&gt;replace&lt;/schedule&gt;
-&lt;text-updated&gt;2007-03-10T03:35:34.000000Z&lt;/text-updated&gt;
-&lt;prop-updated&gt;2007-03-10T03:35:34.000000Z&lt;/prop-updated&gt;
-&lt;checksum&gt;d41d8cd98f00b204e9800998ecf8427e&lt;/checksum&gt;
-&lt;/wc-info&gt;
-&lt;commit
-   revision=&quot;328&quot;&gt;
-&lt;author&gt;stellsmi&lt;/author&gt;
-&lt;date&gt;2007-03-08T02:00:09.035499Z&lt;/date&gt;
-&lt;/commit&gt;
-&lt;conflict&gt;
-&lt;prev-base-file&gt;README_base&lt;/prev-base-file&gt;
-&lt;prev-wc-file&gt;README_wc&lt;/prev-wc-file&gt;
-&lt;cur-base-file&gt;README_file&lt;/cur-base-file&gt;
-&lt;prop-file&gt;README_prop&lt;/prop-file&gt;
-&lt;/conflict&gt;
-&lt;/entry&gt;
-&lt;/info&gt;
-EOF
-
-  def test_should_parse_INFO_XML_OUTPUT
-    expected_result = {:revision =&gt; 328,
-                       :last_changed_revision =&gt; 328,
-                       :last_changed_author =&gt; 'stellsmi'}
-    
-    assert_info_equal expected_result, parse_info(INFO_XML_OUTPUT)
-  end
-    
-  def test_should_parse_INFO_XML_OUTPUT_WITH_WORKING_COPY
-    expected_result = {:revision =&gt; 328,
-                       :last_changed_revision =&gt; 328,
-                       :last_changed_author =&gt; 'stellsmi'}
-    
-    assert_info_equal expected_result, parse_info(INFO_XML_OUTPUT_WITH_WORKING_COPY)
-  end
-
-  def test_should_parse_INFO_XML_OUTPUT_WITH_LOCK
-    expected_result = {:revision =&gt; 328,
-                       :last_changed_revision =&gt; 328,
-                       :last_changed_author =&gt; 'stellsmi'}
-    
-    assert_info_equal expected_result, parse_info(INFO_XML_OUTPUT_WITH_LOCK)
-  end
-  
-  def test_should_parse_INFO_XML_OUTPUT_WITH_CONFLICT
-    expected_result = {:revision =&gt; 328,
-                       :last_changed_revision =&gt; 328,
-                       :last_changed_author =&gt; 'stellsmi'}
-    
-    assert_info_equal expected_result, parse_info(INFO_XML_OUTPUT_WITH_CONFLICT)
-  end
-
   def parse_log(log_entry)
-    SubversionLogParser.new.parse_log(log_entry.split(&quot;\n&quot;))
+    SubversionLogParser.new.parse(log_entry.split(&quot;\n&quot;))
   end
 
-  def parse_update(log_entry)
-    SubversionLogParser.new.parse_update(log_entry.split(&quot;\n&quot;))
-  end
-
-  def parse_info(svn_output)
-    SubversionLogParser.new.parse_info(svn_output)
-  end
-  
-  def assert_info_equal(expected_fields, info)
-    expected_fields.each do |name, value|
-      assert_equal value, info.send(name), &quot;comparing #{name}&quot;
-    end
-  end
 end
\ No newline at end of file</diff>
      <filename>test/unit/subversion_log_parser_test.rb</filename>
    </modified>
    <modified>
      <diff>@@ -69,10 +69,9 @@ class SubversionTest &lt; Test::Unit::TestCase
 
   def test_revisions_since_should_reverse_the_log_entries_and_skip_the_one_corresponding_to_current_revision
     svn = Subversion.new
+    svn.check_externals = false
 
-    svn.expects(:execute).with([&quot;svn&quot;, &quot;--non-interactive&quot;, &quot;log&quot;, &quot;--revision&quot;, &quot;HEAD:15&quot;, &quot;--verbose&quot;, &quot;--xml&quot;],
-                               {:stderr =&gt; './svn.err'}).yields(StringIO.new(LOG_ENTRY))
-
+    svn.expects(:revisions_since_for_url).with(dummy_project, 15).returns([Revision.new(18), Revision.new(17), Revision.new(15)])
     revisions = svn.revisions_since(dummy_project, 15)
 
     assert_equal [17, 18], numbers(revisions)
@@ -80,10 +79,9 @@ class SubversionTest &lt; Test::Unit::TestCase
 
   def test_revisions_since_should_return_all_revisions_when_curreent_revision_is_not_in_the_log_output
     svn = Subversion.new
+    svn.check_externals = false
 
-    svn.expects(:execute).with([&quot;svn&quot;, &quot;--non-interactive&quot;, &quot;log&quot;, &quot;--revision&quot;, &quot;HEAD:14&quot;, &quot;--verbose&quot;, &quot;--xml&quot;],
-                               {:stderr =&gt; './svn.err'}).yields(StringIO.new(LOG_ENTRY))
-
+    svn.expects(:revisions_since_for_url).with(dummy_project, 14).returns([Revision.new(18), Revision.new(17), Revision.new(15)])
     revisions = svn.revisions_since(dummy_project, 14)
 
     assert_equal [15, 17, 18], numbers(revisions)
@@ -91,15 +89,53 @@ class SubversionTest &lt; Test::Unit::TestCase
 
   def test_revisions_since_should_return_an_empty_array_for_empty_log_output
     svn = Subversion.new
+    svn.check_externals = false
 
-    svn.expects(:execute).with([&quot;svn&quot;, &quot;--non-interactive&quot;, &quot;log&quot;, &quot;--revision&quot;, &quot;HEAD:14&quot;, &quot;--verbose&quot;, &quot;--xml&quot;], 
-                               {:stderr =&gt; './svn.err'}).yields(StringIO.new(EMPTY_LOG))
-
+    svn.expects(:revisions_since_for_url).with(dummy_project, 14).returns([])
     revisions = svn.revisions_since(dummy_project, 14)
 
     assert_equal [], numbers(revisions)
   end
 
+  def test_revisions_since_should_support_check_externals_as_well_and_combine_all_revisions_together
+    svn = Subversion.new
+    svn.check_externals = true
+    svn.expects(:externals).returns({&quot;a&quot; =&gt; &quot;svn+ssh://a&quot;, &quot;b&quot; =&gt; &quot;svn+ssh://b&quot;})
+    svn.expects(:revisions_since_for_url).with(dummy_project, 14).returns([Revision.new(18), Revision.new(17)])
+    svn.expects(:revisions_since_for_url).with(dummy_project, 14, &quot;svn+ssh://a&quot;).returns([Revision.new(18), Revision.new(15)])
+    svn.expects(:revisions_since_for_url).with(dummy_project, 14, &quot;svn+ssh://b&quot;).returns([])
+
+    revisions = svn.revisions_since(dummy_project, 14)
+    assert_equal [15, 17, 18], numbers(revisions)
+  end
+
+  def test_externals
+    svn = Subversion.new
+    svn.expects(:execute).with([&quot;svn&quot;, &quot;--non-interactive&quot;, &quot;propget&quot;, &quot;-R&quot;, &quot;svn:externals&quot;], {:stderr =&gt; './svn.err'}).returns(&quot;propget results&quot;)
+    parser = mock(&quot;parser&quot;)
+    SubversionPropgetParser.expects(:new).returns(parser)
+    parser.expects(:parse).returns(&quot;parse results&quot;)
+
+    assert_equal(&quot;parse results&quot;, svn.externals(dummy_project))
+  end
+
+  def test_revisions_since_for_url_should_work_without_url_argument
+    svn = Subversion.new
+
+    svn.expects(:execute).with([&quot;svn&quot;, &quot;--non-interactive&quot;, &quot;log&quot;, &quot;--revision&quot;, &quot;HEAD:14&quot;, &quot;--verbose&quot;, &quot;--xml&quot;],
+                               {:stderr =&gt; './svn.err'}).yields(StringIO.new(LOG_ENTRY))
+    revisions = svn.revisions_since_for_url(dummy_project, 14)
+    assert_equal [18, 17, 15], numbers(revisions)
+  end
+
+  def test_revisions_since_for_url_should_support_url_argument
+    svn = Subversion.new
+    svn.expects(:execute).with([&quot;svn&quot;, &quot;--non-interactive&quot;, &quot;log&quot;, &quot;--revision&quot;, &quot;HEAD:14&quot;, &quot;--verbose&quot;, &quot;--xml&quot;, &quot;svn+ssh://a&quot;],
+                               {:stderr =&gt; './svn.err'}).yields(StringIO.new(LOG_ENTRY))
+    revisions = svn.revisions_since_for_url(dummy_project, 14, &quot;svn+ssh://a&quot;)
+    assert_equal [18, 17, 15], numbers(revisions)
+  end
+
   def test_checkout_with_no_user_password
     svn = Subversion.new(:url =&gt; 'http://foo.com/svn/project')
     svn.expects(:execute).with([&quot;svn&quot;, &quot;--non-interactive&quot;, &quot;co&quot;, &quot;http://foo.com/svn/project&quot;, &quot;.&quot;])</diff>
      <filename>test/unit/subversion_test.rb</filename>
    </modified>
  </modified>
  <removed type="array">
    <removed>
      <filename>vendor/redcloth/redcloth.rb</filename>
    </removed>
  </removed>
  <parents type="array">
    <parent>
      <id>5eb784fdcbfec159e367a7b70ff2d3a3c485824d</id>
    </parent>
  </parents>
  <author>
    <name>stellsmi</name>
    <email>stellsmi@c04ce798-636b-4ca8-9149-0f9336831111</email>
  </author>
  <url>http://github.com/dustin/cruisecontrolrb/commit/3df8d5e6090655b4b3cefb21c5790430c00e84f0</url>
  <id>3df8d5e6090655b4b3cefb21c5790430c00e84f0</id>
  <committed-date>2007-11-16T22:20:58-08:00</committed-date>
  <authored-date>2007-11-16T22:20:58-08:00</authored-date>
  <message>applying svn external patch &amp; moving file sandbox &amp; redcloth to lib dir

git-svn-id: http://cruisecontrolrb.rubyforge.org/svn/trunk@565 c04ce798-636b-4ca8-9149-0f9336831111</message>
  <tree>837ce7eb41c49138722d3e7b7faa5986ad7ce575</tree>
  <committer>
    <name>stellsmi</name>
    <email>stellsmi@c04ce798-636b-4ca8-9149-0f9336831111</email>
  </committer>
</commit>
