Commit 21ae869d authored by Enrique García Cota's avatar Enrique García Cota

increased scm file download in scm views

parent c4f80d5e
...@@ -118,41 +118,48 @@ class RepositoriesController < ApplicationController ...@@ -118,41 +118,48 @@ class RepositoriesController < ApplicationController
# If the entry is a dir, show the browser # If the entry is a dir, show the browser
(show; return) if @entry.is_dir? (show; return) if @entry.is_dir?
@content = @repository.cat(@path, @rev) @repository.cat_to_tempfile(@path, @rev) do |f|
(show_error_not_found; return) unless @content if params[:format] == 'raw' || is_too_large_to_show?(f) || is_binary?(f, @path)
if 'raw' == params[:format] || send_type = Redmine::MimeType.of(@path)
(@content.size && @content.size > Setting.file_max_size_displayed.to_i.kilobyte) || options = {
! is_entry_text_data?(@content, @path) :filename => filename_for_content_disposition(@path.split('/').last),
# Force the download :disposition => 'attachment',
send_opt = { :filename => filename_for_content_disposition(@path.split('/').last) } :x_sendfile => false # x_sendfile does not work well with tempfiles
send_type = Redmine::MimeType.of(@path) }
send_opt[:type] = send_type.to_s if send_type options[:type] = send_type.to_s if send_type
send_data @content, send_opt send_file(f.path, options)
else else
# Prevent empty lines when displaying a file with Windows style eol @content = f.read
# TODO: UTF-16 @changeset = @repository.find_changeset_by_name(@rev)
# Is this needs? AttachmentsController reads file simply. end
@content.gsub!("\r\n", "\n")
@changeset = @repository.find_changeset_by_name(@rev)
end end
rescue Errno::ENOENT
show_error_not_found
end end
def is_entry_text_data?(ent, path) def is_too_large_to_show?(f)
# UTF-16 contains "\x00". f.size > Setting.file_max_size_displayed.to_i.kilobyte
# It is very strict that file contains less than 30% of ascii symbols end
# in non Western Europe. private :is_too_large_to_show?
return true if Redmine::MimeType.is_type?('text', path)
# Ruby 1.8.6 has a bug of integer divisions. def is_binary?(file, path)
# http://apidock.com/ruby/v1_8_6_287/String/is_binary_data%3F
if ent.respond_to?("is_binary_data?") && ent.is_binary_data? # Ruby 1.8.x and <1.9.2 return false if Redmine::MimeType.is_type?('text', path)
return false
elsif ent.respond_to?(:force_encoding) && (ent.dup.force_encoding("UTF-8") != ent.dup.force_encoding("BINARY") ) # Ruby 1.9.2 # First block of the file is examined for odd
# TODO: need to handle edge cases of non-binary content that isn't UTF-8 # characters such as strange control codes or char-
return false # acters with the high bit set. If too many strange
end # characters (>30%) are found, it's a binary file,
true # otherwise it's a text file. Also, any file con-
# taining null in the first block is considered a
# binary file.
blk = file.read(Setting.file_max_size_displayed.to_i.kilobyte)
return blk.size == 0 ||
blk.count("^ -~", "^\r\n") / blk.size > 0.3 ||
blk.count("\x00") > 0
end end
private :is_entry_text_data? private :is_binary?
def annotate def annotate
@entry = @repository.entry(@path, @rev) @entry = @repository.entry(@path, @rev)
......
...@@ -104,6 +104,10 @@ class Repository < ActiveRecord::Base ...@@ -104,6 +104,10 @@ class Repository < ActiveRecord::Base
scm.cat(path, identifier) scm.cat(path, identifier)
end end
def cat_to_tempfile(path, identifier, &block)
scm.cat_to_tempfile(path, identifier, &block)
end
def diff(path, rev, rev_to) def diff(path, rev, rev_to)
scm.diff(path, rev, rev_to) scm.diff(path, rev, rev_to)
end end
......
...@@ -69,7 +69,7 @@ module Redmine ...@@ -69,7 +69,7 @@ module Redmine
end end
def adapter_name def adapter_name
'Abstract' self.class.name.gsub("Adapter","")
end end
def supports_cat? def supports_cat?
...@@ -143,6 +143,19 @@ module Redmine ...@@ -143,6 +143,19 @@ module Redmine
return nil return nil
end end
def cat_to_tempfile(path, identifier, &block)
prefix = path.split("/").last
tmp_path = Rails.root.join('tmp')
Tempfile.open(prefix, tmp_path) do |f|
save_entry_in_file(f,path,identifier)
block.call(f)
end
end
def save_entry_in_file(file, path, identifier)
return nil
end
def with_leading_slash(path) def with_leading_slash(path)
path ||= '' path ||= ''
(path[0,1]!="/") ? "/#{path}" : path (path[0,1]!="/") ? "/#{path}" : path
...@@ -183,34 +196,56 @@ module Redmine ...@@ -183,34 +196,56 @@ module Redmine
self.class.logger self.class.logger
end end
def shellout(cmd, &block) def shellout(cmd, output_path=nil, &block)
self.class.shellout(cmd, &block) self.class.shellout(cmd, output_path, &block)
end
def build_scm_cmd(cmd_args)
([ self.class.sq_bin ] + cmd_args).join(' ')
end
def scm_cmd(cmd_args, output_path=nil, &block)
cmd = build_scm_cmd(cmd_args)
begin
ret = shellout(cmd, output_path, &block)
rescue Exception => e
msg = strip_credential(e.message)
cmd = strip_credential(cmd)
logger.error("Error executing #{adapter_name} command [#{cmd}]: #{msg}")
end
return nil if $? && $?.exitstatus != 0
ret
end end
def self.logger def self.logger
RAILS_DEFAULT_LOGGER RAILS_DEFAULT_LOGGER
end end
def self.shellout(cmd, &block) def self.process_cmd(cmd, output_path)
logger.debug "Shelling out: #{strip_credential(cmd)}" if logger && logger.debug? cmd = Rails.env == 'development' ? "#{cmd} 2>>#{RAILS_ROOT}/log/scm.stderr.log" : cmd
if Rails.env == 'development' cmd = "#{cmd} >> #{output_path}" if output_path.present?
# Capture stderr when running in dev environment cmd
cmd = "#{cmd} 2>>#{RAILS_ROOT}/log/scm.stderr.log" end
end
def self.get_reading_mode_for_ruby_version
RUBY_VERSION < '1.9' ? 'r+' : 'r+:ASCII-8BIT'
end
def self.shellout(cmd, output_path=nil, &block)
logger.debug("Shelling out: #{strip_credential(cmd)}") if logger && logger.respond_to?(:debug)
cmd = process_cmd(cmd, output_path)
mode = get_reading_mode_for_ruby_version
begin begin
if RUBY_VERSION < '1.9' result = nil
mode = "r+"
else
mode = "r+:ASCII-8BIT"
end
IO.popen(cmd, mode) do |io| IO.popen(cmd, mode) do |io|
io.close_write io.close_write
block.call(io) if block_given? result = block.call(io) if block_given?
end end
result
rescue Errno::ENOENT => e rescue Errno::ENOENT => e
msg = strip_credential(e.message) msg = strip_credential(e.message)
# The command failed, log it and re-raise cmd = strip_credential(cmd)
logger.error("SCM command failed, make sure that your SCM binary (eg. svn) is in PATH (#{ENV['PATH']}): #{strip_credential(cmd)}\n with: #{msg}") logger.error("SCM command failed, make sure that your SCM binary (eg. svn) is in PATH (#{ENV['PATH']}): #{cmd}\n with: #{msg}")
raise CommandFailed.new(msg) raise CommandFailed.new(msg)
end end
end end
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
# See doc/COPYRIGHT.rdoc for more details. # See doc/COPYRIGHT.rdoc for more details.
#++ #++
require 'redmine/scm/adapters/abstract_adapter' require_dependency 'redmine/scm/adapters/abstract_adapter'
module Redmine module Redmine
module Scm module Scm
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
# See doc/COPYRIGHT.rdoc for more details. # See doc/COPYRIGHT.rdoc for more details.
#++ #++
require 'redmine/scm/adapters/abstract_adapter' require_dependency 'redmine/scm/adapters/abstract_adapter'
module Redmine module Redmine
module Scm module Scm
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
# See doc/COPYRIGHT.rdoc for more details. # See doc/COPYRIGHT.rdoc for more details.
#++ #++
require 'redmine/scm/adapters/abstract_adapter' require_dependency 'redmine/scm/adapters/abstract_adapter'
require 'rexml/document' require 'rexml/document'
module Redmine module Redmine
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
# See doc/COPYRIGHT.rdoc for more details. # See doc/COPYRIGHT.rdoc for more details.
#++ #++
require 'redmine/scm/adapters/abstract_adapter' require_dependency 'redmine/scm/adapters/abstract_adapter'
require 'find' require 'find'
module Redmine module Redmine
...@@ -91,6 +91,14 @@ module Redmine ...@@ -91,6 +91,14 @@ module Redmine
raise CommandFailed.new(err.message) raise CommandFailed.new(err.message)
end end
def save_entry_in_file(f, path, identifier)
p = scm_iconv(@path_encoding, 'UTF-8', target(path))
FileUtils.cp(p, f.path)
rescue => err
logger.error "scm: filesystem: error: #{err.message}"
raise CommandFailed.new(err.message)
end
private private
# AbstractAdapter::target is implicitly made to quote paths. # AbstractAdapter::target is implicitly made to quote paths.
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
# See doc/COPYRIGHT.rdoc for more details. # See doc/COPYRIGHT.rdoc for more details.
#++ #++
require 'redmine/scm/adapters/abstract_adapter' require_dependency 'redmine/scm/adapters/abstract_adapter'
module Redmine module Redmine
module Scm module Scm
...@@ -24,9 +24,6 @@ module Redmine ...@@ -24,9 +24,6 @@ module Redmine
# Git executable name # Git executable name
GIT_BIN = Redmine::Configuration['scm_git_command'] || "git" unless defined?(GIT_BIN) GIT_BIN = Redmine::Configuration['scm_git_command'] || "git" unless defined?(GIT_BIN)
# raised if scm command exited with error, e.g. unknown revision.
class ScmCommandAborted < CommandFailed; end
class << self class << self
def client_command def client_command
@@bin ||= GIT_BIN @@bin ||= GIT_BIN
...@@ -75,26 +72,22 @@ module Redmine ...@@ -75,26 +72,22 @@ module Redmine
def branches def branches
return @branches if @branches return @branches if @branches
@branches = []
cmd_args = %w|branch --no-color| cmd_args = %w|branch --no-color|
scm_cmd(*cmd_args) do |io| scm_cmd(cmd_args) do |io|
@branches = []
io.each_line do |line| io.each_line do |line|
@branches << line.match('\s*\*?\s*(.*)$')[1] @branches << line.match('\s*\*?\s*(.*)$')[1]
end end
@branches.sort!
end end
@branches.sort!
rescue ScmCommandAborted
nil
end end
def tags def tags
return @tags if @tags return @tags if @tags
cmd_args = %w|tag| cmd_args = %w|tag|
scm_cmd(*cmd_args) do |io| scm_cmd(cmd_args) do |io|
@tags = io.readlines.sort!.map{|t| t.strip} @tags = io.readlines.sort!.map{|t| t.strip}
end end
rescue ScmCommandAborted
nil
end end
def default_branch def default_branch
...@@ -106,11 +99,14 @@ module Redmine ...@@ -106,11 +99,14 @@ module Redmine
def entries(path=nil, identifier=nil) def entries(path=nil, identifier=nil)
path ||= '' path ||= ''
p = scm_iconv(@path_encoding, 'UTF-8', path) p = scm_iconv(@path_encoding, 'UTF-8', path)
entries = Entries.new
cmd_args = %w|ls-tree -l| cmd_args = %w|ls-tree -l|
cmd_args << "HEAD:#{p}" if identifier.nil? cmd_args << "HEAD:#{p}" if identifier.nil?
cmd_args << "#{identifier}:#{p}" if identifier cmd_args << "#{identifier}:#{p}" if identifier
scm_cmd(*cmd_args) do |io|
scm_cmd(cmd_args) do |io|
entries = Entries.new
io.each_line do |line| io.each_line do |line|
e = line.chomp.to_s e = line.chomp.to_s
if e =~ /^\d+\s+(\w+)\s+([0-9a-f]{40})\s+([0-9-]+)\t(.+)$/ if e =~ /^\d+\s+(\w+)\s+([0-9a-f]{40})\s+([0-9-]+)\t(.+)$/
...@@ -124,18 +120,20 @@ module Redmine ...@@ -124,18 +120,20 @@ module Redmine
full_path = p.empty? ? name : "#{p}/#{name}" full_path = p.empty? ? name : "#{p}/#{name}"
n = scm_iconv('UTF-8', @path_encoding, name) n = scm_iconv('UTF-8', @path_encoding, name)
full_p = scm_iconv('UTF-8', @path_encoding, full_path) full_p = scm_iconv('UTF-8', @path_encoding, full_path)
entries << Entry.new({:name => n,
:path => full_p, unless entries.detect{|entry| entry.name == name}
:kind => (type == "tree") ? 'dir' : 'file', entries << Entry.new({:name => n,
:size => (type == "tree") ? nil : size, :path => full_p,
:lastrev => @flag_report_last_commit ? lastrev(full_path, identifier) : Revision.new :kind => (type == "tree") ? 'dir' : 'file',
}) unless entries.detect{|entry| entry.name == name} :size => (type == "tree") ? nil : size,
:lastrev => @flag_report_last_commit ? lastrev(full_path, identifier) : Revision.new
})
end
end end
end end
entries.sort_by_name
end end
entries.sort_by_name
rescue ScmCommandAborted
nil
end end
def lastrev(path, rev) def lastrev(path, rev)
...@@ -143,9 +141,9 @@ module Redmine ...@@ -143,9 +141,9 @@ module Redmine
cmd_args = %w|log --no-color --encoding=UTF-8 --date=iso --pretty=fuller --no-merges -n 1| 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? cmd_args << "--" << path unless path.empty?
lines = [] lines = scm_cmd(cmd_args) { |io| io.readlines }
scm_cmd(*cmd_args) { |io| lines = io.readlines } if lines
begin begin
id = lines[0].split[1] id = lines[0].split[1]
author = lines[1].match('Author:\s+(.*)$')[1] author = lines[1].match('Author:\s+(.*)$')[1]
time = Time.parse(lines[4].match('CommitDate:\s+(.*)$')[1]) time = Time.parse(lines[4].match('CommitDate:\s+(.*)$')[1])
...@@ -157,17 +155,17 @@ module Redmine ...@@ -157,17 +155,17 @@ module Redmine
:time => time, :time => time,
:message => nil, :message => nil,
:paths => nil :paths => nil
}) })
rescue NoMethodError => e rescue NoMethodError => e
logger.error("The revision '#{path}' has a wrong format") logger.error("The revision '#{path}' has a wrong format")
return nil return nil
end
end end
rescue ScmCommandAborted
nil
end end
def revisions(path, identifier_from, identifier_to, options={}) def revisions(path, identifier_from, identifier_to, options={})
revisions = Revisions.new revisions = Revisions.new
cmd_args = %w|log --no-color --encoding=UTF-8 --raw --date=iso --pretty=fuller| cmd_args = %w|log --no-color --encoding=UTF-8 --raw --date=iso --pretty=fuller|
cmd_args << "--reverse" if options[:reverse] cmd_args << "--reverse" if options[:reverse]
cmd_args << "--all" if options[:all] cmd_args << "--all" if options[:all]
...@@ -179,7 +177,7 @@ module Redmine ...@@ -179,7 +177,7 @@ module Redmine
cmd_args << "--since='#{options[:since].strftime("%Y-%m-%d %H:%M:%S")}'" if options[:since] cmd_args << "--since='#{options[:since].strftime("%Y-%m-%d %H:%M:%S")}'" if options[:since]
cmd_args << "--" << scm_iconv(@path_encoding, 'UTF-8', path) if path && !path.empty? cmd_args << "--" << scm_iconv(@path_encoding, 'UTF-8', path) if path && !path.empty?
scm_cmd *cmd_args do |io| scm_cmd cmd_args do |io|
files=[] files=[]
changeset = {} changeset = {}
parsing_descr = 0 #0: not parsing desc or files, 1: parsing desc, 2: parsing files parsing_descr = 0 #0: not parsing desc or files, 1: parsing desc, 2: parsing files
...@@ -255,10 +253,9 @@ module Redmine ...@@ -255,10 +253,9 @@ module Redmine
revisions << revision revisions << revision
end end
end end
revisions
end end
revisions
rescue ScmCommandAborted
revisions
end end
def diff(path, identifier_from, identifier_to=nil) def diff(path, identifier_from, identifier_to=nil)
...@@ -271,14 +268,12 @@ module Redmine ...@@ -271,14 +268,12 @@ module Redmine
end end
cmd_args << "--" << scm_iconv(@path_encoding, 'UTF-8', path) unless path.empty? cmd_args << "--" << scm_iconv(@path_encoding, 'UTF-8', path) unless path.empty?
diff = [] diff = []
scm_cmd *cmd_args do |io| scm_cmd cmd_args do |io|
io.each_line do |line| io.each_line do |line|
diff << line diff << line
end end
diff
end end
diff
rescue ScmCommandAborted
nil
end end
def annotate(path, identifier=nil) def annotate(path, identifier=nil)
...@@ -286,34 +281,36 @@ module Redmine ...@@ -286,34 +281,36 @@ module Redmine
cmd_args = %w|blame| cmd_args = %w|blame|
cmd_args << "-p" << identifier << "--" << scm_iconv(@path_encoding, 'UTF-8', path) cmd_args << "-p" << identifier << "--" << scm_iconv(@path_encoding, 'UTF-8', path)
blame = Annotate.new blame = Annotate.new
content = nil content = scm_cmd(cmd_args) do |io|
scm_cmd(*cmd_args) { |io| io.binmode; content = io.read } io.binmode
# git annotates binary files io.read
if content.respond_to?("is_binary_data?") && content.is_binary_data? # Ruby 1.8.x and <1.9.2
return nil
elsif content.respond_to?(:force_encoding) && (content.dup.force_encoding("UTF-8") != content.dup.force_encoding("BINARY")) # Ruby 1.9.2
# TODO: need to handle edge cases of non-binary content that isn't UTF-8
return nil
end end
identifier = '' if content
# git shows commit author on the first occurrence only # git annotates binary files
authors_by_commit = {} if content.respond_to?("is_binary_data?") && content.is_binary_data? # Ruby 1.8.x and <1.9.2
content.split("\n").each do |line| return nil
if line =~ /^([0-9a-f]{39,40})\s.*/ elsif content.respond_to?(:force_encoding) && (content.dup.force_encoding("UTF-8") != content.dup.force_encoding("BINARY")) # Ruby 1.9.2
identifier = $1 # TODO: need to handle edge cases of non-binary content that isn't UTF-8
elsif line =~ /^author (.+)/ return nil
authors_by_commit[identifier] = $1.strip end
elsif line =~ /^\t(.*)/ identifier = ''
blame.add_line($1, Revision.new( # git shows commit author on the first occurrence only
:identifier => identifier, authors_by_commit = {}
:author => authors_by_commit[identifier])) content.split("\n").each do |line|
identifier = '' if line =~ /^([0-9a-f]{39,40})\s.*/
author = '' identifier = $1
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]))
identifier = ''
author = ''
end
end end
blame
end end
blame
rescue ScmCommandAborted
nil
end end
def cat(path, identifier=nil) def cat(path, identifier=nil)
...@@ -322,14 +319,16 @@ module Redmine ...@@ -322,14 +319,16 @@ module Redmine
end end
cmd_args = %w|show --no-color| cmd_args = %w|show --no-color|
cmd_args << "#{identifier}:#{scm_iconv(@path_encoding, 'UTF-8', path)}" cmd_args << "#{identifier}:#{scm_iconv(@path_encoding, 'UTF-8', path)}"
cat = nil scm_cmd(cmd_args) do |io|
scm_cmd(*cmd_args) do |io|
io.binmode io.binmode
cat = io.read io.read
end end
cat end
rescue ScmCommandAborted
nil def save_entry_in_file(f, path, identifier)
cmd_args = %w|show --no-color|
cmd_args << "#{identifier}:#{scm_iconv(@path_encoding, 'UTF-8', path)}"
scm_cmd(cmd_args, f.path)
end end
class Revision < Redmine::Scm::Adapters::Revision class Revision < Redmine::Scm::Adapters::Revision
...@@ -339,20 +338,15 @@ module Redmine ...@@ -339,20 +338,15 @@ module Redmine
end end
end end
def scm_cmd(*args, &block) private
repo_path = root_url || url # returns the string that will represent the command for shelling out
full_args = [GIT_BIN, '--git-dir', repo_path] def build_scm_cmd(args)
full_args = [GIT_BIN, '--git-dir', root_url || url ]
if self.class.client_version_above?([1, 7, 2]) if self.class.client_version_above?([1, 7, 2])
full_args << '-c' << 'core.quotepath=false' full_args << '-c' << 'core.quotepath=false'
end end
full_args += args (full_args + args).map { |e| shell_quote e.to_s }.join(' ')
ret = shellout(full_args.map { |e| shell_quote e.to_s }.join(' '), &block)
if $? && $?.exitstatus != 0
raise ScmCommandAborted, "git exited with non-zero status: #{$?.exitstatus}"
end
ret
end end
private :scm_cmd
end end
end end
end end
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
# See doc/COPYRIGHT.rdoc for more details. # See doc/COPYRIGHT.rdoc for more details.
#++ #++
require 'redmine/scm/adapters/abstract_adapter' require_dependency 'redmine/scm/adapters/abstract_adapter'
require 'cgi' require 'cgi'
module Redmine module Redmine
...@@ -27,9 +27,6 @@ module Redmine ...@@ -27,9 +27,6 @@ module Redmine
TEMPLATE_NAME = "hg-template" TEMPLATE_NAME = "hg-template"
TEMPLATE_EXTENSION = "tmpl" TEMPLATE_EXTENSION = "tmpl"
# raised if hg command exited with error, e.g. unknown revision.
class HgCommandAborted < CommandFailed; end
class << self class << self
def client_command def client_command
@@bin ||= HG_BIN @@bin ||= HG_BIN
...@@ -51,10 +48,7 @@ module Redmine ...@@ -51,10 +48,7 @@ module Redmine
# The hg version is expressed either as a # The hg version is expressed either as a
# release number (eg 0.9.5 or 1.0) or as a revision # release number (eg 0.9.5 or 1.0) or as a revision
# id composed of 12 hexa characters. # id composed of 12 hexa characters.
theversion = hgversion_from_command_line.dup theversion = to_ascii(hgversion_from_command_line.dup)
if theversion.respond_to?(:force_encoding)
theversion.force_encoding('ASCII-8BIT')
end
if m = theversion.match(%r{\A(.*?)((\d+\.)+\d+)}) if m = theversion.match(%r{\A(.*?)((\d+\.)+\d+)})
m[2].scan(%r{\d+}).collect(&:to_i) m[2].scan(%r{\d+}).collect(&:to_i)
end end
...@@ -114,56 +108,43 @@ module Redmine ...@@ -114,56 +108,43 @@ module Redmine
Hash[*alist.flatten] Hash[*alist.flatten]
end end
def summary
return @summary if @summary
hg 'rhsummary' do |io|
output = io.read
if output.respond_to?(:force_encoding)
output.force_encoding('UTF-8')
end
begin
@summary = ActiveSupport::XmlMini.parse(output)['rhsummary']
rescue
end
end
end
private :summary
def entries(path=nil, identifier=nil) def entries(path=nil, identifier=nil)
p1 = scm_iconv(@path_encoding, 'UTF-8', path) p1 = scm_iconv(@path_encoding, 'UTF-8', path)
manifest = hg('rhmanifest', '-r', CGI.escape(hgrev(identifier)), cmd_args = [
CGI.escape(without_leading_slash(p1.to_s))) do |io| 'rhmanifest',
output = io.read '-r',
if output.respond_to?(:force_encoding) CGI.escape(hgrev(identifier)),
output.force_encoding('UTF-8') CGI.escape(without_leading_slash(p1.to_s))
end ]
begin
ActiveSupport::XmlMini.parse(output)['rhmanifest']['repository']['manifest'] manifest = scm_cmd cmd_args do |io|
rescue output = to_utf8(io.read)
end ActiveSupport::XmlMini.parse(output)['rhmanifest']['repository']['manifest']
end end
path_prefix = path.blank? ? '' : with_trailling_slash(path)
entries = Entries.new if manifest
as_ary(manifest['dir']).each do |e| path_prefix = path.blank? ? '' : with_trailling_slash(path)
n = scm_iconv('UTF-8', @path_encoding, CGI.unescape(e['name']))
p = "#{path_prefix}#{n}"
entries << Entry.new(:name => n, :path => p, :kind => 'dir')
end
as_ary(manifest['file']).each do |e| entries = Entries.new
n = scm_iconv('UTF-8', @path_encoding, CGI.unescape(e['name'])) as_ary(manifest['dir']).each do |e|
p = "#{path_prefix}#{n}" n = unescape(e['name'])
lr = Revision.new(:revision => e['revision'], :scmid => e['node'], p = "#{path_prefix}#{n}"
:identifier => e['node'], entries << Entry.new(:name => n, :path => p, :kind => 'dir')
:time => Time.at(e['time'].to_i)) end
entries << Entry.new(:name => n, :path => p, :kind => 'file',
:size => e['size'].to_i, :lastrev => lr) as_ary(manifest['file']).each do |e|
end n = unescape(e['name'])
p = "#{path_prefix}#{n}"
lr = Revision.new(:revision => e['revision'], :scmid => e['node'],
:identifier => e['node'],
:time => Time.at(e['time'].to_i))
entries << Entry.new(:name => n, :path => p, :kind => 'file',
:size => e['size'].to_i, :lastrev => lr)
end
entries entries
rescue HgCommandAborted end
nil # means not found
end end
def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}) def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
...@@ -175,103 +156,100 @@ module Redmine ...@@ -175,103 +156,100 @@ module Redmine
# Iterates the revisions by using a template file that # Iterates the revisions by using a template file that
# makes Mercurial produce a xml output. # makes Mercurial produce a xml output.
def each_revision(path=nil, identifier_from=nil, identifier_to=nil, options={}) def each_revision(path=nil, identifier_from=nil, identifier_to=nil, options={})
hg_args = ['log', '--debug', '-C', '--style', self.class.template_path] cmd_args = ['log', '--debug', '-C', '--style', self.class.template_path]
hg_args << '-r' << "#{hgrev(identifier_from)}:#{hgrev(identifier_to)}" cmd_args << '-r' << "#{hgrev(identifier_from)}:#{hgrev(identifier_to)}"
hg_args << '--limit' << options[:limit] if options[:limit] cmd_args << '--limit' << options[:limit] if options[:limit]
hg_args << hgtarget(path) unless path.blank? cmd_args << hgtarget(path) unless path.blank?
log = hg(*hg_args) do |io|
log = scm_cmd(cmd_args) do |io|
output = io.read output = io.read
if output.respond_to?(:force_encoding) if output.respond_to?(:force_encoding)
output.force_encoding('UTF-8') output.force_encoding('UTF-8')
end end
begin ActiveSupport::XmlMini.parse("#{output}</log>")['log']
# Mercurial < 1.5 does not support footer template for '</log>'
ActiveSupport::XmlMini.parse("#{output}</log>")['log']
rescue
end
end end
as_ary(log['logentry']).each do |le| if log
cpalist = as_ary(le['paths']['path-copied']).map do |e| as_ary(log['logentry']).each do |le|
[e['__content__'], e['copyfrom-path']].map do |s| cpalist = as_ary(le['paths']['path-copied']).map do |e|
scm_iconv('UTF-8', @path_encoding, CGI.unescape(s)) [e['__content__'], e['copyfrom-path']].map do |s|
unescape s
end
end end
cpmap = Hash[*cpalist.flatten]
paths = as_ary(le['paths']['path']).map do |e|
p = unescape e['__content__']
{:action => e['action'], :path => with_leading_slash(p),
:from_path => (cpmap.member?(p) ? with_leading_slash(cpmap[p]) : nil),
:from_revision => (cpmap.member?(p) ? le['revision'] : nil)}
end.sort { |a, b| a[:path] <=> b[:path] }
yield Revision.new(:revision => le['revision'],
:scmid => le['node'],
:author => (le['author']['__content__'] rescue ''),
:time => Time.parse(le['date']['__content__']),
:message => le['msg']['__content__'],
:paths => paths)
end end
cpmap = Hash[*cpalist.flatten] self
paths = as_ary(le['paths']['path']).map do |e|
p = scm_iconv('UTF-8', @path_encoding, CGI.unescape(e['__content__']) )
{:action => e['action'], :path => with_leading_slash(p),
:from_path => (cpmap.member?(p) ? with_leading_slash(cpmap[p]) : nil),
:from_revision => (cpmap.member?(p) ? le['revision'] : nil)}
end.sort { |a, b| a[:path] <=> b[:path] }
yield Revision.new(:revision => le['revision'],
:scmid => le['node'],
:author => (le['author']['__content__'] rescue ''),
:time => Time.parse(le['date']['__content__']),
:message => le['msg']['__content__'],
:paths => paths)
end end
self
end end
# Returns list of nodes in the specified branch # Returns list of nodes in the specified branch
def nodes_in_branch(branch, options={}) def nodes_in_branch(branch, options={})
hg_args = ['rhlog', '--template', '{node|short}\n', '--rhbranch', CGI.escape(branch)] cmd_args = ['rhlog', '--template', '{node|short}\n', '--rhbranch', CGI.escape(branch)]
hg_args << '--from' << CGI.escape(branch) cmd_args << '--from' << CGI.escape(branch)
hg_args << '--to' << '0' cmd_args << '--to' << '0'
hg_args << '--limit' << options[:limit] if options[:limit] cmd_args << '--limit' << options[:limit] if options[:limit]
hg(*hg_args) { |io| io.readlines.map { |e| e.chomp } } scm_cmd(cmd_args) do |io|
io.readlines.map { |e| e.chomp }
end
end end
def diff(path, identifier_from, identifier_to=nil) def diff(path, identifier_from, identifier_to=nil)
hg_args = %w|rhdiff| cmd_args = %w|rhdiff|
if identifier_to if identifier_to
hg_args << '-r' << hgrev(identifier_to) << '-r' << hgrev(identifier_from) cmd_args << '-r' << hgrev(identifier_to) << '-r' << hgrev(identifier_from)
else else
hg_args << '-c' << hgrev(identifier_from) cmd_args << '-c' << hgrev(identifier_from)
end end
unless path.blank? unless path.blank?
p = scm_iconv(@path_encoding, 'UTF-8', path) p = scm_iconv(@path_encoding, 'UTF-8', path)
hg_args << CGI.escape(hgtarget(p)) cmd_args << CGI.escape(hgtarget(p))
end end
diff = [] scm_cmd cmd_args do |io|
hg *hg_args do |io| diff = []
io.each_line do |line| io.each_line{ |line| diff << line }
diff << line diff
end
end end
diff
rescue HgCommandAborted
nil # means not found
end end
def cat(path, identifier=nil) def cat(path, identifier=nil)
p = CGI.escape(scm_iconv(@path_encoding, 'UTF-8', path)) p = escape(path)
hg 'rhcat', '-r', CGI.escape(hgrev(identifier)), hgtarget(p) do |io| cmd_args = ['rhcat', '-r', CGI.escape(hgrev(identifier)), hgtarget(p)]
scm_cmd cmd_args do |io|
io.binmode io.binmode
io.read io.read
end end
rescue HgCommandAborted
nil # means not found
end end
def annotate(path, identifier=nil) def annotate(path, identifier=nil)
p = CGI.escape(scm_iconv(@path_encoding, 'UTF-8', path)) p = escape(path)
blame = Annotate.new cmd_args = ['rhannotate', '-ncu', '-r', CGI.escape(hgrev(identifier)), hgtarget(p)]
hg 'rhannotate', '-ncu', '-r', CGI.escape(hgrev(identifier)), hgtarget(p) do |io| scm_cmd cmd_args do |io|
blame = Annotate.new
io.each_line do |line| io.each_line do |line|
line.force_encoding('ASCII-8BIT') if line.respond_to?(:force_encoding) to_ascii(line)
next unless line =~ %r{^([^:]+)\s(\d+)\s([0-9a-f]+):\s(.*)$} next unless line =~ %r{^([^:]+)\s(\d+)\s([0-9a-f]+):\s(.*)$}
r = Revision.new(:author => $1.strip, :revision => $2, :scmid => $3, r = Revision.new(:author => $1.strip, :revision => $2, :scmid => $3,
:identifier => $3) :identifier => $3)
blame.add_line($4.rstrip, r) blame.add_line($4.rstrip, r)
end end
blame
end end
blame
rescue HgCommandAborted
nil # means not found or cannot be annotated
end end
class Revision < Redmine::Scm::Adapters::Revision class Revision < Redmine::Scm::Adapters::Revision
...@@ -281,20 +259,16 @@ module Redmine ...@@ -281,20 +259,16 @@ module Redmine
end end
end end
# Runs 'hg' command with the given args private
def hg(*args, &block)
def build_scm_cmd(args)
repo_path = root_url || url repo_path = root_url || url
full_args = [HG_BIN, '-R', repo_path, '--encoding', 'utf-8'] full_args = [HG_BIN, '-R', repo_path, '--encoding', 'utf-8']
full_args << '--config' << "extensions.redminehelper=#{HG_HELPER_EXT}" full_args << '--config' << "extensions.redminehelper=#{HG_HELPER_EXT}"
full_args << '--config' << 'diff.git=false' full_args << '--config' << 'diff.git=false'
full_args += args full_args += args
ret = shellout(full_args.map { |e| shell_quote e.to_s }.join(' '), &block) full_args.map { |e| shell_quote e.to_s }.join(' ')
if $? && $?.exitstatus != 0
raise HgCommandAborted, "hg exited with non-zero status: #{$?.exitstatus}"
end
ret
end end
private :hg
# Returns correct revision identifier # Returns correct revision identifier
def hgrev(identifier, sq=false) def hgrev(identifier, sq=false)
...@@ -302,19 +276,47 @@ module Redmine ...@@ -302,19 +276,47 @@ module Redmine
rev = shell_quote(rev) if sq rev = shell_quote(rev) if sq
rev rev
end end
private :hgrev
def hgtarget(path) def hgtarget(path)
path ||= '' path ||= ''
root_url + '/' + without_leading_slash(path) root_url + '/' + without_leading_slash(path)
end end
private :hgtarget
def as_ary(o) def as_ary(o)
return [] unless o return [] unless o
o.is_a?(Array) ? o : Array[o] o.is_a?(Array) ? o : Array[o]
end end
private :as_ary
def summary
return @summary if @summary
scm_cmd ['rhsummary'] do |io|
output = to_utf8(io.read)
@summary = ActiveSupport::XmlMini.parse(output)['rhsummary']
end
end
def to_utf8(str)
str.force_encoding('UTF-8') if str.respond_to?(:force_encoding)
str
end
def self.to_ascii(str)
str.force_encoding('ASCII-8BIT') if str.respond_to?(:force_encoding)
str
end
def to_ascii(str)
self.class.to_ascii(str)
end
def escape(str)
CGI.escape(scm_iconv(@path_encoding, 'UTF-8', str))
end
def unescape(str)
scm_iconv('UTF-8', @path_encoding, CGI.unescape(str))
end
end end
end end
end end
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
# See doc/COPYRIGHT.rdoc for more details. # See doc/COPYRIGHT.rdoc for more details.
#++ #++
require 'redmine/scm/adapters/abstract_adapter' require_dependency 'redmine/scm/adapters/abstract_adapter'
require 'uri' require 'uri'
module Redmine module Redmine
...@@ -57,194 +57,178 @@ module Redmine ...@@ -57,194 +57,178 @@ module Redmine
# Get info about the svn repository # Get info about the svn repository
def info def info
cmd = "#{self.class.sq_bin} info --xml #{target}" cmd_args = ['info','--xml', target, credentials_string]
cmd << credentials_string scm_cmd(cmd_args) do |io|
info = nil
shellout(cmd) do |io|
output = io.read output = io.read
if output.respond_to?(:force_encoding) if output.respond_to?(:force_encoding)
output.force_encoding('UTF-8') output.force_encoding('UTF-8')
end end
begin doc = ActiveSupport::XmlMini.parse(output)
doc = ActiveSupport::XmlMini.parse(output) #root_url = doc.elements["info/entry/repository/root"].text
#root_url = doc.elements["info/entry/repository/root"].text Info.new({
info = Info.new({:root_url => doc['info']['entry']['repository']['root']['__content__'], :root_url => doc['info']['entry']['repository']['root']['__content__'],
:lastrev => Revision.new({ :lastrev => Revision.new({
:identifier => doc['info']['entry']['commit']['revision'], :identifier => doc['info']['entry']['commit']['revision'],
:time => Time.parse(doc['info']['entry']['commit']['date']['__content__']).localtime, :time => Time.parse(doc['info']['entry']['commit']['date']['__content__']).localtime,
:author => (doc['info']['entry']['commit']['author'] ? doc['info']['entry']['commit']['author']['__content__'] : "") :author => (doc['info']['entry']['commit']['author'] ? doc['info']['entry']['commit']['author']['__content__'] : "")
}) })
}) })
rescue
end
end end
return nil if $? && $?.exitstatus != 0
info
rescue CommandFailed
return nil
end end
# Returns an Entries collection # Returns an Entries collection
# or nil if the given path doesn't exist in the repository # or nil if the given path doesn't exist in the repository
def entries(path=nil, identifier=nil) def entries(path=nil, identifier=nil)
path ||= '' path ||= ''
identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD" identifier = initialize_identifier(identifier)
entries = Entries.new entries = Entries.new
cmd = "#{self.class.sq_bin} list --xml #{target(path)}@#{identifier}" cmd_args = ['list', '--xml', "#{target(path)}@#{identifier}", credentials_string]
cmd << credentials_string scm_cmd(cmd_args) do |io|
shellout(cmd) do |io|
output = io.read output = io.read
if output.respond_to?(:force_encoding) if output.respond_to?(:force_encoding)
output.force_encoding('UTF-8') output.force_encoding('UTF-8')
end end
begin doc = ActiveSupport::XmlMini.parse(output)
doc = ActiveSupport::XmlMini.parse(output) each_xml_element(doc['lists']['list'], 'entry') do |entry|
each_xml_element(doc['lists']['list'], 'entry') do |entry| commit = entry['commit']
commit = entry['commit'] commit_date = commit['date']
commit_date = commit['date'] # Skip directory if there is no commit date (usually that
# Skip directory if there is no commit date (usually that # means that we don't have read access to it)
# means that we don't have read access to it) next if entry['kind'] == 'dir' && commit_date.nil?
next if entry['kind'] == 'dir' && commit_date.nil? name = entry['name']['__content__']
name = entry['name']['__content__'] entries << Entry.new({:name => URI.unescape(name),
entries << Entry.new({:name => URI.unescape(name), :path => ((path.empty? ? "" : "#{path}/") + name),
:path => ((path.empty? ? "" : "#{path}/") + name), :kind => entry['kind'],
:kind => entry['kind'], :size => ((s = entry['size']) ? s['__content__'].to_i : nil),
:size => ((s = entry['size']) ? s['__content__'].to_i : nil), :lastrev => Revision.new({
:lastrev => Revision.new({ :identifier => commit['revision'],
:identifier => commit['revision'], :time => Time.parse(commit_date['__content__'].to_s).localtime,
:time => Time.parse(commit_date['__content__'].to_s).localtime, :author => ((a = commit['author']) ? a['__content__'] : nil)
: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
logger.debug("Found #{entries.size} entries in the repository for #{target(path)}") if logger && logger.debug?
entries.sort_by_name
end end
return nil if $? && $?.exitstatus != 0
logger.debug("Found #{entries.size} entries in the repository for #{target(path)}") if logger && logger.debug?
entries.sort_by_name
end end
def properties(path, identifier=nil) def properties(path, identifier=nil)
# proplist xml output supported in svn 1.5.0 and higher # proplist xml output supported in svn 1.5.0 and higher
return nil unless self.class.client_version_above?([1, 5, 0]) return nil unless self.class.client_version_above?([1, 5, 0])
identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD" identifier = initialize_identifier(identifier)
cmd = "#{self.class.sq_bin} proplist --verbose --xml #{target(path)}@#{identifier}" cmd_args = ['proplist', '--verbose', '--xml', "#{target(path)}@#{identifier}", credentials_string]
cmd << credentials_string
properties = {} properties = {}
shellout(cmd) do |io| scm_cmd(cmd_args) do |io|
output = io.read output = io.read
if output.respond_to?(:force_encoding) if output.respond_to?(:force_encoding)
output.force_encoding('UTF-8') output.force_encoding('UTF-8')
end end
begin doc = ActiveSupport::XmlMini.parse(output)
doc = ActiveSupport::XmlMini.parse(output) each_xml_element(doc['properties']['target'], 'property') do |property|
each_xml_element(doc['properties']['target'], 'property') do |property| properties[ property['name'] ] = property['__content__'].to_s
properties[ property['name'] ] = property['__content__'].to_s
end
rescue
end end
properties
end end
return nil if $? && $?.exitstatus != 0
properties
end end
def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}) def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
path ||= '' path ||= ''
identifier_from = (identifier_from && identifier_from.to_i > 0) ? identifier_from.to_i : "HEAD" identifier_from = initialize_identifier(identifier_from)
identifier_to = (identifier_to && identifier_to.to_i > 0) ? identifier_to.to_i : 1 identifier_to = initialize_identifier(identifier_to, 1)
cmd_args = ['log', '--xml', '-r', "#{identifier_from}:#{identifier_to}", credentials_string]
cmd_args << " --verbose " if options[:with_paths]
cmd_args << " --limit #{options[:limit].to_i}" if options[:limit]
cmd_args << target(path)
revisions = Revisions.new revisions = Revisions.new
cmd = "#{self.class.sq_bin} log --xml -r #{identifier_from}:#{identifier_to}"
cmd << credentials_string scm_cmd(cmd_args) do |io|
cmd << " --verbose " if options[:with_paths]
cmd << " --limit #{options[:limit].to_i}" if options[:limit]
cmd << ' ' + target(path)
shellout(cmd) do |io|
output = io.read output = io.read
if output.respond_to?(:force_encoding) if output.respond_to?(:force_encoding)
output.force_encoding('UTF-8') output.force_encoding('UTF-8')
end end
begin doc = ActiveSupport::XmlMini.parse(output)
doc = ActiveSupport::XmlMini.parse(output) each_xml_element(doc['log'], 'logentry') do |logentry|
each_xml_element(doc['log'], 'logentry') do |logentry| paths = []
paths = []
if logentry['paths'] && logentry['paths']['path']
each_xml_element(logentry['paths'], 'path') do |path| each_xml_element(logentry['paths'], 'path') do |path|
paths << {:action => path['action'], paths << {:action => path['action'],
:path => path['__content__'], :path => path['__content__'],
:from_path => path['copyfrom-path'], :from_path => path['copyfrom-path'],
:from_revision => path['copyfrom-rev'] :from_revision => path['copyfrom-rev']
} }
end if logentry['paths'] && logentry['paths']['path'] end
paths.sort! { |x,y| x[:path] <=> y[:path] }
revisions << Revision.new({:identifier => logentry['revision'],
:author => (logentry['author'] ? logentry['author']['__content__'] : ""),
:time => Time.parse(logentry['date']['__content__'].to_s).localtime,
:message => logentry['msg']['__content__'],
:paths => paths
})
end end
rescue paths.sort! { |x,y| x[:path] <=> y[:path] }
revisions << Revision.new({:identifier => logentry['revision'],
:author => (logentry['author'] ? logentry['author']['__content__'] : ""),
:time => Time.parse(logentry['date']['__content__'].to_s).localtime,
:message => logentry['msg']['__content__'],
:paths => paths
})
end end
revisions
end end
return nil if $? && $?.exitstatus != 0
revisions
end end
def diff(path, identifier_from, identifier_to=nil, type="inline") def diff(path, identifier_from, identifier_to=nil, type="inline")
path ||= '' path ||= ''
identifier_from = (identifier_from and identifier_from.to_i > 0) ? identifier_from.to_i : ''
identifier_to = (identifier_to and identifier_to.to_i > 0) ? identifier_to.to_i : (identifier_from.to_i - 1) identifier_from = initialize_identifier(identifier_from, '')
identifier_to = initialize_identifier(identifier_to, identifier_from.to_i - 1)
cmd = "#{self.class.sq_bin} diff -r " cmd_args = ["diff -r",
cmd << "#{identifier_to}:" "#{identifier_to}:#{identifier_from}",
cmd << "#{identifier_from}" "#{target(path)}@#{identifier_from}",
cmd << " #{target(path)}@#{identifier_from}" credentials_string]
cmd << credentials_string
diff = [] diff = []
shellout(cmd) do |io| scm_cmd(cmd_args) do |io|
io.each_line do |line| io.each_line do |line|
diff << line diff << line
end end
diff
end end
return nil if $? && $?.exitstatus != 0
diff
end end
def cat(path, identifier=nil) def cat(path, identifier=nil)
identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD" identifier = initialize_identifier(identifier)
cmd = "#{self.class.sq_bin} cat #{target(path)}@#{identifier}"
cmd << credentials_string cmd_args = ['cat', "#{target(path)}@#{identifier}", credentials_string]
cat = nil scm_cmd(cmd_args) do |io|
shellout(cmd) do |io|
io.binmode io.binmode
cat = io.read io.read
end end
return nil if $? && $?.exitstatus != 0 end
cat
def save_entry_in_file(f, path, identifier)
identifier = initialize_identifier(identifier)
cmd_args = ['cat', "#{target(path)}@#{identifier}", credentials_string]
scm_cmd(cmd_args, f.path)
end end
def annotate(path, identifier=nil) def annotate(path, identifier=nil)
identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD" identifier = initialize_identifier(identifier)
cmd = "#{self.class.sq_bin} blame #{target(path)}@#{identifier}" cmd_args = ['blame', "#{target(path)}@#{identifier}", credentials_string]
cmd << credentials_string
blame = Annotate.new blame = Annotate.new
shellout(cmd) do |io| scm_cmd(cmd_args) do |io|
io.each_line do |line| io.each_line do |line|
next unless line =~ %r{^\s*(\d+)\s*(\S+)\s(.*)$} next unless line =~ %r{^\s*(\d+)\s*(\S+)\s(.*)$}
blame.add_line($3.rstrip, Revision.new(:identifier => $1.to_i, :author => $2.strip)) blame.add_line($3.rstrip, Revision.new(:identifier => $1.to_i, :author => $2.strip))
end end
blame
end end
return nil if $? && $?.exitstatus != 0
blame
end end
private private
def initialize_identifier(identifier, default="HEAD")
(identifier && identifier.to_i > 0) ? identifier.to_i : default
end
def credentials_string def credentials_string
str = '' str = ''
str << " --username #{shell_quote(@login)}" unless @login.blank? str << " --username #{shell_quote(@login)}" unless @login.blank?
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment