Project

General

Profile

Feature #339 » perforce_scm_adapter.diff

Jeff Rorison, 2009-01-13 07:28

View differences:

redmine/app/helpers/repositories_helper.rb 2009-01-12 19:39:10.000000000 -0800
145 145
    path.gsub(%r{^/+}, '')
146 146
  end
147 147

  
148
  def perforce_field_tags(form, repository)
149
      content_tag('p', form.text_field(:root_url, :label => 'Port', :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)) +
150
                       '<br />(servername:port, ipaddress:port)') +
151
      content_tag('p', form.text_field(:url, :label => 'DepotSpec', :size => 30, :required => true) +
152
                       '<br />(Perforce depot spec format: (//depot/dir1/dir2/...))') +
153
      content_tag('p', form.text_field(:login, :label => 'User', :size => 30, :required => true)) +
154
      content_tag('p', form.password_field(:password, :size => 30, :name => 'ignore',
155
                                           :value => ((repository.new_record? || repository.password.blank?) ? '' : ('x'*15)),
156
                                           :onfocus => "this.value=''; this.name='repository[password]';",
157
                                           :onchange => "this.name='repository[password]';"))
158
  end
159

  
148 160
  def subversion_field_tags(form, repository)
149 161
      content_tag('p', form.text_field(:url, :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)) +
150 162
                       '<br />(http://, https://, svn://, file:///)') +
redmine/app/models/repository/perforce.rb 2009-01-12 22:08:46.000000000 -0800
1
# redMine - project management software
2
# Copyright (C) 2006-2007  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
require 'redmine/scm/adapters/perforce_adapter'
19

  
20
class Repository::Perforce < Repository
21

  
22
  def scm_adapter
23
    Redmine::Scm::Adapters::PerforceAdapter
24
  end
25

  
26
  def self.scm_name
27
    'Perforce'
28
  end
29

  
30
  def supports_annotate?
31
    # not sure on this one?
32
    false
33
  end
34

  
35
  def changesets_for_path(path)
36
    revisions = scm.revisions(path)
37
    revisions ? changesets.find_all_by_revision(revisions.collect(&:identifier), :order => "committed_on DESC") : []
38
  end
39

  
40
  # Returns a path relative to the url of the repository
41
  def relative_path(path)
42
    scm.relative_path(path)
43
  end
44

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

  
68
              revision.paths.each do
69
                |change|
70

  
71
                revpath = '/' + scm.relative_path( change.depot_file )
72
                action = ( change.action )
73
                revaction = case action
74
                  when "add" then "A"
75
                  when "delete" then "D"
76
                  when "edit" then "M"
77
                  when "branch" then "B"
78
                  #when "integrate" then "I"
79
                  #when "import" then "E"
80
                end
81

  
82
                Change.create(:changeset => changeset,
83
                  :action => revaction,
84
                  :path =>  revpath,
85
                  :revision => '#' + ( change.revno.to_s + '/' + change.head.to_s ))
86
              end
87
            end
88
          end unless revisions.nil?
89
          identifier_from = identifier_to + 1
90
        end
91
      end
92
    end
93
  end
94
end
redmine/config/settings.yml 2009-01-12 21:40:21.000000000 -0800
73 73
  - Cvs
74 74
  - Bazaar
75 75
  - Git
76
  - Perforce
76 77
autofetch_changesets:
77 78
  default: 1
78 79
sys_api_enabled:
redmine/lib/redmine.rb 2009-01-12 17:41:27.000000000 -0800
14 14
  # RMagick is not available
15 15
end
16 16

  
17
REDMINE_SUPPORTED_SCM = %w( Subversion Darcs Mercurial Cvs Bazaar Git Filesystem )
17
REDMINE_SUPPORTED_SCM = %w( Subversion Darcs Mercurial Cvs Bazaar Git Filesystem Perforce )
18 18

  
19 19
# Permissions
20 20
Redmine::AccessControl.map do |map|
redmine/lib/redmine/scm/adapters/perforce_adapter.rb 2009-01-12 22:11:32.000000000 -0800
1
# redMine - project management software
2
# Copyright (C) 2006-2007  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
require 'redmine/scm/adapters/abstract_adapter'
19
require 'rexml/document'
20
require "P4"
21

  
22
module Redmine
23
  module Scm
24
    module Adapters
25
      class PerforceAdapter < AbstractAdapter
26

  
27
        # whether we sould be supporting extended
28
        #  revision info on the revision #rev/head (ie. #1/7
29
        def supports_extended_revision?
30
          true
31
        end
32

  
33
        # Get info about the P4 repo
34
        def info
35

  
36
          path = fix_url(url)
37
          p4 = P4.new
