update-hash 4.54 KB
#!/usr/bin/env python
"""
Suggested installation:
$ cp update-hash .git/hooks/

$ $EDITOR .git/hooks/pre-commit
#!/bin/bash
set -e
touch "$(git rev-parse --git-dir)/hooks/.need-post-commit"

$ $EDITOR .git/hooks/post-commit
#!/bin/bash
set -e
MARKER="$(git rev-parse --git-dir)/hooks/.need-post-commit"
UPDATE_HASH="$(git rev-parse --git-dir)/hooks/update-hash"
if [ -e "$MARKER" -a -x "$UPDATE_HASH" ]; then
  rm "$MARKER"
  if git diff-index --quiet HEAD -- ; then
    # nothing
    true
  else
    git stash save --keep-index --quiet "update-hash pre-commit hook"
    trap "git stash pop" EXIT
  fi
  find "$(git rev-parse --show-toplevel)" -name "buildout.hash.cfg" | while IFS= read -r HASHFILE; do
    "$UPDATE_HASH" "$HASHFILE"
    git add "$HASHFILE"
  done
  git commit --amend --no-verify -C HEAD
fi

BEWARE: rebasing does not trigger this hook, so you have to commit each
change explicitely. Improvements welcome.
"""
import hashlib
import os
import shutil
import sys
import tempfile

# Note: this is an intentionally very restrictive and primitive
# ConfigParser-ish parser.
# buildout.hash.cfg files are ConfigParser-compatible, but they are *not*
# ConfigParser syntax in order to be strictly validated, to prevent misuse
# and allow easy extension (ex: to other hashes).

FILENAME_KEY = 'filename'
HASH_MAP = {
    'md5sum': hashlib.md5,
}

def main():
    for infile_path in sys.argv[1:] or ['buildout.hash.cfg']:
        eol = None
        hash_file_path = None
        hash_name = None
        current_section = None
        infile_dirname = os.path.dirname(infile_path)
        outfile_path = infile_path + '.tmp'
        infile = open(infile_path, 'r')
        outfile_fd = os.open(outfile_path, os.O_EXCL | os.O_CREAT | os.O_WRONLY)
        try:
            outfile = os.fdopen(outfile_fd, 'w')
            write = outfile.write
            nextLine = iter(infile).next
            while True:
                try:
                    line = nextLine()
                except StopIteration:
                    line = None
                if line is None or not line.startswith('#'):
                    if line is None or line.startswith('['):
                        if hash_file_path is not None:
                            current_section.insert(
                                len([x for x in current_section if x.strip()]),
                                '%s = %s%s' % (
                                    hash_name,
                                    HASH_MAP[hash_name](
                                        open(
                                            os.path.join(
                                                infile_dirname,
                                                *hash_file_path.split('/')
                                            )
                                        ).read()
                                    ).hexdigest(),
                                    eol,
                                ),
                            )
                            outfile.writelines(current_section)
                            hash_file_path = hash_name = None
                        if line is None:
                            break
                        hash_file_path, _ = line[1:].split(']', 1)
                        current_section = []
                    elif '=' in line:
                        assert current_section is not None, line
                        name, value = line.split('=', 1)
                        name = name.strip()
                        value = value.strip()
                        if name == FILENAME_KEY:
                            hash_file_path = value
                            current_section.append(line)
                        else:
                            for hash_name in HASH_MAP:
                                if name == hash_name:
                                    break
                            else:
                                raise ValueError('Unknown key: %r' % (name, ))
                            # NOT appending this line, it will be re-generated from
                            # scratch
                            eol = ''.join(x for x in line if x in ('\r', '\n'))
                        continue
                if current_section is None:
                    write(line)
                else:
                    current_section.append(line)
            outfile.close()
            shutil.copymode(infile_path, outfile_path)
            shutil.move(outfile_path, infile_path)
        except Exception:
            os.unlink(outfile_path)
            raise

if __name__ == '__main__':
    main()