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.
Files changed (43) hide show
  1. data/README +198 -0
  2. data/Rakefile +118 -0
  3. data/ext/rscm.jar +0 -0
  4. data/lib/rscm.rb +10 -0
  5. data/lib/rscm/abstract_log_parser.rb +49 -0
  6. data/lib/rscm/abstract_scm.rb +229 -0
  7. data/lib/rscm/changes.rb +271 -0
  8. data/lib/rscm/cvs/cvs.rb +363 -0
  9. data/lib/rscm/cvs/cvs_log_parser.rb +161 -0
  10. data/lib/rscm/darcs/darcs.rb +69 -0
  11. data/lib/rscm/line_editor.rb +46 -0
  12. data/lib/rscm/logging.rb +5 -0
  13. data/lib/rscm/monotone/monotone.rb +107 -0
  14. data/lib/rscm/mooky/mooky.rb +13 -0
  15. data/lib/rscm/parser.rb +39 -0
  16. data/lib/rscm/path_converter.rb +92 -0
  17. data/lib/rscm/perforce/perforce.rb +415 -0
  18. data/lib/rscm/starteam/starteam.rb +99 -0
  19. data/lib/rscm/svn/svn.rb +337 -0
  20. data/lib/rscm/svn/svn_log_parser.rb +134 -0
  21. data/lib/rscm/time_ext.rb +125 -0
  22. data/test/rscm/apply_label_scm_tests.rb +26 -0
  23. data/test/rscm/changes_fixture.rb +20 -0
  24. data/test/rscm/changes_test.rb +129 -0
  25. data/test/rscm/cvs/cvs_log_parser_test.rb +575 -0
  26. data/test/rscm/cvs/cvs_test.rb +22 -0
  27. data/test/rscm/darcs/darcs_test.rb +14 -0
  28. data/test/rscm/difftool_test.rb +40 -0
  29. data/test/rscm/file_ext.rb +12 -0
  30. data/test/rscm/generic_scm_tests.rb +282 -0
  31. data/test/rscm/line_editor_test.rb +76 -0
  32. data/test/rscm/mockit.rb +130 -0
  33. data/test/rscm/mockit_test.rb +117 -0
  34. data/test/rscm/monotone/monotone_test.rb +19 -0
  35. data/test/rscm/mooky/mooky_test.rb +14 -0
  36. data/test/rscm/parser_test.rb +47 -0
  37. data/test/rscm/path_converter_test.rb +52 -0
  38. data/test/rscm/perforce/perforce_test.rb +14 -0
  39. data/test/rscm/starteam/starteam_test.rb +36 -0
  40. data/test/rscm/svn/svn_log_parser_test.rb +111 -0
  41. data/test/rscm/svn/svn_test.rb +28 -0
  42. data/test/rscm/tempdir.rb +12 -0
  43. 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
@@ -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