diff --git a/src/lib/Hydra.pm b/src/lib/Hydra.pm index 56165418..42da5f65 100644 --- a/src/lib/Hydra.pm +++ b/src/lib/Hydra.pm @@ -92,7 +92,7 @@ has 'hydra_plugins' => ( after setup_finalize => sub { my $class = shift; - $plugins = [Hydra::Plugin->plugins(db => $class->model('DB'), config => $class->config)]; + $plugins = [Hydra::Plugin->instantiate(db => $class->model('DB'), config => $class->config)]; }; __PACKAGE__->setup(); diff --git a/src/lib/Hydra/Plugin.pm b/src/lib/Hydra/Plugin.pm index 47a5faeb..8b3df782 100644 --- a/src/lib/Hydra/Plugin.pm +++ b/src/lib/Hydra/Plugin.pm @@ -7,11 +7,19 @@ use Module::Pluggable sub new { my ($class, %args) = @_; - my $self = { db => $args{db}, config => $args{config} }; + my $self = { db => $args{db}, config => $args{config}, plugins => $args{plugins} }; bless $self, $class; return $self; } +sub instantiate { + my ($class, %args) = @_; + my $plugins = []; + $args{plugins} = $plugins; + push @$plugins, $class->plugins(%args); + return @$plugins; +} + # Called when build $build has finished. If the build failed, then # $dependents is an array ref to a list of builds that have also # failed as a result (i.e. because they depend on $build or a failed @@ -34,4 +42,12 @@ sub fetchInput { return undef; } +# Get the commits to repository ‘$value’ between revisions ‘$rev1’ and +# ‘$rev2’. Each commit should be a hash ‘{ revision = "..."; author = +# "..."; email = "..."; }’. +sub getCommits { + my ($self, $type, $value, $rev1, $rev2) = @_; + return []; +} + 1; diff --git a/src/lib/Hydra/Plugin/GitInput.pm b/src/lib/Hydra/Plugin/GitInput.pm index 98c4c24a..fc267e70 100644 --- a/src/lib/Hydra/Plugin/GitInput.pm +++ b/src/lib/Hydra/Plugin/GitInput.pm @@ -12,17 +12,9 @@ sub supportedInputTypes { $inputTypes->{'git'} = 'Git checkout'; } -sub fetchInput { - my ($self, $type, $name, $value) = @_; - - return undef if $type ne "git"; - - (my $uri, my $branch, my $deepClone) = split ' ', $value; - $branch = defined $branch ? $branch : "master"; - - my $timestamp = time; - my $sha256; - my $storePath; +# Clone or update a branch of a repository into our SCM cache. +sub _cloneRepo { + my ($self, $uri, $branch, $deepClone) = @_; my $cacheDir = getSCMCacheDir . "/git"; mkpath($cacheDir); @@ -38,8 +30,8 @@ sub fetchInput { chdir $clonePath or die $!; # !!! urgh, shouldn't do a chdir - # This command force the update of the local branch to be in the same as - # the remote branch for whatever the repository state is. This command mirror + # This command forces the update of the local branch to be in the same as + # the remote branch for whatever the repository state is. This command mirrors # only one branch of the remote repository. ($res, $stdout, $stderr) = captureStdoutStderr(600, "git", "fetch", "-fu", "origin", "+$branch:$branch"); @@ -47,16 +39,6 @@ sub fetchInput { "git", "fetch", "-fu", "origin") if $res; die "error fetching latest change from git repo at `$uri':\n$stderr" if $res; - ($res, $stdout, $stderr) = captureStdoutStderr(600, - ("git", "rev-parse", "$branch")); - die "error getting revision number of Git branch '$branch' at `$uri':\n$stderr" if $res; - - my ($revision) = split /\n/, $stdout; - die "error getting a well-formated revision number of Git branch '$branch' at `$uri':\n$stdout" - unless $revision =~ /^[0-9a-fA-F]+$/; - - my $ref = "refs/heads/$branch"; - # If deepClone is defined, then we look at the content of the repository # to determine if this is a top-git branch. if (defined $deepClone) { @@ -74,6 +56,40 @@ sub fetchInput { } } + return $clonePath; +} + +sub _parseValue { + my ($value) = @_; + (my $uri, my $branch, my $deepClone) = split ' ', $value; + $branch = defined $branch ? $branch : "master"; + return ($uri, $branch, $deepClone); + +} + +sub fetchInput { + my ($self, $type, $name, $value) = @_; + + return undef if $type ne "git"; + + my ($uri, $branch, $deepClone) = _parseValue($value); + + my $clonePath = $self->_cloneRepo($uri, $branch, $deepClone); + + my $timestamp = time; + my $sha256; + my $storePath; + + my ($res, $stdout, $stderr) = captureStdoutStderr(600, + ("git", "rev-parse", "$branch")); + die "error getting revision number of Git branch '$branch' at `$uri':\n$stderr" if $res; + + my ($revision) = split /\n/, $stdout; + die "error getting a well-formated revision number of Git branch '$branch' at `$uri':\n$stdout" + unless $revision =~ /^[0-9a-fA-F]+$/; + + my $ref = "refs/heads/$branch"; + # Some simple caching: don't check a uri/branch/revision more than once. # TODO: Fix case where the branch is reset to a previous commit. my $cachedInput ; @@ -145,4 +161,28 @@ sub fetchInput { }; } +sub getCommits { + my ($self, $type, $value, $rev1, $rev2) = @_; + return [] if $type ne "git"; + + return [] unless $rev1 =~ /^[0-9a-f]+$/; + return [] unless $rev2 =~ /^[0-9a-f]+$/; + + my ($uri, $branch, $deepClone) = _parseValue($value); + + my $clonePath = $self->_cloneRepo($uri, $branch, $deepClone); + + my $out; + IPC::Run::run(["git", "log", "--pretty=format:%H%x09%an%x09%ae%x09%at", "$rev1..$rev2"], \undef, \$out) + or die "cannot get git logs: $?"; + + my $res = []; + foreach my $line (split /\n/, $out) { + my ($revision, $author, $email, $date) = split "\t", $line; + push @$res, { revision => $revision, author => $author, email => $email }; + } + + return $res; +} + 1; diff --git a/src/lib/Hydra/Plugin/HipChatNotification.pm b/src/lib/Hydra/Plugin/HipChatNotification.pm index 13ef187f..c7b11352 100644 --- a/src/lib/Hydra/Plugin/HipChatNotification.pm +++ b/src/lib/Hydra/Plugin/HipChatNotification.pm @@ -35,6 +35,36 @@ sub buildFinished { } } + return if scalar keys %rooms == 0; + + # Determine who broke/fixed the build. + my $prevBuild = getPreviousBuild($build); + + my $nrCommits = 0; + my %authors; + + if ($prevBuild) { + foreach my $curInput ($build->buildinputs_builds) { + next unless $curInput->type eq "git"; + my $prevInput = $prevBuild->buildinputs_builds->find({ name => $curInput->name }); + next unless defined $prevInput; + + next if $curInput->type ne $prevInput->type; + next if $curInput->uri ne $prevInput->uri; + + my @commits; + foreach my $plugin (@{$self->{plugins}}) { + push @commits, @{$plugin->getCommits($curInput->type, $curInput->uri, $prevInput->revision, $curInput->revision)}; + } + + foreach my $commit (@commits) { + print STDERR "$commit->{revision} by $commit->{author}\n"; + $authors{$commit->{author}} = $commit->{email}; + $nrCommits++; + } + } + } + # Send a message to each room. foreach my $roomId (keys %rooms) { my $room = $rooms{$roomId}; @@ -53,7 +83,16 @@ sub buildFinished { $msg .= " (and ${\scalar @deps} others)" if scalar @deps > 0; $msg .= ": " . showStatus($build) . ""; + if (scalar keys %authors > 0) { + # FIXME: HTML escaping + my @x = map { "$_" } (sort keys %authors); + $msg .= ", likely due to "; + $msg .= "$nrCommits commits by " if $nrCommits > 1; + $msg .= join(" or ", scalar @x > 1 ? join(", ", @x[0..scalar @x - 2]) : (), $x[-1]); + } + print STDERR "sending hipchat notification to room $roomId: $msg\n"; + next; my $ua = LWP::UserAgent->new(); my $resp = $ua->post('https://api.hipchat.com/v1/rooms/message?format=json&auth_token=' . $room->{room}->{token}, { diff --git a/src/script/hydra-build b/src/script/hydra-build index 902b5bff..ab2650db 100755 --- a/src/script/hydra-build +++ b/src/script/hydra-build @@ -17,7 +17,7 @@ my $db = Hydra::Model::DB->new(); my $config = getHydraConfig(); -my @plugins = Hydra::Plugin->plugins(db => $db, config => $config); +my @plugins = Hydra::Plugin->instantiate(db => $db, config => $config); sub addBuildStepOutputs { diff --git a/src/script/hydra-evaluator b/src/script/hydra-evaluator index dd6ffa7e..50e0e448 100755 --- a/src/script/hydra-evaluator +++ b/src/script/hydra-evaluator @@ -21,7 +21,7 @@ STDOUT->autoflush(); my $db = Hydra::Model::DB->new(); my $config = getHydraConfig(); -my $plugins = [Hydra::Plugin->plugins(db => $db, config => $config)]; +my $plugins = [Hydra::Plugin->instantiate(db => $db, config => $config)]; # Don't check a jobset more than once every five minutes. my $minCheckInterval = 5 * 60;