├── .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 | 6 | <% end %> 7 |
-------------------------------------------------------------------------------- /app/views/scm_extensions/_dir_list.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <% if @show_cb %> 5 | 6 | <% end %> 7 | 8 | 9 | <% if @show_rev %> 10 | 11 | <% end %> 12 | 13 | <% if !@repository.is_a?(Repository::Filesystem) %> 14 | 15 | 16 | <% end %> 17 | 18 | 19 | 20 | <%= render :partial => 'scm_extensions/dir_list_content' %> 21 | 22 |
 <%= l(:field_name) %><%= l(:field_filesize) %><%= l(:label_revision) %><%= l(:label_age) %><%= l(:field_author) %><%= l(:field_comments) %>
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 |

<%= f.text_area :comments, :cols => 100, :rows => 10, :accesskey => accesskey(:edit), :class => 'wiki-edit' %>

9 | <% end %> 10 |


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


<%= render :partial => 'scm_extensions/dir_list' %>

36 |
37 |

<%= f.text_area :comments, :cols => 100, :rows => 10, :accesskey => accesskey(:edit), :class => 'wiki-edit' %>

38 |

39 | <% @project.users.sort.each do |user| -%> 40 | 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 |

<%= f.text_area :comments, :cols => 100, :rows => 10, :accesskey => accesskey(:edit), :class => 'wiki-edit' %>

37 |

38 | <% @project.users.sort.each do |user| -%> 39 | 40 | <% end -%>
41 | 42 | 43 |

44 |
45 |


<%= 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 | <%=h truncate(changeset.comments, :length => 50) unless changeset.nil? %> 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 << "" 57 | if User.current.allowed_to?(:synapse_access, @project) 58 | output << "" 83 | else 84 | output << "" 85 | end 86 | 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 << "#{l(:label_scm_extensions_notify)}" 56 | 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 << "
" 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 | --------------------------------------------------------------------------------