whistle 0.1

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