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