Redmineがリポジトリからファイルの情報を取得する処理を効率良さそうにする 

# 改造前

app/controllers/repositories_controller.rb の `RepositoriesController` クラスの `changes`, `raw`, `entry`, `annotate` メソッドの処理を辿って行くと lib/redmine/scm/adapters/abstract_adapter.rb の `Redmine::Scm::Adapters::AbstractAdapter` クラスの `entry` メソッドに辿り着きます。

http://www.redmine.org/projects/redmine/repository/entry/tags/3.1.2/lib/redmine/scm/adapters/abstract_adapter.rb#L115

```ruby
        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)
            es ? es.detect {|e| e.name == search_name} : nil
          end
        end
```

git の場合はオーバーライドしています。

http://www.redmine.org/projects/redmine/repository/entry/tags/3.1.2/lib/redmine/scm/adapters/git_adapter.rb#L118

```ruby
        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
```

いずれも `entries` メソッドでディレクトリ内のファイル（とディレクトリ）を取得して、その中から目的のファイルを探すようにしています。
ディレクトリ内にファイルが多いとその分の時間がかかると思います。

# 改造後

## git の場合

`entries` メソッドで `git ls-tree` を使っているので

```bash
git --git-dir /path/to/the/git-repos.git HEAD:foo/bar baz.txt
```

というように目的のファイル（ここでは baz.txt です）を明示するようにします。

https://gist.github.com/ayweak/1a75e703e35cde7c5565#file-git_adapter-rb-diff

```diff
--- a/redmine-3.1.2/lib/redmine/scm/adapters/git_adapter.rb	2015-11-14 18:03:27.000000000 +0900
+++ b/redmine-3.1.2/lib/redmine/scm/adapters/git_adapter.rb	2015-11-27 01:04:15.191352707 +0900
@@ -125,7 +125,7 @@
           else
             # Search for the entry in the parent directory
             es = entries(search_path, identifier,
-                         options = {:report_last_commit => false})
+                         options = {:report_last_commit => false, :specific_children => [search_name]})
             es ? es.detect {|e| e.name == search_name} : nil
           end
         end
@@ -137,6 +137,7 @@
           cmd_args = %w|ls-tree -l|
           cmd_args << "HEAD:#{p}"          if identifier.nil?
           cmd_args << "#{identifier}:#{p}" if identifier
+          cmd_args.concat(options[:specific_children]) if options[:specific_children]
           git_cmd(cmd_args) do |io|
             io.each_line do |line|
               e = line.chomp.to_s
```

## subversion の場合

まず `svn info` を使って指定のパスがファイルかディレクトリかを判断します。
ファイルの場合は `entries` メソッド（中で `svn list` を使っています）で取得します。

`svn info` と `svn list` で二回リポジトリにアクセスするので、場合によっては遅くなるかもしれません。

https://gist.github.com/ayweak/1a75e703e35cde7c5565#file-subversion_adapter-rb-diff

```diff
--- a/redmine-3.1.2/lib/redmine/scm/adapters/subversion_adapter.rb	2015-11-14 18:03:27.000000000 +0900
+++ b/redmine-3.1.2/lib/redmine/scm/adapters/subversion_adapter.rb	2015-11-27 23:39:27.041355016 +0900
@@ -83,6 +83,58 @@
           return nil
         end
 
+        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?
+            Entry.new(:path => '', :kind => 'dir')
+          else
+            p = [search_path, search_name].join('/')
+            entry = entry_from_info(p, identifier)
+            if entry && entry.kind == 'file'
+              es = entries(p, identifier, options={:is_file_path => true})
+              entry = es ? es.detect {|e| e.name == search_name} : nil
+            end
+            entry
+          end
+        end
+
+        def entry_from_info(path=nil, identifier=nil)
+          path ||= ''
+          identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
+          cmd = "#{self.class.sq_bin} info --xml #{target(path)}@#{identifier}"
+          cmd << credentials_string
+          entry = nil
+          shellout(cmd) do |io|
+            output = io.read.force_encoding('UTF-8')
+            begin
+              doc = parse_xml(output)
+              info_entry = doc['info']['entry']
+              commit = entry['commit']
+              commit_date = commit['date']
+              if info_entry['kind'] != 'dir' || !commit_date.nil?
+                entry = Entry.new({:name => URI.unescape(info_entry['path']),
+                            :path => path,
+                            :kind => info_entry['kind'],
+                            :size => nil,
+                            :lastrev => Revision.new({
+                              :identifier => commit['revision'],
+                              :time => Time.parse(commit_date['__content__'].to_s).localtime,
+                              :author => ((a = commit['author']) ? a['__content__'] : nil)
+                              })
+                            })
+              end
+            rescue Exception => e
+              logger.error("Error parsing svn output: #{e.message}")
+              logger.error("Output was:\n #{output}")
+            end
+          end
+          return nil if $? && $?.exitstatus != 0
+          entry
+        end
+        private :entry_from_info
+
         # Returns an Entries collection
         # or nil if the given path doesn't exist in the repository
         def entries(path=nil, identifier=nil, options={})
@@ -94,6 +146,15 @@
           shellout(cmd) do |io|
             output = io.read.force_encoding('UTF-8')
             begin
+              path_builder = Proc.new do |path, is_file_path|
+                if is_file_path
+                  Proc.new {|path, name| path}
+                elsif path.empty?
+                  Proc.new {|path, name| name}
+                else
+                  Proc.new {|path, name| "#{path}/#{name}"}
+                end
+              end.call(path, options[:is_file_path])
               doc = parse_xml(output)
               each_xml_element(doc['lists']['list'], 'entry') do |entry|
                 commit = entry['commit']
@@ -103,7 +164,7 @@
                 next if entry['kind'] == 'dir' && commit_date.nil?
                 name = entry['name']['__content__']
                 entries << Entry.new({:name => URI.unescape(name),
-                            :path => ((path.empty? ? "" : "#{path}/") + name),
+                            :path => path_builder.call(path, name),
                             :kind => entry['kind'],
                             :size => ((s = entry['size']) ? s['__content__'].to_i : nil),
                             :lastrev => Revision.new({
```