rscm 0.4.5 → 0.5.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.
Files changed (55) hide show
  1. data/CHANGES +12 -0
  2. data/README +14 -0
  3. data/Rakefile +4 -24
  4. data/lib/rscm.rb +1 -2
  5. data/lib/rscm/base.rb +289 -281
  6. data/lib/rscm/command_line.rb +135 -112
  7. data/lib/rscm/revision.rb +63 -166
  8. data/lib/rscm/revision_file.rb +8 -2
  9. data/lib/rscm/revision_poller.rb +78 -67
  10. data/lib/rscm/revisions.rb +79 -0
  11. data/lib/rscm/scm/clearcase.rb +11 -9
  12. data/lib/rscm/scm/cvs.rb +374 -352
  13. data/lib/rscm/scm/cvs_log_parser.rb +1 -0
  14. data/lib/rscm/scm/darcs.rb +9 -0
  15. data/lib/rscm/scm/perforce.rb +216 -149
  16. data/lib/rscm/scm/subversion.rb +44 -24
  17. data/lib/rscm/scm/subversion_log_parser.rb +37 -51
  18. data/lib/rscm/time_ext.rb +0 -1
  19. data/lib/rscm/version.rb +2 -2
  20. data/test/rscm/command_line_test.rb +7 -5
  21. data/test/rscm/compatibility/config.yml +4 -4
  22. data/test/rscm/compatibility/cvs_metaproject/diff.txt +52 -0
  23. data/test/rscm/compatibility/cvs_metaproject/file.txt +48 -0
  24. data/test/rscm/compatibility/cvs_metaproject/old.yml +13 -0
  25. data/test/rscm/compatibility/full.rb +2 -223
  26. data/test/rscm/compatibility/p4_gfx/files_0.yml +10 -0
  27. data/test/rscm/compatibility/p4_gfx/old.yml +26 -0
  28. data/test/rscm/compatibility/p4_gfx/revisions.yml +24 -0
  29. data/test/rscm/compatibility/p4_gfx/scm.yml +4 -0
  30. data/test/rscm/compatibility/rscm_engine.rb +197 -0
  31. data/test/rscm/compatibility/subversion_rscm/diff.txt +12 -0
  32. data/test/rscm/compatibility/subversion_rscm/file.txt +567 -0
  33. data/test/rscm/compatibility/subversion_rscm/old.yml +14 -0
  34. data/test/rscm/compatibility/subversion_rscm/revisions.yml +17 -0
  35. data/test/rscm/compatibility/subversion_rscm/scm.yml +1 -0
  36. data/test/rscm/revision_file_test.rb +10 -0
  37. data/test/rscm/revision_poller_test.rb +91 -0
  38. data/test/rscm/revision_test.rb +22 -117
  39. data/test/rscm/revisions_test.rb +80 -0
  40. data/test/rscm/scm/cvs_log_parser_test.rb +569 -567
  41. data/test/rscm/scm/cvs_test.rb +6 -3
  42. data/test/rscm/scm/darcs_test.rb +4 -7
  43. data/test/rscm/scm/perforce_test.rb +6 -2
  44. data/test/rscm/scm/star_team_test.rb +10 -0
  45. data/test/rscm/scm/subversion_log_parser_test.rb +38 -5
  46. data/test/rscm/scm/subversion_test.rb +2 -3
  47. data/test/rscm/test_helper.rb +41 -2
  48. data/testproject/damagecontrolled/build.xml +154 -154
  49. data/testproject/damagecontrolled/src/java/com/thoughtworks/damagecontrolled/Thingy.java +6 -6
  50. metadata +19 -7
  51. data/lib/rscm/historic_file.rb +0 -30
  52. data/test/rscm/compatibility/damage_control_minimal.rb +0 -104
  53. data/test/rscm/revision_fixture.rb +0 -20
  54. data/test/rscm/revisions.yaml +0 -42
  55. data/test/rscm/scm/star_team.rb +0 -36
data/CHANGES CHANGED
@@ -1,5 +1,17 @@
1
1
  = RSCM Changelog
2
2
 
