rscm 0.1.0
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/README +198 -0
- data/Rakefile +118 -0
- data/ext/rscm.jar +0 -0
- data/lib/rscm.rb +10 -0
- data/lib/rscm/abstract_log_parser.rb +49 -0
- data/lib/rscm/abstract_scm.rb +229 -0
- data/lib/rscm/changes.rb +271 -0
- data/lib/rscm/cvs/cvs.rb +363 -0
- data/lib/rscm/cvs/cvs_log_parser.rb +161 -0
- data/lib/rscm/darcs/darcs.rb +69 -0
- data/lib/rscm/line_editor.rb +46 -0
- data/lib/rscm/logging.rb +5 -0
- data/lib/rscm/monotone/monotone.rb +107 -0
- data/lib/rscm/mooky/mooky.rb +13 -0
- data/lib/rscm/parser.rb +39 -0
- data/lib/rscm/path_converter.rb +92 -0
- data/lib/rscm/perforce/perforce.rb +415 -0
- data/lib/rscm/starteam/starteam.rb +99 -0
- data/lib/rscm/svn/svn.rb +337 -0
- data/lib/rscm/svn/svn_log_parser.rb +134 -0
- data/lib/rscm/time_ext.rb +125 -0
- data/test/rscm/apply_label_scm_tests.rb +26 -0
- data/test/rscm/changes_fixture.rb +20 -0
- data/test/rscm/changes_test.rb +129 -0
- data/test/rscm/cvs/cvs_log_parser_test.rb +575 -0
- data/test/rscm/cvs/cvs_test.rb +22 -0
- data/test/rscm/darcs/darcs_test.rb +14 -0
- data/test/rscm/difftool_test.rb +40 -0
- data/test/rscm/file_ext.rb +12 -0
- data/test/rscm/generic_scm_tests.rb +282 -0
- data/test/rscm/line_editor_test.rb +76 -0
- data/test/rscm/mockit.rb +130 -0
- data/test/rscm/mockit_test.rb +117 -0
- data/test/rscm/monotone/monotone_test.rb +19 -0
- data/test/rscm/mooky/mooky_test.rb +14 -0
- data/test/rscm/parser_test.rb +47 -0
- data/test/rscm/path_converter_test.rb +52 -0
- data/test/rscm/perforce/perforce_test.rb +14 -0
- data/test/rscm/starteam/starteam_test.rb +36 -0
- data/test/rscm/svn/svn_log_parser_test.rb +111 -0
- data/test/rscm/svn/svn_test.rb +28 -0
- data/test/rscm/tempdir.rb +12 -0
- metadata +81 -0
@@ -0,0 +1,99 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'tempfile'
|
3
|
+
require 'rscm/changes'
|
4
|
+
require 'rscm/abstract_scm'
|
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 < AbstractSCM
|
21
|
+
|
22
|
+
attr_accessor :user_name, :password, :server_name, :server_port, :project_name, :view_name, :folder_name
|
23
|
+
|
24
|
+
def initialize(user_name=nil, password=nil, server_name=nil, server_port=nil, project_name=nil, view_name=nil, folder_name=nil)
|
25
|
+
@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
|
26
|
+
end
|
27
|
+
|
28
|
+
def name
|
29
|
+
"StarTeam"
|
30
|
+
end
|
31
|
+
|
32
|
+
def changesets(checkout_dir, from_identifier=Time.epoch, to_identifier=Time.infinity, files=nil, &proc)
|
33
|
+
# just assuming it is a Time for now, may support labels later.
|
34
|
+
# the java class really wants rfc822 and not rfc2822, but this works ok anyway.
|
35
|
+
from = from_identifier.to_rfc2822
|
36
|
+
to = to_identifier.to_rfc2822
|
37
|
+
|
38
|
+
changesets = java("getChangeSets(\"#{from}\";\"#{to}\")", &proc)
|
39
|
+
raise "changesets must be of type #{ChangeSets.name} - was #{changesets.class.name}" unless changesets.is_a?(::RSCM::ChangeSets)
|
40
|
+
|
41
|
+
# Just a little sanity check
|
42
|
+
if(changesets.latest)
|
43
|
+
latetime = changesets.latest.time
|
44
|
+
if(latetime < from_identifier || to_identifier < latetime)
|
45
|
+
raise "Latest time (#{latetime}) is not within #{from_identifier}-#{to_identifier}"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
changesets
|
49
|
+
end
|
50
|
+
|
51
|
+
def checkout(checkout_dir, to_identifier, &proc)
|
52
|
+
# TODO: Take the to_identifier arg into consideration
|
53
|
+
files = java("checkout(\"#{checkout_dir}\")", &proc)
|
54
|
+
files
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def cmd
|
60
|
+
rscm_jar = File.expand_path(File.dirname(__FILE__) + "../../../../ext/rscm.jar")
|
61
|
+
starteam_jars = Dir["#{ENV['RSCM_STARTEAM']}/Lib/*jar"].join(File::PATH_SEPARATOR)
|
62
|
+
ant_jars = Dir["#{ENV['ANT_HOME']}/lib/*jar"].join(File::PATH_SEPARATOR)
|
63
|
+
classpath = "#{rscm_jar}#{File::PATH_SEPARATOR}#{ant_jars}#{File::PATH_SEPARATOR}#{starteam_jars}"
|
64
|
+
|
65
|
+
"java -Djava.library.path=\"#{ENV['RSCM_STARTEAM']}#{File::SEPARATOR}Lib\" -classpath \"#{classpath}\" org.rubyforge.rscm.Main"
|
66
|
+
end
|
67
|
+
|
68
|
+
def java(m, &proc)
|
69
|
+
raise "The RSCM_STARTEAM environment variable must be defined and point to the StarTeam SDK directory" unless ENV['RSCM_STARTEAM']
|
70
|
+
raise "The ANT_HOME environment variable must be defined and point to the Ant installation directory" unless ENV['ANT_HOME']
|
71
|
+
|
72
|
+
clazz = "org.rubyforge.rscm.starteam.StarTeam"
|
73
|
+
ctor_args = "#{@user_name};#{@password};#{@server_name};#{@server_port};#{@project_name};#{@view_name};#{@folder_name}"
|
74
|
+
|
75
|
+
# Uncomment if you're not Aslak - to run against a bogus java class.
|
76
|
+
# clazz = "org.rubyforge.rscm.TestScm"
|
77
|
+
# ctor_args = "hubba;bubba"
|
78
|
+
|
79
|
+
command = "new #{clazz}(#{ctor_args}).#{m}"
|
80
|
+
tf = Tempfile.new("rscm_starteam")
|
81
|
+
tf.puts(command)
|
82
|
+
tf.close
|
83
|
+
cmdline = "#{cmd} #{tf.path}"
|
84
|
+
IO.popen(cmdline) do |io|
|
85
|
+
yaml_source = io
|
86
|
+
if(block_given?)
|
87
|
+
yaml_source = ""
|
88
|
+
io.each_line do |line|
|
89
|
+
yield line
|
90
|
+
yaml_source << line << "\n"
|
91
|
+
end
|
92
|
+
else
|
93
|
+
end
|
94
|
+
YAML::load(yaml_source)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
end
|
data/lib/rscm/svn/svn.rb
ADDED
@@ -0,0 +1,337 @@
|
|
1
|
+
require 'rscm/abstract_scm'
|
2
|
+
require 'rscm/path_converter'
|
3
|
+
require 'rscm/line_editor'
|
4
|
+
require 'rscm/svn/svn_log_parser'
|
5
|
+
|
6
|
+
module RSCM
|
7
|
+
|
8
|
+
# RSCM implementation for Subversion.
|
9
|
+
#
|
10
|
+
# You need the svn/svnadmin executable on the PATH in order for it to work.
|
11
|
+
#
|
12
|
+
# NOTE: On Cygwin these have to be the win32 builds of svn/svnadmin and not the Cygwin ones.
|
13
|
+
class SVN < AbstractSCM
|
14
|
+
include FileUtils
|
15
|
+
include PathConverter
|
16
|
+
|
17
|
+
attr_accessor :url
|
18
|
+
attr_accessor :path
|
19
|
+
|
20
|
+
def initialize(url=nil, path="")
|
21
|
+
@url, @path = url, path
|
22
|
+
end
|
23
|
+
|
24
|
+
def name
|
25
|
+
"Subversion"
|
26
|
+
end
|
27
|
+
|
28
|
+
def add(checkout_dir, relative_filename)
|
29
|
+
svn(checkout_dir, "add #{relative_filename}")
|
30
|
+
end
|
31
|
+
|
32
|
+
def transactional?
|
33
|
+
true
|
34
|
+
end
|
35
|
+
|
36
|
+
def checkout(checkout_dir, to_identifier=nil)
|
37
|
+
checkout_dir = PathConverter.filepath_to_nativepath(checkout_dir, false)
|
38
|
+
mkdir_p(checkout_dir)
|
39
|
+
checked_out_files = []
|
40
|
+
path_regex = /^[A|D|U]\s+(.*)/
|
41
|
+
if(checked_out?(checkout_dir))
|
42
|
+
svn(checkout_dir, update_command(to_identifier)) do |line|
|
43
|
+
if(line =~ path_regex)
|
44
|
+
absolute_path = "#{checkout_dir}/#{$1}"
|
45
|
+
relative_path = $1.chomp
|
46
|
+
relative_path = relative_path.gsub(/\\/, "/") if WINDOWS
|
47
|
+
checked_out_files << relative_path
|
48
|
+
yield relative_path if block_given?
|
49
|
+
end
|
50
|
+
end
|
51
|
+
else
|
52
|
+
svn(checkout_dir, checkout_command(checkout_dir, to_identifier)) do |line|
|
53
|
+
if(line =~ path_regex)
|
54
|
+
native_absolute_path = $1
|
55
|
+
native_checkout_dir = $1
|
56
|
+
absolute_path = PathConverter.nativepath_to_filepath($1)
|
57
|
+
native_checkout_dir = PathConverter.filepath_to_nativepath(checkout_dir, false)
|
58
|
+
if(File.exist?(absolute_path) && !File.directory?(absolute_path))
|
59
|
+
relative_path = native_absolute_path[native_checkout_dir.length+1..-1].chomp
|
60
|
+
relative_path = relative_path.gsub(/\\/, "/") if WINDOWS
|
61
|
+
checked_out_files << relative_path
|
62
|
+
yield relative_path if block_given?
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
checked_out_files
|
68
|
+
end
|
69
|
+
|
70
|
+
def checkout_commandline
|
71
|
+
"svn checkout #{revision_option(nil)}"
|
72
|
+
end
|
73
|
+
|
74
|
+
def update_commandline
|
75
|
+
"svn update #{url} #{checkout_dir}"
|
76
|
+
end
|
77
|
+
|
78
|
+
def uptodate?(checkout_dir, from_identifier)
|
79
|
+
if(!checked_out?(checkout_dir))
|
80
|
+
false
|
81
|
+
else
|
82
|
+
lr = local_revision(checkout_dir)
|
83
|
+
hr = head_revision(checkout_dir)
|
84
|
+
lr == hr
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def local_revision(checkout_dir)
|
89
|
+
local_revision = nil
|
90
|
+
svn(checkout_dir, "info") do |line|
|
91
|
+
if(line =~ /Revision: ([0-9]*)/)
|
92
|
+
return $1.to_i
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def head_revision(checkout_dir)
|
98
|
+
cmd = "svn log #{repourl} -r HEAD"
|
99
|
+
with_working_dir(checkout_dir) do
|
100
|
+
safer_popen(cmd) do |stdout|
|
101
|
+
parser = SVNLogParser.new(stdout, path, checkout_dir)
|
102
|
+
changesets = parser.parse_changesets
|
103
|
+
changesets[0].revision.to_i
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def commit(checkout_dir, message)
|
109
|
+
svn(checkout_dir, commit_command(message))
|
110
|
+
# We have to do an update to get the local revision right
|
111
|
+
svn(checkout_dir, "update")
|
112
|
+
end
|
113
|
+
|
114
|
+
def label(checkout_dir)
|
115
|
+
local_revision(checkout_dir).to_s
|
116
|
+
end
|
117
|
+
|
118
|
+
def diff(checkout_dir, change, &block)
|
119
|
+
with_working_dir(checkout_dir) do
|
120
|
+
cmd = "svn diff -r #{change.previous_revision}:#{change.revision} #{url}/#{change.path}"
|
121
|
+
safer_popen(cmd) do |io|
|
122
|
+
return(yield(io))
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def can_create?
|
128
|
+
local?
|
129
|
+
end
|
130
|
+
|
131
|
+
def exists?
|
132
|
+
if(local?)
|
133
|
+
File.exists?("#{svnrootdir}/db")
|
134
|
+
else
|
135
|
+
# Do a simple command over the network
|
136
|
+
# If the repo/path doesn't exist, we'll get zero output
|
137
|
+
# on stdout (and an error msg on std err).
|
138
|
+
exists = false
|
139
|
+
cmd = "svn log #{url} -r HEAD"
|
140
|
+
IO.popen(cmd) do |stdout|
|
141
|
+
stdout.each_line do |line|
|
142
|
+
exists = true
|
143
|
+
end
|
144
|
+
end
|
145
|
+
exists
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def supports_trigger?
|
150
|
+
local?
|
151
|
+
end
|
152
|
+
|
153
|
+
def create
|
154
|
+
native_path = PathConverter.filepath_to_nativepath(svnrootdir, true)
|
155
|
+
mkdir_p(PathConverter.nativepath_to_filepath(native_path))
|
156
|
+
svnadmin(svnrootdir, "create #{native_path}")
|
157
|
+
end
|
158
|
+
|
159
|
+
def install_trigger(trigger_command, damagecontrol_install_dir)
|
160
|
+
if (WINDOWS)
|
161
|
+
install_win_trigger(trigger_command, damagecontrol_install_dir)
|
162
|
+
else
|
163
|
+
install_unix_trigger(trigger_command, damagecontrol_install_dir)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def uninstall_trigger(trigger_command, trigger_files_checkout_dir)
|
168
|
+
File.comment_out(post_commit_file, /#{Regexp.escape(trigger_command)}/, nil)
|
169
|
+
end
|
170
|
+
|
171
|
+
def trigger_installed?(trigger_command, trigger_files_checkout_dir)
|
172
|
+
return false unless File.exist?(post_commit_file)
|
173
|
+
not_already_commented = LineEditor.comment_out(File.new(post_commit_file), /#{Regexp.escape(trigger_command)}/, "# ", "")
|
174
|
+
not_already_commented
|
175
|
+
end
|
176
|
+
|
177
|
+
def import(dir, message)
|
178
|
+
import_cmd = "import #{url} -m \"#{message}\""
|
179
|
+
svn(dir, import_cmd)
|
180
|
+
end
|
181
|
+
|
182
|
+
def changesets(checkout_dir, from_identifier, to_identifier=Time.infinity, files=nil)
|
183
|
+
# Return empty changeset if the requested revision doesn't exist yet.
|
184
|
+
return ChangeSets.new if(from_identifier.is_a?(Integer) && head_revision(checkout_dir) < from_identifier)
|
185
|
+
|
186
|
+
checkout_dir = PathConverter.filepath_to_nativepath(checkout_dir, false)
|
187
|
+
changesets = nil
|
188
|
+
command = "svn #{changes_command(from_identifier, to_identifier, files)}"
|
189
|
+
yield command if block_given?
|
190
|
+
|
191
|
+
with_working_dir(checkout_dir) do
|
192
|
+
safer_popen(command) do |stdout|
|
193
|
+
parser = SVNLogParser.new(stdout, path, checkout_dir)
|
194
|
+
changesets = parser.parse_changesets
|
195
|
+
end
|
196
|
+
end
|
197
|
+
changesets
|
198
|
+
end
|
199
|
+
|
200
|
+
# url pointing to the root of the repo
|
201
|
+
def repourl
|
202
|
+
last = (path.nil? || path == "") ? -1 : -(path.length)-2
|
203
|
+
url[0..last]
|
204
|
+
end
|
205
|
+
|
206
|
+
def checked_out?(checkout_dir)
|
207
|
+
rootentries = File.expand_path("#{checkout_dir}/.svn/entries")
|
208
|
+
result = File.exists?(rootentries)
|
209
|
+
result
|
210
|
+
end
|
211
|
+
|
212
|
+
private
|
213
|
+
|
214
|
+
def install_unix_trigger(trigger_command, damagecontrol_install_dir)
|
215
|
+
post_commit_exists = File.exists?(post_commit_file)
|
216
|
+
mode = post_commit_exists ? File::APPEND|File::WRONLY : File::CREAT|File::WRONLY
|
217
|
+
begin
|
218
|
+
File.open(post_commit_file, mode) do |file|
|
219
|
+
file.puts("#!/bin/sh") unless post_commit_exists
|
220
|
+
file.puts("#{trigger_command}\n" )
|
221
|
+
end
|
222
|
+
system("chmod g+x #{post_commit_file}")
|
223
|
+
rescue
|
224
|
+
raise "Didn't have permission to write to #{post_commit_file}. " +
|
225
|
+
"Try to manually add the following line:\n\n#{trigger_command}\n\n" +
|
226
|
+
"Finally make it executable with chmod g+x #{post_commit_file}\n\n"
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
def install_win_trigger(trigger_command, damagecontrol_install_dir)
|
231
|
+
post_commit_exists = File.exists?(post_commit_file)
|
232
|
+
mode = post_commit_exists ? File::APPEND|File::WRONLY : File::CREAT|File::WRONLY
|
233
|
+
File.open(post_commit_file, mode) do |file|
|
234
|
+
file.puts("#{trigger_command}\n" )
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
def svnrootdir
|
239
|
+
last = (path.nil? || path == "") ? -1 : -(path.length)-2
|
240
|
+
result = url["file://".length..last]
|
241
|
+
# for windows, turn /c:/blabla into c:/blabla"
|
242
|
+
if(result =~ /^\/[a-zA-Z]:/)
|
243
|
+
result = result[1..-1]
|
244
|
+
end
|
245
|
+
result
|
246
|
+
end
|
247
|
+
|
248
|
+
def svnadmin(dir, cmd, &proc)
|
249
|
+
svncommand("svnadmin", dir, cmd, &proc)
|
250
|
+
end
|
251
|
+
|
252
|
+
def svn(dir, cmd, &proc)
|
253
|
+
svncommand("svn", dir, cmd, &proc)
|
254
|
+
end
|
255
|
+
|
256
|
+
def svncommand(executable, dir, cmd, &proc)
|
257
|
+
command_line = "#{executable} #{cmd}"
|
258
|
+
dir = File.expand_path(dir)
|
259
|
+
with_working_dir(dir) do
|
260
|
+
safer_popen(command_line) do |stdout|
|
261
|
+
stdout.each_line do |line|
|
262
|
+
yield line if block_given?
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
def checkout_command(checkout_dir, to_identifier)
|
269
|
+
checkout_dir = "\"#{checkout_dir}\""
|
270
|
+
"checkout #{url} #{checkout_dir} #{revision_option(nil,to_identifier)}"
|
271
|
+
end
|
272
|
+
|
273
|
+
def update_command(to_identifier)
|
274
|
+
"update #{revision_option(nil,to_identifier)}"
|
275
|
+
end
|
276
|
+
|
277
|
+
def changes_command(from_identifier, to_identifier, files)
|
278
|
+
# http://svnbook.red-bean.com/svnbook-1.1/svn-book.html#svn-ch-3-sect-3.3
|
279
|
+
# file_list = files.join('\n')
|
280
|
+
# WEIRD cygwin bug garbles this!?!?!?!
|
281
|
+
cmd = "log --verbose #{revision_option(from_identifier, to_identifier)} #{url}"
|
282
|
+
cmd
|
283
|
+
end
|
284
|
+
|
285
|
+
def revision_option(from_identifier, to_identifier)
|
286
|
+
from = nil
|
287
|
+
if(from_identifier.is_a?(Time))
|
288
|
+
from = svndate(from_identifier)
|
289
|
+
else
|
290
|
+
from = from_identifier
|
291
|
+
end
|
292
|
+
|
293
|
+
to = nil
|
294
|
+
if(to_identifier.is_a?(Time))
|
295
|
+
to = svndate(to_identifier)
|
296
|
+
else
|
297
|
+
to = to_identifier
|
298
|
+
end
|
299
|
+
|
300
|
+
revision_option = nil
|
301
|
+
if(from && to.nil?)
|
302
|
+
revision_option = "--revision #{from}:HEAD"
|
303
|
+
elsif(from.nil? && to)
|
304
|
+
revision_option = "--revision #{to}"
|
305
|
+
elsif(from.nil? && to.nil?)
|
306
|
+
revision_option = ""
|
307
|
+
elsif(from && to)
|
308
|
+
revision_option = "--revision #{from}:#{to}"
|
309
|
+
end
|
310
|
+
revision_option
|
311
|
+
end
|
312
|
+
|
313
|
+
def svndate(time)
|
314
|
+
return nil unless time
|
315
|
+
time.utc.strftime("\"{%Y-%m-%d %H:%M:%S\"}")
|
316
|
+
end
|
317
|
+
|
318
|
+
def commit_command(message)
|
319
|
+
"commit -m \"#{message}\""
|
320
|
+
end
|
321
|
+
|
322
|
+
def local?
|
323
|
+
if(url =~ /^file:/)
|
324
|
+
return true
|
325
|
+
else
|
326
|
+
return false
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
def post_commit_file
|
331
|
+
# We actualy need to use the .cmd when on cygwin. The cygwin svn post-commit
|
332
|
+
# hook is hosed. We'll be relying on native windows
|
333
|
+
WINDOWS ? "#{svnrootdir}/hooks/post-commit.cmd" : "#{svnrootdir}/hooks/post-commit"
|
334
|
+
end
|
335
|
+
|
336
|
+
end
|
337
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
require 'rscm/parser'
|
2
|
+
require 'rscm/changes'
|
3
|
+
|
4
|
+
module RSCM
|
5
|
+
|
6
|
+
class SVNLogParser
|
7
|
+
def initialize(io, path, checkout_dir)
|
8
|
+
@io = io
|
9
|
+
@changeset_parser = SVNLogEntryParser.new(path, checkout_dir)
|
10
|
+
end
|
11
|
+
|
12
|
+
def parse_changesets(&line_proc)
|
13
|
+
# skip over the first ------
|
14
|
+
@changeset_parser.parse(@io, true, &line_proc)
|
15
|
+
changesets = ChangeSets.new
|
16
|
+
while(!@io.eof?)
|
17
|
+
changeset = @changeset_parser.parse(@io, &line_proc)
|
18
|
+
if(changeset)
|
19
|
+
changesets.add(changeset)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
changesets
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class SVNLogEntryParser < Parser
|
27
|
+
|
28
|
+
def initialize(path, checkout_dir)
|
29
|
+
super(/^------------------------------------------------------------------------/)
|
30
|
+
@path = path ? path : ""
|
31
|
+
@checkout_dir = checkout_dir
|
32
|
+
end
|
33
|
+
|
34
|
+
def parse(io, skip_line_parsing=false, &line_proc)
|
35
|
+
# We have to trim off the last newline - it's not meant to be part of the message
|
36
|
+
changeset = super
|
37
|
+
changeset.message = changeset.message[0..-2] if changeset
|
38
|
+
changeset
|
39
|
+
end
|
40
|
+
|
41
|
+
protected
|
42
|
+
|
43
|
+
def parse_line(line)
|
44
|
+
if(@changeset.nil?)
|
45
|
+
parse_header(line)
|
46
|
+
elsif(line.strip == "")
|
47
|
+
@parse_state = :parse_message
|
48
|
+
elsif(line =~ /Changed paths/)
|
49
|
+
@parse_state = :parse_changes
|
50
|
+
elsif(@parse_state == :parse_changes)
|
51
|
+
change = parse_change(line)
|
52
|
+
if change
|
53
|
+
# This unless won't work for new directories
|
54
|
+
fullpath = "#{@checkout_dir}/#{change.path}"
|
55
|
+
@changeset << change unless File.directory?(fullpath)
|
56
|
+
end
|
57
|
+
elsif(@parse_state == :parse_message)
|
58
|
+
@changeset.message << line.chomp << "\n"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def next_result
|
63
|
+
result = @changeset
|
64
|
+
@changeset = nil
|
65
|
+
result
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
STATES = {"M" => Change::MODIFIED, "A" => Change::ADDED, "D" => Change::DELETED}
|
71
|
+
|
72
|
+
def parse_header(line)
|
73
|
+
@changeset = ChangeSet.new
|
74
|
+
@changeset.message = ""
|
75
|
+
revision, developer, time, the_rest = line.split("|")
|
76
|
+
@changeset.revision = revision.strip[1..-1].to_i unless revision.nil?
|
77
|
+
@changeset.developer = developer.strip unless developer.nil?
|
78
|
+
@changeset.time = parse_time(time.strip) unless time.nil?
|
79
|
+
end
|
80
|
+
|
81
|
+
def parse_change(line)
|
82
|
+
change = Change.new
|
83
|
+
path_from_root = nil
|
84
|
+
if(line =~ /^ [M|A|D|R] ([^\s]+) \(from (.*)\)/)
|
85
|
+
path_from_root = $1
|
86
|
+
change.status = Change::MOVED
|
87
|
+
elsif(line =~ /^ ([M|A|D]) (.+)$/)
|
88
|
+
status, path_from_root = line.split
|
89
|
+
change.status = STATES[status]
|
90
|
+
else
|
91
|
+
raise "could not parse change line: '#{line}'"
|
92
|
+
end
|
93
|
+
|
94
|
+
path_from_root.gsub!(/\\/, "/")
|
95
|
+
return nil unless path_from_root =~ /^\/#{@path}/
|
96
|
+
if(@path.length+1 == path_from_root.length)
|
97
|
+
change.path = path_from_root[@path.length+1..-1]
|
98
|
+
else
|
99
|
+
change.path = path_from_root[@path.length+2..-1]
|
100
|
+
end
|
101
|
+
change.revision = @changeset.revision
|
102
|
+
# http://jira.codehaus.org/browse/DC-204
|
103
|
+
change.previous_revision = change.revision.to_i - 1;
|
104
|
+
change
|
105
|
+
end
|
106
|
+
|
107
|
+
def parse_time(svn_time)
|
108
|
+
if(svn_time =~ /(.*)-(.*)-(.*) (.*):(.*):(.*) (\+|\-)([0-9]*) (.*)/)
|
109
|
+
year = $1.to_i
|
110
|
+
month = $2.to_i
|
111
|
+
day = $3.to_i
|
112
|
+
hour = $4.to_i
|
113
|
+
min = $5.to_i
|
114
|
+
sec = $6.to_i
|
115
|
+
time = Time.utc(year, month, day, hour, min, sec)
|
116
|
+
|
117
|
+
time = adjust_offset(time, $7, $8)
|
118
|
+
else
|
119
|
+
raise "unexpected time format"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def adjust_offset(time, sign, offset)
|
124
|
+
hour_offset = offset[0..1].to_i
|
125
|
+
min_offset = offset[2..3].to_i
|
126
|
+
sec_offset = 3600*hour_offset + 60*min_offset
|
127
|
+
sec_offset = -sec_offset if(sign == "+")
|
128
|
+
time += sec_offset
|
129
|
+
time
|
130
|
+
end
|
131
|
+
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|