]> source.dussan.org Git - redmine.git/commitdiff
svn browser merged in trunk
authorJean-Philippe Lang <jp_lang@yahoo.fr>
Sun, 24 Dec 2006 13:38:45 +0000 (13:38 +0000)
committerJean-Philippe Lang <jp_lang@yahoo.fr>
Sun, 24 Dec 2006 13:38:45 +0000 (13:38 +0000)
git-svn-id: http://redmine.rubyforge.org/svn/trunk@106 e93f8b46-1217-0410-a6f0-8f06a7374b81

29 files changed:
app/controllers/projects_controller.rb
app/controllers/repositories_controller.rb [new file with mode: 0644]
app/helpers/application_helper.rb
app/helpers/repositories_helper.rb [new file with mode: 0644]
app/models/permission.rb
app/models/project.rb
app/models/repository.rb [new file with mode: 0644]
app/models/svn_repos.rb [new file with mode: 0644]
app/views/layouts/base.rhtml
app/views/projects/_form.rhtml
app/views/repositories/_dir_list.rhtml [new file with mode: 0644]
app/views/repositories/_navigation.rhtml [new file with mode: 0644]
app/views/repositories/browse.rhtml [new file with mode: 0644]
app/views/repositories/diff.rhtml [new file with mode: 0644]
app/views/repositories/revision.rhtml [new file with mode: 0644]
app/views/repositories/revisions.rhtml [new file with mode: 0644]
app/views/repositories/show.rhtml [new file with mode: 0644]
config/routes.rb
db/migrate/015_create_repositories.rb [new file with mode: 0644]
db/migrate/016_add_repositories_permissions.rb [new file with mode: 0644]
doc/CHANGELOG
lang/de.yml
lang/en.yml
lang/es.yml
lang/fr.yml
public/images/file.png [new file with mode: 0644]
public/images/folder.png [new file with mode: 0644]
public/stylesheets/application.css
public/stylesheets/scm.css [new file with mode: 0644]

index b0b00bebf120bfd642f718dc7ea76238cbade28a..e4a47d3d196bbbe87b24ec1f45ddedae2af44be4 100644 (file)
@@ -62,6 +62,10 @@ class ProjectsController < ApplicationController
       @project.custom_fields = CustomField.find(@params[:custom_field_ids]) if @params[:custom_field_ids]\r
       @custom_values = ProjectCustomField.find(:all).collect { |x| CustomValue.new(:custom_field => x, :customized => @project, :value => params["custom_fields"][x.id.to_s]) }\r
       @project.custom_values = @custom_values                  \r
+      if params[:repository_enabled] && params[:repository_enabled] == "1"\r
+        @project.repository = Repository.new\r
+        @project.repository.attributes = params[:repository]\r
+      end\r
       if @project.save
         flash[:notice] = l(:notice_successful_create)
         redirect_to :controller => 'admin', :action => 'projects'\r
@@ -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]) }\r
         @project.custom_values = @custom_values\r
       end\r
-      if @project.update_attributes(params[:project])
+      if params[:repository_enabled]\r
+        case params[:repository_enabled]\r
+        when "0"\r
+          @project.repository = nil\r
+        when "1"\r
+          @project.repository ||= Repository.new\r
+          @project.repository.attributes = params[:repository]\r
+        end\r
+      end\r
+      @project.attributes = params[:project]\r
+      if @project.save
         flash[:notice] = l(:notice_successful_update)
         redirect_to :action => 'settings', :id => @project\r
       else\r
diff --git a/app/controllers/repositories_controller.rb b/app/controllers/repositories_controller.rb
new file mode 100644 (file)
index 0000000..9dbbfeb
--- /dev/null
@@ -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
index 0ca2568ae0595350c1526eb3e952b321731b6fe2..29494d707a8f4a2dd2c52f541de00e7dffdb9ff2 100644 (file)
@@ -164,7 +164,7 @@ class TabularFormBuilder < ActionView::Helpers::FormBuilder
       return super if options.delete :no_label\r
       label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")\r
       label = @template.content_tag("label", label_text, \r
-                    :class => (@object.errors[field] ? "error" : nil), \r
+                    :class => (@object && @object.errors[field] ? "error" : nil), \r
                     :for => (@object_name.to_s + "_" + field.to_s))\r
       label + super\r
     end\r
