initial commit of scm support:

* svn browsing
* diff viewing

git-svn-id: http://redmine.rubyforge.org/svn/branches/work@102 e93f8b46-1217-0410-a6f0-8f06a7374b81
This commit is contained in:
Jean-Philippe Lang
2006-12-23 16:13:40 +00:00
parent fa820c0224
commit 9db7b56f4d
24 changed files with 521 additions and 6 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,2 @@
module RepositoriesHelper
end

View File

@@ -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

View File

@@ -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

184
scm/app/models/svn_repos.rb Normal file
View File

@@ -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

View File

@@ -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 %>

View File

@@ -1,4 +1,5 @@
<%= error_messages_for 'project' %>
<div class="box">
<!--[form:project]-->
<p><%= f.text_field :name, :required => true %></p>
@@ -22,5 +23,17 @@
<%= 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 %></p>
<p><%= repository.text_field :login %></p>
<p><%= repository.password_field :password %></p>
<% end %>
</div>
<%= javascript_tag "Element.hide('repository');" if @project.repository.nil? %>
</div>

View File

@@ -0,0 +1,9 @@
<%= link_to 'root', :action => 'browse', :id => @project, :path => '', :rev => @rev %>
<% link_path = ''
path.split('/').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 %>
<%= "@ #{revision}" if revision %>

View File

@@ -0,0 +1,33 @@
<%= 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 => @entry.path, :revision => @rev } %></h2>
<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 => 'browse', :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>

View File

@@ -0,0 +1,55 @@
<h2><%= render :partial => 'navigation', :locals => { :path => @path, :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>

View File

@@ -0,0 +1,36 @@
<%= 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 => @entry.path, :revision => @rev } %></h2>
<h3><%=h @entry.name %></h3>
<p><%= link_to 'Download', {}, :class => "icon file" %></p>
<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><%= link_to 'View diff', :action => 'diff', :id => @project, :path => @entry.path, :rev => revision.identifier %></td>
</tr>
<% end %>
</tbody>
</table>
<p><%= lwr(:label_modification, @revisions.length) %></p>

View File

@@ -0,0 +1,29 @@
<%= 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><%= l(:field_author) %>: <em><%= @revision.author %></em></p>
<%= simple_format @revision.message %>
<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>

View File

@@ -0,0 +1,3 @@
<h2><%= l(:label_repository) %></h2>
<%= link_to l(:label_browse), :action => 'browse', :id => @project %>

View 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"
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'

View File

@@ -136,6 +136,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 +283,7 @@ label_ago: vor
label_contains: enthält
label_not_contains: enthält nicht
label_day_plural: Tage
label_repository: SVN Behälter
button_login: Einloggen
button_submit: Einreichen

View File

@@ -136,6 +136,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 +283,7 @@ label_ago: days ago
label_contains: contains
label_not_contains: doesn't contain
label_day_plural: days
label_repository: SVN Repository
button_login: Login
button_submit: Submit

View File

@@ -136,6 +136,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 +283,7 @@ label_ago: hace
label_contains: contiene
label_not_contains: no contiene
label_day_plural: días
label_repository: Depósito SVN
button_login: Conexión
button_submit: Someter

View File

@@ -137,6 +137,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 +284,11 @@ 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
button_login: Connexion
button_submit: Soumettre

BIN
scm/public/images/file.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1021 B

View File

@@ -473,7 +473,7 @@ float: right;
font-size: 0.8em;
}
.contextual select {
.contextual select, .contextual input {
font-size: 1em;
}

View File

@@ -0,0 +1,65 @@
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;
}
.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); }
table.diff 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;
}