summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--app/controllers/projects_controller.rb16
-rw-r--r--app/controllers/repositories_controller.rb72
-rw-r--r--app/helpers/application_helper.rb4
-rw-r--r--app/helpers/repositories_helper.rb19
-rw-r--r--app/models/permission.rb1
-rw-r--r--app/models/project.rb2
-rw-r--r--app/models/repository.rb28
-rw-r--r--app/models/svn_repos.rb214
-rw-r--r--app/views/layouts/base.rhtml2
-rw-r--r--app/views/projects/_form.rhtml13
-rw-r--r--app/views/repositories/_dir_list.rhtml23
-rw-r--r--app/views/repositories/_navigation.rhtml18
-rw-r--r--app/views/repositories/browse.rhtml11
-rw-r--r--app/views/repositories/diff.rhtml55
-rw-r--r--app/views/repositories/revision.rhtml35
-rw-r--r--app/views/repositories/revisions.rhtml38
-rw-r--r--app/views/repositories/show.rhtml15
-rw-r--r--config/routes.rb3
-rw-r--r--db/migrate/015_create_repositories.rb12
-rw-r--r--db/migrate/016_add_repositories_permissions.rb19
-rw-r--r--doc/CHANGELOG1
-rw-r--r--lang/de.yml13
-rw-r--r--lang/en.yml13
-rw-r--r--lang/es.yml13
-rw-r--r--lang/fr.yml13
-rw-r--r--public/images/file.pngbin0 -> 278 bytes
-rw-r--r--public/images/folder.pngbin0 -> 1021 bytes
-rw-r--r--public/stylesheets/application.css2
-rw-r--r--public/stylesheets/scm.css66
29 files changed, 715 insertions, 6 deletions
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index b0b00bebf..e4a47d3d1 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -62,6 +62,10 @@ class ProjectsController < ApplicationController
@project.custom_fields = CustomField.find(@params[:custom_field_ids]) if @params[:custom_field_ids]
@custom_values = ProjectCustomField.find(:all).collect { |x| CustomValue.new(:custom_field => x, :customized => @project, :value => params["custom_fields"][x.id.to_s]) }
@project.custom_values = @custom_values
+ if params[:repository_enabled] && params[:repository_enabled] == "1"
+ @project.repository = Repository.new
+ @project.repository.attributes = params[:repository]
+ end
if @project.save
flash[:notice] = l(:notice_successful_create)
redirect_to :controller => 'admin', :action => 'projects'
@@ -96,7 +100,17 @@ class ProjectsController < ApplicationController
@custom_values = ProjectCustomField.find(:all).collect { |x| CustomValue.new(:custom_field => x, :customized => @project, :value => params["custom_fields"][x.id.to_s]) }
@project.custom_values = @custom_values
end
- if @project.update_attributes(params[:project])
+ if params[:repository_enabled]
+ case params[:repository_enabled]
+ when "0"
+ @project.repository = nil
+ when "1"
+ @project.repository ||= Repository.new
+ @project.repository.attributes = params[:repository]
+ end
+ end
+ @project.attributes = params[:project]
+ if @project.save
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'settings', :id => @project
else
diff --git a/app/controllers/repositories_controller.rb b/app/controllers/repositories_controller.rb
new file mode 100644
index 000000000..9dbbfebd9
--- /dev/null
+++ b/app/controllers/repositories_controller.rb
@@ -0,0 +1,72 @@
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class RepositoriesController < ApplicationController
+ layout 'base'
+ before_filter :find_project, :authorize
+
+ def show
+ @entries = @repository.scm.entries('')
+ show_error and return unless @entries
+ @latest_revision = @entries.revisions.latest
+ end
+
+ def browse
+ @entries = @repository.scm.entries(@path, @rev)
+ show_error and return unless @entries
+ end
+
+ def revisions
+ @entry = @repository.scm.entry(@path, @rev)
+ @revisions = @repository.scm.revisions(@path, @rev)
+ show_error and return unless @entry && @revisions
+ end
+
+ def entry
+ if 'raw' == params[:format]
+ content = @repository.scm.cat(@path, @rev)
+ show_error and return unless content
+ send_data content, :filename => @path.split('/').last
+ end
+ end
+
+ def revision
+ @revisions = @repository.scm.revisions '', @rev, @rev, :with_paths => true
+ show_error and return unless @revisions
+ @revision = @revisions.first
+ end
+
+ def diff
+ @rev_to = params[:rev_to] || (@rev-1)
+ @diff = @repository.scm.diff(params[:path], @rev, @rev_to)
+ show_error and return unless @diff
+ end
+
+private
+ def find_project
+ @project = Project.find(params[:id])
+ @repository = @project.repository
+ @path = params[:path].squeeze('/').gsub(/^\//, '') if params[:path]
+ @path ||= ''
+ @rev = params[:rev].to_i if params[:rev] and params[:rev].to_i > 0
+ end
+
+ def show_error
+ flash.now[:notice] = l(:notice_scm_error)
+ render :nothing => true, :layout => true
+ end
+end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 0ca2568ae..29494d707 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -164,7 +164,7 @@ class TabularFormBuilder < ActionView::Helpers::FormBuilder
return super if options.delete :no_label
label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
label = @template.content_tag("label", label_text,
- :class => (@object.errors[field] ? "error" : nil),
+ :class => (@object && @object.errors[field] ? "error" : nil),
:for => (@object_name.to_s + "_" + field.to_s))
label + super
end
@@ -175,7 +175,7 @@ class TabularFormBuilder < ActionView::Helpers::FormBuilder
def select(field, choices, options = {}, html_options = {})
label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
label = @template.content_tag("label", label_text,
- :class => (@object.errors[field] ? "error" : nil),
+ :class => (@object && @object.errors[field] ? "error" : nil),
:for => (@object_name.to_s + "_" + field.to_s))
label + super
end
diff --git a/app/helpers/repositories_helper.rb b/app/helpers/repositories_helper.rb
new file mode 100644
index 000000000..2c7dcdd53
--- /dev/null
+++ b/app/helpers/repositories_helper.rb
@@ -0,0 +1,19 @@
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+module RepositoriesHelper
+end
diff --git a/app/models/permission.rb b/app/models/permission.rb
index b9b61e619..ee4ae56b8 100644
--- a/app/models/permission.rb
+++ b/app/models/permission.rb
@@ -30,6 +30,7 @@ class Permission < ActiveRecord::Base
1100 => :label_news_plural,
1200 => :label_document_plural,
1300 => :label_attachment_plural,
+ 1400 => :label_repository
}.freeze
@@cached_perms_for_public = nil
diff --git a/app/models/project.rb b/app/models/project.rb
index e8493cb3b..702ccc07c 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -25,12 +25,14 @@ class Project < ActiveRecord::Base
has_many :documents, :dependent => true
has_many :news, :dependent => true, :include => :author
has_many :issue_categories, :dependent => true, :order => "issue_categories.name"
+ has_one :repository, :dependent => true
has_and_belongs_to_many :custom_fields, :class_name => 'IssueCustomField', :join_table => 'custom_fields_projects', :association_foreign_key => 'custom_field_id'
acts_as_tree :order => "name", :counter_cache => true
validates_presence_of :name, :description
validates_uniqueness_of :name
validates_associated :custom_values, :on => :update
+ validates_associated :repository
validates_format_of :name, :with => /^[\w\s\'\-]*$/i
# returns 5 last created projects
diff --git a/app/models/repository.rb b/app/models/repository.rb
new file mode 100644
index 000000000..28f2c5a16
--- /dev/null
+++ b/app/models/repository.rb
@@ -0,0 +1,28 @@
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class Repository < ActiveRecord::Base
+ belongs_to :project
+ validates_presence_of :url
+ validates_format_of :url, :with => /^(http|https|svn):\/\/.+/i
+
+ @scm = nil
+
+ def scm
+ @scm ||= SvnRepos::Base.new url
+ end
+end
diff --git a/app/models/svn_repos.rb b/app/models/svn_repos.rb
new file mode 100644
index 000000000..55a9f3ea4
--- /dev/null
+++ b/app/models/svn_repos.rb
@@ -0,0 +1,214 @@
+# redMine - project management software
+# Copyright (C) 2006 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require 'rexml/document'
+
+module SvnRepos
+
+ class CommandFailed < StandardError #:nodoc:
+ end
+
+ class Base
+ @url = nil
+ @login = nil
+ @password = nil
+
+ def initialize(url, login=nil, password=nil)
+ @url = url
+ @login = login if login && !login.empty?
+ @password = (password || "") if @login
+ end
+
+ # Returns the entry identified by path and revision identifier
+ # or nil if entry doesn't exist in the repository
+ def entry(path=nil, identifier=nil)
+ e = entries(path, identifier)
+ e ? e.first : nil
+ end
+
+ # Returns an Entries collection
+ # or nil if the given path doesn't exist in the repository
+ def entries(path=nil, identifier=nil)
+ path ||= ''
+ identifier = 'HEAD' unless identifier and identifier > 0
+ entries = Entries.new
+ cmd = "svn list --xml #{target(path)}@#{identifier}"
+ shellout(cmd) do |io|
+ begin
+ doc = REXML::Document.new(io)
+ doc.elements.each("lists/list/entry") do |entry|
+ entries << Entry.new({:name => entry.elements['name'].text,
+ :path => ((path.empty? ? "" : "#{path}/") + entry.elements['name'].text),
+ :kind => entry.attributes['kind'],
+ :size => (entry.elements['size'] and entry.elements['size'].text).to_i,
+ :lastrev => Revision.new({
+ :identifier => entry.elements['commit'].attributes['revision'],
+ :time => Time.parse(entry.elements['commit'].elements['date'].text),
+ :author => entry.elements['commit'].elements['author'].text
+ })
+ })
+ end
+ rescue
+ end
+ end
+ return nil if $? && $?.exitstatus != 0
+ entries.sort_by_name
+ rescue Errno::ENOENT => e
+ raise CommandFailed
+ end
+
+ def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
+ path ||= ''
+ identifier_from = 'HEAD' unless identifier_from and identifier_from.to_i > 0
+ identifier_to = 1 unless identifier_to and identifier_to.to_i > 0
+ revisions = Revisions.new
+ cmd = "svn log --xml -r #{identifier_from}:#{identifier_to} "
+ cmd << "--verbose " if options[:with_paths]
+ cmd << target(path)
+ shellout(cmd) do |io|
+ begin
+ doc = REXML::Document.new(io)
+ doc.elements.each("log/logentry") do |logentry|
+ paths = []
+ logentry.elements.each("paths/path") do |path|
+ paths << {:action => path.attributes['action'],
+ :path => path.text
+ }
+ end
+ paths.sort! { |x,y| x[:path] <=> y[:path] }
+
+ revisions << Revision.new({:identifier => logentry.attributes['revision'],
+ :author => logentry.elements['author'].text,
+ :time => Time.parse(logentry.elements['date'].text),
+ :message => logentry.elements['msg'].text,
+ :paths => paths
+ })
+ end
+ rescue
+ end
+ end
+ return nil if $? && $?.exitstatus != 0
+ revisions
+ rescue Errno::ENOENT => e
+ raise CommandFailed
+ end
+
+ def diff(path, identifier_from, identifier_to=nil)
+ path ||= ''
+ if identifier_to and identifier_to.to_i > 0
+ identifier_to = identifier_to.to_i
+ else
+ identifier_to = identifier_from.to_i - 1
+ end
+ cmd = "svn diff -r "
+ cmd << "#{identifier_to}:"
+ cmd << "#{identifier_from}"
+ cmd << "#{target(path)}@#{identifier_from}"
+ diff = []
+ shellout(cmd) do |io|
+ io.each_line do |line|
+ diff << line
+ end
+ end
+ return nil if $? && $?.exitstatus != 0
+ diff
+ rescue Errno::ENOENT => e
+ raise CommandFailed
+ end
+
+ def cat(path, identifier=nil)
+ identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
+ cmd = "svn cat #{target(path)}@#{identifier}"
+ cat = nil
+ shellout(cmd) do |io|
+ cat = io.read
+ end
+ return nil if $? && $?.exitstatus != 0
+ cat
+ rescue Errno::ENOENT => e
+ raise CommandFailed
+ end
+
+ private
+ def target(path)
+ " \"" << "#{@url}/#{path}".gsub(/["'?<>\*]/, '') << "\""
+ end
+
+ def logger
+ RAILS_DEFAULT_LOGGER
+ end
+
+ def shellout(cmd, &block)
+ logger.debug "Shelling out: #{cmd}" if logger && logger.debug?
+ IO.popen(cmd) do |io|
+ block.call(io) if block_given?
+ end
+ end
+ end
+
+ class Entries < Array
+ def sort_by_name
+ sort {|x,y|
+ if x.kind == y.kind
+ x.name <=> y.name
+ else
+ x.kind <=> y.kind
+ end
+ }
+ end
+
+ def revisions
+ revisions ||= Revisions.new(collect{|entry| entry.lastrev})
+ end
+ end
+
+ class Entry
+ attr_accessor :name, :path, :kind, :size, :lastrev
+ def initialize(attributes={})
+ self.name = attributes[:name] if attributes[:name]
+ self.path = attributes[:path] if attributes[:path]
+ self.kind = attributes[:kind] if attributes[:kind]
+ self.size = attributes[:size].to_i if attributes[:size]
+ self.lastrev = attributes[:lastrev]
+ end
+
+ def is_file?
+ 'file' == self.kind
+ end
+
+ def is_dir?
+ 'dir' == self.kind
+ end
+ end
+
+ class Revisions < Array
+ def latest
+ sort {|x,y| x.time <=> y.time}.last
+ end
+ end
+
+ class Revision
+ attr_accessor :identifier, :author, :time, :message, :paths
+ def initialize(attributes={})
+ self.identifier = attributes[:identifier]
+ self.author = attributes[:author]
+ self.time = attributes[:time]
+ self.message = attributes[:message] || ""
+ self.paths = attributes[:paths]
+ end
+ end
+end \ No newline at end of file
diff --git a/app/views/layouts/base.rhtml b/app/views/layouts/base.rhtml
index 79dd88cb9..85b550b75 100644
--- a/app/views/layouts/base.rhtml
+++ b/app/views/layouts/base.rhtml
@@ -91,6 +91,7 @@
<%= link_to l(:label_document_plural), {:controller => 'projects', :action => 'list_documents', :id => @project }, :class => "menuItem" %>
<%= link_to l(:label_member_plural), {:controller => 'projects', :action => 'list_members', :id => @project }, :class => "menuItem" %>
<%= link_to l(:label_attachment_plural), {:controller => 'projects', :action => 'list_files', :id => @project }, :class => "menuItem" %>
+ <%= link_to l(:label_repository), {:controller => 'repositories', :action => 'show', :id => @project}, :class => "menuItem" if @project.repository and !@project.repository.new_record? %></li>
<%= link_to_if_authorized l(:label_settings), {:controller => 'projects', :action => 'settings', :id => @project }, :class => "menuItem" %>
</div>
<% end %>
@@ -112,6 +113,7 @@
<li><%= link_to l(:label_document_plural), :controller => 'projects', :action => 'list_documents', :id => @project %></li>
<li><%= link_to l(:label_member_plural), :controller => 'projects', :action => 'list_members', :id => @project %></li>
<li><%= link_to l(:label_attachment_plural), :controller => 'projects', :action => 'list_files', :id => @project %></li>
+ <li><%= link_to l(:label_repository), :controller => 'repositories', :action => 'show', :id => @project if @project.repository and !@project.repository.new_record? %></li>
<li><%= link_to_if_authorized l(:label_settings), :controller => 'projects', :action => 'settings', :id => @project %></li>
</ul>
<% end %>
diff --git a/app/views/projects/_form.rhtml b/app/views/projects/_form.rhtml
index ab0b35fab..a6102e012 100644
--- a/app/views/projects/_form.rhtml
+++ b/app/views/projects/_form.rhtml
@@ -1,4 +1,5 @@
<%= error_messages_for 'project' %>
+
<div class="box">
<!--[form:project]-->
<p><%= f.text_field :name, :required => true %></p>
@@ -22,5 +23,15 @@
<%= custom_field.name %>
<% end %></p>
<% end %>
-<!--[eoform:project]-->
+<!--[eoform:project]-->
+</div>
+
+<div class="box"><h3><%= check_box_tag "repository_enabled", 1, !@project.repository.nil?, :onclick => "Element.toggle('repository');" %> <%= l(:label_repository) %></h3>
+<%= hidden_field_tag "repository_enabled", 0 %>
+<div id="repository">
+<% fields_for :repository, @project.repository, { :builder => TabularFormBuilder, :lang => current_language} do |repository| %>
+<p><%= repository.text_field :url, :size => 60, :required => true %><br />(http://, https://, svn://)</p>
+<% end %>
+</div>
+<%= javascript_tag "Element.hide('repository');" if @project.repository.nil? %>
</div>
diff --git a/app/views/repositories/_dir_list.rhtml b/app/views/repositories/_dir_list.rhtml
new file mode 100644
index 000000000..635fba528
--- /dev/null
+++ b/app/views/repositories/_dir_list.rhtml
@@ -0,0 +1,23 @@
+<table class="list">
+<thead><tr>
+<th><%= l(:field_name) %></th>
+<th><%= l(:field_filesize) %></th>
+<th><%= l(:label_revision) %></th>
+<th><%= l(:field_author) %></th>
+<th><%= l(:label_date) %></th>
+</tr></thead>
+<tbody>
+<% total_size = 0
+@entries.each do |entry| %>
+<tr class="<%= cycle 'odd', 'even' %>">
+<td><%= link_to h(entry.name), { :action => (entry.is_dir? ? 'browse' : 'revisions'), :id => @project, :path => entry.path, :rev => @rev }, :class => "icon " + (entry.is_dir? ? 'folder' : 'file') %></td>
+<td align="right"><%= human_size(entry.size) unless entry.is_dir? %></td>
+<td align="right"><%= link_to entry.lastrev.identifier, :action => 'revision', :id => @project, :rev => entry.lastrev.identifier %></td>
+<td align="center"><em><%=h entry.lastrev.author %></em></td>
+<td align="center"><%= format_time(entry.lastrev.time) %></td>
+</tr>
+<% total_size += entry.size
+end %>
+</tbody>
+</table>
+<p align="right"><em><%= l(:label_total) %>: <%= human_size(total_size) %></em></p> \ No newline at end of file
diff --git a/app/views/repositories/_navigation.rhtml b/app/views/repositories/_navigation.rhtml
new file mode 100644
index 000000000..3ae0f7612
--- /dev/null
+++ b/app/views/repositories/_navigation.rhtml
@@ -0,0 +1,18 @@
+<%= link_to 'root', :action => 'browse', :id => @project, :path => '', :rev => @rev %>
+<%
+dirs = path.split('/')
+if 'file' == kind
+ filename = dirs.pop
+end
+link_path = ''
+dirs.each do |dir|
+ link_path << '/' unless link_path.empty?
+ link_path << "#{dir}"
+ %>
+ / <%= link_to h(dir), :action => 'browse', :id => @project, :path => link_path, :rev => @rev %>
+<% end %>
+<% if filename %>
+ / <%= link_to h(filename), :action => 'revisions', :id => @project, :path => "#{link_path}/#{filename}", :rev => @rev %>
+<% end %>
+
+<%= "@ #{revision}" if revision %> \ No newline at end of file
diff --git a/app/views/repositories/browse.rhtml b/app/views/repositories/browse.rhtml
new file mode 100644
index 000000000..92ad8478b
--- /dev/null
+++ b/app/views/repositories/browse.rhtml
@@ -0,0 +1,11 @@
+<%= stylesheet_link_tag "scm" %>
+
+<div class="contextual">
+<%= start_form_tag %>
+<%= l(:label_revision) %>: <%= text_field_tag 'rev', @rev, :size => 5 %>
+<%= submit_tag 'OK' %>
+</div>
+
+<h2><%= render :partial => 'navigation', :locals => { :path => @path, :kind => 'dir', :revision => @rev } %></h2>
+
+<%= render :partial => 'dir_list' %> \ No newline at end of file
diff --git a/app/views/repositories/diff.rhtml b/app/views/repositories/diff.rhtml
new file mode 100644
index 000000000..d4350cb61
--- /dev/null
+++ b/app/views/repositories/diff.rhtml
@@ -0,0 +1,55 @@
+<h2><%= render :partial => 'navigation', :locals => { :path => @path, :kind => 'file', :revision => @rev } %></h2>
+
+<%= stylesheet_link_tag "scm" %>
+
+<table class="list">
+<thead><tr><th>@<%= @rev %></th><th>@<%= @rev_to %></th><th></th></tr></thead>
+<tbody>
+<% parsing = false
+line_num_l = 0
+line_num_r = 0 %>
+<% @diff.each do |line| %>
+<%
+ if line =~ /^@@ (\+|\-)(\d+),\d+ (\+|\-)(\d+),\d+ @@/
+ line_num_l = $2.to_i
+ line_num_r = $4.to_i
+ if parsing %>
+ <tr class="spacing"><td colspan="3">&nbsp;</td></tr>
+ <% end
+ parsing = true
+ next
+ end
+ next unless parsing
+%>
+
+<tr>
+
+<% case line[0, 1]
+ when " " %>
+<th class="line-num"><%= line_num_l %></th>
+<th class="line-num"><%= line_num_r %></th>
+<td class="line-code">
+<% line_num_l = line_num_l + 1
+ line_num_r = line_num_r + 1
+
+ when "-" %>
+<th class="line-num"></th>
+<th class="line-num"><%= line_num_r %></th>
+<td class="line-code" style="background: #fdd;">
+<% line_num_r = line_num_r + 1
+
+ when "+" %>
+<th class="line-num"><%= line_num_l %></th>
+<th class="line-num"></th>
+<td class="line-code" style="background: #dfd;">
+<% line_num_l = line_num_l + 1
+
+ else
+ next
+ end %>
+
+<%= h(line[1..-1]).gsub(/\s/, "&nbsp;") %></td></tr>
+
+<% end %>
+</tbody>
+</table> \ No newline at end of file
diff --git a/app/views/repositories/revision.rhtml b/app/views/repositories/revision.rhtml
new file mode 100644
index 000000000..6a5a20d97
--- /dev/null
+++ b/app/views/repositories/revision.rhtml
@@ -0,0 +1,35 @@
+<%= stylesheet_link_tag "scm" %>
+
+<div class="contextual">
+<%= start_form_tag %>
+<%= l(:label_revision) %>: <%= text_field_tag 'rev', @rev, :size => 5 %>
+<%= submit_tag 'OK' %>
+</div>
+
+<h2><%= l(:label_revision) %> <%= @revision.identifier %></h2>
+
+<p><em><%= @revision.author %>, <%= format_time(@revision.time) %></em></p>
+<%= simple_format @revision.message %>
+
+<div style="float:right;">
+<div class="square action_A"></div> <div style="float:left;"><%= l(:label_added) %>&nbsp;</div>
+<div class="square action_M"></div> <div style="float:left;"><%= l(:label_modified) %>&nbsp;</div>
+<div class="square action_D"></div> <div style="float:left;"><%= l(:label_deleted) %>&nbsp;</div>
+</div>
+
+<h3><%= l(:label_attachment_plural) %></h3>
+<table class="list">
+<tbody>
+<% @revision.paths.each do |path| %>
+<tr class="<%= cycle 'odd', 'even' %>">
+<td><div class="square action_<%= path[:action] %>"></div> <%= path[:path] %></td>
+<td>
+<% if path[:action] == "M" %>
+<%= link_to 'View diff', :action => 'diff', :id => @project, :path => path[:path].gsub(/^\//, ''), :rev => @revision.identifier %>
+<% end %>
+</td>
+</tr>
+<% end %>
+</tbody>
+</table>
+<p><%= lwr(:label_modification, @revision.paths.length) %></p> \ No newline at end of file
diff --git a/app/views/repositories/revisions.rhtml b/app/views/repositories/revisions.rhtml
new file mode 100644
index 000000000..c2e30d30c
--- /dev/null
+++ b/app/views/repositories/revisions.rhtml
@@ -0,0 +1,38 @@
+<%= stylesheet_link_tag "scm" %>
+
+<div class="contextual">
+<%= start_form_tag %>
+<%= l(:label_revision) %>: <%= text_field_tag 'rev', @rev, :size => 5 %>
+<%= submit_tag 'OK' %>
+</div>
+
+<h2><%= render :partial => 'navigation', :locals => { :path => @path, :kind => @entry.kind, :revision => @rev } %></h2>
+
+<% if @entry.is_file? %>
+<h3><%=h @entry.name %></h3>
+<p><%= link_to 'Download', {:action => 'entry', :id => @project, :path => @path, :rev => @rev, :format => 'raw' }, :class => "icon file" %> (<%= human_size @entry.size %>)</p>
+<% end %>
+
+<h3>Revisions</h3>
+
+<table class="list">
+<thead><tr>
+<th>#</th>
+<th><%= l(:field_author) %></th>
+<th><%= l(:label_date) %></th>
+<th><%= l(:field_description) %></th>
+<th></th>
+</tr></thead>
+<tbody>
+<% @revisions.each do |revision| %>
+<tr class="<%= cycle 'odd', 'even' %>">
+<th align="center"><%= link_to revision.identifier, :action => 'revision', :id => @project, :rev => revision.identifier %></th>
+<td align="center"><em><%=h revision.author %></em></td>
+<td align="center"><%= format_time(revision.time) %></td>
+<td width="70%"><%= simple_format(h(revision.message)) %></td>
+<td align="center"><%= link_to 'Diff', :action => 'diff', :id => @project, :path => @path, :rev => revision.identifier if @entry.is_file? && revision != @revisions.last %></td>
+</tr>
+<% end %>
+</tbody>
+</table>
+<p><%= lwr(:label_modification, @revisions.length) %></p> \ No newline at end of file
diff --git a/app/views/repositories/show.rhtml b/app/views/repositories/show.rhtml
new file mode 100644
index 000000000..4c95f8844
--- /dev/null
+++ b/app/views/repositories/show.rhtml
@@ -0,0 +1,15 @@
+<%= stylesheet_link_tag "scm" %>
+
+<h2><%= l(:label_repository) %></h2>
+
+<h3><%= l(:label_revision_plural) %></h3>
+<% if @latest_revision %>
+ <p><%= l(:label_latest_revision) %>:
+ <%= link_to @latest_revision.identifier, :action => 'revision', :id => @project, :rev => @latest_revision.identifier %><br />
+ <em><%= @latest_revision.author %>, <%= format_time(@latest_revision.time) %></em></p>
+<% end %>
+<p><%= link_to l(:label_view_revisions), :action => 'revisions', :id => @project %></p>
+
+
+<h3><%= l(:label_browse) %></h3>
+<%= render :partial => 'dir_list' %> \ No newline at end of file
diff --git a/config/routes.rb b/config/routes.rb
index 2559159f1..0871cec79 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -9,7 +9,8 @@ ActionController::Routing::Routes.draw do |map|
# You can have the root of your site routed by hooking up ''
# -- just remember to delete public/index.html.
map.connect '', :controller => "welcome"
-
+
+ map.connect 'repositories/:action/:id/:path', :controller => 'repositories'
map.connect 'roles/workflow/:id/:role_id/:tracker_id', :controller => 'roles', :action => 'workflow'
map.connect 'help/:ctrl/:page', :controller => 'help'
map.connect ':controller/:action/:id/:sort_key/:sort_order'
diff --git a/db/migrate/015_create_repositories.rb b/db/migrate/015_create_repositories.rb
new file mode 100644
index 000000000..d8c0524b3
--- /dev/null
+++ b/db/migrate/015_create_repositories.rb
@@ -0,0 +1,12 @@
+class CreateRepositories < ActiveRecord::Migration
+ def self.up
+ create_table :repositories, :force => true do |t|
+ t.column "project_id", :integer, :default => 0, :null => false
+ t.column "url", :string, :default => "", :null => false
+ end
+ end
+
+ def self.down
+ drop_table :repositories
+ end
+end
diff --git a/db/migrate/016_add_repositories_permissions.rb b/db/migrate/016_add_repositories_permissions.rb
new file mode 100644
index 000000000..992f8dccd
--- /dev/null
+++ b/db/migrate/016_add_repositories_permissions.rb
@@ -0,0 +1,19 @@
+class AddRepositoriesPermissions < ActiveRecord::Migration
+ def self.up
+ Permission.create :controller => "repositories", :action => "show", :description => "button_view", :sort => 1450, :is_public => true
+ Permission.create :controller => "repositories", :action => "browse", :description => "label_browse", :sort => 1460, :is_public => true
+ Permission.create :controller => "repositories", :action => "entry", :description => "entry", :sort => 1462, :is_public => true
+ Permission.create :controller => "repositories", :action => "revisions", :description => "label_view_revisions", :sort => 1470, :is_public => true
+ Permission.create :controller => "repositories", :action => "revision", :description => "label_view_revisions", :sort => 1472, :is_public => true
+ Permission.create :controller => "repositories", :action => "diff", :description => "diff", :sort => 1480, :is_public => true
+ end
+
+ def self.down
+ Permission.find(:first, :conditions => ["controller=? and action=?", 'repositories', 'show']).destroy
+ Permission.find(:first, :conditions => ["controller=? and action=?", 'repositories', 'browse']).destroy
+ Permission.find(:first, :conditions => ["controller=? and action=?", 'repositories', 'entry']).destroy
+ Permission.find(:first, :conditions => ["controller=? and action=?", 'repositories', 'revisions']).destroy
+ Permission.find(:first, :conditions => ["controller=? and action=?", 'repositories', 'revision']).destroy
+ Permission.find(:first, :conditions => ["controller=? and action=?", 'repositories', 'diff']).destroy
+ end
+end
diff --git a/doc/CHANGELOG b/doc/CHANGELOG
index 4c4ba17c7..36bac591b 100644
--- a/doc/CHANGELOG
+++ b/doc/CHANGELOG
@@ -7,6 +7,7 @@ http://redmine.org/
== xx/xx/2006 v0.x.x
+* simple SVN browser added (just needs svn binaries in PATH)
* comments can now be added on news
* "my page" is now customizable
* more powerfull and savable filters for issues lists
diff --git a/lang/de.yml b/lang/de.yml
index aa091961b..34fedc1ea 100644
--- a/lang/de.yml
+++ b/lang/de.yml
@@ -63,6 +63,7 @@ notice_successful_delete: Erfolgreiche Auslassung.
notice_successful_connection: Erfolgreicher Anschluß.
notice_file_not_found: Erbetene Akte besteht nicht oder ist gelöscht worden.
notice_locking_conflict: Data have been updated by another user.
+notice_scm_error: Eintragung und/oder Neuausgabe besteht nicht im Behälter.
mail_subject_lost_password: Dein redMine Kennwort
mail_subject_register: redMine Kontoaktivierung
@@ -136,6 +137,7 @@ field_start_date: Beginn
field_done_ratio: %% Getan
field_hide_mail: Mein email address verstecken
field_comment: Anmerkung
+field_url: URL
label_user: Benutzer
label_user_plural: Benutzer
@@ -282,6 +284,17 @@ label_ago: vor
label_contains: enthält
label_not_contains: enthält nicht
label_day_plural: Tage
+label_repository: SVN Behälter
+label_browse: Grasen
+label_modification: %d änderung
+label_modification_plural: %d änderungen
+label_revision: Neuausgabe
+label_revision_plural: Neuausgaben
+label_added: hinzugefügt
+label_modified: geändert
+label_deleted: gelöscht
+label_latest_revision: Neueste Neuausgabe
+label_view_revisions: Die Neuausgaben ansehen
button_login: Einloggen
button_submit: Einreichen
diff --git a/lang/en.yml b/lang/en.yml
index 8c2de4bc3..b6734985a 100644
--- a/lang/en.yml
+++ b/lang/en.yml
@@ -63,6 +63,7 @@ notice_successful_delete: Successful deletion.
notice_successful_connection: Successful connection.
notice_file_not_found: Requested file doesn't exist or has been deleted.
notice_locking_conflict: Data have been updated by another user.
+notice_scm_error: Entry and/or revision doesn't exist in the repository.
mail_subject_lost_password: Your redMine password
mail_subject_register: redMine account activation
@@ -136,6 +137,7 @@ field_start_date: Start
field_done_ratio: %% Done
field_hide_mail: Hide my email address
field_comment: Comment
+field_url: URL
label_user: User
label_user_plural: Users
@@ -282,6 +284,17 @@ label_ago: days ago
label_contains: contains
label_not_contains: doesn't contain
label_day_plural: days
+label_repository: SVN Repository
+label_browse: Browse
+label_modification: %d change
+label_modification_plural: %d changes
+label_revision: Revision
+label_revision_plural: Revisions
+label_added: added
+label_modified: modified
+label_deleted: deleted
+label_latest_revision: Latest revision
+label_view_revisions: View revisions
button_login: Login
button_submit: Submit
diff --git a/lang/es.yml b/lang/es.yml
index ce19ef658..7eb3fa9fd 100644
--- a/lang/es.yml
+++ b/lang/es.yml
@@ -63,6 +63,7 @@ notice_successful_delete: Successful deletion.
notice_successful_connection: Successful connection.
notice_file_not_found: Requested file doesn't exist or has been deleted.
notice_locking_conflict: Data have been updated by another user.
+notice_scm_error: La entrada y/o la revisión no existe en el depósito.
mail_subject_lost_password: Tu contraseña del redMine
mail_subject_register: Activación de la cuenta del redMine
@@ -136,6 +137,7 @@ field_start_date: Comienzo
field_done_ratio: %% Realizado
field_hide_mail: Ocultar mi email address
field_comment: Comentario
+field_url: URL
label_user: Usuario
label_user_plural: Usuarios
@@ -282,6 +284,17 @@ label_ago: hace
label_contains: contiene
label_not_contains: no contiene
label_day_plural: días
+label_repository: Depósito SVN
+label_browse: Hojear
+label_modification: %d modificación
+label_modification_plural: %d modificaciones
+label_revision: Revisión
+label_revision_plural: Revisiones
+label_added: agregado
+label_modified: modificado
+label_deleted: suprimido
+label_latest_revision: La revisión más última
+label_view_revisions: Ver las revisiones
button_login: Conexión
button_submit: Someter
diff --git a/lang/fr.yml b/lang/fr.yml
index a351a5a92..4b4b04925 100644
--- a/lang/fr.yml
+++ b/lang/fr.yml
@@ -63,6 +63,7 @@ notice_successful_delete: Suppression effectuée avec succès.
notice_successful_connection: Connection réussie.
notice_file_not_found: Le fichier demandé n'existe pas ou a été supprimé.
notice_locking_conflict: Les données ont été mises à jour par un autre utilisateur. Mise à jour impossible.
+notice_scm_error: L'entrée et/ou la révision demandée n'existe pas dans le dépôt.
mail_subject_lost_password: Votre mot de passe redMine
mail_subject_register: Activation de votre compte redMine
@@ -137,6 +138,7 @@ field_done_ratio: %% Réalisé
field_auth_source: Mode d'authentification
field_hide_mail: Cacher mon adresse mail
field_comment: Commentaire
+field_url: URL
label_user: Utilisateur
label_user_plural: Utilisateurs
@@ -283,6 +285,17 @@ label_ago: il y a
label_contains: contient
label_not_contains: ne contient pas
label_day_plural: jours
+label_repository: Dépôt SVN
+label_browse: Parcourir
+label_modification: %d modification
+label_modification_plural: %d modifications
+label_revision: Révision
+label_revision_plural: Révisions
+label_added: ajouté
+label_modified: modifié
+label_deleted: supprimé
+label_latest_revision: Dernière révision
+label_view_revisions: Voir les révisions
button_login: Connexion
button_submit: Soumettre
diff --git a/public/images/file.png b/public/images/file.png
new file mode 100644
index 000000000..7a0871941
--- /dev/null
+++ b/public/images/file.png
Binary files differ
diff --git a/public/images/folder.png b/public/images/folder.png
new file mode 100644
index 000000000..32175ebd4
--- /dev/null
+++ b/public/images/folder.png
Binary files differ
diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css
index d85b025fe..25b46c85a 100644
--- a/public/stylesheets/application.css
+++ b/public/stylesheets/application.css
@@ -473,7 +473,7 @@ float: right;
font-size: 0.8em;
}
-.contextual select {
+.contextual select, .contextual input {
font-size: 1em;
}
diff --git a/public/stylesheets/scm.css b/public/stylesheets/scm.css
new file mode 100644
index 000000000..658fb9050
--- /dev/null
+++ b/public/stylesheets/scm.css
@@ -0,0 +1,66 @@
+
+
+div.square {
+ border: 1px solid #999;
+ float: left;
+ margin: .4em .5em 0 0;
+ overflow: hidden;
+ width: .6em; height: .6em;
+}
+
+div.action_M { background: #fd8 }
+div.action_D { background: #f88 }
+div.action_A { background: #bfb }
+
+table.list {
+ width:100%;
+ border-collapse: collapse;
+ border: 1px dotted #d0d0d0;
+ margin-bottom: 6px;
+}
+
+table.list thead th {
+ text-align: center;
+ background: #eee;
+ border: 1px solid #d7d7d7;
+}
+
+table.list tbody th {
+ font-weight: normal;
+ text-align: center;
+ background: #eed;
+ border: 1px solid #d7d7d7;
+}
+
+.icon {
+ background-position: 0% 40%;
+ background-repeat: no-repeat;
+ padding-left: 20px;
+}
+
+.folder { background-image: url(../images/folder.png); }
+.file { background-image: url(../images/file.png); }
+
+
+
+tr.spacing {
+ border: 1px solid #d7d7d7;
+}
+
+.line-num {
+ border: 1px solid #d7d7d7;
+ font-size: 0.8em;
+ text-align: right;
+ width: 3em;
+ padding-right: 3px;
+}
+
+.line-code {
+ font-family: "Courier New", monospace;
+ font-size: 1em;
+}
+
+table p {
+ margin:0;
+ padding:0;
+} \ No newline at end of file