rscm 0.3.16 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/CHANGES CHANGED
@@ -1,5 +1,19 @@
1
1
  = RSCM Changelog
2
2
 
3
+ == Version 0.4.0
4
+
5
+ This release of RSCM modifies mosts API methods to take an options Hash (or alternatively, setting
6
+ the default_options attribute), allowing better capturing of the underlying SCM's IO (stdout/stderr)
7
+
8
+ * Introduced named parameters for several API methods.
9
+ * Added RSCM::VERSION
10
+ * Fixed incorrect arguments to poll_new_revisions in revision_poller.rb
11
+ * Added Base.to_identifier(raw_identifier) for type conversion to the native revision type.
12
+ * Removed support for directory listings (No use case for it anymore, keep things simpler)
13
+ * checkout no longer yields new files, only returns an array (primarily used for testing)
14
+ * Better.popen removed in favour of RSCM::CommandLine.
15
+ * Stdout and stderr logs are now written to disk.
16
+
3
17
  == Version 0.3.16
4
18
 
5
19
  Bugfix release
data/README CHANGED
@@ -1,4 +1,4 @@
1
- = RSCM - Ruby Source Control Management (0.3.16)
1
+ = RSCM - Ruby Source Control Management (0.4.0)
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
 
@@ -8,8 +8,9 @@ RSCM is to SCM what DBI/JDBC/ODBC are to databases - an SCM-independent API for
8
8
  * Add and commit files
9
9
  * Manipluate triggers
10
10
 
11
- Although RSCM's main focus is operations on a working copy of an SCM repository, the API also allows some level of interaction
12
- with the SCM repository itself, like creating new repositories.
11
+ Although RSCM's main focus is operations on a working copy of an SCM repository,
12
+ the API also allows some level of interaction with the SCM repository itself,
13
+ like creating new repositories.
13
14
 
14
15
  == Download
15
16
 
@@ -17,10 +18,14 @@ RSCM is available as a RubyGem, and can be installed like this:
17
18
 
18
19
  gem install rscm
19
20
 
20
- (You may need administer access to do this on a POSIX system).
21
- If you want the latest and greatest, you can get the sources, which live alongside DamageControl's sources:
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:
22
23
 
23
- * See the DamageControl Developer Guide at http://hieraki.lavalamp.ca/
24
+ Anonymous:
25
+ svn co svn://svn.damagecontrol.codehaus.org/damagecontrol/scm/trunk/rscm
26
+
27
+ Developers:
28
+ svn co svn+ssh://developer@beaver.codehaus.org/home/projects/damagecontrol/scm/trunk/rscm
24
29
 
25
30
  == Contributors
26
31
 
@@ -33,10 +38,10 @@ If you want the latest and greatest, you can get the sources, which live alongsi
33
38
 
34
39
  * CVS - http://www.nongnu.org/cvs (stable)
35
40
  * Subversion - http://subversion.tigris.org (stable)
36
- * ClearCase - http://www-306.ibm.com/software/awdtools/clearcase (not thoroughly tested)
37
41
 
38
42
  In progress:
39
43
 
44
+ * ClearCase - http://www-306.ibm.com/software/awdtools/clearcase (not thoroughly tested)
40
45
  * Darcs - http://www.abridgegame.org/darcs (very incomplete)
41
46
  * Monotone - http://www.venge.net/monotone (half complete)
42
47
  * Perforce - http://www.perforce.com (nearly complete - a little out of date with recent API)
@@ -48,7 +53,8 @@ Loads! All of them! How to add support for a new one is described further down i
48
53
 
49
54
  == Related projects
50
55
 
51
- * DamageControl - http://damagecontrol.codehaus.org. DamageControl adds a web interface to RSCM and tons of other features for continuous integration.
56
+ * DamageControl - http://damagecontrol.buildpatterns.com. DamageControl is a Continuous Integration system
57
+ built around RSCM and Ruby on Rails.
52
58
 
53
59
  == Sample usage
54
60
 
