whistle 0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. data/History.txt +3 -0
  2. data/README.txt +38 -0
  3. data/bin/whistle +90 -0
  4. data/lib/config.rb +19 -0
  5. data/lib/phash.rb +16 -0
  6. data/lib/relay.rb +24 -0
  7. data/lib/resource.rb +113 -0
  8. data/lib/ssl_patch.rb +15 -0
  9. data/lib/switchbox.rb +54 -0
  10. data/lib/time_ext.rb +30 -0
  11. data/lib/version.rb +3 -0
  12. data/sample/config.yml +12 -0
  13. data/vendor/rscm-0.5.1-patched-stripped/README +218 -0
  14. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm.rb +14 -0
  15. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/abstract_log_parser.rb +35 -0
  16. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/base.rb +289 -0
  17. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/command_line.rb +146 -0
  18. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/difftool.rb +44 -0
  19. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/line_editor.rb +46 -0
  20. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/mockit.rb +157 -0
  21. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/parser.rb +39 -0
  22. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/path_converter.rb +60 -0
  23. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/platform.rb +26 -0
  24. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/revision.rb +103 -0
  25. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/revision_file.rb +85 -0
  26. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/revision_poller.rb +93 -0
  27. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/revisions.rb +79 -0
  28. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/scm/clearcase.rb +182 -0
  29. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/scm/cvs.rb +374 -0
  30. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/scm/cvs_log_parser.rb +154 -0
  31. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/scm/darcs.rb +120 -0
  32. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/scm/darcs_log_parser.rb +65 -0
  33. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/scm/monotone.rb +338 -0
  34. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/scm/monotone_log_parser.rb +109 -0
  35. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/scm/mooky.rb +6 -0
  36. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/scm/perforce.rb +216 -0
  37. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/scm/star_team.rb +104 -0
  38. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/scm/subversion.rb +397 -0
  39. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/scm/subversion_log_parser.rb +165 -0
  40. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/tempdir.rb +17 -0
  41. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/time_ext.rb +11 -0
  42. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/version.rb +13 -0
  43. data/vendor/ruby-feedparser-0.5-stripped/README +14 -0
  44. data/vendor/ruby-feedparser-0.5-stripped/lib/feedparser.rb +28 -0
  45. data/vendor/ruby-feedparser-0.5-stripped/lib/feedparser/feedparser.rb +300 -0
  46. data/vendor/ruby-feedparser-0.5-stripped/lib/feedparser/filesizes.rb +12 -0
  47. data/vendor/ruby-feedparser-0.5-stripped/lib/feedparser/html-output.rb +126 -0
  48. data/vendor/ruby-feedparser-0.5-stripped/lib/feedparser/html2text-parser.rb +409 -0
  49. data/vendor/ruby-feedparser-0.5-stripped/lib/feedparser/rexml_patch.rb +28 -0
  50. data/vendor/ruby-feedparser-0.5-stripped/lib/feedparser/sgml-parser.rb +332 -0
  51. data/vendor/ruby-feedparser-0.5-stripped/lib/feedparser/text-output.rb +83 -0
  52. data/vendor/ruby-feedparser-0.5-stripped/lib/feedparser/textconverters.rb +120 -0
  53. metadata +132 -0