@@ -175,7 +175,7 @@ class TabularFormBuilder < ActionView::Helpers::FormBuilder
   def select(field, choices, options = {}, html_options = {}) \r
     label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")\r
     label = @template.content_tag("label", label_text, \r
-                  :class => (@object.errors[field] ? "error" : nil), \r
+                  :class => (@object && @object.errors[field] ? "error" : nil), \r
                   :for => (@object_name.to_s + "_" + field.to_s))\r
     label + super\r
   end\r
diff --git a/app/helpers/repositories_helper.rb b/app/helpers/repositories_helper.rb
new file mode 100644 (file)
index 0000000..2c7dcdd
--- /dev/null
@@ -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
index b9b61e619e7572b0ccc10da5c618785a66c1f488..ee4ae56b87e6821f89a6d63df75864e563ab1c96 100644 (file)
@@ -30,6 +30,7 @@ class Permission < ActiveRecord::Base
     1100 => :label_news_plural,\r
     1200 => :label_document_plural,\r
     1300 => :label_attachment_plural,\r
+    1400 => :label_repository\r
   }.freeze\r
   \r
   @@cached_perms_for_public = nil\r
index e8493cb3bf25127f12dc10c7dd26a74aa3e0a9f3..702ccc07c57adb4a844aa2388e73d7584bb216ee 100644 (file)
@@ -25,12 +25,14 @@ class Project < ActiveRecord::Base
   has_many :documents, :dependent => true\r
   has_many :news, :dependent => true, :include => :author\r
   has_many :issue_categories, :dependent => true, :order => "issue_categories.name"\r
+  has_one :repository, :dependent => true\r
   has_and_belongs_to_many :custom_fields, :class_name => 'IssueCustomField', :join_table => 'custom_fields_projects', :association_foreign_key => 'custom_field_id'\r
   acts_as_tree :order => "name", :counter_cache => true\r
 \r
   validates_presence_of :name, :description\r
   validates_uniqueness_of :name\r
   validates_associated :custom_values, :on => :update\r
