Index: redmine/app/helpers/repositories_helper.rb =================================================================== --- redmine.orig/app/helpers/repositories_helper.rb 2009-01-12 19:37:48.000000000 -0800 +++ redmine/app/helpers/repositories_helper.rb 2009-01-12 19:39:10.000000000 -0800 @@ -145,6 +145,18 @@ path.gsub(%r{^/+}, '') end + def perforce_field_tags(form, repository) + content_tag('p', form.text_field(:root_url, :label => 'Port', :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)) + + '
(servername:port, ipaddress:port)') + + content_tag('p', form.text_field(:url, :label => 'DepotSpec', :size => 30, :required => true) + + '
(Perforce depot spec format: (//depot/dir1/dir2/...))') + + content_tag('p', form.text_field(:login, :label => 'User', :size => 30, :required => true)) + + content_tag('p', form.password_field(:password, :size => 30, :name => 'ignore', + :value => ((repository.new_record? || repository.password.blank?) ? '' : ('x'*15)), + :onfocus => "this.value=''; this.name='repository[password]';", + :onchange => "this.name='repository[password]';")) + end + def subversion_field_tags(form, repository) content_tag('p', form.text_field(:url, :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)) + '
(http://, https://, svn://, file:///)') + Index: redmine/app/models/repository/perforce.rb =================================================================== --- /dev/null 1970-01-01 00:00:00.000000000 +0000 +++ redmine/app/models/repository/perforce.rb 2009-01-12 22:08:46.000000000 -0800 @@ -0,0 +1,94 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require 'redmine/scm/adapters/perforce_adapter' + +class Repository::Perforce < Repository + + def scm_adapter + Redmine::Scm::Adapters::PerforceAdapter + end + + def self.scm_name + 'Perforce' + end + + def supports_annotate? + # not sure on this one? + false + end + + def changesets_for_path(path) + revisions = scm.revisions(path) + revisions ? changesets.find_all_by_revision(revisions.collect(&:identifier), :order => "committed_on DESC") : [] + end + + # Returns a path relative to the url of the repository + def relative_path(path) + scm.relative_path(path) + end + + def fetch_changesets + scm_info = scm.info + if scm_info + # latest revision found in database + db_revision = latest_changeset ? latest_changeset.revision.to_i : 0 + # latest revision in the repository + scm_revision = scm_info.lastrev.identifier.to_i + if db_revision < scm_revision + logger.debug "Fetching changesets for repository #{url}" if logger && logger.debug? + identifier_from = db_revision + 1 + while (identifier_from <= scm_revision) + # loads changesets by batches of 200 + identifier_to = [identifier_from + 199, scm_revision].min + revisions = scm.revisions('', identifier_to, identifier_from, :with_paths => true) + transaction do + revisions.reverse_each do + |revision| + changeset = Changeset.create(:repository => self, + :revision => revision.identifier, + :committer => revision.author, + :committed_on => revision.time, + :comments => revision.message) + + revision.paths.each do + |change| + + revpath = '/' + scm.relative_path( change.depot_file ) + action = ( change.action ) + revaction = case action + when "add" then "A" + when "delete" then "D" + when "edit" then "M" + when "branch" then "B" + #when "integrate" then "I" + #when "import" then "E" + end + + Change.create(:changeset => changeset, + :action => revaction, + :path => revpath, + :revision => '#' + ( change.revno.to_s + '/' + change.head.to_s )) + end + end + end unless revisions.nil? + identifier_from = identifier_to + 1 + end + end + end + end +end Index: redmine/config/settings.yml =================================================================== --- redmine.orig/config/settings.yml 2009-01-12 21:39:42.000000000 -0800 +++ redmine/config/settings.yml 2009-01-12 21:40:21.000000000 -0800 @@ -73,6 +73,7 @@ - Cvs - Bazaar - Git + - Perforce autofetch_changesets: default: 1 sys_api_enabled: Index: redmine/lib/redmine.rb =================================================================== --- redmine.orig/lib/redmine.rb 2009-01-12 17:40:52.000000000 -0800 +++ redmine/lib/redmine.rb 2009-01-12 17:41:27.000000000 -0800 @@ -14,7 +14,7 @@ # RMagick is not available end -REDMINE_SUPPORTED_SCM = %w( Subversion Darcs Mercurial Cvs Bazaar Git Filesystem ) +REDMINE_SUPPORTED_SCM = %w( Subversion Darcs Mercurial Cvs Bazaar Git Filesystem Perforce ) # Permissions Redmine::AccessControl.map do |map| Index: redmine/lib/redmine/scm/adapters/perforce_adapter.rb =================================================================== --- /dev/null 1970-01-01 00:00:00.000000000 +0000 +++ redmine/lib/redmine/scm/adapters/perforce_adapter.rb 2009-01-12 22:11:32.000000000 -0800 @@ -0,0 +1,422 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require 'redmine/scm/adapters/abstract_adapter' +require 'rexml/document' +require "P4" + +module Redmine + module Scm + module Adapters + class PerforceAdapter < AbstractAdapter + + # whether we sould be supporting extended + # revision info on the revision #rev/head (ie. #1/7 + def supports_extended_revision? + true + end + + # Get info about the P4 repo + def info + + path = fix_url(url) + p4 = P4.new + begin + p4.port = root_url + p4.user = @login + if (!@password.nil? && !@password.empty?) + p4.password = @password + end + + p4.connect + + # get latest change for the depot spec + h = p4.run_changes("-m 1 -s submitted", "#{path}...").shift + change = P4Change.new( h ) + + info = Info.new({:root_url => url, + :lastrev => Revision.new({ + :identifier => change.change, + :time => change.time, + :author => change.user.downcase + }) + }) + return info + end + rescue P4Exception + p4.errors.each { |e| logger.error("Error executing [p4 changes -m 1 -s submitted #{path}...]:\n #{e}") } + rescue Exception => e + logger.error("Error executing [p4 changes -m 1 -s submitted #{path}...]: #{e.message}") + ensure + p4.disconnect + end + + # Returns an Entries collection + # or nil if the given path doesn't exist in the repository + def entries(path=nil, identifier=nil) + path = (path.blank? ? "#{fix_url(url)}" : "#{fix_url(url)}#{relative_path(path)}") + path = fix_url(path) + + p4 = P4.new + begin + entries = Entries.new + begin + p4.port = root_url + p4.user = @login + if (!@password.nil? && !@password.empty?) + p4.password = @password + end + p4.connect + p4.run_dirs( path + "*" ).each do + |directory| + + directory = directory[ "dir" ] + dirname = entry_name(directory) + dirpath = relative_path(directory) + + h = p4.run_changes("-m 1 -s submitted", directory + "/...").shift + change = P4Change.new( h ) + + entries << Entry.new({:name => dirname, + :path => dirpath, + :kind => 'dir', + :size => nil, + :lastrev => Revision.new({ + :identifier => change.change, + :time => change.time, + :author => change.user.downcase + }) + }) + end + rescue P4Exception + p4.errors.each { |e| logger.error("Error executing [p4 dirs #{path}*]:\n #{e}") } + end + + begin + #run filelog to get files for change + p4.run_filelog( path + "*" ).each do + |depotfile| + + # newest one + rev = depotfile.revisions[0] + pathname = relative_path(rev.depot_file) + name = entry_name(rev.depot_file) + + # iff deleted skip it + next if rev.action == "delete" + entries << Entry.new({:name => name, + :path => pathname, + :kind => 'file', + :size => rev.filesize.to_i, + :lastrev => Revision.new({ + :identifier => rev.change, + :time => rev.time, + :author => (rev.user ? rev.user.downcase : "") + }) + }) + end + rescue P4Exception + p4.errors.each { |e| logger.error("Error executing [p4 filelog #{path}*]:\n #{e}") } + end + + rescue Exception => e + logger.error("Error creating entries: #{e.message}") + ensure + p4.disconnect + end + + logger.debug("Found #{entries.size} entries in the repository for #{path}") if logger && logger.debug? + entries.sort_by_name + end + + def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}) + path = (path.blank? ? "#{fix_url(url)}" : "#{fix_url(url)}#{relative_path(path)}") + + p4 = P4.new + begin + revisions = Revisions.new + p4.port = root_url + p4.user = @login + if (!@password.nil? && !@password.empty?) + p4.password = @password + end + p4.connect + + identifier_from = (identifier_from and identifier_from.to_i > 0) ? identifier_from.to_i : "" + identifier_to = (identifier_to and identifier_to.to_i > 0) ? identifier_to.to_i : 1 + + opts = "-m 10 -s submitted" + spec = path + + if identifier_from.is_a?(Integer) + changecount = identifier_from - identifier_to + opts = "-m " + changecount.to_s + " -s submitted" + spec = path + "...@#{identifier_from}" + end + + changenum = nil + p4.run_changes("-L", opts, spec).each do + |changehash| + # describe the change + changenum = changehash[ "change" ] + describehash = p4.run("describe", "-s", changenum).shift + # create the change + change = P4Change.new( describehash ) + # load files into the change + if ( describehash.has_key?( "depotFile" ) ) + describehash[ "depotFile" ].each_index do + |i| + name = describehash[ "depotFile" ][ i ] + type = describehash[ "type" ][ i ] + rev = describehash[ "rev" ][ i ] + act = describehash[ "action" ][ i ] + # create change file + p4chg = P4ChangeFile.new(name) + p4chg.type = type + p4chg.revno = rev.to_i + p4chg.action = act + + # get head revision if needed + if ( p4chg.revno > 1 ) + flog = p4.run_filelog( p4chg.depot_file ).shift + p4chg.head = flog.revisions.length + end + + change.files.push( p4chg ) + end + end + + revisions << Revision.new({:identifier => change.change, + :author => change.user.downcase, + :time => change.time, + :message => change.desc, + :paths => change.files + }) + end + rescue P4Exception + p4.errors.each { |e| logger.error("Error executing [p4 describe -s #{changenum}]:\n #{e}") } + rescue Exception => e + logger.error("Error executing [p4 describe -s #{changenum}]: #{e.message}") + ensure + p4.disconnect + end + # return + revisions + end + + def diff(path, identifier_from, identifier_to=nil, type="inline") + spec1 = nil + spec2 = nil + diff = [] + begin + fixedpath = (path.blank? ? "#{fix_url(url)}" : "#{fix_url(url)}#{relative_path(path)}") + + if identifier_to.nil? + if(path.empty?) + # this handles when we have NO path..meaning ALL paths in the 'identifier_from' change + describehash = p4.run("describe", "-s", identifier_from).shift + change = P4Change.new( describehash ) + change.load_files( describehash ) + + change.files.each do + |p4dfile| + # the specs to diff + spec1 = p4dfile.depot_file + '#' + p4dfile.revno.to_s + spec2 = p4dfile.depot_file + '#' + (p4dfile.revno.to_i <= 2 ? 1 : p4dfile.revno.to_i-1).to_s + + # run diff + diff += p4diff( spec1, spec2, "@#{identifier_from}") + end + else + # this handles when we have a path..meaning just one path in the 'identifier_from' change + # the specs to diff + spec1 = fixedpath + "@#{identifier_from}" + spec2 = fixedpath + '#head' + + # run diff + diff += p4diff( spec1, spec2, "@#{identifier_from}" ) + end + elsif !identifier_to.nil? + # this handles when we have a path and a 'identifier_to' change number..meaning change-to-change + identifier_from = (identifier_from and identifier_from.to_i > 0) ? ("@#{identifier_from}") : "" + identifier_to = (identifier_to and identifier_to.to_i > 0) ? ("@#{identifier_to}") : "" + + # the specs to diff + spec2 = fixedpath + identifier_from + spec1 = fixedpath + identifier_to + + # run diff + diff += p4diff( spec1, spec2, identifier_from ) + end + rescue Exception => e + logger.error("Error performing diff on #{spec1} #{spec2}]: #{e.message}") + end + + diff + end + + def p4diff(spec1, spec2, change) + diff = [] + p4 = P4.new + begin + # untagged execution + p4.tagged = false + p4.port = root_url + p4.user = @login + if (!@password.nil? && !@password.empty?) + p4.password = @password + end + p4.connect + + p4.run("diff2", "-u", spec1, spec2).each do + |elem| + # normalize to '\n' + elem.gsub(/\r\n?/, "\n"); + elem.split("\n").each do + |line| + # look for file identifier..if found replace date/time with change# + diff << line.gsub(/\d{4}\/\d{2}\/\d{2}\s+\d{2}:\d{2}:\d{2}/, "#{change}") + end + end + rescue P4Exception + p4.errors.each { |e| logger.error("Error executing [p4 diff2 -u #{spec1} #{spec2}]:\n #{e}") } + rescue Exception => e + logger.error("Error executing [p4 diff2 -u #{spec1} #{spec2}]: #{e.message}") + ensure + p4.disconnect + end + diff + end + + def cat(path, identifier=nil) + path = (path.blank? ? "#{fix_url(url)}" : "#{fix_url(url)}#{relative_path(path)}") + + cat = nil + p4 = P4.new + begin + # untagged execution + p4.tagged = false + p4.port = root_url + #p4.user = @login + if (!@password.nil? && !@password.empty?) + p4.password = @password + end + p4.connect + + identifier = (identifier and identifier.to_i > 0) ? ("@#{identifier}") : "" + spec = path + identifier + cat = p4.run_print("-q", spec) + cat = cat.to_s + rescue P4Exception + p4.errors.each { |e| logger.error("Error executing [p4 print -q #{spec}]:\n #{e}") } + rescue Exception => e + logger.error("Error executing [p4 print -q #{spec}]: #{e.message}") + ensure + p4.disconnect + end + cat + end + + # Returns just the name of the entry from the depot spec + def entry_name(path) + pathname = relative_path(path) + @entry_name = ( pathname.include?('/') ? pathname[1+pathname.rindex('/')..-1] : pathname ) + end + + # Returns a path relative to the depotspec(url) of the repository + def relative_path(path) + path.gsub(Regexp.new("^\/?#{Regexp.escape(fix_url(url))}"), '') + end + + # Make sure there is an ending '/' + def fix_url(path) + @fix_url = (path.end_with?("/") ? path : path + "/") + end + end + + class P4Change + # Constructor. Pass the hash returned by P4#run_describe( "-s" ) in + # tagged mode. + def initialize( hash ) + @change = hash[ "change" ] + @user = hash[ "user" ] + @client = hash[ "client" ] + @desc = hash[ "desc" ] + @time = Time.at( hash[ "time" ].to_i ) + + @status = hash[ "status" ] + @files = Array.new + @jobs = Hash.new + + if ( hash.has_key?( "job" ) ) + hash[ "job" ].each_index do + |i| + job = hash[ "job" ][ i ] + status = hash[ "jobstat" ][ i ] + @jobs[ job ] = status + end + end + end + + attr_reader :change, :user, :client, :desc, :time, :status, :files, :jobs + attr_writer :files + + # Shorthand iterator for looking at the files in the change + def each_file( &block ) + @files.each { |f| yield( f ) } + end + + # Shorthand iterator for looking at the jobs fixed by the change + def each_job( &block ) + @jobs.each { |j| yield( j ) } + end + + def load_files( hash ) + if ( hash.has_key?( "depotFile" ) ) + hash[ "depotFile" ].each_index do + |i| + name = hash[ "depotFile" ][ i ] + type = hash[ "type" ][ i ] + rev = hash[ "rev" ][ i ] + act = hash[ "action" ][ i ] + # create change file + p4chg = P4ChangeFile.new(name) + p4chg.type = type + p4chg.revno = rev.to_i + p4chg.action = act + + @files.push( p4chg ) + end + end + end + end + + class P4ChangeFile + def initialize( depotfile ) + @depot_file = depotfile + @revno + @type + @action + @head = 1 + end + + attr_reader :depot_file + attr_accessor :revno, :type, :head, :action + end + end + end +end