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

Overview
Module Structure
Class Hierarchy
Code

Overview

Module Structure

  module: <Toplevel Module>
  module: Redmine#21
  module: Scm#22
  module: Adapters#23
  class: DarcsAdapter#24
inherits from
  AbstractAdapter ( Redmine::Scm::Adapters )
has properties
constant: DARCS_BIN #26
class method: client_command #29
class method: sq_bin #33
class method: client_version #37
class method: client_available #41
class method: darcs_binary_version #45
class method: darcs_binary_version_from_command_line #55
method: initialize / 5 #60
method: supports_cat? #66
method: info #72
method: entries / 3 #79
method: revisions / 4 #107
method: diff / 3 #134
method: cat / 2 #154
method: entry_from_xml / 2 #171
method: get_paths_for_patch / 1 #188
method: get_paths_for_patch_raw / 1 #218

Class Hierarchy

Code

   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  require 'redmine/scm/adapters/abstract_adapter'
  19  require 'rexml/document'
  20 
  21  module Redmine
  22    module Scm
  23      module Adapters
  24        class DarcsAdapter < AbstractAdapter
  25          # Darcs executable name
  26          DARCS_BIN = Redmine::Configuration['scm_darcs_command'] || "darcs"
  27 
  28          class << self
  29            def client_command
  30              @@bin    ||= DARCS_BIN
  31            end
  32 
  33            def sq_bin
  34              @@sq_bin ||= shell_quote_command
  35            end
  36 
  37            def client_version
  38              @@client_version ||= (darcs_binary_version || [])
  39            end
  40 
  41            def client_available
  42              !client_version.empty?
  43            end
  44 
  45            def darcs_binary_version
  46              darcsversion = darcs_binary_version_from_command_line.dup
  47              if darcsversion.respond_to?(:force_encoding)
  48                darcsversion.force_encoding('ASCII-8BIT')
  49              end
  50              if m = darcsversion.match(%r{\A(.*?)((\d+\.)+\d+)})
  51                m[2].scan(%r{\d+}).collect(&:to_i)
  52              end
  53            end
  54 
  55            def darcs_binary_version_from_command_line
  56              shellout("#{sq_bin} --version") { |io| io.read }.to_s
  57            end
  58          end
  59 
  60          def initialize(url, root_url=nil, login=nil, password=nil,
  61                         path_encoding=nil)
  62            @url = url
  63            @root_url = url
  64          end
  65 
  66          def supports_cat?
  67            # cat supported in darcs 2.0.0 and higher
  68            self.class.client_version_above?([2, 0, 0])
  69          end
  70 
  71          # Get info about the darcs repository
  72          def info
  73            rev = revisions(nil,nil,nil,{:limit => 1})
  74            rev ? Info.new({:root_url => @url, :lastrev => rev.last}) : nil
  75          end
  76 
  77          # Returns an Entries collection
  78          # or nil if the given path doesn't exist in the repository
  79          def entries(path=nil, identifier=nil, options={})
  80            path_prefix = (path.blank? ? '' : "#{path}/")
  81            if path.blank?
  82              path = ( self.class.client_version_above?([2, 2, 0]) ? @url : '.' )
  83            end
  84            entries = Entries.new
  85            cmd = "#{self.class.sq_bin} annotate --repodir #{shell_quote @url} --xml-output"
  86            cmd << " --match #{shell_quote("hash #{identifier}")}" if identifier
  87            cmd << " #{shell_quote path}"
  88            shellout(cmd) do |io|
  89              begin
  90                doc = REXML::Document.new(io)
  91                if doc.root.name == 'directory'
  92                  doc.elements.each('directory/*') do |element|
  93                    next unless ['file', 'directory'].include? element.name
  94                    entries << entry_from_xml(element, path_prefix)
  95                  end
  96                elsif doc.root.name == 'file'
  97                  entries << entry_from_xml(doc.root, path_prefix)
  98                end
  99              rescue
 100              end
 101            end
 102            return nil if $? && $?.exitstatus != 0
 103            entries.compact!
 104            entries.sort_by_name
 105          end
 106 
 107          def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
 108            path = '.' if path.blank?
 109            revisions = Revisions.new
 110            cmd = "#{self.class.sq_bin} changes --repodir #{shell_quote @url} --xml-output"
 111            cmd << " --from-match #{shell_quote("hash #{identifier_from}")}" if identifier_from
 112            cmd << " --last #{options[:limit].to_i}" if options[:limit]
 113            shellout(cmd) do |io|
 114              begin
 115                doc = REXML::Document.new(io)
 116                doc.elements.each("changelog/patch") do |patch|
 117                  message = patch.elements['name'].text
 118                  message << "\n" + patch.elements['comment'].text.gsub(/\*\*\*END OF DESCRIPTION\*\*\*.*\z/m, '') if patch.elements['comment']
 119                  revisions << Revision.new({:identifier => nil,
 120                                :author => patch.attributes['author'],
 121                                :scmid => patch.attributes['hash'],
 122                                :time => Time.parse(patch.attributes['local_date']),
 123                                :message => message,
 124                                :paths => (options[:with_path] ? get_paths_for_patch(patch.attributes['hash']) : nil)
 125                              })
 126                end
 127              rescue
 128              end
 129            end
 130            return nil if $? && $?.exitstatus != 0
 131            revisions
 132          end
 133 
 134          def diff(path, identifier_from, identifier_to=nil)
 135            path = '*' if path.blank?
 136            cmd = "#{self.class.sq_bin} diff --repodir #{shell_quote @url}"
 137            if identifier_to.nil?
 138              cmd << " --match #{shell_quote("hash #{identifier_from}")}"
 139            else
 140              cmd << " --to-match #{shell_quote("hash #{identifier_from}")}"
 141              cmd << " --from-match #{shell_quote("hash #{identifier_to}")}"
 142            end
 143            cmd << " -u #{shell_quote path}"
 144            diff = []
 145            shellout(cmd) do |io|
 146              io.each_line do |line|
 147                diff << line
 148              end
 149            end
 150            return nil if $? && $?.exitstatus != 0
 151            diff
 152          end
 153 
 154          def cat(path, identifier=nil)
 155            cmd = "#{self.class.sq_bin} show content --repodir #{shell_quote @url}"
 156            cmd << " --match #{shell_quote("hash #{identifier}")}" if identifier
 157            cmd << " #{shell_quote path}"
 158            cat = nil
 159            shellout(cmd) do |io|
 160              io.binmode
 161              cat = io.read
 162            end
 163            return nil if $? && $?.exitstatus != 0
 164            cat
 165          end
 166 
 167          private
 168 
 169          # Returns an Entry from the given XML element
 170          # or nil if the entry was deleted
 171          def entry_from_xml(element, path_prefix)
 172            modified_element = element.elements['modified']
 173            if modified_element.elements['modified_how'].text.match(/removed/)
 174              return nil
 175            end
 176 
 177            Entry.new({:name => element.attributes['name'],
 178                       :path => path_prefix + element.attributes['name'],
 179                       :kind => element.name == 'file' ? 'file' : 'dir',
 180                       :size => nil,
 181                       :lastrev => Revision.new({
 182                         :identifier => nil,
 183                         :scmid => modified_element.elements['patch'].attributes['hash']
 184                         })
 185                       })
 186          end
 187 
 188          def get_paths_for_patch(hash)
 189            paths = get_paths_for_patch_raw(hash)
 190            if self.class.client_version_above?([2, 4])
 191              orig_paths = paths
 192              paths = []
 193              add_paths = []
 194              add_paths_name = []
 195              mod_paths = []
 196              other_paths = []
 197              orig_paths.each do |path|
 198                if path[:action] == 'A'
 199                  add_paths << path
 200                  add_paths_name << path[:path]
 201                elsif path[:action] == 'M'
 202                  mod_paths << path
 203                else
 204                  other_paths << path
 205                end
 206              end
 207              add_paths_name.each do |add_path|
 208                mod_paths.delete_if { |m| m[:path] == add_path }
 209              end
 210              paths.concat add_paths
 211              paths.concat mod_paths
 212              paths.concat other_paths
 213            end
 214            paths
 215          end
 216 
 217          # Retrieve changed paths for a single patch
 218          def get_paths_for_patch_raw(hash)
 219            cmd = "#{self.class.sq_bin} annotate --repodir #{shell_quote @url} --summary --xml-output"
 220            cmd << " --match #{shell_quote("hash #{hash}")} "
 221            paths = []
 222            shellout(cmd) do |io|
 223              begin
 224                # Darcs xml output has multiple root elements in this case (tested with darcs 1.0.7)
 225                # A root element is added so that REXML doesn't raise an error
 226                doc = REXML::Document.new("<fake_root>" + io.read + "</fake_root>")
 227                doc.elements.each('fake_root/summary/*') do |modif|
 228                  paths << {:action => modif.name[0,1].upcase,
 229                            :path => "/" + modif.text.chomp.gsub(/^\s*/, '')
 230                           }
 231                end
 232              rescue
 233              end
 234            end
 235            paths
 236          rescue CommandFailed
 237            paths
 238          end
 239        end
 240      end
 241    end
 242  end