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
@@ -47,12 +47,18 @@ module RSCM
47
47
  # Returns/yields an IO containing the contents of this file, using the +scm+ this
48
48
  # file lives in.
49
49
  def open(scm, options={}, &block) #:yield: io
50
- scm.open(self, options, &block)
50
+ scm.open(path, native_revision_identifier, options, &block)
51
51
  end
52
52
 
53
53
  # Yields the diff as an IO for this file
54
54
  def diff(scm, options={}, &block)
55
- scm.diff(self, options, &block)
55
+ from_to = case status
56
+ when /#{RevisionFile::MODIFIED}/; [previous_native_revision_identifier, native_revision_identifier]
57
+ when /#{RevisionFile::DELETED}/; [previous_native_revision_identifier, nil]
58
+ when /#{RevisionFile::ADDED}/; [nil, native_revision_identifier]
59
+ end
60
+
61
+ scm.diff(path, from_to[0], from_to[1], options, &block)
56
62
  end
57
63
 
58
64
  # Accepts a visitor that must respond to +visit_file(revision_file)+
@@ -1,82 +1,93 @@
1
1
  module RSCM
2
- class Base
2
+ module RevisionPoller
3
3
  attr_accessor :logger
4
4
 
5
- TWO_WEEKS_AGO = 2*7*24*60*60
6
- THIRTY_TWO_WEEKS_AGO = TWO_WEEKS_AGO * 16
7
- # Default time to wait for scm to be quiet (applies to non-transactional scms only)
8
- DEFAULT_QUIET_PERIOD = 15
5
+ # This is the number of revisions we'll try to stick to for each
6
+ # call to revisions.
7
+ CRITICAL_REVISION_SIZE = 100
8
+ BASE_INCREMENT = 60*60 # 1 hour
9
+ TWENTY_FOUR_HOURS = 24*60*60
9
10
 
10
-
11
- # Polls new revisions for since +last_revision+,
12
- # or if +last_revision+ is nil, polls since 'now' - +options[:seconds_before_now]+.
13
- # If no revisions are found AND the poll was using +options[:seconds_before_now]+
14
- # (i.e. it's the first poll, and no revisions were found),
15
- # calls itself recursively with twice the +options[:seconds_before_now]+.
16
- # This happens until revisions are found, ot until the +options[:seconds_before_now]+
17
- # Exceeds 32 weeks, which means it's probably not worth looking further in
18
- # the past, the scm is either completely idle or not yet active.
19
- def poll_new_revisions(options)
20
- options = {
21
- :latest_revision => nil,
22
- :quiet_period => DEFAULT_QUIET_PERIOD,
23
- :seconds_before_now => TWO_WEEKS_AGO,
24
- :max_time_before_now => THIRTY_TWO_WEEKS_AGO,
25
- }.merge(options)
26
- max_past = Time.new.utc - options[:max_time_before_now]
27
-
28
- # Default value for start time (in case there are no detected revisions yet)
29
- from = Time.new.utc - options[:seconds_before_now]
30
- if(options[:latest_revision])
31
- from = options[:latest_revision].identifier
11
+ # Polls revisions from +point+ and either backwards in time until the beginning
12
+ # of time (Time.epoch) or forward in time until we're past +now+.
13
+ #
14
+ # Whether to poll forwards or backwards in time depends on the value of +direction+.
15
+ #
16
+ # The +point+ argument can be either a Revision, String, Time or Fixnum representing
17
+ # where to start from (upper boundary for backwards polling, lower boundary for
18
+ # forwards polling).
19
+ #
20
+ # The polling starts with a small interval from +point+ (1 hour) and increments (or decrements)
21
+ # gradually in order to try and keep the length of the yielded Revisions to about 100.
22
+ #
23
+ # The passed block will be called several times, each time with a Revisions object.
24
+ # In order to reduce the memory footprint and keep the performance decent, the length
25
+ # of each yielded Revisions object will usually be within the order of magnitude of 100.
26
+ #
27
+ # TODO: handle non-transactional SCMs. There was some handling of this in older revisions
28
+ # of this file. We should dig it out and reenable it.
29
+ def poll(point=nil, direction=:backwards, multiplier=1, now=Time.now.utc, options={}, &proc)
30
+ raise "A block of arity 1 must be called" if proc.nil?
31
+ backwards = direction == :backwards
32
+ point ||= now
33
+
34
+ if point.respond_to?(:time)
35
+ point_time = backwards ? point.time(:min) : point.time(:max)
36
+ point_identifier = backwards ? point.identifier(:min) : point.identifier(:max)
37
+ elsif point.is_a?(Time)
38
+ point_time = point
39
+ point_identifier = point
32
40
  else
33
- if(from < max_past)
34
- logger.info "Checked for revisions as far back as #{max_past}. There were none, so we give up." if logger
35
- return []
36
- else
37
- logger.info "Latest revision is not known. Checking for revisions since: #{from}" if logger
38
- end
41
+ point_time = now
42
+ point_identifier = point
39
43
  end
40
44
 
