rscm 0.4.3 → 0.4.4

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.
@@ -62,6 +62,14 @@ module RSCM
62
62
  result
63
63
  end
64
64
 
65
+ def first
66
+ @revisions.first
67
+ end
68
+
69
+ def last
70
+ @revisions.last
71
+ end
72
+
65
73
  # The latest Revision (with the latest time)
66
74
  # or nil if there are none.
67
75
  def latest
@@ -164,7 +172,12 @@ module RSCM
164
172
  end
165
173
 
166
174
  def ==(other)
167
- other.is_a?(self.class) && @files == other.files
175
+ other.is_a?(self.class) &&
176
+ @developer == other.developer &&
177
+ @identifier == other.identifier &&
178
+ @message == other.message &&
179
+ @time == other.time &&
180
+ @files == other.files
168
181
  end
169
182
 
170
183
  def <=>(other)
@@ -35,6 +35,15 @@ module RSCM
35
35
  @path, @developer, @message, @native_revision_identifier, @time, @status = path, developer, message, native_revision_identifier, time, status
36
36
  end
37
37
 
38
+ def to_yaml_properties #:nodoc:
39
+ # We remove properties that are duplicated in the parent revision.
40
+ props = instance_variables
41
+ props.delete("@developer")
42
+ props.delete("@message")
43
+ props.delete("@time")
44
+ props.sort!
45
+ end
46
+
38
47
  # Returns/yields an IO containing the contents of this file, using the +scm+ this
39
48
  # file lives in.
40
49
  def open(scm, options={}, &block) #:yield: io
@@ -58,6 +67,7 @@ module RSCM
58
67
 
59
68
  def ==(other)
60
69
  return false if !other.is_a?(self.class)
70
+ self.status == other.status &&
61
71
  self.path == other.path &&
62
72
  self.developer == other.developer &&
63
73
  self.message == other.message &&
@@ -211,14 +211,14 @@ module RSCM
211
211
  options = options.dup.merge({
212
212
  :dir => File.dirname(@checkout_dir)
213
213
  })
214
- cvs(checkout_command(target_dir, to_identifier), options)
214
+ cvs(checkout_command(target_dir, to_identifier), options, &proc)
215
215
  end
216
216
  end
217
217
 
218
218
  def ignore_paths
219
219
  [/CVS\/.*/]
220
220
  end
221
-
221
+
222
222
  private
223
223
 
224
224
  def cvs(cmd, options={}, &proc)
@@ -271,7 +271,7 @@ module RSCM
271
271
  if(from_identifier.nil? && to_identifier.nil?)
272
272
  ""
273
273
  else
274
- "-d\"#{cvsdate(from_identifier)}<#{cvsdate(to_identifier)}\" "
274
+ "-d\"#{cvsdate(from_identifier)}<#{cvsdate(to_identifier+1)}\" "
275
275
  end
276
276
  end
277
277
 
@@ -2,11 +2,5 @@ require 'rscm/base'
2
2
 
3
3
  module RSCM
4
4
  class Mooky < Base
5
- attr_accessor :foo
6
- attr_accessor :bar
7
-
8
- def initialize(foo="", bar="chocolate bar")
9
- end
10
-
11
5
  end
12
6
  end
@@ -1,488 +1,149 @@
1
+ # TODO
2
+ # Support int revision numbers AND dates
3
+ # Leverage default P4 client settings (optional)
4
+
1
5
  require 'rscm/base'
2
6
  require 'rscm/path_converter'
3
7
  require 'rscm/line_editor'
4
8
 
5
9
  require 'fileutils'
10
+ require 'time'
6
11
  require 'socket'
7
- require 'pp'
8
- require 'parsedate'
9
- require 'stringio'
10
12
 
11
13
  module RSCM
12
- # Perforce RSCM implementation.
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
14
  class Perforce < Base
