##// END OF EJS Templates
Display svn properties in the browser, svn >= 1.5.0 only (#1581)....
Jean-Philippe Lang -
r1613:12fbd06c02d4
parent child
Show More
@@ -0,0 +1,33
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 'mkmf'
19
20 require File.dirname(__FILE__) + '/../test_helper'
21
22 class SubversionAdapterTest < Test::Unit::TestCase
23
24 if find_executable0('svn')
25 def test_client_version
26 v = Redmine::Scm::Adapters::SubversionAdapter.client_version
27 assert v.is_a?(Array)
28 end
29 else
30 puts "Subversion binary NOT FOUND. Skipping unit tests !!!"
31 def test_fake; assert true end
32 end
33 end
@@ -1,313 +1,315
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'SVG/Graph/Bar'
19 19 require 'SVG/Graph/BarHorizontal'
20 20 require 'digest/sha1'
21 21
22 22 class ChangesetNotFound < Exception; end
23 23 class InvalidRevisionParam < Exception; end
24 24
25 25 class RepositoriesController < ApplicationController
26 26 layout 'base'
27 27 menu_item :repository
28 28 before_filter :find_repository, :except => :edit
29 29 before_filter :find_project, :only => :edit
30 30 before_filter :authorize
31 31 accept_key_auth :revisions
32 32
33 33 rescue_from Redmine::Scm::Adapters::CommandFailed, :with => :show_error_command_failed
34 34
35 35 def edit
36 36 @repository = @project.repository
37 37 if !@repository
38 38 @repository = Repository.factory(params[:repository_scm])
39 39 @repository.project = @project if @repository
40 40 end
41 41 if request.post? && @repository
42 42 @repository.attributes = params[:repository]
43 43 @repository.save
44 44 end
45 45 render(:update) {|page| page.replace_html "tab-content-repository", :partial => 'projects/settings/repository'}
46 46 end
47 47
48 48 def destroy
49 49 @repository.destroy
50 50 redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'repository'
51 51 end
52 52
53 53 def show
54 54 # check if new revisions have been committed in the repository
55 55 @repository.fetch_changesets if Setting.autofetch_changesets?
56 56 # root entries
57 57 @entries = @repository.entries('', @rev)
58 58 # latest changesets
59 59 @changesets = @repository.changesets.find(:all, :limit => 10, :order => "committed_on DESC")
60 60 show_error_not_found unless @entries || @changesets.any?
61 61 end
62 62
63 63 def browse
64 64 @entries = @repository.entries(@path, @rev)
65 65 if request.xhr?
66 66 @entries ? render(:partial => 'dir_list_content') : render(:nothing => true)
67 67 else
68 68 show_error_not_found and return unless @entries
69 @properties = @repository.properties(@path, @rev)
69 70 render :action => 'browse'
70 71 end
71 72 end
72 73
73 74 def changes
74 75 @entry = @repository.entry(@path, @rev)
75 76 show_error_not_found and return unless @entry
76 77 @changesets = @repository.changesets_for_path(@path)
78 @properties = @repository.properties(@path, @rev)
77 79 end
78 80
79 81 def revisions
80 82 @changeset_count = @repository.changesets.count
81 83 @changeset_pages = Paginator.new self, @changeset_count,
82 84 per_page_option,
83 85 params['page']
84 86 @changesets = @repository.changesets.find(:all,
85 87 :limit => @changeset_pages.items_per_page,
86 88 :offset => @changeset_pages.current.offset)
87 89
88 90 respond_to do |format|
89 91 format.html { render :layout => false if request.xhr? }
90 92 format.atom { render_feed(@changesets, :title => "#{@project.name}: #{l(:label_revision_plural)}") }
91 93 end
92 94 end
93 95
94 96 def entry
95 97 @entry = @repository.entry(@path, @rev)
96 98 show_error_not_found and return unless @entry
97 99
98 100 # If the entry is a dir, show the browser
99 101 browse and return if @entry.is_dir?
100 102
101 103 @content = @repository.cat(@path, @rev)
102 104 show_error_not_found and return unless @content
103 105 if 'raw' == params[:format] || @content.is_binary_data?
104 106 # Force the download if it's a binary file
105 107 send_data @content, :filename => @path.split('/').last
106 108 else
107 109 # Prevent empty lines when displaying a file with Windows style eol
108 110 @content.gsub!("\r\n", "\n")
109 end
111 end
110 112 end
111 113
112 114 def annotate
113 115 @annotate = @repository.scm.annotate(@path, @rev)
114 116 render_error l(:error_scm_annotate) and return if @annotate.nil? || @annotate.empty?
115 117 end
116 118
117 119 def revision
118 120 @changeset = @repository.changesets.find_by_revision(@rev)
119 121 raise ChangesetNotFound unless @changeset
120 122 @changes_count = @changeset.changes.size
121 123 @changes_pages = Paginator.new self, @changes_count, 150, params['page']
122 124 @changes = @changeset.changes.find(:all,
123 125 :limit => @changes_pages.items_per_page,
124 126 :offset => @changes_pages.current.offset)
125 127
126 128 respond_to do |format|
127 129 format.html
128 130 format.js {render :layout => false}
129 131 end
130 132 rescue ChangesetNotFound
131 133 show_error_not_found
132 134 end
133 135
134 136 def diff
135 137 if params[:format] == 'diff'
136 138 @diff = @repository.diff(@path, @rev, @rev_to)
137 139 show_error_not_found and return unless @diff
138 140 filename = "changeset_r#{@rev}"
139 141 filename << "_r#{@rev_to}" if @rev_to
140 142 send_data @diff.join, :filename => "#{filename}.diff",
141 143 :type => 'text/x-patch',
142 144 :disposition => 'attachment'
143 145 else
144 146 @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
145 147 @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
146 148
147 149 # Save diff type as user preference
148 150 if User.current.logged? && @diff_type != User.current.pref[:diff_type]
149 151 User.current.pref[:diff_type] = @diff_type
150 152 User.current.preference.save
151 153 end
152 154
153 155 @cache_key = "repositories/diff/#{@repository.id}/" + Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}")
154 156 unless read_fragment(@cache_key)
155 157 @diff = @repository.diff(@path, @rev, @rev_to)
156 158 show_error_not_found unless @diff
157 159 end
158 160 end
159 161 end
160 162
161 163 def stats
162 164 end
163 165
164 166 def graph
165 167 data = nil
166 168 case params[:graph]
167 169 when "commits_per_month"
168 170 data = graph_commits_per_month(@repository)
169 171 when "commits_per_author"
170 172 data = graph_commits_per_author(@repository)
171 173 end
172 174 if data
173 175 headers["Content-Type"] = "image/svg+xml"
174 176 send_data(data, :type => "image/svg+xml", :disposition => "inline")
175 177 else
176 178 render_404
177 179 end
178 180 end
179 181
180 182 private
181 183 def find_project
182 184 @project = Project.find(params[:id])
183 185 rescue ActiveRecord::RecordNotFound
184 186 render_404
185 187 end
186 188
187 189 REV_PARAM_RE = %r{^[a-f0-9]*$}
188 190
189 191 def find_repository
190 192 @project = Project.find(params[:id])
191 193 @repository = @project.repository
192 194 render_404 and return false unless @repository
193 195 @path = params[:path].join('/') unless params[:path].nil?
194 196 @path ||= ''
195 197 @rev = params[:rev]
196 198 @rev_to = params[:rev_to]
197 199 raise InvalidRevisionParam unless @rev.to_s.match(REV_PARAM_RE) && @rev.to_s.match(REV_PARAM_RE)
198 200 rescue ActiveRecord::RecordNotFound
199 201 render_404
200 202 rescue InvalidRevisionParam
201 203 show_error_not_found
202 204 end
203 205
204 206 def show_error_not_found
205 207 render_error l(:error_scm_not_found)
206 208 end
207 209
208 210 # Handler for Redmine::Scm::Adapters::CommandFailed exception
209 211 def show_error_command_failed(exception)
210 212 render_error l(:error_scm_command_failed, exception.message)
211 213 end
212 214
213 215 def graph_commits_per_month(repository)
214 216 @date_to = Date.today
215 217 @date_from = @date_to << 11
216 218 @date_from = Date.civil(@date_from.year, @date_from.month, 1)
217 219 commits_by_day = repository.changesets.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
218 220 commits_by_month = [0] * 12
219 221 commits_by_day.each {|c| commits_by_month[c.first.to_date.months_ago] += c.last }
220 222
221 223 changes_by_day = repository.changes.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
222 224 changes_by_month = [0] * 12
223 225 changes_by_day.each {|c| changes_by_month[c.first.to_date.months_ago] += c.last }
224 226
225 227 fields = []
226 228 month_names = l(:actionview_datehelper_select_month_names_abbr).split(',')
227 229 12.times {|m| fields << month_names[((Date.today.month - 1 - m) % 12)]}
228 230
229 231 graph = SVG::Graph::Bar.new(
230 232 :height => 300,
231 233 :width => 800,
232 234 :fields => fields.reverse,
233 235 :stack => :side,
234 236 :scale_integers => true,
235 237 :step_x_labels => 2,
236 238 :show_data_values => false,
237 239 :graph_title => l(:label_commits_per_month),
238 240 :show_graph_title => true
239 241 )
240 242
241 243 graph.add_data(
242 244 :data => commits_by_month[0..11].reverse,
243 245 :title => l(:label_revision_plural)
244 246 )
245 247
246 248 graph.add_data(
247 249 :data => changes_by_month[0..11].reverse,
248 250 :title => l(:label_change_plural)
249 251 )
250 252
251 253 graph.burn
252 254 end
253 255
254 256 def graph_commits_per_author(repository)
255 257 commits_by_author = repository.changesets.count(:all, :group => :committer)
256 258 commits_by_author.sort! {|x, y| x.last <=> y.last}
257 259
258 260 changes_by_author = repository.changes.count(:all, :group => :committer)
259 261 h = changes_by_author.inject({}) {|o, i| o[i.first] = i.last; o}
260 262
261 263 fields = commits_by_author.collect {|r| r.first}
262 264 commits_data = commits_by_author.collect {|r| r.last}
263 265 changes_data = commits_by_author.collect {|r| h[r.first] || 0}
264 266
265 267 fields = fields + [""]*(10 - fields.length) if fields.length<10
266 268 commits_data = commits_data + [0]*(10 - commits_data.length) if commits_data.length<10
267 269 changes_data = changes_data + [0]*(10 - changes_data.length) if changes_data.length<10
268 270
269 271 # Remove email adress in usernames
270 272 fields = fields.collect {|c| c.gsub(%r{<.+@.+>}, '') }
271 273
272 274 graph = SVG::Graph::BarHorizontal.new(
273 275 :height => 400,
274 276 :width => 800,
275 277 :fields => fields,
276 278 :stack => :side,
277 279 :scale_integers => true,
278 280 :show_data_values => false,
279 281 :rotate_y_labels => false,
280 282 :graph_title => l(:label_commits_per_author),
281 283 :show_graph_title => true
282 284 )
283 285
284 286 graph.add_data(
285 287 :data => commits_data,
286 288 :title => l(:label_revision_plural)
287 289 )
288 290
289 291 graph.add_data(
290 292 :data => changes_data,
291 293 :title => l(:label_change_plural)
292 294 )
293 295
294 296 graph.burn
295 297 end
296 298
297 299 end
298 300
299 301 class Date
300 302 def months_ago(date = Date.today)
301 303 (date.year - self.year)*12 + (date.month - self.month)
302 304 end
303 305
304 306 def weeks_ago(date = Date.today)
305 307 (date.year - self.year)*52 + (date.cweek - self.cweek)
306 308 end
307 309 end
308 310
309 311 class String
310 312 def with_leading_slash
311 313 starts_with?('/') ? self : "/#{self}"
312 314 end
313 315 end
@@ -1,102 +1,112
1 1 # redMine - project management software
2 2 # Copyright (C) 2006 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'iconv'
19 19
20 20 module RepositoriesHelper
21 21 def format_revision(txt)
22 22 txt.to_s[0,8]
23 23 end
24 24
25 def render_properties(properties)
26 unless properties.nil? || properties.empty?
27 content = ''
28 properties.keys.sort.each do |property|
29 content << content_tag('li', "<b>#{h property}</b>: <span>#{h properties[property]}</span>")
30 end
31 content_tag('ul', content, :class => 'properties')
32 end
33 end
34
25 35 def to_path_param(path)
26 36 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
27 37 end
28 38
29 39 def to_utf8(str)
30 40 return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii
31 41 @encodings ||= Setting.repositories_encodings.split(',').collect(&:strip)
32 42 @encodings.each do |encoding|
33 43 begin
34 44 return Iconv.conv('UTF-8', encoding, str)
35 45 rescue Iconv::Failure
36 46 # do nothing here and try the next encoding
37 47 end
38 48 end
39 49 str
40 50 end
41 51
42 52 def repository_field_tags(form, repository)
43 53 method = repository.class.name.demodulize.underscore + "_field_tags"
44 54 send(method, form, repository) if repository.is_a?(Repository) && respond_to?(method)
45 55 end
46 56
47 57 def scm_select_tag(repository)
48 58 scm_options = [["--- #{l(:actionview_instancetag_blank_option)} ---", '']]
49 59 REDMINE_SUPPORTED_SCM.each do |scm|
50 60 scm_options << ["Repository::#{scm}".constantize.scm_name, scm] if Setting.enabled_scm.include?(scm) || (repository && repository.class.name.demodulize == scm)
51 61 end
52 62
53 63 select_tag('repository_scm',
54 64 options_for_select(scm_options, repository.class.name.demodulize),
55 65 :disabled => (repository && !repository.new_record?),
56 66 :onchange => remote_function(:url => { :controller => 'repositories', :action => 'edit', :id => @project }, :method => :get, :with => "Form.serialize(this.form)")
57 67 )
58 68 end
59 69
60 70 def with_leading_slash(path)
61 71 path.to_s.starts_with?('/') ? path : "/#{path}"
62 72 end
63 73
64 74 def without_leading_slash(path)
65 75 path.gsub(%r{^/+}, '')
66 76 end
67 77
68 78 def subversion_field_tags(form, repository)
69 79 content_tag('p', form.text_field(:url, :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)) +
70 80 '<br />(http://, https://, svn://, file:///)') +
71 81 content_tag('p', form.text_field(:login, :size => 30)) +
72 82 content_tag('p', form.password_field(:password, :size => 30, :name => 'ignore',
73 83 :value => ((repository.new_record? || repository.password.blank?) ? '' : ('x'*15)),
74 84 :onfocus => "this.value=''; this.name='repository[password]';",
75 85 :onchange => "this.name='repository[password]';"))
76 86 end
77 87
78 88 def darcs_field_tags(form, repository)
79 89 content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.new_record?)))
80 90 end
81 91
82 92 def mercurial_field_tags(form, repository)
83 93 content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)))
84 94 end
85 95
86 96 def git_field_tags(form, repository)
87 97 content_tag('p', form.text_field(:url, :label => 'Path to .git directory', :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)))
88 98 end
89 99
90 100 def cvs_field_tags(form, repository)
91 101 content_tag('p', form.text_field(:root_url, :label => 'CVSROOT', :size => 60, :required => true, :disabled => !repository.new_record?)) +
92 102 content_tag('p', form.text_field(:url, :label => 'Module', :size => 30, :required => true, :disabled => !repository.new_record?))
93 103 end
94 104
95 105 def bazaar_field_tags(form, repository)
96 106 content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.new_record?)))
97 107 end
98 108
99 109 def filesystem_field_tags(form, repository)
100 110 content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)))
101 111 end
102 112 end
@@ -1,126 +1,130
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Repository < ActiveRecord::Base
19 19 belongs_to :project
20 20 has_many :changesets, :dependent => :destroy, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC"
21 21 has_many :changes, :through => :changesets
22 22
23 23 # Checks if the SCM is enabled when creating a repository
24 24 validate_on_create { |r| r.errors.add(:type, :activerecord_error_invalid) unless Setting.enabled_scm.include?(r.class.name.demodulize) }
25 25
26 26 # Removes leading and trailing whitespace
27 27 def url=(arg)
28 28 write_attribute(:url, arg ? arg.to_s.strip : nil)
29 29 end
30 30
31 31 # Removes leading and trailing whitespace
32 32 def root_url=(arg)
33 33 write_attribute(:root_url, arg ? arg.to_s.strip : nil)
34 34 end
35 35
36 36 def scm
37 37 @scm ||= self.scm_adapter.new url, root_url, login, password
38 38 update_attribute(:root_url, @scm.root_url) if root_url.blank?
39 39 @scm
40 40 end
41 41
42 42 def scm_name
43 43 self.class.scm_name
44 44 end
45 45
46 46 def supports_cat?
47 47 scm.supports_cat?
48 48 end
49 49
50 50 def supports_annotate?
51 51 scm.supports_annotate?
52 52 end
53 53
54 54 def entry(path=nil, identifier=nil)
55 55 scm.entry(path, identifier)
56 56 end
57 57
58 58 def entries(path=nil, identifier=nil)
59 59 scm.entries(path, identifier)
60 60 end
61 61
62 def properties(path, identifier=nil)
63 scm.properties(path, identifier)
64 end
65
62 66 def cat(path, identifier=nil)
63 67 scm.cat(path, identifier)
64 68 end
65 69
66 70 def diff(path, rev, rev_to)
67 71 scm.diff(path, rev, rev_to)
68 72 end
69 73
70 74 # Default behaviour: we search in cached changesets
71 75 def changesets_for_path(path)
72 76 path = "/#{path}" unless path.starts_with?('/')
73 77 Change.find(:all, :include => :changeset,
74 78 :conditions => ["repository_id = ? AND path = ?", id, path],
75 79 :order => "committed_on DESC, #{Changeset.table_name}.id DESC").collect(&:changeset)
76 80 end
77 81
78 82 # Returns a path relative to the url of the repository
79 83 def relative_path(path)
80 84 path
81 85 end
82 86
83 87 def latest_changeset
84 88 @latest_changeset ||= changesets.find(:first)
85 89 end
86 90
87 91 def scan_changesets_for_issue_ids
88 92 self.changesets.each(&:scan_comment_for_issue_ids)
89 93 end
90 94
91 95 # fetch new changesets for all repositories
92 96 # can be called periodically by an external script
93 97 # eg. ruby script/runner "Repository.fetch_changesets"
94 98 def self.fetch_changesets
95 99 find(:all).each(&:fetch_changesets)
96 100 end
97 101
98 102 # scan changeset comments to find related and fixed issues for all repositories
99 103 def self.scan_changesets_for_issue_ids
100 104 find(:all).each(&:scan_changesets_for_issue_ids)
101 105 end
102 106
103 107 def self.scm_name
104 108 'Abstract'
105 109 end
106 110
107 111 def self.available_scm
108 112 subclasses.collect {|klass| [klass.scm_name, klass.name]}
109 113 end
110 114
111 115 def self.factory(klass_name, *args)
112 116 klass = "Repository::#{klass_name}".constantize
113 117 klass.new(*args)
114 118 rescue
115 119 nil
116 120 end
117 121
118 122 private
119 123
120 124 def before_save
121 125 # Strips url and root_url
122 126 url.strip!
123 127 root_url.strip!
124 128 true
125 129 end
126 130 end
@@ -1,13 +1,14
1 1 <div class="contextual">
2 2 <% form_tag do %>
3 3 <%= l(:label_revision) %>: <%= text_field_tag 'rev', @rev, :size => 5 %>
4 4 <% end %>
5 5 </div>
6 6
7 7 <h2><%= render :partial => 'navigation', :locals => { :path => @path, :kind => 'dir', :revision => @rev } %></h2>
8 8
9 9 <%= render :partial => 'dir_list' %>
10 <%= render_properties(@properties) %>
10 11
11 12 <% content_for :header_tags do %>
12 13 <%= stylesheet_link_tag "scm" %>
13 14 <% end %>
@@ -1,19 +1,19
1 1 <h2><%= render :partial => 'navigation', :locals => { :path => @path, :kind => (@entry ? @entry.kind : nil), :revision => @rev } %></h2>
2 2
3 <h3><%=h @entry.name %></h3>
4
5 3 <p>
6 4 <% if @repository.supports_cat? %>
7 5 <%= link_to l(:button_view), {:action => 'entry', :id => @project, :path => to_path_param(@path), :rev => @rev } %> |
8 6 <% end %>
9 7 <% if @repository.supports_annotate? %>
10 8 <%= link_to l(:button_annotate), {:action => 'annotate', :id => @project, :path => to_path_param(@path), :rev => @rev } %> |
11 9 <% end %>
12 10 <%= link_to(l(:button_download), {:action => 'entry', :id => @project, :path => to_path_param(@path), :rev => @rev, :format => 'raw' }) if @repository.supports_cat? %>
13 11 <%= "(#{number_to_human_size(@entry.size)})" if @entry.size %>
14 12 </p>
15 13
14 <%= render_properties(@properties) %>
15
16 16 <%= render(:partial => 'revisions',
17 17 :locals => {:project => @project, :path => @path, :revisions => @changesets, :entry => @entry }) unless @changesets.empty? %>
18 18
19 19 <% html_title(l(:label_change_plural)) -%>
@@ -1,261 +1,287
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'cgi'
19 19
20 20 module Redmine
21 21 module Scm
22 22 module Adapters
23 23 class CommandFailed < StandardError #:nodoc:
24 24 end
25 25
26 26 class AbstractAdapter #:nodoc:
27 class << self
28 # Returns the version of the scm client
29 # Eg: [1, 5, 0]
30 def client_version
31 'Unknown version'
32 end
33
34 # Returns the version string of the scm client
35 # Eg: '1.5.0'
36 def client_version_string
37 client_version.is_a?(Array) ? client_version.join('.') : client_version.to_s
38 end
39 end
40
27 41 def initialize(url, root_url=nil, login=nil, password=nil)
28 42 @url = url
29 43 @login = login if login && !login.empty?
30 44 @password = (password || "") if @login
31 45 @root_url = root_url.blank? ? retrieve_root_url : root_url
32 46 end
33 47
34 48 def adapter_name
35 49 'Abstract'
36 50 end
37 51
38 52 def supports_cat?
39 53 true
40 54 end
41 55
42 56 def supports_annotate?
43 57 respond_to?('annotate')
44 58 end
45 59
46 60 def root_url
47 61 @root_url
48 62 end
49 63
50 64 def url
51 65 @url
52 66 end
53 67
54 68 # get info about the svn repository
55 69 def info
56 70 return nil
57 71 end
58 72
59 73 # Returns the entry identified by path and revision identifier
60 74 # or nil if entry doesn't exist in the repository
61 75 def entry(path=nil, identifier=nil)
62 76 parts = path.to_s.split(%r{[\/\\]}).select {|n| !n.blank?}
63 77 search_path = parts[0..-2].join('/')
64 78 search_name = parts[-1]
65 79 if search_path.blank? && search_name.blank?
66 80 # Root entry
67 81 Entry.new(:path => '', :kind => 'dir')
68 82 else
69 83 # Search for the entry in the parent directory
70 84 es = entries(search_path, identifier)
71 85 es ? es.detect {|e| e.name == search_name} : nil
72 86 end
73 87 end
74 88
75 89 # Returns an Entries collection
76 90 # or nil if the given path doesn't exist in the repository
77 91 def entries(path=nil, identifier=nil)
78 92 return nil
79 93 end
94
95 def properties(path, identifier=nil)
96 return nil
97 end
80 98
81 99 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
82 100 return nil
83 101 end
84 102
85 103 def diff(path, identifier_from, identifier_to=nil)
86 104 return nil
87 105 end
88 106
89 107 def cat(path, identifier=nil)
90 108 return nil
91 109 end
92 110
93 111 def with_leading_slash(path)
94 112 path ||= ''
95 113 (path[0,1]!="/") ? "/#{path}" : path
96 114 end
97 115
98 116 def with_trailling_slash(path)
99 117 path ||= ''
100 118 (path[-1,1] == "/") ? path : "#{path}/"
101 119 end
102 120
103 121 def without_leading_slash(path)
104 122 path ||= ''
105 123 path.gsub(%r{^/+}, '')
106 124 end
107 125
108 126 def without_trailling_slash(path)
109 127 path ||= ''
110 128 (path[-1,1] == "/") ? path[0..-2] : path
111 129 end
112 130
113 131 def shell_quote(str)
114 132 if RUBY_PLATFORM =~ /mswin/
115 133 '"' + str.gsub(/"/, '\\"') + '"'
116 134 else
117 135 "'" + str.gsub(/'/, "'\"'\"'") + "'"
118 136 end
119 137 end
120 138
121 139 private
122 140 def retrieve_root_url
123 141 info = self.info
124 142 info ? info.root_url : nil
125 143 end
126 144
127 145 def target(path)
128 146 path ||= ''
129 147 base = path.match(/^\//) ? root_url : url
130 148 shell_quote("#{base}/#{path}".gsub(/[?<>\*]/, ''))
131 149 end
132 150
133 151 def logger
134 RAILS_DEFAULT_LOGGER
152 self.class.logger
135 153 end
136 154
137 155 def shellout(cmd, &block)
156 self.class.shellout(cmd, &block)
157 end
158
159 def self.logger
160 RAILS_DEFAULT_LOGGER
161 end
162
163 def self.shellout(cmd, &block)
138 164 logger.debug "Shelling out: #{cmd}" if logger && logger.debug?
139 165 begin
140 166 IO.popen(cmd, "r+") do |io|
141 167 io.close_write
142 168 block.call(io) if block_given?
143 169 end
144 170 rescue Errno::ENOENT => e
145 171 msg = strip_credential(e.message)
146 172 # The command failed, log it and re-raise
147 173 logger.error("SCM command failed: #{strip_credential(cmd)}\n with: #{msg}")
148 174 raise CommandFailed.new(msg)
149 175 end
150 176 end
151 177
152 178 # Hides username/password in a given command
153 179 def self.hide_credential(cmd)
154 180 q = (RUBY_PLATFORM =~ /mswin/ ? '"' : "'")
155 181 cmd.to_s.gsub(/(\-\-(password|username))\s+(#{q}[^#{q}]+#{q}|[^#{q}]\S+)/, '\\1 xxxx')
156 182 end
157 183
158 184 def strip_credential(cmd)
159 185 self.class.hide_credential(cmd)
160 186 end
161 187 end
162 188
163 189 class Entries < Array
164 190 def sort_by_name
165 191 sort {|x,y|
166 192 if x.kind == y.kind
167 193 x.name <=> y.name
168 194 else
169 195 x.kind <=> y.kind
170 196 end
171 197 }
172 198 end
173 199
174 200 def revisions
175 201 revisions ||= Revisions.new(collect{|entry| entry.lastrev}.compact)
176 202 end
177 203 end
178 204
179 205 class Info
180 206 attr_accessor :root_url, :lastrev
181 207 def initialize(attributes={})
182 208 self.root_url = attributes[:root_url] if attributes[:root_url]
183 209 self.lastrev = attributes[:lastrev]
184 210 end
185 211 end
186 212
187 213 class Entry
188 214 attr_accessor :name, :path, :kind, :size, :lastrev
189 215 def initialize(attributes={})
190 216 self.name = attributes[:name] if attributes[:name]
191 217 self.path = attributes[:path] if attributes[:path]
192 218 self.kind = attributes[:kind] if attributes[:kind]
193 219 self.size = attributes[:size].to_i if attributes[:size]
194 220 self.lastrev = attributes[:lastrev]
195 221 end
196 222
197 223 def is_file?
198 224 'file' == self.kind
199 225 end
200 226
201 227 def is_dir?
202 228 'dir' == self.kind
203 229 end
204 230
205 231 def is_text?
206 232 Redmine::MimeType.is_type?('text', name)
207 233 end
208 234 end
209 235
210 236 class Revisions < Array
211 237 def latest
212 238 sort {|x,y|
213 239 unless x.time.nil? or y.time.nil?
214 240 x.time <=> y.time
215 241 else
216 242 0
217 243 end
218 244 }.last
219 245 end
220 246 end
221 247
222 248 class Revision
223 249 attr_accessor :identifier, :scmid, :name, :author, :time, :message, :paths, :revision, :branch
224 250 def initialize(attributes={})
225 251 self.identifier = attributes[:identifier]
226 252 self.scmid = attributes[:scmid]
227 253 self.name = attributes[:name] || self.identifier
228 254 self.author = attributes[:author]
229 255 self.time = attributes[:time]
230 256 self.message = attributes[:message] || ""
231 257 self.paths = attributes[:paths]
232 258 self.revision = attributes[:revision]
233 259 self.branch = attributes[:branch]
234 260 end
235 261
236 262 end
237 263
238 264 class Annotate
239 265 attr_reader :lines, :revisions
240 266
241 267 def initialize
242 268 @lines = []
243 269 @revisions = []
244 270 end
245 271
246 272 def add_line(line, revision)
247 273 @lines << line
248 274 @revisions << revision
249 275 end
250 276
251 277 def content
252 278 content = lines.join("\n")
253 279 end
254 280
255 281 def empty?
256 282 lines.empty?
257 283 end
258 284 end
259 285 end
260 286 end
261 287 end
@@ -1,187 +1,228
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'redmine/scm/adapters/abstract_adapter'
19 19 require 'rexml/document'
20 20
21 21 module Redmine
22 22 module Scm
23 23 module Adapters
24 24 class SubversionAdapter < AbstractAdapter
25 25
26 26 # SVN executable name
27 27 SVN_BIN = "svn"
28 28
29 class << self
30 def client_version
31 @@client_version ||= (svn_binary_version || 'Unknown version')
32 end
33
34 def svn_binary_version
35 cmd = "#{SVN_BIN} --version"
36 version = nil
37 shellout(cmd) do |io|
38 # Read svn version in first returned line
39 if m = io.gets.match(%r{((\d+\.)+\d+)})
40 version = m[0].scan(%r{\d+}).collect(&:to_i)
41 end
42 end
43 return nil if $? && $?.exitstatus != 0
44 version
45 end
46 end
47
29 48 # Get info about the svn repository
30 49 def info
31 50 cmd = "#{SVN_BIN} info --xml #{target('')}"
32 51 cmd << credentials_string
33 52 info = nil
34 53 shellout(cmd) do |io|
35 54 begin
36 55 doc = REXML::Document.new(io)
37 56 #root_url = doc.elements["info/entry/repository/root"].text
38 57 info = Info.new({:root_url => doc.elements["info/entry/repository/root"].text,
39 58 :lastrev => Revision.new({
40 59 :identifier => doc.elements["info/entry/commit"].attributes['revision'],
41 60 :time => Time.parse(doc.elements["info/entry/commit/date"].text).localtime,
42 61 :author => (doc.elements["info/entry/commit/author"] ? doc.elements["info/entry/commit/author"].text : "")
43 62 })
44 63 })
45 64 rescue
46 65 end
47 66 end
48 67 return nil if $? && $?.exitstatus != 0
49 68 info
50 69 rescue CommandFailed
51 70 return nil
52 71 end
53 72
54 73 # Returns an Entries collection
55 74 # or nil if the given path doesn't exist in the repository
56 75 def entries(path=nil, identifier=nil)
57 76 path ||= ''
58 77 identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
59 78 entries = Entries.new
60 79 cmd = "#{SVN_BIN} list --xml #{target(path)}@#{identifier}"
61 80 cmd << credentials_string
62 81 shellout(cmd) do |io|
63 82 output = io.read
64 83 begin
65 84 doc = REXML::Document.new(output)
66 85 doc.elements.each("lists/list/entry") do |entry|
67 86 # Skip directory if there is no commit date (usually that
68 87 # means that we don't have read access to it)
69 88 next if entry.attributes['kind'] == 'dir' && entry.elements['commit'].elements['date'].nil?
70 89 entries << Entry.new({:name => entry.elements['name'].text,
71 90 :path => ((path.empty? ? "" : "#{path}/") + entry.elements['name'].text),
72 91 :kind => entry.attributes['kind'],
73 92 :size => (entry.elements['size'] and entry.elements['size'].text).to_i,
74 93 :lastrev => Revision.new({
75 94 :identifier => entry.elements['commit'].attributes['revision'],
76 95 :time => Time.parse(entry.elements['commit'].elements['date'].text).localtime,
77 96 :author => (entry.elements['commit'].elements['author'] ? entry.elements['commit'].elements['author'].text : "")
78 97 })
79 98 })
80 99 end
81 100 rescue Exception => e
82 101 logger.error("Error parsing svn output: #{e.message}")
83 102 logger.error("Output was:\n #{output}")
84 103 end
85 104 end
86 105 return nil if $? && $?.exitstatus != 0
87 106 logger.debug("Found #{entries.size} entries in the repository for #{target(path)}") if logger && logger.debug?
88 107 entries.sort_by_name
89 108 end
90
109
110 def properties(path, identifier=nil)
111 # proplist xml output supported in svn 1.5.0 and higher
112 return nil if (self.class.client_version <=> [1, 5, 0]) < 0
113
114 identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
115 cmd = "#{SVN_BIN} proplist --verbose --xml #{target(path)}@#{identifier}"
116 cmd << credentials_string
117 properties = {}
118 shellout(cmd) do |io|
119 output = io.read
120 begin
121 doc = REXML::Document.new(output)
122 doc.elements.each("properties/target/property") do |property|
123 properties[ property.attributes['name'] ] = property.text
124 end
125 rescue
126 end
127 end
128 return nil if $? && $?.exitstatus != 0
129 properties
130 end
131
91 132 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
92 133 path ||= ''
93 134 identifier_from = (identifier_from and identifier_from.to_i > 0) ? identifier_from.to_i : "HEAD"
94 135 identifier_to = (identifier_to and identifier_to.to_i > 0) ? identifier_to.to_i : 1
95 136 revisions = Revisions.new
96 137 cmd = "#{SVN_BIN} log --xml -r #{identifier_from}:#{identifier_to}"
97 138 cmd << credentials_string
98 139 cmd << " --verbose " if options[:with_paths]
99 140 cmd << ' ' + target(path)
100 141 shellout(cmd) do |io|
101 142 begin
102 143 doc = REXML::Document.new(io)
103 144 doc.elements.each("log/logentry") do |logentry|
104 145 paths = []
105 146 logentry.elements.each("paths/path") do |path|
106 147 paths << {:action => path.attributes['action'],
107 148 :path => path.text,
108 149 :from_path => path.attributes['copyfrom-path'],
109 150 :from_revision => path.attributes['copyfrom-rev']
110 151 }
111 152 end
112 153 paths.sort! { |x,y| x[:path] <=> y[:path] }
113 154
114 155 revisions << Revision.new({:identifier => logentry.attributes['revision'],
115 156 :author => (logentry.elements['author'] ? logentry.elements['author'].text : ""),
116 157 :time => Time.parse(logentry.elements['date'].text).localtime,
117 158 :message => logentry.elements['msg'].text,
118 159 :paths => paths
119 160 })
120 161 end
121 162 rescue
122 163 end
123 164 end
124 165 return nil if $? && $?.exitstatus != 0
125 166 revisions
126 167 end
127 168
128 169 def diff(path, identifier_from, identifier_to=nil, type="inline")
129 170 path ||= ''
130 171 identifier_from = (identifier_from and identifier_from.to_i > 0) ? identifier_from.to_i : ''
131 172 identifier_to = (identifier_to and identifier_to.to_i > 0) ? identifier_to.to_i : (identifier_from.to_i - 1)
132 173
133 174 cmd = "#{SVN_BIN} diff -r "
134 175 cmd << "#{identifier_to}:"
135 176 cmd << "#{identifier_from}"
136 177 cmd << " #{target(path)}@#{identifier_from}"
137 178 cmd << credentials_string
138 179 diff = []
139 180 shellout(cmd) do |io|
140 181 io.each_line do |line|
141 182 diff << line
142 183 end
143 184 end
144 185 return nil if $? && $?.exitstatus != 0
145 186 diff
146 187 end
147 188
148 189 def cat(path, identifier=nil)
149 190 identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
150 191 cmd = "#{SVN_BIN} cat #{target(path)}@#{identifier}"
151 192 cmd << credentials_string
152 193 cat = nil
153 194 shellout(cmd) do |io|
154 195 io.binmode
155 196 cat = io.read
156 197 end
157 198 return nil if $? && $?.exitstatus != 0
158 199 cat
159 200 end
160 201
161 202 def annotate(path, identifier=nil)
162 203 identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
163 204 cmd = "#{SVN_BIN} blame #{target(path)}@#{identifier}"
164 205 cmd << credentials_string
165 206 blame = Annotate.new
166 207 shellout(cmd) do |io|
167 208 io.each_line do |line|
168 209 next unless line =~ %r{^\s*(\d+)\s*(\S+)\s(.*)$}
169 210 blame.add_line($3.rstrip, Revision.new(:identifier => $1.to_i, :author => $2.strip))
170 211 end
171 212 end
172 213 return nil if $? && $?.exitstatus != 0
173 214 blame
174 215 end
175 216
176 217 private
177 218
178 219 def credentials_string
179 220 str = ''
180 221 str << " --username #{shell_quote(@login)}" unless @login.blank?
181 222 str << " --password #{shell_quote(@password)}" unless @login.blank? || @password.blank?
182 223 str
183 224 end
184 225 end
185 226 end
186 227 end
187 228 end
@@ -1,610 +1,614
1 1 body { font-family: Verdana, sans-serif; font-size: 12px; color:#484848; margin: 0; padding: 0; min-width: 900px; }
2 2
3 3 h1, h2, h3, h4 { font-family: "Trebuchet MS", Verdana, sans-serif;}
4 4 h1 {margin:0; padding:0; font-size: 24px;}
5 5 h2, .wiki h1 {font-size: 20px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; border-bottom: 1px solid #bbbbbb; color: #444;}
6 6 h3, .wiki h2 {font-size: 16px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; border-bottom: 1px solid #bbbbbb; color: #444;}
7 7 h4, .wiki h3 {font-size: 13px;padding: 2px 10px 1px 0px;margin-bottom: 5px; border-bottom: 1px dotted #bbbbbb; color: #444;}
8 8
9 9 /***** Layout *****/
10 10 #wrapper {background: white;}
11 11
12 12 #top-menu {background: #2C4056; color: #fff; height:1.8em; font-size: 0.8em; padding: 2px 2px 0px 6px;}
13 13 #top-menu ul {margin: 0; padding: 0;}
14 14 #top-menu li {
15 15 float:left;
16 16 list-style-type:none;
17 17 margin: 0px 0px 0px 0px;
18 18 padding: 0px 0px 0px 0px;
19 19 white-space:nowrap;
20 20 }
21 21 #top-menu a {color: #fff; padding-right: 8px; font-weight: bold;}
22 22 #top-menu #loggedas { float: right; margin-right: 0.5em; color: #fff; }
23 23
24 24 #account {float:right;}
25 25
26 26 #header {height:5.3em;margin:0;background-color:#507AAA;color:#f8f8f8; padding: 4px 8px 0px 6px; position:relative;}
27 27 #header a {color:#f8f8f8;}
28 28 #quick-search {float:right;}
29 29
30 30 #main-menu {position: absolute; bottom: 0px; left:6px; margin-right: -500px;}
31 31 #main-menu ul {margin: 0; padding: 0;}
32 32 #main-menu li {
33 33 float:left;
34 34 list-style-type:none;
35 35 margin: 0px 2px 0px 0px;
36 36 padding: 0px 0px 0px 0px;
37 37 white-space:nowrap;
38 38 }
39 39 #main-menu li a {
40 40 display: block;
41 41 color: #fff;
42 42 text-decoration: none;
43 43 font-weight: bold;
44 44 margin: 0;
45 45 padding: 4px 10px 4px 10px;
46 46 }
47 47 #main-menu li a:hover {background:#759FCF; color:#fff;}
48 48 #main-menu li a.selected, #main-menu li a.selected:hover {background:#fff; color:#555;}
49 49
50 50 #main {background-color:#EEEEEE;}
51 51
52 52 #sidebar{ float: right; width: 17%; position: relative; z-index: 9; min-height: 600px; padding: 0; margin: 0;}
53 53 * html #sidebar{ width: 17%; }
54 54 #sidebar h3{ font-size: 14px; margin-top:14px; color: #666; }
55 55 #sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; }
56 56 * html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; }
57 57
58 58 #content { width: 80%; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; height:600px; min-height: 600px;}
59 59 * html #content{ width: 80%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;}
60 60 html>body #content { height: auto; min-height: 600px; overflow: auto; }
61 61
62 62 #main.nosidebar #sidebar{ display: none; }
63 63 #main.nosidebar #content{ width: auto; border-right: 0; }
64 64
65 65 #footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;}
66 66
67 67 #login-form table {margin-top:5em; padding:1em; margin-left: auto; margin-right: auto; border: 2px solid #FDBF3B; background-color:#FFEBC1; }
68 68 #login-form table td {padding: 6px;}
69 69 #login-form label {font-weight: bold;}
70 70
71 71 .clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; }
72 72
73 73 /***** Links *****/
74 74 a, a:link, a:visited{ color: #2A5685; text-decoration: none; }
75 75 a:hover, a:active{ color: #c61a1a; text-decoration: underline;}
76 76 a img{ border: 0; }
77 77
78 78 a.issue.closed, .issue.closed a { text-decoration: line-through; }
79 79
80 80 /***** Tables *****/
81 81 table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; }
82 82 table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; }
83 83 table.list td { vertical-align: top; }
84 84 table.list td.id { width: 2%; text-align: center;}
85 85 table.list td.checkbox { width: 15px; padding: 0px;}
86 86
87 87 table.list.issues { margin-top: 10px; }
88 88 tr.issue { text-align: center; white-space: nowrap; }
89 89 tr.issue td.subject, tr.issue td.category { white-space: normal; }
90 90 tr.issue td.subject { text-align: left; }
91 91 tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;}
92 92
93 93 tr.entry { border: 1px solid #f8f8f8; }
94 94 tr.entry td { white-space: nowrap; }
95 95 tr.entry td.filename { width: 30%; }
96 96 tr.entry td.size { text-align: right; font-size: 90%; }
97 97 tr.entry td.revision, tr.entry td.author { text-align: center; }
98 98 tr.entry td.age { text-align: right; }
99 99
100 100 tr.entry span.expander {background-image: url(../images/bullet_toggle_plus.png); padding-left: 8px; margin-left: 0; cursor: pointer;}
101 101 tr.entry.open span.expander {background-image: url(../images/bullet_toggle_minus.png);}
102 102 tr.entry.file td.filename a { margin-left: 16px; }
103 103
104 104 tr.changeset td.author { text-align: center; width: 15%; }
105 105 tr.changeset td.committed_on { text-align: center; width: 15%; }
106 106
107 107 tr.message { height: 2.6em; }
108 108 tr.message td.last_message { font-size: 80%; }
109 109 tr.message.locked td.subject a { background-image: url(../images/locked.png); }
110 110 tr.message.sticky td.subject a { background-image: url(../images/sticky.png); font-weight: bold; }
111 111
112 112 tr.user td { width:13%; }
113 113 tr.user td.email { width:18%; }
114 114 tr.user td { white-space: nowrap; }
115 115 tr.user.locked, tr.user.registered { color: #aaa; }
116 116 tr.user.locked a, tr.user.registered a { color: #aaa; }
117 117
118 118 tr.time-entry { text-align: center; white-space: nowrap; }
119 119 tr.time-entry td.subject, tr.time-entry td.comments { text-align: left; white-space: normal; }
120 120 td.hours { text-align: right; font-weight: bold; padding-right: 0.5em; }
121 121 td.hours .hours-dec { font-size: 0.9em; }
122 122
123 123 table.list tbody tr:hover { background-color:#ffffdd; }
124 124 table td {padding:2px;}
125 125 table p {margin:0;}
126 126 .odd {background-color:#f6f7f8;}
127 127 .even {background-color: #fff;}
128 128
129 129 .highlight { background-color: #FCFD8D;}
130 130 .highlight.token-1 { background-color: #faa;}
131 131 .highlight.token-2 { background-color: #afa;}
132 132 .highlight.token-3 { background-color: #aaf;}
133 133
134 134 .box{
135 135 padding:6px;
136 136 margin-bottom: 10px;
137 137 background-color:#f6f6f6;
138 138 color:#505050;
139 139 line-height:1.5em;
140 140 border: 1px solid #e4e4e4;
141 141 }
142 142
143 143 div.square {
144 144 border: 1px solid #999;
145 145 float: left;
146 146 margin: .3em .4em 0 .4em;
147 147 overflow: hidden;
148 148 width: .6em; height: .6em;
149 149 }
150 150 .contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px; padding-left: 10px; font-size:0.9em;}
151 151 .contextual input {font-size:0.9em;}
152 152
153 153 .splitcontentleft{float:left; width:49%;}
154 154 .splitcontentright{float:right; width:49%;}
155 155 form {display: inline;}
156 156 input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;}
157 157 fieldset {border: 1px solid #e4e4e4; margin:0;}
158 158 legend {color: #484848;}
159 159 hr { width: 100%; height: 1px; background: #ccc; border: 0;}
160 160 blockquote { font-style: italic; border-left: 3px solid #e0e0e0; padding-left: 0.6em; margin-left: 2.4em;}
161 161 blockquote blockquote { margin-left: 0;}
162 162 textarea.wiki-edit { width: 99%; }
163 163 li p {margin-top: 0;}
164 164 div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7;}
165 165 p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;}
166 166 p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; }
167 167
168 168 fieldset#filters { padding: 0.7em; }
169 169 fieldset#filters p { margin: 1.2em 0 0.8em 2px; }
170 170 fieldset#filters .buttons { font-size: 0.9em; }
171 171 fieldset#filters table { border-collapse: collapse; }
172 172 fieldset#filters table td { padding: 0; vertical-align: middle; }
173 173 fieldset#filters tr.filter { height: 2em; }
174 174 fieldset#filters td.add-filter { text-align: right; vertical-align: top; }
175 175
176 176 div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;}
177 177 div#issue-changesets .changeset { padding: 4px;}
178 178 div#issue-changesets .changeset { border-bottom: 1px solid #ddd; }
179 179 div#issue-changesets p { margin-top: 0; margin-bottom: 1em;}
180 180
181 181 div#activity dl, #search-results { margin-left: 2em; }
182 182 div#activity dd, #search-results dd { margin-bottom: 1em; padding-left: 18px; font-size: 0.9em; }
183 183 div#activity dt, #search-results dt { margin-bottom: 0px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; }
184 184 div#activity dt.me .time { border-bottom: 1px solid #999; }
185 185 div#activity dt .time { color: #777; font-size: 80%; }
186 186 div#activity dd .description, #search-results dd .description { font-style: italic; }
187 187 div#activity span.project:after, #search-results span.project:after { content: " -"; }
188 188 #search-results dd { margin-bottom: 1em; padding-left: 20px; margin-left:0px;}
189 189 div#activity dd span.description, #search-results dd span.description { display:block; }
190 190
191 191 dt.issue { background-image: url(../images/ticket.png); }
192 192 dt.issue-edit { background-image: url(../images/ticket_edit.png); }
193 193 dt.issue-closed { background-image: url(../images/ticket_checked.png); }
194 194 dt.issue-note { background-image: url(../images/ticket_note.png); }
195 195 dt.changeset { background-image: url(../images/changeset.png); }
196 196 dt.news { background-image: url(../images/news.png); }
197 197 dt.message { background-image: url(../images/message.png); }
198 198 dt.reply { background-image: url(../images/comments.png); }
199 199 dt.wiki-page { background-image: url(../images/wiki_edit.png); }
200 200 dt.attachment { background-image: url(../images/attachment.png); }
201 201 dt.document { background-image: url(../images/document.png); }
202 202 dt.project { background-image: url(../images/projects.png); }
203 203
204 204 div#roadmap fieldset.related-issues { margin-bottom: 1em; }
205 205 div#roadmap fieldset.related-issues ul { margin-top: 0.3em; margin-bottom: 0.3em; }
206 206 div#roadmap .wiki h1:first-child { display: none; }
207 207 div#roadmap .wiki h1 { font-size: 120%; }
208 208 div#roadmap .wiki h2 { font-size: 110%; }
209 209
210 210 div#version-summary { float:right; width:380px; margin-left: 16px; margin-bottom: 16px; background-color: #fff; }
211 211 div#version-summary fieldset { margin-bottom: 1em; }
212 212 div#version-summary .total-hours { text-align: right; }
213 213
214 214 table#time-report td.hours, table#time-report th.period, table#time-report th.total { text-align: right; padding-right: 0.5em; }
215 215 table#time-report tbody tr { font-style: italic; color: #777; }
216 216 table#time-report tbody tr.last-level { font-style: normal; color: #555; }
217 217 table#time-report tbody tr.total { font-style: normal; font-weight: bold; color: #555; background-color:#EEEEEE; }
218 218 table#time-report .hours-dec { font-size: 0.9em; }
219 219
220 ul.properties {padding:0; font-size: 0.9em; color: #777;}
221 ul.properties li {list-style-type:none;}
222 ul.properties li span {font-style:italic;}
223
220 224 .total-hours { font-size: 110%; font-weight: bold; }
221 225 .total-hours span.hours-int { font-size: 120%; }
222 226
223 227 .autoscroll {overflow-x: auto; padding:1px; margin-bottom: 1.2em;}
224 228 #user_firstname, #user_lastname, #user_mail, #my_account_form select { width: 90%; }
225 229
226 230 .pagination {font-size: 90%}
227 231 p.pagination {margin-top:8px;}
228 232
229 233 /***** Tabular forms ******/
230 234 .tabular p{
231 235 margin: 0;
232 236 padding: 5px 0 8px 0;
233 237 padding-left: 180px; /*width of left column containing the label elements*/
234 238 height: 1%;
235 239 clear:left;
236 240 }
237 241
238 242 html>body .tabular p {overflow:hidden;}
239 243
240 244 .tabular label{
241 245 font-weight: bold;
242 246 float: left;
243 247 text-align: right;
244 248 margin-left: -180px; /*width of left column*/
245 249 width: 175px; /*width of labels. Should be smaller than left column to create some right
246 250 margin*/
247 251 }
248 252
249 253 .tabular label.floating{
250 254 font-weight: normal;
251 255 margin-left: 0px;
252 256 text-align: left;
253 257 width: 200px;
254 258 }
255 259
256 260 input#time_entry_comments { width: 90%;}
257 261
258 262 #preview fieldset {margin-top: 1em; background: url(../images/draft.png)}
259 263
260 264 .tabular.settings p{ padding-left: 300px; }
261 265 .tabular.settings label{ margin-left: -300px; width: 295px; }
262 266
263 267 .required {color: #bb0000;}
264 268 .summary {font-style: italic;}
265 269
266 270 #attachments_fields input[type=text] {margin-left: 8px; }
267 271
268 272 div.attachments p { margin:4px 0 2px 0; }
269 273 div.attachments img { vertical-align: middle; }
270 274 div.attachments span.author { font-size: 0.9em; color: #888; }
271 275
272 276 p.other-formats { text-align: right; font-size:0.9em; color: #666; }
273 277 .other-formats span + span:before { content: "| "; }
274 278
275 279 a.feed { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; }
276 280
277 281 /***** Flash & error messages ****/
278 282 #errorExplanation, div.flash, .nodata, .warning {
279 283 padding: 4px 4px 4px 30px;
280 284 margin-bottom: 12px;
281 285 font-size: 1.1em;
282 286 border: 2px solid;
283 287 }
284 288
285 289 div.flash {margin-top: 8px;}
286 290
287 291 div.flash.error, #errorExplanation {
288 292 background: url(../images/false.png) 8px 5px no-repeat;
289 293 background-color: #ffe3e3;
290 294 border-color: #dd0000;
291 295 color: #550000;
292 296 }
293 297
294 298 div.flash.notice {
295 299 background: url(../images/true.png) 8px 5px no-repeat;
296 300 background-color: #dfffdf;
297 301 border-color: #9fcf9f;
298 302 color: #005f00;
299 303 }
300 304
301 305 .nodata, .warning {
302 306 text-align: center;
303 307 background-color: #FFEBC1;
304 308 border-color: #FDBF3B;
305 309 color: #A6750C;
306 310 }
307 311
308 312 #errorExplanation ul { font-size: 0.9em;}
309 313
310 314 /***** Ajax indicator ******/
311 315 #ajax-indicator {
312 316 position: absolute; /* fixed not supported by IE */
313 317 background-color:#eee;
314 318 border: 1px solid #bbb;
315 319 top:35%;
316 320 left:40%;
317 321 width:20%;
318 322 font-weight:bold;
319 323 text-align:center;
320 324 padding:0.6em;
321 325 z-index:100;
322 326 filter:alpha(opacity=50);
323 327 opacity: 0.5;
324 328 }
325 329
326 330 html>body #ajax-indicator { position: fixed; }
327 331
328 332 #ajax-indicator span {
329 333 background-position: 0% 40%;
330 334 background-repeat: no-repeat;
331 335 background-image: url(../images/loading.gif);
332 336 padding-left: 26px;
333 337 vertical-align: bottom;
334 338 }
335 339
336 340 /***** Calendar *****/
337 341 table.cal {border-collapse: collapse; width: 100%; margin: 8px 0 6px 0;border: 1px solid #d7d7d7;}
338 342 table.cal thead th {width: 14%;}
339 343 table.cal tbody tr {height: 100px;}
340 344 table.cal th { background-color:#EEEEEE; padding: 4px; }
341 345 table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;}
342 346 table.cal td p.day-num {font-size: 1.1em; text-align:right;}
343 347 table.cal td.odd p.day-num {color: #bbb;}
344 348 table.cal td.today {background:#ffffdd;}
345 349 table.cal td.today p.day-num {font-weight: bold;}
346 350
347 351 /***** Tooltips ******/
348 352 .tooltip{position:relative;z-index:24;}
349 353 .tooltip:hover{z-index:25;color:#000;}
350 354 .tooltip span.tip{display: none; text-align:left;}
351 355
352 356 div.tooltip:hover span.tip{
353 357 display:block;
354 358 position:absolute;
355 359 top:12px; left:24px; width:270px;
356 360 border:1px solid #555;
357 361 background-color:#fff;
358 362 padding: 4px;
359 363 font-size: 0.8em;
360 364 color:#505050;
361 365 }
362 366
363 367 /***** Progress bar *****/
364 368 table.progress {
365 369 border: 1px solid #D7D7D7;
366 370 border-collapse: collapse;
367 371 border-spacing: 0pt;
368 372 empty-cells: show;
369 373 text-align: center;
370 374 float:left;
371 375 margin: 1px 6px 1px 0px;
372 376 }
373 377
374 378 table.progress td { height: 0.9em; }
375 379 table.progress td.closed { background: #BAE0BA none repeat scroll 0%; }
376 380 table.progress td.done { background: #DEF0DE none repeat scroll 0%; }
377 381 table.progress td.open { background: #FFF none repeat scroll 0%; }
378 382 p.pourcent {font-size: 80%;}
379 383 p.progress-info {clear: left; font-style: italic; font-size: 80%;}
380 384
381 385 /***** Tabs *****/
382 386 #content .tabs {height: 2.6em; border-bottom: 1px solid #bbbbbb; margin-bottom:1.2em; position:relative;}
383 387 #content .tabs ul {margin:0; position:absolute; bottom:-2px; padding-left:1em;}
384 388 #content .tabs>ul { bottom:-1px; } /* others */
385 389 #content .tabs ul li {
386 390 float:left;
387 391 list-style-type:none;
388 392 white-space:nowrap;
389 393 margin-right:8px;
390 394 background:#fff;
391 395 }
392 396 #content .tabs ul li a{
393 397 display:block;
394 398 font-size: 0.9em;
395 399 text-decoration:none;
396 400 line-height:1.3em;
397 401 padding:4px 6px 4px 6px;
398 402 border: 1px solid #ccc;
399 403 border-bottom: 1px solid #bbbbbb;
400 404 background-color: #eeeeee;
401 405 color:#777;
402 406 font-weight:bold;
403 407 }
404 408
405 409 #content .tabs ul li a:hover {
406 410 background-color: #ffffdd;
407 411 text-decoration:none;
408 412 }
409 413
410 414 #content .tabs ul li a.selected {
411 415 background-color: #fff;
412 416 border: 1px solid #bbbbbb;
413 417 border-bottom: 1px solid #fff;
414 418 }
415 419
416 420 #content .tabs ul li a.selected:hover {
417 421 background-color: #fff;
418 422 }
419 423
420 424 /***** Diff *****/
421 425 .diff_out { background: #fcc; }
422 426 .diff_in { background: #cfc; }
423 427
424 428 /***** Wiki *****/
425 429 div.wiki table {
426 430 border: 1px solid #505050;
427 431 border-collapse: collapse;
428 432 margin-bottom: 1em;
429 433 }
430 434
431 435 div.wiki table, div.wiki td, div.wiki th {
432 436 border: 1px solid #bbb;
433 437 padding: 4px;
434 438 }
435 439
436 440 div.wiki .external {
437 441 background-position: 0% 60%;
438 442 background-repeat: no-repeat;
439 443 padding-left: 12px;
440 444 background-image: url(../images/external.png);
441 445 }
442 446
443 447 div.wiki a.new {
444 448 color: #b73535;
445 449 }
446 450
447 451 div.wiki pre {
448 452 margin: 1em 1em 1em 1.6em;
449 453 padding: 2px;
450 454 background-color: #fafafa;
451 455 border: 1px solid #dadada;
452 456 width:95%;
453 457 overflow-x: auto;
454 458 }
455 459
456 460 div.wiki div.toc {
457 461 background-color: #ffffdd;
458 462 border: 1px solid #e4e4e4;
459 463 padding: 4px;
460 464 line-height: 1.2em;
461 465 margin-bottom: 12px;
462 466 margin-right: 12px;
463 467 display: table
464 468 }
465 469 * html div.wiki div.toc { width: 50%; } /* IE6 doesn't autosize div */
466 470
467 471 div.wiki div.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; }
468 472 div.wiki div.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; }
469 473
470 474 div.wiki div.toc a {
471 475 display: block;
472 476 font-size: 0.9em;
473 477 font-weight: normal;
474 478 text-decoration: none;
475 479 color: #606060;
476 480 }
477 481 div.wiki div.toc a:hover { color: #c61a1a; text-decoration: underline;}
478 482
479 483 div.wiki div.toc a.heading2 { margin-left: 6px; }
480 484 div.wiki div.toc a.heading3 { margin-left: 12px; font-size: 0.8em; }
481 485
482 486 /***** My page layout *****/
483 487 .block-receiver {
484 488 border:1px dashed #c0c0c0;
485 489 margin-bottom: 20px;
486 490 padding: 15px 0 15px 0;
487 491 }
488 492
489 493 .mypage-box {
490 494 margin:0 0 20px 0;
491 495 color:#505050;
492 496 line-height:1.5em;
493 497 }
494 498
495 499 .handle {
496 500 cursor: move;
497 501 }
498 502
499 503 a.close-icon {
500 504 display:block;
501 505 margin-top:3px;
502 506 overflow:hidden;
503 507 width:12px;
504 508 height:12px;
505 509 background-repeat: no-repeat;
506 510 cursor:pointer;
507 511 background-image:url('../images/close.png');
508 512 }
509 513
510 514 a.close-icon:hover {
511 515 background-image:url('../images/close_hl.png');
512 516 }
513 517
514 518 /***** Gantt chart *****/
515 519 .gantt_hdr {
516 520 position:absolute;
517 521 top:0;
518 522 height:16px;
519 523 border-top: 1px solid #c0c0c0;
520 524 border-bottom: 1px solid #c0c0c0;
521 525 border-right: 1px solid #c0c0c0;
522 526 text-align: center;
523 527 overflow: hidden;
524 528 }
525 529
526 530 .task {
527 531 position: absolute;
528 532 height:8px;
529 533 font-size:0.8em;
530 534 color:#888;
531 535 padding:0;
532 536 margin:0;
533 537 line-height:0.8em;
534 538 }
535 539
536 540 .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
537 541 .task_done { background:#66f url(../images/task_done.png); border: 1px solid #66f; }
538 542 .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
539 543 .milestone { background-image:url(../images/milestone.png); background-repeat: no-repeat; border: 0; }
540 544
541 545 /***** Icons *****/
542 546 .icon {
543 547 background-position: 0% 40%;
544 548 background-repeat: no-repeat;
545 549 padding-left: 20px;
546 550 padding-top: 2px;
547 551 padding-bottom: 3px;
548 552 }
549 553
550 554 .icon22 {
551 555 background-position: 0% 40%;
552 556 background-repeat: no-repeat;
553 557 padding-left: 26px;
554 558 line-height: 22px;
555 559 vertical-align: middle;
556 560 }
557 561
558 562 .icon-add { background-image: url(../images/add.png); }
559 563 .icon-edit { background-image: url(../images/edit.png); }
560 564 .icon-copy { background-image: url(../images/copy.png); }
561 565 .icon-del { background-image: url(../images/delete.png); }
562 566 .icon-move { background-image: url(../images/move.png); }
563 567 .icon-save { background-image: url(../images/save.png); }
564 568 .icon-cancel { background-image: url(../images/cancel.png); }
565 569 .icon-file { background-image: url(../images/file.png); }
566 570 .icon-folder { background-image: url(../images/folder.png); }
567 571 .open .icon-folder { background-image: url(../images/folder_open.png); }
568 572 .icon-package { background-image: url(../images/package.png); }
569 573 .icon-home { background-image: url(../images/home.png); }
570 574 .icon-user { background-image: url(../images/user.png); }
571 575 .icon-mypage { background-image: url(../images/user_page.png); }
572 576 .icon-admin { background-image: url(../images/admin.png); }
573 577 .icon-projects { background-image: url(../images/projects.png); }
574 578 .icon-logout { background-image: url(../images/logout.png); }
575 579 .icon-help { background-image: url(../images/help.png); }
576 580 .icon-attachment { background-image: url(../images/attachment.png); }
577 581 .icon-index { background-image: url(../images/index.png); }
578 582 .icon-history { background-image: url(../images/history.png); }
579 583 .icon-time { background-image: url(../images/time.png); }
580 584 .icon-stats { background-image: url(../images/stats.png); }
581 585 .icon-warning { background-image: url(../images/warning.png); }
582 586 .icon-fav { background-image: url(../images/fav.png); }
583 587 .icon-fav-off { background-image: url(../images/fav_off.png); }
584 588 .icon-reload { background-image: url(../images/reload.png); }
585 589 .icon-lock { background-image: url(../images/locked.png); }
586 590 .icon-unlock { background-image: url(../images/unlock.png); }
587 591 .icon-checked { background-image: url(../images/true.png); }
588 592 .icon-details { background-image: url(../images/zoom_in.png); }
589 593 .icon-report { background-image: url(../images/report.png); }
590 594
591 595 .icon22-projects { background-image: url(../images/22x22/projects.png); }
592 596 .icon22-users { background-image: url(../images/22x22/users.png); }
593 597 .icon22-tracker { background-image: url(../images/22x22/tracker.png); }
594 598 .icon22-role { background-image: url(../images/22x22/role.png); }
595 599 .icon22-workflow { background-image: url(../images/22x22/workflow.png); }
596 600 .icon22-options { background-image: url(../images/22x22/options.png); }
597 601 .icon22-notifications { background-image: url(../images/22x22/notifications.png); }
598 602 .icon22-authent { background-image: url(../images/22x22/authent.png); }
599 603 .icon22-info { background-image: url(../images/22x22/info.png); }
600 604 .icon22-comment { background-image: url(../images/22x22/comment.png); }
601 605 .icon22-package { background-image: url(../images/22x22/package.png); }
602 606 .icon22-settings { background-image: url(../images/22x22/settings.png); }
603 607 .icon22-plugin { background-image: url(../images/22x22/plugin.png); }
604 608
605 609 /***** Media print specific styles *****/
606 610 @media print {
607 611 #top-menu, #header, #main-menu, #sidebar, #footer, .contextual, .other-formats { display:none; }
608 612 #main { background: #fff; }
609 613 #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; }
610 614 }
@@ -1,155 +1,168
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19 require 'repositories_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class RepositoriesController; def rescue_action(e) raise e end; end
23 23
24 24 class RepositoriesSubversionControllerTest < Test::Unit::TestCase
25 25 fixtures :projects, :users, :roles, :members, :repositories, :issues, :issue_statuses, :changesets, :changes, :issue_categories, :enumerations, :custom_fields, :custom_values, :trackers
26 26
27 27 # No '..' in the repository path for svn
28 28 REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/subversion_repository'
29 29
30 30 def setup
31 31 @controller = RepositoriesController.new
32 32 @request = ActionController::TestRequest.new
33 33 @response = ActionController::TestResponse.new
34 34 Setting.default_language = 'en'
35 35 User.current = nil
36 36 end
37 37
38 38 if File.directory?(REPOSITORY_PATH)
39 39 def test_show
40 40 get :show, :id => 1
41 41 assert_response :success
42 42 assert_template 'show'
43 43 assert_not_nil assigns(:entries)
44 44 assert_not_nil assigns(:changesets)
45 45 end
46 46
47 47 def test_browse_root
48 48 get :browse, :id => 1
49 49 assert_response :success
50 50 assert_template 'browse'
51 51 assert_not_nil assigns(:entries)
52 52 entry = assigns(:entries).detect {|e| e.name == 'subversion_test'}
53 53 assert_equal 'dir', entry.kind
54 54 end
55 55
56 56 def test_browse_directory
57 57 get :browse, :id => 1, :path => ['subversion_test']
58 58 assert_response :success
59 59 assert_template 'browse'
60 60 assert_not_nil assigns(:entries)
61 61 assert_equal ['folder', '.project', 'helloworld.c', 'textfile.txt'], assigns(:entries).collect(&:name)
62 62 entry = assigns(:entries).detect {|e| e.name == 'helloworld.c'}
63 63 assert_equal 'file', entry.kind
64 64 assert_equal 'subversion_test/helloworld.c', entry.path
65 65 end
66 66
67 67 def test_browse_at_given_revision
68 68 get :browse, :id => 1, :path => ['subversion_test'], :rev => 4
69 69 assert_response :success
70 70 assert_template 'browse'
71 71 assert_not_nil assigns(:entries)
72 72 assert_equal ['folder', '.project', 'helloworld.c', 'helloworld.rb', 'textfile.txt'], assigns(:entries).collect(&:name)
73 73 end
74
75 def test_changes
76 get :changes, :id => 1, :path => ['subversion_test', 'folder', 'helloworld.rb' ]
77 assert_response :success
78 assert_template 'changes'
79 # svn properties
80 assert_not_nil assigns(:properties)
81 assert_equal 'native', assigns(:properties)['svn:eol-style']
82 assert_tag :ul,
83 :child => { :tag => 'li',
84 :child => { :tag => 'b', :content => 'svn:eol-style' },
85 :child => { :tag => 'span', :content => 'native' } }
86 end
74 87
75 88 def test_entry
76 89 get :entry, :id => 1, :path => ['subversion_test', 'helloworld.c']
77 90 assert_response :success
78 91 assert_template 'entry'
79 92 end
80 93
81 94 def test_entry_at_given_revision
82 95 get :entry, :id => 1, :path => ['subversion_test', 'helloworld.rb'], :rev => 2
83 96 assert_response :success
84 97 assert_template 'entry'
85 98 # this line was removed in r3 and file was moved in r6
86 99 assert_tag :tag => 'td', :attributes => { :class => /line-code/},
87 100 :content => /Here's the code/
88 101 end
89 102
90 103 def test_entry_not_found
91 104 get :entry, :id => 1, :path => ['subversion_test', 'zzz.c']
92 105 assert_tag :tag => 'div', :attributes => { :class => /error/ },
93 106 :content => /The entry or revision was not found in the repository/
94 107 end
95 108
96 109 def test_entry_download
97 110 get :entry, :id => 1, :path => ['subversion_test', 'helloworld.c'], :format => 'raw'
98 111 assert_response :success
99 112 end
100 113
101 114 def test_directory_entry
102 115 get :entry, :id => 1, :path => ['subversion_test', 'folder']
103 116 assert_response :success
104 117 assert_template 'browse'
105 118 assert_not_nil assigns(:entry)
106 119 assert_equal 'folder', assigns(:entry).name
107 120 end
108 121
109 122 def test_revision
110 123 get :revision, :id => 1, :rev => 2
111 124 assert_response :success
112 125 assert_template 'revision'
113 126 assert_tag :tag => 'tr',
114 127 :child => { :tag => 'td',
115 128 # link to the entry at rev 2
116 129 :child => { :tag => 'a', :attributes => {:href => 'repositories/entry/ecookbook/test/some/path/in/the/repo?rev=2'},
117 130 :content => %r{/test/some/path/in/the/repo} }
118 131 },
119 132 :child => { :tag => 'td',
120 133 # link to partial diff
121 134 :child => { :tag => 'a', :attributes => { :href => '/repositories/diff/ecookbook/test/some/path/in/the/repo?rev=2' } }
122 135 }
123 136 end
124 137
125 138 def test_revision_with_repository_pointing_to_a_subdirectory
126 139 r = Project.find(1).repository
127 140 # Changes repository url to a subdirectory
128 141 r.update_attribute :url, (r.url + '/test/some')
129 142
130 143 get :revision, :id => 1, :rev => 2
131 144 assert_response :success
132 145 assert_template 'revision'
133 146 assert_tag :tag => 'tr',
134 147 :child => { :tag => 'td', :content => %r{/test/some/path/in/the/repo} },
135 148 :child => { :tag => 'td',
136 149 :child => { :tag => 'a', :attributes => { :href => '/repositories/diff/ecookbook/path/in/the/repo?rev=2' } }
137 150 }
138 151 end
139 152
140 153 def test_diff
141 154 get :diff, :id => 1, :rev => 3
142 155 assert_response :success
143 156 assert_template 'diff'
144 157 end
145 158
146 159 def test_annotate
147 160 get :annotate, :id => 1, :path => ['subversion_test', 'helloworld.c']
148 161 assert_response :success
149 162 assert_template 'annotate'
150 163 end
151 164 else
152 165 puts "Subversion test repository NOT FOUND. Skipping functional tests !!!"
153 166 def test_fake; assert true end
154 167 end
155 168 end
General Comments 0
You need to be logged in to leave comments. Login now