38
          begin
39
            p4.port = root_url
40
            p4.user = @login
41
            if (!@password.nil? && !@password.empty?)
42
              p4.password = @password
43
            end
44

  
45
            p4.connect
46

  
47
            # get latest change for the depot spec
48
            h = p4.run_changes("-m 1 -s submitted", "#{path}...").shift
49
            change = P4Change.new( h )
50

  
51
            info = Info.new({:root_url => url,
52
                :lastrev => Revision.new({
53
                    :identifier => change.change,
54
                    :time => change.time,
55
                    :author => change.user.downcase
56
                  })
57
              })
58
            return info
59
          end
60
        rescue P4Exception
61
          p4.errors.each { |e| logger.error("Error executing [p4 changes -m 1 -s submitted #{path}...]:\n #{e}") }
62
        rescue Exception => e
63
          logger.error("Error executing [p4 changes -m 1 -s submitted #{path}...]: #{e.message}")
64
        ensure
65
          p4.disconnect
66
        end
67

  
68
        # Returns an Entries collection
69
        # or nil if the given path doesn't exist in the repository
70
        def entries(path=nil, identifier=nil)
71
          path = (path.blank? ? "#{fix_url(url)}" : "#{fix_url(url)}#{relative_path(path)}")
72
          path = fix_url(path)
73

  
74
          p4 = P4.new
75
          begin
76
            entries = Entries.new
77
            begin
78
              p4.port = root_url
79
              p4.user = @login
80
              if (!@password.nil? && !@password.empty?)
81
                p4.password = @password
82
              end
83
              p4.connect
84
              p4.run_dirs( path + "*" ).each do
85
                |directory|
86

  
87
                directory = directory[ "dir" ]
88
                dirname = entry_name(directory)
89
                dirpath = relative_path(directory)
90

  
91
                h = p4.run_changes("-m 1 -s submitted",  directory + "/...").shift
92
                change = P4Change.new( h )
93

  
94
                entries << Entry.new({:name => dirname,
95
                    :path => dirpath,
96
                    :kind => 'dir',
97
                    :size => nil,
98
                    :lastrev => Revision.new({
99
                        :identifier => change.change,
100
                        :time => change.time,
101
                        :author => change.user.downcase
102
                      })
103
                  })
104
              end
105
            rescue P4Exception
106
              p4.errors.each { |e| logger.error("Error executing [p4 dirs #{path}*]:\n #{e}") }
107
            end
108

  
109
            begin
110
            #run filelog to get files for change
111
            p4.run_filelog( path + "*" ).each do
112
                |depotfile|
113

  
114
                # newest one
115
                rev = depotfile.revisions[0]
116
                pathname = relative_path(rev.depot_file)
117
                name = entry_name(rev.depot_file)
118

  
119
                # iff deleted skip it
120
                next if rev.action == "delete"
121
                entries << Entry.new({:name => name,
122
                    :path => pathname,
123
                    :kind => 'file',
124
                    :size => rev.filesize.to_i,
125
                    :lastrev => Revision.new({
126
                        :identifier => rev.change,
127
                        :time => rev.time,
128
                        :author => (rev.user ? rev.user.downcase : "")
129
                      })
130
                  })
131
              end
132
            rescue P4Exception
133
              p4.errors.each { |e| logger.error("Error executing [p4 filelog #{path}*]:\n #{e}") }
134
            end
135

  
136
          rescue Exception => e
137
            logger.error("Error creating entries: #{e.message}")
138
          ensure
139
            p4.disconnect
140
          end
141

  
142
          logger.debug("Found #{entries.size} entries in the repository for #{path}") if logger && logger.debug?
143
          entries.sort_by_name
144
        end
145

  
146
        def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
147
          path = (path.blank? ? "#{fix_url(url)}" : "#{fix_url(url)}#{relative_path(path)}")
148

  
149
          p4 = P4.new
150
          begin
151
            revisions = Revisions.new
152
            p4.port = root_url
153
            p4.user = @login
154
            if (!@password.nil? && !@password.empty?)
155
              p4.password = @password
156
            end
157
            p4.connect
158

  
159
            identifier_from = (identifier_from and identifier_from.to_i > 0) ? identifier_from.to_i : ""
160
            identifier_to = (identifier_to and identifier_to.to_i > 0) ? identifier_to.to_i : 1
161

  
162
            opts = "-m 10 -s submitted"
163
            spec = path
164

  
165
            if identifier_from.is_a?(Integer)
166
              changecount = identifier_from - identifier_to
167
              opts = "-m " + changecount.to_s + " -s submitted"
168
              spec = path + "...@#{identifier_from}"
169
            end
170

  
171
            changenum = nil
