Project

General

Profile

Feature #339 » 2.3-perforce.patch

George Gensure, 2013-07-16 17:58

View differences:

app/helpers/repositories_helper.rb
162 162
                            :onchange => "this.name='repository[password]';"))
163 163
  end
164 164

  
165
  def perforce_field_tags(form, repository)
166
    content_tag('p', form.text_field(:root_url, :label => 'P4PORT', :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?))) +
167
    content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true) +
168
                     '<br />(//depot/foo/bar/...)'.html_safe) +
169
    content_tag('p', form.text_field(:login, :size => 30)) +
170
    content_tag('p', form.password_field(:password, :size => 30, :name => 'ignore',
171
                                         :value => ((repository.new_record? || repository.password.blank?) ? '' : ('x'*15)),
172
                                         :onfocus => "this.value=''; this.name='repository[password]';",
173
                                         :onchange => "this.name='repository[password]';"))
174
  end
175

  
176

  
165 177
  def darcs_field_tags(form, repository)
166 178
    content_tag('p', form.text_field(
167 179
                     :url, :label => l(:field_path_to_repository),
app/models/repository/perforce.rb
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/perforce_adapter'
23

  
24
class Repository::Perforce < Repository
25
  validates_presence_of :url
26

  
27
  safe_attributes 'root_url', :if => lambda {|repository, user| repository.new_record? || repository.root_url.blank?}
28

  
29
  def self.scm_adapter_class
30
    Redmine::Scm::Adapters::PerforceAdapter
31
  end
32

  
33
  def self.scm_name
34
    'Perforce'
35
  end
36

  
37
  def supports_directory_revisions?
38
    false
39
  end
40

  
41
  def repo_log_encoding
42
    'UTF-8'
43
  end
44

  
45
  def latest_changesets(path, rev, limit=10)
46
    revisions = scm.revisions(path, rev, nil, :limit => limit)
47
    revisions ? changesets.find_all_by_revision(revisions.collect(&:identifier), :order => "committed_on DESC", :include => :user) : []
48
  end
49

  
50
  def fetch_changesets
51
    logger.info("Executing Perforce fetch_changesets")
52
    scm_info = scm.info
53
    if scm_info
54
      logger.info("SCM info retrieved")
55
      # latest revision found in database
56
      db_revision = latest_changeset ? latest_changeset.revision.to_i : 0
57
      logger.info("SCM: db_revision " + db_revision.to_s )
58
      # latest revision in the repository
59
      scm_revision = scm_info.lastrev.identifier.to_i
60
      logger.info("SCM: scm_revision " + scm_revision.to_s )
61
      if db_revision < scm_revision
62
        logger.debug "Fetching changesets for repository #{url}" if logger && logger.debug?
63
        identifier_from = db_revision + 1
64
        while (identifier_from <= scm_revision)
65
          # loads changesets by batches of 200
66
          identifier_to = [identifier_from + 199, scm_revision].min
67
          revisions = scm.revisions('', identifier_to, identifier_from, :with_paths => true)
68
          revisions.reverse_each do |revision|
69
            transaction do
70
              changeset = Changeset.create(:repository   => self,
71
                                           :revision     => revision.identifier,
72
                                           :committer    => revision.author,
73
                                           :committed_on => revision.time,
74
                                           :comments     => revision.message)
75

  
76
              revision.paths.each do |change|
77
                changeset.create_change(change)
78
              end unless changeset.new_record?
79
            end
80
          end unless revisions.nil?
81
          identifier_from = identifier_to + 1
82
        end
83
      end
84
    end
85
  end
86

  
87
end
config/configuration.yml.example
80 80

  
81 81
# default configuration options for all environments
82 82
default:
83
  scm_perforce_command: /usr/local/bin/p4
83 84
  # Outgoing emails configuration (see examples above)
84 85
  email_delivery:
85 86
    delivery_method: :smtp
config/settings.yml
91 91
  serialized: true
92 92
  default:
93 93
  - Subversion
94
  - Perforce
94 95
  - Darcs
95 96
  - Mercurial
96 97
  - Cvs
lib/redmine.rb
66 66
end
67 67

  
68 68
Redmine::Scm::Base.add "Subversion"
69
Redmine::Scm::Base.add "Perforce"
69 70
Redmine::Scm::Base.add "Darcs"
70 71
Redmine::Scm::Base.add "Mercurial"
71 72
Redmine::Scm::Base.add "Cvs"
lib/redmine/scm/adapters/perforce_adapter.rb
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
              logger.info("DEBUG SCM " + line)
77
              change = parse_change(line)
78
              next unless change
79
              begin
80
                info = Info.new({:root_url => url,
81
                                 :lastrev => Revision.new({
82
                                   :identifier => change[:id],
83
                                   :author => change[:author],
84
                                   :time => change[:time],
85
                                   :message => change[:desc]
86
                                 })
87
                               })
88
              rescue
89
              end
90
            end
91
          end
92
          return nil if $? && $?.exitstatus != 0
93
          info
94
        rescue CommandFailed
95
          return nil
96
        end
97

  
98
        # Returns an Entries collection
99
        # or nil if the given path doesn't exist in the repository
100
        def entries(path=nil, identifier=nil, options={})
101
          query_path = path.empty? ? "#{depot_no_dots}" : "#{depot_no_dots}#{path}"
102
          query_path.chomp!
103
          query_path = query_path.gsub(%r{\/\Z}, '') + "/*"
104
          identifier = (identifier and identifier.to_i > 0) ? "@#{identifier}" : nil
105
          entries = Entries.new
106

  
107
          p4login = credentials_string
108

  
109
          # Dirs
110
          cmd = "#{self.class.sq_bin}"
111
          cmd << p4login
112
          cmd << " dirs "
113
          cmd << shell_quote(query_path)
114
          cmd << "#{identifier}" if identifier
115
          # <path>/* - no such file(s).
116
          # -or-
117
          # <path>
118
          shellout(cmd) do |io|
119
            io.each_line do |line|
120
              # TODO this is actually unnecessary as the cmd will
121
              # write to stderr, not stdin, so we'll never even get
122
              # to this line
123
              next if line =~ %r{ - no such file\(s\)\.$}
124
              full_path = line.gsub(Regexp.new("#{Regexp.escape(depot_no_dots)}"), '')
125
              full_path.chomp!
126
              name = full_path.split("/").last()
127
              entries << Entry.new({
128
                                     :name => name,
129
                                     :path => full_path,
130
                                     :kind => 'dir',
131
                                     :size => nil,
132
                                     :lastrev => make_revision(p4login, full_path + "/...", identifier)
133
                                   })
134
            end
135
          end
136

  
137
          # Files
138
          cmd = "#{self.class.sq_bin}"
139
          cmd << p4login
140
          cmd << " files "
141
          cmd << shell_quote(query_path)
142
          cmd << "#{identifier}" if identifier
143
          # <path>#<n> - <action> change <n> (<type>)
144
          shellout(cmd) do |io|
145
            io.each_line do |line|
146
              next unless line =~ %r{(.+)#(\d+) - (\S+) change (\d+) \((.+)\)}
147
              full_path = $1
148
              action = $3
149
              id = $4
150
              next if action == 'delete'
151
              fixed_path = full_path.gsub(Regexp.new("#{Regexp.escape(depot_no_dots)}"), '')
152
              fixed_path.chomp!
153
              name = fixed_path.split("/").last()
154
              size = nil
155

  
156
              subcmd = "#{self.class.sq_bin}"
157
              subcmd << p4login
158
              subcmd << " fstat -Ol "
159
              subcmd << shell_quote(full_path)
160
              shellout(subcmd) do |subio|
161
                subio.each_line do |subline|
162
                  next unless subline =~ %r{\.\.\. fileSize (\d+)}
163
                  size = $1
164
                end
165
              end
166

  
167
              entries << Entry.new({
168
                                     :name => name,
169
                                     :path => fixed_path,
170
                                     :kind => 'file',
171
                                     :size => size,
172
                                     :lastrev => make_revision(p4login, fixed_path, identifier)
173
                                   })
174
            end
175
          end
176

  
177
          return nil if entries.empty?
178
          logger.debug("Found #{entries.size} entries in the repository for #{target(path)}") if logger && logger.debug?
179
          entries.sort_by_name
180
        end
181

  
182
        def properties(path, identifier=nil)
183
          return nil
184
        end
185

  
186
        def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
187
          base_path = path.empty? ? "#{depot_no_dots}" : "#{depot_no_dots}#{path}"
188
          base_path.chomp!
189
          base_path = base_path.gsub(%r{\/\Z}, '')
190
          # We don't know if 'path' is a file or directory, and p4 has different syntax requirements for each --
191
          #  luckily, we can try both at the same time, and one will work while the other gets effectively ignored.
192
          query_path_file = base_path
193
          query_path_dir = query_path_file + "/..."
194
          # options[:reverse] doesn't make any sense to Perforce
195
          identifer_from = nil if options[:all]
196
          identifier_to = nil if options[:all]
197
          identifier_from = (identifier_from and identifier_from.to_i > 0) ? "@#{identifier_from}" : nil
198
          identifier_to = (identifier_to and identifier_to.to_i > 0) ? "@#{identifier_to}" : nil
199
          revisions = Revisions.new
200

  
201
          p4login = credentials_string
202

  
203
          cmd = "#{self.class.sq_bin}"
204
          cmd << p4login
205
          cmd << " changes -t "
206
          cmd << "-m #{options[:limit]} " if options[:limit]
207
          cmd << shell_quote(query_path_file)
208
          cmd << "#{identifier_to}," if identifier_to
209
          cmd << "#{identifier_from}" if identifier_from
210
          cmd << " "
211
          cmd << shell_quote(query_path_dir)
212
          cmd << "#{identifier_to}," if identifier_to
213
          cmd << "#{identifier_from}" if identifier_from
214
          shellout(cmd) do |io|
215
            io.each_line do |line|
216
              change = parse_change(line)
217
              next unless change
218
              full_desc = ''
219
              paths = []
220
              subcmd = "#{self.class.sq_bin}"
221
              subcmd << p4login
222
              subcmd << " describe -s #{change[:id]}"
223
              shellout(subcmd) do |subio|
224
                subio.each_line do |subline|
225
                  if subline =~ %r{\AChange #{change[:id]}}
226
                    next
227
                  elsif subline =~ %r{\AAffected files \.\.\.}
228
                    next
229
                  elsif subline =~ %r{\A\.\.\. (.+)#(\d+) (\S+)}
230
                    if options[:with_paths]
231
                      subpath = $1
232
                      revision = $2
233
                      action_full = $3
234
                      next if subpath !~ %r{^#{Regexp.escape(base_path)}}
235
                      case
236
                        when action_full == 'add'
237
                          action = 'A'
238
                        when action_full == 'edit'
239
                          action = 'M'
240
                        when action_full == 'delete'
241
                          action = 'D'
242
                        when action_full == 'branch'
243
                          action = 'C'
244
                        when action_full == 'import'
245
                          action = 'A'
246
                        when action_full == 'integrate'
247
                          action = 'M'
248
                        else
249
                          action = 'A' # FIXME: best guess, it's a new file?
250
                      end
251
                      fixed_path = subpath.gsub(Regexp.new("#{Regexp.escape(depot_no_dots)}"), '')
252
                      paths << {:action => action, :path => fixed_path, :revision => revision}
253
                    end
254
                  else
255
                    full_desc << subline
256
                  end
257
                end
258
              end
259
              revisions << Revision.new({
260
                                          :identifier => change[:id],
261
                                          :author => change[:author],
262
                                          :time => change[:time],
263
                                          :message => full_desc.empty? ? change[:desc] : full_desc,
264
                                          :paths => paths
265
                                        })
266
            end
267
          end
268
          return nil if revisions.empty?
269
          revisions
270
        end
271

  
272
        def diff(path, identifier_from, identifier_to=nil)
273
          base_path = path.empty? ? "#{depot_no_dots}" : "#{depot_no_dots}#{path}"
274
          base_path.chomp!
275
          base_path = base_path.gsub(%r{\/\Z}, '')
276
          # We don't know if 'path' is a file or directory, and p4 has different syntax requirements for each.
277
          query_path_file = base_path
278
          query_path_dir = query_path_file + "/..."
279

  
280
          identifier_to = (identifier_to and identifier_to.to_i > 0) ? "@#{identifier_to}" : "@#{identifier_from.to_i - 1}"
281
          identifier_from = (identifier_from and identifier_from.to_i > 0) ? "@#{identifier_from}" : "#head"
282

  
283
          p4login = credentials_string
284

  
285
          diff = []
286

  
287
          # File
288
          cmd = "#{self.class.sq_bin}"
289
          cmd << p4login
290
          cmd << " diff2 -du "
291
          cmd << shell_quote(query_path_file)
292
          cmd << "#{identifier_to} "
293
          cmd << shell_quote(query_path_file)
294
          cmd << "#{identifier_from}"
295
          shellout(cmd) do |io|
296
            io.each_line do |line|
297
              next if line =~ %r{ - no such file\(s\)\.$}
298
              next if line =~ %r{\A====.+==== identical\Z}
299

  
300
              if line =~ %r{\A==== (.+) - (.+) ==== ?(.*)}
301
                file1 = $1
302
                file2 = $2
303
                action = $3
304
                filename = file1
305
                if(file1 =~ %r{<\s*none\s*>})
306
                  filename = file2
307
                end
308
                filename = filename.gsub(%r{ \(.*\)\Z}, '') # remove filetype declaration
309
                filename = filename.gsub(%r{\#\d+\Z}, '') # remove file revision
310

  
311
                diff << "Index: #{filename}"
312
                diff << "==========================================================================="
313
                diff << "--- #{filename}#{identifier_to}"
314
                diff << "+++ #{filename}#{identifier_from}"
315
              else
316
                diff << line
317
              end
318
            end
319
          end
320

  
321
          # Dir
322
          cmd = "#{self.class.sq_bin}"
323
          cmd << p4login
324
          cmd << " diff2 -du "
325
          cmd << shell_quote(query_path_dir)
326
          cmd << "#{identifier_to} "
327
          cmd << shell_quote(query_path_dir)
328
          cmd << "#{identifier_from}"
329
          shellout(cmd) do |io|
330
            io.each_line do |line|
331
              next if line =~ %r{ - no such file\(s\)\.$}
332
              next if line =~ %r{\A====.+==== identical\Z}
333

  
334
              if line =~ %r{\A==== (.+) - (.+) ==== ?(.*)}
335
                file1 = $1
336
                file2 = $2
337
                action = $3
338
                filename = file1
339
                if(file1 =~ %r{<\s*none\s*>})
340
                  filename = file2
341
                end
342
                filename = filename.gsub(%r{ \(.*\)\Z}, '') # remove filetype declaration
343
                filename = filename.gsub(%r{\#\d+\Z}, '') # remove file revision
344

  
345
                diff << "Index: #{filename}"
346
                diff << "==========================================================================="
347
                diff << "--- #{filename}#{identifier_to}"
348
                diff << "+++ #{filename}#{identifier_from}"
349
              else
350
                diff << line
351
              end
352
            end
353
          end
354

  
355
          return nil if diff.empty?
356
          diff
357
        end
358

  
359
        def cat(path, identifier=nil)
360
          return nil if path.empty?
361
          query_path = "#{depot_no_dots}#{path}"
362
          cmd = "#{self.class.sq_bin}"
363
          cmd << credentials_string
364
          cmd << " print -q "
365
          cmd << shell_quote(query_path)
366
          cat = nil
367
          shellout(cmd) do |io|
368
            io.binmode
369
            cat = io.read
370
          end
371
          return nil if $? && $?.exitstatus != 0
372
          cat
373
        end
374

  
375
        def annotate(path, identifier=nil)
376
          return nil if path.empty?
377
          query_path = "#{depot_no_dots}#{path}"
378
          cmd = "#{self.class.sq_bin}"
379
          cmd << credentials_string
380
          cmd << " annotate -q -c "
381
          cmd << shell_quote(query_path)
382
          blame = Annotate.new
383
          shellout(cmd) do |io|
384
            io.each_line do |line|
385
              # <n>: <line>
386
              next unless line =~ %r{(\d+)\:\s(.*)$}
387
              id = $1
388
              rest = $2
389
              blame.add_line(rest.rstrip, Revision.new(:identifier => id))
390
            end
391
          end
392
          return nil if $? && $?.exitstatus != 0
393
          blame
394
        end
395

  
396
        private
397

  
398
        def credentials_login
399
          return '' unless !@login.blank? && !@password.blank?
400

  
401
          File.open("/tmp/perforce_adapter_login", 'w') { |f| f.write(@password) }
402
          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
403
          File.delete("/tmp/perforce_adapter_login")
404
          if ticket.respond_to?(:force_encoding)
405
            ticket.force_encoding('ASCII-8BIT')
406
          end
407

  
408
          str = " -P "
409
          if m = ticket.match(%r/[0-9A-Za-z]{32}/)
410
            str << "#{shell_quote(m[0])}"
411
          else
412
            str << "#{shell_quote(@password)}"
413
          end
414
          str
415
        end
416

  
417
        def credentials_string
418
          str = ''
419
          str << " -p #{shell_quote(@root_url)}"
420
          str << " -u #{shell_quote(@login)}" unless @login.blank?
421
          str << credentials_login
422
          str
423
        end
424

  
425
        def depot
426
          url
427
        end
428

  
429
        def depot_no_dots
430
          url.gsub(Regexp.new("#{Regexp.escape('...')}$"), '')
431
        end
432

  
433
        def parse_change(line)
434
          # Change <n> on <day> <time> by <user>@<client> '<desc>'
435
          return nil unless line =~ %r{Change (\d+) on (\S+) (\S+) by (\S+) '(.*)'$}
436
          id = $1
437
          day = $2
438
          time = $3
439
          author = $4
440
          desc = $5
441
          # FIXME: inefficient to say the least
442
          #cmd = "#{self.class.sq_bin} users #{author.split('@').first}"
443
          #shellout(cmd) do |io|
444
          #  io.each_line do |line|
445
          #    # <user> <<email>> (<name>) accessed <date>
446
          #    next unless line =~ %r{\S+ \S+ \((.*)\) accessed \S+}
447
          #    author = $1
448
          #  end
449
          #end
450
          author = author.split('@').first
451
          return {:id => id, :author => author, :time => Time.parse("#{day} #{time}").localtime, :desc => desc}
452
        end
453

  
454
        def make_revision(p4login, path, identifier)
455
          return nil if path.empty?
456
          identifier ||= ''
457
          query_path = "#{depot_no_dots}#{path}"
458

  
459
          cmd = "#{self.class.sq_bin}"
460
          cmd << p4login
461
          cmd << " changes -m 1 -s submitted -t "
462
          cmd << shell_quote(query_path)
463
          cmd << "#{identifier}" if identifier
464
          revisions = []
465
          shellout(cmd) do |io|
466
            io.each_line do |line|
467
              change = parse_change(line)
468
              next unless change
469
              revisions << Revision.new({
470
                                          :identifier => change[:id],
471
                                          :author => change[:author],
472
                                          :time => change[:time],
473
                                          :message => change[:desc]
474
                                        })
475
            end
476
          end
477
          return nil if revisions.empty?
478
          revisions.first
479
        end
480

  
481
      end
482
    end
483
  end
484
end
(10-10/11)