summaryrefslogtreecommitdiffstats
path: root/lib/redmine
diff options
context:
space:
mode:
Diffstat (limited to 'lib/redmine')
-rw-r--r--lib/redmine/scm/adapters/abstract_adapter.rb341
-rw-r--r--lib/redmine/scm/adapters/cvs_adapter.rb352
-rw-r--r--lib/redmine/scm/adapters/mercurial_adapter.rb163
-rw-r--r--lib/redmine/scm/adapters/subversion_adapter.rb173
4 files changed, 1029 insertions, 0 deletions
diff --git a/lib/redmine/scm/adapters/abstract_adapter.rb b/lib/redmine/scm/adapters/abstract_adapter.rb
new file mode 100644
index 000000000..b74fa1b18
--- /dev/null
+++ b/lib/redmine/scm/adapters/abstract_adapter.rb
@@ -0,0 +1,341 @@
+# redMine - project management software
+# Copyright (C) 2006-2007 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.
+
+require 'cgi'
+
+module Redmine
+ module Scm
+ module Adapters
+ class CommandFailed < StandardError #:nodoc:
+ end
+
+ class AbstractAdapter #:nodoc:
+ def initialize(url, root_url=nil, login=nil, password=nil)
+ @url = url
+ @login = login if login && !login.empty?
+ @password = (password || "") if @login
+ @root_url = root_url.blank? ? retrieve_root_url : root_url
+ end
+
+ def adapter_name
+ 'Abstract'
+ end
+
+ def root_url
+ @root_url
+ end
+
+ def url
+ @url
+ end
+
+ # get info about the svn repository
+ def info
+ return nil
+ end
+
+ # Returns the entry identified by path and revision identifier
+ # or nil if entry doesn't exist in the repository
+ def entry(path=nil, identifier=nil)
+ e = entries(path, identifier)
+ e ? e.first : nil
+ end
+
+ # Returns an Entries collection
+ # or nil if the given path doesn't exist in the repository
+ def entries(path=nil, identifier=nil)
+ return nil
+ end
+
+ def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
+ return nil
+ end
+
+ def diff(path, identifier_from, identifier_to=nil, type="inline")
+ return nil
+ end
+
+ def cat(path, identifier=nil)
+ return nil
+ end
+
+ def with_leading_slash(path)
+ path ||= ''
+ (path[0,1]!="/") ? "/#{path}" : path
+ end
+
+ private
+ def retrieve_root_url
+ info = self.info
+ info ? info.root_url : nil
+ end
+
+ def target(path)
+ path ||= ""
+ base = path.match(/^\//) ? root_url : url
+ " \"" << "#{base}/#{path}".gsub(/["?<>\*]/, '') << "\""
+ end
+
+ def logger
+ RAILS_DEFAULT_LOGGER
+ end
+
+ def shellout(cmd, &block)
+ logger.debug "Shelling out: #{cmd}" if logger && logger.debug?
+ IO.popen(cmd, "r+") do |io|
+ io.close_write
+ block.call(io) if block_given?
+ end
+ end
+ end
+
+ class Entries < Array
+ def sort_by_name
+ sort {|x,y|
+ if x.kind == y.kind
+ x.name <=> y.name
+ else
+ x.kind <=> y.kind
+ end
+ }
+ end
+
+ def revisions
+ revisions ||= Revisions.new(collect{|entry| entry.lastrev}.compact)
+ end
+ end
+
+ class Info
+ attr_accessor :root_url, :lastrev
+ def initialize(attributes={})
+ self.root_url = attributes[:root_url] if attributes[:root_url]
+ self.lastrev = attributes[:lastrev]
+ end
+ end
+
+ class Entry
+ attr_accessor :name, :path, :kind, :size, :lastrev
+ def initialize(attributes={})
+ self.name = attributes[:name] if attributes[:name]
+ self.path = attributes[:path] if attributes[:path]
+ self.kind = attributes[:kind] if attributes[:kind]
+ self.size = attributes[:size].to_i if attributes[:size]
+ self.lastrev = attributes[:lastrev]
+ end
+
+ def is_file?
+ 'file' == self.kind
+ end
+
+ def is_dir?
+ 'dir' == self.kind
+ end
+
+ def is_text?
+ Redmine::MimeType.is_type?('text', name)
+ end
+ end
+
+ class Revisions < Array
+ def latest
+ sort {|x,y|
+ unless x.time.nil? or y.time.nil?
+ x.time <=> y.time
+ else
+ 0
+ end
+ }.last
+ end
+ end
+
+ class Revision
+ attr_accessor :identifier, :scmid, :name, :author, :time, :message, :paths, :revision, :branch
+ def initialize(attributes={})
+ self.identifier = attributes[:identifier]
+ self.scmid = attributes[:scmid]
+ self.name = attributes[:name] || self.identifier
+ self.author = attributes[:author]
+ self.time = attributes[:time]
+ self.message = attributes[:message] || ""
+ self.paths = attributes[:paths]
+ self.revision = attributes[:revision]
+ self.branch = attributes[:branch]
+ end
+
+ end
+
+ # A line of Diff
+ class Diff
+ attr_accessor :nb_line_left
+ attr_accessor :line_left
+ attr_accessor :nb_line_right
+ attr_accessor :line_right
+ attr_accessor :type_diff_right
+ attr_accessor :type_diff_left
+
+ def initialize ()
+ self.nb_line_left = ''
+ self.nb_line_right = ''
+ self.line_left = ''
+ self.line_right = ''
+ self.type_diff_right = ''
+ self.type_diff_left = ''
+ end
+
+ def inspect
+ puts '### Start Line Diff ###'
+ puts self.nb_line_left
+ puts self.line_left
+ puts self.nb_line_right
+ puts self.line_right
+ end
+ end
+
+ class DiffTableList < Array
+ def initialize (diff, type="inline")
+ diff_table = DiffTable.new type
+ diff.each do |line|
+ if line =~ /^(Index:|diff) (.*)$/
+ self << diff_table if diff_table.length > 1
+ diff_table = DiffTable.new type
+ end
+ a = diff_table.add_line line
+ end
+ self << diff_table
+ end
+ end
+
+ # Class for create a Diff
+ class DiffTable < Hash
+ attr_reader :file_name, :line_num_l, :line_num_r
+
+ # Initialize with a Diff file and the type of Diff View
+ # The type view must be inline or sbs (side_by_side)
+ def initialize (type="inline")
+ @parsing = false
+ @nb_line = 1
+ @start = false
+ @before = 'same'
+ @second = true
+ @type = type
+ end
+
+ # Function for add a line of this Diff
+ def add_line(line)
+ unless @parsing
+ if line =~ /^(Index:|diff) (.*)$/
+ @file_name = $2
+ return false
+ elsif line =~ /^@@ (\+|\-)(\d+)(,\d+)? (\+|\-)(\d+)(,\d+)? @@/
+ @line_num_l = $5.to_i
+ @line_num_r = $2.to_i
+ @parsing = true
+ end
+ else
+ if line =~ /^[^\+\-\s@\\]/
+ self.delete(self.keys.sort.last)
+ @parsing = false
+ return false
+ elsif line =~ /^@@ (\+|\-)(\d+)(,\d+)? (\+|\-)(\d+)(,\d+)? @@/
+ @line_num_l = $5.to_i
+ @line_num_r = $2.to_i
+ else
+ @nb_line += 1 if parse_line(line, @type)
+ end
+ end
+ return true
+ end
+
+ def inspect
+ puts '### DIFF TABLE ###'
+ puts "file : #{file_name}"
+ self.each do |d|
+ d.inspect
+ end
+ end
+
+ private
+ # Test if is a Side By Side type
+ def sbs?(type, func)
+ if @start and type == "sbs"
+ if @before == func and @second
+ tmp_nb_line = @nb_line
+ self[tmp_nb_line] = Diff.new
+ else
+ @second = false
+ tmp_nb_line = @start
+ @start += 1
+ @nb_line -= 1
+ end
+ else
+ tmp_nb_line = @nb_line
+ @start = @nb_line
+ self[tmp_nb_line] = Diff.new
+ @second = true
+ end
+ unless self[tmp_nb_line]
+ @nb_line += 1
+ self[tmp_nb_line] = Diff.new
+ else
+ self[tmp_nb_line]
+ end
+ end
+
+ # Escape the HTML for the diff
+ def escapeHTML(line)
+ CGI.escapeHTML(line).gsub(/\s/, '&nbsp;')
+ end
+
+ def parse_line (line, type="inline")
+ if line[0, 1] == "+"
+ diff = sbs? type, 'add'
+ @before = 'add'
+ diff.line_left = escapeHTML line[1..-1]
+ diff.nb_line_left = @line_num_l
+ diff.type_diff_left = 'diff_in'
+ @line_num_l += 1
+ true
+ elsif line[0, 1] == "-"
+ diff = sbs? type, 'remove'
+ @before = 'remove'
+ diff.line_right = escapeHTML line[1..-1]
+ diff.nb_line_right = @line_num_r
+ diff.type_diff_right = 'diff_out'
+ @line_num_r += 1
+ true
+ elsif line[0, 1] =~ /\s/
+ @before = 'same'
+ @start = false
+ diff = Diff.new
+ diff.line_right = escapeHTML line[1..-1]
+ diff.nb_line_right = @line_num_r
+ diff.line_left = escapeHTML line[1..-1]
+ diff.nb_line_left = @line_num_l
+ self[@nb_line] = diff
+ @line_num_l += 1
+ @line_num_r += 1
+ true
+ elsif line[0, 1] = "\\"
+ true
+ else
+ false
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/redmine/scm/adapters/cvs_adapter.rb b/lib/redmine/scm/adapters/cvs_adapter.rb
new file mode 100644
index 000000000..c5f85f1c6
--- /dev/null
+++ b/lib/redmine/scm/adapters/cvs_adapter.rb
@@ -0,0 +1,352 @@
+# redMine - project management software
+# Copyright (C) 2006-2007 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.
+
+require 'redmine/scm/adapters/abstract_adapter'
+
+module Redmine
+ module Scm
+ module Adapters
+ class CvsAdapter < AbstractAdapter
+
+ # CVS executable name
+ CVS_BIN = "cvs"
+
+ # Guidelines for the input:
+ # url -> the project-path, relative to the cvsroot (eg. module name)
+ # root_url -> the good old, sometimes damned, CVSROOT
+ # login -> unnecessary
+ # password -> unnecessary too
+ def initialize(url, root_url=nil, login=nil, password=nil)
+ @url = url
+ @login = login if login && !login.empty?
+ @password = (password || "") if @login
+ #TODO: better Exception here (IllegalArgumentException)
+ raise CommandFailed if root_url.blank?
+ @root_url = root_url
+ end
+
+ def root_url
+ @root_url
+ end
+
+ def url
+ @url
+ end
+
+ def info
+ logger.debug "<cvs> info"
+ Info.new({:root_url => @root_url, :lastrev => nil})
+ end
+
+ def get_previous_revision(revision)
+ CvsRevisionHelper.new(revision).prevRev
+ end
+
+ # Returns the entry identified by path and revision identifier
+ # or nil if entry doesn't exist in the repository
+ # this method returns all revisions from one single SCM-Entry
+ def entry(path=nil, identifier="HEAD")
+ e = entries(path, identifier)
+ logger.debug("<cvs-result> #{e.first.inspect}") if e
+ e ? e.first : nil
+ end
+
+ # Returns an Entries collection
+ # or nil if the given path doesn't exist in the repository
+ # this method is used by the repository-browser (aka LIST)
+ def entries(path=nil, identifier=nil)
+ logger.debug "<cvs> entries '#{path}' with identifier '#{identifier}'"
+ path_with_project="#{url}#{with_leading_slash(path)}"
+ entries = Entries.new
+ cmd = "#{CVS_BIN} -d #{root_url} rls -ed #{path_with_project}"
+ shellout(cmd) do |io|
+ io.each_line(){|line|
+ fields=line.chop.split('/',-1)
+ logger.debug(">>InspectLine #{fields.inspect}")
+
+ if fields[0]!="D"
+ entries << Entry.new({:name => fields[-5],
+ #:path => fields[-4].include?(path)?fields[-4]:(path + "/"+ fields[-4]),
+ :path => "#{path}/#{fields[-5]}",
+ :kind => 'file',
+ :size => nil,
+ :lastrev => Revision.new({
+ :revision => fields[-4],
+ :name => fields[-4],
+ :time => Time.parse(fields[-3]),
+ :author => ''
+ })
+ })
+ else
+ entries << Entry.new({:name => fields[1],
+ :path => "#{path}/#{fields[1]}",
+ :kind => 'dir',
+ :size => nil,
+ :lastrev => nil
+ })
+ end
+ }
+ end
+ return nil if $? && $?.exitstatus != 0
+ entries.sort_by_name
+ rescue Errno::ENOENT => e
+ raise CommandFailed
+ end
+
+ STARTLOG="----------------------------"
+ ENDLOG ="============================================================================="
+
+ # Returns all revisions found between identifier_from and identifier_to
+ # in the repository. both identifier have to be dates or nil.
+ # these method returns nothing but yield every result in block
+ def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}, &block)
+ logger.debug "<cvs> revisions path:'#{path}',identifier_from #{identifier_from}, identifier_to #{identifier_to}"
+
+ path_with_project="#{url}#{with_leading_slash(path)}"
+ cmd = "#{CVS_BIN} -d #{root_url} rlog"
+ cmd << " -d\">#{time_to_cvstime(identifier_from)}\"" if identifier_from
+ cmd << " #{path_with_project}"
+ shellout(cmd) do |io|
+ state="entry_start"
+
+ commit_log=String.new
+ revision=nil
+ date=nil
+ author=nil
+ entry_path=nil
+ entry_name=nil
+ file_state=nil
+ branch_map=nil
+
+ io.each_line() do |line|
+
+ if state!="revision" && /^#{ENDLOG}/ =~ line
+ commit_log=String.new
+ revision=nil
+ state="entry_start"
+ end
+
+ if state=="entry_start"
+ branch_map=Hash.new
+ if /^RCS file: #{Regexp.escape(root_url)}\/#{Regexp.escape(path_with_project)}(.+),v$/ =~ line
+ entry_path = normalize_cvs_path($1)
+ entry_name = normalize_path(File.basename($1))
+ logger.debug("Path #{entry_path} <=> Name #{entry_name}")
+ elsif /^head: (.+)$/ =~ line
+ entry_headRev = $1 #unless entry.nil?
+ elsif /^symbolic names:/ =~ line
+ state="symbolic" #unless entry.nil?
+ elsif /^#{STARTLOG}/ =~ line
+ commit_log=String.new
+ state="revision"
+ end
+ next
+ elsif state=="symbolic"
+ if /^(.*):\s(.*)/ =~ (line.strip)
+ branch_map[$1]=$2
+ else
+ state="tags"
+ next
+ end
+ elsif state=="tags"
+ if /^#{STARTLOG}/ =~ line
+ commit_log = ""
+ state="revision"
+ elsif /^#{ENDLOG}/ =~ line
+ state="head"
+ end
+ next
+ elsif state=="revision"
+ if /^#{ENDLOG}/ =~ line || /^#{STARTLOG}/ =~ line
+ if revision
+
+ revHelper=CvsRevisionHelper.new(revision)
+ revBranch="HEAD"
+
+ branch_map.each() do |branch_name,branch_point|
+ if revHelper.is_in_branch_with_symbol(branch_point)
+ revBranch=branch_name
+ end
+ end
+
+ logger.debug("********** YIELD Revision #{revision}::#{revBranch}")
+
+ yield Revision.new({
+ :time => date,
+ :author => author,
+ :message=>commit_log.chomp,
+ :paths => [{
+ :revision => revision,
+ :branch=> revBranch,
+ :path=>entry_path,
+ :name=>entry_name,
+ :kind=>'file',
+ :action=>file_state
+ }]
+ })
+ end
+
+ commit_log=String.new
+ revision=nil
+
+ if /^#{ENDLOG}/ =~ line
+ state="entry_start"
+ end
+ next
+ end
+
+ if /^branches: (.+)$/ =~ line
+ #TODO: version.branch = $1
+ elsif /^revision (\d+(?:\.\d+)+).*$/ =~ line
+ revision = $1
+ elsif /^date:\s+(\d+.\d+.\d+\s+\d+:\d+:\d+)/ =~ line
+ date = Time.parse($1)
+ author = /author: ([^;]+)/.match(line)[1]
+ file_state = /state: ([^;]+)/.match(line)[1]
+ #TODO: linechanges only available in CVS.... maybe a feature our SVN implementation. i'm sure, they are
+ # useful for stats or something else
+ # linechanges =/lines: \+(\d+) -(\d+)/.match(line)
+ # unless linechanges.nil?
+ # version.line_plus = linechanges[1]
+ # version.line_minus = linechanges[2]
+ # else
+ # version.line_plus = 0
+ # version.line_minus = 0
+ # end
+ else
+ commit_log << line unless line =~ /^\*\*\* empty log message \*\*\*/
+ end
+ end
+ end
+ end
+ rescue Errno::ENOENT => e
+ raise CommandFailed
+ end
+
+ def diff(path, identifier_from, identifier_to=nil, type="inline")
+ logger.debug "<cvs> diff path:'#{path}',identifier_from #{identifier_from}, identifier_to #{identifier_to}"
+ path_with_project="#{url}#{with_leading_slash(path)}"
+ cmd = "#{CVS_BIN} -d #{root_url} rdiff -u -r#{identifier_to} -r#{identifier_from} #{path_with_project}"
+ diff = []
+ shellout(cmd) do |io|
+ io.each_line do |line|
+ diff << line
+ end
+ end
+ return nil if $? && $?.exitstatus != 0
+ DiffTableList.new diff, type
+ rescue Errno::ENOENT => e
+ raise CommandFailed
+ end
+
+ def cat(path, identifier=nil)
+ identifier = (identifier) ? identifier : "HEAD"
+ logger.debug "<cvs> cat path:'#{path}',identifier #{identifier}"
+ path_with_project="#{url}#{with_leading_slash(path)}"
+ cmd = "#{CVS_BIN} -d #{root_url} co -r#{identifier} -p #{path_with_project}"
+ cat = nil
+ shellout(cmd) do |io|
+ cat = io.read
+ end
+ return nil if $? && $?.exitstatus != 0
+ cat
+ rescue Errno::ENOENT => e
+ raise CommandFailed
+ end
+
+ private
+
+ # convert a date/time into the CVS-format
+ def time_to_cvstime(time)
+ return nil if time.nil?
+ unless time.kind_of? Time
+ time = Time.parse(time)
+ end
+ return time.strftime("%Y-%m-%d %H:%M:%S")
+ end
+
+ def normalize_cvs_path(path)
+ normalize_path(path.gsub(/Attic\//,''))
+ end
+
+ def normalize_path(path)
+ path.sub(/^(\/)*(.*)/,'\2').sub(/(.*)(,v)+/,'\1')
+ end
+ end
+
+ class CvsRevisionHelper
+ attr_accessor :complete_rev, :revision, :base, :branchid
+
+ def initialize(complete_rev)
+ @complete_rev = complete_rev
+ parseRevision()
+ end
+
+ def branchPoint
+ return @base
+ end
+
+ def branchVersion
+ if isBranchRevision
+ return @base+"."+@branchid
+ end
+ return @base
+ end
+
+ def isBranchRevision
+ !@branchid.nil?
+ end
+
+ def prevRev
+ unless @revision==0
+ return buildRevision(@revision-1)
+ end
+ return buildRevision(@revision)
+ end
+
+ def is_in_branch_with_symbol(branch_symbol)
+ bpieces=branch_symbol.split(".")
+ branch_start="#{bpieces[0..-3].join(".")}.#{bpieces[-1]}"
+ return (branchVersion==branch_start)
+ end
+
+ private
+ def buildRevision(rev)
+ if rev== 0
+ @base
+ elsif @branchid.nil?
+ @base+"."+rev.to_s
+ else
+ @base+"."+@branchid+"."+rev.to_s
+ end
+ end
+
+ # Interpretiert die cvs revisionsnummern wie z.b. 1.14 oder 1.3.0.15
+ def parseRevision()
+ pieces=@complete_rev.split(".")
+ @revision=pieces.last.to_i
+ baseSize=1
+ baseSize+=(pieces.size/2)
+ @base=pieces[0..-baseSize].join(".")
+ if baseSize > 2
+ @branchid=pieces[-2]
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/redmine/scm/adapters/mercurial_adapter.rb b/lib/redmine/scm/adapters/mercurial_adapter.rb
new file mode 100644
index 000000000..54fa8c4f8
--- /dev/null
+++ b/lib/redmine/scm/adapters/mercurial_adapter.rb
@@ -0,0 +1,163 @@
+# redMine - project management software
+# Copyright (C) 2006-2007 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.
+
+require 'redmine/scm/adapters/abstract_adapter'
+
+module Redmine
+ module Scm
+ module Adapters
+ class MercurialAdapter < AbstractAdapter
+
+ # Mercurial executable name
+ HG_BIN = "hg"
+
+ def info
+ cmd = "#{HG_BIN} -R #{target('')} root"
+ root_url = nil
+ shellout(cmd) do |io|
+ root_url = io.gets
+ end
+ return nil if $? && $?.exitstatus != 0
+ info = Info.new({:root_url => root_url.chomp,
+ :lastrev => revisions(nil,nil,nil,{:limit => 1}).last
+ })
+ info
+ rescue Errno::ENOENT => e
+ return nil
+ end
+
+ def entries(path=nil, identifier=nil)
+ path ||= ''
+ entries = Entries.new
+ cmd = "#{HG_BIN} -R #{target('')} --cwd #{target(path)} locate -X */*/*"
+ cmd << " -r #{identifier.to_i}" if identifier
+ cmd << " * */*"
+ shellout(cmd) do |io|
+ io.each_line do |line|
+ e = line.chomp.split('\\')
+ entries << Entry.new({:name => e.first,
+ :path => (path.empty? ? e.first : "#{path}/#{e.first}"),
+ :kind => (e.size > 1 ? 'dir' : 'file'),
+ :lastrev => Revision.new
+ }) unless entries.detect{|entry| entry.name == e.first}
+ end
+ end
+ return nil if $? && $?.exitstatus != 0
+ entries.sort_by_name
+ rescue Errno::ENOENT => e
+ raise CommandFailed
+ end
+
+ def entry(path=nil, identifier=nil)
+ path ||= ''
+ search_path = path.split('/')[0..-2].join('/')
+ entry_name = path.split('/').last
+ e = entries(search_path, identifier)
+ e ? e.detect{|entry| entry.name == entry_name} : nil
+ end
+
+ def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
+ revisions = Revisions.new
+ cmd = "#{HG_BIN} -v -R #{target('')} log"
+ cmd << " -r #{identifier_from.to_i}:" if identifier_from
+ cmd << " --limit #{options[:limit].to_i}" if options[:limit]
+ shellout(cmd) do |io|
+ changeset = {}
+ parsing_descr = false
+ line_feeds = 0
+
+ io.each_line do |line|
+ if line =~ /^(\w+):\s*(.*)$/
+ key = $1
+ value = $2
+ if parsing_descr && line_feeds > 1
+ parsing_descr = false
+ revisions << Revision.new({:identifier => changeset[:changeset].split(':').first.to_i,
+ :scmid => changeset[:changeset].split(':').last,
+ :author => changeset[:user],
+ :time => Time.parse(changeset[:date]),
+ :message => changeset[:description],
+ :paths => changeset[:files].split.collect{|path| {:action => 'X', :path => "/#{path}"}}
+ })
+ changeset = {}
+ end
+ if !parsing_descr
+ changeset.store key.to_sym, value
+ if $1 == "description"
+ parsing_descr = true
+ line_feeds = 0
+ next
+ end
+ end
+ end
+ if parsing_descr
+ changeset[:description] << line
+ line_feeds += 1 if line.chomp.empty?
+ end
+ end
+ revisions << Revision.new({:identifier => changeset[:changeset].split(':').first.to_i,
+ :scmid => changeset[:changeset].split(':').last,
+ :author => changeset[:user],
+ :time => Time.parse(changeset[:date]),
+ :message => changeset[:description],
+ :paths => changeset[:files].split.collect{|path| {:action => 'X', :path => "/#{path}"}}
+ })
+ end
+ return nil if $? && $?.exitstatus != 0
+ revisions
+ rescue Errno::ENOENT => e
+ raise CommandFailed
+ end
+
+ def diff(path, identifier_from, identifier_to=nil, type="inline")
+ path ||= ''
+ if identifier_to
+ identifier_to = identifier_to.to_i
+ else
+ identifier_to = identifier_from.to_i - 1
+ end
+ cmd = "#{HG_BIN} -R #{target('')} diff -r #{identifier_to} -r #{identifier_from} --nodates"
+ cmd << " -I #{target(path)}" unless path.empty?
+ diff = []
+ shellout(cmd) do |io|
+ io.each_line do |line|
+ diff << line
+ end
+ end
+ return nil if $? && $?.exitstatus != 0
+ DiffTableList.new diff, type
+
+ rescue Errno::ENOENT => e
+ raise CommandFailed
+ end
+
+ def cat(path, identifier=nil)
+ cmd = "#{HG_BIN} -R #{target('')} cat #{target(path)}"
+ cat = nil
+ shellout(cmd) do |io|
+ io.binmode
+ cat = io.read
+ end
+ return nil if $? && $?.exitstatus != 0
+ cat
+ rescue Errno::ENOENT => e
+ raise CommandFailed
+ end
+ end
+ end
+ end
+end
diff --git a/lib/redmine/scm/adapters/subversion_adapter.rb b/lib/redmine/scm/adapters/subversion_adapter.rb
new file mode 100644
index 000000000..f58bdb13d
--- /dev/null
+++ b/lib/redmine/scm/adapters/subversion_adapter.rb
@@ -0,0 +1,173 @@
+# redMine - project management software
+# Copyright (C) 2006-2007 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.
+
+require 'redmine/scm/adapters/abstract_adapter'
+require 'rexml/document'
+
+module Redmine
+ module Scm
+ module Adapters
+ class SubversionAdapter < AbstractAdapter
+
+ # SVN executable name
+ SVN_BIN = "svn"
+
+ # Get info about the svn repository
+ def info
+ cmd = "#{SVN_BIN} info --xml #{target('')}"
+ cmd << " --username #{@login} --password #{@password}" if @login
+ info = nil
+ shellout(cmd) do |io|
+ begin
+ doc = REXML::Document.new(io)
+ #root_url = doc.elements["info/entry/repository/root"].text
+ info = Info.new({:root_url => doc.elements["info/entry/repository/root"].text,
+ :lastrev => Revision.new({
+ :identifier => doc.elements["info/entry/commit"].attributes['revision'],
+ :time => Time.parse(doc.elements["info/entry/commit/date"].text),
+ :author => (doc.elements["info/entry/commit/author"] ? doc.elements["info/entry/commit/author"].text : "")
+ })
+ })
+ rescue
+ end
+ end
+ return nil if $? && $?.exitstatus != 0
+ info
+ rescue Errno::ENOENT => e
+ return nil
+ end
+
+ # Returns the entry identified by path and revision identifier
+ # or nil if entry doesn't exist in the repository
+ def entry(path=nil, identifier=nil)
+ e = entries(path, identifier)
+ e ? e.first : nil
+ end
+
+ # Returns an Entries collection
+ # or nil if the given path doesn't exist in the repository
+ def entries(path=nil, identifier=nil)
+ path ||= ''
+ identifier = 'HEAD' unless identifier and identifier > 0
+ entries = Entries.new
+ cmd = "#{SVN_BIN} list --xml #{target(path)}@#{identifier}"
+ cmd << " --username #{@login} --password #{@password}" if @login
+ shellout(cmd) do |io|
+ begin
+ doc = REXML::Document.new(io)
+ doc.elements.each("lists/list/entry") do |entry|
+ entries << Entry.new({:name => entry.elements['name'].text,
+ :path => ((path.empty? ? "" : "#{path}/") + entry.elements['name'].text),
+ :kind => entry.attributes['kind'],
+ :size => (entry.elements['size'] and entry.elements['size'].text).to_i,
+ :lastrev => Revision.new({
+ :identifier => entry.elements['commit'].attributes['revision'],
+ :time => Time.parse(entry.elements['commit'].elements['date'].text),
+ :author => (entry.elements['commit'].elements['author'] ? entry.elements['commit'].elements['author'].text : "")
+ })
+ })
+ end
+ rescue
+ end
+ end
+ return nil if $? && $?.exitstatus != 0
+ entries.sort_by_name
+ rescue Errno::ENOENT => e
+ raise CommandFailed
+ end
+
+ def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
+ path ||= ''
+ identifier_from = 'HEAD' unless identifier_from and identifier_from.to_i > 0
+ identifier_to = 1 unless identifier_to and identifier_to.to_i > 0
+ revisions = Revisions.new
+ cmd = "#{SVN_BIN} log --xml -r #{identifier_from}:#{identifier_to}"
+ cmd << " --username #{@login} --password #{@password}" if @login
+ cmd << " --verbose " if options[:with_paths]
+ cmd << target(path)
+ shellout(cmd) do |io|
+ begin
+ doc = REXML::Document.new(io)
+ doc.elements.each("log/logentry") do |logentry|
+ paths = []
+ logentry.elements.each("paths/path") do |path|
+ paths << {:action => path.attributes['action'],
+ :path => path.text,
+ :from_path => path.attributes['copyfrom-path'],
+ :from_revision => path.attributes['copyfrom-rev']
+ }
+ end
+ paths.sort! { |x,y| x[:path] <=> y[:path] }
+
+ revisions << Revision.new({:identifier => logentry.attributes['revision'],
+ :author => (logentry.elements['author'] ? logentry.elements['author'].text : ""),
+ :time => Time.parse(logentry.elements['date'].text),
+ :message => logentry.elements['msg'].text,
+ :paths => paths
+ })
+ end
+ rescue
+ end
+ end
+ return nil if $? && $?.exitstatus != 0
+ revisions
+ rescue Errno::ENOENT => e
+ raise CommandFailed
+ end
+
+ def diff(path, identifier_from, identifier_to=nil, type="inline")
+ path ||= ''
+ if identifier_to and identifier_to.to_i > 0
+ identifier_to = identifier_to.to_i
+ else
+ identifier_to = identifier_from.to_i - 1
+ end
+ cmd = "#{SVN_BIN} diff -r "
+ cmd << "#{identifier_to}:"
+ cmd << "#{identifier_from}"
+ cmd << "#{target(path)}@#{identifier_from}"
+ cmd << " --username #{@login} --password #{@password}" if @login
+ diff = []
+ shellout(cmd) do |io|
+ io.each_line do |line|
+ diff << line
+ end
+ end
+ return nil if $? && $?.exitstatus != 0
+ DiffTableList.new diff, type
+ rescue Errno::ENOENT => e
+ raise CommandFailed
+ end
+
+ def cat(path, identifier=nil)
+ identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
+ cmd = "#{SVN_BIN} cat #{target(path)}@#{identifier}"
+ cmd << " --username #{@login} --password #{@password}" if @login
+ cat = nil
+ shellout(cmd) do |io|
+ io.binmode
+ cat = io.read
+ end
+ return nil if $? && $?.exitstatus != 0
+ cat
+ rescue Errno::ENOENT => e
+ raise CommandFailed
+ end
+ end
+ end
+ end
+end