User groups branch added.
git-svn-id: http://redmine.rubyforge.org/svn/branches/work@1372 e93f8b46-1217-0410-a6f0-8f06a7374b81
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006-2007 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
module Redmine
|
||||
module AccessControl
|
||||
|
||||
class << self
|
||||
def map
|
||||
mapper = Mapper.new
|
||||
yield mapper
|
||||
@permissions ||= []
|
||||
@permissions += mapper.mapped_permissions
|
||||
end
|
||||
|
||||
def permissions
|
||||
@permissions
|
||||
end
|
||||
|
||||
def allowed_actions(permission_name)
|
||||
perm = @permissions.detect {|p| p.name == permission_name}
|
||||
perm ? perm.actions : []
|
||||
end
|
||||
|
||||
def public_permissions
|
||||
@public_permissions ||= @permissions.select {|p| p.public?}
|
||||
end
|
||||
|
||||
def members_only_permissions
|
||||
@members_only_permissions ||= @permissions.select {|p| p.require_member?}
|
||||
end
|
||||
|
||||
def loggedin_only_permissions
|
||||
@loggedin_only_permissions ||= @permissions.select {|p| p.require_loggedin?}
|
||||
end
|
||||
|
||||
def available_project_modules
|
||||
@available_project_modules ||= @permissions.collect(&:project_module).uniq.compact
|
||||
end
|
||||
|
||||
def modules_permissions(modules)
|
||||
@permissions.select {|p| p.project_module.nil? || modules.include?(p.project_module.to_s)}
|
||||
end
|
||||
end
|
||||
|
||||
class Mapper
|
||||
def initialize
|
||||
@project_module = nil
|
||||
end
|
||||
|
||||
def permission(name, hash, options={})
|
||||
@permissions ||= []
|
||||
options.merge!(:project_module => @project_module)
|
||||
@permissions << Permission.new(name, hash, options)
|
||||
end
|
||||
|
||||
def project_module(name, options={})
|
||||
@project_module = name
|
||||
yield self
|
||||
@project_module = nil
|
||||
end
|
||||
|
||||
def mapped_permissions
|
||||
@permissions
|
||||
end
|
||||
end
|
||||
|
||||
class Permission
|
||||
attr_reader :name, :actions, :project_module
|
||||
|
||||
def initialize(name, hash, options)
|
||||
@name = name
|
||||
@actions = []
|
||||
@public = options[:public] || false
|
||||
@require = options[:require]
|
||||
@project_module = options[:project_module]
|
||||
hash.each do |controller, actions|
|
||||
if actions.is_a? Array
|
||||
@actions << actions.collect {|action| "#{controller}/#{action}"}
|
||||
else
|
||||
@actions << "#{controller}/#{actions}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def public?
|
||||
@public
|
||||
end
|
||||
|
||||
def require_member?
|
||||
@require && @require == :member
|
||||
end
|
||||
|
||||
def require_loggedin?
|
||||
@require && (@require == :member || @require == :loggedin)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,31 @@
|
||||
# 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 AccessKeys
|
||||
ACCESSKEYS = {:edit => 'e',
|
||||
:preview => 'r',
|
||||
:quick_search => 'f',
|
||||
:search => '4',
|
||||
:new_issue => '7'
|
||||
}.freeze unless const_defined?(:ACCESSKEYS)
|
||||
|
||||
def self.key_for(action)
|
||||
ACCESSKEYS[action]
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1 @@
|
||||
Dir[File.dirname(__FILE__) + "/core_ext/*.rb"].each { |file| require(file) }
|
||||
@@ -0,0 +1,5 @@
|
||||
require File.dirname(__FILE__) + '/string/conversions'
|
||||
|
||||
class String #:nodoc:
|
||||
include Redmine::CoreExtensions::String::Conversions
|
||||
end
|
||||
@@ -0,0 +1,40 @@
|
||||
# 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.
|
||||
|
||||
module Redmine #:nodoc:
|
||||
module CoreExtensions #:nodoc:
|
||||
module String #:nodoc:
|
||||
# Custom string conversions
|
||||
module Conversions
|
||||
# Parses hours format and returns a float
|
||||
def to_hours
|
||||
s = self.dup
|
||||
s.strip!
|
||||
unless s =~ %r{^[\d\.,]+$}
|
||||
# 2:30 => 2.5
|
||||
s.gsub!(%r{^(\d+):(\d+)$}) { $1.to_i + $2.to_i / 60.0 }
|
||||
# 2h30, 2h, 30m => 2.5, 2, 0.5
|
||||
s.gsub!(%r{^((\d+)\s*(h|hours?))?\s*((\d+)\s*(m|min)?)?$}) { |m| ($1 || $4) ? ($2.to_i + $5.to_i / 60.0) : m[0] }
|
||||
end
|
||||
# 2,5 => 2.5
|
||||
s.gsub!(',', '.')
|
||||
s.to_f
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,169 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006-2007 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
module Redmine
|
||||
module DefaultData
|
||||
class DataAlreadyLoaded < Exception; end
|
||||
|
||||
module Loader
|
||||
include GLoc
|
||||
|
||||
class << self
|
||||
# Returns true if no data is already loaded in the database
|
||||
# otherwise false
|
||||
def no_data?
|
||||
!Role.find(:first, :conditions => {:builtin => 0}) &&
|
||||
!Tracker.find(:first) &&
|
||||
!IssueStatus.find(:first) &&
|
||||
!Enumeration.find(:first)
|
||||
end
|
||||
|
||||
# Loads the default data
|
||||
# Raises a RecordNotSaved exception if something goes wrong
|
||||
def load(lang=nil)
|
||||
raise DataAlreadyLoaded.new("Some configuration data is already loaded.") unless no_data?
|
||||
set_language_if_valid(lang)
|
||||
|
||||
Role.transaction do
|
||||
# Roles
|
||||
manager = Role.create! :name => l(:default_role_manager),
|
||||
:position => 1
|
||||
manager.permissions = manager.setable_permissions.collect {|p| p.name}
|
||||
manager.save!
|
||||
|
||||
developper = Role.create! :name => l(:default_role_developper),
|
||||
:position => 2,
|
||||
:permissions => [:manage_versions,
|
||||
:manage_categories,
|
||||
:add_issues,
|
||||
:edit_issues,
|
||||
:manage_issue_relations,
|
||||
:add_issue_notes,
|
||||
:save_queries,
|
||||
:view_gantt,
|
||||
:view_calendar,
|
||||
:log_time,
|
||||
:view_time_entries,
|
||||
:comment_news,
|
||||
:view_documents,
|
||||
:view_wiki_pages,
|
||||
:edit_wiki_pages,
|
||||
:delete_wiki_pages,
|
||||
:add_messages,
|
||||
:view_files,
|
||||
:manage_files,
|
||||
:browse_repository,
|
||||
:view_changesets]
|
||||
|
||||
reporter = Role.create! :name => l(:default_role_reporter),
|
||||
:position => 3,
|
||||
:permissions => [:add_issues,
|
||||
:add_issue_notes,
|
||||
:save_queries,
|
||||
:view_gantt,
|
||||
:view_calendar,
|
||||
:log_time,
|
||||
:view_time_entries,
|
||||
:comment_news,
|
||||
:view_documents,
|
||||
:view_wiki_pages,
|
||||
:add_messages,
|
||||
:view_files,
|
||||
:browse_repository,
|
||||
:view_changesets]
|
||||
|
||||
Role.non_member.update_attribute :permissions, [:add_issues,
|
||||
:add_issue_notes,
|
||||
:save_queries,
|
||||
:view_gantt,
|
||||
:view_calendar,
|
||||
:view_time_entries,
|
||||
:comment_news,
|
||||
:view_documents,
|
||||
:view_wiki_pages,
|
||||
:add_messages,
|
||||
:view_files,
|
||||
:browse_repository,
|
||||
:view_changesets]
|
||||
|
||||
Role.anonymous.update_attribute :permissions, [:view_gantt,
|
||||
:view_calendar,
|
||||
:view_time_entries,
|
||||
:view_documents,
|
||||
:view_wiki_pages,
|
||||
:view_files,
|
||||
:browse_repository,
|
||||
:view_changesets]
|
||||
|
||||
# Trackers
|
||||
Tracker.create!(:name => l(:default_tracker_bug), :is_in_chlog => true, :is_in_roadmap => false, :position => 1)
|
||||
Tracker.create!(:name => l(:default_tracker_feature), :is_in_chlog => true, :is_in_roadmap => true, :position => 2)
|
||||
Tracker.create!(:name => l(:default_tracker_support), :is_in_chlog => false, :is_in_roadmap => false, :position => 3)
|
||||
|
||||
# Issue statuses
|
||||
new = IssueStatus.create!(:name => l(:default_issue_status_new), :is_closed => false, :is_default => true, :position => 1)
|
||||
assigned = IssueStatus.create!(:name => l(:default_issue_status_assigned), :is_closed => false, :is_default => false, :position => 2)
|
||||
resolved = IssueStatus.create!(:name => l(:default_issue_status_resolved), :is_closed => false, :is_default => false, :position => 3)
|
||||
feedback = IssueStatus.create!(:name => l(:default_issue_status_feedback), :is_closed => false, :is_default => false, :position => 4)
|
||||
closed = IssueStatus.create!(:name => l(:default_issue_status_closed), :is_closed => true, :is_default => false, :position => 5)
|
||||
rejected = IssueStatus.create!(:name => l(:default_issue_status_rejected), :is_closed => true, :is_default => false, :position => 6)
|
||||
|
||||
# Workflow
|
||||
Tracker.find(:all).each { |t|
|
||||
IssueStatus.find(:all).each { |os|
|
||||
IssueStatus.find(:all).each { |ns|
|
||||
Workflow.create!(:tracker_id => t.id, :role_id => manager.id, :old_status_id => os.id, :new_status_id => ns.id) unless os == ns
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Tracker.find(:all).each { |t|
|
||||
[new, assigned, resolved, feedback].each { |os|
|
||||
[assigned, resolved, feedback, closed].each { |ns|
|
||||
Workflow.create!(:tracker_id => t.id, :role_id => developper.id, :old_status_id => os.id, :new_status_id => ns.id) unless os == ns
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Tracker.find(:all).each { |t|
|
||||
[new, assigned, resolved, feedback].each { |os|
|
||||
[closed].each { |ns|
|
||||
Workflow.create!(:tracker_id => t.id, :role_id => reporter.id, :old_status_id => os.id, :new_status_id => ns.id) unless os == ns
|
||||
}
|
||||
}
|
||||
Workflow.create!(:tracker_id => t.id, :role_id => reporter.id, :old_status_id => resolved.id, :new_status_id => feedback.id)
|
||||
}
|
||||
|
||||
# Enumerations
|
||||
Enumeration.create!(:opt => "DCAT", :name => l(:default_doc_category_user), :position => 1)
|
||||
Enumeration.create!(:opt => "DCAT", :name => l(:default_doc_category_tech), :position => 2)
|
||||
|
||||
Enumeration.create!(:opt => "IPRI", :name => l(:default_priority_low), :position => 1)
|
||||
Enumeration.create!(:opt => "IPRI", :name => l(:default_priority_normal), :position => 2, :is_default => true)
|
||||
Enumeration.create!(:opt => "IPRI", :name => l(:default_priority_high), :position => 3)
|
||||
Enumeration.create!(:opt => "IPRI", :name => l(:default_priority_urgent), :position => 4)
|
||||
Enumeration.create!(:opt => "IPRI", :name => l(:default_priority_immediate), :position => 5)
|
||||
|
||||
Enumeration.create!(:opt => "ACTI", :name => l(:default_activity_design), :position => 1)
|
||||
Enumeration.create!(:opt => "ACTI", :name => l(:default_activity_development), :position => 2)
|
||||
end
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,76 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006-2007 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
module Redmine
|
||||
module Helpers
|
||||
|
||||
# Simple class to compute the start and end dates of a calendar
|
||||
class Calendar
|
||||
include GLoc
|
||||
attr_reader :startdt, :enddt
|
||||
|
||||
def initialize(date, lang = current_language, period = :month)
|
||||
@date = date
|
||||
@events = []
|
||||
@ending_events_by_days = {}
|
||||
@starting_events_by_days = {}
|
||||
set_language lang
|
||||
case period
|
||||
when :month
|
||||
@startdt = Date.civil(date.year, date.month, 1)
|
||||
@enddt = (@startdt >> 1)-1
|
||||
# starts from the first day of the week
|
||||
@startdt = @startdt - (@startdt.cwday - first_wday)%7
|
||||
# ends on the last day of the week
|
||||
@enddt = @enddt + (last_wday - @enddt.cwday)%7
|
||||
when :week
|
||||
@startdt = date - (date.cwday - first_wday)%7
|
||||
@enddt = date + (last_wday - date.cwday)%7
|
||||
else
|
||||
raise 'Invalid period'
|
||||
end
|
||||
end
|
||||
|
||||
# Sets calendar events
|
||||
def events=(events)
|
||||
@events = events
|
||||
@ending_events_by_days = @events.group_by {|event| event.due_date}
|
||||
@starting_events_by_days = @events.group_by {|event| event.start_date}
|
||||
end
|
||||
|
||||
# Returns events for the given day
|
||||
def events_on(day)
|
||||
((@ending_events_by_days[day] || []) + (@starting_events_by_days[day] || [])).uniq
|
||||
end
|
||||
|
||||
# Calendar current month
|
||||
def month
|
||||
@date.month
|
||||
end
|
||||
|
||||
# Return the first day of week
|
||||
# 1 = Monday ... 7 = Sunday
|
||||
def first_wday
|
||||
@first_dow ||= (l(:general_first_day_of_week).to_i - 1)%7 + 1
|
||||
end
|
||||
|
||||
def last_wday
|
||||
@last_dow ||= (first_wday + 5)%7 + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,10 @@
|
||||
module Redmine
|
||||
module Info
|
||||
class << self
|
||||
def app_name; 'Redmine' end
|
||||
def url; 'http://www.redmine.org/' end
|
||||
def help_url; 'http://www.redmine.org/guide' end
|
||||
def versioned_name; "#{app_name} #{Redmine::VERSION}" end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,146 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006-2007 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
require 'gloc'
|
||||
|
||||
module Redmine
|
||||
module MenuManager
|
||||
module MenuController
|
||||
def self.included(base)
|
||||
base.extend(ClassMethods)
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
@@menu_items = Hash.new {|hash, key| hash[key] = {:default => key, :actions => {}}}
|
||||
mattr_accessor :menu_items
|
||||
|
||||
# Set the menu item name for a controller or specific actions
|
||||
# Examples:
|
||||
# * menu_item :tickets # => sets the menu name to :tickets for the whole controller
|
||||
# * menu_item :tickets, :only => :list # => sets the menu name to :tickets for the 'list' action only
|
||||
# * menu_item :tickets, :only => [:list, :show] # => sets the menu name to :tickets for 2 actions only
|
||||
#
|
||||
# The default menu item name for a controller is controller_name by default
|
||||
# Eg. the default menu item name for ProjectsController is :projects
|
||||
def menu_item(id, options = {})
|
||||
if actions = options[:only]
|
||||
actions = [] << actions unless actions.is_a?(Array)
|
||||
actions.each {|a| menu_items[controller_name.to_sym][:actions][a.to_sym] = id}
|
||||
else
|
||||
menu_items[controller_name.to_sym][:default] = id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def menu_items
|
||||
self.class.menu_items
|
||||
end
|
||||
|
||||
# Returns the menu item name according to the current action
|
||||
def current_menu_item
|
||||
menu_items[controller_name.to_sym][:actions][action_name.to_sym] ||
|
||||
menu_items[controller_name.to_sym][:default]
|
||||
end
|
||||
end
|
||||
|
||||
module MenuHelper
|
||||
# Returns the current menu item name
|
||||
def current_menu_item
|
||||
@controller.current_menu_item
|
||||
end
|
||||
|
||||
# Renders the application main menu
|
||||
def render_main_menu(project)
|
||||
render_menu((project && !project.new_record?) ? :project_menu : :application_menu, project)
|
||||
end
|
||||
|
||||
def render_menu(menu, project=nil)
|
||||
links = []
|
||||
Redmine::MenuManager.allowed_items(menu, User.current, project).each do |item|
|
||||
unless item.condition && !item.condition.call(project)
|
||||
url = case item.url
|
||||
when Hash
|
||||
project.nil? ? item.url : {item.param => project}.merge(item.url)
|
||||
when Symbol
|
||||
send(item.url)
|
||||
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)
|
||||
links << content_tag('li',
|
||||
link_to(l(item.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"))
|
||||
end
|
||||
end
|
||||
|
||||
class << self
|
||||
def map(menu_name)
|
||||
mapper = Mapper.new
|
||||
yield mapper
|
||||
@items ||= {}
|
||||
@items[menu_name.to_sym] ||= []
|
||||
@items[menu_name.to_sym] += mapper.items
|
||||
end
|
||||
|
||||
def items(menu_name)
|
||||
@items[menu_name.to_sym] || []
|
||||
end
|
||||
|
||||
def allowed_items(menu_name, user, project)
|
||||
project ? items(menu_name).select {|item| user && user.allowed_to?(item.url, project)} : items(menu_name)
|
||||
end
|
||||
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)
|
||||
end
|
||||
|
||||
def items
|
||||
@items ||= []
|
||||
end
|
||||
end
|
||||
|
||||
class MenuItem
|
||||
include GLoc
|
||||
attr_reader :name, :url, :param, :condition, :html_options
|
||||
|
||||
def initialize(name, url, options)
|
||||
raise "Invalid option :if for menu item '#{name}'" if options[:if] && !options[:if].respond_to?(:call)
|
||||
raise "Invalid option :html for menu item '#{name}'" if options[:html] && !options[:html].is_a?(Hash)
|
||||
@name = name
|
||||
@url = url
|
||||
@condition = options[:if]
|
||||
@param = options[:param] || :id
|
||||
@caption_key = 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))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,70 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006-2007 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
module Redmine
|
||||
module MimeType
|
||||
|
||||
MIME_TYPES = {
|
||||
'text/plain' => 'txt,tpl,properties,patch,diff,ini,readme,install,upgrade',
|
||||
'text/css' => 'css',
|
||||
'text/html' => 'html,htm,xhtml',
|
||||
'text/jsp' => 'jsp',
|
||||
'text/x-c' => 'c,cpp,cc,h,hh',
|
||||
'text/x-java' => 'java',
|
||||
'text/x-javascript' => 'js',
|
||||
'text/x-html-template' => 'rhtml',
|
||||
'text/x-perl' => 'pl,pm',
|
||||
'text/x-php' => 'php,php3,php4,php5',
|
||||
'text/x-python' => 'py',
|
||||
'text/x-ruby' => 'rb,rbw,ruby,rake',
|
||||
'text/x-csh' => 'csh',
|
||||
'text/x-sh' => 'sh',
|
||||
'text/xml' => 'xml,xsd,mxml',
|
||||
'text/yaml' => 'yml,yaml',
|
||||
'image/gif' => 'gif',
|
||||
'image/jpeg' => 'jpg,jpeg,jpe',
|
||||
'image/png' => 'png',
|
||||
'image/tiff' => 'tiff,tif',
|
||||
'image/x-ms-bmp' => 'bmp',
|
||||
'image/x-xpixmap' => 'xpm',
|
||||
}.freeze
|
||||
|
||||
EXTENSIONS = MIME_TYPES.inject({}) do |map, (type, exts)|
|
||||
exts.split(',').each {|ext| map[ext.strip] = type}
|
||||
map
|
||||
end
|
||||
|
||||
# returns mime type for name or nil if unknown
|
||||
def self.of(name)
|
||||
return nil unless name
|
||||
m = name.to_s.match(/(^|\.)([^\.]+)$/)
|
||||
EXTENSIONS[m[2].downcase] if m
|
||||
end
|
||||
|
||||
def self.main_mimetype_of(name)
|
||||
mimetype = of(name)
|
||||
mimetype.split('/').first if mimetype
|
||||
end
|
||||
|
||||
# return true if mime-type for name is type/*
|
||||
# otherwise false
|
||||
def self.is_type?(type, name)
|
||||
main_mimetype = main_mimetype_of(name)
|
||||
type.to_s == main_mimetype
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,125 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006-2007 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
module Redmine #:nodoc:
|
||||
|
||||
# Base class for Redmine plugins.
|
||||
# Plugins are registered using the <tt>register</tt> class method that acts as the public constructor.
|
||||
#
|
||||
# Redmine::Plugin.register :example do
|
||||
# name 'Example plugin'
|
||||
# author 'John Smith'
|
||||
# description 'This is an example plugin for Redmine'
|
||||
# version '0.0.1'
|
||||
# settings :default => {'foo'=>'bar'}, :partial => 'settings/settings'
|
||||
# end
|
||||
#
|
||||
# === Plugin attributes
|
||||
#
|
||||
# +settings+ is an optional attribute that let the plugin be configurable.
|
||||
# It must be a hash with the following keys:
|
||||
# * <tt>:default</tt>: default value for the plugin settings
|
||||
# * <tt>:partial</tt>: path of the configuration partial view, relative to the plugin <tt>app/views</tt> directory
|
||||
# Example:
|
||||
# settings :default => {'foo'=>'bar'}, :partial => 'settings/settings'
|
||||
# In this example, the settings partial will be found here in the plugin directory: <tt>app/views/settings/_settings.rhtml</tt>.
|
||||
#
|
||||
# When rendered, the plugin settings value is available as the local variable +settings+
|
||||
class Plugin
|
||||
@registered_plugins = {}
|
||||
class << self
|
||||
attr_reader :registered_plugins
|
||||
private :new
|
||||
|
||||
def def_field(*names)
|
||||
class_eval do
|
||||
names.each do |name|
|
||||
define_method(name) do |*args|
|
||||
args.empty? ? instance_variable_get("@#{name}") : instance_variable_set("@#{name}", *args)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
def_field :name, :description, :author, :version, :settings
|
||||
|
||||
# Plugin constructor
|
||||
def self.register(name, &block)
|
||||
p = new
|
||||
p.instance_eval(&block)
|
||||
Plugin.registered_plugins[name] = p
|
||||
end
|
||||
|
||||
# Adds an item to the given +menu+.
|
||||
# The +id+ parameter (equals to the project id) is automatically added to the url.
|
||||
# menu :project_menu, :plugin_example, { :controller => 'example', :action => 'say_hello' }, :caption => 'Sample'
|
||||
#
|
||||
# +name+ parameter can be: :top_menu, :account_menu, :application_menu or :project_menu
|
||||
#
|
||||
def menu(name, item, url, options={})
|
||||
Redmine::MenuManager.map(name) {|menu| menu.push item, url, options}
|
||||
end
|
||||
|
||||
# Defines a permission called +name+ for the given +actions+.
|
||||
#
|
||||
# The +actions+ argument is a hash with controllers as keys and actions as values (a single value or an array):
|
||||
# permission :destroy_contacts, { :contacts => :destroy }
|
||||
# permission :view_contacts, { :contacts => [:index, :show] }
|
||||
#
|
||||
# The +options+ argument can be used to make the permission public (implicitly given to any user)
|
||||
# or to restrict users the permission can be given to.
|
||||
#
|
||||
# Examples
|
||||
# # A permission that is implicitly given to any user
|
||||
# # This permission won't appear on the Roles & Permissions setup screen
|
||||
# permission :say_hello, { :example => :say_hello }, :public => true
|
||||
#
|
||||
# # A permission that can be given to any user
|
||||
# permission :say_hello, { :example => :say_hello }
|
||||
#
|
||||
# # A permission that can be given to registered users only
|
||||
# permission :say_hello, { :example => :say_hello }, :require => loggedin
|
||||
#
|
||||
# # A permission that can be given to project members only
|
||||
# permission :say_hello, { :example => :say_hello }, :require => member
|
||||
def permission(name, actions, options = {})
|
||||
if @project_module
|
||||
Redmine::AccessControl.map {|map| map.project_module(@project_module) {|map|map.permission(name, actions, options)}}
|
||||
else
|
||||
Redmine::AccessControl.map {|map| map.permission(name, actions, options)}
|
||||
end
|
||||
end
|
||||
|
||||
# Defines a project module, that can be enabled/disabled for each project.
|
||||
# Permissions defined inside +block+ will be bind to the module.
|
||||
#
|
||||
# project_module :things do
|
||||
# permission :view_contacts, { :contacts => [:list, :show] }, :public => true
|
||||
# permission :destroy_contacts, { :contacts => :destroy }
|
||||
# end
|
||||
def project_module(name, &block)
|
||||
@project_module = name
|
||||
self.instance_eval(&block)
|
||||
@project_module = nil
|
||||
end
|
||||
|
||||
# Returns +true+ if the plugin can be configured.
|
||||
def configurable?
|
||||
settings && settings.is_a?(Hash) && !settings[:partial].blank?
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,395 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006-2007 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
require 'cgi'
|
||||
|
||||
module Redmine
|
||||
module Scm
|
||||
module Adapters
|
||||
class CommandFailed < StandardError #:nodoc:
|
||||
end
|
||||
|
||||
class AbstractAdapter #:nodoc:
|
||||
def initialize(url, root_url=nil, login=nil, password=nil)
|
||||
@url = url
|
||||
@login = login if login && !login.empty?
|
||||
@password = (password || "") if @login
|
||||
@root_url = root_url.blank? ? retrieve_root_url : root_url
|
||||
end
|
||||
|
||||
def adapter_name
|
||||
'Abstract'
|
||||
end
|
||||
|
||||
def supports_cat?
|
||||
true
|
||||
end
|
||||
|
||||
def supports_annotate?
|
||||
respond_to?('annotate')
|
||||
end
|
||||
|
||||
def root_url
|
||||
@root_url
|
||||
end
|
||||
|
||||
def url
|
||||
@url
|
||||
end
|
||||
|
||||
# get info about the svn repository
|
||||
def info
|
||||
return nil
|
||||
end
|
||||
|
||||
# Returns the entry identified by path and revision identifier
|
||||
# or nil if entry doesn't exist in the repository
|
||||
def entry(path=nil, identifier=nil)
|
||||
parts = path.to_s.split(%r{[\/\\]}).select {|n| !n.blank?}
|
||||
search_path = parts[0..-2].join('/')
|
||||
search_name = parts[-1]
|
||||
if search_path.blank? && search_name.blank?
|
||||
# Root entry
|
||||
Entry.new(:path => '', :kind => 'dir')
|
||||
else
|
||||
# Search for the entry in the parent directory
|
||||
es = entries(search_path, identifier)
|
||||
es ? es.detect {|e| e.name == search_name} : nil
|
||||
end
|
||||
end
|
||||
|
||||
# Returns an Entries collection
|
||||
# or nil if the given path doesn't exist in the repository
|
||||
def entries(path=nil, identifier=nil)
|
||||
return nil
|
||||
end
|
||||
|
||||
def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
|
||||
return nil
|
||||
end
|
||||
|
||||
def diff(path, identifier_from, identifier_to=nil, type="inline")
|
||||
return nil
|
||||
end
|
||||
|
||||
def cat(path, identifier=nil)
|
||||
return nil
|
||||
end
|
||||
|
||||
def with_leading_slash(path)
|
||||
path ||= ''
|
||||
(path[0,1]!="/") ? "/#{path}" : path
|
||||
end
|
||||
|
||||
def shell_quote(str)
|
||||
if RUBY_PLATFORM =~ /mswin/
|
||||
'"' + str.gsub(/"/, '\\"') + '"'
|
||||
else
|
||||
"'" + str.gsub(/'/, "'\"'\"'") + "'"
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def retrieve_root_url
|
||||
info = self.info
|
||||
info ? info.root_url : nil
|
||||
end
|
||||
|
||||
def target(path)
|
||||
path ||= ''
|
||||
base = path.match(/^\//) ? root_url : url
|
||||
shell_quote("#{base}/#{path}".gsub(/[?<>\*]/, ''))
|
||||
end
|
||||
|
||||
def logger
|
||||
RAILS_DEFAULT_LOGGER
|
||||
end
|
||||
|
||||
def shellout(cmd, &block)
|
||||
logger.debug "Shelling out: #{cmd}" if logger && logger.debug?
|
||||
begin
|
||||
IO.popen(cmd, "r+") do |io|
|
||||
io.close_write
|
||||
block.call(io) if block_given?
|
||||
end
|
||||
rescue Errno::ENOENT => e
|
||||
# The command failed, log it and re-raise
|
||||
logger.error("SCM command failed: #{cmd}\n with: #{e.message}")
|
||||
raise CommandFailed.new(e.message)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class Entries < Array
|
||||
def sort_by_name
|
||||
sort {|x,y|
|
||||
if x.kind == y.kind
|
||||
x.name <=> y.name
|
||||
else
|
||||
x.kind <=> y.kind
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
def revisions
|
||||
revisions ||= Revisions.new(collect{|entry| entry.lastrev}.compact)
|
||||
end
|
||||
end
|
||||
|
||||
class Info
|
||||
attr_accessor :root_url, :lastrev
|
||||
def initialize(attributes={})
|
||||
self.root_url = attributes[:root_url] if attributes[:root_url]
|
||||
self.lastrev = attributes[:lastrev]
|
||||
end
|
||||
end
|
||||
|
||||
class Entry
|
||||
attr_accessor :name, :path, :kind, :size, :lastrev
|
||||
def initialize(attributes={})
|
||||
self.name = attributes[:name] if attributes[:name]
|
||||
self.path = attributes[:path] if attributes[:path]
|
||||
self.kind = attributes[:kind] if attributes[:kind]
|
||||
self.size = attributes[:size].to_i if attributes[:size]
|
||||
self.lastrev = attributes[:lastrev]
|
||||
end
|
||||
|
||||
def is_file?
|
||||
'file' == self.kind
|
||||
end
|
||||
|
||||
def is_dir?
|
||||
'dir' == self.kind
|
||||
end
|
||||
|
||||
def is_text?
|
||||
Redmine::MimeType.is_type?('text', name)
|
||||
end
|
||||
end
|
||||
|
||||
class Revisions < Array
|
||||
def latest
|
||||
sort {|x,y|
|
||||
unless x.time.nil? or y.time.nil?
|
||||
x.time <=> y.time
|
||||
else
|
||||
0
|
||||
end
|
||||
}.last
|
||||
end
|
||||
end
|
||||
|
||||
class Revision
|
||||
attr_accessor :identifier, :scmid, :name, :author, :time, :message, :paths, :revision, :branch
|
||||
def initialize(attributes={})
|
||||
self.identifier = attributes[:identifier]
|
||||
self.scmid = attributes[:scmid]
|
||||
self.name = attributes[:name] || self.identifier
|
||||
self.author = attributes[:author]
|
||||
self.time = attributes[:time]
|
||||
self.message = attributes[:message] || ""
|
||||
self.paths = attributes[:paths]
|
||||
self.revision = attributes[:revision]
|
||||
self.branch = attributes[:branch]
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
# A line of Diff
|
||||
class Diff
|
||||
attr_accessor :nb_line_left
|
||||
attr_accessor :line_left
|
||||
attr_accessor :nb_line_right
|
||||
attr_accessor :line_right
|
||||
attr_accessor :type_diff_right
|
||||
attr_accessor :type_diff_left
|
||||
|
||||
def initialize ()
|
||||
self.nb_line_left = ''
|
||||
self.nb_line_right = ''
|
||||
self.line_left = ''
|
||||
self.line_right = ''
|
||||
self.type_diff_right = ''
|
||||
self.type_diff_left = ''
|
||||
end
|
||||
|
||||
def inspect
|
||||
puts '### Start Line Diff ###'
|
||||
puts self.nb_line_left
|
||||
puts self.line_left
|
||||
puts self.nb_line_right
|
||||
puts self.line_right
|
||||
end
|
||||
end
|
||||
|
||||
class DiffTableList < Array
|
||||
def initialize (diff, type="inline")
|
||||
diff_table = DiffTable.new type
|
||||
diff.each do |line|
|
||||
if line =~ /^(---|\+\+\+) (.*)$/
|
||||
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
|
||||
|
||||
def initialize
|
||||
@lines = []
|
||||
@revisions = []
|
||||
end
|
||||
|
||||
def add_line(line, revision)
|
||||
@lines << line
|
||||
@revisions << revision
|
||||
end
|
||||
|
||||
def content
|
||||
content = lines.join("\n")
|
||||
end
|
||||
|
||||
def empty?
|
||||
lines.empty?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,185 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006-2007 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
require 'redmine/scm/adapters/abstract_adapter'
|
||||
|
||||
module Redmine
|
||||
module Scm
|
||||
module Adapters
|
||||
class BazaarAdapter < AbstractAdapter
|
||||
|
||||
# Bazaar executable name
|
||||
BZR_BIN = "bzr"
|
||||
|
||||
# Get info about the repository
|
||||
def info
|
||||
cmd = "#{BZR_BIN} revno #{target('')}"
|
||||
info = nil
|
||||
shellout(cmd) do |io|
|
||||
if io.read =~ %r{^(\d+)$}
|
||||
info = Info.new({:root_url => url,
|
||||
:lastrev => Revision.new({
|
||||
:identifier => $1
|
||||
})
|
||||
})
|
||||
end
|
||||
end
|
||||
return nil if $? && $?.exitstatus != 0
|
||||
info
|
||||
rescue CommandFailed
|
||||
return nil
|
||||
end
|
||||
|
||||
# Returns an Entries collection
|
||||
# or nil if the given path doesn't exist in the repository
|
||||
def entries(path=nil, identifier=nil)
|
||||
path ||= ''
|
||||
entries = Entries.new
|
||||
cmd = "#{BZR_BIN} ls -v --show-ids"
|
||||
cmd << " -r#{identifier.to_i}" if identifier && identifier.to_i > 0
|
||||
cmd << " #{target(path)}"
|
||||
shellout(cmd) do |io|
|
||||
prefix = "#{url}/#{path}".gsub('\\', '/')
|
||||
logger.debug "PREFIX: #{prefix}"
|
||||
re = %r{^V\s+#{Regexp.escape(prefix)}(\/?)([^\/]+)(\/?)\s+(\S+)$}
|
||||
io.each_line do |line|
|
||||
next unless line =~ re
|
||||
entries << Entry.new({:name => $2.strip,
|
||||
:path => ((path.empty? ? "" : "#{path}/") + $2.strip),
|
||||
:kind => ($3.blank? ? 'file' : 'dir'),
|
||||
:size => nil,
|
||||
:lastrev => Revision.new(:revision => $4.strip)
|
||||
})
|
||||
end
|
||||
end
|
||||
return nil if $? && $?.exitstatus != 0
|
||||
logger.debug("Found #{entries.size} entries in the repository for #{target(path)}") if logger && logger.debug?
|
||||
entries.sort_by_name
|
||||
end
|
||||
|
||||
def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
|
||||
path ||= ''
|
||||
identifier_from = 'last:1' unless identifier_from and identifier_from.to_i > 0
|
||||
identifier_to = 1 unless identifier_to and identifier_to.to_i > 0
|
||||
revisions = Revisions.new
|
||||
cmd = "#{BZR_BIN} log -v --show-ids -r#{identifier_to.to_i}..#{identifier_from} #{target(path)}"
|
||||
shellout(cmd) do |io|
|
||||
revision = nil
|
||||
parsing = nil
|
||||
io.each_line do |line|
|
||||
if line =~ /^----/
|
||||
revisions << revision if revision
|
||||
revision = Revision.new(:paths => [], :message => '')
|
||||
parsing = nil
|
||||
else
|
||||
next unless revision
|
||||
|
||||
if line =~ /^revno: (\d+)$/
|
||||
revision.identifier = $1.to_i
|
||||
elsif line =~ /^committer: (.+)$/
|
||||
revision.author = $1.strip
|
||||
elsif line =~ /^revision-id:(.+)$/
|
||||
revision.scmid = $1.strip
|
||||
elsif line =~ /^timestamp: (.+)$/
|
||||
revision.time = Time.parse($1).localtime
|
||||
elsif line =~ /^ -----/
|
||||
# partial revisions
|
||||
parsing = nil unless parsing == 'message'
|
||||
elsif line =~ /^(message|added|modified|removed|renamed):/
|
||||
parsing = $1
|
||||
elsif line =~ /^ (.*)$/
|
||||
if parsing == 'message'
|
||||
revision.message << "#{$1}\n"
|
||||
else
|
||||
if $1 =~ /^(.*)\s+(\S+)$/
|
||||
path = $1.strip
|
||||
revid = $2
|
||||
case parsing
|
||||
when 'added'
|
||||
revision.paths << {:action => 'A', :path => "/#{path}", :revision => revid}
|
||||
when 'modified'
|
||||
revision.paths << {:action => 'M', :path => "/#{path}", :revision => revid}
|
||||
when 'removed'
|
||||
revision.paths << {:action => 'D', :path => "/#{path}", :revision => revid}
|
||||
when 'renamed'
|
||||
new_path = path.split('=>').last
|
||||
revision.paths << {:action => 'M', :path => "/#{new_path.strip}", :revision => revid} if new_path
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
parsing = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
revisions << revision if revision
|
||||
end
|
||||
return nil if $? && $?.exitstatus != 0
|
||||
revisions
|
||||
end
|
||||
|
||||
def diff(path, identifier_from, identifier_to=nil, type="inline")
|
||||
path ||= ''
|
||||
if identifier_to
|
||||
identifier_to = identifier_to.to_i
|
||||
else
|
||||
identifier_to = identifier_from.to_i - 1
|
||||
end
|
||||
cmd = "#{BZR_BIN} diff -r#{identifier_to}..#{identifier_from} #{target(path)}"
|
||||
diff = []
|
||||
shellout(cmd) do |io|
|
||||
io.each_line do |line|
|
||||
diff << line
|
||||
end
|
||||
end
|
||||
#return nil if $? && $?.exitstatus != 0
|
||||
DiffTableList.new diff, type
|
||||
end
|
||||
|
||||
def cat(path, identifier=nil)
|
||||
cmd = "#{BZR_BIN} cat"
|
||||
cmd << " -r#{identifier.to_i}" if identifier && identifier.to_i > 0
|
||||
cmd << " #{target(path)}"
|
||||
cat = nil
|
||||
shellout(cmd) do |io|
|
||||
io.binmode
|
||||
cat = io.read
|
||||
end
|
||||
return nil if $? && $?.exitstatus != 0
|
||||
cat
|
||||
end
|
||||
|
||||
def annotate(path, identifier=nil)
|
||||
cmd = "#{BZR_BIN} annotate --all"
|
||||
cmd << " -r#{identifier.to_i}" if identifier && identifier.to_i > 0
|
||||
cmd << " #{target(path)}"
|
||||
blame = Annotate.new
|
||||
shellout(cmd) do |io|
|
||||
author = nil
|
||||
identifier = nil
|
||||
io.each_line do |line|
|
||||
next unless line =~ %r{^(\d+) ([^|]+)\| (.*)$}
|
||||
blame.add_line($3.rstrip, Revision.new(:identifier => $1.to_i, :author => $2.strip))
|
||||
end
|
||||
end
|
||||
return nil if $? && $?.exitstatus != 0
|
||||
blame
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,354 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006-2007 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
require 'redmine/scm/adapters/abstract_adapter'
|
||||
|
||||
module Redmine
|
||||
module Scm
|
||||
module Adapters
|
||||
class CvsAdapter < AbstractAdapter
|
||||
|
||||
# CVS executable name
|
||||
CVS_BIN = "cvs"
|
||||
|
||||
# Guidelines for the input:
|
||||
# url -> the project-path, relative to the cvsroot (eg. module name)
|
||||
# root_url -> the good old, sometimes damned, CVSROOT
|
||||
# login -> unnecessary
|
||||
# password -> unnecessary too
|
||||
def initialize(url, root_url=nil, login=nil, password=nil)
|
||||
@url = url
|
||||
@login = login if login && !login.empty?
|
||||
@password = (password || "") if @login
|
||||
#TODO: better Exception here (IllegalArgumentException)
|
||||
raise CommandFailed if root_url.blank?
|
||||
@root_url = root_url
|
||||
end
|
||||
|
||||
def root_url
|
||||
@root_url
|
||||
end
|
||||
|
||||
def url
|
||||
@url
|
||||
end
|
||||
|
||||
def info
|
||||
logger.debug "<cvs> info"
|
||||
Info.new({:root_url => @root_url, :lastrev => nil})
|
||||
end
|
||||
|
||||
def get_previous_revision(revision)
|
||||
CvsRevisionHelper.new(revision).prevRev
|
||||
end
|
||||
|
||||
# Returns an Entries collection
|
||||
# or nil if the given path doesn't exist in the repository
|
||||
# this method is used by the repository-browser (aka LIST)
|
||||
def entries(path=nil, identifier=nil)
|
||||
logger.debug "<cvs> entries '#{path}' with identifier '#{identifier}'"
|
||||
path_with_project="#{url}#{with_leading_slash(path)}"
|
||||
entries = Entries.new
|
||||
cmd = "#{CVS_BIN} -d #{root_url} rls -ed"
|
||||
cmd << " -D \"#{time_to_cvstime(identifier)}\"" if identifier
|
||||
cmd << " #{path_with_project}"
|
||||
shellout(cmd) do |io|
|
||||
io.each_line(){|line|
|
||||
fields=line.chop.split('/',-1)
|
||||
logger.debug(">>InspectLine #{fields.inspect}")
|
||||
|
||||
if fields[0]!="D"
|
||||
entries << Entry.new({:name => fields[-5],
|
||||
#:path => fields[-4].include?(path)?fields[-4]:(path + "/"+ fields[-4]),
|
||||
:path => "#{path}/#{fields[-5]}",
|
||||
:kind => 'file',
|
||||
:size => nil,
|
||||
:lastrev => Revision.new({
|
||||
:revision => fields[-4],
|
||||
:name => fields[-4],
|
||||
:time => Time.parse(fields[-3]),
|
||||
:author => ''
|
||||
})
|
||||
})
|
||||
else
|
||||
entries << Entry.new({:name => fields[1],
|
||||
:path => "#{path}/#{fields[1]}",
|
||||
:kind => 'dir',
|
||||
:size => nil,
|
||||
:lastrev => nil
|
||||
})
|
||||
end
|
||||
}
|
||||
end
|
||||
return nil if $? && $?.exitstatus != 0
|
||||
entries.sort_by_name
|
||||
end
|
||||
|
||||
STARTLOG="----------------------------"
|
||||
ENDLOG ="============================================================================="
|
||||
|
||||
# Returns all revisions found between identifier_from and identifier_to
|
||||
# in the repository. both identifier have to be dates or nil.
|
||||
# these method returns nothing but yield every result in block
|
||||
def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}, &block)
|
||||
logger.debug "<cvs> revisions path:'#{path}',identifier_from #{identifier_from}, identifier_to #{identifier_to}"
|
||||
|
||||
path_with_project="#{url}#{with_leading_slash(path)}"
|
||||
cmd = "#{CVS_BIN} -d #{root_url} rlog"
|
||||
cmd << " -d\">#{time_to_cvstime(identifier_from)}\"" if identifier_from
|
||||
cmd << " #{path_with_project}"
|
||||
shellout(cmd) do |io|
|
||||
state="entry_start"
|
||||
|
||||
commit_log=String.new
|
||||
revision=nil
|
||||
date=nil
|
||||
author=nil
|
||||
entry_path=nil
|
||||
entry_name=nil
|
||||
file_state=nil
|
||||
branch_map=nil
|
||||
|
||||
io.each_line() do |line|
|
||||
|
||||
if state!="revision" && /^#{ENDLOG}/ =~ line
|
||||
commit_log=String.new
|
||||
revision=nil
|
||||
state="entry_start"
|
||||
end
|
||||
|
||||
if state=="entry_start"
|
||||
branch_map=Hash.new
|
||||
# gsub(/^:.*@[^:]+:\d*/, '') is here to remove :pserver:anonymous@foo.bar: string if present in the url
|
||||
if /^RCS file: #{Regexp.escape(root_url.gsub(/^:.*@[^:]+:\d*/, ''))}\/#{Regexp.escape(path_with_project)}(.+),v$/ =~ line
|
||||
entry_path = normalize_cvs_path($1)
|
||||
entry_name = normalize_path(File.basename($1))
|
||||
logger.debug("Path #{entry_path} <=> Name #{entry_name}")
|
||||
elsif /^head: (.+)$/ =~ line
|
||||
entry_headRev = $1 #unless entry.nil?
|
||||
elsif /^symbolic names:/ =~ line
|
||||
state="symbolic" #unless entry.nil?
|
||||
elsif /^#{STARTLOG}/ =~ line
|
||||
commit_log=String.new
|
||||
state="revision"
|
||||
end
|
||||
next
|
||||
elsif state=="symbolic"
|
||||
if /^(.*):\s(.*)/ =~ (line.strip)
|
||||
branch_map[$1]=$2
|
||||
else
|
||||
state="tags"
|
||||
next
|
||||
end
|
||||
elsif state=="tags"
|
||||
if /^#{STARTLOG}/ =~ line
|
||||
commit_log = ""
|
||||
state="revision"
|
||||
elsif /^#{ENDLOG}/ =~ line
|
||||
state="head"
|
||||
end
|
||||
next
|
||||
elsif state=="revision"
|
||||
if /^#{ENDLOG}/ =~ line || /^#{STARTLOG}/ =~ line
|
||||
if revision
|
||||
|
||||
revHelper=CvsRevisionHelper.new(revision)
|
||||
revBranch="HEAD"
|
||||
|
||||
branch_map.each() do |branch_name,branch_point|
|
||||
if revHelper.is_in_branch_with_symbol(branch_point)
|
||||
revBranch=branch_name
|
||||
end
|
||||
end
|
||||
|
||||
logger.debug("********** YIELD Revision #{revision}::#{revBranch}")
|
||||
|
||||
yield Revision.new({
|
||||
:time => date,
|
||||
:author => author,
|
||||
:message=>commit_log.chomp,
|
||||
:paths => [{
|
||||
:revision => revision,
|
||||
:branch=> revBranch,
|
||||
:path=>entry_path,
|
||||
:name=>entry_name,
|
||||
:kind=>'file',
|
||||
:action=>file_state
|
||||
}]
|
||||
})
|
||||
end
|
||||
|
||||
commit_log=String.new
|
||||
revision=nil
|
||||
|
||||
if /^#{ENDLOG}/ =~ line
|
||||
state="entry_start"
|
||||
end
|
||||
next
|
||||
end
|
||||
|
||||
if /^branches: (.+)$/ =~ line
|
||||
#TODO: version.branch = $1
|
||||
elsif /^revision (\d+(?:\.\d+)+).*$/ =~ line
|
||||
revision = $1
|
||||
elsif /^date:\s+(\d+.\d+.\d+\s+\d+:\d+:\d+)/ =~ line
|
||||
date = Time.parse($1)
|
||||
author = /author: ([^;]+)/.match(line)[1]
|
||||
file_state = /state: ([^;]+)/.match(line)[1]
|
||||
#TODO: linechanges only available in CVS.... maybe a feature our SVN implementation. i'm sure, they are
|
||||
# useful for stats or something else
|
||||
# linechanges =/lines: \+(\d+) -(\d+)/.match(line)
|
||||
# unless linechanges.nil?
|
||||
# version.line_plus = linechanges[1]
|
||||
# version.line_minus = linechanges[2]
|
||||
# else
|
||||
# version.line_plus = 0
|
||||
# version.line_minus = 0
|
||||
# end
|
||||
else
|
||||
commit_log << line unless line =~ /^\*\*\* empty log message \*\*\*/
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def diff(path, identifier_from, identifier_to=nil, type="inline")
|
||||
logger.debug "<cvs> diff path:'#{path}',identifier_from #{identifier_from}, identifier_to #{identifier_to}"
|
||||
path_with_project="#{url}#{with_leading_slash(path)}"
|
||||
cmd = "#{CVS_BIN} -d #{root_url} rdiff -u -r#{identifier_to} -r#{identifier_from} #{path_with_project}"
|
||||
diff = []
|
||||
shellout(cmd) do |io|
|
||||
io.each_line do |line|
|
||||
diff << line
|
||||
end
|
||||
end
|
||||
return nil if $? && $?.exitstatus != 0
|
||||
DiffTableList.new diff, type
|
||||
end
|
||||
|
||||
def cat(path, identifier=nil)
|
||||
identifier = (identifier) ? identifier : "HEAD"
|
||||
logger.debug "<cvs> cat path:'#{path}',identifier #{identifier}"
|
||||
path_with_project="#{url}#{with_leading_slash(path)}"
|
||||
cmd = "#{CVS_BIN} -d #{root_url} co -r#{identifier} -p #{path_with_project}"
|
||||
cat = nil
|
||||
shellout(cmd) do |io|
|
||||
cat = io.read
|
||||
end
|
||||
return nil if $? && $?.exitstatus != 0
|
||||
cat
|
||||
end
|
||||
|
||||
def annotate(path, identifier=nil)
|
||||
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}"
|
||||
blame = Annotate.new
|
||||
shellout(cmd) do |io|
|
||||
io.each_line do |line|
|
||||
next unless line =~ %r{^([\d\.]+)\s+\(([^\)]+)\s+[^\)]+\):\s(.*)$}
|
||||
blame.add_line($3.rstrip, Revision.new(:revision => $1, :author => $2.strip))
|
||||
end
|
||||
end
|
||||
return nil if $? && $?.exitstatus != 0
|
||||
blame
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# convert a date/time into the CVS-format
|
||||
def time_to_cvstime(time)
|
||||
return nil if time.nil?
|
||||
unless time.kind_of? Time
|
||||
time = Time.parse(time)
|
||||
end
|
||||
return time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
end
|
||||
|
||||
def normalize_cvs_path(path)
|
||||
normalize_path(path.gsub(/Attic\//,''))
|
||||
end
|
||||
|
||||
def normalize_path(path)
|
||||
path.sub(/^(\/)*(.*)/,'\2').sub(/(.*)(,v)+/,'\1')
|
||||
end
|
||||
end
|
||||
|
||||
class CvsRevisionHelper
|
||||
attr_accessor :complete_rev, :revision, :base, :branchid
|
||||
|
||||
def initialize(complete_rev)
|
||||
@complete_rev = complete_rev
|
||||
parseRevision()
|
||||
end
|
||||
|
||||
def branchPoint
|
||||
return @base
|
||||
end
|
||||
|
||||
def branchVersion
|
||||
if isBranchRevision
|
||||
return @base+"."+@branchid
|
||||
end
|
||||
return @base
|
||||
end
|
||||
|
||||
def isBranchRevision
|
||||
!@branchid.nil?
|
||||
end
|
||||
|
||||
def prevRev
|
||||
unless @revision==0
|
||||
return buildRevision(@revision-1)
|
||||
end
|
||||
return buildRevision(@revision)
|
||||
end
|
||||
|
||||
def is_in_branch_with_symbol(branch_symbol)
|
||||
bpieces=branch_symbol.split(".")
|
||||
branch_start="#{bpieces[0..-3].join(".")}.#{bpieces[-1]}"
|
||||
return (branchVersion==branch_start)
|
||||
end
|
||||
|
||||
private
|
||||
def buildRevision(rev)
|
||||
if rev== 0
|
||||
@base
|
||||
elsif @branchid.nil?
|
||||
@base+"."+rev.to_s
|
||||
else
|
||||
@base+"."+@branchid+"."+rev.to_s
|
||||
end
|
||||
end
|
||||
|
||||
# Interpretiert die cvs revisionsnummern wie z.b. 1.14 oder 1.3.0.15
|
||||
def parseRevision()
|
||||
pieces=@complete_rev.split(".")
|
||||
@revision=pieces.last.to_i
|
||||
baseSize=1
|
||||
baseSize+=(pieces.size/2)
|
||||
@base=pieces[0..-baseSize].join(".")
|
||||
if baseSize > 2
|
||||
@branchid=pieces[-2]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,156 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006-2007 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
require 'redmine/scm/adapters/abstract_adapter'
|
||||
require 'rexml/document'
|
||||
|
||||
module Redmine
|
||||
module Scm
|
||||
module Adapters
|
||||
class DarcsAdapter < AbstractAdapter
|
||||
# Darcs executable name
|
||||
DARCS_BIN = "darcs"
|
||||
|
||||
def initialize(url, root_url=nil, login=nil, password=nil)
|
||||
@url = url
|
||||
@root_url = url
|
||||
end
|
||||
|
||||
def supports_cat?
|
||||
false
|
||||
end
|
||||
|
||||
# Get info about the svn repository
|
||||
def info
|
||||
rev = revisions(nil,nil,nil,{:limit => 1})
|
||||
rev ? Info.new({:root_url => @url, :lastrev => rev.last}) : nil
|
||||
end
|
||||
|
||||
# Returns an Entries collection
|
||||
# or nil if the given path doesn't exist in the repository
|
||||
def entries(path=nil, identifier=nil)
|
||||
path_prefix = (path.blank? ? '' : "#{path}/")
|
||||
path = '.' if path.blank?
|
||||
entries = Entries.new
|
||||
cmd = "#{DARCS_BIN} annotate --repodir #{@url} --xml-output"
|
||||
cmd << " --match \"hash #{identifier}\"" if identifier
|
||||
cmd << " #{path}"
|
||||
shellout(cmd) do |io|
|
||||
begin
|
||||
doc = REXML::Document.new(io)
|
||||
if doc.root.name == 'directory'
|
||||
doc.elements.each('directory/*') do |element|
|
||||
next unless ['file', 'directory'].include? element.name
|
||||
entries << entry_from_xml(element, path_prefix)
|
||||
end
|
||||
elsif doc.root.name == 'file'
|
||||
entries << entry_from_xml(doc.root, path_prefix)
|
||||
end
|
||||
rescue
|
||||
end
|
||||
end
|
||||
return nil if $? && $?.exitstatus != 0
|
||||
entries.sort_by_name
|
||||
end
|
||||
|
||||
def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
|
||||
path = '.' if path.blank?
|
||||
revisions = Revisions.new
|
||||
cmd = "#{DARCS_BIN} changes --repodir #{@url} --xml-output"
|
||||
cmd << " --from-match \"hash #{identifier_from}\"" if identifier_from
|
||||
cmd << " --last #{options[:limit].to_i}" if options[:limit]
|
||||
shellout(cmd) do |io|
|
||||
begin
|
||||
doc = REXML::Document.new(io)
|
||||
doc.elements.each("changelog/patch") do |patch|
|
||||
message = patch.elements['name'].text
|
||||
message << "\n" + patch.elements['comment'].text.gsub(/\*\*\*END OF DESCRIPTION\*\*\*.*\z/m, '') if patch.elements['comment']
|
||||
revisions << Revision.new({:identifier => nil,
|
||||
:author => patch.attributes['author'],
|
||||
:scmid => patch.attributes['hash'],
|
||||
:time => Time.parse(patch.attributes['local_date']),
|
||||
:message => message,
|
||||
:paths => (options[:with_path] ? get_paths_for_patch(patch.attributes['hash']) : nil)
|
||||
})
|
||||
end
|
||||
rescue
|
||||
end
|
||||
end
|
||||
return nil if $? && $?.exitstatus != 0
|
||||
revisions
|
||||
end
|
||||
|
||||
def diff(path, identifier_from, identifier_to=nil, type="inline")
|
||||
path = '*' if path.blank?
|
||||
cmd = "#{DARCS_BIN} diff --repodir #{@url}"
|
||||
if identifier_to.nil?
|
||||
cmd << " --match \"hash #{identifier_from}\""
|
||||
else
|
||||
cmd << " --to-match \"hash #{identifier_from}\""
|
||||
cmd << " --from-match \"hash #{identifier_to}\""
|
||||
end
|
||||
cmd << " -u #{path}"
|
||||
diff = []
|
||||
shellout(cmd) do |io|
|
||||
io.each_line do |line|
|
||||
diff << line
|
||||
end
|
||||
end
|
||||
return nil if $? && $?.exitstatus != 0
|
||||
DiffTableList.new diff, type
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def entry_from_xml(element, path_prefix)
|
||||
Entry.new({:name => element.attributes['name'],
|
||||
:path => path_prefix + element.attributes['name'],
|
||||
:kind => element.name == 'file' ? 'file' : 'dir',
|
||||
:size => nil,
|
||||
:lastrev => Revision.new({
|
||||
:identifier => nil,
|
||||
:scmid => element.elements['modified'].elements['patch'].attributes['hash']
|
||||
})
|
||||
})
|
||||
end
|
||||
|
||||
# Retrieve changed paths for a single patch
|
||||
def get_paths_for_patch(hash)
|
||||
cmd = "#{DARCS_BIN} annotate --repodir #{@url} --summary --xml-output"
|
||||
cmd << " --match \"hash #{hash}\" "
|
||||
paths = []
|
||||
shellout(cmd) do |io|
|
||||
begin
|
||||
# Darcs xml output has multiple root elements in this case (tested with darcs 1.0.7)
|
||||
# A root element is added so that REXML doesn't raise an error
|
||||
doc = REXML::Document.new("<fake_root>" + io.read + "</fake_root>")
|
||||
doc.elements.each('fake_root/summary/*') do |modif|
|
||||
paths << {:action => modif.name[0,1].upcase,
|
||||
:path => "/" + modif.text.chomp.gsub(/^\s*/, '')
|
||||
}
|
||||
end
|
||||
rescue
|
||||
end
|
||||
end
|
||||
paths
|
||||
rescue CommandFailed
|
||||
paths
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,256 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006-2007 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
require 'redmine/scm/adapters/abstract_adapter'
|
||||
|
||||
module Redmine
|
||||
module Scm
|
||||
module Adapters
|
||||
class GitAdapter < AbstractAdapter
|
||||
|
||||
# Git executable name
|
||||
GIT_BIN = "git"
|
||||
|
||||
# Get the revision of a particuliar file
|
||||
def get_rev (rev,path)
|
||||
cmd="git --git-dir #{target('')} show #{shell_quote rev} -- #{shell_quote path}" if rev!='latest' and (! rev.nil?)
|
||||
cmd="git --git-dir #{target('')} log -1 master -- #{shell_quote path}" if
|
||||
rev=='latest' or rev.nil?
|
||||
rev=[]
|
||||
i=0
|
||||
shellout(cmd) do |io|
|
||||
files=[]
|
||||
changeset = {}
|
||||
parsing_descr = 0 #0: not parsing desc or files, 1: parsing desc, 2: parsing files
|
||||
|
||||
io.each_line do |line|
|
||||
if line =~ /^commit ([0-9a-f]{40})$/
|
||||
key = "commit"
|
||||
value = $1
|
||||
if (parsing_descr == 1 || parsing_descr == 2)
|
||||
parsing_descr = 0
|
||||
rev = Revision.new({:identifier => changeset[:commit],
|
||||
:scmid => changeset[:commit],
|
||||
:author => changeset[:author],
|
||||
:time => Time.parse(changeset[:date]),
|
||||
:message => changeset[:description],
|
||||
:paths => files
|
||||
})
|
||||
changeset = {}
|
||||
files = []
|
||||
end
|
||||
changeset[:commit] = $1
|
||||
elsif (parsing_descr == 0) && line =~ /^(\w+):\s*(.*)$/
|
||||
key = $1
|
||||
value = $2
|
||||
if key == "Author"
|
||||
changeset[:author] = value
|
||||
elsif key == "Date"
|
||||
changeset[:date] = value
|
||||
end
|
||||
elsif (parsing_descr == 0) && line.chomp.to_s == ""
|
||||
parsing_descr = 1
|
||||
changeset[:description] = ""
|
||||
elsif (parsing_descr == 1 || parsing_descr == 2) && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\s+(.+)$/
|
||||
parsing_descr = 2
|
||||
fileaction = $1
|
||||
filepath = $2
|
||||
files << {:action => fileaction, :path => filepath}
|
||||
elsif (parsing_descr == 1) && line.chomp.to_s == ""
|
||||
parsing_descr = 2
|
||||
elsif (parsing_descr == 1)
|
||||
changeset[:description] << line
|
||||
end
|
||||
end
|
||||
rev = Revision.new({:identifier => changeset[:commit],
|
||||
:scmid => changeset[:commit],
|
||||
:author => changeset[:author],
|
||||
:time => (changeset[:date] ? Time.parse(changeset[:date]) : nil),
|
||||
:message => changeset[:description],
|
||||
:paths => files
|
||||
})
|
||||
|
||||
end
|
||||
|
||||
get_rev('latest',path) if rev == []
|
||||
|
||||
return nil if $? && $?.exitstatus != 0
|
||||
return rev
|
||||
end
|
||||
|
||||
|
||||
def info
|
||||
revs = revisions(url,nil,nil,{:limit => 1})
|
||||
if revs && revs.any?
|
||||
Info.new(:root_url => url, :lastrev => revs.first)
|
||||
else
|
||||
nil
|
||||
end
|
||||
rescue Errno::ENOENT => e
|
||||
return nil
|
||||
end
|
||||
|
||||
def entries(path=nil, identifier=nil)
|
||||
path ||= ''
|
||||
entries = Entries.new
|
||||
cmd = "#{GIT_BIN} --git-dir #{target('')} ls-tree -l "
|
||||
cmd << shell_quote("HEAD:" + path) if identifier.nil?
|
||||
cmd << shell_quote(identifier + ":" + path) if identifier
|
||||
shellout(cmd) do |io|
|
||||
io.each_line do |line|
|
||||
e = line.chomp.to_s
|
||||
if e =~ /^\d+\s+(\w+)\s+([0-9a-f]{40})\s+([0-9-]+)\s+(.+)$/
|
||||
type = $1
|
||||
sha = $2
|
||||
size = $3
|
||||
name = $4
|
||||
entries << Entry.new({:name => name,
|
||||
:path => (path.empty? ? name : "#{path}/#{name}"),
|
||||
:kind => ((type == "tree") ? 'dir' : 'file'),
|
||||
:size => ((type == "tree") ? nil : size),
|
||||
:lastrev => get_rev(identifier,(path.empty? ? name : "#{path}/#{name}"))
|
||||
|
||||
}) unless entries.detect{|entry| entry.name == name}
|
||||
end
|
||||
end
|
||||
end
|
||||
return nil if $? && $?.exitstatus != 0
|
||||
entries.sort_by_name
|
||||
end
|
||||
|
||||
def revisions(path, identifier_from, identifier_to, options={})
|
||||
revisions = Revisions.new
|
||||
cmd = "#{GIT_BIN} --git-dir #{target('')} log --raw "
|
||||
cmd << " -n #{options[:limit].to_i} " if (!options.nil?) && options[:limit]
|
||||
cmd << " #{shell_quote(identifier_from + '..')} " if identifier_from
|
||||
cmd << " #{shell_quote identifier_to} " if identifier_to
|
||||
#cmd << " HEAD " if !identifier_to
|
||||
shellout(cmd) do |io|
|
||||
files=[]
|
||||
changeset = {}
|
||||
parsing_descr = 0 #0: not parsing desc or files, 1: parsing desc, 2: parsing files
|
||||
revno = 1
|
||||
|
||||
io.each_line do |line|
|
||||
if line =~ /^commit ([0-9a-f]{40})$/
|
||||
key = "commit"
|
||||
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
|
||||
})
|
||||
changeset = {}
|
||||
files = []
|
||||
revno = revno + 1
|
||||
end
|
||||
changeset[:commit] = $1
|
||||
elsif (parsing_descr == 0) && line =~ /^(\w+):\s*(.*)$/
|
||||
key = $1
|
||||
value = $2
|
||||
if key == "Author"
|
||||
changeset[:author] = value
|
||||
elsif key == "Date"
|
||||
changeset[:date] = value
|
||||
end
|
||||
elsif (parsing_descr == 0) && line.chomp.to_s == ""
|
||||
parsing_descr = 1
|
||||
changeset[:description] = ""
|
||||
elsif (parsing_descr == 1 || parsing_descr == 2) && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\s+(.+)$/
|
||||
parsing_descr = 2
|
||||
fileaction = $1
|
||||
filepath = $2
|
||||
files << {:action => fileaction, :path => filepath}
|
||||
elsif (parsing_descr == 1) && line.chomp.to_s == ""
|
||||
parsing_descr = 2
|
||||
elsif (parsing_descr == 1)
|
||||
changeset[:description] << line[4..-1]
|
||||
end
|
||||
end
|
||||
|
||||
revisions << Revision.new({:identifier => changeset[:commit],
|
||||
:scmid => changeset[:commit],
|
||||
:author => changeset[:author],
|
||||
:time => Time.parse(changeset[:date]),
|
||||
:message => changeset[:description],
|
||||
:paths => files
|
||||
}) if changeset[:commit]
|
||||
|
||||
end
|
||||
|
||||
return nil if $? && $?.exitstatus != 0
|
||||
revisions
|
||||
end
|
||||
|
||||
def diff(path, identifier_from, identifier_to=nil, type="inline")
|
||||
path ||= ''
|
||||
if !identifier_to
|
||||
identifier_to = nil
|
||||
end
|
||||
|
||||
cmd = "#{GIT_BIN} --git-dir #{target('')} show #{shell_quote identifier_from}" if identifier_to.nil?
|
||||
cmd = "#{GIT_BIN} --git-dir #{target('')} diff #{shell_quote identifier_to} #{shell_quote identifier_from}" if !identifier_to.nil?
|
||||
cmd << " -- #{shell_quote path}" unless path.empty?
|
||||
diff = []
|
||||
shellout(cmd) do |io|
|
||||
io.each_line do |line|
|
||||
diff << line
|
||||
end
|
||||
end
|
||||
return nil if $? && $?.exitstatus != 0
|
||||
DiffTableList.new diff, type
|
||||
end
|
||||
|
||||
def annotate(path, identifier=nil)
|
||||
identifier = 'HEAD' if identifier.blank?
|
||||
cmd = "#{GIT_BIN} --git-dir #{target('')} blame -l #{shell_quote identifier} -- #{shell_quote path}"
|
||||
blame = Annotate.new
|
||||
content = nil
|
||||
shellout(cmd) { |io| io.binmode; content = io.read }
|
||||
return nil if $? && $?.exitstatus != 0
|
||||
# git annotates binary files
|
||||
return nil if content.is_binary_data?
|
||||
content.split("\n").each do |line|
|
||||
next unless line =~ /([0-9a-f]{39,40})\s\((\w*)[^\)]*\)(.*)/
|
||||
blame.add_line($3.rstrip, Revision.new(:identifier => $1, :author => $2.strip))
|
||||
end
|
||||
blame
|
||||
end
|
||||
|
||||
def cat(path, identifier=nil)
|
||||
if identifier.nil?
|
||||
identifier = 'HEAD'
|
||||
end
|
||||
cmd = "#{GIT_BIN} --git-dir #{target('')} show #{shell_quote(identifier + ':' + path)}"
|
||||
cat = nil
|
||||
shellout(cmd) do |io|
|
||||
io.binmode
|
||||
cat = io.read
|
||||
end
|
||||
return nil if $? && $?.exitstatus != 0
|
||||
cat
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006-2007 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
require 'redmine/scm/adapters/abstract_adapter'
|
||||
|
||||
module Redmine
|
||||
module Scm
|
||||
module Adapters
|
||||
class MercurialAdapter < AbstractAdapter
|
||||
|
||||
# Mercurial executable name
|
||||
HG_BIN = "hg"
|
||||
|
||||
def info
|
||||
cmd = "#{HG_BIN} -R #{target('')} root"
|
||||
root_url = nil
|
||||
shellout(cmd) do |io|
|
||||
root_url = io.gets
|
||||
end
|
||||
return nil if $? && $?.exitstatus != 0
|
||||
info = Info.new({:root_url => root_url.chomp,
|
||||
:lastrev => revisions(nil,nil,nil,{:limit => 1}).last
|
||||
})
|
||||
info
|
||||
rescue CommandFailed
|
||||
return nil
|
||||
end
|
||||
|
||||
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:**')
|
||||
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}
|
||||
end
|
||||
end
|
||||
return nil if $? && $?.exitstatus != 0
|
||||
entries.sort_by_name
|
||||
end
|
||||
|
||||
def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
|
||||
revisions = Revisions.new
|
||||
cmd = "#{HG_BIN} -v --encoding utf8 -R #{target('')} log"
|
||||
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]
|
||||
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
|
||||
end
|
||||
end
|
||||
end
|
||||
if parsing_descr
|
||||
changeset[:description] << line
|
||||
line_feeds += 1 if line.chomp.empty?
|
||||
end
|
||||
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")
|
||||
path ||= ''
|
||||
if identifier_to
|
||||
identifier_to = identifier_to.to_i
|
||||
else
|
||||
identifier_to = identifier_from.to_i - 1
|
||||
end
|
||||
cmd = "#{HG_BIN} -R #{target('')} diff -r #{identifier_to} -r #{identifier_from} --nodates"
|
||||
cmd << " -I #{target(path)}" unless path.empty?
|
||||
diff = []
|
||||
shellout(cmd) do |io|
|
||||
io.each_line do |line|
|
||||
diff << line
|
||||
end
|
||||
end
|
||||
return nil if $? && $?.exitstatus != 0
|
||||
DiffTableList.new diff, type
|
||||
end
|
||||
|
||||
def cat(path, identifier=nil)
|
||||
cmd = "#{HG_BIN} -R #{target('')} cat"
|
||||
cmd << " -r #{identifier.to_i}" if identifier
|
||||
cmd << " #{target(path)}"
|
||||
cat = nil
|
||||
shellout(cmd) do |io|
|
||||
io.binmode
|
||||
cat = io.read
|
||||
end
|
||||
return nil if $? && $?.exitstatus != 0
|
||||
cat
|
||||
end
|
||||
|
||||
def annotate(path, identifier=nil)
|
||||
path ||= ''
|
||||
cmd = "#{HG_BIN} -R #{target('')}"
|
||||
cmd << " annotate -n -u"
|
||||
cmd << " -r #{identifier.to_i}" if identifier
|
||||
cmd << " #{target(path)}"
|
||||
blame = Annotate.new
|
||||
shellout(cmd) do |io|
|
||||
io.each_line do |line|
|
||||
next unless line =~ %r{^([^:]+)\s(\d+):(.*)$}
|
||||
blame.add_line($3.rstrip, Revision.new(:identifier => $2.to_i, :author => $1.strip))
|
||||
end
|
||||
end
|
||||
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
|
||||
end
|
||||
@@ -0,0 +1,184 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006-2007 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
require 'redmine/scm/adapters/abstract_adapter'
|
||||
require 'rexml/document'
|
||||
|
||||
module Redmine
|
||||
module Scm
|
||||
module Adapters
|
||||
class SubversionAdapter < AbstractAdapter
|
||||
|
||||
# SVN executable name
|
||||
SVN_BIN = "svn"
|
||||
|
||||
# Get info about the svn repository
|
||||
def info
|
||||
cmd = "#{SVN_BIN} info --xml #{target('')}"
|
||||
cmd << credentials_string
|
||||
info = nil
|
||||
shellout(cmd) do |io|
|
||||
begin
|
||||
doc = REXML::Document.new(io)
|
||||
#root_url = doc.elements["info/entry/repository/root"].text
|
||||
info = Info.new({:root_url => doc.elements["info/entry/repository/root"].text,
|
||||
:lastrev => Revision.new({
|
||||
:identifier => doc.elements["info/entry/commit"].attributes['revision'],
|
||||
:time => Time.parse(doc.elements["info/entry/commit/date"].text).localtime,
|
||||
:author => (doc.elements["info/entry/commit/author"] ? doc.elements["info/entry/commit/author"].text : "")
|
||||
})
|
||||
})
|
||||
rescue
|
||||
end
|
||||
end
|
||||
return nil if $? && $?.exitstatus != 0
|
||||
info
|
||||
rescue CommandFailed
|
||||
return nil
|
||||
end
|
||||
|
||||
# Returns an Entries collection
|
||||
# or nil if the given path doesn't exist in the repository
|
||||
def entries(path=nil, identifier=nil)
|
||||
path ||= ''
|
||||
identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
|
||||
entries = Entries.new
|
||||
cmd = "#{SVN_BIN} list --xml #{target(path)}@#{identifier}"
|
||||
cmd << credentials_string
|
||||
shellout(cmd) do |io|
|
||||
output = io.read
|
||||
begin
|
||||
doc = REXML::Document.new(output)
|
||||
doc.elements.each("lists/list/entry") do |entry|
|
||||
entries << Entry.new({:name => entry.elements['name'].text,
|
||||
:path => ((path.empty? ? "" : "#{path}/") + entry.elements['name'].text),
|
||||
:kind => entry.attributes['kind'],
|
||||
:size => (entry.elements['size'] and entry.elements['size'].text).to_i,
|
||||
:lastrev => Revision.new({
|
||||
:identifier => entry.elements['commit'].attributes['revision'],
|
||||
:time => Time.parse(entry.elements['commit'].elements['date'].text).localtime,
|
||||
:author => (entry.elements['commit'].elements['author'] ? entry.elements['commit'].elements['author'].text : "")
|
||||
})
|
||||
})
|
||||
end
|
||||
rescue Exception => e
|
||||
logger.error("Error parsing svn output: #{e.message}")
|
||||
logger.error("Output was:\n #{output}")
|
||||
end
|
||||
end
|
||||
return nil if $? && $?.exitstatus != 0
|
||||
logger.debug("Found #{entries.size} entries in the repository for #{target(path)}") if logger && logger.debug?
|
||||
entries.sort_by_name
|
||||
end
|
||||
|
||||
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"
|
||||
identifier_to = (identifier_to and identifier_to.to_i > 0) ? identifier_to.to_i : 1
|
||||
revisions = Revisions.new
|
||||
cmd = "#{SVN_BIN} log --xml -r #{identifier_from}:#{identifier_to}"
|
||||
cmd << credentials_string
|
||||
cmd << " --verbose " if options[:with_paths]
|
||||
cmd << ' ' + target(path)
|
||||
shellout(cmd) do |io|
|
||||
begin
|
||||
doc = REXML::Document.new(io)
|
||||
doc.elements.each("log/logentry") do |logentry|
|
||||
paths = []
|
||||
logentry.elements.each("paths/path") do |path|
|
||||
paths << {:action => path.attributes['action'],
|
||||
:path => path.text,
|
||||
:from_path => path.attributes['copyfrom-path'],
|
||||
:from_revision => path.attributes['copyfrom-rev']
|
||||
}
|
||||
end
|
||||
paths.sort! { |x,y| x[:path] <=> y[:path] }
|
||||
|
||||
revisions << Revision.new({:identifier => logentry.attributes['revision'],
|
||||
:author => (logentry.elements['author'] ? logentry.elements['author'].text : ""),
|
||||
:time => Time.parse(logentry.elements['date'].text).localtime,
|
||||
:message => logentry.elements['msg'].text,
|
||||
:paths => paths
|
||||
})
|
||||
end
|
||||
rescue
|
||||
end
|
||||
end
|
||||
return nil if $? && $?.exitstatus != 0
|
||||
revisions
|
||||
end
|
||||
|
||||
def diff(path, identifier_from, identifier_to=nil, type="inline")
|
||||
path ||= ''
|
||||
identifier_from = (identifier_from and identifier_from.to_i > 0) ? identifier_from.to_i : ''
|
||||
identifier_to = (identifier_to and identifier_to.to_i > 0) ? identifier_to.to_i : (identifier_from.to_i - 1)
|
||||
|
||||
cmd = "#{SVN_BIN} diff -r "
|
||||
cmd << "#{identifier_to}:"
|
||||
cmd << "#{identifier_from}"
|
||||
cmd << " #{target(path)}@#{identifier_from}"
|
||||
cmd << credentials_string
|
||||
diff = []
|
||||
shellout(cmd) do |io|
|
||||
io.each_line do |line|
|
||||
diff << line
|
||||
end
|
||||
end
|
||||
return nil if $? && $?.exitstatus != 0
|
||||
DiffTableList.new diff, type
|
||||
end
|
||||
|
||||
def cat(path, identifier=nil)
|
||||
identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
|
||||
cmd = "#{SVN_BIN} cat #{target(path)}@#{identifier}"
|
||||
cmd << credentials_string
|
||||
cat = nil
|
||||
shellout(cmd) do |io|
|
||||
io.binmode
|
||||
cat = io.read
|
||||
end
|
||||
return nil if $? && $?.exitstatus != 0
|
||||
cat
|
||||
end
|
||||
|
||||
def annotate(path, identifier=nil)
|
||||
identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
|
||||
cmd = "#{SVN_BIN} blame #{target(path)}@#{identifier}"
|
||||
cmd << credentials_string
|
||||
blame = Annotate.new
|
||||
shellout(cmd) do |io|
|
||||
io.each_line do |line|
|
||||
next unless line =~ %r{^\s*(\d+)\s*(\S+)\s(.*)$}
|
||||
blame.add_line($3.rstrip, Revision.new(:identifier => $1.to_i, :author => $2.strip))
|
||||
end
|
||||
end
|
||||
return nil if $? && $?.exitstatus != 0
|
||||
blame
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def credentials_string
|
||||
str = ''
|
||||
str << " --username #{shell_quote(@login)}" unless @login.blank?
|
||||
str << " --password #{shell_quote(@password)}" unless @login.blank? || @password.blank?
|
||||
str
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,76 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006-2007 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
module Redmine
|
||||
module Themes
|
||||
|
||||
# Return an array of installed themes
|
||||
def self.themes
|
||||
@@installed_themes ||= scan_themes
|
||||
end
|
||||
|
||||
# Rescan themes directory
|
||||
def self.rescan
|
||||
@@installed_themes = scan_themes
|
||||
end
|
||||
|
||||
# Return theme for given id, or nil if it's not found
|
||||
def self.theme(id)
|
||||
themes.find {|t| t.id == id}
|
||||
end
|
||||
|
||||
# Class used to represent a theme
|
||||
class Theme
|
||||
attr_reader :name, :dir, :stylesheets
|
||||
|
||||
def initialize(path)
|
||||
@dir = File.basename(path)
|
||||
@name = @dir.humanize
|
||||
@stylesheets = Dir.glob("#{path}/stylesheets/*.css").collect {|f| File.basename(f).gsub(/\.css$/, '')}
|
||||
end
|
||||
|
||||
# Directory name used as the theme id
|
||||
def id; dir end
|
||||
|
||||
def <=>(theme)
|
||||
name <=> theme.name
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def self.scan_themes
|
||||
dirs = Dir.glob("#{RAILS_ROOT}/public/themes/*").select do |f|
|
||||
# A theme should at least override application.css
|
||||
File.directory?(f) && File.exist?("#{f}/stylesheets/application.css")
|
||||
end
|
||||
dirs.collect {|dir| Theme.new(dir)}.sort
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module ApplicationHelper
|
||||
def stylesheet_path(source)
|
||||
@current_theme ||= Redmine::Themes.theme(Setting.ui_theme)
|
||||
super((@current_theme && @current_theme.stylesheets.include?(source)) ?
|
||||
"/themes/#{@current_theme.dir}/stylesheets/#{source}" : source)
|
||||
end
|
||||
|
||||
def path_to_stylesheet(source)
|
||||
stylesheet_path source
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,35 @@
|
||||
require 'rexml/document'
|
||||
|
||||
module Redmine
|
||||
module VERSION #:nodoc:
|
||||
MAJOR = 0
|
||||
MINOR = 7
|
||||
TINY = 'devel'
|
||||
|
||||
def self.revision
|
||||
revision = nil
|
||||
entries_path = "#{RAILS_ROOT}/.svn/entries"
|
||||
if File.readable?(entries_path)
|
||||
begin
|
||||
f = File.open(entries_path, 'r')
|
||||
entries = f.read
|
||||
f.close
|
||||
if entries.match(%r{^\d+})
|
||||
revision = $1.to_i if entries.match(%r{^\d+\s+dir\s+(\d+)\s})
|
||||
else
|
||||
xml = REXML::Document.new(entries)
|
||||
revision = xml.elements['wc-entries'].elements[1].attributes['revision'].to_i
|
||||
end
|
||||
rescue
|
||||
# Could not find the current revision
|
||||
end
|
||||
end
|
||||
revision
|
||||
end
|
||||
|
||||
REVISION = self.revision
|
||||
STRING = [MAJOR, MINOR, TINY, REVISION].compact.join('.')
|
||||
|
||||
def self.to_s; STRING end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,168 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006-2007 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
require 'redcloth'
|
||||
require 'coderay'
|
||||
|
||||
module Redmine
|
||||
module WikiFormatting
|
||||
|
||||
private
|
||||
|
||||
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]
|
||||
|
||||
def initialize(*args)
|
||||
super
|
||||
self.hard_breaks=true
|
||||
self.no_span_caps=true
|
||||
end
|
||||
|
||||
def to_html(*rules, &block)
|
||||
@toc = []
|
||||
@macros_runner = block
|
||||
super(*RULES).to_s
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# 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
|
||||
end
|
||||
|
||||
# Patch to add code highlighting support to RedCloth
|
||||
def smooth_offtags( text )
|
||||
unless @pre_list.empty?
|
||||
## replace <pre> content
|
||||
text.gsub!(/<redpre#(\d+)>/) do
|
||||
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)
|
||||
end
|
||||
content
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# 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]
|
||||
end
|
||||
content = "<a name=\"#{@toc.length}\" class=\"wiki-page\"></a>" + content
|
||||
textile_p(tag, atts, cite, content)
|
||||
end
|
||||
|
||||
alias :textile_h1 :textile_p_withtoc
|
||||
alias :textile_h2 :textile_p_withtoc
|
||||
alias :textile_h3 :textile_p_withtoc
|
||||
|
||||
def inline_toc(text)
|
||||
text.gsub!(/<p>\{\{([<>]?)toc\}\}<\/p>/i) do
|
||||
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>"
|
||||
end
|
||||
out << '</div>'
|
||||
out
|
||||
end
|
||||
end
|
||||
|
||||
MACROS_RE = /
|
||||
(!)? # escaping
|
||||
(
|
||||
\{\{ # opening tag
|
||||
([\w]+) # macro name
|
||||
(\(([^\}]*)\))? # optional arguments
|
||||
\}\} # closing tag
|
||||
)
|
||||
/x unless const_defined?(:MACROS_RE)
|
||||
|
||||
def inline_macros(text)
|
||||
text.gsub!(MACROS_RE) do
|
||||
esc, all, macro = $1, $2, $3.downcase
|
||||
args = ($5 || '').split(',').each(&:strip)
|
||||
if esc.nil?
|
||||
begin
|
||||
@macros_runner.call(macro, args)
|
||||
rescue => e
|
||||
"<div class=\"flash error\">Error executing the <strong>#{macro}</strong> macro (#{e})</div>"
|
||||
end || all
|
||||
else
|
||||
all
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
AUTO_LINK_RE = %r{
|
||||
( # leading text
|
||||
<\w+.*?>| # leading HTML tag, or
|
||||
[^=<>!:'"/]| # leading punctuation, or
|
||||
^ # beginning of line
|
||||
)
|
||||
(
|
||||
(?:https?://)| # protocol spec, or
|
||||
(?:www\.) # www.*
|
||||
)
|
||||
(
|
||||
(\S+?) # url
|
||||
(\/)? # slash
|
||||
)
|
||||
([^\w\=\/;]*?) # post
|
||||
(?=<|\s|$)
|
||||
}x unless const_defined?(:AUTO_LINK_RE)
|
||||
|
||||
# Turns all urls into clickable links (code from Rails).
|
||||
def inline_auto_link(text)
|
||||
text.gsub!(AUTO_LINK_RE) do
|
||||
all, leading, proto, url, post = $&, $1, $2, $3, $6
|
||||
if leading =~ /<a\s/i || leading =~ /![<>=]?/
|
||||
# don't replace URL's that are already linked
|
||||
# and URL's prefixed with ! !> !< != (textile images)
|
||||
all
|
||||
else
|
||||
%(#{leading}<a class="external" href="#{proto=="www."?"http://www.":proto}#{url}">#{proto + url}</a>#{post})
|
||||
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>}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
public
|
||||
|
||||
def self.to_html(text, options = {}, &block)
|
||||
TextileFormatter.new(text).to_html(&block)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,101 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006-2007 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
module Redmine
|
||||
module WikiFormatting
|
||||
module Macros
|
||||
module Definitions
|
||||
def exec_macro(name, obj, args)
|
||||
method_name = "macro_#{name}"
|
||||
send(method_name, obj, args) if respond_to?(method_name)
|
||||
end
|
||||
end
|
||||
|
||||
@@available_macros = {}
|
||||
|
||||
class << self
|
||||
# Called with a block to define additional macros.
|
||||
# Macro blocks accept 2 arguments:
|
||||
# * obj: the object that is rendered
|
||||
# * args: macro arguments
|
||||
#
|
||||
# Plugins can use this method to define new macros:
|
||||
#
|
||||
# Redmine::WikiFormatting::Macros.register do
|
||||
# desc "This is my macro"
|
||||
# macro :my_macro do |obj, args|
|
||||
# "My macro output"
|
||||
# end
|
||||
# end
|
||||
def register(&block)
|
||||
class_eval(&block) if block_given?
|
||||
end
|
||||
|
||||
private
|
||||
# Defines a new macro with the given name and block.
|
||||
def macro(name, &block)
|
||||
name = name.to_sym if name.is_a?(String)
|
||||
@@available_macros[name] = @@desc || ''
|
||||
@@desc = nil
|
||||
raise "Can not create a macro without a block!" unless block_given?
|
||||
Definitions.send :define_method, "macro_#{name}".downcase, &block
|
||||
end
|
||||
|
||||
# Sets description for the next macro to be defined
|
||||
def desc(txt)
|
||||
@@desc = txt
|
||||
end
|
||||
end
|
||||
|
||||
# Builtin macros
|
||||
desc "Sample macro."
|
||||
macro :hello_world do |obj, args|
|
||||
"Hello world! Object: #{obj.class.name}, " + (args.empty? ? "Called with no argument." : "Arguments: #{args.join(', ')}")
|
||||
end
|
||||
|
||||
desc "Displays a list of all available macros, including description if available."
|
||||
macro :macro_list do
|
||||
out = ''
|
||||
@@available_macros.keys.collect(&:to_s).sort.each do |macro|
|
||||
out << content_tag('dt', content_tag('code', macro))
|
||||
out << content_tag('dd', textilizable(@@available_macros[macro.to_sym]))
|
||||
end
|
||||
content_tag('dl', out)
|
||||
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
|
||||
title = args.first.to_s
|
||||
if title =~ %r{^([^\:]+)\:(.*)$}
|
||||
project_identifier, title = $1, $2
|
||||
project = Project.find_by_identifier(project_identifier) || Project.find_by_name(project_identifier)
|
||||
end
|
||||
raise 'Unknow project' unless project && User.current.allowed_to?(:view_wiki_pages, project)
|
||||
raise 'No wiki for this project' unless !project.wiki.nil?
|
||||
page = project.wiki.find_page(title)
|
||||
raise "Page #{args.first} doesn't exist" unless page && page.content
|
||||
@included_wiki_pages ||= []
|
||||
raise 'Circular inclusion detected' if @included_wiki_pages.include?(page.title)
|
||||
@included_wiki_pages << page.title
|
||||
out = textilizable(page.content, :text, :attachments => page.attachments)
|
||||
@included_wiki_pages.pop
|
||||
out
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user