├── .gitignore
├── config
├── routes.rb
└── locales
│ ├── zh.yml
│ ├── tr.yml
│ ├── en.yml
│ └── fr.yml
├── lib
├── scm_extensions.rb
├── scm_extensions_application_helper_patch.rb
├── scm_extensions_macros.rb
├── scm_extensions_repository_view_hook.rb
├── scm_extensions_filesystem_adapter_patch.rb
└── scm_extensions_subversion_adapter_patch.rb
├── app
├── views
│ ├── scm_extensions
│ │ ├── _issue_box.html.erb
│ │ ├── _dir_list.html.erb
│ │ ├── mkdir.html.erb
│ │ ├── notify.html.erb
│ │ ├── upload.html.erb
│ │ ├── _file.html.erb
│ │ └── _dir_list_content.html.erb
│ └── scm_extensions_mailer
│ │ ├── notify.text.erb
│ │ ├── notify.html.erb
│ │ ├── send_upload.text.erb
│ │ └── send_upload.html.erb
├── models
│ ├── scm_extensions_mailer.rb
│ └── scm_extensions_write.rb
└── controllers
│ └── scm_extensions_controller.rb
├── LICENSE
├── init.rb
└── README.textile
/.gitignore:
--------------------------------------------------------------------------------
1 | /.project
2 |
--------------------------------------------------------------------------------
/config/routes.rb:
--------------------------------------------------------------------------------
1 | #map.connect ':controller/:action/:id'
2 | match 'projects/:id/scm_extensions/:action', :controller => 'scm_extensions'
3 |
--------------------------------------------------------------------------------
/lib/scm_extensions.rb:
--------------------------------------------------------------------------------
1 | #Extend the ActionMailer to include plugin in its paths
2 | ActionMailer::Base.append_view_path(File.expand_path(File.dirname(__FILE__) + '/../app/views'))
3 |
--------------------------------------------------------------------------------
/app/views/scm_extensions/_issue_box.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <% @issues.each do |issue| %>
3 |
4 | <%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %>: <%= issue.subject %>
5 |
6 | <% end %>
7 |
--------------------------------------------------------------------------------
/app/views/scm_extensions/_dir_list.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <% if @show_cb %>
5 |
6 | <% end %>
7 | <%= l(:field_name) %>
8 | <%= l(:field_filesize) %>
9 | <% if @show_rev %>
10 | <%= l(:label_revision) %>
11 | <% end %>
12 | <%= l(:label_age) %>
13 | <% if !@repository.is_a?(Repository::Filesystem) %>
14 | <%= l(:field_author) %>
15 | <%= l(:field_comments) %>
16 | <% end %>
17 |
18 |
19 |
20 | <%= render :partial => 'scm_extensions/dir_list_content' %>
21 |
22 |
23 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | # SCM Extensions plugin for Redmine
2 | # Copyright (C) 2010 Arnaud MARTEL
3 | #
4 | # This program is free software; you can redistribute it and/or
5 | # modify it under the terms of the GNU General Public License
6 | # as published by the Free Software Foundation; either version 2
7 | # of the License, or (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program; if not, write to the Free Software
16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 |
--------------------------------------------------------------------------------
/app/views/scm_extensions/mkdir.html.erb:
--------------------------------------------------------------------------------
1 | <%=l(:label_scm_extensions_new_folder)%>
2 |
3 | <%= @scm_extensions.path %>
4 | <%= form_for :scm_extensions, :url => {:controller => 'scm_extensions', :action => 'mkdir', :id => @scm_extensions.project, :repository_id => @repository.identifier}, :html => {:multipart => true, :id => 'folder_form'} do |f| %>
5 | <%= f.hidden_field :path %>
6 |
7 | <% if !@repository.is_a?(Repository::Filesystem) %>
8 | <%=l(:field_comments)%> <%= f.text_area :comments, :cols => 100, :rows => 10, :accesskey => accesskey(:edit), :class => 'wiki-edit' %>
9 | <% end %>
10 | <%=l(:label_scm_extensions_folder_name)%> <%= f.text_field :new_folder, :size => 60 %>
11 |
12 | <%= submit_tag l(:button_save) %>
13 |
14 | <% end %>
15 |
16 | <% content_for :header_tags do %>
17 | <%= stylesheet_link_tag 'scm' %>
18 | <% end %>
19 |
20 | <% html_title(l(:label_scm_extensions_new_folder)) -%>
21 |
--------------------------------------------------------------------------------
/app/models/scm_extensions_mailer.rb:
--------------------------------------------------------------------------------
1 | class ScmExtensionsMailer < Mailer
2 | def send_upload(obj, attachments, language, rec )
3 | @obj = obj
4 | @attachments = attachments
5 | set_language_if_valid language
6 | path_root = @obj.repository.identifier.blank? ? 'root' : @obj.repository.identifier
7 | sub = l(:label_scm_extensions_upload_subject, obj.project.name)
8 | reg = Regexp.new("^#{path_root}")
9 | @folder_path = @obj.path.sub(reg,'').sub(/^\//,'')
10 | mail :to => rec, :reply_to => User.current.mail,
11 | :subject => sub
12 | end
13 |
14 | def notify(obj, selectedfiles, language, rec )
15 | @obj = obj
16 | @selectedfiles = selectedfiles
17 | set_language_if_valid language
18 | path_root = @obj.repository.identifier.blank? ? 'root' : @obj.repository.identifier
19 | sub = l(:label_scm_extensions_upload_subject, obj.project.name)
20 | reg = Regexp.new("^#{path_root}")
21 | @folder_path = @obj.path.sub(reg,'').sub(/^\//,'')
22 | mail :to => rec, :reply_to => User.current.mail,
23 | :subject => sub
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/config/locales/zh.yml:
--------------------------------------------------------------------------------
1 | # Simplified Chinese strings go
2 | zh:
3 | project_module_scm_extensions: "SCM扩展"
4 | permission_scm_write_access: "更新库"
5 | label_scm_extensions_upload: "上传文件"
6 | notice_scm_extensions_upload_success: "上传成功。"
7 | error_scm_extensions_upload_failed: "系统错误。取消更新。"
8 | error_scm_extensions_no_path_head: "路径在仓库的当前修订版本中不存在。"
9 | error_scm_extensions_delete_failed: "系统个错误。取消删除。"
10 | notice_scm_extensions_delete_success: "删除成功。"
11 | notice_scm_extensions_mkdir_success: "文件夹/目录已创建。"
12 | error_scm_extensions_mkdir_failed: "系统错误。文件夹/目录无法创建。"
13 | label_scm_extensions_new_folder: "新建文件夹/目录"
14 | label_scm_extensions_delete_folder: "删除文件夹/目录"
15 | label_scm_extensions_delete_file: "删除文件"
16 | label_scm_extensions_folder_name: "文件夹/目录名称"
17 |
18 | label_scm_extensions_upload_subject: "%{value}: 上传文件可用。"
19 | label_scm_extensions_upload_body: "以下文件已上传至对应文件夹/目录。"
20 | label_scm_extensions_notify: Notify (email)
21 | label_scm_select_files: select files and folders that will be referenced in email
22 | field_scm_mail_recipients: recipients
23 | notice_scm_extensions_email_success: Notification done
24 | button_send_notification: Send notification
25 | label_scm_extensions_notify_body: "Following files can be found in folder "
26 | label_scm_extensions_notify_by: "Mail from %{author}"
--------------------------------------------------------------------------------
/init.rb:
--------------------------------------------------------------------------------
1 | # SCM Extensions plugin for Redmine
2 | # Copyright (C) 2010 Arnaud MARTEL
3 | #
4 | # This program is free software; you can redistribute it and/or
5 | # modify it under the terms of the GNU General Public License
6 | # as published by the Free Software Foundation; either version 2
7 | # of the License, or (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program; if not, write to the Free Software
16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 | require 'redmine'
18 | Dir::foreach(File.join(File.dirname(__FILE__), 'lib')) do |file|
19 | next unless /\.rb$/ =~ file
20 | require file
21 | end
22 |
23 | Redmine::Plugin.register :redmine_scm_extensions do
24 | name 'SCM extensions plugin'
25 | author 'Arnaud Martel'
26 | description 'plugin to allow write operations for subversion repositories and provide new wiki macro'
27 | version '0.4.0'
28 | requires_redmine :version_or_higher => '2.0.3'
29 |
30 |
31 | project_module :scm_extensions do
32 | permission :scm_write_access, {:scm_extensions => [:upload, :mkdir, :delete, :notify]}
33 | end
34 |
35 | end
--------------------------------------------------------------------------------
/app/views/scm_extensions_mailer/notify.text.erb:
--------------------------------------------------------------------------------
1 | <%= l(:label_scm_extensions_notify_by, :author => User.current) %>:
2 |
3 | <%= @obj.comments %>
4 |
5 |
6 | <%
7 | path_root = @obj.repository.identifier.blank? ? 'root' : @obj.repository.identifier
8 | link_path = ""
9 | link_path << path_root
10 | link_path << '/' unless @folder_path.empty?
11 | link_path << @folder_path
12 | %>
13 | <%=l(:label_scm_extensions_notify_body)%><%= if @obj.repository.identifier.blank?
14 | link_to h(link_path), url_for(:controller => 'repositories', :action => 'show', :id => @obj.project, :path => to_path_param(@folder_path), :rev => nil, :only_path => false)
15 | else
16 | link_to h(link_path), url_for(:controller => 'repositories', :action => 'show', :id => @obj.project, :repository_id => @obj.repository.identifier, :path => to_path_param(@folder_path), :rev => nil, :only_path => false)
17 | end
18 | %>
19 |
20 | <% @selectedfiles.each do |filename| %>
21 | * <%= if @obj.repository.identifier.blank?
22 | link_to h(filename), url_for(:controller => 'repositories', :action => 'raw', :id => @obj.project, :path => to_path_param(@folder_path+ '/' + filename), :rev => nil, :only_path => false)
23 | else
24 | link_to h(filename), url_for(:controller => 'repositories', :action => 'raw', :id => @obj.project, :repository_id => @obj.repository.identifier, :path => to_path_param(@folder_path+ '/' + filename), :rev => nil, :only_path => false)
25 | end
26 | %>
27 | <% end %>
28 |
29 |
30 |
--------------------------------------------------------------------------------
/config/locales/tr.yml:
--------------------------------------------------------------------------------
1 | # Turkish strings go
2 | tr:
3 | project_module_scm_extensions: "SCM eklentileri"
4 | permission_scm_write_access: "Depoyu güncelle"
5 | label_scm_extensions_upload: "Dosya(lar) yükle"
6 | notice_scm_extensions_upload_success: "Yüklendi"
7 | error_scm_extensions_upload_failed: "Hata. Yükleme iptal edildi"
8 | error_scm_extensions_no_path_head: "Deponun mevcut sürümünde dosya yolu bulunamıyor"
9 | error_scm_extensions_delete_failed: "Hata. Silme iptal edildi"
10 | notice_scm_extensions_delete_success: "Silindi"
11 | notice_scm_extensions_mkdir_success: "Klasör oluşturuldu."
12 | error_scm_extensions_mkdir_failed: "Hata. Klasör oluşturulamadı."
13 | label_scm_extensions_new_folder: "Yeni Klasör"
14 | label_scm_extensions_delete_folder: "Klasörü sil"
15 | label_scm_extensions_delete_file: "Dosyayı sil"
16 | label_scm_extensions_folder_name: "Yeni klasörün adı"
17 |
18 | label_scm_extensions_upload_subject: "%{value}: Yeni dosyalar var"
19 | label_scm_extensions_upload_body: "Aşağıdaki dosyalar, klasöre yüklendi "
20 | label_scm_extensions_notify: Haber ver (eposta)
21 | label_scm_select_files: Eposta'da bahsedilecek dosyaları ve klasörleri seçin
22 | field_scm_mail_recipients: alıcılar
23 | notice_scm_extensions_email_success: Bildirim tamamlandı
24 | button_send_notification: Bildirim gönder
25 | label_scm_extensions_notify_body: "Aşağıdaki dosyalar, klasörde bulunabilir "
26 | label_scm_extensions_notify_by: "%{author}'dan eposta"
27 |
--------------------------------------------------------------------------------
/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | # English strings go
2 | en:
3 | project_module_scm_extensions: "SCM extensions"
4 | permission_scm_write_access: "Update repository"
5 | label_scm_extensions_upload: "Upload files"
6 | notice_scm_extensions_upload_success: "Uploaded successfully"
7 | error_scm_extensions_upload_failed: "Error. Update canceled"
8 | error_scm_extensions_no_path_head: "Path doesn't exist in current revision of repository"
9 | error_scm_extensions_delete_failed: "Error. Delete canceled"
10 | notice_scm_extensions_delete_success: "Deleted successfully"
11 | notice_scm_extensions_mkdir_success: "Folder/directory created successfully"
12 | error_scm_extensions_mkdir_failed: "Error. Folder/directory not created"
13 | label_scm_extensions_new_folder: "New folder/directory"
14 | label_scm_extensions_delete_folder: "Delete folder/directory"
15 | label_scm_extensions_delete_file: "Delete file"
16 | label_scm_extensions_folder_name: "Name of the new folder/directory"
17 |
18 | label_scm_extensions_upload_subject: "%{value}: New files are available"
19 | label_scm_extensions_upload_body: "The following files have been uploaded in the folder/directory "
20 | label_scm_extensions_notify: Notify (email)
21 | label_scm_select_files: select files and folders that will be referenced in email
22 | field_scm_mail_recipients: recipients
23 | notice_scm_extensions_email_success: Notification done
24 | button_send_notification: Send notification
25 | label_scm_extensions_notify_body: "Following files can be found in folder "
26 | label_scm_extensions_notify_by: "Mail from %{author}"
--------------------------------------------------------------------------------
/app/views/scm_extensions_mailer/notify.html.erb:
--------------------------------------------------------------------------------
1 | <%= l(:label_scm_extensions_notify_by, :author => User.current).html_safe %>:
2 |
3 | <%= textilizable(@obj, :comments, :only_path => false) %>
4 |
5 |
6 | <%
7 | path_root = @obj.repository.identifier.blank? ? 'root' : @obj.repository.identifier
8 | link_path = ""
9 | link_path << path_root
10 | link_path << '/' unless @folder_path.empty?
11 | link_path << @folder_path
12 | %>
13 | <%=l(:label_scm_extensions_notify_body)%><%= if @obj.repository.identifier.blank?
14 | link_to h(link_path), url_for(:controller => 'repositories', :action => 'show', :id => @obj.project, :path => to_path_param(@folder_path), :rev => nil, :only_path => false)
15 | else
16 | link_to h(link_path), url_for(:controller => 'repositories', :action => 'show', :id => @obj.project, :repository_id => @obj.repository.identifier, :path => to_path_param(@folder_path), :rev => nil, :only_path => false)
17 | end
18 | %>
19 |
20 |
21 | <% @selectedfiles.each do |filename| %>
22 | <%= if @obj.repository.identifier.blank?
23 | link_to h(filename), url_for(:controller => 'repositories', :action => 'raw', :id => @obj.project, :path => to_path_param(@folder_path+ '/' + filename), :rev => nil, :only_path => false)
24 | else
25 | link_to h(filename), url_for(:controller => 'repositories', :action => 'raw', :id => @obj.project, :repository_id => @obj.repository.identifier, :path => to_path_param(@folder_path+ '/' + filename), :rev => nil, :only_path => false)
26 | end
27 | %>
28 | <% end %>
29 |
30 |
31 |
--------------------------------------------------------------------------------
/config/locales/fr.yml:
--------------------------------------------------------------------------------
1 | # French strings go
2 | fr:
3 | project_module_scm_extensions: "SCM extensions"
4 | permission_scm_write_access: "Modifier le contenu du dépôt"
5 | label_scm_extensions_upload: "Ajoût/mise à jour de fichiers"
6 | notice_scm_extensions_upload_success: "Sauvegarde effectuée"
7 | error_scm_extensions_upload_failed: "Erreur. Sauvegarde non effectuée"
8 | error_scm_extensions_no_path_head: "Chemin inexistant dans la version courante du dépôt"
9 | error_scm_extensions_delete_failed: "Erreur. Suppression non effectuée"
10 | notice_scm_extensions_delete_success: "Suppression effectuée"
11 | notice_scm_extensions_mkdir_success: "Dossier/répertoire créé"
12 | error_scm_extensions_mkdir_failed: "Erreur. Dossier/répertoire non créé"
13 | label_scm_extensions_new_folder: "Nouveau dossier/répertoire"
14 | label_scm_extensions_delete_folder: "Supprimer dossier/répertoire"
15 | label_scm_extensions_delete_file: "Supprimer fichier"
16 | label_scm_extensions_folder_name: "Nom du nouveau dossier/répertoire"
17 |
18 | label_scm_extensions_upload_subject: "%{value}: Nouveaux fichiers disponibles"
19 | label_scm_extensions_upload_body: "Les fichiers suivants ont été transférés dans le répertoire "
20 | label_scm_extensions_notify: "Notifier (email)"
21 | label_scm_select_files: "Sélectionnez les fichiers et dossiers à référencer dans le message"
22 | field_scm_mail_recipients: Destinataires
23 | notice_scm_extensions_email_success: Notification envoyée
24 | button_send_notification: Envoyer le message
25 | label_scm_extensions_notify_body: "Les fichiers/dossiers suivants sont accessibles depuis le dossier "
26 | label_scm_extensions_notify_by: "Message de %{author}"
--------------------------------------------------------------------------------
/app/views/scm_extensions_mailer/send_upload.text.erb:
--------------------------------------------------------------------------------
1 | <%= l(:label_added_time_by, :author => User.current, :age => time_tag(Time.now)) %>:
2 |
3 | <%= @obj.comments %>
4 |
5 |
6 | <%
7 | path_root = @obj.repository.identifier.blank? ? 'root' : @obj.repository.identifier
8 | link_path = ""
9 | link_path << path_root
10 | link_path << '/' unless @folder_path.empty?
11 | link_path << @folder_path
12 | %>
13 | <%=l(:label_scm_extensions_upload_body)%><%= if @obj.repository.identifier.blank?
14 | link_to h(link_path), url_for(:controller => 'repositories', :action => 'show', :id => @obj.project, :path => to_path_param(@folder_path), :rev => nil, :only_path => false)
15 | else
16 | link_to h(link_path), url_for(:controller => 'repositories', :action => 'show', :id => @obj.project, :repository_id => @obj.repository.identifier, :path => to_path_param(@folder_path), :rev => nil, :only_path => false)
17 | end
18 | %>
19 |
20 | <% @attachments.each_value do |attachment|
21 | filename = nil?
22 | if attachment.has_key?("token")
23 | filename = attachment['filename']
24 | else
25 | file = attachment['file']
26 | filename = File.basename(file.original_filename) if file
27 | end
28 | next unless filename
29 | %>
30 | * <%= if @obj.repository.identifier.blank?
31 | link_to h(filename), url_for(:controller => 'repositories', :action => 'raw', :id => @obj.project, :path => to_path_param(@folder_path+ '/' + filename), :rev => nil, :only_path => false)
32 | else
33 | link_to h(filename), url_for(:controller => 'repositories', :action => 'raw', :id => @obj.project, :repository_id => @obj.repository.identifier, :path => to_path_param(@folder_path+ '/' + filename), :rev => nil, :only_path => false)
34 | end
35 | %>
36 | <% end %>
37 |
38 |
39 |
--------------------------------------------------------------------------------
/lib/scm_extensions_application_helper_patch.rb:
--------------------------------------------------------------------------------
1 | # SCM Extensions plugin for Redmine
2 | # Copyright (C) 2010 Arnaud MARTEL
3 | #
4 | # This program is free software; you can redistribute it and/or
5 | # modify it under the terms of the GNU General Public License
6 | # as published by the Free Software Foundation; either version 2
7 | # of the License, or (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program; if not, write to the Free Software
16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 |
18 | require_dependency 'application_helper'
19 |
20 | module ScmExtensionsApplicationHelperPatch
21 | def self.included(base) # :nodoc:
22 | base.send(:include, ApplicationHelperMethodsScmExtensions)
23 |
24 | base.class_eval do
25 | unloadable # Send unloadable so it will not be unloaded in development
26 | end
27 |
28 | end
29 | end
30 |
31 | module ApplicationHelperMethodsScmExtensions
32 | def scm_extensions_format_revision(txt)
33 | txt.to_s[0,8]
34 | end
35 |
36 | def scm_extensions_link_to_revision(revision, project, repository, options={})
37 | text = options.delete(:text) || scm_extensions_format_revision(revision)
38 | link_to(text, {:controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier, :rev => revision}, :title => l(:label_revision_id, revision))
39 | end
40 |
41 | end
42 |
43 | ApplicationHelper.send(:include, ScmExtensionsApplicationHelperPatch)
44 |
--------------------------------------------------------------------------------
/app/models/scm_extensions_write.rb:
--------------------------------------------------------------------------------
1 | class ScmExtensionsWrite
2 |
3 | #acts_as_watchable
4 |
5 | attr_accessor :comments
6 | attr_accessor :new_folder
7 | attr_accessor :path
8 | attr_accessor :project
9 | attr_accessor :recipients
10 | attr_accessor :repository
11 |
12 | def initialize(options = { })
13 | self.comments = options[:comments]
14 | self.new_folder = options[:new_folder]
15 | self.path = options[:path]
16 | self.project = options[:project]
17 | self.repository = options[:repository]
18 | self.recipients = {}
19 | end
20 |
21 | def deliver(attachments)
22 | recipientsWithLang = {}
23 | if !self.recipients.nil?
24 | self.recipients.each do |mail|
25 | user = User.find_by_mail(mail);
26 | if !user.nil?
27 | lang = user.language
28 | if recipientsWithLang[lang].nil?
29 | recipientsWithLang[lang] = [ mail ]
30 | else
31 | recipientsWithLang[lang] << mail
32 | end
33 | end
34 | end
35 | recipientsWithLang.each do |language,rec|
36 | ScmExtensionsMailer.send_upload(self, attachments, language, rec).deliver
37 | end
38 | end
39 | return true
40 |
41 | end
42 |
43 | def notify(selectedfiles)
44 | recipientsWithLang = {}
45 | if !self.recipients.nil?
46 | self.recipients.each do |mail|
47 | user = User.find_by_mail(mail);
48 | if !user.nil?
49 | lang = user.language
50 | if recipientsWithLang[lang].nil?
51 | recipientsWithLang[lang] = [ mail ]
52 | else
53 | recipientsWithLang[lang] << mail
54 | end
55 | end
56 | end
57 | recipientsWithLang.each do |language,rec|
58 | ScmExtensionsMailer.notify(self, selectedfiles, language, rec).deliver
59 | end
60 | end
61 | return true
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/app/views/scm_extensions_mailer/send_upload.html.erb:
--------------------------------------------------------------------------------
1 | <%= l(:label_added_time_by, :author => User.current, :age => time_tag(Time.now)).html_safe %>:
2 |
3 | <%= textilizable(@obj, :comments, :only_path => false) %>
4 |
5 |
6 | <%
7 | path_root = @obj.repository.identifier.blank? ? 'root' : @obj.repository.identifier
8 | link_path = ""
9 | link_path << path_root
10 | link_path << '/' unless @folder_path.empty?
11 | link_path << @folder_path
12 | %>
13 | <%=l(:label_scm_extensions_upload_body)%><%= if @obj.repository.identifier.blank?
14 | link_to h(link_path), url_for(:controller => 'repositories', :action => 'show', :id => @obj.project, :path => to_path_param(@folder_path), :rev => nil, :only_path => false)
15 | else
16 | link_to h(link_path), url_for(:controller => 'repositories', :action => 'show', :id => @obj.project, :repository_id => @obj.repository.identifier, :path => to_path_param(@folder_path), :rev => nil, :only_path => false)
17 | end
18 | %>
19 |
20 |
21 | <% @attachments.each_value do |attachment|
22 | filename = nil?
23 | if attachment.has_key?("token")
24 | filename = attachment['filename']
25 | else
26 | file = attachment['file']
27 | filename = File.basename(file.original_filename) if file
28 | end
29 | next unless filename
30 | %>
31 | <%= if @obj.repository.identifier.blank?
32 | link_to h(filename), url_for(:controller => 'repositories', :action => 'raw', :id => @obj.project, :path => to_path_param(@folder_path+ '/' + filename), :rev => nil, :only_path => false)
33 | else
34 | link_to h(filename), url_for(:controller => 'repositories', :action => 'raw', :id => @obj.project, :repository_id => @obj.repository.identifier, :path => to_path_param(@folder_path+ '/' + filename), :rev => nil, :only_path => false)
35 | end
36 | %>
37 | <% end %>
38 |
39 |
40 |
--------------------------------------------------------------------------------
/README.textile:
--------------------------------------------------------------------------------
1 | h1. Introduction
2 |
3 | Main features of the plugin:
4 | * Add 3 actions in repository views: "upload files", "new folder" and "delete file/folder". Right now, only subversion and filesystem SCM are supported...
5 | * Add a new macro _scm_show_ to include repository inside a wiki page
6 |
7 | Development was done using REDMINE trunk r9901 (=> 2.0.3 +) and any release after 2.0.3 should work
8 |
9 | About subversion support:
10 | To commit changes in Subversion, the plugin opens the repository with the file protocol. For this reason, you need the following:
11 | * The repositories have to be installed on the REDMINE server.
12 | * Plugin will replace the beginning of your repository location ([protocol]://[server]/" with "file:///svnroot/". You may need to create a symbolic link /svnroot for this to work...
13 |
14 | h1. Setup
15 |
16 | h3. 1. Install plugin into vendor/plugins
17 |
18 | Install redmine_scm_extensions with:
19 | * cd [redmine-install-dir]/plugins
20 | * git clone git://github.com/amartel/redmine_scm_extensions.git
21 |
22 | No DB migration is required...
23 |
24 | h3. 2. Restart your web server
25 |
26 |
27 | h3. 3. Configure REDMINE with your web browser
28 |
29 | If everything is OK, you should see SCM extensions in the plugin list (Administration -> Plugins)
30 |
31 | A new permission is now available (SCM extensions -> Update repository) and you have to assign it to the roles you need
32 |
33 |
34 | h1. History
35 |
36 | 0.4.0:
37 | * New: add button to send a notification email about existing files
38 | * New: redmine 2.3.0 or higher is required
39 |
40 | 0.3.0: 2012-08-21
41 | * New: redmine 2.0.3 or higher is required
42 |
43 | 0.2.0: 2012-01-18
44 | * New: redmine 1.3.1 or higher is required (support for multi-repositories)
45 |
46 | 0.1.0: 2011-01-14
47 | * Fixed: support for redmine 1.1.0 (icon display)
48 |
49 | 0.0.2: 2010-08-03
50 | * New: support for filesystem SCM
51 | * New: Members can be selected in upload form and the plugin will notify them by email if upload complete successfully
52 |
53 | 0.0.1: Initial release
--------------------------------------------------------------------------------
/app/views/scm_extensions/notify.html.erb:
--------------------------------------------------------------------------------
1 | <%=l(:label_scm_extensions_notify)%>
2 |
3 |
32 | <%= @scm_extensions.path %>
33 | <%= form_for :scm_extensions, :url => {:controller => 'scm_extensions', :action => 'notify', :id => @scm_extensions.project, :repository_id => @repository.identifier}, :html => {:multipart => true, :id => 'files_form'} do |f| %>
34 | <%= f.hidden_field :path %>
35 | <%=l(:label_scm_select_files)%> <%= render :partial => 'scm_extensions/dir_list' %>
36 |
37 | <%=l(:field_comments)%> <%= f.text_area :comments, :cols => 100, :rows => 10, :accesskey => accesskey(:edit), :class => 'wiki-edit' %>
38 | <%= l(:field_scm_mail_recipients) %>
39 | <% @project.users.sort.each do |user| -%>
40 | <%= check_box_tag 'watchers[]', user.mail, false %> <%=h user %>
41 | <% end -%>
42 |
43 |
44 |
45 |
46 | <%= submit_tag l(:button_send_notification) %>
47 |
48 | <% end %>
49 |
50 | <% content_for :header_tags do %>
51 | <%= stylesheet_link_tag 'scm' %>
52 | <% end %>
53 |
54 | <% html_title(l(:label_scm_extensions_upload)) -%>
55 |
--------------------------------------------------------------------------------
/app/views/scm_extensions/upload.html.erb:
--------------------------------------------------------------------------------
1 | <%=l(:label_scm_extensions_upload)%>
2 |
3 |
32 | <%= @scm_extensions.path %>
33 | <%= form_for :scm_extensions, :url => {:controller => 'scm_extensions', :action => 'upload', :id => @scm_extensions.project, :repository_id => @repository.identifier}, :html => {:multipart => true, :id => 'files_form'} do |f| %>
34 |
35 | <%= f.hidden_field :path %>
36 |
<%=l(:field_comments)%> <%= f.text_area :comments, :cols => 100, :rows => 10, :accesskey => accesskey(:edit), :class => 'wiki-edit' %>
37 |
<%= l(:field_mail_notification) %>
38 | <% @project.users.sort.each do |user| -%>
39 | <%= check_box_tag 'watchers[]', user.mail, false %> <%=h user %>
40 | <% end -%>
41 |
42 |
43 |
44 |
45 |
<%=l(:label_attachment_plural)%> <%= render :partial => 'scm_extensions/file' %>
46 |
47 | <%= submit_tag l(:button_save) %>
48 |
49 | <% end %>
50 |
51 | <% content_for :header_tags do %>
52 | <%= stylesheet_link_tag 'scm' %>
53 | <% end %>
54 |
55 | <% html_title(l(:label_scm_extensions_upload)) -%>
56 |
--------------------------------------------------------------------------------
/app/views/scm_extensions/_file.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
26 |
27 | <% if defined?(container) && container && container.saved_attachments %>
28 | <% container.saved_attachments.each_with_index do |attachment, i| %>
29 |
30 | <%= text_field_tag("attachments[p#{i}][filename]", attachment.filename, :class => 'filename') +
31 | link_to(' '.html_safe, attachment_path(attachment, :attachment_id => "p#{i}", :format => 'js'), :method => 'delete', :remote => true, :class => 'remove-upload') %>
32 | <%= hidden_field_tag "attachments[p#{i}][token]", "#{attachment.token}" %>
33 |
34 | <% end %>
35 | <% end %>
36 |
37 |
38 | <%= file_field_tag 'attachments[dummy][file]',
39 | :id => nil,
40 | :class => 'file_selector',
41 | :multiple => true,
42 | :onchange => 'addInputFiles(this);',
43 | :data => {
44 | :max_file_size => Setting.attachment_max_size.to_i.kilobytes,
45 | :max_file_size_message => l(:error_attachment_too_big, :max_size => number_to_human_size(Setting.attachment_max_size.to_i.kilobytes)),
46 | :max_concurrent_uploads => Redmine::Configuration['max_concurrent_ajax_uploads'].to_i,
47 | :upload_path => uploads_path(:format => 'js'),
48 | :description_placeholder => l(:label_optional_description)
49 | } %>
50 | (<%= l(:label_max_size) %>: <%= number_to_human_size(Setting.attachment_max_size.to_i.kilobytes) %>)
51 |
52 |
53 | <% content_for :header_tags do %>
54 | <%= javascript_include_tag 'attachments' %>
55 | <% end %>
56 |
--------------------------------------------------------------------------------
/app/views/scm_extensions/_dir_list_content.html.erb:
--------------------------------------------------------------------------------
1 | <% @entries.each do |entry| %>
2 | <% if !User.current.allowed_to?(:synapse_access, @project) || !(entry.name =~ /^\./) %>
3 | <% tr_id = Digest::MD5.hexdigest(entry.path)
4 | depth = params[:depth].to_i %>
5 | <% ent_path = Redmine::CodesetUtil.replace_invalid_utf8(entry.path) %>
6 | <% ent_name = Redmine::CodesetUtil.replace_invalid_utf8(entry.name) %>
7 |
8 | <% if @show_cb %>
9 | <%= check_box_tag "selectedfiles[]", entry.path, false %>
10 | <% end %>
11 |
12 | ">
14 | <% if entry.is_dir? %>
15 |
24 | <% end %>
25 | <% if @link_details %>
26 | <%= link_to h(entry.name),
27 | {:controller => 'repositories', :action => (entry.is_dir? ? 'show' : 'changes'), :id => @project, :repository_id => @repository.identifier, :path => to_path_param(entry.path), :rev => @rev},
28 | :class => (entry.is_dir? ? 'icon icon-folder' : "icon icon-file #{Redmine::MimeType.css_class_of(entry.name)}")%>
29 | <% else %>
30 | <% if entry.is_dir? %>
31 | <%= entry.name %>
32 | <% else %>
33 | <%= link_to h(entry.name),
34 | {:controller => 'scm_extensions', :action => 'download', :id => @project, :repository_id => @repository.identifier, :path => to_path_param(entry.path), :rev => @rev},
35 | :class => "icon icon-file #{Redmine::MimeType.css_class_of(entry.name)}"%>
36 | <% end %>
37 | <% end %>
38 |
39 | <%= (entry.size ? number_to_human_size(entry.size) : "?") unless entry.is_dir? %>
40 | <% changeset = @repository.changesets.find_by_revision(entry.lastrev.identifier) if entry.lastrev && entry.lastrev.identifier %>
41 | <% if @show_rev %>
42 | <%= scm_extensions_link_to_revision(changeset.revision, @project, @repository) if changeset %>
43 | <% end %>
44 | <%= distance_of_time_in_words(entry.lastrev.time, Time.now) if entry.lastrev && entry.lastrev.time %>
45 | <% if !@repository.is_a?(Repository::Filesystem) %>
46 | <%= changeset.nil? ? h(entry.lastrev.author.to_s.split('<').first) : changeset.author if entry.lastrev %>
47 |
48 | <% end %>
49 |
50 | <% end %>
51 | <% end %>
52 |
--------------------------------------------------------------------------------
/lib/scm_extensions_macros.rb:
--------------------------------------------------------------------------------
1 | # SCM Extensions plugin for Redmine
2 | # Copyright (C) 2010 Arnaud MARTEL
3 | #
4 | # This program is free software; you can redistribute it and/or
5 | # modify it under the terms of the GNU General Public License
6 | # as published by the Free Software Foundation; either version 2
7 | # of the License, or (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program; if not, write to the Free Software
16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 | require 'redmine'
18 | require 'sort_helper'
19 |
20 | module SCMExtensionsProjectMacro
21 | Redmine::WikiFormatting::Macros.register do
22 | desc "Display repository files. Examples:\n\n" +
23 | " !{{scm_show}} -- Show all default repository folders/files\n" +
24 | " !{{scm_show(path)}} -- Show folders/files in a specific folder\n" +
25 | " !{{scm_show(path,revision)}} -- Idem but at a specific revision\n" +
26 | " !{{scm_show(path,revision,show_rev)}} -- Idem with column revision displayed\n" +
27 | " !{{scm_show(path,revision,show_rev,link_to_details)}} -- Idem with links to details (no direct download)\n"
28 | macro :scm_show do |obj, args|
29 |
30 | return "" if !User.current.allowed_to?(:browse_repository, @project)
31 | path = ""
32 | path = args[0].strip if args[0]
33 | @rev = nil
34 | @rev = args[1].strip if (args[1] && !args[1].empty?)
35 | @show_rev = nil
36 | @show_rev = !args[2].nil? && !args[2].empty?
37 | @link_details = nil
38 | @link_details = !args[3].nil? && !args[3].empty?
39 | #need @entries, @rev, @project
40 | @repository = @project.repository
41 | @entries = @repository.entries(path, @rev)
42 | return "" if @entries.nil?
43 |
44 | o = ""
45 | o << render(:partial => 'scm_extensions/dir_list')
46 |
47 | return o.html_safe
48 | end
49 | end
50 |
51 | Redmine::WikiFormatting::Macros.register do
52 | desc "Display repository files for a specific repository. Examples:\n\n" +
53 | " !{{scm_show2(repo_id)}} -- Show all folders/files\n" +
54 | " !{{scm_show2(repo_id,path)}} -- Show folders/files in a specific folder\n" +
55 | " !{{scm_show2(repo_id,path,revision)}} -- Idem but at a specific revision\n" +
56 | " !{{scm_show2(repo_id,path,revision,show_rev)}} -- Idem with column revision displayed\n" +
57 | " !{{scm_show2(repo_id,path,revision,show_rev,link_to_details)}} -- Idem with links to details (no direct download)\n"
58 | macro :scm_show2 do |obj, args|
59 |
60 | return "" if !User.current.allowed_to?(:browse_repository, @project)
61 | repository_id = nil
62 | repository_id = args[0].strip if args[0]
63 | path = ""
64 | path = args[1].strip if args[1]
65 | @rev = nil
66 | @rev = args[2].strip if (args[2] && !args[2].empty?)
67 | @show_rev = nil
68 | @show_rev = !args[3].nil? && !args[3].empty?
69 | @link_details = nil
70 | @link_details = !args[4].nil? && !args[4].empty?
71 | #need @entries, @rev, @project
72 | @repository = @project.repositories.find_by_identifier_param(repository_id)
73 | return "" if @repository.nil?
74 | @entries = @repository.entries(path, @rev)
75 | return "" if @entries.nil?
76 |
77 | o = ""
78 | o << render(:partial => 'scm_extensions/dir_list')
79 |
80 | return o.html_safe
81 | end
82 | end
83 |
84 | Redmine::WikiFormatting::Macros.register do
85 | desc "Display list of issues. Examples:\n\n" +
86 | " !{{issue_box(query_id)}} -- Show issues filtered by a specific public query\n"
87 | macro :issue_box do |obj, args|
88 |
89 | return "" if !User.current.allowed_to?(:view_issues, @project)
90 | return "" if !args[0]
91 | queryId = args[0].strip
92 | cond = "project_id IS NULL"
93 | cond << " OR project_id = #{@project.id}" if @project
94 | @query = Query.find(queryId, :conditions => cond)
95 | @query.project = @project
96 |
97 | @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
98 | :order => "issues.id desc")
99 |
100 | return "" if @issues.nil?
101 |
102 | o = ""
103 | o << render(:partial => 'scm_extensions/issue_box')
104 |
105 | return o.html_safe
106 | end
107 | end
108 | end
109 |
--------------------------------------------------------------------------------
/lib/scm_extensions_repository_view_hook.rb:
--------------------------------------------------------------------------------
1 | # SCM Extensions plugin for Redmine
2 | # Copyright (C) 2010 Arnaud MARTEL
3 | #
4 | # This program is free software; you can redistribute it and/or
5 | # modify it under the terms of the GNU General Public License
6 | # as published by the Free Software Foundation; either version 2
7 | # of the License, or (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program; if not, write to the Free Software
16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 | class ScmExtensionsRepositoryViewHook < Redmine::Hook::ViewListener
18 | def suburi(url)
19 | baseurl = Redmine::Utils.relative_url_root
20 | if not url.match(/^#{baseurl}/)
21 | url = baseurl + url
22 | end
23 | return url
24 | end
25 | def view_repositories_show_contextual(context = { })
26 | @project = context[:project]
27 | @repository = context[:repository]
28 | @path = context[:controller].instance_variable_get("@path")
29 | @revision = context[:controller].instance_variable_get("@rev")
30 | output = ""
31 | return output if !@repository.scm.respond_to?('scm_extensions_upload')
32 | return output if (@revision && !@revision.empty? && @revision != "HEAD" && @repository.is_a?(Repository::Subversion))
33 | return output if !(User.current.allowed_to?(:scm_write_access, @project) && User.current.allowed_to?(:commit_access, @project))
34 | entry = @repository.entry(@path)
35 | output << ""
36 | if entry.is_dir?
37 | url = suburi(url_for(:controller => 'scm_extensions', :action => 'upload', :id => @project, :repository_id => @repository.identifier, :path => @path, :only_path => true))
38 | output << "#{l(:label_scm_extensions_upload)} " if @repository.scm.respond_to?('scm_extensions_upload')
39 | #output << link_to(l(:label_scm_extensions_upload), {:controller => 'scm_extensions', :action => 'upload', :id => @project, :repository_id => @repository.identifier, :path => @path, :only_path => true}, :class => 'icon icon-add') if @repository.scm.respond_to?('scm_extensions_upload')
40 | output << " "
41 | #output << link_to(l(:label_scm_extensions_new_folder), {:controller => 'scm_extensions', :action => 'mkdir', :id => @project, :repository_id => @repository.identifier, :path => @path, :only_path => true}, :class => 'icon icon-add') if @repository.scm.respond_to?('scm_extensions_mkdir')
42 | url = suburi(url_for(:controller => 'scm_extensions', :action => 'mkdir', :id => @project, :repository_id => @repository.identifier, :path => @path, :only_path => true))
43 | output << "#{l(:label_scm_extensions_new_folder)} " if @repository.scm.respond_to?('scm_extensions_mkdir')
44 | output << " "
45 | #output << link_to(l(:label_scm_extensions_delete_folder), {:controller => 'scm_extensions', :action => 'delete', :id => @project, :repository_id => @repository.identifier, :path => @path, :only_path => true}, :class => 'icon icon-del', :confirm => l(:text_are_you_sure)) if @repository.scm.respond_to?('scm_extensions_delete')
46 | url = suburi(url_for(:controller => 'scm_extensions', :action => 'delete', :id => @project, :repository_id => @repository.identifier, :path => @path, :only_path => true))
47 | output << "#{l(:label_scm_extensions_delete_folder)} " if @repository.scm.respond_to?('scm_extensions_delete')
48 | else
49 | #output << link_to(l(:label_scm_extensions_delete_file), {:controller => 'scm_extensions', :action => 'delete', :id => @project, :repository_id => @repository.identifier, :path => @path, :only_path => true}, :class => 'icon icon-del', :confirm => l(:text_are_you_sure)) if @repository.scm.respond_to?('scm_extensions_delete')
50 | url = suburi(url_for(:controller => 'scm_extensions', :action => 'delete', :id => @project, :repository_id => @repository.identifier, :path => @path, :only_path => true))
51 | output << "#{l(:label_scm_extensions_delete_file)} " if @repository.scm.respond_to?('scm_extensions_delete')
52 | end
53 | output << " "
54 | url = suburi(url_for(:controller => 'scm_extensions', :action => 'notify', :id => @project, :repository_id => @repository.identifier, :path => @path, :only_path => true))
55 | output << ""
56 | output << " "
57 | if User.current.allowed_to?(:synapse_access, @project)
58 | output << " "
59 | options={}
60 | options[:target]='_blank'
61 | begin
62 | if @repository.is_a?(Repository::Filesystem)
63 | rootdir = @repository.scm.url
64 | mountdir = rootdir.sub(/\/files$/, '')
65 | repo_size=""
66 | repo_size = `/opt/appli/checksize #{mountdir} #{@project.identifier}` if File.exist?("/opt/appli/checksize")
67 | output << repo_size + " "
68 | end
69 | if !Setting.plugin_redmine_synapse['url_help_files'].empty?
70 | url = Setting.plugin_redmine_synapse['url_help_files']
71 | link = ""+ l(:label_help) + " "
72 | output << " #{link}"
73 | end
74 | if !Setting.plugin_redmine_synapse['url_video_files'].empty?
75 | url = Setting.plugin_redmine_synapse['url_video_files']
76 | link = ""+ l(:label_synapse_video) + " "
77 | output << " #{link}"
78 | end
79 | rescue
80 | output << ""
81 | end
82 | output << " "
83 | else
84 | output << " "
85 | end
86 | output << "
"
87 | return output
88 | end
89 | end
90 |
--------------------------------------------------------------------------------
/lib/scm_extensions_filesystem_adapter_patch.rb:
--------------------------------------------------------------------------------
1 | # SCM Extensions plugin for Redmine
2 | # Copyright (C) 2010 Arnaud MARTEL
3 | #
4 | # This program is free software; you can redistribute it and/or
5 | # modify it under the terms of the GNU General Public License
6 | # as published by the Free Software Foundation; either version 2
7 | # of the License, or (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program; if not, write to the Free Software
16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 |
18 | module ScmExtensionsFilesystemAdapterPatch
19 | def self.included(base) # :nodoc:
20 | base.send(:include, FilesystemAdapterMethodsScmExtensions)
21 |
22 | base.class_eval do
23 | unloadable # Send unloadable so it will not be unloaded in development
24 | end
25 |
26 | end
27 | end
28 |
29 | module FilesystemAdapterMethodsScmExtensions
30 |
31 | def scm_extensions_upload(repository, folder_path, attachments, comments, identifier)
32 | return -1 if attachments.nil? || !attachments.is_a?(Hash)
33 | return -1 if scm_extensions_invalid_path(folder_path)
34 | metapath = (self.url =~ /\/files\/$/ && File.exist?(self.url.sub(/\/files\//, "/attributes")))
35 |
36 | rev = identifier ? "@{identifier}" : ""
37 | fullpath = File.join(repository.scm.url, folder_path)
38 | if File.exist?(fullpath) && File.directory?(fullpath)
39 | error = false
40 |
41 | if repository.supports_all_revisions?
42 | rev = -1
43 | rev = repository.latest_changeset.revision.to_i if repository.latest_changeset
44 | rev = rev + 1
45 | changeset = Changeset.create(:repository => repository,
46 | :revision => rev,
47 | :committer => User.current.login,
48 | :committed_on => Time.now,
49 | :comments => comments)
50 |
51 | end
52 | attachments.each_value do |attachment|
53 | ajaxuploaded = attachment.has_key?("token")
54 |
55 | if ajaxuploaded
56 | filename = attachment['filename']
57 | token = attachment['token']
58 | tmp_att = Attachment.find_by_token(token)
59 | file = tmp_att.diskfile
60 | else
61 | file = attachment['file']
62 | next unless file && file.size > 0 && !error
63 | filename = File.basename(file.original_filename)
64 | next if scm_extensions_invalid_path(filename)
65 | end
66 |
67 | begin
68 | if repository.supports_all_revisions?
69 | action = "A"
70 | action = "M" if File.exists?(File.join(repository.scm.url, folder_path, filename))
71 | Change.create( :changeset => changeset, :action => action, :path => File.join("/", folder_path, filename))
72 | end
73 | outfile = File.join(repository.scm.url, folder_path, filename)
74 | if ajaxuploaded
75 | if File.exist?(outfile)
76 | File.delete(outfile)
77 | end
78 | FileUtils.mv file, outfile
79 | tmp_att.destroy
80 | else
81 | File.open(outfile, "wb") do |f|
82 | buffer = ""
83 | while (buffer = file.read(8192))
84 | f.write(buffer)
85 | end
86 | end
87 | end
88 | if metapath
89 | metapathtarget = File.join(repository.scm.url, folder_path, filename).sub(/\/files\//, "/attributes/")
90 | FileUtils.mkdir_p File.dirname(metapathtarget)
91 | File.open(metapathtarget, "w") do |f|
92 | f.write("#{User.current}\n")
93 | f.write("#{rev}\n")
94 | end
95 | end
96 |
97 | rescue
98 | error = true
99 | end
100 | end
101 |
102 | if error
103 | return 1
104 | else
105 | return 0
106 | end
107 | else
108 | return 2
109 | end
110 | end
111 |
112 | def scm_extensions_delete(repository, path, comments, identifier)
113 | return -1 if path.nil? || path.empty?
114 | return -1 if scm_extensions_invalid_path(path)
115 | metapath = (self.url =~ /\/files\/$/ && File.exist?(self.url.sub(/\/files\//, "/attributes")))
116 | if File.exist?(File.join(repository.scm.url, path)) && path != "/"
117 | error = false
118 |
119 | begin
120 | if repository.supports_all_revisions?
121 | rev = -1
122 | rev = repository.latest_changeset.revision.to_i if repository.latest_changeset
123 | rev = rev + 1
124 | changeset = Changeset.create(:repository => repository,
125 | :revision => rev,
126 | :committer => User.current.login,
127 | :committed_on => Time.now,
128 | :comments => comments)
129 | Change.create( :changeset => changeset, :action => 'D', :path => File.join("/", path))
130 | end
131 |
132 | FileUtils.remove_entry_secure File.join(repository.scm.url, path)
133 | if metapath
134 | metapathtarget = File.join(repository.scm.url, path).sub(/\/files\//, "/attributes/")
135 | FileUtils.remove_entry_secure metapathtarget if File.exist?(metapathtarget)
136 | end
137 | rescue
138 | error = true
139 | end
140 |
141 | return error ? 1 : 0
142 | end
143 | end
144 |
145 | def scm_extensions_mkdir(repository, path, comments, identifier)
146 | return -1 if path.nil? || path.empty?
147 | return -1 if scm_extensions_invalid_path(path)
148 |
149 | error = false
150 | begin
151 | if repository.supports_all_revisions?
152 | rev = -1
153 | rev = repository.latest_changeset.revision.to_i if repository.latest_changeset
154 | rev = rev + 1
155 | changeset = Changeset.create(:repository => repository,
156 | :revision => rev,
157 | :committer => User.current.login,
158 | :committed_on => Time.now,
159 | :comments => "created folder: #{path}")
160 | Change.create( :changeset => changeset, :action => 'A', :path => File.join("/", path))
161 | end
162 | Dir.mkdir(File.join(repository.scm.url, path))
163 | rescue
164 | error = true
165 | end
166 |
167 | return error ? 1 : 0
168 | end
169 |
170 | def scm_extensions_invalid_path(path)
171 | return path =~ /\/\.\.\//
172 | end
173 |
174 | end
175 |
176 | Redmine::Scm::Adapters::FilesystemAdapter.send(:include, ScmExtensionsFilesystemAdapterPatch)
177 |
--------------------------------------------------------------------------------
/lib/scm_extensions_subversion_adapter_patch.rb:
--------------------------------------------------------------------------------
1 | # SCM Extensions plugin for Redmine
2 | # Copyright (C) 2010 Arnaud MARTEL
3 | #
4 | # This program is free software; you can redistribute it and/or
5 | # modify it under the terms of the GNU General Public License
6 | # as published by the Free Software Foundation; either version 2
7 | # of the License, or (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program; if not, write to the Free Software
16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 |
18 | module ScmExtensionsSubversionAdapterPatch
19 | def self.included(base) # :nodoc:
20 | base.send(:include, SubversionAdapterMethodsScmExtensions)
21 |
22 | base.class_eval do
23 | unloadable # Send unloadable so it will not be unloaded in development
24 | end
25 |
26 | end
27 | end
28 |
29 | module SubversionAdapterMethodsScmExtensions
30 | def scm_extensions_gettmpdir(create = true)
31 | tmpdir = Dir.tmpdir
32 | t = Time.now.strftime("%Y%m%d")
33 | n = nil
34 | begin
35 | path = "#{tmpdir}/#{t}-#{$$}-#{rand(0x100000000).to_s(36)}"
36 | path << "-#{n}" if n
37 | Dir.mkdir(path, 0700)
38 | Dir.rmdir(path) unless create
39 | rescue Errno::EEXIST
40 | n ||= 0
41 | n += 1
42 | retry
43 | end
44 |
45 | if block_given?
46 | begin
47 | yield path
48 | ensure
49 | FileUtils.remove_entry_secure path if File.exist?(path)
50 | fname = "#{path}.txt"
51 | FileUtils.remove_entry_secure fname if File.exist?(fname)
52 | end
53 | else
54 | path
55 | end
56 | end
57 |
58 | def scm_extensions_target(repository, path = '')
59 | base = repository.url
60 | base = base.sub(/^.*:\/\/[^\/]*\//,"file:///svnroot/") if !base.match('^file:')
61 | uri = "#{base}/#{path}"
62 | uri = URI.escape(URI.escape(uri), '[]')
63 | shell_quote(uri.gsub(/[?<>\*]/, ''))
64 | end
65 |
66 | def scm_extensions_upload(repository, folder_path, attachments, comments, identifier)
67 | return -1 if attachments.nil? || !attachments.is_a?(Hash)
68 | rev = identifier ? "@#{identifier}" : ""
69 | container = entries(folder_path, identifier)
70 | if container
71 | error = false
72 | #use co +update + ci
73 | scm_extensions_gettmpdir(false) do |dir|
74 | commentfile = "#{dir}.txt"
75 | File.open(commentfile, 'w') {|f|
76 | f.write(comments)
77 | f.flush
78 | }
79 |
80 | cmd = "#{Redmine::Scm::Adapters::SubversionAdapter::SVN_BIN} checkout #{scm_extensions_target(repository, folder_path)}#{rev} #{dir} --depth empty --username #{User.current.login}"
81 | shellout(cmd)
82 | error = true if ($? != 0)
83 |
84 | attachments.each_value do |attachment|
85 | ajaxuploaded = attachment.has_key?("token")
86 |
87 | if ajaxuploaded
88 | filename = attachment['filename']
89 | token = attachment['token']
90 | tmp_att = Attachment.find_by_token(token)
91 | file = tmp_att.diskfile
92 | else
93 | file = attachment['file']
94 | next unless file && file.size > 0 && !error
95 | filename = File.basename(file.original_filename)
96 | next if scm_extensions_invalid_path(filename)
97 | end
98 |
99 | if filename.respond_to?(:force_encoding)
100 | filename.force_encoding("UTF-8-MAC")
101 | if !filename.valid_encoding?
102 | filename.force_encoding("UTF-8")
103 | else
104 | filename.encode!(Encoding::UTF_8)
105 | end
106 | end
107 |
108 | entry = entries(File.join(folder_path,filename), identifier)
109 | if entry && entry.size > 0
110 | cmd = "#{Redmine::Scm::Adapters::SubversionAdapter::SVN_BIN} update \"#{File.join(dir, filename)}\" --username #{User.current.login}"
111 | shellout(cmd)
112 | error = true if ($? != 0)
113 | end
114 |
115 | outfile = File.join(dir, filename)
116 | if ajaxuploaded
117 | if File.exist?(outfile)
118 | File.delete(outfile)
119 | end
120 | FileUtils.mv file, outfile
121 | tmp_att.destroy
122 | else
123 | File.open(outfile, "wb") do |f|
124 | buffer = ""
125 | while (buffer = file.read(8192))
126 | f.write(buffer)
127 | end
128 | end
129 | end
130 |
131 | if !entry || entry.size == 0
132 | cmd = "#{Redmine::Scm::Adapters::SubversionAdapter::SVN_BIN} add \"#{File.join(dir, filename)}\" --username #{User.current.login}"
133 | shellout(cmd)
134 | error = true if ($? != 0)
135 | end
136 | end
137 | if !error
138 | cmd = "#{Redmine::Scm::Adapters::SubversionAdapter::SVN_BIN} commit #{dir} -F #{commentfile} --username #{User.current.login}"
139 | shellout(cmd)
140 | error = true if ($? != 0 && $? != 256)
141 | end
142 |
143 | end
144 |
145 | if error
146 | return 1
147 | else
148 | return 0
149 | end
150 | else
151 | return 2
152 | end
153 | end
154 |
155 | def scm_extensions_delete(repository, path, comments, identifier)
156 | return -1 if path.nil? || path.empty?
157 | rev = identifier ? "@#{identifier}" : ""
158 | container = entries(path, identifier)
159 | if container && path != "/"
160 | error = false
161 | scm_extensions_gettmpdir(false) do |dir|
162 | commentfile = "#{dir}.txt"
163 | File.open(commentfile, 'w') {|f|
164 | f.write(comments)
165 | f.flush
166 | }
167 | cmd = "#{Redmine::Scm::Adapters::SubversionAdapter::SVN_BIN} delete #{scm_extensions_target(repository, path)}#{rev} -F #{commentfile} --username #{User.current.login}"
168 | shellout(cmd)
169 | error = true if ($? != 0 && $? != 256)
170 | end
171 | return error ? 1 : 0
172 | end
173 | end
174 |
175 | def scm_extensions_mkdir(repository, path, comments, identifier)
176 | return -1 if path.nil? || path.empty?
177 | rev = identifier ? "@#{identifier}" : ""
178 | error = false
179 | scm_extensions_gettmpdir(false) do |dir|
180 | commentfile = "#{dir}.txt"
181 | File.open(commentfile, 'w') {|f|
182 | f.write(comments)
183 | f.flush
184 | }
185 | cmd = "#{Redmine::Scm::Adapters::SubversionAdapter::SVN_BIN} mkdir #{scm_extensions_target(repository, path)}#{rev} -F #{commentfile} --username #{User.current.login}"
186 | shellout(cmd)
187 | error = true if ($? != 0 && $? != 256)
188 | end
189 | return error ? 1 : 0
190 | end
191 |
192 | def scm_extensions_invalid_path(path)
193 | return path =~ /\/\.\.\//
194 | end
195 |
196 | end
197 |
198 | Redmine::Scm::Adapters::SubversionAdapter.send(:include, ScmExtensionsSubversionAdapterPatch)
199 |
--------------------------------------------------------------------------------
/app/controllers/scm_extensions_controller.rb:
--------------------------------------------------------------------------------
1 | # SCM Extensions plugin for Redmine
2 | # Copyright (C) 2010 Arnaud MARTEL
3 | #
4 | # This program is free software; you can redistribute it and/or
5 | # modify it under the terms of the GNU General Public License
6 | # as published by the Free Software Foundation; either version 2
7 | # of the License, or (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program; if not, write to the Free Software
16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 | require 'tmpdir'
18 | require 'fileutils'
19 |
20 | class ScmExtensionsController < ApplicationController
21 | unloadable
22 |
23 | layout 'base'
24 | before_filter :find_project, :except => [:show, :download]
25 | before_filter :find_repository, :only => [:show, :download]
26 | before_filter :authorize, :except => [:show, :download]
27 |
28 | helper :attachments
29 | include AttachmentsHelper
30 |
31 | def upload
32 | path_root = @repository.identifier.blank? ? "root" : @repository.identifier
33 | path = ""
34 | path << path_root
35 | path << "/#{params[:path]}" if (params[:path] && !params[:path].empty?)
36 | @scm_extensions = ScmExtensionsWrite.new(:path => path, :project => @project, :repository => @repository)
37 |
38 | if !request.get? && !request.xhr?
39 | @scm_extensions.path = params[:scm_extensions][:path]
40 | @scm_extensions.comments = params[:scm_extensions][:comments]
41 | @scm_extensions.recipients = params[:watchers]
42 | reg = Regexp.new("^#{path_root}")
43 | path = params[:scm_extensions][:path].sub(reg,'').sub(/^\//,'')
44 | attached = []
45 | if params[:attachments] && params[:attachments].is_a?(Hash)
46 | svnpath = path.empty? ? "/" : path
47 |
48 | if @repository.scm.respond_to?('scm_extensions_upload')
49 | ret = @repository.scm.scm_extensions_upload(@repository, svnpath, params[:attachments], params[:scm_extensions][:comments], nil)
50 | case ret
51 | when 0
52 | flash[:notice] = l(:notice_scm_extensions_upload_success)
53 | @scm_extensions.deliver(params[:attachments]) if @scm_extensions.recipients
54 | when 1
55 | flash[:error] = l(:error_scm_extensions_upload_failed)
56 | when 2
57 | flash[:error] = l(:error_scm_extensions_no_path_head)
58 | end
59 | end
60 |
61 | end
62 | if @repository.identifier.blank?
63 | redirect_to :controller => 'repositories', :action => 'show', :id => @project, :path => path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
64 | else
65 | redirect_to :controller => 'repositories', :action => 'show', :id => @project, :repository_id => @repository.identifier, :path => path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
66 | end
67 | return
68 | end
69 | end
70 |
71 | def delete
72 | path = params[:path]
73 | parent = path
74 | svnpath = path.empty? ? "/" : path
75 |
76 | if @repository.scm.respond_to?('scm_extensions_delete')
77 | ret = @repository.scm.scm_extensions_delete(@repository, svnpath, "deleted #{path}", nil)
78 | case ret
79 | when 0
80 | parent = File.dirname(svnpath).sub(/^\//,'')
81 | flash[:notice] = l(:notice_scm_extensions_delete_success)
82 | when 1
83 | flash[:error] = l(:error_scm_extensions_delete_failed)
84 | end
85 | end
86 |
87 | if @repository.identifier.blank?
88 | redirect_to :controller => 'repositories', :action => 'show', :id => @project, :path => parent.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
89 | else
90 | redirect_to :controller => 'repositories', :action => 'show', :id => @project, :repository_id => @repository.identifier, :path => parent.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
91 | end
92 | return
93 | end
94 |
95 | def mkdir
96 | path_root = @repository.identifier.blank? ? "root" : @repository.identifier
97 | path = ""
98 | path << path_root
99 | path << "/#{params[:path]}" if (params[:path] && !params[:path].empty?)
100 | @scm_extensions = ScmExtensionsWrite.new(:path => path, :project => @project)
101 |
102 | if !request.get? && !request.xhr?
103 | path = params[:scm_extensions][:path].sub(/^#{path_root}/,'').sub(/^\//,'')
104 | foldername = params[:scm_extensions][:new_folder]
105 | svnpath = path.empty? ? "/" : path
106 |
107 | if @repository.scm.respond_to?('scm_extensions_mkdir')
108 | ret = @repository.scm.scm_extensions_mkdir(@repository, File.join(svnpath, foldername), params[:scm_extensions][:comments], nil)
109 | case ret
110 | when 0
111 | flash[:notice] = l(:notice_scm_extensions_mkdir_success)
112 | when 1
113 | flash[:error] = l(:error_scm_extensions_mkdir_failed)
114 | end
115 | end
116 | if @repository.identifier.blank?
117 | redirect_to :controller => 'repositories', :action => 'show', :id => @project, :path => path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
118 | else
119 | redirect_to :controller => 'repositories', :action => 'show', :id => @project, :repository_id => @repository.identifier, :path => path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
120 | end
121 | return
122 | end
123 | end
124 |
125 | def show
126 | return if !User.current.allowed_to?(:browse_repository, @project)
127 | @show_cb = params[:show_cb] if params[:show_cb] && !(params[:show_cb] =~ (/(false|f|no|n|0)$/i))
128 | @show_rev = params[:show_rev] if params[:show_rev] && !(params[:show_rev] =~ (/(false|f|no|n|0)$/i))
129 | @link_details = params[:link_details] if params[:link_details] && !(params[:link_details] =~ (/(false|f|no|n|0)$/i))
130 | @entries = @repository.entries(@path, @rev)
131 | if request.xhr?
132 | @entries ? render(:partial => 'scm_extensions/dir_list_content') : render(:nothing => true)
133 | end
134 | end
135 |
136 | def download
137 | return if !User.current.allowed_to?(:browse_repository, @project)
138 | @entry = @repository.entry(@path, @rev)
139 | (show_error_not_found; return) unless @entry
140 |
141 | # If the entry is a dir, show the browser
142 | (show; return) if @entry.is_dir?
143 |
144 | if @repository.is_a?(Repository::Filesystem)
145 | data_to_send = File.new(File.join(@repository.scm.url, @path))
146 | (show_error_not_found; return) unless File.exists?(data_to_send.path)
147 | send_file File.expand_path(data_to_send.path), :filename => @path.split('/').last, :stream => true
148 | else
149 | @content = @repository.cat(@path, @rev)
150 | (show_error_not_found; return) unless @content
151 | # Force the download
152 | send_data @content, :filename => @path.split('/').last, :disposition => "inline", :type => Redmine::MimeType.of(@path.split('/').last)
153 | end
154 | end
155 |
156 | def notify
157 | path_root = @repository.identifier.blank? ? "root" : @repository.identifier
158 | path = ""
159 | path << path_root
160 | path << "/#{params[:path]}" if (params[:path] && !params[:path].empty?)
161 | @scm_extensions = ScmExtensionsWrite.new(:path => path, :project => @project, :repository => @repository)
162 | @show_cb = true
163 |
164 | @rev = nil
165 | @show_rev = nil
166 | @link_details = nil
167 | #need @entries, @rev, @project
168 | spath = ""
169 | spath = params[:path] if (params[:path] && !params[:path].empty?)
170 | @entries = @repository.entries(spath, @rev)
171 |
172 | if !request.get? && !request.xhr?
173 | @scm_extensions.path = params[:scm_extensions][:path]
174 | @scm_extensions.comments = params[:scm_extensions][:comments]
175 | @scm_extensions.recipients = params[:watchers]
176 | reg = Regexp.new("^#{path_root}")
177 | path = params[:scm_extensions][:path].sub(reg,'').sub(/^\//,'')
178 | attached = []
179 | svnpath = path.empty? ? "/" : path
180 | selectedfiles = []
181 | if params[:selectedfiles]
182 | reg2 = Regexp.new("^#{path}")
183 | params[:selectedfiles].each do |entrypath|
184 | selectedfiles << entrypath.sub(reg2,'').sub(/^\//,'')
185 | end
186 | end
187 |
188 | @scm_extensions.notify(selectedfiles)
189 | flash[:notice] = l(:notice_scm_extensions_email_success) if @scm_extensions.recipients
190 |
191 | if @repository.identifier.blank?
192 | redirect_to :controller => 'repositories', :action => 'show', :id => @project, :path => path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
193 | else
194 | redirect_to :controller => 'repositories', :action => 'show', :id => @project, :repository_id => @repository.identifier, :path => path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
195 | end
196 | return
197 | end
198 | end
199 |
200 | private
201 |
202 | def find_project
203 | @project = Project.find(params[:id])
204 | if params[:repository_id].present?
205 | @repository = @project.repositories.find_by_identifier_param(params[:repository_id])
206 | else
207 | @repository = @project.repository
208 | end
209 | rescue ActiveRecord::RecordNotFound
210 | render_404
211 | end
212 |
213 | def find_repository
214 | @project = Project.find(params[:id])
215 | if params[:repository_id].present?
216 | @repository = @project.repositories.find_by_identifier_param(params[:repository_id])
217 | else
218 | @repository = @project.repository
219 | end
220 | (render_404; return false) unless @repository
221 | @path = (params[:path].kind_of?(Array) ? params[:path].join('/') : params[:path]) unless params[:path].nil?
222 | @path ||= ''
223 | @rev = params[:rev].blank? ? @repository.default_branch : params[:rev].strip
224 | @rev_to = params[:rev_to]
225 | rescue ActiveRecord::RecordNotFound
226 | render_404
227 | rescue InvalidRevisionParam
228 | show_error_not_found
229 | end
230 |
231 | def svn_target(repository, path = '')
232 | base = repository.url
233 | base = base.sub(/^.*:\/\/[^\/]*\//,"file:///svnroot/")
234 | uri = "#{base}/#{path}"
235 | uri = URI.escape(URI.escape(uri), '[]')
236 | shell_quote(uri.gsub(/[?<>\*]/, ''))
237 | end
238 |
239 | def gettmpdir(create = true)
240 | tmpdir = Dir.tmpdir
241 | t = Time.now.strftime("%Y%m%d")
242 | n = nil
243 | begin
244 | path = "#{tmpdir}/#{t}-#{$$}-#{rand(0x100000000).to_s(36)}"
245 | path << "-#{n}" if n
246 | Dir.mkdir(path, 0700)
247 | Dir.rmdir(path) unless create
248 | rescue Errno::EEXIST
249 | n ||= 0
250 | n += 1
251 | retry
252 | end
253 |
254 | if block_given?
255 | begin
256 | yield path
257 | ensure
258 | FileUtils.remove_entry_secure path if File.exist?(path)
259 | fname = "#{path}.txt"
260 | FileUtils.remove_entry_secure fname if File.exist?(fname)
261 | end
262 | else
263 | path
264 | end
265 | end
266 |
267 | def shell_quote(str)
268 | if Redmine::Platform.mswin?
269 | '"' + str.gsub(/"/, '\\"') + '"'
270 | else
271 | "'" + str.gsub(/'/, "'\"'\"'") + "'"
272 | end
273 | end
274 |
275 | end
276 |
--------------------------------------------------------------------------------