18
- @@counter = 0
19
-
20
- attr_accessor :client_name
21
- attr_accessor :port
22
- attr_accessor :user
23
- attr_accessor :pwd
24
- attr_accessor :repository_root_dir
25
-
26
- def initialize(port = "1666", user = ENV["LOGNAME"], pwd = "", client_name = Perforce.next_client_name)
27
- @port, @user, @pwd, @client_name = port, user, pwd, client_name
28
- end
29
-
30
- def p4admin
31
- @p4admin ||= P4Admin.new(@port, @user, @pwd)
32
- end
33
-
34
- def p4client
35
- @p4client ||= p4admin.create_client(@checkout_dir, @client_name)
36
- end
37
-
38
- def can_create_central?
39
- true
40
- end
41
-
42
- def create_central
43
- raise "perforce depot can be created only from tests" unless @repository_root_dir
44
- @p4d = P4Daemon.new(@repository_root_dir)
45
- @p4d.start
46
- end
47
-
48
- def destroy_central
49
- @p4d.shutdown
50
- end
51
-
52
- def central_exists?
53
- p4admin.central_exists?
54
- end
55
-
56
- def can_create_central?
57
- true
58
- end
59
-
60
- def supports_trigger?
61
- true
62
- end
63
-
64
- def transactional?
65
- true
66
- end
67
-
68
- def import_central(dir, comment)
69
- with_create_client(dir) do |client|
70
- client.add_all(list_files)
71
- client.submit(comment)
15
+ DATE_FORMAT = "%Y/%m/%d:%H:%M:%S"
16
+ # Doesn't work for empty messages, (Like 21358 in Aslak's P4 repo)
17
+ CHANGELIST_PATTERN = /^Change \d+ by (.*)@.* on (.*)\n\n(.*)\n\nAffected files ...\n\n(.*)/m
18
+ # But this one does
19
+ CHANGELIST_PATTERN_NO_MSG = /^Change \d+ by (.*)@.* on (.*)\n\nAffected files ...\n\n(.*)/m
20
+
21
+ def initialize(user=nil, password=nil, view=nil)
22
+ @user, @password = user, password
23
+ end
24
+
25
+ def revisions(from_identifier, options={})
26
+ from_identifier = Time.epoch unless from_identifier
27
+ from_identifier = Time.epoch if (from_identifier.is_a? Time and from_identifier < Time.epoch)
28
+ from = revision_spec(from_identifier+1) # We have to add 1 because of the constract of this method.
29
+
30
+ to_identifier = options[:to_identifier] ? options[:to_identifier] : Time.infinity
31
+ to = revision_spec(to_identifier)
32
+
33
+ changes = execute("p4 #{p4_opts(false)} changes //...@#{from},#{to}", options) do |io|
34
+ io.read
72
35
  end
73
- end
74
-
75
- def checkout(to_identifier = nil, &proc)
76
- p4client.checkout(to_identifier, &proc)
77
- end
78
-
79
- def add(relative_filename)
80
- p4client.add(relative_filename)
81
- end
82
-
83
- def move(relative_src, relative_dest)
84
- p4client.move(checkout_dir, relative_src, relative_dest)
85
- end
86
-
87
- def commit(message, &proc)
88
- p4client.submit(message, &proc)
89
- end
90
-
91
- def revisions(from_identifier, to_identifier=Time.infinity)
92
- p4client.revisions(from_identifier, to_identifier)
93
- end
94
-
95
- def uptodate?(from_identifier)
96
- p4client.uptodate?
97
- end
98
-
99
- def edit(file)
100
- p4client.edit(file)
101
- end
102
-
103
- def trigger_installed?(trigger_command, trigger_files_checkout_dir)
104
- p4admin.trigger_installed?(trigger_command)
105
- end
106
-
107
- def install_trigger(trigger_command, damagecontrol_install_dir)
108
- p4admin.install_trigger(trigger_command)
109
- end
110
-
111
- def uninstall_trigger(trigger_command, trigger_files_checkout_dir)
112
- p4admin.uninstall_trigger(trigger_command)
113
- end
114
-
115
- def trigger_mechanism
116
- "p4 triggers -i"
117
- end
118
-
119
- def diff(revfile, &proc)
120
- p4client.diff(revfile, &proc)
121
- end
36
+
37
+ revs = changes.collect do |line|
38
+ revision = Revision.new
39
+ revision.identifier = line.match(/^Change (\d+)/)[1].to_i
40
+
41
+ execute("p4 #{p4_opts(false)} describe -s #{revision.identifier}", options) do |io|
42
+ log = io.read
43
+
44
+ if log =~ CHANGELIST_PATTERN
45
+ revision.developer, time, revision.message, files = $1, $2, $3, $4
46
+ elsif log =~ CHANGELIST_PATTERN_NO_MSG
47
+ revision.developer, time, files = $1, $2, $3
48
+ else
49
+ puts "PARSE ERROR:"
50
+ puts log
51
+ puts "\nDIDN'T MATCH:"
52
+ puts CHANGELIST_PATTERN
53
+ end
122
54
 
