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