File: lib/redmine/scm/adapters/cvs_adapter.rb

Overview
Module Structure
Class Hierarchy
Code

Overview

Module Structure

  module: <Toplevel Module>
  module: Redmine#20
  module: Scm#21
  module: Adapters#22
  class: CvsAdapter#23
inherits from
  AbstractAdapter ( Redmine::Scm::Adapters )
has properties
constant: CVS_BIN #26
class method: client_command #29
class method: sq_bin #33
class method: client_version #37
class method: client_available #41
class method: scm_command_version #45
class method: scm_version_from_command_line #55
method: initialize / 5 #65
method: path_encoding #78
method: info #82
method: get_previous_revision / 1 #87
method: entries / 3 #94
constant: STARTLOG #149
constant: ENDLOG #150
method: revisions / 5 #155
method: diff / 3 #274
method: cat / 2 #292
method: annotate / 2 #308
method: root_url_path #337
method: time_to_cvstime / 1 #342
method: time_to_cvstime_rlog / 1 #352
method: normalize_cvs_path / 1 #358
method: normalize_path / 1 #362
method: path_with_proj / 1 #366
method: scm_cmd / 2 #378
  class: Revision#371
inherits from
  Revision ( Redmine::Scm::Adapters )
has properties
method: format_identifier #373
  class: CvsRevisionHelper#397
inherits from
  Object ( Builtin-Module )
has properties
attribute: complete_rev [RW] #398
attribute: revision [RW] #398
attribute: base [RW] #398
attribute: branchid [RW] #398
method: initialize / 1 #400
method: branchPoint #405
method: branchVersion #409
method: isBranchRevision #416
method: prevRev #420
method: is_in_branch_with_symbol / 1 #427
method: buildRevision / 1 #434
method: parseRevision #449

