Project

General

Profile

Feature #339 » perforce_adapter.rb

Wade Brainerd, 2009-03-18 22:52

 
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
          
226
          begin
227
            fixedpath = (path.blank? ? "#{fix_url(url)}" : "#{fix_url(url)}#{relative_path(path)}")
228

    
229
            if identifier_to.nil?
230
              if(path.empty?)
231
                # this handles when we have NO path..meaning ALL paths in the 'identifier_from' change
232
                p4 = P4.new
233
                begin
234
                  p4.port = root_url
235
                  p4.user = @login
236
                  if (!@password.nil? && !@password.empty?)
237
                    p4.password = @password
238
                  end
239
                  p4.connect
240
                  
241
                  describehash = p4.run("describe", "-s", identifier_from).shift
242
                  change = P4Change.new( describehash )
243
                  change.load_files( describehash )
244
  
245
                  change.files.each do
246
                    |p4dfile|
247
                    # the specs to diff
248
                    spec1 = p4dfile.depot_file + '#' + p4dfile.revno.to_s
249
                    spec2 = p4dfile.depot_file + '#' + (p4dfile.revno.to_i <= 2 ? 1 : p4dfile.revno.to_i-1).to_s
250
  
251
                    # run diff
252
                    diff += p4diff( spec1, spec2, "@#{identifier_from}")
253
                  end
254
                rescue P4Exception
255
                  p4.errors.each { |e| logger.error("Error executing [p4 describe -s #{identifier_from}]:\n #{e}") }
256
                rescue Exception => e
257
                  logger.error("Error executing [p4 describe -s #{identifier_from}]: #{e.message}")
258
                ensure
259
                  p4.disconnect
260
                end
261
              else
262
                # this handles when we have a path..meaning just one path in the 'identifier_from' change
263
                # the specs to diff
264
                spec1 = fixedpath + ((identifier_from.to_i <= 2) ? "@#{identifier_from}" : "@#{identifier_from.to_i-1}")
265
                spec2 = fixedpath + "@#{identifier_from}"
266

    
267
                # run diff
268
                diff += p4diff( spec1, spec2, identifier_from )
269
              end
270
            elsif !identifier_to.nil?
271
              # this handles when we have a path and a 'identifier_to' change number..meaning change-to-change
272
              identifier_from = (identifier_from and identifier_from.to_i > 0) ? ("@#{identifier_from}") : ""
273
              identifier_to = (identifier_to and identifier_to.to_i > 0) ? ("@#{identifier_to}") : ""
274

    
275
              # the specs to diff
276
              spec2 = fixedpath + identifier_from
277
              spec1 = fixedpath + identifier_to
278

    
279
              # run diff
280
              diff += p4diff( spec1, spec2, identifier_from )
281
            end
282
          rescue Exception => e
283
            logger.error("Error performing diff on #{spec1} #{spec2}]: #{e.message}")
284
          end
285

    
286
          diff
287
        end
288

    
289
        def p4diff(spec1, spec2, change)
290
          diff = []
291
          p4 = P4.new
292
          begin
293
            # untagged execution
294
            p4.tagged = false
295
            p4.port = root_url
296
            p4.user = @login
297
            if (!@password.nil? && !@password.empty?)
298
              p4.password = @password
299
            end
300
            p4.connect
301

    
302
            p4.run("diff2", "-u", spec1, spec2).each do
303
              |elem|
304
              # normalize to '\n'
305
              elem.gsub(/\r\n?/, "\n");
306
              elem.split("\n").each do
307
                |line|
308
                # look for file identifier..if found replace date/time with change#
309
                diff << line.gsub(/\d{4}\/\d{2}\/\d{2}\s+\d{2}:\d{2}:\d{2}/, "#{change}")
310
              end
311
            end
312
          rescue P4Exception
313
            p4.errors.each { |e| logger.error("Error executing [p4 diff2 -u #{spec1} #{spec2}]:\n #{e}") }
314
          rescue Exception => e
315
            logger.error("Error executing [p4 diff2 -u #{spec1} #{spec2}]: #{e.message}")
316
          ensure
317
            p4.disconnect
318
          end
319
          diff