@@ -0,0 +1,85 @@
1
+ module RSCM
2
+ # Represents a file within a Revision, and also information about how this file
3
+ # was modified compared with the previous revision.
4
+ class RevisionFile
5
+
6
+ MODIFIED = "MODIFIED"
7
+ DELETED = "DELETED"
8
+ ADDED = "ADDED"
9
+ MOVED = "MOVED"
10
+
11
+ # MODIFIED, DELETED, ADDED or MOVED
12
+ attr_accessor :status
13
+
14
+ # Relative path from the root of the RSCM::Base instance
15
+ attr_accessor :path
16
+
17
+ # The native SCM's previous revision for this file. For non-transactional SCMs this is different from
18
+ # the parent Revision's
19
+ attr_accessor :previous_native_revision_identifier
20
+
21
+ # The native SCM's revision for this file. For non-transactional SCMs this is different from
22
+ # the parent Revision's
23
+ attr_accessor :native_revision_identifier
24
+
25
+ # The developer who modified this file
26
+ attr_accessor :developer
27
+
28
+ # The commit message for this file
29
+ attr_accessor :message
30
+
31
+ # This is a UTC ruby time
32
+ attr_accessor :time
33
+
34
+ def initialize(path=nil, status=nil, developer=nil, message=nil, native_revision_identifier=nil, time=nil)
35
+ @path, @developer, @message, @native_revision_identifier, @time, @status = path, developer, message, native_revision_identifier, time, status
36
+ end
37
+
38
+ def to_yaml_properties #:nodoc:
39
+ # We remove properties that are duplicated in the parent revision.
40
+ props = instance_variables
41
+ props.delete("@developer")
42
+ props.delete("@message")
43
+ props.delete("@time")
44
+ props.sort!
45
+ end
46
+
47
+ # Returns/yields an IO containing the contents of this file, using the +scm+ this
48
+ # file lives in.
49
+ def open(scm, options={}, &block) #:yield: io
50
+ scm.open(path, native_revision_identifier, options, &block)
51
+ end
52
+
53
+ # Yields the diff as an IO for this file
54
+ def diff(scm, 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)
62
+ end
63
+
64
+ # Accepts a visitor that must respond to +visit_file(revision_file)+
65
+ def accept(visitor)
66
+ visitor.visit_file(self)
67
+ end
68
+
69
+ # A simple string representation. Useful for debugging.
70
+ def to_s
71
+ "#{path} | #{native_revision_identifier}"
72
+ end
73
+
74
+ def ==(other)
75
+ return false if !other.is_a?(self.class)
76
+ self.status == other.status &&
77
+ self.path == other.path &&
78
+ self.developer == other.developer &&
79
+ self.message == other.message &&
80
+ self.native_revision_identifier == other.native_revision_identifier &&
81
+ self.time == other.time
82
+ end
83
+
84
+ end
85
+ end
@@ -0,0 +1,93 @@
1
+ module RSCM
2
+ module RevisionPoller
3
+ attr_accessor :logger
4
+
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
10
+
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
40
+ else
41
+ point_time = now
42
+ point_identifier = point
43
+ end
44
+
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
+ end
53
+ from = Time.epoch if from < Time.epoch
54
+ else
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
61
+ end
62
+
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
80
+ end
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)
90
+ end
91
+
92
+ end
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
@@ -0,0 +1,182 @@
1
+ require 'rscm/base'
2
+ require 'rscm/path_converter'
3
+ require 'fileutils'
4
+ require 'tempfile'
5
+
6
+ module RSCM
7
+ class ClearCase < Base
8
+
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
20
+
21
+ attr_accessor :stream, :stgloc, :tag, :config_spec
22
+
23
+ def initialize(stream=nil, stgloc=nil, tag=nil, config_spec=DEFAULT_CONFIG_SPEC)
24
+ @stream, @stgloc, @tag, @config_spec = stream, stgloc, tag, config_spec
25
+ end
26
+
27
+ def revisions(from_identifier, options={})
28
+ options = {
29
+ :from_identifier => from_identifier,
30
+ :to_identifier => Time.infinity,
31
+ :relative_path => nil
32
+ }.merge(options)
33
+
34
+ checkout unless checked_out?
35
+ rules = load_rules
36
+ vob = vob(rules[0])
37
+ result = Revisions.new
38
+
39
+ unless vob
40
+ STDERR.puts "No vob found. Please set load rules in the view: #{checkout_dir}"
41
+ return result
42
+ end
43
+ with_working_dir(checkout_dir) do
44
+ since = (from_identifier + 1).strftime(TIME_FORMAT)
45
+ cmd = "cleartool lshistory -recurse -nco -since #{since} -fmt \"#{LOG_FORMAT}\" -pname #{vob}"
46
+ execute(cmd, options) do |io|
47
+ # escape all quotes, except the one at the beginning and end. this is a bit ugly...
48
+ raw_yaml = io.read
49
+ fixed_yaml = raw_yaml.gsub(/^ message: \"/, " message: #{MAGIC_TOKEN}")
50
+ fixed_yaml = fixed_yaml.gsub(/\"\n\n/, "#{MAGIC_TOKEN}\n\n")
51
+ fixed_yaml = fixed_yaml.gsub(/\"/, "\\\"")
52
+ fixed_yaml = fixed_yaml.gsub(MAGIC_TOKEN, "\"")
53
+
54
+ files = YAML.load(fixed_yaml)
55
+ files.each do |file|
56
+ file.path.gsub!(/\\/, "/")
57
+ file.status = STATUSES[file.status]
58
+ rev = revision(file.native_revision_identifier)
59
+ if(rev && matches_load_rules?(rules, file.path))
60
+ file.native_revision_identifier = rev
61
+ file.previous_native_revision_identifier = revision(file.previous_native_revision_identifier)
62
+ t = file.time
63
+ # the time now has escaped quotes..
64
+ file.time = Time.utc(t[2..5],t[6..7],t[8..9],t[11..12],t[13..14],t[15..16])
65
+ file.message.strip!
66
+ result.add(file)
67
+ end
68
+ end
69
+ end
70
+ end
71
+ result
72
+ end
73
+
74
+ def checked_out?
75
+ !Dir["#{checkout_dir}/*"].empty?
76
+ end
77
+
78
+ def destroy_working_copy(options={})
79
+ execute("cleartool rmview #{checkout_dir}", options) do |io|
80
+ io.read
81
+ end
82
+ end
83
+
84
+ def import_central(options={})
85
+ execute("clearfsimport -recurse -nsetevent #{options[:dir]} #{checkout_dir}", options) do |io|
86
+ io.read
87
+ end
88
+ end
89
+
90
+ ## Non-RSCM API methods
91
+
92
+ def mkview!(options={})
93
+ # Create view (working copy)
94
+ mkview_cmd = "cleartool mkview -snapshot -stream #{@stream} -stgloc #{@stgloc} -tag #{@tag} #{@checkout_dir}"
95
+ execute(mkview_cmd, options) do |io|
96
+ puts io.read
97
+ end
98
+ end
99
+
100
+ def update_load_rules!(options={})
101
+ Dir.chdir(checkout_dir) do
102
+ # tempfile is broken on windows (!!)
103
+ cfg_spec_file = "__rscm.cfgspec"
104
+ config_spec_file = File.open(cfg_spec_file, "w") do |io|
105
+ io.write(@config_spec)
106
+ end
107
+
108
+ setcs_cmd = "cleartool setcs #{cfg_spec_file}"
109
+ Better.popen(setcs_cmd, "w") do |io|
110
+ io.write "yes\n"
111
+ end
112
+ end
113
+ end
114
+
115
+ def catcs(options={})
116
+ Dir.chdir(checkout_dir) do
117
+ catcs_cmd = "cleartool catcs"
118
+ execute(catcs_cmd, options) do |io|
119
+ yield io
120
+ end
121
+ end
122
+ end
123
+
124
+ def vob(load_rule)
125
+ if(load_rule =~ /[\\\/]*([\w]*)/)
126
+ $1
127
+ else
128
+ nil
129
+ end
130
+ end
131
+
132
+ # What's loaded into view
133
+ def load_rules
134
+ result = []
135
+ catcs do |io|
136
+ io.each_line do |line|
137
+ if(line =~ /^load[\s]*(.*)$/)
138
+ return result << $1
139
+ end
140
+ end
141
+ end
142
+ result
143
+ end
144
+
145
+ protected
146
+
147
+ def checkout_silent(to_identifier, options={}, &proc)
148
+ if(checked_out?)
149
+ execute("cleartool update .", options)
150
+ else
151
+ mkview!
152
+
153
+ # Set load rules (by setting config spec)
154
+ #update_load_rules!
155
+ end
156
+ end
157
+
158
+ # Administrative files that should be ignored when counting files.
159
+ def ignore_paths
160
+ return [/.*\.updt/]
161
+ end
162
+
163
+ private
164
+
165
+ def revision(s)
166
+ if(s =~ /.*\\([\d]*)/)
167
+ $1.to_i
168
+ else
169
+ nil
170
+ end
171
+ end
172
+
173
+ def matches_load_rules?(rules, path)
174
+ rules.each do |rule|
175
+ rule.gsub!(/\\/, "/")
176
+ return true if path =~ /#{rule[1..-1]}/
177
+ end
178
+ false
179
+ end
180
+
181
+ end
182
+ end
@@ -0,0 +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 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