##// END OF EJS Templates
Fixed: view file at given revision with CVS....
Jean-Philippe Lang -
r1539:ca6e69ec247e
parent child
Show More
@@ -1,324 +1,324
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; end
22 class ChangesetNotFound < Exception; end
23 class InvalidRevisionParam < Exception; end
23 class InvalidRevisionParam < Exception; 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 if @repository
37 @repository.project = @project if @repository
38 end
38 end
39 if request.post? && @repository
39 if request.post? && @repository
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 # root entries
54 # root entries
55 @entries = @repository.entries('', @rev)
55 @entries = @repository.entries('', @rev)
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 and return unless @entries
68 show_error_not_found and return unless @entries
69 render :action => 'browse'
69 render :action => 'browse'
70 end
70 end
71 rescue Redmine::Scm::Adapters::CommandFailed => e
71 rescue Redmine::Scm::Adapters::CommandFailed => e
72 show_error_command_failed(e.message)
72 show_error_command_failed(e.message)
73 end
73 end
74
74
75 def changes
75 def changes
76 @entry = @repository.scm.entry(@path, @rev)
76 @entry = @repository.entry(@path, @rev)
77 show_error_not_found and return unless @entry
77 show_error_not_found and return unless @entry
78 @changesets = @repository.changesets_for_path(@path)
78 @changesets = @repository.changesets_for_path(@path)
79 rescue Redmine::Scm::Adapters::CommandFailed => e
79 rescue Redmine::Scm::Adapters::CommandFailed => e
80 show_error_command_failed(e.message)
80 show_error_command_failed(e.message)
81 end
81 end
82
82
83 def revisions
83 def revisions
84 @changeset_count = @repository.changesets.count
84 @changeset_count = @repository.changesets.count
85 @changeset_pages = Paginator.new self, @changeset_count,
85 @changeset_pages = Paginator.new self, @changeset_count,
86 per_page_option,
86 per_page_option,
87 params['page']
87 params['page']
88 @changesets = @repository.changesets.find(:all,
88 @changesets = @repository.changesets.find(:all,
89 :limit => @changeset_pages.items_per_page,
89 :limit => @changeset_pages.items_per_page,
90 :offset => @changeset_pages.current.offset)
90 :offset => @changeset_pages.current.offset)
91
91
92 respond_to do |format|
92 respond_to do |format|
93 format.html { render :layout => false if request.xhr? }
93 format.html { render :layout => false if request.xhr? }
94 format.atom { render_feed(@changesets, :title => "#{@project.name}: #{l(:label_revision_plural)}") }
94 format.atom { render_feed(@changesets, :title => "#{@project.name}: #{l(:label_revision_plural)}") }
95 end
95 end
96 end
96 end
97
97
98 def entry
98 def entry
99 @entry = @repository.scm.entry(@path, @rev)
99 @entry = @repository.entry(@path, @rev)
100 show_error_not_found and return unless @entry
100 show_error_not_found and return unless @entry
101
101
102 # If the entry is a dir, show the browser
102 # If the entry is a dir, show the browser
103 browse and return if @entry.is_dir?
103 browse and return if @entry.is_dir?
104
104
105 @content = @repository.scm.cat(@path, @rev)
105 @content = @repository.cat(@path, @rev)
106 show_error_not_found and return unless @content
106 show_error_not_found and return unless @content
107 if 'raw' == params[:format] || @content.is_binary_data?
107 if 'raw' == params[:format] || @content.is_binary_data?
108 # Force the download if it's a binary file
108 # Force the download if it's a binary file
109 send_data @content, :filename => @path.split('/').last
109 send_data @content, :filename => @path.split('/').last
110 else
110 else
111 # Prevent empty lines when displaying a file with Windows style eol
111 # Prevent empty lines when displaying a file with Windows style eol
112 @content.gsub!("\r\n", "\n")
112 @content.gsub!("\r\n", "\n")
113 end
113 end
114 rescue Redmine::Scm::Adapters::CommandFailed => e
114 rescue Redmine::Scm::Adapters::CommandFailed => e
115 show_error_command_failed(e.message)
115 show_error_command_failed(e.message)
116 end
116 end
117
117
118 def annotate
118 def annotate
119 @annotate = @repository.scm.annotate(@path, @rev)
119 @annotate = @repository.scm.annotate(@path, @rev)
120 render_error l(:error_scm_annotate) and return if @annotate.nil? || @annotate.empty?
120 render_error l(:error_scm_annotate) and return if @annotate.nil? || @annotate.empty?
121 rescue Redmine::Scm::Adapters::CommandFailed => e
121 rescue Redmine::Scm::Adapters::CommandFailed => e
122 show_error_command_failed(e.message)
122 show_error_command_failed(e.message)
123 end
123 end
124
124
125 def revision
125 def revision
126 @changeset = @repository.changesets.find_by_revision(@rev)
126 @changeset = @repository.changesets.find_by_revision(@rev)
127 raise ChangesetNotFound unless @changeset
127 raise ChangesetNotFound unless @changeset
128 @changes_count = @changeset.changes.size
128 @changes_count = @changeset.changes.size
129 @changes_pages = Paginator.new self, @changes_count, 150, params['page']
129 @changes_pages = Paginator.new self, @changes_count, 150, params['page']
130 @changes = @changeset.changes.find(:all,
130 @changes = @changeset.changes.find(:all,
131 :limit => @changes_pages.items_per_page,
131 :limit => @changes_pages.items_per_page,
132 :offset => @changes_pages.current.offset)
132 :offset => @changes_pages.current.offset)
133
133
134 respond_to do |format|
134 respond_to do |format|
135 format.html
135 format.html
136 format.js {render :layout => false}
136 format.js {render :layout => false}
137 end
137 end
138 rescue ChangesetNotFound
138 rescue ChangesetNotFound
139 show_error_not_found
139 show_error_not_found
140 rescue Redmine::Scm::Adapters::CommandFailed => e
140 rescue Redmine::Scm::Adapters::CommandFailed => e
141 show_error_command_failed(e.message)
141 show_error_command_failed(e.message)
142 end
142 end
143
143
144 def diff
144 def diff
145 if params[:format] == 'diff'
145 if params[:format] == 'diff'
146 @diff = @repository.diff(@path, @rev, @rev_to)
146 @diff = @repository.diff(@path, @rev, @rev_to)
147 show_error_not_found and return unless @diff
147 show_error_not_found and return unless @diff
148 filename = "changeset_r#{@rev}"
148 filename = "changeset_r#{@rev}"
149 filename << "_r#{@rev_to}" if @rev_to
149 filename << "_r#{@rev_to}" if @rev_to
150 send_data @diff.join, :filename => "#{filename}.diff",
150 send_data @diff.join, :filename => "#{filename}.diff",
151 :type => 'text/x-patch',
151 :type => 'text/x-patch',
152 :disposition => 'attachment'
152 :disposition => 'attachment'
153 else
153 else
154 @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
154 @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
155 @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
155 @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
156
156
157 # Save diff type as user preference
157 # Save diff type as user preference
158 if User.current.logged? && @diff_type != User.current.pref[:diff_type]
158 if User.current.logged? && @diff_type != User.current.pref[:diff_type]
159 User.current.pref[:diff_type] = @diff_type
159 User.current.pref[:diff_type] = @diff_type
160 User.current.preference.save
160 User.current.preference.save
161 end
161 end
162
162
163 @cache_key = "repositories/diff/#{@repository.id}/" + Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}")
163 @cache_key = "repositories/diff/#{@repository.id}/" + Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}")
164 unless read_fragment(@cache_key)
164 unless read_fragment(@cache_key)
165 @diff = @repository.diff(@path, @rev, @rev_to)
165 @diff = @repository.diff(@path, @rev, @rev_to)
166 show_error_not_found unless @diff
166 show_error_not_found unless @diff
167 end
167 end
168 end
168 end
169 rescue Redmine::Scm::Adapters::CommandFailed => e
169 rescue Redmine::Scm::Adapters::CommandFailed => e
170 show_error_command_failed(e.message)
170 show_error_command_failed(e.message)
171 end
171 end
172
172
173 def stats
173 def stats
174 end
174 end
175
175
176 def graph
176 def graph
177 data = nil
177 data = nil
178 case params[:graph]
178 case params[:graph]
179 when "commits_per_month"
179 when "commits_per_month"
180 data = graph_commits_per_month(@repository)
180 data = graph_commits_per_month(@repository)
181 when "commits_per_author"
181 when "commits_per_author"
182 data = graph_commits_per_author(@repository)
182 data = graph_commits_per_author(@repository)
183 end
183 end
184 if data
184 if data
185 headers["Content-Type"] = "image/svg+xml"
185 headers["Content-Type"] = "image/svg+xml"
186 send_data(data, :type => "image/svg+xml", :disposition => "inline")
186 send_data(data, :type => "image/svg+xml", :disposition => "inline")
187 else
187 else
188 render_404
188 render_404
189 end
189 end
190 end
190 end
191
191
192 private
192 private
193 def find_project
193 def find_project
194 @project = Project.find(params[:id])
194 @project = Project.find(params[:id])
195 rescue ActiveRecord::RecordNotFound
195 rescue ActiveRecord::RecordNotFound
196 render_404
196 render_404
197 end
197 end
198
198
199 REV_PARAM_RE = %r{^[a-f0-9]*$}
199 REV_PARAM_RE = %r{^[a-f0-9]*$}
200
200
201 def find_repository
201 def find_repository
202 @project = Project.find(params[:id])
202 @project = Project.find(params[:id])
203 @repository = @project.repository
203 @repository = @project.repository
204 render_404 and return false unless @repository
204 render_404 and return false unless @repository
205 @path = params[:path].join('/') unless params[:path].nil?
205 @path = params[:path].join('/') unless params[:path].nil?
206 @path ||= ''
206 @path ||= ''
207 @rev = params[:rev]
207 @rev = params[:rev]
208 @rev_to = params[:rev_to]
208 @rev_to = params[:rev_to]
209 raise InvalidRevisionParam unless @rev.to_s.match(REV_PARAM_RE) && @rev.to_s.match(REV_PARAM_RE)
209 raise InvalidRevisionParam unless @rev.to_s.match(REV_PARAM_RE) && @rev.to_s.match(REV_PARAM_RE)
210 rescue ActiveRecord::RecordNotFound
210 rescue ActiveRecord::RecordNotFound
211 render_404
211 render_404
212 rescue InvalidRevisionParam
212 rescue InvalidRevisionParam
213 show_error_not_found
213 show_error_not_found
214 end
214 end
215
215
216 def show_error_not_found
216 def show_error_not_found
217 render_error l(:error_scm_not_found)
217 render_error l(:error_scm_not_found)
218 end
218 end
219
219
220 def show_error_command_failed(msg)
220 def show_error_command_failed(msg)
221 render_error l(:error_scm_command_failed, msg)
221 render_error l(:error_scm_command_failed, msg)
222 end
222 end
223
223
224 def graph_commits_per_month(repository)
224 def graph_commits_per_month(repository)
225 @date_to = Date.today
225 @date_to = Date.today
226 @date_from = @date_to << 11
226 @date_from = @date_to << 11
227 @date_from = Date.civil(@date_from.year, @date_from.month, 1)
227 @date_from = Date.civil(@date_from.year, @date_from.month, 1)
228 commits_by_day = repository.changesets.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
228 commits_by_day = repository.changesets.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
229 commits_by_month = [0] * 12
229 commits_by_month = [0] * 12
230 commits_by_day.each {|c| commits_by_month[c.first.to_date.months_ago] += c.last }
230 commits_by_day.each {|c| commits_by_month[c.first.to_date.months_ago] += c.last }
231
231
232 changes_by_day = repository.changes.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
232 changes_by_day = repository.changes.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
233 changes_by_month = [0] * 12
233 changes_by_month = [0] * 12
234 changes_by_day.each {|c| changes_by_month[c.first.to_date.months_ago] += c.last }
234 changes_by_day.each {|c| changes_by_month[c.first.to_date.months_ago] += c.last }
235
235
236 fields = []
236 fields = []
237 month_names = l(:actionview_datehelper_select_month_names_abbr).split(',')
237 month_names = l(:actionview_datehelper_select_month_names_abbr).split(',')
238 12.times {|m| fields << month_names[((Date.today.month - 1 - m) % 12)]}
238 12.times {|m| fields << month_names[((Date.today.month - 1 - m) % 12)]}
239
239
240 graph = SVG::Graph::Bar.new(
240 graph = SVG::Graph::Bar.new(
241 :height => 300,
241 :height => 300,
242 :width => 500,
242 :width => 500,
243 :fields => fields.reverse,
243 :fields => fields.reverse,
244 :stack => :side,
244 :stack => :side,
245 :scale_integers => true,
245 :scale_integers => true,
246 :step_x_labels => 2,
246 :step_x_labels => 2,
247 :show_data_values => false,
247 :show_data_values => false,
248 :graph_title => l(:label_commits_per_month),
248 :graph_title => l(:label_commits_per_month),
249 :show_graph_title => true
249 :show_graph_title => true
250 )
250 )
251
251
252 graph.add_data(
252 graph.add_data(
253 :data => commits_by_month[0..11].reverse,
253 :data => commits_by_month[0..11].reverse,
254 :title => l(:label_revision_plural)
254 :title => l(:label_revision_plural)
255 )
255 )
256
256
257 graph.add_data(
257 graph.add_data(
258 :data => changes_by_month[0..11].reverse,
258 :data => changes_by_month[0..11].reverse,
259 :title => l(:label_change_plural)
259 :title => l(:label_change_plural)
260 )
260 )
261
261
262 graph.burn
262 graph.burn
263 end
263 end
264
264
265 def graph_commits_per_author(repository)
265 def graph_commits_per_author(repository)
266 commits_by_author = repository.changesets.count(:all, :group => :committer)
266 commits_by_author = repository.changesets.count(:all, :group => :committer)
267 commits_by_author.sort! {|x, y| x.last <=> y.last}
267 commits_by_author.sort! {|x, y| x.last <=> y.last}
268
268
269 changes_by_author = repository.changes.count(:all, :group => :committer)
269 changes_by_author = repository.changes.count(:all, :group => :committer)
270 h = changes_by_author.inject({}) {|o, i| o[i.first] = i.last; o}
270 h = changes_by_author.inject({}) {|o, i| o[i.first] = i.last; o}
271
271
272 fields = commits_by_author.collect {|r| r.first}
272 fields = commits_by_author.collect {|r| r.first}
273 commits_data = commits_by_author.collect {|r| r.last}
273 commits_data = commits_by_author.collect {|r| r.last}
274 changes_data = commits_by_author.collect {|r| h[r.first] || 0}
274 changes_data = commits_by_author.collect {|r| h[r.first] || 0}
275
275
276 fields = fields + [""]*(10 - fields.length) if fields.length<10
276 fields = fields + [""]*(10 - fields.length) if fields.length<10
277 commits_data = commits_data + [0]*(10 - commits_data.length) if commits_data.length<10
277 commits_data = commits_data + [0]*(10 - commits_data.length) if commits_data.length<10
278 changes_data = changes_data + [0]*(10 - changes_data.length) if changes_data.length<10
278 changes_data = changes_data + [0]*(10 - changes_data.length) if changes_data.length<10
279
279
280 # Remove email adress in usernames
280 # Remove email adress in usernames
281 fields = fields.collect {|c| c.gsub(%r{<.+@.+>}, '') }
281 fields = fields.collect {|c| c.gsub(%r{<.+@.+>}, '') }
282
282
283 graph = SVG::Graph::BarHorizontal.new(
283 graph = SVG::Graph::BarHorizontal.new(
284 :height => 300,
284 :height => 300,
285 :width => 500,
285 :width => 500,
286 :fields => fields,
286 :fields => fields,
287 :stack => :side,
287 :stack => :side,
288 :scale_integers => true,
288 :scale_integers => true,
289 :show_data_values => false,
289 :show_data_values => false,
290 :rotate_y_labels => false,
290 :rotate_y_labels => false,
291 :graph_title => l(:label_commits_per_author),
291 :graph_title => l(:label_commits_per_author),
292 :show_graph_title => true
292 :show_graph_title => true
293 )
293 )
294
294
295 graph.add_data(
295 graph.add_data(
296 :data => commits_data,
296 :data => commits_data,
297 :title => l(:label_revision_plural)
297 :title => l(:label_revision_plural)
298 )
298 )
299
299
300 graph.add_data(
300 graph.add_data(
301 :data => changes_data,
301 :data => changes_data,
302 :title => l(:label_change_plural)
302 :title => l(:label_change_plural)
303 )
303 )
304
304
305 graph.burn
305 graph.burn
306 end
306 end
307
307
308 end
308 end
309
309
310 class Date
310 class Date
311 def months_ago(date = Date.today)
311 def months_ago(date = Date.today)
312 (date.year - self.year)*12 + (date.month - self.month)
312 (date.year - self.year)*12 + (date.month - self.month)
313 end
313 end
314
314
315 def weeks_ago(date = Date.today)
315 def weeks_ago(date = Date.today)
316 (date.year - self.year)*52 + (date.cweek - self.cweek)
316 (date.year - self.year)*52 + (date.cweek - self.cweek)
317 end
317 end
318 end
318 end
319
319
320 class String
320 class String
321 def with_leading_slash
321 def with_leading_slash
322 starts_with?('/') ? self : "/#{self}"
322 starts_with?('/') ? self : "/#{self}"
323 end
323 end
324 end
324 end
@@ -1,118 +1,126
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}.committed_on DESC, #{Changeset.table_name}.id 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 # Checks if the SCM is enabled when creating a repository
23 # Checks if the SCM is enabled when creating a repository
24 validate_on_create { |r| r.errors.add(:type, :activerecord_error_invalid) unless Setting.enabled_scm.include?(r.class.name.demodulize) }
24 validate_on_create { |r| r.errors.add(:type, :activerecord_error_invalid) unless Setting.enabled_scm.include?(r.class.name.demodulize) }
25
25
26 # Removes leading and trailing whitespace
26 # Removes leading and trailing whitespace
27 def url=(arg)
27 def url=(arg)
28 write_attribute(:url, arg ? arg.to_s.strip : nil)
28 write_attribute(:url, arg ? arg.to_s.strip : nil)
29 end
29 end
30
30
31 # Removes leading and trailing whitespace
31 # Removes leading and trailing whitespace
32 def root_url=(arg)
32 def root_url=(arg)
33 write_attribute(:root_url, arg ? arg.to_s.strip : nil)
33 write_attribute(:root_url, arg ? arg.to_s.strip : nil)
34 end
34 end
35
35
36 def scm
36 def scm
37 @scm ||= self.scm_adapter.new url, root_url, login, password
37 @scm ||= self.scm_adapter.new url, root_url, login, password
38 update_attribute(:root_url, @scm.root_url) if root_url.blank?
38 update_attribute(:root_url, @scm.root_url) if root_url.blank?
39 @scm
39 @scm
40 end
40 end
41
41
42 def scm_name
42 def scm_name
43 self.class.scm_name
43 self.class.scm_name
44 end
44 end
45
45
46 def supports_cat?
46 def supports_cat?
47 scm.supports_cat?
47 scm.supports_cat?
48 end
48 end
49
49
50 def supports_annotate?
50 def supports_annotate?
51 scm.supports_annotate?
51 scm.supports_annotate?
52 end
52 end
53
53
54 def entry(path=nil, identifier=nil)
55 scm.entry(path, identifier)
56 end
57
54 def entries(path=nil, identifier=nil)
58 def entries(path=nil, identifier=nil)
55 scm.entries(path, identifier)
59 scm.entries(path, identifier)
56 end
60 end
57
61
62 def cat(path, identifier=nil)
63 scm.cat(path, identifier)
64 end
65
58 def diff(path, rev, rev_to)
66 def diff(path, rev, rev_to)
59 scm.diff(path, rev, rev_to)
67 scm.diff(path, rev, rev_to)
60 end
68 end
61
69
62 # Default behaviour: we search in cached changesets
70 # Default behaviour: we search in cached changesets
63 def changesets_for_path(path)
71 def changesets_for_path(path)
64 path = "/#{path}" unless path.starts_with?('/')
72 path = "/#{path}" unless path.starts_with?('/')
65 Change.find(:all, :include => :changeset,
73 Change.find(:all, :include => :changeset,
66 :conditions => ["repository_id = ? AND path = ?", id, path],
74 :conditions => ["repository_id = ? AND path = ?", id, path],
67 :order => "committed_on DESC, #{Changeset.table_name}.id DESC").collect(&:changeset)
75 :order => "committed_on DESC, #{Changeset.table_name}.id DESC").collect(&:changeset)
68 end
76 end
69
77
70 # Returns a path relative to the url of the repository
78 # Returns a path relative to the url of the repository
71 def relative_path(path)
79 def relative_path(path)
72 path
80 path
73 end
81 end
74
82
75 def latest_changeset
83 def latest_changeset
76 @latest_changeset ||= changesets.find(:first)
84 @latest_changeset ||= changesets.find(:first)
77 end
85 end
78
86
79 def scan_changesets_for_issue_ids
87 def scan_changesets_for_issue_ids
80 self.changesets.each(&:scan_comment_for_issue_ids)
88 self.changesets.each(&:scan_comment_for_issue_ids)
81 end
89 end
82
90
83 # fetch new changesets for all repositories
91 # fetch new changesets for all repositories
84 # can be called periodically by an external script
92 # can be called periodically by an external script
85 # eg. ruby script/runner "Repository.fetch_changesets"
93 # eg. ruby script/runner "Repository.fetch_changesets"
86 def self.fetch_changesets
94 def self.fetch_changesets
87 find(:all).each(&:fetch_changesets)
95 find(:all).each(&:fetch_changesets)
88 end
96 end
89
97
90 # scan changeset comments to find related and fixed issues for all repositories
98 # scan changeset comments to find related and fixed issues for all repositories
91 def self.scan_changesets_for_issue_ids
99 def self.scan_changesets_for_issue_ids
92 find(:all).each(&:scan_changesets_for_issue_ids)
100 find(:all).each(&:scan_changesets_for_issue_ids)
93 end
101 end
94
102
95 def self.scm_name
103 def self.scm_name
96 'Abstract'
104 'Abstract'
97 end
105 end
98
106
99 def self.available_scm
107 def self.available_scm
100 subclasses.collect {|klass| [klass.scm_name, klass.name]}
108 subclasses.collect {|klass| [klass.scm_name, klass.name]}
101 end
109 end
102
110
103 def self.factory(klass_name, *args)
111 def self.factory(klass_name, *args)
104 klass = "Repository::#{klass_name}".constantize
112 klass = "Repository::#{klass_name}".constantize
105 klass.new(*args)
113 klass.new(*args)
106 rescue
114 rescue
107 nil
115 nil
108 end
116 end
109
117
110 private
118 private
111
119
112 def before_save
120 def before_save
113 # Strips url and root_url
121 # Strips url and root_url
114 url.strip!
122 url.strip!
115 root_url.strip!
123 root_url.strip!
116 true
124 true
117 end
125 end
118 end
126 end
@@ -1,156 +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/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=nil, identifier=nil)
33 e = entries(path, identifier)
33 rev = identifier.nil? ? nil : changesets.find_by_revision(identifier)
34 e ? e.first : nil
34 scm.entry(path, rev.nil? ? nil : rev.committed_on)
35 end
35 end
36
36
37 def entries(path=nil, identifier=nil)
37 def entries(path=nil, identifier=nil)
38 rev = identifier.nil? ? nil : changesets.find_by_revision(identifier)
38 rev = identifier.nil? ? nil : changesets.find_by_revision(identifier)
39 entries = scm.entries(path, rev.nil? ? nil : rev.committed_on)
39 entries = scm.entries(path, rev.nil? ? nil : rev.committed_on)
40 if entries
40 if entries
41 entries.each() do |entry|
41 entries.each() do |entry|
42 unless entry.lastrev.nil? || entry.lastrev.identifier
42 unless entry.lastrev.nil? || entry.lastrev.identifier
43 change=changes.find_by_revision_and_path( entry.lastrev.revision, scm.with_leading_slash(entry.path) )
43 change=changes.find_by_revision_and_path( entry.lastrev.revision, scm.with_leading_slash(entry.path) )
44 if change
44 if change
45 entry.lastrev.identifier=change.changeset.revision
45 entry.lastrev.identifier=change.changeset.revision
46 entry.lastrev.author=change.changeset.committer
46 entry.lastrev.author=change.changeset.committer
47 entry.lastrev.revision=change.revision
47 entry.lastrev.revision=change.revision
48 entry.lastrev.branch=change.branch
48 entry.lastrev.branch=change.branch
49 end
49 end
50 end
50 end
51 end
51 end
52 end
52 end
53 entries
53 entries
54 end
54 end
55
55
56 def cat(path, identifier=nil)
57 rev = identifier.nil? ? nil : changesets.find_by_revision(identifier)
58 scm.cat(path, rev.nil? ? nil : rev.committed_on)
59 end
60
56 def diff(path, rev, rev_to)
61 def diff(path, rev, rev_to)
57 #convert rev to revision. CVS can't handle changesets here
62 #convert rev to revision. CVS can't handle changesets here
58 diff=[]
63 diff=[]
59 changeset_from=changesets.find_by_revision(rev)
64 changeset_from=changesets.find_by_revision(rev)
60 if rev_to.to_i > 0
65 if rev_to.to_i > 0
61 changeset_to=changesets.find_by_revision(rev_to)
66 changeset_to=changesets.find_by_revision(rev_to)
62 end
67 end
63 changeset_from.changes.each() do |change_from|
68 changeset_from.changes.each() do |change_from|
64
69
65 revision_from=nil
70 revision_from=nil
66 revision_to=nil
71 revision_to=nil
67
72
68 revision_from=change_from.revision if path.nil? || (change_from.path.starts_with? scm.with_leading_slash(path))
73 revision_from=change_from.revision if path.nil? || (change_from.path.starts_with? scm.with_leading_slash(path))
69
74
70 if revision_from
75 if revision_from
71 if changeset_to
76 if changeset_to
72 changeset_to.changes.each() do |change_to|
77 changeset_to.changes.each() do |change_to|
73 revision_to=change_to.revision if change_to.path==change_from.path
78 revision_to=change_to.revision if change_to.path==change_from.path
74 end
79 end
75 end
80 end
76 unless revision_to
81 unless revision_to
77 revision_to=scm.get_previous_revision(revision_from)
82 revision_to=scm.get_previous_revision(revision_from)
78 end
83 end
79 file_diff = scm.diff(change_from.path, revision_from, revision_to)
84 file_diff = scm.diff(change_from.path, revision_from, revision_to)
80 diff = diff + file_diff unless file_diff.nil?
85 diff = diff + file_diff unless file_diff.nil?
81 end
86 end
82 end
87 end
83 return diff
88 return diff
84 end
89 end
85
90
86 def fetch_changesets
91 def fetch_changesets
87 # some nifty bits to introduce a commit-id with cvs
92 # some nifty bits to introduce a commit-id with cvs
88 # natively cvs doesn't provide any kind of changesets, there is only a revision per file.
93 # natively cvs doesn't provide any kind of changesets, there is only a revision per file.
89 # we now take a guess using the author, the commitlog and the commit-date.
94 # we now take a guess using the author, the commitlog and the commit-date.
90
95
91 # last one is the next step to take. the commit-date is not equal for all
96 # last one is the next step to take. the commit-date is not equal for all
92 # commits in one changeset. cvs update the commit-date when the *,v file was touched. so
97 # commits in one changeset. cvs update the commit-date when the *,v file was touched. so
93 # we use a small delta here, to merge all changes belonging to _one_ changeset
98 # we use a small delta here, to merge all changes belonging to _one_ changeset
94 time_delta=10.seconds
99 time_delta=10.seconds
95
100
96 fetch_since = latest_changeset ? latest_changeset.committed_on : nil
101 fetch_since = latest_changeset ? latest_changeset.committed_on : nil
97 transaction do
102 transaction do
98 tmp_rev_num = 1
103 tmp_rev_num = 1
99 scm.revisions('', fetch_since, nil, :with_paths => true) do |revision|
104 scm.revisions('', fetch_since, nil, :with_paths => true) do |revision|
100 # only add the change to the database, if it doen't exists. the cvs log
105 # only add the change to the database, if it doen't exists. the cvs log
101 # is not exclusive at all.
106 # is not exclusive at all.
102 unless changes.find_by_path_and_revision(scm.with_leading_slash(revision.paths[0][:path]), revision.paths[0][:revision])
107 unless changes.find_by_path_and_revision(scm.with_leading_slash(revision.paths[0][:path]), revision.paths[0][:revision])
103 revision
108 revision
104 cs = changesets.find(:first, :conditions=>{
109 cs = changesets.find(:first, :conditions=>{
105 :committed_on=>revision.time-time_delta..revision.time+time_delta,
110 :committed_on=>revision.time-time_delta..revision.time+time_delta,
106 :committer=>revision.author,
111 :committer=>revision.author,
107 :comments=>revision.message
112 :comments=>revision.message
108 })
113 })
109
114
110 # create a new changeset....
115 # create a new changeset....
111 unless cs
116 unless cs
112 # we use a temporaray revision number here (just for inserting)
117 # we use a temporaray revision number here (just for inserting)
113 # later on, we calculate a continous positive number
118 # later on, we calculate a continous positive number
114 latest = changesets.find(:first, :order => 'id DESC')
119 latest = changesets.find(:first, :order => 'id DESC')
115 cs = Changeset.create(:repository => self,
120 cs = Changeset.create(:repository => self,
116 :revision => "_#{tmp_rev_num}",
121 :revision => "_#{tmp_rev_num}",
117 :committer => revision.author,
122 :committer => revision.author,
118 :committed_on => revision.time,
123 :committed_on => revision.time,
119 :comments => revision.message)
124 :comments => revision.message)
120 tmp_rev_num += 1
125 tmp_rev_num += 1
121 end
126 end
122
127
123 #convert CVS-File-States to internal Action-abbrevations
128 #convert CVS-File-States to internal Action-abbrevations
124 #default action is (M)odified
129 #default action is (M)odified
125 action="M"
130 action="M"
126 if revision.paths[0][:action]=="Exp" && revision.paths[0][:revision]=="1.1"
131 if revision.paths[0][:action]=="Exp" && revision.paths[0][:revision]=="1.1"
127 action="A" #add-action always at first revision (= 1.1)
132 action="A" #add-action always at first revision (= 1.1)
128 elsif revision.paths[0][:action]=="dead"
133 elsif revision.paths[0][:action]=="dead"
129 action="D" #dead-state is similar to Delete
134 action="D" #dead-state is similar to Delete
130 end
135 end
131
136
132 Change.create(:changeset => cs,
137 Change.create(:changeset => cs,
133 :action => action,
138 :action => action,
134 :path => scm.with_leading_slash(revision.paths[0][:path]),
139 :path => scm.with_leading_slash(revision.paths[0][:path]),
135 :revision => revision.paths[0][:revision],
140 :revision => revision.paths[0][:revision],
136 :branch => revision.paths[0][:branch]
141 :branch => revision.paths[0][:branch]
137 )
142 )
138 end
143 end
139 end
144 end
140
145
141 # Renumber new changesets in chronological order
146 # Renumber new changesets in chronological order
142 changesets.find(:all, :order => 'committed_on ASC, id ASC', :conditions => "revision LIKE '_%'").each do |changeset|
147 changesets.find(:all, :order => 'committed_on ASC, id ASC', :conditions => "revision LIKE '_%'").each do |changeset|
143 changeset.update_attribute :revision, next_revision_number
148 changeset.update_attribute :revision, next_revision_number
144 end
149 end
145 end # transaction
150 end # transaction
146 end
151 end
147
152
148 private
153 private
149
154
150 # Returns the next revision number to assign to a CVS changeset
155 # Returns the next revision number to assign to a CVS changeset
151 def next_revision_number
156 def next_revision_number
152 # Need to retrieve existing revision numbers to sort them as integers
157 # Need to retrieve existing revision numbers to sort them as integers
153 @current_revision_number ||= (connection.select_values("SELECT revision FROM #{Changeset.table_name} WHERE repository_id = #{id} AND revision NOT LIKE '_%'").collect(&:to_i).max || 0)
158 @current_revision_number ||= (connection.select_values("SELECT revision FROM #{Changeset.table_name} WHERE repository_id = #{id} AND revision NOT LIKE '_%'").collect(&:to_i).max || 0)
154 @current_revision_number += 1
159 @current_revision_number += 1
155 end
160 end
156 end
161 end
@@ -1,354 +1,356
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
19
20 module Redmine
20 module Redmine
21 module Scm
21 module Scm
22 module Adapters
22 module Adapters
23 class CvsAdapter < AbstractAdapter
23 class CvsAdapter < AbstractAdapter
24
24
25 # CVS executable name
25 # CVS executable name
26 CVS_BIN = "cvs"
26 CVS_BIN = "cvs"
27
27
28 # Guidelines for the input:
28 # Guidelines for the input:
29 # url -> the project-path, relative to the cvsroot (eg. module name)
29 # url -> the project-path, relative to the cvsroot (eg. module name)
30 # root_url -> the good old, sometimes damned, CVSROOT
30 # root_url -> the good old, sometimes damned, CVSROOT
31 # login -> unnecessary
31 # login -> unnecessary
32 # password -> unnecessary too
32 # password -> unnecessary too
33 def initialize(url, root_url=nil, login=nil, password=nil)
33 def initialize(url, root_url=nil, login=nil, password=nil)
34 @url = url
34 @url = url
35 @login = login if login && !login.empty?
35 @login = login if login && !login.empty?
36 @password = (password || "") if @login
36 @password = (password || "") if @login
37 #TODO: better Exception here (IllegalArgumentException)
37 #TODO: better Exception here (IllegalArgumentException)
38 raise CommandFailed if root_url.blank?
38 raise CommandFailed if root_url.blank?
39 @root_url = root_url
39 @root_url = root_url
40 end
40 end
41
41
42 def root_url
42 def root_url
43 @root_url
43 @root_url
44 end
44 end
45
45
46 def url
46 def url
47 @url
47 @url
48 end
48 end
49
49
50 def info
50 def info
51 logger.debug "<cvs> info"
51 logger.debug "<cvs> info"
52 Info.new({:root_url => @root_url, :lastrev => nil})
52 Info.new({:root_url => @root_url, :lastrev => nil})
53 end
53 end
54
54
55 def get_previous_revision(revision)
55 def get_previous_revision(revision)
56 CvsRevisionHelper.new(revision).prevRev
56 CvsRevisionHelper.new(revision).prevRev
57 end
57 end
58
58
59 # Returns an Entries collection
59 # Returns an Entries collection
60 # or nil if the given path doesn't exist in the repository
60 # or nil if the given path doesn't exist in the repository
61 # this method is used by the repository-browser (aka LIST)
61 # this method is used by the repository-browser (aka LIST)
62 def entries(path=nil, identifier=nil)
62 def entries(path=nil, identifier=nil)
63 logger.debug "<cvs> entries '#{path}' with identifier '#{identifier}'"
63 logger.debug "<cvs> entries '#{path}' with identifier '#{identifier}'"
64 path_with_project="#{url}#{with_leading_slash(path)}"
64 path_with_project="#{url}#{with_leading_slash(path)}"
65 entries = Entries.new
65 entries = Entries.new
66 cmd = "#{CVS_BIN} -d #{root_url} rls -ed"
66 cmd = "#{CVS_BIN} -d #{root_url} rls -ed"
67 cmd << " -D \"#{time_to_cvstime(identifier)}\"" if identifier
67 cmd << " -D \"#{time_to_cvstime(identifier)}\"" if identifier
68 cmd << " #{shell_quote path_with_project}"
68 cmd << " #{shell_quote path_with_project}"
69 shellout(cmd) do |io|
69 shellout(cmd) do |io|
70 io.each_line(){|line|
70 io.each_line(){|line|
71 fields=line.chop.split('/',-1)
71 fields=line.chop.split('/',-1)
72 logger.debug(">>InspectLine #{fields.inspect}")
72 logger.debug(">>InspectLine #{fields.inspect}")
73
73
74 if fields[0]!="D"
74 if fields[0]!="D"
75 entries << Entry.new({:name => fields[-5],
75 entries << Entry.new({:name => fields[-5],
76 #:path => fields[-4].include?(path)?fields[-4]:(path + "/"+ fields[-4]),
76 #:path => fields[-4].include?(path)?fields[-4]:(path + "/"+ fields[-4]),
77 :path => "#{path}/#{fields[-5]}",
77 :path => "#{path}/#{fields[-5]}",
78 :kind => 'file',
78 :kind => 'file',
79 :size => nil,
79 :size => nil,
80 :lastrev => Revision.new({
80 :lastrev => Revision.new({
81 :revision => fields[-4],
81 :revision => fields[-4],
82 :name => fields[-4],
82 :name => fields[-4],
83 :time => Time.parse(fields[-3]),
83 :time => Time.parse(fields[-3]),
84 :author => ''
84 :author => ''
85 })
85 })
86 })
86 })
87 else
87 else
88 entries << Entry.new({:name => fields[1],
88 entries << Entry.new({:name => fields[1],
89 :path => "#{path}/#{fields[1]}",
89 :path => "#{path}/#{fields[1]}",
90 :kind => 'dir',
90 :kind => 'dir',
91 :size => nil,
91 :size => nil,
92 :lastrev => nil
92 :lastrev => nil
93 })
93 })
94 end
94 end
95 }
95 }
96 end
96 end
97 return nil if $? && $?.exitstatus != 0
97 return nil if $? && $?.exitstatus != 0
98 entries.sort_by_name
98 entries.sort_by_name
99 end
99 end
100
100
101 STARTLOG="----------------------------"
101 STARTLOG="----------------------------"
102 ENDLOG ="============================================================================="
102 ENDLOG ="============================================================================="
103
103
104 # Returns all revisions found between identifier_from and identifier_to
104 # Returns all revisions found between identifier_from and identifier_to
105 # in the repository. both identifier have to be dates or nil.
105 # in the repository. both identifier have to be dates or nil.
106 # these method returns nothing but yield every result in block
106 # these method returns nothing but yield every result in block
107 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}, &block)
107 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}, &block)
108 logger.debug "<cvs> revisions path:'#{path}',identifier_from #{identifier_from}, identifier_to #{identifier_to}"
108 logger.debug "<cvs> revisions path:'#{path}',identifier_from #{identifier_from}, identifier_to #{identifier_to}"
109
109
110 path_with_project="#{url}#{with_leading_slash(path)}"
110 path_with_project="#{url}#{with_leading_slash(path)}"
111 cmd = "#{CVS_BIN} -d #{root_url} rlog"
111 cmd = "#{CVS_BIN} -d #{root_url} rlog"
112 cmd << " -d\">#{time_to_cvstime(identifier_from)}\"" if identifier_from
112 cmd << " -d\">#{time_to_cvstime(identifier_from)}\"" if identifier_from
113 cmd << " #{shell_quote path_with_project}"
113 cmd << " #{shell_quote path_with_project}"
114 shellout(cmd) do |io|
114 shellout(cmd) do |io|
115 state="entry_start"
115 state="entry_start"
116
116
117 commit_log=String.new
117 commit_log=String.new
118 revision=nil
118 revision=nil
119 date=nil
119 date=nil
120 author=nil
120 author=nil
121 entry_path=nil
121 entry_path=nil
122 entry_name=nil
122 entry_name=nil
123 file_state=nil
123 file_state=nil
124 branch_map=nil
124 branch_map=nil
125
125
126 io.each_line() do |line|
126 io.each_line() do |line|
127
127
128 if state!="revision" && /^#{ENDLOG}/ =~ line
128 if state!="revision" && /^#{ENDLOG}/ =~ line
129 commit_log=String.new
129 commit_log=String.new
130 revision=nil
130 revision=nil
131 state="entry_start"
131 state="entry_start"
132 end
132 end
133
133
134 if state=="entry_start"
134 if state=="entry_start"
135 branch_map=Hash.new
135 branch_map=Hash.new
136 # gsub(/^:.*@[^:]+:\d*/, '') is here to remove :pserver:anonymous@foo.bar: string if present in the url
136 # gsub(/^:.*@[^:]+:\d*/, '') is here to remove :pserver:anonymous@foo.bar: string if present in the url
137 if /^RCS file: #{Regexp.escape(root_url.gsub(/^:.*@[^:]+:\d*/, ''))}\/#{Regexp.escape(path_with_project)}(.+),v$/ =~ line
137 if /^RCS file: #{Regexp.escape(root_url.gsub(/^:.*@[^:]+:\d*/, ''))}\/#{Regexp.escape(path_with_project)}(.+),v$/ =~ line
138 entry_path = normalize_cvs_path($1)
138 entry_path = normalize_cvs_path($1)
139 entry_name = normalize_path(File.basename($1))
139 entry_name = normalize_path(File.basename($1))
140 logger.debug("Path #{entry_path} <=> Name #{entry_name}")
140 logger.debug("Path #{entry_path} <=> Name #{entry_name}")
141 elsif /^head: (.+)$/ =~ line
141 elsif /^head: (.+)$/ =~ line
142 entry_headRev = $1 #unless entry.nil?
142 entry_headRev = $1 #unless entry.nil?
143 elsif /^symbolic names:/ =~ line
143 elsif /^symbolic names:/ =~ line
144 state="symbolic" #unless entry.nil?
144 state="symbolic" #unless entry.nil?
145 elsif /^#{STARTLOG}/ =~ line
145 elsif /^#{STARTLOG}/ =~ line
146 commit_log=String.new
146 commit_log=String.new
147 state="revision"
147 state="revision"
148 end
148 end
149 next
149 next
150 elsif state=="symbolic"
150 elsif state=="symbolic"
151 if /^(.*):\s(.*)/ =~ (line.strip)
151 if /^(.*):\s(.*)/ =~ (line.strip)
152 branch_map[$1]=$2
152 branch_map[$1]=$2
153 else
153 else
154 state="tags"
154 state="tags"
155 next
155 next
156 end
156 end
157 elsif state=="tags"
157 elsif state=="tags"
158 if /^#{STARTLOG}/ =~ line
158 if /^#{STARTLOG}/ =~ line
159 commit_log = ""
159 commit_log = ""
160 state="revision"
160 state="revision"
161 elsif /^#{ENDLOG}/ =~ line
161 elsif /^#{ENDLOG}/ =~ line
162 state="head"
162 state="head"
163 end
163 end
164 next
164 next
165 elsif state=="revision"
165 elsif state=="revision"
166 if /^#{ENDLOG}/ =~ line || /^#{STARTLOG}/ =~ line
166 if /^#{ENDLOG}/ =~ line || /^#{STARTLOG}/ =~ line
167 if revision
167 if revision
168
168
169 revHelper=CvsRevisionHelper.new(revision)
169 revHelper=CvsRevisionHelper.new(revision)
170 revBranch="HEAD"
170 revBranch="HEAD"
171
171
172 branch_map.each() do |branch_name,branch_point|
172 branch_map.each() do |branch_name,branch_point|
173 if revHelper.is_in_branch_with_symbol(branch_point)
173 if revHelper.is_in_branch_with_symbol(branch_point)
174 revBranch=branch_name
174 revBranch=branch_name
175 end
175 end
176 end
176 end
177
177
178 logger.debug("********** YIELD Revision #{revision}::#{revBranch}")
178 logger.debug("********** YIELD Revision #{revision}::#{revBranch}")
179
179
180 yield Revision.new({
180 yield Revision.new({
181 :time => date,
181 :time => date,
182 :author => author,
182 :author => author,
183 :message=>commit_log.chomp,
183 :message=>commit_log.chomp,
184 :paths => [{
184 :paths => [{
185 :revision => revision,
185 :revision => revision,
186 :branch=> revBranch,
186 :branch=> revBranch,
187 :path=>entry_path,
187 :path=>entry_path,
188 :name=>entry_name,
188 :name=>entry_name,
189 :kind=>'file',
189 :kind=>'file',
190 :action=>file_state
190 :action=>file_state
191 }]
191 }]
192 })
192 })
193 end
193 end
194
194
195 commit_log=String.new
195 commit_log=String.new
196 revision=nil
196 revision=nil
197
197
198 if /^#{ENDLOG}/ =~ line
198 if /^#{ENDLOG}/ =~ line
199 state="entry_start"
199 state="entry_start"
200 end
200 end
201 next
201 next
202 end
202 end
203
203
204 if /^branches: (.+)$/ =~ line
204 if /^branches: (.+)$/ =~ line
205 #TODO: version.branch = $1
205 #TODO: version.branch = $1
206 elsif /^revision (\d+(?:\.\d+)+).*$/ =~ line
206 elsif /^revision (\d+(?:\.\d+)+).*$/ =~ line
207 revision = $1
207 revision = $1
208 elsif /^date:\s+(\d+.\d+.\d+\s+\d+:\d+:\d+)/ =~ line
208 elsif /^date:\s+(\d+.\d+.\d+\s+\d+:\d+:\d+)/ =~ line
209 date = Time.parse($1)
209 date = Time.parse($1)
210 author = /author: ([^;]+)/.match(line)[1]
210 author = /author: ([^;]+)/.match(line)[1]
211 file_state = /state: ([^;]+)/.match(line)[1]
211 file_state = /state: ([^;]+)/.match(line)[1]
212 #TODO: linechanges only available in CVS.... maybe a feature our SVN implementation. i'm sure, they are
212 #TODO: linechanges only available in CVS.... maybe a feature our SVN implementation. i'm sure, they are
213 # useful for stats or something else
213 # useful for stats or something else
214 # linechanges =/lines: \+(\d+) -(\d+)/.match(line)
214 # linechanges =/lines: \+(\d+) -(\d+)/.match(line)
215 # unless linechanges.nil?
215 # unless linechanges.nil?
216 # version.line_plus = linechanges[1]
216 # version.line_plus = linechanges[1]
217 # version.line_minus = linechanges[2]
217 # version.line_minus = linechanges[2]
218 # else
218 # else
219 # version.line_plus = 0
219 # version.line_plus = 0
220 # version.line_minus = 0
220 # version.line_minus = 0
221 # end
221 # end
222 else
222 else
223 commit_log << line unless line =~ /^\*\*\* empty log message \*\*\*/
223 commit_log << line unless line =~ /^\*\*\* empty log message \*\*\*/
224 end
224 end
225 end
225 end
226 end
226 end
227 end
227 end
228 end
228 end
229
229
230 def diff(path, identifier_from, identifier_to=nil)
230 def diff(path, identifier_from, identifier_to=nil)
231 logger.debug "<cvs> diff path:'#{path}',identifier_from #{identifier_from}, identifier_to #{identifier_to}"
231 logger.debug "<cvs> diff path:'#{path}',identifier_from #{identifier_from}, identifier_to #{identifier_to}"
232 path_with_project="#{url}#{with_leading_slash(path)}"
232 path_with_project="#{url}#{with_leading_slash(path)}"
233 cmd = "#{CVS_BIN} -d #{root_url} rdiff -u -r#{identifier_to} -r#{identifier_from} #{shell_quote path_with_project}"
233 cmd = "#{CVS_BIN} -d #{root_url} rdiff -u -r#{identifier_to} -r#{identifier_from} #{shell_quote path_with_project}"
234 diff = []
234 diff = []
235 shellout(cmd) do |io|
235 shellout(cmd) do |io|
236 io.each_line do |line|
236 io.each_line do |line|
237 diff << line
237 diff << line
238 end
238 end
239 end
239 end
240 return nil if $? && $?.exitstatus != 0
240 return nil if $? && $?.exitstatus != 0
241 diff
241 diff
242 end
242 end
243
243
244 def cat(path, identifier=nil)
244 def cat(path, identifier=nil)
245 identifier = (identifier) ? identifier : "HEAD"
245 identifier = (identifier) ? identifier : "HEAD"
246 logger.debug "<cvs> cat path:'#{path}',identifier #{identifier}"
246 logger.debug "<cvs> cat path:'#{path}',identifier #{identifier}"
247 path_with_project="#{url}#{with_leading_slash(path)}"
247 path_with_project="#{url}#{with_leading_slash(path)}"
248 cmd = "#{CVS_BIN} -d #{root_url} co -r#{identifier} -p #{shell_quote path_with_project}"
248 cmd = "#{CVS_BIN} -d #{root_url} co"
249 cmd << " -D \"#{time_to_cvstime(identifier)}\"" if identifier
250 cmd << " -p #{shell_quote path_with_project}"
249 cat = nil
251 cat = nil
250 shellout(cmd) do |io|
252 shellout(cmd) do |io|
251 cat = io.read
253 cat = io.read
252 end
254 end
253 return nil if $? && $?.exitstatus != 0
255 return nil if $? && $?.exitstatus != 0
254 cat
256 cat
255 end
257 end
256
258
257 def annotate(path, identifier=nil)
259 def annotate(path, identifier=nil)
258 identifier = (identifier) ? identifier : "HEAD"
260 identifier = (identifier) ? identifier : "HEAD"
259 logger.debug "<cvs> annotate path:'#{path}',identifier #{identifier}"
261 logger.debug "<cvs> annotate path:'#{path}',identifier #{identifier}"
260 path_with_project="#{url}#{with_leading_slash(path)}"
262 path_with_project="#{url}#{with_leading_slash(path)}"
261 cmd = "#{CVS_BIN} -d #{root_url} rannotate -r#{identifier} #{shell_quote path_with_project}"
263 cmd = "#{CVS_BIN} -d #{root_url} rannotate -r#{identifier} #{shell_quote path_with_project}"
262 blame = Annotate.new
264 blame = Annotate.new
263 shellout(cmd) do |io|
265 shellout(cmd) do |io|
264 io.each_line do |line|
266 io.each_line do |line|
265 next unless line =~ %r{^([\d\.]+)\s+\(([^\)]+)\s+[^\)]+\):\s(.*)$}
267 next unless line =~ %r{^([\d\.]+)\s+\(([^\)]+)\s+[^\)]+\):\s(.*)$}
266 blame.add_line($3.rstrip, Revision.new(:revision => $1, :author => $2.strip))
268 blame.add_line($3.rstrip, Revision.new(:revision => $1, :author => $2.strip))
267 end
269 end
268 end
270 end
269 return nil if $? && $?.exitstatus != 0
271 return nil if $? && $?.exitstatus != 0
270 blame
272 blame
271 end
273 end
272
274
273 private
275 private
274
276
275 # convert a date/time into the CVS-format
277 # convert a date/time into the CVS-format
276 def time_to_cvstime(time)
278 def time_to_cvstime(time)
277 return nil if time.nil?
279 return nil if time.nil?
278 unless time.kind_of? Time
280 unless time.kind_of? Time
279 time = Time.parse(time)
281 time = Time.parse(time)
280 end
282 end
281 return time.strftime("%Y-%m-%d %H:%M:%S")
283 return time.strftime("%Y-%m-%d %H:%M:%S")
282 end
284 end
283
285
284 def normalize_cvs_path(path)
286 def normalize_cvs_path(path)
285 normalize_path(path.gsub(/Attic\//,''))
287 normalize_path(path.gsub(/Attic\//,''))
286 end
288 end
287
289
288 def normalize_path(path)
290 def normalize_path(path)
289 path.sub(/^(\/)*(.*)/,'\2').sub(/(.*)(,v)+/,'\1')
291 path.sub(/^(\/)*(.*)/,'\2').sub(/(.*)(,v)+/,'\1')
290 end
292 end
291 end
293 end
292
294
293 class CvsRevisionHelper
295 class CvsRevisionHelper
294 attr_accessor :complete_rev, :revision, :base, :branchid
296 attr_accessor :complete_rev, :revision, :base, :branchid
295
297
296 def initialize(complete_rev)
298 def initialize(complete_rev)
297 @complete_rev = complete_rev
299 @complete_rev = complete_rev
298 parseRevision()
300 parseRevision()
299 end
301 end
300
302
301 def branchPoint
303 def branchPoint
302 return @base
304 return @base
303 end
305 end
304
306
305 def branchVersion
307 def branchVersion
306 if isBranchRevision
308 if isBranchRevision
307 return @base+"."+@branchid
309 return @base+"."+@branchid
308 end
310 end
309 return @base
311 return @base
310 end
312 end
311
313
312 def isBranchRevision
314 def isBranchRevision
313 !@branchid.nil?
315 !@branchid.nil?
314 end
316 end
315
317
316 def prevRev
318 def prevRev
317 unless @revision==0
319 unless @revision==0
318 return buildRevision(@revision-1)
320 return buildRevision(@revision-1)
319 end
321 end
320 return buildRevision(@revision)
322 return buildRevision(@revision)
321 end
323 end
322
324
323 def is_in_branch_with_symbol(branch_symbol)
325 def is_in_branch_with_symbol(branch_symbol)
324 bpieces=branch_symbol.split(".")
326 bpieces=branch_symbol.split(".")
325 branch_start="#{bpieces[0..-3].join(".")}.#{bpieces[-1]}"
327 branch_start="#{bpieces[0..-3].join(".")}.#{bpieces[-1]}"
326 return (branchVersion==branch_start)
328 return (branchVersion==branch_start)
327 end
329 end
328
330
329 private
331 private
330 def buildRevision(rev)
332 def buildRevision(rev)
331 if rev== 0
333 if rev== 0
332 @base
334 @base
333 elsif @branchid.nil?
335 elsif @branchid.nil?
334 @base+"."+rev.to_s
336 @base+"."+rev.to_s
335 else
337 else
336 @base+"."+@branchid+"."+rev.to_s
338 @base+"."+@branchid+"."+rev.to_s
337 end
339 end
338 end
340 end
339
341
340 # Interpretiert die cvs revisionsnummern wie z.b. 1.14 oder 1.3.0.15
342 # Interpretiert die cvs revisionsnummern wie z.b. 1.14 oder 1.3.0.15
341 def parseRevision()
343 def parseRevision()
342 pieces=@complete_rev.split(".")
344 pieces=@complete_rev.split(".")
343 @revision=pieces.last.to_i
345 @revision=pieces.last.to_i
344 baseSize=1
346 baseSize=1
345 baseSize+=(pieces.size/2)
347 baseSize+=(pieces.size/2)
346 @base=pieces[0..-baseSize].join(".")
348 @base=pieces[0..-baseSize].join(".")
347 if baseSize > 2
349 if baseSize > 2
348 @branchid=pieces[-2]
350 @branchid=pieces[-2]
349 end
351 end
350 end
352 end
351 end
353 end
352 end
354 end
353 end
355 end
354 end
356 end
@@ -1,152 +1,165
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 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 'repositories_controller'
19 require 'repositories_controller'
20
20
21 # Re-raise errors caught by the controller.
21 # Re-raise errors caught by the controller.
22 class RepositoriesController; def rescue_action(e) raise e end; end
22 class RepositoriesController; def rescue_action(e) raise e end; end
23
23
24 class RepositoriesCvsControllerTest < Test::Unit::TestCase
24 class RepositoriesCvsControllerTest < Test::Unit::TestCase
25
25
26 # No '..' in the repository path
26 # No '..' in the repository path
27 REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/cvs_repository'
27 REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/cvs_repository'
28 REPOSITORY_PATH.gsub!(/\//, "\\") if RUBY_PLATFORM =~ /mswin/
28 REPOSITORY_PATH.gsub!(/\//, "\\") if RUBY_PLATFORM =~ /mswin/
29 # CVS module
29 # CVS module
30 MODULE_NAME = 'test'
30 MODULE_NAME = 'test'
31
31
32 def setup
32 def setup
33 @controller = RepositoriesController.new
33 @controller = RepositoriesController.new
34 @request = ActionController::TestRequest.new
34 @request = ActionController::TestRequest.new
35 @response = ActionController::TestResponse.new
35 @response = ActionController::TestResponse.new
36 Setting.default_language = 'en'
36 Setting.default_language = 'en'
37 User.current = nil
37 User.current = nil
38
38
39 @project = Project.find(1)
39 @project = Project.find(1)
40 @project.repository = Repository::Cvs.create(:root_url => REPOSITORY_PATH,
40 @project.repository = Repository::Cvs.create(:root_url => REPOSITORY_PATH,
41 :url => MODULE_NAME)
41 :url => MODULE_NAME)
42 end
42 end
43
43
44 if File.directory?(REPOSITORY_PATH)
44 if File.directory?(REPOSITORY_PATH)
45 def test_show
45 def test_show
46 get :show, :id => 1
46 get :show, :id => 1
47 assert_response :success
47 assert_response :success
48 assert_template 'show'
48 assert_template 'show'
49 assert_not_nil assigns(:entries)
49 assert_not_nil assigns(:entries)
50 assert_not_nil assigns(:changesets)
50 assert_not_nil assigns(:changesets)
51 end
51 end
52
52
53 def test_browse_root
53 def test_browse_root
54 get :browse, :id => 1
54 get :browse, :id => 1
55 assert_response :success
55 assert_response :success
56 assert_template 'browse'
56 assert_template 'browse'
57 assert_not_nil assigns(:entries)
57 assert_not_nil assigns(:entries)
58 assert_equal 3, assigns(:entries).size
58 assert_equal 3, assigns(:entries).size
59
59
60 entry = assigns(:entries).detect {|e| e.name == 'images'}
60 entry = assigns(:entries).detect {|e| e.name == 'images'}
61 assert_equal 'dir', entry.kind
61 assert_equal 'dir', entry.kind
62
62
63 entry = assigns(:entries).detect {|e| e.name == 'README'}
63 entry = assigns(:entries).detect {|e| e.name == 'README'}
64 assert_equal 'file', entry.kind
64 assert_equal 'file', entry.kind
65 end
65 end
66
66
67 def test_browse_directory
67 def test_browse_directory
68 get :browse, :id => 1, :path => ['images']
68 get :browse, :id => 1, :path => ['images']
69 assert_response :success
69 assert_response :success
70 assert_template 'browse'
70 assert_template 'browse'
71 assert_not_nil assigns(:entries)
71 assert_not_nil assigns(:entries)
72 assert_equal ['add.png', 'delete.png', 'edit.png'], assigns(:entries).collect(&:name)
72 assert_equal ['add.png', 'delete.png', 'edit.png'], assigns(:entries).collect(&:name)
73 entry = assigns(:entries).detect {|e| e.name == 'edit.png'}
73 entry = assigns(:entries).detect {|e| e.name == 'edit.png'}
74 assert_not_nil entry
74 assert_not_nil entry
75 assert_equal 'file', entry.kind
75 assert_equal 'file', entry.kind
76 assert_equal 'images/edit.png', entry.path
76 assert_equal 'images/edit.png', entry.path
77 end
77 end
78
78
79 def test_browse_at_given_revision
79 def test_browse_at_given_revision
80 Project.find(1).repository.fetch_changesets
80 Project.find(1).repository.fetch_changesets
81 get :browse, :id => 1, :path => ['images'], :rev => 1
81 get :browse, :id => 1, :path => ['images'], :rev => 1
82 assert_response :success
82 assert_response :success
83 assert_template 'browse'
83 assert_template 'browse'
84 assert_not_nil assigns(:entries)
84 assert_not_nil assigns(:entries)
85 assert_equal ['delete.png', 'edit.png'], assigns(:entries).collect(&:name)
85 assert_equal ['delete.png', 'edit.png'], assigns(:entries).collect(&:name)
86 end
86 end
87
87
88 def test_entry
88 def test_entry
89 get :entry, :id => 1, :path => ['sources', 'watchers_controller.rb']
89 get :entry, :id => 1, :path => ['sources', 'watchers_controller.rb']
90 assert_response :success
90 assert_response :success
91 assert_template 'entry'
91 assert_template 'entry'
92 assert_no_tag :tag => 'td', :attributes => { :class => /line-code/},
93 :content => /before_filter/
94 end
95
96 def test_entry_at_given_revision
97 # changesets must be loaded
98 Project.find(1).repository.fetch_changesets
99 get :entry, :id => 1, :path => ['sources', 'watchers_controller.rb'], :rev => 2
100 assert_response :success
101 assert_template 'entry'
102 # this line was removed in r3
103 assert_tag :tag => 'td', :attributes => { :class => /line-code/},
104 :content => /before_filter/
92 end
105 end
93
106
94 def test_entry_not_found
107 def test_entry_not_found
95 get :entry, :id => 1, :path => ['sources', 'zzz.c']
108 get :entry, :id => 1, :path => ['sources', 'zzz.c']
96 assert_tag :tag => 'div', :attributes => { :class => /error/ },
109 assert_tag :tag => 'div', :attributes => { :class => /error/ },
97 :content => /The entry or revision was not found in the repository/
110 :content => /The entry or revision was not found in the repository/
98 end
111 end
99
112
100 def test_entry_download
113 def test_entry_download
101 get :entry, :id => 1, :path => ['sources', 'watchers_controller.rb'], :format => 'raw'
114 get :entry, :id => 1, :path => ['sources', 'watchers_controller.rb'], :format => 'raw'
102 assert_response :success
115 assert_response :success
103 end
116 end
104
117
105 def test_directory_entry
118 def test_directory_entry
106 get :entry, :id => 1, :path => ['sources']
119 get :entry, :id => 1, :path => ['sources']
107 assert_response :success
120 assert_response :success
108 assert_template 'browse'
121 assert_template 'browse'
109 assert_not_nil assigns(:entry)
122 assert_not_nil assigns(:entry)
110 assert_equal 'sources', assigns(:entry).name
123 assert_equal 'sources', assigns(:entry).name
111 end
124 end
112
125
113 def test_diff
126 def test_diff
114 Project.find(1).repository.fetch_changesets
127 Project.find(1).repository.fetch_changesets
115 get :diff, :id => 1, :rev => 3, :type => 'inline'
128 get :diff, :id => 1, :rev => 3, :type => 'inline'
116 assert_response :success
129 assert_response :success
117 assert_template 'diff'
130 assert_template 'diff'
118 assert_tag :tag => 'td', :attributes => { :class => 'line-code diff_out' },
131 assert_tag :tag => 'td', :attributes => { :class => 'line-code diff_out' },
119 :content => /watched.remove_watcher/
132 :content => /watched.remove_watcher/
120 assert_tag :tag => 'td', :attributes => { :class => 'line-code diff_in' },
133 assert_tag :tag => 'td', :attributes => { :class => 'line-code diff_in' },
121 :content => /watched.remove_all_watcher/
134 :content => /watched.remove_all_watcher/
122 end
135 end
123
136
124 def test_annotate
137 def test_annotate
125 Project.find(1).repository.fetch_changesets
138 Project.find(1).repository.fetch_changesets
126 get :annotate, :id => 1, :path => ['sources', 'watchers_controller.rb']
139 get :annotate, :id => 1, :path => ['sources', 'watchers_controller.rb']
127 assert_response :success
140 assert_response :success
128 assert_template 'annotate'
141 assert_template 'annotate'
129 # 1.1 line
142 # 1.1 line
130 assert_tag :tag => 'th', :attributes => { :class => 'line-num' },
143 assert_tag :tag => 'th', :attributes => { :class => 'line-num' },
131 :content => '18',
144 :content => '18',
132 :sibling => { :tag => 'td', :attributes => { :class => 'revision' },
145 :sibling => { :tag => 'td', :attributes => { :class => 'revision' },
133 :content => /1.1/,
146 :content => /1.1/,
134 :sibling => { :tag => 'td', :attributes => { :class => 'author' },
147 :sibling => { :tag => 'td', :attributes => { :class => 'author' },
135 :content => /LANG/
148 :content => /LANG/
136 }
149 }
137 }
150 }
138 # 1.2 line
151 # 1.2 line
139 assert_tag :tag => 'th', :attributes => { :class => 'line-num' },
152 assert_tag :tag => 'th', :attributes => { :class => 'line-num' },
140 :content => '32',
153 :content => '32',
141 :sibling => { :tag => 'td', :attributes => { :class => 'revision' },
154 :sibling => { :tag => 'td', :attributes => { :class => 'revision' },
142 :content => /1.2/,
155 :content => /1.2/,
143 :sibling => { :tag => 'td', :attributes => { :class => 'author' },
156 :sibling => { :tag => 'td', :attributes => { :class => 'author' },
144 :content => /LANG/
157 :content => /LANG/
145 }
158 }
146 }
159 }
147 end
160 end
148 else
161 else
149 puts "CVS test repository NOT FOUND. Skipping functional tests !!!"
162 puts "CVS test repository NOT FOUND. Skipping functional tests !!!"
150 def test_fake; assert true end
163 def test_fake; assert true end
151 end
164 end
152 end
165 end
@@ -1,146 +1,155
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 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 'repositories_controller'
19 require 'repositories_controller'
20
20
21 # Re-raise errors caught by the controller.
21 # Re-raise errors caught by the controller.
22 class RepositoriesController; def rescue_action(e) raise e end; end
22 class RepositoriesController; def rescue_action(e) raise e end; end
23
23
24 class RepositoriesSubversionControllerTest < Test::Unit::TestCase
24 class RepositoriesSubversionControllerTest < Test::Unit::TestCase
25 fixtures :projects, :users, :roles, :members, :repositories, :issues, :issue_statuses, :changesets, :changes, :issue_categories, :enumerations, :custom_fields, :custom_values, :trackers
25 fixtures :projects, :users, :roles, :members, :repositories, :issues, :issue_statuses, :changesets, :changes, :issue_categories, :enumerations, :custom_fields, :custom_values, :trackers
26
26
27 # No '..' in the repository path for svn
27 # No '..' in the repository path for svn
28 REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/subversion_repository'
28 REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/subversion_repository'
29
29
30 def setup
30 def setup
31 @controller = RepositoriesController.new
31 @controller = RepositoriesController.new
32 @request = ActionController::TestRequest.new
32 @request = ActionController::TestRequest.new
33 @response = ActionController::TestResponse.new
33 @response = ActionController::TestResponse.new
34 Setting.default_language = 'en'
34 Setting.default_language = 'en'
35 User.current = nil
35 User.current = nil
36 end
36 end
37
37
38 if File.directory?(REPOSITORY_PATH)
38 if File.directory?(REPOSITORY_PATH)
39 def test_show
39 def test_show
40 get :show, :id => 1
40 get :show, :id => 1
41 assert_response :success
41 assert_response :success
42 assert_template 'show'
42 assert_template 'show'
43 assert_not_nil assigns(:entries)
43 assert_not_nil assigns(:entries)
44 assert_not_nil assigns(:changesets)
44 assert_not_nil assigns(:changesets)
45 end
45 end
46
46
47 def test_browse_root
47 def test_browse_root
48 get :browse, :id => 1
48 get :browse, :id => 1
49 assert_response :success
49 assert_response :success
50 assert_template 'browse'
50 assert_template 'browse'
51 assert_not_nil assigns(:entries)
51 assert_not_nil assigns(:entries)
52 entry = assigns(:entries).detect {|e| e.name == 'subversion_test'}
52 entry = assigns(:entries).detect {|e| e.name == 'subversion_test'}
53 assert_equal 'dir', entry.kind
53 assert_equal 'dir', entry.kind
54 end
54 end
55
55
56 def test_browse_directory
56 def test_browse_directory
57 get :browse, :id => 1, :path => ['subversion_test']
57 get :browse, :id => 1, :path => ['subversion_test']
58 assert_response :success
58 assert_response :success
59 assert_template 'browse'
59 assert_template 'browse'
60 assert_not_nil assigns(:entries)
60 assert_not_nil assigns(:entries)
61 assert_equal ['folder', '.project', 'helloworld.c', 'textfile.txt'], assigns(:entries).collect(&:name)
61 assert_equal ['folder', '.project', 'helloworld.c', 'textfile.txt'], assigns(:entries).collect(&:name)
62 entry = assigns(:entries).detect {|e| e.name == 'helloworld.c'}
62 entry = assigns(:entries).detect {|e| e.name == 'helloworld.c'}
63 assert_equal 'file', entry.kind
63 assert_equal 'file', entry.kind
64 assert_equal 'subversion_test/helloworld.c', entry.path
64 assert_equal 'subversion_test/helloworld.c', entry.path
65 end
65 end
66
66
67 def test_browse_at_given_revision
67 def test_browse_at_given_revision
68 get :browse, :id => 1, :path => ['subversion_test'], :rev => 4
68 get :browse, :id => 1, :path => ['subversion_test'], :rev => 4
69 assert_response :success
69 assert_response :success
70 assert_template 'browse'
70 assert_template 'browse'
71 assert_not_nil assigns(:entries)
71 assert_not_nil assigns(:entries)
72 assert_equal ['folder', '.project', 'helloworld.c', 'helloworld.rb', 'textfile.txt'], assigns(:entries).collect(&:name)
72 assert_equal ['folder', '.project', 'helloworld.c', 'helloworld.rb', 'textfile.txt'], assigns(:entries).collect(&:name)
73 end
73 end
74
74
75 def test_entry
75 def test_entry
76 get :entry, :id => 1, :path => ['subversion_test', 'helloworld.c']
76 get :entry, :id => 1, :path => ['subversion_test', 'helloworld.c']
77 assert_response :success
77 assert_response :success
78 assert_template 'entry'
78 assert_template 'entry'
79 end
79 end
80
80
81 def test_entry_at_given_revision
82 get :entry, :id => 1, :path => ['subversion_test', 'helloworld.rb'], :rev => 2
83 assert_response :success
84 assert_template 'entry'
85 # this line was removed in r3 and file was moved in r6
86 assert_tag :tag => 'td', :attributes => { :class => /line-code/},
87 :content => /Here's the code/
88 end
89
81 def test_entry_not_found
90 def test_entry_not_found
82 get :entry, :id => 1, :path => ['subversion_test', 'zzz.c']
91 get :entry, :id => 1, :path => ['subversion_test', 'zzz.c']
83 assert_tag :tag => 'div', :attributes => { :class => /error/ },
92 assert_tag :tag => 'div', :attributes => { :class => /error/ },
84 :content => /The entry or revision was not found in the repository/
93 :content => /The entry or revision was not found in the repository/
85 end
94 end
86
95
87 def test_entry_download
96 def test_entry_download
88 get :entry, :id => 1, :path => ['subversion_test', 'helloworld.c'], :format => 'raw'
97 get :entry, :id => 1, :path => ['subversion_test', 'helloworld.c'], :format => 'raw'
89 assert_response :success
98 assert_response :success
90 end
99 end
91
100
92 def test_directory_entry
101 def test_directory_entry
93 get :entry, :id => 1, :path => ['subversion_test', 'folder']
102 get :entry, :id => 1, :path => ['subversion_test', 'folder']
94 assert_response :success
103 assert_response :success
95 assert_template 'browse'
104 assert_template 'browse'
96 assert_not_nil assigns(:entry)
105 assert_not_nil assigns(:entry)
97 assert_equal 'folder', assigns(:entry).name
106 assert_equal 'folder', assigns(:entry).name
98 end
107 end
99
108
100 def test_revision
109 def test_revision
101 get :revision, :id => 1, :rev => 2
110 get :revision, :id => 1, :rev => 2
102 assert_response :success
111 assert_response :success
103 assert_template 'revision'
112 assert_template 'revision'
104 assert_tag :tag => 'tr',
113 assert_tag :tag => 'tr',
105 :child => { :tag => 'td',
114 :child => { :tag => 'td',
106 # link to the entry at rev 2
115 # link to the entry at rev 2
107 :child => { :tag => 'a', :attributes => {:href => 'repositories/entry/ecookbook/test/some/path/in/the/repo?rev=2'},
116 :child => { :tag => 'a', :attributes => {:href => 'repositories/entry/ecookbook/test/some/path/in/the/repo?rev=2'},
108 :content => %r{/test/some/path/in/the/repo} }
117 :content => %r{/test/some/path/in/the/repo} }
109 },
118 },
110 :child => { :tag => 'td',
119 :child => { :tag => 'td',
111 # link to partial diff
120 # link to partial diff
112 :child => { :tag => 'a', :attributes => { :href => '/repositories/diff/ecookbook/test/some/path/in/the/repo?rev=2' } }
121 :child => { :tag => 'a', :attributes => { :href => '/repositories/diff/ecookbook/test/some/path/in/the/repo?rev=2' } }
113 }
122 }
114 end
123 end
115
124
116 def test_revision_with_repository_pointing_to_a_subdirectory
125 def test_revision_with_repository_pointing_to_a_subdirectory
117 r = Project.find(1).repository
126 r = Project.find(1).repository
118 # Changes repository url to a subdirectory
127 # Changes repository url to a subdirectory
119 r.update_attribute :url, (r.url + '/test/some')
128 r.update_attribute :url, (r.url + '/test/some')
120
129
121 get :revision, :id => 1, :rev => 2
130 get :revision, :id => 1, :rev => 2
122 assert_response :success
131 assert_response :success
123 assert_template 'revision'
132 assert_template 'revision'
124 assert_tag :tag => 'tr',
133 assert_tag :tag => 'tr',
125 :child => { :tag => 'td', :content => %r{/test/some/path/in/the/repo} },
134 :child => { :tag => 'td', :content => %r{/test/some/path/in/the/repo} },
126 :child => { :tag => 'td',
135 :child => { :tag => 'td',
127 :child => { :tag => 'a', :attributes => { :href => '/repositories/diff/ecookbook/path/in/the/repo?rev=2' } }
136 :child => { :tag => 'a', :attributes => { :href => '/repositories/diff/ecookbook/path/in/the/repo?rev=2' } }
128 }
137 }
129 end
138 end
130
139
131 def test_diff
140 def test_diff
132 get :diff, :id => 1, :rev => 3
141 get :diff, :id => 1, :rev => 3
133 assert_response :success
142 assert_response :success
134 assert_template 'diff'
143 assert_template 'diff'
135 end
144 end
136
145
137 def test_annotate
146 def test_annotate
138 get :annotate, :id => 1, :path => ['subversion_test', 'helloworld.c']
147 get :annotate, :id => 1, :path => ['subversion_test', 'helloworld.c']
139 assert_response :success
148 assert_response :success
140 assert_template 'annotate'
149 assert_template 'annotate'
141 end
150 end
142 else
151 else
143 puts "Subversion test repository NOT FOUND. Skipping functional tests !!!"
152 puts "Subversion test repository NOT FOUND. Skipping functional tests !!!"
144 def test_fake; assert true end
153 def test_fake; assert true end
145 end
154 end
146 end
155 end
General Comments 0
You need to be logged in to leave comments. Login now