Redmine 3.4.4
This commit is contained in:
commit
64924a6376
2112 changed files with 259028 additions and 0 deletions
213
extra/mail_handler/rdm-mailhandler.rb
Normal file
213
extra/mail_handler/rdm-mailhandler.rb
Normal file
|
@ -0,0 +1,213 @@
|
|||
#!/usr/bin/env ruby
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
require 'net/http'
|
||||
require 'net/https'
|
||||
require 'uri'
|
||||
require 'optparse'
|
||||
|
||||
module Net
|
||||
class HTTPS < HTTP
|
||||
def self.post_form(url, params, headers, options={})
|
||||
request = Post.new(url.path)
|
||||
request.form_data = params
|
||||
request.initialize_http_header(headers)
|
||||
request.basic_auth url.user, url.password if url.user
|
||||
http = new(url.host, url.port)
|
||||
http.use_ssl = (url.scheme == 'https')
|
||||
if options[:certificate_bundle]
|
||||
http.ca_file = options[:certificate_bundle]
|
||||
end
|
||||
if options[:no_check_certificate]
|
||||
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
||||
end
|
||||
http.start {|h| h.request(request) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class RedmineMailHandler
|
||||
VERSION = '0.2.3'
|
||||
|
||||
attr_accessor :verbose, :issue_attributes, :allow_override, :unknown_user, :default_group, :no_permission_check,
|
||||
:url, :key, :no_check_certificate, :certificate_bundle, :no_account_notice, :no_notification, :project_from_subaddress
|
||||
|
||||
def initialize
|
||||
self.issue_attributes = {}
|
||||
|
||||
optparse = OptionParser.new do |opts|
|
||||
opts.banner = "Usage: rdm-mailhandler.rb [options] --url=<Redmine URL> --key=<API key>"
|
||||
opts.separator("")
|
||||
opts.separator("Reads an email from standard input and forwards it to a Redmine server through a HTTP request.")
|
||||
opts.separator("")
|
||||
opts.separator("Required arguments:")
|
||||
opts.on("-u", "--url URL", "URL of the Redmine server") {|v| self.url = v}
|
||||
opts.on("-k", "--key KEY", "Redmine API key") {|v| self.key = v}
|
||||
opts.separator("")
|
||||
opts.separator("General options:")
|
||||
opts.on("--key-file FILE", "full path to a file that contains your Redmine",
|
||||
"API key (use this option instead of --key if",
|
||||
"you don't want the key to appear in the command",
|
||||
"line)") {|v| read_key_from_file(v)}
|
||||
opts.on("--no-check-certificate", "do not check server certificate") {self.no_check_certificate = true}
|
||||
opts.on("--certificate-bundle FILE", "certificate bundle to use") {|v| self.certificate_bundle = v}
|
||||
opts.on("-h", "--help", "show this help") {puts opts; exit 1}
|
||||
opts.on("-v", "--verbose", "show extra information") {self.verbose = true}
|
||||
opts.on("-V", "--version", "show version information and exit") {puts VERSION; exit}
|
||||
opts.separator("")
|
||||
opts.separator("User and permissions options:")
|
||||
opts.on("--unknown-user ACTION", "how to handle emails from an unknown user",
|
||||
"ACTION can be one of the following values:",
|
||||
"* ignore: email is ignored (default)",
|
||||
"* accept: accept as anonymous user",
|
||||
"* create: create a user account") {|v| self.unknown_user = v}
|
||||
opts.on("--no-permission-check", "disable permission checking when receiving",
|
||||
"the email") {self.no_permission_check = '1'}
|
||||
opts.on("--default-group GROUP", "add created user to GROUP (none by default)",
|
||||
"GROUP can be a comma separated list of groups") { |v| self.default_group = v}
|
||||
opts.on("--no-account-notice", "don't send account information to the newly",
|
||||
"created user") { |v| self.no_account_notice = '1'}
|
||||
opts.on("--no-notification", "disable email notifications for the created",
|
||||
"user") { |v| self.no_notification = '1'}
|
||||
opts.separator("")
|
||||
opts.separator("Issue attributes control options:")
|
||||
opts.on( "--project-from-subaddress ADDR", "select project from subadress of ADDR found",
|
||||
"in To, Cc, Bcc headers") {|v| self.project_from_subaddress = v}
|
||||
opts.on("-p", "--project PROJECT", "identifier of the target project") {|v| self.issue_attributes['project'] = v}
|
||||
opts.on("-s", "--status STATUS", "name of the target status") {|v| self.issue_attributes['status'] = v}
|
||||
opts.on("-t", "--tracker TRACKER", "name of the target tracker") {|v| self.issue_attributes['tracker'] = v}
|
||||
opts.on( "--category CATEGORY", "name of the target category") {|v| self.issue_attributes['category'] = v}
|
||||
opts.on( "--priority PRIORITY", "name of the target priority") {|v| self.issue_attributes['priority'] = v}
|
||||
opts.on( "--assigned-to ASSIGNEE", "assignee (username or group name)") {|v| self.issue_attributes['assigned_to'] = v}
|
||||
opts.on( "--fixed-version VERSION","name of the target version") {|v| self.issue_attributes['fixed_version'] = v}
|
||||
opts.on( "--private", "create new issues as private") {|v| self.issue_attributes['is_private'] = '1'}
|
||||
opts.on("-o", "--allow-override ATTRS", "allow email content to set attributes values",
|
||||
"ATTRS is a comma separated list of attributes",
|
||||
"or 'all' to allow all attributes to be",
|
||||
"overridable (see below for details)") {|v| self.allow_override = v}
|
||||
|
||||
opts.separator <<-END_DESC
|
||||
|
||||
Overrides:
|
||||
ATTRS is a comma separated list of attributes among:
|
||||
* project, tracker, status, priority, category, assigned_to, fixed_version,
|
||||
start_date, due_date, estimated_hours, done_ratio
|
||||
* custom fields names with underscores instead of spaces (case insensitive)
|
||||
Example: --allow-override=project,priority,my_custom_field
|
||||
|
||||
If the --project option is not set, project is overridable by default for
|
||||
emails that create new issues.
|
||||
|
||||
You can use --allow-override=all to allow all attributes to be overridable.
|
||||
|
||||
Examples:
|
||||
No project specified, emails MUST contain the 'Project' keyword, otherwise
|
||||
they will be dropped (not recommended):
|
||||
|
||||
rdm-mailhandler.rb --url http://redmine.domain.foo --key secret
|
||||
|
||||
Fixed project and default tracker specified, but emails can override
|
||||
both tracker and priority attributes using keywords:
|
||||
|
||||
rdm-mailhandler.rb --url https://domain.foo/redmine --key secret \\
|
||||
--project myproject \\
|
||||
--tracker bug \\
|
||||
--allow-override tracker,priority
|
||||
|
||||
Project selected by subaddress of redmine@example.net. Sending the email
|
||||
to redmine+myproject@example.net will add the issue to myproject:
|
||||
|
||||
rdm-mailhandler.rb --url http://redmine.domain.foo --key secret \\
|
||||
--project-from-subaddress redmine@example.net
|
||||
END_DESC
|
||||
|
||||
opts.summary_width = 27
|
||||
end
|
||||
optparse.parse!
|
||||
|
||||
unless url && key
|
||||
puts "Some arguments are missing. Use `rdm-mailhandler.rb --help` for getting help."
|
||||
exit 1
|
||||
end
|
||||
end
|
||||
|
||||
def submit(email)
|
||||
uri = url.gsub(%r{/*$}, '') + '/mail_handler'
|
||||
|
||||
headers = { 'User-Agent' => "Redmine mail handler/#{VERSION}" }
|
||||
|
||||
data = { 'key' => key, 'email' => email,
|
||||
'allow_override' => allow_override,
|
||||
'unknown_user' => unknown_user,
|
||||
'default_group' => default_group,
|
||||
'no_account_notice' => no_account_notice,
|
||||
'no_notification' => no_notification,
|
||||
'no_permission_check' => no_permission_check,
|
||||
'project_from_subaddress' => project_from_subaddress}
|
||||
issue_attributes.each { |attr, value| data["issue[#{attr}]"] = value }
|
||||
|
||||
debug "Posting to #{uri}..."
|
||||
begin
|
||||
response = Net::HTTPS.post_form(URI.parse(uri), data, headers, :no_check_certificate => no_check_certificate, :certificate_bundle => certificate_bundle)
|
||||
rescue SystemCallError, IOError => e # connection refused, etc.
|
||||
warn "An error occurred while contacting your Redmine server: #{e.message}"
|
||||
return 75 # temporary failure
|
||||
end
|
||||
debug "Response received: #{response.code}"
|
||||
|
||||
case response.code.to_i
|
||||
when 403
|
||||
warn "Request was denied by your Redmine server. " +
|
||||
"Make sure that 'WS for incoming emails' is enabled in application settings and that you provided the correct API key."
|
||||
return 77
|
||||
when 422
|
||||
warn "Request was denied by your Redmine server. " +
|
||||
"Possible reasons: email is sent from an invalid email address or is missing some information."
|
||||
return 77
|
||||
when 400..499
|
||||
warn "Request was denied by your Redmine server (#{response.code})."
|
||||
return 77
|
||||
when 500..599
|
||||
warn "Failed to contact your Redmine server (#{response.code})."
|
||||
return 75
|
||||
when 201
|
||||
debug "Proccessed successfully"
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def debug(msg)
|
||||
puts msg if verbose
|
||||
end
|
||||
|
||||
def read_key_from_file(filename)
|
||||
begin
|
||||
self.key = File.read(filename).strip
|
||||
rescue Exception => e
|
||||
$stderr.puts "Unable to read the key from #{filename}:\n#{e.message}"
|
||||
exit 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
handler = RedmineMailHandler.new
|
||||
exit(handler.submit(STDIN.read))
|
15
extra/sample_plugin/README
Normal file
15
extra/sample_plugin/README
Normal file
|
@ -0,0 +1,15 @@
|
|||
== Sample plugin
|
||||
|
||||
This is a sample plugin for Redmine
|
||||
|
||||
== Installation
|
||||
|
||||
1. Copy the plugin directory into the "plugins" directory at the root
|
||||
of your Redmine directory
|
||||
|
||||
2. Migrate and copy plugin assets:
|
||||
rake redmine:plugins
|
||||
|
||||
3. Start Redmine
|
||||
|
||||
Installed plugins are listed and can be configured from 'Admin -> Plugins' screen.
|
20
extra/sample_plugin/app/controllers/example_controller.rb
Normal file
20
extra/sample_plugin/app/controllers/example_controller.rb
Normal file
|
@ -0,0 +1,20 @@
|
|||
# Sample plugin controller
|
||||
class ExampleController < ApplicationController
|
||||
unloadable
|
||||
|
||||
layout 'base'
|
||||
before_action :find_project, :authorize
|
||||
menu_item :sample_plugin
|
||||
|
||||
def say_hello
|
||||
@value = Setting.plugin_sample_plugin['sample_setting']
|
||||
end
|
||||
|
||||
def say_goodbye
|
||||
end
|
||||
|
||||
private
|
||||
def find_project
|
||||
@project=Project.find(params[:id])
|
||||
end
|
||||
end
|
12
extra/sample_plugin/app/models/meeting.rb
Normal file
12
extra/sample_plugin/app/models/meeting.rb
Normal file
|
@ -0,0 +1,12 @@
|
|||
class Meeting < ActiveRecord::Base
|
||||
belongs_to :project
|
||||
|
||||
acts_as_event :title => Proc.new {|o| "#{o.scheduled_on} Meeting"},
|
||||
:datetime => :scheduled_on,
|
||||
:author => nil,
|
||||
:url => Proc.new {|o| {:controller => 'meetings', :action => 'show', :id => o.id}}
|
||||
|
||||
acts_as_activity_provider :timestamp => 'scheduled_on',
|
||||
:scope => includes(:project),
|
||||
:permission => nil
|
||||
end
|
|
@ -0,0 +1,7 @@
|
|||
<h2>Good Bye</h2>
|
||||
|
||||
<p class="icon icon-example-works"><%= l(:text_say_goodbye) %></p>
|
||||
|
||||
<% content_for :header_tags do %>
|
||||
<%= stylesheet_link_tag 'example', :plugin => 'sample_plugin', :media => "screen" %>
|
||||
<% end %>
|
15
extra/sample_plugin/app/views/example/say_hello.html.erb
Normal file
15
extra/sample_plugin/app/views/example/say_hello.html.erb
Normal file
|
@ -0,0 +1,15 @@
|
|||
<h2>Hello</h2>
|
||||
|
||||
<p class="icon icon-example-works"><%= l(:text_say_hello) %></p>
|
||||
|
||||
<p><label>Example setting</label>: <%= @value %></p>
|
||||
|
||||
<%= link_to('Good bye', :action => 'say_goodbye', :id => @project) if User.current.allowed_to?(:example_say_goodbye, @project) %>
|
||||
|
||||
<% content_for :sidebar do %>
|
||||
<p>Adding content to the sidebar...</p>
|
||||
<% end %>
|
||||
|
||||
<% content_for :header_tags do %>
|
||||
<%= stylesheet_link_tag 'example', :plugin => 'sample_plugin', :media => "screen" %>
|
||||
<% end %>
|
|
@ -0,0 +1,3 @@
|
|||
<h3>Sample block</h3>
|
||||
|
||||
You are <strong><%= h(User.current) %></strong> and this is a sample block for My Page added from a plugin.
|
|
@ -0,0 +1,3 @@
|
|||
<p><label>Example setting</label><%= text_field_tag 'settings[sample_setting]', @settings['sample_setting'] %></p>
|
||||
|
||||
<p><label>Foo</label><%= text_field_tag 'settings[foo]', @settings['foo'] %></p>
|
BIN
extra/sample_plugin/assets/images/it_works.png
Normal file
BIN
extra/sample_plugin/assets/images/it_works.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.5 KiB |
1
extra/sample_plugin/assets/stylesheets/example.css
Normal file
1
extra/sample_plugin/assets/stylesheets/example.css
Normal file
|
@ -0,0 +1 @@
|
|||
.icon-example-works { background-image: url(../images/it_works.png); }
|
6
extra/sample_plugin/config/locales/en.yml
Normal file
6
extra/sample_plugin/config/locales/en.yml
Normal file
|
@ -0,0 +1,6 @@
|
|||
# Sample plugin
|
||||
en:
|
||||
label_plugin_example: Sample Plugin
|
||||
label_meeting_plural: Meetings
|
||||
text_say_hello: Plugin say 'Hello'
|
||||
text_say_goodbye: Plugin say 'Good bye'
|
6
extra/sample_plugin/config/locales/fr.yml
Normal file
6
extra/sample_plugin/config/locales/fr.yml
Normal file
|
@ -0,0 +1,6 @@
|
|||
# Sample plugin
|
||||
fr:
|
||||
label_plugin_example: Plugin exemple
|
||||
label_meeting_plural: Meetings
|
||||
text_say_hello: Plugin dit 'Bonjour'
|
||||
text_say_goodbye: Plugin dit 'Au revoir'
|
7
extra/sample_plugin/config/routes.rb
Normal file
7
extra/sample_plugin/config/routes.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
# Plugin's routes
|
||||
# See: http://guides.rubyonrails.org/routing.html
|
||||
|
||||
match 'projects/:id/hello', :to => 'example#say_hello', :via => 'get'
|
||||
match 'projects/:id/bye', :to => 'example#say_goodbye', :via => 'get'
|
||||
|
||||
resources 'meetings'
|
15
extra/sample_plugin/db/migrate/001_create_meetings.rb
Normal file
15
extra/sample_plugin/db/migrate/001_create_meetings.rb
Normal file
|
@ -0,0 +1,15 @@
|
|||
# Sample plugin migration
|
||||
# Use rake db:migrate_plugins to migrate installed plugins
|
||||
class CreateMeetings < ActiveRecord::Migration
|
||||
def self.up
|
||||
create_table :meetings do |t|
|
||||
t.column :project_id, :integer, :null => false
|
||||
t.column :description, :string
|
||||
t.column :scheduled_on, :datetime
|
||||
end
|
||||
end
|
||||
|
||||
def self.down
|
||||
drop_table :meetings
|
||||
end
|
||||
end
|
27
extra/sample_plugin/init.rb
Normal file
27
extra/sample_plugin/init.rb
Normal file
|
@ -0,0 +1,27 @@
|
|||
Rails.logger.info 'Starting Example plugin for RedMine'
|
||||
|
||||
Redmine::Plugin.register :sample_plugin do
|
||||
name 'Example plugin'
|
||||
author 'Author name'
|
||||
description 'This is a sample plugin for Redmine'
|
||||
version '0.0.1'
|
||||
settings :default => {'sample_setting' => 'value', 'foo'=>'bar'}, :partial => 'settings/sample_plugin_settings'
|
||||
|
||||
# This plugin adds a project module
|
||||
# It can be enabled/disabled at project level (Project settings -> Modules)
|
||||
project_module :example_module do
|
||||
# A public action
|
||||
permission :example_say_hello, {:example => [:say_hello]}, :public => true
|
||||
# This permission has to be explicitly given
|
||||
# It will be listed on the permissions screen
|
||||
permission :example_say_goodbye, {:example => [:say_goodbye]}
|
||||
# This permission can be given to project members only
|
||||
permission :view_meetings, {:meetings => [:index, :show]}, :require => :member
|
||||
end
|
||||
|
||||
# A new item is added to the project menu
|
||||
menu :project_menu, :sample_plugin, { :controller => 'example', :action => 'say_hello' }, :caption => 'Sample'
|
||||
|
||||
# Meetings are added to the activity view
|
||||
activity_provider :meetings
|
||||
end
|
12
extra/sample_plugin/test/integration/routing_test.rb
Normal file
12
extra/sample_plugin/test/integration/routing_test.rb
Normal file
|
@ -0,0 +1,12 @@
|
|||
|
||||
require File.expand_path(File.dirname(__FILE__) + '../../../../../test/test_helper')
|
||||
|
||||
class SamplePluginRoutingTest < ActionDispatch::IntegrationTest
|
||||
def test_example
|
||||
assert_routing(
|
||||
{ :method => 'get', :path => "/projects/1234/hello" },
|
||||
{ :controller => 'example', :action => 'say_hello',
|
||||
:id => '1234' }
|
||||
)
|
||||
end
|
||||
end
|
560
extra/svn/Redmine.pm
Normal file
560
extra/svn/Redmine.pm
Normal file
|
@ -0,0 +1,560 @@
|
|||
package Apache::Authn::Redmine;
|
||||
|
||||
=head1 Apache::Authn::Redmine
|
||||
|
||||
Redmine - a mod_perl module to authenticate webdav subversion users
|
||||
against redmine database
|
||||
|
||||
=head1 SYNOPSIS
|
||||
|
||||
This module allow anonymous users to browse public project and
|
||||
registred users to browse and commit their project. Authentication is
|
||||
done against the redmine database or the LDAP configured in redmine.
|
||||
|
||||
This method is far simpler than the one with pam_* and works with all
|
||||
database without an hassle but you need to have apache/mod_perl on the
|
||||
svn server.
|
||||
|
||||
=head1 INSTALLATION
|
||||
|
||||
For this to automagically work, you need to have a recent reposman.rb
|
||||
(after r860) and if you already use reposman, read the last section to
|
||||
migrate.
|
||||
|
||||
Sorry ruby users but you need some perl modules, at least mod_perl2,
|
||||
DBI and DBD::mysql (or the DBD driver for you database as it should
|
||||
work on allmost all databases).
|
||||
|
||||
On debian/ubuntu you must do :
|
||||
|
||||
aptitude install libapache-dbi-perl libapache2-mod-perl2 libdbd-mysql-perl
|
||||
|
||||
If your Redmine users use LDAP authentication, you will also need
|
||||
Authen::Simple::LDAP (and IO::Socket::SSL if LDAPS is used):
|
||||
|
||||
aptitude install libauthen-simple-ldap-perl libio-socket-ssl-perl
|
||||
|
||||
=head1 CONFIGURATION
|
||||
|
||||
## This module has to be in your perl path
|
||||
## eg: /usr/lib/perl5/Apache/Authn/Redmine.pm
|
||||
PerlLoadModule Apache::Authn::Redmine
|
||||
<Location /svn>
|
||||
DAV svn
|
||||
SVNParentPath "/var/svn"
|
||||
|
||||
AuthType Basic
|
||||
AuthName redmine
|
||||
Require valid-user
|
||||
|
||||
PerlAccessHandler Apache::Authn::Redmine::access_handler
|
||||
PerlAuthenHandler Apache::Authn::Redmine::authen_handler
|
||||
|
||||
## for mysql
|
||||
RedmineDSN "DBI:mysql:database=databasename;host=my.db.server"
|
||||
## for postgres
|
||||
# RedmineDSN "DBI:Pg:dbname=databasename;host=my.db.server"
|
||||
|
||||
RedmineDbUser "redmine"
|
||||
RedmineDbPass "password"
|
||||
## Optional where clause (fulltext search would be slow and
|
||||
## database dependent).
|
||||
# RedmineDbWhereClause "and members.role_id IN (1,2)"
|
||||
## Optional credentials cache size
|
||||
# RedmineCacheCredsMax 50
|
||||
</Location>
|
||||
|
||||
To be able to browse repository inside redmine, you must add something
|
||||
like that :
|
||||
|
||||
<Location /svn-private>
|
||||
DAV svn
|
||||
SVNParentPath "/var/svn"
|
||||
Order deny,allow
|
||||
Deny from all
|
||||
# only allow reading orders
|
||||
<Limit GET PROPFIND OPTIONS REPORT>
|
||||
Allow from redmine.server.ip
|
||||
</Limit>
|
||||
</Location>
|
||||
|
||||
and you will have to use this reposman.rb command line to create repository :
|
||||
|
||||
reposman.rb --redmine my.redmine.server --svn-dir /var/svn --owner www-data -u http://svn.server/svn-private/
|
||||
|
||||
=head1 REPOSITORIES NAMING
|
||||
|
||||
A project repository must be named with the project identifier. In case
|
||||
of multiple repositories for the same project, use the project identifier
|
||||
and the repository identifier separated with a dot:
|
||||
|
||||
/var/svn/foo
|
||||
/var/svn/foo.otherrepo
|
||||
|
||||
=head1 MIGRATION FROM OLDER RELEASES
|
||||
|
||||
If you use an older reposman.rb (r860 or before), you need to change
|
||||
rights on repositories to allow the apache user to read and write
|
||||
S<them :>
|
||||
|
||||
sudo chown -R www-data /var/svn/*
|
||||
sudo chmod -R u+w /var/svn/*
|
||||
|
||||
And you need to upgrade at least reposman.rb (after r860).
|
||||
|
||||
=head1 GIT SMART HTTP SUPPORT
|
||||
|
||||
Git's smart HTTP protocol (available since Git 1.7.0) will not work with the
|
||||
above settings. Redmine.pm normally does access control depending on the HTTP
|
||||
method used: read-only methods are OK for everyone in public projects and
|
||||
members with read rights in private projects. The rest require membership with
|
||||
commit rights in the project.
|
||||
|
||||
However, this scheme doesn't work for Git's smart HTTP protocol, as it will use
|
||||
POST even for a simple clone. Instead, read-only requests must be detected using
|
||||
the full URL (including the query string): anything that doesn't belong to the
|
||||
git-receive-pack service is read-only.
|
||||
|
||||
To activate this mode of operation, add this line inside your <Location /git>
|
||||
block:
|
||||
|
||||
RedmineGitSmartHttp yes
|
||||
|
||||
Here's a sample Apache configuration which integrates git-http-backend with
|
||||
a MySQL database and this new option:
|
||||
|
||||
SetEnv GIT_PROJECT_ROOT /var/www/git/
|
||||
SetEnv GIT_HTTP_EXPORT_ALL
|
||||
ScriptAlias /git/ /usr/libexec/git-core/git-http-backend/
|
||||
<Location /git>
|
||||
Order allow,deny
|
||||
Allow from all
|
||||
|
||||
AuthType Basic
|
||||
AuthName Git
|
||||
Require valid-user
|
||||
|
||||
PerlAccessHandler Apache::Authn::Redmine::access_handler
|
||||
PerlAuthenHandler Apache::Authn::Redmine::authen_handler
|
||||
# for mysql
|
||||
RedmineDSN "DBI:mysql:database=redmine;host=127.0.0.1"
|
||||
RedmineDbUser "redmine"
|
||||
RedmineDbPass "xxx"
|
||||
RedmineGitSmartHttp yes
|
||||
</Location>
|
||||
|
||||
Make sure that all the names of the repositories under /var/www/git/ have a
|
||||
matching identifier for some project: /var/www/git/myproject and
|
||||
/var/www/git/myproject.git will work. You can put both bare and non-bare
|
||||
repositories in /var/www/git, though bare repositories are strongly
|
||||
recommended. You should create them with the rights of the user running Redmine,
|
||||
like this:
|
||||
|
||||
cd /var/www/git
|
||||
sudo -u user-running-redmine mkdir myproject
|
||||
cd myproject
|
||||
sudo -u user-running-redmine git init --bare
|
||||
|
||||
Once you have activated this option, you have three options when cloning a
|
||||
repository:
|
||||
|
||||
- Cloning using "http://user@host/git/repo(.git)" works, but will ask for the password
|
||||
all the time.
|
||||
|
||||
- Cloning with "http://user:pass@host/git/repo(.git)" does not have this problem, but
|
||||
this could reveal accidentally your password to the console in some versions
|
||||
of Git, and you would have to ensure that .git/config is not readable except
|
||||
by the owner for each of your projects.
|
||||
|
||||
- Use "http://host/git/repo(.git)", and store your credentials in the ~/.netrc
|
||||
file. This is the recommended solution, as you only have one file to protect
|
||||
and passwords will not be leaked accidentally to the console.
|
||||
|
||||
IMPORTANT NOTE: It is *very important* that the file cannot be read by other
|
||||
users, as it will contain your password in cleartext. To create the file, you
|
||||
can use the following commands, replacing yourhost, youruser and yourpassword
|
||||
with the right values:
|
||||
|
||||
touch ~/.netrc
|
||||
chmod 600 ~/.netrc
|
||||
echo -e "machine yourhost\nlogin youruser\npassword yourpassword" > ~/.netrc
|
||||
|
||||
=cut
|
||||
|
||||
use strict;
|
||||
use warnings FATAL => 'all', NONFATAL => 'redefine';
|
||||
|
||||
use DBI;
|
||||
use Digest::SHA;
|
||||
# optional module for LDAP authentication
|
||||
my $CanUseLDAPAuth = eval("use Authen::Simple::LDAP; 1");
|
||||
|
||||
use Apache2::Module;
|
||||
use Apache2::Access;
|
||||
use Apache2::ServerRec qw();
|
||||
use Apache2::RequestRec qw();
|
||||
use Apache2::RequestUtil qw();
|
||||
use Apache2::Const qw(:common :override :cmd_how);
|
||||
use APR::Pool ();
|
||||
use APR::Table ();
|
||||
|
||||
# use Apache2::Directive qw();
|
||||
|
||||
my @directives = (
|
||||
{
|
||||
name => 'RedmineDSN',
|
||||
req_override => OR_AUTHCFG,
|
||||
args_how => TAKE1,
|
||||
errmsg => 'Dsn in format used by Perl DBI. eg: "DBI:Pg:dbname=databasename;host=my.db.server"',
|
||||
},
|
||||
{
|
||||
name => 'RedmineDbUser',
|
||||
req_override => OR_AUTHCFG,
|
||||
args_how => TAKE1,
|
||||
},
|
||||
{
|
||||
name => 'RedmineDbPass',
|
||||
req_override => OR_AUTHCFG,
|
||||
args_how => TAKE1,
|
||||
},
|
||||
{
|
||||
name => 'RedmineDbWhereClause',
|
||||
req_override => OR_AUTHCFG,
|
||||
args_how => TAKE1,
|
||||
},
|
||||
{
|
||||
name => 'RedmineCacheCredsMax',
|
||||
req_override => OR_AUTHCFG,
|
||||
args_how => TAKE1,
|
||||
errmsg => 'RedmineCacheCredsMax must be decimal number',
|
||||
},
|
||||
{
|
||||
name => 'RedmineGitSmartHttp',
|
||||
req_override => OR_AUTHCFG,
|
||||
args_how => TAKE1,
|
||||
},
|
||||
);
|
||||
|
||||
sub RedmineDSN {
|
||||
my ($self, $parms, $arg) = @_;
|
||||
$self->{RedmineDSN} = $arg;
|
||||
my $query = "SELECT
|
||||
users.hashed_password, users.salt, users.auth_source_id, roles.permissions, projects.status
|
||||
FROM projects, users, roles
|
||||
WHERE
|
||||
users.login=?
|
||||
AND projects.identifier=?
|
||||
AND EXISTS (SELECT 1 FROM enabled_modules em WHERE em.project_id = projects.id AND em.name = 'repository')
|
||||
AND users.type='User'
|
||||
AND users.status=1
|
||||
AND (
|
||||
roles.id IN (SELECT member_roles.role_id FROM members, member_roles WHERE members.user_id = users.id AND members.project_id = projects.id AND members.id = member_roles.member_id)
|
||||
OR
|
||||
(cast(projects.is_public as CHAR) IN ('t', '1')
|
||||
AND (roles.builtin=1
|
||||
OR roles.id IN (SELECT member_roles.role_id FROM members, member_roles, users g
|
||||
WHERE members.user_id = g.id AND members.project_id = projects.id AND members.id = member_roles.member_id
|
||||
AND g.type = 'GroupNonMember'))
|
||||
)
|
||||
)
|
||||
AND roles.permissions IS NOT NULL";
|
||||
$self->{RedmineQuery} = trim($query);
|
||||
}
|
||||
|
||||
sub RedmineDbUser { set_val('RedmineDbUser', @_); }
|
||||
sub RedmineDbPass { set_val('RedmineDbPass', @_); }
|
||||
sub RedmineDbWhereClause {
|
||||
my ($self, $parms, $arg) = @_;
|
||||
$self->{RedmineQuery} = trim($self->{RedmineQuery}.($arg ? $arg : "")." ");
|
||||
}
|
||||
|
||||
sub RedmineCacheCredsMax {
|
||||
my ($self, $parms, $arg) = @_;
|
||||
if ($arg) {
|
||||
$self->{RedmineCachePool} = APR::Pool->new;
|
||||
$self->{RedmineCacheCreds} = APR::Table::make($self->{RedmineCachePool}, $arg);
|
||||
$self->{RedmineCacheCredsCount} = 0;
|
||||
$self->{RedmineCacheCredsMax} = $arg;
|
||||
}
|
||||
}
|
||||
|
||||
sub RedmineGitSmartHttp {
|
||||
my ($self, $parms, $arg) = @_;
|
||||
$arg = lc $arg;
|
||||
|
||||
if ($arg eq "yes" || $arg eq "true") {
|
||||
$self->{RedmineGitSmartHttp} = 1;
|
||||
} else {
|
||||
$self->{RedmineGitSmartHttp} = 0;
|
||||
}
|
||||
}
|
||||
|
||||
sub trim {
|
||||
my $string = shift;
|
||||
$string =~ s/\s{2,}/ /g;
|
||||
return $string;
|
||||
}
|
||||
|
||||
sub set_val {
|
||||
my ($key, $self, $parms, $arg) = @_;
|
||||
$self->{$key} = $arg;
|
||||
}
|
||||
|
||||
Apache2::Module::add(__PACKAGE__, \@directives);
|
||||
|
||||
|
||||
my %read_only_methods = map { $_ => 1 } qw/GET HEAD PROPFIND REPORT OPTIONS/;
|
||||
|
||||
sub request_is_read_only {
|
||||
my ($r) = @_;
|
||||
my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config);
|
||||
|
||||
# Do we use Git's smart HTTP protocol, or not?
|
||||
if (defined $cfg->{RedmineGitSmartHttp} and $cfg->{RedmineGitSmartHttp}) {
|
||||
my $uri = $r->unparsed_uri;
|
||||
my $location = $r->location;
|
||||
my $is_read_only = $uri !~ m{^$location/*[^/]+/+(info/refs\?service=)?git\-receive\-pack$}o;
|
||||
return $is_read_only;
|
||||
} else {
|
||||
# Standard behaviour: check the HTTP method
|
||||
my $method = $r->method;
|
||||
return defined $read_only_methods{$method};
|
||||
}
|
||||
}
|
||||
|
||||
sub access_handler {
|
||||
my $r = shift;
|
||||
|
||||
unless ($r->some_auth_required) {
|
||||
$r->log_reason("No authentication has been configured");
|
||||
return FORBIDDEN;
|
||||
}
|
||||
|
||||
return OK unless request_is_read_only($r);
|
||||
|
||||
my $project_id = get_project_identifier($r);
|
||||
|
||||
if (is_public_project($project_id, $r) && anonymous_allowed_to_browse_repository($project_id, $r)) {
|
||||
$r->user("");
|
||||
$r->set_handlers(PerlAuthenHandler => [\&OK]);
|
||||
}
|
||||
|
||||
return OK
|
||||
}
|
||||
|
||||
sub authen_handler {
|
||||
my $r = shift;
|
||||
|
||||
my ($res, $redmine_pass) = $r->get_basic_auth_pw();
|
||||
return $res unless $res == OK;
|
||||
|
||||
if (is_member($r->user, $redmine_pass, $r)) {
|
||||
return OK;
|
||||
} else {
|
||||
$r->note_auth_failure();
|
||||
return DECLINED;
|
||||
}
|
||||
}
|
||||
|
||||
# check if authentication is forced
|
||||
sub is_authentication_forced {
|
||||
my $r = shift;
|
||||
|
||||
my $dbh = connect_database($r);
|
||||
my $sth = $dbh->prepare(
|
||||
"SELECT value FROM settings where settings.name = 'login_required';"
|
||||
);
|
||||
|
||||
$sth->execute();
|
||||
my $ret = 0;
|
||||
if (my @row = $sth->fetchrow_array) {
|
||||
if ($row[0] eq "1" || $row[0] eq "t") {
|
||||
$ret = 1;
|
||||
}
|
||||
}
|
||||
$sth->finish();
|
||||
undef $sth;
|
||||
|
||||
$dbh->disconnect();
|
||||
undef $dbh;
|
||||
|
||||
$ret;
|
||||
}
|
||||
|
||||
sub is_public_project {
|
||||
my $project_id = shift;
|
||||
my $r = shift;
|
||||
|
||||
if (is_authentication_forced($r)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
my $dbh = connect_database($r);
|
||||
my $sth = $dbh->prepare(
|
||||
"SELECT is_public FROM projects
|
||||
WHERE projects.identifier = ? AND projects.status <> 9
|
||||
AND EXISTS (SELECT 1 FROM enabled_modules em WHERE em.project_id = projects.id AND em.name = 'repository');"
|
||||
);
|
||||
|
||||
$sth->execute($project_id);
|
||||
my $ret = 0;
|
||||
if (my @row = $sth->fetchrow_array) {
|
||||
if ($row[0] eq "1" || $row[0] eq "t") {
|
||||
$ret = 1;
|
||||
}
|
||||
}
|
||||
$sth->finish();
|
||||
undef $sth;
|
||||
$dbh->disconnect();
|
||||
undef $dbh;
|
||||
|
||||
$ret;
|
||||
}
|
||||
|
||||
sub anonymous_allowed_to_browse_repository {
|
||||
my $project_id = shift;
|
||||
my $r = shift;
|
||||
|
||||
my $dbh = connect_database($r);
|
||||
my $sth = $dbh->prepare(
|
||||
"SELECT permissions FROM roles WHERE permissions like '%browse_repository%'
|
||||
AND (roles.builtin = 2
|
||||
OR roles.id IN (SELECT member_roles.role_id FROM projects, members, member_roles, users
|
||||
WHERE members.user_id = users.id AND members.project_id = projects.id AND members.id = member_roles.member_id
|
||||
AND projects.identifier = ? AND users.type = 'GroupAnonymous'));"
|
||||
);
|
||||
|
||||
$sth->execute($project_id);
|
||||
my $ret = 0;
|
||||
if (my @row = $sth->fetchrow_array) {
|
||||
if ($row[0] =~ /:browse_repository/) {
|
||||
$ret = 1;
|
||||
}
|
||||
}
|
||||
$sth->finish();
|
||||
undef $sth;
|
||||
$dbh->disconnect();
|
||||
undef $dbh;
|
||||
|
||||
$ret;
|
||||
}
|
||||
|
||||
# perhaps we should use repository right (other read right) to check public access.
|
||||
# it could be faster BUT it doesn't work for the moment.
|
||||
# sub is_public_project_by_file {
|
||||
# my $project_id = shift;
|
||||
# my $r = shift;
|
||||
|
||||
# my $tree = Apache2::Directive::conftree();
|
||||
# my $node = $tree->lookup('Location', $r->location);
|
||||
# my $hash = $node->as_hash;
|
||||
|
||||
# my $svnparentpath = $hash->{SVNParentPath};
|
||||
# my $repos_path = $svnparentpath . "/" . $project_id;
|
||||
# return 1 if (stat($repos_path))[2] & 00007;
|
||||
# }
|
||||
|
||||
sub is_member {
|
||||
my $redmine_user = shift;
|
||||
my $redmine_pass = shift;
|
||||
my $r = shift;
|
||||
|
||||
my $project_id = get_project_identifier($r);
|
||||
|
||||
my $pass_digest = Digest::SHA::sha1_hex($redmine_pass);
|
||||
|
||||
my $access_mode = request_is_read_only($r) ? "R" : "W";
|
||||
|
||||
my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config);
|
||||
my $usrprojpass;
|
||||
if ($cfg->{RedmineCacheCredsMax}) {
|
||||
$usrprojpass = $cfg->{RedmineCacheCreds}->get($redmine_user.":".$project_id.":".$access_mode);
|
||||
return 1 if (defined $usrprojpass and ($usrprojpass eq $pass_digest));
|
||||
}
|
||||
my $dbh = connect_database($r);
|
||||
my $query = $cfg->{RedmineQuery};
|
||||
my $sth = $dbh->prepare($query);
|
||||
$sth->execute($redmine_user, $project_id);
|
||||
|
||||
my $ret;
|
||||
while (my ($hashed_password, $salt, $auth_source_id, $permissions, $project_status) = $sth->fetchrow_array) {
|
||||
if ($project_status eq "9" || ($project_status ne "1" && $access_mode eq "W")) {
|
||||
last;
|
||||
}
|
||||
|
||||
unless ($auth_source_id) {
|
||||
my $method = $r->method;
|
||||
my $salted_password = Digest::SHA::sha1_hex($salt.$pass_digest);
|
||||
if ($hashed_password eq $salted_password && (($access_mode eq "R" && $permissions =~ /:browse_repository/) || $permissions =~ /:commit_access/) ) {
|
||||
$ret = 1;
|
||||
last;
|
||||
}
|
||||
} elsif ($CanUseLDAPAuth) {
|
||||
my $sthldap = $dbh->prepare(
|
||||
"SELECT host,port,tls,account,account_password,base_dn,attr_login from auth_sources WHERE id = ?;"
|
||||
);
|
||||
$sthldap->execute($auth_source_id);
|
||||
while (my @rowldap = $sthldap->fetchrow_array) {
|
||||
my $bind_as = $rowldap[3] ? $rowldap[3] : "";
|
||||
my $bind_pw = $rowldap[4] ? $rowldap[4] : "";
|
||||
if ($bind_as =~ m/\$login/) {
|
||||
# replace $login with $redmine_user and use $redmine_pass
|
||||
$bind_as =~ s/\$login/$redmine_user/g;
|
||||
$bind_pw = $redmine_pass
|
||||
}
|
||||
my $ldap = Authen::Simple::LDAP->new(
|
||||
host => ($rowldap[2] eq "1" || $rowldap[2] eq "t") ? "ldaps://$rowldap[0]:$rowldap[1]" : $rowldap[0],
|
||||
port => $rowldap[1],
|
||||
basedn => $rowldap[5],
|
||||
binddn => $bind_as,
|
||||
bindpw => $bind_pw,
|
||||
filter => "(".$rowldap[6]."=%s)"
|
||||
);
|
||||
my $method = $r->method;
|
||||
$ret = 1 if ($ldap->authenticate($redmine_user, $redmine_pass) && (($access_mode eq "R" && $permissions =~ /:browse_repository/) || $permissions =~ /:commit_access/));
|
||||
|
||||
}
|
||||
$sthldap->finish();
|
||||
undef $sthldap;
|
||||
}
|
||||
}
|
||||
$sth->finish();
|
||||
undef $sth;
|
||||
$dbh->disconnect();
|
||||
undef $dbh;
|
||||
|
||||
if ($cfg->{RedmineCacheCredsMax} and $ret) {
|
||||
if (defined $usrprojpass) {
|
||||
$cfg->{RedmineCacheCreds}->set($redmine_user.":".$project_id.":".$access_mode, $pass_digest);
|
||||
} else {
|
||||
if ($cfg->{RedmineCacheCredsCount} < $cfg->{RedmineCacheCredsMax}) {
|
||||
$cfg->{RedmineCacheCreds}->set($redmine_user.":".$project_id.":".$access_mode, $pass_digest);
|
||||
$cfg->{RedmineCacheCredsCount}++;
|
||||
} else {
|
||||
$cfg->{RedmineCacheCreds}->clear();
|
||||
$cfg->{RedmineCacheCredsCount} = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$ret;
|
||||
}
|
||||
|
||||
sub get_project_identifier {
|
||||
my $r = shift;
|
||||
|
||||
my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config);
|
||||
my $location = $r->location;
|
||||
$location =~ s/\.git$// if (defined $cfg->{RedmineGitSmartHttp} and $cfg->{RedmineGitSmartHttp});
|
||||
my ($identifier) = $r->uri =~ m{$location/*([^/.]+)};
|
||||
$identifier;
|
||||
}
|
||||
|
||||
sub connect_database {
|
||||
my $r = shift;
|
||||
|
||||
my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config);
|
||||
return DBI->connect($cfg->{RedmineDSN}, $cfg->{RedmineDbUser}, $cfg->{RedmineDbPass});
|
||||
}
|
||||
|
||||
1;
|
288
extra/svn/reposman.rb
Executable file
288
extra/svn/reposman.rb
Executable file
|
@ -0,0 +1,288 @@
|
|||
#!/usr/bin/env ruby
|
||||
|
||||
require 'optparse'
|
||||
require 'find'
|
||||
require 'etc'
|
||||
require 'rubygems'
|
||||
|
||||
Version = "1.5"
|
||||
SUPPORTED_SCM = %w( Subversion Darcs Mercurial Bazaar Git Filesystem )
|
||||
|
||||
$verbose = 0
|
||||
$quiet = false
|
||||
$redmine_host = ''
|
||||
$repos_base = ''
|
||||
$svn_owner = 'root'
|
||||
$svn_group = 'root'
|
||||
$use_groupid = true
|
||||
$svn_url = false
|
||||
$test = false
|
||||
$force = false
|
||||
$scm = 'Subversion'
|
||||
|
||||
def log(text, options={})
|
||||
level = options[:level] || 0
|
||||
puts text unless $quiet or level > $verbose
|
||||
exit 1 if options[:exit]
|
||||
end
|
||||
|
||||
def system_or_raise(command)
|
||||
raise "\"#{command}\" failed" unless system command
|
||||
end
|
||||
|
||||
module SCM
|
||||
|
||||
module Subversion
|
||||
def self.create(path)
|
||||
system_or_raise "svnadmin create #{path}"
|
||||
end
|
||||
end
|
||||
|
||||
module Git
|
||||
def self.create(path)
|
||||
Dir.mkdir path
|
||||
Dir.chdir(path) do
|
||||
system_or_raise "git --bare init --shared"
|
||||
system_or_raise "git update-server-info"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
def read_key_from_file(filename)
|
||||
begin
|
||||
$api_key = File.read(filename).strip
|
||||
rescue Exception => e
|
||||
$stderr.puts "Unable to read the key from #{filename}: #{e.message}"
|
||||
exit 1
|
||||
end
|
||||
end
|
||||
|
||||
def set_scm(scm)
|
||||
$scm = scm.capitalize
|
||||
unless SUPPORTED_SCM.include?($scm)
|
||||
log("Invalid SCM: #{$scm}\nValid SCM are: #{SUPPORTED_SCM.join(', ')}", :exit => true)
|
||||
end
|
||||
end
|
||||
|
||||
optparse = OptionParser.new do |opts|
|
||||
opts.banner = "Usage: reposman.rb [OPTIONS...] -s [DIR] -r [HOST] -k [KEY]"
|
||||
opts.separator("")
|
||||
opts.separator("Manages your repositories with Redmine.")
|
||||
opts.separator("")
|
||||
opts.separator("Required arguments:")
|
||||
opts.on("-s", "--svn-dir DIR", "use DIR as base directory for svn repositories") {|v| $repos_base = v}
|
||||
opts.on("-r", "--redmine-host HOST","assume Redmine is hosted on HOST. Examples:",
|
||||
" -r redmine.example.net",
|
||||
" -r http://redmine.example.net",
|
||||
" -r https://redmine.example.net") {|v| $redmine_host = v}
|
||||
opts.on("-k", "--key KEY", "use KEY as the Redmine API key",
|
||||
"(you can use --key-file option as an alternative)") {|v| $api_key = v}
|
||||
opts.separator("")
|
||||
opts.separator("Options:")
|
||||
opts.on("-o", "--owner OWNER", "owner of the repository. using the rails login",
|
||||
"allows users to browse the repository within",
|
||||
"Redmine even for private projects. If you want to",
|
||||
"share repositories through Redmine.pm, you need",
|
||||
"to use the apache owner.") {|v| $svn_owner = v; $use_groupid = false}
|
||||
opts.on("-g", "--group GROUP", "group of the repository (default: root)") {|v| $svn_group = v; $use_groupid = false}
|
||||
opts.on("-u", "--url URL", "the base url Redmine will use to access your",
|
||||
"repositories. This option is used to register",
|
||||
"the repositories in Redmine automatically. The",
|
||||
"project identifier will be appended to this url.",
|
||||
"Examples:",
|
||||
" -u https://example.net/svn",
|
||||
" -u file:///var/svn/",
|
||||
"if this option isn't set, reposman won't register",
|
||||
"the repositories in Redmine") {|v| $svn_url = v}
|
||||
opts.on( "--scm SCM", "the kind of SCM repository you want to create",
|
||||
"(and register) in Redmine (default: Subversion).",
|
||||
"reposman is able to create Git and Subversion",
|
||||
"repositories.",
|
||||
"For all other kind, you must specify a --command",
|
||||
"option") {|v| set_scm(v)}
|
||||
opts.on("-c", "--command COMMAND", "use this command instead of `svnadmin create` to",
|
||||
"create a repository. This option can be used to",
|
||||
"create repositories other than subversion and git",
|
||||
"kind.",
|
||||
"This command override the default creation for",
|
||||
"git and subversion.") {|v| $command = v}
|
||||
opts.on( "--key-file FILE", "path to a file that contains the Redmine API key",
|
||||
"(use this option instead of --key if you don't",
|
||||
"want the key to appear in the command line)") {|v| read_key_from_file(v)}
|
||||
opts.on("-t", "--test", "only show what should be done") {$test = true}
|
||||
opts.on("-f", "--force", "force repository creation even if the project", "repository is already declared in Redmine") {$force = true}
|
||||
opts.on("-v", "--verbose", "verbose") {$verbose += 1}
|
||||
opts.on("-V", "--version", "show version and exit") {puts Version; exit}
|
||||
opts.on("-h", "--help", "show help and exit") {puts opts; exit 1}
|
||||
opts.on("-q", "--quiet", "no log") {$quiet = true}
|
||||
opts.separator("")
|
||||
opts.separator("Examples:")
|
||||
opts.separator(" reposman.rb --svn-dir=/var/svn --redmine-host=redmine.host")
|
||||
opts.separator(" reposman.rb -s /var/git -r redmine.host -u http://git.host --scm git")
|
||||
opts.separator("")
|
||||
opts.separator("You can find more information on the redmine's wiki:\nhttp://www.redmine.org/projects/redmine/wiki/HowTos")
|
||||
|
||||
opts.summary_width = 25
|
||||
end
|
||||
optparse.parse!
|
||||
|
||||
if $test
|
||||
log("running in test mode")
|
||||
end
|
||||
|
||||
# Make sure command is overridden if SCM vendor is not handled internally (for the moment Subversion and Git)
|
||||
if $command.nil?
|
||||
begin
|
||||
scm_module = SCM.const_get($scm)
|
||||
rescue
|
||||
log("Please use --command option to specify how to create a #{$scm} repository.", :exit => true)
|
||||
end
|
||||
end
|
||||
|
||||
$svn_url += "/" if $svn_url and not $svn_url.match(/\/$/)
|
||||
|
||||
if ($redmine_host.empty? or $repos_base.empty?)
|
||||
puts "Some arguments are missing. Use reposman.rb --help for getting help."
|
||||
exit 1
|
||||
end
|
||||
|
||||
unless File.directory?($repos_base)
|
||||
log("directory '#{$repos_base}' doesn't exists", :exit => true)
|
||||
end
|
||||
|
||||
begin
|
||||
require 'active_resource'
|
||||
rescue LoadError
|
||||
log("This script requires activeresource.\nRun 'gem install activeresource' to install it.", :exit => true)
|
||||
end
|
||||
|
||||
class Project < ActiveResource::Base
|
||||
self.headers["User-agent"] = "Redmine repository manager/#{Version}"
|
||||
self.format = :json
|
||||
end
|
||||
|
||||
log("querying Redmine for active projects with repository module enabled...", :level => 1);
|
||||
|
||||
$redmine_host.gsub!(/^/, "http://") unless $redmine_host.match("^https?://")
|
||||
$redmine_host.gsub!(/\/$/, '')
|
||||
|
||||
Project.site = "#{$redmine_host}/sys";
|
||||
|
||||
begin
|
||||
# Get all active projects that have the Repository module enabled
|
||||
projects = Project.find(:all, :params => {:key => $api_key})
|
||||
rescue ActiveResource::ForbiddenAccess
|
||||
log("Request was denied by your Redmine server. Make sure that 'WS for repository management' is enabled in application settings and that you provided the correct API key.")
|
||||
rescue => e
|
||||
log("Unable to connect to #{Project.site}: #{e}", :exit => true)
|
||||
end
|
||||
|
||||
if projects.nil?
|
||||
log('No project found, perhaps you forgot to "Enable WS for repository management"', :exit => true)
|
||||
end
|
||||
|
||||
log("retrieved #{projects.size} projects", :level => 1)
|
||||
|
||||
def set_owner_and_rights(project, repos_path, &block)
|
||||
if mswin?
|
||||
yield if block_given?
|
||||
else
|
||||
uid, gid = Etc.getpwnam($svn_owner).uid, ($use_groupid ? Etc.getgrnam(project.identifier).gid : Etc.getgrnam($svn_group).gid)
|
||||
right = project.is_public ? 0775 : 0770
|
||||
yield if block_given?
|
||||
Find.find(repos_path) do |f|
|
||||
File.chmod right, f
|
||||
File.chown uid, gid, f
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def other_read_right?(file)
|
||||
(File.stat(file).mode & 0007).zero? ? false : true
|
||||
end
|
||||
|
||||
def owner_name(file)
|
||||
mswin? ?
|
||||
$svn_owner :
|
||||
Etc.getpwuid( File.stat(file).uid ).name
|
||||
end
|
||||
|
||||
def mswin?
|
||||
(RUBY_PLATFORM =~ /(:?mswin|mingw)/) || (RUBY_PLATFORM == 'java' && (ENV['OS'] || ENV['os']) =~ /windows/i)
|
||||
end
|
||||
|
||||
projects.each do |project|
|
||||
if project.identifier.empty?
|
||||
log("\tno identifier for project #{project.name}")
|
||||
next
|
||||
elsif not project.identifier.match(/^[a-z0-9\-_]+$/)
|
||||
log("\tinvalid identifier for project #{project.name} : #{project.identifier}");
|
||||
next;
|
||||
end
|
||||
log("processing project #{project.identifier} (#{project.name})", :level => 1)
|
||||
|
||||
repos_path = File.join($repos_base, project.identifier).gsub(File::SEPARATOR, File::ALT_SEPARATOR || File::SEPARATOR)
|
||||
|
||||
if File.directory?(repos_path)
|
||||
# we must verify that repository has the good owner and the good
|
||||
# rights before leaving
|
||||
other_read = other_read_right?(repos_path)
|
||||
owner = owner_name(repos_path)
|
||||
next if project.is_public == other_read and owner == $svn_owner
|
||||
|
||||
if $test
|
||||
log("\tchange mode on #{repos_path}")
|
||||
next
|
||||
end
|
||||
|
||||
begin
|
||||
set_owner_and_rights(project, repos_path)
|
||||
rescue Errno::EPERM => e
|
||||
log("\tunable to change mode on #{repos_path} : #{e}\n")
|
||||
next
|
||||
end
|
||||
|
||||
log("\tmode change on #{repos_path}");
|
||||
|
||||
else
|
||||
# if repository is already declared in redmine, we don't create
|
||||
# unless user use -f with reposman
|
||||
if $force == false and project.respond_to?(:repository)
|
||||
log("\trepository for project #{project.identifier} already exists in Redmine", :level => 1)
|
||||
next
|
||||
end
|
||||
|
||||
project.is_public ? File.umask(0002) : File.umask(0007)
|
||||
|
||||
if $test
|
||||
log("\trepository #{repos_path} created")
|
||||
log("\trepository #{repos_path} registered in Redmine with url #{$svn_url}#{project.identifier}") if $svn_url;
|
||||
next
|
||||
end
|
||||
|
||||
begin
|
||||
set_owner_and_rights(project, repos_path) do
|
||||
if scm_module.nil?
|
||||
system_or_raise "#{$command} #{repos_path}"
|
||||
else
|
||||
scm_module.create(repos_path)
|
||||
end
|
||||
end
|
||||
rescue => e
|
||||
log("\tunable to create #{repos_path} : #{e}\n")
|
||||
next
|
||||
end
|
||||
log("\trepository #{repos_path} created");
|
||||
|
||||
if $svn_url
|
||||
begin
|
||||
project.post(:repository, :vendor => $scm, :repository => {:url => "#{$svn_url}#{project.identifier}"}, :key => $api_key)
|
||||
log("\trepository #{repos_path} registered in Redmine with url #{$svn_url}#{project.identifier}");
|
||||
rescue => e
|
||||
log("\trepository #{repos_path} not registered in Redmine: #{e.message}");
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Add table
Add a link
Reference in a new issue