320
        end
321

    
322
        def cat(path, identifier=nil)
323
          path = (path.blank? ? "#{fix_url(url)}" : "#{fix_url(url)}#{relative_path(path)}")
324

    
325
          cat = nil
326
          p4 = P4.new
327
          begin
328
            # untagged execution
329
            p4.tagged = false
330
            p4.port = root_url
331
            p4.user = @login
332
            if (!@password.nil? && !@password.empty?)
333
              p4.password = @password
334
            end
335
            p4.connect
336

    
337
            identifier = (identifier and identifier.to_i > 0) ? ("@#{identifier}") : ""
338
            spec = path + identifier
339
            cat = p4.run_print("-q", spec)
340
            cat = cat.to_s
341
          rescue P4Exception
342
            p4.errors.each { |e| logger.error("Error executing [p4 print -q #{spec}]:\n #{e}") }
343
          rescue Exception => e
344
            logger.error("Error executing [p4 print -q #{spec}]: #{e.message}")
345
          ensure
346
            p4.disconnect
347
          end
348
          cat
349
        end
350

    
351
        # Returns just the name of the entry from the depot spec
352
        def entry_name(path)
353
          @entry_name = ( path.include?('/') ? path[1+path.rindex('/')..-1] : path )
354
        end
355

    
356
        # Returns a path relative to the depotspec(url) of the repository
357
        def relative_path(path)
358
          @relative_path = path.gsub(fix_url(url), '')
359
        end
360

    
361
        # Make sure there is a begining '//' and an ending '/', also remove trailing ...
362
        def fix_url(path)
363
          path = (path.starts_with?("//") ? path : "//" + path)
364
          path = path.gsub('/...', '')
365
          @fix_url = (path.ends_with?("/") ? path : path + "/")
366
        end
367
      end
368

    
369
      class P4Change
370
        # Constructor. Pass the hash returned by P4#run_describe( "-s" ) in
371
        # tagged mode.
372
        def initialize( hash )
373
          @change = hash[ "change"  ]
374
          @user	= hash[ "user"    ]
375
          @client = hash[ "client"  ]
376
          @desc	= hash[ "desc"    ]
377
          @time 	= Time.at( hash[ "time" ].to_i )
378

    
379
          @status	= hash[ "status"  ]
380
          @files	= Array.new
381
          @jobs = Hash.new
382

    
383
          if ( hash.has_key?( "job" ) )
384
            hash[ "job" ].each_index do
385
              |i|
386
              job 	= hash[ "job" 	  ][ i ]
387
              status	= hash[ "jobstat" ][ i ]
388
              @jobs[ job ] = status
389
            end
390
          end
391
        end
392

    
393
        attr_reader :change, :user, :client, :desc, :time, :status, :files, :jobs
394
        attr_writer :files
395

    
396
        # Shorthand iterator for looking at the files in the change
397
        def each_file( &block )
398
          @files.each { |f| yield( f ) }
399
        end
400

    
401
        # Shorthand iterator for looking at the jobs fixed by the change
402
        def each_job( &block )
403
          @jobs.each { |j| yield( j ) }
404
        end
405

    
406
        def load_files( hash )
407
          if ( hash.has_key?( "depotFile" ) )
408
            hash[ "depotFile" ].each_index do
409
              |i|
410
              name = hash[ "depotFile"	][ i ]
411
              type = hash[ "type"	][ i ]
412
              rev	 = hash[ "rev" ][ i ]
413
              act	 = hash[ "action"	][ i ]
414
              # create change file
415
              p4chg = P4ChangeFile.new(name)
416
              p4chg.type = type
417
              p4chg.revno = rev.to_i
418
              p4chg.action = act
419

    
420
              @files.push( p4chg )
421
            end
422
          end
423
        end
424
      end
425

    
426
      class P4ChangeFile
427
        def initialize( depotfile )
428
          @depot_file = depotfile
429
          @revno
430
          @type
431
          @action
432
          @head = 1
433
        end
434

    
435
        attr_reader :depot_file
436
        attr_accessor :revno, :type, :head, :action
437
      end
438
    end
439
  end
440
end
(3-3/11)