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,92 @@
1
+ require 'fileutils'
2
+ require 'rscm/logging'
3
+
4
+ WIN32 = RUBY_PLATFORM == "i386-mswin32"
5
+ CYGWIN = RUBY_PLATFORM == "i386-cygwin"
6
+ WINDOWS = WIN32 || CYGWIN
7
+
8
+ # TODO: change to override IO.popen, using that neat trick we
9
+ # used in threadfile.rb (which is now gone)
10
+ def safer_popen(cmd, mode="r", expected_exit=0, &proc)
11
+ Log.info "Executing command: '#{cmd}'"
12
+ ret = IO.popen(cmd, mode, &proc)
13
+ exit_code = $? >> 8
14
+ raise "#{cmd} failed with code #{exit_code} in #{Dir.pwd}. Expected exit code: #{expected_exit}" if exit_code != expected_exit
15
+ ret
16
+ end
17
+
18
+ def with_working_dir(dir)
19
+ # Can't use Dir.chdir{ block } - will fail with multithreaded code.
20
+ # http://www.ruby-doc.org/core/classes/Dir.html#M000790
21
+ #
22
+ prev = Dir.pwd
23
+ begin
24
+ Log.info "Making directory: '#{File.expand_path(dir)}'"
25
+ FileUtils.mkdir_p(dir)
26
+ Dir.chdir(dir)
27
+ Log.info "In directory: '#{File.expand_path(dir)}'"
28
+ yield
29
+ ensure
30
+ Dir.chdir(prev)
31
+ end
32
+ end
33
+
34
+ # Utility for converting between win32 and cygwin paths. Does nothing on *nix.
35
+ module RSCM
36
+ module PathConverter
37
+ def filepath_to_nativepath(path, escaped)
38
+ return nil if path.nil?
39
+ path = File.expand_path(path)
40
+ if(WIN32)
41
+ path.gsub(/\//, "\\")
42
+ elsif(CYGWIN)
43
+ cmd = "cygpath --windows #{path}"
44
+ safer_popen(cmd) do |io|
45
+ cygpath = io.read.chomp
46
+ escaped ? cygpath.gsub(/\\/, "\\\\\\\\") : cygpath
47
+ end
48
+ else
49
+ path
50
+ end
51
+ end
52
+
53
+ def filepath_to_nativeurl(path)
54
+ return nil if path.nil?
55
+ if(WINDOWS)
56
+ urlpath = filepath_to_nativepath(path, false).gsub(/\\/, "/")
57
+ "file:///#{urlpath}"
58
+ else
59
+ "file://#{File.expand_path(path)}"
60
+ end
61
+ end
62
+
63
+ def nativepath_to_filepath(path)
64
+ return nil if path.nil?
65
+ if(WIN32)
66
+ path.gsub(/\//, "\\")
67
+ elsif(CYGWIN)
68
+ path = path.gsub(/\\/, "/")
69
+ cmd = "cygpath --unix #{path}"
70
+ safer_popen(cmd) do |io|
71
+ io.read.chomp
72
+ end
73
+ else
74
+ path
75
+ end
76
+ end
77
+
78
+ def ensure_trailing_slash(url)
79
+ return nil if url.nil?
80
+ if(url && url[-1..-1] != "/")
81
+ "#{url}/"
82
+ else
83
+ url
84
+ end
85
+ end
86
+
87
+ module_function :filepath_to_nativepath
88
+ module_function :filepath_to_nativeurl
89
+ module_function :nativepath_to_filepath
90
+ module_function :ensure_trailing_slash
91
+ end
92
+ end
@@ -0,0 +1,415 @@
1
+ require 'rscm/abstract_scm'
2
+ require 'rscm/path_converter'
3
+ require 'rscm/line_editor'
4
+
5
+ require 'fileutils'
6
+ require 'socket'
7
+ require 'pp'
8
+ require 'parsedate'
9
+ require 'stringio'
10
+
11
+ module RSCM
12
+ # RSCM implementation for Perforce.
13
+ #
14
+ # Understands operations against multiple client-workspaces
15
+ # You need the p4/p4d executable on the PATH in order for it to work.
16
+ #
17
+ class Perforce < AbstractSCM
18
+ include FileUtils
19
+
20
+ attr_accessor :depotpath
21
+
22
+ def initialize(repository_root_dir = nil)
23
+ @clients = {}
24
+ @depotpath = repository_root_dir
25
+ end
26
+
27
+ def create
28
+ P4Daemon.new(@depotpath).start
29
+ end
30
+
31
+ def name
32
+ "Perforce"
33
+ end
34
+
35
+ def transactional?
36
+ true
37
+ end
38
+
39
+ def import(dir, comment)
40
+ with_create_client(dir) do |client|
41
+ client.add_all(list_files)
42
+ client.submit(comment)
43
+ end
44
+ end
45
+
46
+ def checkout(checkout_dir, to_identifier = nil, &proc)
47
+ client(checkout_dir).checkout(to_identifier, &proc)
48
+ end
49
+
50
+ def add(checkout_dir, relative_filename)
51
+ client(checkout_dir).add(relative_filename)
52
+ end
53
+
54
+ def commit(checkout_dir, message, &proc)
55
+ client(checkout_dir).submit(message, &proc)
56
+ end
57
+
58
+ def changesets(checkout_dir, from_identifier, to_identifier = nil, files = nil)
59
+ client(checkout_dir).changesets(from_identifier, to_identifier, files)
60
+ end
61
+
62
+ def uptodate?(checkout_dir, from_identifier)
63
+ client(checkout_dir).uptodate?
64
+ end
65
+
66
+ def edit(file)
67
+ client_containing(file).edit(file)
68
+ end
69
+
70
+ def trigger_installed?(trigger_command, trigger_files_checkout_dir)
71
+ p4admin.trigger_installed?(trigger_command)
72
+ end
73
+
74
+ def install_trigger(trigger_command, damagecontrol_install_dir)
75
+ p4admin.install_trigger(trigger_command)
76
+ end
77
+
78
+ def uninstall_trigger(trigger_command, trigger_files_checkout_dir)
79
+ p4admin.uninstall_trigger(trigger_command)
80
+ end
81
+
82
+ private
83
+
84
+ def p4admin
85
+ @p4admin ||= P4Admin.new
86
+ end
87
+
88
+ def client_containing(path)
89
+ @clients.values.find {|client| client.contains?(path)}
90
+ end
91
+
92
+ def client(rootdir)
93
+ @clients[rootdir] ||= create_client(rootdir)
94
+ end
95
+
96
+ def with_create_client(rootdir)
97
+ raise "needs a block" unless block_given?
98
+ rootdir = File.expand_path(rootdir)
99
+ with_working_dir(rootdir) do
100
+ client = create_client(rootdir)
101
+ begin
102
+ yield client
103
+ ensure
104
+ delete_client(client)
105
+ end
106
+ end
107
+ end
108
+
109
+ def delete_client(client)
110
+ p4admin.delete_client(client.name)
111
+ end
112
+
113
+ def create_client(rootdir)
114
+ rootdir = File.expand_path(rootdir) if rootdir =~ /\.\./
115
+ mkdir_p(rootdir)
116
+ p4admin.create_client(rootdir)
117
+ end
118
+
119
+ def list_files
120
+ files = Dir["**/*"].delete_if{|f| File.directory?(f)}
121
+ files.collect{|f| File.expand_path(f)}
122
+ end
123
+
124
+ class P4Daemon
125
+ include FileUtils
126
+
127
+ def initialize(depotpath)
128
+ @depotpath = depotpath
129
+ end
130
+
131
+ def start
132
+ shutdown if running?
133
+ launch
134
+ assert_running
135
+ end
136
+
137
+ def assert_running
138
+ raise "p4d did not start properly" if timeout(10) { running? }
139
+ end
140
+
141
+ def launch
142
+ fork do
143
+ mkdir_p(@depotpath)
144
+ cd(@depotpath)
145
+ debug "starting p4 server"
146
+ exec("p4d")
147
+ end
148
+ at_exit { shutdown }
149
+ end
150
+
151
+ def shutdown
152
+ `p4 -p 1666 admin stop`
153
+ end
154
+
155
+ def running?
156
+ !`p4 -p 1666 info`.empty?
157
+ end
158
+ end
159
+ end
160
+
161
+ # Understands p4 administrative operations (not specific to a client)
162
+ class P4Admin
163
+ @@counter = 0
164
+
165
+ def create_client(rootdir)
166
+ name = next_name
167
+ popen("client -i", "w+", clientspec(name, rootdir))
168
+ P4Client.new(name, rootdir)
169
+ end
170
+
171
+ def delete_client(name)
172
+ execute("client -d #{name}")
173
+ end
174
+
175
+ def trigger_installed?(trigger_command)
176
+ triggers.any? {|line| line =~ /#{trigger_command}/}
177
+ end
178
+
179
+ def install_trigger(trigger_command)
180
+ popen("triggers -i", "a+", triggerspec_with(trigger_command))
181
+ end
182
+
183
+ def uninstall_trigger(trigger_command)
184
+ popen("triggers -i", "a+", triggerspec_without(trigger_command))
185
+ end
186
+
187
+ def triggerspec_with(trigger_command)
188
+ new_trigger = " damagecontrol commit //depot/... \"#{trigger_command}\" "
189
+ triggers + $/ + new_trigger
190
+ end
191
+
192
+ def triggerspec_without(trigger_command)
193
+ triggers.reject {|line| line =~ /#{trigger_command}/}.join
194
+ end
195
+
196
+ def clientspec(name, rootdir)
197
+ s = StringIO.new
198
+ s.puts "Client: #{name}"
199
+ s.puts "Owner: #{ENV["LOGNAME"]}"
200
+ s.puts "Host: #{ENV["HOSTNAME"]}"
201
+ s.puts "Description: another one"
202
+ s.puts "Root: #{rootdir}"
203
+ s.puts "Options: noallwrite noclobber nocompress unlocked nomodtime normdir"
204
+ s.puts "LineEnd: local"
205
+ s.puts "View: //depot/... //#{name}/..."
206
+ s.string
207
+ end
208
+
209
+ def triggers
210
+ execute("triggers -o")
211
+ end
212
+
213
+ def popen(cmd, mode, input)
214
+ IO.popen("p4 -p 1666 #{cmd}", mode) do |io|
215
+ io.puts(input)
216
+ io.close_write
217
+ io.each_line {|line| debug(line)}
218
+ end
219
+ end
220
+
221
+ def execute(cmd)
222
+ cmd = "p4 -p 1666 #{cmd}"
223
+ puts "> executing: #{cmd}"
224
+ `#{cmd}`
225
+ end
226
+
227
+ def next_name
228
+ "client#{@@counter += 1}"
229
+ end
230
+ end
231
+
232
+ # Understands operations against a client-workspace
233
+ class P4Client
234
+ DATE_FORMAT = "%Y/%m/%d:%H:%M:%S"
235
+ STATUS = { "add" => Change::ADDED, "edit" => Change::MODIFIED, "delete" => Change::DELETED }
236
+ PERFORCE_EPOCH = Time.utc(1970, 1, 1, 6, 0, 1) #perforce doesn't like Time.utc(1970)
237
+
238
+ attr_accessor :name, :rootdir
239
+
240
+ def initialize(name, rootdir)
241
+ @name = name
242
+ @rootdir = rootdir
243
+ end
244
+
245
+ def contains?(file)
246
+ file = File.expand_path(file)
247
+ file =~ /^#{@rootdir}/
248
+ end
249
+
250
+ def uptodate?
251
+ p4("sync -n").empty?
252
+ end
253
+
254
+ def changesets(from_identifier, to_identifier, files)
255
+ changesets = changelists(from_identifier, to_identifier).collect {|changelist| to_changeset(changelist)}
256
+ ChangeSets.new(changesets)
257
+ end
258
+
259
+ def edit(file)
260
+ file = File.expand_path(file)
261
+ p4("edit #{file}")
262
+ end
263
+
264
+ def add(relative_path)
265
+ add_file(@rootdir + "/" + relative_path)
266
+ end
267
+
268
+ def add_all(files)
269
+ files.each {|file| add_file(file)}
270
+ end
271
+
272
+ def submit(comment)
273
+ IO.popen(p4cmd("submit -i"), "w+") do |io|
274
+ io.puts(submitspec(comment))
275
+ io.close_write
276
+ io.each_line {|progress| debug progress}
277
+ end
278
+ end
279
+
280
+ def checkout(to_identifier)
281
+ cmd = to_identifier.nil? ? "sync" : "sync //...@#{to_identifier}"
282
+ checked_out_files = []
283
+ p4(cmd).collect do |output|
284
+ puts "output: '#{output}'"
285
+ if(output =~ /.* - (added as|updating|deleted as) #{@rootdir}[\/|\\](.*)/)
286
+ path = $2.gsub(/\\/, "/")
287
+ checked_out_files << path
288
+ yield path if block_given?
289
+ end
290
+ end
291
+ checked_out_files
292
+ end
293
+
294
+ private
295
+
296
+ def add_file(absolute_path)
297
+ absolute_path = PathConverter.filepath_to_nativepath(absolute_path, true)
298
+ p4("add #{absolute_path}")
299
+ end
300
+
301
+ def changelists(from_identifier, to_identifier)
302
+ p4changes(from_identifier, to_identifier).collect do |line|
303
+ if line =~ /^Change (\d+) /
304
+ log = p4describe($1)
305
+ P4Changelist.new(log) unless log == ""
306
+ end
307
+ end
308
+ end
309
+
310
+ def to_changeset(changelist)
311
+ return nil if changelist.nil? # Ugly, but it seems to be nil some times on windows.
312
+ changes = changelist.files.collect do |filespec|
313
+ change = Change.new(filespec.path, changelist.developer, changelist.message, filespec.revision, changelist.time)
314
+ change.status = STATUS[filespec.status]
315
+ change.previous_revision = filespec.revision - 1
316
+ change
317
+ end
318
+ changeset = ChangeSet.new(changes)
319
+ changeset.revision = changelist.number
320
+ changeset.developer = changelist.developer
321
+ changeset.message = changelist.message
322
+ changeset.time = changelist.time
323
+ changeset
324
+ end
325
+
326
+ def p4describe(chnum)
327
+ p4("describe -s #{chnum}")
328
+ end
329
+
330
+ def p4changes(from_identifier, to_identifier)
331
+ if from_identifier.nil? || from_identifier.is_a?(Time)
332
+ from_identifier = PERFORCE_EPOCH if from_identifier.nil? || from_identifier < PERFORCE_EPOCH
333
+ to_identifier = Time.infinity if to_identifier.nil?
334
+ from = from_identifier.strftime(DATE_FORMAT)
335
+ to = to_identifier.strftime(DATE_FORMAT)
336
+ p4("changes //...@#{from},#{to}")
337
+ else
338
+ p4("changes //...@#{from_identifier},#{from_identifier}")
339
+ end
340
+ end
341
+
342
+ def p4(cmd)
343
+ cmd = "#{p4cmd(cmd)}"
344
+ puts "> executing: #{cmd}"
345
+ output = `#{cmd}`
346
+ puts output
347
+ output
348
+ end
349
+
350
+ def p4cmd(cmd)
351
+ "p4 -p 1666 -c #{@name} #{cmd}"
352
+ end
353
+
354
+ def submitspec(comment)
355
+ s = StringIO.new
356
+ s.puts "Change: new"
357
+ s.puts "Client: #{@name}"
358
+ s.puts "Description: #{comment.gsub(/\n/, "\n\t")}"
359
+ s.puts "Files: "
360
+ p4("opened").each do |line|
361
+ if line =~ /^(.+)#\d+ - (\w+) /
362
+ status, revision = $1, $2
363
+ s.puts "\t#{status} # #{revision}"
364
+ end
365
+ end
366
+ s.string
367
+ end
368
+
369
+ FileSpec = Struct.new(:path, :revision, :status)
370
+
371
+ class P4Changelist
372
+ attr_reader :number, :developer, :message, :time, :files
373
+
374
+ def initialize(log)
375
+ debug log
376
+ if(log =~ /^Change (\d+) by (.*) on (.*)$/)
377
+ # @number, @developer, @time = $1.to_i, $2, Time.utc(*ParseDate.parsedate($3)[0..5])
378
+ @number, @developer, @time = $1.to_i, $2, Time.utc(*ParseDate.parsedate($3))
379
+ else
380
+ raise "Bad log format: '#{log}'"
381
+ end
382
+
383
+ if log =~ /Change (.*)\n\n(.*)\n\nAffected/m
384
+ @message = $2.strip.gsub(/\n\t/, "\n")
385
+ end
386
+
387
+ @files = []
388
+ log.each do |line|
389
+ if line =~ /^\.\.\. \/\/depot\/(.+)#(\d+) (.+)/
390
+ files << FileSpec.new($1, Integer($2), $3)
391
+ end
392
+ end
393
+ end
394
+ end
395
+ end
396
+
397
+ end
398
+
399
+ module Kernel
400
+
401
+ # TODO: use Ruby's built-in timeout? (require 'timeout')
402
+ def timeout(attempts=5, &proc)
403
+ 0.upto(attempts) do
404
+ sleep 1
405
+ return false if proc.call
406
+ end
407
+ true
408
+ end
409
+
410
+ #todo: replace with logger
411
+ def debug(msg)
412
+ puts msg
413
+ end
414
+
415
+ end