172
            p4.run_changes("-L", opts, spec).each do
173
              |changehash|
174
              # describe the change
175
              changenum = changehash[ "change" ]
176
              describehash = p4.run("describe", "-s", changenum).shift
177
              # create the change
178
              change = P4Change.new( describehash )
179
              # load files into the change
180
              if ( describehash.has_key?( "depotFile" ) )
181
                describehash[ "depotFile" ].each_index do
182
                  |i|
183
                  name = describehash[ "depotFile"	][ i ]
184
                  type = describehash[ "type"	][ i ]
185
                  rev	 = describehash[ "rev" ][ i ]
186
                  act	 = describehash[ "action"	][ i ]
187
                  # create change file
188
                  p4chg = P4ChangeFile.new(name)
189
                  p4chg.type = type
190
                  p4chg.revno = rev.to_i
191
                  p4chg.action = act
192

  
193
                  # get head revision if needed
194
                  if ( p4chg.revno > 1 )
195
                    flog = p4.run_filelog( p4chg.depot_file ).shift
196
                    p4chg.head = flog.revisions.length
197
                  end
198

  
199
                  change.files.push( p4chg )
200
                end
201
              end
202

  
203
              revisions << Revision.new({:identifier => change.change,
204
                  :author => change.user.downcase,
205
                  :time => change.time,
206
                  :message => change.desc,
207
                  :paths => change.files
208
                })
209
            end
210
          rescue P4Exception
211
            p4.errors.each { |e| logger.error("Error executing [p4 describe -s #{changenum}]:\n #{e}") }
212
          rescue Exception => e
213
            logger.error("Error executing [p4 describe -s #{changenum}]: #{e.message}")
214
          ensure
215
            p4.disconnect
216
          end
217
          # return
218
          revisions
219
        end
220

  
221
        def diff(path, identifier_from, identifier_to=nil, type="inline")
222
          spec1 = nil
223
          spec2 = nil
224
          diff = []
225
          begin
226
            fixedpath = (path.blank? ? "#{fix_url(url)}" : "#{fix_url(url)}#{relative_path(path)}")
227

  
228
            if identifier_to.nil?
229
              if(path.empty?)
230
                # this handles when we have NO path..meaning ALL paths in the 'identifier_from' change
231
                describehash = p4.run("describe", "-s", identifier_from).shift
232
                change = P4Change.new( describehash )
233
                change.load_files( describehash )
234

  
235
                change.files.each do
236
                  |p4dfile|
237
                  # the specs to diff
238
                  spec1 = p4dfile.depot_file + '#' + p4dfile.revno.to_s
239
                  spec2 = p4dfile.depot_file + '#' + (p4dfile.revno.to_i <= 2 ? 1 : p4dfile.revno.to_i-1).to_s
240

  
241
                  # run diff
242
                  diff += p4diff( spec1, spec2, "@#{identifier_from}")
243
                end
244
              else
245
                # this handles when we have a path..meaning just one path in the 'identifier_from' change
246
                # the specs to diff
247
                spec1 = fixedpath + "@#{identifier_from}"
248
                spec2 = fixedpath + '#head'
249

  
250
                # run diff
251
                diff += p4diff( spec1, spec2, "@#{identifier_from}" )
252
              end
253
            elsif !identifier_to.nil?
254
              # this handles when we have a path and a 'identifier_to' change number..meaning change-to-change
255
              identifier_from = (identifier_from and identifier_from.to_i > 0) ? ("@#{identifier_from}") : ""
256
              identifier_to = (identifier_to and identifier_to.to_i > 0) ? ("@#{identifier_to}") : ""
257

  
258
              # the specs to diff
259
              spec2 = fixedpath + identifier_from
260
              spec1 = fixedpath + identifier_to
261

  
262
              # run diff
263
              diff += p4diff( spec1, spec2, identifier_from )
264
            end
265
          rescue Exception => e
266
            logger.error("Error performing diff on #{spec1} #{spec2}]: #{e.message}")
267
          end
268

  
269
          diff
270
        end
271

  
272
        def p4diff(spec1, spec2, change)
273
          diff = []
274
          p4 = P4.new
275
          begin
276
            # untagged execution
277
            p4.tagged = false
278
            p4.port = root_url
279
            p4.user = @login
280
            if (!@password.nil? && !@password.empty?)
281
              p4.password = @password
282
            end
283
            p4.connect
284

  
285
            p4.run("diff2", "-u", spec1, spec2).each do
286
              |elem|
287
              # normalize to '\n'
288
              elem.gsub(/\r\n?/, "\n");
289
              elem.split("\n").each do
290
                |line|
291
                # look for file identifier..if found replace date/time with change#
