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