Merged trunk r1773.

git-svn-id: http://redmine.rubyforge.org/svn/branches/work@1774 e93f8b46-1217-0410-a6f0-8f06a7374b81
This commit is contained in:
Jean-Philippe Lang
2008-08-30 14:41:07 +00:00
parent ab83ed5d8e
commit a377069fb2
398 changed files with 14736 additions and 5683 deletions
+1 -1
View File
@@ -829,7 +829,7 @@ module SVG
@doc << DocType.new( %q{svg PUBLIC "-//W3C//DTD SVG 1.0//EN" } +
%q{"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"} )
if style_sheet && style_sheet != ''
@doc << ProcessingInstruction.new( "xml-stylesheet",
@doc << Instruction.new( "xml-stylesheet",
%Q{href="#{style_sheet}" type="text/css"} )
end
@root = @doc.add_element( "svg", {
+37 -19
View File
@@ -299,6 +299,8 @@ class RedCloth < String
hard_break text
unless @lite_mode
refs text
# need to do this before text is split by #blocks
block_textile_quotes text
blocks text
end
inline text
@@ -376,13 +378,13 @@ class RedCloth < String
re =
case rtype
when :limit
/(^|[>\s])
/(^|[>\s\(])
(#{rcq})
(#{C})
(?::(\S+?))?
([^\s\-].*?[^\s\-]|\w)
#{rcq}
(?=[[:punct:]]|\s|$)/x
(?=[[:punct:]]|\s|\)|$)/x
else
/(#{rcq})
(#{C})
@@ -502,26 +504,19 @@ class RedCloth < String
tatts = shelve( tatts ) if tatts
rows = []
fullrow.
split( /\|$/m ).
delete_if { |x| x.empty? }.
each do |row|
fullrow.each_line do |row|
ratts, row = pba( $1, 'tr' ), $2 if row =~ /^(#{A}#{C}\. )(.*)/m
cells = []
#row.split( /\(?!\[\[[^\]])|(?![^\[]\]\])/ ).each do |cell|
row.split( /\|(?![^\[\|]*\]\])/ ).each do |cell|
row.split( /(\|)(?![^\[\|]*\]\])/ )[1..-2].each do |cell|
next if cell == '|'
ctyp = 'd'
ctyp = 'h' if cell =~ /^_/
catts = ''
catts, cell = pba( $1, 'td' ), $2 if cell =~ /^(_?#{S}#{A}#{C}\. ?)(.*)/
unless cell.strip.empty?
catts = shelve( catts ) if catts
cells << "\t\t\t<t#{ ctyp }#{ catts }>#{ cell }</t#{ ctyp }>"
end
catts = shelve( catts ) if catts
cells << "\t\t\t<t#{ ctyp }#{ catts }>#{ cell }</t#{ ctyp }>"
end
ratts = shelve( ratts ) if ratts
rows << "\t\t<tr#{ ratts }>\n#{ cells.join( "\n" ) }\n\t\t</tr>"
@@ -576,6 +571,29 @@ class RedCloth < String
lines.join( "\n" )
end
end
QUOTES_RE = /(^>+([^\n]*?)\n?)+/m
QUOTES_CONTENT_RE = /^([> ]+)(.*)$/m
def block_textile_quotes( text )
text.gsub!( QUOTES_RE ) do |match|
lines = match.split( /\n/ )
quotes = ''
indent = 0
lines.each do |line|
line =~ QUOTES_CONTENT_RE
bq,content = $1, $2
l = bq.count('>')
if l != indent
quotes << ("\n\n" + (l>indent ? '<blockquote>' * (l-indent) : '</blockquote>' * (indent-l)) + "\n\n")
indent = l
end
quotes << (content + "\n")
end
quotes << ("\n" + '</blockquote>' * indent + "\n\n")
quotes
end
end
CODE_RE = /(\W)
@
@@ -726,7 +744,7 @@ class RedCloth < String
end
MARKDOWN_RULE_RE = /^(#{
['*', '-', '_'].collect { |ch| '( ?' + Regexp::quote( ch ) + ' ?){3,}' }.join( '|' )
['*', '-', '_'].collect { |ch| ' ?(' + Regexp::quote( ch ) + ' ?){3,}' }.join( '|' )
})$/
def block_markdown_rule( text )
@@ -764,11 +782,11 @@ class RedCloth < String
([\s\[{(]|[#{PUNCT}])? # $pre
" # start
(#{C}) # $atts
([^"]+?) # $text
([^"\n]+?) # $text
\s?
(?:\(([^)]+?)\)(?="))? # $title
":
(\S+?) # $url
([\w\/]\S+?) # $url
(\/)? # $slash
([^\w\/;]*?) # $post
(?=<|\s|$)
@@ -1131,10 +1149,10 @@ class RedCloth < String
end
end
ALLOWED_TAGS = %w(redpre pre code)
ALLOWED_TAGS = %w(redpre pre code notextile)
def escape_html_tags(text)
text.gsub!(%r{<(\/?(\w+)[^>\n]*)(>?)}) {|m| ALLOWED_TAGS.include?($2) ? "<#{$1}#{$3}" : "&lt;#{$1}#{'&gt;' if $3}" }
text.gsub!(%r{<(\/?([!\w]+)[^<>\n]*)(>?)}) {|m| ALLOWED_TAGS.include?($2) ? "<#{$1}#{$3}" : "&lt;#{$1}#{'&gt;' unless $3.blank?}" }
end
end
+25 -10
View File
@@ -1,5 +1,6 @@
require 'redmine/access_control'
require 'redmine/menu_manager'
require 'redmine/activity'
require 'redmine/mime_type'
require 'redmine/core_ext'
require 'redmine/themes'
@@ -11,7 +12,7 @@ rescue LoadError
# RMagick is not available
end
REDMINE_SUPPORTED_SCM = %w( Subversion Darcs Mercurial Cvs Bazaar Git )
REDMINE_SUPPORTED_SCM = %w( Subversion Darcs Mercurial Cvs Bazaar Git Filesystem )
# Permissions
Redmine::AccessControl.map do |map|
@@ -32,9 +33,9 @@ Redmine::AccessControl.map do |map|
:queries => :index,
:reports => :issue_report}, :public => true
map.permission :add_issues, {:issues => :new}
map.permission :edit_issues, {:issues => [:edit, :bulk_edit, :destroy_attachment]}
map.permission :edit_issues, {:issues => [:edit, :reply, :bulk_edit, :destroy_attachment]}
map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy]}
map.permission :add_issue_notes, {:issues => :edit}
map.permission :add_issue_notes, {:issues => [:edit, :reply]}
map.permission :edit_issue_notes, {:journals => :edit}, :require => :loggedin
map.permission :edit_own_issue_notes, {:journals => :edit}, :require => :loggedin
map.permission :move_issues, {:issues => :move}, :require => :loggedin
@@ -45,6 +46,9 @@ Redmine::AccessControl.map do |map|
# Gantt & calendar
map.permission :view_gantt, :projects => :gantt
map.permission :view_calendar, :projects => :calendar
# Watchers
map.permission :view_issue_watchers, {}
map.permission :add_issue_watchers, {:watchers => :new}
end
map.project_module :time_tracking do |map|
@@ -76,6 +80,7 @@ Redmine::AccessControl.map do |map|
map.permission :delete_wiki_pages, {:wiki => :destroy}, :require => :member
map.permission :view_wiki_pages, :wiki => [:index, :history, :diff, :annotate, :special]
map.permission :edit_wiki_pages, :wiki => [:edit, :preview, :add_attachment, :destroy_attachment]
map.permission :protect_wiki_pages, {:wiki => :protect}, :require => :member
end
map.project_module :repository do |map|
@@ -87,25 +92,25 @@ Redmine::AccessControl.map do |map|
map.project_module :boards do |map|
map.permission :manage_boards, {:boards => [:new, :edit, :destroy]}, :require => :member
map.permission :view_messages, {:boards => [:index, :show], :messages => [:show]}, :public => true
map.permission :add_messages, {:messages => [:new, :reply]}
map.permission :add_messages, {:messages => [:new, :reply, :quote]}
map.permission :edit_messages, {:messages => :edit}, :require => :member
map.permission :delete_messages, {:messages => :destroy}, :require => :member
end
end
Redmine::MenuManager.map :top_menu do |menu|
menu.push :home, :home_url, :html => { :class => 'home' }
menu.push :home, :home_path, :html => { :class => 'home' }
menu.push :my_page, { :controller => 'my', :action => 'page' }, :html => { :class => 'mypage' }, :if => Proc.new { User.current.logged? }
menu.push :projects, { :controller => 'projects', :action => 'index' }, :caption => :label_project_plural, :html => { :class => 'projects' }
menu.push :administration, { :controller => 'admin', :action => 'index' }, :html => { :class => 'admin' }, :if => Proc.new { User.current.admin? }
menu.push :help, Redmine::Info.help_url, :html => { :class => 'help' }
menu.push :administration, { :controller => 'admin', :action => 'index' }, :html => { :class => 'admin' }, :if => Proc.new { User.current.admin? }, :last => true
menu.push :help, Redmine::Info.help_url, :html => { :class => 'help' }, :last => true
end
Redmine::MenuManager.map :account_menu do |menu|
menu.push :login, :signin_url, :html => { :class => 'login' }, :if => Proc.new { !User.current.logged? }
menu.push :login, :signin_path, :html => { :class => 'login' }, :if => Proc.new { !User.current.logged? }
menu.push :register, { :controller => 'account', :action => 'register' }, :html => { :class => 'register' }, :if => Proc.new { !User.current.logged? && Setting.self_registration? }
menu.push :my_account, { :controller => 'my', :action => 'account' }, :html => { :class => 'myaccount' }, :if => Proc.new { User.current.logged? }
menu.push :logout, :signout_url, :html => { :class => 'logout' }, :if => Proc.new { User.current.logged? }
menu.push :logout, :signout_path, :html => { :class => 'logout' }, :if => Proc.new { User.current.logged? }
end
Redmine::MenuManager.map :application_menu do |menu|
@@ -129,5 +134,15 @@ Redmine::MenuManager.map :project_menu do |menu|
menu.push :files, { :controller => 'projects', :action => 'list_files' }, :caption => :label_attachment_plural
menu.push :repository, { :controller => 'repositories', :action => 'show' },
:if => Proc.new { |p| p.repository && !p.repository.new_record? }
menu.push :settings, { :controller => 'projects', :action => 'settings' }
menu.push :settings, { :controller => 'projects', :action => 'settings' }, :last => true
end
Redmine::Activity.map do |activity|
activity.register :issues, :class_name => %w(Issue Journal)
activity.register :changesets
activity.register :news
activity.register :documents, :class_name => %w(Document Attachment)
activity.register :files, :class_name => 'Attachment'
activity.register :wiki_pages, :class_name => 'WikiContent::Version', :default => false
activity.register :messages, :default => false
end
+46
View File
@@ -0,0 +1,46 @@
# Redmine - project management software
# Copyright (C) 2006-2008 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 Activity
mattr_accessor :available_event_types, :default_event_types, :providers
@@available_event_types = []
@@default_event_types = []
@@providers = Hash.new {|h,k| h[k]=[] }
class << self
def map(&block)
yield self
end
# Registers an activity provider
def register(event_type, options={})
options.assert_valid_keys(:class_name, :default)
event_type = event_type.to_s
providers = options[:class_name] || event_type.classify
providers = ([] << providers) unless providers.is_a?(Array)
@@available_event_types << event_type unless @@available_event_types.include?(event_type)
@@default_event_types << event_type unless options[:default] == false
@@providers[event_type] += providers
end
end
end
end
+79
View File
@@ -0,0 +1,79 @@
# Redmine - project management software
# Copyright (C) 2006-2008 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 Activity
# Class used to retrieve activity events
class Fetcher
attr_reader :user, :project, :scope
# Needs to be unloaded in development mode
@@constantized_providers = Hash.new {|h,k| h[k] = Redmine::Activity.providers[k].collect {|t| t.constantize } }
def initialize(user, options={})
options.assert_valid_keys(:project, :with_subprojects)
@user = user
@project = options[:project]
@options = options
@scope = event_types
end
# Returns an array of available event types
def event_types
return @event_types unless @event_types.nil?
@event_types = Redmine::Activity.available_event_types
@event_types = @event_types.select {|o| @user.allowed_to?("view_#{o}".to_sym, @project)} if @project
@event_types
end
# Yields to filter the activity scope
def scope_select(&block)
@scope = @scope.select {|t| yield t }
end
# Sets the scope
def scope=(s)
@scope = s & event_types
end
# Resets the scope to the default scope
def default_scope!
@scope = Redmine::Activity.default_event_types
end
# Returns an array of events for the given date range
def events(from, to)
e = []
@scope.each do |event_type|
constantized_providers(event_type).each do |provider|
e += provider.find_events(event_type, @user, from, to, @options)
end
end
e
end
private
def constantized_providers(event_type)
@@constantized_providers[event_type]
end
end
end
end
@@ -32,7 +32,7 @@ module Redmine #:nodoc:
end
# 2,5 => 2.5
s.gsub!(',', '.')
s.to_f
begin; Kernel.Float(s); rescue; nil; end
end
end
end
+51
View File
@@ -0,0 +1,51 @@
# redMine - project management software
# Copyright (C) 2006-2008 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 'net/imap'
module Redmine
module IMAP
class << self
def check(imap_options={}, options={})
host = imap_options[:host] || '127.0.0.1'
port = imap_options[:port] || '143'
ssl = !imap_options[:ssl].nil?
folder = imap_options[:folder] || 'INBOX'
imap = Net::IMAP.new(host, port, ssl)
imap.login(imap_options[:username], imap_options[:password]) unless imap_options[:username].nil?
imap.select(folder)
imap.search(['NOT', 'SEEN']).each do |message_id|
msg = imap.fetch(message_id,'RFC822')[0].attr['RFC822']
logger.debug "Receiving message #{message_id}" if logger && logger.debug?
if MailHandler.receive(msg, options)
imap.store(message_id, "+FLAGS", [:Seen, :Deleted])
else
imap.store(message_id, "+FLAGS", [:Seen])
end
end
imap.expunge
end
private
def logger
RAILS_DEFAULT_LOGGER
end
end
end
end
+53 -19
View File
@@ -80,9 +80,10 @@ module Redmine
else
item.url
end
#url = (project && item.url.is_a?(Hash)) ? {item.param => project}.merge(item.url) : (item.url.is_a?(Symbol) ? send(item.url) : item.url)
caption = item.caption(project)
caption = l(caption) if caption.is_a?(Symbol)
links << content_tag('li',
link_to(l(item.caption), url, (current_menu_item == item.name ? item.html_options.merge(:class => 'selected') : item.html_options)))
link_to(h(caption), url, (current_menu_item == item.name ? item.html_options.merge(:class => 'selected') : item.html_options)))
end
end
links.empty? ? nil : content_tag('ul', links.join("\n"))
@@ -91,11 +92,9 @@ module Redmine
class << self
def map(menu_name)
mapper = Mapper.new
yield mapper
@items ||= {}
@items[menu_name.to_sym] ||= []
@items[menu_name.to_sym] += mapper.items
mapper = Mapper.new(menu_name.to_sym, @items)
yield mapper
end
def items(menu_name)
@@ -108,17 +107,46 @@ module Redmine
end
class Mapper
# Adds an item at the end of the menu. Available options:
# * param: the parameter name that is used for the project id (default is :id)
# * if: a proc that is called before rendering the item, the item is displayed only if it returns true
# * caption: the localized string key that is used as the item label
# * html_options: a hash of html options that are passed to link_to
def push(name, url, options={})
items << MenuItem.new(name, url, options)
def initialize(menu, items)
items[menu] ||= []
@menu = menu
@menu_items = items[menu]
end
def items
@items ||= []
@@last_items_count = Hash.new {|h,k| h[k] = 0}
# Adds an item at the end of the menu. Available options:
# * param: the parameter name that is used for the project id (default is :id)
# * if: a Proc that is called before rendering the item, the item is displayed only if it returns true
# * caption that can be:
# * a localized string Symbol
# * a String
# * a Proc that can take the project as argument
# * before, after: specify where the menu item should be inserted (eg. :after => :activity)
# * last: menu item will stay at the end (eg. :last => true)
# * html_options: a hash of html options that are passed to link_to
def push(name, url, options={})
options = options.dup
# menu item position
if before = options.delete(:before)
position = @menu_items.collect(&:name).index(before)
elsif after = options.delete(:after)
position = @menu_items.collect(&:name).index(after)
position += 1 unless position.nil?
elsif options.delete(:last)
position = @menu_items.size
@@last_items_count[@menu] += 1
end
# default position
position ||= @menu_items.size - @@last_items_count[@menu]
@menu_items.insert(position, MenuItem.new(name, url, options))
end
# Removes a menu item
def delete(name)
@menu_items.delete_if {|i| i.name == name}
end
end
@@ -133,13 +161,19 @@ module Redmine
@url = url
@condition = options[:if]
@param = options[:param] || :id
@caption_key = options[:caption]
@caption = options[:caption]
@html_options = options[:html] || {}
end
def caption
# check if localized string exists on first render (after GLoc strings are loaded)
@caption ||= (@caption_key || (l_has_string?("label_#{@name}".to_sym) ? "label_#{@name}".to_sym : @name.to_s.humanize))
def caption(project=nil)
if @caption.is_a?(Proc)
c = @caption.call(project).to_s
c = @name.to_s.humanize if c.blank?
c
else
# check if localized string exists on first render (after GLoc strings are loaded)
@caption_key ||= (@caption || (l_has_string?("label_#{@name}".to_sym) ? "label_#{@name}".to_sym : @name.to_s.humanize))
end
end
end
end
+26
View File
@@ -0,0 +1,26 @@
# Redmine - project management software
# Copyright (C) 2006-2008 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 Platform
class << self
def mswin?
(RUBY_PLATFORM =~ /(:?mswin|mingw)/) || (RUBY_PLATFORM == 'java' && (ENV['OS'] || ENV['os']) =~ /windows/i)
end
end
end
end
+26
View File
@@ -116,6 +116,32 @@ module Redmine #:nodoc:
self.instance_eval(&block)
@project_module = nil
end
# Registers an activity provider.
#
# Options:
# * <tt>:class_name</tt> - one or more model(s) that provide these events (inferred from event_type by default)
# * <tt>:default</tt> - setting this option to false will make the events not displayed by default
#
# A model can provide several activity event types.
#
# Examples:
# register :news
# register :scrums, :class_name => 'Meeting'
# register :issues, :class_name => ['Issue', 'Journal']
#
# Retrieving events:
# Associated model(s) must implement the find_events class method.
# ActiveRecord models can use acts_as_activity_provider as a way to implement this class method.
#
# The following call should return all the scrum events visible by current user that occured in the 5 last days:
# Meeting.find_events('scrums', User.current, 5.days.ago, Date.today)
# Meeting.find_events('scrums', User.current, 5.days.ago, Date.today, :project => foo) # events for project foo only
#
# Note that :view_scrums permission is required to view these events in the activity view.
def activity_provider(*args)
Redmine::Activity.register(*args)
end
# Returns +true+ if the plugin can be configured.
def configurable?
@@ -24,6 +24,29 @@ module Redmine
end
class AbstractAdapter #:nodoc:
class << self
# Returns the version of the scm client
# Eg: [1, 5, 0] or [] if unknown
def client_version
[]
end
# Returns the version string of the scm client
# Eg: '1.5.0' or 'Unknown version' if unknown
def client_version_string
v = client_version || 'Unknown version'
v.is_a?(Array) ? v.join('.') : v.to_s
end
# Returns true if the current client version is above
# or equals the given one
# If option is :unknown is set to true, it will return
# true if the client version is unknown
def client_version_above?(v, options={})
((client_version <=> v) >= 0) || (client_version.empty? && options[:unknown])
end
end
def initialize(url, root_url=nil, login=nil, password=nil)
@url = url
@login = login if login && !login.empty?
@@ -77,12 +100,16 @@ module Redmine
def entries(path=nil, identifier=nil)
return nil
end
def properties(path, 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")
def diff(path, identifier_from, identifier_to=nil)
return nil
end
@@ -94,15 +121,30 @@ module Redmine
path ||= ''
(path[0,1]!="/") ? "/#{path}" : path
end
def with_trailling_slash(path)
path ||= ''
(path[-1,1] == "/") ? path : "#{path}/"
end
def without_leading_slash(path)
path ||= ''
path.gsub(%r{^/+}, '')
end
def without_trailling_slash(path)
path ||= ''
(path[-1,1] == "/") ? path[0..-2] : path
end
def shell_quote(str)
if RUBY_PLATFORM =~ /mswin/
if Redmine::Platform.mswin?
'"' + str.gsub(/"/, '\\"') + '"'
else
"'" + str.gsub(/'/, "'\"'\"'") + "'"
end
end
private
def retrieve_root_url
info = self.info
@@ -116,10 +158,18 @@ module Redmine
end
def logger
self.class.logger
end
def shellout(cmd, &block)
self.class.shellout(cmd, &block)
end
def self.logger
RAILS_DEFAULT_LOGGER
end
def shellout(cmd, &block)
def self.shellout(cmd, &block)
logger.debug "Shelling out: #{cmd}" if logger && logger.debug?
begin
IO.popen(cmd, "r+") do |io|
@@ -127,11 +177,22 @@ module Redmine
block.call(io) if block_given?
end
rescue Errno::ENOENT => e
msg = strip_credential(e.message)
# The command failed, log it and re-raise
logger.error("SCM command failed: #{cmd}\n with: #{e.message}")
raise CommandFailed.new(e.message)
logger.error("SCM command failed, make sure that your SCM binary (eg. svn) is in PATH (#{ENV['PATH']}): #{strip_credential(cmd)}\n with: #{msg}")
raise CommandFailed.new(msg)
end
end
# Hides username/password in a given command
def self.strip_credential(cmd)
q = (Redmine::Platform.mswin? ? '"' : "'")
cmd.to_s.gsub(/(\-\-(password|username))\s+(#{q}[^#{q}]+#{q}|[^#{q}]\S+)/, '\\1 xxxx')
end
def strip_credential(cmd)
self.class.strip_credential(cmd)
end
end
class Entries < Array
@@ -208,167 +269,7 @@ module Redmine
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 =~ /^(---|\+\+\+) (.*)$/
self << diff_table if diff_table.length > 1
diff_table = DiffTable.new type
end
a = diff_table.add_line line
end
self << diff_table unless diff_table.empty?
self
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 =~ /^(---|\+\+\+) (.*)$/
@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)
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
class Annotate
attr_reader :lines, :revisions
@@ -132,7 +132,7 @@ module Redmine
revisions
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 = identifier_to.to_i
@@ -147,7 +147,7 @@ module Redmine
end
end
#return nil if $? && $?.exitstatus != 0
DiffTableList.new diff, type
diff
end
def cat(path, identifier=nil)
@@ -65,7 +65,7 @@ module Redmine
entries = Entries.new
cmd = "#{CVS_BIN} -d #{root_url} rls -ed"
cmd << " -D \"#{time_to_cvstime(identifier)}\"" if identifier
cmd << " #{path_with_project}"
cmd << " #{shell_quote path_with_project}"
shellout(cmd) do |io|
io.each_line(){|line|
fields=line.chop.split('/',-1)
@@ -110,7 +110,7 @@ module Redmine
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}"
cmd << " #{shell_quote path_with_project}"
shellout(cmd) do |io|
state="entry_start"
@@ -227,10 +227,10 @@ module Redmine
end
end
def diff(path, identifier_from, identifier_to=nil, type="inline")
def diff(path, identifier_from, identifier_to=nil)
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}"
cmd = "#{CVS_BIN} -d #{root_url} rdiff -u -r#{identifier_to} -r#{identifier_from} #{shell_quote path_with_project}"
diff = []
shellout(cmd) do |io|
io.each_line do |line|
@@ -238,14 +238,16 @@ module Redmine
end
end
return nil if $? && $?.exitstatus != 0
DiffTableList.new diff, type
diff
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}"
cmd = "#{CVS_BIN} -d #{root_url} co"
cmd << " -D \"#{time_to_cvstime(identifier)}\"" if identifier
cmd << " -p #{shell_quote path_with_project}"
cat = nil
shellout(cmd) do |io|
cat = io.read
@@ -258,7 +260,7 @@ module Redmine
identifier = (identifier) ? identifier : "HEAD"
logger.debug "<cvs> annotate path:'#{path}',identifier #{identifier}"
path_with_project="#{url}#{with_leading_slash(path)}"
cmd = "#{CVS_BIN} -d #{root_url} rannotate -r#{identifier} #{path_with_project}"
cmd = "#{CVS_BIN} -d #{root_url} rannotate -r#{identifier} #{shell_quote path_with_project}"
blame = Annotate.new
shellout(cmd) do |io|
io.each_line do |line|
@@ -25,16 +25,36 @@ module Redmine
# Darcs executable name
DARCS_BIN = "darcs"
class << self
def client_version
@@client_version ||= (darcs_binary_version || [])
end
def darcs_binary_version
cmd = "#{DARCS_BIN} --version"
version = nil
shellout(cmd) do |io|
# Read darcs version in first returned line
if m = io.gets.match(%r{((\d+\.)+\d+)})
version = m[0].scan(%r{\d+}).collect(&:to_i)
end
end
return nil if $? && $?.exitstatus != 0
version
end
end
def initialize(url, root_url=nil, login=nil, password=nil)
@url = url
@root_url = url
end
def supports_cat?
false
# cat supported in darcs 2.0.0 and higher
self.class.client_version_above?([2, 0, 0])
end
# Get info about the svn repository
# Get info about the darcs repository
def info
rev = revisions(nil,nil,nil,{:limit => 1})
rev ? Info.new({:root_url => @url, :lastrev => rev.last}) : nil
@@ -94,7 +114,7 @@ module Redmine
revisions
end
def diff(path, identifier_from, identifier_to=nil, type="inline")
def diff(path, identifier_from, identifier_to=nil)
path = '*' if path.blank?
cmd = "#{DARCS_BIN} diff --repodir #{@url}"
if identifier_to.nil?
@@ -111,9 +131,22 @@ module Redmine
end
end
return nil if $? && $?.exitstatus != 0
DiffTableList.new diff, type
diff
end
def cat(path, identifier=nil)
cmd = "#{DARCS_BIN} show content --repodir #{@url}"
cmd << " --match \"hash #{identifier}\"" if identifier
cmd << " #{shell_quote path}"
cat = nil
shellout(cmd) do |io|
io.binmode
cat = io.read
end
return nil if $? && $?.exitstatus != 0
cat
end
private
def entry_from_xml(element, path_prefix)
@@ -0,0 +1,93 @@
# redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang
#
# FileSystem adapter
# File written by Paul Rivier, at Demotera.
#
# 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 'find'
module Redmine
module Scm
module Adapters
class FilesystemAdapter < AbstractAdapter
def initialize(url, root_url=nil, login=nil, password=nil)
@url = with_trailling_slash(url)
end
def format_path_ends(path, leading=true, trailling=true)
path = leading ? with_leading_slash(path) :
without_leading_slash(path)
trailling ? with_trailling_slash(path) :
without_trailling_slash(path)
end
def info
info = Info.new({:root_url => target(),
:lastrev => nil
})
info
rescue CommandFailed
return nil
end
def entries(path="", identifier=nil)
entries = Entries.new
Dir.new(target(path)).each do |e|
relative_path = format_path_ends((format_path_ends(path,
false,
true) + e),
false,false)
target = target(relative_path)
entries <<
Entry.new({ :name => File.basename(e),
# below : list unreadable files, but dont link them.
:path => File.readable?(target) ? relative_path : "",
:kind => (File.directory?(target) ? 'dir' : 'file'),
:size => (File.directory?(target) ? nil : [File.size(target)].pack('l').unpack('L').first),
:lastrev =>
Revision.new({:time => (File.mtime(target)).localtime,
})
}) if File.exist?(target) and # paranoid test
%w{file directory}.include?(File.ftype(target)) and # avoid special types
not File.basename(e).match(/^\.+$/) # avoid . and ..
end
entries.sort_by_name
end
def cat(path, identifier=nil)
File.new(target(path), "rb").read
end
private
# AbstractAdapter::target is implicitly made to quote paths.
# Here we do not shell-out, so we do not want quotes.
def target(path=nil)
#Prevent the use of ..
if path and !path.match(/(^|\/)\.\.(\/|$)/)
return "#{self.url}#{without_leading_slash(path)}"
end
return self.url
end
end
end
end
end
+31 -16
View File
@@ -27,9 +27,13 @@ module Redmine
# 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?
if rev != 'latest' && !rev.nil?
cmd="#{GIT_BIN} --git-dir #{target('')} show #{shell_quote rev} -- #{shell_quote path}"
else
branch = shellout("#{GIT_BIN} --git-dir #{target('')} branch") { |io| io.grep(/\*/)[0].strip.match(/\* (.*)/)[1] }
cmd="#{GIT_BIN} --git-dir #{target('')} log -1 #{branch} -- #{shell_quote path}"
end
rev=[]
i=0
shellout(cmd) do |io|
@@ -135,10 +139,10 @@ module Redmine
def revisions(path, identifier_from, identifier_to, options={})
revisions = Revisions.new
cmd = "#{GIT_BIN} --git-dir #{target('')} log --raw "
cmd << " --reverse" if options[:reverse]
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|
files=[]
changeset = {}
@@ -151,13 +155,18 @@ module Redmine
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
revisions << revision
end
changeset = {}
files = []
revno = revno + 1
@@ -186,21 +195,27 @@ module Redmine
end
end
revisions << Revision.new({:identifier => changeset[:commit],
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 changeset[:commit]
})
if block_given?
yield revision
else
revisions << revision
end
end
end
return nil if $? && $?.exitstatus != 0
revisions
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
@@ -216,7 +231,7 @@ module Redmine
end
end
return nil if $? && $?.exitstatus != 0
DiffTableList.new diff, type
diff
end
def annotate(path, identifier=nil)
@@ -0,0 +1,12 @@
changeset = 'This template must be used with --debug option\n'
changeset_quiet = 'This template must be used with --debug option\n'
changeset_verbose = 'This template must be used with --debug option\n'
changeset_debug = '<logentry revision="{rev}" node="{node|short}">\n<author>{author|escape}</author>\n<date>{date|isodate}</date>\n<paths>\n{files}{file_adds}{file_dels}{file_copies}</paths>\n<msg>{desc|escape}</msg>\n{tags}</logentry>\n\n'
file = '<path action="M">{file|escape}</path>\n'
file_add = '<path action="A">{file_add|escape}</path>\n'
file_del = '<path action="D">{file_del|escape}</path>\n'
file_copy = '<path-copied copyfrom-path="{source|escape}">{name|urlescape}</path-copied>\n'
tag = '<tag>{tag|escape}</tag>\n'
header='<?xml version="1.0" encoding="UTF-8" ?>\n<log>\n\n'
# footer="</log>"
@@ -0,0 +1,12 @@
changeset = 'This template must be used with --debug option\n'
changeset_quiet = 'This template must be used with --debug option\n'
changeset_verbose = 'This template must be used with --debug option\n'
changeset_debug = '<logentry revision="{rev}" node="{node|short}">\n<author>{author|escape}</author>\n<date>{date|isodate}</date>\n<paths>\n{file_mods}{file_adds}{file_dels}{file_copies}</paths>\n<msg>{desc|escape}</msg>\n{tags}</logentry>\n\n'
file_mod = '<path action="M">{file_mod|escape}</path>\n'
file_add = '<path action="A">{file_add|escape}</path>\n'
file_del = '<path action="D">{file_del|escape}</path>\n'
file_copy = '<path-copied copyfrom-path="{source|escape}">{name|urlescape}</path-copied>\n'
tag = '<tag>{tag|escape}</tag>\n'
header='<?xml version="1.0" encoding="UTF-8" ?>\n<log>\n\n'
# footer="</log>"
@@ -21,9 +21,45 @@ module Redmine
module Scm
module Adapters
class MercurialAdapter < AbstractAdapter
# Mercurial executable name
HG_BIN = "hg"
TEMPLATES_DIR = File.dirname(__FILE__) + "/mercurial"
TEMPLATE_NAME = "hg-template"
TEMPLATE_EXTENSION = "tmpl"
class << self
def client_version
@@client_version ||= (hgversion || [])
end
def hgversion
# The hg version is expressed either as a
# release number (eg 0.9.5 or 1.0) or as a revision
# id composed of 12 hexa characters.
theversion = hgversion_from_command_line
if theversion.match(/^\d+(\.\d+)+/)
theversion.split(".").collect(&:to_i)
end
end
def hgversion_from_command_line
%x{#{HG_BIN} --version}.match(/\(version (.*)\)/)[1]
end
def template_path
@@template_path ||= template_path_for(client_version)
end
def template_path_for(version)
if ((version <=> [0,9,5]) > 0) || version.empty?
ver = "1.0"
else
ver = "0.9.5"
end
"#{TEMPLATES_DIR}/#{TEMPLATE_NAME}-#{ver}.#{TEMPLATE_EXTENSION}"
end
end
def info
cmd = "#{HG_BIN} -R #{target('')} root"
@@ -33,8 +69,8 @@ module Redmine
end
return nil if $? && $?.exitstatus != 0
info = Info.new({:root_url => root_url.chomp,
:lastrev => revisions(nil,nil,nil,{:limit => 1}).last
})
:lastrev => revisions(nil,nil,nil,{:limit => 1}).last
})
info
rescue CommandFailed
return nil
@@ -43,68 +79,78 @@ module Redmine
def entries(path=nil, identifier=nil)
path ||= ''
entries = Entries.new
cmd = "#{HG_BIN} -R #{target('')} --cwd #{target(path)} locate"
cmd << " -r #{identifier.to_i}" if identifier
cmd << " " + shell_quote('glob:**')
cmd = "#{HG_BIN} -R #{target('')} --cwd #{target('')} locate"
cmd << " -r " + (identifier ? identifier.to_s : "tip")
cmd << " " + shell_quote("path:#{path}") unless path.empty?
shellout(cmd) do |io|
io.each_line do |line|
e = line.chomp.split(%r{[\/\\]})
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}
# HG uses antislashs as separator on Windows
line = line.gsub(/\\/, "/")
if path.empty? or e = line.gsub!(%r{^#{with_trailling_slash(path)}},'')
e ||= line
e = e.chomp.split(%r{[\/\\]})
entries << Entry.new({:name => e.first,
:path => (path.nil? or path.empty? ? e.first : "#{with_trailling_slash(path)}#{e.first}"),
:kind => (e.size > 1 ? 'dir' : 'file'),
:lastrev => Revision.new
}) unless entries.detect{|entry| entry.name == e.first}
end
end
end
return nil if $? && $?.exitstatus != 0
entries.sort_by_name
end
def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
# Fetch the revisions by using a template file that
# makes Mercurial produce a xml output.
def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
revisions = Revisions.new
cmd = "#{HG_BIN} -v --encoding utf8 -R #{target('')} log"
cmd = "#{HG_BIN} --debug --encoding utf8 -R #{target('')} log -C --style #{self.class.template_path}"
if identifier_from && identifier_to
cmd << " -r #{identifier_from.to_i}:#{identifier_to.to_i}"
elsif identifier_from
cmd << " -r #{identifier_from.to_i}:"
end
cmd << " --limit #{options[:limit].to_i}" if options[:limit]
cmd << " #{path}" if path
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 << build_revision_from_changeset(changeset)
changeset = {}
end
if !parsing_descr
changeset.store key.to_sym, value
if $1 == "description"
parsing_descr = true
line_feeds = 0
next
begin
# HG doesn't close the XML Document...
doc = REXML::Document.new(io.read << "</log>")
doc.elements.each("log/logentry") do |logentry|
paths = []
copies = logentry.get_elements('paths/path-copied')
logentry.elements.each("paths/path") do |path|
# Detect if the added file is a copy
if path.attributes['action'] == 'A' and c = copies.find{ |e| e.text == path.text }
from_path = c.attributes['copyfrom-path']
from_rev = logentry.attributes['revision']
end
paths << {:action => path.attributes['action'],
:path => "/#{path.text}",
:from_path => from_path ? "/#{from_path}" : nil,
:from_revision => from_rev ? from_rev : nil
}
end
paths.sort! { |x,y| x[:path] <=> y[:path] }
revisions << Revision.new({:identifier => logentry.attributes['revision'],
:scmid => logentry.attributes['node'],
:author => (logentry.elements['author'] ? logentry.elements['author'].text : ""),
:time => Time.parse(logentry.elements['date'].text).localtime,
:message => logentry.elements['msg'].text,
:paths => paths
})
end
if parsing_descr
changeset[:description] << line
line_feeds += 1 if line.chomp.empty?
end
rescue
logger.debug($!)
end
# Add the last changeset if there is one left
revisions << build_revision_from_changeset(changeset) if changeset[:date]
end
return nil if $? && $?.exitstatus != 0
revisions
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 = identifier_to.to_i
@@ -120,12 +166,12 @@ module Redmine
end
end
return nil if $? && $?.exitstatus != 0
DiffTableList.new diff, type
diff
end
def cat(path, identifier=nil)
cmd = "#{HG_BIN} -R #{target('')} cat"
cmd << " -r #{identifier.to_i}" if identifier
cmd << " -r " + (identifier ? identifier.to_s : "tip")
cmd << " #{target(path)}"
cat = nil
shellout(cmd) do |io|
@@ -140,6 +186,7 @@ module Redmine
path ||= ''
cmd = "#{HG_BIN} -R #{target('')}"
cmd << " annotate -n -u"
cmd << " -r " + (identifier ? identifier.to_s : "tip")
cmd << " -r #{identifier.to_i}" if identifier
cmd << " #{target(path)}"
blame = Annotate.new
@@ -152,47 +199,6 @@ module Redmine
return nil if $? && $?.exitstatus != 0
blame
end
private
# Builds a revision objet from the changeset returned by hg command
def build_revision_from_changeset(changeset)
rev_id = changeset[:changeset].to_s.split(':').first.to_i
# Changes
paths = (rev_id == 0) ?
# Can't get changes for revision 0 with hg status
changeset[:files].to_s.split.collect{|path| {:action => 'A', :path => "/#{path}"}} :
status(rev_id)
Revision.new({:identifier => rev_id,
:scmid => changeset[:changeset].to_s.split(':').last,
:author => changeset[:user],
:time => Time.parse(changeset[:date]),
:message => changeset[:description],
:paths => paths
})
end
# Returns the file changes for a given revision
def status(rev_id)
cmd = "#{HG_BIN} -R #{target('')} status --rev #{rev_id.to_i - 1}:#{rev_id.to_i}"
result = []
shellout(cmd) do |io|
io.each_line do |line|
action, file = line.chomp.split
next unless action && file
file.gsub!("\\", "/")
case action
when 'R'
result << { :action => 'D' , :path => "/#{file}" }
else
result << { :action => action, :path => "/#{file}" }
end
end
end
result
end
end
end
end
@@ -26,6 +26,25 @@ module Redmine
# SVN executable name
SVN_BIN = "svn"
class << self
def client_version
@@client_version ||= (svn_binary_version || [])
end
def svn_binary_version
cmd = "#{SVN_BIN} --version"
version = nil
shellout(cmd) do |io|
# Read svn version in first returned line
if m = io.gets.match(%r{((\d+\.)+\d+)})
version = m[0].scan(%r{\d+}).collect(&:to_i)
end
end
return nil if $? && $?.exitstatus != 0
version
end
end
# Get info about the svn repository
def info
cmd = "#{SVN_BIN} info --xml #{target('')}"
@@ -64,6 +83,9 @@ module Redmine
begin
doc = REXML::Document.new(output)
doc.elements.each("lists/list/entry") do |entry|
# Skip directory if there is no commit date (usually that
# means that we don't have read access to it)
next if entry.attributes['kind'] == 'dir' && entry.elements['commit'].elements['date'].nil?
entries << Entry.new({:name => entry.elements['name'].text,
:path => ((path.empty? ? "" : "#{path}/") + entry.elements['name'].text),
:kind => entry.attributes['kind'],
@@ -84,7 +106,29 @@ module Redmine
logger.debug("Found #{entries.size} entries in the repository for #{target(path)}") if logger && logger.debug?
entries.sort_by_name
end
def properties(path, identifier=nil)
# proplist xml output supported in svn 1.5.0 and higher
return nil unless self.class.client_version_above?([1, 5, 0])
identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
cmd = "#{SVN_BIN} proplist --verbose --xml #{target(path)}@#{identifier}"
cmd << credentials_string
properties = {}
shellout(cmd) do |io|
output = io.read
begin
doc = REXML::Document.new(output)
doc.elements.each("properties/target/property") do |property|
properties[ property.attributes['name'] ] = property.text
end
rescue
end
end
return nil if $? && $?.exitstatus != 0
properties
end
def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
path ||= ''
identifier_from = (identifier_from and identifier_from.to_i > 0) ? identifier_from.to_i : "HEAD"
@@ -139,7 +183,7 @@ module Redmine
end
end
return nil if $? && $?.exitstatus != 0
DiffTableList.new diff, type
diff
end
def cat(path, identifier=nil)
+178
View File
@@ -0,0 +1,178 @@
# redMine - project management software
# Copyright (C) 2006-2008 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
# Class used to parse unified diffs
class UnifiedDiff < Array
def initialize(diff, type="inline")
diff_table = DiffTable.new type
diff.each do |line|
if line =~ /^(---|\+\+\+) (.*)$/
self << diff_table if diff_table.length > 1
diff_table = DiffTable.new type
end
a = diff_table.add_line line
end
self << diff_table unless diff_table.empty?
self
end
end
# Class that represents a file 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 =~ /^(---|\+\+\+) (.*)$/
@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@\\]/
@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)
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
# 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
end
+31 -15
View File
@@ -26,7 +26,7 @@ module Redmine
class TextileFormatter < RedCloth
# auto_link rule after textile rules so that it doesn't break !image_url! tags
RULES = [:textile, :inline_auto_link, :inline_auto_mailto, :inline_toc, :inline_macros]
RULES = [:textile, :block_markdown_rule, :inline_auto_link, :inline_auto_mailto, :inline_toc, :inline_macros]
def initialize(*args)
super
@@ -45,7 +45,7 @@ module Redmine
# Patch for RedCloth. Fixed in RedCloth r128 but _why hasn't released it yet.
# <a href="http://code.whytheluckystiff.net/redcloth/changeset/128">http://code.whytheluckystiff.net/redcloth/changeset/128</a>
def hard_break( text )
text.gsub!( /(.)\n(?!\n|\Z| *([#*=]+(\s|$)|[{|]))/, "\\1<br />" ) if hard_breaks
text.gsub!( /(.)\n(?!\n|\Z|>| *(>? *[#*=]+(\s|$)|[{|]))/, "\\1<br />\n" ) if hard_breaks
end
# Patch to add code highlighting support to RedCloth
@@ -56,7 +56,7 @@ module Redmine
content = @pre_list[$1.to_i]
if content.match(/<code\s+class="(\w+)">\s?(.+)/m)
content = "<code class=\"#{$1} CodeRay\">" +
CodeRay.scan($2, $1).html(:escape => false, :line_numbers => :inline)
CodeRay.scan($2, $1.downcase).html(:escape => false, :line_numbers => :inline)
end
content
end
@@ -65,10 +65,22 @@ module Redmine
# Patch to add 'table of content' support to RedCloth
def textile_p_withtoc(tag, atts, cite, content)
if tag =~ /^h(\d)$/
@toc << [$1.to_i, content]
# removes wiki links from the item
toc_item = content.gsub(/(\[\[|\]\])/, '')
# removes styles
# eg. %{color:red}Triggers% => Triggers
toc_item.gsub! %r[%\{[^\}]*\}([^%]+)%], '\\1'
# replaces non word caracters by dashes
anchor = toc_item.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
unless anchor.blank?
if tag =~ /^h(\d)$/
@toc << [$1.to_i, anchor, toc_item]
end
atts << " id=\"#{anchor}\""
content = content + "<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a>"
end
content = "<a name=\"#{@toc.length}\" class=\"wiki-page\"></a>" + content
textile_p(tag, atts, cite, content)
end
@@ -81,13 +93,12 @@ module Redmine
div_class = 'toc'
div_class << ' right' if $1 == '>'
div_class << ' left' if $1 == '<'
out = "<div class=\"#{div_class}\">"
@toc.each_with_index do |heading, index|
# remove wiki links from the item
toc_item = heading.last.gsub(/(\[\[|\]\])/, '')
out << "<a href=\"##{index+1}\" class=\"heading#{heading.first}\">#{toc_item}</a>"
out = "<ul class=\"#{div_class}\">"
@toc.each do |heading|
level, anchor, toc_item = heading
out << "<li class=\"heading#{level}\"><a href=\"##{anchor}\">#{toc_item}</a></li>\n"
end
out << '</div>'
out << '</ul>'
out
end
end
@@ -126,6 +137,7 @@ module Redmine
)
(
(?:https?://)| # protocol spec, or
(?:ftp://)|
(?:www\.) # www.*
)
(
@@ -149,12 +161,16 @@ module Redmine
end
end
end
# Turns all email addresses into clickable links (code from Rails).
def inline_auto_mailto(text)
text.gsub!(/([\w\.!#\$%\-+.]+@[A-Za-z0-9\-]+(\.[A-Za-z0-9\-]+)+)/) do
text = $1
%{<a href="mailto:#{$1}" class="email">#{text}</a>}
mail = $1
if text.match(/<a\b[^>]*>(.*)(#{Regexp.escape(mail)})(.*)<\/a>/)
mail
else
%{<a href="mailto:#{mail}" class="email">#{mail}</a>}
end
end
end
end
@@ -77,6 +77,12 @@ module Redmine
content_tag('dl', out)
end
desc "Displays a list of child pages."
macro :child_pages do |obj, args|
raise 'This macro applies to wiki pages only.' unless obj.is_a?(WikiContent)
render_page_hierarchy(obj.page.descendants.group_by(&:parent_id), obj.page.id)
end
desc "Include a wiki page. Example:\n\n !{{include(Foo)}}\n\nor to include a page of a specific project wiki:\n\n !{{include(projectname:Foo)}}"
macro :include do |obj, args|
project = @project
+1 -1
View File
@@ -22,7 +22,7 @@ class TabularFormBuilder < ActionView::Helpers::FormBuilder
def initialize(object_name, object, template, options, proc)
set_language_if_valid options.delete(:lang)
@object_name, @object, @template, @options, @proc = object_name, object, template, options, proc
super
end
(field_helpers - %w(radio_button hidden_field) + %w(date_select)).each do |selector|
+105
View File
@@ -0,0 +1,105 @@
# redMine - project management software
# Copyright (C) 2006-2008 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.
namespace :redmine do
namespace :email do
desc <<-END_DESC
Read an email from standard input.
Issue attributes control options:
project=PROJECT identifier of the target project
tracker=TRACKER name of the target tracker
category=CATEGORY name of the target category
priority=PRIORITY name of the target priority
allow_override=ATTRS allow email content to override attributes
specified by previous options
ATTRS is a comma separated list of attributes
Examples:
# No project specified. Emails MUST contain the 'Project' keyword:
rake redmine:email:read RAILS_ENV="production" < raw_email
# Fixed project and default tracker specified, but emails can override
# both tracker and priority attributes:
rake redmine:email:read RAILS_ENV="production" \\
project=foo \\
tracker=bug \\
allow_override=tracker,priority < raw_email
END_DESC
task :read => :environment do
options = { :issue => {} }
%w(project tracker category priority).each { |a| options[:issue][a.to_sym] = ENV[a] if ENV[a] }
options[:allow_override] = ENV['allow_override'] if ENV['allow_override']
MailHandler.receive(STDIN.read, options)
end
desc <<-END_DESC
Read emails from an IMAP server.
Available IMAP options:
host=HOST IMAP server host (default: 127.0.0.1)
port=PORT IMAP server port (default: 143)
ssl=SSL Use SSL? (default: false)
username=USERNAME IMAP account
password=PASSWORD IMAP password
folder=FOLDER IMAP folder to read (default: INBOX)
Issue attributes control options:
project=PROJECT identifier of the target project
tracker=TRACKER name of the target tracker
category=CATEGORY name of the target category
priority=PRIORITY name of the target priority
allow_override=ATTRS allow email content to override attributes
specified by previous options
ATTRS is a comma separated list of attributes
Examples:
# No project specified. Emails MUST contain the 'Project' keyword:
rake redmine:email:receive_iamp RAILS_ENV="production" \\
host=imap.foo.bar username=redmine@example.net password=xxx
# Fixed project and default tracker specified, but emails can override
# both tracker and priority attributes:
rake redmine:email:receive_iamp RAILS_ENV="production" \\
host=imap.foo.bar username=redmine@example.net password=xxx ssl=1 \\
project=foo \\
tracker=bug \\
allow_override=tracker,priority
END_DESC
task :receive_imap => :environment do
imap_options = {:host => ENV['host'],
:port => ENV['port'],
:ssl => ENV['ssl'],
:username => ENV['username'],
:password => ENV['password'],
:folder => ENV['folder']}
options = { :issue => {} }
%w(project tracker category priority).each { |a| options[:issue][a.to_sym] = ENV[a] if ENV[a] }
options[:allow_override] = ENV['allow_override'] if ENV['allow_override']
Redmine::IMAP.check(imap_options, options)
end
end
end
+42 -7
View File
@@ -92,12 +92,17 @@ namespace :redmine do
set_table_name :milestone
def due
if read_attribute(:due) > 0
if read_attribute(:due) && read_attribute(:due) > 0
Time.at(read_attribute(:due)).to_date
else
nil
end
end
def description
# Attribute is named descr in Trac v0.8.x
has_attribute?(:descr) ? read_attribute(:descr) : read_attribute(:description)
end
end
class TracTicketCustom < ActiveRecord::Base
@@ -126,6 +131,10 @@ namespace :redmine do
File.open("#{trac_fullpath}", 'rb').read
end
def description
read_attribute(:description).to_s.slice(0,255)
end
private
def trac_fullpath
attachment_type = read_attribute(:type)
@@ -140,7 +149,10 @@ namespace :redmine do
# ticket changes: only migrate status changes and comments
has_many :changes, :class_name => "TracTicketChange", :foreign_key => :ticket
has_many :attachments, :class_name => "TracAttachment", :foreign_key => :id, :conditions => "#{TracMigrate::TracAttachment.table_name}.type = 'ticket'"
has_many :attachments, :class_name => "TracAttachment",
:finder_sql => "SELECT DISTINCT attachment.* FROM #{TracMigrate::TracAttachment.table_name}" +
" WHERE #{TracMigrate::TracAttachment.table_name}.type = 'ticket'" +
' AND #{TracMigrate::TracAttachment.table_name}.id = \'#{id}\''
has_many :customs, :class_name => "TracTicketCustom", :foreign_key => :ticket
def ticket_type
@@ -177,7 +189,10 @@ namespace :redmine do
set_table_name :wiki
set_primary_key :name
has_many :attachments, :class_name => "TracAttachment", :foreign_key => :id, :conditions => "#{TracMigrate::TracAttachment.table_name}.type = 'wiki'"
has_many :attachments, :class_name => "TracAttachment",
:finder_sql => "SELECT DISTINCT attachment.* FROM #{TracMigrate::TracAttachment.table_name}" +
" WHERE #{TracMigrate::TracAttachment.table_name}.type = 'wiki'" +
' AND #{TracMigrate::TracAttachment.table_name}.id = \'#{id}\''
def self.columns
# Hides readonly Trac field to prevent clash with AR readonly? method (Rails 2.0)
@@ -191,6 +206,10 @@ namespace :redmine do
set_table_name :permission
end
class TracSessionAttribute < ActiveRecord::Base
set_table_name :session_attribute
end
def self.find_or_create_user(username, project_member = false)
return User.anonymous if username.blank?
@@ -198,10 +217,23 @@ namespace :redmine do
if !u
# Create a new user if not found
mail = username[0,limit_for(User, 'mail')]
if mail_attr = TracSessionAttribute.find_by_sid_and_name(username, 'email')
mail = mail_attr.value
end
mail = "#{mail}@foo.bar" unless mail.include?("@")
u = User.new :firstname => username[0,limit_for(User, 'firstname')].gsub(/[^\w\s\'\-]/i, '-'),
:lastname => '-',
:mail => mail.gsub(/[^-@a-z0-9\.]/i, '-')
name = username
if name_attr = TracSessionAttribute.find_by_sid_and_name(username, 'name')
name = name_attr.value
end
name =~ (/(.*)(\s+\w+)?/)
fn = $1.strip
ln = ($2 || '-').strip
u = User.new :mail => mail.gsub(/[^-@a-z0-9\.]/i, '-'),
:firstname => fn[0, limit_for(User, 'firstname')].gsub(/[^\w\s\'\-]/i, '-'),
:lastname => ln[0, limit_for(User, 'lastname')].gsub(/[^\w\s\'\-]/i, '-')
u.login = username[0,limit_for(User, 'login')].gsub(/[^a-z0-9_\-@\.]/i, '-')
u.password = 'trac'
u.admin = true if TracPermission.find_by_username_and_action(username, 'admin')
@@ -233,7 +265,8 @@ namespace :redmine do
text = text.gsub(/\[\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
text = text.gsub(/\[wiki:([^\s\]]+).*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
text = text.gsub(/\[wiki:([^\s\]]+)\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
text = text.gsub(/\[wiki:([^\s\]]+)\s(.*)\]/) {|s| "[[#{$1.delete(',./?;|:')}|#{$2.delete(',./?;|:')}]]"}
# Links to pages UsingJustWikiCaps
text = text.gsub(/([^!]|^)(^| )([A-Z][a-z]+[A-Z][a-zA-Z]+)/, '\\1\\2[[\3]]')
@@ -408,6 +441,7 @@ namespace :redmine do
a.file = attachment
a.author = find_or_create_user(attachment.author)
a.container = i
a.description = attachment.description
migrated_ticket_attachments += 1 if a.save
end
@@ -456,6 +490,7 @@ namespace :redmine do
a = Attachment.new :created_on => attachment.time
a.file = attachment
a.author = find_or_create_user(attachment.author)
a.description = attachment.description
a.container = p
migrated_wiki_attachments += 1 if a.save
end
+39
View File
@@ -0,0 +1,39 @@
# redMine - project management software
# Copyright (C) 2008 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.
desc <<-END_DESC
Send reminders about issues due in the next days.
Available options:
* days => number of days to remind about (defaults to 7)
* tracker => id of tracker (defaults to all trackers)
* project => id or identifier of project (defaults to all projects)
Example:
rake redmine:send_reminders days=7 RAILS_ENV="production"
END_DESC
namespace :redmine do
task :send_reminders => :environment do
options = {}
options[:days] = ENV['days'].to_i if ENV['days']
options[:project] = ENV['project'] if ENV['project']
options[:tracker] = ENV['tracker'].to_i if ENV['tracker']
Mailer.reminders(options)
end
end
+46
View File
@@ -0,0 +1,46 @@
### From http://svn.geekdaily.org/public/rails/plugins/generally_useful/tasks/coverage_via_rcov.rake
### Inspired by http://blog.labratz.net/articles/2006/12/2/a-rake-task-for-rcov
begin
require 'rcov/rcovtask'
rcov_options = "--rails --aggregate test/coverage.data --exclude '/gems/'"
namespace :test do
desc "Aggregate code coverage for all tests"
Rcov::RcovTask.new('coverage') do |t|
t.libs << 'test'
t.test_files = FileList['test/{unit,integration,functional}/*_test.rb']
t.verbose = true
t.rcov_opts << rcov_options
end
namespace :coverage do
desc "Delete coverage test data"
task :clean do
rm_f "test/coverage.data"
rm_rf "test/coverage"
end
desc "Aggregate code coverage for all tests with HTML output"
Rcov::RcovTask.new('html') do |t|
t.libs << 'test'
t.test_files = FileList['test/{unit,integration,functional}/*_test.rb']
t.output_dir = "test/coverage"
t.verbose = true
t.rcov_opts << rcov_options
end
desc "Open the HTML coverage report"
task :show_results do
system "open test/coverage/index.html"
end
task :full => "test:coverage:clean"
task :full => "test:coverage:html"
task :full => "test:coverage:show_results"
end
end
rescue LoadError
# rcov not available
end