-# 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.
module Redmine
module Scm
- module Adapters
+ module Adapters
class GitAdapter < AbstractAdapter
-
+
# Git executable name
- GIT_BIN = "git"
+ GIT_BIN = Redmine::Configuration['scm_git_command'] || "git"
- # Get the revision of a particuliar file
- def get_rev (rev,path)
- cmd="git --git-dir #{target('')} show #{shell_quote rev} -- #{shell_quote path}" if rev!='latest' and (! rev.nil?)
- cmd="git --git-dir #{target('')} log -1 master -- #{shell_quote path}" if
- rev=='latest' or rev.nil?
- rev=[]
- i=0
- shellout(cmd) do |io|
- files=[]
- changeset = {}
- parsing_descr = 0 #0: not parsing desc or files, 1: parsing desc, 2: parsing files
+ # raised if scm command exited with error, e.g. unknown revision.
+ class ScmCommandAborted < CommandFailed; end
- io.each_line do |line|
- if line =~ /^commit ([0-9a-f]{40})$/
- key = "commit"
- value = $1
- if (parsing_descr == 1 || parsing_descr == 2)
- parsing_descr = 0
- rev = Revision.new({:identifier => changeset[:commit],
- :scmid => changeset[:commit],
- :author => changeset[:author],
- :time => Time.parse(changeset[:date]),
- :message => changeset[:description],
- :paths => files
- })
- changeset = {}
- files = []
- end
- changeset[:commit] = $1
- elsif (parsing_descr == 0) && line =~ /^(\w+):\s*(.*)$/
- key = $1
- value = $2
- if key == "Author"
- changeset[:author] = value
- elsif key == "Date"
- changeset[:date] = value
- end
- elsif (parsing_descr == 0) && line.chomp.to_s == ""
- 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)\s+(.+)$/
- parsing_descr = 2
- fileaction = $1
- filepath = $2
- files << {:action => fileaction, :path => filepath}
- elsif (parsing_descr == 1) && line.chomp.to_s == ""
- parsing_descr = 2
- elsif (parsing_descr == 1)
- changeset[:description] << line
- end
- end
- rev = Revision.new({:identifier => changeset[:commit],
- :scmid => changeset[:commit],
- :author => changeset[:author],
- :time => (changeset[:date] ? Time.parse(changeset[:date]) : nil),
- :message => changeset[:description],
- :paths => files
- })
+ class << self
+ def client_command
+ @@bin ||= GIT_BIN
+ end
+ def sq_bin
+ @@sq_bin ||= shell_quote(GIT_BIN)
end
- get_rev('latest',path) if rev == []
+ def client_version
+ @@client_version ||= (scm_command_version || [])
+ end
- return nil if $? && $?.exitstatus != 0
- return rev
+ def client_available
+ !client_version.empty?
+ end
+
+ def scm_command_version
+ scm_version = scm_version_from_command_line.dup
+ if scm_version.respond_to?(:force_encoding)
+ scm_version.force_encoding('ASCII-8BIT')
+ end
+ if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)})
+ m[2].scan(%r{\d+}).collect(&:to_i)
+ end
+ end
+
+ def scm_version_from_command_line
+ shellout("#{sq_bin} --version --no-color") { |io| io.read }.to_s
+ end
end
+ def initialize(url, root_url=nil, login=nil, password=nil, path_encoding=nil)
+ super
+ @path_encoding = path_encoding.blank? ? 'UTF-8' : path_encoding
+ end
+
+ def path_encoding
+ @path_encoding
+ end
def info
- revs = revisions(url,nil,nil,{:limit => 1})
- if revs && revs.any?
- Info.new(:root_url => url, :lastrev => revs.first)
- else
+ begin
+ Info.new(:root_url => url, :lastrev => lastrev('',nil))
+ rescue
nil
end
- rescue Errno::ENOENT => e
- return nil
end
-
- def entries(path=nil, identifier=nil)
+
+ def branches
+ return @branches if @branches
+ @branches = []
+ 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_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
+ bras = self.branches
+ return nil if bras.nil?
+ bras.include?('master') ? 'master' : bras.first
+ 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?
+ # 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 = "#{GIT_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-]+)\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
- entries << Entry.new({:name => name,
- :path => (path.empty? ? name : "#{path}/#{name}"),
- :kind => ((type == "tree") ? 'dir' : 'file'),
- :size => ((type == "tree") ? nil : size),
- :lastrev => get_rev(identifier,(path.empty? ? name : "#{path}/#{name}"))
-
- }) unless entries.detect{|entry| entry.name == name}
+ 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 => 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 << "--" << path unless path.empty?
+ lines = []
+ scm_cmd(*cmd_args) { |io| lines = io.readlines }
+ begin
+ id = lines[0].split[1]
+ author = lines[1].match('Author:\s+(.*)$')[1]
+ time = Time.parse(lines[4].match('CommitDate:\s+(.*)$')[1])
+
+ Revision.new({
+ :identifier => id,
+ :scmid => id,
+ :author => author,
+ :time => time,
+ :message => nil,
+ :paths => nil
+ })
+ rescue NoMethodError => e
+ logger.error("The revision '#{path}' has a wrong format")
+ return nil
+ end
+ rescue ScmCommandAborted
+ nil
+ end
+
def revisions(path, identifier_from, identifier_to, options={})
- revisions = Revisions.new
- cmd = "#{GIT_BIN} --git-dir #{target('')} log --raw "
- cmd << " -n #{options[:limit].to_i} " if (!options.nil?) && options[:limit]
- cmd << " #{shell_quote(identifier_from + '..')} " if identifier_from
- cmd << " #{shell_quote identifier_to} " if identifier_to
- #cmd << " HEAD " if !identifier_to
- shellout(cmd) do |io|
+ 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]
+ cmd_args << "-n" << "#{options[:limit].to_i}" if options[:limit]
+ from_to = ""
+ from_to << "#{identifier_from}.." if identifier_from
+ 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 << "--" << 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})$/
value = $1
if (parsing_descr == 1 || parsing_descr == 2)
parsing_descr = 0
- revisions << Revision.new({:identifier => changeset[:commit],
- :scmid => changeset[:commit],
- :author => changeset[:author],
- :time => Time.parse(changeset[:date]),
- :message => changeset[:description],
- :paths => files
- })
+ revision = Revision.new({
+ :identifier => changeset[:commit],
+ :scmid => changeset[:commit],
+ :author => changeset[:author],
+ :time => Time.parse(changeset[:date]),
+ :message => changeset[:description],
+ :paths => files
+ })
+ if block_given?
+ yield revision
+ else
+ revs << revision
+ end
changeset = {}
files = []
- revno = revno + 1
end
changeset[:commit] = $1
elsif (parsing_descr == 0) && line =~ /^(\w+):\s*(.*)$/
value = $2
if key == "Author"
changeset[:author] = value
- elsif key == "Date"
+ elsif key == "CommitDate"
changeset[:date] = value
end
elsif (parsing_descr == 0) && line.chomp.to_s == ""
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)\s+(.+)$/
+ elsif (parsing_descr == 1 || parsing_descr == 2) \
+ && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\t(.+)$/
+ parsing_descr = 2
+ 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(.+)$/
parsing_descr = 2
- fileaction = $1
- filepath = $2
- 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
-
- revisions << Revision.new({:identifier => changeset[:commit],
- :scmid => changeset[:commit],
- :author => changeset[:author],
- :time => Time.parse(changeset[:date]),
- :message => changeset[:description],
- :paths => files
- }) if changeset[:commit]
+ 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
+ })
+ if block_given?
+ yield revision
+ else
+ revs << revision
+ end
+ end
end
-
- return nil if $? && $?.exitstatus != 0
- 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, type="inline")
+
+ def diff(path, identifier_from, identifier_to=nil)
path ||= ''
- if !identifier_to
- identifier_to = nil
+ cmd_args = []
+ if identifier_to
+ cmd_args << "diff" << "--no-color" << identifier_to << identifier_from
+ else
+ cmd_args << "show" << "--no-color" << identifier_from
end
-
- cmd = "#{GIT_BIN} --git-dir #{target('')} show #{shell_quote identifier_from}" if identifier_to.nil?
- cmd = "#{GIT_BIN} --git-dir #{target('')} diff #{shell_quote identifier_to} #{shell_quote identifier_from}" if !identifier_to.nil?
- 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
- DiffTableList.new diff, type
+ diff
+ rescue ScmCommandAborted
+ nil
end
-
+
def annotate(path, identifier=nil)
identifier = 'HEAD' if identifier.blank?
- cmd = "#{GIT_BIN} --git-dir #{target('')} blame -l #{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 = ''
+ # git shows commit author on the first occurrence only
+ authors_by_commit = {}
content.split("\n").each do |line|
- next unless line =~ /([0-9a-f]{39,40})\s\((\w*)[^\)]*\)(.*)/
- blame.add_line($3.rstrip, Revision.new(:identifier => $1, :author => $2.strip))
+ if line =~ /^([0-9a-f]{39,40})\s.*/
+ identifier = $1
+ elsif line =~ /^author (.+)/
+ authors_by_commit[identifier] = $1.strip
+ elsif line =~ /^\t(.*)/
+ 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 = "#{GIT_BIN} --git-dir #{target('')} show #{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
+ # Returns the readable identifier
+ def format_identifier
+ identifier[0,8]
+ end
+ end
+
+ 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
+ raise ScmCommandAborted, "git exited with non-zero status: #{$?.exitstatus}"
+ end
+ ret
end
+ private :scm_cmd
end
end
end
-
end
-