123
- private
55
+ revision.time = Time.parse(time)
56
+
57
+ files.each_line do |line|
58
+ if line =~ /^\.\.\. (\/\/.+)#(\d+) (.+)/
59
+ if(STATES[$3.strip])
60
+ file = RevisionFile.new
61
+ file.path = $1
62
+ file.native_revision_identifier = $2.to_i
63
+ file.previous_native_revision_identifier = file.native_revision_identifier-1
64
+ file.status = STATES[$3.strip]
65
+ file.time = revision.time
66
+ file.message = revision.message
67
+ file.developer = revision.developer
68
+ revision << file
69
+ end
70
+ end
71
+ end
124
72
 
125
- def with_create_client(rootdir)
126
- raise "needs a block" unless block_given?
127
- rootdir = File.expand_path(rootdir)
128
- with_working_dir(rootdir) do
129
- FileUtils.mkdir_p(rootdir)
130
- client = p4admin.create_client(rootdir, Perforce.next_client_name)
131
- begin
132
- yield client
133
- ensure
134
- delete_client(client)
135
73
  end
136
- end
137
- end
138
-
139
- def self.next_client_name
140
- "temp_client_#{@@counter += 1}"
141
- end
142
-
143
- def delete_client(client)
144
- p4admin.delete_client(client)
145
- end
146
-
147
- def list_files
148
- files = Dir["**/*"].delete_if{|f| File.directory?(f)}
149
- files.collect{|f| File.expand_path(f)}
150
- end
151
- end
152
74
 
153
- # Understands p4 administrative operations (not specific to a client)
154
- class P4Admin
155
-
156
- def initialize(port, user, pwd)
157
- @port, @user, @pwd = port, user, pwd
158
- end
159
-
160
- def create_client(rootdir, clientname)
161
- rootdir = File.expand_path(rootdir) if rootdir =~ /\.\./
162
- unless client_exists?(rootdir, clientname)
163
- execute_popen("client -i", "w+", clientspec(clientname, rootdir))
75
+ revision
164
76
  end
165
- P4Client.new(rootdir, clientname, @port, @user, @pwd)
166
- end
167
-
168
- def client_exists?(rootdir, clientname)
169
- dir_regex = Regexp.new(rootdir)
170
- name_regex = Regexp.new(clientname)
171
- execute("clients").split("\n").find {|c| c =~ dir_regex && c =~ name_regex}
77
+ Revisions.new(revs.reverse)
172
78
  end
173
79
 
