diff --git a/scm/app/controllers/projects_controller.rb b/scm/app/controllers/projects_controller.rb index b0b00bebf..e4a47d3d1 100644 --- a/scm/app/controllers/projects_controller.rb +++ b/scm/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/scm/app/controllers/repositories_controller.rb b/scm/app/controllers/repositories_controller.rb new file mode 100644 index 000000000..75881084d --- /dev/null +++ b/scm/app/controllers/repositories_controller.rb @@ -0,0 +1,44 @@ +class RepositoriesController < ApplicationController + layout 'base' + before_filter :find_project + + def show + end + + def browse + @rev = params[:rev].to_i if params[:rev] and params[:rev].to_i > 0 + @entry = @repository.scm.entry(@path, @rev) + redirect_to :action => 'show', :id => @project and return unless @entry + if @entry.is_dir? + # if entry is a dir, shows directory listing + @entries = @repository.scm.entries(@path, @rev) + redirect_to :action => 'show', :id => @project and return unless @entries + else + # else, shows file's revisions + @revisions = @repository.scm.revisions(@path, @rev) + redirect_to :action => 'show', :id => @project and return unless @revisions + render :action => 'entry_revisions' + end + end + + def revision + @rev = params[:rev].to_i if params[:rev] and params[:rev].to_i > 0 + @revisions = @repository.scm.revisions '', @rev, @rev, :with_paths => true + redirect_to :action => 'show', :id => @project and return unless @revisions + @revision = @revisions.first + end + + def diff + @rev = params[:rev].to_i if params[:rev] and params[:rev].to_i > 0 + @rev_to = params[:rev_to] || (@rev-1) + @diff = @repository.scm.diff(params[:path], @rev, @rev_to) + redirect_to :action => 'show', :id => @project 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] + end +end diff --git a/scm/app/helpers/application_helper.rb b/scm/app/helpers/application_helper.rb index 0ca2568ae..29494d707 100644 --- a/scm/app/helpers/application_helper.rb +++ b/scm/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/scm/app/helpers/repositories_helper.rb b/scm/app/helpers/repositories_helper.rb new file mode 100644 index 000000000..2860b5a91 --- /dev/null +++ b/scm/app/helpers/repositories_helper.rb @@ -0,0 +1,2 @@ +module RepositoriesHelper +end diff --git a/scm/app/models/project.rb b/scm/app/models/project.rb index e8493cb3b..702ccc07c 100644 --- a/scm/app/models/project.rb +++ b/scm/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/scm/app/models/repository.rb b/scm/app/models/repository.rb new file mode 100644 index 000000000..dc5b9d9df --- /dev/null +++ b/scm/app/models/repository.rb @@ -0,0 +1,11 @@ +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/scm/app/models/svn_repos.rb b/scm/app/models/svn_repos.rb new file mode 100644 index 000000000..1893374b1 --- /dev/null +++ b/scm/app/models/svn_repos.rb @@ -0,0 +1,184 @@ +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) + path ||= '' + identifier = 'HEAD' unless identifier and identifier > 0 + entry = nil + cmd = "svn info --xml -r #{identifier} #{target(path)}" + IO.popen(cmd) do |io| + begin + doc = REXML::Document.new(io) + doc.elements.each("info/entry") do |info| + entry = Entry.new({:name => info.attributes['path'], + :path => path, + :kind => info.attributes['kind'], + :lastrev => Revision.new({ + :identifier => info.elements['commit'].attributes['revision'], + :author => info.elements['commit'].elements['author'].text, + :time => Time.parse(info.elements['commit'].elements['date'].text) + }) + }) + end + rescue + end + end + return nil if $? && $?.exitstatus != 0 + entry + rescue Errno::ENOENT + raise RepositoryCmdFailed + 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 -r #{identifier} #{target(path)}" + IO.popen(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 = [] + cmd = "svn log --xml -r #{identifier_from}:#{identifier_to} " + cmd << "--verbose " if options[:with_paths] + cmd << target(path) + IO.popen(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 + 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) + diff = [] + IO.popen(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 + + private + def target(path) + " \"" << "#{@url}/#{path}".gsub(/["'<>]/, '') << "\"" + 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 + 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 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/scm/app/views/layouts/base.rhtml b/scm/app/views/layouts/base.rhtml index 79dd88cb9..85b550b75 100644 --- a/scm/app/views/layouts/base.rhtml +++ b/scm/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? %> <%= link_to_if_authorized l(:label_settings), {:controller => 'projects', :action => 'settings', :id => @project }, :class => "menuItem" %> <% end %> @@ -112,6 +113,7 @@
<%= f.text_field :name, :required => true %>
@@ -22,5 +23,17 @@ <%= custom_field.name %> <% end %> <% end %> - + +<%= repository.text_field :url, :size => 60, :required => true %>
+<%= repository.text_field :login %>
+<%= repository.password_field :password %>
+<% end %> +| <%= l(:field_name) %> | +<%= l(:field_filesize) %> | +<%= l(:label_revision) %> | +<%= l(:field_author) %> | +<%= l(:label_date) %> | +
|---|---|---|---|---|
| <%= link_to h(entry.name), { :action => 'browse', :id => @project, :path => entry.path, :rev => @rev }, :class => "icon " + (entry.is_dir? ? 'folder' : 'file') %> | +<%= human_size(entry.size) unless entry.is_dir? %> | +<%= link_to entry.lastrev.identifier, :action => 'revision', :id => @project, :rev => entry.lastrev.identifier %> | +<%=h entry.lastrev.author %> | +<%= format_time(entry.lastrev.time) %> | +
<%= l(:label_total) %>: <%= human_size(total_size) %>
\ No newline at end of file diff --git a/scm/app/views/repositories/diff.rhtml b/scm/app/views/repositories/diff.rhtml new file mode 100644 index 000000000..001e2e480 --- /dev/null +++ b/scm/app/views/repositories/diff.rhtml @@ -0,0 +1,55 @@ +| @<%= @rev %> | @<%= @rev_to %> | |||||||
|---|---|---|---|---|---|---|---|---|
| <%= line_num_l %> | +<%= line_num_r %> | ++<% line_num_l = line_num_l + 1 + line_num_r = line_num_r + 1 + + when "-" %> + | + | <%= line_num_r %> | ++<% line_num_r = line_num_r + 1 + + when "+" %> + | <%= line_num_l %> | ++ | +<% line_num_l = line_num_l + 1 + + else + next + end %> + +<%= h(line[1..-1]).gsub(/\s/, " ") %> |
<%= link_to 'Download', {}, :class => "icon file" %>
+ +| # | +<%= l(:field_author) %> | +<%= l(:label_date) %> | +<%= l(:field_description) %> | ++ |
|---|---|---|---|---|
| <%= link_to revision.identifier, :action => 'revision', :id => @project, :rev => revision.identifier %> | +<%=h revision.author %> | +<%= format_time(revision.time) %> | +<%= simple_format(h(revision.message)) %> | +<%= link_to 'View diff', :action => 'diff', :id => @project, :path => @entry.path, :rev => revision.identifier %> | +
<%= lwr(:label_modification, @revisions.length) %>
\ No newline at end of file diff --git a/scm/app/views/repositories/revision.rhtml b/scm/app/views/repositories/revision.rhtml new file mode 100644 index 000000000..e2504b4fb --- /dev/null +++ b/scm/app/views/repositories/revision.rhtml @@ -0,0 +1,29 @@ +<%= stylesheet_link_tag "scm" %> + +<%= l(:field_author) %>: <%= @revision.author %>
+<%= simple_format @revision.message %> + +| <%= path[:path] %> | ++<% if path[:action] == "M" %> +<%= link_to 'View diff', :action => 'diff', :id => @project, :path => path[:path].gsub(/^\//, ''), :rev => @revision.identifier %> +<% end %> + | +
<%= lwr(:label_modification, @revision.paths.length) %>
\ No newline at end of file diff --git a/scm/app/views/repositories/show.rhtml b/scm/app/views/repositories/show.rhtml new file mode 100644 index 000000000..ec11a651b --- /dev/null +++ b/scm/app/views/repositories/show.rhtml @@ -0,0 +1,3 @@ +