Ruby Git Server
January 05, 2025
I’ve been toying with a side project's POC and for one the moving pieces, I needed a Git server I could self-host.
One option I considered was using something like Gitea, but I eventually preferred something that was easier to customize and maintain by myself, so I needed a lighter solution.
It turns out that Git itself already includes git-http-backend
, a CGI program that allows to interact with a Git repository through the http[s] protocol.
In my case I wanted to integrate it with my Rails app to handle everything, including the authentication/authorization layer, from the same codebase.
The solution ended being quite straightforward, I just had to create a small Rack app that calls the git-http-backend
program and sends its output to the client:
class GitCGIHandler
def initialize(script, project_root)
@script = script
@project_root = project_root
end
def call(env)
request = ::Rack::Request.new(env)
cgi_env = {
"QUERY_STRING" => request.query_string,
"REQUEST_METHOD" => request.request_method,
"CONTENT_TYPE" => request.content_type,
"CONTENT_LENGTH" => request.content_length.to_s,
"SCRIPT_NAME" => request.script_name,
"PATH_INFO" => request.path_info,
"GIT_PROJECT_ROOT" => @project_root,
"GIT_HTTP_EXPORT_ALL" => "1"
}
output, _, _ = Open3.capture3(cgi_env, @script, stdin_data: request.body)
headers, body = parse_cgi_output(output)
[ 200, headers, [ body ] ]
rescue => error
[ 500, { "Content-Type" => "text/plain" }, [ error.message ] ]
end
private
def parse_cgi_output(output)
headers, body = output.b.split("\r\n\r\n")
headers = headers.split("\r\n").map { |line| line.split(": ") }.to_h
[ headers, body ]
end
end
git_cgi_handler.rb
And then we just need to plug it into our routes config:
mount GitCGIHandler.new("git-http-backend", "/var/data/git"), at: "/git"
config/routes.rb
Then we need to initialize the remote repository:
git init --bare
git config http.receivepack true # this is needed to push the commits from the client to the server
And now we can start using the remote repository:
git clone http://localhost:3000/git/example.git
cd example
git commit --allow-empty -m 'first commit'
git push
And that’s it! Now we can customize it as much as we need/want. For example, if we want to require some sort of authentication/authorization, we can use any of the different available libraries or implement our own. Eg:
mount Rack::Builder.new {
use Rack::Auth::Basic do |username, password|
# validate username and password
false
end
run GitCGIHandler.new("git-http-backend", "/var/data/git")
}, at: "/git"