41
- logger.info "Polling revisions after #{from} (#{from.class.name})" if logger
42
-
43
- revisions = revisions(from, options)
44
- if(revisions.empty?)
45
- logger.info "No new revisions after #{from}" if logger
46
- unless(options[:latest_revision])
47
- double_seconds_before_now = 2*options[:seconds_before_now]
48
- logger.info "Last revision still not found, checking since #{double_seconds_before_now.ago}" if logger
49
- new_opts = options.dup
50
- new_opts[:seconds_before_now] = double_seconds_before_now
51
- return poll_new_revisions(new_opts)
45
+ increment = multiplier * BASE_INCREMENT
46
+ if backwards
47
+ to = point_identifier
48
+ begin
49
+ from = point_time - increment
50
+ rescue ArgumentError
51
+ from = Time.epoch
52
52
  end
53
+ from = Time.epoch if from < Time.epoch
53
54
  else
54
- logger.info "There were #{revisions.length} new revision(s) after #{from}" if logger
55
+ from = point_identifier
56
+ begin
57
+ to = point_time + increment
58
+ rescue RangeError
59
+ raise "RSCM will not work this far in the future (#{from} plus #{increment})"
60
+ end
55
61
  end
56
62
 
57
- if(!revisions.empty? && !transactional?)
58
- # We're dealing with a non-transactional SCM (like CVS/StarTeam/ClearCase,
59
- # unlike Subversion/Monotone). Sleep a little, get the revisions again.
60
- # When the revisions are not changing, we can consider the last commit done
61
- # and the quiet period elapsed. This is not 100% failsafe, but will work
62
- # under most circumstances. In the worst case, we'll miss some files in
63
- # the revisions for really slow commits, but they will be part of the next
64
- # revision (on next poll).
65
- commit_in_progress = true
66
- while(commit_in_progress)
67
- logger.info "Sleeping for #{options[:quiet_period]} seconds because #{visual_name} is not transactional." if logger
68
-
69
- sleep(options[:quiet_period])
70
- previous_revisions = revisions
71
- revisions = revisions(from)
72
- commit_in_progress = revisions != previous_revisions
73
- if(commit_in_progress)
74
- logger.info "Commit still in progress." if logger
75
- end
76
- end
77
- logger.info "Quiet period elapsed" if logger
63
+ options = options.merge({:to_identifier => to})
64
+
65
+ revs = revisions(from, options)
66
+ raise "Got nil revision for from=#{from.inspect}" if revs.nil?
67
+ revs.sort!
68
+ proc.call(revs)
69
+
70
+ if from == Time.epoch
71
+ return
72
+ end
73
+ if !backwards and to.is_a?(Time) and (to) > now + TWENTY_FOUR_HOURS
74
+ return
75
+ end
76
+
77
+ if(revs.length < CRITICAL_REVISION_SIZE)
78
+ # We can do more
79
+ multiplier *= 2
78
80
  end
79
- return revisions
81
+ if(revs.length > 2*CRITICAL_REVISION_SIZE)
82
+ # We must do less
83
+ multiplier /= 2
84
+ end
85
+
86
+ unless(revs.empty?)
87
+ point = backwards ? revs[0] : revs[-1]
88
+ end
89
+ poll(point, direction, multiplier, now, options, &proc)
80
90
  end
91
+
81
92
  end
82
93
  end
@@ -0,0 +1,79 @@
1
+ require 'rscm/time_ext'
2
+ require 'rscm/revision_file'
3
+
4
+ module RSCM
5
+
6
+ # A Revisions object is a collection of Revision objects with some
7
+ # additional behaviour.
8
+ #
9
+ # Most importantly, it provides logic to group individual RevisionFile
10
+ # objects into Revision objects internally. This means that implementors
11
+ # of RSCM adapters that don't support atomic changesets can still emulate
12
+ # them, simply by adding RevisionFile objects to a Revisions object. Example:
13
+ #
14
+ # revisions = Revisions.new
15
+ # revisions.add revision_file_1
16
+ # revisions.add revision_file_2
17
+ # revisions.add revision_file_3
18
+ #
19
+ # The added RevisionFile objects will end up in Revision objects grouped by
20
+ # their comment, developer and timestamp. A set of RevisionFile object with
21
+ # identical developer and message will end up in the same Revision provided
22
+ # their <tt>time</tt> attributes are a minute apart or less.
23
+ #
24
+ # Each Revisions object also has an attribute <tt>cmd</tt> which should contain
25
+ # the command used to retrieve the revision data and populate it. This is useful
26
+ # for debugging an RSCM adapter that might behaving incorrectly. Keep in mind that
27
+ # it is the responsibility of each RSCM adapter implementation to set this attribute,
28
+ # and that it should omit setting it if the <tt>store_revisions_command</tt> is
29
+ # <tt>true</tt>
30
+ class Revisions
31
+ include Enumerable
32
+ attr_accessor :cmd
33
+
34
+ def initialize(revisions=[])
35
+ @revisions = revisions
36
+ end
37
+
38
+ def add(file_or_revision)
39
+ if(file_or_revision.is_a?(Revision))
40
+ @revisions << file_or_revision
41
+ else
42
+ revision = find { |a_revision| a_revision.accept?(file_or_revision) }
43
+ if(revision.nil?)
44
+ revision = Revision.new
45
+ @revisions << revision
46
+ end
47
+ revision.add file_or_revision
48
+ end
49
+ end
50
+
51
+ def sort!
52
+ @revisions.sort!{|r1,r2| r1.time<=>r2.time}
53
+ end
54
+
55
+ def to_s
56
+ @revisions.collect{|revision| revision.to_s}.join("\n-----------")
57
+ end
58
+
59
+ def ==(other)
60
+ self.to_s == other.to_s
61
+ end
62
+
63
+ def each(&block)
64
+ @revisions.each(&block)
65
+ end
66
+
67
+ def [](n)
68
+ @revisions[n]
69
+ end
70
+
71
+ def length
72
+ @revisions.length
73
+ end
74
+
75
+ def empty?
76
+ @revisions.empty?
77
+ end
78
+ end
79
+ end
@@ -6,15 +6,17 @@ require 'tempfile'
6
6
  module RSCM
