├── app ├── helpers │ └── git_hook_helper.rb └── controllers │ └── gitolite_hook_controller.rb ├── lang └── en.yml ├── test ├── test_helper.rb └── functional │ └── github_hook_controller_test.rb ├── init.rb ├── LICENSE ├── contrib └── hooks │ └── post-receive-redmine_gitolite └── README.rdoc /app/helpers/git_hook_helper.rb: -------------------------------------------------------------------------------- 1 | module GitHookHelper 2 | end 3 | -------------------------------------------------------------------------------- /lang/en.yml: -------------------------------------------------------------------------------- 1 | # English strings go here 2 | my_label: "My label" 3 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # Load the normal Rails helper 2 | require File.expand_path(File.dirname(__FILE__) + '/../../../../test/test_helper') 3 | 4 | # Ensure that we are using the temporary fixture path 5 | Engines::Testing.set_fixture_path 6 | -------------------------------------------------------------------------------- /init.rb: -------------------------------------------------------------------------------- 1 | require 'redmine' 2 | 3 | Redmine::Plugin.register :redmine_gitolite_hook do 4 | name 'Redmine Gitolite Hook plugin' 5 | author 'Kah Seng Tay, Jakob Skjerning' 6 | description 'This plugin allows your Redmine installation to receive Gitolite post-receive notifications' 7 | version '0.1.1' 8 | end 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 Kah Seng Tay - Gitolite modifications 2 | Copyright (c) 2010 Jakob Skjerning - Original code 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /contrib/hooks/post-receive-redmine_gitolite: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | HOOK_URL=gitolite_hook 4 | FETCH_URL=sys/fetch_changesets 5 | LOG=/home/git/logs/post-receive.log 6 | 7 | KEY=$(git config hooks.redmine_gitolite.key 2>/dev/null) 8 | if [ -z "$KEY" ]; then 9 | KEY=your_redmine_api_key 10 | fi 11 | 12 | REDMINE_SERVER=$(git config hooks.redmine_gitolite.server 2>/dev/null) 13 | if [ -z "$REDMINE_SERVER" ]; then 14 | REDMINE_SERVER=http://your.redmine.server.com 15 | fi 16 | 17 | REDMINE_PROJECT_ID=$(git config hooks.redmine_gitolite.projectid 2>/dev/null) 18 | if [ -z "$REDMINE_PROJECT_ID" ]; then 19 | REDMINE_PROJECT_ID=$GL_REPO 20 | fi 21 | 22 | CURL_IGNORE_SECURITY_CFG=$(git config --bool hooks.redmine_gitolite.curlignoresecurity 2>/dev/null) 23 | if [ -z "$CURL_IGNORE_SECURITY_CFG" ]; then 24 | CURL_IGNORE_SECURITY_CFG="false" 25 | fi 26 | 27 | case "$CURL_IGNORE_SECURITY_CFG" in 28 | true) 29 | CURL_IGNORE_SECURITY=" -k " 30 | ;; 31 | false) 32 | CURL_IGNORE_SECURITY=" " 33 | ;; 34 | esac 35 | 36 | echo 37 | echo "Notifying Redmine (${REDMINE_SERVER}) about changes to this repo (${GL_REPO} => ${REDMINE_PROJECT_ID})" 38 | echo 39 | 40 | # Read from stdin 41 | while read old new refname; do 42 | echo "Hitting the Redmine Gitolite hook for $old $new $refname" 43 | echo -n "Response: " 44 | curl $CURL_IGNORE_SECURITY -S -s -d "oldrev=$old&newrev=$new&refname=$refname&user=$GL_USER" "$REDMINE_SERVER/$HOOK_URL?project_id=$REDMINE_PROJECT_ID" 45 | echo "" 46 | echo "" 47 | echo "Hitting the Redmine fetch changesets URL" 48 | curl -S -s $CURL_IGNORE_SECURITY "$REDMINE_SERVER/$FETCH_URL?id=$REDMINE_PROJECT_ID&key=$KEY" 49 | echo "" 50 | done 51 | -------------------------------------------------------------------------------- /app/controllers/gitolite_hook_controller.rb: -------------------------------------------------------------------------------- 1 | require 'open3' 2 | 3 | class GitoliteHookController < ApplicationController 4 | 5 | skip_before_filter :verify_authenticity_token, :check_if_login_required 6 | 7 | def index 8 | repository = find_repository 9 | 10 | # Fetch the changes from Gitolite 11 | update_repository(repository) 12 | 13 | # Fetch the new changesets into Redmine 14 | repository.fetch_changesets 15 | 16 | render(:text => 'OK') 17 | end 18 | 19 | private 20 | 21 | def exec(command) 22 | logger.debug { "GitoliteHook: Executing command: '#{command}'" } 23 | stdin, stdout, stderr = Open3.popen3(command) 24 | 25 | output = stdout.readlines.collect(&:strip) 26 | errors = stderr.readlines.collect(&:strip) 27 | 28 | logger.debug { "GitoliteHook: Output from git:" } 29 | logger.debug { "GitoliteHook: * STDOUT: #{output}"} 30 | logger.debug { "GitoliteHook: * STDERR: #{output}"} 31 | end 32 | 33 | # Fetches updates from the remote repository 34 | def update_repository(repository) 35 | command = "cd '#{repository.url}' && git fetch origin && git reset --soft refs/remotes/origin/master" 36 | exec(command) 37 | end 38 | 39 | # Gets the project identifier from the querystring parameters. 40 | def get_identifier 41 | identifier = params[:project_id] 42 | # TODO: Can obtain 'oldrev', 'newrev', 'refname', 'user' in POST params for further action if needed. 43 | raise ActiveRecord::RecordNotFound, "Project identifier not specified" if identifier.nil? 44 | return identifier 45 | end 46 | 47 | # Finds the Redmine project in the database based on the given project identifier 48 | def find_project 49 | identifier = get_identifier 50 | project = Project.find_by_identifier(identifier.downcase) 51 | raise ActiveRecord::RecordNotFound, "No project found with identifier '#{identifier}'" if project.nil? 52 | return project 53 | end 54 | 55 | # Returns the Redmine Repository object we are trying to update 56 | def find_repository 57 | project = find_project 58 | repository = project.repository 59 | raise TypeError, "Project '#{project.to_s}' ('#{project.identifier}') has no repository" if repository.nil? 60 | raise TypeError, "Repository for project '#{project.to_s}' ('#{project.identifier}') is not a Git repository" unless repository.is_a?(Repository::Git) 61 | return repository 62 | end 63 | 64 | end 65 | -------------------------------------------------------------------------------- /test/functional/github_hook_controller_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../test_helper' 2 | 3 | require 'mocha' 4 | 5 | class GithubHookControllerTest < ActionController::TestCase 6 | 7 | def setup 8 | # Sample JSON post from http://github.com/guides/post-receive-hooks 9 | @json = '{ 10 | "before": "5aef35982fb2d34e9d9d4502f6ede1072793222d", 11 | "repository": { 12 | "url": "http://github.com/defunkt/github", 13 | "name": "github", 14 | "description": "You\'re lookin\' at it.", 15 | "watchers": 5, 16 | "forks": 2, 17 | "private": 1, 18 | "owner": { 19 | "email": "chris@ozmm.org", 20 | "name": "defunkt" 21 | } 22 | }, 23 | "commits": [ 24 | { 25 | "id": "41a212ee83ca127e3c8cf465891ab7216a705f59", 26 | "url": "http://github.com/defunkt/github/commit/41a212ee83ca127e3c8cf465891ab7216a705f59", 27 | "author": { 28 | "email": "chris@ozmm.org", 29 | "name": "Chris Wanstrath" 30 | }, 31 | "message": "okay i give in", 32 | "timestamp": "2008-02-15T14:57:17-08:00", 33 | "added": ["filepath.rb"] 34 | }, 35 | { 36 | "id": "de8251ff97ee194a289832576287d6f8ad74e3d0", 37 | "url": "http://github.com/defunkt/github/commit/de8251ff97ee194a289832576287d6f8ad74e3d0", 38 | "author": { 39 | "email": "chris@ozmm.org", 40 | "name": "Chris Wanstrath" 41 | }, 42 | "message": "update pricing a tad", 43 | "timestamp": "2008-02-15T14:36:34-08:00" 44 | } 45 | ], 46 | "after": "de8251ff97ee194a289832576287d6f8ad74e3d0", 47 | "ref": "refs/heads/master" 48 | }' 49 | @repository = Repository::Git.new 50 | @repository.stubs(:fetch_changesets).returns(true) 51 | 52 | @project = Project.new 53 | @project.stubs(:repository).returns(@repository) 54 | Project.stubs(:find_by_identifier).with('github').returns(@project) 55 | 56 | # Make sure we don't run actual commands in test 57 | Open3.stubs(:popen3) 58 | 59 | Repository.expects(:fetch_changesets).never 60 | end 61 | 62 | def mock_descriptor(kind, contents = []) 63 | descriptor = mock(kind) 64 | descriptor.expects(:readlines).returns(contents) 65 | descriptor 66 | end 67 | 68 | def do_post(payload = nil) 69 | payload = @json if payload.nil? 70 | payload = payload.to_json if payload.is_a?(Hash) 71 | post :index, :payload => payload 72 | end 73 | 74 | def test_should_use_the_repository_name_as_project_identifier 75 | Project.expects(:find_by_identifier).with('github').returns(@project) 76 | @controller.stubs(:exec).returns(true) 77 | do_post 78 | end 79 | 80 | def test_should_update_the_repository_using_git_on_the_commandline 81 | Project.expects(:find_by_identifier).with('github').returns(@project) 82 | @controller.expects(:exec).returns(true) 83 | do_post 84 | end 85 | 86 | def test_should_use_project_identifier_from_request 87 | Project.expects(:find_by_identifier).with('redmine').returns(@project) 88 | @controller.stubs(:exec).returns(true) 89 | post :index, :project_id => 'redmine', :payload => @json 90 | end 91 | 92 | def test_should_downcase_identifier 93 | # Redmine project identifiers are always downcase 94 | Project.expects(:find_by_identifier).with('redmine').returns(@project) 95 | @controller.stubs(:exec).returns(true) 96 | post :index, :project_id => 'ReDmInE', :payload => @json 97 | end 98 | 99 | def test_should_render_ok_when_done 100 | @controller.expects(:exec).returns(true) 101 | do_post 102 | assert_response :success 103 | assert_equal 'OK', @response.body 104 | end 105 | 106 | def test_should_fetch_changesets_into_the_repository 107 | @controller.expects(:exec).returns(true) 108 | @repository.expects(:fetch_changesets).returns(true) 109 | do_post 110 | assert_response :success 111 | assert_equal 'OK', @response.body 112 | end 113 | 114 | def test_should_return_404_if_project_identifier_not_given 115 | assert_raises ActiveRecord::RecordNotFound do 116 | do_post :repository => {} 117 | end 118 | end 119 | 120 | def test_should_return_404_if_project_not_found 121 | assert_raises ActiveRecord::RecordNotFound do 122 | Project.expects(:find_by_identifier).with('foobar').returns(nil) 123 | do_post :repository => {:name => 'foobar'} 124 | end 125 | end 126 | 127 | def test_should_return_500_if_project_has_no_repository 128 | assert_raises TypeError do 129 | project = mock('project', :to_s => 'My Project', :identifier => 'github') 130 | project.expects(:repository).returns(nil) 131 | Project.expects(:find_by_identifier).with('github').returns(project) 132 | do_post :repository => {:name => 'github'} 133 | end 134 | end 135 | 136 | def test_should_return_500_if_repository_is_not_git 137 | assert_raises TypeError do 138 | project = mock('project', :to_s => 'My Project', :identifier => 'github') 139 | repository = Repository::Subversion.new 140 | project.expects(:repository).at_least(1).returns(repository) 141 | Project.expects(:find_by_identifier).with('github').returns(project) 142 | do_post :repository => {:name => 'github'} 143 | end 144 | end 145 | 146 | def test_should_not_require_login 147 | @controller.expects(:exec).returns(true) 148 | @controller.expects(:check_if_login_required).never 149 | do_post 150 | end 151 | 152 | def test_exec_should_log_output_from_git_as_debug 153 | stdout = mock_descriptor('STDOUT', ["output 1\n", "output 2\n"]) 154 | stderr = mock_descriptor('STDERR', ["error 1\n", "error 2\n"]) 155 | Open3.expects(:popen3).returns(['STDIN', stdout, stderr]) 156 | 157 | @controller.logger.expects(:debug).at_least(4) 158 | do_post 159 | end 160 | 161 | end 162 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = Redmine Gitolite Hook 2 | 3 | This plugin allows you to update your local Git repositories in Redmine when changes have been pushed to your Gitolite server. 4 | 5 | == Description 6 | 7 | This project is a fork of {koppen/redmine_github_hook}[https://github.com/koppen/redmine_github_hook] 8 | by {Jakob Skjerning}[https://github.com/koppen] with modifications made to support 9 | {Gitolite}[https://github.com/sitaramc/gitolite], 10 | a centralised git server by {Sitaram Chamarty}[https://github.com/sitaramc]. 11 | 12 | {Redmine}[http://redmine.org] has supported Git repositories for a long time, allowing you to browse 13 | your code and view your changesets directly in Redmine. 14 | For this purpose, Redmine relies on local clones of the Git repositories. 15 | 16 | If your shared repository is on a remote machine - for example on a Gitolite server - this unfortunately 17 | means a bit of legwork to keep the local, Redmine-accessible repository up-to-date. The common approach 18 | is to set up a cronjob that pulls in any changes with regular intervals and updates Redmine with them. 19 | 20 | That approach works perfectly fine, but is a bit heavy-handed and cumbersome. The Redmine Gitolite 21 | Hook plugin allows Gitolite to notify your Redmine installation when changes have been pushed to a 22 | repository, triggering an update of your local repository and Redmine data only when it is actually 23 | necessary. 24 | 25 | 26 | == Installation 27 | 28 | 1. Installing the plugin 29 | 1. Follow the plugin installation procedure at http://www.redmine.org/wiki/redmine/Plugins. 30 | 1. For convenience, the latest steps for Redmine 1.0.3 are to go to $REDMINE_DIR/vendor/plugins 31 | 2. git clone git://github.com/kahseng/redmine_gitolite_hook.git 32 | 2. Restart your Redmine. 33 | 3. If you already have a local Git repository set up and working from Redmine go to step 3, otherwise continue at step 2. 34 | 35 | 2. Adding a Git repository to a project (note, this should work whether you want to use Redmine Gitolite Hook or not). 36 | Either follow the instructions at http://www.redmine.org/wiki/redmine/HowTo_keep_in_sync_your_git_repository_for_redmine or the ones below: 37 | 1. Go to the directory on your Redmine machine where you want to keep your repository $REDMINE_REPO_DIR, 38 | for example /home/redmine/repositories/. 39 | 2. Get a clone of the Gitolite repository into that location: 40 | 1. git clone git@your_gitolite_server:repository_name 41 | 2. This creates a .git directory at REDMINE_REPO_DIR/repository_name/.git 42 | 3. Open Redmine in your browser and navigate to the Settings for the project you want to add a Git repository to. 43 | 4. Under the Repository tab, choose Git as your SCM and enter the full path to the .git directory from step 2; 44 | $REDMINE_REPO_DIR/repository_name/.git/. Click "Create". 45 | 5. Click the new "Repository" link in the main navigation to verify that your repository integration works - 46 | this might take a while as Redmine is fetching all commits. 47 | 48 | 3. Connecting Gitolite to Redmine 49 | 1. Go to your Gitolite repository on your Gitolite server $GL_REPO_DIR, for example /home/git/repositories/testing.git/. 50 | 2. mv $GL_REPO_DIR/hooks/post-receive.sample $GL_REPO_DIR/hooks/post-receive 51 | 3. Replace the hook file's contents with: 52 | 53 | #!/bin/sh 54 | # 55 | # An example hook script for the "post-receive" event. 56 | # 57 | # The "post-receive" script is run after receive-pack has accepted a pack 58 | # and the repository has been updated. It is passed arguments in through 59 | # stdin in the form 60 | # 61 | # For example: 62 | # aa453216d1b3e49e7f6f98441fa56946ddcd6a20 68f7abf4e6f922807889f52bc043ecd31b79f814 refs/heads/master 63 | # 64 | # see contrib/hooks/ for a sample, or uncomment the next line and 65 | # rename the file to "post-receive". 66 | 67 | #. /usr/share/doc/git-core/contrib/hooks/post-receive-email 68 | 69 | REDMINE_SERVER=http://your.redmine.server.com 70 | HOOK_URL=gitolite_hook 71 | FETCH_URL=sys/fetch_changesets 72 | KEY=your_redmine_api_key 73 | LOG=/home/git/logs/post-receive.log 74 | 75 | # Read from stdin 76 | while read old new refname; do 77 | echo "Post-receive hook for this repo: [$GL_REPO] $old $new $refname" 78 | # Hit the Redmine Gitolite hook for this repo 79 | curl -d "oldrev=$old&newrev=$new&refname=$refname&user=$GL_USER" "$REDMINE_SERVER/$HOOK_URL?project_id=$GL_REPO" 80 | # Hit the fetch changesets URL 81 | curl "$REDMINE_SERVER/$FETCH_URL?id=$GL_REPO&key=$KEY" 82 | echo "Done." 83 | echo "" 84 | done 85 | 86 | That's it. Gitolite will now send a HTTP POST to the Redmine Gitolite Hook plugin whenever changes are pushed to the Gitolite server. 87 | The plugin then takes care of pulling the changes to the local repository and updating the Redmine database with them. 88 | 89 | == Assumptions 90 | 91 | * Redmine 0.9 running on a *nix-like system. It will probably work on Redmine 0.8 as well. 92 | * Git available on the commandline. 93 | * You have Gitolite set up and are comfortable with creating hooks. 94 | 95 | == Todos 96 | 97 | * Update tests to work with Gitolite instead of Github 98 | 99 | == License 100 | 101 | Copyright (c) 2010 Kah Seng Tay - Gitolite modifications 102 | 103 | Copyright (c) 2009 Jakob Skjerning - Original 104 | 105 | Permission is hereby granted, free of charge, to any person 106 | obtaining a copy of this software and associated documentation 107 | files (the "Software"), to deal in the Software without 108 | restriction, including without limitation the rights to use, 109 | copy, modify, merge, publish, distribute, sublicense, and/or sell 110 | copies of the Software, and to permit persons to whom the 111 | Software is furnished to do so, subject to the following 112 | conditions: 113 | 114 | The above copyright notice and this permission notice shall be 115 | included in all copies or substantial portions of the Software. 116 | 117 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 118 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 119 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 120 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 121 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 122 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 123 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 124 | OTHER DEALINGS IN THE SOFTWARE. 125 | --------------------------------------------------------------------------------