添加grack
This commit is contained in:
parent
38b97aa558
commit
acd4f33d72
|
@ -0,0 +1,14 @@
|
||||||
|
language: ruby
|
||||||
|
env:
|
||||||
|
- TRAVIS=true
|
||||||
|
branches:
|
||||||
|
only:
|
||||||
|
- 'master'
|
||||||
|
rvm:
|
||||||
|
- 1.9.3-p327
|
||||||
|
- 2.0.0
|
||||||
|
before_script:
|
||||||
|
- "bundle install"
|
||||||
|
- "git submodule init"
|
||||||
|
- "git submodule update"
|
||||||
|
script: "bundle exec rake"
|
|
@ -0,0 +1,16 @@
|
||||||
|
2.0.2
|
||||||
|
- Revert MR that broke smart HTTP clients.
|
||||||
|
|
||||||
|
2.0.1
|
||||||
|
- Make sure child processes get reaped after popen, again.
|
||||||
|
|
||||||
|
2.0.0
|
||||||
|
- Use safer shell commands and avoid Dir.chdir
|
||||||
|
- Restrict the environment for shell commands
|
||||||
|
- Make Grack::Server thread-safe (zimbatm)
|
||||||
|
- Make sure child processes get reaped after popen
|
||||||
|
- Verify requested path is actually a Git directory (Ryan Canty)
|
||||||
|
|
||||||
|
|
||||||
|
1.1.0
|
||||||
|
- Modifies service_rpc to use chunked transfer (https://github.com/gitlabhq/grack/pull/1)
|
|
@ -0,0 +1,10 @@
|
||||||
|
source "http://ruby.taobao.org"
|
||||||
|
|
||||||
|
gemspec
|
||||||
|
|
||||||
|
group :development do
|
||||||
|
gem 'byebug'
|
||||||
|
gem 'rake'
|
||||||
|
gem 'pry'
|
||||||
|
gem 'rack-test'
|
||||||
|
end
|
|
@ -0,0 +1,37 @@
|
||||||
|
PATH
|
||||||
|
remote: .
|
||||||
|
specs:
|
||||||
|
gitlab-grack (2.0.2)
|
||||||
|
rack (~> 1.5.1)
|
||||||
|
|
||||||
|
GEM
|
||||||
|
remote: http://ruby.taobao.org/
|
||||||
|
specs:
|
||||||
|
byebug (4.0.5)
|
||||||
|
columnize (= 0.9.0)
|
||||||
|
coderay (1.1.0)
|
||||||
|
columnize (0.9.0)
|
||||||
|
metaclass (0.0.1)
|
||||||
|
method_source (0.8.2)
|
||||||
|
mocha (0.14.0)
|
||||||
|
metaclass (~> 0.0.1)
|
||||||
|
pry (0.10.1)
|
||||||
|
coderay (~> 1.1.0)
|
||||||
|
method_source (~> 0.8.1)
|
||||||
|
slop (~> 3.4)
|
||||||
|
rack (1.5.2)
|
||||||
|
rack-test (0.6.2)
|
||||||
|
rack (>= 1.0)
|
||||||
|
rake (10.1.0)
|
||||||
|
slop (3.6.0)
|
||||||
|
|
||||||
|
PLATFORMS
|
||||||
|
ruby
|
||||||
|
|
||||||
|
DEPENDENCIES
|
||||||
|
byebug
|
||||||
|
gitlab-grack!
|
||||||
|
mocha (~> 0.11)
|
||||||
|
pry
|
||||||
|
rack-test
|
||||||
|
rake
|
|
@ -0,0 +1,95 @@
|
||||||
|
Grack - Ruby/Rack Git Smart-HTTP Server Handler
|
||||||
|
===============================================
|
||||||
|
|
||||||
|
[![Build Status](https://travis-ci.org/gitlabhq/grack.png)](https://travis-ci.org/gitlabhq/grack)
|
||||||
|
[![Code Climate](https://codeclimate.com/github/gitlabhq/grack.png)](https://codeclimate.com/github/gitlabhq/grack)
|
||||||
|
|
||||||
|
This project aims to replace the builtin git-http-backend CGI handler
|
||||||
|
distributed with C Git with a Rack application. This reason for doing this
|
||||||
|
is to allow far more webservers to be able to handle Git smart http requests.
|
||||||
|
|
||||||
|
The default git-http-backend only runs as a CGI script, and specifically is
|
||||||
|
only targeted for Apache 2.x usage (it requires PATH_INFO to be set and
|
||||||
|
specifically formatted). So, instead of trying to get it to work with
|
||||||
|
other CGI capable webservers (Lighttpd, etc), we can get it running on nearly
|
||||||
|
every major and minor webserver out there by making it Rack capable. Rack
|
||||||
|
applications can run with the following handlers:
|
||||||
|
|
||||||
|
* CGI
|
||||||
|
* FCGI
|
||||||
|
* Mongrel (and EventedMongrel and SwiftipliedMongrel)
|
||||||
|
* WEBrick
|
||||||
|
* SCGI
|
||||||
|
* LiteSpeed
|
||||||
|
* Thin
|
||||||
|
|
||||||
|
These web servers include Rack handlers in their distributions:
|
||||||
|
|
||||||
|
* Ebb
|
||||||
|
* Fuzed
|
||||||
|
* Phusion Passenger (which is mod_rack for Apache and for nginx)
|
||||||
|
* Unicorn
|
||||||
|
* Puma
|
||||||
|
|
||||||
|
With [Warbler](http://caldersphere.rubyforge.org/warbler/classes/Warbler.html),
|
||||||
|
and JRuby, we can also generate a WAR file that can be deployed in any Java
|
||||||
|
web application server (Tomcat, Glassfish, Websphere, JBoss, etc).
|
||||||
|
|
||||||
|
Since the git-http-backend is really just a simple wrapper for the upload-pack
|
||||||
|
and receive-pack processes with the '--stateless-rpc' option, it does not
|
||||||
|
actually re-implement very much.
|
||||||
|
|
||||||
|
Dependencies
|
||||||
|
========================
|
||||||
|
* Ruby - http://www.ruby-lang.org
|
||||||
|
* Rack - http://rack.rubyforge.org
|
||||||
|
* A Rack-compatible web server
|
||||||
|
* Git >= 1.7 (currently the 'pu' branch)
|
||||||
|
* Mocha (only for running the tests)
|
||||||
|
|
||||||
|
Quick Start
|
||||||
|
========================
|
||||||
|
$ gem install rack
|
||||||
|
$ (edit config.ru to set git project path)
|
||||||
|
$ rackup --host 127.0.0.1 -p 8080 config.ru
|
||||||
|
$ git clone http://127.0.0.1:8080/schacon/grit.git
|
||||||
|
|
||||||
|
Contributing
|
||||||
|
========================
|
||||||
|
If you would like to contribute to the Grack project, I prefer to get
|
||||||
|
pull-requests via GitHub. You should include tests for whatever functionality
|
||||||
|
you add. Just fork this project, push your changes to your fork and click
|
||||||
|
the 'pull request' button. To run the tests, you first need to install the
|
||||||
|
'mocha' mocking library and initialize the submodule.
|
||||||
|
|
||||||
|
$ sudo gem install mocha
|
||||||
|
$ git submodule init
|
||||||
|
$ git submodule update
|
||||||
|
|
||||||
|
Then you should be able to run the tests with a 'rake' command. You can also
|
||||||
|
run coverage tests with 'rake rcov' if you have rcov installed.
|
||||||
|
|
||||||
|
License
|
||||||
|
========================
|
||||||
|
(The MIT License)
|
||||||
|
|
||||||
|
Copyright (c) 2009 Scott Chacon <schacon@gmail.com>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of this software and associated documentation files (the
|
||||||
|
'Software'), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be
|
||||||
|
included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
|
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@ -0,0 +1,27 @@
|
||||||
|
#!/usr/bin/env rake
|
||||||
|
require "bundler/gem_tasks"
|
||||||
|
|
||||||
|
task :default => :test
|
||||||
|
|
||||||
|
desc "Run the tests."
|
||||||
|
task :test do
|
||||||
|
Dir.glob("tests/*_test.rb").each do |f|
|
||||||
|
system "ruby #{f}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "Run test coverage."
|
||||||
|
task :rcov do
|
||||||
|
system "rcov tests/*_test.rb -i lib/git_http.rb -x rack -x Library -x tests"
|
||||||
|
system "open coverage/index.html"
|
||||||
|
end
|
||||||
|
|
||||||
|
namespace :grack do
|
||||||
|
desc "Start Grack"
|
||||||
|
task :start do
|
||||||
|
system('./bin/testserver')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "Start everything."
|
||||||
|
multitask :start => [ 'grack:start' ]
|
|
@ -0,0 +1,6 @@
|
||||||
|
#! /bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
cd $(dirname "$0")/..
|
||||||
|
exec /usr/bin/env bundle exec pry -Ilib -r grack
|
|
@ -0,0 +1,24 @@
|
||||||
|
#! /usr/bin/env ruby
|
||||||
|
libdir = File.absolute_path( File.join( File.dirname(__FILE__), '../lib' ) )
|
||||||
|
$LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
|
||||||
|
|
||||||
|
Bundler.require(:default, :development)
|
||||||
|
|
||||||
|
|
||||||
|
require 'grack'
|
||||||
|
require 'rack'
|
||||||
|
root = File.absolute_path( File.join( File.dirname(__FILE__), '../examples' ) )
|
||||||
|
app = Grack::Server.new({
|
||||||
|
project_root: root,
|
||||||
|
upload_pack: true,
|
||||||
|
receive_pack:true
|
||||||
|
})
|
||||||
|
|
||||||
|
app1= Rack::Builder.new do
|
||||||
|
use Grack::Auth do |username, password|
|
||||||
|
[username, password] == ['123', '455']
|
||||||
|
end
|
||||||
|
run app
|
||||||
|
end
|
||||||
|
|
||||||
|
Rack::Server.start app: app1, Port: 3001
|
|
@ -0,0 +1 @@
|
||||||
|
ref: refs/heads/master
|
|
@ -0,0 +1,6 @@
|
||||||
|
[core]
|
||||||
|
repositoryformatversion = 0
|
||||||
|
filemode = true
|
||||||
|
bare = true
|
||||||
|
ignorecase = true
|
||||||
|
precomposeunicode = true
|
|
@ -0,0 +1 @@
|
||||||
|
Unnamed repository; edit this file 'description' to name the repository.
|
|
@ -0,0 +1,15 @@
|
||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# An example hook script to check the commit log message taken by
|
||||||
|
# applypatch from an e-mail message.
|
||||||
|
#
|
||||||
|
# The hook should exit with non-zero status after issuing an
|
||||||
|
# appropriate message if it wants to stop the commit. The hook is
|
||||||
|
# allowed to edit the commit message file.
|
||||||
|
#
|
||||||
|
# To enable this hook, rename this file to "applypatch-msg".
|
||||||
|
|
||||||
|
. git-sh-setup
|
||||||
|
test -x "$GIT_DIR/hooks/commit-msg" &&
|
||||||
|
exec "$GIT_DIR/hooks/commit-msg" ${1+"$@"}
|
||||||
|
:
|
|
@ -0,0 +1,24 @@
|
||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# An example hook script to check the commit log message.
|
||||||
|
# Called by "git commit" with one argument, the name of the file
|
||||||
|
# that has the commit message. The hook should exit with non-zero
|
||||||
|
# status after issuing an appropriate message if it wants to stop the
|
||||||
|
# commit. The hook is allowed to edit the commit message file.
|
||||||
|
#
|
||||||
|
# To enable this hook, rename this file to "commit-msg".
|
||||||
|
|
||||||
|
# Uncomment the below to add a Signed-off-by line to the message.
|
||||||
|
# Doing this in a hook is a bad idea in general, but the prepare-commit-msg
|
||||||
|
# hook is more suited to it.
|
||||||
|
#
|
||||||
|
# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
|
||||||
|
# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1"
|
||||||
|
|
||||||
|
# This example catches duplicate Signed-off-by lines.
|
||||||
|
|
||||||
|
test "" = "$(grep '^Signed-off-by: ' "$1" |
|
||||||
|
sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || {
|
||||||
|
echo >&2 Duplicate Signed-off-by lines.
|
||||||
|
exit 1
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# An example hook script to prepare a packed repository for use over
|
||||||
|
# dumb transports.
|
||||||
|
#
|
||||||
|
# To enable this hook, rename this file to "post-update".
|
||||||
|
|
||||||
|
exec git update-server-info
|
|
@ -0,0 +1,14 @@
|
||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# An example hook script to verify what is about to be committed
|
||||||
|
# by applypatch from an e-mail message.
|
||||||
|
#
|
||||||
|
# The hook should exit with non-zero status after issuing an
|
||||||
|
# appropriate message if it wants to stop the commit.
|
||||||
|
#
|
||||||
|
# To enable this hook, rename this file to "pre-applypatch".
|
||||||
|
|
||||||
|
. git-sh-setup
|
||||||
|
test -x "$GIT_DIR/hooks/pre-commit" &&
|
||||||
|
exec "$GIT_DIR/hooks/pre-commit" ${1+"$@"}
|
||||||
|
:
|
|
@ -0,0 +1,49 @@
|
||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# An example hook script to verify what is about to be committed.
|
||||||
|
# Called by "git commit" with no arguments. The hook should
|
||||||
|
# exit with non-zero status after issuing an appropriate message if
|
||||||
|
# it wants to stop the commit.
|
||||||
|
#
|
||||||
|
# To enable this hook, rename this file to "pre-commit".
|
||||||
|
|
||||||
|
if git rev-parse --verify HEAD >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
against=HEAD
|
||||||
|
else
|
||||||
|
# Initial commit: diff against an empty tree object
|
||||||
|
against=4b825dc642cb6eb9a060e54bf8d69288fbee4904
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If you want to allow non-ASCII filenames set this variable to true.
|
||||||
|
allownonascii=$(git config --bool hooks.allownonascii)
|
||||||
|
|
||||||
|
# Redirect output to stderr.
|
||||||
|
exec 1>&2
|
||||||
|
|
||||||
|
# Cross platform projects tend to avoid non-ASCII filenames; prevent
|
||||||
|
# them from being added to the repository. We exploit the fact that the
|
||||||
|
# printable range starts at the space character and ends with tilde.
|
||||||
|
if [ "$allownonascii" != "true" ] &&
|
||||||
|
# Note that the use of brackets around a tr range is ok here, (it's
|
||||||
|
# even required, for portability to Solaris 10's /usr/bin/tr), since
|
||||||
|
# the square bracket bytes happen to fall in the designated range.
|
||||||
|
test $(git diff --cached --name-only --diff-filter=A -z $against |
|
||||||
|
LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0
|
||||||
|
then
|
||||||
|
cat <<\EOF
|
||||||
|
Error: Attempt to add a non-ASCII file name.
|
||||||
|
|
||||||
|
This can cause problems if you want to work with people on other platforms.
|
||||||
|
|
||||||
|
To be portable it is advisable to rename the file.
|
||||||
|
|
||||||
|
If you know what you are doing you can disable this check using:
|
||||||
|
|
||||||
|
git config hooks.allownonascii true
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If there are whitespace errors, print the offending file names and fail.
|
||||||
|
exec git diff-index --check --cached $against --
|
|
@ -0,0 +1,53 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# An example hook script to verify what is about to be pushed. Called by "git
|
||||||
|
# push" after it has checked the remote status, but before anything has been
|
||||||
|
# pushed. If this script exits with a non-zero status nothing will be pushed.
|
||||||
|
#
|
||||||
|
# This hook is called with the following parameters:
|
||||||
|
#
|
||||||
|
# $1 -- Name of the remote to which the push is being done
|
||||||
|
# $2 -- URL to which the push is being done
|
||||||
|
#
|
||||||
|
# If pushing without using a named remote those arguments will be equal.
|
||||||
|
#
|
||||||
|
# Information about the commits which are being pushed is supplied as lines to
|
||||||
|
# the standard input in the form:
|
||||||
|
#
|
||||||
|
# <local ref> <local sha1> <remote ref> <remote sha1>
|
||||||
|
#
|
||||||
|
# This sample shows how to prevent push of commits where the log message starts
|
||||||
|
# with "WIP" (work in progress).
|
||||||
|
|
||||||
|
remote="$1"
|
||||||
|
url="$2"
|
||||||
|
|
||||||
|
z40=0000000000000000000000000000000000000000
|
||||||
|
|
||||||
|
while read local_ref local_sha remote_ref remote_sha
|
||||||
|
do
|
||||||
|
if [ "$local_sha" = $z40 ]
|
||||||
|
then
|
||||||
|
# Handle delete
|
||||||
|
:
|
||||||
|
else
|
||||||
|
if [ "$remote_sha" = $z40 ]
|
||||||
|
then
|
||||||
|
# New branch, examine all commits
|
||||||
|
range="$local_sha"
|
||||||
|
else
|
||||||
|
# Update to existing branch, examine new commits
|
||||||
|
range="$remote_sha..$local_sha"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for WIP commit
|
||||||
|
commit=`git rev-list -n 1 --grep '^WIP' "$range"`
|
||||||
|
if [ -n "$commit" ]
|
||||||
|
then
|
||||||
|
echo >&2 "Found WIP commit in $local_ref, not pushing"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
exit 0
|
|
@ -0,0 +1,169 @@
|
||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# Copyright (c) 2006, 2008 Junio C Hamano
|
||||||
|
#
|
||||||
|
# The "pre-rebase" hook is run just before "git rebase" starts doing
|
||||||
|
# its job, and can prevent the command from running by exiting with
|
||||||
|
# non-zero status.
|
||||||
|
#
|
||||||
|
# The hook is called with the following parameters:
|
||||||
|
#
|
||||||
|
# $1 -- the upstream the series was forked from.
|
||||||
|
# $2 -- the branch being rebased (or empty when rebasing the current branch).
|
||||||
|
#
|
||||||
|
# This sample shows how to prevent topic branches that are already
|
||||||
|
# merged to 'next' branch from getting rebased, because allowing it
|
||||||
|
# would result in rebasing already published history.
|
||||||
|
|
||||||
|
publish=next
|
||||||
|
basebranch="$1"
|
||||||
|
if test "$#" = 2
|
||||||
|
then
|
||||||
|
topic="refs/heads/$2"
|
||||||
|
else
|
||||||
|
topic=`git symbolic-ref HEAD` ||
|
||||||
|
exit 0 ;# we do not interrupt rebasing detached HEAD
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$topic" in
|
||||||
|
refs/heads/??/*)
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
exit 0 ;# we do not interrupt others.
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Now we are dealing with a topic branch being rebased
|
||||||
|
# on top of master. Is it OK to rebase it?
|
||||||
|
|
||||||
|
# Does the topic really exist?
|
||||||
|
git show-ref -q "$topic" || {
|
||||||
|
echo >&2 "No such branch $topic"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Is topic fully merged to master?
|
||||||
|
not_in_master=`git rev-list --pretty=oneline ^master "$topic"`
|
||||||
|
if test -z "$not_in_master"
|
||||||
|
then
|
||||||
|
echo >&2 "$topic is fully merged to master; better remove it."
|
||||||
|
exit 1 ;# we could allow it, but there is no point.
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Is topic ever merged to next? If so you should not be rebasing it.
|
||||||
|
only_next_1=`git rev-list ^master "^$topic" ${publish} | sort`
|
||||||
|
only_next_2=`git rev-list ^master ${publish} | sort`
|
||||||
|
if test "$only_next_1" = "$only_next_2"
|
||||||
|
then
|
||||||
|
not_in_topic=`git rev-list "^$topic" master`
|
||||||
|
if test -z "$not_in_topic"
|
||||||
|
then
|
||||||
|
echo >&2 "$topic is already up-to-date with master"
|
||||||
|
exit 1 ;# we could allow it, but there is no point.
|
||||||
|
else
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"`
|
||||||
|
/usr/bin/perl -e '
|
||||||
|
my $topic = $ARGV[0];
|
||||||
|
my $msg = "* $topic has commits already merged to public branch:\n";
|
||||||
|
my (%not_in_next) = map {
|
||||||
|
/^([0-9a-f]+) /;
|
||||||
|
($1 => 1);
|
||||||
|
} split(/\n/, $ARGV[1]);
|
||||||
|
for my $elem (map {
|
||||||
|
/^([0-9a-f]+) (.*)$/;
|
||||||
|
[$1 => $2];
|
||||||
|
} split(/\n/, $ARGV[2])) {
|
||||||
|
if (!exists $not_in_next{$elem->[0]}) {
|
||||||
|
if ($msg) {
|
||||||
|
print STDERR $msg;
|
||||||
|
undef $msg;
|
||||||
|
}
|
||||||
|
print STDERR " $elem->[1]\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
' "$topic" "$not_in_next" "$not_in_master"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
|
|
||||||
|
################################################################
|
||||||
|
|
||||||
|
This sample hook safeguards topic branches that have been
|
||||||
|
published from being rewound.
|
||||||
|
|
||||||
|
The workflow assumed here is:
|
||||||
|
|
||||||
|
* Once a topic branch forks from "master", "master" is never
|
||||||
|
merged into it again (either directly or indirectly).
|
||||||
|
|
||||||
|
* Once a topic branch is fully cooked and merged into "master",
|
||||||
|
it is deleted. If you need to build on top of it to correct
|
||||||
|
earlier mistakes, a new topic branch is created by forking at
|
||||||
|
the tip of the "master". This is not strictly necessary, but
|
||||||
|
it makes it easier to keep your history simple.
|
||||||
|
|
||||||
|
* Whenever you need to test or publish your changes to topic
|
||||||
|
branches, merge them into "next" branch.
|
||||||
|
|
||||||
|
The script, being an example, hardcodes the publish branch name
|
||||||
|
to be "next", but it is trivial to make it configurable via
|
||||||
|
$GIT_DIR/config mechanism.
|
||||||
|
|
||||||
|
With this workflow, you would want to know:
|
||||||
|
|
||||||
|
(1) ... if a topic branch has ever been merged to "next". Young
|
||||||
|
topic branches can have stupid mistakes you would rather
|
||||||
|
clean up before publishing, and things that have not been
|
||||||
|
merged into other branches can be easily rebased without
|
||||||
|
affecting other people. But once it is published, you would
|
||||||
|
not want to rewind it.
|
||||||
|
|
||||||
|
(2) ... if a topic branch has been fully merged to "master".
|
||||||
|
Then you can delete it. More importantly, you should not
|
||||||
|
build on top of it -- other people may already want to
|
||||||
|
change things related to the topic as patches against your
|
||||||
|
"master", so if you need further changes, it is better to
|
||||||
|
fork the topic (perhaps with the same name) afresh from the
|
||||||
|
tip of "master".
|
||||||
|
|
||||||
|
Let's look at this example:
|
||||||
|
|
||||||
|
o---o---o---o---o---o---o---o---o---o "next"
|
||||||
|
/ / / /
|
||||||
|
/ a---a---b A / /
|
||||||
|
/ / / /
|
||||||
|
/ / c---c---c---c B /
|
||||||
|
/ / / \ /
|
||||||
|
/ / / b---b C \ /
|
||||||
|
/ / / / \ /
|
||||||
|
---o---o---o---o---o---o---o---o---o---o---o "master"
|
||||||
|
|
||||||
|
|
||||||
|
A, B and C are topic branches.
|
||||||
|
|
||||||
|
* A has one fix since it was merged up to "next".
|
||||||
|
|
||||||
|
* B has finished. It has been fully merged up to "master" and "next",
|
||||||
|
and is ready to be deleted.
|
||||||
|
|
||||||
|
* C has not merged to "next" at all.
|
||||||
|
|
||||||
|
We would want to allow C to be rebased, refuse A, and encourage
|
||||||
|
B to be deleted.
|
||||||
|
|
||||||
|
To compute (1):
|
||||||
|
|
||||||
|
git rev-list ^master ^topic next
|
||||||
|
git rev-list ^master next
|
||||||
|
|
||||||
|
if these match, topic has not merged in next at all.
|
||||||
|
|
||||||
|
To compute (2):
|
||||||
|
|
||||||
|
git rev-list master..topic
|
||||||
|
|
||||||
|
if this is empty, it is fully merged to "master".
|
|
@ -0,0 +1,36 @@
|
||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# An example hook script to prepare the commit log message.
|
||||||
|
# Called by "git commit" with the name of the file that has the
|
||||||
|
# commit message, followed by the description of the commit
|
||||||
|
# message's source. The hook's purpose is to edit the commit
|
||||||
|
# message file. If the hook fails with a non-zero status,
|
||||||
|
# the commit is aborted.
|
||||||
|
#
|
||||||
|
# To enable this hook, rename this file to "prepare-commit-msg".
|
||||||
|
|
||||||
|
# This hook includes three examples. The first comments out the
|
||||||
|
# "Conflicts:" part of a merge commit.
|
||||||
|
#
|
||||||
|
# The second includes the output of "git diff --name-status -r"
|
||||||
|
# into the message, just before the "git status" output. It is
|
||||||
|
# commented because it doesn't cope with --amend or with squashed
|
||||||
|
# commits.
|
||||||
|
#
|
||||||
|
# The third example adds a Signed-off-by line to the message, that can
|
||||||
|
# still be edited. This is rarely a good idea.
|
||||||
|
|
||||||
|
case "$2,$3" in
|
||||||
|
merge,)
|
||||||
|
/usr/bin/perl -i.bak -ne 's/^/# /, s/^# #/#/ if /^Conflicts/ .. /#/; print' "$1" ;;
|
||||||
|
|
||||||
|
# ,|template,)
|
||||||
|
# /usr/bin/perl -i.bak -pe '
|
||||||
|
# print "\n" . `git diff --cached --name-status -r`
|
||||||
|
# if /^#/ && $first++ == 0' "$1" ;;
|
||||||
|
|
||||||
|
*) ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
|
||||||
|
# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1"
|
|
@ -0,0 +1,128 @@
|
||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# An example hook script to blocks unannotated tags from entering.
|
||||||
|
# Called by "git receive-pack" with arguments: refname sha1-old sha1-new
|
||||||
|
#
|
||||||
|
# To enable this hook, rename this file to "update".
|
||||||
|
#
|
||||||
|
# Config
|
||||||
|
# ------
|
||||||
|
# hooks.allowunannotated
|
||||||
|
# This boolean sets whether unannotated tags will be allowed into the
|
||||||
|
# repository. By default they won't be.
|
||||||
|
# hooks.allowdeletetag
|
||||||
|
# This boolean sets whether deleting tags will be allowed in the
|
||||||
|
# repository. By default they won't be.
|
||||||
|
# hooks.allowmodifytag
|
||||||
|
# This boolean sets whether a tag may be modified after creation. By default
|
||||||
|
# it won't be.
|
||||||
|
# hooks.allowdeletebranch
|
||||||
|
# This boolean sets whether deleting branches will be allowed in the
|
||||||
|
# repository. By default they won't be.
|
||||||
|
# hooks.denycreatebranch
|
||||||
|
# This boolean sets whether remotely creating branches will be denied
|
||||||
|
# in the repository. By default this is allowed.
|
||||||
|
#
|
||||||
|
|
||||||
|
# --- Command line
|
||||||
|
refname="$1"
|
||||||
|
oldrev="$2"
|
||||||
|
newrev="$3"
|
||||||
|
|
||||||
|
# --- Safety check
|
||||||
|
if [ -z "$GIT_DIR" ]; then
|
||||||
|
echo "Don't run this script from the command line." >&2
|
||||||
|
echo " (if you want, you could supply GIT_DIR then run" >&2
|
||||||
|
echo " $0 <ref> <oldrev> <newrev>)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then
|
||||||
|
echo "usage: $0 <ref> <oldrev> <newrev>" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Config
|
||||||
|
allowunannotated=$(git config --bool hooks.allowunannotated)
|
||||||
|
allowdeletebranch=$(git config --bool hooks.allowdeletebranch)
|
||||||
|
denycreatebranch=$(git config --bool hooks.denycreatebranch)
|
||||||
|
allowdeletetag=$(git config --bool hooks.allowdeletetag)
|
||||||
|
allowmodifytag=$(git config --bool hooks.allowmodifytag)
|
||||||
|
|
||||||
|
# check for no description
|
||||||
|
projectdesc=$(sed -e '1q' "$GIT_DIR/description")
|
||||||
|
case "$projectdesc" in
|
||||||
|
"Unnamed repository"* | "")
|
||||||
|
echo "*** Project description file hasn't been set" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# --- Check types
|
||||||
|
# if $newrev is 0000...0000, it's a commit to delete a ref.
|
||||||
|
zero="0000000000000000000000000000000000000000"
|
||||||
|
if [ "$newrev" = "$zero" ]; then
|
||||||
|
newrev_type=delete
|
||||||
|
else
|
||||||
|
newrev_type=$(git cat-file -t $newrev)
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$refname","$newrev_type" in
|
||||||
|
refs/tags/*,commit)
|
||||||
|
# un-annotated tag
|
||||||
|
short_refname=${refname##refs/tags/}
|
||||||
|
if [ "$allowunannotated" != "true" ]; then
|
||||||
|
echo "*** The un-annotated tag, $short_refname, is not allowed in this repository" >&2
|
||||||
|
echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
refs/tags/*,delete)
|
||||||
|
# delete tag
|
||||||
|
if [ "$allowdeletetag" != "true" ]; then
|
||||||
|
echo "*** Deleting a tag is not allowed in this repository" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
refs/tags/*,tag)
|
||||||
|
# annotated tag
|
||||||
|
if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1
|
||||||
|
then
|
||||||
|
echo "*** Tag '$refname' already exists." >&2
|
||||||
|
echo "*** Modifying a tag is not allowed in this repository." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
refs/heads/*,commit)
|
||||||
|
# branch
|
||||||
|
if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then
|
||||||
|
echo "*** Creating a branch is not allowed in this repository" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
refs/heads/*,delete)
|
||||||
|
# delete branch
|
||||||
|
if [ "$allowdeletebranch" != "true" ]; then
|
||||||
|
echo "*** Deleting a branch is not allowed in this repository" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
refs/remotes/*,commit)
|
||||||
|
# tracking branch
|
||||||
|
;;
|
||||||
|
refs/remotes/*,delete)
|
||||||
|
# delete tracking branch
|
||||||
|
if [ "$allowdeletebranch" != "true" ]; then
|
||||||
|
echo "*** Deleting a tracking branch is not allowed in this repository" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
# Anything else (is there anything else?)
|
||||||
|
echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# --- Finished
|
||||||
|
exit 0
|
|
@ -0,0 +1,6 @@
|
||||||
|
# git ls-files --others --exclude-from=.git/info/exclude
|
||||||
|
# Lines that start with '#' are comments.
|
||||||
|
# For a project mostly in C, the following would be a good set of
|
||||||
|
# exclude patterns (uncomment them if you want to use them):
|
||||||
|
# *.[oa]
|
||||||
|
# *~
|
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,3 @@
|
||||||
|
x•ÍM
|
||||||
|
à @á®=ÅìÁñgœ<67>Rz£Æ5B0÷o®Ðíƒ<C3AD>—Fïmúð˜g)@(†eCâš )‹c‘Í$Y)ÊEÆ$FÅkîã„zÅ£x1“eã>µÇö]Òèo@g½g
|
||||||
|
<EFBFBD>à©Yku×û5ËJµ£Mõíæ/0
|
|
@ -0,0 +1 @@
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
e1022324d23146d29075a3e7c6f637cb67f091b5
|
|
@ -0,0 +1,9 @@
|
||||||
|
#! /usr/bin/env ruby
|
||||||
|
$LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/lib')
|
||||||
|
require 'lib/git_http'
|
||||||
|
config = {
|
||||||
|
:project_root => "/opt",
|
||||||
|
:upload_pack => true,
|
||||||
|
:receive_pack => false,
|
||||||
|
}
|
||||||
|
Rack::Handler::FastCGI.run(GitHttp::App.new(config))
|
|
@ -0,0 +1,20 @@
|
||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
require File.expand_path('../lib/grack/version', __FILE__)
|
||||||
|
|
||||||
|
Gem::Specification.new do |gem|
|
||||||
|
gem.authors = ["Scott Chacon"]
|
||||||
|
gem.email = ["schacon@gmail.com"]
|
||||||
|
gem.description = %q{Ruby/Rack Git Smart-HTTP Server Handler}
|
||||||
|
gem.summary = %q{Ruby/Rack Git Smart-HTTP Server Handler}
|
||||||
|
gem.homepage = "https://github.com/gitlabhq/grack"
|
||||||
|
|
||||||
|
gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
||||||
|
gem.files = `git ls-files`.split("\n")
|
||||||
|
gem.test_files = `git ls-files -- tests/*`.split("\n")
|
||||||
|
gem.name = "grack"
|
||||||
|
gem.require_paths = ["lib"]
|
||||||
|
gem.version = Grack::VERSION
|
||||||
|
|
||||||
|
gem.add_dependency("rack", "~> 1.4.5")
|
||||||
|
gem.add_development_dependency("mocha", "~> 0.11")
|
||||||
|
end
|
|
@ -0,0 +1,60 @@
|
||||||
|
Installation
|
||||||
|
========================
|
||||||
|
|
||||||
|
** This documentation is not finished yet. I haven't tested all of
|
||||||
|
these and it's obviously incomplete - these are currently just notes.
|
||||||
|
|
||||||
|
FastCGI
|
||||||
|
---------------------------------------
|
||||||
|
Here is an example config from lighttpd server:
|
||||||
|
----
|
||||||
|
# main fastcgi entry
|
||||||
|
$HTTP["url"] =~ "^/myapp/.+$" {
|
||||||
|
fastcgi.server = ( "/myapp" =>
|
||||||
|
( "localhost" =>
|
||||||
|
( "bin-path" => "/var/www/localhost/cgi-bin/dispatch.fcgi",
|
||||||
|
"docroot" => "/var/www/localhost/htdocs/myapp",
|
||||||
|
"host" => "127.0.0.1",
|
||||||
|
"port" => 1026,
|
||||||
|
"check-local" => "disable"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} # HTTP[url]
|
||||||
|
----
|
||||||
|
You can use the examples/dispatch.fcgi file as your dispatcher.
|
||||||
|
|
||||||
|
(Example Apache setup?)
|
||||||
|
|
||||||
|
Installing in a Java application server
|
||||||
|
---------------------------------------
|
||||||
|
# install Warbler
|
||||||
|
$ sudo gem install warbler
|
||||||
|
$ cd gitsmart
|
||||||
|
$ (edit config.ru)
|
||||||
|
$ warble
|
||||||
|
$ cp gitsmart.war /path/to/java/autodeploy/dir
|
||||||
|
|
||||||
|
Unicorn
|
||||||
|
---------------------------------------
|
||||||
|
With Unicorn (http://unicorn.bogomips.org/) you can just run 'unicorn'
|
||||||
|
in the directory with the config.ru file.
|
||||||
|
|
||||||
|
Thin
|
||||||
|
---------------------------------------
|
||||||
|
thin.yml
|
||||||
|
---
|
||||||
|
pid: /home/deploy/myapp/server/thin.pid
|
||||||
|
log: /home/deploy/myapp/logs/thin.log
|
||||||
|
timeout: 30
|
||||||
|
port: 7654
|
||||||
|
max_conns: 1024
|
||||||
|
chdir: /home/deploy/myapp/site_files
|
||||||
|
rackup: /home/deploy/myapp/server/config.ru
|
||||||
|
max_persistent_conns: 512
|
||||||
|
environment: production
|
||||||
|
address: 127.0.0.1
|
||||||
|
servers: 1
|
||||||
|
daemonize: true
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
require "grack/bundle"
|
||||||
|
|
||||||
|
module Grack
|
||||||
|
|
||||||
|
end
|
|
@ -0,0 +1,37 @@
|
||||||
|
require 'rack/auth/basic'
|
||||||
|
require 'rack/auth/abstract/handler'
|
||||||
|
require 'rack/auth/abstract/request'
|
||||||
|
|
||||||
|
module Grack
|
||||||
|
class Auth < Rack::Auth::Basic
|
||||||
|
def call(env)
|
||||||
|
@env = env
|
||||||
|
@request = Rack::Request.new(env)
|
||||||
|
@auth = Request.new(env)
|
||||||
|
|
||||||
|
if not @auth.provided?
|
||||||
|
unauthorized
|
||||||
|
elsif not @auth.basic?
|
||||||
|
bad_request
|
||||||
|
else
|
||||||
|
result = if (access = valid?(@auth) and access == true)
|
||||||
|
@env['REMOTE_USER'] = @auth.username
|
||||||
|
@app.call(env)
|
||||||
|
else
|
||||||
|
if access == '404'
|
||||||
|
render_not_found
|
||||||
|
elsif access == '403'
|
||||||
|
render_no_access
|
||||||
|
else
|
||||||
|
unauthorized
|
||||||
|
end
|
||||||
|
end
|
||||||
|
result
|
||||||
|
end
|
||||||
|
end# method call
|
||||||
|
|
||||||
|
# def valid?
|
||||||
|
# false
|
||||||
|
# end
|
||||||
|
end# class Auth
|
||||||
|
end# module Grack
|
|
@ -0,0 +1,19 @@
|
||||||
|
require 'rack/builder'
|
||||||
|
require 'grack/auth'
|
||||||
|
require 'grack/server'
|
||||||
|
|
||||||
|
module Grack
|
||||||
|
module Bundle
|
||||||
|
extend self
|
||||||
|
|
||||||
|
def new(config)
|
||||||
|
Rack::Builder.new do
|
||||||
|
use Grack::Auth do |username, password|
|
||||||
|
yield(username, password)
|
||||||
|
end
|
||||||
|
run Grack::Server.new(config)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,72 @@
|
||||||
|
module Grack
|
||||||
|
class Git
|
||||||
|
attr_reader :repo
|
||||||
|
|
||||||
|
def initialize(git_path, repo_path)
|
||||||
|
@git_path = git_path
|
||||||
|
@repo = repo_path
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_server_info
|
||||||
|
execute(%W(update-server-info))
|
||||||
|
end
|
||||||
|
|
||||||
|
def command(cmd)
|
||||||
|
[@git_path || 'git'] + cmd
|
||||||
|
end
|
||||||
|
|
||||||
|
def capture(cmd)
|
||||||
|
# _Not_ the same as `IO.popen(...).read`
|
||||||
|
# By using a block we tell IO.popen to close (wait for) the child process
|
||||||
|
# after we are done reading its output.
|
||||||
|
IO.popen(popen_env, cmd, popen_options) { |p| p.read }
|
||||||
|
end
|
||||||
|
|
||||||
|
def execute(cmd)
|
||||||
|
cmd = command(cmd)
|
||||||
|
if block_given?
|
||||||
|
IO.popen(popen_env, cmd, File::RDWR, popen_options) do |pipe|
|
||||||
|
yield(pipe)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
capture(cmd).chomp
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def popen_options
|
||||||
|
{ chdir: repo, unsetenv_others: true }
|
||||||
|
end
|
||||||
|
|
||||||
|
def popen_env
|
||||||
|
{ 'PATH' => ENV['PATH'], 'GL_ID' => ENV['GL_ID'] }
|
||||||
|
end
|
||||||
|
|
||||||
|
def config_setting(service_name)
|
||||||
|
service_name = service_name.gsub('-', '')
|
||||||
|
setting = config("http.#{service_name}")
|
||||||
|
|
||||||
|
if service_name == 'uploadpack'
|
||||||
|
setting != 'false'
|
||||||
|
else
|
||||||
|
setting == 'true'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def config(config_name)
|
||||||
|
execute(%W(config #{config_name}))
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid_repo?
|
||||||
|
return false unless File.exists?(repo) && File.realpath(repo) == repo
|
||||||
|
|
||||||
|
match = execute(%W(rev-parse --git-dir)).match(/\.$|\.git$/)
|
||||||
|
|
||||||
|
if match.to_s == '.git'
|
||||||
|
# Since the parent could be a git repo, we want to make sure the actual repo contains a git dir.
|
||||||
|
return false unless Dir.entries(repo).include?('.git')
|
||||||
|
end
|
||||||
|
|
||||||
|
match
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,308 @@
|
||||||
|
require 'zlib'
|
||||||
|
require 'rack/request'
|
||||||
|
require 'rack/response'
|
||||||
|
require 'rack/utils'
|
||||||
|
require 'time'
|
||||||
|
|
||||||
|
require 'grack/git'
|
||||||
|
|
||||||
|
module Grack
|
||||||
|
class Server
|
||||||
|
attr_reader :git
|
||||||
|
|
||||||
|
SERVICES = [
|
||||||
|
["POST", 'service_rpc', "(.*?)/git-upload-pack$", 'upload-pack'],
|
||||||
|
["POST", 'service_rpc', "(.*?)/git-receive-pack$", 'receive-pack'],
|
||||||
|
|
||||||
|
["GET", 'get_info_refs', "(.*?)/info/refs$"],
|
||||||
|
["GET", 'get_text_file', "(.*?)/HEAD$"],
|
||||||
|
["GET", 'get_text_file', "(.*?)/objects/info/alternates$"],
|
||||||
|
["GET", 'get_text_file', "(.*?)/objects/info/http-alternates$"],
|
||||||
|
["GET", 'get_info_packs', "(.*?)/objects/info/packs$"],
|
||||||
|
["GET", 'get_text_file', "(.*?)/objects/info/[^/]*$"],
|
||||||
|
["GET", 'get_loose_object', "(.*?)/objects/[0-9a-f]{2}/[0-9a-f]{38}$"],
|
||||||
|
["GET", 'get_pack_file', "(.*?)/objects/pack/pack-[0-9a-f]{40}\\.pack$"],
|
||||||
|
["GET", 'get_idx_file', "(.*?)/objects/pack/pack-[0-9a-f]{40}\\.idx$"],
|
||||||
|
]
|
||||||
|
|
||||||
|
def initialize(config = false)
|
||||||
|
set_config(config)
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_config(config)
|
||||||
|
@config = config || {}
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_config_setting(key, value)
|
||||||
|
@config[key] = value
|
||||||
|
end
|
||||||
|
|
||||||
|
def call(env)
|
||||||
|
dup._call(env)
|
||||||
|
end
|
||||||
|
|
||||||
|
def _call(env)
|
||||||
|
@env = env
|
||||||
|
@req = Rack::Request.new(env)
|
||||||
|
|
||||||
|
cmd, path, @reqfile, @rpc = match_routing
|
||||||
|
|
||||||
|
return render_method_not_allowed if cmd == 'not_allowed'
|
||||||
|
return render_not_found unless cmd
|
||||||
|
|
||||||
|
@git = get_git(path)
|
||||||
|
return render_not_found unless git.valid_repo?
|
||||||
|
|
||||||
|
self.method(cmd).call
|
||||||
|
end
|
||||||
|
|
||||||
|
# ---------------------------------
|
||||||
|
# actual command handling functions
|
||||||
|
# ---------------------------------
|
||||||
|
|
||||||
|
# Uses chunked (streaming) transfer, otherwise response
|
||||||
|
# blocks to calculate Content-Length header
|
||||||
|
# http://en.wikipedia.org/wiki/Chunked_transfer_encoding
|
||||||
|
|
||||||
|
CRLF = "\r\n"
|
||||||
|
|
||||||
|
def service_rpc
|
||||||
|
return render_no_access unless has_access?(@rpc, true)
|
||||||
|
|
||||||
|
input = read_body
|
||||||
|
|
||||||
|
@res = Rack::Response.new
|
||||||
|
@res.status = 200
|
||||||
|
@res["Content-Type"] = "application/x-git-%s-result" % @rpc
|
||||||
|
@res["Transfer-Encoding"] = "chunked"
|
||||||
|
@res["Cache-Control"] = "no-cache"
|
||||||
|
|
||||||
|
@res.finish do
|
||||||
|
git.execute([@rpc, '--stateless-rpc', git.repo]) do |pipe|
|
||||||
|
pipe.write(input)
|
||||||
|
pipe.close_write
|
||||||
|
|
||||||
|
while block = pipe.read(8192) # 8KB at a time
|
||||||
|
@res.write encode_chunk(block) # stream it to the client
|
||||||
|
end
|
||||||
|
|
||||||
|
@res.write terminating_chunk
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def encode_chunk(chunk)
|
||||||
|
size_in_hex = chunk.size.to_s(16)
|
||||||
|
[size_in_hex, CRLF, chunk, CRLF].join
|
||||||
|
end
|
||||||
|
|
||||||
|
def terminating_chunk
|
||||||
|
[0, CRLF, CRLF].join
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_info_refs
|
||||||
|
service_name = get_service_type
|
||||||
|
return dumb_info_refs unless has_access?(service_name)
|
||||||
|
|
||||||
|
refs = git.execute([service_name, '--stateless-rpc', '--advertise-refs', git.repo])
|
||||||
|
|
||||||
|
@res = Rack::Response.new
|
||||||
|
@res.status = 200
|
||||||
|
@res["Content-Type"] = "application/x-git-%s-advertisement" % service_name
|
||||||
|
hdr_nocache
|
||||||
|
|
||||||
|
@res.write(pkt_write("# service=git-#{service_name}\n"))
|
||||||
|
@res.write(pkt_flush)
|
||||||
|
@res.write(refs)
|
||||||
|
|
||||||
|
@res.finish
|
||||||
|
end
|
||||||
|
|
||||||
|
def dumb_info_refs
|
||||||
|
git.update_server_info
|
||||||
|
send_file(@reqfile, "text/plain; charset=utf-8") do
|
||||||
|
hdr_nocache
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_info_packs
|
||||||
|
# objects/info/packs
|
||||||
|
send_file(@reqfile, "text/plain; charset=utf-8") do
|
||||||
|
hdr_nocache
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_loose_object
|
||||||
|
send_file(@reqfile, "application/x-git-loose-object") do
|
||||||
|
hdr_cache_forever
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_pack_file
|
||||||
|
send_file(@reqfile, "application/x-git-packed-objects") do
|
||||||
|
hdr_cache_forever
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_idx_file
|
||||||
|
send_file(@reqfile, "application/x-git-packed-objects-toc") do
|
||||||
|
hdr_cache_forever
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_text_file
|
||||||
|
send_file(@reqfile, "text/plain") do
|
||||||
|
hdr_nocache
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# ------------------------
|
||||||
|
# logic helping functions
|
||||||
|
# ------------------------
|
||||||
|
|
||||||
|
# some of this borrowed from the Rack::File implementation
|
||||||
|
def send_file(reqfile, content_type)
|
||||||
|
reqfile = File.join(git.repo, reqfile)
|
||||||
|
return render_not_found unless File.exists?(reqfile)
|
||||||
|
|
||||||
|
return render_not_found unless reqfile == File.realpath(reqfile)
|
||||||
|
|
||||||
|
# reqfile looks legit: no path traversal, no leading '|'
|
||||||
|
|
||||||
|
@res = Rack::Response.new
|
||||||
|
@res.status = 200
|
||||||
|
@res["Content-Type"] = content_type
|
||||||
|
@res["Last-Modified"] = File.mtime(reqfile).httpdate
|
||||||
|
|
||||||
|
yield
|
||||||
|
|
||||||
|
if size = File.size?(reqfile)
|
||||||
|
@res["Content-Length"] = size.to_s
|
||||||
|
@res.finish do
|
||||||
|
File.open(reqfile, "rb") do |file|
|
||||||
|
while part = file.read(8192)
|
||||||
|
@res.write part
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
body = [File.read(reqfile)]
|
||||||
|
size = Rack::Utils.bytesize(body.first)
|
||||||
|
@res["Content-Length"] = size
|
||||||
|
@res.write body
|
||||||
|
@res.finish
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_git(path)
|
||||||
|
root = @config[:project_root] || Dir.pwd
|
||||||
|
path = File.join(root, path)
|
||||||
|
Grack::Git.new(@config[:git_path], path)
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_service_type
|
||||||
|
service_type = @req.params['service']
|
||||||
|
return false unless service_type
|
||||||
|
return false if service_type[0, 4] != 'git-'
|
||||||
|
service_type.gsub('git-', '')
|
||||||
|
end
|
||||||
|
|
||||||
|
def match_routing
|
||||||
|
cmd = nil
|
||||||
|
path = nil
|
||||||
|
|
||||||
|
SERVICES.each do |method, handler, match, rpc|
|
||||||
|
next unless m = Regexp.new(match).match(@req.path_info)
|
||||||
|
|
||||||
|
return ['not_allowed'] unless method == @req.request_method
|
||||||
|
|
||||||
|
cmd = handler
|
||||||
|
path = m[1]
|
||||||
|
file = @req.path_info.sub(path + '/', '')
|
||||||
|
|
||||||
|
return [cmd, path, file, rpc]
|
||||||
|
end
|
||||||
|
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def has_access?(rpc, check_content_type = false)
|
||||||
|
if check_content_type
|
||||||
|
conten_type = "application/x-git-%s-request" % rpc
|
||||||
|
return false unless @req.content_type == conten_type
|
||||||
|
end
|
||||||
|
|
||||||
|
return false unless ['upload-pack', 'receive-pack'].include?(rpc)
|
||||||
|
|
||||||
|
if rpc == 'receive-pack'
|
||||||
|
return @config[:receive_pack] if @config.include?(:receive_pack)
|
||||||
|
end
|
||||||
|
|
||||||
|
if rpc == 'upload-pack'
|
||||||
|
return @config[:upload_pack] if @config.include?(:upload_pack)
|
||||||
|
end
|
||||||
|
|
||||||
|
git.config_setting(rpc)
|
||||||
|
end
|
||||||
|
|
||||||
|
def read_body
|
||||||
|
if @env["HTTP_CONTENT_ENCODING"] =~ /gzip/
|
||||||
|
Zlib::GzipReader.new(@req.body).read
|
||||||
|
else
|
||||||
|
@req.body.read
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# --------------------------------------
|
||||||
|
# HTTP error response handling functions
|
||||||
|
# --------------------------------------
|
||||||
|
|
||||||
|
PLAIN_TYPE = { "Content-Type" => "text/plain" }
|
||||||
|
|
||||||
|
def render_method_not_allowed
|
||||||
|
if @env['SERVER_PROTOCOL'] == "HTTP/1.1"
|
||||||
|
[405, PLAIN_TYPE, ["Method Not Allowed"]]
|
||||||
|
else
|
||||||
|
[400, PLAIN_TYPE, ["Bad Request"]]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def render_not_found
|
||||||
|
[404, PLAIN_TYPE, ["Not Found"]]
|
||||||
|
end
|
||||||
|
|
||||||
|
def render_no_access
|
||||||
|
[403, PLAIN_TYPE, ["Forbidden"]]
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------
|
||||||
|
# packet-line handling functions
|
||||||
|
# ------------------------------
|
||||||
|
|
||||||
|
def pkt_flush
|
||||||
|
'0000'
|
||||||
|
end
|
||||||
|
|
||||||
|
def pkt_write(str)
|
||||||
|
(str.size + 4).to_s(16).rjust(4, '0') + str
|
||||||
|
end
|
||||||
|
|
||||||
|
# ------------------------
|
||||||
|
# header writing functions
|
||||||
|
# ------------------------
|
||||||
|
|
||||||
|
def hdr_nocache
|
||||||
|
@res["Expires"] = "Fri, 01 Jan 1980 00:00:00 GMT"
|
||||||
|
@res["Pragma"] = "no-cache"
|
||||||
|
@res["Cache-Control"] = "no-cache, max-age=0, must-revalidate"
|
||||||
|
end
|
||||||
|
|
||||||
|
def hdr_cache_forever
|
||||||
|
now = Time.now().to_i
|
||||||
|
@res["Date"] = now.to_s
|
||||||
|
@res["Expires"] = (now + 31536000).to_s;
|
||||||
|
@res["Cache-Control"] = "public, max-age=31536000";
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,3 @@
|
||||||
|
module Grack
|
||||||
|
VERSION = "2.0.2"
|
||||||
|
end
|
|
@ -0,0 +1,264 @@
|
||||||
|
require 'rack'
|
||||||
|
require 'rack/test'
|
||||||
|
require 'test/unit'
|
||||||
|
require 'mocha'
|
||||||
|
require 'digest/sha1'
|
||||||
|
|
||||||
|
require_relative '../lib/grack/server.rb'
|
||||||
|
require_relative '../lib/grack/git.rb'
|
||||||
|
require 'pp'
|
||||||
|
|
||||||
|
class GitHttpTest < Test::Unit::TestCase
|
||||||
|
include Rack::Test::Methods
|
||||||
|
|
||||||
|
def example
|
||||||
|
File.expand_path(File.dirname(__FILE__))
|
||||||
|
end
|
||||||
|
|
||||||
|
def app
|
||||||
|
config = {
|
||||||
|
:project_root => example,
|
||||||
|
:upload_pack => true,
|
||||||
|
:receive_pack => true,
|
||||||
|
}
|
||||||
|
Grack::Server.new(config)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_upload_pack_advertisement
|
||||||
|
get "/example/info/refs?service=git-upload-pack"
|
||||||
|
assert_equal 200, r.status
|
||||||
|
assert_equal "application/x-git-upload-pack-advertisement", r.headers["Content-Type"]
|
||||||
|
assert_equal "001e# service=git-upload-pack", r.body.split("\n").first
|
||||||
|
assert_match 'multi_ack_detailed', r.body
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_no_access_wrong_content_type_up
|
||||||
|
post "/example/git-upload-pack"
|
||||||
|
assert_equal 403, r.status
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_no_access_wrong_content_type_rp
|
||||||
|
post "/example/git-receive-pack"
|
||||||
|
assert_equal 403, r.status
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_no_access_wrong_method_rcp
|
||||||
|
get "/example/git-upload-pack"
|
||||||
|
assert_equal 400, r.status
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_no_access_wrong_command_rcp
|
||||||
|
post "/example/git-upload-packfile"
|
||||||
|
assert_equal 404, r.status
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_no_access_wrong_path_rcp
|
||||||
|
Grack::Git.any_instance.stubs(:valid_repo?).returns(false)
|
||||||
|
post "/example-wrong/git-upload-pack"
|
||||||
|
assert_equal 404, r.status
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_upload_pack_rpc
|
||||||
|
Grack::Git.any_instance.stubs(:valid_repo?).returns(true)
|
||||||
|
IO.stubs(:popen).returns(MockProcess.new)
|
||||||
|
post "/example/git-upload-pack", {}, {"CONTENT_TYPE" => "application/x-git-upload-pack-request"}
|
||||||
|
assert_equal 200, r.status
|
||||||
|
assert_equal "application/x-git-upload-pack-result", r.headers["Content-Type"]
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_receive_pack_advertisement
|
||||||
|
get "/example/info/refs?service=git-receive-pack"
|
||||||
|
assert_equal 200, r.status
|
||||||
|
assert_equal "application/x-git-receive-pack-advertisement", r.headers["Content-Type"]
|
||||||
|
assert_equal "001f# service=git-receive-pack", r.body.split("\n").first
|
||||||
|
assert_match 'report-status', r.body
|
||||||
|
assert_match 'delete-refs', r.body
|
||||||
|
assert_match 'ofs-delta', r.body
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_recieve_pack_rpc
|
||||||
|
Grack::Git.any_instance.stubs(:valid_repo?).returns(true)
|
||||||
|
IO.stubs(:popen).yields(MockProcess.new)
|
||||||
|
post "/example/git-receive-pack", {}, {"CONTENT_TYPE" => "application/x-git-receive-pack-request"}
|
||||||
|
assert_equal 200, r.status
|
||||||
|
assert_equal "application/x-git-receive-pack-result", r.headers["Content-Type"]
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_info_refs_dumb
|
||||||
|
get "/example/.git/info/refs"
|
||||||
|
assert_equal 200, r.status
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_info_packs
|
||||||
|
get "/example/.git/objects/info/packs"
|
||||||
|
assert_equal 200, r.status
|
||||||
|
assert_match /P pack-(.*?).pack/, r.body
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_loose_objects
|
||||||
|
path, content = write_test_objects
|
||||||
|
get "/example/.git/objects/#{path}"
|
||||||
|
assert_equal 200, r.status
|
||||||
|
assert_equal content, r.body
|
||||||
|
remove_test_objects
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_pack_file
|
||||||
|
path, content = write_test_objects
|
||||||
|
get "/example/.git/objects/pack/pack-#{content}.pack"
|
||||||
|
assert_equal 200, r.status
|
||||||
|
assert_equal content, r.body
|
||||||
|
remove_test_objects
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_index_file
|
||||||
|
path, content = write_test_objects
|
||||||
|
get "/example/.git/objects/pack/pack-#{content}.idx"
|
||||||
|
assert_equal 200, r.status
|
||||||
|
assert_equal content, r.body
|
||||||
|
remove_test_objects
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_text_file
|
||||||
|
get "/example/.git/HEAD"
|
||||||
|
assert_equal 200, r.status
|
||||||
|
assert_equal 41, r.body.size # submodules have detached head
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_no_size_avail
|
||||||
|
File.stubs('size?').returns(false)
|
||||||
|
get "/example/.git/HEAD"
|
||||||
|
assert_equal 200, r.status
|
||||||
|
assert_equal 46, r.body.size # submodules have detached head
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_config_upload_pack_off
|
||||||
|
a1 = app
|
||||||
|
a1.set_config_setting(:upload_pack, false)
|
||||||
|
session = Rack::Test::Session.new(a1)
|
||||||
|
session.get "/example/info/refs?service=git-upload-pack"
|
||||||
|
assert_equal 404, session.last_response.status
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_config_receive_pack_off
|
||||||
|
a1 = app
|
||||||
|
a1.set_config_setting(:receive_pack, false)
|
||||||
|
session = Rack::Test::Session.new(a1)
|
||||||
|
session.get "/example/info/refs?service=git-receive-pack"
|
||||||
|
assert_equal 404, session.last_response.status
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_config_bad_service
|
||||||
|
get "/example/info/refs?service=git-receive-packfile"
|
||||||
|
assert_equal 404, r.status
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_git_config_receive_pack
|
||||||
|
app1 = Grack::Server.new({:project_root => example})
|
||||||
|
app1.instance_variable_set(:@git, Grack::Git.new('git', example ))
|
||||||
|
session = Rack::Test::Session.new(app1)
|
||||||
|
git = Grack::Git
|
||||||
|
git.any_instance.stubs(:config).with('http.receivepack').returns('')
|
||||||
|
session.get "/example/info/refs?service=git-receive-pack"
|
||||||
|
assert_equal 404, session.last_response.status
|
||||||
|
|
||||||
|
git.any_instance.stubs(:config).with('http.receivepack').returns('true')
|
||||||
|
session.get "/example/info/refs?service=git-receive-pack"
|
||||||
|
assert_equal 200, session.last_response.status
|
||||||
|
|
||||||
|
git.any_instance.stubs(:config).with('http.receivepack').returns('false')
|
||||||
|
session.get "/example/info/refs?service=git-receive-pack"
|
||||||
|
assert_equal 404, session.last_response.status
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_git_config_upload_pack
|
||||||
|
app1 = Grack::Server.new({:project_root => example})
|
||||||
|
# app1.instance_variable_set(:@git, Grack::Git.new('git', example ))
|
||||||
|
session = Rack::Test::Session.new(app1)
|
||||||
|
git = Grack::Git
|
||||||
|
git.any_instance.stubs(:config).with('http.uploadpack').returns('')
|
||||||
|
session.get "/example/info/refs?service=git-upload-pack"
|
||||||
|
assert_equal 200, session.last_response.status
|
||||||
|
|
||||||
|
git.any_instance.stubs(:config).with('http.uploadpack').returns('true')
|
||||||
|
session.get "/example/info/refs?service=git-upload-pack"
|
||||||
|
assert_equal 200, session.last_response.status
|
||||||
|
|
||||||
|
git.any_instance.stubs(:config).with('http.uploadpack').returns('false')
|
||||||
|
session.get "/example/info/refs?service=git-upload-pack"
|
||||||
|
assert_equal 404, session.last_response.status
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_send_file
|
||||||
|
app1 = app
|
||||||
|
app1.instance_variable_set(:@git, Grack::Git.new('git', Dir.pwd))
|
||||||
|
# Reject path traversal
|
||||||
|
assert_equal 404, app1.send_file('tests/../tests', 'text/plain').first
|
||||||
|
# Reject paths starting with '|', avoid File.read('|touch /tmp/pawned; ls /tmp')
|
||||||
|
assert_equal 404, app1.send_file('|tests', 'text/plain').first
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_get_git
|
||||||
|
# Guard against non-existent directories
|
||||||
|
git1 = Grack::Git.new('git', 'foobar')
|
||||||
|
assert_equal false, git1.valid_repo?
|
||||||
|
# Guard against path traversal
|
||||||
|
git2 = Grack::Git.new('git', '/../tests')
|
||||||
|
assert_equal false, git2.valid_repo?
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def r
|
||||||
|
last_response
|
||||||
|
end
|
||||||
|
|
||||||
|
def write_test_objects
|
||||||
|
content = Digest::SHA1.hexdigest('gitrocks')
|
||||||
|
base = File.join(File.expand_path(File.dirname(__FILE__)), 'example', '.git', 'objects')
|
||||||
|
obj = File.join(base, '20')
|
||||||
|
Dir.mkdir(obj) rescue nil
|
||||||
|
file = File.join(obj, content[0, 38])
|
||||||
|
File.open(file, 'w') { |f| f.write(content) }
|
||||||
|
pack = File.join(base, 'pack', "pack-#{content}.pack")
|
||||||
|
File.open(pack, 'w') { |f| f.write(content) }
|
||||||
|
idx = File.join(base, 'pack', "pack-#{content}.idx")
|
||||||
|
File.open(idx, 'w') { |f| f.write(content) }
|
||||||
|
["20/#{content[0,38]}", content]
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_test_objects
|
||||||
|
content = Digest::SHA1.hexdigest('gitrocks')
|
||||||
|
base = File.join(File.expand_path(File.dirname(__FILE__)), 'example', '.git', 'objects')
|
||||||
|
obj = File.join(base, '20')
|
||||||
|
file = File.join(obj, content[0, 38])
|
||||||
|
pack = File.join(base, 'pack', "pack-#{content}.pack")
|
||||||
|
idx = File.join(base, 'pack', "pack-#{content}.idx")
|
||||||
|
File.unlink(file)
|
||||||
|
File.unlink(pack)
|
||||||
|
File.unlink(idx)
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
class MockProcess
|
||||||
|
def initialize
|
||||||
|
@counter = 0
|
||||||
|
end
|
||||||
|
|
||||||
|
def write(data)
|
||||||
|
end
|
||||||
|
|
||||||
|
def read(data = nil)
|
||||||
|
''
|
||||||
|
end
|
||||||
|
|
||||||
|
def eof?
|
||||||
|
@counter += 1
|
||||||
|
@counter > 1 ? true : false
|
||||||
|
end
|
||||||
|
|
||||||
|
def close_write
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in New Issue