##// END OF EJS Templates
Merged Git support branch (r1200 to r1226)....
Jean-Philippe Lang -
r1222:3a9b0988c751
parent child
Show More
@@ -0,0 +1,70
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 # Copyright (C) 2007 Patrick Aljord patcito@Ε‹mail.com
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 'redmine/scm/adapters/git_adapter'
19
20 class Repository::Git < Repository
21 attr_protected :root_url
22 validates_presence_of :url
23
24 def scm_adapter
25 Redmine::Scm::Adapters::GitAdapter
26 end
27
28 def self.scm_name
29 'Git'
30 end
31
32 def changesets_for_path(path)
33 Change.find(:all, :include => :changeset,
34 :conditions => ["repository_id = ? AND path = ?", id, path],
35 :order => "committed_on DESC, #{Changeset.table_name}.revision DESC").collect(&:changeset)
36 end
37
38 def fetch_changesets
39 scm_info = scm.info
40 if scm_info
41 # latest revision found in database
42 db_revision = latest_changeset ? latest_changeset.revision : nil
43 # latest revision in the repository
44 scm_revision = scm_info.lastrev.scmid
45
46 unless changesets.find_by_scmid(scm_revision)
47
48 revisions = scm.revisions('', db_revision, nil)
49 transaction do
50 revisions.reverse_each do |revision|
51 changeset = Changeset.create(:repository => self,
52 :revision => revision.identifier,
53 :scmid => revision.scmid,
54 :committer => revision.author,
55 :committed_on => revision.time,
56 :comments => revision.message)
57
58 revision.paths.each do |change|
59 Change.create(:changeset => changeset,
60 :action => change[:action],
61 :path => change[:path],
62 :from_path => change[:from_path],
63 :from_revision => change[:from_revision])
64 end
65 end
66 end
67 end
68 end
69 end
70 end
@@ -0,0 +1,9
1 class ChangeChangesetsRevisionToString < ActiveRecord::Migration
2 def self.up
3 change_column :changesets, :revision, :string, :null => false
4 end
5
6 def self.down
7 change_column :changesets, :revision, :integer, :null => false
8 end
9 end
@@ -0,0 +1,9
1 class ChangeChangesFromRevisionToString < ActiveRecord::Migration
2 def self.up
3 change_column :changes, :from_revision, :string
4 end
5
6 def self.down
7 change_column :changes, :from_revision, :integer
8 end
9 end
@@ -0,0 +1,261
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 'redmine/scm/adapters/abstract_adapter'
19
20 module Redmine
21 module Scm
22 module Adapters
23 class GitAdapter < AbstractAdapter
24
25 # Git executable name
26 GIT_BIN = "git"
27
28 # Get the revision of a particuliar file
29 def get_rev (rev,path)
30 cmd="git --git-dir #{target('')} show #{shell_quote rev} -- #{shell_quote path}" if rev!='latest' and (! rev.nil?)
31 cmd="git --git-dir #{target('')} log -1 master -- #{shell_quote path}" if
32 rev=='latest' or rev.nil?
33 rev=[]
34 i=0
35 shellout(cmd) do |io|
36 files=[]
37 changeset = {}
38 parsing_descr = 0 #0: not parsing desc or files, 1: parsing desc, 2: parsing files
39
40 io.each_line do |line|
41 if line =~ /^commit ([0-9a-f]{40})$/
42 key = "commit"
43 value = $1
44 if (parsing_descr == 1 || parsing_descr == 2)
45 parsing_descr = 0
46 rev = Revision.new({:identifier => changeset[:commit],
47 :scmid => changeset[:commit],
48 :author => changeset[:author],
49 :time => Time.parse(changeset[:date]),
50 :message => changeset[:description],
51 :paths => files
52 })
53 changeset = {}
54 files = []
55 end
56 changeset[:commit] = $1
57 elsif (parsing_descr == 0) && line =~ /^(\w+):\s*(.*)$/
58 key = $1
59 value = $2
60 if key == "Author"
61 changeset[:author] = value
62 elsif key == "Date"
63 changeset[:date] = value
64 end
65 elsif (parsing_descr == 0) && line.chomp.to_s == ""
66 parsing_descr = 1
67 changeset[:description] = ""
68 elsif (parsing_descr == 1 || parsing_descr == 2) && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\s+(.+)$/
69 parsing_descr = 2
70 fileaction = $1
71 filepath = $2
72 files << {:action => fileaction, :path => filepath}
73 elsif (parsing_descr == 1) && line.chomp.to_s == ""
74 parsing_descr = 2
75 elsif (parsing_descr == 1)
76 changeset[:description] << line
77 end
78 end
79 rev = Revision.new({:identifier => changeset[:commit],
80 :scmid => changeset[:commit],
81 :author => changeset[:author],
82 :time => Time.parse(changeset[:date]),
83 :message => changeset[:description],
84 :paths => files
85 })
86
87 end
88
89 get_rev('latest',path) if rev == []
90
91 return nil if $? && $?.exitstatus != 0
92 return rev
93 end
94
95
96 def info
97 root_url = target('')
98 info = Info.new({:root_url => target(''),
99 :lastrev => revisions(root_url,nil,nil,{:limit => 1}).first
100 })
101 info
102 rescue Errno::ENOENT => e
103 return nil
104 end
105
106 def entries(path=nil, identifier=nil)
107 path ||= ''
108 entries = Entries.new
109 cmd = "#{GIT_BIN} --git-dir #{target('')} ls-tree -l "
110 cmd << shell_quote("HEAD:" + path) if identifier.nil?
111 cmd << shell_quote(identifier + ":" + path) if identifier
112 shellout(cmd) do |io|
113 io.each_line do |line|
114 e = line.chomp.to_s
115 if e =~ /^\d+\s+(\w+)\s+([0-9a-f]{40})\s+([0-9-]+)\s+(.+)$/
116 type = $1
117 sha = $2
118 size = $3
119 name = $4
120 entries << Entry.new({:name => name,
121 :path => (path.empty? ? name : "#{path}/#{name}"),
122 :kind => ((type == "tree") ? 'dir' : 'file'),
123 :size => ((type == "tree") ? nil : size),
124 :lastrev => get_rev(identifier,(path.empty? ? name : "#{path}/#{name}"))
125
126 }) unless entries.detect{|entry| entry.name == name}
127 end
128 end
129 end
130 return nil if $? && $?.exitstatus != 0
131 entries.sort_by_name
132 end
133
134 def entry(path=nil, identifier=nil)
135 path ||= ''
136 search_path = path.split('/')[0..-2].join('/')
137 entry_name = path.split('/').last
138 e = entries(search_path, identifier)
139 e ? e.detect{|entry| entry.name == entry_name} : nil
140 end
141
142 def revisions(path, identifier_from, identifier_to, options={})
143 revisions = Revisions.new
144 cmd = "#{GIT_BIN} --git-dir #{target('')} log --raw "
145 cmd << " -n #{options[:limit].to_i} " if (!options.nil?) && options[:limit]
146 cmd << " #{shell_quote(identifier_from + '..')} " if identifier_from
147 cmd << " #{shell_quote identifier_to} " if identifier_to
148 #cmd << " HEAD " if !identifier_to
149 shellout(cmd) do |io|
150 files=[]
151 changeset = {}
152 parsing_descr = 0 #0: not parsing desc or files, 1: parsing desc, 2: parsing files
153 revno = 1
154
155 io.each_line do |line|
156 if line =~ /^commit ([0-9a-f]{40})$/
157 key = "commit"
158 value = $1
159 if (parsing_descr == 1 || parsing_descr == 2)
160 parsing_descr = 0
161 revisions << Revision.new({:identifier => changeset[:commit],
162 :scmid => changeset[:commit],
163 :author => changeset[:author],
164 :time => Time.parse(changeset[:date]),
165 :message => changeset[:description],
166 :paths => files
167 })
168 changeset = {}
169 files = []
170 revno = revno + 1
171 end
172 changeset[:commit] = $1
173 elsif (parsing_descr == 0) && line =~ /^(\w+):\s*(.*)$/
174 key = $1
175 value = $2
176 if key == "Author"
177 changeset[:author] = value
178 elsif key == "Date"
179 changeset[:date] = value
180 end
181 elsif (parsing_descr == 0) && line.chomp.to_s == ""
182 parsing_descr = 1
183 changeset[:description] = ""
184 elsif (parsing_descr == 1 || parsing_descr == 2) && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\s+(.+)$/
185 parsing_descr = 2
186 fileaction = $1
187 filepath = $2
188 files << {:action => fileaction, :path => filepath}
189 elsif (parsing_descr == 1) && line.chomp.to_s == ""
190 parsing_descr = 2
191 elsif (parsing_descr == 1)
192 changeset[:description] << line[4..-1]
193 end
194 end
195
196 revisions << Revision.new({:identifier => changeset[:commit],
197 :scmid => changeset[:commit],
198 :author => changeset[:author],
199 :time => Time.parse(changeset[:date]),
200 :message => changeset[:description],
201 :paths => files
202 }) if changeset[:commit]
203
204 end
205
206 return nil if $? && $?.exitstatus != 0
207 revisions
208 end
209
210 def diff(path, identifier_from, identifier_to=nil, type="inline")
211 path ||= ''
212 if !identifier_to
213 identifier_to = nil
214 end
215
216 cmd = "#{GIT_BIN} --git-dir #{target('')} show #{shell_quote identifier_from}" if identifier_to.nil?
217 cmd = "#{GIT_BIN} --git-dir #{target('')} diff #{shell_quote identifier_to} #{shell_quote identifier_from}" if !identifier_to.nil?
218 cmd << " -- #{shell_quote path}" unless path.empty?
219 diff = []
220 shellout(cmd) do |io|
221 io.each_line do |line|
222 diff << line
223 end
224 end
225 return nil if $? && $?.exitstatus != 0
226 DiffTableList.new diff, type
227 end
228
229 def annotate(path, identifier=nil)
230 identifier = 'HEAD' if identifier.blank?
231 cmd = "#{GIT_BIN} --git-dir #{target('')} blame -l #{shell_quote identifier} -- #{shell_quote path}"
232 blame = Annotate.new
233 shellout(cmd) do |io|
234 io.each_line do |line|
235 next unless line =~ /([0-9a-f]{39,40})\s\((\w*)[^\)]*\)(.*)$/
236 blame.add_line($3.rstrip, Revision.new(:identifier => $1, :author => $2.strip))
237 end
238 end
239 return nil if $? && $?.exitstatus != 0
240 blame
241 end
242
243 def cat(path, identifier=nil)
244 if identifier.nil?
245 identifier = 'HEAD'
246 end
247 cmd = "#{GIT_BIN} --git-dir #{target('')} show #{shell_quote(identifier + ':' + path)}"
248 cat = nil
249 shellout(cmd) do |io|
250 io.binmode
251 cat = io.read
252 end
253 return nil if $? && $?.exitstatus != 0
254 cat
255 end
256 end
257 end
258 end
259
260 end
261
1 NO CONTENT: new file 100644, binary diff hidden
NO CONTENT: new file 100644, binary diff hidden
1 NO CONTENT: new file 100644, binary diff hidden
NO CONTENT: new file 100644, binary diff hidden
@@ -0,0 +1,94
1 # redMine - project management software
2 # Copyright (C) 2006-2008 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 require 'repositories_controller'
20
21 # Re-raise errors caught by the controller.
22 class RepositoriesController; def rescue_action(e) raise e end; end
23
24 class RepositoriesDarcsControllerTest < Test::Unit::TestCase
25 fixtures :projects, :users, :roles, :members, :repositories, :enabled_modules
26
27 # No '..' in the repository path
28 REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/darcs_repository'
29
30 def setup
31 @controller = RepositoriesController.new
32 @request = ActionController::TestRequest.new
33 @response = ActionController::TestResponse.new
34 User.current = nil
35 Repository::Darcs.create(:project => Project.find(3), :url => REPOSITORY_PATH)
36 end
37
38 if File.directory?(REPOSITORY_PATH)
39 def test_show
40 get :show, :id => 3
41 assert_response :success
42 assert_template 'show'
43 assert_not_nil assigns(:entries)
44 assert_not_nil assigns(:changesets)
45 end
46
47 def test_browse_root
48 get :browse, :id => 3
49 assert_response :success
50 assert_template 'browse'
51 assert_not_nil assigns(:entries)
52 assert_equal 3, assigns(:entries).size
53 assert assigns(:entries).detect {|e| e.name == 'images' && e.kind == 'dir'}
54 assert assigns(:entries).detect {|e| e.name == 'sources' && e.kind == 'dir'}
55 assert assigns(:entries).detect {|e| e.name == 'README' && e.kind == 'file'}
56 end
57
58 def test_browse_directory
59 get :browse, :id => 3, :path => ['images']
60 assert_response :success
61 assert_template 'browse'
62 assert_not_nil assigns(:entries)
63 assert_equal 2, assigns(:entries).size
64 entry = assigns(:entries).detect {|e| e.name == 'edit.png'}
65 assert_not_nil entry
66 assert_equal 'file', entry.kind
67 assert_equal 'images/edit.png', entry.path
68 end
69
70 def test_changes
71 get :changes, :id => 3, :path => ['images', 'edit.png']
72 assert_response :success
73 assert_template 'changes'
74 assert_tag :tag => 'h2', :content => 'edit.png'
75 end
76
77 def test_diff
78 Project.find(3).repository.fetch_changesets
79 # Full diff of changeset 5
80 get :diff, :id => 3, :rev => 5
81 assert_response :success
82 assert_template 'diff'
83 # Line 22 removed
84 assert_tag :tag => 'th',
85 :content => /22/,
86 :sibling => { :tag => 'td',
87 :attributes => { :class => /diff_out/ },
88 :content => /def remove/ }
89 end
90 else
91 puts "Darcs test repository NOT FOUND. Skipping functional tests !!!"
92 def test_fake; assert true end
93 end
94 end
@@ -0,0 +1,123
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 require 'repositories_controller'
20
21 # Re-raise errors caught by the controller.
22 class RepositoriesController; def rescue_action(e) raise e end; end
23
24 class RepositoriesGitControllerTest < Test::Unit::TestCase
25 fixtures :projects, :users, :roles, :members, :repositories, :enabled_modules
26
27 # No '..' in the repository path
28 REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/git_repository'
29 REPOSITORY_PATH.gsub!(/\//, "\\") if RUBY_PLATFORM =~ /mswin/
30
31 def setup
32 @controller = RepositoriesController.new
33 @request = ActionController::TestRequest.new
34 @response = ActionController::TestResponse.new
35 User.current = nil
36 Repository::Git.create(:project => Project.find(3), :url => REPOSITORY_PATH)
37 end
38
39 if File.directory?(REPOSITORY_PATH)
40 def test_show
41 get :show, :id => 3
42 assert_response :success
43 assert_template 'show'
44 assert_not_nil assigns(:entries)
45 assert_not_nil assigns(:changesets)
46 end
47
48 def test_browse_root
49 get :browse, :id => 3
50 assert_response :success
51 assert_template 'browse'
52 assert_not_nil assigns(:entries)
53 assert_equal 3, assigns(:entries).size
54 assert assigns(:entries).detect {|e| e.name == 'images' && e.kind == 'dir'}
55 assert assigns(:entries).detect {|e| e.name == 'sources' && e.kind == 'dir'}
56 assert assigns(:entries).detect {|e| e.name == 'README' && e.kind == 'file'}
57 end
58
59 def test_browse_directory
60 get :browse, :id => 3, :path => ['images']
61 assert_response :success
62 assert_template 'browse'
63 assert_not_nil assigns(:entries)
64 assert_equal 2, assigns(:entries).size
65 entry = assigns(:entries).detect {|e| e.name == 'edit.png'}
66 assert_not_nil entry
67 assert_equal 'file', entry.kind
68 assert_equal 'images/edit.png', entry.path
69 end
70
71 def test_changes
72 get :changes, :id => 3, :path => ['images', 'edit.png']
73 assert_response :success
74 assert_template 'changes'
75 assert_tag :tag => 'h2', :content => 'edit.png'
76 end
77
78 def test_entry_show
79 get :entry, :id => 3, :path => ['sources', 'watchers_controller.rb']
80 assert_response :success
81 assert_template 'entry'
82 # Line 19
83 assert_tag :tag => 'th',
84 :content => /10/,
85 :attributes => { :class => /line-num/ },
86 :sibling => { :tag => 'td', :content => /WITHOUT ANY WARRANTY/ }
87 end
88
89 def test_entry_download
90 get :entry, :id => 3, :path => ['sources', 'watchers_controller.rb'], :format => 'raw'
91 assert_response :success
92 # File content
93 assert @response.body.include?('WITHOUT ANY WARRANTY')
94 end
95
96 def test_diff
97 # Full diff of changeset 2f9c0091
98 get :diff, :id => 3, :rev => '2f9c0091c754a91af7a9c478e36556b4bde8dcf7'
99 assert_response :success
100 assert_template 'diff'
101 # Line 22 removed
102 assert_tag :tag => 'th',
103 :content => /22/,
104 :sibling => { :tag => 'td',
105 :attributes => { :class => /diff_out/ },
106 :content => /def remove/ }
107 end
108
109 def test_annotate
110 get :annotate, :id => 3, :path => ['sources', 'watchers_controller.rb']
111 assert_response :success
112 assert_template 'annotate'
113 # Line 23, changeset 2f9c0091
114 assert_tag :tag => 'th', :content => /23/,
115 :sibling => { :tag => 'td', :child => { :tag => 'a', :content => /2f9c0091/ } },
116 :sibling => { :tag => 'td', :content => /jsmith/ },
117 :sibling => { :tag => 'td', :content => /watcher =/ }
118 end
119 else
120 puts "Git test repository NOT FOUND. Skipping functional tests !!!"
121 def test_fake; assert true end
122 end
123 end
@@ -0,0 +1,55
1 # redMine - project management software
2 # Copyright (C) 2006-2008 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 RepositoryDarcsTest < Test::Unit::TestCase
21 fixtures :projects
22
23 # No '..' in the repository path
24 REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/darcs_repository'
25
26 def setup
27 @project = Project.find(1)
28 assert @repository = Repository::Darcs.create(:project => @project, :url => REPOSITORY_PATH)
29 end
30
31 if File.directory?(REPOSITORY_PATH)
32 def test_fetch_changesets_from_scratch
33 @repository.fetch_changesets
34 @repository.reload
35
36 assert_equal 6, @repository.changesets.count
37 assert_equal 13, @repository.changes.count
38 assert_equal "Initial commit.", @repository.changesets.find_by_revision(1).comments
39 end
40
41 def test_fetch_changesets_incremental
42 @repository.fetch_changesets
43 # Remove changesets with revision > 3
44 @repository.changesets.find(:all, :conditions => 'revision > 3').each(&:destroy)
45 @repository.reload
46 assert_equal 3, @repository.changesets.count
47
48 @repository.fetch_changesets
49 assert_equal 6, @repository.changesets.count
50 end
51 else
52 puts "Darcs test repository NOT FOUND. Skipping unit tests !!!"
53 def test_fake; assert true end
54 end
55 end
@@ -0,0 +1,56
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 RepositoryGitTest < Test::Unit::TestCase
21 fixtures :projects
22
23 # No '..' in the repository path
24 REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/git_repository'
25 REPOSITORY_PATH.gsub!(/\//, "\\") if RUBY_PLATFORM =~ /mswin/
26
27 def setup
28 @project = Project.find(1)
29 assert @repository = Repository::Git.create(:project => @project, :url => REPOSITORY_PATH)
30 end
31
32 if File.directory?(REPOSITORY_PATH)
33 def test_fetch_changesets_from_scratch
34 @repository.fetch_changesets
35 @repository.reload
36
37 assert_equal 6, @repository.changesets.count
38 assert_equal 11, @repository.changes.count
39 assert_equal "Initial import.\nThe repository contains 3 files.", @repository.changesets.find(:first, :order => 'id ASC').comments
40 end
41
42 def test_fetch_changesets_incremental
43 @repository.fetch_changesets
44 # Remove the 3 latest changesets
45 @repository.changesets.find(:all, :order => 'id DESC', :limit => 3).each(&:destroy)
46 @repository.reload
47 assert_equal 3, @repository.changesets.count
48
49 @repository.fetch_changesets
50 assert_equal 6, @repository.changesets.count
51 end
52 else
53 puts "Git test repository NOT FOUND. Skipping unit tests !!!"
54 def test_fake; assert true end
55 end
56 end
@@ -1,298 +1,298
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'SVG/Graph/Bar'
18 require 'SVG/Graph/Bar'
19 require 'SVG/Graph/BarHorizontal'
19 require 'SVG/Graph/BarHorizontal'
20 require 'digest/sha1'
20 require 'digest/sha1'
21
21
22 class ChangesetNotFound < Exception
22 class ChangesetNotFound < Exception
23 end
23 end
24
24
25 class RepositoriesController < ApplicationController
25 class RepositoriesController < ApplicationController
26 layout 'base'
26 layout 'base'
27 menu_item :repository
27 menu_item :repository
28 before_filter :find_repository, :except => :edit
28 before_filter :find_repository, :except => :edit
29 before_filter :find_project, :only => :edit
29 before_filter :find_project, :only => :edit
30 before_filter :authorize
30 before_filter :authorize
31 accept_key_auth :revisions
31 accept_key_auth :revisions
32
32
33 def edit
33 def edit
34 @repository = @project.repository
34 @repository = @project.repository
35 if !@repository
35 if !@repository
36 @repository = Repository.factory(params[:repository_scm])
36 @repository = Repository.factory(params[:repository_scm])
37 @repository.project = @project
37 @repository.project = @project
38 end
38 end
39 if request.post?
39 if request.post?
40 @repository.attributes = params[:repository]
40 @repository.attributes = params[:repository]
41 @repository.save
41 @repository.save
42 end
42 end
43 render(:update) {|page| page.replace_html "tab-content-repository", :partial => 'projects/settings/repository'}
43 render(:update) {|page| page.replace_html "tab-content-repository", :partial => 'projects/settings/repository'}
44 end
44 end
45
45
46 def destroy
46 def destroy
47 @repository.destroy
47 @repository.destroy
48 redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'repository'
48 redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'repository'
49 end
49 end
50
50
51 def show
51 def show
52 # check if new revisions have been committed in the repository
52 # check if new revisions have been committed in the repository
53 @repository.fetch_changesets if Setting.autofetch_changesets?
53 @repository.fetch_changesets if Setting.autofetch_changesets?
54 # get entries for the browse frame
54 # get entries for the browse frame
55 @entries = @repository.entries('')
55 @entries = @repository.entries('')
56 # latest changesets
56 # latest changesets
57 @changesets = @repository.changesets.find(:all, :limit => 10, :order => "committed_on DESC")
57 @changesets = @repository.changesets.find(:all, :limit => 10, :order => "committed_on DESC")
58 show_error_not_found unless @entries || @changesets.any?
58 show_error_not_found unless @entries || @changesets.any?
59 rescue Redmine::Scm::Adapters::CommandFailed => e
59 rescue Redmine::Scm::Adapters::CommandFailed => e
60 show_error_command_failed(e.message)
60 show_error_command_failed(e.message)
61 end
61 end
62
62
63 def browse
63 def browse
64 @entries = @repository.entries(@path, @rev)
64 @entries = @repository.entries(@path, @rev)
65 if request.xhr?
65 if request.xhr?
66 @entries ? render(:partial => 'dir_list_content') : render(:nothing => true)
66 @entries ? render(:partial => 'dir_list_content') : render(:nothing => true)
67 else
67 else
68 show_error_not_found unless @entries
68 show_error_not_found unless @entries
69 end
69 end
70 rescue Redmine::Scm::Adapters::CommandFailed => e
70 rescue Redmine::Scm::Adapters::CommandFailed => e
71 show_error_command_failed(e.message)
71 show_error_command_failed(e.message)
72 end
72 end
73
73
74 def changes
74 def changes
75 @entry = @repository.scm.entry(@path, @rev)
75 @entry = @repository.scm.entry(@path, @rev)
76 show_error_not_found and return unless @entry
76 show_error_not_found and return unless @entry
77 @changesets = @repository.changesets_for_path(@path)
77 @changesets = @repository.changesets_for_path(@path)
78 rescue Redmine::Scm::Adapters::CommandFailed => e
78 rescue Redmine::Scm::Adapters::CommandFailed => e
79 show_error_command_failed(e.message)
79 show_error_command_failed(e.message)
80 end
80 end
81
81
82 def revisions
82 def revisions
83 @changeset_count = @repository.changesets.count
83 @changeset_count = @repository.changesets.count
84 @changeset_pages = Paginator.new self, @changeset_count,
84 @changeset_pages = Paginator.new self, @changeset_count,
85 per_page_option,
85 per_page_option,
86 params['page']
86 params['page']
87 @changesets = @repository.changesets.find(:all,
87 @changesets = @repository.changesets.find(:all,
88 :limit => @changeset_pages.items_per_page,
88 :limit => @changeset_pages.items_per_page,
89 :offset => @changeset_pages.current.offset)
89 :offset => @changeset_pages.current.offset)
90
90
91 respond_to do |format|
91 respond_to do |format|
92 format.html { render :layout => false if request.xhr? }
92 format.html { render :layout => false if request.xhr? }
93 format.atom { render_feed(@changesets, :title => "#{@project.name}: #{l(:label_revision_plural)}") }
93 format.atom { render_feed(@changesets, :title => "#{@project.name}: #{l(:label_revision_plural)}") }
94 end
94 end
95 end
95 end
96
96
97 def entry
97 def entry
98 @content = @repository.scm.cat(@path, @rev)
98 @content = @repository.scm.cat(@path, @rev)
99 show_error_not_found and return unless @content
99 show_error_not_found and return unless @content
100 if 'raw' == params[:format]
100 if 'raw' == params[:format]
101 send_data @content, :filename => @path.split('/').last
101 send_data @content, :filename => @path.split('/').last
102 else
102 else
103 # Prevent empty lines when displaying a file with Windows style eol
103 # Prevent empty lines when displaying a file with Windows style eol
104 @content.gsub!("\r\n", "\n")
104 @content.gsub!("\r\n", "\n")
105 end
105 end
106 rescue Redmine::Scm::Adapters::CommandFailed => e
106 rescue Redmine::Scm::Adapters::CommandFailed => e
107 show_error_command_failed(e.message)
107 show_error_command_failed(e.message)
108 end
108 end
109
109
110 def annotate
110 def annotate
111 @annotate = @repository.scm.annotate(@path, @rev)
111 @annotate = @repository.scm.annotate(@path, @rev)
112 show_error_not_found and return if @annotate.nil? || @annotate.empty?
112 show_error_not_found and return if @annotate.nil? || @annotate.empty?
113 rescue Redmine::Scm::Adapters::CommandFailed => e
113 rescue Redmine::Scm::Adapters::CommandFailed => e
114 show_error_command_failed(e.message)
114 show_error_command_failed(e.message)
115 end
115 end
116
116
117 def revision
117 def revision
118 @changeset = @repository.changesets.find_by_revision(@rev)
118 @changeset = @repository.changesets.find_by_revision(@rev)
119 raise ChangesetNotFound unless @changeset
119 raise ChangesetNotFound unless @changeset
120 @changes_count = @changeset.changes.size
120 @changes_count = @changeset.changes.size
121 @changes_pages = Paginator.new self, @changes_count, 150, params['page']
121 @changes_pages = Paginator.new self, @changes_count, 150, params['page']
122 @changes = @changeset.changes.find(:all,
122 @changes = @changeset.changes.find(:all,
123 :limit => @changes_pages.items_per_page,
123 :limit => @changes_pages.items_per_page,
124 :offset => @changes_pages.current.offset)
124 :offset => @changes_pages.current.offset)
125
125
126 respond_to do |format|
126 respond_to do |format|
127 format.html
127 format.html
128 format.js {render :layout => false}
128 format.js {render :layout => false}
129 end
129 end
130 rescue ChangesetNotFound
130 rescue ChangesetNotFound
131 show_error_not_found
131 show_error_not_found
132 rescue Redmine::Scm::Adapters::CommandFailed => e
132 rescue Redmine::Scm::Adapters::CommandFailed => e
133 show_error_command_failed(e.message)
133 show_error_command_failed(e.message)
134 end
134 end
135
135
136 def diff
136 def diff
137 @rev_to = params[:rev_to] ? params[:rev_to].to_i : (@rev - 1)
137 @rev_to = params[:rev_to]
138 @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
138 @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
139 @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
139 @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
140
140
141 # Save diff type as user preference
141 # Save diff type as user preference
142 if User.current.logged? && @diff_type != User.current.pref[:diff_type]
142 if User.current.logged? && @diff_type != User.current.pref[:diff_type]
143 User.current.pref[:diff_type] = @diff_type
143 User.current.pref[:diff_type] = @diff_type
144 User.current.preference.save
144 User.current.preference.save
145 end
145 end
146
146
147 @cache_key = "repositories/diff/#{@repository.id}/" + Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}")
147 @cache_key = "repositories/diff/#{@repository.id}/" + Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}")
148 unless read_fragment(@cache_key)
148 unless read_fragment(@cache_key)
149 @diff = @repository.diff(@path, @rev, @rev_to, @diff_type)
149 @diff = @repository.diff(@path, @rev, @rev_to, @diff_type)
150 show_error_not_found unless @diff
150 show_error_not_found unless @diff
151 end
151 end
152 rescue Redmine::Scm::Adapters::CommandFailed => e
152 rescue Redmine::Scm::Adapters::CommandFailed => e
153 show_error_command_failed(e.message)
153 show_error_command_failed(e.message)
154 end
154 end
155
155
156 def stats
156 def stats
157 end
157 end
158
158
159 def graph
159 def graph
160 data = nil
160 data = nil
161 case params[:graph]
161 case params[:graph]
162 when "commits_per_month"
162 when "commits_per_month"
163 data = graph_commits_per_month(@repository)
163 data = graph_commits_per_month(@repository)
164 when "commits_per_author"
164 when "commits_per_author"
165 data = graph_commits_per_author(@repository)
165 data = graph_commits_per_author(@repository)
166 end
166 end
167 if data
167 if data
168 headers["Content-Type"] = "image/svg+xml"
168 headers["Content-Type"] = "image/svg+xml"
169 send_data(data, :type => "image/svg+xml", :disposition => "inline")
169 send_data(data, :type => "image/svg+xml", :disposition => "inline")
170 else
170 else
171 render_404
171 render_404
172 end
172 end
173 end
173 end
174
174
175 private
175 private
176 def find_project
176 def find_project
177 @project = Project.find(params[:id])
177 @project = Project.find(params[:id])
178 rescue ActiveRecord::RecordNotFound
178 rescue ActiveRecord::RecordNotFound
179 render_404
179 render_404
180 end
180 end
181
181
182 def find_repository
182 def find_repository
183 @project = Project.find(params[:id])
183 @project = Project.find(params[:id])
184 @repository = @project.repository
184 @repository = @project.repository
185 render_404 and return false unless @repository
185 render_404 and return false unless @repository
186 @path = params[:path].join('/') unless params[:path].nil?
186 @path = params[:path].join('/') unless params[:path].nil?
187 @path ||= ''
187 @path ||= ''
188 @rev = params[:rev].to_i if params[:rev]
188 @rev = params[:rev]
189 rescue ActiveRecord::RecordNotFound
189 rescue ActiveRecord::RecordNotFound
190 render_404
190 render_404
191 end
191 end
192
192
193 def show_error_not_found
193 def show_error_not_found
194 render_error l(:error_scm_not_found)
194 render_error l(:error_scm_not_found)
195 end
195 end
196
196
197 def show_error_command_failed(msg)
197 def show_error_command_failed(msg)
198 render_error l(:error_scm_command_failed, msg)
198 render_error l(:error_scm_command_failed, msg)
199 end
199 end
200
200
201 def graph_commits_per_month(repository)
201 def graph_commits_per_month(repository)
202 @date_to = Date.today
202 @date_to = Date.today
203 @date_from = @date_to << 11
203 @date_from = @date_to << 11
204 @date_from = Date.civil(@date_from.year, @date_from.month, 1)
204 @date_from = Date.civil(@date_from.year, @date_from.month, 1)
205 commits_by_day = repository.changesets.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
205 commits_by_day = repository.changesets.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
206 commits_by_month = [0] * 12
206 commits_by_month = [0] * 12
207 commits_by_day.each {|c| commits_by_month[c.first.to_date.months_ago] += c.last }
207 commits_by_day.each {|c| commits_by_month[c.first.to_date.months_ago] += c.last }
208
208
209 changes_by_day = repository.changes.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
209 changes_by_day = repository.changes.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
210 changes_by_month = [0] * 12
210 changes_by_month = [0] * 12
211 changes_by_day.each {|c| changes_by_month[c.first.to_date.months_ago] += c.last }
211 changes_by_day.each {|c| changes_by_month[c.first.to_date.months_ago] += c.last }
212
212
213 fields = []
213 fields = []
214 month_names = l(:actionview_datehelper_select_month_names_abbr).split(',')
214 month_names = l(:actionview_datehelper_select_month_names_abbr).split(',')
215 12.times {|m| fields << month_names[((Date.today.month - 1 - m) % 12)]}
215 12.times {|m| fields << month_names[((Date.today.month - 1 - m) % 12)]}
216
216
217 graph = SVG::Graph::Bar.new(
217 graph = SVG::Graph::Bar.new(
218 :height => 300,
218 :height => 300,
219 :width => 500,
219 :width => 500,
220 :fields => fields.reverse,
220 :fields => fields.reverse,
221 :stack => :side,
221 :stack => :side,
222 :scale_integers => true,
222 :scale_integers => true,
223 :step_x_labels => 2,
223 :step_x_labels => 2,
224 :show_data_values => false,
224 :show_data_values => false,
225 :graph_title => l(:label_commits_per_month),
225 :graph_title => l(:label_commits_per_month),
226 :show_graph_title => true
226 :show_graph_title => true
227 )
227 )
228
228
229 graph.add_data(
229 graph.add_data(
230 :data => commits_by_month[0..11].reverse,
230 :data => commits_by_month[0..11].reverse,
231 :title => l(:label_revision_plural)
231 :title => l(:label_revision_plural)
232 )
232 )
233
233
234 graph.add_data(
234 graph.add_data(
235 :data => changes_by_month[0..11].reverse,
235 :data => changes_by_month[0..11].reverse,
236 :title => l(:label_change_plural)
236 :title => l(:label_change_plural)
237 )
237 )
238
238
239 graph.burn
239 graph.burn
240 end
240 end
241
241
242 def graph_commits_per_author(repository)
242 def graph_commits_per_author(repository)
243 commits_by_author = repository.changesets.count(:all, :group => :committer)
243 commits_by_author = repository.changesets.count(:all, :group => :committer)
244 commits_by_author.sort! {|x, y| x.last <=> y.last}
244 commits_by_author.sort! {|x, y| x.last <=> y.last}
245
245
246 changes_by_author = repository.changes.count(:all, :group => :committer)
246 changes_by_author = repository.changes.count(:all, :group => :committer)
247 h = changes_by_author.inject({}) {|o, i| o[i.first] = i.last; o}
247 h = changes_by_author.inject({}) {|o, i| o[i.first] = i.last; o}
248
248
249 fields = commits_by_author.collect {|r| r.first}
249 fields = commits_by_author.collect {|r| r.first}
250 commits_data = commits_by_author.collect {|r| r.last}
250 commits_data = commits_by_author.collect {|r| r.last}
251 changes_data = commits_by_author.collect {|r| h[r.first] || 0}
251 changes_data = commits_by_author.collect {|r| h[r.first] || 0}
252
252
253 fields = fields + [""]*(10 - fields.length) if fields.length<10
253 fields = fields + [""]*(10 - fields.length) if fields.length<10
254 commits_data = commits_data + [0]*(10 - commits_data.length) if commits_data.length<10
254 commits_data = commits_data + [0]*(10 - commits_data.length) if commits_data.length<10
255 changes_data = changes_data + [0]*(10 - changes_data.length) if changes_data.length<10
255 changes_data = changes_data + [0]*(10 - changes_data.length) if changes_data.length<10
256
256
257 graph = SVG::Graph::BarHorizontal.new(
257 graph = SVG::Graph::BarHorizontal.new(
258 :height => 300,
258 :height => 300,
259 :width => 500,
259 :width => 500,
260 :fields => fields,
260 :fields => fields,
261 :stack => :side,
261 :stack => :side,
262 :scale_integers => true,
262 :scale_integers => true,
263 :show_data_values => false,
263 :show_data_values => false,
264 :rotate_y_labels => false,
264 :rotate_y_labels => false,
265 :graph_title => l(:label_commits_per_author),
265 :graph_title => l(:label_commits_per_author),
266 :show_graph_title => true
266 :show_graph_title => true
267 )
267 )
268
268
269 graph.add_data(
269 graph.add_data(
270 :data => commits_data,
270 :data => commits_data,
271 :title => l(:label_revision_plural)
271 :title => l(:label_revision_plural)
272 )
272 )
273
273
274 graph.add_data(
274 graph.add_data(
275 :data => changes_data,
275 :data => changes_data,
276 :title => l(:label_change_plural)
276 :title => l(:label_change_plural)
277 )
277 )
278
278
279 graph.burn
279 graph.burn
280 end
280 end
281
281
282 end
282 end
283
283
284 class Date
284 class Date
285 def months_ago(date = Date.today)
285 def months_ago(date = Date.today)
286 (date.year - self.year)*12 + (date.month - self.month)
286 (date.year - self.year)*12 + (date.month - self.month)
287 end
287 end
288
288
289 def weeks_ago(date = Date.today)
289 def weeks_ago(date = Date.today)
290 (date.year - self.year)*52 + (date.cweek - self.cweek)
290 (date.year - self.year)*52 + (date.cweek - self.cweek)
291 end
291 end
292 end
292 end
293
293
294 class String
294 class String
295 def with_leading_slash
295 def with_leading_slash
296 starts_with?('/') ? self : "/#{self}"
296 starts_with?('/') ? self : "/#{self}"
297 end
297 end
298 end
298 end
@@ -1,466 +1,471
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 module ApplicationHelper
18 module ApplicationHelper
19 include Redmine::WikiFormatting::Macros::Definitions
19 include Redmine::WikiFormatting::Macros::Definitions
20
20
21 def current_role
21 def current_role
22 @current_role ||= User.current.role_for_project(@project)
22 @current_role ||= User.current.role_for_project(@project)
23 end
23 end
24
24
25 # Return true if user is authorized for controller/action, otherwise false
25 # Return true if user is authorized for controller/action, otherwise false
26 def authorize_for(controller, action)
26 def authorize_for(controller, action)
27 User.current.allowed_to?({:controller => controller, :action => action}, @project)
27 User.current.allowed_to?({:controller => controller, :action => action}, @project)
28 end
28 end
29
29
30 # Display a link if user is authorized
30 # Display a link if user is authorized
31 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
31 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
32 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
32 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
33 end
33 end
34
34
35 # Display a link to user's account page
35 # Display a link to user's account page
36 def link_to_user(user)
36 def link_to_user(user)
37 user ? link_to(user, :controller => 'account', :action => 'show', :id => user) : 'Anonymous'
37 user ? link_to(user, :controller => 'account', :action => 'show', :id => user) : 'Anonymous'
38 end
38 end
39
39
40 def link_to_issue(issue)
40 def link_to_issue(issue)
41 link_to "#{issue.tracker.name} ##{issue.id}", :controller => "issues", :action => "show", :id => issue
41 link_to "#{issue.tracker.name} ##{issue.id}", :controller => "issues", :action => "show", :id => issue
42 end
42 end
43
43
44 def toggle_link(name, id, options={})
44 def toggle_link(name, id, options={})
45 onclick = "Element.toggle('#{id}'); "
45 onclick = "Element.toggle('#{id}'); "
46 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
46 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
47 onclick << "return false;"
47 onclick << "return false;"
48 link_to(name, "#", :onclick => onclick)
48 link_to(name, "#", :onclick => onclick)
49 end
49 end
50
50
51 def show_and_goto_link(name, id, options={})
51 def show_and_goto_link(name, id, options={})
52 onclick = "Element.show('#{id}'); "
52 onclick = "Element.show('#{id}'); "
53 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
53 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
54 onclick << "Element.scrollTo('#{id}'); "
54 onclick << "Element.scrollTo('#{id}'); "
55 onclick << "return false;"
55 onclick << "return false;"
56 link_to(name, "#", options.merge(:onclick => onclick))
56 link_to(name, "#", options.merge(:onclick => onclick))
57 end
57 end
58
58
59 def image_to_function(name, function, html_options = {})
59 def image_to_function(name, function, html_options = {})
60 html_options.symbolize_keys!
60 html_options.symbolize_keys!
61 tag(:input, html_options.merge({
61 tag(:input, html_options.merge({
62 :type => "image", :src => image_path(name),
62 :type => "image", :src => image_path(name),
63 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
63 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
64 }))
64 }))
65 end
65 end
66
66
67 def prompt_to_remote(name, text, param, url, html_options = {})
67 def prompt_to_remote(name, text, param, url, html_options = {})
68 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
68 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
69 link_to name, {}, html_options
69 link_to name, {}, html_options
70 end
70 end
71
71
72 def format_date(date)
72 def format_date(date)
73 return nil unless date
73 return nil unless date
74 # "Setting.date_format.size < 2" is a temporary fix (content of date_format setting changed)
74 # "Setting.date_format.size < 2" is a temporary fix (content of date_format setting changed)
75 @date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format)
75 @date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format)
76 date.strftime(@date_format)
76 date.strftime(@date_format)
77 end
77 end
78
78
79 def format_time(time, include_date = true)
79 def format_time(time, include_date = true)
80 return nil unless time
80 return nil unless time
81 time = time.to_time if time.is_a?(String)
81 time = time.to_time if time.is_a?(String)
82 zone = User.current.time_zone
82 zone = User.current.time_zone
83 if time.utc?
83 if time.utc?
84 local = zone ? zone.adjust(time) : time.getlocal
84 local = zone ? zone.adjust(time) : time.getlocal
85 else
85 else
86 local = zone ? zone.adjust(time.getutc) : time
86 local = zone ? zone.adjust(time.getutc) : time
87 end
87 end
88 @date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format)
88 @date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format)
89 @time_format ||= (Setting.time_format.blank? ? l(:general_fmt_time) : Setting.time_format)
89 @time_format ||= (Setting.time_format.blank? ? l(:general_fmt_time) : Setting.time_format)
90 include_date ? local.strftime("#{@date_format} #{@time_format}") : local.strftime(@time_format)
90 include_date ? local.strftime("#{@date_format} #{@time_format}") : local.strftime(@time_format)
91 end
91 end
92
92
93 def html_hours(text)
93 def html_hours(text)
94 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
94 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
95 end
95 end
96
96
97 def authoring(created, author)
97 def authoring(created, author)
98 time_tag = content_tag('acronym', distance_of_time_in_words(Time.now, created), :title => format_time(created))
98 time_tag = content_tag('acronym', distance_of_time_in_words(Time.now, created), :title => format_time(created))
99 l(:label_added_time_by, author || 'Anonymous', time_tag)
99 l(:label_added_time_by, author || 'Anonymous', time_tag)
100 end
100 end
101
101
102 def l_or_humanize(s)
102 def l_or_humanize(s)
103 l_has_string?("label_#{s}".to_sym) ? l("label_#{s}".to_sym) : s.to_s.humanize
103 l_has_string?("label_#{s}".to_sym) ? l("label_#{s}".to_sym) : s.to_s.humanize
104 end
104 end
105
105
106 def day_name(day)
106 def day_name(day)
107 l(:general_day_names).split(',')[day-1]
107 l(:general_day_names).split(',')[day-1]
108 end
108 end
109
109
110 def month_name(month)
110 def month_name(month)
111 l(:actionview_datehelper_select_month_names).split(',')[month-1]
111 l(:actionview_datehelper_select_month_names).split(',')[month-1]
112 end
112 end
113
113
114 def pagination_links_full(paginator, count=nil, options={})
114 def pagination_links_full(paginator, count=nil, options={})
115 page_param = options.delete(:page_param) || :page
115 page_param = options.delete(:page_param) || :page
116 url_param = params.dup
116 url_param = params.dup
117 # don't reuse params if filters are present
117 # don't reuse params if filters are present
118 url_param.clear if url_param.has_key?(:set_filter)
118 url_param.clear if url_param.has_key?(:set_filter)
119
119
120 html = ''
120 html = ''
121 html << link_to_remote(('&#171; ' + l(:label_previous)),
121 html << link_to_remote(('&#171; ' + l(:label_previous)),
122 {:update => 'content',
122 {:update => 'content',
123 :url => url_param.merge(page_param => paginator.current.previous),
123 :url => url_param.merge(page_param => paginator.current.previous),
124 :complete => 'window.scrollTo(0,0)'},
124 :complete => 'window.scrollTo(0,0)'},
125 {:href => url_for(:params => url_param.merge(page_param => paginator.current.previous))}) + ' ' if paginator.current.previous
125 {:href => url_for(:params => url_param.merge(page_param => paginator.current.previous))}) + ' ' if paginator.current.previous
126
126
127 html << (pagination_links_each(paginator, options) do |n|
127 html << (pagination_links_each(paginator, options) do |n|
128 link_to_remote(n.to_s,
128 link_to_remote(n.to_s,
129 {:url => {:params => url_param.merge(page_param => n)},
129 {:url => {:params => url_param.merge(page_param => n)},
130 :update => 'content',
130 :update => 'content',
131 :complete => 'window.scrollTo(0,0)'},
131 :complete => 'window.scrollTo(0,0)'},
132 {:href => url_for(:params => url_param.merge(page_param => n))})
132 {:href => url_for(:params => url_param.merge(page_param => n))})
133 end || '')
133 end || '')
134
134
135 html << ' ' + link_to_remote((l(:label_next) + ' &#187;'),
135 html << ' ' + link_to_remote((l(:label_next) + ' &#187;'),
136 {:update => 'content',
136 {:update => 'content',
137 :url => url_param.merge(page_param => paginator.current.next),
137 :url => url_param.merge(page_param => paginator.current.next),
138 :complete => 'window.scrollTo(0,0)'},
138 :complete => 'window.scrollTo(0,0)'},
139 {:href => url_for(:params => url_param.merge(page_param => paginator.current.next))}) if paginator.current.next
139 {:href => url_for(:params => url_param.merge(page_param => paginator.current.next))}) if paginator.current.next
140
140
141 unless count.nil?
141 unless count.nil?
142 html << [" (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})", per_page_links(paginator.items_per_page)].compact.join(' | ')
142 html << [" (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})", per_page_links(paginator.items_per_page)].compact.join(' | ')
143 end
143 end
144
144
145 html
145 html
146 end
146 end
147
147
148 def per_page_links(selected=nil)
148 def per_page_links(selected=nil)
149 url_param = params.dup
149 url_param = params.dup
150 url_param.clear if url_param.has_key?(:set_filter)
150 url_param.clear if url_param.has_key?(:set_filter)
151
151
152 links = Setting.per_page_options_array.collect do |n|
152 links = Setting.per_page_options_array.collect do |n|
153 n == selected ? n : link_to_remote(n, {:update => "content", :url => params.dup.merge(:per_page => n)},
153 n == selected ? n : link_to_remote(n, {:update => "content", :url => params.dup.merge(:per_page => n)},
154 {:href => url_for(url_param.merge(:per_page => n))})
154 {:href => url_for(url_param.merge(:per_page => n))})
155 end
155 end
156 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
156 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
157 end
157 end
158
158
159 def html_title(*args)
159 def html_title(*args)
160 if args.empty?
160 if args.empty?
161 title = []
161 title = []
162 title << @project.name if @project
162 title << @project.name if @project
163 title += @html_title if @html_title
163 title += @html_title if @html_title
164 title << Setting.app_title
164 title << Setting.app_title
165 title.compact.join(' - ')
165 title.compact.join(' - ')
166 else
166 else
167 @html_title ||= []
167 @html_title ||= []
168 @html_title += args
168 @html_title += args
169 end
169 end
170 end
170 end
171
171
172 def accesskey(s)
172 def accesskey(s)
173 Redmine::AccessKeys.key_for s
173 Redmine::AccessKeys.key_for s
174 end
174 end
175
175
176 # Formats text according to system settings.
176 # Formats text according to system settings.
177 # 2 ways to call this method:
177 # 2 ways to call this method:
178 # * with a String: textilizable(text, options)
178 # * with a String: textilizable(text, options)
179 # * with an object and one of its attribute: textilizable(issue, :description, options)
179 # * with an object and one of its attribute: textilizable(issue, :description, options)
180 def textilizable(*args)
180 def textilizable(*args)
181 options = args.last.is_a?(Hash) ? args.pop : {}
181 options = args.last.is_a?(Hash) ? args.pop : {}
182 case args.size
182 case args.size
183 when 1
183 when 1
184 obj = nil
184 obj = nil
185 text = args.shift
185 text = args.shift
186 when 2
186 when 2
187 obj = args.shift
187 obj = args.shift
188 text = obj.send(args.shift).to_s
188 text = obj.send(args.shift).to_s
189 else
189 else
190 raise ArgumentError, 'invalid arguments to textilizable'
190 raise ArgumentError, 'invalid arguments to textilizable'
191 end
191 end
192 return '' if text.blank?
192 return '' if text.blank?
193
193
194 only_path = options.delete(:only_path) == false ? false : true
194 only_path = options.delete(:only_path) == false ? false : true
195
195
196 # when using an image link, try to use an attachment, if possible
196 # when using an image link, try to use an attachment, if possible
197 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
197 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
198
198
199 if attachments
199 if attachments
200 text = text.gsub(/!((\<|\=|\>)?(\([^\)]+\))?(\[[^\]]+\])?(\{[^\}]+\})?)(\S+\.(gif|jpg|jpeg|png))!/) do |m|
200 text = text.gsub(/!((\<|\=|\>)?(\([^\)]+\))?(\[[^\]]+\])?(\{[^\}]+\})?)(\S+\.(gif|jpg|jpeg|png))!/) do |m|
201 style = $1
201 style = $1
202 filename = $6
202 filename = $6
203 rf = Regexp.new(filename, Regexp::IGNORECASE)
203 rf = Regexp.new(filename, Regexp::IGNORECASE)
204 # search for the picture in attachments
204 # search for the picture in attachments
205 if found = attachments.detect { |att| att.filename =~ rf }
205 if found = attachments.detect { |att| att.filename =~ rf }
206 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found.id
206 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found.id
207 "!#{style}#{image_url}!"
207 "!#{style}#{image_url}!"
208 else
208 else
209 "!#{style}#{filename}!"
209 "!#{style}#{filename}!"
210 end
210 end
211 end
211 end
212 end
212 end
213
213
214 text = (Setting.text_formatting == 'textile') ?
214 text = (Setting.text_formatting == 'textile') ?
215 Redmine::WikiFormatting.to_html(text) { |macro, args| exec_macro(macro, obj, args) } :
215 Redmine::WikiFormatting.to_html(text) { |macro, args| exec_macro(macro, obj, args) } :
216 simple_format(auto_link(h(text)))
216 simple_format(auto_link(h(text)))
217
217
218 # different methods for formatting wiki links
218 # different methods for formatting wiki links
219 case options[:wiki_links]
219 case options[:wiki_links]
220 when :local
220 when :local
221 # used for local links to html files
221 # used for local links to html files
222 format_wiki_link = Proc.new {|project, title| "#{title}.html" }
222 format_wiki_link = Proc.new {|project, title| "#{title}.html" }
223 when :anchor
223 when :anchor
224 # used for single-file wiki export
224 # used for single-file wiki export
225 format_wiki_link = Proc.new {|project, title| "##{title}" }
225 format_wiki_link = Proc.new {|project, title| "##{title}" }
226 else
226 else
227 format_wiki_link = Proc.new {|project, title| url_for(:only_path => only_path, :controller => 'wiki', :action => 'index', :id => project, :page => title) }
227 format_wiki_link = Proc.new {|project, title| url_for(:only_path => only_path, :controller => 'wiki', :action => 'index', :id => project, :page => title) }
228 end
228 end
229
229
230 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
230 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
231
231
232 # Wiki links
232 # Wiki links
233 #
233 #
234 # Examples:
234 # Examples:
235 # [[mypage]]
235 # [[mypage]]
236 # [[mypage|mytext]]
236 # [[mypage|mytext]]
237 # wiki links can refer other project wikis, using project name or identifier:
237 # wiki links can refer other project wikis, using project name or identifier:
238 # [[project:]] -> wiki starting page
238 # [[project:]] -> wiki starting page
239 # [[project:|mytext]]
239 # [[project:|mytext]]
240 # [[project:mypage]]
240 # [[project:mypage]]
241 # [[project:mypage|mytext]]
241 # [[project:mypage|mytext]]
242 text = text.gsub(/(!)?(\[\[([^\]\|]+)(\|([^\]\|]+))?\]\])/) do |m|
242 text = text.gsub(/(!)?(\[\[([^\]\|]+)(\|([^\]\|]+))?\]\])/) do |m|
243 link_project = project
243 link_project = project
244 esc, all, page, title = $1, $2, $3, $5
244 esc, all, page, title = $1, $2, $3, $5
245 if esc.nil?
245 if esc.nil?
246 if page =~ /^([^\:]+)\:(.*)$/
246 if page =~ /^([^\:]+)\:(.*)$/
247 link_project = Project.find_by_name($1) || Project.find_by_identifier($1)
247 link_project = Project.find_by_name($1) || Project.find_by_identifier($1)
248 page = $2
248 page = $2
249 title ||= $1 if page.blank?
249 title ||= $1 if page.blank?
250 end
250 end
251
251
252 if link_project && link_project.wiki
252 if link_project && link_project.wiki
253 # check if page exists
253 # check if page exists
254 wiki_page = link_project.wiki.find_page(page)
254 wiki_page = link_project.wiki.find_page(page)
255 link_to((title || page), format_wiki_link.call(link_project, Wiki.titleize(page)),
255 link_to((title || page), format_wiki_link.call(link_project, Wiki.titleize(page)),
256 :class => ('wiki-page' + (wiki_page ? '' : ' new')))
256 :class => ('wiki-page' + (wiki_page ? '' : ' new')))
257 else
257 else
258 # project or wiki doesn't exist
258 # project or wiki doesn't exist
259 title || page
259 title || page
260 end
260 end
261 else
261 else
262 all
262 all
263 end
263 end
264 end
264 end
265
265
266 # Redmine links
266 # Redmine links
267 #
267 #
268 # Examples:
268 # Examples:
269 # Issues:
269 # Issues:
270 # #52 -> Link to issue #52
270 # #52 -> Link to issue #52
271 # Changesets:
271 # Changesets:
272 # r52 -> Link to revision 52
272 # r52 -> Link to revision 52
273 # commit:a85130f -> Link to scmid starting with a85130f
273 # Documents:
274 # Documents:
274 # document#17 -> Link to document with id 17
275 # document#17 -> Link to document with id 17
275 # document:Greetings -> Link to the document with title "Greetings"
276 # document:Greetings -> Link to the document with title "Greetings"
276 # document:"Some document" -> Link to the document with title "Some document"
277 # document:"Some document" -> Link to the document with title "Some document"
277 # Versions:
278 # Versions:
278 # version#3 -> Link to version with id 3
279 # version#3 -> Link to version with id 3
279 # version:1.0.0 -> Link to version named "1.0.0"
280 # version:1.0.0 -> Link to version named "1.0.0"
280 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
281 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
281 # Attachments:
282 # Attachments:
282 # attachment:file.zip -> Link to the attachment of the current object named file.zip
283 # attachment:file.zip -> Link to the attachment of the current object named file.zip
283 text = text.gsub(%r{([\s\(,-^])(!)?(attachment|document|version)?((#|r)(\d+)|(:)([^"][^\s<>]+|"[^"]+"))(?=[[:punct:]]|\s|<|$)}) do |m|
284 text = text.gsub(%r{([\s\(,-^])(!)?(attachment|document|version|commit)?((#|r)(\d+)|(:)([^"][^\s<>]+|"[^"]+"))(?=[[:punct:]]|\s|<|$)}) do |m|
284 leading, esc, prefix, sep, oid = $1, $2, $3, $5 || $7, $6 || $8
285 leading, esc, prefix, sep, oid = $1, $2, $3, $5 || $7, $6 || $8
285 link = nil
286 link = nil
286 if esc.nil?
287 if esc.nil?
287 if prefix.nil? && sep == 'r'
288 if prefix.nil? && sep == 'r'
288 if project && (changeset = project.changesets.find_by_revision(oid))
289 if project && (changeset = project.changesets.find_by_revision(oid))
289 link = link_to("r#{oid}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project.id, :rev => oid},
290 link = link_to("r#{oid}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project.id, :rev => oid},
290 :class => 'changeset',
291 :class => 'changeset',
291 :title => truncate(changeset.comments, 100))
292 :title => truncate(changeset.comments, 100))
292 end
293 end
293 elsif sep == '#'
294 elsif sep == '#'
294 oid = oid.to_i
295 oid = oid.to_i
295 case prefix
296 case prefix
296 when nil
297 when nil
297 if issue = Issue.find_by_id(oid, :include => [:project, :status], :conditions => Project.visible_by(User.current))
298 if issue = Issue.find_by_id(oid, :include => [:project, :status], :conditions => Project.visible_by(User.current))
298 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
299 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
299 :class => (issue.closed? ? 'issue closed' : 'issue'),
300 :class => (issue.closed? ? 'issue closed' : 'issue'),
300 :title => "#{truncate(issue.subject, 100)} (#{issue.status.name})")
301 :title => "#{truncate(issue.subject, 100)} (#{issue.status.name})")
301 link = content_tag('del', link) if issue.closed?
302 link = content_tag('del', link) if issue.closed?
302 end
303 end
303 when 'document'
304 when 'document'
304 if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
305 if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
305 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
306 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
306 :class => 'document'
307 :class => 'document'
307 end
308 end
308 when 'version'
309 when 'version'
309 if version = Version.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
310 if version = Version.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
310 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
311 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
311 :class => 'version'
312 :class => 'version'
312 end
313 end
313 end
314 end
314 elsif sep == ':'
315 elsif sep == ':'
315 # removes the double quotes if any
316 # removes the double quotes if any
316 name = oid.gsub(%r{^"(.*)"$}, "\\1")
317 name = oid.gsub(%r{^"(.*)"$}, "\\1")
317 case prefix
318 case prefix
318 when 'document'
319 when 'document'
319 if project && document = project.documents.find_by_title(name)
320 if project && document = project.documents.find_by_title(name)
320 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
321 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
321 :class => 'document'
322 :class => 'document'
322 end
323 end
323 when 'version'
324 when 'version'
324 if project && version = project.versions.find_by_name(name)
325 if project && version = project.versions.find_by_name(name)
325 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
326 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
326 :class => 'version'
327 :class => 'version'
327 end
328 end
329 when 'commit'
330 if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
331 link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project.id, :rev => changeset.revision}, :class => 'changeset', :title => truncate(changeset.comments, 100)
332 end
328 when 'attachment'
333 when 'attachment'
329 if attachments && attachment = attachments.detect {|a| a.filename == name }
334 if attachments && attachment = attachments.detect {|a| a.filename == name }
330 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
335 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
331 :class => 'attachment'
336 :class => 'attachment'
332 end
337 end
333 end
338 end
334 end
339 end
335 end
340 end
336 leading + (link || "#{prefix}#{sep}#{oid}")
341 leading + (link || "#{prefix}#{sep}#{oid}")
337 end
342 end
338
343
339 text
344 text
340 end
345 end
341
346
342 # Same as Rails' simple_format helper without using paragraphs
347 # Same as Rails' simple_format helper without using paragraphs
343 def simple_format_without_paragraph(text)
348 def simple_format_without_paragraph(text)
344 text.to_s.
349 text.to_s.
345 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
350 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
346 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
351 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
347 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
352 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
348 end
353 end
349
354
350 def error_messages_for(object_name, options = {})
355 def error_messages_for(object_name, options = {})
351 options = options.symbolize_keys
356 options = options.symbolize_keys
352 object = instance_variable_get("@#{object_name}")
357 object = instance_variable_get("@#{object_name}")
353 if object && !object.errors.empty?
358 if object && !object.errors.empty?
354 # build full_messages here with controller current language
359 # build full_messages here with controller current language
355 full_messages = []
360 full_messages = []
356 object.errors.each do |attr, msg|
361 object.errors.each do |attr, msg|
357 next if msg.nil?
362 next if msg.nil?
358 msg = msg.first if msg.is_a? Array
363 msg = msg.first if msg.is_a? Array
359 if attr == "base"
364 if attr == "base"
360 full_messages << l(msg)
365 full_messages << l(msg)
361 else
366 else
362 full_messages << "&#171; " + (l_has_string?("field_" + attr) ? l("field_" + attr) : object.class.human_attribute_name(attr)) + " &#187; " + l(msg) unless attr == "custom_values"
367 full_messages << "&#171; " + (l_has_string?("field_" + attr) ? l("field_" + attr) : object.class.human_attribute_name(attr)) + " &#187; " + l(msg) unless attr == "custom_values"
363 end
368 end
364 end
369 end
365 # retrieve custom values error messages
370 # retrieve custom values error messages
366 if object.errors[:custom_values]
371 if object.errors[:custom_values]
367 object.custom_values.each do |v|
372 object.custom_values.each do |v|
368 v.errors.each do |attr, msg|
373 v.errors.each do |attr, msg|
369 next if msg.nil?
374 next if msg.nil?
370 msg = msg.first if msg.is_a? Array
375 msg = msg.first if msg.is_a? Array
371 full_messages << "&#171; " + v.custom_field.name + " &#187; " + l(msg)
376 full_messages << "&#171; " + v.custom_field.name + " &#187; " + l(msg)
372 end
377 end
373 end
378 end
374 end
379 end
375 content_tag("div",
380 content_tag("div",
376 content_tag(
381 content_tag(
377 options[:header_tag] || "span", lwr(:gui_validation_error, full_messages.length) + ":"
382 options[:header_tag] || "span", lwr(:gui_validation_error, full_messages.length) + ":"
378 ) +
383 ) +
379 content_tag("ul", full_messages.collect { |msg| content_tag("li", msg) }),
384 content_tag("ul", full_messages.collect { |msg| content_tag("li", msg) }),
380 "id" => options[:id] || "errorExplanation", "class" => options[:class] || "errorExplanation"
385 "id" => options[:id] || "errorExplanation", "class" => options[:class] || "errorExplanation"
381 )
386 )
382 else
387 else
383 ""
388 ""
384 end
389 end
385 end
390 end
386
391
387 def lang_options_for_select(blank=true)
392 def lang_options_for_select(blank=true)
388 (blank ? [["(auto)", ""]] : []) +
393 (blank ? [["(auto)", ""]] : []) +
389 GLoc.valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
394 GLoc.valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
390 end
395 end
391
396
392 def label_tag_for(name, option_tags = nil, options = {})
397 def label_tag_for(name, option_tags = nil, options = {})
393 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
398 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
394 content_tag("label", label_text)
399 content_tag("label", label_text)
395 end
400 end
396
401
397 def labelled_tabular_form_for(name, object, options, &proc)
402 def labelled_tabular_form_for(name, object, options, &proc)
398 options[:html] ||= {}
403 options[:html] ||= {}
399 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
404 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
400 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
405 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
401 end
406 end
402
407
403 def check_all_links(form_name)
408 def check_all_links(form_name)
404 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
409 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
405 " | " +
410 " | " +
406 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
411 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
407 end
412 end
408
413
409 def progress_bar(pcts, options={})
414 def progress_bar(pcts, options={})
410 pcts = [pcts, pcts] unless pcts.is_a?(Array)
415 pcts = [pcts, pcts] unless pcts.is_a?(Array)
411 pcts[1] = pcts[1] - pcts[0]
416 pcts[1] = pcts[1] - pcts[0]
412 pcts << (100 - pcts[1] - pcts[0])
417 pcts << (100 - pcts[1] - pcts[0])
413 width = options[:width] || '100px;'
418 width = options[:width] || '100px;'
414 legend = options[:legend] || ''
419 legend = options[:legend] || ''
415 content_tag('table',
420 content_tag('table',
416 content_tag('tr',
421 content_tag('tr',
417 (pcts[0] > 0 ? content_tag('td', '', :width => "#{pcts[0].floor}%;", :class => 'closed') : '') +
422 (pcts[0] > 0 ? content_tag('td', '', :width => "#{pcts[0].floor}%;", :class => 'closed') : '') +
418 (pcts[1] > 0 ? content_tag('td', '', :width => "#{pcts[1].floor}%;", :class => 'done') : '') +
423 (pcts[1] > 0 ? content_tag('td', '', :width => "#{pcts[1].floor}%;", :class => 'done') : '') +
419 (pcts[2] > 0 ? content_tag('td', '', :width => "#{pcts[2].floor}%;", :class => 'todo') : '')
424 (pcts[2] > 0 ? content_tag('td', '', :width => "#{pcts[2].floor}%;", :class => 'todo') : '')
420 ), :class => 'progress', :style => "width: #{width};") +
425 ), :class => 'progress', :style => "width: #{width};") +
421 content_tag('p', legend, :class => 'pourcent')
426 content_tag('p', legend, :class => 'pourcent')
422 end
427 end
423
428
424 def context_menu_link(name, url, options={})
429 def context_menu_link(name, url, options={})
425 options[:class] ||= ''
430 options[:class] ||= ''
426 if options.delete(:selected)
431 if options.delete(:selected)
427 options[:class] << ' icon-checked disabled'
432 options[:class] << ' icon-checked disabled'
428 options[:disabled] = true
433 options[:disabled] = true
429 end
434 end
430 if options.delete(:disabled)
435 if options.delete(:disabled)
431 options.delete(:method)
436 options.delete(:method)
432 options.delete(:confirm)
437 options.delete(:confirm)
433 options.delete(:onclick)
438 options.delete(:onclick)
434 options[:class] << ' disabled'
439 options[:class] << ' disabled'
435 url = '#'
440 url = '#'
436 end
441 end
437 link_to name, url, options
442 link_to name, url, options
438 end
443 end
439
444
440 def calendar_for(field_id)
445 def calendar_for(field_id)
441 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
446 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
442 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
447 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
443 end
448 end
444
449
445 def wikitoolbar_for(field_id)
450 def wikitoolbar_for(field_id)
446 return '' unless Setting.text_formatting == 'textile'
451 return '' unless Setting.text_formatting == 'textile'
447
452
448 help_link = l(:setting_text_formatting) + ': ' +
453 help_link = l(:setting_text_formatting) + ': ' +
449 link_to(l(:label_help), compute_public_path('wiki_syntax', 'help', 'html'),
454 link_to(l(:label_help), compute_public_path('wiki_syntax', 'help', 'html'),
450 :onclick => "window.open(\"#{ compute_public_path('wiki_syntax', 'help', 'html') }\", \"\", \"resizable=yes, location=no, width=300, height=640, menubar=no, status=no, scrollbars=yes\"); return false;")
455 :onclick => "window.open(\"#{ compute_public_path('wiki_syntax', 'help', 'html') }\", \"\", \"resizable=yes, location=no, width=300, height=640, menubar=no, status=no, scrollbars=yes\"); return false;")
451
456
452 javascript_include_tag('jstoolbar/jstoolbar') +
457 javascript_include_tag('jstoolbar/jstoolbar') +
453 javascript_include_tag("jstoolbar/lang/jstoolbar-#{current_language}") +
458 javascript_include_tag("jstoolbar/lang/jstoolbar-#{current_language}") +
454 javascript_tag("var toolbar = new jsToolBar($('#{field_id}')); toolbar.setHelpLink('#{help_link}'); toolbar.draw();")
459 javascript_tag("var toolbar = new jsToolBar($('#{field_id}')); toolbar.setHelpLink('#{help_link}'); toolbar.draw();")
455 end
460 end
456
461
457 def content_for(name, content = nil, &block)
462 def content_for(name, content = nil, &block)
458 @has_content ||= {}
463 @has_content ||= {}
459 @has_content[name] = true
464 @has_content[name] = true
460 super(name, content, &block)
465 super(name, content, &block)
461 end
466 end
462
467
463 def has_content?(name)
468 def has_content?(name)
464 (@has_content && @has_content[name]) || false
469 (@has_content && @has_content[name]) || false
465 end
470 end
466 end
471 end
@@ -1,87 +1,95
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'coderay'
18 require 'coderay'
19 require 'coderay/helpers/file_type'
19 require 'coderay/helpers/file_type'
20 require 'iconv'
20 require 'iconv'
21
21
22 module RepositoriesHelper
22 module RepositoriesHelper
23 def syntax_highlight(name, content)
23 def syntax_highlight(name, content)
24 type = CodeRay::FileType[name]
24 type = CodeRay::FileType[name]
25 type ? CodeRay.scan(content, type).html : h(content)
25 type ? CodeRay.scan(content, type).html : h(content)
26 end
26 end
27
27
28 def format_revision(txt)
29 txt.to_s[0,8]
30 end
31
28 def to_utf8(str)
32 def to_utf8(str)
29 return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii
33 return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii
30 @encodings ||= Setting.repositories_encodings.split(',').collect(&:strip)
34 @encodings ||= Setting.repositories_encodings.split(',').collect(&:strip)
31 @encodings.each do |encoding|
35 @encodings.each do |encoding|
32 begin
36 begin
33 return Iconv.conv('UTF-8', encoding, str)
37 return Iconv.conv('UTF-8', encoding, str)
34 rescue Iconv::Failure
38 rescue Iconv::Failure
35 # do nothing here and try the next encoding
39 # do nothing here and try the next encoding
36 end
40 end
37 end
41 end
38 str
42 str
39 end
43 end
40
44
41 def repository_field_tags(form, repository)
45 def repository_field_tags(form, repository)
42 method = repository.class.name.demodulize.underscore + "_field_tags"
46 method = repository.class.name.demodulize.underscore + "_field_tags"
43 send(method, form, repository) if repository.is_a?(Repository) && respond_to?(method)
47 send(method, form, repository) if repository.is_a?(Repository) && respond_to?(method)
44 end
48 end
45
49
46 def scm_select_tag(repository)
50 def scm_select_tag(repository)
47 container = [[]]
51 container = [[]]
48 REDMINE_SUPPORTED_SCM.each {|scm| container << ["Repository::#{scm}".constantize.scm_name, scm]}
52 REDMINE_SUPPORTED_SCM.each {|scm| container << ["Repository::#{scm}".constantize.scm_name, scm]}
49 select_tag('repository_scm',
53 select_tag('repository_scm',
50 options_for_select(container, repository.class.name.demodulize),
54 options_for_select(container, repository.class.name.demodulize),
51 :disabled => (repository && !repository.new_record?),
55 :disabled => (repository && !repository.new_record?),
52 :onchange => remote_function(:url => { :controller => 'repositories', :action => 'edit', :id => @project }, :method => :get, :with => "Form.serialize(this.form)")
56 :onchange => remote_function(:url => { :controller => 'repositories', :action => 'edit', :id => @project }, :method => :get, :with => "Form.serialize(this.form)")
53 )
57 )
54 end
58 end
55
59
56 def with_leading_slash(path)
60 def with_leading_slash(path)
57 path ||= ''
61 path ||= ''
58 path.starts_with?('/') ? path : "/#{path}"
62 path.starts_with?('/') ? path : "/#{path}"
59 end
63 end
60
64
61 def subversion_field_tags(form, repository)
65 def subversion_field_tags(form, repository)
62 content_tag('p', form.text_field(:url, :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)) +
66 content_tag('p', form.text_field(:url, :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)) +
63 '<br />(http://, https://, svn://, file:///)') +
67 '<br />(http://, https://, svn://, file:///)') +
64 content_tag('p', form.text_field(:login, :size => 30)) +
68 content_tag('p', form.text_field(:login, :size => 30)) +
65 content_tag('p', form.password_field(:password, :size => 30, :name => 'ignore',
69 content_tag('p', form.password_field(:password, :size => 30, :name => 'ignore',
66 :value => ((repository.new_record? || repository.password.blank?) ? '' : ('x'*15)),
70 :value => ((repository.new_record? || repository.password.blank?) ? '' : ('x'*15)),
67 :onfocus => "this.value=''; this.name='repository[password]';",
71 :onfocus => "this.value=''; this.name='repository[password]';",
68 :onchange => "this.name='repository[password]';"))
72 :onchange => "this.name='repository[password]';"))
69 end
73 end
70
74
71 def darcs_field_tags(form, repository)
75 def darcs_field_tags(form, repository)
72 content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.new_record?)))
76 content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.new_record?)))
73 end
77 end
74
78
75 def mercurial_field_tags(form, repository)
79 def mercurial_field_tags(form, repository)
76 content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)))
80 content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)))
77 end
81 end
78
82
83 def git_field_tags(form, repository)
84 content_tag('p', form.text_field(:url, :label => 'Path to .git directory', :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)))
85 end
86
79 def cvs_field_tags(form, repository)
87 def cvs_field_tags(form, repository)
80 content_tag('p', form.text_field(:root_url, :label => 'CVSROOT', :size => 60, :required => true, :disabled => !repository.new_record?)) +
88 content_tag('p', form.text_field(:root_url, :label => 'CVSROOT', :size => 60, :required => true, :disabled => !repository.new_record?)) +
81 content_tag('p', form.text_field(:url, :label => 'Module', :size => 30, :required => true, :disabled => !repository.new_record?))
89 content_tag('p', form.text_field(:url, :label => 'Module', :size => 30, :required => true, :disabled => !repository.new_record?))
82 end
90 end
83
91
84 def bazaar_field_tags(form, repository)
92 def bazaar_field_tags(form, repository)
85 content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.new_record?)))
93 content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.new_record?)))
86 end
94 end
87 end
95 end
@@ -1,124 +1,127
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Changeset < ActiveRecord::Base
18 class Changeset < ActiveRecord::Base
19 belongs_to :repository
19 belongs_to :repository
20 has_many :changes, :dependent => :delete_all
20 has_many :changes, :dependent => :delete_all
21 has_and_belongs_to_many :issues
21 has_and_belongs_to_many :issues
22
22
23 acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.revision}" + (o.comments.blank? ? '' : (': ' + o.comments))},
23 acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.revision}" + (o.comments.blank? ? '' : (': ' + o.comments))},
24 :description => :comments,
24 :description => :comments,
25 :datetime => :committed_on,
25 :datetime => :committed_on,
26 :author => :committer,
26 :author => :committer,
27 :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project_id, :rev => o.revision}}
27 :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project_id, :rev => o.revision}}
28
28
29 acts_as_searchable :columns => 'comments',
29 acts_as_searchable :columns => 'comments',
30 :include => :repository,
30 :include => :repository,
31 :project_key => "#{Repository.table_name}.project_id",
31 :project_key => "#{Repository.table_name}.project_id",
32 :date_column => 'committed_on'
32 :date_column => 'committed_on'
33
33
34 validates_presence_of :repository_id, :revision, :committed_on, :commit_date
34 validates_presence_of :repository_id, :revision, :committed_on, :commit_date
35 validates_numericality_of :revision, :only_integer => true
36 validates_uniqueness_of :revision, :scope => :repository_id
35 validates_uniqueness_of :revision, :scope => :repository_id
37 validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true
36 validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true
38
37
39 def comments=(comment)
38 def comments=(comment)
40 write_attribute(:comments, comment.strip)
39 write_attribute(:comments, comment.strip)
41 end
40 end
42
41
43 def committed_on=(date)
42 def committed_on=(date)
44 self.commit_date = date
43 self.commit_date = date
45 super
44 super
46 end
45 end
47
46
48 def project
47 def project
49 repository.project
48 repository.project
50 end
49 end
51
50
52 def after_create
51 def after_create
53 scan_comment_for_issue_ids
52 scan_comment_for_issue_ids
54 end
53 end
55 require 'pp'
54 require 'pp'
56
55
57 def scan_comment_for_issue_ids
56 def scan_comment_for_issue_ids
58 return if comments.blank?
57 return if comments.blank?
59 # keywords used to reference issues
58 # keywords used to reference issues
60 ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
59 ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
61 # keywords used to fix issues
60 # keywords used to fix issues
62 fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip)
61 fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip)
63 # status and optional done ratio applied
62 # status and optional done ratio applied
64 fix_status = IssueStatus.find_by_id(Setting.commit_fix_status_id)
63 fix_status = IssueStatus.find_by_id(Setting.commit_fix_status_id)
65 done_ratio = Setting.commit_fix_done_ratio.blank? ? nil : Setting.commit_fix_done_ratio.to_i
64 done_ratio = Setting.commit_fix_done_ratio.blank? ? nil : Setting.commit_fix_done_ratio.to_i
66
65
67 kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
66 kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
68 return if kw_regexp.blank?
67 return if kw_regexp.blank?
69
68
70 referenced_issues = []
69 referenced_issues = []
71
70
72 if ref_keywords.delete('*')
71 if ref_keywords.delete('*')
73 # find any issue ID in the comments
72 # find any issue ID in the comments
74 target_issue_ids = []
73 target_issue_ids = []
75 comments.scan(%r{([\s\(,-^])#(\d+)(?=[[:punct:]]|\s|<|$)}).each { |m| target_issue_ids << m[1] }
74 comments.scan(%r{([\s\(,-^])#(\d+)(?=[[:punct:]]|\s|<|$)}).each { |m| target_issue_ids << m[1] }
76 referenced_issues += repository.project.issues.find_all_by_id(target_issue_ids)
75 referenced_issues += repository.project.issues.find_all_by_id(target_issue_ids)
77 end
76 end
78
77
79 comments.scan(Regexp.new("(#{kw_regexp})[\s:]+(([\s,;&]*#?\\d+)+)", Regexp::IGNORECASE)).each do |match|
78 comments.scan(Regexp.new("(#{kw_regexp})[\s:]+(([\s,;&]*#?\\d+)+)", Regexp::IGNORECASE)).each do |match|
80 action = match[0]
79 action = match[0]
81 target_issue_ids = match[1].scan(/\d+/)
80 target_issue_ids = match[1].scan(/\d+/)
82 target_issues = repository.project.issues.find_all_by_id(target_issue_ids)
81 target_issues = repository.project.issues.find_all_by_id(target_issue_ids)
83 if fix_status && fix_keywords.include?(action.downcase)
82 if fix_status && fix_keywords.include?(action.downcase)
84 # update status of issues
83 # update status of issues
85 logger.debug "Issues fixed by changeset #{self.revision}: #{issue_ids.join(', ')}." if logger && logger.debug?
84 logger.debug "Issues fixed by changeset #{self.revision}: #{issue_ids.join(', ')}." if logger && logger.debug?
86 target_issues.each do |issue|
85 target_issues.each do |issue|
87 # the issue may have been updated by the closure of another one (eg. duplicate)
86 # the issue may have been updated by the closure of another one (eg. duplicate)
88 issue.reload
87 issue.reload
89 # don't change the status is the issue is closed
88 # don't change the status is the issue is closed
90 next if issue.status.is_closed?
89 next if issue.status.is_closed?
91 user = committer_user || User.anonymous
90 user = committer_user || User.anonymous
92 journal = issue.init_journal(user, l(:text_status_changed_by_changeset, "r#{self.revision}"))
91 csettext = "r#{self.revision}"
92 if self.scmid && (! (csettext =~ /^r[0-9]+$/))
93 csettext = "commit:\"#{self.scmid}\""
94 end
95 journal = issue.init_journal(user, l(:text_status_changed_by_changeset, csettext))
93 issue.status = fix_status
96 issue.status = fix_status
94 issue.done_ratio = done_ratio if done_ratio
97 issue.done_ratio = done_ratio if done_ratio
95 issue.save
98 issue.save
96 Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
99 Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
97 end
100 end
98 end
101 end
99 referenced_issues += target_issues
102 referenced_issues += target_issues
100 end
103 end
101
104
102 self.issues = referenced_issues.uniq
105 self.issues = referenced_issues.uniq
103 end
106 end
104
107
105 # Returns the Redmine User corresponding to the committer
108 # Returns the Redmine User corresponding to the committer
106 def committer_user
109 def committer_user
107 if committer && committer.strip =~ /^([^<]+)(<(.*)>)?$/
110 if committer && committer.strip =~ /^([^<]+)(<(.*)>)?$/
108 username, email = $1.strip, $3
111 username, email = $1.strip, $3
109 u = User.find_by_login(username)
112 u = User.find_by_login(username)
110 u ||= User.find_by_mail(email) unless email.blank?
113 u ||= User.find_by_mail(email) unless email.blank?
111 u
114 u
112 end
115 end
113 end
116 end
114
117
115 # Returns the previous changeset
118 # Returns the previous changeset
116 def previous
119 def previous
117 @previous ||= Changeset.find(:first, :conditions => ['revision < ? AND repository_id = ?', self.revision, self.repository_id], :order => 'revision DESC')
120 @previous ||= Changeset.find(:first, :conditions => ['id < ? AND repository_id = ?', self.id, self.repository_id], :order => 'id DESC')
118 end
121 end
119
122
120 # Returns the next changeset
123 # Returns the next changeset
121 def next
124 def next
122 @next ||= Changeset.find(:first, :conditions => ['revision > ? AND repository_id = ?', self.revision, self.repository_id], :order => 'revision ASC')
125 @next ||= Changeset.find(:first, :conditions => ['id > ? AND repository_id = ?', self.id, self.repository_id], :order => 'id ASC')
123 end
126 end
124 end
127 end
@@ -1,91 +1,91
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Repository < ActiveRecord::Base
18 class Repository < ActiveRecord::Base
19 belongs_to :project
19 belongs_to :project
20 has_many :changesets, :dependent => :destroy, :order => "#{Changeset.table_name}.revision DESC"
20 has_many :changesets, :dependent => :destroy, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC"
21 has_many :changes, :through => :changesets
21 has_many :changes, :through => :changesets
22
22
23 def scm
23 def scm
24 @scm ||= self.scm_adapter.new url, root_url, login, password
24 @scm ||= self.scm_adapter.new url, root_url, login, password
25 update_attribute(:root_url, @scm.root_url) if root_url.blank?
25 update_attribute(:root_url, @scm.root_url) if root_url.blank?
26 @scm
26 @scm
27 end
27 end
28
28
29 def scm_name
29 def scm_name
30 self.class.scm_name
30 self.class.scm_name
31 end
31 end
32
32
33 def supports_cat?
33 def supports_cat?
34 scm.supports_cat?
34 scm.supports_cat?
35 end
35 end
36
36
37 def supports_annotate?
37 def supports_annotate?
38 scm.supports_annotate?
38 scm.supports_annotate?
39 end
39 end
40
40
41 def entries(path=nil, identifier=nil)
41 def entries(path=nil, identifier=nil)
42 scm.entries(path, identifier)
42 scm.entries(path, identifier)
43 end
43 end
44
44
45 def diff(path, rev, rev_to, type)
45 def diff(path, rev, rev_to, type)
46 scm.diff(path, rev, rev_to, type)
46 scm.diff(path, rev, rev_to, type)
47 end
47 end
48
48
49 # Default behaviour: we search in cached changesets
49 # Default behaviour: we search in cached changesets
50 def changesets_for_path(path)
50 def changesets_for_path(path)
51 path = "/#{path}" unless path.starts_with?('/')
51 path = "/#{path}" unless path.starts_with?('/')
52 Change.find(:all, :include => :changeset,
52 Change.find(:all, :include => :changeset,
53 :conditions => ["repository_id = ? AND path = ?", id, path],
53 :conditions => ["repository_id = ? AND path = ?", id, path],
54 :order => "committed_on DESC, #{Changeset.table_name}.revision DESC").collect(&:changeset)
54 :order => "committed_on DESC, #{Changeset.table_name}.id DESC").collect(&:changeset)
55 end
55 end
56
56
57 def latest_changeset
57 def latest_changeset
58 @latest_changeset ||= changesets.find(:first)
58 @latest_changeset ||= changesets.find(:first)
59 end
59 end
60
60
61 def scan_changesets_for_issue_ids
61 def scan_changesets_for_issue_ids
62 self.changesets.each(&:scan_comment_for_issue_ids)
62 self.changesets.each(&:scan_comment_for_issue_ids)
63 end
63 end
64
64
65 # fetch new changesets for all repositories
65 # fetch new changesets for all repositories
66 # can be called periodically by an external script
66 # can be called periodically by an external script
67 # eg. ruby script/runner "Repository.fetch_changesets"
67 # eg. ruby script/runner "Repository.fetch_changesets"
68 def self.fetch_changesets
68 def self.fetch_changesets
69 find(:all).each(&:fetch_changesets)
69 find(:all).each(&:fetch_changesets)
70 end
70 end
71
71
72 # scan changeset comments to find related and fixed issues for all repositories
72 # scan changeset comments to find related and fixed issues for all repositories
73 def self.scan_changesets_for_issue_ids
73 def self.scan_changesets_for_issue_ids
74 find(:all).each(&:scan_changesets_for_issue_ids)
74 find(:all).each(&:scan_changesets_for_issue_ids)
75 end
75 end
76
76
77 def self.scm_name
77 def self.scm_name
78 'Abstract'
78 'Abstract'
79 end
79 end
80
80
81 def self.available_scm
81 def self.available_scm
82 subclasses.collect {|klass| [klass.scm_name, klass.name]}
82 subclasses.collect {|klass| [klass.scm_name, klass.name]}
83 end
83 end
84
84
85 def self.factory(klass_name, *args)
85 def self.factory(klass_name, *args)
86 klass = "Repository::#{klass_name}".constantize
86 klass = "Repository::#{klass_name}".constantize
87 klass.new(*args)
87 klass.new(*args)
88 rescue
88 rescue
89 nil
89 nil
90 end
90 end
91 end
91 end
@@ -1,86 +1,86
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'redmine/scm/adapters/bazaar_adapter'
18 require 'redmine/scm/adapters/bazaar_adapter'
19
19
20 class Repository::Bazaar < Repository
20 class Repository::Bazaar < Repository
21 attr_protected :root_url
21 attr_protected :root_url
22 validates_presence_of :url
22 validates_presence_of :url
23
23
24 def scm_adapter
24 def scm_adapter
25 Redmine::Scm::Adapters::BazaarAdapter
25 Redmine::Scm::Adapters::BazaarAdapter
26 end
26 end
27
27
28 def self.scm_name
28 def self.scm_name
29 'Bazaar'
29 'Bazaar'
30 end
30 end
31
31
32 def entries(path=nil, identifier=nil)
32 def entries(path=nil, identifier=nil)
33 entries = scm.entries(path, identifier)
33 entries = scm.entries(path, identifier)
34 if entries
34 if entries
35 entries.each do |e|
35 entries.each do |e|
36 next if e.lastrev.revision.blank?
36 next if e.lastrev.revision.blank?
37 c = Change.find(:first,
37 c = Change.find(:first,
38 :include => :changeset,
38 :include => :changeset,
39 :conditions => ["#{Change.table_name}.revision = ? and #{Changeset.table_name}.repository_id = ?", e.lastrev.revision, id],
39 :conditions => ["#{Change.table_name}.revision = ? and #{Changeset.table_name}.repository_id = ?", e.lastrev.revision, id],
40 :order => "#{Changeset.table_name}.revision DESC")
40 :order => "#{Changeset.table_name}.revision DESC")
41 if c
41 if c
42 e.lastrev.identifier = c.changeset.revision
42 e.lastrev.identifier = c.changeset.revision
43 e.lastrev.name = c.changeset.revision
43 e.lastrev.name = c.changeset.revision
44 e.lastrev.author = c.changeset.committer
44 e.lastrev.author = c.changeset.committer
45 end
45 end
46 end
46 end
47 end
47 end
48 end
48 end
49
49
50 def fetch_changesets
50 def fetch_changesets
51 scm_info = scm.info
51 scm_info = scm.info
52 if scm_info
52 if scm_info
53 # latest revision found in database
53 # latest revision found in database
54 db_revision = latest_changeset ? latest_changeset.revision : 0
54 db_revision = latest_changeset ? latest_changeset.revision.to_i : 0
55 # latest revision in the repository
55 # latest revision in the repository
56 scm_revision = scm_info.lastrev.identifier.to_i
56 scm_revision = scm_info.lastrev.identifier.to_i
57 if db_revision < scm_revision
57 if db_revision < scm_revision
58 logger.debug "Fetching changesets for repository #{url}" if logger && logger.debug?
58 logger.debug "Fetching changesets for repository #{url}" if logger && logger.debug?
59 identifier_from = db_revision + 1
59 identifier_from = db_revision + 1
60 while (identifier_from <= scm_revision)
60 while (identifier_from <= scm_revision)
61 # loads changesets by batches of 200
61 # loads changesets by batches of 200
62 identifier_to = [identifier_from + 199, scm_revision].min
62 identifier_to = [identifier_from + 199, scm_revision].min
63 revisions = scm.revisions('', identifier_to, identifier_from, :with_paths => true)
63 revisions = scm.revisions('', identifier_to, identifier_from, :with_paths => true)
64 transaction do
64 transaction do
65 revisions.reverse_each do |revision|
65 revisions.reverse_each do |revision|
66 changeset = Changeset.create(:repository => self,
66 changeset = Changeset.create(:repository => self,
67 :revision => revision.identifier,
67 :revision => revision.identifier,
68 :committer => revision.author,
68 :committer => revision.author,
69 :committed_on => revision.time,
69 :committed_on => revision.time,
70 :scmid => revision.scmid,
70 :scmid => revision.scmid,
71 :comments => revision.message)
71 :comments => revision.message)
72
72
73 revision.paths.each do |change|
73 revision.paths.each do |change|
74 Change.create(:changeset => changeset,
74 Change.create(:changeset => changeset,
75 :action => change[:action],
75 :action => change[:action],
76 :path => change[:path],
76 :path => change[:path],
77 :revision => change[:revision])
77 :revision => change[:revision])
78 end
78 end
79 end
79 end
80 end unless revisions.nil?
80 end unless revisions.nil?
81 identifier_from = identifier_to + 1
81 identifier_from = identifier_to + 1
82 end
82 end
83 end
83 end
84 end
84 end
85 end
85 end
86 end
86 end
@@ -1,150 +1,148
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'redmine/scm/adapters/cvs_adapter'
18 require 'redmine/scm/adapters/cvs_adapter'
19 require 'digest/sha1'
19 require 'digest/sha1'
20
20
21 class Repository::Cvs < Repository
21 class Repository::Cvs < Repository
22 validates_presence_of :url, :root_url
22 validates_presence_of :url, :root_url
23
23
24 def scm_adapter
24 def scm_adapter
25 Redmine::Scm::Adapters::CvsAdapter
25 Redmine::Scm::Adapters::CvsAdapter
26 end
26 end
27
27
28 def self.scm_name
28 def self.scm_name
29 'CVS'
29 'CVS'
30 end
30 end
31
31
32 def entry(path, identifier)
32 def entry(path, identifier)
33 e = entries(path, identifier)
33 e = entries(path, identifier)
34 e ? e.first : nil
34 e ? e.first : nil
35 end
35 end
36
36
37 def entries(path=nil, identifier=nil)
37 def entries(path=nil, identifier=nil)
38 entries=scm.entries(path, identifier)
38 entries=scm.entries(path, identifier)
39 if entries
39 if entries
40 entries.each() do |entry|
40 entries.each() do |entry|
41 unless entry.lastrev.nil? || entry.lastrev.identifier
41 unless entry.lastrev.nil? || entry.lastrev.identifier
42 change=changes.find_by_revision_and_path( entry.lastrev.revision, scm.with_leading_slash(entry.path) )
42 change=changes.find_by_revision_and_path( entry.lastrev.revision, scm.with_leading_slash(entry.path) )
43 if change
43 if change
44 entry.lastrev.identifier=change.changeset.revision
44 entry.lastrev.identifier=change.changeset.revision
45 entry.lastrev.author=change.changeset.committer
45 entry.lastrev.author=change.changeset.committer
46 entry.lastrev.revision=change.revision
46 entry.lastrev.revision=change.revision
47 entry.lastrev.branch=change.branch
47 entry.lastrev.branch=change.branch
48 end
48 end
49 end
49 end
50 end
50 end
51 end
51 end
52 entries
52 entries
53 end
53 end
54
54
55 def diff(path, rev, rev_to, type)
55 def diff(path, rev, rev_to, type)
56 #convert rev to revision. CVS can't handle changesets here
56 #convert rev to revision. CVS can't handle changesets here
57 diff=[]
57 diff=[]
58 changeset_from=changesets.find_by_revision(rev)
58 changeset_from=changesets.find_by_revision(rev)
59 if rev_to.to_i > 0
59 if rev_to.to_i > 0
60 changeset_to=changesets.find_by_revision(rev_to)
60 changeset_to=changesets.find_by_revision(rev_to)
61 end
61 end
62 changeset_from.changes.each() do |change_from|
62 changeset_from.changes.each() do |change_from|
63
63
64 revision_from=nil
64 revision_from=nil
65 revision_to=nil
65 revision_to=nil
66
66
67 revision_from=change_from.revision if path.nil? || (change_from.path.starts_with? scm.with_leading_slash(path))
67 revision_from=change_from.revision if path.nil? || (change_from.path.starts_with? scm.with_leading_slash(path))
68
68
69 if revision_from
69 if revision_from
70 if changeset_to
70 if changeset_to
71 changeset_to.changes.each() do |change_to|
71 changeset_to.changes.each() do |change_to|
72 revision_to=change_to.revision if change_to.path==change_from.path
72 revision_to=change_to.revision if change_to.path==change_from.path
73 end
73 end
74 end
74 end
75 unless revision_to
75 unless revision_to
76 revision_to=scm.get_previous_revision(revision_from)
76 revision_to=scm.get_previous_revision(revision_from)
77 end
77 end
78 diff=diff+scm.diff(change_from.path, revision_from, revision_to, type)
78 diff=diff+scm.diff(change_from.path, revision_from, revision_to, type)
79 end
79 end
80 end
80 end
81 return diff
81 return diff
82 end
82 end
83
83
84 def fetch_changesets
84 def fetch_changesets
85 #not the preferred way with CVS. maybe we should introduce always a cron-job for this
86 last_commit = changesets.maximum(:committed_on)
87
88 # some nifty bits to introduce a commit-id with cvs
85 # some nifty bits to introduce a commit-id with cvs
89 # natively cvs doesn't provide any kind of changesets, there is only a revision per file.
86 # natively cvs doesn't provide any kind of changesets, there is only a revision per file.
90 # we now take a guess using the author, the commitlog and the commit-date.
87 # we now take a guess using the author, the commitlog and the commit-date.
91
88
92 # last one is the next step to take. the commit-date is not equal for all
89 # last one is the next step to take. the commit-date is not equal for all
93 # commits in one changeset. cvs update the commit-date when the *,v file was touched. so
90 # commits in one changeset. cvs update the commit-date when the *,v file was touched. so
94 # we use a small delta here, to merge all changes belonging to _one_ changeset
91 # we use a small delta here, to merge all changes belonging to _one_ changeset
95 time_delta=10.seconds
92 time_delta=10.seconds
96
93
94 fetch_since = latest_changeset ? latest_changeset.committed_on : nil
97 transaction do
95 transaction do
98 scm.revisions('', last_commit, nil, :with_paths => true) do |revision|
96 tmp_rev_num = 1
97 scm.revisions('', fetch_since, nil, :with_paths => true) do |revision|
99 # only add the change to the database, if it doen't exists. the cvs log
98 # only add the change to the database, if it doen't exists. the cvs log
100 # is not exclusive at all.
99 # is not exclusive at all.
101 unless changes.find_by_path_and_revision(scm.with_leading_slash(revision.paths[0][:path]), revision.paths[0][:revision])
100 unless changes.find_by_path_and_revision(scm.with_leading_slash(revision.paths[0][:path]), revision.paths[0][:revision])
102 revision
101 revision
103 cs = changesets.find(:first, :conditions=>{
102 cs = changesets.find(:first, :conditions=>{
104 :committed_on=>revision.time-time_delta..revision.time+time_delta,
103 :committed_on=>revision.time-time_delta..revision.time+time_delta,
105 :committer=>revision.author,
104 :committer=>revision.author,
106 :comments=>revision.message
105 :comments=>revision.message
107 })
106 })
108
107
109 # create a new changeset....
108 # create a new changeset....
110 unless cs
109 unless cs
111 # we use a negative changeset-number here (just for inserting)
110 # we use a temporaray revision number here (just for inserting)
112 # later on, we calculate a continous positive number
111 # later on, we calculate a continous positive number
113 next_rev = changesets.minimum(:revision)
112 latest = changesets.find(:first, :order => 'id DESC')
114 next_rev = 0 if next_rev.nil? or next_rev > 0
115 next_rev = next_rev - 1
116
117 cs=Changeset.create(:repository => self,
113 cs = Changeset.create(:repository => self,
118 :revision => next_rev,
114 :revision => "_#{tmp_rev_num}",
119 :committer => revision.author,
115 :committer => revision.author,
120 :committed_on => revision.time,
116 :committed_on => revision.time,
121 :comments => revision.message)
117 :comments => revision.message)
118 tmp_rev_num += 1
122 end
119 end
123
120
124 #convert CVS-File-States to internal Action-abbrevations
121 #convert CVS-File-States to internal Action-abbrevations
125 #default action is (M)odified
122 #default action is (M)odified
126 action="M"
123 action="M"
127 if revision.paths[0][:action]=="Exp" && revision.paths[0][:revision]=="1.1"
124 if revision.paths[0][:action]=="Exp" && revision.paths[0][:revision]=="1.1"
128 action="A" #add-action always at first revision (= 1.1)
125 action="A" #add-action always at first revision (= 1.1)
129 elsif revision.paths[0][:action]=="dead"
126 elsif revision.paths[0][:action]=="dead"
130 action="D" #dead-state is similar to Delete
127 action="D" #dead-state is similar to Delete
131 end
128 end
132
129
133 Change.create(:changeset => cs,
130 Change.create(:changeset => cs,
134 :action => action,
131 :action => action,
135 :path => scm.with_leading_slash(revision.paths[0][:path]),
132 :path => scm.with_leading_slash(revision.paths[0][:path]),
136 :revision => revision.paths[0][:revision],
133 :revision => revision.paths[0][:revision],
137 :branch => revision.paths[0][:branch]
134 :branch => revision.paths[0][:branch]
138 )
135 )
139 end
136 end
140 end
137 end
141
138
142 next_rev = [changesets.maximum(:revision) || 0, 0].max
139 # Renumber new changesets in chronological order
143 changesets.find(:all, :conditions=>["revision < 0"], :order=>"committed_on ASC").each() do |changeset|
140 c = changesets.find(:first, :order => 'committed_on DESC, id DESC', :conditions => "revision NOT LIKE '_%'")
144 next_rev = next_rev + 1
141 next_rev = c.nil? ? 1 : (c.revision.to_i + 1)
145 changeset.revision = next_rev
142 changesets.find(:all, :order => 'committed_on ASC, id ASC', :conditions => "revision LIKE '_%'").each do |changeset|
146 changeset.save!
143 changeset.update_attribute :revision, next_rev
147 end
144 next_rev += 1
148 end
145 end
146 end # transaction
149 end
147 end
150 end
148 end
@@ -1,90 +1,89
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'redmine/scm/adapters/darcs_adapter'
18 require 'redmine/scm/adapters/darcs_adapter'
19
19
20 class Repository::Darcs < Repository
20 class Repository::Darcs < Repository
21 validates_presence_of :url
21 validates_presence_of :url
22
22
23 def scm_adapter
23 def scm_adapter
24 Redmine::Scm::Adapters::DarcsAdapter
24 Redmine::Scm::Adapters::DarcsAdapter
25 end
25 end
26
26
27 def self.scm_name
27 def self.scm_name
28 'Darcs'
28 'Darcs'
29 end
29 end
30
30
31 def entries(path=nil, identifier=nil)
31 def entries(path=nil, identifier=nil)
32 entries=scm.entries(path, identifier)
32 entries=scm.entries(path, identifier)
33 if entries
33 if entries
34 entries.each do |entry|
34 entries.each do |entry|
35 # Search the DB for the entry's last change
35 # Search the DB for the entry's last change
36 changeset = changesets.find_by_scmid(entry.lastrev.scmid) if entry.lastrev && !entry.lastrev.scmid.blank?
36 changeset = changesets.find_by_scmid(entry.lastrev.scmid) if entry.lastrev && !entry.lastrev.scmid.blank?
37 if changeset
37 if changeset
38 entry.lastrev.identifier = changeset.revision
38 entry.lastrev.identifier = changeset.revision
39 entry.lastrev.name = changeset.revision
39 entry.lastrev.name = changeset.revision
40 entry.lastrev.time = changeset.committed_on
40 entry.lastrev.time = changeset.committed_on
41 entry.lastrev.author = changeset.committer
41 entry.lastrev.author = changeset.committer
42 end
42 end
43 end
43 end
44 end
44 end
45 entries
45 entries
46 end
46 end
47
47
48 def diff(path, rev, rev_to, type)
48 def diff(path, rev, rev_to, type)
49 patch_from = changesets.find_by_revision(rev)
49 patch_from = changesets.find_by_revision(rev)
50 return nil if patch_from.nil?
50 patch_to = changesets.find_by_revision(rev_to) if rev_to
51 patch_to = changesets.find_by_revision(rev_to) if rev_to
51 if path.blank?
52 if path.blank?
52 path = patch_from.changes.collect{|change| change.path}.join(' ')
53 path = patch_from.changes.collect{|change| change.path}.join(' ')
53 end
54 end
54 scm.diff(path, patch_from.scmid, patch_to.scmid, type)
55 patch_from ? scm.diff(path, patch_from.scmid, patch_to ? patch_to.scmid : nil, type) : nil
55 end
56 end
56
57
57 def fetch_changesets
58 def fetch_changesets
58 scm_info = scm.info
59 scm_info = scm.info
59 if scm_info
60 if scm_info
60 db_last_id = latest_changeset ? latest_changeset.scmid : nil
61 db_last_id = latest_changeset ? latest_changeset.scmid : nil
61 next_rev = latest_changeset ? latest_changeset.revision + 1 : 1
62 next_rev = latest_changeset ? latest_changeset.revision.to_i + 1 : 1
62 # latest revision in the repository
63 # latest revision in the repository
63 scm_revision = scm_info.lastrev.scmid
64 scm_revision = scm_info.lastrev.scmid
64 unless changesets.find_by_scmid(scm_revision)
65 unless changesets.find_by_scmid(scm_revision)
65 revisions = scm.revisions('', db_last_id, nil, :with_path => true)
66 revisions = scm.revisions('', db_last_id, nil, :with_path => true)
66 transaction do
67 transaction do
67 revisions.reverse_each do |revision|
68 revisions.reverse_each do |revision|
68 changeset = Changeset.create(:repository => self,
69 changeset = Changeset.create(:repository => self,
69 :revision => next_rev,
70 :revision => next_rev,
70 :scmid => revision.scmid,
71 :scmid => revision.scmid,
71 :committer => revision.author,
72 :committer => revision.author,
72 :committed_on => revision.time,
73 :committed_on => revision.time,
73 :comments => revision.message)
74 :comments => revision.message)
74
75
75 next if changeset.new_record?
76
77 revision.paths.each do |change|
76 revision.paths.each do |change|
78 Change.create(:changeset => changeset,
77 Change.create(:changeset => changeset,
79 :action => change[:action],
78 :action => change[:action],
80 :path => change[:path],
79 :path => change[:path],
81 :from_path => change[:from_path],
80 :from_path => change[:from_path],
82 :from_revision => change[:from_revision])
81 :from_revision => change[:from_revision])
83 end
82 end
84 next_rev += 1
83 next_rev += 1
85 end if revisions
84 end if revisions
86 end
85 end
87 end
86 end
88 end
87 end
89 end
88 end
90 end
89 end
@@ -1,74 +1,74
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'redmine/scm/adapters/subversion_adapter'
18 require 'redmine/scm/adapters/subversion_adapter'
19
19
20 class Repository::Subversion < Repository
20 class Repository::Subversion < Repository
21 attr_protected :root_url
21 attr_protected :root_url
22 validates_presence_of :url
22 validates_presence_of :url
23 validates_format_of :url, :with => /^(http|https|svn|svn\+ssh|file):\/\/.+/i
23 validates_format_of :url, :with => /^(http|https|svn|svn\+ssh|file):\/\/.+/i
24
24
25 def scm_adapter
25 def scm_adapter
26 Redmine::Scm::Adapters::SubversionAdapter
26 Redmine::Scm::Adapters::SubversionAdapter
27 end
27 end
28
28
29 def self.scm_name
29 def self.scm_name
30 'Subversion'
30 'Subversion'
31 end
31 end
32
32
33 def changesets_for_path(path)
33 def changesets_for_path(path)
34 revisions = scm.revisions(path)
34 revisions = scm.revisions(path)
35 revisions ? changesets.find_all_by_revision(revisions.collect(&:identifier), :order => "committed_on DESC") : []
35 revisions ? changesets.find_all_by_revision(revisions.collect(&:identifier), :order => "committed_on DESC") : []
36 end
36 end
37
37
38 def fetch_changesets
38 def fetch_changesets
39 scm_info = scm.info
39 scm_info = scm.info
40 if scm_info
40 if scm_info
41 # latest revision found in database
41 # latest revision found in database
42 db_revision = latest_changeset ? latest_changeset.revision : 0
42 db_revision = latest_changeset ? latest_changeset.revision.to_i : 0
43 # latest revision in the repository
43 # latest revision in the repository
44 scm_revision = scm_info.lastrev.identifier.to_i
44 scm_revision = scm_info.lastrev.identifier.to_i
45 if db_revision < scm_revision
45 if db_revision < scm_revision
46 logger.debug "Fetching changesets for repository #{url}" if logger && logger.debug?
46 logger.debug "Fetching changesets for repository #{url}" if logger && logger.debug?
47 identifier_from = db_revision + 1
47 identifier_from = db_revision + 1
48 while (identifier_from <= scm_revision)
48 while (identifier_from <= scm_revision)
49 # loads changesets by batches of 200
49 # loads changesets by batches of 200
50 identifier_to = [identifier_from + 199, scm_revision].min
50 identifier_to = [identifier_from + 199, scm_revision].min
51 revisions = scm.revisions('', identifier_to, identifier_from, :with_paths => true)
51 revisions = scm.revisions('', identifier_to, identifier_from, :with_paths => true)
52 transaction do
52 transaction do
53 revisions.reverse_each do |revision|
53 revisions.reverse_each do |revision|
54 changeset = Changeset.create(:repository => self,
54 changeset = Changeset.create(:repository => self,
55 :revision => revision.identifier,
55 :revision => revision.identifier,
56 :committer => revision.author,
56 :committer => revision.author,
57 :committed_on => revision.time,
57 :committed_on => revision.time,
58 :comments => revision.message)
58 :comments => revision.message)
59
59
60 revision.paths.each do |change|
60 revision.paths.each do |change|
61 Change.create(:changeset => changeset,
61 Change.create(:changeset => changeset,
62 :action => change[:action],
62 :action => change[:action],
63 :path => change[:path],
63 :path => change[:path],
64 :from_path => change[:from_path],
64 :from_path => change[:from_path],
65 :from_revision => change[:from_revision])
65 :from_revision => change[:from_revision])
66 end
66 end
67 end
67 end
68 end unless revisions.nil?
68 end unless revisions.nil?
69 identifier_from = identifier_to + 1
69 identifier_from = identifier_to + 1
70 end
70 end
71 end
71 end
72 end
72 end
73 end
73 end
74 end
74 end
@@ -1,32 +1,32
1 <% @entries.each do |entry| %>
1 <% @entries.each do |entry| %>
2 <% tr_id = Digest::MD5.hexdigest(entry.path)
2 <% tr_id = Digest::MD5.hexdigest(entry.path)
3 depth = params[:depth].to_i %>
3 depth = params[:depth].to_i %>
4 <tr id="<%= tr_id %>" class="<%= params[:parent_id] %> entry">
4 <tr id="<%= tr_id %>" class="<%= params[:parent_id] %> entry">
5 <td class="filename">
5 <td class="filename">
6 <%= if entry.is_dir?
6 <%= if entry.is_dir?
7 link_to_remote h(entry.name),
7 link_to_remote h(entry.name),
8 {:url => {:action => 'browse', :id => @project, :path => entry.path, :rev => @rev, :depth => (depth + 1), :parent_id => tr_id},
8 {:url => {:action => 'browse', :id => @project, :path => entry.path, :rev => @rev, :depth => (depth + 1), :parent_id => tr_id},
9 :update => { :success => tr_id },
9 :update => { :success => tr_id },
10 :position => :after,
10 :position => :after,
11 :success => "scmEntryLoaded('#{tr_id}')",
11 :success => "scmEntryLoaded('#{tr_id}')",
12 :condition => "scmEntryClick('#{tr_id}')"
12 :condition => "scmEntryClick('#{tr_id}')"
13 },
13 },
14 {:href => url_for({:action => 'browse', :id => @project, :path => entry.path, :rev => @rev}),
14 {:href => url_for({:action => 'browse', :id => @project, :path => entry.path, :rev => @rev}),
15 :class => ('icon icon-folder'),
15 :class => ('icon icon-folder'),
16 :style => "margin-left: #{18 * depth}px;"
16 :style => "margin-left: #{18 * depth}px;"
17 }
17 }
18 else
18 else
19 link_to h(entry.name),
19 link_to h(entry.name),
20 {:action => (entry.is_dir? ? 'browse' : 'changes'), :id => @project, :path => entry.path, :rev => @rev},
20 {:action => (entry.is_dir? ? 'browse' : 'changes'), :id => @project, :path => entry.path, :rev => @rev},
21 :class => 'icon icon-file',
21 :class => 'icon icon-file',
22 :style => "margin-left: #{18 * depth}px;"
22 :style => "margin-left: #{18 * depth}px;"
23 end %>
23 end %>
24 </td>
24 </td>
25 <td class="size"><%= (entry.size ? number_to_human_size(entry.size) : "?") unless entry.is_dir? %></td>
25 <td class="size"><%= (entry.size ? number_to_human_size(entry.size) : "?") unless entry.is_dir? %></td>
26 <td class="revision"><%= link_to(entry.lastrev.name, :action => 'revision', :id => @project, :rev => entry.lastrev.identifier) if entry.lastrev && entry.lastrev.identifier %></td>
26 <td class="revision"><%= link_to(format_revision(entry.lastrev.name), :action => 'revision', :id => @project, :rev => entry.lastrev.identifier) if entry.lastrev && entry.lastrev.identifier %></td>
27 <td class="age"><%= distance_of_time_in_words(entry.lastrev.time, Time.now) if entry.lastrev && entry.lastrev.time %></td>
27 <td class="age"><%= distance_of_time_in_words(entry.lastrev.time, Time.now) if entry.lastrev && entry.lastrev.time %></td>
28 <td class="author"><%=h(entry.lastrev.author.to_s.split('<').first) if entry.lastrev %></td>
28 <td class="author"><%=h(entry.lastrev.author.to_s.split('<').first) if entry.lastrev %></td>
29 <% changeset = @project.repository.changesets.find_by_revision(entry.lastrev.identifier) if entry.lastrev %>
29 <% changeset = @project.repository.changesets.find_by_revision(entry.lastrev.identifier) if entry.lastrev %>
30 <td class="comments"><%=h truncate(changeset.comments, 50) unless changeset.nil? %></td>
30 <td class="comments"><%=h truncate(changeset.comments, 50) unless changeset.nil? %></td>
31 </tr>
31 </tr>
32 <% end %>
32 <% end %>
@@ -1,28 +1,28
1 <% form_tag({:controller => 'repositories', :action => 'diff', :id => @project, :path => path}, :method => :get) do %>
1 <% form_tag({:controller => 'repositories', :action => 'diff', :id => @project, :path => path}, :method => :get) do %>
2 <table class="list changesets">
2 <table class="list changesets">
3 <thead><tr>
3 <thead><tr>
4 <th>#</th>
4 <th>#</th>
5 <th></th>
5 <th></th>
6 <th></th>
6 <th></th>
7 <th><%= l(:label_date) %></th>
7 <th><%= l(:label_date) %></th>
8 <th><%= l(:field_author) %></th>
8 <th><%= l(:field_author) %></th>
9 <th><%= l(:field_comments) %></th>
9 <th><%= l(:field_comments) %></th>
10 </tr></thead>
10 </tr></thead>
11 <tbody>
11 <tbody>
12 <% show_diff = entry && entry.is_file? && revisions.size > 1 %>
12 <% show_diff = entry && entry.is_file? && revisions.size > 1 %>
13 <% line_num = 1 %>
13 <% line_num = 1 %>
14 <% revisions.each do |changeset| %>
14 <% revisions.each do |changeset| %>
15 <tr class="changeset <%= cycle 'odd', 'even' %>">
15 <tr class="changeset <%= cycle 'odd', 'even' %>">
16 <td class="id"><%= link_to changeset.revision, :action => 'revision', :id => project, :rev => changeset.revision %></td>
16 <td class="id"><%= link_to format_revision(changeset.revision), :action => 'revision', :id => project, :rev => changeset.revision %></td>
17 <td class="checkbox"><%= radio_button_tag('rev', changeset.revision, (line_num==1), :id => "cb-#{line_num}", :onclick => "$('cbto-#{line_num+1}').checked=true;") if show_diff && (line_num < revisions.size) %></td>
17 <td class="checkbox"><%= radio_button_tag('rev', changeset.revision, (line_num==1), :id => "cb-#{line_num}", :onclick => "$('cbto-#{line_num+1}').checked=true;") if show_diff && (line_num < revisions.size) %></td>
18 <td class="checkbox"><%= radio_button_tag('rev_to', changeset.revision, (line_num==2), :id => "cbto-#{line_num}", :onclick => "if ($('cb-#{line_num}').checked==true) {$('cb-#{line_num-1}').checked=true;}") if show_diff && (line_num > 1) %></td>
18 <td class="checkbox"><%= radio_button_tag('rev_to', changeset.revision, (line_num==2), :id => "cbto-#{line_num}", :onclick => "if ($('cb-#{line_num}').checked==true) {$('cb-#{line_num-1}').checked=true;}") if show_diff && (line_num > 1) %></td>
19 <td class="committed_on"><%= format_time(changeset.committed_on) %></td>
19 <td class="committed_on"><%= format_time(changeset.committed_on) %></td>
20 <td class="author"><%=h changeset.committer.to_s.split('<').first %></td>
20 <td class="author"><%=h changeset.committer.to_s.split('<').first %></td>
21 <td class="comments"><%= textilizable(changeset.comments) %></td>
21 <td class="comments"><%= textilizable(changeset.comments) %></td>
22 </tr>
22 </tr>
23 <% line_num += 1 %>
23 <% line_num += 1 %>
24 <% end %>
24 <% end %>
25 </tbody>
25 </tbody>
26 </table>
26 </table>
27 <%= submit_tag(l(:label_view_diff), :name => nil) if show_diff %>
27 <%= submit_tag(l(:label_view_diff), :name => nil) if show_diff %>
28 <% end %>
28 <% end %>
@@ -1,28 +1,28
1 <h2><%= render :partial => 'navigation', :locals => { :path => @path, :kind => 'file', :revision => @rev } %></h2>
1 <h2><%= render :partial => 'navigation', :locals => { :path => @path, :kind => 'file', :revision => @rev } %></h2>
2
2
3 <% colors = Hash.new {|k,v| k[v] = (k.size % 12) } %>
3 <% colors = Hash.new {|k,v| k[v] = (k.size % 12) } %>
4
4
5 <div class="autoscroll">
5 <div class="autoscroll">
6 <table class="filecontent annotate CodeRay">
6 <table class="filecontent annotate CodeRay">
7 <tbody>
7 <tbody>
8 <% line_num = 1 %>
8 <% line_num = 1 %>
9 <% syntax_highlight(@path, to_utf8(@annotate.content)).each_line do |line| %>
9 <% syntax_highlight(@path, to_utf8(@annotate.content)).each_line do |line| %>
10 <% revision = @annotate.revisions[line_num-1] %>
10 <% revision = @annotate.revisions[line_num-1] %>
11 <tr class="bloc-<%= revision.nil? ? 0 : colors[revision.identifier || revision.revision] %>">
11 <tr class="bloc-<%= revision.nil? ? 0 : colors[revision.identifier || revision.revision] %>">
12 <th class="line-num"><%= line_num %></th>
12 <th class="line-num"><%= line_num %></th>
13 <td class="revision">
13 <td class="revision">
14 <%= (revision.identifier ? link_to(revision.identifier, :action => 'revision', :id => @project, :rev => revision.identifier) : revision.revision) if revision %></td>
14 <%= (revision.identifier ? link_to(format_revision(revision.identifier), :action => 'revision', :id => @project, :rev => revision.identifier) : format_revision(revision.revision)) if revision %></td>
15 <td class="author"><%= h(revision.author.to_s.split('<').first) if revision %></td>
15 <td class="author"><%= h(revision.author.to_s.split('<').first) if revision %></td>
16 <td class="line-code"><pre><%= line %></pre></td>
16 <td class="line-code"><pre><%= line %></pre></td>
17 </tr>
17 </tr>
18 <% line_num += 1 %>
18 <% line_num += 1 %>
19 <% end %>
19 <% end %>
20 </tbody>
20 </tbody>
21 </table>
21 </table>
22 </div>
22 </div>
23
23
24 <% html_title(l(:button_annotate)) -%>
24 <% html_title(l(:button_annotate)) -%>
25
25
26 <% content_for :header_tags do %>
26 <% content_for :header_tags do %>
27 <%= stylesheet_link_tag 'scm' %>
27 <%= stylesheet_link_tag 'scm' %>
28 <% end %>
28 <% end %>
@@ -1,95 +1,95
1 <h2><%= l(:label_revision) %> <%= @rev %>: <%= @path.gsub(/^.*\//, '') %></h2>
1 <h2><%= l(:label_revision) %> <%= format_revision(@rev) %> <%= @path.gsub(/^.*\//, '') %></h2>
2
2
3 <!-- Choose view type -->
3 <!-- Choose view type -->
4 <% form_tag({ :controller => 'repositories', :action => 'diff'}, :method => 'get') do %>
4 <% form_tag({ :controller => 'repositories', :action => 'diff'}, :method => 'get') do %>
5 <% params.each do |k, p| %>
5 <% params.each do |k, p| %>
6 <% if k != "type" %>
6 <% if k != "type" %>
7 <%= hidden_field_tag(k,p) %>
7 <%= hidden_field_tag(k,p) %>
8 <% end %>
8 <% end %>
9 <% end %>
9 <% end %>
10 <p><label><%= l(:label_view_diff) %></label>
10 <p><label><%= l(:label_view_diff) %></label>
11 <%= select_tag 'type', options_for_select([[l(:label_diff_inline), "inline"], [l(:label_diff_side_by_side), "sbs"]], @diff_type), :onchange => "if (this.value != '') {this.form.submit()}" %></p>
11 <%= select_tag 'type', options_for_select([[l(:label_diff_inline), "inline"], [l(:label_diff_side_by_side), "sbs"]], @diff_type), :onchange => "if (this.value != '') {this.form.submit()}" %></p>
12 <% end %>
12 <% end %>
13
13
14 <% cache(@cache_key) do %>
14 <% cache(@cache_key) do %>
15 <% @diff.each do |table_file| %>
15 <% @diff.each do |table_file| %>
16 <div class="autoscroll">
16 <div class="autoscroll">
17 <% if @diff_type == 'sbs' %>
17 <% if @diff_type == 'sbs' %>
18 <table class="filecontent CodeRay">
18 <table class="filecontent CodeRay">
19 <thead>
19 <thead>
20 <tr>
20 <tr>
21 <th colspan="4" class="filename">
21 <th colspan="4" class="filename">
22 <%= table_file.file_name %>
22 <%= table_file.file_name %>
23 </th>
23 </th>
24 </tr>
24 </tr>
25 <tr>
25 <tr>
26 <th colspan="2">@<%= @rev %></th>
26 <th colspan="2">@<%= format_revision @rev %></th>
27 <th colspan="2">@<%= @rev_to %></th>
27 <th colspan="2">@<%= format_revision @rev_to %></th>
28 </tr>
28 </tr>
29 </thead>
29 </thead>
30 <tbody>
30 <tbody>
31 <% table_file.keys.sort.each do |key| %>
31 <% table_file.keys.sort.each do |key| %>
32 <tr>
32 <tr>
33 <th class="line-num">
33 <th class="line-num">
34 <%= table_file[key].nb_line_left %>
34 <%= table_file[key].nb_line_left %>
35 </th>
35 </th>
36 <td class="line-code <%= table_file[key].type_diff_left %>">
36 <td class="line-code <%= table_file[key].type_diff_left %>">
37 <pre><%=to_utf8 table_file[key].line_left %></pre>
37 <pre><%=to_utf8 table_file[key].line_left %></pre>
38 </td>
38 </td>
39 <th class="line-num">
39 <th class="line-num">
40 <%= table_file[key].nb_line_right %>
40 <%= table_file[key].nb_line_right %>
41 </th>
41 </th>
42 <td class="line-code <%= table_file[key].type_diff_right %>">
42 <td class="line-code <%= table_file[key].type_diff_right %>">
43 <pre><%=to_utf8 table_file[key].line_right %></pre>
43 <pre><%=to_utf8 table_file[key].line_right %></pre>
44 </td>
44 </td>
45 </tr>
45 </tr>
46 <% end %>
46 <% end %>
47 </tbody>
47 </tbody>
48 </table>
48 </table>
49
49
50 <% else %>
50 <% else %>
51 <table class="filecontent CodeRay">
51 <table class="filecontent CodeRay">
52 <thead>
52 <thead>
53 <tr>
53 <tr>
54 <th colspan="3" class="filename">
54 <th colspan="3" class="filename">
55 <%= table_file.file_name %>
55 <%= table_file.file_name %>
56 </th>
56 </th>
57 </tr>
57 </tr>
58 <tr>
58 <tr>
59 <th>@<%= @rev %></th>
59 <th>@<%= format_revision @rev %></th>
60 <th>@<%= @rev_to %></th>
60 <th>@<%= format_revision @rev_to %></th>
61 <th></th>
61 <th></th>
62 </tr>
62 </tr>
63 </thead>
63 </thead>
64 <tbody>
64 <tbody>
65 <% table_file.keys.sort.each do |key, line| %>
65 <% table_file.keys.sort.each do |key, line| %>
66 <tr>
66 <tr>
67 <th class="line-num">
67 <th class="line-num">
68 <%= table_file[key].nb_line_left %>
68 <%= table_file[key].nb_line_left %>
69 </th>
69 </th>
70 <th class="line-num">
70 <th class="line-num">
71 <%= table_file[key].nb_line_right %>
71 <%= table_file[key].nb_line_right %>
72 </th>
72 </th>
73 <% if table_file[key].line_left.empty? %>
73 <% if table_file[key].line_left.empty? %>
74 <td class="line-code <%= table_file[key].type_diff_right %>">
74 <td class="line-code <%= table_file[key].type_diff_right %>">
75 <pre><%=to_utf8 table_file[key].line_right %></pre>
75 <pre><%=to_utf8 table_file[key].line_right %></pre>
76 </td>
76 </td>
77 <% else %>
77 <% else %>
78 <td class="line-code <%= table_file[key].type_diff_left %>">
78 <td class="line-code <%= table_file[key].type_diff_left %>">
79 <pre><%=to_utf8 table_file[key].line_left %></pre>
79 <pre><%=to_utf8 table_file[key].line_left %></pre>
80 </td>
80 </td>
81 <% end %>
81 <% end %>
82 </tr>
82 </tr>
83 <% end %>
83 <% end %>
84 </tbody>
84 </tbody>
85 </table>
85 </table>
86 <% end %>
86 <% end %>
87 </div>
87 </div>
88 <% end %>
88 <% end %>
89 <% end %>
89 <% end %>
90
90
91 <% html_title(with_leading_slash(@path), 'Diff') -%>
91 <% html_title(with_leading_slash(@path), 'Diff') -%>
92
92
93 <% content_for :header_tags do %>
93 <% content_for :header_tags do %>
94 <%= stylesheet_link_tag "scm" %>
94 <%= stylesheet_link_tag "scm" %>
95 <% end %>
95 <% end %>
@@ -1,65 +1,65
1 <div class="contextual">
1 <div class="contextual">
2 &#171;
2 &#171;
3 <% unless @changeset.previous.nil? -%>
3 <% unless @changeset.previous.nil? -%>
4 <%= link_to l(:label_previous), :controller => 'repositories', :action => 'revision', :id => @project, :rev => @changeset.previous.revision %>
4 <%= link_to l(:label_previous), :controller => 'repositories', :action => 'revision', :id => @project, :rev => @changeset.previous.revision %>
5 <% else -%>
5 <% else -%>
6 <%= l(:label_previous) %>
6 <%= l(:label_previous) %>
7 <% end -%>
7 <% end -%>
8 |
8 |
9 <% unless @changeset.next.nil? -%>
9 <% unless @changeset.next.nil? -%>
10 <%= link_to l(:label_next), :controller => 'repositories', :action => 'revision', :id => @project, :rev => @changeset.next.revision %>
10 <%= link_to l(:label_next), :controller => 'repositories', :action => 'revision', :id => @project, :rev => @changeset.next.revision %>
11 <% else -%>
11 <% else -%>
12 <%= l(:label_next) %>
12 <%= l(:label_next) %>
13 <% end -%>
13 <% end -%>
14 &#187;&nbsp;
14 &#187;&nbsp;
15
15
16 <% form_tag do %>
16 <% form_tag do %>
17 <%= text_field_tag 'rev', @rev, :size => 5 %>
17 <%= text_field_tag 'rev', @rev, :size => 5 %>
18 <%= submit_tag 'OK' %>
18 <%= submit_tag 'OK' %>
19 <% end %>
19 <% end %>
20 </div>
20 </div>
21
21
22 <h2><%= l(:label_revision) %> <%= @changeset.revision %></h2>
22 <h2><%= l(:label_revision) %> <%= format_revision(@changeset.revision) %></h2>
23
23
24 <p><% if @changeset.scmid %>ID: <%= @changeset.scmid %><br /><% end %>
24 <p><% if @changeset.scmid %>ID: <%= @changeset.scmid %><br /><% end %>
25 <em><%= @changeset.committer.to_s.split('<').first %>, <%= format_time(@changeset.committed_on) %></em></p>
25 <em><%= @changeset.committer.to_s.split('<').first %>, <%= format_time(@changeset.committed_on) %></em></p>
26
26
27 <%= textilizable @changeset.comments %>
27 <%= textilizable @changeset.comments %>
28
28
29 <% if @changeset.issues.any? %>
29 <% if @changeset.issues.any? %>
30 <h3><%= l(:label_related_issues) %></h3>
30 <h3><%= l(:label_related_issues) %></h3>
31 <ul>
31 <ul>
32 <% @changeset.issues.each do |issue| %>
32 <% @changeset.issues.each do |issue| %>
33 <li><%= link_to_issue issue %>: <%=h issue.subject %></li>
33 <li><%= link_to_issue issue %>: <%=h issue.subject %></li>
34 <% end %>
34 <% end %>
35 </ul>
35 </ul>
36 <% end %>
36 <% end %>
37
37
38 <h3><%= l(:label_attachment_plural) %></h3>
38 <h3><%= l(:label_attachment_plural) %></h3>
39 <div style="float:right;">
39 <div style="float:right;">
40 <div class="square action_A"></div> <div style="float:left;"><%= l(:label_added) %>&nbsp;</div>
40 <div class="square action_A"></div> <div style="float:left;"><%= l(:label_added) %>&nbsp;</div>
41 <div class="square action_M"></div> <div style="float:left;"><%= l(:label_modified) %>&nbsp;</div>
41 <div class="square action_M"></div> <div style="float:left;"><%= l(:label_modified) %>&nbsp;</div>
42 <div class="square action_D"></div> <div style="float:left;"><%= l(:label_deleted) %>&nbsp;</div>
42 <div class="square action_D"></div> <div style="float:left;"><%= l(:label_deleted) %>&nbsp;</div>
43 </div>
43 </div>
44 <p><%= link_to(l(:label_view_diff), :action => 'diff', :id => @project, :path => "", :rev => @changeset.revision) if @changeset.changes.any? %></p>
44 <p><%= link_to(l(:label_view_diff), :action => 'diff', :id => @project, :path => "", :rev => @changeset.revision) if @changeset.changes.any? %></p>
45 <table class="list">
45 <table class="list">
46 <tbody>
46 <tbody>
47 <% @changes.each do |change| %>
47 <% @changes.each do |change| %>
48 <tr class="<%= cycle 'odd', 'even' %>">
48 <tr class="<%= cycle 'odd', 'even' %>">
49 <td><div class="square action_<%= change.action %>"></div> <%= change.path %> <%= "(#{change.revision})" unless change.revision.blank? %></td>
49 <td><div class="square action_<%= change.action %>"></div> <%= change.path %> <%= "(#{change.revision})" unless change.revision.blank? %></td>
50 <td align="right">
50 <td align="right">
51 <% if change.action == "M" %>
51 <% if change.action == "M" %>
52 <%= link_to l(:label_view_diff), :action => 'diff', :id => @project, :path => change.path, :rev => @changeset.revision %>
52 <%= link_to l(:label_view_diff), :action => 'diff', :id => @project, :path => change.path, :rev => @changeset.revision %>
53 <% end %>
53 <% end %>
54 </td>
54 </td>
55 </tr>
55 </tr>
56 <% end %>
56 <% end %>
57 </tbody>
57 </tbody>
58 </table>
58 </table>
59 <p class="pagination"><%= pagination_links_full @changes_pages %></p>
59 <p class="pagination"><%= pagination_links_full @changes_pages %></p>
60
60
61 <% content_for :header_tags do %>
61 <% content_for :header_tags do %>
62 <%= stylesheet_link_tag "scm" %>
62 <%= stylesheet_link_tag "scm" %>
63 <% end %>
63 <% end %>
64
64
65 <% html_title("#{l(:label_revision)} #{@changeset.revision}") -%>
65 <% html_title("#{l(:label_revision)} #{@changeset.revision}") -%>
@@ -1,21 +1,37
1 Creating test repositories
1 Creating test repositories
2 ===================
2 ===================
3
3
4 mkdir tmp/test
4 mkdir tmp/test
5
5
6 Subversion
6 Subversion
7 ----------
7 ----------
8 svnadmin create tmp/test/subversion_repository
8 svnadmin create tmp/test/subversion_repository
9 gunzip < test/fixtures/repositories/subversion_repository.dump.gz | svnadmin load tmp/test/subversion_repository
9 gunzip < test/fixtures/repositories/subversion_repository.dump.gz | svnadmin load tmp/test/subversion_repository
10
10
11 CVS
11 CVS
12 ---
12 ---
13 gunzip < test/fixtures/repositories/cvs_repository.tar.gz | tar -xv -C tmp/test
13 gunzip < test/fixtures/repositories/cvs_repository.tar.gz | tar -xv -C tmp/test
14
14
15 Bazaar
15 Bazaar
16 ------
16 ------
17 gunzip < test/fixtures/repositories/bazaar_repository.tar.gz | tar -xv -C tmp/test
17 gunzip < test/fixtures/repositories/bazaar_repository.tar.gz | tar -xv -C tmp/test
18
18
19 Mercurial
19 Mercurial
20 ---------
20 ---------
21 gunzip < test/fixtures/repositories/mercurial_repository.tar.gz | tar -xv -C tmp/test
21 gunzip < test/fixtures/repositories/mercurial_repository.tar.gz | tar -xv -C tmp/test
22
23 Git
24 ---
25 gunzip < test/fixtures/repositories/git_repository.tar.gz | tar -xv -C tmp/test
26
27
28 Running Tests
29 =============
30
31 Run
32
33 rake --tasks | grep test
34
35 to see available tests.
36
37 RAILS_ENV=test rake test will run tests.
@@ -1,130 +1,130
1 require 'redmine/access_control'
1 require 'redmine/access_control'
2 require 'redmine/menu_manager'
2 require 'redmine/menu_manager'
3 require 'redmine/mime_type'
3 require 'redmine/mime_type'
4 require 'redmine/themes'
4 require 'redmine/themes'
5 require 'redmine/plugin'
5 require 'redmine/plugin'
6
6
7 begin
7 begin
8 require_library_or_gem 'RMagick' unless Object.const_defined?(:Magick)
8 require_library_or_gem 'RMagick' unless Object.const_defined?(:Magick)
9 rescue LoadError
9 rescue LoadError
10 # RMagick is not available
10 # RMagick is not available
11 end
11 end
12
12
13 REDMINE_SUPPORTED_SCM = %w( Subversion Darcs Mercurial Cvs Bazaar )
13 REDMINE_SUPPORTED_SCM = %w( Subversion Darcs Mercurial Cvs Bazaar Git )
14
14
15 # Permissions
15 # Permissions
16 Redmine::AccessControl.map do |map|
16 Redmine::AccessControl.map do |map|
17 map.permission :view_project, {:projects => [:show, :activity]}, :public => true
17 map.permission :view_project, {:projects => [:show, :activity]}, :public => true
18 map.permission :search_project, {:search => :index}, :public => true
18 map.permission :search_project, {:search => :index}, :public => true
19 map.permission :edit_project, {:projects => [:settings, :edit]}, :require => :member
19 map.permission :edit_project, {:projects => [:settings, :edit]}, :require => :member
20 map.permission :select_project_modules, {:projects => :modules}, :require => :member
20 map.permission :select_project_modules, {:projects => :modules}, :require => :member
21 map.permission :manage_members, {:projects => :settings, :members => [:new, :edit, :destroy]}, :require => :member
21 map.permission :manage_members, {:projects => :settings, :members => [:new, :edit, :destroy]}, :require => :member
22 map.permission :manage_versions, {:projects => [:settings, :add_version], :versions => [:edit, :destroy]}, :require => :member
22 map.permission :manage_versions, {:projects => [:settings, :add_version], :versions => [:edit, :destroy]}, :require => :member
23
23
24 map.project_module :issue_tracking do |map|
24 map.project_module :issue_tracking do |map|
25 # Issue categories
25 # Issue categories
26 map.permission :manage_categories, {:projects => [:settings, :add_issue_category], :issue_categories => [:edit, :destroy]}, :require => :member
26 map.permission :manage_categories, {:projects => [:settings, :add_issue_category], :issue_categories => [:edit, :destroy]}, :require => :member
27 # Issues
27 # Issues
28 map.permission :view_issues, {:projects => [:changelog, :roadmap],
28 map.permission :view_issues, {:projects => [:changelog, :roadmap],
29 :issues => [:index, :changes, :show, :context_menu],
29 :issues => [:index, :changes, :show, :context_menu],
30 :versions => [:show, :status_by],
30 :versions => [:show, :status_by],
31 :queries => :index,
31 :queries => :index,
32 :reports => :issue_report}, :public => true
32 :reports => :issue_report}, :public => true
33 map.permission :add_issues, {:issues => :new}
33 map.permission :add_issues, {:issues => :new}
34 map.permission :edit_issues, {:issues => [:edit, :bulk_edit, :destroy_attachment]}
34 map.permission :edit_issues, {:issues => [:edit, :bulk_edit, :destroy_attachment]}
35 map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy]}
35 map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy]}
36 map.permission :add_issue_notes, {:issues => :edit}
36 map.permission :add_issue_notes, {:issues => :edit}
37 map.permission :edit_issue_notes, {:journals => :edit}, :require => :loggedin
37 map.permission :edit_issue_notes, {:journals => :edit}, :require => :loggedin
38 map.permission :edit_own_issue_notes, {:journals => :edit}, :require => :loggedin
38 map.permission :edit_own_issue_notes, {:journals => :edit}, :require => :loggedin
39 map.permission :move_issues, {:issues => :move}, :require => :loggedin
39 map.permission :move_issues, {:issues => :move}, :require => :loggedin
40 map.permission :delete_issues, {:issues => :destroy}, :require => :member
40 map.permission :delete_issues, {:issues => :destroy}, :require => :member
41 # Queries
41 # Queries
42 map.permission :manage_public_queries, {:queries => [:new, :edit, :destroy]}, :require => :member
42 map.permission :manage_public_queries, {:queries => [:new, :edit, :destroy]}, :require => :member
43 map.permission :save_queries, {:queries => [:new, :edit, :destroy]}, :require => :loggedin
43 map.permission :save_queries, {:queries => [:new, :edit, :destroy]}, :require => :loggedin
44 # Gantt & calendar
44 # Gantt & calendar
45 map.permission :view_gantt, :projects => :gantt
45 map.permission :view_gantt, :projects => :gantt
46 map.permission :view_calendar, :projects => :calendar
46 map.permission :view_calendar, :projects => :calendar
47 end
47 end
48
48
49 map.project_module :time_tracking do |map|
49 map.project_module :time_tracking do |map|
50 map.permission :log_time, {:timelog => :edit}, :require => :loggedin
50 map.permission :log_time, {:timelog => :edit}, :require => :loggedin
51 map.permission :view_time_entries, :timelog => [:details, :report]
51 map.permission :view_time_entries, :timelog => [:details, :report]
52 end
52 end
53
53
54 map.project_module :news do |map|
54 map.project_module :news do |map|
55 map.permission :manage_news, {:news => [:new, :edit, :destroy, :destroy_comment]}, :require => :member
55 map.permission :manage_news, {:news => [:new, :edit, :destroy, :destroy_comment]}, :require => :member
56 map.permission :view_news, {:news => [:index, :show]}, :public => true
56 map.permission :view_news, {:news => [:index, :show]}, :public => true
57 map.permission :comment_news, {:news => :add_comment}
57 map.permission :comment_news, {:news => :add_comment}
58 end
58 end
59
59
60 map.project_module :documents do |map|
60 map.project_module :documents do |map|
61 map.permission :manage_documents, {:documents => [:new, :edit, :destroy, :add_attachment, :destroy_attachment]}, :require => :loggedin
61 map.permission :manage_documents, {:documents => [:new, :edit, :destroy, :add_attachment, :destroy_attachment]}, :require => :loggedin
62 map.permission :view_documents, :documents => [:index, :show, :download]
62 map.permission :view_documents, :documents => [:index, :show, :download]
63 end
63 end
64
64
65 map.project_module :files do |map|
65 map.project_module :files do |map|
66 map.permission :manage_files, {:projects => :add_file, :versions => :destroy_file}, :require => :loggedin
66 map.permission :manage_files, {:projects => :add_file, :versions => :destroy_file}, :require => :loggedin
67 map.permission :view_files, :projects => :list_files, :versions => :download
67 map.permission :view_files, :projects => :list_files, :versions => :download
68 end
68 end
69
69
70 map.project_module :wiki do |map|
70 map.project_module :wiki do |map|
71 map.permission :manage_wiki, {:wikis => [:edit, :destroy]}, :require => :member
71 map.permission :manage_wiki, {:wikis => [:edit, :destroy]}, :require => :member
72 map.permission :rename_wiki_pages, {:wiki => :rename}, :require => :member
72 map.permission :rename_wiki_pages, {:wiki => :rename}, :require => :member
73 map.permission :delete_wiki_pages, {:wiki => :destroy}, :require => :member
73 map.permission :delete_wiki_pages, {:wiki => :destroy}, :require => :member
74 map.permission :view_wiki_pages, :wiki => [:index, :history, :diff, :annotate, :special]
74 map.permission :view_wiki_pages, :wiki => [:index, :history, :diff, :annotate, :special]
75 map.permission :edit_wiki_pages, :wiki => [:edit, :preview, :add_attachment, :destroy_attachment]
75 map.permission :edit_wiki_pages, :wiki => [:edit, :preview, :add_attachment, :destroy_attachment]
76 end
76 end
77
77
78 map.project_module :repository do |map|
78 map.project_module :repository do |map|
79 map.permission :manage_repository, {:repositories => [:edit, :destroy]}, :require => :member
79 map.permission :manage_repository, {:repositories => [:edit, :destroy]}, :require => :member
80 map.permission :browse_repository, :repositories => [:show, :browse, :entry, :annotate, :changes, :diff, :stats, :graph]
80 map.permission :browse_repository, :repositories => [:show, :browse, :entry, :annotate, :changes, :diff, :stats, :graph]
81 map.permission :view_changesets, :repositories => [:show, :revisions, :revision]
81 map.permission :view_changesets, :repositories => [:show, :revisions, :revision]
82 end
82 end
83
83
84 map.project_module :boards do |map|
84 map.project_module :boards do |map|
85 map.permission :manage_boards, {:boards => [:new, :edit, :destroy]}, :require => :member
85 map.permission :manage_boards, {:boards => [:new, :edit, :destroy]}, :require => :member
86 map.permission :view_messages, {:boards => [:index, :show], :messages => [:show]}, :public => true
86 map.permission :view_messages, {:boards => [:index, :show], :messages => [:show]}, :public => true
87 map.permission :add_messages, {:messages => [:new, :reply]}
87 map.permission :add_messages, {:messages => [:new, :reply]}
88 map.permission :edit_messages, {:messages => :edit}, :require => :member
88 map.permission :edit_messages, {:messages => :edit}, :require => :member
89 map.permission :delete_messages, {:messages => :destroy}, :require => :member
89 map.permission :delete_messages, {:messages => :destroy}, :require => :member
90 end
90 end
91 end
91 end
92
92
93 Redmine::MenuManager.map :top_menu do |menu|
93 Redmine::MenuManager.map :top_menu do |menu|
94 menu.push :home, :home_url, :html => { :class => 'home' }
94 menu.push :home, :home_url, :html => { :class => 'home' }
95 menu.push :my_page, { :controller => 'my', :action => 'page' }, :html => { :class => 'mypage' }, :if => Proc.new { User.current.logged? }
95 menu.push :my_page, { :controller => 'my', :action => 'page' }, :html => { :class => 'mypage' }, :if => Proc.new { User.current.logged? }
96 menu.push :projects, { :controller => 'projects', :action => 'index' }, :caption => :label_project_plural, :html => { :class => 'projects' }
96 menu.push :projects, { :controller => 'projects', :action => 'index' }, :caption => :label_project_plural, :html => { :class => 'projects' }
97 menu.push :administration, { :controller => 'admin', :action => 'index' }, :html => { :class => 'admin' }, :if => Proc.new { User.current.admin? }
97 menu.push :administration, { :controller => 'admin', :action => 'index' }, :html => { :class => 'admin' }, :if => Proc.new { User.current.admin? }
98 menu.push :help, Redmine::Info.help_url, :html => { :class => 'help' }
98 menu.push :help, Redmine::Info.help_url, :html => { :class => 'help' }
99 end
99 end
100
100
101 Redmine::MenuManager.map :account_menu do |menu|
101 Redmine::MenuManager.map :account_menu do |menu|
102 menu.push :login, :signin_url, :html => { :class => 'login' }, :if => Proc.new { !User.current.logged? }
102 menu.push :login, :signin_url, :html => { :class => 'login' }, :if => Proc.new { !User.current.logged? }
103 menu.push :register, { :controller => 'account', :action => 'register' }, :html => { :class => 'register' }, :if => Proc.new { !User.current.logged? && Setting.self_registration? }
103 menu.push :register, { :controller => 'account', :action => 'register' }, :html => { :class => 'register' }, :if => Proc.new { !User.current.logged? && Setting.self_registration? }
104 menu.push :my_account, { :controller => 'my', :action => 'account' }, :html => { :class => 'myaccount' }, :if => Proc.new { User.current.logged? }
104 menu.push :my_account, { :controller => 'my', :action => 'account' }, :html => { :class => 'myaccount' }, :if => Proc.new { User.current.logged? }
105 menu.push :logout, :signout_url, :html => { :class => 'logout' }, :if => Proc.new { User.current.logged? }
105 menu.push :logout, :signout_url, :html => { :class => 'logout' }, :if => Proc.new { User.current.logged? }
106 end
106 end
107
107
108 Redmine::MenuManager.map :application_menu do |menu|
108 Redmine::MenuManager.map :application_menu do |menu|
109 # Empty
109 # Empty
110 end
110 end
111
111
112 Redmine::MenuManager.map :project_menu do |menu|
112 Redmine::MenuManager.map :project_menu do |menu|
113 menu.push :overview, { :controller => 'projects', :action => 'show' }
113 menu.push :overview, { :controller => 'projects', :action => 'show' }
114 menu.push :activity, { :controller => 'projects', :action => 'activity' }
114 menu.push :activity, { :controller => 'projects', :action => 'activity' }
115 menu.push :roadmap, { :controller => 'projects', :action => 'roadmap' },
115 menu.push :roadmap, { :controller => 'projects', :action => 'roadmap' },
116 :if => Proc.new { |p| p.versions.any? }
116 :if => Proc.new { |p| p.versions.any? }
117 menu.push :issues, { :controller => 'issues', :action => 'index' }, :param => :project_id, :caption => :label_issue_plural
117 menu.push :issues, { :controller => 'issues', :action => 'index' }, :param => :project_id, :caption => :label_issue_plural
118 menu.push :new_issue, { :controller => 'issues', :action => 'new' }, :param => :project_id, :caption => :label_issue_new,
118 menu.push :new_issue, { :controller => 'issues', :action => 'new' }, :param => :project_id, :caption => :label_issue_new,
119 :html => { :accesskey => Redmine::AccessKeys.key_for(:new_issue) }
119 :html => { :accesskey => Redmine::AccessKeys.key_for(:new_issue) }
120 menu.push :news, { :controller => 'news', :action => 'index' }, :param => :project_id, :caption => :label_news_plural
120 menu.push :news, { :controller => 'news', :action => 'index' }, :param => :project_id, :caption => :label_news_plural
121 menu.push :documents, { :controller => 'documents', :action => 'index' }, :param => :project_id, :caption => :label_document_plural
121 menu.push :documents, { :controller => 'documents', :action => 'index' }, :param => :project_id, :caption => :label_document_plural
122 menu.push :wiki, { :controller => 'wiki', :action => 'index', :page => nil },
122 menu.push :wiki, { :controller => 'wiki', :action => 'index', :page => nil },
123 :if => Proc.new { |p| p.wiki && !p.wiki.new_record? }
123 :if => Proc.new { |p| p.wiki && !p.wiki.new_record? }
124 menu.push :boards, { :controller => 'boards', :action => 'index', :id => nil }, :param => :project_id,
124 menu.push :boards, { :controller => 'boards', :action => 'index', :id => nil }, :param => :project_id,
125 :if => Proc.new { |p| p.boards.any? }, :caption => :label_board_plural
125 :if => Proc.new { |p| p.boards.any? }, :caption => :label_board_plural
126 menu.push :files, { :controller => 'projects', :action => 'list_files' }, :caption => :label_attachment_plural
126 menu.push :files, { :controller => 'projects', :action => 'list_files' }, :caption => :label_attachment_plural
127 menu.push :repository, { :controller => 'repositories', :action => 'show' },
127 menu.push :repository, { :controller => 'repositories', :action => 'show' },
128 :if => Proc.new { |p| p.repository && !p.repository.new_record? }
128 :if => Proc.new { |p| p.repository && !p.repository.new_record? }
129 menu.push :settings, { :controller => 'projects', :action => 'settings' }
129 menu.push :settings, { :controller => 'projects', :action => 'settings' }
130 end
130 end
@@ -1,157 +1,161
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'redmine/scm/adapters/abstract_adapter'
18 require 'redmine/scm/adapters/abstract_adapter'
19 require 'rexml/document'
19 require 'rexml/document'
20
20
21 module Redmine
21 module Redmine
22 module Scm
22 module Scm
23 module Adapters
23 module Adapters
24 class DarcsAdapter < AbstractAdapter
24 class DarcsAdapter < AbstractAdapter
25 # Darcs executable name
25 # Darcs executable name
26 DARCS_BIN = "darcs"
26 DARCS_BIN = "darcs"
27
27
28 def initialize(url, root_url=nil, login=nil, password=nil)
28 def initialize(url, root_url=nil, login=nil, password=nil)
29 @url = url
29 @url = url
30 @root_url = url
30 @root_url = url
31 end
31 end
32
32
33 def supports_cat?
33 def supports_cat?
34 false
34 false
35 end
35 end
36
36
37 # Get info about the svn repository
37 # Get info about the svn repository
38 def info
38 def info
39 rev = revisions(nil,nil,nil,{:limit => 1})
39 rev = revisions(nil,nil,nil,{:limit => 1})
40 rev ? Info.new({:root_url => @url, :lastrev => rev.last}) : nil
40 rev ? Info.new({:root_url => @url, :lastrev => rev.last}) : nil
41 end
41 end
42
42
43 # Returns the entry identified by path and revision identifier
43 # Returns the entry identified by path and revision identifier
44 # or nil if entry doesn't exist in the repository
44 # or nil if entry doesn't exist in the repository
45 def entry(path=nil, identifier=nil)
45 def entry(path=nil, identifier=nil)
46 e = entries(path, identifier)
46 e = entries(path, identifier)
47 e ? e.first : nil
47 e ? e.first : nil
48 end
48 end
49
49
50 # Returns an Entries collection
50 # Returns an Entries collection
51 # or nil if the given path doesn't exist in the repository
51 # or nil if the given path doesn't exist in the repository
52 def entries(path=nil, identifier=nil)
52 def entries(path=nil, identifier=nil)
53 path_prefix = (path.blank? ? '' : "#{path}/")
53 path_prefix = (path.blank? ? '' : "#{path}/")
54 path = '.' if path.blank?
54 path = '.' if path.blank?
55 entries = Entries.new
55 entries = Entries.new
56 cmd = "#{DARCS_BIN} annotate --repodir #{@url} --xml-output #{path}"
56 cmd = "#{DARCS_BIN} annotate --repodir #{@url} --xml-output #{path}"
57 shellout(cmd) do |io|
57 shellout(cmd) do |io|
58 begin
58 begin
59 doc = REXML::Document.new(io)
59 doc = REXML::Document.new(io)
60 if doc.root.name == 'directory'
60 if doc.root.name == 'directory'
61 doc.elements.each('directory/*') do |element|
61 doc.elements.each('directory/*') do |element|
62 next unless ['file', 'directory'].include? element.name
62 next unless ['file', 'directory'].include? element.name
63 entries << entry_from_xml(element, path_prefix)
63 entries << entry_from_xml(element, path_prefix)
64 end
64 end
65 elsif doc.root.name == 'file'
65 elsif doc.root.name == 'file'
66 entries << entry_from_xml(doc.root, path_prefix)
66 entries << entry_from_xml(doc.root, path_prefix)
67 end
67 end
68 rescue
68 rescue
69 end
69 end
70 end
70 end
71 return nil if $? && $?.exitstatus != 0
71 return nil if $? && $?.exitstatus != 0
72 entries.sort_by_name
72 entries.sort_by_name
73 end
73 end
74
74
75 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
75 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
76 path = '.' if path.blank?
76 path = '.' if path.blank?
77 revisions = Revisions.new
77 revisions = Revisions.new
78 cmd = "#{DARCS_BIN} changes --repodir #{@url} --xml-output"
78 cmd = "#{DARCS_BIN} changes --repodir #{@url} --xml-output"
79 cmd << " --from-match \"hash #{identifier_from}\"" if identifier_from
79 cmd << " --from-match \"hash #{identifier_from}\"" if identifier_from
80 cmd << " --last #{options[:limit].to_i}" if options[:limit]
80 cmd << " --last #{options[:limit].to_i}" if options[:limit]
81 shellout(cmd) do |io|
81 shellout(cmd) do |io|
82 begin
82 begin
83 doc = REXML::Document.new(io)
83 doc = REXML::Document.new(io)
84 doc.elements.each("changelog/patch") do |patch|
84 doc.elements.each("changelog/patch") do |patch|
85 message = patch.elements['name'].text
85 message = patch.elements['name'].text
86 message << "\n" + patch.elements['comment'].text.gsub(/\*\*\*END OF DESCRIPTION\*\*\*.*\z/m, '') if patch.elements['comment']
86 message << "\n" + patch.elements['comment'].text.gsub(/\*\*\*END OF DESCRIPTION\*\*\*.*\z/m, '') if patch.elements['comment']
87 revisions << Revision.new({:identifier => nil,
87 revisions << Revision.new({:identifier => nil,
88 :author => patch.attributes['author'],
88 :author => patch.attributes['author'],
89 :scmid => patch.attributes['hash'],
89 :scmid => patch.attributes['hash'],
90 :time => Time.parse(patch.attributes['local_date']),
90 :time => Time.parse(patch.attributes['local_date']),
91 :message => message,
91 :message => message,
92 :paths => (options[:with_path] ? get_paths_for_patch(patch.attributes['hash']) : nil)
92 :paths => (options[:with_path] ? get_paths_for_patch(patch.attributes['hash']) : nil)
93 })
93 })
94 end
94 end
95 rescue
95 rescue
96 end
96 end
97 end
97 end
98 return nil if $? && $?.exitstatus != 0
98 return nil if $? && $?.exitstatus != 0
99 revisions
99 revisions
100 end
100 end
101
101
102 def diff(path, identifier_from, identifier_to=nil, type="inline")
102 def diff(path, identifier_from, identifier_to=nil, type="inline")
103 path = '*' if path.blank?
103 path = '*' if path.blank?
104 cmd = "#{DARCS_BIN} diff --repodir #{@url}"
104 cmd = "#{DARCS_BIN} diff --repodir #{@url}"
105 if identifier_to.nil?
106 cmd << " --match \"hash #{identifier_from}\""
107 else
105 cmd << " --to-match \"hash #{identifier_from}\""
108 cmd << " --to-match \"hash #{identifier_from}\""
106 cmd << " --from-match \"hash #{identifier_to}\"" if identifier_to
109 cmd << " --from-match \"hash #{identifier_to}\""
110 end
107 cmd << " -u #{path}"
111 cmd << " -u #{path}"
108 diff = []
112 diff = []
109 shellout(cmd) do |io|
113 shellout(cmd) do |io|
110 io.each_line do |line|
114 io.each_line do |line|
111 diff << line
115 diff << line
112 end
116 end
113 end
117 end
114 return nil if $? && $?.exitstatus != 0
118 return nil if $? && $?.exitstatus != 0
115 DiffTableList.new diff, type
119 DiffTableList.new diff, type
116 end
120 end
117
121
118 private
122 private
119
123
120 def entry_from_xml(element, path_prefix)
124 def entry_from_xml(element, path_prefix)
121 Entry.new({:name => element.attributes['name'],
125 Entry.new({:name => element.attributes['name'],
122 :path => path_prefix + element.attributes['name'],
126 :path => path_prefix + element.attributes['name'],
123 :kind => element.name == 'file' ? 'file' : 'dir',
127 :kind => element.name == 'file' ? 'file' : 'dir',
124 :size => nil,
128 :size => nil,
125 :lastrev => Revision.new({
129 :lastrev => Revision.new({
126 :identifier => nil,
130 :identifier => nil,
127 :scmid => element.elements['modified'].elements['patch'].attributes['hash']
131 :scmid => element.elements['modified'].elements['patch'].attributes['hash']
128 })
132 })
129 })
133 })
130 end
134 end
131
135
132 # Retrieve changed paths for a single patch
136 # Retrieve changed paths for a single patch
133 def get_paths_for_patch(hash)
137 def get_paths_for_patch(hash)
134 cmd = "#{DARCS_BIN} annotate --repodir #{@url} --summary --xml-output"
138 cmd = "#{DARCS_BIN} annotate --repodir #{@url} --summary --xml-output"
135 cmd << " --match \"hash #{hash}\" "
139 cmd << " --match \"hash #{hash}\" "
136 paths = []
140 paths = []
137 shellout(cmd) do |io|
141 shellout(cmd) do |io|
138 begin
142 begin
139 # Darcs xml output has multiple root elements in this case (tested with darcs 1.0.7)
143 # Darcs xml output has multiple root elements in this case (tested with darcs 1.0.7)
140 # A root element is added so that REXML doesn't raise an error
144 # A root element is added so that REXML doesn't raise an error
141 doc = REXML::Document.new("<fake_root>" + io.read + "</fake_root>")
145 doc = REXML::Document.new("<fake_root>" + io.read + "</fake_root>")
142 doc.elements.each('fake_root/summary/*') do |modif|
146 doc.elements.each('fake_root/summary/*') do |modif|
143 paths << {:action => modif.name[0,1].upcase,
147 paths << {:action => modif.name[0,1].upcase,
144 :path => "/" + modif.text.chomp.gsub(/^\s*/, '')
148 :path => "/" + modif.text.chomp.gsub(/^\s*/, '')
145 }
149 }
146 end
150 end
147 rescue
151 rescue
148 end
152 end
149 end
153 end
150 paths
154 paths
151 rescue CommandFailed
155 rescue CommandFailed
152 paths
156 paths
153 end
157 end
154 end
158 end
155 end
159 end
156 end
160 end
157 end
161 end
@@ -1,60 +1,60
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19 require 'pp'
19 require 'pp'
20 class RepositoryCvsTest < Test::Unit::TestCase
20 class RepositoryCvsTest < Test::Unit::TestCase
21 fixtures :projects
21 fixtures :projects
22
22
23 # No '..' in the repository path
23 # No '..' in the repository path
24 REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/cvs_repository'
24 REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/cvs_repository'
25 REPOSITORY_PATH.gsub!(/\//, "\\") if RUBY_PLATFORM =~ /mswin/
25 REPOSITORY_PATH.gsub!(/\//, "\\") if RUBY_PLATFORM =~ /mswin/
26 # CVS module
26 # CVS module
27 MODULE_NAME = 'test'
27 MODULE_NAME = 'test'
28
28
29 def setup
29 def setup
30 @project = Project.find(1)
30 @project = Project.find(1)
31 assert @repository = Repository::Cvs.create(:project => @project,
31 assert @repository = Repository::Cvs.create(:project => @project,
32 :root_url => REPOSITORY_PATH,
32 :root_url => REPOSITORY_PATH,
33 :url => MODULE_NAME)
33 :url => MODULE_NAME)
34 end
34 end
35
35
36 if File.directory?(REPOSITORY_PATH)
36 if File.directory?(REPOSITORY_PATH)
37 def test_fetch_changesets_from_scratch
37 def test_fetch_changesets_from_scratch
38 @repository.fetch_changesets
38 @repository.fetch_changesets
39 @repository.reload
39 @repository.reload
40
40
41 assert_equal 5, @repository.changesets.count
41 assert_equal 5, @repository.changesets.count
42 assert_equal 14, @repository.changes.count
42 assert_equal 14, @repository.changes.count
43 assert_equal 'Two files changed', @repository.changesets.find_by_revision(3).comments
43 assert_not_nil @repository.changesets.find_by_comments('Two files changed')
44 end
44 end
45
45
46 def test_fetch_changesets_incremental
46 def test_fetch_changesets_incremental
47 @repository.fetch_changesets
47 @repository.fetch_changesets
48 # Remove changesets with revision > 2
48 # Remove the 3 latest changesets
49 @repository.changesets.find(:all, :conditions => 'revision > 2').each(&:destroy)
49 @repository.changesets.find(:all, :order => 'committed_on DESC', :limit => 3).each(&:destroy)
50 @repository.reload
50 @repository.reload
51 assert_equal 2, @repository.changesets.count
51 assert_equal 2, @repository.changesets.count
52
52
53 @repository.fetch_changesets
53 @repository.fetch_changesets
54 assert_equal 5, @repository.changesets.count
54 assert_equal 5, @repository.changesets.count
55 end
55 end
56 else
56 else
57 puts "CVS test repository NOT FOUND. Skipping unit tests !!!"
57 puts "CVS test repository NOT FOUND. Skipping unit tests !!!"
58 def test_fake; assert true end
58 def test_fake; assert true end
59 end
59 end
60 end
60 end
General Comments 0
You need to be logged in to leave comments. Login now