7
7
  class ClearCase < Base
8
8
 
9
- LOG_FORMAT = "- !ruby/object:RSCM::RevisionFile\\n developer: %u\\n time: \\\"%Nd\\\"\\n native_revision_identifier: %Vn\\n previous_native_revision_identifier: %PVn\\n path: %En\\n status: %o\\n message: \\\"%Nc\\\"\\n\\n"
10
- TIME_FORMAT = "%d-%b-%Y.%H:%M:%S"
11
- MAGIC_TOKEN = "9q8w7e6r5t4y"
12
- STATUSES = {
13
- "checkin" => RevisionFile::MODIFIED,
14
- "mkelem" => RevisionFile::ADDED,
15
- "rmelem" => RevisionFile::DELETED,
16
- }
17
- DEFAULT_CONFIG_SPEC = "element * CHECKEDOUT\nelement * /main/LATEST"
9
+ unless defined? LOG_FORMAT
10
+ LOG_FORMAT = "- !ruby/object:RSCM::RevisionFile\\n developer: %u\\n time: \\\"%Nd\\\"\\n native_revision_identifier: %Vn\\n previous_native_revision_identifier: %PVn\\n path: %En\\n status: %o\\n message: \\\"%Nc\\\"\\n\\n"
11
+ TIME_FORMAT = "%d-%b-%Y.%H:%M:%S"
12
+ MAGIC_TOKEN = "9q8w7e6r5t4y"
13
+ STATUSES = {
14
+ "checkin" => RevisionFile::MODIFIED,
15
+ "mkelem" => RevisionFile::ADDED,
16
+ "rmelem" => RevisionFile::DELETED,
17
+ }
18
+ DEFAULT_CONFIG_SPEC = "element * CHECKEDOUT\nelement * /main/LATEST"
19
+ end
18
20
 
19
21
  attr_accessor :stream, :stgloc, :tag, :config_spec
20
22
 