@@ -57,6 +63,7 @@ Here is an example of how to use RSCM to get a list of revisions (aka changesets
57
63
  require 'rscm'
58
64
 
59
65
  scm = RSCM::Subversion.new("svn://some.server/some/path/trunk")
66
+ scm.default_options = {:stdout => "stdout.log", :stderr => "stderr.log"}
60
67
  # What follows would look the same for any supported SCM
61
68
  revisions = scm.revisions(Time.utc(2004, 11, 10, 12, 34, 22)) # For Subversion, you can also pass a revision number (int)
62
69
  revisions.each do |revision|
data/Rakefile CHANGED
@@ -7,10 +7,11 @@ require 'rake/gempackagetask'
7
7
  require 'rake/contrib/sshpublisher'
8
8
  require 'rake/contrib/rubyforgepublisher'
9
9
  require 'meta_project'
10
+ require 'lib/rscm/version'
10
11
 
11
12
  PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : ''
12
13
  PKG_NAME = 'rscm'
13
- PKG_VERSION = '0.3.16' + PKG_BUILD
14
+ PKG_VERSION = RSCM::VERSION::STRING + PKG_BUILD
14
15
  PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
15
16
 
16
17
  desc "Default Task"
@@ -32,6 +33,9 @@ Rake::TestTask.new { |t|
32
33
  t.libs << "test"
33
34
  t.test_files = fl
34
35
  t.verbose = true
36
+
37
+ # turn on code coverage
38
+ #t.ruby_opts << "-rcoverage/coverage"
35
39
  }
36
40
 
37
41
  rd = Rake::RDocTask.new { |rdoc|
data/lib/rscm.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  require 'rscm/path_converter'
2
2
  require 'rscm/difftool'
3
- require 'rscm/better'
3
+ require 'rscm/platform'
4
+ require 'rscm/command_line'
4
5
  require 'rscm/base'
5
6
  require 'rscm/revision'
6
7
  require 'rscm/revision_poller'
data/lib/rscm/base.rb CHANGED
@@ -3,48 +3,20 @@ require 'rscm/revision'
3
3
  require 'rscm/path_converter'
4
4
 
5
5
  module RSCM
6
- # This class defines the RSCM API. The documentation of the various methods
7
- # uses CVS and Subversion's terminology. (For example, checkout means 'get working copy',
8
- # not 'lock for private edit' as in ClearCase or VSS terminology).
6
+ # This class defines the RSCM API, which offers access to an SCM working copy
7
+ # as well as a 'central' repository.
9
8
  #
10
- # Concrete subclasses of this class provide an API to manage a local working copy
11
- # as well as an associated 'central' repository. The main responsibility is working
12
- # copy operations:
9
+ # Concrete subclasses of this class (concrete adapters) implement the integration
10
+ # with the respective SCMs.
13
11
  #
14
- # * add
15
- # * checkout
16
- # * checked_out?
17
- # * diff
18
- # * edit
19
- # * ls
20
- # * move
21
- # * revisions
22
- # * uptodate?
23
- # * file
24
- # * destroy_working_copy
12
+ # Most of the methods take an optional +options+ Hash (named parameters), allowing
13
+ # the following options:
25
14
  #
26
- # In addition to operations related to working copies, the same instance should provide
27
- # methods to administer the working copy's associated 'central' repository. These are:
15
+ # * <tt>:stdout</tt>: Path to file name where stdout of SCM operations are written.
16
+ # * <tt>:stdout</tt>: Path to file name where stderr of SCM operations are written.
28
17
  #
29
- # * central_exists?
30
- # * create_central
31
- # * can_create_central?
32
- # * import_central
33
- # * install_trigger
34
- # * supports_trigger? / can_install_trigger?
35
- # * trigger_installed?
36
- # * trigger_mechanism
37
- # * uninstall_trigger
38
- #
39
- # Some methods are a bit fuzzy with respect to their relevance to the working copy or
40
- # the associated central repository, as it depends on the nature of the individual underlying
41
- # SCMs. These methods are:
42
- #
43
- # * checkout_command_line
44
- # * label
45
- # * name
46
- # * transactional? / atomic?
47
- # * update_command_line
18
+ # In stead of specifying the +options+ parameters for every API method, it's possible
19
+ # to assign default options via the +default_options+ attribute.
48
20
  #
49
21
  # Some of the methods in this API use +from_identifier+ and +to_identifier+.
50
22
  # These identifiers can be either a UTC Time (according to the SCM's clock)
@@ -54,29 +26,30 @@ module RSCM
54
26
  # If +from_identifier+ or +to_identifier+ are +nil+ they should respectively default to
55
27
  # Time.epoch or Time.infinite.
56
28
  #
57
- # TODO: rename this superclass to 'Base'
58
- #
59
29
  class Base
60
30
 
61
- # TODO: Make revisions yield revisions as they are determined, to avoid
62
- # having to load them all into memory before the method exits. Careful not to
63
- # use yielded revisions to do another scm hit - like get diffs. Some SCMs
64
- # might dead lock on this. Implement a guard for that.
65
- # TODO: Add some visitor support here too?
66
-
67
- public
31
+ attr_accessor :default_options
68
32
 
33
+ # Transforms +raw_identifier+ into the native rype used for revisions.
34
+ def to_identifier(raw_identifier)
35
+ raw_identifier.to_s
36
+ end
37
+
38
+ # Sets the checkout dir (working copy). Should be set prior to most other method
39
+ # invocations (depending on the implementation).
69
40
  def checkout_dir=(dir)
70
41
  @checkout_dir = PathConverter.filepath_to_nativepath(dir, false)
71
42
  end
72
43
 
44
+ # Gets the working copy directory.
73
45
  def checkout_dir
74
46
  @checkout_dir
75
47
  end
76
48
 
77
- def to_yaml_properties
49
+ def to_yaml_properties #:nodoc:
78
50
  props = instance_variables
79
51
  props.delete("@checkout_dir")
52
+ props.delete("@default_options")
80
53
  props.sort!
81
54
  end
82
55
 
@@ -85,8 +58,7 @@ module RSCM
85
58
  FileUtils.rm_rf(checkout_dir) unless checkout_dir.nil?
86
59
  end
87
60
 
88
- # Whether the physical SCM represented by this instance exists.
89
- #
61
+ # Whether or not the SCM represented by this instance exists.
90
62
  def central_exists?
91
63
  # The default implementation assumes yes - override if it can be
92
64
  # determined programmatically.
@@ -94,7 +66,6 @@ module RSCM
94
66
  end
95
67
 
96
68
  # Whether or not this SCM is transactional (atomic).
97
- #
98
69
  def transactional?
99
70
  false
100
71
  end
@@ -103,12 +74,12 @@ module RSCM
103
74
  # Creates a new 'central' repository. This is intended only for creation of 'central'
104
75
  # repositories (not for working copies). You shouldn't have to call this method if a central repository
105
76
  # already exists. This method is used primarily for testing of RSCM, but can also
106
- # be used if you *really* want to create a central repository.
77
+ # be used if you *really* want to use RSCM to create a central repository.
107
78
  #
108
79
  # This method should throw an exception if the repository cannot be created (for
109
80
  # example if the repository is 'remote' or if it already exists).
110
81
  #
111
- def create_central
82
+ def create_central(options={})
112
83
  raise NotImplementedError
113
84
  end
114
85
 
@@ -125,28 +96,29 @@ module RSCM
125
96
  end
126
97
 
127
98
  # Adds +relative_filename+ to the working copy.
128
- def add(relative_filename)
99
+ def add(relative_filename, options={})
129
100
  raise NotImplementedError
130
101
  end
131
102
 
132
103
  # Schedules a move of +relative_src+ to +relative_dest+
133
104
  # Should not take effect in the central repository until
134
105
  # +commit+ is invoked.
135
- def move(relative_src, relative_dest)
106
+ def move(relative_src, relative_dest, options={})
136
107
  raise NotImplementedError
137
108
  end
138
109
 
139
- # Recursively imports files from a +dir+ into the central scm
140
- def import_central(dir, message)
141
- raise "Not implemented"
110
+ # Recursively imports files from <tt>:dir</tt> into the central scm,
111
+ # using commit message <tt>:message</tt>
112
+ def import_central(options)
113
+ raise NotImplementedError
142
114
  end
143
115
 
144
116
  # Open a file for edit - required by scms that check out files in read-only mode e.g. perforce
145
- def edit(file)
117
+ def edit(file, options={})
146
118
  end
147
119
 
148
120
  # Commit (check in) modified files.
149
- def commit(message)
121
+ def commit(message, options={})
150
122
  raise NotImplementedError
151
123
  end
152
124
 
@@ -166,12 +138,14 @@ module RSCM
166
138
  # For some SCMs this is not possible, or at least very hard. In that case, just override
167
139
  # the checkout_silent method instead of this method (should be protected).
168
140
  #
169
- def checkout(to_identifier=Time.infinity) # :yield: file
141
+ def checkout(to_identifier=Time.infinity, options={}) # :yield: file
142
+ to_identifier=Time.infinity if to_identifier.nil?
143
+
170
144
  # the OS doesn't store file timestamps with fractions.
171
145
  before_checkout_time = Time.now.utc - 1
172
146
 
173
147
  # We expect subclasses to implement this as a protected method (unless this whole method is overridden).
174
- checkout_silent(to_identifier)
148
+ checkout_silent(to_identifier, options)
175
149
  files = Dir["#{@checkout_dir}/**/*"]
176
150
  added = []
177
151
  files.each do |file|
@@ -184,9 +158,6 @@ module RSCM
184
158
  File.file?(path)
185
159
  end
186
160
  relative_added_file_paths = to_relative(checkout_dir, added_file_paths)
187
- relative_added_file_paths.each do |path|
188
- yield path if block_given?
189
- end
190
161
  relative_added_file_paths
191
162
  end
192
163
 
@@ -194,7 +165,7 @@ module RSCM
194
165
  # and +to_identifier+ (inclusive). If +relative_path+ is specified, the result will only contain
195
166
  # revisions pertaining to that path.
196
167
  #
197
- def revisions(from_identifier, to_identifier=Time.infinity, relative_path=nil)
168
+ def revisions(from_identifier, options={})
198
169
  raise NotImplementedError
199
170
  end
200
171
 
@@ -208,11 +179,6 @@ module RSCM
208
179
  HistoricFile.new(relative_path, dir, self)
209
180
  end
210
181
 
211
- # Returns an Array of the children under +relative_path+
212
- def ls(relative_path)
213
- raise NotImplementedError
214
- end
215
-
216
182
  # Opens a revision_file
217
183
  def open(revision_file, &block) #:yield: io
218
184
  raise NotImplementedError
@@ -222,12 +188,8 @@ module RSCM
222
188
  # repository's revision/time identified by +identifier+.
223
189
  # If +identifier+ is nil, 'HEAD' of repository should be assumed.
224
190
  #
225
- # TODO: rename to in_synch?
226
191
  def uptodate?(identifier)
227
- # Suboptimal algorithm that works for all SCMs.
228
- # Subclasses can override this to improve efficiency.
229
-
230
- revisions(identifier).empty?
192
+ raise NotImplementedError
231
193
  end
232
194
 
233
195
  # Whether the project is checked out from the central repository or not.
@@ -300,6 +262,19 @@ module RSCM
300
262
  end
301
263
 
302
264
  protected
265
+
266
+ # Wrapper for CommandLine.execute that provides default values for
267
+ # dir plus any options set in default_options (typically stdout and stderr).
268
+ def execute(cmd, options={}, &proc)
269
+ default_dir = @checkout_dir.nil? ? Dir.pwd : @checkout_dir
270
+ options = {:dir => default_dir}.merge(default_options).merge(options)
271
+ begin
272
+ CommandLine.execute(cmd, options, &proc)
273
+ rescue CommandLine::OptionError => e
274
+ e.message += "\nEither specify default_options on the scm object, or pass the required options to the method"
275
+ raise e
276
+ end
277
+ end
303
278
 
304
279
  # Takes an array of +absolute_paths+ and turns it into an array
305
280
  # of paths relative to +dir+
@@ -0,0 +1,66 @@
1
+ module RSCM
2
+ # Utility for running a +cmd+ in a +dir+ with a specified +env+.
3
+ # If a block is passed, the standard out stream is passed to that block (and returns)
4
+ # the result from the block. Otherwise, if a block is not passed, standard output
5
+ # is redirected to +stdout_file+. The standard error stream is always redirected
6
+ # to +stderr_file+. Note that both +stdout_file+ and +stderr_file+ must always
7
+ # be specified with non-nil values, as both of them will always have the command lines
8
+ # written to them.
9
+ module CommandLine
10
+ class OptionError < StandardError; end
11
+ class ExecutionError < StandardError
12
+ attr_reader :cmd, :dir, :exitstatus, :stderr
13
+ def initialize(cmd, dir, exitstatus, stderr); @cmd, @dir, @exitstatus, @stderr = cmd, dir, exitstatus, stderr; end
14
+ def to_s
15
+ "\ndir : #{@dir}\n" +
16
+ "command : #{@cmd}\n" +
17
+ "exitstatus: #{@exitstatus}\n" +
18
+ "stderr : #{@stderr}\n"
19
+ end
20
+ end
21
+
22
+ def execute(cmd, options={}, &proc)
23
+ options = {
24
+ :dir => Dir.pwd,
25
+ :env => {},
26
+ :exitstatus => 0
27
+ }.merge(options)
28
+
29
+ raise OptionError.new(":stdout can't be nil") if options[:stdout].nil?
30
+ raise OptionError.new(":stderr can't be nil") if options[:stderr].nil?
31
+ options[:stdout] = File.expand_path(options[:stdout])
32
+ options[:stderr] = File.expand_path(options[:stderr])
33
+
34
+ commands = cmd.split("&&").collect{|c| c.strip}
35
+ Dir.chdir(options[:dir]) do
36
+ redirected_cmd = commands.collect do |c|
37
+ redirection = block_given? ? "#{c} 2>> #{options[:stderr]}" : "#{c} >> #{options[:stdout]} 2>> #{options[:stderr]}"
38
+
39
+ "echo #{RSCM::Platform.prompt} #{c} >> #{options[:stdout]} && " +
40
+ "echo #{RSCM::Platform.prompt} #{c} >> #{options[:stderr]} && " +
41
+ redirection
42
+ end.join(" && ")
43
+
44
+ options[:env].each{|k,v| ENV[k]=v}
45
+ begin
46
+ IO.popen(redirected_cmd) do |io|
47
+ if(block_given?)
48
+ return(proc.call(io))
49
+ else
50
+ io.read
51
+ end
52
+ end
53
+ rescue Errno::ENOENT => e
54
+ File.open(options[:stderr], "a") {|io| io.write(e.message)}
55
+ ensure
56
+ if($?.exitstatus != options[:exitstatus])
57
+ error_message = File.exist?(options[:stderr]) ? File.read(options[:stderr]) : "#{options[:stderr]} doesn't exist"
58
+ raise ExecutionError.new(cmd, options[:dir], $?.exitstatus, error_message)
59
+ end
60
+ end
61
+ end
62
+ $?.exitstatus
63
+ end
64
+ module_function :execute
65
+ end
66
+ end
@@ -13,8 +13,8 @@ module RSCM
13
13
  end
14
14
 
15
15
  # Returns an Array of RevisionFile - from Time.epoch until Time.infinity (now)
16
- def revision_files
17
- @scm.revisions(Time.epoch, Time.infinity, @relative_path).collect do |revision|
16
+ def revision_files(options={})
17
+ @scm.revisions(Time.epoch, options.dup.merge({:to_identifier => Time.infinity, :relative_path => @relative_path})).collect do |revision|
18
18
  if revision.files.length != 1
19
19
  files_s = revision.files.collect{|f| f.to_s}.join("\n")
20
20
  raise "The file-specific revision didn't have exactly one file, but #{revision.files.length}:\n#{files_s}"
@@ -26,9 +26,5 @@ module RSCM
26
26
  end
27
27
  end
28
28
 
29
- def children
30
- raise "Not a directory" unless directory?
31
- @scm.ls(@relative_path)
32
- end
33
29
  end
34
30
  end