whistle 0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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,109 @@
|
|
1
|
+
require 'rscm'
|
2
|
+
require 'time'
|
3
|
+
require 'stringio'
|
4
|
+
|
5
|
+
module RSCM
|
6
|
+
|
7
|
+
class MonotoneLogParser
|
8
|
+
|
9
|
+
def parse_revisions(io, from_identifier=Time.epoch, to_identifier=Time.infinity)
|
10
|
+
# skip first separator
|
11
|
+
io.readline
|
12
|
+
|
13
|
+
all_revisions = []
|
14
|
+
revision_string = ""
|
15
|
+
|
16
|
+
# hash of path => [array of revisions]
|
17
|
+
path_revisions = {}
|
18
|
+
io.each_line do |line|
|
19
|
+
if(line =~ /-----------------------------------------------------------------/)
|
20
|
+
revision = parse_revision(StringIO.new(revision_string), path_revisions)
|
21
|
+
all_revisions << revision
|
22
|
+
revision_string = ""
|
23
|
+
else
|
24
|
+
revision_string << line
|
25
|
+
end
|
26
|
+
end
|
27
|
+
revision = parse_revision(StringIO.new(revision_string), path_revisions)
|
28
|
+
all_revisions << revision
|
29
|
+
|
30
|
+
# Filter out the revisions and set the previous revisions, knowing that most recent is at index 0.
|
31
|
+
|
32
|
+
from_time = time(all_revisions, from_identifier, Time.epoch)
|
33
|
+
to_time = time(all_revisions, to_identifier, Time.infinity)
|
34
|
+
|
35
|
+
revisions = Revisions.new
|
36
|
+
|
37
|
+
all_revisions.each do |revision|
|
38
|
+
if((from_time < revision.time) && (revision.time <= to_time))
|
39
|
+
revisions.add(revision)
|
40
|
+
revision.each do |change|
|
41
|
+
current_index = path_revisions[change.path].index(change.native_revision_identifier)
|
42
|
+
change.previous_native_revision_identifier = path_revisions[change.path][current_index + 1]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
revisions
|
47
|
+
end
|
48
|
+
|
49
|
+
def parse_revision(revision_io, path_revisions)
|
50
|
+
revision = Revision.new
|
51
|
+
state = nil
|
52
|
+
revision_io.each_line do |line|
|
53
|
+
if(line =~ /^Revision: (.*)$/ && revision.identifier.nil?)
|
54
|
+
revision.identifier = $1
|
55
|
+
elsif(line =~ /^Author: (.*)$/ && revision.developer.nil?)
|
56
|
+
revision.developer = $1
|
57
|
+
elsif(line =~ /^Date: (.*)$/ && revision.time.nil?)
|
58
|
+
revision.time = Time.utc(
|
59
|
+
$1[0..3].to_i,
|
60
|
+
$1[5..6].to_i,
|
61
|
+
$1[8..9].to_i,
|
62
|
+
$1[11..12].to_i,
|
63
|
+
$1[14..15].to_i,
|
64
|
+
$1[17..18].to_i
|
65
|
+
)
|
66
|
+
elsif(line =~ /^ChangeLog:\s*$/ && revision.message.nil?)
|
67
|
+
state = :message
|
68
|
+
elsif(state == :message && revision.message.nil?)
|
69
|
+
revision.message = ""
|
70
|
+
elsif(state == :message && revision.message)
|
71
|
+
revision.message << line
|
72
|
+
elsif(line =~ /^Added files:\s*$/)
|
73
|
+
state = :added
|
74
|
+
elsif(state == :added)
|
75
|
+
add_changes(revision, line, RevisionFile::ADDED, path_revisions)
|
76
|
+
elsif(line =~ /^Modified files:\s*$/)
|
77
|
+
state = :modified
|
78
|
+
elsif(state == :modified)
|
79
|
+
add_changes(revision, line, RevisionFile::MODIFIED, path_revisions)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
revision.message.chomp! rescue revision.message = ''
|
83
|
+
revision
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def time(revisions, identifier, default)
|
89
|
+
cs = revisions.find do |revision|
|
90
|
+
revision.identifier == identifier
|
91
|
+
end
|
92
|
+
cs ? cs.time : (identifier.is_a?(Time) ? identifier : default)
|
93
|
+
end
|
94
|
+
|
95
|
+
def add_changes(revision, line, state, path_revisions)
|
96
|
+
paths = line.split(" ")
|
97
|
+
paths.each do |path|
|
98
|
+
revision << RevisionFile.new(path, state, revision.developer, nil, revision.identifier, revision.time)
|
99
|
+
|
100
|
+
# now record path revisions so we can keep track of previous rev for each path
|
101
|
+
# doesn't work for moved files, and have no idea how to make it work either.
|
102
|
+
path_revisions[path] ||= []
|
103
|
+
path_revisions[path] << revision.identifier
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
@@ -0,0 +1,216 @@
|
|
1
|
+
# TODO
|
2
|
+
# Support int revision numbers AND dates
|
3
|
+
# Leverage default P4 client settings (optional)
|
4
|
+
|
5
|
+
require 'rscm/base'
|
6
|
+
require 'rscm/path_converter'
|
7
|
+
require 'rscm/line_editor'
|
8
|
+
|
9
|
+
require 'fileutils'
|
10
|
+
require 'time'
|
11
|
+
require 'socket'
|
12
|
+
|
13
|
+
module RSCM
|
14
|
+
class Perforce < Base
|
15
|
+
unless defined? DATE_FORMAT
|
16
|
+
DATE_FORMAT = "%Y/%m/%d:%H:%M:%S"
|
17
|
+
# Doesn't work for empty messages, (Like 21358 in Aslak's P4 repo)
|
18
|
+
CHANGELIST_PATTERN = /^Change \d+ by (.*)@.* on (.*)\n\n(.*)\n\nAffected files ...\n\n(.*)/m
|
19
|
+
# But this one does
|
20
|
+
CHANGELIST_PATTERN_NO_MSG = /^Change \d+ by (.*)@.* on (.*)\n\nAffected files ...\n\n(.*)/m
|
21
|
+
|
22
|
+
STATES = {
|
23
|
+
"add" => RevisionFile::ADDED,
|
24
|
+
"edit" => RevisionFile::MODIFIED,
|
25
|
+
"delete" => RevisionFile::DELETED
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
attr_accessor :view
|
30
|
+
attr_accessor :username
|
31
|
+
attr_accessor :password
|
32
|
+
|
33
|
+
def installed?
|
34
|
+
begin
|
35
|
+
execute("p4 info", {})
|
36
|
+
true
|
37
|
+
rescue
|
38
|
+
false
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def revisions(from_identifier=Time.new.utc, options={})
|
43
|
+
raise "from_identifer cannot be nil" if from_identifier.nil?
|
44
|
+
set_utc_offset(options)
|
45
|
+
view_as_regexp = "^" + @view.gsub(/\.\.\./, "(.*)")
|
46
|
+
relative_path_pattern = Regexp.new(view_as_regexp)
|
47
|
+
|
48
|
+
from_identifier = Time.epoch unless from_identifier
|
49
|
+
from_identifier = Time.epoch if (from_identifier.is_a? Time and from_identifier < Time.epoch)
|
50
|
+
from = revision_spec(from_identifier + 1) # We have to add 1 because of the contract of this method.
|
51
|
+
|
52
|
+
to_identifier = options[:to_identifier] ? options[:to_identifier] : Time.infinity
|
53
|
+
to = revision_spec(to_identifier - 1) # We have to subtract 1 because of the contract of this method.
|
54
|
+
|
55
|
+
cmd = "p4 #{p4_opts(false)} changes #{@view}@#{from},#{to}"
|
56
|
+
revisions = Revisions.new
|
57
|
+
revisions.cmd = cmd if store_revisions_command?
|
58
|
+
|
59
|
+
changes = execute(cmd, options) do |io|
|
60
|
+
io.read
|
61
|
+
end
|
62
|
+
|
63
|
+
changes.each do |line|
|
64
|
+
revision = nil
|
65
|
+
identifier = line.match(/^Change (\d+)/)[1].to_i
|
66
|
+
|
67
|
+
execute("p4 #{p4_opts(false)} describe -s #{identifier}", options) do |io|
|
68
|
+
log = io.read
|
69
|
+
|
70
|
+
if log =~ CHANGELIST_PATTERN
|
71
|
+
developer, time, message, files = $1, $2, $3, $4
|
72
|
+
elsif log =~ CHANGELIST_PATTERN_NO_MSG
|
73
|
+
developer, time, files = $1, $2, $3
|
74
|
+
else
|
75
|
+
puts "PARSE ERROR:"
|
76
|
+
puts log
|
77
|
+
puts "\nDIDN'T MATCH:"
|
78
|
+
puts CHANGELIST_PATTERN
|
79
|
+
end
|
80
|
+
|
81
|
+
# The parsed time doesn't have timezone info. We'll tweak it.
|
82
|
+
time = Time.parse(time + " UTC") - @utc_offset
|
83
|
+
|
84
|
+
files.each_line do |line|
|
85
|
+
if line =~ /^\.\.\. (\/\/.+)#(\d+) (.+)/
|
86
|
+
depot_path = $1
|
87
|
+
file_identifier = $2.to_i
|
88
|
+
state = $3.strip
|
89
|
+
if(STATES[state])
|
90
|
+
if(depot_path =~ relative_path_pattern)
|
91
|
+
relative_path = $1
|
92
|
+
|
93
|
+
if revision.nil?
|
94
|
+
revision = Revision.new
|
95
|
+
revision.identifier = identifier
|
96
|
+
revision.developer = developer
|
97
|
+
revision.message = message
|
98
|
+
revision.time = time
|
99
|
+
revisions.add revision
|
100
|
+
end
|
101
|
+
|
102
|
+
file = RevisionFile.new
|
103
|
+
file.path = relative_path
|
104
|
+
file.native_revision_identifier = file_identifier
|
105
|
+
file.previous_native_revision_identifier = file.native_revision_identifier-1
|
106
|
+
file.status = STATES[state]
|
107
|
+
revision.add file
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
revisions
|
115
|
+
end
|
116
|
+
|
117
|
+
def destroy_working_copy(options={})
|
118
|
+
execute("p4 #{p4_opts(false)} client -d #{client_name}", options)
|
119
|
+
end
|
120
|
+
|
121
|
+
def open(revision_file, options={}, &block)
|
122
|
+
path = @view.gsub(/\.\.\./, revision_file.path) # + "@" + revision_file.native_revision_identifier
|
123
|
+
cmd = "p4 #{p4_opts(false)} print -q #{path}"
|
124
|
+
execute(cmd, options) do |io|
|
125
|
+
block.call io
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def diff
|
130
|
+
#p4 diff2 //depot/trunk/build.xml@26405 //depot/trunk/build.xml@26409
|
131
|
+
end
|
132
|
+
|
133
|
+
protected
|
134
|
+
|
135
|
+
def checkout_silent(to_identifier, options)
|
136
|
+
checkout_dir = PathConverter.filepath_to_nativepath(@checkout_dir, false)
|
137
|
+
FileUtils.mkdir_p(@checkout_dir)
|
138
|
+
|
139
|
+
ensure_client(options)
|
140
|
+
execute("p4 #{p4_opts} sync #{@view}@#{to_identifier}", options)
|
141
|
+
end
|
142
|
+
|
143
|
+
def ignore_paths
|
144
|
+
[]
|
145
|
+
end
|
146
|
+
|
147
|
+
private
|
148
|
+
|
149
|
+
def p4_opts(with_client=true)
|
150
|
+
user_opt = @username.to_s.empty? ? "" : "-u #{@username}"
|
151
|
+
password_opt = @password.to_s.empty? ? "" : "-P #{@password}"
|
152
|
+
client_opt = with_client ? "-c \"#{client_name}\"" : ""
|
153
|
+
"#{user_opt} #{password_opt} #{client_opt}"
|
154
|
+
end
|
155
|
+
|
156
|
+
def client_name
|
157
|
+
raise "checkout_dir not set" unless @checkout_dir
|
158
|
+
Socket.gethostname + ":" + @checkout_dir
|
159
|
+
end
|
160
|
+
|
161
|
+
def ensure_client(options)
|
162
|
+
create_client(options)
|
163
|
+
end
|
164
|
+
|
165
|
+
def create_client(options)
|
166
|
+
options = {:mode => "w+"}.merge(options)
|
167
|
+
FileUtils.mkdir_p(@checkout_dir)
|
168
|
+
execute("p4 #{p4_opts(false)} client -i", options) do |io|
|
169
|
+
io.puts(client_spec)
|
170
|
+
io.close_write
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def client_spec
|
175
|
+
<<-EOF
|
176
|
+
Client: #{client_name}
|
177
|
+
Owner: #{@username}
|
178
|
+
Host: #{Socket.gethostname}
|
179
|
+
Description: RSCM client
|
180
|
+
Root: #{@checkout_dir}
|
181
|
+
Options: noallwrite noclobber nocompress unlocked nomodtime normdir
|
182
|
+
LineEnd: local
|
183
|
+
View: #{@view} //#{client_name}/...
|
184
|
+
EOF
|
185
|
+
end
|
186
|
+
|
187
|
+
def revision_spec(identifier)
|
188
|
+
if identifier.is_a?(Time)
|
189
|
+
# The p4 client uses local time, but rscm uses utc
|
190
|
+
# We have to convert to local time
|
191
|
+
identifier += @utc_offset
|
192
|
+
identifier.strftime(DATE_FORMAT)
|
193
|
+
else
|
194
|
+
identifier.to_i
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
# Queries the server for the time offset. Required in order to get proper
|
199
|
+
# timezone for revisions
|
200
|
+
def set_utc_offset(options)
|
201
|
+
unless @utc_offset
|
202
|
+
execute("p4 #{p4_opts(false)} info", options) do |io|
|
203
|
+
io.each do |line|
|
204
|
+
if line =~ /^Server date: (.*)/
|
205
|
+
server_time = Time.parse($1)
|
206
|
+
@utc_offset = server_time.utc_offset
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
raise "Couldn't get server's UTC offset" if @utc_offset.nil?
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
end
|
215
|
+
|
216
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'tempfile'
|
3
|
+
require 'rscm/revision'
|
4
|
+
require 'rscm/base'
|
5
|
+
require 'yaml'
|
6
|
+
|
7
|
+
class Time
|
8
|
+
def to_rfc2822
|
9
|
+
utc.strftime("%a, %d %b %Y %H:%M:%S +0000")
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
module RSCM
|
14
|
+
# The RSCM StarTeam class requires that the following software be installed:
|
15
|
+
#
|
16
|
+
# * Java Runtime (1.4.2)
|
17
|
+
# * StarTeam SDK
|
18
|
+
# * Apache Ant (http://ant.apache.org/)
|
19
|
+
#
|
20
|
+
class StarTeam < Base
|
21
|
+
attr_accessor :user_name
|
22
|
+
attr_accessor :password
|
23
|
+
attr_accessor :server_name
|
24
|
+
attr_accessor :server_port
|
25
|
+
attr_accessor :project_name
|
26
|
+
attr_accessor :view_name
|
27
|
+
attr_accessor :folder_name
|
28
|
+
|
29
|
+
def initialize(user_name="", password="", server_name="", server_port="", project_name="", view_name="", folder_name="")
|
30
|
+
@user_name, @password, @server_name, @server_port, @project_name, @view_name, @folder_name = user_name, password, server_name, server_port, project_name, view_name, folder_name
|
31
|
+
end
|
32
|
+
|
33
|
+
def revisions(checkout_dir, from_identifier=Time.epoch, to_identifier=Time.infinity, &proc)
|
34
|
+
# just assuming it is a Time for now, may support labels later.
|
35
|
+
# the java class really wants rfc822 and not rfc2822, but this works ok anyway.
|
36
|
+
from = from_identifier.to_rfc2822
|
37
|
+
to = to_identifier.to_rfc2822
|
38
|
+
|
39
|
+
revisions = java("getRevisions(\"#{from}\";\"#{to}\")", &proc)
|
40
|
+
raise "revisions must be of type #{Revisions.name} - was #{revisions.class.name}" unless revisions.is_a?(::RSCM::Revisions)
|
41
|
+
|
42
|
+
# Just a little sanity check
|
43
|
+
if(revisions.latest)
|
44
|
+
latetime = revisions.latest.time
|
45
|
+
if(latetime < from_identifier || to_identifier < latetime)
|
46
|
+
raise "Latest time (#{latetime}) is not within #{from_identifier}-#{to_identifier}"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
revisions
|
50
|
+
end
|
51
|
+
|
52
|
+
def checkout(checkout_dir, to_identifier, &proc)
|
53
|
+
# TODO: Take the to_identifier arg into consideration
|
54
|
+
files = java("checkout(\"#{checkout_dir}\")", &proc)
|
55
|
+
files
|
56
|
+
end
|
57
|
+
|
58
|
+
def supports_trigger?
|
59
|
+
true
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def cmd
|
65
|
+
rscm_jar = File.expand_path(File.dirname(__FILE__) + "../../../../ext/rscm.jar")
|
66
|
+
starteam_jars = Dir["#{ENV['RSCM_STARTEAM']}/Lib/*jar"].join(File::PATH_SEPARATOR)
|
67
|
+
ant_jars = Dir["#{ENV['ANT_HOME']}/lib/*jar"].join(File::PATH_SEPARATOR)
|
68
|
+
classpath = "#{rscm_jar}#{File::PATH_SEPARATOR}#{ant_jars}#{File::PATH_SEPARATOR}#{starteam_jars}"
|
69
|
+
|
70
|
+
"java -Djava.library.path=\"#{ENV['RSCM_STARTEAM']}#{File::SEPARATOR}Lib\" -classpath \"#{classpath}\" org.rubyforge.rscm.Main"
|
71
|
+
end
|
72
|
+
|
73
|
+
def java(m, &proc)
|
74
|
+
raise "The RSCM_STARTEAM environment variable must be defined and point to the StarTeam SDK directory" unless ENV['RSCM_STARTEAM']
|
75
|
+
raise "The ANT_HOME environment variable must be defined and point to the Ant installation directory" unless ENV['ANT_HOME']
|
76
|
+
|
77
|
+
clazz = "org.rubyforge.rscm.starteam.StarTeam"
|
78
|
+
ctor_args = "#{@user_name};#{@password};#{@server_name};#{@server_port};#{@project_name};#{@view_name};#{@folder_name}"
|
79
|
+
|
80
|
+
# Uncomment if you're not Aslak - to run against a bogus java class.
|
81
|
+
# clazz = "org.rubyforge.rscm.TestScm"
|
82
|
+
# ctor_args = "hubba;bubba"
|
83
|
+
|
84
|
+
command = "new #{clazz}(#{ctor_args}).#{m}"
|
85
|
+
tf = Tempfile.new("rscm_starteam")
|
86
|
+
tf.puts(command)
|
87
|
+
tf.close
|
88
|
+
cmdline = "#{cmd} #{tf.path}"
|
89
|
+
IO.popen(cmdline) do |io|
|
90
|
+
yaml_source = io
|
91
|
+
if(block_given?)
|
92
|
+
yaml_source = ""
|
93
|
+
io.each_line do |line|
|
94
|
+
yield line
|
95
|
+
yaml_source << line << "\n"
|
96
|
+
end
|
97
|
+
else
|
98
|
+
end
|
99
|
+
YAML::load(yaml_source)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,397 @@
|
|
1
|
+
require 'rscm/base'
|
2
|
+
require 'rscm/path_converter'
|
3
|
+
require 'rscm/line_editor'
|
4
|
+
require 'rscm/scm/subversion_log_parser'
|
5
|
+
require 'stringio'
|
6
|
+
|
7
|
+
module RSCM
|
8
|
+
|
9
|
+
# RSCM implementation for Subversion.
|
10
|
+
#
|
11
|
+
# You need the svn/svnadmin executable on the PATH in order for it to work.
|
12
|
+
#
|
13
|
+
# NOTE: On Cygwin these have to be the win32 builds of svn/svnadmin and not the Cygwin ones.
|
14
|
+
class Subversion < Base
|
15
|
+
|
16
|
+
include FileUtils
|
17
|
+
include PathConverter
|
18
|
+
|
19
|
+
attr_accessor :url
|
20
|
+
# Must be specified in order to create repo and install triggers.
|
21
|
+
# Must also be specified as "" if the url represents the root of the repository, or files in revisions will not be detected.
|
22
|
+
attr_accessor :path
|
23
|
+
attr_accessor :username
|
24
|
+
attr_accessor :password
|
25
|
+
|
26
|
+
def initialize(url="", path="")
|
27
|
+
@url, @path = url, path
|
28
|
+
@username = ""
|
29
|
+
@password = ""
|
30
|
+
end
|
31
|
+
|
32
|
+
def path
|
33
|
+
@path ||= ""
|
34
|
+
end
|
35
|
+
|
36
|
+
def installed?
|
37
|
+
begin
|
38
|
+
svn("--version", {})
|
39
|
+
true
|
40
|
+
rescue
|
41
|
+
false
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def to_identifier(raw_identifier)
|
46
|
+
raw_identifier.to_i
|
47
|
+
end
|
48
|
+
|
49
|
+
def add(relative_filename, options={})
|
50
|
+
svn("add #{checkout_dir}/#{relative_filename}", options)
|
51
|
+
end
|
52
|
+
|
53
|
+
def move(relative_src, relative_dest, options={})
|
54
|
+
svn("mv #{checkout_dir}/#{relative_src} #{checkout_dir}/#{relative_dest}", options)
|
55
|
+
end
|
56
|
+
|
57
|
+
def transactional?
|
58
|
+
true
|
59
|
+
end
|
60
|
+
|
61
|
+
def uptodate?(identifier, options={})
|
62
|
+
if(!checked_out?)
|
63
|
+
false
|
64
|
+
else
|
65
|
+
rev = identifier.nil? ? head_revision_identifier(options) : identifier
|
66
|
+
local_revision_identifier(options) == rev
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def commit(message, options={})
|
71
|
+
svn(commit_command(message), options)
|
72
|
+
# We have to do an update to get the local revision right
|
73
|
+
checkout_silent(nil, options)
|
74
|
+
end
|
75
|
+
|
76
|
+
def label
|
77
|
+
local_revision_identifier.to_s
|
78
|
+
end
|
79
|
+
|
80
|
+
def diff(path, from, to, options={}, &block)
|
81
|
+
cmd = "svn diff --revision #{from}:#{to} \"#{url}/#{path}\""
|
82
|
+
execute(cmd, options) do |io|
|
83
|
+
return(block.call(io))
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def open(path, native_revision_identifier, options={}, &block)
|
88
|
+
raise "native_revision_identifier cannot be nil" if native_revision_identifier.nil?
|
89
|
+
cmd = "svn cat #{url}/#{path}@#{native_revision_identifier}"
|
90
|
+
execute(cmd, options) do |io|
|
91
|
+
return(block.call(io))
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def can_create_central?
|
96
|
+
local? && !path.nil? && !(path == "")
|
97
|
+
end
|
98
|
+
|
99
|
+
def destroy_central
|
100
|
+
if(File.exist?(svnrootdir) && local?)
|
101
|
+
FileUtils.rm_rf(svnrootdir)
|
102
|
+
else
|
103
|
+
raise "Cannot destroy central repository. '#{svnrootdir}' doesn't exist or central repo isn't local to this machine"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def central_exists?
|
108
|
+
if(local?)
|
109
|
+
File.exists?("#{svnrootdir}/db")
|
110
|
+
else
|
111
|
+
# Do a simple command over the network
|
112
|
+
# If the repo/path doesn't exist, we'll get zero output
|
113
|
+
# on stdout (and an error msg on std err).
|
114
|
+
exists = false
|
115
|
+
cmd = "svn log #{url} -r HEAD"
|
116
|
+
execute(cmd) do |stdout|
|
117
|
+
stdout.each_line do |line|
|
118
|
+
exists = true
|
119
|
+
end
|
120
|
+
end
|
121
|
+
exists
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def supports_trigger?
|
126
|
+
true
|
127
|
+
# we'll assume it supports trigger even if not local. this is to ensure user interfaces
|
128
|
+
# can display appropriate options, even if the object is not 'fully initialised'
|
129
|
+
# local?
|
130
|
+
end
|
131
|
+
|
132
|
+
def trigger_mechanism
|
133
|
+
"hooks/post-commit"
|
134
|
+
end
|
135
|
+
|
136
|
+
def create_central(options={})
|
137
|
+
options = options.dup.merge({:dir => svnrootdir})
|
138
|
+
native_path = PathConverter.filepath_to_nativepath(svnrootdir, false)
|
139
|
+
mkdir_p(PathConverter.nativepath_to_filepath(native_path))
|
140
|
+
svnadmin("create #{native_path}", options)
|
141
|
+
if(path != "")
|
142
|
+
options = options.dup.merge({:dir => "."})
|
143
|
+
# create the directories
|
144
|
+
paths = path.split("/")
|
145
|
+
paths.each_with_index do |p,i|
|
146
|
+
p = paths[0..i]
|
147
|
+
u = "#{repourl}/#{p.join('/')}"
|
148
|
+
svn("mkdir #{u} -m \"Adding directories\"", options)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def install_trigger(trigger_command, trigger_files_checkout_dir, options={})
|
154
|
+
if (WINDOWS)
|
155
|
+
install_win_trigger(trigger_command, trigger_files_checkout_dir, options)
|
156
|
+
else
|
157
|
+
install_unix_trigger(trigger_command, trigger_files_checkout_dir, options)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def uninstall_trigger(trigger_command, trigger_files_checkout_dir, options={})
|
162
|
+
File.comment_out(post_commit_file, /#{Regexp.escape(trigger_command)}/, nil)
|
163
|
+
end
|
164
|
+
|
165
|
+
def trigger_installed?(trigger_command, trigger_files_checkout_dir, options={})
|
166
|
+
return false unless File.exist?(post_commit_file)
|
167
|
+
not_already_commented = LineEditor.comment_out(File.new(post_commit_file), /#{Regexp.escape(trigger_command)}/, "# ", "")
|
168
|
+
not_already_commented
|
169
|
+
end
|
170
|
+
|
171
|
+
def import_central(dir, options={})
|
172
|
+
import_cmd = "import #{dir} #{url} -m \"#{options[:message]}\""
|
173
|
+
svn(import_cmd, options)
|
174
|
+
end
|
175
|
+
|
176
|
+
def revisions(from_identifier=Time.new.utc, options={})
|
177
|
+
raise "from_identifer cannot be nil" if from_identifier.nil?
|
178
|
+
options = {
|
179
|
+
:from_identifier => from_identifier,
|
180
|
+
:to_identifier => Time.infinity,
|
181
|
+
:relative_path => "",
|
182
|
+
:dir => Dir.pwd
|
183
|
+
}.merge(options)
|
184
|
+
|
185
|
+
checkout_dir = PathConverter.filepath_to_nativepath(@checkout_dir, false)
|
186
|
+
revisions = nil
|
187
|
+
command = "svn #{changes_command(options[:from_identifier], options[:to_identifier], options[:relative_path])}"
|
188
|
+
execute(command, options) do |stdout|
|
189
|
+
stdout = StringIO.new(stdout.read)
|
190
|
+
parser = SubversionLogParser.new(stdout, @url, options[:from_identifier], options[:to_identifier], path)
|
191
|
+
revisions = parser.parse_revisions
|
192
|
+
end
|
193
|
+
revisions.cmd = command if store_revisions_command?
|
194
|
+
revisions
|
195
|
+
end
|
196
|
+
|
197
|
+
# url pointing to the root of the repo
|
198
|
+
def repourl
|
199
|
+
last = (path.nil? || path == "") ? -1 : -(path.length)-2
|
200
|
+
url[0..last]
|
201
|
+
end
|
202
|
+
|
203
|
+
def checked_out?
|
204
|
+
rootentries = File.expand_path("#{checkout_dir}/.svn/entries")
|
205
|
+
result = File.exists?(rootentries)
|
206
|
+
result
|
207
|
+
end
|
208
|
+
|
209
|
+
protected
|
210
|
+
|
211
|
+
def checkout_silent(to_identifier, options)
|
212
|
+
checkout_dir = PathConverter.filepath_to_nativepath(@checkout_dir, false)
|
213
|
+
mkdir_p(@checkout_dir)
|
214
|
+
if(checked_out?)
|
215
|
+
svn(update_command(to_identifier), options)
|
216
|
+
else
|
217
|
+
svn(checkout_command(to_identifier), options)
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
def ignore_paths
|
222
|
+
[/\.svn\/.*/]
|
223
|
+
end
|
224
|
+
|
225
|
+
private
|
226
|
+
|
227
|
+
def local_revision_identifier(options)
|
228
|
+
local_revision_identifier = nil
|
229
|
+
svn("info #{quoted_checkout_dir}", options) do |line|
|
230
|
+
if(line =~ /Revision: ([0-9]*)/)
|
231
|
+
return $1.to_i
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
def head_revision_identifier(options)
|
237
|
+
# This command only seems to yield any changesets if the url is the root of
|
238
|
+
# the repo, which we don't know in the case where path is not specified (likely)
|
239
|
+
# We therefore don't specify it and get the latest revision from the full url instead.
|
240
|
+
# cmd = "svn log #{login_options} #{repourl} -r HEAD"
|
241
|
+
cmd = "svn log #{login_options} #{url}"
|
242
|
+
execute(cmd, options) do |stdout|
|
243
|
+
parser = SubversionLogParser.new(stdout, @url)
|
244
|
+
revisions = parser.parse_revisions
|
245
|
+
revisions[0].identifier
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
def install_unix_trigger(trigger_command, damagecontrol_install_dir, options)
|
250
|
+
post_commit_exists = File.exists?(post_commit_file)
|
251
|
+
mode = post_commit_exists ? File::APPEND|File::WRONLY : File::CREAT|File::WRONLY
|
252
|
+
begin
|
253
|
+
File.open(post_commit_file, mode) do |file|
|
254
|
+
file.puts("#!/bin/sh") unless post_commit_exists
|
255
|
+
file.puts("#{trigger_command}\n" )
|
256
|
+
end
|
257
|
+
File.chmod(0744, post_commit_file)
|
258
|
+
rescue
|
259
|
+
raise ["Didn't have permission to write to #{post_commit_file}.",
|
260
|
+
"Try to manually add the following line:",
|
261
|
+
trigger_command,
|
262
|
+
"Finally make it executable with chmod g+x #{post_commit_file}"]
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
def install_win_trigger(trigger_command, damagecontrol_install_dir, options)
|
267
|
+
post_commit_exists = File.exists?(post_commit_file)
|
268
|
+
mode = post_commit_exists ? File::APPEND|File::WRONLY : File::CREAT|File::WRONLY
|
269
|
+
begin
|
270
|
+
File.open(post_commit_file, mode) do |file|
|
271
|
+
file.puts("#{trigger_command}\n" )
|
272
|
+
end
|
273
|
+
rescue
|
274
|
+
raise ["Didn't have permission to write to #{post_commit_file}.",
|
275
|
+
"Try to manually add the following line:",
|
276
|
+
trigger_command]
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
def svnrootdir
|
281
|
+
last = (path.nil? || path == "") ? -1 : -(path.length)-2
|
282
|
+
result = url["file://".length..last]
|
283
|
+
# for windows, turn /c:/blabla into c:/blabla"
|
284
|
+
if(result =~ /^\/[a-zA-Z]:/)
|
285
|
+
result = result[1..-1]
|
286
|
+
end
|
287
|
+
result
|
288
|
+
end
|
289
|
+
|
290
|
+
def svnadmin(cmd, options={}, &proc)
|
291
|
+
svncommand("svnadmin", cmd, options, &proc)
|
292
|
+
end
|
293
|
+
|
294
|
+
def svn(cmd, options={}, &proc)
|
295
|
+
svncommand("svn", cmd, options, &proc)
|
296
|
+
end
|
297
|
+
|
298
|
+
def svncommand(executable, cmd, options, &proc)
|
299
|
+
command_line = "#{executable} #{cmd}"
|
300
|
+
execute(command_line, options) do |stdout|
|
301
|
+
stdout.each_line do |line|
|
302
|
+
yield line if block_given?
|
303
|
+
end
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
def checkout_command(to_identifier)
|
308
|
+
"checkout #{login_options} #{url} #{revision_option(nil,to_identifier)} #{quoted_checkout_dir}"
|
309
|
+
end
|
310
|
+
|
311
|
+
def update_command(to_identifier)
|
312
|
+
"update #{login_options} #{revision_option(nil,to_identifier)} #{quoted_checkout_dir}"
|
313
|
+
end
|
314
|
+
|
315
|
+
def changes_command(from_identifier, to_identifier, relative_path)
|
316
|
+
# http://svnbook.red-bean.com/svnbook-1.1/svn-book.html#svn-ch-3-sect-3.3
|
317
|
+
# file_list = files.join('\n')
|
318
|
+
"log --verbose #{login_options} #{revision_option(from_identifier, to_identifier)} #{@url}"
|
319
|
+
end
|
320
|
+
|
321
|
+
def login_options
|
322
|
+
result = ""
|
323
|
+
u = @username ? @username.strip : ""
|
324
|
+
p = @password ? @password.strip : ""
|
325
|
+
result << "--username #{u} " unless u == ""
|
326
|
+
result << "--password #{p} " unless p == ""
|
327
|
+
result
|
328
|
+
end
|
329
|
+
|
330
|
+
def revision_option(from_identifier, to_identifier)
|
331
|
+
# The inclusive start
|
332
|
+
from = nil
|
333
|
+
if(from_identifier.is_a?(Time))
|
334
|
+
from = svndate(from_identifier)
|
335
|
+
elsif(from_identifier.is_a?(Numeric))
|
336
|
+
from = from_identifier
|
337
|
+
elsif(!from_identifier.nil?)
|
338
|
+
raise "from_identifier must be Numeric, Time or nil. Was: #{from_identifier} (#{from_identifier.class.name})"
|
339
|
+
end
|
340
|
+
|
341
|
+
to = nil
|
342
|
+
if(to_identifier.is_a?(Time))
|
343
|
+
to = svndate(to_identifier)
|
344
|
+
elsif(to_identifier.is_a?(Numeric))
|
345
|
+
to = to_identifier
|
346
|
+
elsif(!from_identifier.nil?)
|
347
|
+
raise "to_identifier must be Numeric, Time or nil. Was: #{to_identifier} (#{to_identifier.class.name})"
|
348
|
+
end
|
349
|
+
|
350
|
+
revision_option = nil
|
351
|
+
if(from && to.nil?)
|
352
|
+
revision_option = "--revision #{from}:HEAD"
|
353
|
+
elsif(from.nil? && to)
|
354
|
+
revision_option = "--revision #{to}"
|
355
|
+
elsif(from.nil? && to.nil?)
|
356
|
+
revision_option = ""
|
357
|
+
elsif(from && to)
|
358
|
+
revision_option = "--revision #{from}:#{to}"
|
359
|
+
end
|
360
|
+
revision_option
|
361
|
+
end
|
362
|
+
|
363
|
+
def svndate(time)
|
364
|
+
return nil unless time
|
365
|
+
time.utc.strftime("{\"%Y-%m-%d %H:%M:%S\"}")
|
366
|
+
end
|
367
|
+
|
368
|
+
def commit_command(message)
|
369
|
+
"commit #{login_options} --force-log -m \"#{message}\" #{quoted_checkout_dir}"
|
370
|
+
end
|
371
|
+
|
372
|
+
def quoted_checkout_dir
|
373
|
+
cd = '"' + PathConverter.filepath_to_nativepath(checkout_dir, false) + '"'
|
374
|
+
raise "checkout_dir not set" if cd == ""
|
375
|
+
cd
|
376
|
+
end
|
377
|
+
|
378
|
+
def local?
|
379
|
+
if(url =~ /^file:/)
|
380
|
+
return true
|
381
|
+
else
|
382
|
+
return false
|
383
|
+
end
|
384
|
+
end
|
385
|
+
|
386
|
+
def post_commit_file
|
387
|
+
# We actualy need to use the .cmd when on cygwin. The cygwin svn post-commit
|
388
|
+
# hook is hosed. We'll be relying on native windows
|
389
|
+
if(local?)
|
390
|
+
WINDOWS ? "#{svnrootdir}/hooks/post-commit.cmd" : "#{svnrootdir}/hooks/post-commit"
|
391
|
+
else
|
392
|
+
raise "The repository is not local. Cannot install or uninstall trigger."
|
393
|
+
end
|
394
|
+
end
|
395
|
+
|
396
|
+
end
|
397
|
+
end
|