# HG changeset patch
# User Yuya Nishihara <yuya@tcha.org>
# Date 1270952263 -32400
# Branch 0.9-stable
# Node ID 281023f2f915feaa7faf9c6ab52ff3e0f266c73e
# Parent  8094fc1c083c5a0b3f7e4c5c984b8c2c2f72fedd
mercurial: escape any filenames by urlescape

In Mercurial, filename is not UTF-8 but byte-string.
So it should be escaped to UTF-8-safe string before parsing as XML document.

NOTE: Mercurial's {|obfuscate} filter cannot be used here, because it treats
filename as encoded by HGENCODING.

refs #2664

diff --git a/lib/redmine/scm/adapters/mercurial/hg-template-0.9.5.tmpl b/lib/redmine/scm/adapters/mercurial/hg-template-0.9.5.tmpl
--- a/lib/redmine/scm/adapters/mercurial/hg-template-0.9.5.tmpl
+++ b/lib/redmine/scm/adapters/mercurial/hg-template-0.9.5.tmpl
@@ -3,10 +3,10 @@ changeset_quiet =  'This template must b
 changeset_verbose = 'This template must be used with --debug option\n'
 changeset_debug = '<logentry revision="{rev}" node="{node|short}">\n<author>{author|escape}</author>\n<date>{date|isodate}</date>\n<paths>\n{files}{file_adds}{file_dels}{file_copies}</paths>\n<msg>{desc|escape}</msg>\n{tags}</logentry>\n\n'
 
-file = '<path action="M">{file|escape}</path>\n'
-file_add = '<path action="A">{file_add|escape}</path>\n'
-file_del = '<path action="D">{file_del|escape}</path>\n'
-file_copy = '<path-copied copyfrom-path="{source|escape}">{name|urlescape}</path-copied>\n'
+file = '<path action="M">{file|urlescape}</path>\n'
+file_add = '<path action="A">{file_add|urlescape}</path>\n'
+file_del = '<path action="D">{file_del|urlescape}</path>\n'
+file_copy = '<path-copied copyfrom-path="{source|urlescape}">{name|urlescape}</path-copied>\n'
 tag = '<tag>{tag|escape}</tag>\n'
 header='<?xml version="1.0" encoding="UTF-8" ?>\n<log>\n\n'
 # footer="</log>"
\ No newline at end of file
diff --git a/lib/redmine/scm/adapters/mercurial/hg-template-1.0.tmpl b/lib/redmine/scm/adapters/mercurial/hg-template-1.0.tmpl
--- a/lib/redmine/scm/adapters/mercurial/hg-template-1.0.tmpl
+++ b/lib/redmine/scm/adapters/mercurial/hg-template-1.0.tmpl
@@ -3,10 +3,10 @@ changeset_quiet =  'This template must b
 changeset_verbose = 'This template must be used with --debug option\n'
 changeset_debug = '<logentry revision="{rev}" node="{node|short}">\n<author>{author|escape}</author>\n<date>{date|isodate}</date>\n<paths>\n{file_mods}{file_adds}{file_dels}{file_copies}</paths>\n<msg>{desc|escape}</msg>\n{tags}</logentry>\n\n'
 
-file_mod = '<path action="M">{file_mod|escape}</path>\n'
-file_add = '<path action="A">{file_add|escape}</path>\n'
-file_del = '<path action="D">{file_del|escape}</path>\n'
-file_copy = '<path-copied copyfrom-path="{source|escape}">{name|urlescape}</path-copied>\n'
+file_mod = '<path action="M">{file_mod|urlescape}</path>\n'
+file_add = '<path action="A">{file_add|urlescape}</path>\n'
+file_del = '<path action="D">{file_del|urlescape}</path>\n'
+file_copy = '<path-copied copyfrom-path="{source|urlescape}">{name|urlescape}</path-copied>\n'
 tag = '<tag>{tag|escape}</tag>\n'
 header='<?xml version="1.0" encoding="UTF-8" ?>\n<log>\n\n'
 # footer="</log>"
diff --git a/lib/redmine/scm/adapters/mercurial_adapter.rb b/lib/redmine/scm/adapters/mercurial_adapter.rb
--- a/lib/redmine/scm/adapters/mercurial_adapter.rb
+++ b/lib/redmine/scm/adapters/mercurial_adapter.rb
@@ -16,6 +16,7 @@
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 
 require 'redmine/scm/adapters/abstract_adapter'
+require 'cgi'
 
 module Redmine
   module Scm
@@ -127,8 +128,8 @@ module Redmine
                     from_rev = logentry.attributes['revision']
                   end
                   paths << {:action => path.attributes['action'],
-                    :path => "/#{path.text}",
-                    :from_path => from_path ? "/#{from_path}" : nil,
+                    :path => "/#{CGI.unescape(path.text)}",
+                    :from_path => from_path ? "/#{CGI.unescape(from_path)}" : nil,
                     :from_revision => from_rev ? from_rev : nil
                   }
                 end
# HG changeset patch
# User Yuya Nishihara <yuya@tcha.org>
# Date 1270952258 -32400
# Branch 0.9-stable
# Node ID e8724d8a1beacb2c425926e344532bff1f8d1f53
# Parent  281023f2f915feaa7faf9c6ab52ff3e0f266c73e
repository: add filename encoding conversion

This introduces repositories.path_encoding column, which specifies
filename's character encoding.

