rscm 0.4.3 → 0.4.4

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGES CHANGED
@@ -1,5 +1,20 @@
1
1
  = RSCM Changelog
2
2
 
3
+ == 0.4.4
4
+ This release introduces a new test suite that makes it a LOT easier to implement
5
+ new RSCM adapters. The main difference is that implementations no longer have to
6
+ implement the entire API. There are two test suites - one for the full API, and
7
+ one for DamageControl (which is only a small subset of the full API).
8
+
9
+ New RSCM adapters can be written only to pass the DamageControl compatibility suite,
10
+ which requires a much smaller effort than implementing the full API. The DamageControl
11
+ compatibility suite assumes there is an existing repository somewhere, and none of
12
+ the API methods that modify repository state need to be implemented.
13
+
14
+ * Added damagecontrol compatibility suite
15
+ * RSCM::Base.default_options now defaults to {}
16
+ * Fixed bug that would cause a bad commandline when stdout is not specified and a block is passed.
17
+
3
18
  == 0.4.3
4
19
 
5
20
  This release fixes a subtle bug in revision detection for Subversion
data/README CHANGED
@@ -1,4 +1,4 @@
1
- = RSCM - Ruby Source Control Management (0.4.3)
1
+ = RSCM - Ruby Source Control Management (0.4.4)
2
2
 
3
3
  RSCM is to SCM what DBI/JDBC/ODBC are to databases - an SCM-independent API for accessing different SCMs. The high level features are roughly:
4
4
 
@@ -18,14 +18,12 @@ RSCM is available as a RubyGem, and can be installed like this:
18
18
 
19
19
  gem install rscm
20
20
 
21
- (You may need administrator access to do this on a POSIX system).
22
- If you want the latest and greatest, you can get the sources:
21
+ (You may need administrator access to do this on a POSIX system). You can also download prebuilt gems from
22
+ http://rubyforge.org/frs/?group_id=490
23
23
 
24
- Anonymous:
25
- svn co svn://svn.damagecontrol.codehaus.org/damagecontrol/scm/trunk/rscm
24
+ If you want the latest and greatest, you can get the sources from subversion:
26
25
 
27
- Developers:
28
- svn co svn+ssh://developer@beaver.codehaus.org/home/projects/damagecontrol/scm/trunk/rscm
26
+ svn co svn://buildpatterns.com/svn/repos/rscm/trunk
29
27
 
30
28
  == Contributors
31
29
 
@@ -53,8 +51,8 @@ Loads! All of them! How to add support for a new one is described further down i
53
51
 
54
52
  == Related projects
55
53
 
56
- * DamageControl - http://damagecontrol.buildpatterns.com. DamageControl is a Continuous Integration system
57
- built around RSCM and Ruby on Rails.
54
+ * DamageControl - http://dev.buildpatterns.com/trac/wiki/DamageControl (Continuous Integration system
55
+ built on top of RSCM and Ruby on Rails).
58
56
 
59
57
  == Sample usage
60
58
 
