rscm 0.4.5 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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