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 +14 -0
- data/README +15 -8
- data/Rakefile +5 -1
- data/lib/rscm.rb +2 -1
- data/lib/rscm/base.rb +51 -76
- data/lib/rscm/command_line.rb +66 -0
- data/lib/rscm/historic_file.rb +2 -6
- data/lib/rscm/path_converter.rb +1 -15
- data/lib/rscm/platform.rb +25 -0
- data/lib/rscm/revision_file.rb +4 -4
- data/lib/rscm/revision_poller.rb +23 -20
- data/lib/rscm/scm/cvs.rb +108 -136
- data/lib/rscm/scm/cvs_log_parser.rb +2 -8
- data/lib/rscm/scm/monotone.rb +1 -1
- data/lib/rscm/scm/subversion.rb +75 -102
- data/lib/rscm/time_ext.rb +2 -1
- data/lib/rscm/version.rb +13 -0
- data/test/rscm/generic_scm_tests.rb +38 -49
- data/test/rscm/scm/cvs_test.rb +0 -25
- metadata +5 -3
- data/lib/rscm/better.rb +0 -12
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.
|
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,
|
12
|
-
with the SCM repository itself,
|
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
|
21
|
-
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).
|
22
|
+
If you want the latest and greatest, you can get the sources:
|
22
23
|
|
23
|
-
|
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.
|
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 =
|
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
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
|
7
|
-
#
|
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
|
11
|
-
#
|
12
|
-
# copy operations:
|
9
|
+
# Concrete subclasses of this class (concrete adapters) implement the integration
|
10
|
+
# with the respective SCMs.
|
13
11
|
#
|
14
|
-
#
|
15
|
-
#
|
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
|
-
#
|
27
|
-
#
|
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
|
-
#
|
30
|
-
#
|
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
|
-
|
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
|
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
|
140
|
-
|
141
|
-
|
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,
|
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
|
-
|
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
|
data/lib/rscm/historic_file.rb
CHANGED
@@ -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
|