#!/usr/bin/perl use strict; use Config::Simple; use DBI; use File::Basename; use POSIX; use SVN::Core; use SVN::Repos; use SVN::Fs; use Net::LDAP; use Net::LDAP qw(LDAP_SUCCESS); use URI; use Mail::Sendmail; use LWP::UserAgent; my $CFG_FILE = '@CFGDIR@/codepot.ini'; my $REPOFS = $ARGV[0]; my $REPOBASE = basename($REPOFS); my $REV = $ARGV[1]; my $QC = ''; sub get_config { my $cfg = new Config::Simple(); if (!$cfg->read($CFG_FILE)) { return undef; } my $config = { login_model => $cfg->param('login_model'), ldap_server_uri => $cfg->param('ldap_server_uri'), ldap_server_protocol_version => $cfg->param('ldap_server_protocol_version'), ldap_auth_mode => $cfg->param('ldap_auth_mode'), ldap_userid_format => $cfg->param('ldap_userid_format'), ldap_password_format => $cfg->param('ldap_password_format'), ldap_admin_binddn => $cfg->param('ldap_admin_binddn'), ldap_admin_password => $cfg->param('ldap_admin_password'), ldap_userid_search_base => $cfg->param('ldap_userid_search_base'), ldap_userid_search_filter => $cfg->param('ldap_userid_search_filter'), ldap_mail_attribute_name => $cfg->param('ldap_mail_attribute_name'), database_hostname => $cfg->param("database_hostname"), database_port => $cfg->param("database_port"), database_username => $cfg->param("database_username"), database_password => $cfg->param("database_password"), database_name => $cfg->param("database_name"), database_driver => $cfg->param("database_driver"), database_prefix => $cfg->param("database_prefix"), database_store_gmt => $cfg->param("database_store_gmt"), email_sender => $cfg->param("email_sender"), commit_notification => $cfg->param("commit_notification"), commit_notification_url => $cfg->param("commit_notification_url") }; return $config; } sub open_database { my ($cfg) = @_; my $dbtype = $cfg->{database_driver}; my $dbname = $cfg->{database_name}; my $dbhost = $cfg->{database_hostname}; my $dbport = $cfg->{database_port}; if ($dbtype eq 'postgre') { $dbtype = 'Pg'; } elsif ($dbtype eq 'oci8') { $dbtype = 'Oracle'; } elsif ($dbtype eq 'mysqli') { $dbtype = 'mysql'; } elsif ($dbtype eq 'sqlite') { $dbtype = 'SQLite'; } my $dbstr; my $dbuser; my $dbpass; if ($dbtype eq 'Oracle') { $QC = '"'; $dbstr = "DBI:$dbtype:"; $dbuser = $cfg->{database_username} . '/' . $cfg->{database_password} . '@' . $dbhost; $dbpass = ''; } elsif ($dbtype eq 'SQLite') { $dbstr = "DBI:$dbtype:database=$dbhost;"; $dbuser = $cfg->{database_username}; $dbpass = $cfg->{database_password}; } else { $dbstr = "DBI:$dbtype:database=$dbname;"; if (length($dbhost) > 0) { $dbstr .= "host=$dbhost;"; } if (length($dbport) > 0) { $dbstr .= "port=$dbport;"; } $dbuser = $cfg->{database_username}; $dbpass = $cfg->{database_password}; } my $dbh = DBI->connect( $dbstr, $dbuser, $dbpass, { RaiseError => 0, PrintError => 0, AutoCommit => 0 } ); return $dbh; } sub close_database { my ($dbh) = @_; $dbh->disconnect (); } sub find_issue_reference_in_commit_message { my ($dbh, $prefix, $projectid, $revision, $commit_message) = @_; # find [[#IXXXX]] my @issue_ids = ($commit_message =~ /\[\[#I(\d+)\]\]/g); # find #XXXX my @issue_ids2 = ($commit_message =~ /(^|[^#])#(\d+)(\D|$)/g); # find unique issue ids in the findings. my %tmp; @tmp{@issue_ids} = 1; for (my $i = 0; $i < scalar(@issue_ids2); $i += 3) { my $id = @issue_ids2[$i + 1]; @tmp{$id} = 1; } my @unique_issue_ids = keys(%tmp); $dbh->begin_work (); my $query = $dbh->prepare("DELETE FROM ${QC}${prefix}issue_coderev${QC} WHERE ${QC}codeproid${QC}=? AND ${QC}coderev${QC}=?"); if (!$query || !$query->execute($projectid, $revision)) { my $errstr = $dbh->errstr(); if ($query) { $query->finish (); } $dbh->rollback (); return (-1, $errstr); } $query->finish (); for my $issue_id(@unique_issue_ids) { my $query = $dbh->prepare("INSERT INTO ${QC}${prefix}issue_coderev${QC} (${QC}projectid${QC},${QC}issueid${QC},${QC}codeproid${QC},${QC}coderev${QC}) VALUES (?,?,?,?)"); if ($query) { # ignore errors $query->execute ($projectid, $issue_id, $projectid, $revision); $query->finish (); } } $dbh->commit (); return (0, undef); } sub write_commit_log { my ($dbh, $prefix, $projectid, $revision, $userid, $store_gmt) = @_; #+------+---------+-----------+---------------------------+---------------------+---------------+-----------------+ #| id | type | projectid | message | createdon | action | userid | #+------+---------+-----------+---------------------------+---------------------+---------------+-----------------+ #| 895 | code | codepot | svn,codepot,72 | 2011-10-10 14:26:43 | commit | hyunghwan.chung | my $message = "svn,$projectid,$revision"; my $timestamp; if (($store_gmt =~ /^\d+?$/ && int($store_gmt) != 0) || (lc($store_gmt) eq 'yes')) { $timestamp = POSIX::strftime('%Y-%m-%d %H:%M:%S', gmtime()); } else { $timestamp = POSIX::strftime('%Y-%m-%d %H:%M:%S', localtime()); } # the PHP side is executing ALTER SESSION SET NLS_TIMESTAMP_FORMAT='YYYY-MM-DD HH24:MI:SS.FF. # do i have to do it here or use the database time (CURRENT_TIMESTAMP) instead? # make sure that you have the same time between the app server and the data server. # to minize side-effect of using the time of data server. #my $createdon = POSIX::strftime ('%Y-%m-%d %H:%M:%S', localtime()); $dbh->begin_work (); #my $query = $dbh->prepare ("INSERT INTO ${QC}${prefix}log${QC} (${QC}type${QC},${QC}projectid${QC},${QC}message${QC},${QC}createdon${QC},${QC}action${QC},${QC}userid${QC}) VALUES (?,?,?,?,?,?)"); #if (!$query || !$query->execute('code', $projectid, $message, $createdon, 'commit', $userid)) my $query = $dbh->prepare ("INSERT INTO ${QC}${prefix}log${QC} (${QC}type${QC},${QC}projectid${QC},${QC}message${QC},${QC}createdon${QC},${QC}action${QC},${QC}userid${QC}) VALUES (?,?,?,?,?,?)"); if (!$query || !$query->execute('code', $projectid, $message, $timestamp, 'commit', $userid)) { my $errstr = $dbh->errstr(); if ($query) { $query->finish (); } $dbh->rollback (); return (-1, $errstr); } $query->finish (); $dbh->commit (); return (0, undef); } sub get_author { my $pool = SVN::Pool->new(undef); my $svn = eval { SVN::Repos::open ($REPOFS, $pool) }; if (!defined($svn)) { print (STDERR "Cannot open svn - $REPOFS\n"); return undef; } my $fs = $svn->fs(); if (!defined($fs)) { print (STDERR "Cannot open fs - $REPOFS\n"); return undef; } my $author = $fs->revision_prop ($REV, 'svn:author'); return $author; } sub get_commit_message { my $pool = SVN::Pool->new(undef); my $svn = eval { SVN::Repos::open ($REPOFS, $pool) }; if (!defined($svn)) { print (STDERR "Cannot open svn - $REPOFS\n"); return undef; } my $fs = $svn->fs(); if (!defined($fs)) { print (STDERR "Cannot open fs - $REPOFS\n"); return undef; } my $logmsg = $fs->revision_prop($REV, 'svn:log'); return $logmsg; } sub format_string { my ($fmt, $userid, $password) = @_; my $out = $fmt; $out =~ s/\$\{userid\}/$userid/g; $out =~ s/\$\{password\}/$password/g; return $out; } sub get_author_email_ldap { my ($cfg, $userid) = @_; my $uri = URI->new ($cfg->{ldap_server_uri}); my $ldap = Net::LDAP->new ( $uri->host, scheme => $uri->scheme, port => $uri->port, version => $cfg->{ldap_server_protocol_version} ); if (!defined($ldap)) { print (STDERR 'Cannot create LDAP'); return (-1, undef); } my $f_rootdn = format_string($cfg->{ldap_admin_binddn}, $userid, ''); my $f_rootpw = format_string($cfg->{ldap_admin_password}, $userid, ''); my $res = $ldap->bind($f_rootdn, password => $f_rootpw); if ($res->code != LDAP_SUCCESS) { print (STDERR "Cannot bind LDAP as $f_rootdn - " . $res->error()); $ldap->unbind(); return (-1, undef); } my $f_basedn = ''; my $f_filter = ''; if ($cfg->{ldap_auth_mode} == 2) { $f_basedn = format_string($cfg->{ldap_userid_search_base}, $userid, ''); $f_filter = format_string($cfg->{ldap_userid_search_filter}, $userid, ''); $res = $ldap->search(base => $f_basedn, scope => 'sub', filter => $f_filter); if ($res->code != LDAP_SUCCESS) { $ldap->unbind(); return (-1, undef); } my $entry = $res->entry(0); # get the first entry only if (!defined($entry)) { $ldap->unbind(); return (0, undef); } $f_basedn = $entry->dn(); } else { $f_basedn = format_string($cfg->{ldap_userid_format}, $userid, ''); } $f_filter = '(' . $cfg->{ldap_mail_attribute_name} . '=*)'; $res = $ldap->search(base => $f_basedn, scope => 'sub', filter => $f_filter); if ($res->code != LDAP_SUCCESS) { $ldap->unbind(); return (0, undef); } my $entry = $res->entry(0); # get the first entry only if (!defined($entry)) { $ldap->unbind(); return (0, undef); } my $xret = 0; my $email = ''; my @attrs = $entry->attributes (); foreach my $attr (@attrs) { if ($attr eq $cfg->{ldap_mail_attribute_name}) { $email = $entry->get_value ($attr); $xret = 1; last; } } $ldap->unbind(); return ($xret, $email); } sub get_author_email_db { my ($cfg, $dbh, $prefix, $userid) = @_; my $query = $dbh->prepare("SELECT ${QC}email${QC} FROM ${QC}${prefix}user_account${QC} WHERE ${QC}userid${QC}=?"); if (!$query || !$query->execute($userid)) { return (-1, $dbh->errstr()); } if (my @row = $query->fetchrow_array()) { return (1, @row[0]); } return (0, undef); } sub email_message_to_project_members { my ($cfg, $dbh, $prefix, $projectid, $subject, $message) = @_; my $query = $dbh->prepare("SELECT ${QC}userid${QC} FROM ${QC}${prefix}project_membership${QC} WHERE ${QC}projectid${QC}=?"); if (!$query || !$query->execute($projectid)) { if ($query) { $query->finish (); } return (-1, $dbh->errstr()); } my @members; while (my @row = $query->fetchrow_array()) { push (@members, $row[0]); } $query->finish (); my $recipients = ''; foreach my $member (@members) { my $xret; my $email; if ($cfg->{login_model} eq 'LdapLoginModel') { ($xret, $email) = get_author_email_ldap($cfg, $member) } elsif ($cfg->{login_model} eq 'DbLoginModel') { ($xret, $email) = get_author_email_db($cfg, $dbh, $prefix, $member); } else { $xret = -2; $email = ''; } if ($xret >= 1 && defined($email) && length($email) > 0) { if (length($recipients) > 0) { $recipients .= ', '; } $recipients .= $email; } } if (length($recipients) <= 0) { return (0, undef); } my %mail = ( To => $recipients, Subject => $subject, Message => $message ); if (length($cfg->{email_sender}) > 0) { $mail{From} .= $cfg->{email_sender}; } Mail::Sendmail::sendmail (%mail); return (1, undef); } sub format_commit_url { my ($fmt, $projectid, $author, $rev) = @_; my $out = $fmt; $out =~ s/\$\{PROJECTID\}/$projectid/g; $out =~ s/\$\{AUTHOR\}/$author/g; $out =~ s/\$\{REV\}/$rev/g; return $out; } sub trigger_webhooks { my ($cfg, $dbh, $prefix, $projectid, $commit_message) = @_; # find [skip ci], [no ci] or something similar my @skip = (); while ($commit_message =~ /\[(skip|no)[[:space:]]+([[:alpha:]]+)\]/g) { push (@skip, $2); } my $query = $dbh->prepare("SELECT ${QC}webhooks${QC} FROM ${QC}${prefix}project${QC} WHERE ${QC}id${QC}=?"); if (!$query || !$query->execute($projectid)) { if ($query) { $query->finish (); } return (-1, $dbh->errstr()); } my $webhooks = ''; if (my @row = $query->fetchrow_array()) { $webhooks = $row[0]; } my $ua = LWP::UserAgent->new(ssl_opts => { verify_hostname => 0 }); $ua->timeout (5); foreach my $webhook (split(/\n/ ,$webhooks)) { $webhook =~ s/^\s+|\s+$//g; if ($webhook ne '') { my @tmp = split(/[[:space:]]+/, $webhook); my $type = 'ci'; my $url = ''; if (scalar(@tmp) == 1) { $url = @tmp[0]; } elsif (scalar(@tmp) == 2) { $type = @tmp[0]; $url = @tmp[1]; } else { next; } if (grep(/^$type$/, @skip)) { next; } ## TODO: some formatting on url? my $res = $ua->get($url); if ($res->is_success) { ##print $res->decoded_content . "\n"; } else { ##print $res->status_line . "\n"; } } } $query->finish (); return (0, undef); } #------------------------------------------------------------ # MAIN #------------------------------------------------------------ my $AUTHOR = get_author(); if (!defined($AUTHOR)) { print (STDERR "Cannot get author for $REPOBASE $REV\n"); exit (1); } chomp ($AUTHOR); my $cfg = get_config(); if (!defined($cfg)) { print (STDERR "Cannot load codepot configuration file\n"); exit (1); } my $dbh = open_database($cfg); if (!defined($dbh)) { printf (STDERR "Cannot open database - %s\n", $DBI::errstr); exit (1); } my $raw_commit_message = get_commit_message(); find_issue_reference_in_commit_message ($dbh, $cfg->{database_prefix}, $REPOBASE, $REV, $raw_commit_message); write_commit_log ($dbh, $cfg->{database_prefix}, $REPOBASE, $REV, $AUTHOR, $cfg->{database_store_gmt}); if (lc($cfg->{commit_notification}) eq 'yes') { my $commit_subject = "Commit r$REV by $AUTHOR in $REPOBASE"; my $commit_message = ''; if ($cfg->{commit_notification_url} eq '') { $commit_message = $commit_subject; } else { $commit_message = format_commit_url($cfg->{commit_notification_url}, $REPOBASE, $AUTHOR, $REV); } if (defined($raw_commit_message)) { $commit_message = $commit_message . "\n" . $raw_commit_message; } email_message_to_project_members ($cfg, $dbh, $cfg->{database_prefix}, $REPOBASE, $commit_subject, $commit_message); } trigger_webhooks ($cfg, $dbh, $cfg->{database_prefix}, $REPOBASE, $raw_commit_message); close_database ($dbh); exit (0);