1 package Girocco
::Notify
;
8 use Girocco
::HashUtil
qw(hmac_sha1);
11 $have_hmac_sha256 = 0;
13 Digest
::SHA
->import( qw(hmac_sha256) );
14 $have_hmac_sha256 = 1;
20 #use RPC::XML::Client;
22 # This Perl code creates json payload within post-receive hook.
25 # The %ai %ci formats are ISO 8601 like "1999-12-31 23:59:59 -0100"
26 # The %aI %cI strict ISO 8601 formats aren't available until Git v2.2.0
28 s/ /T/; # "1999-12-31 23:59:59 -0100" -> "1999-12-31T23:59:59 -0100"
29 s/ //; # "1999-12-31T23:59:59 -0100" -> "1999-12-31T23:59:59-0100"
30 s/(?<!:)(\d\d)$/:$1/; # "1999-12-31T23:59:59-0100" -> "1999-12-31T23:59:59-01:00"
35 my ($proj, $commit, $root) = @_;
37 my @gcmd = ($Girocco::Config
::git_bin
, '--git-dir='.$proj->{path
});
40 open $fd, '-|', @gcmd, 'log', '-1', '--pretty=format:%T%n%ae %an%n%ai%n%ce %cn%n%ci%n%s%n%n%b', $commit, '--'
41 or die "cannot do git log: $! $?";
46 my ($ae, $an) = ($l[1] =~ /^(.*?) (.*)$/);
47 #my ($ai) = _fixlaxiso($l[2]);
48 my ($ce, $cn) = ($l[3] =~ /^(.*?) (.*)$/);
49 my ($ci) = _fixlaxiso
($l[4]);
50 my $msg = join("\n", splice(@l, 5));
51 # Up to three trailing newlines in case of no body.
52 chomp $msg; chomp $msg; chomp $msg;
54 my ($rf, $af, $mf) = ([], [], []);
55 my $parent = ($commit eq $root) ?
"--root" : "$commit^";
56 open $fd, '-|', @gcmd, 'diff-tree', '--name-status', '-r', $parent, $commit, '--'
57 or die "cannot do git diff-tree: $! $?";
60 my ($s, $file) = split(/\t/, $_);
77 "url" => $Girocco::Config
::gitweburl
."/".$proj->{name
}.".git/commit/".$commit,
78 "author" => { "name" => $an, "email" => $ae },
79 "committer" => { "name" => $cn, "email" => $ce },
80 "distinct" => json_bool
(1),
88 return "girocco/1.0 (JSON Push Notification) (" .
89 $Girocco::Config
::gitweburl
."/".$proj->{name
}.".git" .
94 my ($base, $suffix) = @_;
95 defined($base) && $base ne "" or return undef;
96 defined($suffix) && $suffix ne "" or return undef;
97 return $base.'/'.$suffix.'.git';
103 "name" => $proj->{name
},
104 "default_branch" => $proj->{HEAD
},
105 "master_branch" => $proj->{HEAD
},
106 "description" => $proj->{desc
},
107 # Girocco extension: full_name is full project name,
108 # equivalent to GitHub's "owner[name]/name".
109 "full_name" => $proj->{name
}.".git",
111 "url" => _projurl
($Girocco::Config
::gitweburl
,$proj->{name
}),
112 "html_url" => _projurl
($Girocco::Config
::gitweburl
,$proj->{name
}),
113 "clone_url" => _projurl
($Girocco::Config
::httppullurl
,$proj->{name
}),
114 "git_url" => _projurl
($Girocco::Config
::gitpullurl
,$proj->{name
}),
115 "ssh_url" => _projurl
($Girocco::Config
::pushurl
,$proj->{name
}),
116 "mirror_url" => $proj->{url
},
117 # Girocco extension: Git Pull URL.
118 "pull_url" => _projurl
($Girocco::Config
::gitpullurl
,$proj->{name
}),
120 "owner" => { "email" => $proj->{email
} }
125 my ($proj, $payload) = @_;
126 my $ct = $proj->{jsontype
};
127 if (!defined($ct) || $ct ne 'application/json') {
130 # Spaces are expected to be encoded as '+' rather than %20
131 # This is not part of any RFC, but matches the behavior
132 # of the specification that we're emulating here
133 $payload =~ s/([^ A-Za-z0-9_.~-])/sprintf("%%%02X",ord($1))/ge;
134 $payload =~ s/[ ]/+/g; # expected but NOT part of any RFC!
136 $payload = 'payload=' . $payload;
141 sub _jsonsigheaders
{
142 my ($proj, $payload) = @_;
143 my $hmackey = $proj->{jsonsecret
};
145 if (defined($hmackey) && $hmackey ne "") {
146 my $sig = "sha1=".lc(unpack('H*',hmac_sha1
($hmackey, $payload)));
147 push(@sigheaders, X_Hub_Signature
=> $sig);
148 if ($have_hmac_sha256) {
149 # Yes, the argument order is different! #%@#%^@!
150 # hmac_sha1 is provided by Girocco::HashUtil and it uses the
151 # sane order of "key" then "text" as that's the order they are
152 # mentioned in RFC 2104. The @#%^^@* Digest::SHA module, on the
153 # other hand, is the one providing hmac_sha256 and for some
154 # inscrutable reason uses the order of "text" then "key"!
155 my $sig256 = "sha256=".lc(unpack('H*',hmac_sha256
($payload, $hmackey)));
156 push(@sigheaders, X_Hub_Signature_256
=> $sig256);
163 my ($url, $proj, $user, $ref, $oldrev, $newrev, $forced) = @_;
168 $pusher = { "name" => $user->{name
} };
169 $sender = { "login" => $user->{name
} };
170 if ($user->{name
} ne 'mob') {
171 $pusher->{"email"} = $user->{email
};
177 my @commits = get_commits
($proj, $ref, $oldrev, $newrev);
178 my $root = ($oldrev =~ /^0+$/) ?
$commits[$#commits] : "";
179 foreach my $commit (@commits) {
180 push @
$commits, json_commit
($proj, $commit, $root);
183 # This is backwards-compatible with GitHub (except the GitHub-specific
184 # full project name construction sometimes performed in clients)
185 my $payload = to_json
{
189 "created" => json_bool
($oldrev =~ /^0+$/ ?
1 : 0),
190 "deleted" => json_bool
($newrev =~ /^0+$/ ?
1 : 0),
191 "forced" => json_bool
($forced ?
1 : 0),
192 "repository" => _jsonrepo
($proj),
195 "commits" => $commits
198 # print "$payload\n";
199 my $ua = LWP
::UserAgent
->new(agent
=> _jsonagent
($proj));
200 $ua->max_redirect(0); # no redirects allowed
201 $ua->max_size(32768); # there really shouldn't be hardly any response content at all
203 my $ct = $proj->{jsontype
};
204 defined($ct) && $ct eq 'application/json' or
205 $ct = 'application/x-www-form-urlencoded';
206 $payload = _jsonpayload
($proj, $payload);
207 my @headers = ( Content_Type
=> $ct, Content_Length
=> length($payload) );
208 $ua->post($url, @headers, _jsonsigheaders
($proj, $payload), Content
=> $payload);
213 my ($proj, $url) = @_;
214 defined($url) && $url ne "" or $url = $proj->{notifyjson
};
215 defined($url) && $url ne "" or return undef;
216 my $ct = $proj->{jsontype
};
217 defined($ct) && $ct eq 'application/json' or
218 $ct = 'application/x-www-form-urlencoded';
220 "active" => json_bool
(1),
222 "content_type" => (($ct =~ /json/) ?
"json" : "form"),
225 "events" => [ "push" ]
227 my $payload = to_json
{
228 "zen" => 'Why is a raven like a writing desk?',
229 "repository" => _jsonrepo
($proj),
232 "url" => _projurl
($Girocco::Config
::gitweburl
,$proj->{name
}),
233 "html_url" => _projurl
($Girocco::Config
::gitweburl
,$proj->{name
}),
234 "email" => $proj->{email
}
237 $payload = _jsonpayload
($proj, $payload);
238 my $ua = LWP
::UserAgent
->new(agent
=> _jsonagent
($proj));
239 $ua->max_redirect(0); # no redirects allowed
240 $ua->max_size(32768); # there really shouldn't be hardly any response content at all
241 $ua->timeout(15); # Allow extra time on initial check
242 my @headers = ( Content_Type
=> $ct, Content_Length
=> length($payload) );
243 my $r = $ua->post($url, @headers, _jsonsigheaders
($proj, $payload), Content
=> $payload);
244 return $r->protocol !~ m{HTTP/0\.9}i && $r->is_success; # HTTP/0.9 is NOT okay for this test
249 my ($cianame, $proj, $branch, $commit, $root) = @_;
251 my @gcmd = ($Girocco::Config
::git_bin
, '--git-dir='.$proj->{path
});
254 open $fd, '-|', @gcmd, 'log', '-1', '--pretty=format:%an <%ae>%n%at%n%s', $commit, '--'
255 or die "cannot do git log: $! $?";
259 foreach (@l) { s/&/&/g; s/</</g; s/>/>/g; }
260 my ($a, $at, $subj) = @l;
263 my $parent = ($commit eq $root) ?
"--root" : "$commit^";
264 open $fd, '-|', @gcmd, 'diff-tree', '--name-status', '-r', $parent, $commit, '--'
265 or die "cannot do git diff-tree: $! $?";
268 s/&/&/g; s/</</g; s/>/>/g;
269 my ($status, $file) = split(/\t/, $_);
274 my $rev = substr($commit, 0, 12);
279 <name>Girocco::Notify</name>
280 <version>1.0</version>
283 <project>$cianame</project>
285 if ($branch ne 'master') { # XXX: Check HEAD instead
286 $msg .= "<branch>$branch</branch>";
288 $msg .= "</source>\n";
289 $msg .= "<timestamp>$at</timestamp>\n";
290 $msg .= "<body><commit><author>$a</author><revision>$rev</revision>\n";
291 $msg .= "<url>$Girocco::Config::gitweburl/$proj->{name}.git/commit/$commit</url>\n";
293 foreach (@f) { $msg .= "<file>$_</file>\n"; }
294 $msg .= "</files><log>$subj</log></commit></body></message>\n";
297 #my $rpc_client = new RPC::XML::Client "http://cia.vc/RPC2";
298 #my $rpc_request = RPC::XML::request->new('hub.deliver', $msg);
299 #my $rpc_response = $rpc_client->send_request($rpc_request);
300 #ref $rpc_response or print STDERR "XML-RPC Error: $RPC::XML::ERROR\n";
304 my ($cianame, $proj, $ref, $oldrev, $newrev) = @_;
306 # CIA notifications for branches only
308 $branch =~ s
#^refs/heads/## or return;
310 my @commits = get_commits
($proj, $ref, $oldrev, $newrev);
311 my $root = ($oldrev =~ /^0+$/) ?
$commits[$#commits] : "";
312 foreach my $commit (@commits) {
313 cia_commit
($cianame, $proj, $branch, $commit, $root);
315 print "$proj->{name}.git: CIA.vc is defunct ($cianame)\n";
320 my ($proj, $ref, $oldrev, $newrev) = @_;
322 return () if $newrev =~ /^0+$/;
324 my @gcmd = ($Girocco::Config
::git_bin
, '--git-dir='.$proj->{path
});
327 open $fd, '-|', @gcmd, 'for-each-ref', '--format=%(refname)', 'refs/heads/'
328 or die "cannot do git for-each-ref: $! $?";
331 @refs = grep { $_ ne $ref } @refs;
336 open $fd, '-|', @gcmd, 'rev-parse', '--not', @refs
337 or die "cannot do git rev-list for revlims: $! $?";
343 my $revspec = (($oldrev =~ /^0+$/) ?
$newrev : "$oldrev..$newrev");
344 open $fd, '-|', @gcmd, 'rev-list', @revlims, $revspec
345 or die "cannot do git rev-list: $! $?";
353 sub _get_sender_uuid
{
354 my ($proj, $user) = @_;
358 my $senderemail = $proj->{email
};
359 defined($senderemail) && $senderemail ne ''
360 or $senderemail = $Girocco::Config
::sender
;
362 if ($user->{name
} eq 'mob') {
363 $sender = "The Mob User <$senderemail>";
365 my $useremail = $user->{email
};
366 defined($useremail) && $useremail ne ''
367 or $useremail = $senderemail;
368 $sender = "$user->{name} <$useremail>";
369 $xtrahdr = "X-User-UUID: $user->{uuid}" if $user->{uuid
};
372 $sender = "$proj->{name} <$senderemail>";
375 return ($sender, $xtrahdr);
378 sub _notify_for_ref
{
379 $_[0] =~ m{^refs/heads/.} || $_[0] =~ m{^refs/tags/.};
383 BEGIN {$_ref_change = sub {
384 my ($proj, $user, $ref, $oldrev, $newrev) = @_;
385 _notify_for_ref
($ref) or return (0, 0, undef);
390 chdir($proj->{path
});
392 # First, possibly send out various mails
393 if (($ref =~ m{^refs/heads/.} && $proj->{notifymail
}) ||
394 ($ref =~ m{^refs/tags/.} && ($proj->{notifymail
} || $proj->{notifytag
}))) {
395 my ($sender, $xtrahdr) = _get_sender_uuid
($proj, $user);
396 my @cmd = ($Girocco::Config
::basedir
.'/taskd/mail.sh',
397 "$ref", "$oldrev", "$newrev", $proj->{name
},
400 if ($?
< 0 && exists($ENV{MAIL_SH_OTHER_BRANCHES
})) {
402 delete $ENV{MAIL_SH_OTHER_BRANCHES
};
405 $?
and warn "mail.sh failed";
410 # Next, send JSON packet to given URL if enabled.
411 if ($proj->{notifyjson
}) {
412 ($ind, $git_ran) = ref_indicator
($proj->{path
}, $oldrev, $newrev);
413 json
($proj->{notifyjson
}, $proj, $user, $ref, $oldrev, $newrev, $ind eq '...');
416 # Also send CIA notifications.
417 if ($proj->{notifycia
}) {
418 cia
($proj->{notifycia
}, $proj, $ref, $oldrev, $newrev);
421 return ($mail_sh_ran, $git_ran, $ind);
424 # ref_changes($proj, $user, [$coderef,] [$oldrefs,] @changes)
425 # $coderef gets called with ($oldrev, $newrev, $refname, $mail_sh_ran, $git_ran, $ind)
426 # as each update is processed where $mail_sh_ran will be true if mail.sh was run
427 # $ran_git will be true if git was run and $ind will be undef unless ref_indicator
428 # was run in which case it will be the ref_indicator '..' or '...' result
429 # $oldrefs is an optional hash of $$oldrefs{$refname} = $oldvalue
430 # @changes is array of [$oldrev, $newrev, $ref]
438 $proc = shift if ref($_[0]) eq 'CODE';
439 $oldrefs = shift if ref($_[0]) eq 'HASH';
440 return if !@_ || ref($_[0]) ne 'ARRAY' || @
{$_[0]} != 3 ||
441 !${$_[0]}[0] || !${$_[0]}[1] || !${$_[0]}[2];
443 # run custom notify if present
444 # hook protocol is the same as for Git's post-receive hook where each
445 # line sent to the hook's stdin has the form:
446 # oldhash newhash fullrefname
447 # with the following additions:
448 # * four command line arguments are passed:
449 # 1. project name (e.g. "proj" "proj/fork" "proj/fork/sub" etc.)
450 # 2. "user" responsible for the changes
451 # 3. total number of lines coming on stdin
452 # 4. how many of those lines are "context" lines (sent first)
453 # * "context" lines are sent first before any actual change lines
454 # * "context" lines have the same format except oldhash equals newhash
455 # * "context" lines give the value unchanged refs had at the time
456 # the changes to the non-"context" refs were made
457 # * currently "context" lines are only provided for "refs/heads/..."
458 # * current directory will always be the repository's top-level $GIT_DIR
459 # * the PATH is guaranteed to find the correct Git, utils and $basedir/bin
460 # * the hook need not consume all (or any) of stdin
461 # * the exit code for the hook is ignored (just like Git's post-receive)
462 # * the GIROCCO_BASEDIR environment variable is set to $Girocco::Config::basedir
464 if (($customhook = $proj->_has_notifyhook)) {{
467 if (is_shellish
($customhook)) {
468 $argv0 = $Girocco::Config
::posix_sh_bin
|| "/bin/sh";
469 push(@argv, $argv0, "-c", $customhook.' "$@"');
471 -f
$customhook && -x _
or last;
472 $argv0 = $customhook;
475 $username = $user->{name
} if $user && defined($user->{name
});
476 $username ne "" or $username = "-";
477 push(@argv, $argv0, $proj->{name
}, $username);
478 my @mod = grep({$$_[2] =~ m{^refs/.} && $$_[1] ne "" && $$_[2] ne "" && $$_[1] ne $$_[2]} @_);
479 my %modref = map({($$_[2] => 1)} @mod);
481 do {do {$same{$_} = $$oldrefs{$_} if !exists($modref{$_})} foreach keys %$oldrefs} if $oldrefs;
483 push(@same, [$same{$_}, $same{$_}, $_]) foreach sort keys %same;
484 push(@argv, @same + @mod, @same + 0);
485 chdir($proj->{path
}) or last;
486 local $ENV{GIROCCO_BASEDIR
} = $Girocco::Config
::basedir
;
487 local $ENV{PATH
} = util_path
;
488 if (open my $hookpipe, '|-', @argv) {
489 local $SIG{'PIPE'} = sub {};
490 print $hookpipe map("$$_[0] $$_[1] $$_[2]\n", @same, @mod);
493 print STDERR
"$proj->{name}: failed to run notifyhook \"$customhook\": $!\n";
497 # don't even try the fancy stuff if there are too many heads
498 my $maxheads = int(100000 / (1 + length(${$_[0]}[0])));
499 my $newheadsdone = 0;
500 my @newheads = grep({$$_[2] =~ m{^refs/heads/.} && $$_[1] !~ /^0+$/} @_);
502 if ($oldrefs && @newheads && keys(%$oldrefs) + @newheads <= $maxheads) {{
503 my $projarg = '--git-dir='.$proj->{path
};
505 # git merge-base --independent requires v1.7.3 or later
506 # We run it and if it fails just end up not processing indep heads last
507 # There is no version of merge-base --independent that accepts stdin revs
508 my %indep = map { $_ => 1 } split(' ',
509 get_git
($projarg, 'merge-base', '--independent', map($$_[1], @newheads)) || '');
511 # We pass the revisions on stdin so this should never fail unless one of
512 # the input revisions is no longer valid (which should never happen).
513 # However, if it does, we just fall back to the old non-fancy technique.
514 my ($inp, $out, $pid2);
515 $pid2 = eval { open2
($out, $inp, $Girocco::Config
::git_bin
, $projarg,
516 'rev-list', '--no-walk', '--reverse', '--stdin') };
519 local $SIG{'PIPE'} = sub {};
520 print $inp map("$$_[1]\n", @newheads);
523 my @ordered = <$out>;
526 @ordered or last; # if we got nothing it failed
530 push(@
{$updates{$$_[1]}}, $_) foreach @newheads;
531 my %curheads = %$oldrefs;
533 if ($_[0] eq 'refs/heads/master') {
534 return ($_[1] eq 'refs/heads/master') ?
0 : -1;
535 } elsif ($_[1] eq 'refs/heads/master') {
538 return $_[0] cmp $_[1];
541 foreach (grep(!$indep{$_}, @ordered), grep($indep{$_}, @ordered)) {
542 foreach my $change (sort {&$headcmp($$a[2], $$b[2])} @
{$updates{$_}}) {
543 my ($old, $new, $ref) = @
$change;
544 $ENV{MAIL_SH_OTHER_BRANCHES
} = join(' ', values(%curheads));
545 my ($mail_sh_ran, $git_ran, $ind) =
546 &$_ref_change($proj, $user, $ref, $old, $new);
547 &$proc($old, $new, $ref, $mail_sh_ran, $git_ran, $ind) if $proc;
548 $curheads{$ref} = $new;
554 delete $ENV{MAIL_SH_OTHER_BRANCHES
};
556 my ($old, $new, $ref) = @
$_;
557 if (!$newheadsdone || $ref !~ m{^refs/heads/.} || $new =~ /^0+$/) {
558 my $ran_mail_sh = &$_ref_change($proj, $user, $ref, $old, $new);
559 &$proc($old, $new, $ref, $ran_mail_sh) if $proc;