174
- def delete_client(client)
175
- execute("client -d #{client.name}")
176
- end
177
-
178
- def trigger_installed?(trigger_command)
179
- triggers.any? {|line| line =~ /#{trigger_command}/}
180
- end
181
-
182
- def install_trigger(trigger_command)
183
- execute_popen("triggers -i", "a+", triggerspec_append(trigger_command))
184
- end
185
-
186
- def uninstall_trigger(trigger_command)
187
- execute_popen("triggers -i", "a+", triggerspec_remove(trigger_command))
188
- end
189
-
190
- def triggerspec_append(trigger_command)
191
- new_trigger = " damagecontrol commit //depot/... \"#{trigger_command}\" "
192
- triggers + $/ + new_trigger
193
- end
194
-
195
- def triggerspec_remove(trigger_command)
196
- triggers.reject {|line| line =~ /#{trigger_command}/}.join
197
- end
80
+ protected
198
81
 
199
- def central_exists?
200
- execute("info").split.join(" ") !~ /Connect to server failed/
82
+ def checkout_silent(to_identifier, options)
83
+ checkout_dir = PathConverter.filepath_to_nativepath(@checkout_dir, false)
84
+ FileUtils.mkdir_p(@checkout_dir)
85
+
86
+ ensure_client(options)
87
+ execute("p4 #{p4_opts} sync", options)
201
88
  end
202
89
 
203
- def clientspec(name, rootdir)
204
- s = StringIO.new
205
- s.puts "Client: #{name}"
206
- s.puts "Owner: #{ENV["LOGNAME"]}"
207
- s.puts "Host: #{ENV["HOSTNAME"]}"
208
- s.puts "Description: another one"
209
- s.puts "Root: #{rootdir}"
210
- s.puts "Options: noallwrite noclobber nocompress unlocked nomodtime normdir"
211
- s.puts "LineEnd: local"
212
- s.puts "View: //depot/... //#{name}/..."
213
- s.string
214
- end
215
-
216
- def triggers
217
- execute("triggers -o")
218
- end
219
-
220
- def execute_popen(cmd, mode, input)
221
- IO.popen(format_cmd(cmd), mode) do |io|
222
- io.puts(input)
223
- io.close_write
224
- io.each_line {|line| debug(line)}
225
- end
226
- end
227
-
228
- def execute(cmd)
229
- cmd = format_cmd(cmd)
230
- $stderr.puts "> executing: #{cmd}"
231
- `#{cmd}`
232
- end
233
-
234
- def format_cmd(cmd)
235
- "p4 -p #{@port} -u '#{@user}' -P '#{@pwd}' #{cmd} 2>&1"
236
- end
237
- end
238
-
239
- # Understands operations against a client-workspace
240
- class P4Client
241
- DATE_FORMAT = "%Y/%m/%d:%H:%M:%S"
242
- STATUS = { "add" => RevisionFile::ADDED, "edit" => RevisionFile::MODIFIED, "delete" => RevisionFile::DELETED }
243
-
244
- def initialize(checkout_dir, name, port, user, pwd)
245
- @checkout_dir, @name, @port, @user, @pwd = checkout_dir, name, port, user, pwd
246
- end
247
-
248
- def uptodate?
249
- p4("sync -n").empty?
250
- end
251
-
252
- def revisions(from_identifier, to_identifier)
253
- revisions = changelists(from_identifier, to_identifier).collect {|changelist| to_revision(changelist)}
254
- # We have to reverse the revisions in order to make them appear in chronological order,
255
- # P4 lists the newest ones first.
256
- Revisions.new(revisions).reverse
257
- end
258
-
259
- def name
260
- @name
261
- end
262
-
263
- def edit(file)
264
- file = File.expand_path(file)
265
- p4("edit #{file}")
266
- end
267
-
268
- def add(relative_path)
269
- add_file(rootdir + "/" + relative_path)
90
+ def ignore_paths
91
+ []
270
92
  end
271
93
 
272
- # http://www.perforce.com/perforce/doc.051/manuals/cmdref/rename.html#1040665
273
- def move(checkout_dir, relative_src, relative_dest)
274
- with_working_dir(checkout_dir) do
275
- absolute_src = PathConverter.filepath_to_nativepath(relative_src, true)
276
- absolute_dest = PathConverter.filepath_to_nativepath(relative_dest, true)
277
- FileUtils.mv(absolute_src, absolute_dest)
278
- p4("integrate #{absolute_src} #{absolute_dest}")
279
- p4("delete #{absolute_src}")
280
- end
281
- # p4("submit #{absolute_src}")
282
- end
283
-
284
- def add_all(files)
285
- files.each {|file| add_file(file)}
286
- end
287
-
288
- def submit(comment)
289
- IO.popen(p4cmd("submit -i"), "w+") do |io|
290
- io.puts(submitspec(comment))
291
- io.close_write
292
- io.each_line {|progress| debug progress}
293
- end
294
- end
295
-
296
- def checkout(to_identifier)
297
- cmd = to_identifier.nil? ? "sync" : "sync //...@#{to_identifier}"
298
- checked_out_files = []
299
- p4(cmd).collect do |output|
300
- #puts "output: '#{output}'"
301
- if(output =~ /.* - (added as|updating|deleted as) #{rootdir}[\/|\\](.*)/)
302
- path = $2.gsub(/\\/, "/")
303
- checked_out_files << path
304
- yield path if block_given?
305
- end
306
- end
307
- checked_out_files
308
- end
309
-
310
- def diff(r)
311
- path = File.expand_path(@checkout_dir + "/" + r.path)
312
- from = r.previous_native_revision_identifier
313
- to = r.native_revision_identifier
314
- cmd = p4cmd("diff2 -du #{path}@#{from} #{path}@#{to}")
315
- Better.popen(cmd) do |io|
316
- return(yield(io))
317
- end
318
- end
319
-
320
94
  private
321
-
322
- def rootdir
323
- unless @rootdir
324
- p4("info") =~ /Client root: (.+)/
325
- @rootdir = $1
326
- end
327
- @rootdir
95
+
96
+ STATES = {
97
+ "add" => RevisionFile::ADDED,
98
+ "edit" => RevisionFile::MODIFIED,
99
+ "delete" => RevisionFile::DELETED
100
+ }
101
+
102
+ def p4_opts(with_client=true)
103
+ user_opt = @user.to_s.empty? ? "" : "-u #{@user}"
104
+ password_opt = @password.to_s.empty? ? "" : "-P #{@password}"
105
+ client_opt = with_client ? "-c \"#{client_name}\"" : ""
106
+ "#{user_opt} #{password_opt} #{client_opt}"
328
107
  end
329
-
330
- def add_file(absolute_path)
331
- absolute_path = PathConverter.filepath_to_nativepath(absolute_path, true)
332
- p4("add #{absolute_path}")
108
+
109
+ def client_name
110
+ raise "checkout_dir not set" unless @checkout_dir
111
+ Socket.gethostname + ":" + @checkout_dir
333
112
  end
334
-
335
- def changelists(from_identifier, to_identifier)
336
- p4changes(from_identifier, to_identifier).collect do |line|
337
- if line =~ /^Change (\d+) /
338
- log = p4describe($1)
339
- P4Changelist.new(log) unless log == ""
340
- end
341
- end
113
+
114
+ def ensure_client(options)
115
+ create_client(options)
342
116
  end
343
-
344
- def to_revision(changelist)
345
- return nil if changelist.nil? # Ugly, but it seems to be nil some times on windows.
346
- changes = changelist.files.collect do |filespec|
347
- change = RevisionFile.new(filespec.path, changelist.developer, changelist.message, filespec.revision, changelist.time)
348
- change.status = STATUS[filespec.status]
349
- change.previous_native_revision_identifier = filespec.revision - 1
350
- change
117
+
118
+ def create_client(options)
119
+ options = {:mode => "w+"}.merge(options)
120
+ execute("p4 #{p4_opts(false)} client -i", options) do |io|
121
+ io.puts(client_spec)
122
+ io.close_write
351
123
  end
352
- revision = Revision.new(changes)
353
- revision.identifier = changelist.number
354
- revision.developer = changelist.developer
355
- revision.message = changelist.message
356
- revision.time = changelist.time
357
- revision
358
124
  end
359
-
360
- def p4changes(from_identifier, to_identifier)
361
- from = p4timespec(from_identifier, Time.epoch)
362
- to = p4timespec(to_identifier, Time.infinity)
363
- $stderr.puts "in p4changes translated #{from_identifier},#{to_identifier} to #{from},#{to}"
364
- p4("changes //...@#{from},#{to}")
125
+
126
+ def client_spec
127
+ <<-EOF
128
+ Client: #{client_name}
129
+ Owner: #{@user}
130
+ Host: #{Socket.gethostname}
131
+ Description: RSCM client
132
+ Root: #{@checkout_dir}
133
+ Options: noallwrite noclobber nocompress unlocked nomodtime normdir
134
+ LineEnd: local
135
+ View: #{@view} //#{client_name}/...
136
+ EOF
365
137
  end
366
-
367
- def p4timespec(identifier, default)
368
- identifier = default if identifier.nil?
138
+
139
+ def revision_spec(identifier)
369
140
  if identifier.is_a?(Time)
370
- identifier = Time.epoch if identifier < Time.epoch
371
- (identifier+1).strftime(DATE_FORMAT)
141
+ identifier.strftime(DATE_FORMAT)
372
142
  else
373
- "#{identifier + 1}"
374
- end
375
- end
376
-
377
- def p4describe(chnum)
378
- p4("describe -s #{chnum}")
379
- end
380
-
381
- def p4(cmd)
382
- cmd = "#{p4cmd(cmd)}"
383
- $stderr.puts "> executing: #{cmd}"
384
- output = `#{cmd}`
385
- #puts output
386
- output
387
- end
388
-
389
- def p4cmd(cmd)
390
- "p4 -p #{@port} -c '#{@name}' -u '#{@user}' -P '#{@pwd}' #{cmd}"
391
- end
392
-
393
- def submitspec(comment)
394
- s = StringIO.new
395
- s.puts "Change: new"
396
- s.puts "Client: #{@name}"
397
- s.puts "Description: #{comment.gsub(/\n/, "\n\t")}"
398
- s.puts "Files: "
399
- p4("opened").each do |line|
400
- if line =~ /^(.+)#\d+ - (\w+) /
401
- status, revision = $1, $2
402
- s.puts "\t#{status} # #{revision}"
403
- end
404
- end
405
- s.string
406
- end
407
-
408
- FileSpec = Struct.new(:path, :revision, :status)
409
-
410
- class P4Changelist
411
- attr_reader :number, :developer, :message, :time, :files
412
-
413
- def initialize(log)
414
- debug log
415
- if(log =~ /^Change (\d+) by (.*) on (.*)$/)
416
- #@number, @developer, @time = $1.to_i, $2, Time.utc(*ParseDate.parsedate($3)[0..5])
417
- @number, @developer, @time = $1.to_i, $2, Time.utc(*ParseDate.parsedate($3))
418
- else
419
- raise "Bad log format: '#{log}'"
420
- end
421
-
422
- if log =~ /Change (.*)\n\n(.*)\n\nAffected/m
423
- @message = $2.strip.gsub(/\n\t/, "\n")
424
- end
425
-
426
- @files = []
427
- log.each do |line|
428
- if line =~ /^\.\.\. \/\/depot\/(.+)#(\d+) (.+)/
429
- files << FileSpec.new($1, Integer($2), $3)
430
- end
431
- end
143
+ identifier.to_i
432
144
  end
433
145
  end
434
- end
435
-
436
- class P4Daemon
437
- include FileUtils
438
-
439
- def initialize(depotpath)
440
- @depotpath = depotpath
441
- end
442
-
443
- def start
444
- launch
445
- assert_running
446
- end
447
-
448
- def assert_running
449
- raise "p4d did not start properly" if timeout(10) { running? }
450
- end
451
-
452
- def launch
453
- fork do
454
- mkdir_p(@depotpath)
455
- cd(@depotpath)
456
- debug "starting p4 server"
457
- exec("p4d")
458
- end
459
- at_exit { shutdown }
460
- end
461
-
462
- def shutdown
463
- `p4 -p 1666 admin stop` if running?
464
- end
465
-
466
- def running?
467
- `p4 -p 1666 info 2>&1`!~ /Connect to server failed/
468
- end
469
- end
470
- end
471
-
472
- module Kernel
473
-
474
- # TODO: use Ruby's built-in timeout? (require 'timeout')
475
- def timeout(attempts=5, &proc)
476
- 0.upto(attempts) do
477
- sleep 1
478
- return false if proc.call
479
- end
480
- true
481
- end
482
146
 
483
- #todo: replace with logger
484
- def debug(msg)
485
- #puts msg
486
147
  end
487
148
 
488
149
  end