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
@@ -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