##// END OF EJS Templates
Adds Filesystem adapter (patch #1393 by Paul R)....
Jean-Philippe Lang -
r1494:e69b4647f201
parent child
Show More
@@ -0,0 +1,43
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
4 # FileSystem adapter
5 # File written by Paul Rivier, at Demotera.
6 #
7 # This program is free software; you can redistribute it and/or
8 # modify it under the terms of the GNU General Public License
9 # as published by the Free Software Foundation; either version 2
10 # of the License, or (at your option) any later version.
11 #
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with this program; if not, write to the Free Software
19 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20
21 require 'redmine/scm/adapters/filesystem_adapter'
22
23 class Repository::Filesystem < Repository
24 attr_protected :root_url
25 validates_presence_of :url
26
27 def scm_adapter
28 Redmine::Scm::Adapters::FilesystemAdapter
29 end
30
31 def self.scm_name
32 'Filesystem'
33 end
34
35 def entries(path=nil, identifier=nil)
36 scm.entries(path, identifier)
37 end
38
39 def fetch_changesets
40 nil
41 end
42
43 end
@@ -0,0 +1,94
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
4 # FileSystem adapter
5 # File written by Paul Rivier, at Demotera.
6 #
7 # This program is free software; you can redistribute it and/or
8 # modify it under the terms of the GNU General Public License
9 # as published by the Free Software Foundation; either version 2
10 # of the License, or (at your option) any later version.
11 #
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with this program; if not, write to the Free Software
19 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20
21 require 'redmine/scm/adapters/abstract_adapter'
22 require 'find'
23
24 module Redmine
25 module Scm
26 module Adapters
27 class FilesystemAdapter < AbstractAdapter
28
29
30 def initialize(url, root_url=nil, login=nil, password=nil)
31 @url = with_trailling_slash(url)
32 end
33
34 def format_path_ends(path, leading=true, trailling=true)
35 path = leading ? with_leading_slash(path) :
36 without_leading_slash(path)
37 trailling ? with_trailling_slash(path) :
38 without_trailling_slash(path)
39 end
40
41 def info
42 info = Info.new({:root_url => target(),
43 :lastrev => nil
44 })
45 info
46 rescue CommandFailed
47 return nil
48 end
49
50 def entries(path="", identifier=nil)
51 entries = Entries.new
52 Dir.new(target(path)).each do |e|
53 relative_path = format_path_ends((format_path_ends(path,
54 false,
55 true) + e),
56 false,false)
57 target = target(relative_path)
58 entries <<
59 Entry.new({ :name => File.basename(e),
60 # below : list unreadable files, but dont link them.
61 :path => File.readable?(target) ? relative_path : "",
62 :kind => (File.directory?(target) ? 'dir' : 'file'),
63 :size => if (File.directory?(target))
64 nil else File.size(target) end,
65 :lastrev =>
66 Revision.new({:time => (File.mtime(target)).localtime,
67 })
68 }) if File.exist?(target) and # paranoid test
69 %w{file directory}.include?(File.ftype(target)) and # avoid special types
70 not File.basename(e).match(/^\.+$/) # avoid . and ..
71 end
72 entries.sort_by_name
73 end
74
75 def cat(path, identifier=nil)
76 File.new(target(path)).read
77 end
78
79 private
80
81 # AbstractAdapter::target is implicitly made to quote paths.
82 # Here we do not shell-out, so we do not want quotes.
83 def target(path=nil)
84 #Prevent the use of ..
85 if path and !path.match(/(^|\/)\.\.(\/|$)/)
86 return "#{self.url}#{without_leading_slash(path)}"
87 end
88 return self.url
89 end
90
91 end
92 end
93 end
94 end
1 NO CONTENT: new file 100644, binary diff hidden
@@ -0,0 +1,42
1
2 require File.dirname(__FILE__) + '/../test_helper'
3
4
5 class FilesystemAdapterTest < Test::Unit::TestCase
6
7 REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/filesystem_repository'
8
9 if File.directory?(REPOSITORY_PATH)
10 def setup
11 @adapter = Redmine::Scm::Adapters::FilesystemAdapter.new(REPOSITORY_PATH)
12 end
13
14 def test_entries
15 assert_equal 2, @adapter.entries.size
16 assert_equal ["dir", "test"], @adapter.entries.collect(&:name)
17 assert_equal ["dir", "test"], @adapter.entries(nil).collect(&:name)
18 assert_equal ["dir", "test"], @adapter.entries("/").collect(&:name)
19 ["dir", "/dir", "/dir/", "dir/"].each do |path|
20 assert_equal ["subdir", "dirfile"], @adapter.entries(path).collect(&:name)
21 end
22 # If y try to use "..", the path is ignored
23 ["/../","dir/../", "..", "../", "/..", "dir/.."].each do |path|
24 assert_equal ["dir", "test"], @adapter.entries(path).collect(&:name), ".. must be ignored in path argument"
25 end
26 end
27
28 def test_cat
29 assert_equal "TEST CAT\n", @adapter.cat("test")
30 assert_equal "TEST CAT\n", @adapter.cat("/test")
31 # Revision number is ignored
32 assert_equal "TEST CAT\n", @adapter.cat("/test", 1)
33 end
34
35 else
36 puts "Filesystem test repository NOT FOUND. Skipping unit tests !!! See doc/RUNNING_TESTS."
37 def test_fake; assert true end
38 end
39
40 end
41
42
@@ -0,0 +1,54
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 require File.dirname(__FILE__) + '/../test_helper'
19
20 class RepositoryFilesystemTest < Test::Unit::TestCase
21 fixtures :projects
22
23 # No '..' in the repository path
24 REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/filesystem_repository'
25
26 def setup
27 @project = Project.find(1)
28 Setting.enabled_scm << 'Filesystem' unless Setting.enabled_scm.include?('Filesystem')
29 assert @repository = Repository::Filesystem.create(:project => @project, :url => REPOSITORY_PATH)
30 end
31
32 if File.directory?(REPOSITORY_PATH)
33 def test_fetch_changesets
34 @repository.fetch_changesets
35 @repository.reload
36
37 assert_equal 0, @repository.changesets.count
38 assert_equal 0, @repository.changes.count
39 end
40
41 def test_entries
42 assert_equal 2, @repository.entries("", 2).size
43 assert_equal 2, @repository.entries("dir", 3).size
44 end
45
46 def test_cat
47 assert_equal "TEST CAT\n", @repository.scm.cat("test")
48 end
49
50 else
51 puts "Filesystem test repository NOT FOUND. Skipping unit tests !!! See doc/RUNNING_TESTS."
52 def test_fake; assert true end
53 end
54 end
@@ -1,101 +1,105
1 1 # redMine - project management software
2 2 # Copyright (C) 2006 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'coderay'
19 19 require 'coderay/helpers/file_type'
20 20 require 'iconv'
21 21
22 22 module RepositoriesHelper
23 23 def syntax_highlight(name, content)
24 24 type = CodeRay::FileType[name]
25 25 type ? CodeRay.scan(content, type).html : h(content)
26 26 end
27 27
28 28 def format_revision(txt)
29 29 txt.to_s[0,8]
30 30 end
31 31
32 32 def to_utf8(str)
33 33 return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii
34 34 @encodings ||= Setting.repositories_encodings.split(',').collect(&:strip)
35 35 @encodings.each do |encoding|
36 36 begin
37 37 return Iconv.conv('UTF-8', encoding, str)
38 38 rescue Iconv::Failure
39 39 # do nothing here and try the next encoding
40 40 end
41 41 end
42 42 str
43 43 end
44 44
45 45 def repository_field_tags(form, repository)
46 46 method = repository.class.name.demodulize.underscore + "_field_tags"
47 47 send(method, form, repository) if repository.is_a?(Repository) && respond_to?(method)
48 48 end
49 49
50 50 def scm_select_tag(repository)
51 51 scm_options = [["--- #{l(:actionview_instancetag_blank_option)} ---", '']]
52 52 REDMINE_SUPPORTED_SCM.each do |scm|
53 53 scm_options << ["Repository::#{scm}".constantize.scm_name, scm] if Setting.enabled_scm.include?(scm) || (repository && repository.class.name.demodulize == scm)
54 54 end
55 55
56 56 select_tag('repository_scm',
57 57 options_for_select(scm_options, repository.class.name.demodulize),
58 58 :disabled => (repository && !repository.new_record?),
59 59 :onchange => remote_function(:url => { :controller => 'repositories', :action => 'edit', :id => @project }, :method => :get, :with => "Form.serialize(this.form)")
60 60 )
61 61 end
62 62
63 63 def with_leading_slash(path)
64 64 path.to_s.starts_with?('/') ? path : "/#{path}"
65 65 end
66 66
67 67 def without_leading_slash(path)
68 68 path.gsub(%r{^/+}, '')
69 69 end
70 70
71 71 def subversion_field_tags(form, repository)
72 72 content_tag('p', form.text_field(:url, :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)) +
73 73 '<br />(http://, https://, svn://, file:///)') +
74 74 content_tag('p', form.text_field(:login, :size => 30)) +
75 75 content_tag('p', form.password_field(:password, :size => 30, :name => 'ignore',
76 76 :value => ((repository.new_record? || repository.password.blank?) ? '' : ('x'*15)),
77 77 :onfocus => "this.value=''; this.name='repository[password]';",
78 78 :onchange => "this.name='repository[password]';"))
79 79 end
80 80
81 81 def darcs_field_tags(form, repository)
82 82 content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.new_record?)))
83 83 end
84 84
85 85 def mercurial_field_tags(form, repository)
86 86 content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)))
87 87 end
88 88
89 89 def git_field_tags(form, repository)
90 90 content_tag('p', form.text_field(:url, :label => 'Path to .git directory', :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)))
91 91 end
92 92
93 93 def cvs_field_tags(form, repository)
94 94 content_tag('p', form.text_field(:root_url, :label => 'CVSROOT', :size => 60, :required => true, :disabled => !repository.new_record?)) +
95 95 content_tag('p', form.text_field(:url, :label => 'Module', :size => 30, :required => true, :disabled => !repository.new_record?))
96 96 end
97 97
98 98 def bazaar_field_tags(form, repository)
99 99 content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.new_record?)))
100 100 end
101
102 def filesystem_field_tags(form, repository)
103 content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)))
104 end
101 105 end
@@ -1,37 +1,41
1 1 Creating test repositories
2 2 ===================
3 3
4 4 mkdir tmp/test
5 5
6 6 Subversion
7 7 ----------
8 8 svnadmin create tmp/test/subversion_repository
9 9 gunzip < test/fixtures/repositories/subversion_repository.dump.gz | svnadmin load tmp/test/subversion_repository
10 10
11 11 CVS
12 12 ---
13 13 gunzip < test/fixtures/repositories/cvs_repository.tar.gz | tar -xv -C tmp/test
14 14
15 15 Bazaar
16 16 ------
17 17 gunzip < test/fixtures/repositories/bazaar_repository.tar.gz | tar -xv -C tmp/test
18 18
19 19 Mercurial
20 20 ---------
21 21 gunzip < test/fixtures/repositories/mercurial_repository.tar.gz | tar -xv -C tmp/test
22 22
23 23 Git
24 24 ---
25 25 gunzip < test/fixtures/repositories/git_repository.tar.gz | tar -xv -C tmp/test
26 26
27 FileSystem
28 ----------
29 gunzip < test/fixtures/repositories/filesystem_repository.tar.gz | tar -xv -C tmp/test
30
27 31
28 32 Running Tests
29 33 =============
30 34
31 35 Run
32 36
33 37 rake --tasks | grep test
34 38
35 39 to see available tests.
36 40
37 41 RAILS_ENV=test rake test will run tests.
@@ -1,134 +1,134
1 1 require 'redmine/access_control'
2 2 require 'redmine/menu_manager'
3 3 require 'redmine/mime_type'
4 4 require 'redmine/core_ext'
5 5 require 'redmine/themes'
6 6 require 'redmine/plugin'
7 7
8 8 begin
9 9 require_library_or_gem 'RMagick' unless Object.const_defined?(:Magick)
10 10 rescue LoadError
11 11 # RMagick is not available
12 12 end
13 13
14 REDMINE_SUPPORTED_SCM = %w( Subversion Darcs Mercurial Cvs Bazaar Git )
14 REDMINE_SUPPORTED_SCM = %w( Subversion Darcs Mercurial Cvs Bazaar Git Filesystem )
15 15
16 16 # Permissions
17 17 Redmine::AccessControl.map do |map|
18 18 map.permission :view_project, {:projects => [:show, :activity]}, :public => true
19 19 map.permission :search_project, {:search => :index}, :public => true
20 20 map.permission :edit_project, {:projects => [:settings, :edit]}, :require => :member
21 21 map.permission :select_project_modules, {:projects => :modules}, :require => :member
22 22 map.permission :manage_members, {:projects => :settings, :members => [:new, :edit, :destroy]}, :require => :member
23 23 map.permission :manage_versions, {:projects => [:settings, :add_version], :versions => [:edit, :destroy]}, :require => :member
24 24
25 25 map.project_module :issue_tracking do |map|
26 26 # Issue categories
27 27 map.permission :manage_categories, {:projects => [:settings, :add_issue_category], :issue_categories => [:edit, :destroy]}, :require => :member
28 28 # Issues
29 29 map.permission :view_issues, {:projects => [:changelog, :roadmap],
30 30 :issues => [:index, :changes, :show, :context_menu],
31 31 :versions => [:show, :status_by],
32 32 :queries => :index,
33 33 :reports => :issue_report}, :public => true
34 34 map.permission :add_issues, {:issues => :new}
35 35 map.permission :edit_issues, {:issues => [:edit, :reply, :bulk_edit, :destroy_attachment]}
36 36 map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy]}
37 37 map.permission :add_issue_notes, {:issues => [:edit, :reply]}
38 38 map.permission :edit_issue_notes, {:journals => :edit}, :require => :loggedin
39 39 map.permission :edit_own_issue_notes, {:journals => :edit}, :require => :loggedin
40 40 map.permission :move_issues, {:issues => :move}, :require => :loggedin
41 41 map.permission :delete_issues, {:issues => :destroy}, :require => :member
42 42 # Queries
43 43 map.permission :manage_public_queries, {:queries => [:new, :edit, :destroy]}, :require => :member
44 44 map.permission :save_queries, {:queries => [:new, :edit, :destroy]}, :require => :loggedin
45 45 # Gantt & calendar
46 46 map.permission :view_gantt, :projects => :gantt
47 47 map.permission :view_calendar, :projects => :calendar
48 48 end
49 49
50 50 map.project_module :time_tracking do |map|
51 51 map.permission :log_time, {:timelog => :edit}, :require => :loggedin
52 52 map.permission :view_time_entries, :timelog => [:details, :report]
53 53 map.permission :edit_time_entries, {:timelog => [:edit, :destroy]}, :require => :member
54 54 map.permission :edit_own_time_entries, {:timelog => [:edit, :destroy]}, :require => :loggedin
55 55 end
56 56
57 57 map.project_module :news do |map|
58 58 map.permission :manage_news, {:news => [:new, :edit, :destroy, :destroy_comment]}, :require => :member
59 59 map.permission :view_news, {:news => [:index, :show]}, :public => true
60 60 map.permission :comment_news, {:news => :add_comment}
61 61 end
62 62
63 63 map.project_module :documents do |map|
64 64 map.permission :manage_documents, {:documents => [:new, :edit, :destroy, :add_attachment, :destroy_attachment]}, :require => :loggedin
65 65 map.permission :view_documents, :documents => [:index, :show, :download]
66 66 end
67 67
68 68 map.project_module :files do |map|
69 69 map.permission :manage_files, {:projects => :add_file, :versions => :destroy_file}, :require => :loggedin
70 70 map.permission :view_files, :projects => :list_files, :versions => :download
71 71 end
72 72
73 73 map.project_module :wiki do |map|
74 74 map.permission :manage_wiki, {:wikis => [:edit, :destroy]}, :require => :member
75 75 map.permission :rename_wiki_pages, {:wiki => :rename}, :require => :member
76 76 map.permission :delete_wiki_pages, {:wiki => :destroy}, :require => :member
77 77 map.permission :view_wiki_pages, :wiki => [:index, :history, :diff, :annotate, :special]
78 78 map.permission :edit_wiki_pages, :wiki => [:edit, :preview, :add_attachment, :destroy_attachment]
79 79 map.permission :protect_wiki_pages, {:wiki => :protect}, :require => :member
80 80 end
81 81
82 82 map.project_module :repository do |map|
83 83 map.permission :manage_repository, {:repositories => [:edit, :destroy]}, :require => :member
84 84 map.permission :browse_repository, :repositories => [:show, :browse, :entry, :annotate, :changes, :diff, :stats, :graph]
85 85 map.permission :view_changesets, :repositories => [:show, :revisions, :revision]
86 86 end
87 87
88 88 map.project_module :boards do |map|
89 89 map.permission :manage_boards, {:boards => [:new, :edit, :destroy]}, :require => :member
90 90 map.permission :view_messages, {:boards => [:index, :show], :messages => [:show]}, :public => true
91 91 map.permission :add_messages, {:messages => [:new, :reply]}
92 92 map.permission :edit_messages, {:messages => :edit}, :require => :member
93 93 map.permission :delete_messages, {:messages => :destroy}, :require => :member
94 94 end
95 95 end
96 96
97 97 Redmine::MenuManager.map :top_menu do |menu|
98 98 menu.push :home, :home_path, :html => { :class => 'home' }
99 99 menu.push :my_page, { :controller => 'my', :action => 'page' }, :html => { :class => 'mypage' }, :if => Proc.new { User.current.logged? }
100 100 menu.push :projects, { :controller => 'projects', :action => 'index' }, :caption => :label_project_plural, :html => { :class => 'projects' }
101 101 menu.push :administration, { :controller => 'admin', :action => 'index' }, :html => { :class => 'admin' }, :if => Proc.new { User.current.admin? }
102 102 menu.push :help, Redmine::Info.help_url, :html => { :class => 'help' }
103 103 end
104 104
105 105 Redmine::MenuManager.map :account_menu do |menu|
106 106 menu.push :login, :signin_path, :html => { :class => 'login' }, :if => Proc.new { !User.current.logged? }
107 107 menu.push :register, { :controller => 'account', :action => 'register' }, :html => { :class => 'register' }, :if => Proc.new { !User.current.logged? && Setting.self_registration? }
108 108 menu.push :my_account, { :controller => 'my', :action => 'account' }, :html => { :class => 'myaccount' }, :if => Proc.new { User.current.logged? }
109 109 menu.push :logout, :signout_path, :html => { :class => 'logout' }, :if => Proc.new { User.current.logged? }
110 110 end
111 111
112 112 Redmine::MenuManager.map :application_menu do |menu|
113 113 # Empty
114 114 end
115 115
116 116 Redmine::MenuManager.map :project_menu do |menu|
117 117 menu.push :overview, { :controller => 'projects', :action => 'show' }
118 118 menu.push :activity, { :controller => 'projects', :action => 'activity' }
119 119 menu.push :roadmap, { :controller => 'projects', :action => 'roadmap' },
120 120 :if => Proc.new { |p| p.versions.any? }
121 121 menu.push :issues, { :controller => 'issues', :action => 'index' }, :param => :project_id, :caption => :label_issue_plural
122 122 menu.push :new_issue, { :controller => 'issues', :action => 'new' }, :param => :project_id, :caption => :label_issue_new,
123 123 :html => { :accesskey => Redmine::AccessKeys.key_for(:new_issue) }
124 124 menu.push :news, { :controller => 'news', :action => 'index' }, :param => :project_id, :caption => :label_news_plural
125 125 menu.push :documents, { :controller => 'documents', :action => 'index' }, :param => :project_id, :caption => :label_document_plural
126 126 menu.push :wiki, { :controller => 'wiki', :action => 'index', :page => nil },
127 127 :if => Proc.new { |p| p.wiki && !p.wiki.new_record? }
128 128 menu.push :boards, { :controller => 'boards', :action => 'index', :id => nil }, :param => :project_id,
129 129 :if => Proc.new { |p| p.boards.any? }, :caption => :label_board_plural
130 130 menu.push :files, { :controller => 'projects', :action => 'list_files' }, :caption => :label_attachment_plural
131 131 menu.push :repository, { :controller => 'repositories', :action => 'show' },
132 132 :if => Proc.new { |p| p.repository && !p.repository.new_record? }
133 133 menu.push :settings, { :controller => 'projects', :action => 'settings' }
134 134 end
@@ -1,410 +1,420
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'cgi'
19 19
20 20 module Redmine
21 21 module Scm
22 22 module Adapters
23 23 class CommandFailed < StandardError #:nodoc:
24 24 end
25 25
26 26 class AbstractAdapter #:nodoc:
27 27 def initialize(url, root_url=nil, login=nil, password=nil)
28 28 @url = url
29 29 @login = login if login && !login.empty?
30 30 @password = (password || "") if @login
31 31 @root_url = root_url.blank? ? retrieve_root_url : root_url
32 32 end
33 33
34 34 def adapter_name
35 35 'Abstract'
36 36 end
37 37
38 38 def supports_cat?
39 39 true
40 40 end
41 41
42 42 def supports_annotate?
43 43 respond_to?('annotate')
44 44 end
45 45
46 46 def root_url
47 47 @root_url
48 48 end
49 49
50 50 def url
51 51 @url
52 52 end
53 53
54 54 # get info about the svn repository
55 55 def info
56 56 return nil
57 57 end
58 58
59 59 # Returns the entry identified by path and revision identifier
60 60 # or nil if entry doesn't exist in the repository
61 61 def entry(path=nil, identifier=nil)
62 62 parts = path.to_s.split(%r{[\/\\]}).select {|n| !n.blank?}
63 63 search_path = parts[0..-2].join('/')
64 64 search_name = parts[-1]
65 65 if search_path.blank? && search_name.blank?
66 66 # Root entry
67 67 Entry.new(:path => '', :kind => 'dir')
68 68 else
69 69 # Search for the entry in the parent directory
70 70 es = entries(search_path, identifier)
71 71 es ? es.detect {|e| e.name == search_name} : nil
72 72 end
73 73 end
74 74
75 75 # Returns an Entries collection
76 76 # or nil if the given path doesn't exist in the repository
77 77 def entries(path=nil, identifier=nil)
78 78 return nil
79 79 end
80 80
81 81 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
82 82 return nil
83 83 end
84 84
85 85 def diff(path, identifier_from, identifier_to=nil, type="inline")
86 86 return nil
87 87 end
88 88
89 89 def cat(path, identifier=nil)
90 90 return nil
91 91 end
92 92
93 93 def with_leading_slash(path)
94 94 path ||= ''
95 95 (path[0,1]!="/") ? "/#{path}" : path
96 96 end
97 97
98 98 def with_trailling_slash(path)
99 99 path ||= ''
100 100 (path[-1,1] == "/") ? path : "#{path}/"
101 101 end
102 102
103 def without_leading_slash(path)
104 path ||= ''
105 path.gsub(%r{^/+}, '')
106 end
107
108 def without_trailling_slash(path)
109 path ||= ''
110 (path[-1,1] == "/") ? path[0..-2] : path
111 end
112
103 113 def shell_quote(str)
104 114 if RUBY_PLATFORM =~ /mswin/
105 115 '"' + str.gsub(/"/, '\\"') + '"'
106 116 else
107 117 "'" + str.gsub(/'/, "'\"'\"'") + "'"
108 118 end
109 119 end
110 120
111 121 private
112 122 def retrieve_root_url
113 123 info = self.info
114 124 info ? info.root_url : nil
115 125 end
116 126
117 127 def target(path)
118 128 path ||= ''
119 129 base = path.match(/^\//) ? root_url : url
120 130 shell_quote("#{base}/#{path}".gsub(/[?<>\*]/, ''))
121 131 end
122 132
123 133 def logger
124 134 RAILS_DEFAULT_LOGGER
125 135 end
126 136
127 137 def shellout(cmd, &block)
128 138 logger.debug "Shelling out: #{cmd}" if logger && logger.debug?
129 139 begin
130 140 IO.popen(cmd, "r+") do |io|
131 141 io.close_write
132 142 block.call(io) if block_given?
133 143 end
134 144 rescue Errno::ENOENT => e
135 145 msg = strip_credential(e.message)
136 146 # The command failed, log it and re-raise
137 147 logger.error("SCM command failed: #{strip_credential(cmd)}\n with: #{msg}")
138 148 raise CommandFailed.new(msg)
139 149 end
140 150 end
141 151
142 152 # Hides username/password in a given command
143 153 def self.hide_credential(cmd)
144 154 q = (RUBY_PLATFORM =~ /mswin/ ? '"' : "'")
145 155 cmd.to_s.gsub(/(\-\-(password|username))\s+(#{q}[^#{q}]+#{q}|[^#{q}]\S+)/, '\\1 xxxx')
146 156 end
147 157
148 158 def strip_credential(cmd)
149 159 self.class.hide_credential(cmd)
150 160 end
151 161 end
152 162
153 163 class Entries < Array
154 164 def sort_by_name
155 165 sort {|x,y|
156 166 if x.kind == y.kind
157 167 x.name <=> y.name
158 168 else
159 169 x.kind <=> y.kind
160 170 end
161 171 }
162 172 end
163 173
164 174 def revisions
165 175 revisions ||= Revisions.new(collect{|entry| entry.lastrev}.compact)
166 176 end
167 177 end
168 178
169 179 class Info
170 180 attr_accessor :root_url, :lastrev
171 181 def initialize(attributes={})
172 182 self.root_url = attributes[:root_url] if attributes[:root_url]
173 183 self.lastrev = attributes[:lastrev]
174 184 end
175 185 end
176 186
177 187 class Entry
178 188 attr_accessor :name, :path, :kind, :size, :lastrev
179 189 def initialize(attributes={})
180 190 self.name = attributes[:name] if attributes[:name]
181 191 self.path = attributes[:path] if attributes[:path]
182 192 self.kind = attributes[:kind] if attributes[:kind]
183 193 self.size = attributes[:size].to_i if attributes[:size]
184 194 self.lastrev = attributes[:lastrev]
185 195 end
186 196
187 197 def is_file?
188 198 'file' == self.kind
189 199 end
190 200
191 201 def is_dir?
192 202 'dir' == self.kind
193 203 end
194 204
195 205 def is_text?
196 206 Redmine::MimeType.is_type?('text', name)
197 207 end
198 208 end
199 209
200 210 class Revisions < Array
201 211 def latest
202 212 sort {|x,y|
203 213 unless x.time.nil? or y.time.nil?
204 214 x.time <=> y.time
205 215 else
206 216 0
207 217 end
208 218 }.last
209 219 end
210 220 end
211 221
212 222 class Revision
213 223 attr_accessor :identifier, :scmid, :name, :author, :time, :message, :paths, :revision, :branch
214 224 def initialize(attributes={})
215 225 self.identifier = attributes[:identifier]
216 226 self.scmid = attributes[:scmid]
217 227 self.name = attributes[:name] || self.identifier
218 228 self.author = attributes[:author]
219 229 self.time = attributes[:time]
220 230 self.message = attributes[:message] || ""
221 231 self.paths = attributes[:paths]
222 232 self.revision = attributes[:revision]
223 233 self.branch = attributes[:branch]
224 234 end
225 235
226 236 end
227 237
228 238 # A line of Diff
229 239 class Diff
230 240 attr_accessor :nb_line_left
231 241 attr_accessor :line_left
232 242 attr_accessor :nb_line_right
233 243 attr_accessor :line_right
234 244 attr_accessor :type_diff_right
235 245 attr_accessor :type_diff_left
236 246
237 247 def initialize ()
238 248 self.nb_line_left = ''
239 249 self.nb_line_right = ''
240 250 self.line_left = ''
241 251 self.line_right = ''
242 252 self.type_diff_right = ''
243 253 self.type_diff_left = ''
244 254 end
245 255
246 256 def inspect
247 257 puts '### Start Line Diff ###'
248 258 puts self.nb_line_left
249 259 puts self.line_left
250 260 puts self.nb_line_right
251 261 puts self.line_right
252 262 end
253 263 end
254 264
255 265 class DiffTableList < Array
256 266 def initialize (diff, type="inline")
257 267 diff_table = DiffTable.new type
258 268 diff.each do |line|
259 269 if line =~ /^(---|\+\+\+) (.*)$/
260 270 self << diff_table if diff_table.length > 1
261 271 diff_table = DiffTable.new type
262 272 end
263 273 a = diff_table.add_line line
264 274 end
265 275 self << diff_table unless diff_table.empty?
266 276 self
267 277 end
268 278 end
269 279
270 280 # Class for create a Diff
271 281 class DiffTable < Hash
272 282 attr_reader :file_name, :line_num_l, :line_num_r
273 283
274 284 # Initialize with a Diff file and the type of Diff View
275 285 # The type view must be inline or sbs (side_by_side)
276 286 def initialize(type="inline")
277 287 @parsing = false
278 288 @nb_line = 1
279 289 @start = false
280 290 @before = 'same'
281 291 @second = true
282 292 @type = type
283 293 end
284 294
285 295 # Function for add a line of this Diff
286 296 def add_line(line)
287 297 unless @parsing
288 298 if line =~ /^(---|\+\+\+) (.*)$/
289 299 @file_name = $2
290 300 return false
291 301 elsif line =~ /^@@ (\+|\-)(\d+)(,\d+)? (\+|\-)(\d+)(,\d+)? @@/
292 302 @line_num_l = $5.to_i
293 303 @line_num_r = $2.to_i
294 304 @parsing = true
295 305 end
296 306 else
297 307 if line =~ /^[^\+\-\s@\\]/
298 308 @parsing = false
299 309 return false
300 310 elsif line =~ /^@@ (\+|\-)(\d+)(,\d+)? (\+|\-)(\d+)(,\d+)? @@/
301 311 @line_num_l = $5.to_i
302 312 @line_num_r = $2.to_i
303 313 else
304 314 @nb_line += 1 if parse_line(line, @type)
305 315 end
306 316 end
307 317 return true
308 318 end
309 319
310 320 def inspect
311 321 puts '### DIFF TABLE ###'
312 322 puts "file : #{file_name}"
313 323 self.each do |d|
314 324 d.inspect
315 325 end
316 326 end
317 327
318 328 private
319 329 # Test if is a Side By Side type
320 330 def sbs?(type, func)
321 331 if @start and type == "sbs"
322 332 if @before == func and @second
323 333 tmp_nb_line = @nb_line
324 334 self[tmp_nb_line] = Diff.new
325 335 else
326 336 @second = false
327 337 tmp_nb_line = @start
328 338 @start += 1
329 339 @nb_line -= 1
330 340 end
331 341 else
332 342 tmp_nb_line = @nb_line
333 343 @start = @nb_line
334 344 self[tmp_nb_line] = Diff.new
335 345 @second = true
336 346 end
337 347 unless self[tmp_nb_line]
338 348 @nb_line += 1
339 349 self[tmp_nb_line] = Diff.new
340 350 else
341 351 self[tmp_nb_line]
342 352 end
343 353 end
344 354
345 355 # Escape the HTML for the diff
346 356 def escapeHTML(line)
347 357 CGI.escapeHTML(line)
348 358 end
349 359
350 360 def parse_line(line, type="inline")
351 361 if line[0, 1] == "+"
352 362 diff = sbs? type, 'add'
353 363 @before = 'add'
354 364 diff.line_left = escapeHTML line[1..-1]
355 365 diff.nb_line_left = @line_num_l
356 366 diff.type_diff_left = 'diff_in'
357 367 @line_num_l += 1
358 368 true
359 369 elsif line[0, 1] == "-"
360 370 diff = sbs? type, 'remove'
361 371 @before = 'remove'
362 372 diff.line_right = escapeHTML line[1..-1]
363 373 diff.nb_line_right = @line_num_r
364 374 diff.type_diff_right = 'diff_out'
365 375 @line_num_r += 1
366 376 true
367 377 elsif line[0, 1] =~ /\s/
368 378 @before = 'same'
369 379 @start = false
370 380 diff = Diff.new
371 381 diff.line_right = escapeHTML line[1..-1]
372 382 diff.nb_line_right = @line_num_r
373 383 diff.line_left = escapeHTML line[1..-1]
374 384 diff.nb_line_left = @line_num_l
375 385 self[@nb_line] = diff
376 386 @line_num_l += 1
377 387 @line_num_r += 1
378 388 true
379 389 elsif line[0, 1] = "\\"
380 390 true
381 391 else
382 392 false
383 393 end
384 394 end
385 395 end
386 396
387 397 class Annotate
388 398 attr_reader :lines, :revisions
389 399
390 400 def initialize
391 401 @lines = []
392 402 @revisions = []
393 403 end
394 404
395 405 def add_line(line, revision)
396 406 @lines << line
397 407 @revisions << revision
398 408 end
399 409
400 410 def content
401 411 content = lines.join("\n")
402 412 end
403 413
404 414 def empty?
405 415 lines.empty?
406 416 end
407 417 end
408 418 end
409 419 end
410 420 end
@@ -1,118 +1,120
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19
20 20 class RepositoryTest < Test::Unit::TestCase
21 21 fixtures :projects,
22 22 :trackers,
23 23 :projects_trackers,
24 24 :repositories,
25 25 :issues,
26 26 :issue_statuses,
27 27 :changesets,
28 28 :changes,
29 29 :users,
30 30 :enumerations
31 31
32 32 def setup
33 33 @repository = Project.find(1).repository
34 34 end
35 35
36 36 def test_create
37 37 repository = Repository::Subversion.new(:project => Project.find(3))
38 38 assert !repository.save
39 39
40 40 repository.url = "svn://localhost"
41 41 assert repository.save
42 42 repository.reload
43 43
44 44 project = Project.find(3)
45 45 assert_equal repository, project.repository
46 46 end
47 47
48 48 def test_should_not_create_with_disabled_scm
49 49 # disable Subversion
50 50 Setting.enabled_scm = ['Darcs', 'Git']
51 51 repository = Repository::Subversion.new(:project => Project.find(3), :url => "svn://localhost")
52 52 assert !repository.save
53 53 assert_equal :activerecord_error_invalid, repository.errors.on(:type)
54 # re-enable Subversion for following tests
55 Setting.delete_all
54 56 end
55 57
56 58 def test_scan_changesets_for_issue_ids
57 59 # choosing a status to apply to fix issues
58 60 Setting.commit_fix_status_id = IssueStatus.find(:first, :conditions => ["is_closed = ?", true]).id
59 61 Setting.commit_fix_done_ratio = "90"
60 62 Setting.commit_ref_keywords = 'refs , references, IssueID'
61 63 Setting.commit_fix_keywords = 'fixes , closes'
62 64 Setting.default_language = 'en'
63 65 ActionMailer::Base.deliveries.clear
64 66
65 67 # make sure issue 1 is not already closed
66 68 fixed_issue = Issue.find(1)
67 69 assert !fixed_issue.status.is_closed?
68 70 old_status = fixed_issue.status
69 71
70 72 Repository.scan_changesets_for_issue_ids
71 73 assert_equal [101, 102], Issue.find(3).changeset_ids
72 74
73 75 # fixed issues
74 76 fixed_issue.reload
75 77 assert fixed_issue.status.is_closed?
76 78 assert_equal 90, fixed_issue.done_ratio
77 79 assert_equal [101], fixed_issue.changeset_ids
78 80
79 81 # issue change
80 82 journal = fixed_issue.journals.find(:first, :order => 'created_on desc')
81 83 assert_equal User.find_by_login('dlopper'), journal.user
82 84 assert_equal 'Applied in changeset r2.', journal.notes
83 85
84 86 # 2 email notifications
85 87 assert_equal 2, ActionMailer::Base.deliveries.size
86 88 mail = ActionMailer::Base.deliveries.first
87 89 assert_kind_of TMail::Mail, mail
88 90 assert mail.subject.starts_with?("[#{fixed_issue.project.name} - #{fixed_issue.tracker.name} ##{fixed_issue.id}]")
89 91 assert mail.body.include?("Status changed from #{old_status} to #{fixed_issue.status}")
90 92
91 93 # ignoring commits referencing an issue of another project
92 94 assert_equal [], Issue.find(4).changesets
93 95 end
94 96
95 97 def test_for_changeset_comments_strip
96 98 repository = Repository::Mercurial.create( :project => Project.find( 4 ), :url => '/foo/bar/baz' )
97 99 comment = <<-COMMENT
98 100 This is a loooooooooooooooooooooooooooong comment
99 101
100 102
101 103 COMMENT
102 104 changeset = Changeset.new(
103 105 :comments => comment, :commit_date => Time.now, :revision => 0, :scmid => 'f39b7922fb3c',
104 106 :committer => 'foo <foo@example.com>', :committed_on => Time.now, :repository => repository )
105 107 assert( changeset.save )
106 108 assert_not_equal( comment, changeset.comments )
107 109 assert_equal( 'This is a loooooooooooooooooooooooooooong comment', changeset.comments )
108 110 end
109 111
110 112 def test_for_urls_strip
111 113 repository = Repository::Cvs.create(:project => Project.find(4), :url => ' :pserver:login:password@host:/path/to/the/repository',
112 114 :root_url => 'foo ')
113 115 assert repository.save
114 116 repository.reload
115 117 assert_equal ':pserver:login:password@host:/path/to/the/repository', repository.url
116 118 assert_equal 'foo', repository.root_url
117 119 end
118 120 end
General Comments 0
You need to be logged in to leave comments. Login now