rscm 0.4.3 → 0.4.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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