OSDN Git Service

scm: git: override "path_encoding" method in adapter (#5251).
[redminele/redmine.git] / lib / redmine / scm / adapters / git_adapter.rb
index 6ad7426..ad04b62 100644 (file)
@@ -1,16 +1,16 @@
-# redMine - project management software
-# Copyright (C) 2006-2007  Jean-Philippe Lang
+# Redmine - project management software
+# Copyright (C) 2006-2011  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.
@@ -22,8 +22,6 @@ module Redmine
     module Adapters
       class GitAdapter < AbstractAdapter
 
-        SCM_GIT_REPORT_LAST_COMMIT = true
-
         # Git executable name
         GIT_BIN = Redmine::Configuration['scm_git_command'] || "git"
 
@@ -48,7 +46,7 @@ module Redmine
           end
 
           def scm_command_version
-            scm_version = scm_version_from_command_line
+            scm_version = scm_version_from_command_line.dup
             if scm_version.respond_to?(:force_encoding)
               scm_version.force_encoding('ASCII-8BIT')
             end
@@ -64,7 +62,11 @@ module Redmine
 
         def initialize(url, root_url=nil, login=nil, password=nil, path_encoding=nil)
           super
-          @flag_report_last_commit = SCM_GIT_REPORT_LAST_COMMIT
+          @path_encoding = path_encoding.blank? ? 'UTF-8' : path_encoding
+        end
+
+        def path_encoding
+          @path_encoding
         end
 
         def info
@@ -78,59 +80,88 @@ module Redmine
         def branches
           return @branches if @branches
           @branches = []
-          cmd = "#{self.class.sq_bin} --git-dir #{target('')} branch --no-color"
-          shellout(cmd) do |io|
+          cmd_args = %w|branch --no-color|
+          scm_cmd(*cmd_args) do |io|
             io.each_line do |line|
               @branches << line.match('\s*\*?\s*(.*)$')[1]
             end
           end
           @branches.sort!
+        rescue ScmCommandAborted
+          nil
         end
 
         def tags
           return @tags if @tags
-          cmd = "#{self.class.sq_bin} --git-dir #{target('')} tag"
-          shellout(cmd) do |io|
+          cmd_args = %w|tag|
+          scm_cmd(*cmd_args) do |io|
             @tags = io.readlines.sort!.map{|t| t.strip}
           end
+        rescue ScmCommandAborted
+          nil
         end
 
         def default_branch
-          branches.include?('master') ? 'master' : branches.first
+          bras = self.branches
+          return nil if bras.nil?
+          bras.include?('master') ? 'master' : bras.first
         end
 
-        def entries(path=nil, identifier=nil)
+        def entry(path=nil, identifier=nil)
+          parts = path.to_s.split(%r{[\/\\]}).select {|n| !n.blank?}
+          search_path = parts[0..-2].join('/')
+          search_name = parts[-1]
+          if search_path.blank? && search_name.blank?
+            # Root entry
+            Entry.new(:path => '', :kind => 'dir')
+          else
+            # Search for the entry in the parent directory
+            es = entries(search_path, identifier,
+                         options = {:report_last_commit => false})
+            es ? es.detect {|e| e.name == search_name} : nil
+          end
+        end
+
+        def entries(path=nil, identifier=nil, options={})
           path ||= ''
+          p = scm_iconv(@path_encoding, 'UTF-8', path)
           entries = Entries.new
-          cmd = "#{self.class.sq_bin} --git-dir #{target('')} ls-tree -l "
-          cmd << shell_quote("HEAD:" + path) if identifier.nil?
-          cmd << shell_quote(identifier + ":" + path) if identifier
-          shellout(cmd)  do |io|
+          cmd_args = %w|ls-tree -l|
+          cmd_args << "HEAD:#{p}"          if identifier.nil?
+          cmd_args << "#{identifier}:#{p}" if identifier
+          scm_cmd(*cmd_args) do |io|
             io.each_line do |line|
               e = line.chomp.to_s
               if e =~ /^\d+\s+(\w+)\s+([0-9a-f]{40})\s+([0-9-]+)\t(.+)$/
                 type = $1
-                sha = $2
+                sha  = $2
                 size = $3
                 name = $4
-                full_path = path.empty? ? name : "#{path}/#{name}"
-                entries << Entry.new({:name => name,
-                 :path => full_path,
+                if name.respond_to?(:force_encoding)
+                  name.force_encoding(@path_encoding)
+                end
+                full_path = p.empty? ? name : "#{p}/#{name}"
+                n      = scm_iconv('UTF-8', @path_encoding, name)
+                full_p = scm_iconv('UTF-8', @path_encoding, full_path)
+                entries << Entry.new({:name => n,
+                 :path => full_p,
                  :kind => (type == "tree") ? 'dir' : 'file',
                  :size => (type == "tree") ? nil : size,
-                 :lastrev => @flag_report_last_commit ? lastrev(full_path,identifier) : Revision.new
+                 :lastrev => options[:report_last_commit] ?
+                                 lastrev(full_path, identifier) : Revision.new
                 }) unless entries.detect{|entry| entry.name == name}
               end
             end
           end
-          return nil if $? && $?.exitstatus != 0
           entries.sort_by_name
+        rescue ScmCommandAborted
+          nil
         end
 
         def lastrev(path, rev)
           return nil if path.nil?
           cmd_args = %w|log --no-color --encoding=UTF-8 --date=iso --pretty=fuller --no-merges -n 1|
-          cmd_args << rev if rev 
+          cmd_args << rev if rev
           cmd_args << "--" << path unless path.empty?
           lines = []
           scm_cmd(*cmd_args) { |io| lines = io.readlines }
@@ -141,11 +172,11 @@ module Redmine
 
               Revision.new({
                 :identifier => id,
-                :scmid => id,
-                :author => author, 
-                :time => time,
-                :message => nil, 
-                :paths => nil 
+                :scmid      => id,
+                :author     => author,
+                :time       => time,
+                :message    => nil,
+                :paths      => nil
                 })
           rescue NoMethodError => e
               logger.error("The revision '#{path}' has a wrong format")
@@ -156,7 +187,7 @@ module Redmine
         end
 
         def revisions(path, identifier_from, identifier_to, options={})
-          revisions = Revisions.new
+          revs = Revisions.new
           cmd_args = %w|log --no-color --encoding=UTF-8 --raw --date=iso --pretty=fuller|
           cmd_args << "--reverse" if options[:reverse]
           cmd_args << "--all" if options[:all]
@@ -166,13 +197,12 @@ module Redmine
           from_to << "#{identifier_to}" if identifier_to
           cmd_args << from_to if !from_to.empty?
           cmd_args << "--since=#{options[:since].strftime("%Y-%m-%d %H:%M:%S")}" if options[:since]
-          cmd_args << "--" << path if path && !path.empty?
+          cmd_args << "--" << scm_iconv(@path_encoding, 'UTF-8', path) if path && !path.empty?
 
           scm_cmd *cmd_args do |io|
             files=[]
             changeset = {}
             parsing_descr = 0  #0: not parsing desc or files, 1: parsing desc, 2: parsing files
-            revno = 1
 
             io.each_line do |line|
               if line =~ /^commit ([0-9a-f]{40})$/
@@ -182,20 +212,19 @@ module Redmine
                   parsing_descr = 0
                   revision = Revision.new({
                     :identifier => changeset[:commit],
-                    :scmid => changeset[:commit],
-                    :author => changeset[:author],
-                    :time => Time.parse(changeset[:date]),
-                    :message => changeset[:description],
-                    :paths => files
+                    :scmid      => changeset[:commit],
+                    :author     => changeset[:author],
+                    :time       => Time.parse(changeset[:date]),
+                    :message    => changeset[:description],
+                    :paths      => files
                   })
                   if block_given?
                     yield revision
                   else
-                    revisions << revision
+                    revs << revision
                   end
                   changeset = {}
                   files = []
-                  revno = revno + 1
                 end
                 changeset[:commit] = $1
               elsif (parsing_descr == 0) && line =~ /^(\w+):\s*(.*)$/
@@ -210,73 +239,75 @@ module Redmine
                 parsing_descr = 1
                 changeset[:description] = ""
               elsif (parsing_descr == 1 || parsing_descr == 2) \
-              && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\t(.+)$/
+                  && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\t(.+)$/
                 parsing_descr = 2
-                fileaction = $1
-                filepath = $2
-                files << {:action => fileaction, :path => filepath}
+                fileaction    = $1
+                filepath      = $2
+                p = scm_iconv('UTF-8', @path_encoding, filepath)
+                files << {:action => fileaction, :path => p}
               elsif (parsing_descr == 1 || parsing_descr == 2) \
-              && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\d+\s+(\S+)\t(.+)$/
+                  && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\d+\s+(\S+)\t(.+)$/
                 parsing_descr = 2
-                fileaction = $1
-                filepath = $3
-                files << {:action => fileaction, :path => filepath}
+                fileaction    = $1
+                filepath      = $3
+                p = scm_iconv('UTF-8', @path_encoding, filepath)
+                files << {:action => fileaction, :path => p}
               elsif (parsing_descr == 1) && line.chomp.to_s == ""
                 parsing_descr = 2
               elsif (parsing_descr == 1)
                 changeset[:description] << line[4..-1]
               end
-            end 
+            end
 
             if changeset[:commit]
               revision = Revision.new({
                 :identifier => changeset[:commit],
-                :scmid => changeset[:commit],
-                :author => changeset[:author],
-                :time => Time.parse(changeset[:date]),
-                :message => changeset[:description],
-                :paths => files
-              })
-
+                :scmid      => changeset[:commit],
+                :author     => changeset[:author],
+                :time       => Time.parse(changeset[:date]),
+                :message    => changeset[:description],
+                :paths      => files
+                 })
               if block_given?
                 yield revision
               else
-                revisions << revision
+                revs << revision
               end
             end
           end
-          revisions
-        rescue ScmCommandAborted
-          revisions
+          revs
+        rescue ScmCommandAborted => e
+          logger.error("git log #{from_to.to_s} error: #{e.message}")
+          revs
         end
 
         def diff(path, identifier_from, identifier_to=nil)
           path ||= ''
-
+          cmd_args = []
           if identifier_to
-            cmd = "#{self.class.sq_bin} --git-dir #{target('')} diff --no-color #{shell_quote identifier_to} #{shell_quote identifier_from}" 
+            cmd_args << "diff" << "--no-color" <<  identifier_to << identifier_from
           else
-            cmd = "#{self.class.sq_bin} --git-dir #{target('')} show --no-color #{shell_quote identifier_from}"
+            cmd_args << "show" << "--no-color" << identifier_from
           end
-
-          cmd << " -- #{shell_quote path}" unless path.empty?
+          cmd_args << "--" <<  scm_iconv(@path_encoding, 'UTF-8', path) unless path.empty?
           diff = []
-          shellout(cmd) do |io|
+          scm_cmd *cmd_args do |io|
             io.each_line do |line|
               diff << line
             end
           end
-          return nil if $? && $?.exitstatus != 0
           diff
+        rescue ScmCommandAborted
+          nil
         end
-        
+
         def annotate(path, identifier=nil)
           identifier = 'HEAD' if identifier.blank?
-          cmd = "#{self.class.sq_bin} --git-dir #{target('')} blame -p #{shell_quote identifier} -- #{shell_quote path}"
+          cmd_args = %w|blame|
+          cmd_args << "-p" << identifier << "--" <<  scm_iconv(@path_encoding, 'UTF-8', path)
           blame = Annotate.new
           content = nil
-          shellout(cmd) { |io| io.binmode; content = io.read }
-          return nil if $? && $?.exitstatus != 0
+          scm_cmd(*cmd_args) { |io| io.binmode; content = io.read }
           # git annotates binary files
           return nil if content.is_binary_data?
           identifier = ''
@@ -288,26 +319,35 @@ module Redmine
             elsif line =~ /^author (.+)/
               authors_by_commit[identifier] = $1.strip
             elsif line =~ /^\t(.*)/
-              blame.add_line($1, Revision.new(:identifier => identifier, :author => authors_by_commit[identifier]))
+              blame.add_line($1, Revision.new(
+                                    :identifier => identifier,
+                                    :revision   => identifier,
+                                    :scmid      => identifier,
+                                    :author     => authors_by_commit[identifier]
+                                    ))
               identifier = ''
               author = ''
             end
           end
           blame
+        rescue ScmCommandAborted
+          nil
         end
 
         def cat(path, identifier=nil)
           if identifier.nil?
             identifier = 'HEAD'
           end
-          cmd = "#{self.class.sq_bin} --git-dir #{target('')} show --no-color #{shell_quote(identifier + ':' + path)}"
+          cmd_args = %w|show --no-color|
+          cmd_args << "#{identifier}:#{scm_iconv(@path_encoding, 'UTF-8', path)}"
           cat = nil
-          shellout(cmd) do |io|
+          scm_cmd(*cmd_args) do |io|
             io.binmode
             cat = io.read
           end
-          return nil if $? && $?.exitstatus != 0
           cat
+        rescue ScmCommandAborted
+          nil
         end
 
         class Revision < Redmine::Scm::Adapters::Revision
@@ -320,6 +360,10 @@ module Redmine
         def scm_cmd(*args, &block)
           repo_path = root_url || url
           full_args = [GIT_BIN, '--git-dir', repo_path]
+          if self.class.client_version_above?([1, 7, 2])
+            full_args << '-c' << 'core.quotepath=false'
+            full_args << '-c' << 'log.decorate=no'
+          end
           full_args += args
           ret = shellout(full_args.map { |e| shell_quote e.to_s }.join(' '), &block)
           if $? && $?.exitstatus != 0