Merged trunk r1773.
git-svn-id: http://redmine.rubyforge.org/svn/branches/work@1774 e93f8b46-1217-0410-a6f0-8f06a7374b81
This commit is contained in:
@@ -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
@@ -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}" : "<#{$1}#{'>' if $3}" }
|
||||
text.gsub!(%r{<(\/?([!\w]+)[^<>\n]*)(>?)}) {|m| ALLOWED_TAGS.include?($2) ? "<#{$1}#{$3}" : "<#{$1}#{'>' unless $3.blank?}" }
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
+25
-10
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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\">¶</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
|
||||
|
||||
@@ -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|
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user