#!/usr/bin/env perl
#
# PSYC notification script for Git repositories, written by the lynX and improved by tg
# based on cvs2psyc and bartman's sender-side post-commit hook.
#
# it simply uses git log to find out where it stands. for more elaborate
# ways, find inspiration in contrib/hooks/post-receive-email from the git
# distribution.
#
# HOW TO INSTALL:
#
# put git2psyc.conf in the project.git/ directory and
# make a link to this script in the project.git/hooks/post-receive:
#
# cd project.git/hooks
# ln -s /path/to/git2psyc post-receive
#
# or if you want to use a different config file, call it with an argument:
#
# #!/bin/sh
# git2psyc /path/to/git2psyc.conf
#
# HOW TO DEBUG:
# set $debug to 1 in the config then run e.g.:
# echo a4ddfedb23c106fe484831d0e6df6f7f273f7db9 6a9d18cfa5d3334f446ff74710ad7cde5e0026ef refs/heads/master | ./git2psyc git2psyc.conf

#use strict;
#use warnings;

use Cwd qw/cwd realpath/;
use File::Basename qw/basename/;
use Socket;

chdir '.git' if -d '.git';
my $confname = 'git2psyc.conf';
my $config;
$config = $ARGV[0] if @ARGV && -f $ARGV[0];
$config = "./$confname" if !$config && -f $confname;
$config = "../$confname" unless $config && -f $config;
$config = "../../$confname" unless $config && -f $config;
$config = "../../../$confname" unless $config && -f $config;
$config = "../../../../$confname" unless $config && -f $config;
die "Config file not found" unless $config && -f $config;

our ($project, $target, $host, $port, $webview, $psycver, $debug);
our $pwd = cwd;
# extract the project name from the path if nothing provided
# this works for either project/ or project.git/ directories
$project=$1 if $pwd =~ m#/([^/]+)/?\.git\b#;

do $config;

# GIT handling:
my (@commits, @added, @deleted);

# received refs with old & new hashes
while (<STDIN>) { # <old-value> SP <new-value> SP <ref-name> LF
    print STDERR ">> $_" if $debug;
    next unless m,([0-9a-f]+) ([0-9a-f]+) (\S+),;
    #next if $3 ne 'refs/heads/master'; # only process master branch
    my ($old, $new, $ref) = ($1, $2, $3);

    push @added, $ref if $old =~ /^0+$/;
    push @deleted, $ref if $new =~ /^0+$/;
    next if $old =~ /^0+$/ || $new =~ /^0+$/;

    $_ = `git whatchanged --pretty=fuller --shortstat $old..`;
    print STDERR if $debug;

    # extract commit hashes & msgs from git output
    while (/commit\s+([0-9a-f]+)\n
            Author:\s+(.*?)\ <(.*?)>\n
            AuthorDate:\s+(.*?)\n
            Commit:\s+(.*?)\ <(.*?)>\n
            CommitDate:\s+(.*?)\n
            \n
            \s+(.*?)\n
            .*?
	    ^\ (?:(\d+)\ files?\ changed)?
	       (?:,\ (\d+)\ insertion\S*)?
	       (?:,\ (\d+)\ deletion\S*)?
           /gmsx) {
	# after so many years, somebody broke the output of this git command!
        push @commits, {
            ref => $ref, hash => $1, abbrev_hash => substr($1, 0, 8),
            author => $2, author_email => $3, author_date => $4,
            commit => $5, commit_email => $6, commit_date => $7,
            title => $8, stat_files_changed => $9 || '0', 
	    stat_insertions => $10 || '0', stat_deletions => $11 || '0',
        };
    }
}

if ($debug) {
    use Data::Dumper;
    print STDERR Dumper \@commits, \@added, \@deleted;
}

die <<X unless @commits || @added || @deleted;
$0 is meant to be issued from a Git post-receive hook.
X

print <<X;
Delivering notice to $target.
X

# PSYC socket stuff:
my ($delim, $delimre);
if ($psycver >= 1.0) {
    $delim = '|';
    $delimre = qr/\|/;
} else {
    $delim = '.';
    $delimre = qr/\./;
}

if ($target =~ m#^psyc://([\w.-]+)(?::(\d+))?#i) {
    $host ||= $1;
    $port ||= $2 || 4404;
} else {
    die "target invalid: $target";
}

my $iaddr = inet_aton($host) or die "could not resolve host: $host";
my $paddr = sockaddr_in($port, $iaddr);

# there is no real reason why we aren't using UDP here...
socket(S, PF_INET, SOCK_STREAM, getprotobyname('tcp')) or die "socket: $!";

connect(S, $paddr) or die "connect: $!";
select S; $|=1; select STDOUT;

print S <<X;
$delim
X

# wait for greeting
if (defined($_ = <S>) && !/^$delimre/) {
    die "Error while establishing circuit: invalid greeting";
}
# wait for first packet
while (defined($_ = <S>) && !/^$delimre/) {
#	print if s/^=//;
}
#print "\n";

# SEND THE MESSAGES:
# reverse them so they're in chronological order
for my $c (reverse @commits) {
    my $packet = <<X;
:_target\t$target

:_project\t$project
:_ref\t$c->{ref}
:_hash_commit_long\t$c->{hash}
:_hash_commit\t$c->{abbrev_hash}
:_page_commit\t$webview$c->{abbrev_hash}
:_name_editor\t$c->{author}
:_name_committer\t$c->{commit}
:_mailto_editor\t$c->{author_email}
:_mailto_committer\t$c->{commit_email}
:_date_editor\t$c->{author_date}
:_date_committer\t$c->{commit_date}
:_comment\t$c->{title}
:_stat_files_changed\t$c->{stat_files_changed}
:_stat_insertions\t$c->{stat_insertions}
:_stat_deletions\t$c->{stat_deletions}
_notice_update_software_git
[_project]: [_name_editor] "[_comment]" ([_stat_files_changed] files: +[_stat_insertions] -[_stat_deletions]) [_page_commit]
$delim
X
    print S $packet;
    print STDERR $packet if $debug;
    print "  \"$c->{title}\"\n" if $c->{title};
}

sub print_refs {
    my $op = shift;
    my $_list_refs = join '|', @_;
    my @refs;
    for (@_) { s,^refs/,,; push @refs, $_; }
    my $_refs = join ', ', @refs;
    my $packet = <<X;
:_target\t$target

:_project\t$project
:_refs\t$_refs
:_list_refs\t|$_list_refs
_notice_update_software_git_refs_$op
[_project]: $op refs: [_refs]
$delim
X
    print S $packet;
    print STDERR $packet if $debug;
}

print_refs('deleted', @deleted) if @deleted;
print_refs('added', @added) if @added;

print S <<X;
_request_circuit_shutdown
$delim
X
# the _request_circuit_shutdown shouldn't be necessary for a simple
# one-way message submission, but psyced needs to become easier about that

# at this point we should wait for the other side to close the socket.. argl
close (S)	or die "close: $!";
exit;
