Last active
August 29, 2015 14:08
-
-
Save DavidAntaramian/5bd7231119289c57a2ff to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
# Redmine-Gitolite Hook for Redmine Git Hosting v 0.7.7 | |
# Copyright (c) 2014 by David Antaramian ([email protected]) | |
# Released under the MIT License (SPDX:MIT;https://spdx.org/licenses/MIT) | |
# | |
# Modeled after the original hook by Nicolas Rodriguez, et al. | |
# Original hook available at: | |
# https://github.com/jbox-web/redmine_git_hosting/blob/0.7.7/contrib/hooks/post-receive.redmine_gitolite.rb | |
# | |
# This script expects to be run inside of a git repository with redminegitolite.* | |
# configuration settings available via `git config`. It expects to receive lines | |
# via `STDIN` where each line is of the form | |
# | |
# old_value<SP>new_value<SP>refs<LF> | |
# | |
# where <SP> is a space and <LF> is a Unix-style line terminator. These lines | |
# are automatically fed to the script by Git as part of the post-receive hook. | |
# The script will then post each reference over HTTP/S to the Redmine installation | |
# specified by the configuration settings. | |
# | |
# All output is sent to the user via STDOUT through the use of a Logger object. | |
require 'digest/sha2' | |
require 'net/http' | |
require 'uri' | |
require 'logger' | |
# Internal: The logging utitlity for the script which writes to `STDOUT`. | |
# By default, `STDOUT` is piped through the SSH connection to the user pushing | |
# the repository upstream. Therefore, anything the logger writes out will show | |
# directly in the output of whatever utility the user pushing is using | |
# (whether that be the git command line tool, SourceTree, Eclipse, etc.). | |
$logger = Logger.new(STDOUT) | |
# Sets the log output to something like the following | |
# `Thu Oct 23 15:37:45 EDT 2014 (INFO): Processing new commits` | |
$logger.formatter = proc do |severity, datetime, progname, msg| | |
"#{datetime} (#{severity}): #{msg}\n" | |
end | |
# Since Gitolite::get_config depends on $logger, we have to check the debug mode | |
# setting using the `git config` command directly. By default, the logger is | |
# set to DEBUG, meaning it will output all logging messages. Because get_config | |
# outputs the configuration settings at the DEBUG level, this would result in | |
# a large amount of information being output. | |
# | |
# If debug mode is not set at the configuration level, we use INFO which outputs | |
# everything except for DEBUG severity messages | |
$logger.level = %x[git config redminegitolite.debugmode].chomp == "true" ? | |
Logger::DEBUG : Logger::INFO | |
# Public: Namespaces the Config constant and the function that bootstraps the | |
# constant. | |
module Gitolite | |
# Internal: This constructs a hash of configuration key/value pairs provided | |
# by the `git config` command and returns it to the caller. This method is | |
# meant to be called only by the Config constant. | |
# | |
# Returns a hash of the git configuration settings for the working directory | |
def self.get_config | |
config_hash = {} | |
settings = %x[git config --list] | |
settings.each_line do |l| | |
name, value = l.chomp.split('=') | |
$logger.debug("Gitolite Configuration: #{name} has value #{value}") | |
config_hash[name] = value | |
end | |
return config_hash | |
end | |
# Public: A hash of git configuration settings which is bootstrapped | |
# by #get_config | |
Config = self.get_config() | |
end | |
# Public: Represents a Redmine installation which is able to notify itself | |
# of updates to repositories. | |
class Redmine | |
# Public: The URL of the Redmine installation | |
attr_reader :url | |
# Public: Constructs a new Redmine object using the url provided | |
# via Gitolite::Config in the `redminegitolite.redmineurl` settings | |
# | |
# Returns a new Redmine object | |
def initialize | |
@url = Gitolite::Config["redminegitolite.redmineurl"] | |
end | |
# Public: Notifies the Redmine installation of the updated references | |
# by constructing a new APIRequest. | |
# | |
# repository - A Repository object representing the repository being | |
# updated | |
# reference - A Reference object representing the reference that has | |
# been passed in via STDIN | |
# | |
# Returns nothing | |
def notify(repository, reference) | |
request = APIRequest.new(self, repository) | |
request.send(reference) | |
end | |
end | |
# Public: Represents a request to the "API" which handles updates to the | |
# project's repository on Redmine. | |
class APIRequest | |
# Public: Constructs a new APIRequest object | |
# | |
# redmine - A Redmine object representing the Redmine installation | |
# which will receive the updates | |
# repository - A Repostiroy object representing | |
# | |
# Returns a new APIRequest object | |
def initialize(redmine, repository) | |
# The server uses this to generate the server side encoded_time | |
clear_time = Time.new.utc.strftime("%s") | |
# The repository key acts as a shared secret between the client script | |
# and the server. The server accepts the clear_time above as a parameter, | |
# calculates the same hash and then checks it against the result given | |
# in encoded_time. | |
encoded_time = Digest::SHA1.hexdigest(clear_time + repository.key) | |
# The URI of the project where the request will be HTTP POSTed to. | |
@uri = URI("#{redmine.url}/#{repository.project}") | |
# The hash of the parameters to be HTTP POSTed | |
@params = { | |
clear_time: clear_time, | |
encoded_time: encoded_time, | |
} | |
# Add repository.id to the hash if it exists. The repository.id | |
# will only be present when the repository being pushed is *not* | |
# the main repository for the project | |
@params["repositoryid"] = repository.id if repository.id | |
end | |
# Public: Posts the APIRequest to the server | |
# | |
# reference - A Reference object representing the the reference that has | |
# been passed in via STDIN | |
# | |
# Returns nothing | |
def send(reference) | |
# Add the reference to the parameters using the key refs[] and concatenating | |
# the values together using commas with no spaces in between | |
@params["refs[]"] = [reference.old_value, reference.new_value, reference.ref_name].join(',') | |
$logger.debug("Parameters to be posted #{@params}") | |
$logger.debug("Posting to #{@uri.host}:#{@uri.port}#{@uri.path}") | |
# Construct the new HTTP Post object | |
request = Net::HTTP::Post.new(@uri.path) | |
# Set the form data to be posted in the body of the request as @params | |
request.set_form_data(@params) | |
# Performs the HTTP request and checks the response. SSL is used if the URI | |
# specifies HTTPS as the scheme. | |
Net::HTTP.start(@uri.host, @uri.port, :use_ssl => @uri.scheme == "https") do |http| | |
response = http.request(request) | |
if !response.is_a? Net::HTTPSuccess | |
$logger.warn("Failed to receive favorable request from Redmine server") | |
$logger.warn("Server responded with HTTP code #{response.code} (#{response.message})") | |
$logger.warn("Updating #{reference} failed") | |
else | |
$logger.info("Successfuly updated #{reference}") | |
end | |
end | |
end | |
end | |
# Public: Represents a git repository for a project on the server | |
class Repository | |
# The repository id which may be nil | |
attr_reader :id | |
# The project id | |
attr_reader :project | |
# The repository key which acts as a shared secret | |
attr_reader :key | |
# Public: Constructs a new Repository object | |
# | |
# Returns a new Repository object | |
def initialize | |
# The repositoryid setting may not exist depending on whether this | |
# is the main repository for the project or not | |
if Gitolite::Config["redminegitolite.repositoryid"] | |
@id = Gitolite::Config["redminegitolite.repositoryid"] | |
end | |
@project = Gitolite::Config["redminegitolite.projectid"] | |
@key = Gitolite::Config["redminegitolite.repositorykey"] | |
end | |
# Public: Outputs a text representation of the Repository | |
# | |
# Returns a String representation of the object | |
def to_s | |
if id | |
"repository #{id} on project #{project}" | |
else | |
"project #{project}'s default repository" | |
end | |
end | |
end | |
# Public: Represents a reference which has been passed in via STDIN | |
class Reference | |
# Public: The old value of the reference | |
attr_reader :old_value | |
# Public: The new value of the reference | |
attr_reader :new_value | |
# Public: The reference name | |
attr_reader :ref_name | |
# Public: Constructs a new Reference object | |
# | |
# old_value - The old value of the reference | |
# new_value - The new value of the reference | |
# ref_name - The reference name | |
def initialize(old_value, new_value, ref_name) | |
@old_value = old_value | |
@new_value = new_value | |
@ref_name = ref_name | |
end | |
# Public: Outputs a text representation of the Reference | |
# | |
# Returns a String representation of the object | |
def to_s | |
"#{ref_name} with #{old_value} to #{new_value}" | |
end | |
end | |
repo = Repository.new | |
redmine = Redmine.new | |
$logger.info("Processing new commits for #{repo}") | |
$logger.info("Starting to read pushed references") | |
# For each line in STDIN ($<) construct a new refernce object and | |
# notify the Redmine installation of the update. | |
$<.each do |reference| | |
old_value, new_value, ref_name = reference.chomp.strip.split | |
r = Reference.new(old_value, new_value, ref_name) | |
$logger.debug("Received #{r}") | |
redmine.notify(repo, r) | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment