git, gitweb, and per-user repos

2014-04-01: fixed a bug in section git-http-backend

There’s a lot of tutorials on setting up gitweb; even one here written by someone from the same organization.

Like with most sysadmin tasks, nothing quite works exactly out of the box as you’d like. Hopefully this will help someone else out there.


requirements

We want to provide anonymous, read-only access to git repository on a site and per-user basis. (Protected, read-only access is another article. Read/write access is probably best done with gitlab or gitolite.)

There are two types of repositories we have:

  1. a organization-wide git repository like https://ant.isi.edu/git/
  2. a per-user git repository like https://ant.isi.edu/git/~calvin/

This allows users to host as many of their own personal git repositories without cluttering the organization’s repository.

getting started

We use Fedora 20 (as of this writing), git 1.8.5.3, and Apache 2.4 (important, some ways of doing things in Apache 2.2 and earlier have been deprecated) with mod_alias and mod_rewrite.

The following documentation is extremely helpful:

installation and testing

If you haven’t installed Apache w/ SSL and git, do so now.

I live dangerously and do all my configuration testing on a production machine. If you feel inclined to do the same, continue reading. Writing rewrite rules can be kind of painful, so I wanted to get the most detail (the rest at warn) in ssl.conf. Change the LogLevel to however you see fit:

LogLevel warn rewrite:trace8

Then to sanity check a configuration file and reload the daemon:

apachectl configtest && service httpd reload

Install gitweb:

yum install gitweb

There are are a few things that gitweb will install.

We’ll use the default locations; adjust for your own setup.

Furthermore, we have 2 locations for repositories:

  1. /nfs/git_public/
  2. /home/$user/public_git/

where $user is some valid user on a shared system.

gitweb

Getting gitweb up and running is relatively simple. Our configuration files will do the following:

  1. make gitweb accessible at https://hostname/git
  2. access gitweb repositories via https://hostname/git/repo.git

Our git.conf for httpd:

Alias /git /var/www/git

# Redirect http:// to https://
RewriteEngine on
RewriteCond %{HTTPS} off
RewriteRule ^/git.*$ https://%{HTTP_HOST}%{REQUEST_URI}

<Directory /var/www/git>
  SSLRequireSSL

  Require all granted

  Options +ExecCGI

  # Run .cgi files using mod_cgi
  AddHandler cgi-script .cgi

  # Use gitweb.cgi for directory listings. This takes
  # care of requests to /git and /git/
  DirectoryIndex gitweb.cgi

  # Set various environment variables, some to pass to
  # gitweb
  SetEnv         GITWEB_CONFIG      /etc/gitweb.conf
  SetEnv         GITWEB_PROJECTROOT /nfs/git_public

  RewriteEngine on

  # PATH_INFO usage for pretty URLs
  # make sure to enable it in gitweb.conf
  #
  #   $feature{'pathinfo'}{'default'} = [1];
  #
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteCond %{REQUEST_FILENAME} !-d
  RewriteRule ^.* /git/gitweb.cgi/$0 [L,PT]
</Directory>

Our gitweb.conf for gitweb:

# Set the path to git projects.  This is an absolute
# filesystem path which will be prepended to the project
# path.
our $projectroot = $ENV{'GITWEB_PROJECTROOT'} || "/nfs/git_public"