3
+ == 0.5.0
4
+ This release improves command line and logging, as well as improved support for slurping all historic revisions incrementally (to save memory). The latter will be improved in upcoming versions.
5
+
6
+ * Diffing and opening specific files is now part of the rscm_engine/damagecontrol compatibility test
7
+ * The interface of RSCM::Base.diff has changed to take a path and two revision identifiers.
8
+ * Different timezones for client and server are now (hopefully) properly handled for Perforce.
9
+ * Made RSCM::Base.execute not do a chdir unless it's needed (makes direct usage from a Rails app more feasable).
10
+ * Fixed broken pipe error for CVS triggers.
11
+ * Store the commandline used to retrieve revisions in the Revisions object (useful for debugging).
12
+ * New RSCM::Base.poll method that yields revisions incrementally from a start date (backwards or forwards in time).
13
+ * Removed RSCM::Base.file and RSCM::HistoricFile - too hard to implement correctly across SCMs and too little value.
14
+
3
15
  == 0.4.5
4
16
  This is a minor bugfix release that corrects the tests for command_line.rb to work on win32
5
17
 
data/README CHANGED
@@ -173,6 +173,20 @@ in modification time, commit message and developer.
173
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
174
  sorted by their path.
175
175
 
176
+ === Create old.yml
177
+ This file should contain a start time and all the revision identifiers before that time.
178
+ The start time should be a carefully selected timestamp close to the start of the scm.
179
+ "identifiers" should be a list of all identifiers from the beginning of time up until
180
+ the start identifier.
181
+
182
+ === Create diff.txt
183
+ This file should contain a diff. It should be the diff of the first revision file in revisions.yml -
184
+ between its native_revision_identifier and the previous_revision_identifier.
185
+
186
+ === Create file.txt
187
+ This file should contain the contents of the first revision file in revisions.yml (at the revision
188
+ specified by native_revision_identifier).
189
+
176
190
  == Implement the methods
177
191
  Now that we have set up everything needed for the tests, we can run the tests again:
178
192
 
data/Rakefile CHANGED
@@ -13,23 +13,16 @@ PKG_NAME = 'rscm'
13
13
  PKG_VERSION = RSCM::VERSION::STRING + PKG_BUILD
14
14
  PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
15
15
 
16
- desc "Default Task"
17
16
  task :default => [:test, :gem]
18
- #task :gem => [:test]
19
- task :test => [:starteam]
20
17
 
21
18
  # Run the unit tests
22
19
  # To run a specific test: rake test TEST=path/to/test
23
20
  fl = FileList.new('test/**/*_test.rb')
24
21
  fl.exclude('test/**/mooky*.rb')
