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.
- data/History.txt +3 -0
- data/README.txt +38 -0
- data/bin/whistle +90 -0
- data/lib/config.rb +19 -0
- data/lib/phash.rb +16 -0
- data/lib/relay.rb +24 -0
- data/lib/resource.rb +113 -0
- data/lib/ssl_patch.rb +15 -0
- data/lib/switchbox.rb +54 -0
- data/lib/time_ext.rb +30 -0
- data/lib/version.rb +3 -0
- data/sample/config.yml +12 -0
- data/vendor/rscm-0.5.1-patched-stripped/README +218 -0
- data/vendor/rscm-0.5.1-patched-stripped/lib/rscm.rb +14 -0
- data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/abstract_log_parser.rb +35 -0
- data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/base.rb +289 -0
- data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/command_line.rb +146 -0
- data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/difftool.rb +44 -0
- data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/line_editor.rb +46 -0
- data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/mockit.rb +157 -0
- data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/parser.rb +39 -0
- data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/path_converter.rb +60 -0
- data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/platform.rb +26 -0
- data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/revision.rb +103 -0
- data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/revision_file.rb +85 -0
- data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/revision_poller.rb +93 -0
- data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/revisions.rb +79 -0
- data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/scm/clearcase.rb +182 -0
- data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/scm/cvs.rb +374 -0
- data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/scm/cvs_log_parser.rb +154 -0
- data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/scm/darcs.rb +120 -0
- data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/scm/darcs_log_parser.rb +65 -0
- data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/scm/monotone.rb +338 -0
- data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/scm/monotone_log_parser.rb +109 -0
- data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/scm/mooky.rb +6 -0
- data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/scm/perforce.rb +216 -0
- data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/scm/star_team.rb +104 -0
- data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/scm/subversion.rb +397 -0
- data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/scm/subversion_log_parser.rb +165 -0
- data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/tempdir.rb +17 -0
- data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/time_ext.rb +11 -0
- data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/version.rb +13 -0
- data/vendor/ruby-feedparser-0.5-stripped/README +14 -0
- data/vendor/ruby-feedparser-0.5-stripped/lib/feedparser.rb +28 -0
- data/vendor/ruby-feedparser-0.5-stripped/lib/feedparser/feedparser.rb +300 -0
- data/vendor/ruby-feedparser-0.5-stripped/lib/feedparser/filesizes.rb +12 -0
- data/vendor/ruby-feedparser-0.5-stripped/lib/feedparser/html-output.rb +126 -0
- data/vendor/ruby-feedparser-0.5-stripped/lib/feedparser/html2text-parser.rb +409 -0
- data/vendor/ruby-feedparser-0.5-stripped/lib/feedparser/rexml_patch.rb +28 -0
- data/vendor/ruby-feedparser-0.5-stripped/lib/feedparser/sgml-parser.rb +332 -0
- data/vendor/ruby-feedparser-0.5-stripped/lib/feedparser/text-output.rb +83 -0
- data/vendor/ruby-feedparser-0.5-stripped/lib/feedparser/textconverters.rb +120 -0
- 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
|