Mercurial is a so called DRCS (Distributed Revision Control System). I have been using Subversion for a couple of years, both at work and for my own projects. Now I thought it was about time to try something different.
But first, why do I want to switch from SVN to Mercurial? Basically the most appealing argument for me was the fact, that with Mercurial I am able to work offline with my repository. Besides that, I always had issues with the way SVN was handling tags and branches. Especially merging changes from a branch back into the trunk was always a pain. I did not need to use that functionality often but when I did, I always ended up doing it twice, because I could not remember which way to do it right.
So, in this article I will describe my setup of Mercurial served via FastCGI behind the nginx webserver. The approach is similar to the integration of PHP into nginx. You need the spawn-fcgi tool from the lighttpd distribution. The following steps should work on a recent Debian or Ubuntu distribution.
The first step is to install Mercurial and some necessary libraries to create the fcgi-wrapper for Mercurial:
aptitude install mercurial python-flup
Now you can already start defining your Mercurial repositories. Here are some steps to create a small example repository:
cd /tmp hg init hgtest cd hgtest echo "Hello world." > readme.txt hg add readme.txt hg commit -m "Initial commit"
You now have a Mercurial repository with a single file. The command hg log should show you a single changeset with our commit comment.
Now, let’s start configuring nginx to integrate Mercurial. We will use FastCGI talk connect Mercurial to nginx. User authentication will be done via nginx. It is a good idea to use HTTPS for communication with Mercurial, but we will focus on a standard HTTP setup.
Let’s setup a virtual host for Mercurial. Open /etc/nginx/sites-available/your_domain_name and add the following server definition:
server {
listen 80;
server_name YOUR_MERCURIAL_DOMAIN;
location / {
auth_basic "Secure Login";
auth_basic_user_file /tmp/mercurial_users;
fastcgi_pass 127.0.0.1:9001;
fastcgi_param SCRIPT_FILENAME /tmp$fastcgi_script_name;
fastcgi_param PATH_INFO $uri;
fastcgi_param REMOTE_USER $remote_user;
include fastcgi_params;
}
}The above settings will setup a new virtual host, where all traffic is redirected to the Mercurial FastCGI wrapper. It is important that you forward the PATH_INFO and REMOTE_USER variables. Mercurial will not work correctly without these.
Now reload the nginx configuration:
/etc/init.d/nginx reload
And create the password file:
htpasswd -c /tmp/mercurial_users MYLOGIN
Mercurial uses a central configuration file. In this file we can specify locations for our mercurial repositories. The following file will enable all mercurial repositories found in the /tmp directory. It also changes the theme to gitweb which is a bit clearer than the default theme. Create the file /tmp/hgweb.config with the following contents:
[collections] /tmp = /tmp [web] style = gitweb baseurl =
Mercurial uses a second configuration file for each repository where you may specify details about the repository and security settings like who may push changes into the repository. The configuration should be placed into the file /tmp/hgtest/.hg/hgrc and could look like this:
[web] contact = YOUR NAME description = DESCRIPTION OF PROJECT style = gitweb push_ssl = false allow_archive = bz2 gz zip allow_push = LOGIN_NAME
Now we need to grab the following script from the Mercurial repository and place it into the /tmp directory: http://selenic.com/repo/hg/raw-file/tip/contrib/hgwebdir.fcgi. Now edit this file and replace the line WSGIServer(hgwebdir('hgweb.config')).run() with WSGIServer(hgwebdir('/tmp/hgweb.config')).run().
The last step is to set the correct filesystem rights for your repository:
chown -R www-data.www-data /tmp/hgtest
That’s it. Now we can start the FastCGI process via:
spawn-fcgi -a 127.0.0.1 -p 9001 -u www-data -g www-data -f /tmp/hgwebdir.fcgi -P /var/run/fastcgi-mercurial.pid -C 1
It makes sense to write the above line into /etc/rc.local so that it will start up automatically when you reboot the server.
Further information about configuring your Mercurial server can be found in the Mercurial Wiki.
This looks quite interesting and I am eager to give it a try. However, in terms of security I guess you are also using fail2ban for this
Is it sufficient just do create a jail for nginx or is there more one should consider?
Cheers,
Michael
Yes, I am indeed using fail2ban to secure my repository. I have defined a new filter file
/etc/fail2ban/filter.d/nginx-auth.confto identify failed login attempts in the nginx access log (note: nginx does not log failed attempts to the error.log like Apache):This filter is then called from a custom jail definition:
So far this is working out nice.
I ran into problems where an empty repository list was shown.
To solve this, you have to edit the file /tmp/hgwebdir.fcgi
Replace this line at the bottom:
WSGIServer(hgwebdir('hgweb.config')).run()with a direct reference to the hgweb.config file:
WSGIServer(hgwebdir('/tmp/hgweb.config')).run()Thanks, I forgot to mention this modification in my howto. The posting is now updated.
It’s running!!! Thank you very much!
After running
spawn-fcgi -a 127.0.0.1 -p 9001 -u www-data -g www-data -f /tmp/hgwebdir.fcgi -P /var/run/fastcgi-mercurial.pid -C 1
I get the following
spawn-fcgi: child exited with: 2
Any thoughts?
To answer my own problem.
The executable bit needs to be set on hgwebdir.fcgi
(chmod +x hgwebdir.fcgi)
Otherwise we get:
spawn-fcgi: child exited with: 2
Also (I could be incorrect), but instead of:
chmod -R www-data.www-data /tmp/hgtest
I think it was meant to be:
chown -R www-data.www-data /tmp/hgtest
Thanks for the great post!
You are right. Thanks for pointing out the error!
I updated the posting.
Great how-to !
Thanks !
Thanks for the how-to! Got things up and running. One question: How do you do public access to the repository while restricting pushes? The problem is illustrated in the nginx config below:
server {
listen xx.xxx.xxx.xxx:443;
server_name hg.curtisjewell.name;
server_name hg;
ssl on;
ssl_certificate /.../hg.chained.crt;
ssl_certificate_key /.../hg.key;
access_log /.../hg.log;
keepalive_timeout 70;
location / {
fastcgi_pass 127.0.0.1:xxxxx;
fastcgi_param PATH_INFO $fastcgi_script_name;
fastcgi_param QUERY_STRING $query_string;
fastcgi_param REQUEST_METHOD $request_method;
fastcgi_param CONTENT_TYPE $content_type;
fastcgi_param CONTENT_LENGTH $content_length;
fastcgi_param SERVER_PROTOCOL $server_protocol;
fastcgi_param SERVER_PORT $server_port;
fastcgi_param SERVER_NAME $server_name;
fastcgi_param GATEWAY_INTERFACE CGI/1.1;
fastcgi_param SERVER_SOFTWARE nginx/$nginx_version;
fastcgi_param REMOTE_ADDR $remote_addr;
fastcgi_param REMOTE_PORT $remote_port;
fastcgi_param REMOTE_USER $remote_user;
# The commented lines, when commented, make pushing return a 401 because the user is not passed to hgweb.
# limit_except GET { # do this for all requests but GET requests
auth_basic "Mercurial repositories on curtisjewell.name";
auth_basic_user_file /var/hg/hgusers_basic;
# }
}
}
Mercurial 1.4.3, nginx 0.7.65, by the way.
Hi Curtis, unfortunately I don’t have a solution for this problem. I also tried to get the limit_except approach working, but without success. I don’t need public access to my repository, so I did not investigate further.
Thanks a lot for the fail2ban filter