@@ -1,352 +1,374 @@
1
- require 'stringio'
2
- require 'rscm/base'
3
- require 'rscm/path_converter'
4
- require 'rscm/line_editor'
5
- require 'rscm/scm/cvs_log_parser'
6
-
7
- module RSCM
8
-
9
- # RSCM implementation for CVS.
10
- #
11
- # You need a cvs executable on the PATH in order for it to work.
12
- #
13
- # NOTE: On Cygwin this has to be the win32 build of cvs and not the Cygwin one.
14
- class Cvs < Base
15
- attr_accessor :root
16
- attr_accessor :mod
17
- attr_accessor :branch
18
- attr_accessor :password
19
-
20
- def initialize(root=nil, mod=nil, branch=nil, password=nil)
21
- @root, @mod, @branch, @password = root, mod, branch, password
22
- end
23
-
24
- def import_central(options={})
25
- modname = File.basename(options[:dir])
26
- cvs("import -m \"#{options[:message]}\" #{modname} VENDOR START", options)
27
- end
28
-
29
- def add(relative_filename, options={})
30
- cvs("add #{relative_filename}", options)
31
- end
32
-
33
- def move(relative_src, relative_dest, options={})
34
- FileUtils.mv(@checkout_dir + '/' + relative_src, @checkout_dir + '/' + relative_dest, :force=>true)
35
- cvs("rm #{relative_src}", options)
36
- # This will fail if the directories are new. More advanced support for adding can be added if needed.
37
- cvs("add #{relative_dest}", options)
38
- end
39
-
40
- def commit(message, options={})
41
- cvs(commit_command(message), options)
42
- end
43
-
44
- def uptodate?(identifier, options={})
45
- if(!checked_out?)
46
- return false
47
- end
48
-
49
- checkout_silent(identifier, options.dup.merge({:simulate => true})) do |io|
50
- path_regex = /^[U|P|C] (.*)/
51
- io.each_line do |line|
52
- return false if(line =~ path_regex)
53
- end
54
- end
55
- return true
56
- end
57
-
58
- def revisions(from_identifier, options={})
59
- options = {
60
- :from_identifier => from_identifier,
61
- :to_identifier => Time.infinity,
62
- :relative_path => nil
63
- }.merge(options)
64
- checkout(options[:to_identifier], options) unless checked_out? # must checkout to get revisions
65
- parse_log(changes_command(options), options)
66
- end
67
-
68
- def diff(revision_file, options={})
69
- opts = case revision_file.status
70
- when /#{RevisionFile::MODIFIED}/; "#{revision_option(revision_file.previous_native_revision_identifier)} #{revision_option(revision_file.native_revision_identifier)}"
71
- when /#{RevisionFile::DELETED}/; "#{revision_option(revision_file.previous_native_revision_identifier)}"
72
- when /#{RevisionFile::ADDED}/; "#{revision_option(Time.epoch)} #{revision_option(revision_file.native_revision_identifier)}"
73
- end
74
-
75
- # IMPORTANT! CVS NT has a bug in the -N diff option
76
- # http://www.cvsnt.org/pipermail/cvsnt-bugs/2004-November/000786.html
77
- cmd = command_line("diff -Nu #{opts} #{revision_file.path}")
78
- execute(cmd, options.dup.merge({:exitstatus => 1})) do |io|
79
- yield io
80
- end
81
- end
82
-
83
- def open(revision_file, options, &block)
84
- cmd = "cvs -Q update -p -r #{revision_file.native_revision_identifier} #{revision_file.path}"
85
- execute(cmd, options) do |io|
86
- block.call io
87
- end
88
- end
89
-
90
- def apply_label(label)
91
- cvs("tag -c #{label}")
92
- end
93
-
94
- def trigger_mechanism
95
- "CVSROOT/loginfo"
96
- end
97
-
98
- def trigger_installed?(trigger_command, trigger_files_checkout_dir, options={})
99
- loginfo_line = "#{mod} #{trigger_command}"
100
- regex = Regexp.new(Regexp.escape(loginfo_line))
101
-
102
- root_cvs = create_root_cvs(trigger_files_checkout_dir)
103
- begin
104
- root_cvs.checkout(nil, options)
105
- loginfo = File.join(trigger_files_checkout_dir, "loginfo")
106
- return false if !File.exist?(loginfo)
107
-
108
- # returns true if commented out. doesn't modify the file.
109
- in_local_copy = LineEditor.comment_out(File.new(loginfo), regex, "# ", "")
110
- # Also verify that loginfo has been committed back to the repo
111
- entries = File.join(trigger_files_checkout_dir, "CVS", "Entries")
112
- committed = File.mtime(entries) >= File.mtime(loginfo)
113
-
114
- in_local_copy && committed
115
- rescue Exception => e
116
- $stderr.puts(e.message)
117
- $stderr.puts(e.backtrace.join("\n"))
118
- false
119
- end
120
- end
121
-
122
- def install_trigger(trigger_command, trigger_files_checkout_dir, options={})
123
- raise "mod can't be null or empty" if (mod.nil? || mod == "")
124
-
125
- root_cvs = create_root_cvs(trigger_files_checkout_dir)
126
- root_cvs.checkout(nil, options)
127
- Dir.chdir(trigger_files_checkout_dir) do
128
- trigger_line = "#{mod} #{trigger_command}\n"
129
- File.open("loginfo", File::WRONLY | File::APPEND) do |file|
130
- file.puts(trigger_line)
131
- end
132
- end
133
-
134
- begin
135
- root_cvs.commit("Installed trigger for CVS module '#{mod}'", options)
136
- rescue Errno::EACCES
137
- raise ["Didn't have permission to commit CVSROOT/loginfo.",
138
- "Try to manually add the following line:",
139
- trigger_command,
140
- "Finally make commit the file to the repository"].join("\n")
141
- end
142
- end
143
-
144
- def uninstall_trigger(trigger_command, trigger_files_checkout_dir, options={})
145
- loginfo_line = "#{mod} #{trigger_command}"
146
- regex = Regexp.new(Regexp.escape(loginfo_line))
147
-
148
- root_cvs = create_root_cvs(trigger_files_checkout_dir)
149
- root_cvs.checkout nil, options
150
- loginfo_path = File.join(trigger_files_checkout_dir, "loginfo")
151
- File.comment_out(loginfo_path, regex, "# ")
152
- root_cvs.commit("Uninstalled trigger for CVS mod '#{mod}'", options)
153
- raise "Couldn't uninstall/commit trigger to loginfo" if trigger_installed?(trigger_command, trigger_files_checkout_dir, options)
154
- end
155
-
156
- def create_central(options={})
157
- options = options.dup.merge({:dir => path})
158
- raise "Can't create central CVS repository for #{root}" unless can_create_central?
159
- File.mkpath(path)
160
- cvs("init", options)
161
- end
162
-
163
- def destroy_central
164
- if(File.exist?(path) && local?)
165
- FileUtils.rm_rf(path)
166
- else
167
- raise "Cannot destroy central repository. '#{path}' doesn't exist or central repo isn't local to this machine"
168
- end
169
- end
170
-
171
- def central_exists?
172
- if(local?)
173
- File.exists?("#{path}/CVSROOT/loginfo")
174
- else
175
- # don't know. assume yes.
176
- true
177
- end
178
- end
179
-
180
- def can_create_central?
181
- begin
182
- local?
183
- rescue
184
- false
185
- end
186
- end
187
-
188
- def supports_trigger?
189
- true
190
- end
191
-
192
- def checked_out?
193
- rootcvs = File.expand_path("#{checkout_dir}/CVS/Root")
194
- File.exists?(rootcvs)
195
- end
196
-
197
- protected
198
-
199
- def checkout_silent(to_identifier, options={}, &proc)
200
- to_identifier = nil if to_identifier == Time.infinity
201
- if(checked_out?)
202
- options = options.dup.merge({
203
- :dir => @checkout_dir
204
- })
205
- cvs(update_command(to_identifier), options, &proc)
206
- else
207
- # This is a workaround for the fact that -d . doesn't work - must be an existing sub folder.
208
- FileUtils.mkdir_p(@checkout_dir) unless File.exist?(@checkout_dir)
209
- target_dir = File.basename(@checkout_dir)
210
- # -D is sticky, but subsequent updates will reset stickiness with -A
211
- options = options.dup.merge({
212
- :dir => File.dirname(@checkout_dir)
213
- })
214
- cvs(checkout_command(target_dir, to_identifier), options, &proc)
215
- end
216
- end
217
-
218
- def ignore_paths
219
- [/CVS\/.*/]
220
- end
221
-
222
- private
223
-
224
- def cvs(cmd, options={}, &proc)
225
- options = {
226
- :simulate => false,
227
- :dir => @checkout_dir
228
- }.merge(options)
229
-
230
- options[:dir] = PathConverter.nativepath_to_filepath(options[:dir])
231
- execed_command_line = command_line(cmd, password, options[:simulate])
232
- execute(execed_command_line, options, &proc)
233
- end
234
-
235
- def parse_log(cmd, options, &proc)
236
- execed_command_line = command_line(cmd, password)
237
- revisions = nil
238
-
239
- execute(execed_command_line, options) do |io|
240
- parser = CvsLogParser.new(io)
241
- parser.cvspath = path
242
- parser.cvsmodule = mod
243
- revisions = parser.parse_revisions
244
- end
245
- revisions
246
- end
247
-
248
- def changes_command(options)
249
- # https://www.cvshome.org/docs/manual/cvs-1.11.17/cvs_16.html#SEC144
250
- # -N => Suppress the header if no RevisionFiles are selected.
251
- "log #{branch_option} -N #{period_option(options[:from_identifier], options[:to_identifier])} #{options[:relative_path]}"
252
- end
253
-
254
- def branch_specified?
255
- branch && branch.strip != ""
256
- end
257
-
258
- def branch_option
259
- branch_specified? ? "-r#{branch}" : ""
260
- end
261
-
262
- def update_command(to_identifier)
263
- "update #{branch_option} -d -P -A #{revision_option(to_identifier)}"
264
- end
265
-
266
- def checkout_command(target_dir, to_identifier)
267
- "checkout #{branch_option} -d #{target_dir} #{revision_option(to_identifier)} #{mod}"
268
- end
269
-
270
- def period_option(from_identifier, to_identifier)
271
- if(from_identifier.nil? && to_identifier.nil?)
272
- ""
273
- else
274
- "-d\"#{cvsdate(from_identifier)}<#{cvsdate(to_identifier+1)}\" "
275
- end
276
- end
277
-
278
- def cvsdate(time)
279
- return "" unless time
280
- # CVS wants all dates as UTC.
281
- time.utc.strftime("%Y-%m-%d %H:%M:%S UTC")
282
- end
283
-
284
- def root_with_password(password)
285
- result = nil
286
- if local?
287
- result = root
288
- elsif password && password != ""
289
- protocol, user, host, path = parse_cvs_root
290
- result = ":#{protocol}:#{user}:#{password}@#{host}:#{path}"
291
- else
292
- result = root
293
- end
294
- end
295
-
296
- def command_line(cmd, password=nil, simulate=false)
297
- cvs_options = simulate ? "-n" : ""
298
- "cvs -f \"-d#{root_with_password(password)}\" #{cvs_options} -q #{cmd}"
299
- end
300
-
301
- def create_root_cvs(checkout_dir)
302
- cvs = Cvs.new(self.root, "CVSROOT", nil, self.password)
303
- cvs.checkout_dir = checkout_dir
304
- cvs.default_options = default_options
305
- cvs
306
- end
307
-
308
- def revision_option(identifier)
309
- option = nil
310
- if(identifier.is_a?(Time))
311
- option = "-D\"#{cvsdate(identifier)}\""
312
- elsif(identifier.is_a?(String))
313
- option = "-r#{identifier}"
314
- else
315
- ""
316
- end
317
- end
318
-
319
- def commit_command(message)
320
- "commit -m \"#{message}\""
321
- end
322
-
323
- def local?
324
- protocol == "local"
325
- end
326
-
327
- def path
328
- parse_cvs_root[3]
329
- end
330
-
331
- def protocol
332
- parse_cvs_root[0]
333
- end
334
-
335
- # parses the root into tokens
336
- # [protocol, user, host, path]
337
- #
338
- def parse_cvs_root
339
- md = case
340
- when root =~ /^:local:/ then /^:(local):(.*)/.match(root)
341
- when root =~ /^:ext:/ then /^:(ext):(.*)@(.*):(.*)/.match(root)
342
- when root =~ /^:pserver:/ then /^:(pserver):(.*)@(.*):(.*)/.match(root)
343
- end
344
- result = case
345
- when root =~ /^:local:/ then [md[1], nil, nil, md[2]]
346
- when root =~ /^:ext:/ then md[1..4]
347
- when root =~ /^:pserver:/ then md[1..4]
348
- else ["local", nil, nil, root]
349
- end
350
- end
351
- end
352
- end
1
+ require 'stringio'
2
+ require 'rscm/base'
3
+ require 'rscm/path_converter'
4
+ require 'rscm/line_editor'
5
+ require 'rscm/scm/cvs_log_parser'
6
+
7
+ module RSCM
8
+
9
+ # RSCM implementation for CVS.
10
+ #
11
+ # You need a cvs executable on the PATH in order for it to work.
12
+ #
13
+ # NOTE: On Cygwin this has to be the win32 build of cvs and not the Cygwin one.
14
+ class Cvs < Base
15
+ attr_accessor :root
16
+ attr_accessor :mod
17
+ attr_accessor :branch
18
+ attr_accessor :password
19
+
20
+ def installed?
21
+ begin
22
+ cvs("--version", {})
23
+ true
24
+ rescue
25
+ false
26
+ end
27
+ end
28
+
29
+ def initialize(root=nil, mod=nil, branch=nil, password=nil)
30
+ @root, @mod, @branch, @password = root, mod, branch, password
31
+ end
32
+
33
+ def import_central(dir, options={})
34
+ modname = File.basename(dir)
35
+ FileUtils.mkdir_p(@checkout_dir) unless File.exist?(@checkout_dir)
36
+ options = options.dup.merge :dir => dir
37
+ cvs("import -m \"#{options[:message]}\" #{modname} VENDOR START", options)
38
+ end
39
+
40
+ def add(relative_filename, options={})
41
+ cvs("add #{relative_filename}", options)
42
+ end
43
+
44
+ def move(relative_src, relative_dest, options={})
45
+ FileUtils.mv(@checkout_dir + '/' + relative_src, @checkout_dir + '/' + relative_dest, :force=>true)
46
+ cvs("rm #{relative_src}", options)
47
+ # This will fail if the directories are new. More advanced support for adding can be added if needed.
48
+ cvs("add #{relative_dest}", options)
49
+ end
50
+
51
+ def commit(message, options={})
52
+ cvs(commit_command(message), options)
53
+ end
54
+
55
+ def uptodate?(identifier, options={})
56
+ if(!checked_out?)
57
+ return false
58
+ end
59
+
60
+ checkout_silent(identifier, options.dup.merge({:simulate => true})) do |io|
61
+ path_regex = /^[U|P|C] (.*)/
62
+ io.each_line do |line|
63
+ return false if(line =~ path_regex)
64
+ end
65
+ end
66
+ return true
67
+ end
68
+
69
+ def revisions(from_identifier=Time.new.utc, options={})
70
+ raise "from_identifer cannot be nil" if from_identifier.nil?
71
+ options = {
72
+ :from_identifier => from_identifier,
73
+ :to_identifier => Time.infinity,
74
+ :relative_path => nil
75
+ }.merge(options)
76
+ checkout(options[:to_identifier], options) unless checked_out? # must checkout to get revisions
77
+ parse_log(changes_command(options), options)
78
+ end
79
+
80
+ def diff(path, from, to, options={}, &block)
81
+ # IMPORTANT! CVS NT has a bug in the -N diff option
82
+ # http://www.cvsnt.org/pipermail/cvsnt-bugs/2004-November/000786.html
83
+ from ||= Time.epoch
84
+ cmd = command_line("diff -Nu #{revision_option(from)} #{revision_option(to)} #{path}")
85
+ execute(cmd, options.dup.merge({:exitstatus => 1})) do |io|
86
+ block.call io
87
+ end
88
+ end
89
+
90
+ def open(path, native_revision_identifier, options={}, &block)
91
+ raise "native_revision_identifier cannot be nil" if native_revision_identifier.nil?
92
+ cmd = "cvs -Q update -p -r #{native_revision_identifier} #{path}"
93
+ execute(cmd, options) do |io|
94
+ block.call io
95
+ end
96
+ end
97
+
98
+ def apply_label(label)
99
+ cvs("tag -c #{label}")
100
+ end
101
+
102
+ def trigger_mechanism
103
+ "CVSROOT/loginfo"
104
+ end
105
+
106
+ def trigger_installed?(trigger_command, trigger_files_checkout_dir, options={})
107
+ trigger_command = fix_trigger_command(trigger_command)
108
+ loginfo_line = "#{mod} #{trigger_command}"
109
+ regex = Regexp.new(Regexp.escape(loginfo_line))
110
+
111
+ root_cvs = create_root_cvs(trigger_files_checkout_dir)
112
+ begin
113
+ root_cvs.checkout(nil, options)
114
+ loginfo = File.join(trigger_files_checkout_dir, "loginfo")
115
+ return false if !File.exist?(loginfo)
116
+
117
+ # returns true if commented out. doesn't modify the file.
118
+ in_local_copy = LineEditor.comment_out(File.new(loginfo), regex, "# ", "")
119
+ # Also verify that loginfo has been committed back to the repo
120
+ entries = File.join(trigger_files_checkout_dir, "CVS", "Entries")
121
+ committed = File.mtime(entries) >= File.mtime(loginfo)
122
+
123
+ in_local_copy && committed
124
+ rescue Exception => e
125
+ $stderr.puts(e.message)
126
+ $stderr.puts(e.backtrace.join("\n"))
127
+ false
128
+ end
129
+ end
130
+
131
+ def install_trigger(trigger_command, trigger_files_checkout_dir, options={})
132
+ raise "mod can't be null or empty" if (mod.nil? || mod == "")
133
+ trigger_command = fix_trigger_command(trigger_command)
134
+
135
+ root_cvs = create_root_cvs(trigger_files_checkout_dir)
136
+ root_cvs.checkout(nil, options)
137
+ Dir.chdir(trigger_files_checkout_dir) do
138
+ trigger_line = "#{mod} #{trigger_command}\n"
139
+ File.open("loginfo", File::WRONLY | File::APPEND) do |file|
140
+ file.puts(trigger_line)
141
+ end
142
+ end
143
+
144
+ begin
145
+ root_cvs.commit("Installed trigger for CVS module '#{mod}'", options)
146
+ rescue Errno::EACCES
147
+ raise ["Didn't have permission to commit CVSROOT/loginfo.",
148
+ "Try to manually add the following line:",
149
+ trigger_command,
150
+ "Finally make commit the file to the repository"].join("\n")
151
+ end
152
+ end
153
+
154
+ def uninstall_trigger(trigger_command, trigger_files_checkout_dir, options={})
155
+ trigger_command = fix_trigger_command(trigger_command)
156
+ loginfo_line = "#{mod} #{trigger_command}"
157
+ regex = Regexp.new(Regexp.escape(loginfo_line))
158
+
159
+ root_cvs = create_root_cvs(trigger_files_checkout_dir)
160
+ root_cvs.checkout nil, options
161
+ loginfo_path = File.join(trigger_files_checkout_dir, "loginfo")
162
+ File.comment_out(loginfo_path, regex, "# ")
163
+ root_cvs.commit("Uninstalled trigger for CVS mod '#{mod}'", options)
164
+ raise "Couldn't uninstall/commit trigger to loginfo" if trigger_installed?(trigger_command, trigger_files_checkout_dir, options)
165
+ end
166
+
167
+ def create_central(options={})
168
+ options = options.dup.merge({:dir => path})
169
+ raise "Can't create central CVS repository for #{root}" unless can_create_central?
170
+ File.mkpath(path)
171
+ cvs("init", options)
172
+ end
173
+
174
+ def destroy_central
175
+ if(File.exist?(path) && local?)
176
+ FileUtils.rm_rf(path)
177
+ else
178
+ raise "Cannot destroy central repository. '#{path}' doesn't exist or central repo isn't local to this machine"
179
+ end
180
+ end
181
+
182
+ def central_exists?
183
+ if(local?)
184
+ File.exists?("#{path}/CVSROOT/loginfo")
185
+ else
186
+ # don't know. assume yes.
187
+ true
188
+ end
189
+ end
190
+
191
+ def can_create_central?
192
+ begin
193
+ local?
194
+ rescue
195
+ false
196
+ end
197
+ end
198
+
199
+ def supports_trigger?
200
+ true
201
+ end
202
+
203
+ def checked_out?
204
+ rootcvs = File.expand_path("#{checkout_dir}/CVS/Root")
205
+ File.exists?(rootcvs)
206
+ end
207
+
208
+ protected
209
+
210
+ def cmd_dir
211
+ @checkout_dir
212
+ end
213
+
214
+ def checkout_silent(to_identifier, options={}, &proc)
215
+ to_identifier = nil if to_identifier == Time.infinity
216
+ if(checked_out?)
217
+ options = options.dup.merge({
218
+ :dir => @checkout_dir
219
+ })
220
+ cvs(update_command(to_identifier), options, &proc)
221
+ else
222
+ # This is a workaround for the fact that -d . doesn't work - must be an existing sub folder.
223
+ FileUtils.mkdir_p(@checkout_dir) unless File.exist?(@checkout_dir)
224
+ target_dir = File.basename(@checkout_dir)
225
+ # -D is sticky, but subsequent updates will reset stickiness with -A
226
+ options = options.dup.merge({
227
+ :dir => File.dirname(@checkout_dir)
228
+ })
229
+ cvs(checkout_command(target_dir, to_identifier), options, &proc)
230
+ end
231
+ end
232
+
233
+ def ignore_paths
234
+ [/CVS\/.*/]
235
+ end
236
+
237
+ private
238
+
239
+ # Prepends a cat that will slurp stdin. Needed for triggers that don't read all of stdin passed
240
+ # from CVS to avoid broken pipe.
241
+ def fix_trigger_command(cmd)
242
+ "cat && #{cmd}"
243
+ end
244
+
245
+ def cvs(cmd, options={}, &proc)
246
+ options = {
247
+ :simulate => false,
248
+ :dir => @checkout_dir
249
+ }.merge(options)
250
+
251
+ options[:dir] = PathConverter.nativepath_to_filepath(options[:dir])
252
+ execed_command_line = command_line(cmd, password, options[:simulate])
253
+ execute(execed_command_line, options, &proc)
254
+ end
255
+
256
+ def parse_log(cmd, options, &proc)
257
+ execed_command_line = command_line(cmd, password)
258
+ revisions = nil
259
+
260
+ execute(execed_command_line, options) do |io|
261
+ parser = CvsLogParser.new(io)
262
+ parser.cvspath = path
263
+ parser.cvsmodule = mod
264
+ revisions = parser.parse_revisions
265
+ end
266
+ revisions.cmd = execed_command_line if store_revisions_command?
267
+ revisions
268
+ end
269
+
270
+ def changes_command(options)
271
+ # https://www.cvshome.org/docs/manual/cvs-1.11.17/cvs_16.html#SEC144
272
+ # -N => Suppress the header if no RevisionFiles are selected.
273
+ "log #{branch_option} -N #{period_option(options[:from_identifier], options[:to_identifier])} #{options[:relative_path]}"
274
+ end
275
+
276
+ def branch_specified?
277
+ branch && branch.strip != ""
278
+ end
279
+
280
+ def branch_option
281
+ branch_specified? ? "-r#{branch}" : ""
282
+ end
283
+
284
+ def update_command(to_identifier)
285
+ "update #{branch_option} -d -P -A #{revision_option(to_identifier)}"
286
+ end
287
+
288
+ def checkout_command(target_dir, to_identifier)
289
+ "checkout #{branch_option} -d #{target_dir} #{revision_option(to_identifier)} #{mod}"
290
+ end
291
+
292
+ def period_option(from_identifier, to_identifier)
293
+ if(from_identifier.nil? && to_identifier.nil?)
294
+ ""
295
+ else
296
+ "-d\"#{cvsdate(from_identifier)}<#{cvsdate(to_identifier)}\" "
297
+ end
298
+ end
299
+
300
+ def cvsdate(time)
301
+ return "" unless time
302
+ # CVS wants all dates as UTC.
303
+ time.utc.strftime("%Y-%m-%d %H:%M:%S UTC")
304
+ end
305
+
306
+ def root_with_password(password)
307
+ result = nil
308
+ if local?
309
+ result = root
310
+ elsif password && password != ""
311
+ protocol, user, host, path = parse_cvs_root
312
+ result = ":#{protocol}:#{user}:#{password}@#{host}:#{path}"
313
+ else
314
+ result = root
315
+ end
316
+ end
317
+
318
+ def command_line(cmd, password=nil, simulate=false)
319
+ cvs_options = simulate ? "-n" : ""
320
+ "cvs -f \"-d#{root_with_password(password)}\" #{cvs_options} -q #{cmd}"
321
+ end
322
+
323
+ def create_root_cvs(checkout_dir)
324
+ cvs = Cvs.new(self.root, "CVSROOT", nil, self.password)
325
+ cvs.checkout_dir = checkout_dir
326
+ cvs.default_options = default_options
327
+ cvs
328
+ end
329
+
330
+ def revision_option(identifier)
331
+ option = nil
332
+ if(identifier.is_a?(Time))
333
+ option = "-D\"#{cvsdate(identifier)}\""
334
+ elsif(identifier.is_a?(String))
335
+ option = "-r#{identifier}"
336
+ else
337
+ ""
338
+ end
339
+ end
340
+
341
+ def commit_command(message)
342
+ "commit -m \"#{message}\""
343
+ end
344
+
345
+ def local?
346
+ protocol == "local"
347
+ end
348
+
349
+ def path
350
+ parse_cvs_root[3]
351
+ end
352
+
353
+ def protocol
354
+ parse_cvs_root[0]
355
+ end
356
+
357
+ # parses the root into tokens
358
+ # [protocol, user, host, path]
359
+ #
360
+ def parse_cvs_root
361
+ md = case
362
+ when root =~ /^:local:/ then /^:(local):(.*)/.match(root)
363
+ when root =~ /^:ext:/ then /^:(ext):(.*)@(.*):(.*)/.match(root)
364
+ when root =~ /^:pserver:/ then /^:(pserver):(.*)@(.*):(.*)/.match(root)
365
+ end
366
+ result = case
367
+ when root =~ /^:local:/ then [md[1], nil, nil, md[2]]
368
+ when root =~ /^:ext:/ then md[1..4]
369
+ when root =~ /^:pserver:/ then md[1..4]
370
+ else ["local", nil, nil, root]
371
+ end
372
+ end
373
+ end
374
+ end