25
- fl.exclude('test/**/monotone*.rb') # Incomplete/unsupported for now - reactivate when more complete!
26
- fl.exclude('test/**/clearcase*.rb') # Incomplete/unsupported for now - reactivate when more complete!
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!
30
- fl.exclude('test/**/perforce*.rb') # Incomplete/unsupported for now - reactivate when more complete!
31
- fl.exclude('test/**/p4client*.rb') # Incomplete/unsupported for now - reactivate when more complete!
32
- fl.exclude('test/**/starteam*.rb') # Too bloody hard to test without a StarTeam server license! Tested ad-hoc.
22
+ fl.exclude('test/**/monotone*.rb')
23
+ fl.exclude('test/**/clearcase*.rb')
24
+ fl.exclude('test/**/p4client*.rb')
25
+ fl.exclude('test/**/darcs*.rb')
33
26
  Rake::TestTask.new { |t|
34
27
  t.libs << "test"
35
28
  t.test_files = fl
@@ -48,19 +41,6 @@ rd = Rake::RDocTask.new { |rdoc|
48
41
  rdoc.rdoc_files.include('docs/**/*.rd')
49
42
  }
50
43
 
51
- task :starteam do |t|
52
- if(!ENV['RSCM_STARTEAM'])
53
- puts "WARNING - NOT BUILDING STARTEAM SUPPORT SINCE 'RSCM_STARTEAM' IS NOT DEFINED. It should point to the StarTeam SDK directory"
54
- else
55
- ant = RUBY_PLATFORM == "i386-mswin32" ? "ant.bat" : system("which ant.sh") ? "ant.sh" : "ant"
56
- IO.popen("#{ant} -f ext/java/build.xml clean jar") do |io|
57
- io.each_line do |line|
58
- puts line
59
- end
60
- end
61
- end
62
- end
63
-
64
44
  PKG_FILES = FileList[
65
45
  '[A-Z]*',
66
46
  'lib/**/*',
@@ -1,12 +1,11 @@
1
+ require 'rscm/revision_poller'
1
2
  require 'rscm/path_converter'
2
3
  require 'rscm/difftool'
3
4
  require 'rscm/platform'
4
5
  require 'rscm/command_line'
5
6
  require 'rscm/base'
6
7
  require 'rscm/revision'
7
- require 'rscm/revision_poller'
8
8
  require 'rscm/revision_file'
9
- require 'rscm/historic_file'
10
9
  require 'rscm/time_ext'
11
10
  # Load all sources under scm
12
11
  Dir[File.dirname(__FILE__) + "/rscm/scm/*.rb"].each do |src|
@@ -1,281 +1,289 @@
1
- require 'fileutils'
2
- require 'rscm/revision'
3
- require 'rscm/path_converter'
4
-
5
- module RSCM
6
- # This class defines the RSCM API, which offers access to an SCM working copy
7
- # as well as a 'central' repository.
8
- #
9
- # Concrete subclasses of this class (concrete adapters) implement the integration
10
- # with the respective SCMs.
11
- #
12
- # Most of the methods take an optional +options+ Hash (named parameters), allowing
13
- # the following options:
14
- #
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.
17
- #
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.
20
- #
21
- # Some of the methods in this API use +from_identifier+ and +to_identifier+.
22
- # These identifiers can be either a UTC Time (according to the SCM's clock)
23
- # or a String or Integer representing a label/revision
24
- # (according to the SCM's native label/revision scheme).
25
- #
26
- # If +from_identifier+ or +to_identifier+ are +nil+ they should respectively default to
27
- # Time.epoch or Time.infinite.
28
- #
29
- class Base
30
-
31
- attr_writer :default_options
32
- def default_options
33
- @default_options ||= {:stdout=>'stdout.log', :stderr=>'stderr.log'}
34
- end
35
-
36
- # Transforms +raw_identifier+ into the native rype used for revisions.
37
- def to_identifier(raw_identifier)
38
- raw_identifier.to_s
39
- end
40
-
41
- # Sets the checkout dir (working copy). Should be set prior to most other method
42
- # invocations (depending on the implementation).
43
- def checkout_dir=(dir)
44
- @checkout_dir = PathConverter.filepath_to_nativepath(dir, false)
45
- end
46
-
47
- # Gets the working copy directory.
48
- def checkout_dir
49
- @checkout_dir
50
- end
51
-
52
- def to_yaml_properties #:nodoc:
53
- props = instance_variables
54
- props.delete("@checkout_dir")
55
- props.delete("@default_options")
56
- props.sort!
57
- end
58
-
59
- # Destroys the working copy
60
- def destroy_working_copy
61
- FileUtils.rm_rf(checkout_dir) unless checkout_dir.nil?
62
- end
63
-
64
- # Whether or not the SCM represented by this instance exists.
65
- def central_exists?
66
- # The default implementation assumes yes - override if it can be
67
- # determined programmatically.
68
- true
69
- end
70
-
71
- # Whether or not this SCM is transactional (atomic).
72
- def transactional?
73
- false
74
- end
75
- alias :atomic? :transactional?
76
-
77
- # Creates a new 'central' repository. This is intended only for creation of 'central'
78
- # repositories (not for working copies). You shouldn't have to call this method if a central repository
79
- # already exists. This method is used primarily for testing of RSCM, but can also
80
- # be used if you *really* want to use RSCM to create a central repository.
81
- #
82
- # This method should throw an exception if the repository cannot be created (for
83
- # example if the repository is 'remote' or if it already exists).
84
- #
85
- def create_central(options={})
86
- raise NotImplementedError
87
- end
88
-
89
- # Destroys the central repository. Shuts down any server processes and deletes the repository.
90
- # WARNING: calling this may result in loss of data. Only call this if you really want to wipe
91
- # it out for good!
92
- def destroy_central
93
- raise NotImplementedError
94
- end
95
-
96
- # Whether a repository can be created.
97
- def can_create_central?
98
- false
99
- end
100
-
101
- # Adds +relative_filename+ to the working copy.
102
- def add(relative_filename, options={})
103
- raise NotImplementedError
104
- end
105
-
106
- # Schedules a move of +relative_src+ to +relative_dest+
107
- # Should not take effect in the central repository until
108
- # +commit+ is invoked.
109
- def move(relative_src, relative_dest, options={})
110
- raise NotImplementedError
111
- end
112
-
113
- # Recursively imports files from <tt>:dir</tt> into the central scm,
114
- # using commit message <tt>:message</tt>
115
- def import_central(options)
116
- raise NotImplementedError
117
- end
118
-
119
- # Open a file for edit - required by scms that check out files in read-only mode e.g. perforce
120
- def edit(file, options={})
121
- end
122
-
123
- # Commit (check in) modified files.
124
- def commit(message, options={})
125
- raise NotImplementedError
126
- end
127
-
128
- # Checks out or updates contents from a central SCM to +checkout_dir+ - a local working copy.
129
- # If this is a distributed SCM, this method should create a 'working copy' repository
130
- # if one doesn't already exist. Then the contents of the central SCM should be pulled into
131
- # the working copy.
132
- #
133
- # The +to_identifier+ parameter may be optionally specified to obtain files up to a
134
- # particular time or label. +to_identifier+ should either be a Time (in UTC - according to
135
- # the clock on the SCM machine) or a String - reprsenting a label or revision.
136
- #
137
- # This method will yield the relative file name of each checked out file, and also return
138
- # them in an array. Only files, not directories, should be yielded/returned.
139
- #
140
- # This method should be overridden for SCMs that are able to yield checkouts as they happen.
141
- # For some SCMs this is not possible, or at least very hard. In that case, just override
142
- # the checkout_silent method instead of this method (should be protected).
143
- #
144
- def checkout(to_identifier=Time.infinity, options={}) # :yield: file
145
- to_identifier = Time.infinity if to_identifier.nil?
146
-
147
- before = checked_out_files
148
- # We expect subclasses to implement this as a protected method (unless this whole method is overridden).
149
- checkout_silent(to_identifier, options)
150
- after = checked_out_files
151
-
152
- (after - before).sort!
153
- end
154
-
155
- def checked_out_files
156
- files = Dir["#{@checkout_dir}/**/*"]
157
- files.delete_if{|file| File.directory?(file)}
158
- ignore_paths.each do |regex|
159
- files.delete_if{|file| file =~ regex}
160
- end
161
- dir = File.expand_path(@checkout_dir)
162
- files.collect{|file| File.expand_path(file)[dir.length+1..-1]}
163
- end
164
-
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
167
- # revisions pertaining to that path.
168
- #
169
- def revisions(from_identifier, options={})
170
- raise NotImplementedError
171
- end
172
-
173
- # Returns the HistoricFile representing the root of the repo
174
- def rootdir
175
- file("", true)
176
- end
177
-
178
- # Returns a HistoricFile for +relative_path+
179
- def file(relative_path, dir)
180
- HistoricFile.new(relative_path, dir, self)
181
- end
182
-
183
- # Opens a revision_file
184
- def open(revision_file, &block) #:yield: io
185
- raise NotImplementedError
186
- end
187
-
188
- # Whether the working copy is in synch with the central
189
- # repository's revision/time identified by +identifier+.
190
- # If +identifier+ is nil, 'HEAD' of repository should be assumed.
191
- #
192
- def uptodate?(identifier)
193
- raise NotImplementedError
194
- end
195
-
196
- # Whether the project is checked out from the central repository or not.
197
- # Subclasses should override this to check for SCM-specific administrative
198
- # files if appliccable
199
- def checked_out?
200
- File.exists?(@checkout_dir)
201
- end
202
-
203
- # Whether triggers are supported by this SCM. A trigger is a command that can be executed
204
- # upon a completed commit to the SCM.
205
- def supports_trigger?
206
- # The default implementation assumes no - override if it can be
207
- # determined programmatically.
208
- false
209
- end
210
- alias :can_install_trigger? :supports_trigger?
211
-
212
- # Descriptive name of the trigger mechanism
213
- def trigger_mechanism
214
- raise NotImplementedError
215
- end
216
-
217
- # Installs +trigger_command+ in the SCM.
218
- # The +install_dir+ parameter should be an empty local
219
- # directory that the SCM can use for temporary files
220
- # if necessary (CVS needs this to check out its administrative files).
221
- # Most implementations will ignore this parameter.
222
- #
223
- def install_trigger(trigger_command, install_dir)
224
- raise NotImplementedError
225
- end
226
-
227
- # Uninstalls +trigger_command+ from the SCM.
228
- #
229
- def uninstall_trigger(trigger_command, install_dir)
230
- raise NotImplementedError
231
- end
232
-
233
- # Whether the command denoted by +trigger_command+ is installed in the SCM.
234
- #
235
- def trigger_installed?(trigger_command, install_dir)
236
- raise NotImplementedError
237
- end
238
-
239
- # The command line to run in order to check out a fresh working copy.
240
- #
241
- def checkout_commandline(to_identifier=Time.infinity)
242
- raise NotImplementedError
243
- end
244
-
245
- # The command line to run in order to update a working copy.
246
- #
247
- def update_commandline(to_identifier=Time.infinity)
248
- raise NotImplementedError
249
- end
250
-
251
- # Returns/yields an IO containing the unified diff of the change.
252
- # Also see RevisionFile#diff
253
- def diff(change, &block)
254
- raise NotImplementedError
255
- end
256
-
257
- def ==(other_scm)
258
- return false if self.class != other_scm.class
259
- self.instance_variables.each do |var|
260
- return false if self.instance_eval(var) != other_scm.instance_eval(var)
261
- end
262
- true
263
- end
264
-
265
- protected
266
-
267
- # Wrapper for CommandLine.execute that provides default values for
268
- # dir plus any options set in default_options (typically stdout and stderr).
269
- def execute(cmd, options={}, &proc)
270
- default_dir = @checkout_dir.nil? ? Dir.pwd : @checkout_dir
271
- options = {:dir => default_dir}.merge(default_options).merge(options)
272
- begin
273
- CommandLine.execute(cmd, options, &proc)
274
- rescue CommandLine::OptionError => e
275
- e.message += "\nEither specify default_options on the scm object, or pass the required options to the method"
276
- raise e
277
- end
278
- end
279
-
280
- end
281
- end
1
+ require 'fileutils'
2
+ require 'rscm/revision'
3
+ require 'rscm/path_converter'
4
+
5
+ module RSCM
6
+ # This class defines the RSCM API, which offers access to an SCM working copy
7
+ # as well as a 'central' repository.
8
+ #
9
+ # Concrete subclasses of this class (concrete adapters) implement the integration
10
+ # with the respective SCMs.
11
+ #
12
+ # Most of the methods take an optional +options+ Hash (named parameters), allowing
13
+ # the following options:
14
+ #
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.
17
+ #
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.
20
+ #
21
+ # Some of the methods in this API use +from_identifier+ and +to_identifier+.
22
+ # These identifiers can be either a UTC Time (according to the SCM's clock)
23
+ # or a String or Integer representing a label/revision
24
+ # (according to the SCM's native label/revision scheme).
25
+ #
26
+ # If +from_identifier+ or +to_identifier+ are +nil+ they should respectively default to
27
+ # Time.epoch or Time.infinite.
28
+ #
29
+ class Base
30
+ include RevisionPoller
31
+
32
+ attr_writer :default_options
33
+ attr_writer :store_revisions_command
34
+
35
+ def default_options
36
+ @default_options ||= {}
37
+ end
38
+
39
+ # Returns true if the underlying SCM tool is available on this system.
40
+ def available?
41
+ raise NotImplementedError
42
+ end
43
+
44
+ # Transforms +raw_identifier+ into the native rype used for revisions.
45
+ def to_identifier(raw_identifier)
46
+ raw_identifier.to_s
47
+ end
48
+
49
+ # Sets the checkout dir (working copy). Should be set prior to most other method
50
+ # invocations (depending on the implementation).
51
+ def checkout_dir=(dir)
52
+ @checkout_dir = PathConverter.filepath_to_nativepath(dir, false)
53
+ end
54
+
55
+ # Gets the working copy directory.
56
+ def checkout_dir
57
+ @checkout_dir
58
+ end
59
+
60
+ def to_yaml_properties #:nodoc:
61
+ props = instance_variables
62
+ props.delete("@checkout_dir")
63
+ props.delete("@default_options")
64
+ props.sort!
65
+ end
66
+
67
+ # Destroys the working copy
68
+ def destroy_working_copy(options={})
69
+ FileUtils.rm_rf(checkout_dir) unless checkout_dir.nil?
70
+ end
71
+
72
+ # Whether or not the SCM represented by this instance exists.
73
+ def central_exists?
74
+ # The default implementation assumes yes - override if it can be
75
+ # determined programmatically.
76
+ true
77
+ end
78
+
79
+ # Whether or not this SCM is transactional (atomic).
80
+ def transactional?
81
+ false
82
+ end
83
+ alias :atomic? :transactional?
84
+
85
+ # Creates a new 'central' repository. This is intended only for creation of 'central'
86
+ # repositories (not for working copies). You shouldn't have to call this method if a central repository
87
+ # already exists. This method is used primarily for testing of RSCM, but can also
88
+ # be used if you *really* want to use RSCM to create a central repository.
89
+ #
90
+ # This method should throw an exception if the repository cannot be created (for
91
+ # example if the repository is 'remote' or if it already exists).
92
+ #
93
+ def create_central(options={})
94
+ raise NotImplementedError
95
+ end
96
+
97
+ # Destroys the central repository. Shuts down any server processes and deletes the repository.
98
+ # WARNING: calling this may result in loss of data. Only call this if you really want to wipe
99
+ # it out for good!
100
+ def destroy_central
101
+ raise NotImplementedError
102
+ end
103
+
104
+ # Whether a repository can be created.
105
+ def can_create_central?
106
+ false
107
+ end
108
+
109
+ # Adds +relative_filename+ to the working copy.
110
+ def add(relative_filename, options={})
111
+ raise NotImplementedError
112
+ end
113
+
114
+ # Schedules a move of +relative_src+ to +relative_dest+
115
+ # Should not take effect in the central repository until
116
+ # +commit+ is invoked.
117
+ def move(relative_src, relative_dest, options={})
118
+ raise NotImplementedError
119
+ end
120
+
121
+ # Recursively imports files from <tt>:dir</tt> into the central scm,
122
+ # using commit message <tt>:message</tt>
123
+ def import_central(options)
124
+ raise NotImplementedError
125
+ end
126
+
127
+ # Open a file for edit - required by scms that check out files in read-only mode e.g. perforce
128
+ def edit(file, options={})
129
+ end
130
+
131
+ # Commit (check in) modified files.
132
+ def commit(message, options={})
133
+ raise NotImplementedError
134
+ end
135
+
136
+ # Checks out or updates contents from a central SCM to +checkout_dir+ - a local working copy.
137
+ # If this is a distributed SCM, this method should create a 'working copy' repository
138
+ # if one doesn't already exist. Then the contents of the central SCM should be pulled into
139
+ # the working copy.
140
+ #
141
+ # The +to_identifier+ parameter may be optionally specified to obtain files up to a
142
+ # particular time or label. +to_identifier+ should either be a Time (in UTC - according to
143
+ # the clock on the SCM machine) or a String - reprsenting a label or revision.
144
+ #
145
+ # This method will yield the relative file name of each checked out file, and also return
146
+ # them in an array. Only files, not directories, should be yielded/returned.
147
+ #
148
+ # This method should be overridden for SCMs that are able to yield checkouts as they happen.
149
+ # For some SCMs this is not possible, or at least very hard. In that case, just override
150
+ # the checkout_silent method instead of this method (should be protected).
151
+ #
152
+ def checkout(to_identifier=Time.infinity, options={}) # :yield: file
153
+ to_identifier = Time.infinity if to_identifier.nil?
154
+
155
+ before = checked_out_files
156
+ # We expect subclasses to implement this as a protected method (unless this whole method is overridden).
157
+ checkout_silent(to_identifier, options)
158
+ after = checked_out_files
159
+
160
+ (after - before).sort!
161
+ end
162
+
163
+ def checked_out_files
164
+ raise "checkout_dir not set" if @checkout_dir.nil?
165
+
166
+ files = Dir["#{@checkout_dir}/**/*"]
167
+ files.delete_if{|file| File.directory?(file)}
168
+ ignore_paths.each do |regex|
169
+ files.delete_if{|file| file =~ regex}
170
+ end
171
+ dir = File.expand_path(@checkout_dir)
172
+ files.collect{|file| File.expand_path(file)[dir.length+1..-1]}
173
+ end
174
+
175
+ # Returns a Revisions object for the interval specified by +from_identifier+ (exclusive, i.e. after)
176
+ # and optionally +:to_identifier+ (exclusive too). If +relative_path+ is specified, the result will only contain
177
+ # revisions pertaining to that path.
178
+ #
179
+ # For example, revisions(223, 229) should return revisions 224..228
180
+ def revisions(from_identifier, options={})
181
+ raise NotImplementedError
182
+ end
183
+
184
+ # Opens a readonly IO to a file at +path+
185
+ def open(path, native_revision_identifier, options={}, &block) #:yield: io
186
+ raise NotImplementedError
187
+ end
188
+
189
+ # Whether the working copy is in synch with the central
190
+ # repository's revision/time identified by +identifier+.
191
+ # If +identifier+ is nil, 'HEAD' of repository should be assumed.
192
+ #
193
+ def uptodate?(identifier)
194
+ raise NotImplementedError
195
+ end
196
+
197
+ # Whether the project is checked out from the central repository or not.
198
+ # Subclasses should override this to check for SCM-specific administrative
199
+ # files if appliccable
200
+ def checked_out?
201
+ File.exists?(@checkout_dir)
202
+ end
203
+
204
+ # Whether triggers are supported by this SCM. A trigger is a command that can be executed
205
+ # upon a completed commit to the SCM.
206
+ def supports_trigger?
207
+ # The default implementation assumes no - override if it can be
208
+ # determined programmatically.
209
+ false
210
+ end
211
+ alias :can_install_trigger? :supports_trigger?
212
+
213
+ # Descriptive name of the trigger mechanism
214
+ def trigger_mechanism
215
+ raise NotImplementedError
216
+ end
217
+
218
+ # Installs +trigger_command+ in the SCM.
219
+ # The +install_dir+ parameter should be an empty local
220
+ # directory that the SCM can use for temporary files
221
+ # if necessary (CVS needs this to check out its administrative files).
222
+ # Most implementations will ignore this parameter.
223
+ #
224
+ def install_trigger(trigger_command, install_dir)
225
+ raise NotImplementedError
226
+ end
227
+
228
+ # Uninstalls +trigger_command+ from the SCM.
229
+ #
230
+ def uninstall_trigger(trigger_command, install_dir)
231
+ raise NotImplementedError
232
+ end
233
+
234
+ # Whether the command denoted by +trigger_command+ is installed in the SCM.
235
+ #
236
+ def trigger_installed?(trigger_command, install_dir)
237
+ raise NotImplementedError
238
+ end
239
+
240
+ # The command line to run in order to check out a fresh working copy.
241
+ #
242
+ def checkout_commandline(to_identifier=Time.infinity)
243
+ raise NotImplementedError
244
+ end
245
+
246
+ # The command line to run in order to update a working copy.
247
+ #
248
+ def update_commandline(to_identifier=Time.infinity)
249
+ raise NotImplementedError
250
+ end
251
+
252
+ # Yields an IO containing the unified diff of the change.
253
+ # Also see RevisionFile#diff
254
+ def diff(path, from, to, options={}, &block)
255
+ raise NotImplementedError
256
+ end
257
+
258
+ def ==(other_scm)
259
+ return false if self.class != other_scm.class
260
+ self.instance_variables.each do |var|
261
+ return false if self.instance_eval(var) != other_scm.instance_eval(var)
262
+ end
263
+ true
264
+ end
265
+
266
+ # Whether or not to store the revision command in the Revisions instance returned by <tt>revisions</tt>
267
+ def store_revisions_command?; @store_revisions_command.nil? ? true : @store_revisions_command; end
268
+
269
+ protected
270
+
271
+ # Directory where commands must be run
272
+ def cmd_dir
273
+ nil
274
+ end
275
+
276
+ # Wrapper for CommandLine.execute that provides default values for
277
+ # dir plus any options set in default_options (typically stdout and stderr).
278
+ def execute(cmd, options={}, &proc)
279
+ options = {:dir => cmd_dir}.merge(default_options).merge(options)
280
+ begin
281
+ CommandLine.execute(cmd, options, &proc)
282
+ rescue CommandLine::OptionError => e
283
+ e.message += "\nEither specify default_options on the scm object, or pass the required options to the method"
284
+ raise e
285
+ end
286
+ end
287
+
288
+ end
289
+ end