Author: lacton Date: Wed Aug 27 15:04:19 2008 New Revision: 689640 URL: http://svn.apache.org/viewvc?rev=689640&view=rev Log: Release task: added support for alternative SVN layout, fixed regexp for THIS_VERSION, fixed calls to Buildr.application.buildfile.
Added tests for Release task. Extracted an Svn class to make testing easier. SVN has two official layouts. Reference: http://svnbook.red-bean.com/en/1.4/svn.reposadmin.planning.html#svn.reposadmin.projects.chooselayout Added support for the other one. Buildr.application.buildfile is no longer a file path, but a FileTask. This change requires to convert the Task to a file path. Fixed a few typos and extracted a few methods. Modified: incubator/buildr/trunk/CHANGELOG incubator/buildr/trunk/lib/buildr/core/build.rb incubator/buildr/trunk/spec/build_spec.rb Modified: incubator/buildr/trunk/CHANGELOG URL: http://svn.apache.org/viewvc/incubator/buildr/trunk/CHANGELOG?rev=689640&r1=689639&r2=689640&view=diff ============================================================================== --- incubator/buildr/trunk/CHANGELOG (original) +++ incubator/buildr/trunk/CHANGELOG Wed Aug 27 15:04:19 2008 @@ -2,6 +2,8 @@ * Added: Growl notifications (OS X only). * Added: error, info and trace methods. * Added: BUILDR-128 Emma support +* Added: Release task support for alternative SVN repository layout + (e.g., http://my.repo.org/trunk/foo). * Change: Error reporting now shows 'buildr aborted!' (used to say rake), more of the stack trace without running --trace, and when running with supported terminal, error message is red. @@ -34,6 +36,7 @@ * Fixed: BUILDR-126 Tests options are shared between unrelated projects when using #options instead of #using (Lacton). * Fixed: Should not display "(in `pwd`, development)" when using --quiet. +* Fixed: Release task's regexp to find either THIS_VERSION and VERSION_NUMBER. * Docs: BUILDR-111 Troubleshoot tip when Buildr's bin directory shows up in RUBYLIB (Geoffrey Ruscoe). Modified: incubator/buildr/trunk/lib/buildr/core/build.rb URL: http://svn.apache.org/viewvc/incubator/buildr/trunk/lib/buildr/core/build.rb?rev=689640&r1=689639&r2=689640&view=diff ============================================================================== --- incubator/buildr/trunk/lib/buildr/core/build.rb (original) +++ incubator/buildr/trunk/lib/buildr/core/build.rb Wed Aug 27 15:04:19 2008 @@ -27,7 +27,7 @@ # Runs the build in parallel when true (defaults to false). You can force a parallel build by # setting this option directly, or by running the parallel task ahead of the build task. # - # This option only affects recurvise tasks. For example: + # This option only affects recursive tasks. For example: # buildr parallel package # will run all package tasks (from the sub-projects) in parallel, but each sub-project's package # task runs its child tasks (prepare, compile, resources, etc) in sequence. @@ -48,7 +48,7 @@ desc 'Clean files generated during a build' Project.local_task('clean') { |name| "Cleaning #{name}" } - desc 'The default task it build' + desc 'The default task is build' task 'default'=>'build' end @@ -109,9 +109,47 @@ end + class Svn + + class << self + def commit file, message + svn 'commit', '-m', message, file + end + + def copy dir, url, message + svn 'copy', dir, url, '-m', message + end + + # Return the current SVN URL + def repo_url + url = svn('info').scan(/URL: (.*)/)[0][0] + end + + 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) + # + # Executes SVN command and returns the output. + def svn(*args) + cmd = 'svn ' + args.map { |arg| arg[' '] ? %Q{"#{arg}"} : arg }.join(' ') + info cmd + `#{cmd}`.tap { fail 'SVN command failed' unless $?.exitstatus == 0 } + end + end + end + + class Release - THIS_VERSION_PATTERN = /THIS_VERSION|VERSION_NUMBER\s*=\s*(["'])(.*)\1/ + THIS_VERSION_PATTERN = /(THIS_VERSION|VERSION_NUMBER)\s*=\s*(["'])(.*)\2/ NEXT_VERSION_PATTERN = /NEXT_VERSION\s*=\s*(["'])(.*)\1/ class << self @@ -122,7 +160,7 @@ # Make a release. def make() check - version = with_next_version do |filename, version| + version = with_next_version do |filename| options = ['--buildfile', filename, 'DEBUG=no'] options << '--environment' << Buildr.environment unless Buildr.environment.to_s.empty? sh "#{command} _#{Buildr::VERSION}_ clean upload #{options.join(' ')}" @@ -131,23 +169,55 @@ commit version + '-SNAPSHOT' end - protected - - def command() #:nodoc: - Config::CONFIG['arch'] =~ /dos|win32/i ? $PROGRAM_NAME.ext('cmd') : $PROGRAM_NAME + # :call-seq: + # extract_versions(buildfile) => this_version, next_version + # + # Extract the current and next version numbers from a buildfile. + # Raise an error if not found. + def extract_versions buildfile + begin + this_version = buildfile.scan(THIS_VERSION_PATTERN)[0][2] + rescue + fail 'Looking for THIS_VERSION = "..." in your Buildfile, none found' + end + begin + next_version = buildfile.scan(NEXT_VERSION_PATTERN)[0][1] + rescue + fail 'Looking for NEXT_VERSION = "..." in your Buildfile, none found' + end + [this_version, next_version] 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] + prefix + '/tags' + suffix + '/' + version + 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 end with 'trunk' or 'branches/...'" unless svn_url =~ /(trunk)|(branches.*)$/ - # Status check reveals modified file, but also SVN externals which we can safely ignore. - status = svn('status', '--ignore-externals').reject { |line| line =~ /^X\s/ } - fail "Uncommitted SVN files violate the First Principle Of Release!\n#{status}" unless - status.empty? + 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 + + def command() #:nodoc: + Config::CONFIG['arch'] =~ /dos|win32/i ? $PROGRAM_NAME.ext('cmd') : $PROGRAM_NAME end # :call-seq: @@ -170,7 +240,7 @@ # NEXT_VERSION = 1.2.1 # and the method will return 1.2.0. def with_next_version() - new_filename = Buildr.application.buildfile + '.next' + new_filename = Buildr.application.buildfile.to_s + '.next' modified = change_version do |this_version, next_version| one_after = next_version.split('.') one_after[-1] = one_after[-1].to_i + 1 @@ -179,11 +249,11 @@ File.open(new_filename, 'w') { |file| file.write modified } begin yield new_filename - mv new_filename, Buildr.application.buildfile + mv new_filename, Buildr.application.buildfile.to_s ensure rm new_filename rescue nil end - File.read(Buildr.application.buildfile).scan(THIS_VERSION_PATTERN)[0][1] + extract_versions(File.read(Buildr.application.buildfile.to_s))[0] end # :call-seq: @@ -195,11 +265,8 @@ # This method yields to the block with the current (this) and next version numbers and expects # an array with the new this and next version numbers. def change_version() - buildfile = File.read(Buildr.application.buildfile) - this_version = buildfile.scan(THIS_VERSION_PATTERN)[0][1] or - fail "Looking for THIS_VERSION = \"...\" in your Buildfile, none found" - next_version = buildfile.scan(NEXT_VERSION_PATTERN)[0][1] or - fail "Looking for NEXT_VERSION = \"...\" in your Buildfile, none found" + buildfile = File.read(Buildr.application.buildfile.to_s) + this_version, next_version = extract_versions buildfile this_version, next_version = yield(this_version, next_version) if verbose puts 'Upgrading version numbers:' @@ -215,9 +282,9 @@ # # Tags the current working copy with the release version number. def tag(version) - url = svn_url.sub(/(trunk$)|(branches.*)$/, "tags/#{version}") - svn 'remove', url, '-m', 'Removing old copy' rescue nil - svn 'copy', Dir.pwd, url, '-m', "Release #{version}" + url = tag_url Svn.repo_url, version + Svn.remove url, 'Removing old copy' rescue nil + Svn.copy Dir.pwd, url, "Release #{version}" end # :call-seq: @@ -225,30 +292,15 @@ # # Last, we commit what we currently have in the working copy. def commit(version) - buildfile = File.read(Buildr.application.buildfile). + buildfile = File.read(Buildr.application.buildfile.to_s). gsub(THIS_VERSION_PATTERN) { |ver| ver.sub(/(["']).*\1/, %Q{"#{version}"}) } - File.open(Buildr.application.buildfile, 'w') { |file| file.write buildfile } - svn 'commit', '-m', "Changed version number to #{version}", Buildr.application.buildfile - end - - # :call-seq: - # svn(*args) - # - # Executes SVN command and returns the output. - def svn(*args) - cmd = 'svn ' + args.map { |arg| arg[' '] ? %Q{"#{arg}"} : arg }.join(' ') - info cmd - `#{cmd}`.tap { fail 'SVN command failed' unless $?.exitstatus == 0 } - end - - # Return the current SVN URL - def svn_url - url = svn('info').scan(/URL: (.*)/)[0][0] + File.open(Buildr.application.buildfile.to_s, 'w') { |file| file.write buildfile } + Svn.commit Buildr.application.buildfile.to_s, "Changed version number to #{version}" end end - end + desc 'Make a release' task 'release' do |task| Release.make Modified: incubator/buildr/trunk/spec/build_spec.rb URL: http://svn.apache.org/viewvc/incubator/buildr/trunk/spec/build_spec.rb?rev=689640&r1=689639&r2=689640&view=diff ============================================================================== --- incubator/buildr/trunk/spec/build_spec.rb (original) +++ incubator/buildr/trunk/spec/build_spec.rb Wed Aug 27 15:04:19 2008 @@ -191,3 +191,184 @@ @project.reports.should eql('baz') end end + + +describe Buildr::Release, '#check' do + before do + Buildr::Svn.stub!(:uncommitted_files).and_return('') + end + + it 'should accept to release from the trunk' do + Buildr::Svn.stub!(:repo_url).and_return('http://my.repo.org/foo/trunk') + lambda { Release.check }.should_not raise_error + end + + it 'should accept to release from a branch' do + Buildr::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 to release from a tag' do + Buildr::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 + Buildr::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/...'") + end + + it 'should reject an uncommitted file' do + Buildr::Svn.stub!(:repo_url).and_return('http://my.repo.org/foo/trunk') + Buildr::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") + end +end + + +describe Buildr::Release, '#extract_versions' do + + it 'should extract VERSION_NUMBER and NEXT_VERSION with single quotes' do + buildfile = ["VERSION_NUMBER = '1.0.0-SNAPSHOT'", "NEXT_VERSION = '1.0.1'"].join("\n") + Release.extract_versions(buildfile).should == ['1.0.0-SNAPSHOT', '1.0.1'] + end + + it 'should extract VERSION_NUMBER and NEXT_VERSION with double quotes' do + buildfile = [%{VERSION_NUMBER = "1.0.1-SNAPSHOT"}, %{NEXT_VERSION = "1.0.2"}].join("\n") + Release.extract_versions(buildfile).should == ['1.0.1-SNAPSHOT', '1.0.2'] + end + + it 'should extract VERSION_NUMBER and NEXT_VERSION without any spaces' do + buildfile = ["VERSION_NUMBER='1.0.2-SNAPSHOT'", "NEXT_VERSION='1.0.3'"].join("\n") + Release.extract_versions(buildfile).should == ['1.0.2-SNAPSHOT', '1.0.3'] + end + + it 'should extract THIS_VERSION as an alternative to VERSION_NUMBER' do + buildfile = ["THIS_VERSION = '1.0.3-SNAPSHOT'", "NEXT_VERSION = '1.0.4'"].join("\n") + Release.extract_versions(buildfile).should == ['1.0.3-SNAPSHOT', '1.0.4'] + end + + it 'should complain if no current version number' do + buildfile = "NEXT_VERSION = '1.0.1'" + lambda { Release.extract_versions(buildfile) }.should raise_error('Looking for THIS_VERSION = "..." in your Buildfile, none found') + end + + it 'should complain if no next version number' do + buildfile = "VERSION_NUMBER = '1.0.0-SNAPSHOT'" + lambda { Release.extract_versions(buildfile) }.should raise_error('Looking for NEXT_VERSION = "..." in your Buildfile, none found') + end +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' + end +end + + +# Reference: http://svnbook.red-bean.com/en/1.4/svn.reposadmin.planning.html#svn.reposadmin.projects.chooselayout +describe Buildr::Release, '#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' + 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' + 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' + 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' + end +end + + +describe Buildr::Release, '#with_next_version' do + before do + Buildr.application.stub!(:buildfile).and_return(file('buildfile')) + write 'buildfile', <<-EOF + THIS_VERSION = '1.1.0' + NEXT_VERSION = '1.2.0' + EOF + end + + it 'should yield the name of an updated buildfile' do + Release.send :with_next_version do |new_filename| + File.read(new_filename).should == <<-EOF + THIS_VERSION = "1.2.0" + NEXT_VERSION = "1.2.1" + EOF + end + end + + it 'should yield a name different from the original buildfile' do + Release.send :with_next_version do |new_filename| + new_filename.should_not point_to_path('buildfile') + end + end + + it 'should return the new version number' do + new_version = Release.send(:with_next_version) {} + new_version.should == '1.2.0' + end +end + + +describe Buildr::Release, '#tag' do + before do + Svn.stub!(:repo_url).and_return('http://my.repo.org/foo/trunk') + Svn.stub!(:copy) + end + + it 'should tag the working copy' do + Svn.stub!(:remove) + Svn.should_receive(:copy).with(Dir.pwd, 'http://my.repo.org/foo/tags/1.0.1', 'Release 1.0.1') + Release.send :tag, '1.0.1' + 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, '1.0.1' + end + + it 'should accept that the tag does not exist' do + Svn.stub!(:remove).and_raise(RuntimeError) + Release.send :tag, '1.0.1' + end +end + + +describe Buildr::Release, '#commit' do + before do + write 'buildfile', 'THIS_VERSION = "1.0.0"' + end + + it 'should update the buildfile with the given version number' do + Svn.stub!(:commit) + Release.send :commit, '1.0.1-SNAPSHOT' + file('buildfile').should contain('THIS_VERSION = "1.0.1-SNAPSHOT"') + 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, '1.0.1-SNAPSHOT' + end +end \ No newline at end of file
