mwolson.org Blog - /Tech

Fri, 21 Sep 2007

A month and a half of git

It's been over a month since I started using git. Time to share some of the more interesting things I've picked up from it.

One surprise was realizing that I actually like having multiple branches share the same working directory. It's nice to be able to do git checkout <name-of-branch> and immediately get either a particular branch, tag, or even commit. One of the reasons for this change of opinion is due to the excellent gitweb interface, which displays these branches and tags in a nice-looking way.

One thing I would like to investigate is just how similar Mercurial is to git when it comes to the use of its SHA1 hashes for verification of a project's integrity. I don't recall reading anything about it in the Mercurial documentation, whereas git notes in its manpage, "[S]ince the SHA1 signature of a commit refers to the SHA1 signatures of the tree it is associated with and the signatures of the parent, a single named commit specifies uniquely a whole set of history, with full contents. You can't later fake any step of the way once you have the name of a commit."

Using multiple locations

I like being able to say git pull shared to get changes from the shared public repo for my projects. To set this up, my per-project .git/config file looks something like this:

[core]
	repositoryformatversion = 0
	filemode = true
	bare = false
	logallrefupdates = true
[remote "shared"]
        url = ssh://repo.or.cz/srv/git/muse-el.git
	fetch = +refs/heads/*:refs/remotes/shared/*
[branch "master"]
	remote = shared
	merge = refs/heads/master

If you were to add another location, just copy the last two stanzas and replace "shared" with your name for the location. The idea is to store all references for the new location into its own directory under refs/remotes.

Providing commit email for publicly-hosted git projects

One of the challenges with using Savannah and repo.or.cz for hosting projects is that they have no way (that I know of) to generate email messages when new commits are added. If you have access to a machine with cron (such as HCoop), however, there is a way of dealing with that. I call it the "push-me-pull-you" method, for lack of a less corny name.

The idea is to periodically pull from the public repo (call this the "staging" directory), and push into another repo on the same machine that has an update hook (call this the "target" directory). The staging directory has to be a working directory, but the target can be a bare repo (without a working directory) to save space. To make the target directory, clone from the staging directory with the "-s" option, so that you share objects with the staging directory.

To actually do the emailing of commits, do chmod +x hooks/update in the target directory, and edit the contents to call some sort of email commit script. It's probably possible to use the post-receive hook as well, as in /usr/share/doc/git-core/contrib/hooks/post-receive-email which comes with Debian, but I wanted to write my own.

hooks/update

The hook itself will look like this:

#!/bin/sh
#
# A hook script that sends commit notifications.
# Called by git-receive-pack with arguments: refname sha1-old sha1-new
#
# To enable this hook, make this file executable by "chmod +x update".

# --- 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" ] || [ -z "$oldrev" ] || [ -z "$newrev" ]; then
        echo "Usage: $0 <ref> <oldrev> <newrev>" >&2
        exit 1
fi

# Send email
for commit_id in $(git rev-list ${oldrev}..${newrev} | tac) ; do
    ~mwolson/scripts/email-git erc ${refname} ${commit_id}
done

# --- Finished
exit 0

email-git script

My email-git script is as follows.

#!/bin/bash
#
# Send commit notifications via email for git.

# Parameters
proj="$1"
refname="$2"
commit_id="$3"

# Function to send email message
function sendMail() {
  local from="$1"
  local recipients="$2"
  local commit_id="$3"
  local refname="${4##refs/heads/}"
  local summary=$(git show --pretty=format:%s ${commit_id} | head -n 1)
  git show --pretty=medium ${commit_id} | \
      mail -a "From: ${from}" -s "[commit][${refname}] ${summary}" \
          ${recipients}
}

# Determine where to send the messages
#
# Note: Unobfuscate your email addresses, rather than using "AT"
# in real usage of this script.
case $proj in
    erc)
        from_email=mwolson AT gnu.org
        to_email=erc-commit AT gnu.org
        ;;
    muse)
        from_email=mwolson AT gnu.org
        to_email=muse-el-logs AT gna.org
        ;;
    *)
        to_email=
        ;;
esac

# Send email
[ -n "$to_email" ] && sendMail $from_email $to_email $commit_id $refname

make-git-projects

I have cron set up to call this script every 10 minutes or so. A custom-compiled version of git resides in ~mwolson/bin. The staging directory has subdirectories which are named after each project, and each is a checkout of that project.

#!/bin/bash
# Sync git projects from various shared repos

PATH=~mwolson/bin:"$PATH"

# Place to prepare and mangle files before publishing
DIST_DIR=~mwolson/dist-git

# Process each config file and move to examples directory
for i in $(find $DIST_DIR/staging -maxdepth 1 -mindepth 1 -type d); do

    # Pull the latest changes
    (cd $i && git pull shared >/dev/null 2>&1)

    # Push to another local repo in order to trigger hooks
    (cd $i && git push target >/dev/null 2>&1)
done

This requires something like the following in each of the staging directories in the .git/config file.

[remote "shared"]
        url = http://repo.or.cz/r/muse-el.git
        fetch = +refs/heads/master:refs/remotes/origin/master
[remote "target"]
        url = ~mwolson/dist-git/target/muse.git
        push = master