#!/usr/bin/perl

use strict;
use warnings;

my $who = ($ENV{REMOTE_USER} || die "Auth required\n")."\@$ENV{REMOTE_ADDR}";
my $r = "\x01"; # enable bit 2^(fileno STDIN)
die localtime().": [$who] git-server: NO pre-receive STDIN? Commandline Execution Worked!\n" if !select $r, undef, undef, 0.2;
$SIG{__DIE__} = sub {
    my $why = shift;
    $why =~ s/\s*$/\n/;
    ipclog("  CRASH: $why");
    warn $why;
    exit 1;
};
while (my $push_info = <STDIN>) {
    my ($old,$new,$ref) = $push_info =~ m{^(\w+) (\w+) ([\w/\-.@]+)} or die localtime().": [$who] git-server: Invalid pre-receive STDIN!\n";
    die localtime().": [$who] git-server: Invalid pre-receive ref!\n" unless $ref;
    my $branch = ($ref =~ m{^refs/(?:heads|tags)/(.+)} || $ref =~ m{.*/([^/]+)}) ? $1 : $ref;
    my $type = ($ref =~ m{^refs/tags/}) ? "tag" : "branch";
    ipclog("REF:$branch");
    ipclog("  TYPE:$type");
    ipclog("  OLD:$old");
    ipclog("  NEW:$new");
    my $conf = $0 =~ m{(.*)/} ? `$1/configs` : `git config --list`;
    if ($conf =~ s/^acl.restrictemail=(.+)//m) {
        my $restrictemail = $1;
        my $sniffers = [];
        foreach my $sniff (split /,/, $restrictemail) {
            $sniff =~ s/^@/\*@/;
            my $regex = $sniff =~ m{^/(.+)/$} ? qr{$1} : eval {
                my $wild = $sniff;
                $wild =~ s/\\/\\\\/g;
                $wild =~ s/\./\\./g;
                $wild =~ s/\*/.+/g;
                return qr{^$wild$};
            } or die localtime().": [$who] git-server: Unimplemented [acl.restrictemail] syntax? [$sniff]\n";
            push @$sniffers, $regex;
        }
        my $commits = `git log --reverse --format='%H %ae' $old..$new`;
        my $prev = $old;
        while ($commits =~ s/^(\w+)\s+(.*)\n//) {
            my $commit = $1;
            my $email = $2;
            my $matched = 0;
            foreach my $regex (@$sniffers) {
                $matched = 1 if $email =~ $regex;
            }
            die localtime().": [$who] git-server: [acl.restrictemail] Illegal email [$email] on commit [$commit]. (Hint: 'git rebase -i $prev ; git commit --reset-author --amend')\n" if !$matched;
            $prev = $commit;
        }
    }
    while ($conf =~ s{^restrictedbranch\.([^\*\s]+|/.+/)\.pushers=(.+)$}{}m or $conf =~ s/^restrictedbranch\.(.+)\.pushers=(.+)$//m) {
        my $sniff = $1;
        my $users = $2;
        my $regex = $sniff =~ m{^/(.+)/$} ? qr{$1} : eval {
            my $wild = $sniff;
            $wild =~ s/\\/\\\\/g;
            $wild =~ s/\./\\./g;
            $wild =~ s/\*/.+/g;
            return qr{^$wild$};
        } or die localtime().": [$who] git-server: Unimplemented [restrictedbranch.pushers] syntax? [$sniff]=>[$users]\n";
        if ($branch =~ $regex) {
            my $allowed = 0;
            foreach my $user (split /,/, $users) {
                $allowed = $sniff if $user eq $ENV{REMOTE_USER};
            }
            $allowed or die localtime().": [$who] git-server: Missing [restrictedbranch.pushers] permission! Failed to modify restricted $type [$branch].\n";
            last;
        }
    }
    my $can_change_history = 1;
    while ($conf =~ s{^restrictedbranch\.([^\*\s]+|/.+/)\.forcers=(.+)$}{}m or $conf =~ s/^restrictedbranch\.(.+)\.forcers=(.+)$//m) {
        my $sniff = $1;
        my $users = $2;
        my $regex = $sniff =~ m{^/(.+)/$} ? qr{$1} : eval {
            my $wild = $sniff;
            $wild =~ s/\\/\\\\/g;
            $wild =~ s/\./\\./g;
            $wild =~ s/\*/.+/g;
            return qr{^$wild$};
        } or die localtime().": [$who] git-server: Unimplemented [restrictedbranch.forcers] syntax? [$sniff]=>[$users]\n";
        if ($branch =~ $regex) {
            $can_change_history = 0;
            foreach my $user (split /,/, $users) {
                $can_change_history = $sniff if $user eq $ENV{REMOTE_USER};
            }
            last;
        }
    }
    if ($old =~ /^0+$/) {
        ipclog("  FORCE:0");
        if ($can_change_history) {
            warn localtime().": [$who] git-server: Creating new $type [$branch]\n";
        }
        else {
            warn localtime().": [$who] git-server: Creating unremovable $type [$branch]. (You will not be able to delete it without [restrictedbranch.forcers] permission.)\n";
        }
    }
    elsif ($new =~ /^0+$/) {
        ipclog("  FORCE:1");
        if ($can_change_history) {
            if ($type eq "tag") {
                warn localtime().": [$who] git-server: Deleting tag [$branch] from [$old]\n";
            }
            else {
                warn localtime().": [$who] git-server: DANGER! Pruning entire branch [$branch] from [$old]!\n";
            }
        }
        else {
            if ($type eq "tag") {
                die localtime().": [$who] git-server: Missing [restrictedbranch.forcers] permission! Unable to remove restricted tag [$branch]. Do not run 'git push --delete origin $branch'. Remaining at $old\n";
            }
            else {
                die localtime().": [$who] git-server: Missing [restrictedbranch.forcers] permission! Unable to prune git history on restricted $type [$branch]. Do not run 'git push --delete origin $branch'. Remaining at $old\n";
            }
        }
    }
    elsif ($type eq "tag") {
        ipclog("  FORCE:1");
        if ($can_change_history) {
            warn localtime().": [$who] git-server: FORCE moving existing tag [$branch] from $old to $new\n";
        }
        else {
            die localtime().": [$who] git-server: Missing [restrictedbranch.forcers] permission! You cannot move restricted tag [$branch]. Do not run 'git push --force origin $branch'\n";
        }
    }
    elsif (`git log $new -- 2>&1 | grep '^commit $old'`) {
        #warn localtime().": [$who] git-server: Pushing $new changes to $type [$branch] tip without --force\n";
        ipclog("  FORCE:0");
    }
    else {
        # MUST HAVE USED: git push --force
        ipclog("  FORCE:1");
        if ($can_change_history) {
            my $tip_hashes = `git show-ref 2>&1`;
            $tip_hashes =~ s{ refs/\w+/}{ }g;
            my $matching = {};
            my $scan_hashes = $tip_hashes;
            while ($scan_hashes =~ s{^\Q$old\E\s+(\S+)}{}m) {
                $matching->{$1} = $old;
            }
            if (my $found = delete $matching->{$branch}) {
                if ($found ne $old) {
                    die localtime().": [$who] git-server: Server branch [$branch] unexpectedly shows $found instead of $old?\n";
                }
                if (!keys %$matching) {
                    my (undef,undef,undef,$mday,$mon,$year) = gmtime;
                    my $suggest = sprintf "%04d-%02d-%02d-%s-BAK-%s", $year+1900, $mon+1, $mday, $branch, substr($old,0,6);
                    die localtime().": [$who] git-server: Refusing to lose $type $branch history upto $old without a backup ref to hold it.\n\n  Hint: Create a tag on the old $type tip:\n    export ANCHOR=$suggest ; git tag \$ANCHOR $old ; git push origin \$ANCHOR ;\n    git push --force\n\n  Hint: And then if you really wish to purge these changes forever, run this:\n    git tag -d \$ANCHOR ; git push --delete origin \$ANCHOR\n";
                }
            }
            else {
                die localtime().": [$who] git-server: SERVER BUG! Unable to find $type [$branch]\n$tip_hashes\nOperation 'push --force' failed!\n";
            }
            warn localtime().": [$who] git-server: Rewriting git history for $type [$branch] using 'push --force' since found backup ref(s) (".join(" ",sort keys %$matching).") still on old tip [$old].\n";
        }
        else {
            die localtime().": [$who] git-server: Missing [restrictedbranch.forcers] permission! Failed to rewrite git history for restricted $type [$branch]. Do not run 'git push --force'. Try 'git pull --rebase' or 'git pull' to clean up your local repo.\n";
        }
    }
    if ($conf =~ /^restrictedfile\./m) {
        my $diff = `git diff $old $new | grep '^diff '`;
        my $files_changed = {};
        my $files_blocked = {};
        while ($diff =~ s{^diff .*\bb/(.+?)\r?\n}{}m) {
            $files_changed->{$1} = 1;
        }
        while (keys %$files_changed and $conf =~ s/^restrictedfile\.(.+)\.pushers=(.+)$//m) {
            my $sniff = $1;
            my $users = $2;
            my $regex = $sniff =~ m{^/(.+)/$} ? qr{$1} : eval {
                my $wild = $sniff;
                $wild =~ s/\\/\\\\/g;
                $wild =~ s/\./\\./g;
                $wild =~ s/\*/.+/g;
                return qr{^$wild$};
            } or die localtime().": [$who] git-server: Unimplemented [restrictedfile.pushers] syntax? [$sniff]=>[$users]\n";
            foreach my $file (keys %$files_changed) {
                if ($file =~ $regex) {
                    my $restrict = 1;
                    foreach my $user (split /,/, $users) {
                        $restrict = 0 if $user eq $ENV{REMOTE_USER};
                    }
                    $files_blocked->{$file} = $regex if $restrict;
                }
            }
        }
        if (%$files_blocked) {
            die localtime().": [$who] git-server: Missing [restrictedfile.pushers] permission! You can't modify [".join(" ",sort keys %$files_blocked)."] Do not 'git push' until changes have been reverted. (Hint: 'git diff origin HEAD ".[sort keys %$files_blocked]->[0]." ; git rebase -i $old')\n";
        }
    }
}

# Remember this info that might be needed later
sub ipclog {
    my $entry = shift;
    $entry =~ s/\s*$/\n/;
    if ($entry and my $ipc = $ENV{IPC}) {
        if (open my $fh, ">>", "$ipc/pushinfo.log") {
            print $fh $entry;
            close $fh;
        }
    }
}