Code

   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 
  20  module Redmine
  21    module Scm
  22      module Adapters
  23        class CvsAdapter < AbstractAdapter
  24 
  25          # CVS executable name
  26          CVS_BIN = Redmine::Configuration['scm_cvs_command'] || "cvs"
  27 
  28          class << self
  29            def client_command
  30              @@bin    ||= CVS_BIN
  31            end
  32 
  33            def sq_bin
  34              @@sq_bin ||= shell_quote_command
  35            end
  36 
  37            def client_version
  38              @@client_version ||= (scm_command_version || [])
  39            end
  40 
  41            def client_available
  42              client_version_above?([1, 12])
  43            end
  44 
  45            def scm_command_version
  46              scm_version = scm_version_from_command_line.dup
  47              if scm_version.respond_to?(:force_encoding)
  48                scm_version.force_encoding('ASCII-8BIT')
  49              end
  50              if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)}m)
  51                m[2].scan(%r{\d+}).collect(&:to_i)
  52              end
  53            end
  54 
  55            def scm_version_from_command_line
  56              shellout("#{sq_bin} --version") { |io| io.read }.to_s
  57            end
  58          end
  59 
  60          # Guidelines for the input:
  61          #  url      -> the project-path, relative to the cvsroot (eg. module name)
  62          #  root_url -> the good old, sometimes damned, CVSROOT
  63          #  login    -> unnecessary
  64          #  password -> unnecessary too
  65          def initialize(url, root_url=nil, login=nil, password=nil,
  66                         path_encoding=nil)
  67            @path_encoding = path_encoding.blank? ? 'UTF-8' : path_encoding
  68            @url      = url
  69            # TODO: better Exception here (IllegalArgumentException)
  70            raise CommandFailed if root_url.blank?
  71            @root_url  = root_url
  72 
  73            # These are unused.
  74            @login    = login if login && !login.empty?
  75            @password = (password || "") if @login
  76          end
  77 
  78          def path_encoding
  79            @path_encoding
  80          end
  81 
  82          def info
  83            logger.debug "<cvs> info"
  84            Info.new({:root_url => @root_url, :lastrev => nil})
  85          end
  86 
  87          def get_previous_revision(revision)
  88            CvsRevisionHelper.new(revision).prevRev
  89          end
  90 
  91          # Returns an Entries collection
  92          # or nil if the given path doesn't exist in the repository
  93          # this method is used by the repository-browser (aka LIST)
  94          def entries(path=nil, identifier=nil, options={})
  95            logger.debug "<cvs> entries '#{path}' with identifier '#{identifier}'"
  96            path_locale = scm_iconv(@path_encoding, 'UTF-8', path)
  97            path_locale.force_encoding("ASCII-8BIT") if path_locale.respond_to?(:force_encoding)
  98            entries = Entries.new
  99            cmd_args = %w|-q rls -e|
 100            cmd_args << "-D" << time_to_cvstime_rlog(identifier) if identifier
 101            cmd_args << path_with_proj(path)
 102            scm_cmd(*cmd_args) do |io|
 103              io.each_line() do |line|
 104                fields = line.chop.split('/',-1)
 105                logger.debug(">>InspectLine #{fields.inspect}")
 106                if fields[0]!="D"
 107                  time = nil
 108                  # Thu Dec 13 16:27:22 2007
 109                  time_l = fields[-3].split(' ')
 110                  if time_l.size == 5 && time_l[4].length == 4
 111                    begin
 112                      time = Time.parse(
 113                               "#{time_l[1]} #{time_l[2]} #{time_l[3]} GMT #{time_l[4]}")
 114                    rescue
 115                    end
 116                  end
 117                  entries << Entry.new(
 118                   {
 119                    :name => scm_iconv('UTF-8', @path_encoding, fields[-5]),
 120                    #:path => fields[-4].include?(path)?fields[-4]:(path + "/"+ fields[-4]),
 121                    :path => scm_iconv('UTF-8', @path_encoding, "#{path_locale}/#{fields[-5]}"),
 122                    :kind => 'file',
 123                    :size => nil,
 124                    :lastrev => Revision.new(
 125                        {
 126                          :revision => fields[-4],
 127                          :name     => scm_iconv('UTF-8', @path_encoding, fields[-4]),
 128                          :time     => time,
 129                          :author   => ''
 130                        })
 131                    })
 132                else
 133                  entries << Entry.new(
 134                   {
 135                    :name    => scm_iconv('UTF-8', @path_encoding, fields[1]),
 136                    :path    => scm_iconv('UTF-8', @path_encoding, "#{path_locale}/#{fields[1]}"),
 137                    :kind    => 'dir',
 138                    :size    => nil,
 139                    :lastrev => nil
 140                   })
 141                end
 142              end
 143            end
 144            entries.sort_by_name
 145          rescue ScmCommandAborted
 146            nil
 147          end
 148 
 149          STARTLOG="----------------------------"
 150          ENDLOG  ="============================================================================="
 151 
 152          # Returns all revisions found between identifier_from and identifier_to
 153          # in the repository. both identifier have to be dates or nil.
 154          # these method returns nothing but yield every result in block
 155          def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}, &block)
 156            path_with_project_utf8   = path_with_proj(path)
 157            path_with_project_locale = scm_iconv(@path_encoding, 'UTF-8', path_with_project_utf8)
 158            logger.debug "<cvs> revisions path:" +
 159                "'#{path}',identifier_from #{identifier_from}, identifier_to #{identifier_to}"
 160            cmd_args = %w|-q rlog|
 161            cmd_args << "-d" << ">#{time_to_cvstime_rlog(identifier_from)}" if identifier_from
 162            cmd_args << path_with_project_utf8
 163            scm_cmd(*cmd_args) do |io|
 164              state      = "entry_start"
 165              commit_log = String.new
 166              revision   = nil
 167              date       = nil
 168              author     = nil
 169              entry_path = nil
 170              entry_name = nil
 171              file_state = nil
 172              branch_map = nil
 173              io.each_line() do |line|
 174                if state != "revision" && /^#{ENDLOG}/ =~ line
 175                  commit_log = String.new
 176                  revision   = nil
 177                  state      = "entry_start"
 178                end
 179                if state == "entry_start"
 180                  branch_map = Hash.new
 181                  if /^RCS file: #{Regexp.escape(root_url_path)}\/#{Regexp.escape(path_with_project_locale)}(.+),v$/ =~ line
 182                    entry_path = normalize_cvs_path($1)
 183                    entry_name = normalize_path(File.basename($1))
 184                    logger.debug("Path #{entry_path} <=> Name #{entry_name}")
 185                  elsif /^head: (.+)$/ =~ line
 186                    entry_headRev = $1 #unless entry.nil?
 187                  elsif /^symbolic names:/ =~ line
 188                    state = "symbolic" #unless entry.nil?
 189                  elsif /^#{STARTLOG}/ =~ line
 190                    commit_log = String.new
 191                    state      = "revision"
 192                  end
 193                  next
 194                elsif state == "symbolic"
 195                  if /^(.*):\s(.*)/ =~ (line.strip)
 196                    branch_map[$1] = $2
 197                  else
 198                    state = "tags"
 199                    next
 200                  end
 201                elsif state == "tags"
 202                  if /^#{STARTLOG}/ =~ line
 203                    commit_log = ""
 204                    state = "revision"
 205                  elsif /^#{ENDLOG}/ =~ line
 206                    state = "head"
 207                  end
 208                  next
 209                elsif state == "revision"
 210                  if /^#{ENDLOG}/ =~ line || /^#{STARTLOG}/ =~ line
 211                    if revision
 212                      revHelper = CvsRevisionHelper.new(revision)
 213                      revBranch = "HEAD"
 214                      branch_map.each() do |branch_name, branch_point|
 215                        if revHelper.is_in_branch_with_symbol(branch_point)
 216                          revBranch = branch_name
 217                        end
 218                      end
 219                      logger.debug("********** YIELD Revision #{revision}::#{revBranch}")
 220                      yield Revision.new({
 221                        :time    => date,
 222                        :author  => author,
 223                        :message => commit_log.chomp,
 224                        :paths => [{
 225                          :revision => revision,
 226                          :branch   => revBranch,
 227                          :path     => scm_iconv('UTF-8', @path_encoding, entry_path),
 228                          :name     => scm_iconv('UTF-8', @path_encoding, entry_name),
 229                          :kind     => 'file',
 230                          :action   => file_state
 231                             }]
 232                           })
 233                    end
 234                    commit_log = String.new
 235                    revision   = nil
 236                    if /^#{ENDLOG}/ =~ line
 237                      state = "entry_start"
 238                    end
 239                    next
 240                  end
 241 
 242                  if /^branches: (.+)$/ =~ line
 243                    # TODO: version.branch = $1
 244                  elsif /^revision (\d+(?:\.\d+)+).*$/ =~ line
 245                    revision = $1
 246                  elsif /^date:\s+(\d+.\d+.\d+\s+\d+:\d+:\d+)/ =~ line
 247                    date       = Time.parse($1)
 248                    line_utf8    = scm_iconv('UTF-8', options[:log_encoding], line)
 249                    author_utf8  = /author: ([^;]+)/.match(line_utf8)[1]
 250                    author       = scm_iconv(options[:log_encoding], 'UTF-8', author_utf8)
 251                    file_state   = /state: ([^;]+)/.match(line)[1]
 252                    # TODO:
 253                    #    linechanges only available in CVS....
 254                    #    maybe a feature our SVN implementation.
 255                    #    I'm sure, they are useful for stats or something else
 256                    #                linechanges =/lines: \+(\d+) -(\d+)/.match(line)
 257                    #                unless linechanges.nil?
 258                    #                  version.line_plus  = linechanges[1]
 259                    #                  version.line_minus = linechanges[2]
 260                    #                else
 261                    #                  version.line_plus  = 0
 262                    #                  version.line_minus = 0
 263                    #                end
 264                  else
 265                    commit_log << line unless line =~ /^\*\*\* empty log message \*\*\*/
 266                  end
 267                end
 268              end
 269            end
 270          rescue ScmCommandAborted
 271            Revisions.new
 272          end
 273 
 274          def diff(path, identifier_from, identifier_to=nil)
 275            logger.debug "<cvs> diff path:'#{path}'" +
 276                ",identifier_from #{identifier_from}, identifier_to #{identifier_to}"
 277            cmd_args = %w|rdiff -u|
 278            cmd_args << "-r#{identifier_to}"
 279            cmd_args << "-r#{identifier_from}"
 280            cmd_args << path_with_proj(path)
 281            diff = []
 282            scm_cmd(*cmd_args) do |io|
 283              io.each_line do |line|
 284                diff << line
 285              end
 286            end
 287            diff
 288          rescue ScmCommandAborted
 289            nil
 290          end
 291 
 292          def cat(path, identifier=nil)
 293            identifier = (identifier) ? identifier : "HEAD"
 294            logger.debug "<cvs> cat path:'#{path}',identifier #{identifier}"
 295            cmd_args = %w|-q co|
 296            cmd_args << "-D" << time_to_cvstime(identifier) if identifier
 297            cmd_args << "-p" << path_with_proj(path)
 298            cat = nil
 299            scm_cmd(*cmd_args) do |io|
 300              io.binmode
 301              cat = io.read
 302            end
 303            cat
 304          rescue ScmCommandAborted
 305            nil
 306          end
 307 
 308          def annotate(path, identifier=nil)
 309            identifier = (identifier) ? identifier : "HEAD"
 310            logger.debug "<cvs> annotate path:'#{path}',identifier #{identifier}"
 311            cmd_args = %w|rannotate|
 312            cmd_args << "-D" << time_to_cvstime(identifier) if identifier
 313            cmd_args << path_with_proj(path)
 314            blame = Annotate.new
 315            scm_cmd(*cmd_args) do |io|
 316              io.each_line do |line|
 317                next unless line =~ %r{^([\d\.]+)\s+\(([^\)]+)\s+[^\)]+\):\s(.*)$}
 318                blame.add_line(
 319                    $3.rstrip,
 320                    Revision.new(
 321                      :revision   => $1,
 322                      :identifier => nil,
 323                      :author     => $2.strip
 324                      ))
 325              end
 326            end
 327            blame
 328          rescue ScmCommandAborted
 329            Annotate.new
 330          end
 331 
 332          private
 333 
 334          # Returns the root url without the connexion string
 335          # :pserver:anonymous@foo.bar:/path => /path
 336          # :ext:cvsservername:/path => /path
 337          def root_url_path
 338            root_url.to_s.gsub(/^:.+:\d*/, '')
 339          end
 340 
 341          # convert a date/time into the CVS-format
 342          def time_to_cvstime(time)
 343            return nil if time.nil?
 344            time = Time.now if time == 'HEAD'
 345 
 346            unless time.kind_of? Time
 347              time = Time.parse(time)
 348            end
 349            return time_to_cvstime_rlog(time)
 350          end
 351 
 352          def time_to_cvstime_rlog(time)
 353            return nil if time.nil?
 354            t1 = time.clone.localtime
 355            return t1.strftime("%Y-%m-%d %H:%M:%S")
 356          end
 357 
 358          def normalize_cvs_path(path)
 359            normalize_path(path.gsub(/Attic\//,''))
 360          end
 361 
 362          def normalize_path(path)
 363            path.sub(/^(\/)*(.*)/,'\2').sub(/(.*)(,v)+/,'\1')
 364          end
 365 
 366          def path_with_proj(path)
 367            "#{url}#{with_leading_slash(path)}"
 368          end
 369          private :path_with_proj
 370 
 371          class Revision < Redmine::Scm::Adapters::Revision
 372            # Returns the readable identifier
 373            def format_identifier
 374              revision.to_s
 375            end
 376          end
 377 
 378          def scm_cmd(*args, &block)
 379            full_args = ['-d', root_url]
 380            full_args += args
 381            full_args_locale = []
 382            full_args.map do |e|
 383              full_args_locale << scm_iconv(@path_encoding, 'UTF-8', e)
 384            end
 385            ret = shellout(
 386                     self.class.sq_bin + ' ' + full_args_locale.map { |e| shell_quote e.to_s }.join(' '),
 387                     &block
 388                     )
 389            if $? && $?.exitstatus != 0
 390              raise ScmCommandAborted, "cvs exited with non-zero status: #{$?.exitstatus}"
 391            end
 392            ret
 393          end
 394          private :scm_cmd
 395        end
 396 
 397        class CvsRevisionHelper
 398          attr_accessor :complete_rev, :revision, :base, :branchid
 399 
 400          def initialize(complete_rev)
 401            @complete_rev = complete_rev
 402            parseRevision()
 403          end
 404 
 405          def branchPoint
 406            return @base
 407          end
 408 
 409          def branchVersion
 410            if isBranchRevision
 411              return @base+"."+@branchid
 412            end
 413            return @base
 414          end
 415 
 416          def isBranchRevision
 417            !@branchid.nil?
 418          end
 419 
 420          def prevRev
 421            unless @revision == 0
 422              return buildRevision( @revision - 1 )
 423            end
 424            return buildRevision( @revision )
 425          end
 426 
 427          def is_in_branch_with_symbol(branch_symbol)
 428            bpieces = branch_symbol.split(".")
 429            branch_start = "#{bpieces[0..-3].join(".")}.#{bpieces[-1]}"
 430            return ( branchVersion == branch_start )
 431          end
 432 
 433          private
 434          def buildRevision(rev)
 435            if rev == 0
 436              if @branchid.nil?
 437                @base + ".0"
 438              else
 439                @base
 440              end
 441            elsif @branchid.nil?
 442              @base + "." + rev.to_s
 443            else
 444              @base + "." + @branchid + "." + rev.to_s
 445            end
 446          end
 447 
 448          # Interpretiert die cvs revisionsnummern wie z.b. 1.14 oder 1.3.0.15
 449          def parseRevision()
 450            pieces = @complete_rev.split(".")
 451            @revision = pieces.last.to_i
 452            baseSize = 1
 453            baseSize += (pieces.size / 2)
 454            @base = pieces[0..-baseSize].join(".")
 455            if baseSize > 2
 456              @branchid = pieces[-2]
 457            end
 458          end
 459        end
 460      end
 461    end
 462  end