@@ -63,28 +61,15 @@ Here is an example of how to use RSCM to get a list of revisions (aka changesets
63
61
  require 'rscm'
64
62
 
65
63
  scm = RSCM::Subversion.new("svn://some.server/some/path/trunk")
66
- scm.default_options = {:stdout => "stdout.log", :stderr => "stderr.log"}
67
64
  # What follows would look the same for any supported SCM
68
65
  revisions = scm.revisions(Time.utc(2004, 11, 10, 12, 34, 22)) # For Subversion, you can also pass a revision number (int)
69
66
  revisions.each do |revision|
70
67
  puts revision # or do something more funky with it
71
68
  end
72
69
 
73
- === Using visitors
74
-
75
- Although the Revisions and Revision classes support external iteration
76
- (with +each+ as in the example above), they also support
77
- visitor traversal via their +accept+ methods. A visitor
78
- must respond to the following methods:
79
-
80
- def visit_revisions(revisions); end
81
- def visit_revision(revision); end
82
- def visit_file(revision_file); end
83
-
84
70
  == Future plans
85
71
 
86
72
  === Cross-SCM synchronisation
87
-
88
73
  RSCM could be used as a tool to migrate files from one SCM to another (of a different type)
89
74
  while keeping the history. -Similar to cvs2svn or http://nautilus.homeip.net/~lele/projects/tailor/
90
75
 
@@ -92,88 +77,113 @@ RSCM could also be used as a continuous synchronisation service between SCMs. Th
92
77
  when you have to work with an SCM and you'd rather use a different one. RSCM could synchronise between
93
78
  the central SCM and one that you set up on your local machine.
94
79
 
95
- = Implementing support for a new SCM
80
+ === SCM browser
81
+ A rails webapp that allows browsing of a repository, using RSCM to access it. -Perhaps even with a simple
82
+ editor allowing people to modify files and commit them via the browser.
96
83
 
97
- We'd be happy to receive contributions for more SCMs. You can always
98
- file a JIRA issue (http://jira.codehaus.org/browse/DC) and hope for someone to implement it for you, or
99
- you can do it yourself. The rest of this file should get you started.
84
+ = Implementing a new RSCM adapter
100
85
 
101
- N.B. IF YOU START IMPLEMENTING A NEW RSCM PLUGIN, PLEASE SUBMIT YOUR CODE
102
- TO JIRA AT AN EARLY STAGE (BEFORE IT'S COMPLETE). THIS WAY IT'S EASIER
103
- FOR EXISTING DEVELOPERS TO PROVIDE TIPS AND HELP UNDERWAY.
86
+ If you want RSCM to support a new SCM, you must implement a subclass of RSCM::Base.
87
+ You should focus on implementing only the features that you need. For example, if
88
+ you plan to use your new RSCM adapter with DamageControl, you only need to implement
89
+ the parts of the API that are used by DamageControl.
104
90
 
105
- Let's write an RSCM implementation for the imaginary SCM called Mooky.
106
- You should be able to use the same recipe for most SCMs.
91
+ We'll see what steps are needed to make an adapter that passes the DamageControl compatibility suite
92
+ (which is part of RSCM). Let's imagine we want DamageControl to be able to work with the imaginary SCM
93
+ called Mooky. The rest of this section explains how to get started. You're going to need a preexisting
94
+ repository with some existing contents, basic knowledge of the SCM's command line tools and some Ruby
95
+ programming skills.
107
96
 
108
- == Writing the API
97
+ == Create the test class and the implementation class
109
98
 
110
- Start by writing a test:
99
+ Start by writing a test that includes the compatibility test suite you're interested in:
111
100
 
112
- test/rscm/mooky/mooky_test.rb
101
+ test/rscm/scm/mooky_test.rb
113
102
 
114
- By including GenericSCMTests your test will automatically include the
115
- acceptance test suite for RSCM. By doing this you'll actually follow a TDD
116
- approach for your new Mooky class - except that the tests are already written
117
- for you!
103
+ With the following content:
118
104
 
119
- IMPORTANT NOTE: If your SCM doesn't provide an easy way to create new local repositories
120
- (such as with StarTeam) you're probably better off writing the tests from scratch and not
121
- include GenericSCMTests. Instead, just make sure you have an SCM repository set up somewhere
122
- and write tests to work against that repository. This way you won't be able to pass the
123
- generic acceptance test suite, and other people (like the RSCM dev team) will probably
124
- not be able to run the tests for it. -But it's better than nothing. We'll happily accept
125
- contributions that don't use the generic tests, although it would be best if they did.
105
+ require 'rscm/test_helper'
126
106
 
127
- OK, back to mooky. As you will see in a minute, the generic test suite will be of great
128
- help when developing the Mooky class. The tests will attempt to check in some sample files
129
- and call various methods on the mooky object to verify that it behaves correctly according
130
- to the RSCM API. (The sample files consist of some java sources, but you don't need Java installed.
131
- They're just files).
107
+ module RSCM
108
+ class MookyTest < Test::Unit::TestCase
109
+ include Compatibility::DamageControlMinimal
110
+ end
111
+ end
132
112
 
133
- Let's implement the Mooky class. Take a look at.
113
+ Now create the implementation class:
134
114
 
135
115
  lib/rscm/scm/mooky.rb
136
116
 
137
- Try running Mooky's test:
117
+ With the following content:
118
+
119
+ require 'rscm/base'
120
+
121
+ module RSCM
122
+ class Cvs < Base
123
+ end
124
+ end
125
+
126
+ Now that we have set up the basics, we can run the tests:
138
127
 
139
128
  rake test TEST=test/rscm/scm/mooky_test.rb
140
-
141
- Whoops - we got some failures! It failed because our checkout method returned
142
- nothing (nil). Let'see if we can get the a little further by implementing this
143
- method.
144
129
 
145
- The Mooky SCM happens to have a command line utility to perform a checkout.
146
- From the command line a checkout with mooky would be done like this:
130
+ It will fail - there is still some setup to do:
147
131
 
148
- cd somewhere
149
- mooky checkout --repo mooky://some/where/else
132
+ == Create testdata directory
133
+ You must create a new directory under test/rscm/compatibility to contain testdata. Give it a name representative
134
+ of the scm type and the contents of the scm. For example, if the existing mooky repository we're going to test
135
+ against contains source code for a chess engine, we could call the directory test/rscm/compatibility/mooky_chess.
150
136
 
151
- Running this command will print the following to standard out:
137
+ Also add an entry in test/rscm/compatibility/config.yml mapping your test class to the testdata directory.
152
138
 
153
- checkout build.xml
154
- checkout project.xml
155
- checkout src/java/com/thoughtworks/damagecontrolled/Thingy.java
156
- checkout src/test/com/thoughtworks/damagecontrolled/ThingyTestCase.java
157
-
158
- What we need to do is to execute these commands from Ruby. We also need to
159
- parse the output from the mooky command to determine the files that were checked out,
160
- so that we can return an array with the file names of the checked out files (the method
161
- should also yield each file name as the execution proceeds).
139
+ The DamageControl compatibility suite expects to find three YAML files in this directory, scm.yml, revisions.yml
140
+ and files_0.yml.
141
+
142
+ === Create scm.yml
143
+ This file should contain a YAML representation of the SCM instance used for testing. The test suite will load it to create an instance of your class. You're free to use whatever properties you want in your SCM implementation, and the YAML file should
144
+ contain the necessary values to connect to the preexisting repository.
145
+
146
+ === Create revisions.yml
147
+ This file should contain a RSCM::Revisions object with two RSCM::Revision objects. You can start off by making a copy of one of the
148
+ existing revisions.yml files, but you should hand-edit this file to represent two revisions in the existing repository.
149
+
150
+ You cannot choose any revision though. There are some constraints that need to be followed:
151
+ * There must be exactly two RSCM::Revision objects
152
+ * The two RSCM::Revision objects must represent to adjacent revisions from the repository
153
+ * Each of the RSCM::Revision objects must contain at least two RSCM::RevisionFile objects
154
+ * The second RSCM::Revision object must contain at least one RSCM::RevisionFile with "ADDED" state
155
+
156
+ Given these constraints you should spend some time locating two revisions that follow these constraints.
157
+ This would be a good time to familiarize yourself with the SCM's command line tool (or whatever kind of tool
158
+ the SCM provides to access it).
162
159
 
163
- Once your checkout command works okay, the test will get you a little further. Just keep
164
- on going until all tests pass.
160
+ ==== Special note for non-transactional SCMs
165
161
 
166
- NOTE: If the SCM doesn't have a command line utility (unlikely) or a 3rd party Ruby API, but instead
167
- provides libraries (perhaps in C), then you should consider writing a Ruby C extension
168
- instead.
162
+ Non-transactional SCMs usually use dates (and not revision identifiers) to report changes. Most SCMs report changes to files
163
+ (which will become RSCM::RevisionFile instances) in some sort of log. These changes will typically not be logically
164
+ grouped. RSCM::Revisions.add will group revision files that have:
165
+ * similar modification time (max 1 minute apart)
166
+ * the same commit message
167
+ * the same developer
169
168
 
170
- If the SCM has a Java interface, you can take the same approach as for StarTeam. There are
171
- Java classes for Revisions that allow easy interaction between Ruby and Java over YAML.
172
- You can reuse these classes for other Java based SCMs (if there are any, I don't know).
169
+ So when mining for revisions that follow the constraints for revisions.yml, you should also be looking for groupings
170
+ in modification time, commit message and developer.
171
+
172
+ === Create files_0.yml
173
+ This file should contain the files that will be in the working copy after a checkout of the 1st revision in revisions.yml,
174
+ sorted by their path.
175
+
176
+ == Implement the methods
177
+ Now that we have set up everything needed for the tests, we can run the tests again:
178
+
179
+ rake test TEST=test/rscm/scm/mooky_test.rb
173
180
 
174
- == Web interface (DamageControl only)
181
+ Now you should get errors about methods not being implemented. At this point you should start implementing the methods.
175
182
 
176
- DamageControl automatically detects new SCM classes in RSCM and generates a default web interface.
183
+ == Implementation tips
184
+ * Run the tests often. Let the error messages guide you in what you do next.
185
+ * Use the execute method to invoke command line tools.
186
+ * Split the implementation into two classes. One class for parsing logs and one for translating API calls to command line executions. This will allow you to test the log parsing against hard coded logs in your tests.
177
187
 
178
188
  = Building RSCM
179
189
  This section is for developers who are new to ruby development and do not already know how to build and install Ruby gems.
@@ -189,6 +199,6 @@ Now change to the RSCM root directory and type
189
199
 
190
200
  This will create a gem for RSCM. To install this gem, you have to change to the pkg directory and type
191
201
 
192
- sudo gem install pkg/rscm-0.3.11.gem
202
+ sudo gem install pkg/rscm-0.4.X.gem
193
203
 
194
204
  Now you can use RSCM in other Ruby apps with a simple require 'rscm'.
data/Rakefile CHANGED
@@ -6,7 +6,6 @@ require 'rake/packagetask'
6
6
  require 'rake/gempackagetask'
7
7
  require 'rake/contrib/sshpublisher'
8
8
  require 'rake/contrib/rubyforgepublisher'
9
- require 'meta_project'
10
9
  require 'lib/rscm/version'
11
10
 
12
11
  PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : ''
@@ -26,6 +25,8 @@ fl.exclude('test/**/mooky*.rb')
26
25
  fl.exclude('test/**/monotone*.rb') # Incomplete/unsupported for now - reactivate when more complete!
27
26
  fl.exclude('test/**/clearcase*.rb') # Incomplete/unsupported for now - reactivate when more complete!
28
27
  fl.exclude('test/**/darcs*.rb') # Incomplete/unsupported for now - reactivate when more complete!
28
+ #fl.exclude('test/**/cvs*.rb') # Incomplete/unsupported for now - reactivate when more complete!
29
+ #fl.exclude('test/**/subversion*.rb') # Incomplete/unsupported for now - reactivate when more complete!
29
30
  fl.exclude('test/**/perforce*.rb') # Incomplete/unsupported for now - reactivate when more complete!
30
31
  fl.exclude('test/**/p4client*.rb') # Incomplete/unsupported for now - reactivate when more complete!
31
32
  fl.exclude('test/**/starteam*.rb') # Too bloody hard to test without a StarTeam server license! Tested ad-hoc.
@@ -124,6 +125,7 @@ end
124
125
 
125
126
  desc "Release files on RubyForge"
126
127
  task :release_files => [:gem] do
128
+ require 'meta_project'
127
129
  release_files = FileList[
128
130
  "pkg/#{PKG_FILE_NAME}.gem"
129
131
  ]
@@ -140,12 +142,14 @@ end
140
142
 
141
143
  desc "Publish docs/website"
142
144
  task :publish_doc => [:rdoc] do
145
+ require 'meta_project'
143
146
  publisher = Rake::RubyForgePublisher.new(PKG_NAME, ENV['RUBYFORGE_USER'])
144
147
  publisher.upload
145
148
  end
146
149
 
147
150
  desc "Publish news on RubyForge"
148
151
  task :publish_news => [:gem] do
152
+ require 'meta_project'
149
153
  release_files = FileList[
150
154
  "pkg/#{PKG_FILE_NAME}.gem"
151
155
  ]
@@ -28,7 +28,10 @@ module RSCM
28
28
  #
29
29
  class Base
30
30
 
31
- attr_accessor :default_options
31
+ attr_writer :default_options
32
+ def default_options
33
+ @default_options ||= {:stdout=>'stdout.log', :stderr=>'stderr.log'}
34
+ end
32
35
 
33
36
  # Transforms +raw_identifier+ into the native rype used for revisions.
34
37
  def to_identifier(raw_identifier)
@@ -54,12 +57,12 @@ module RSCM
54
57
  end
55
58
 
56
59
  # Destroys the working copy
57
- def destroy_working_copy(options={})
60
+ def destroy_working_copy
58
61
  FileUtils.rm_rf(checkout_dir) unless checkout_dir.nil?
59
62
  end
60
63
 
61
64
  # Whether or not the SCM represented by this instance exists.
62
- def central_exists?(options={})
65
+ def central_exists?
63
66
  # The default implementation assumes yes - override if it can be
64
67
  # determined programmatically.
65
68
  true
@@ -139,30 +142,28 @@ module RSCM
139
142
  # the checkout_silent method instead of this method (should be protected).
140
143
  #
141
144
  def checkout(to_identifier=Time.infinity, options={}) # :yield: file
142
- to_identifier=Time.infinity if to_identifier.nil?
143
-
144
- # the OS doesn't store file timestamps with fractions.
145
- before_checkout_time = Time.now.utc - 1
145
+ to_identifier = Time.infinity if to_identifier.nil?
146
146
 
147
+ before = checked_out_files
147
148
  # We expect subclasses to implement this as a protected method (unless this whole method is overridden).
148
149
  checkout_silent(to_identifier, options)
150
+ after = checked_out_files
151
+
152
+ (after - before).sort!
153
+ end
154
+
155
+ def checked_out_files
149
156
  files = Dir["#{@checkout_dir}/**/*"]
150
- added = []
151
- files.each do |file|
152
- added << file if File.mtime(file).utc > before_checkout_time
153
- end
157
+ files.delete_if{|file| File.directory?(file)}
154
158
  ignore_paths.each do |regex|
155
- added.delete_if{|path| path =~ regex}
159
+ files.delete_if{|file| file =~ regex}
156
160
  end
157
- added_file_paths = added.find_all do |path|
158
- File.file?(path)
159
- end
160
- relative_added_file_paths = to_relative(checkout_dir, added_file_paths)
161
- relative_added_file_paths
161
+ dir = File.expand_path(@checkout_dir)
162
+ files.collect{|file| File.expand_path(file)[dir.length+1..-1]}
162
163
  end
163
164
 
164
- # Returns a Revisions object for the period specified by +from_identifier+ (exclusive, i.e. after)
165
- # and +to_identifier+ (inclusive). If +relative_path+ is specified, the result will only contain
165
+ # Returns a Revisions object for the interval specified by +from_identifier+ (exclusive, i.e. after)
166
+ # and optionally +:to_identifier+ (inclusive). If +relative_path+ is specified, the result will only contain
166
167
  # revisions pertaining to that path.
167
168
  #
168
169
  def revisions(from_identifier, options={})
@@ -276,13 +277,5 @@ module RSCM
276
277
  end
277
278
  end
278
279
 
279
- # Takes an array of +absolute_paths+ and turns it into an array
280
- # of paths relative to +dir+
281
- #
282
- def to_relative(dir, absolute_paths)
283
- dir = File.expand_path(dir)
284
- absolute_paths.collect{|p| File.expand_path(p)[dir.length+1..-1]}
285
- end
286
-
287
280
  end
288
281
  end
@@ -64,7 +64,7 @@ module RSCM
64
64
  Dir.chdir(options[:dir]) do
65
65
  stdout_opt = options[:stdout] ? ">> #{options[:stdout]}" : ""
66
66
  stderr_opt = options[:stderr] ? "2>> #{options[:stderr]}" : ""
67
- capture_info_command = block_given? ? "echo [output captured and therefore not logged] >> #{options[:stdout]} && " : ""
67
+ capture_info_command = (block_given? && options[:stdout])? "echo [output captured and therefore not logged] >> #{options[:stdout]} && " : ""
68
68
 
69
69
  full_cmd = commands.collect do |c|
70
70
  escaped_command = c.gsub(/"/, "\\\"").gsub(/</, "\\<")
@@ -93,7 +93,7 @@ module RSCM
93
93
  if options[:stderr] && File.exist?(options[:stderr])
94
94
  File.open(options[:stderr]) do |errio|
95
95
  begin
96
- errio.seek(-800, IO::SEEK_END)
96
+ errio.seek(-1200, IO::SEEK_END)
97
97
  rescue Errno::EINVAL
98
98
  # ignore - it just means we didn't have 400 bytes.
99
99
  end
@@ -6,25 +6,36 @@ module RSCM
6
6
  module Difftool
7
7
  # assertion method that reports differences as diff.
8
8
  # useful when comparing big strings
9
- def assert_equal_with_diff(expected, actual, temp_basedir=File.dirname(__FILE__) + "/../../target")
10
- diff(expected, actual, temp_basedir) do |diff_io|
9
+ def assert_equal_with_diff(expected, actual, message="", temp_basedir=File.dirname(__FILE__) + "/../../target")
10
+ diff(expected, actual, temp_basedir) do |diff_io, cmd|
11
11
  diff_string = diff_io.read
12
- assert_equal("", diff_string, diff_string)
12
+ if(diff_string.strip != "")
13
+ flunk "#{message}\nThere were differences\ndiff command: #{cmd}\ndiff:\n#{diff_string}"
14
+ end
13
15
  end
14
16
  end
15
17
  module_function :assert_equal_with_diff
16
18
 
17
19
  def diff(expected, actual, temp_basedir, &block)
18
20
  dir = RSCM.new_temp_dir("diff", temp_basedir)
19
-
20
- expected_file = "#{dir}/expected"
21
+
22
+ expected_file = nil
23
+ if(File.exist?(expected))
24
+ expected_file = expected
25
+ else
26
+ expected_file = "#{dir}/expected"
27
+ File.open(expected_file, "w") {|io| io.write(expected)}
28
+ end
29
+
21
30
  actual_file = "#{dir}/actual"
22
- File.open(expected_file, "w") {|io| io.write(expected)}
23
31
  File.open(actual_file, "w") {|io| io.write(actual)}
24
32
 
25
33
  difftool = WINDOWS ? File.dirname(__FILE__) + "/../../bin/diff.exe" : "diff"
26
- IO.popen("#{difftool} #{RSCM::PathConverter.filepath_to_nativepath(expected_file, false)} #{RSCM::PathConverter.filepath_to_nativepath(actual_file, false)}") do |io|
27
- yield io
34
+ e = RSCM::PathConverter.filepath_to_nativepath(expected_file, false)
35
+ a = RSCM::PathConverter.filepath_to_nativepath(actual_file, false)
36
+ cmd = "#{difftool} --strip-trailing-cr #{e} #{a}"
37
+ IO.popen(cmd) do |io|
38
+ yield io, cmd
28
39
  end
29
40
  end
30
41
  module_function :diff