# Set the list of git base URLs used for URL to where
# fetch project from, i.e.  # the full URL is
# "$git_base_url/$project". By default this is empty
our @git_base_url_list = qw(https://hostname/git);

our $site_name = "Git Repository";
$my_uri = "/git";
$home_link = "/git";
$home_link_str = "https://hostname/git";

# logo needs to be 72x27
$logo       = "https://hostname/git/static/git-logo.png";
$logo_url   = "https://hostname/";
$favicon    = "https://hostname/git/static/git-favicon.png";
$stylesheet = "https://hostname/git/static/gitweb.css";

$per_request_config = 0;

$feature{'pathinfo'}{'default'}  = [1];
$projects_list_description_width = 140; # like twitter
$omit_owner                      = 1;

Now you should be able to access gitweb via https://hostname/git and repositories at https://hostname/git/repo.git.

git-http-backend

(Most of this section is from git-http-backend(1).)

While we can access gitweb via https://hostname/git/repo.git, some more work is needed to be able to clone the repository via git over HTTP (e.g., git clone https://hostname/git/repo.git).

We’ll send requests that come from git to git-http-backend; the rest will get routed to gitweb. (We don’t handle URLs with git://, but that shouldn’t be too difficult.)

In git.conf, add the following:

ScriptAliasMatch \
  "(?x)^/git/(.*/(HEAD | \
      info/refs | \
      objects/(info/[^/]+ | \
         [0-9a-f]{2}/[0-9a-f]{38} | \
         pack/pack-[0-9a-f]{40}\.(pack|idx)) | \
      git-(upload|receive)-pack))$" \
  /usr/libexec/git-core/git-http-backend/$1

<Directory "/usr/libexec/git-core/">
  SSLRequireSSL

  SetEnv GIT_PROJECT_ROOT /nfs/git_public
  SetEnv GIT_HTTP_EXPORT_ALL

  Options +ExecCGI
  Require all granted
</Directory>

You should now be able to clone repositories via git using the same gitweb URLs.

per-user repositories

gitweb

The section titled “Webserver configuration with multiple projects' root” on the gitweb(1) manual page details how you could use mod_rewrite in Apache to serve gitweb in home directories or different locations. I was never able to get this to work properly on the Apache side, but found an older guide here that adds some Perl code to gitweb.conf to serve user repositories.

I’ve modified the configuration slightly to additionally check if the public_git folder exists for a user.

if ($ENV{"PATH_INFO"} =~ m#^/~([^/]+)#) {
  my $username = $1;

  # check whether the user really exists
  my @pwent = getpwnam($username);

  # check if user has a public_git folder, otherwise fall back
  if (@pwent && -d $pwent[7] . "/public_git") {
      @git_base_url_list = "https://hostname/git/~" . $username;

      # we only serve $HOME/public_git/
      $projectroot = $pwent[7] . "/public_git";

      # Fix up git's idea of the urls :/
      $my_url .= "/~$username";
      $my_uri .= "/~$username";
      $home_link .= "/~$username";
      $path_info =~ s#^/~[^/]+##;

      # fix up some strings
      $home_link_str .= "/~$username";
      $site_name = "$username" . "'s repository";
  }
}

A further improvement (TODO) would be to store symlinks in a directory like /var/www/git/user/$user which points to /home/$user/public_git and set the $projectroot to that symlink.

git-http-backend

To get per-user repositories via git, we set an environment variable GIT_PROJECT_ROOT based on the requested username and pass that to git-http-backend.

In git.conf add the following to the beginning of the file:

(2014-04-01: Fixed a bug where the second group in the regex for Request_URI should be (.*) not (/.*/).)

SetEnvIf Request_URI "(?x)^/git/\~([^/]*)(.*)$" \
  GIT_PROJECT_ROOT=/home/$1/public_git

# this takes care of user directories
ScriptAliasMatch \
  "(?x)^/git/\~([^/]*)(/.*/(HEAD | \
      info/refs | \
      objects/(info/[^/]+ | \
         [0-9a-f]{2}/[0-9a-f]{38} | \
         pack/pack-[0-9a-f]{40}\.(pack|idx)) | \
      git-(upload|receive)-pack))$" \
  /usr/libexec/git-core/git-http-backend/$2

To prevent overwriting of the environment variable, we’ll set GIT_PROJECT_ROOT to a default value if there’s no value:

<Directory "/usr/libexec/git-core/">
  SSLRequireSSL

  # <If></If> requires apache 2.4
  <If "-z %{ENV:GIT_PROJECT_ROOT}">
    SetEnv GIT_PROJECT_ROOT /home/ant/ANT_GIT_PUBLIC
  </If>

  SetEnv GIT_HTTP_EXPORT_ALL
  Options +ExecCGI

  Require all granted
</Directory>

other notes

I have not tested the possible security implications for the above configurations.