292
                diff << line.gsub(/\d{4}\/\d{2}\/\d{2}\s+\d{2}:\d{2}:\d{2}/, "#{change}")
293
              end
294
            end
295
          rescue P4Exception
296
            p4.errors.each { |e| logger.error("Error executing [p4 diff2 -u #{spec1} #{spec2}]:\n #{e}") }
297
          rescue Exception => e
298
            logger.error("Error executing [p4 diff2 -u #{spec1} #{spec2}]: #{e.message}")
299
          ensure
300
            p4.disconnect
301
          end
302
          diff
303
        end
304

  
305
        def cat(path, identifier=nil)
306
          path = (path.blank? ? "#{fix_url(url)}" : "#{fix_url(url)}#{relative_path(path)}")
307

  
308
          cat = nil
309
          p4 = P4.new
310
          begin
311
            # untagged execution
312
            p4.tagged = false
313
            p4.port = root_url
314
            #p4.user = @login
315
            if (!@password.nil? && !@password.empty?)
316
              p4.password = @password
317
            end
318
            p4.connect
319

  
320
            identifier = (identifier and identifier.to_i > 0) ? ("@#{identifier}") : ""
321
            spec = path + identifier
322
            cat = p4.run_print("-q", spec)
323
            cat = cat.to_s
324
          rescue P4Exception
325
            p4.errors.each { |e| logger.error("Error executing [p4 print -q #{spec}]:\n #{e}") }
326
          rescue Exception => e
327
            logger.error("Error executing [p4 print -q #{spec}]: #{e.message}")
328
          ensure
329
            p4.disconnect
330
          end
331
          cat
332
        end
333

  
334
        # Returns just the name of the entry from the depot spec
335
        def entry_name(path)
336
          pathname = relative_path(path)
337
          @entry_name = ( pathname.include?('/') ? pathname[1+pathname.rindex('/')..-1] : pathname )
338
        end
339

  
340
        # Returns a path relative to the depotspec(url) of the repository
341
        def relative_path(path)
342
          path.gsub(Regexp.new("^\/?#{Regexp.escape(fix_url(url))}"), '')
343
        end
344

  
345
        # Make sure there is an ending '/'
346
        def fix_url(path)
347
          @fix_url = (path.end_with?("/") ? path : path + "/")
348
        end
349
      end
350

  
351
      class P4Change
352
        # Constructor. Pass the hash returned by P4#run_describe( "-s" ) in
353
        # tagged mode.
354
        def initialize( hash )
355
          @change = hash[ "change"  ]
356
          @user	= hash[ "user"    ]
357
          @client = hash[ "client"  ]
358
          @desc	= hash[ "desc"    ]
359
          @time 	= Time.at( hash[ "time" ].to_i )
360

  
361
          @status	= hash[ "status"  ]
362
          @files	= Array.new
363
          @jobs = Hash.new
364

  
365
          if ( hash.has_key?( "job" ) )
366
            hash[ "job" ].each_index do
367
              |i|
368
              job 	= hash[ "job" 	  ][ i ]
369
              status	= hash[ "jobstat" ][ i ]
370
              @jobs[ job ] = status
371
            end
372
          end
373
        end
374

  
375
        attr_reader :change, :user, :client, :desc, :time, :status, :files, :jobs
376
        attr_writer :files
377

  
378
        # Shorthand iterator for looking at the files in the change
379
        def each_file( &block )
380
          @files.each { |f| yield( f ) }
381
        end
382

  
383
        # Shorthand iterator for looking at the jobs fixed by the change
384
        def each_job( &block )
385
          @jobs.each { |j| yield( j ) }
386
        end
387

  
388
        def load_files( hash )
389
          if ( hash.has_key?( "depotFile" ) )
390
            hash[ "depotFile" ].each_index do
391
              |i|
392
              name = hash[ "depotFile"	][ i ]
393
              type = hash[ "type"	][ i ]
394
              rev	 = hash[ "rev" ][ i ]
395
              act	 = hash[ "action"	][ i ]
396
              # create change file
397
              p4chg = P4ChangeFile.new(name)
398
              p4chg.type = type
399
              p4chg.revno = rev.to_i
400
              p4chg.action = act
401

  
402
              @files.push( p4chg )
403
            end
404
          end
405
        end
406
      end
407

  
408
      class P4ChangeFile
409
        def initialize( depotfile )
410
          @depot_file = depotfile
411
          @revno
412
          @type
413
          @action
414
          @head = 1
415
        end
416

  
417
        attr_reader :depot_file
418
        attr_accessor :revno, :type, :head, :action
419
      end
420
    end
421
  end
422
end
(2-2/11)