If path_encoding specified, scm adapter is wrapped by PathEncodableWrapper,
then any filenames are converted between UTF-8 and path_encoding.

Currently path_encoding is only available for Mercurial, but it should be
usable for Git. Since Subversion and Bazaar normalize filenames to UTF-8
by theirselves, path_encoding is unnecessary for them.

fixes #2664

diff --git a/app/helpers/repositories_helper.rb b/app/helpers/repositories_helper.rb
--- a/app/helpers/repositories_helper.rb
+++ b/app/helpers/repositories_helper.rb
@@ -160,7 +160,9 @@ module RepositoriesHelper
   end
   
   def mercurial_field_tags(form, repository)
-      content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)))
+    content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?))) +
+    content_tag('p', form.select(:path_encoding, [nil] + Setting::ENCODINGS,
+                                 :label => 'Path encoding'))
   end
 
   def git_field_tags(form, repository)
diff --git a/app/models/repository.rb b/app/models/repository.rb
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -15,6 +15,8 @@
 # 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/path_encodable_wrapper'
+
 class Repository < ActiveRecord::Base
   belongs_to :project
   has_many :changesets, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC"
@@ -38,7 +40,7 @@ class Repository < ActiveRecord::Base
   end
 
   def scm
-    @scm ||= self.scm_adapter.new url, root_url, login, password
+    @scm ||= new_scm
     update_attribute(:root_url, @scm.root_url) if root_url.blank?
     @scm
   end
@@ -198,6 +200,12 @@ class Repository < ActiveRecord::Base
   end
   
   private
+
+  def new_scm
+    scm = self.scm_adapter.new url, root_url, login, password
+    scm = Redmine::Scm::Adapters::PathEncodableWrapper.new(scm, path_encoding) unless path_encoding.blank?
+    scm
+  end
   
   def before_save
     # Strips url and root_url
diff --git a/db/migrate/20100410053957_add_repositories_path_encoding.rb b/db/migrate/20100410053957_add_repositories_path_encoding.rb
new file mode 100644
--- /dev/null
+++ b/db/migrate/20100410053957_add_repositories_path_encoding.rb
@@ -0,0 +1,9 @@
+class AddRepositoriesPathEncoding < ActiveRecord::Migration
+  def self.up
+    add_column :repositories, :path_encoding, :string, :limit => 64, :default => nil
+  end
+
+  def self.down
+    remove_column :repositories, :path_encoding
+  end
+end
diff --git a/lib/redmine/scm/adapters/path_encodable_wrapper.rb b/lib/redmine/scm/adapters/path_encodable_wrapper.rb
new file mode 100644
--- /dev/null
+++ b/lib/redmine/scm/adapters/path_encodable_wrapper.rb
@@ -0,0 +1,109 @@
+# redMine - project management software
+# Copyright (C) 2006-2010  Jean-Philippe Lang
+# Copyright (C) 2010 Yuya Nishihara <yuya@tcha.org>
+#
+# 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 'delegate'
+require 'iconv'
+
+module Redmine
+  module Scm
+    module Adapters
+      # wraps scm adapter to convert path encodings
+      class PathEncodableWrapper < SimpleDelegator  # :nodoc:
+        def initialize(scm, path_encoding)
+          super(scm)
+          @path_encoding = path_encoding
+        end
+
+        def entry(path=nil, identifier=nil)
+          convert_entry!(super(to_scm_path(path), identifier))
+        end
+
+        def entries(path=nil, identifier=nil)
+          convert_entries!(super(to_scm_path(path), identifier))
+        end
+
+        def properties(path, identifier=nil)
+          super(to_scm_path(path), identifier)
+        end
+
+        def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
+          convert_revisions!(super(to_scm_path(path), identifier_from, identifier_to, options))
+        end
+
+        def diff(path, identifier_from, identifier_to=nil)
+          super(to_scm_path(path), identifier_from, identifier_to)
+        end
+
+        def cat(path, identifier=nil)
+          super(to_scm_path(path), identifier)
+        end
+
+        def annotate(path, identifier=nil)
+          super(to_scm_path(path), identifier)
+        end
+
+        private
+
+        def convert_entry!(entry)
+          return unless entry
+          entry.name = from_scm_path(entry.name)
+          entry.path = from_scm_path(entry.path)
+          entry
+        end
+
+        def convert_entries!(entries)
+          return unless entries
+          entries.each { |e| convert_entry!(e) }
+          entries
+        end
+
+        def convert_revisions!(revisions)
+          return unless revisions
+          revisions.each do |rev|
+            next unless rev.paths
+            rev.paths.each do |e|
+              e[:path] = from_scm_path(e[:path])
+              e[:from_path] = from_scm_path(e[:from_path])
+            end
+          end
+          revisions
+        end
+
+        # convert repository path string to utf-8
+        def from_scm_path(s)
+          return unless s
+          begin
+            Iconv.conv('UTF-8', @path_encoding, s)
+          rescue Iconv::Failure => err
+            raise CommandFailed, "failed to convert path from #{@path_encoding} to UTF-8. #{err}"
+          end
+        end
+
+        # convert utf-8 path string to repository encoding
+        def to_scm_path(s)
+          return unless s
+          begin
+            Iconv.conv(@path_encoding, 'UTF-8', s)
+          rescue Iconv::Failure => err
+            raise CommandFailed, "failed to convert path from UTF-8 to #{@path_encoding}. #{err}"
+          end
+        end
+      end
+    end
+  end
+end
