URI: 
       tcommit hooks, various fixes (inc, inc by) - gitzone - git-based zone management tool for static and dynamic domains
  HTML git clone https://git.parazyd.org/gitzone
   DIR Log
   DIR Files
   DIR Refs
       ---
   DIR commit 43c5984e73ab7974ebe569dc7be69ca57b5ac91e
   DIR parent 4956a8bbf1abe8bf0f6dfef2468f9acb13cb384a
  HTML Author: tg(x) <*@tg-x.net>
       Date:   Thu,  3 Mar 2011 18:15:28 +0100
       
       commit hooks, various fixes (inc, inc by)
       
       Diffstat:
         M README.org                          |      12 ++++++++----
         M bin/gitzone                         |     202 ++++++++++++++++++++-----------
         M bin/gitzone-shell                   |       8 ++------
         A hooks/post-commit                   |       5 +++++
         A hooks/pre-commit                    |       3 +++
       
       5 files changed, 147 insertions(+), 83 deletions(-)
       ---
   DIR diff --git a/README.org b/README.org
       t@@ -37,9 +37,13 @@ key management.
          : # ln -s /usr/libexec/gitzone/pre-receive
          : # ln -s /usr/libexec/gitzone/post-receive
        
       +- if you want to use a repository locally add these hooks as well / instead:
       +  : # ln -s /usr/libexec/gitzone/pre-commit
       +  : # ln -s /usr/libexec/gitzone/post-commit
       +
        - create a .gitconfig for each user that contains user name & user email (used
          for auto increment commits):
       -  : # git config -f ~$user/.gitconfig user.name Auto Incrementer
       +  : # git config -f ~$user/.gitconfig user.name $user
          : # git config -f ~$user/.gitconfig user.email "$user@ns.example.com"
        
        - add ssh keys to ~$user/.ssh/authorized_keys and enable ssh key editing if desired:
       t@@ -66,9 +70,9 @@ key management.
            : }
        
          - put user zone configuration in a separate file for each user and include them:
       -    : include "/etc/bind/users/user1.conf";
       -    : include "/etc/bind/users/user2.conf";
       -    : include "/etc/bind/users/user3.conf";
       +    : include "/etc/bind/repos/user1.conf";
       +    : include "/etc/bind/repos/user2.conf";
       +    : include "/etc/bind/repos/user3.conf";
        
        * Usage
        
   DIR diff --git a/bin/gitzone b/bin/gitzone
       t@@ -2,23 +2,27 @@
        #
        # gitzone by tg
        #
       -# this program is called from a pre-receive & post-receive git hook, if a push
       -# is made to the master branch changed files are validated with named-checkzone,
       -# the push is rejected if there's an error in one of the zone files specified in
       -# the config file, if everything is OK, the zone files are copied to $zone_dir
       -# and the zone is reloaded with rndc reload $zone $class $view
       +# This program is called from a pre-receive & post-receive or pre-commit &
       +# post-commit git hook. If a push is made to the master branch, changed files
       +# are validated with named-checkzone>. The push or commit is rejected if there's
       +# an error in one of the zone files specified in the config file. If everything
       +# is OK, the zone files are copied to $zone_dir and the zone is reloaded with
       +# the following command: rndc reload $zone $class $view
        
        use warnings;
        use strict;
        use POSIX qw/strftime/;
        use Cwd qw/cwd realpath/;
        use File::Basename qw/fileparse basename/;
       +use File::Temp;
        
        @ARGV >= 2 or die "Usage: gitzone /path/to/gitzone.conf <command>\n";
       +chdir '.git' if -d '.git';
        basename(realpath) eq '.git' or die "gitzone has to be run from a .git directory\n";
        
        my $lock_file = realpath '.gitzone-lock';
        my $list_file = realpath '.gitzone-list';
       +my $stash_file;
        chdir '..';
        
        our $user = getpwuid $<;
       t@@ -28,19 +32,21 @@ our ($zone_dir, $git, $named_checkzone, $rndc, $class, $default_view, $update_re
        my ($config_file, $cmd) = @ARGV;
        do $config_file or die "Can't load config: $!\n";
        
       -my (%files, %inc_files, @zones, $date);
       +my (%files, %inc_files, @zones, @changed_files, $date, $cleanup);
        delete $ENV{GIT_DIR};
        
        !-e $lock_file or die "Error: lock file exists\n";
        open FILE, '>', $lock_file or die $!; close FILE;
        
       -sub cleanup { unlink $lock_file }
       +sub cleanup { unlink $lock_file; &$cleanup() if ref $cleanup }
        sub clean_exit { cleanup; exit shift }
        $SIG{__DIE__} = \&cleanup;
        
        ($_ = $cmd) &&
            /^pre-receive$/ && pre_receive() ||
            /^post-receive$/ && post_receive() ||
       +    /^pre-commit$/ && pre_commit() ||
       +    /^post-commit$/ && post_commit() ||
            $update_record && /^update-record$/ && update_record($ARGV[2]);
        cleanup;
        
       t@@ -64,7 +70,7 @@ sub git {
        
        # Load BIND config files specified in the $repos config variable.
        # First load the -default key, then the $repo key.
       -sub load_repos_config {
       +sub load_repo_config {
          my $key = shift || '-default';
        
          # move files not in a dir to a . dir for easier processing
       t@@ -93,23 +99,42 @@ sub load_repos_config {
            }
          }
        
       -  load_repos_config($repo) if $key eq '-default';
       +  load_repo_config($repo) if $key eq '-default';
       +}
       +
       +sub check_what_changed {
       +  my ($old, $new) = @_;
       +
       +  # diff with empty tree if there's no previous commit
       +  $old = '4b825dc642cb6eb9a060e54bf8d69288fbee4904' if !$old || $old =~ /^0+$/;
       +
       +  $_ = git "diff --raw ". ($new ? "$old..$new" : $old);
       +
       +  # parse diff output, add only valid zone names to %files for parsing
       +  $files{$1} = 0 while m,^:(?:[\w.]+\s+){5}([a-z0-9./-]+)$,gm;
        }
        
        sub process_files {
          $files{$_} = 0 for (@_);
       -  $files{$_} += process_file($_) for keys %files;
       -  find_inc_by($_) for keys %inc_files;
       +  process_file($_) for keys %files;
          check_zones();
       +
       +  if (@changed_files) {
       +    print "adding changed files: @changed_files\n" if $verbosity >= 2;
       +    git "add @changed_files";
       +  }
        }
        
        sub process_file {
       -  my $file = shift;
       +  my ($file, $depth) = @_;
          my (@newfile, $changed, @inc_by);
          print ">> process_file($file)\n" if $verbosity >= 3;
        
          return 0 if $files{$file}; # already processed
       -  return -1 unless -f $file; # deleted
       +  return -1 unless -f $file;
       +
       +  print ">>> processing $file\n" if $verbosity >= 3;
       +  $files{$_}++;
        
          open FILE, '<', $file or die $!;
          my $n = 0;
       t@@ -124,7 +149,7 @@ sub process_file {
              $s = ($s =~ /^$date/ || $s < 2000000000 || $s >= 2100000000) ? $s + 1 : $date.'00';
              $line = "$a$s$z\n";
              $changed = 1;
       -    } elsif (/^(\W*\$INCLUDE\W+)(\S+)(.*)$/) {
       +    } elsif (/^(\s*\$INCLUDE\s+)(\S+)(.*)$/) {
              my ($a,$inc_file,$z) = ($1,$2,$3);
              unless ($unrestricted_includes) {
                # check $INCLUDE lines for files outside the repo dir
       t@@ -147,64 +172,66 @@ sub process_file {
          close FILE;
        
          if ($changed) {
       +    print ">>> $file changed, saving\n" if $verbosity >= 3;
       +
            open FILE, '>', $file or die $!;
            print FILE for @newfile;
            close FILE;
        
       -    git "commit -m 'auto increment: $file' '$file'", 1;
       +    push @changed_files, $file;
          }
        
       -  return 1;
       -}
       -
       -sub find_inc_by {
       -  my $file = shift;
       -  my $depth = shift || 1; # recursion depth
       -  my @inc_by;
       -  print ">> find_inc_by($file)\n" if $verbosity >= 3;
       -
       -  return 0 if $files{$file}; # already processed
       -  return -1 unless -f $file; # deleted
       -  $files{$_}++;
       -
       -  open FILE, '<', $file or die $!;
       -  if (<FILE> =~ /^;INCLUDED_BY\s+(.*)$/) {
       -    # add files listed after ;INCLUDED_BY to %files
       -    @inc_by = split /\s+/, $1;
       -    for (@inc_by) {
       -      $files{$_} = 0 unless exists $files{$_};
       -    }
       -  }
       -  close FILE;
       -
          if ($depth++ < $max_depth) {
       -    find_inc_by($_, $depth) for @inc_by;
       +    process_file($_, $depth) for @inc_by;
          } else {
            print "Warning: ;INCLUDED_BY is followed only up to $max_depth levels,\n".
                  "  the following files are not reloaded: @inc_by\n";
          }
       +
       +  return 1;
        }
        
        sub check_zones {
          for my $file (keys %files) {
       -    # skip files with errors and those that are not in the config
            my ($zone, $dir) = fileparse $file;
            $dir = substr $dir, 0, -1;
       +    # skip files with errors and those that are not in the config
            next unless $files{$file} > 0 && exists $repos->{$repo}->{$dir}->{$zone};
        
       +    print "Checking zone $zone\n";
            print `$named_checkzone -w .. '$zone' '$repo/$file'`;
            clean_exit 1 if $?; # error, reject push
            push @zones, $file;
          }
        }
        
       +sub save_list_file {
       +  if (@zones) {
       +    print "Zone check passed: @zones\n";
       +    # save changed zone list for post-receive hook
       +    open FILE, '>>', $list_file or die $!;
       +    print FILE join(' ', @zones), "\n";
       +    close FILE;
       +  } else {
       +    print "No zones to reload\n";
       +  }
       +}
       +
       +sub load_list_file {
       +  return unless -f $list_file;
       +  my %zones;
       +  open FILE, '<', $list_file or die $!;
       +  while (<FILE>) {
       +    $zones{$_} = 1 for split /[\s\n\r]+/;
       +  }
       +  close FILE;
       +  @zones = keys %zones;
       +}
       +
        sub install_zones {
          print "Reloading changed zones: @zones\n";
        
          my $cwd = cwd;
       -  # move master to new
       -  git 'checkout -f master';
       -  git 'reset --hard new';
        
          chdir "$zone_dir/$repo" or die $!;
          git "clone $cwd ." unless -d '.git';
       t@@ -221,6 +248,24 @@ sub install_zones {
          unlink $list_file;
        }
        
       +# save working dir state
       +# (git stash wouldn't work without conflicts if there's a
       +# change in both the index & working tree in the same file)
       +sub stash_save {
       +  $stash_file = File::Temp::tempnam('.git', '.gitzone-stash-');
       +  print "Saving working tree to $stash_file\n";
       +  git "update-index --refresh -q", 0, -1;
       +  git "diff >$stash_file";
       +  git 'checkout .';
       +}
       +
       +# restore working dir
       +sub stash_pop {
       +  print "Restoring working tree from $stash_file\n";
       +  git "apply --reject --whitespace=nowarn $stash_file", 1, -1;
       +  unlink $stash_file unless $?;
       +}
       +
        sub pre_receive {
          my ($old, $new, $ref);
        
       t@@ -235,46 +280,62 @@ sub pre_receive {
          # nothing for master branch, exit
          clean_exit 0 unless $ref;
        
       -  # check what changed
       +  # checkout changes
          git "checkout -qf $new";
       -  if ($old =~ /^0+$/) {
       -    $_ = git "whatchanged $new";
       -  } else {
       -    $_ = git "diff --raw $old..$new";
       -  }
       -  # parse diff output, add only valid zone names to %files for parsing
       -  $files{$1} = 0 while m,^:(?:[\w.]+\s+){5}([a-z0-9./-]+)$,gm;
       +  check_what_changed($old, $new);
       +  load_repo_config;
       +  process_files;
       +  git "commit -m 'auto increment: @changed_files'", 1 if @changed_files;
       +  save_list_file;
       +
       +  # save new commits in a new branch
       +  git 'checkout -B new';
       +}
        
       -  load_repos_config;
       +sub pre_commit {
       +  stash_save;
       +
       +  $cleanup = sub {
       +    # reset any changes, e.g. auto inc.
       +    git 'checkout .';
       +    stash_pop;
       +  };
       +
       +  git 'rev-parse --verify HEAD';
       +  check_what_changed($? ? undef : 'HEAD');
       +  load_repo_config;
          process_files;
        
       -  if (@zones) {
       -    print "Zone check passed: @zones\n";
       -    # save changed zone list for post-receive hook
       -    open FILE, '>>', $list_file or die $!;
       -    print FILE join(' ', @zones), "\n";
       -    close FILE;
       -  } else {
       -    print "No zones to reload\n";
       -  }
       +  $cleanup = sub {
       +    stash_pop;
       +  };
        
       -  # save new commits in a new branch
       -  git 'branch -D new', 0, -1;
       -  git 'checkout -b new';
       +  save_list_file;
        }
        
        sub post_receive {
          print "\n";
        
       -  open FILE, '<', $list_file or die $!;
       -  push @zones, split /[\s\n\r]+/ while <FILE>;
       -  close FILE;
       +  # move master to new
       +  git 'checkout -f master';
       +  git 'reset --hard new';
        
       -  load_repos_config;
       +  load_repo_config;
       +  load_list_file;
          install_zones;
       +
          print "Done. Don't forget to pull if you use auto increment.\n";
        }
        
       +sub post_commit {
       +  print "\n";
       +
       +  load_repo_config;
       +  load_list_file;
       +  install_zones;
       +  print "Done.\n";
       +}
       +
        sub update_record {
          my ($c, $file, @record) = split /\s+/, shift;
          my ($ip) = $ENV{SSH_CLIENT} =~ /^([\d.]+|[a-f\d:]+)\s/i or die "Invalid IP address\n";
       t@@ -314,10 +375,5 @@ sub update_record {
          git "commit -m 'update-record: $file' '$file'", 1;
        
          process_files $file;
       -
       -  # save new commits in a new branch
       -  git 'branch -D new';
       -  git 'checkout -b new';
       -
          install_zones if @zones;
        }
   DIR diff --git a/bin/gitzone-shell b/bin/gitzone-shell
       t@@ -35,12 +35,8 @@ elif [ -f $allow_key_mgmt_file ]; then
            cat .ssh/authorized_keys
          elif [[ "$cmd" == add-key* ]]; then
            key="${cmd:8}"
       -    #if [[ "$key" =~ ^ssh-(rsa|dss)\ [a-zA-Z0-9/+]+=*\ [a-zA-Z0-9_.]+@[a-zA-Z0-9.-]+$ ]]; then
       -      echo "$key" >> .ssh/authorized_keys && \
       -        echo "key added"
       -    #else
       -    #  echo "invalid key"
       -    #fi
       +    echo "$key" >> .ssh/authorized_keys && \
       +      echo "key added"
          elif [[ "$cmd" == del-key* ]]; then
            key="${cmd:8}"
            $grep -v "$key" .ssh/authorized_keys > .ssh/authorized_keys-new && \
   DIR diff --git a/hooks/post-commit b/hooks/post-commit
       t@@ -0,0 +1,5 @@
       +#!/bin/sh
       +
       +if [ -f .gitzone-list ]; then
       +  /usr/bin/gitzone /etc/gitzone.conf post-commit
       +fi
   DIR diff --git a/hooks/pre-commit b/hooks/pre-commit
       t@@ -0,0 +1,3 @@
       +#!/bin/sh
       +
       +/usr/bin/gitzone /etc/gitzone.conf pre-commit