1
|
# Redmine - project management software
|
2
|
# Copyright (C) 2006-2011 Jean-Philippe Lang
|
3
|
#
|
4
|
# This program is free software; you can redistribute it and/or
|
5
|
# modify it under the terms of the GNU General Public License
|
6
|
# as published by the Free Software Foundation; either version 2
|
7
|
# of the License, or (at your option) any later version.
|
8
|
#
|
9
|
# This program is distributed in the hope that it will be useful,
|
10
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
11
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
12
|
# GNU General Public License for more details.
|
13
|
#
|
14
|
# You should have received a copy of the GNU General Public License
|
15
|
# along with this program; if not, write to the Free Software
|
16
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
17
|
|
18
|
# Portions of this code adapted from Michael Vance, see:
|
19
|
# http://www.redmine.org/issues/339
|
20
|
# Adapted by Terry Suereth.
|
21
|
|
22
|
require 'redmine/scm/adapters/abstract_adapter'
|
23
|
require 'uri'
|
24
|
|
25
|
module Redmine
|
26
|
module Scm
|
27
|
module Adapters
|
28
|
class PerforceAdapter < AbstractAdapter
|
29
|
|
30
|
# P4 executable name
|
31
|
P4_BIN = Redmine::Configuration['scm_perforce_command'] || "p4"
|
32
|
|
33
|
class << self
|
34
|
def client_command
|
35
|
@@bin ||= P4_BIN
|
36
|
end
|
37
|
|
38
|
def sq_bin
|
39
|
@@sq_bin ||= shell_quote_command
|
40
|
end
|
41
|
|
42
|
def client_version
|
43
|
@@client_version ||= (p4_binary_version || [])
|
44
|
end
|
45
|
|
46
|
def client_available
|
47
|
!client_version.empty?
|
48
|
end
|
49
|
|
50
|
def p4_binary_version
|
51
|
scm_version = scm_version_from_command_line.dup
|
52
|
bin_version = ''
|
53
|
if scm_version.respond_to?(:force_encoding)
|
54
|
scm_version.force_encoding('ASCII-8BIT')
|
55
|
end
|
56
|
if m = scm_version.match(%r{Rev\. P4/[^/]+/([^/]+)/})
|
57
|
bin_version = m[1].scan(%r{\d+}).collect(&:to_i)
|
58
|
end
|
59
|
bin_version
|
60
|
end
|
61
|
|
62
|
def scm_version_from_command_line
|
63
|
shellout("#{sq_bin} -V") { |io| io.read }.to_s
|
64
|
end
|
65
|
end
|
66
|
|
67
|
# Get info about the p4 repository
|
68
|
def info
|
69
|
cmd = "#{self.class.sq_bin}"
|
70
|
cmd << credentials_string
|
71
|
cmd << " changes -m 1 -s submitted -t "
|
72
|
cmd << shell_quote(depot)
|
73
|
info = nil
|
74
|
shellout(cmd) do |io|
|
75
|
io.each_line do |line|
|
76
|
change = parse_change(line)
|
77
|
next unless change
|
78
|
begin
|
79
|
info = Info.new({:root_url => url,
|
80
|
:lastrev => Revision.new({
|
81
|
:identifier => change[:id],
|
82
|
:author => change[:author],
|
83
|
:time => change[:time],
|
84
|
:message => change[:desc]
|
85
|
})
|
86
|
})
|
87
|
rescue
|
88
|
end
|
89
|
end
|
90
|
end
|
91
|
return nil if $? && $?.exitstatus != 0
|
92
|
info
|
93
|
rescue CommandFailed
|
94
|
return nil
|
95
|
end
|
96
|
|
97
|
# Returns an Entries collection
|
98
|
# or nil if the given path doesn't exist in the repository
|
99
|
def entries(path=nil, identifier=nil, options={})
|
100
|
query_path = path.empty? ? "#{depot_no_dots}" : "#{depot_no_dots}#{path}"
|
101
|
query_path.chomp!
|
102
|
query_path = query_path.gsub(%r{\/\Z}, '') + "/*"
|
103
|
identifier = (identifier and identifier.to_i > 0) ? "@#{identifier}" : nil
|
104
|
entries = Entries.new
|
105
|
|
106
|
p4login = credentials_string
|
107
|
|
108
|
# Dirs
|
109
|
cmd = "#{self.class.sq_bin}"
|
110
|
cmd << p4login
|
111
|
cmd << " dirs "
|
112
|
cmd << shell_quote(query_path)
|
113
|
cmd << "#{identifier}" if identifier
|
114
|
# <path>/* - no such file(s).
|
115
|
# -or-
|
116
|
# <path>
|
117
|
shellout(cmd) do |io|
|
118
|
io.each_line do |line|
|
119
|
# TODO this is actually unnecessary as the cmd will
|
120
|
# write to stderr, not stdin, so we'll never even get
|
121
|
# to this line
|
122
|
next if line =~ %r{ - no such file\(s\)\.$}
|
123
|
full_path = line.gsub(Regexp.new("#{Regexp.escape(depot_no_dots)}"), '')
|
124
|
full_path.chomp!
|
125
|
name = full_path.split("/").last()
|
126
|
entries << Entry.new({
|
127
|
:name => name,
|
128
|
:path => full_path,
|
129
|
:kind => 'dir',
|
130
|
:size => nil,
|
131
|
:lastrev => make_revision(p4login, full_path + "/...", identifier)
|
132
|
})
|
133
|
end
|
134
|
end
|
135
|
|
136
|
# Files
|
137
|
cmd = "#{self.class.sq_bin}"
|
138
|
cmd << p4login
|
139
|
cmd << " files "
|
140
|
cmd << shell_quote(query_path)
|
141
|
cmd << "#{identifier}" if identifier
|
142
|
# <path>#<n> - <action> change <n> (<type>)
|
143
|
shellout(cmd) do |io|
|
144
|
io.each_line do |line|
|
145
|
next unless line =~ %r{(.+)#(\d+) - (\S+) change (\d+) \((.+)\)}
|
146
|
full_path = $1
|
147
|
action = $3
|
148
|
id = $4
|
149
|
next if action == 'delete'
|
150
|
fixed_path = full_path.gsub(Regexp.new("#{Regexp.escape(depot_no_dots)}"), '')
|
151
|
fixed_path.chomp!
|
152
|
name = fixed_path.split("/").last()
|
153
|
size = nil
|
154
|
|
155
|
subcmd = "#{self.class.sq_bin}"
|
156
|
subcmd << p4login
|
157
|
subcmd << " fstat -Ol "
|
158
|
subcmd << shell_quote(full_path)
|
159
|
shellout(subcmd) do |subio|
|
160
|
subio.each_line do |subline|
|
161
|
next unless subline =~ %r{\.\.\. fileSize (\d+)}
|
162
|
size = $1
|
163
|
end
|
164
|
end
|
165
|
|
166
|
entries << Entry.new({
|
167
|
:name => name,
|
168
|
:path => fixed_path,
|
169
|
:kind => 'file',
|
170
|
:size => size,
|
171
|
:lastrev => make_revision(p4login, fixed_path, identifier)
|
172
|
})
|
173
|
end
|
174
|
end
|
175
|
|
176
|
return nil if entries.empty?
|
177
|
logger.debug("Found #{entries.size} entries in the repository for #{target(path)}") if logger && logger.debug?
|
178
|
entries.sort_by_name
|
179
|
end
|
180
|
|
181
|
def properties(path, identifier=nil)
|
182
|
return nil
|
183
|
end
|
184
|
|
185
|
def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
|
186
|
base_path = path.empty? ? "#{depot_no_dots}" : "#{depot_no_dots}#{path}"
|
187
|
base_path.chomp!
|
188
|
base_path = base_path.gsub(%r{\/\Z}, '')
|
189
|
# We don't know if 'path' is a file or directory, and p4 has different syntax requirements for each --
|
190
|
# luckily, we can try both at the same time, and one will work while the other gets effectively ignored.
|
191
|
query_path_file = base_path
|
192
|
query_path_dir = query_path_file + "/..."
|
193
|
# options[:reverse] doesn't make any sense to Perforce
|
194
|
identifer_from = nil if options[:all]
|
195
|
identifier_to = nil if options[:all]
|
196
|
identifier_from = (identifier_from and identifier_from.to_i > 0) ? "@#{identifier_from}" : nil
|
197
|
identifier_to = (identifier_to and identifier_to.to_i > 0) ? "@#{identifier_to}" : nil
|
198
|
revisions = Revisions.new
|
199
|
|
200
|
p4login = credentials_string
|
201
|
|
202
|
cmd = "#{self.class.sq_bin}"
|
203
|
cmd << p4login
|
204
|
cmd << " changes -t "
|
205
|
cmd << "-m #{options[:limit]} " if options[:limit]
|
206
|
cmd << shell_quote(query_path_file)
|
207
|
cmd << "#{identifier_to}," if identifier_to
|
208
|
cmd << "#{identifier_from}" if identifier_from
|
209
|
cmd << " "
|
210
|
cmd << shell_quote(query_path_dir)
|
211
|
cmd << "#{identifier_to}," if identifier_to
|
212
|
cmd << "#{identifier_from}" if identifier_from
|
213
|
shellout(cmd) do |io|
|
214
|
io.each_line do |line|
|
215
|
change = parse_change(line)
|
216
|
next unless change
|
217
|
full_desc = ''
|
218
|
paths = []
|
219
|
subcmd = "#{self.class.sq_bin}"
|
220
|
subcmd << p4login
|
221
|
subcmd << " describe -s #{change[:id]}"
|
222
|
shellout(subcmd) do |subio|
|
223
|
subio.each_line do |subline|
|
224
|
if subline =~ %r{\AChange #{change[:id]}}
|
225
|
next
|
226
|
elsif subline =~ %r{\AAffected files \.\.\.}
|
227
|
next
|
228
|
elsif subline =~ %r{\A\.\.\. (.+)#(\d+) (\S+)}
|
229
|
if options[:with_paths]
|
230
|
subpath = $1
|
231
|
revision = $2
|
232
|
action_full = $3
|
233
|
next if subpath !~ %r{^#{Regexp.escape(base_path)}}
|
234
|
case
|
235
|
when action_full == 'add'
|
236
|
action = 'A'
|
237
|
when action_full == 'edit'
|
238
|
action = 'M'
|
239
|
when action_full == 'delete'
|
240
|
action = 'D'
|
241
|
when action_full == 'branch'
|
242
|
action = 'C'
|
243
|
when action_full == 'import'
|
244
|
action = 'A'
|
245
|
when action_full == 'integrate'
|
246
|
action = 'M'
|
247
|
else
|
248
|
action = 'A' # FIXME: best guess, it's a new file?
|
249
|
end
|
250
|
fixed_path = subpath.gsub(Regexp.new("#{Regexp.escape(depot_no_dots)}"), '')
|
251
|
paths << {:action => action, :path => fixed_path, :revision => revision}
|
252
|
end
|
253
|
else
|
254
|
full_desc << subline
|
255
|
end
|
256
|
end
|
257
|
end
|
258
|
revisions << Revision.new({
|
259
|
:identifier => change[:id],
|
260
|
:author => change[:author],
|
261
|
:time => change[:time],
|
262
|
:message => full_desc.empty? ? change[:desc] : full_desc,
|
263
|
:paths => paths
|
264
|
})
|
265
|
end
|
266
|
end
|
267
|
return nil if revisions.empty?
|
268
|
revisions
|
269
|
end
|
270
|
|
271
|
def diff(path, identifier_from, identifier_to=nil)
|
272
|
base_path = path.empty? ? "#{depot_no_dots}" : "#{depot_no_dots}#{path}"
|
273
|
base_path.chomp!
|
274
|
base_path = base_path.gsub(%r{\/\Z}, '')
|
275
|
# We don't know if 'path' is a file or directory, and p4 has different syntax requirements for each.
|
276
|
query_path_file = base_path
|
277
|
query_path_dir = query_path_file + "/..."
|
278
|
|
279
|
identifier_to = (identifier_to and identifier_to.to_i > 0) ? "@#{identifier_to}" : "@#{identifier_from.to_i - 1}"
|
280
|
identifier_from = (identifier_from and identifier_from.to_i > 0) ? "@#{identifier_from}" : "#head"
|
281
|
|
282
|
p4login = credentials_string
|
283
|
|
284
|
diff = []
|
285
|
|
286
|
# File
|
287
|
cmd = "#{self.class.sq_bin}"
|
288
|
cmd << p4login
|
289
|
cmd << " diff2 -du "
|
290
|
cmd << shell_quote(query_path_file)
|
291
|
cmd << "#{identifier_to} "
|
292
|
cmd << shell_quote(query_path_file)
|
293
|
cmd << "#{identifier_from}"
|
294
|
shellout(cmd) do |io|
|
295
|
io.each_line do |line|
|
296
|
next if line =~ %r{ - no such file\(s\)\.$}
|
297
|
next if line =~ %r{\A====.+==== identical\Z}
|
298
|
|
299
|
if line =~ %r{\A==== (.+) - (.+) ==== ?(.*)}
|
300
|
file1 = $1
|
301
|
file2 = $2
|
302
|
action = $3
|
303
|
filename = file1
|
304
|
if(file1 =~ %r{<\s*none\s*>})
|
305
|
filename = file2
|
306
|
end
|
307
|
filename = filename.gsub(%r{ \(.*\)\Z}, '') # remove filetype declaration
|
308
|
filename = filename.gsub(%r{\#\d+\Z}, '') # remove file revision
|
309
|
|
310
|
diff << "Index: #{filename}"
|
311
|
diff << "==========================================================================="
|
312
|
diff << "--- #{filename}#{identifier_to}"
|
313
|
diff << "+++ #{filename}#{identifier_from}"
|
314
|
else
|
315
|
diff << line
|
316
|
end
|
317
|
end
|
318
|
end
|
319
|
|
320
|
# Dir
|
321
|
cmd = "#{self.class.sq_bin}"
|
322
|
cmd << p4login
|
323
|
cmd << " diff2 -du "
|
324
|
cmd << shell_quote(query_path_dir)
|
325
|
cmd << "#{identifier_to} "
|
326
|
cmd << shell_quote(query_path_dir)
|
327
|
cmd << "#{identifier_from}"
|
328
|
shellout(cmd) do |io|
|
329
|
io.each_line do |line|
|
330
|
next if line =~ %r{ - no such file\(s\)\.$}
|
331
|
next if line =~ %r{\A====.+==== identical\Z}
|
332
|
|
333
|
if line =~ %r{\A==== (.+) - (.+) ==== ?(.*)}
|
334
|
file1 = $1
|
335
|
file2 = $2
|
336
|
action = $3
|
337
|
filename = file1
|
338
|
if(file1 =~ %r{<\s*none\s*>})
|
339
|
filename = file2
|
340
|
end
|
341
|
filename = filename.gsub(%r{ \(.*\)\Z}, '') # remove filetype declaration
|
342
|
filename = filename.gsub(%r{\#\d+\Z}, '') # remove file revision
|
343
|
|
344
|
diff << "Index: #{filename}"
|
345
|
diff << "==========================================================================="
|
346
|
diff << "--- #{filename}#{identifier_to}"
|
347
|
diff << "+++ #{filename}#{identifier_from}"
|
348
|
else
|
349
|
diff << line
|
350
|
end
|
351
|
end
|
352
|
end
|
353
|
|
354
|
return nil if diff.empty?
|
355
|
diff
|
356
|
end
|
357
|
|
358
|
def cat(path, identifier=nil)
|
359
|
return nil if path.empty?
|
360
|
query_path = "#{depot_no_dots}#{path}"
|
361
|
cmd = "#{self.class.sq_bin}"
|
362
|
cmd << credentials_string
|
363
|
cmd << " print -q "
|
364
|
cmd << shell_quote(query_path)
|
365
|
cat = nil
|
366
|
shellout(cmd) do |io|
|
367
|
io.binmode
|
368
|
cat = io.read
|
369
|
end
|
370
|
return nil if $? && $?.exitstatus != 0
|
371
|
cat
|
372
|
end
|
373
|
|
374
|
def annotate(path, identifier=nil)
|
375
|
return nil if path.empty?
|
376
|
query_path = "#{depot_no_dots}#{path}"
|
377
|
cmd = "#{self.class.sq_bin}"
|
378
|
cmd << credentials_string
|
379
|
cmd << " annotate -q -c "
|
380
|
cmd << shell_quote(query_path)
|
381
|
blame = Annotate.new
|
382
|
shellout(cmd) do |io|
|
383
|
io.each_line do |line|
|
384
|
# <n>: <line>
|
385
|
next unless line =~ %r{(\d+)\:\s(.*)$}
|
386
|
id = $1
|
387
|
rest = $2
|
388
|
blame.add_line(rest.rstrip, Revision.new(:identifier => id))
|
389
|
end
|
390
|
end
|
391
|
return nil if $? && $?.exitstatus != 0
|
392
|
blame
|
393
|
end
|
394
|
|
395
|
private
|
396
|
|
397
|
def credentials_login
|
398
|
return nil unless !@login.blank? && !@password.blank?
|
399
|
|
400
|
File.open("/tmp/perforce_adapter_login", 'w') { |f| f.write(@password) }
|
401
|
ticket = shellout("#{self.class.sq_bin} -p #{shell_quote(@root_url)} -u #{shell_quote(@login)} login -p </tmp/perforce_adapter_login 2>/dev/null") { |io| io.read }.to_s
|
402
|
File.delete("/tmp/perforce_adapter_login")
|
403
|
if ticket.respond_to?(:force_encoding)
|
404
|
ticket.force_encoding('ASCII-8BIT')
|
405
|
end
|
406
|
|
407
|
str = " -P "
|
408
|
if m = ticket.match(%r/[0-9A-Za-z]{32}/)
|
409
|
str << "#{shell_quote(m[0])}"
|
410
|
else
|
411
|
str << "#{shell_quote(@password)}"
|
412
|
end
|
413
|
str
|
414
|
end
|
415
|
|
416
|
def credentials_string
|
417
|
str = ''
|
418
|
str << " -p #{shell_quote(@root_url)}"
|
419
|
str << " -u #{shell_quote(@login)}" unless @login.blank?
|
420
|
str << credentials_login
|
421
|
str
|
422
|
end
|
423
|
|
424
|
def depot
|
425
|
url
|
426
|
end
|
427
|
|
428
|
def depot_no_dots
|
429
|
url.gsub(Regexp.new("#{Regexp.escape('...')}$"), '')
|
430
|
end
|
431
|
|
432
|
def parse_change(line)
|
433
|
# Change <n> on <day> <time> by <user>@<client> '<desc>'
|
434
|
return nil unless line =~ %r{Change (\d+) on (\S+) (\S+) by (\S+) '(.*)'$}
|
435
|
id = $1
|
436
|
day = $2
|
437
|
time = $3
|
438
|
author = $4
|
439
|
desc = $5
|
440
|
# FIXME: inefficient to say the least
|
441
|
#cmd = "#{self.class.sq_bin} users #{author.split('@').first}"
|
442
|
#shellout(cmd) do |io|
|
443
|
# io.each_line do |line|
|
444
|
# # <user> <<email>> (<name>) accessed <date>
|
445
|
# next unless line =~ %r{\S+ \S+ \((.*)\) accessed \S+}
|
446
|
# author = $1
|
447
|
# end
|
448
|
#end
|
449
|
author = author.split('@').first
|
450
|
return {:id => id, :author => author, :time => Time.parse("#{day} #{time}").localtime, :desc => desc}
|
451
|
end
|
452
|
|
453
|
def make_revision(p4login, path, identifier)
|
454
|
return nil if path.empty?
|
455
|
identifier ||= ''
|
456
|
query_path = "#{depot_no_dots}#{path}"
|
457
|
|
458
|
cmd = "#{self.class.sq_bin}"
|
459
|
cmd << p4login
|
460
|
cmd << " changes -m 1 -s submitted -t "
|
461
|
cmd << shell_quote(query_path)
|
462
|
cmd << "#{identifier}" if identifier
|
463
|
revisions = []
|
464
|
shellout(cmd) do |io|
|
465
|
io.each_line do |line|
|
466
|
change = parse_change(line)
|
467
|
next unless change
|
468
|
revisions << Revision.new({
|
469
|
:identifier => change[:id],
|
470
|
:author => change[:author],
|
471
|
:time => change[:time],
|
472
|
:message => change[:desc]
|
473
|
})
|
474
|
end
|
475
|
end
|
476
|
return nil if revisions.empty?
|
477
|
revisions.first
|
478
|
end
|
479
|
|
480
|
end
|
481
|
end
|
482
|
end
|
483
|
end
|