5d63d1205274771af2ce4cc48a4125c252a42a4d

Author: Robin Luckey

Date: 2009-02-17 14:27:39 -0800

[CHANGE] Move the Subversion chaining functionality into a dedicated subclass. This preserves the original --stop-on-copy basic behavior in its own class. * This keeps the base SvnAdapter class uncomplicated. * The base SvnAdapter can still be used if desired. * I can foresee another, better Subversion importer built around svnsync that will not require chaining.

diff --git a/lib/scm.rb b/lib/scm.rb index 3089c09..8702a65 100644 --- a/lib/scm.rb +++ b/lib/scm.rb @@ -13,6 +13,7 @@ require 'lib/scm/diff' require 'lib/scm/adapters/abstract_adapter' require 'lib/scm/adapters/cvs_adapter' require 'lib/scm/adapters/svn_adapter' +require 'lib/scm/adapters/svn_chain_adapter' require 'lib/scm/adapters/git_adapter' require 'lib/scm/adapters/hg_adapter' require 'lib/scm/adapters/bzr_adapter' diff --git a/lib/scm/adapters/svn/cat_file.rb b/lib/scm/adapters/svn/cat_file.rb index fb2a8d1..45a8739 100644 --- a/lib/scm/adapters/svn/cat_file.rb +++ b/lib/scm/adapters/svn/cat_file.rb @@ -9,10 +9,6 @@ module Scm::Adapters end def cat(path, revision) - parent_svn(revision) ? parent_svn.cat(path, revision) : base_cat(path, revision) - end - - def base_cat(path, revision) begin run "svn cat -r #{revision} '#{SvnAdapter.uri_encode(File.join(self.root, self.branch_name.to_s, path.to_s))}@#{revision}'" rescue diff --git a/lib/scm/adapters/svn/chain.rb b/lib/scm/adapters/svn/chain.rb deleted file mode 100644 index 5f6435a..0000000 --- a/lib/scm/adapters/svn/chain.rb +++ /dev/null @@ -1,95 +0,0 @@ -module Scm::Adapters - class SvnAdapter < AbstractAdapter - - # Some explanation is in order about "chaining." - # - # First, realize that a base SvnAdapter only tracks the history of a single - # subdirectory. If you point an adapter at /trunk, then that adapter is - # going to ignore eveything in /branches and /tags. - # - # The problem with this is that directories often get moved about. What is - # called "/trunk" today might have been in a branch directory at some point - # in the past. But since we completely ignore other directories, we never see - # that old history. - # - # Suppose for example that from revisions 1 to 100, development occured in - # /branches/beta. Then at revision 101, /trunk was created by copying - # /branches/beta, and this /trunk lives on to this day. - # - # The log for revision 101 is going to look something like this: - # - # Changed paths: - # D /branches/beta - # A /trunk (from /branches/beta:100) - # - # A single SvnAdapter pointed at today's /trunk will only see revisions 101 - # through HEAD, because /trunk didn't even exist before revision 101. - # - # To capture the prior history, we need to create *another* SvnAdapter - # which points at /branches/beta, and which considers revisions from 1 to 100. - # - # That's what chaining is: when we find that the first commit of an adapter - # indicates the wholesale renaming or copying of the entire tree from - # another location, then we generate a new SvnAdapter that points to that - # prior location, and process that SvnAdapter as well. - # - # This behavior recurses ("chains") all the way back to revision 1. - # - # It only works if the *entire branch* moves. We don't chain when - # subdirectories or individual files are copied. - - # Returns the entire SvnAdapter ancestry chain as a simple array. - def chain - (parent_svn ? parent_svn.chain : []) << self - end - - # If this adapter's branch was created by copying or renaming another branch, - # then return a new adapter that points to that prior branch. - # - # Only commits following +since+ are considered, so if the copy or rename - # occured on or before +since+, then no parent will be found or returned. - def parent_svn(since=0) - @parent_svn ||={} # Poor man's memoize - - @parent_svn[since] ||= begin - parent = nil - c = first_commit(since) - if c - c.diffs.each do |d| - if (b = parent_branch_name(d)) - parent = SvnAdapter.new(:url => File.join(root, b), :branch_name => b, - :username => username, :password => password, - :final_token => d.from_revision).normalize - break - end - end - end - parent - end - end - - def first_token(since=0) - c = first_commit(since) - c && c.token - end - - def first_commit(since=0) - Scm::Parsers::SvnXmlParser.parse(next_revision_xml(since)).first - end - - # Returns the first commit with a revision number greater than the provided revision number - def next_revision_xml(since=0) - return "<?xml?>" if since.to_i >= head_token - run "svn log --verbose --xml --stop-on-copy -r #{since.to_i+1}:#{final_token || 'HEAD'} --limit 1 #{opt_auth} '#{SvnAdapter.uri_encode(File.join(self.root, self.branch_name))}@#{final_token || 'HEAD'}'" - end - - # If the passed diff represents the wholesale movement of the entire - # code tree from one directory to another, this method returns the name - # of the previous directory. - def parent_branch_name(d) - if d.action == 'A' && branch_name[0, d.path.size] == d.path && d.from_path && d.from_revision - d.from_path + branch_name[d.path.size..-1] - end - end - end -end diff --git a/lib/scm/adapters/svn/commits.rb b/lib/scm/adapters/svn/commits.rb index 9170f85..29d6462 100644 --- a/lib/scm/adapters/svn/commits.rb +++ b/lib/scm/adapters/svn/commits.rb @@ -19,56 +19,15 @@ module Scm::Adapters # this adapter ever return information regarding commits after this point. attr_accessor :final_token - #------------------------------------------------------------------ - # Recursive or "chained" versions of the commit accessors. - # - # These methods recurse through the chain of ancestors for this - # adapter, calling the base_* method in turn for each ancestor. - #------------------------------------------------------------------ - # Returns the count of commits following revision number 'since'. def commit_count(since=0) - (parent_svn ? parent_svn.commit_count(since) : 0) + base_commit_count(since) - end - - # Returns an array of revision numbers for all commits following revision number 'since'. - def commit_tokens(since=0) - (parent_svn(since) ? parent_svn.commit_tokens(since) : []) + base_commit_tokens(since) - end - - # Returns an array of commits following revision number 'since'. - def commits(since=0) - (parent_svn(since) ? parent_svn.commits(since) : []) + base_commits(since) - end - - # Yield verbose commits following revision number 'since', one at a time. - def each_commit(since=0, &block) - parent_svn.each_commit(since, &block) if parent_svn - base_each_commit(since) do |commit| - block.call commit - end - end - - def verbose_commit(since=0) - parent_svn(since) ? parent_svn.verbose_commit(since) : base_verbose_commit(since) - end - - #------------------------------------------------------------------ - # Base versions of the commit accessors. - # - # These are the original, simple commit accessors that are - # unaware of branch "chaining". - #------------------------------------------------------------------ - - # Returns the count of commits following revision number 'since'. - def base_commit_count(since=0) since ||= 0 return 0 if final_token && since >= final_token run("svn log -q -r #{since.to_i + 1}:#{final_token || 'HEAD'} --stop-on-copy '#{SvnAdapter.uri_encode(File.join(root, branch_name.to_s))}@#{final_token || 'HEAD'}' | grep -E -e '^r[0-9]+ ' | wc -l").strip.to_i end # Returns an array of revision numbers for all commits following revision number 'since'. - def base_commit_tokens(since=0) + def commit_tokens(since=0) since ||= 0 return [] if final_token && since >= final_token cmd = "svn log -q -r #{since.to_i + 1}:#{final_token || 'HEAD'} --stop-on-copy '#{SvnAdapter.uri_encode(File.join(root, branch_name.to_s))}@#{final_token || 'HEAD'}' | grep -E -e '^r[0-9]+ ' | cut -f 1 -d '|' | cut -c 2-" @@ -77,7 +36,7 @@ module Scm::Adapters # Returns an array of commits following revision number 'since'. # These commit objects do not include diffs. - def base_commits(since=0) + def commits(since=0) list = [] open_log_file(since) do |io| list = Scm::Parsers::SvnXmlParser.parse(io) @@ -93,9 +52,9 @@ module Scm::Adapters # directories, the complexity (and time) of this method comes in expanding directories with a recursion # through every file in the directory. # - def base_each_commit(since=nil) - base_commit_tokens(since).each do |rev| - yield base_verbose_commit(rev) + def each_commit(since=nil) + commit_tokens(since).each do |rev| + yield verbose_commit(rev) end end @@ -134,12 +93,7 @@ module Scm::Adapters # Note that if the directory was deleted, we have to look at the previous revision to see what it held. recurse_rev = (diff.action == 'D') ? rev-1 : rev - if diff.action == 'A' && diff.path == '' && rev == first_token && parent_svn - # A very special case. This is the first commit, and the entire tree is being - # copied from somewhere else. In this case, there isn't actually any change, just - # a change of branch_name. Return no diffs at all. - nil - elsif (diff.action == 'D' or diff.action == 'A') && is_directory?(diff.path, recurse_rev) + if (diff.action == 'D' or diff.action == 'A') && is_directory?(diff.path, recurse_rev) # Deleting or adding a directory. Expand it out to show every file. recurse_files(diff.path, recurse_rev).collect do |f| Scm::Diff.new(:action => diff.action, :path => File.join(diff.path, f)) @@ -178,7 +132,7 @@ module Scm::Adapters end end - def base_verbose_commit(rev) + def verbose_commit(rev) c = Scm::Parsers::SvnXmlParser.parse(single_revision_xml(rev)).first c.scm = self deepen_commit(strip_commit_branch(c)) diff --git a/lib/scm/adapters/svn_adapter.rb b/lib/scm/adapters/svn_adapter.rb index bd57f41..cb2569f 100644 --- a/lib/scm/adapters/svn_adapter.rb +++ b/lib/scm/adapters/svn_adapter.rb @@ -13,4 +13,3 @@ require 'lib/scm/adapters/svn/push' require 'lib/scm/adapters/svn/pull' require 'lib/scm/adapters/svn/head' require 'lib/scm/adapters/svn/misc' -require 'lib/scm/adapters/svn/chain' diff --git a/lib/scm/adapters/svn_chain/cat_file.rb b/lib/scm/adapters/svn_chain/cat_file.rb new file mode 100644 index 0000000..26657f7 --- /dev/null +++ b/lib/scm/adapters/svn_chain/cat_file.rb @@ -0,0 +1,8 @@ +module Scm::Adapters + class SvnChainAdapter < SvnAdapter + def cat(path, revision) + parent_svn(revision) ? parent_svn.cat(path, revision) : super(path, revision) + end + end +end + diff --git a/lib/scm/adapters/svn_chain/chain.rb b/lib/scm/adapters/svn_chain/chain.rb new file mode 100644 index 0000000..210add2 --- /dev/null +++ b/lib/scm/adapters/svn_chain/chain.rb @@ -0,0 +1,59 @@ +module Scm::Adapters + class SvnChainAdapter < SvnAdapter + + # Returns the entire SvnAdapter ancestry chain as a simple array. + def chain + (parent_svn ? parent_svn.chain : []) << self + end + + # If this adapter's branch was created by copying or renaming another branch, + # then return a new adapter that points to that prior branch. + # + # Only commits following +since+ are considered, so if the copy or rename + # occured on or before +since+, then no parent will be found or returned. + def parent_svn(since=0) + @parent_svn ||={} # Poor man's memoize + + @parent_svn[since] ||= begin + parent = nil + c = first_commit(since) + if c + c.diffs.each do |d| + if (b = parent_branch_name(d)) + parent = SvnChainAdapter.new( + :url => File.join(root, b), :branch_name => b, + :username => username, :password => password, + :final_token => d.from_revision).normalize + break + end + end + end + parent + end + end + + def first_token(since=0) + c = first_commit(since) + c && c.token + end + + def first_commit(since=0) + Scm::Parsers::SvnXmlParser.parse(next_revision_xml(since)).first + end + + # Returns the first commit with a revision number greater than the provided revision number + def next_revision_xml(since=0) + return "<?xml?>" if since.to_i >= head_token + run "svn log --verbose --xml --stop-on-copy -r #{since.to_i+1}:#{final_token || 'HEAD'} --limit 1 #{opt_auth} '#{SvnAdapter.uri_encode(File.join(self.root, self.branch_name))}@#{final_token || 'HEAD'}'" + end + + # If the passed diff represents the wholesale movement of the entire + # code tree from one directory to another, this method returns the name + # of the previous directory. + def parent_branch_name(d) + if d.action == 'A' && branch_name[0, d.path.size] == d.path && d.from_path && d.from_revision + d.from_path + branch_name[d.path.size..-1] + end + end + end +end diff --git a/lib/scm/adapters/svn_chain/commits.rb b/lib/scm/adapters/svn_chain/commits.rb new file mode 100644 index 0000000..4c9c6a2 --- /dev/null +++ b/lib/scm/adapters/svn_chain/commits.rb @@ -0,0 +1,37 @@ +module Scm::Adapters + class SvnChainAdapter < SvnAdapter + + # Returns the count of commits following revision number 'since'. + def commit_count(since=0) + (parent_svn ? parent_svn.commit_count(since) : 0) + super(since) + end + + # Returns an array of revision numbers for all commits following revision number 'since'. + def commit_tokens(since=0) + (parent_svn(since) ? parent_svn.commit_tokens(since) : []) + super(since) + end + + # Returns an array of commits following revision number 'since'. + def commits(since=0) + (parent_svn(since) ? parent_svn.commits(since) : []) + super(since) + end + + def verbose_commit(since=0) + parent_svn(since) ? parent_svn.verbose_commit(since) : super(since) + end + + # If the diff points to a file, simply returns the diff. + # If the diff points to a directory, returns an array of diffs for every file in the directory. + def deepen_diff(diff, rev) + if diff.action == 'A' && diff.path == '' && parent_svn && rev == first_token + # A very special case that is important for chaining. + # This is the first commit, and the entire tree is being created by copying from parent_svn. + # In this case, there isn't actually any change, just + # a change of branch_name. Return no diffs at all. + nil + else + super(diff, rev) + end + end + end +end diff --git a/lib/scm/adapters/svn_chain_adapter.rb b/lib/scm/adapters/svn_chain_adapter.rb new file mode 100644 index 0000000..674e6f0 --- /dev/null +++ b/lib/scm/adapters/svn_chain_adapter.rb @@ -0,0 +1,44 @@ +module Scm::Adapters + # Some explanation is in order about "chaining." + # + # First, realize that a base SvnAdapter only tracks the history of a single + # subdirectory. If you point an adapter at /trunk, then that adapter is + # going to ignore eveything in /branches and /tags. + # + # The problem with this is that directories often get moved about. What is + # called "/trunk" today might have been in a branch directory at some point + # in the past. But since we completely ignore other directories, we never see + # that old history. + # + # Suppose for example that from revisions 1 to 100, development occured in + # /branches/beta. Then at revision 101, /trunk was created by copying + # /branches/beta, and this /trunk lives on to this day. + # + # The log for revision 101 is going to look something like this: + # + # Changed paths: + # D /branches/beta + # A /trunk (from /branches/beta:100) + # + # A single SvnAdapter pointed at today's /trunk will only see revisions 101 + # through HEAD, because /trunk didn't even exist before revision 101. + # + # To capture the prior history, we need to create *another* SvnAdapter + # which points at /branches/beta, and which considers revisions from 1 to 100. + # + # That's what chaining is: when we find that the first commit of an adapter + # indicates the wholesale renaming or copying of the entire tree from + # another location, then we generate a new SvnAdapter that points to that + # prior location, and process that SvnAdapter as well. + # + # This behavior recurses ("chains") all the way back to revision 1. + # + # It only works if the *entire branch* moves. We don't chain when + # subdirectories or individual files are copied. + class SvnChainAdapter < SvnAdapter + end +end + +require 'lib/scm/adapters/svn_chain/chain' +require 'lib/scm/adapters/svn_chain/commits' +require 'lib/scm/adapters/svn_chain/cat_file' diff --git a/test/test_helper.rb b/test/test_helper.rb index c84fa24..3c8651c 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -69,6 +69,15 @@ class Scm::Test < Test::Unit::TestCase end end + def with_svn_chain_repository(name, branch_name='') + with_repository(Scm::Adapters::SvnChainAdapter, name) do |svn| + svn.branch_name = branch_name + svn.url = File.join(svn.root, svn.branch_name) + svn.url = svn.url[0..-2] if svn.url[-1..-1] == '/' # Strip trailing / + yield svn + end + end + def with_cvs_repository(name) with_repository(Scm::Adapters::CvsAdapter, name) { |cvs| yield cvs } end diff --git a/test/unit/svn_cat_file_test.rb b/test/unit/svn_cat_file_test.rb index 188ad62..f7e47ea 100644 --- a/test/unit/svn_cat_file_test.rb +++ b/test/unit/svn_cat_file_test.rb @@ -18,23 +18,5 @@ EXPECTED assert_equal nil, svn.cat_file(Scm::Commit.new(:token => 1), Scm::Diff.new(:path => "file not found")) end end - - def test_cat_file_with_chaining -goodbye = <<-EXPECTED -#include <stdio.h> -main() -{ - printf("Goodbye, world!\\n"); -} -EXPECTED - with_svn_repository('svn_with_branching', '/trunk') do |svn| - # The first case asks for the file on the HEAD, so it should easily be found - assert_equal goodbye, svn.cat_file(Scm::Commit.new(:token => 8), Scm::Diff.new(:path => "goodbyeworld.c")) - - # The next test asks for the file as it appeared before /branches/development was moved to /trunk, - # so this request requires traversal up the chain to the parent SvnAdapter. - assert_equal goodbye, svn.cat_file(Scm::Commit.new(:token => 5), Scm::Diff.new(:path => "goodbyeworld.c")) - end - end end end diff --git a/test/unit/svn_chain_cat_file_test.rb b/test/unit/svn_chain_cat_file_test.rb new file mode 100644 index 0000000..579397f --- /dev/null +++ b/test/unit/svn_chain_cat_file_test.rb @@ -0,0 +1,24 @@ +require File.dirname(__FILE__) + '/../test_helper' + +module Scm::Adapters + class SvnChainCatFileTest < Scm::Test + + def test_cat_file_with_chaining +goodbye = <<-EXPECTED +#include <stdio.h> +main() +{ + printf("Goodbye, world!\\n"); +} +EXPECTED + with_svn_chain_repository('svn_with_branching', '/trunk') do |svn| + # The first case asks for the file on the HEAD, so it should easily be found + assert_equal goodbye, svn.cat_file(Scm::Commit.new(:token => 8), Scm::Diff.new(:path => "goodbyeworld.c")) + + # The next test asks for the file as it appeared before /branches/development was moved to /trunk, + # so this request requires traversal up the chain to the parent SvnAdapter. + assert_equal goodbye, svn.cat_file(Scm::Commit.new(:token => 5), Scm::Diff.new(:path => "goodbyeworld.c")) + end + end + end +end diff --git a/test/unit/svn_chain_commits_test.rb b/test/unit/svn_chain_commits_test.rb new file mode 100644 index 0000000..77266b2 --- /dev/null +++ b/test/unit/svn_chain_commits_test.rb @@ -0,0 +1,168 @@ +require File.dirname(__FILE__) + '/../test_helper' + +module Scm::Parsers + class SvnChainTest < Scm::Test + + def test_chained_commit_tokens + with_svn_chain_repository('svn_with_branching', '/trunk') do |svn| + assert_equal [1,2,4,5,8,9], svn.commit_tokens + assert_equal [2,4,5,8,9], svn.commit_tokens(1) + assert_equal [4,5,8,9], svn.commit_tokens(2) + assert_equal [4,5,8,9], svn.commit_tokens(3) + assert_equal [5,8,9], svn.commit_tokens(4) + assert_equal [8,9], svn.commit_tokens(5) + assert_equal [8,9], svn.commit_tokens(6) + assert_equal [8,9], svn.commit_tokens(7) + assert_equal [9], svn.commit_tokens(8) + assert_equal [], svn.commit_tokens(9) + assert_equal [], svn.commit_tokens(10) + end + end + + def test_chained_commit_count + with_svn_chain_repository('svn_with_branching', '/trunk') do |svn| + assert_equal 6, svn.commit_count + assert_equal 5, svn.commit_count(1) + assert_equal 4, svn.commit_count(2) + assert_equal 4, svn.commit_count(3) + assert_equal 3, svn.commit_count(4) + assert_equal 2, svn.commit_count(5) + assert_equal 2, svn.commit_count(6) + assert_equal 2, svn.commit_count(7) + assert_equal 1, svn.commit_count(8) + assert_equal 0, svn.commit_count(9) + end + end + + def test_chained_commits + with_svn_chain_repository('svn_with_branching', '/trunk') do |svn| + assert_equal [1,2,4,5,8,9], svn.commits.collect { |c| c.token } + assert_equal [2,4,5,8,9], svn.commits(1).collect { |c| c.token } + assert_equal [4,5,8,9], svn.commits(2).collect { |c| c.token } + assert_equal [4,5,8,9], svn.commits(3).collect { |c| c.token } + assert_equal [5,8,9], svn.commits(4).collect { |c| c.token } + assert_equal [8,9], svn.commits(5).collect { |c| c.token } + assert_equal [8,9], svn.commits(6).collect { |c| c.token } + assert_equal [8,9], svn.commits(7).collect { |c| c.token } + assert_equal [9], svn.commits(8).collect { |c| c.token } + assert_equal [], svn.commits(9).collect { |c| c.token } + end + end + + # This test is primarly concerned with the checking the diffs + # of commits. Specifically, when an entire branch is moved + # to a new name, we should not see any diffs. From our + # point of view the code is unchanged; only the base directory + # has moved. + def test_chained_each_commit + commits = [] + with_svn_chain_repository('svn_with_branching', '/trunk') do |svn| + svn.each_commit do |c| + assert c.scm # To support checkout of chained commits, the + # commit must include a link to its containing adapter. + commits << c + end + end + + assert_equal [1,2,4,5,8,9], commits.collect { |c| c.token } + + # This repository spends a lot of energy moving directories around. + # File edits actually occur in just 3 commits. + + # Revision 1: /trunk directory created, but it is empty + assert_equal 0, commits[0].diffs.size + + # Revision 2: /trunk/helloworld.c is added + assert_equal 1, commits[1].diffs.size + assert_equal 'A', commits[1].diffs.first.action + assert_equal '/helloworld.c', commits[1].diffs.first.path + + # Revision 3: /trunk is deleted. We can't see this revision. + + # Revision 4: /trunk is re-created by copying it from revision 2. + # From our point of view, there has been no change at all, and thus no diffs. + assert_equal 0, commits[2].diffs.size + + # Revision 5: /branches/development is created by copying /trunk. + # From our point of view, the contents of the repository are unchanged, so + # no diffs result from the copy. + # However, /branches/development/goodbyeworld.c is also created, so we should + # have a diff for that. + assert_equal 1, commits[3].diffs.size + assert_equal 'A', commits[3].diffs.first.action + assert_equal '/goodbyeworld.c', commits[3].diffs.first.path + + # Revision 6: /trunk/goodbyeworld.c is created, but we only see activity + # on /branches/development, so no commit reported. + + # Revision 7: /trunk is deleted, but again we don't see it. + + # Revision 8: /branches/development is moved to become the new /trunk. + # The directory contents are unchanged, so no diffs result. + assert_equal 0, commits[4].diffs.size + + # Revision 9: an edit to /trunk/helloworld.c + assert_equal 1, commits[5].diffs.size + assert_equal 'M', commits[5].diffs.first.action + assert_equal '/helloworld.c', commits[5].diffs.first.path + end + + # Specifically tests this case: + # Suppose we're importing /myproject/trunk, and the log + # contains the following: + # + # A /myproject (from /all/myproject:1) + # D /all/myproject + # + # We need to make sure we detect the move here, even though + # "/myproject" is not an exact match for "/myproject/trunk". + def test_tree_move + with_svn_chain_repository('svn_with_tree_move', '/myproject/trunk') do |svn| + assert_equal svn.url, svn.root + '/myproject/trunk' + assert_equal svn.branch_name, '/myproject/trunk' + + p = svn.parent_svn + assert_equal p.url, svn.root + '/all/myproject/trunk' + assert_equal p.branch_name, '/all/myproject/trunk' + assert_equal p.final_token, 1 + + assert_equal [1, 2], svn.commit_tokens + end + end + + def test_verbose_commit_with_chaining + with_svn_chain_repository('svn_with_branching','/trunk') do |svn| + + c = svn.verbose_commit(9) + assert_equal 'modified helloworld.c', c.message + assert_equal ['/helloworld.c'], c.diffs.collect { |d| d.path } + assert_equal '/trunk', c.scm.branch_name + + c = svn.verbose_commit(8) + assert_equal [], c.diffs + assert_equal '/trunk', c.scm.branch_name + + # Reaching these commits requires chaining + c = svn.verbose_commit(5) + assert_equal 'add a new branch, with goodbyeworld.c', c.message + assert_equal ['/goodbyeworld.c'], c.diffs.collect { |d| d.path } + assert_equal '/branches/development', c.scm.branch_name + + # Reaching these commits requires chaining twice + c = svn.verbose_commit(4) + assert_equal [], c.diffs + assert_equal '/trunk', c.scm.branch_name + + # And now a fourth chain (to skip over /trunk deletion in rev 3) + c = svn.verbose_commit(2) + assert_equal 'Added helloworld.c to trunk', c.message + assert_equal ['/helloworld.c'], c.diffs.collect { |d| d.path } + assert_equal '/trunk', c.scm.branch_name + + c = svn.verbose_commit(1) + assert_equal [], c.diffs + assert_equal '/trunk', c.scm.branch_name + end + end + end +end diff --git a/test/unit/svn_chain_test.rb b/test/unit/svn_chain_test.rb index eaaef8e..22a74da 100644 --- a/test/unit/svn_chain_test.rb +++ b/test/unit/svn_chain_test.rb @@ -4,7 +4,7 @@ module Scm::Parsers class SvnChainTest < Scm::Test def test_chain - with_svn_repository('svn_with_branching', '/trunk') do |svn| + with_svn_chain_repository('svn_with_branching', '/trunk') do |svn| chain = svn.chain assert_equal 4, chain.size @@ -33,12 +33,10 @@ module Scm::Parsers end def test_parent_svn - with_svn_repository('svn_with_branching', '/trunk') do |svn| + with_svn_chain_repository('svn_with_branching', '/trunk') do |svn| # In this repository, /branches/development becomes - # the /trunk in revision 8. So there should be no record - # before revision 8 in the 'traditional' base commit parser. - assert_equal [8,9], svn.base_commit_tokens - + # the /trunk in revision 8. So there should be a parent + # will final_token 7. p1 = svn.parent_svn assert_equal p1.url, svn.root + '/branches/development' assert_equal p1.branch_name, '/branches/development' @@ -53,131 +51,8 @@ module Scm::Parsers end end - def test_chained_commit_tokens - with_svn_repository('svn_with_branching', '/trunk') do |svn| - assert_equal [1,2,4,5,8,9], svn.commit_tokens - assert_equal [2,4,5,8,9], svn.commit_tokens(1) - assert_equal [4,5,8,9], svn.commit_tokens(2) - assert_equal [4,5,8,9], svn.commit_tokens(3) - assert_equal [5,8,9], svn.commit_tokens(4) - assert_equal [8,9], svn.commit_tokens(5) - assert_equal [8,9], svn.commit_tokens(6) - assert_equal [8,9], svn.commit_tokens(7) - assert_equal [9], svn.commit_tokens(8) - assert_equal [], svn.commit_tokens(9) - assert_equal [], svn.commit_tokens(10) - end - end - - def test_chained_commit_count - with_svn_repository('svn_with_branching', '/trunk') do |svn| - assert_equal 6, svn.commit_count - assert_equal 5, svn.commit_count(1) - assert_equal 4, svn.commit_count(2) - assert_equal 4, svn.commit_count(3) - assert_equal 3, svn.commit_count(4) - assert_equal 2, svn.commit_count(5) - assert_equal 2, svn.commit_count(6) - assert_equal 2, svn.commit_count(7) - assert_equal 1, svn.commit_count(8) - assert_equal 0, svn.commit_count(9) - end - end - - def test_chained_commits - with_svn_repository('svn_with_branching', '/trunk') do |svn| - assert_equal [1,2,4,5,8,9], svn.commits.collect { |c| c.token } - assert_equal [2,4,5,8,9], svn.commits(1).collect { |c| c.token } - assert_equal [4,5,8,9], svn.commits(2).collect { |c| c.token } - assert_equal [4,5,8,9], svn.commits(3).collect { |c| c.token } - assert_equal [5,8,9], svn.commits(4).collect { |c| c.token } - assert_equal [8,9], svn.commits(5).collect { |c| c.token } - assert_equal [8,9], svn.commits(6).collect { |c| c.token } - assert_equal [8,9], svn.commits(7).collect { |c| c.token } - assert_equal [9], svn.commits(8).collect { |c| c.token } - assert_equal [], svn.commits(9).collect { |c| c.token } - end - end - - # This test is primarly concerned with the checking the diffs - # of commits. Specifically, when an entire branch is moved - # to a new name, we should not see any diffs. From our - # point of view the code is unchanged; only the base directory - # has moved. - def test_chained_each_commit - commits = [] - with_svn_repository('svn_with_branching', '/trunk') do |svn| - svn.each_commit { |c| commits << c } - end - - assert_equal [1,2,4,5,8,9], commits.collect { |c| c.token } - - # This repository spends a lot of energy moving directories around. - # File edits actually occur in just 3 commits. - - # Revision 1: /trunk directory created, but it is empty - assert_equal 0, commits[0].diffs.size - - # Revision 2: /trunk/helloworld.c is added - assert_equal 1, commits[1].diffs.size - assert_equal 'A', commits[1].diffs.first.action - assert_equal '/helloworld.c', commits[1].diffs.first.path - - # Revision 3: /trunk is deleted. We can't see this revision. - - # Revision 4: /trunk is re-created by copying it from revision 2. - # From our point of view, there has been no change at all, and thus no diffs. - assert_equal 0, commits[2].diffs.size - - # Revision 5: /branches/development is created by copying /trunk. - # From our point of view, the contents of the repository are unchanged, so - # no diffs result from the copy. - # However, /branches/development/goodbyeworld.c is also created, so we should - # have a diff for that. - assert_equal 1, commits[3].diffs.size - assert_equal 'A', commits[3].diffs.first.action - assert_equal '/goodbyeworld.c', commits[3].diffs.first.path - - # Revision 6: /trunk/goodbyeworld.c is created, but we only see activity - # on /branches/development, so no commit reported. - - # Revision 7: /trunk is deleted, but again we don't see it. - - # Revision 8: /branches/development is moved to become the new /trunk. - # The directory contents are unchanged, so no diffs result. - assert_equal 0, commits[4].diffs.size - - # Revision 9: an edit to /trunk/helloworld.c - assert_equal 1, commits[5].diffs.size - assert_equal 'M', commits[5].diffs.first.action - assert_equal '/helloworld.c', commits[5].diffs.first.path - end - - # Specifically tests this case: - # Suppose we're importing /myproject/trunk, and the log - # contains the following: - # - # A /myproject (from /all/myproject:1) - # D /all/myproject - # - # We need to make sure we detect the move here, even though - # "/myproject" is not an exact match for "/myproject/trunk". - def test_tree_move - with_svn_repository('svn_with_tree_move', '/myproject/trunk') do |svn| - assert_equal svn.url, svn.root + '/myproject/trunk' - assert_equal svn.branch_name, '/myproject/trunk' - - p = svn.parent_svn - assert_equal p.url, svn.root + '/all/myproject/trunk' - assert_equal p.branch_name, '/all/myproject/trunk' - assert_equal p.final_token, 1 - - assert_equal [1, 2], svn.commit_tokens - end - end - def test_parent_branch_name - svn = Scm::Adapters::SvnAdapter.new(:branch_name => "/trunk") + svn = Scm::Adapters::SvnChainAdapter.new(:branch_name => "/trunk") assert_equal "/branches/b", svn.parent_branch_name(Scm::Diff.new(:action => 'A', :path => "/trunk", :from_revision => 1, :from_path => "/branches/b")) diff --git a/test/unit/svn_commits_test.rb b/test/unit/svn_commits_test.rb index 6f051d8..ef90191 100644 --- a/test/unit/svn_commits_test.rb +++ b/test/unit/svn_commits_test.rb @@ -121,40 +121,19 @@ module Scm::Adapters # The full repository contains 4 revisions... assert_equal 4, svn.commit_count - # ..however, looking only at the history of /trunk@4 shows 3 revisions. - # - # That's because /trunk@3 was created by copying /branches/b@1 + # ...however, the current trunk contains only revisions 3 and 4. + # That's because the branch was moved to replace the trunk at revision 3. # - # I hope the following diagram helps to explain: - # - # r1 | r2 | r3 | r4 - # ----------------|----------------|---------------------|--------------- - # | | | - # /trunk(x) ------|-> deleted(x) | /--> /trunk(*) --|--> /trunk(*) - # | | / | - # /branches/b(*)--|----------------|--/ | - # | | | - # - # (*) activity we track - # (x) activity we ignore + # Even though there was a different trunk directory present in + # revisions 1 and 2, it is not visible to Ohloh. trunk = SvnAdapter.new(:url => File.join(svn.url,'trunk'), :branch_name => '/trunk').normalize - assert_equal 3, trunk.commit_count - assert_equal [1,3,4], trunk.commit_tokens - - commits = [] - trunk.each_commit { |c| commits << c } + assert_equal 2, trunk.commit_count + assert_equal [3,4], trunk.commit_tokens - # The first commit we should see is /branches/b@1. - # It should contain two files. - assert_equal 1, commits.first.token - assert_equal 2, commits.first.diffs.size # Two files seen - - assert_equal 'A', commits.first.diffs[0].action - assert_equal '/subdir/bar.rb', commits.first.diffs[0].path - assert_equal 'A', commits.first.diffs[1].action - assert_equal '/subdir/foo.rb', commits.first.diffs[1].path + deep_commits = [] + trunk.each_commit { |c| deep_commits << c } # When the branch is moved to replace the trunk in revision 3, # the Subversion log shows @@ -162,34 +141,46 @@ module Scm::Adapters # D /branches/b # A /trunk (from /branches/b:2) # - # The branch_name changes at this point, but none of the file contents - # actually change. So there are no diffs to report at this point. - assert_equal 3, commits[1].token - assert_equal 0, commits[1].diffs.size + # However, there are files in those directories. Make sure the commits + # that we generate include all of those files not shown by the log. + # + # Also, our commits do not include diffs for the actual directories; + # only the files within those directories. + # + # Also, since we are only tracking the /trunk and not /branches/b, then + # there should not be anything referring to activity in /branches/b. - # In Revision 4, a subdirectory is renamed. This shows in the Subversion log as + assert_equal 3, deep_commits.first.token # Make sure this is the right revision + assert_equal 2, deep_commits.first.diffs.size # Two files seen + + assert_equal 'A', deep_commits.first.diffs[0].action + assert_equal '/subdir/bar.rb', deep_commits.first.diffs[0].path + assert_equal 'A', deep_commits.first.diffs[1].action + assert_equal '/subdir/foo.rb', deep_commits.first.diffs[1].path + + # In Revision 4, a directory is renamed. This shows in the Subversion log as # # A /trunk/newdir (from /trunk/subdir:3) # D /trunk/subdir # - # There are files in this directory, so make sure our commit includes + # Again, there are files in this directory, so make sure our commit includes # both delete and add events for all of the files in this directory, but does # not actually refer to the directories themselves. - assert_equal 4, commits.last.token + assert_equal 4, deep_commits.last.token # Make sure we're checking the right revision # There should be 2 files removed and two files added - assert_equal 4, commits.last.diffs.size + assert_equal 4, deep_commits.last.diffs.size - assert_equal 'A', commits.last.diffs[0].action - assert_equal '/newdir/bar.rb', commits.last.diffs[0].path - assert_equal 'A', commits.last.diffs[1].action - assert_equal '/newdir/foo.rb', commits.last.diffs[1].path + assert_equal 'A', deep_commits.last.diffs[0].action + assert_equal '/newdir/bar.rb', deep_commits.last.diffs[0].path + assert_equal 'A', deep_commits.last.diffs[1].action + assert_equal '/newdir/foo.rb', deep_commits.last.diffs[1].path - assert_equal 'D', commits.last.diffs[2].action - assert_equal '/subdir/bar.rb', commits.last.diffs[2].path - assert_equal 'D', commits.last.diffs[3].action - assert_equal '/subdir/foo.rb', commits.last.diffs[3].path + assert_equal 'D', deep_commits.last.diffs[2].action + assert_equal '/subdir/bar.rb', deep_commits.last.diffs[2].path + assert_equal 'D', deep_commits.last.diffs[3].action + assert_equal '/subdir/foo.rb', deep_commits.last.diffs[3].path end end @@ -209,7 +200,6 @@ module Scm::Adapters assert d.action.length == 1 assert d.path.length > 0 end - assert_equal svn, e.scm # Commit points back to its containing scm. end assert !FileTest.exist?(svn.log_filename) # Make sure we cleaned up after ourselves end @@ -254,40 +244,5 @@ module Scm::Adapters assert_equal '/trunk/COPYING', commits[4].diffs[1].path end - def test_verbose_commit_with_chaining - with_svn_repository('svn_with_branching','/trunk') do |svn| - - c = svn.verbose_commit(9) - assert_equal 'modified helloworld.c', c.message - assert_equal ['/helloworld.c'], c.diffs.collect { |d| d.path } - assert_equal '/trunk', c.scm.branch_name - - c = svn.verbose_commit(8) - assert_equal [], c.diffs - assert_equal '/trunk', c.scm.branch_name - - # Reaching these commits requires chaining - c = svn.verbose_commit(5) - assert_equal 'add a new branch, with goodbyeworld.c', c.message - assert_equal ['/goodbyeworld.c'], c.diffs.collect { |d| d.path } - assert_equal '/branches/development', c.scm.branch_name - - # Reaching these commits requires chaining twice - c = svn.verbose_commit(4) - assert_equal [], c.diffs - assert_equal '/trunk', c.scm.branch_name - - # And now a fourth chain (to skip over /trunk deletion in rev 3) - c = svn.verbose_commit(2) - assert_equal 'Added helloworld.c to trunk', c.message - assert_equal ['/helloworld.c'], c.diffs.collect { |d| d.path } - assert_equal '/trunk', c.scm.branch_name - - c = svn.verbose_commit(1) - assert_equal [], c.diffs - assert_equal '/trunk', c.scm.branch_name - end - end - end end