+  validates_associated :repository\r
   validates_format_of :name, :with => /^[\w\s\'\-]*$/i\r
 \r
   # returns 5 last created projects\r
diff --git a/app/models/repository.rb b/app/models/repository.rb
new file mode 100644 (file)
index 0000000..28f2c5a
--- /dev/null
@@ -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 (file)
index 0000000..55a9f3e
--- /dev/null
@@ -0,0 +1,214 @@
+# redMine - project management software\r
+# Copyright (C) 2006  Jean-Philippe Lang\r
+#\r
+# This program is free software; you can redistribute it and/or\r
+# modify it under the terms of the GNU General Public License\r
+# as published by the Free Software Foundation; either version 2\r
+# of the License, or (at your option) any later version.\r
+# \r
+# This program is distributed in the hope that it will be useful,\r
+# but WITHOUT ANY WARRANTY; without even the implied warranty of\r
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r
+# GNU General Public License for more details.\r
+# \r
+# You should have received a copy of the GNU General Public License\r
+# along with this program; if not, write to the Free Software\r
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.\r
+\r
+require 'rexml/document'\r
+\r
+module SvnRepos\r
+\r
+  class CommandFailed < StandardError #:nodoc:\r
+  end\r
+\r
+  class Base\r
+    @url = nil\r
+    @login = nil\r
+    @password = nil\r
+        \r
+    def initialize(url, login=nil, password=nil)\r
+      @url = url\r
+      @login = login if login && !login.empty?\r
+      @password = (password || "") if @login    \r
+    end\r
+    \r
+    # Returns the entry identified by path and revision identifier\r
+    # or nil if entry doesn't exist in the repository\r
+    def entry(path=nil, identifier=nil)\r
+      e = entries(path, identifier)\r
+      e ? e.first : nil\r
+    end\r
+    \r
+    # Returns an Entries collection\r
+    # or nil if the given path doesn't exist in the repository\r
+    def entries(path=nil, identifier=nil)\r
+      path ||= ''\r
+      identifier = 'HEAD' unless identifier and identifier > 0\r
+      entries = Entries.new\r
+      cmd = "svn list --xml #{target(path)}@#{identifier}"\r
+      shellout(cmd) do |io|\r
+        begin\r
+          doc = REXML::Document.new(io)\r
+          doc.elements.each("lists/list/entry") do |entry|\r
+            entries << Entry.new({:name => entry.elements['name'].text,\r
+                        :path => ((path.empty? ? "" : "#{path}/") + entry.elements['name'].text),\r
+                        :kind => entry.attributes['kind'],\r
+                        :size => (entry.elements['size'] and entry.elements['size'].text).to_i,\r
+                        :lastrev => Revision.new({\r
+                          :identifier => entry.elements['commit'].attributes['revision'],\r
+                          :time => Time.parse(entry.elements['commit'].elements['date'].text),\r
+                          :author => entry.elements['commit'].elements['author'].text\r
+                          })\r
+                        })\r
+          end\r
+        rescue\r
+        end\r
+      end\r
+      return nil if $? && $?.exitstatus != 0\r
+      entries.sort_by_name\r
+    rescue Errno::ENOENT => e\r
+      raise CommandFailed\r
+    end\r
+\r
+    def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})\r
+      path ||= ''\r
+      identifier_from = 'HEAD' unless identifier_from and identifier_from.to_i > 0\r
+      identifier_to = 1 unless identifier_to and identifier_to.to_i > 0\r
+      revisions = Revisions.new\r
+      cmd = "svn log --xml -r #{identifier_from}:#{identifier_to} "\r
+      cmd << "--verbose " if  options[:with_paths]\r
+      cmd << target(path)\r
+      shellout(cmd) do |io|\r
+        begin\r
+          doc = REXML::Document.new(io)\r
+          doc.elements.each("log/logentry") do |logentry|\r
+            paths = []\r
+            logentry.elements.each("paths/path") do |path|\r
+              paths << {:action => path.attributes['action'],\r
+                        :path => path.text\r
+                        }\r
+            end\r
+            paths.sort! { |x,y| x[:path] <=> y[:path] }\r
+            \r
+            revisions << Revision.new({:identifier => logentry.attributes['revision'],\r
+                          :author => logentry.elements['author'].text,\r
+                          :time => Time.parse(logentry.elements['date'].text),\r
+                          :message => logentry.elements['msg'].text,\r
+                          :paths => paths\r
+                        })\r
+          end\r
+        rescue\r
+        end\r
+      end\r
+      return nil if $? && $?.exitstatus != 0\r
+      revisions\r
+    rescue Errno::ENOENT => e\r
+      raise CommandFailed    \r
+    end\r
+    \r
+    def diff(path, identifier_from, identifier_to=nil)\r
+      path ||= ''\r
+      if identifier_to and identifier_to.to_i > 0\r
+        identifier_to = identifier_to.to_i \r
+      else\r
+        identifier_to = identifier_from.to_i - 1\r
+      end\r
+      cmd = "svn diff -r "\r
+      cmd << "#{identifier_to}:"\r
+      cmd << "#{identifier_from}"\r
+      cmd << "#{target(path)}@#{identifier_from}"\r
+      diff = []\r
+      shellout(cmd) do |io|\r
+        io.each_line do |line|\r
+          diff << line\r
+        end\r
+      end\r
+      return nil if $? && $?.exitstatus != 0\r
+      diff\r
+    rescue Errno::ENOENT => e\r
+      raise CommandFailed    \r
+    end\r
+    \r
+    def cat(path, identifier=nil)\r
+      identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"\r
+      cmd = "svn cat #{target(path)}@#{identifier}"\r
+      cat = nil\r
+      shellout(cmd) do |io|\r
+        cat = io.read\r
+      end\r
+      return nil if $? && $?.exitstatus != 0\r
+      cat\r
+    rescue Errno::ENOENT => e\r
+      raise CommandFailed    \r
+    end\r
+  \r
+  private\r
+    def target(path)\r
+      " \"" << "#{@url}/#{path}".gsub(/["'?<>\*]/, '') << "\""\r
+    end\r
+    \r
+    def logger\r
+      RAILS_DEFAULT_LOGGER\r
+    end\r
+    \r
+    def shellout(cmd, &block)\r
+      logger.debug "Shelling out: #{cmd}" if logger && logger.debug?\r
+      IO.popen(cmd) do |io|\r
+        block.call(io) if block_given?\r
+      end\r
+    end\r
+  end\r
+  \r
+  class Entries < Array\r
+    def sort_by_name\r
+      sort {|x,y| \r
+        if x.kind == y.kind\r
+          x.name <=> y.name\r
+        else\r
+          x.kind <=> y.kind\r
+        end\r
+      }   \r
+    end\r
+    \r
+    def revisions\r
+      revisions ||= Revisions.new(collect{|entry| entry.lastrev})\r
+    end\r
+  end\r
+  \r
+  class Entry\r
+    attr_accessor :name, :path, :kind, :size, :lastrev\r
+    def initialize(attributes={})\r
+      self.name = attributes[:name] if attributes[:name]\r
+      self.path = attributes[:path] if attributes[:path]\r
+      self.kind = attributes[:kind] if attributes[:kind]\r
+      self.size = attributes[:size].to_i if attributes[:size]\r
+      self.lastrev = attributes[:lastrev]\r
+    end\r
+    \r
+    def is_file?\r
+      'file' == self.kind\r
+    end\r
+    \r
+    def is_dir?\r
+      'dir' == self.kind\r
+    end\r
+  end\r
+  \r
+  class Revisions < Array\r
+    def latest\r
+      sort {|x,y| x.time <=> y.time}.last    \r
+    end \r
+  end\r
+  \r
+  class Revision\r
+    attr_accessor :identifier, :author, :time, :message, :paths\r
+    def initialize(attributes={})\r
+      self.identifier = attributes[:identifier]\r
+      self.author = attributes[:author]\r
+      self.time = attributes[:time]\r
+      self.message = attributes[:message] || ""\r
+      self.paths = attributes[:paths]\r
+    end\r
+  end\r
+end
\ No newline at end of file
index 79dd88cb99d56da45357d81afb8ea602c783f0b8..85b550b753b7492dea89a2c8593e86f24887848d 100644 (file)
@@ -91,6 +91,7 @@
         <%= link_to l(:label_document_plural), {:controller => 'projects', :action => 'list_documents', :id => @project }, :class => "menuItem" %>\r
         <%= link_to l(:label_member_plural), {:controller => 'projects', :action => 'list_members', :id => @project }, :class => "menuItem" %>\r
         <%= link_to l(:label_attachment_plural), {:controller => 'projects', :action => 'list_files', :id => @project }, :class => "menuItem" %>\r
+        <%= link_to l(:label_repository), {:controller => 'repositories', :action => 'show', :id => @project}, :class => "menuItem" if @project.repository and !@project.repository.new_record? %></li>\r
         <%= link_to_if_authorized l(:label_settings), {:controller => 'projects', :action => 'settings', :id => @project }, :class => "menuItem" %>\r
     </div>\r
     <% end %>\r
                                <li><%= link_to l(:label_document_plural), :controller => 'projects', :action => 'list_documents', :id => @project %></li>\r
                                <li><%= link_to l(:label_member_plural), :controller => 'projects', :action => 'list_members', :id => @project %></li>\r
                                <li><%= link_to l(:label_attachment_plural), :controller => 'projects', :action => 'list_files', :id => @project %></li>\r
+                               <li><%= link_to l(:label_repository), :controller => 'repositories', :action => 'show', :id => @project if @project.repository and !@project.repository.new_record? %></li>\r
                                <li><%= link_to_if_authorized l(:label_settings), :controller => 'projects', :action => 'settings', :id => @project %></li>\r
                        </ul>\r
                <% end %>\r
index ab0b35fabc45cb7040b301d29bfaec14578433ba..a6102e01265b357c1e1ab719c4d2792c2a3f60a6 100644 (file)
@@ -1,4 +1,5 @@
 <%= error_messages_for 'project' %>
+
 <div class="box">
 <!--[form:project]-->
 <p><%= f.text_field :name, :required => true %></p>
        <%= custom_field.name %>        \r
 <% end %></p>
 <% end %>\r
-<!--[eoform:project]-->\r
+<!--[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 (file)
index 0000000..635fba5
--- /dev/null
@@ -0,0 +1,23 @@
+<table class="list">\r
+<thead><tr>\r
+<th><%= l(:field_name) %></th>\r
+<th><%= l(:field_filesize) %></th>\r
+<th><%= l(:label_revision) %></th>\r
+<th><%= l(:field_author) %></th>\r
+<th><%= l(:label_date) %></th>\r
+</tr></thead>\r
+<tbody>\r
+<% total_size = 0\r
+@entries.each do |entry| %>\r
+<tr class="<%= cycle 'odd', 'even' %>">\r
+<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>\r
+<td align="right"><%= human_size(entry.size) unless entry.is_dir? %></td>\r
+<td align="right"><%= link_to entry.lastrev.identifier, :action => 'revision', :id => @project, :rev => entry.lastrev.identifier %></td>\r
+<td align="center"><em><%=h entry.lastrev.author %></em></td>\r
+<td align="center"><%= format_time(entry.lastrev.time) %></td>\r
+</tr>\r
+<% total_size += entry.size\r
+end %>\r
+</tbody>\r
+</table>\r
+<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 (file)
index 0000000..3ae0f76
--- /dev/null
@@ -0,0 +1,18 @@
+<%= link_to 'root', :action => 'browse', :id => @project, :path => '', :rev => @rev %>\r
+<% \r
+dirs = path.split('/')\r
+if 'file' == kind\r
+    filename = dirs.pop\r
+end\r
+link_path = ''\r
+dirs.each do |dir| \r
+    link_path << '/' unless link_path.empty?\r
+    link_path << "#{dir}" \r
+    %>\r
+    / <%= link_to h(dir), :action => 'browse', :id => @project, :path => link_path, :rev => @rev %>\r
+<% end %>\r
+<% if filename %>\r
+    / <%= link_to h(filename), :action => 'revisions', :id => @project, :path => "#{link_path}/#{filename}", :rev => @rev %>\r
+<% end %>\r
+\r
+<%= "@ #{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 (file)
index 0000000..92ad847
--- /dev/null
@@ -0,0 +1,11 @@
+<%= stylesheet_link_tag "scm" %>\r
+\r
+<div class="contextual">\r
+<%= start_form_tag %>\r
+<%= l(:label_revision) %>: <%= text_field_tag 'rev', @rev, :size => 5 %>\r
+<%= submit_tag 'OK' %>\r
+</div>\r
+\r
+<h2><%= render :partial => 'navigation', :locals => { :path => @path, :kind => 'dir', :revision => @rev } %></h2>\r
+\r
+<%= 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 (file)
index 0000000..d4350cb
--- /dev/null
@@ -0,0 +1,55 @@
+<h2><%= render :partial => 'navigation', :locals => { :path => @path, :kind => 'file', :revision => @rev } %></h2>\r
+\r
+<%= stylesheet_link_tag "scm" %>\r
+\r
+<table class="list">\r
+<thead><tr><th>@<%= @rev %></th><th>@<%= @rev_to  %></th><th></th></tr></thead>\r
+<tbody>\r
+<% parsing = false\r
+line_num_l = 0\r
+line_num_r = 0 %>\r
+<% @diff.each do |line| %>\r
+<% \r
+   if line =~ /^@@ (\+|\-)(\d+),\d+ (\+|\-)(\d+),\d+ @@/\r
+     line_num_l = $2.to_i\r
+     line_num_r = $4.to_i\r
+     if parsing %>\r
+     <tr class="spacing"><td colspan="3">&nbsp;</td></tr>\r
+  <% end\r
+     parsing = true\r
+     next\r
+   end\r
+   next unless parsing\r
+%>\r
+\r
+<tr>\r
+\r
+<% case line[0, 1] \r
+   when " " %>\r
+<th class="line-num"><%= line_num_l %></th>\r
+<th class="line-num"><%= line_num_r %></th>\r
+<td class="line-code">\r
+<% line_num_l = line_num_l + 1\r
+   line_num_r = line_num_r + 1\r
+   \r
+   when "-" %>\r
+<th class="line-num"></th>\r
+<th class="line-num"><%= line_num_r %></th>\r
+<td class="line-code" style="background: #fdd;">\r
+<% line_num_r = line_num_r + 1\r
+\r
+   when "+" %>\r
+<th class="line-num"><%= line_num_l %></th>\r
+<th class="line-num"></th>\r
+<td class="line-code" style="background: #dfd;">\r
+<% line_num_l = line_num_l + 1\r
+\r
+   else\r
+     next\r
+   end %>\r
+   \r
+<%= h(line[1..-1]).gsub(/\s/, "&nbsp;") %></td></tr>\r
+\r
+<% end %>\r
+</tbody>\r
+</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 (file)
index 0000000..6a5a20d
--- /dev/null
@@ -0,0 +1,35 @@
+<%= stylesheet_link_tag "scm" %>\r
+\r
+<div class="contextual">\r
+<%= start_form_tag %>\r
+<%= l(:label_revision) %>: <%= text_field_tag 'rev', @rev, :size => 5 %>\r
+<%= submit_tag 'OK' %>\r
+</div>\r
+\r
+<h2><%= l(:label_revision) %> <%= @revision.identifier %></h2>\r
+\r
+<p><em><%= @revision.author %>, <%= format_time(@revision.time) %></em></p>\r
+<%= simple_format @revision.message %>\r
+\r
+<div style="float:right;">\r
+<div class="square action_A"></div> <div style="float:left;"><%= l(:label_added) %>&nbsp;</div>\r
+<div class="square action_M"></div> <div style="float:left;"><%= l(:label_modified) %>&nbsp;</div>\r
+<div class="square action_D"></div> <div style="float:left;"><%= l(:label_deleted) %>&nbsp;</div>\r
+</div>\r
+\r
+<h3><%= l(:label_attachment_plural) %></h3>\r
+<table class="list">\r
+<tbody>\r
+<% @revision.paths.each do |path| %>\r
+<tr class="<%= cycle 'odd', 'even' %>">\r
+<td><div class="square action_<%= path[:action] %>"></div> <%= path[:path] %></td>\r
+<td>\r
+<% if path[:action] == "M" %>\r
+<%= link_to 'View diff', :action => 'diff', :id => @project, :path => path[:path].gsub(/^\//, ''), :rev => @revision.identifier %>\r
+<% end %>\r
+</td>\r
+</tr>\r
+<% end %>\r
+</tbody>\r
+</table>\r
+<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 (file)
index 0000000..c2e30d3
--- /dev/null
@@ -0,0 +1,38 @@
+<%= stylesheet_link_tag "scm" %>\r
+\r
+<div class="contextual">\r
+<%= start_form_tag %>\r
+<%= l(:label_revision) %>: <%= text_field_tag 'rev', @rev, :size => 5 %>\r
+<%= submit_tag 'OK' %>\r
+</div>\r
+\r
+<h2><%= render :partial => 'navigation', :locals => { :path => @path, :kind => @entry.kind, :revision => @rev } %></h2>\r
+\r
+<% if @entry.is_file? %>\r
+<h3><%=h @entry.name %></h3>\r
+<p><%= link_to 'Download', {:action => 'entry', :id => @project, :path => @path, :rev => @rev, :format => 'raw' }, :class => "icon file" %> (<%= human_size @entry.size %>)</p>\r
+<% end %>\r
+\r
+<h3>Revisions</h3>\r
+\r
+<table class="list">\r
+<thead><tr>\r
+<th>#</th>\r
+<th><%= l(:field_author) %></th>\r
+<th><%= l(:label_date) %></th>\r
+<th><%= l(:field_description) %></th>\r
+<th></th>\r
+</tr></thead>\r
+<tbody>\r
+<% @revisions.each do |revision| %>\r
+<tr class="<%= cycle 'odd', 'even' %>">\r
+<th align="center"><%= link_to revision.identifier, :action => 'revision', :id => @project, :rev => revision.identifier %></th>\r
+<td align="center"><em><%=h revision.author %></em></td>\r
+<td align="center"><%= format_time(revision.time) %></td>\r
+<td width="70%"><%= simple_format(h(revision.message)) %></td>\r
+<td align="center"><%= link_to 'Diff', :action => 'diff', :id => @project, :path => @path, :rev => revision.identifier if @entry.is_file? && revision != @revisions.last %></td>\r
+</tr>\r
+<% end %>\r
+</tbody>\r
+</table>\r
+<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 (file)
index 0000000..4c95f88
--- /dev/null
@@ -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
index 2559159f1c36ac454b24551372e86a7109b6dcc6..0871cec79236d8c132bf979f5e17111d4aaf9a0d 100644 (file)
@@ -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"
-\r
+
+  map.connect 'repositories/:action/:id/:path', :controller => 'repositories'\r
   map.connect 'roles/workflow/:id/:role_id/:tracker_id', :controller => 'roles', :action => 'workflow'\r
   map.connect 'help/:ctrl/:page', :controller => 'help'\r
   map.connect ':controller/:action/:id/:sort_key/:sort_order'\r
diff --git a/db/migrate/015_create_repositories.rb b/db/migrate/015_create_repositories.rb
new file mode 100644 (file)
index 0000000..d8c0524
--- /dev/null
@@ -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 (file)
index 0000000..992f8dc
--- /dev/null
@@ -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
index 4c4ba17c7efc2f1d602deed762511bad793881f0..36bac591b8d5c4903d6d0954a9cf14101a6680e3 100644 (file)
@@ -7,6 +7,7 @@ http://redmine.org/
 \r
 == xx/xx/2006 v0.x.x\r
 \r
+* simple SVN browser added (just needs svn binaries in PATH)\r
 * comments can now be added on news\r
 * "my page" is now customizable \r
 * more powerfull and savable filters for issues lists\r
index aa091961bebf0f79763e82ad94324669b8cf7ed5..34fedc1ea3f74765db2b94ad46df9ef785d7addd 100644 (file)
@@ -63,6 +63,7 @@ notice_successful_delete: Erfolgreiche Auslassung.
 notice_successful_connection: Erfolgreicher Anschluß.\r
 notice_file_not_found: Erbetene Akte besteht nicht oder ist gelöscht worden.\r
 notice_locking_conflict: Data have been updated by another user.\r
+notice_scm_error: Eintragung und/oder Neuausgabe besteht nicht im Behälter.\r
 \r
 mail_subject_lost_password: Dein redMine Kennwort\r
 mail_subject_register: redMine Kontoaktivierung\r
@@ -136,6 +137,7 @@ field_start_date: Beginn
 field_done_ratio: %% Getan\r
 field_hide_mail: Mein email address verstecken\r
 field_comment: Anmerkung\r
+field_url: URL\r
 \r
 label_user: Benutzer\r
 label_user_plural: Benutzer\r
@@ -282,6 +284,17 @@ label_ago: vor
 label_contains: enthält\r
 label_not_contains: enthält nicht\r
 label_day_plural: Tage\r
+label_repository: SVN Behälter\r
+label_browse: Grasen\r
+label_modification: %d änderung\r
+label_modification_plural: %d änderungen\r
+label_revision: Neuausgabe\r
+label_revision_plural: Neuausgaben\r
+label_added: hinzugefügt\r
+label_modified: geändert\r
+label_deleted: gelöscht\r
+label_latest_revision: Neueste Neuausgabe\r
+label_view_revisions: Die Neuausgaben ansehen\r
 \r
 button_login: Einloggen\r
 button_submit: Einreichen\r
index 8c2de4bc3688e40e4ac89b32a6ec767e0b4a3f4a..b6734985a472c8a0e6787a47d3c5cd79a1fb491d 100644 (file)
@@ -63,6 +63,7 @@ notice_successful_delete: Successful deletion.
 notice_successful_connection: Successful connection.\r
 notice_file_not_found: Requested file doesn't exist or has been deleted.\r
 notice_locking_conflict: Data have been updated by another user.\r
+notice_scm_error: Entry and/or revision doesn't exist in the repository.\r
 \r
 mail_subject_lost_password: Your redMine password\r
 mail_subject_register: redMine account activation\r
@@ -136,6 +137,7 @@ field_start_date: Start
 field_done_ratio: %% Done\r
 field_hide_mail: Hide my email address\r
 field_comment: Comment\r
+field_url: URL\r
 \r
 label_user: User\r
 label_user_plural: Users\r
@@ -282,6 +284,17 @@ label_ago: days ago
 label_contains: contains\r
 label_not_contains: doesn't contain\r
 label_day_plural: days\r
+label_repository: SVN Repository\r
+label_browse: Browse\r
+label_modification: %d change\r
+label_modification_plural: %d changes\r
+label_revision: Revision\r
+label_revision_plural: Revisions\r
+label_added: added\r
+label_modified: modified\r
+label_deleted: deleted\r
+label_latest_revision: Latest revision\r
+label_view_revisions: View revisions\r
 \r
 button_login: Login\r
 button_submit: Submit\r
index ce19ef6588194c4d03f8e5d79c975e4312191d03..7eb3fa9fdc9a2f9d4ce9e699a0b83cf66f899bef 100644 (file)
@@ -63,6 +63,7 @@ notice_successful_delete: Successful deletion.
 notice_successful_connection: Successful connection.\r
 notice_file_not_found: Requested file doesn't exist or has been deleted.\r
 notice_locking_conflict: Data have been updated by another user.\r
+notice_scm_error: La entrada y/o la revisión no existe en el depósito.\r
 \r
 mail_subject_lost_password: Tu contraseña del redMine\r
 mail_subject_register: Activación de la cuenta del redMine\r
@@ -136,6 +137,7 @@ field_start_date: Comienzo
 field_done_ratio: %% Realizado\r
 field_hide_mail: Ocultar mi email address\r
 field_comment: Comentario\r
+field_url: URL\r
 \r
 label_user: Usuario\r
 label_user_plural: Usuarios\r
@@ -282,6 +284,17 @@ label_ago: hace
 label_contains: contiene \r
 label_not_contains: no contiene\r
 label_day_plural: días\r
+label_repository: Depósito SVN\r
+label_browse: Hojear\r
+label_modification: %d modificación\r
+label_modification_plural: %d modificaciones\r
+label_revision: Revisión\r
+label_revision_plural: Revisiones\r
+label_added: agregado\r
+label_modified: modificado\r
+label_deleted: suprimido\r
+label_latest_revision: La revisión más última\r
+label_view_revisions: Ver las revisiones\r
 \r
 button_login: Conexión\r
 button_submit: Someter\r
index a351a5a92e3b7a2b2bf5328c48b8403280b504e8..4b4b04925bb947e7ed6b4d8d2e7d77dc46512888 100644 (file)
@@ -63,6 +63,7 @@ notice_successful_delete: Suppression effectuée avec succès.
 notice_successful_connection: Connection réussie.\r
 notice_file_not_found: Le fichier demandé n'existe pas ou a été supprimé.\r
 notice_locking_conflict: Les données ont été mises à jour par un autre utilisateur. Mise à jour impossible.\r
+notice_scm_error: L'entrée et/ou la révision demandée n'existe pas dans le dépôt.\r
 \r
 mail_subject_lost_password: Votre mot de passe redMine\r
 mail_subject_register: Activation de votre compte redMine\r
@@ -137,6 +138,7 @@ field_done_ratio: %% Réalisé
 field_auth_source: Mode d'authentification\r
 field_hide_mail: Cacher mon adresse mail\r
 field_comment: Commentaire\r
+field_url: URL\r
 \r
 label_user: Utilisateur\r
 label_user_plural: Utilisateurs\r
@@ -283,6 +285,17 @@ label_ago: il y a
 label_contains: contient\r
 label_not_contains: ne contient pas\r
 label_day_plural: jours\r
+label_repository: Dépôt SVN\r
+label_browse: Parcourir\r
+label_modification: %d modification\r
+label_modification_plural: %d modifications\r
+label_revision: Révision\r
+label_revision_plural: Révisions\r
+label_added: ajouté\r
+label_modified: modifié\r
+label_deleted: supprimé\r
+label_latest_revision: Dernière révision\r
+label_view_revisions: Voir les révisions\r
 \r
 button_login: Connexion\r
 button_submit: Soumettre\r
diff --git a/public/images/file.png b/public/images/file.png
new file mode 100644 (file)
index 0000000..7a08719
Binary files /dev/null and b/public/images/file.png differ
diff --git a/public/images/folder.png b/public/images/folder.png
new file mode 100644 (file)
index 0000000..32175eb
Binary files /dev/null and b/public/images/folder.png differ
index d85b025fef510e4d4184dffa018793ce3a247a5e..25b46c85aa6a5128aa6a616fda807d2ba297316b 100644 (file)
@@ -473,7 +473,7 @@ float: right;
 font-size: 0.8em;\r
 }\r
 \r
-.contextual select {\r
+.contextual select, .contextual input {\r
 font-size: 1em;\r
 }\r
 \r
diff --git a/public/stylesheets/scm.css b/public/stylesheets/scm.css
new file mode 100644 (file)
index 0000000..658fb90
--- /dev/null
@@ -0,0 +1,66 @@
+\r
+\r
+div.square {\r
+ border: 1px solid #999;\r
+ float: left;\r
+ margin: .4em .5em 0 0;\r
+ overflow: hidden;\r
+ width: .6em; height: .6em;\r
+}\r
+\r
+div.action_M { background: #fd8 }\r
+div.action_D { background: #f88 }\r
+div.action_A { background: #bfb }\r
+\r
+table.list {\r
+    width:100%;\r
+    border-collapse: collapse;\r
+    border: 1px dotted #d0d0d0;\r
+    margin-bottom: 6px;\r
+}\r
+\r
+table.list thead th {\r
+    text-align: center;\r
+    background: #eee;\r
+    border: 1px solid #d7d7d7;\r
+}\r
+\r
+table.list tbody th {\r
+       font-weight: normal;\r
+    text-align: center;\r
+    background: #eed;\r
+    border: 1px solid #d7d7d7;\r
+}\r
+\r
+.icon {\r
+ background-position: 0% 40%;\r
+ background-repeat: no-repeat;\r
+ padding-left: 20px;\r
+}\r
+\r
+.folder { background-image: url(../images/folder.png); }\r
+.file   { background-image: url(../images/file.png); }\r
+\r
+\r
+\r
+tr.spacing {\r
+    border: 1px solid #d7d7d7;\r
+}\r
+\r
+.line-num {\r
+    border: 1px solid #d7d7d7;\r
+       font-size: 0.8em;\r
+       text-align: right;\r
+       width: 3em;\r
+       padding-right: 3px;\r
+}\r
+\r
+.line-code {\r
+       font-family: "Courier New", monospace;\r
+       font-size: 1em;\r
+}\r
+\r
+table p {\r
+    margin:0;\r
+    padding:0;\r
+}
\ No newline at end of file