File Coverage

blib/lib/Gerrit/Client.pm
Criterion Covered Total %
statement 222 424 52.3
branch 32 116 27.5
condition 18 90 20.0
subroutine 41 59 69.4
pod 9 9 100.0
total 322 698 46.1


line stmt bran cond sub pod time code
1             #############################################################################
2             ##
3             ## Copyright (C) 2012-2014 Rohan McGovern
4             ## Copyright (C) 2012 Digia Plc and/or its subsidiary(-ies)
5             ##
6             ## This library is free software; you can redistribute it and/or
7             ## modify it under the terms of the GNU Lesser General Public
8             ## License as published by the Free Software Foundation; either
9             ## version 2.1 of the License, or (at your option) any later version.
10             ##
11             ## This library is distributed in the hope that it will be useful,
12             ## but WITHOUT ANY WARRANTY; without even the implied warranty of
13             ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14             ## Lesser General Public License for more details.
15             ##
16             ## You should have received a copy of the GNU Lesser General Public
17             ## License along with this library; if not, write to the Free Software
18             ## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19             ##
20             ##
21             #############################################################################
22              
23             =head1 NAME
24              
25             Gerrit::Client - interact with Gerrit code review tool
26              
27             =head1 SYNOPSIS
28              
29             use AnyEvent;
30             use Gerrit::Client qw(stream_events);
31              
32             # alert me when new patch sets arrive in
33             # ssh://gerrit.example.com:29418/myproject
34             my $stream = stream_events(
35             url => 'ssh://gerrit.example.com:29418',
36             on_event => sub {
37             my ($event) = @_;
38             if ($event->{type} eq 'patchset-added'
39             && $event->{change}{project} eq 'myproject') {
40             system("xmessage", "New patch set arrived!");
41             }
42             }
43             );
44              
45             AE::cv()->recv(); # must run an event loop for callbacks to be activated
46              
47             This module provides some utility functions for interacting with the Gerrit code
48             review tool.
49              
50             This module is an L user and may be used with any event loop supported
51             by AnyEvent.
52              
53             =cut
54              
55             package Gerrit::Client;
56 6     6   216953 use strict;
  6         17  
  6         214  
57 6     6   37 use warnings;
  6         783  
  6         213  
58              
59 6     6   6795 use AnyEvent::HTTP;
  6         158797  
  6         578  
60 6     6   71 use AnyEvent::Handle;
  6         11  
  6         144  
61 6     6   32 use AnyEvent::Util;
  6         11  
  6         433  
62 6     6   34 use AnyEvent;
  6         10  
  6         145  
63 6     6   2196 use Capture::Tiny qw(capture);
  6         41622  
  6         346  
64 6     6   40 use Carp;
  6         13  
  6         369  
65 6     6   5707 use Data::Alias;
  6         6218  
  6         418  
66 6     6   3949 use Data::Dumper;
  6         32102  
  6         542  
67 6     6   45 use Digest::MD5 qw(md5_hex);
  6         11  
  6         314  
68 6     6   866 use English qw(-no_match_vars);
  6         1977  
  6         209  
69 6     6   2824 use File::Path;
  6         12  
  6         307  
70 6     6   965 use File::Spec::Functions;
  6         755  
  6         525  
71 6     6   30 use File::Temp;
  6         11  
  6         531  
72 6     6   812 use File::chdir;
  6         1615  
  6         484  
73 6     6   7188 use JSON;
  6         79096  
  6         38  
74 6     6   3972 use Params::Validate qw(:all);
  6         38662  
  6         1449  
75 6     6   52 use Scalar::Util qw(weaken);
  6         14  
  6         328  
76 6     6   2979 use URI;
  6         14236  
  6         159  
77 6     6   43 use URI::Escape;
  6         9  
  6         357  
78              
79 6     6   33 use base 'Exporter';
  6         12  
  6         32098  
80             our @EXPORT_OK = qw(
81             for_each_patchset
82             stream_events
83             git_environment
84             next_change_id
85             random_change_id
86             review
87             query
88             quote
89             );
90              
91             our @GIT = ('git');
92             our @SSH = ('ssh');
93             our $VERSION = 20140611;
94             our $DEBUG = !!$ENV{GERRIT_CLIENT_DEBUG};
95             our $MAX_CONNECTIONS = 2;
96             our $MAX_FORKS = 4;
97              
98             sub _debug_print {
99 138 50   138   1058 return unless $DEBUG;
100 0         0 print STDERR __PACKAGE__ . ': ', @_, "\n";
101             }
102              
103             # parses a gerrit URL and returns a hashref with following keys:
104             # cmd => arrayref, base ssh command for interacting with gerrit
105             # project => the gerrit project name (e.g. "my/project")
106             sub _gerrit_parse_url {
107 113     113   1067 my ($url) = @_;
108              
109 113 50 33     834 if ( !ref($url) || !$url->isa('URI') ) {
110 113         3456 $url = URI->new($url);
111             }
112              
113 113 50       39423 if ( $url->scheme() ne 'ssh' ) {
114 0         0 croak "gerrit URL $url is not supported; only ssh URLs are supported\n";
115             }
116              
117 113         4997 my $project = $url->path();
118 113         2121 $url->path(undef);
119              
120             # remove useless leading/trailing components
121 113         5689 $project =~ s{\A/+}{};
122 113         906 $project =~ s{\.git\z}{}i;
123              
124             return {
125 113 50       1626 cmd => [
    50          
126             @SSH,
127             '-oBatchMode=yes', # never do interactive prompts
128             '-oServerAliveInterval=30'
129             , # try to avoid the server silently dropping connection
130             ( $url->port() ? ( '-p', $url->port() ) : () ),
131             ( $url->user() ? ( $url->user() . '@' ) : q{} ) . $url->host(),
132             'gerrit',
133             ],
134             project => $project,
135             gerrit => $url->as_string(),
136             };
137             }
138              
139             # Like qx, but takes a list, so no quoting issues
140             sub _safeqx {
141 17     17   159 my (@cmd) = @_;
142 17         44 my $status;
143 17     17   1782 my $output = capture { $status = system(@cmd) };
  17         227323  
144 17         27375 $? = $status;
145 17         432 return $output;
146             }
147              
148             =head1 FUNCTIONS
149              
150             =over
151              
152             =item B<< stream_events ssh_url => $gerrit_url, ... >>
153              
154             Connect to "gerrit stream-events" on the given gerrit host and
155             register one or more callbacks for events. Returns an opaque handle to
156             the stream-events connection; the connection will be aborted if the
157             handle is destroyed.
158              
159             $gerrit_url should be a URL with ssh schema referring to a valid
160             Gerrit installation (e.g. "ssh://user@gerrit.example.com:29418/").
161              
162             Supported callbacks are documented below. All callbacks receive the
163             stream-events handle as their first argument.
164              
165             =over
166              
167             =item B<< on_event => $cb->($data) >>
168              
169             Called when an event has been received.
170             $data is a reference to a hash representing the event.
171              
172             See L
173             documentation|http://gerrit.googlecode.com/svn/documentation/2.2.1/cmd-stream-events.html>
174             for information on the possible events.
175              
176             =item B<< on_error => $cb->($error) >>
177              
178             Called when an error occurs in the connection.
179             $error is a human-readable string.
180              
181             Examples of errors include network disruptions between your host and
182             the Gerrit server, or the ssh process being killed
183             unexpectedly. Receiving any kind of error means that some Gerrit
184             events may have been lost.
185              
186             If this callback returns a true value, stream_events will attempt to
187             reconnect to Gerrit and resume processing; otherwise, the connection
188             is terminated and no more events will occur.
189              
190             The default error callback will warn and return 1, retrying on all
191             errors.
192              
193             =back
194              
195             =cut
196              
197             sub stream_events {
198 4     4 1 6050 my (%args) = @_;
199              
200 4   66     27 $args{ssh_url} ||= $args{url};
201              
202 4   33     16 my $url = $args{ssh_url} || croak 'missing ssh_url argument';
203 4   33     13 my $on_event = $args{on_event} || croak 'missing on_event argument';
204             my $on_error = $args{on_error} || sub {
205 2     2   11 my ($error) = @_;
206 2         35 warn __PACKAGE__ . ": $error\n";
207 2         25 return 1;
208 4   100     24 };
209              
210 4         8 my $INIT_SLEEP = 2;
211 4         8 my $MAX_SLEEP = 60 * 10;
212 4         7 my $sleep = $INIT_SLEEP;
213              
214 4         9 my @ssh = ( @{ _gerrit_parse_url($url)->{cmd} }, 'stream-events' );
  4         21  
215              
216             my $cleanup = sub {
217 15     15   36 my ($handle) = @_;
218 15         58 delete $handle->{timer};
219 15         79 foreach my $key (qw(r_h r_h_stderr)) {
220 30 100       1019 if ( my $r_h = delete $handle->{$key} ) {
221 22         396 $r_h->destroy();
222             }
223             }
224 15 100       501 if ( my $cv = delete $handle->{cv} ) {
225 11         231 $cv->cb( sub { } );
  11         63925  
226 11 50       281 if ( my $pid = $handle->{pid} ) {
227 11         451 kill( 15, $pid );
228             }
229             }
230 4         607 };
231              
232 4         12 my $restart;
233             my $out_weak;
234              
235             my $handle_error = sub {
236 14     14   118 my ( $handle, $error ) = @_;
237 14         23 my $retry;
238 14         28 eval { $retry = $on_error->($error); };
  14         58  
239 14 50       64 if ($retry) {
240              
241             # retry after $sleep seconds only
242             $handle->{timer} =
243 14         285 AnyEvent->timer( after => $sleep, cb => sub { $restart->($handle) } );
  7         27430053  
244 14         397 $sleep *= 2;
245 14 50       96 if ( $sleep > $MAX_SLEEP ) {
246 0         0 $sleep = $MAX_SLEEP;
247             }
248             }
249             else {
250 0         0 $cleanup->($handle);
251             }
252 4         33 };
253              
254             $restart = sub {
255 11     11   53 my ($handle) = @_;
256 11         45 $cleanup->($handle);
257              
258 11         91 $sleep = $INIT_SLEEP;
259              
260 11         115 my ( $r, $w ) = portable_pipe();
261 11         544 my ( $r2, $w2 ) = portable_pipe();
262              
263 11         529 $handle->{r_h} = AnyEvent::Handle->new( fh => $r, );
264             $handle->{r_h}->on_error(
265             sub {
266 7         1809 my ( undef, undef, $error ) = @_;
267 7         66 $handle_error->( $handle, $error );
268             }
269 11         1433 );
270 11         89 $handle->{r_h_stderr} = AnyEvent::Handle->new( fh => $r2, );
271 11         621 $handle->{r_h_stderr}->on_error( sub { } );
  7         1106771  
272 11         61 $handle->{warn_on_stderr} = 1;
273              
274             # run stream-events with stdout connected to pipe ...
275 11         133 $handle->{cv} = run_cmd(
276             \@ssh,
277             '>' => $w,
278             '2>' => $w2,
279             '$$' => \$handle->{pid},
280             );
281             $handle->{cv}->cb(
282             sub {
283 7         8508 my ($status) = shift->recv();
284 7         123 $handle_error->( $handle, "ssh exited with status $status" );
285             }
286 11         44508 );
287              
288 11         416 my %read_req;
289             %read_req = (
290              
291             # read one json item at a time
292             json => sub {
293 11         551760 my ( $h, $data ) = @_;
294              
295             # every successful read resets sleep period
296 11         29 $sleep = $INIT_SLEEP;
297              
298 11         107 $h->push_read(%read_req);
299 11         554 $on_event->($data);
300             }
301 11         450 );
302 11         410 $handle->{r_h}->push_read(%read_req);
303              
304 11         3948 my %err_read_req;
305             %err_read_req = (
306             line => sub {
307 0         0 my ( $h, $line ) = @_;
308              
309 0 0       0 if ( $handle->{warn_on_stderr} ) {
310 0         0 warn __PACKAGE__ . ': ssh stderr: ' . $line;
311             }
312 0         0 $h->push_read(%err_read_req);
313             }
314 11         374 );
315 11         109 $handle->{r_h_stderr}->push_read(%err_read_req);
316 4         50 };
317              
318 4         8 my $stash = {};
319 4         20 $restart->($stash);
320              
321 4         684 my $out = { stash => $stash };
322 4 50       74 if ( defined wantarray ) {
323             $out->{guard} = guard {
324 4     4   6583 $cleanup->($stash);
325 4         139 };
326 4         13 $out_weak = $out;
327 4         30 weaken($out_weak);
328             }
329             else {
330 0         0 $out_weak = $out;
331             }
332 4         242 return $out;
333             }
334              
335             =item B<< for_each_patchset(ssh_url => $ssh_url, workdir => $workdir, ...) >>
336              
337             Set up a high-level event watcher to invoke a custom callback or
338             command for each existing or incoming patch set on Gerrit. This method
339             is suitable for performing automated testing or sanity checks on
340             incoming patches.
341              
342             For each patch set, a git repository is set up with the working tree
343             and HEAD set to the patch. The callback is invoked with the current
344             working directory set to the top level of this git repository.
345              
346             Returns a guard object. Event processing terminates when the object is
347             destroyed.
348              
349             Options:
350              
351             =over
352              
353             =item B
354              
355             The Gerrit ssh URL, e.g. C.
356             May also be specified as 'url' for backwards compatibility.
357             Mandatory.
358              
359             =item B
360              
361             =item B<< http_auth_cb => $sub->($response_headers, $request_headers) >>
362              
363             =item B
364              
365             =item B
366              
367             These arguments have the same meaning as for the L function.
368             Provide them if you want to post reviews via REST.
369              
370             =item B
371              
372             The top-level working directory under which git repositories and other data
373             should be stored. Mandatory. Will be created if it does not exist.
374              
375             The working directory is persistent across runs. Removing the
376             directory may cause the processing of patch sets which have already
377             been processed.
378              
379             =item B<< on_patchset => $sub->($change, $patchset) >>
380              
381             =item B<< on_patchset_fork => $sub->($change, $patchset) >>
382              
383             =item B<< on_patchset_cmd => $sub->($change, $patchset) | $cmd_ref >>
384              
385             Callbacks invoked for each patchset. Only one of the above callback
386             forms may be used.
387              
388             =over
389              
390             =item *
391              
392             B invokes a subroutine in the current process. The callback
393             is blocking, which means that only one patch may be processed at a
394             time. This is the simplest form and is suitable when the processing
395             for each patch is expected to be fast or the rate of incoming patches
396             is low.
397              
398             =item *
399              
400             B invokes a subroutine in a new child process. The
401             child terminates when the callback returns. Multiple patches may be
402             handled in parallel.
403              
404             The caveats which apply to C also apply here;
405             namely, it is not permitted to run the event loop in the child process.
406              
407             =item *
408              
409             B runs a command to handle the patch.
410             Multiple patches may be handled in parallel.
411              
412             The argument to B may be either a reference to an array
413             holding the command and its arguments, or a reference to a subroutine
414             which generates and returns an array for the command and its arguments.
415              
416             B since B has no way to return a value, it
417             can't be used to generate ReviewInput objects for REST-based reviewing.
418             See below.
419              
420             =back
421              
422             All on_patchset callbacks receive B and B hashref arguments.
423             Note that a change may hold several patchsets.
424              
425             Output and the returned value of the callbacks may be used for posting a review
426             back to Gerrit; see documentation of the C argument below.
427              
428             =item B<< on_error => $sub->($error) >>
429              
430             Callback invoked when an error occurs. $error is a human-readable error string.
431              
432             All errors are treated as recoverable. To abort on an error, explicitly undefine
433             the loop guard object from within the callback.
434              
435             By default, a warning message is printed for each error.
436              
437             =item B<< review => 0 | 1 | 'code-review' | 'verified' | ... >>
438              
439             If false (the default), patch sets are not automatically reviewed
440             (though they may be reviewed explicitly within the B
441             callbacks).
442              
443             If true, patch sets are automatically reviewed according to the following:
444              
445             =over
446              
447             =item *
448              
449             If an B callback returned a ReviewInput hashref, a review
450             is done via REST (see the L function). Note that B
451             doesn't support returning a value.
452              
453             =item *
454              
455             If an B callback printed text on stdout/stderr, a review
456             is done via SSH using the text as the top-level review message.
457              
458             =item *
459              
460             If both of the above are true, the review is attempted by REST first,
461             and then via SSH if the REST review failed. Therefore, writing a callback
462             to both print out a meaningful message I return a ReviewInput hashref
463             enables a script to be portable across Gerrit versions with and without REST.
464              
465             =back
466              
467             See the B discussion in the documentation for the L function
468             for more information on choosing between REST or SSH.
469              
470             If a string is passed, it is construed as a Gerrit approval category
471             and a review score will be posted in that category. The score comes
472             from the return value of the callback (or exit code in the case of
473             B). The returned value must be an integer, so this cannot
474             be combined with reviewing by REST, which requires callbacks to return a
475             hashref.
476              
477             =item B<< on_review => $sub->( $change, $patchset, $message, $score, $returnvalue ) >>
478              
479             Optional callback invoked prior to performing a review (when the `review'
480             option is set to a true value).
481              
482             The callback should return a true value if the review should be
483             posted, false otherwise. This may be useful for the implementation of
484             a dry-run mode.
485              
486             The callback may be invoked with an undefined $message and $score, which
487             indicates that a patchset was successfully processed but no message
488             or score was produced.
489              
490             =item B<< wanted => $sub->( $change, $patchset ) >>
491              
492             The optional `wanted' subroutine may be used to limit the patch sets processed.
493              
494             If given, a patchset will only be processed if this callback returns a
495             true value. This can be used to avoid git clones of unwanted projects.
496              
497             For example, patchsets for all Gerrit projects under a 'test/' namespace could
498             be excluded from processing by the following:
499              
500             wanted => sub { $_[0]->{project} !~ m{^test/} }
501              
502             =item B<< git_work_tree => 0 | 1 >>
503              
504             By default, while processing a patchset, a git work tree is set up
505             with content set to the appropriate revision.
506              
507             C<< git_work_tree => 0 >> may be passed to disable the work tree, saving
508             some time and disk space. In this case, a bare clone is used, with HEAD
509             referring to the revision to be processed.
510              
511             This may be useful when the patch set processing does not require a
512             work tree (e.g. the incoming patch is directly scanned).
513              
514             Defaults to 1.
515              
516             =item B<< query => $query | 0 >>
517              
518             The Gerrit query used to find the initial set of patches to be
519             processed. The query is executed when the loop begins and whenever
520             the connection to Gerrit is interrupted, to avoid missed patchsets.
521              
522             Defaults to "status:open", meaning every open patch will be processed.
523              
524             Note that the query is not applied to incoming patchsets observed via
525             stream-events. The B parameter may be used for that case.
526              
527             If a false value is passed, querying is disabled altogether. This
528             means only patchsets arriving while the loop is running will be
529             processed.
530              
531             =back
532              
533             =cut
534              
535             sub for_each_patchset {
536 3     3 1 958 my (%args) = @_;
537              
538 3   33     36 $args{ssh_url} ||= $args{url};
539 3 50 33     14 if ($args{http_username} && $args{http_password}) {
540 0   0     0 $args{http_auth_cb} ||= http_digest_auth($args{http_username}, $args{http_password});
541             }
542              
543 3 50       11 $args{ssh_url} || croak 'missing ssh_url argument';
544 3 50 100     23 $args{on_patchset}
      66        
545             || $args{on_patchset_cmd}
546             || $args{on_patchset_fork}
547             || croak 'missing on_patchset{_cmd,_fork} argument';
548 3 50       13 $args{workdir} || croak 'missing workdir argument';
549 3   50 12   44 $args{on_error} ||= sub { warn __PACKAGE__, ': ', @_ };
  12         1775  
550              
551 3 50       10 if ( !exists( $args{git_work_tree} ) ) {
552 3         11 $args{git_work_tree} = 1;
553             }
554              
555 3 50       11 if ( !exists( $args{query} ) ) {
556 0         0 $args{query} = 'status:open';
557             }
558              
559 3 50       83 if ( !-d $args{workdir} ) {
560 3         714 mkpath( $args{workdir} );
561             }
562              
563             # drop the path section of the URL to get base gerrit URL
564 3         45 my $url = URI->new($args{ssh_url});
565 3         10067 $url->path( undef );
566 3         326 $args{ssh_url} = $url->as_string();
567              
568 3         50 require "Gerrit/Client/ForEach.pm";
569 3         18 my $self = bless {}, 'Gerrit::Client::ForEach';
570 3         30 $self->{args} = \%args;
571              
572 3         10 my $weakself = $self;
573 3         13 weaken($weakself);
574              
575             # stream_events takes care of incoming changes, perform a query to find
576             # existing changes
577             my $do_query = sub {
578 6 50   6   1369 return unless $args{query};
579              
580             query(
581             $args{query},
582             ssh_url => $args{ssh_url},
583             current_patch_set => 1,
584 0         0 on_error => sub { $args{on_error}->(@_) },
585             on_success => sub {
586 6 50       2558 return unless $weakself;
587 6         26 my (@results) = @_;
588 6         20 foreach my $change (@results) {
589              
590             # simulate patch set creation
591 12         231 my ($event) = {
592             type => 'patchset-created',
593             change => $change,
594             patchSet => delete $change->{currentPatchSet},
595             };
596 12         207 $weakself->_handle_for_each_event($event);
597             }
598             },
599 6         485 );
600 3         26 };
601              
602             # Unfortunately, we have no idea how long it takes between starting the
603             # stream-events command and when the streaming of events begins, so if
604             # we query straight away, we could miss some changes which arrive while
605             # stream-events is e.g. still in ssh negotiation.
606             # Therefore, introduce this arbitrary delay between when we start
607             # stream-events and when we'll perform a query.
608 3         5 my $query_timer;
609             my $do_query_soon = sub {
610 15     15   176 $query_timer = AE::timer( 4, 0, $do_query );
611 3         19 };
612              
613             $self->{stream} = Gerrit::Client::stream_events(
614             ssh_url => $args{ssh_url},
615             on_event => sub {
616 9     9   108 $weakself->_handle_for_each_event(@_);
617             },
618             on_error => sub {
619 12     12   24 my ($error) = @_;
620              
621 12         88 $args{on_error}->("connection lost: $error, attempting to recover\n");
622              
623             # after a few seconds to allow reconnect, perform the base query again
624 12         56 $do_query_soon->();
625              
626 12         97 return 1;
627             },
628 3         78 );
629              
630 3         46 $do_query_soon->();
631              
632 3         81 return $self;
633             }
634              
635             =item B
636              
637             Returns a random Change-Id (the character 'I' followed by 40
638             hexadecimal digits), suitable for usage as the Change-Id field in a
639             commit to be pushed to gerrit.
640              
641             =cut
642              
643             sub random_change_id {
644 60         153 return 'I' . sprintf(
645              
646             # 40 hex digits, one 32 bit integer gives 8 hex digits,
647             # therefore 5 random integers
648             "%08x" x 5,
649 12     12 1 8153 map { rand() * ( 2**32 ) } ( 1 .. 5 )
650             );
651             }
652              
653             =item B
654              
655             Returns the 'next' Change-Id which should be used for a commit created
656             by the current git author/committer (which should be set by
657             L
658             prior to calling this method). The current working directory must be
659             within a git repository.
660              
661             This method is suitable for usage within a script which periodically
662             creates commits for review, but should have only one outstanding
663             review (per branch) at any given time. The returned Change-Id is
664             (hopefully) unique, and stable; it only changes when a new commit
665             arrives in the git repository from the current script.
666              
667             For example, consider a script which is run once per day to clone a
668             repository, generate a change and push it for review. If this function
669             is used to generate the Change-Id on the commit, the script will
670             update the same change in gerrit until that change is merged. Once the
671             change is merged, next_change_id returns a different value, resulting
672             in a new change. This ensures the script has a maximum of one pending
673             review any given time.
674              
675             If any problems occur while determining the next Change-Id, a warning
676             is printed and a random Change-Id is returned.
677              
678             =cut
679              
680             sub next_change_id {
681 9 100 66 9 1 144626 if ( !$ENV{GIT_AUTHOR_NAME} || !$ENV{GIT_AUTHOR_EMAIL} ) {
682 2         34 carp __PACKAGE__ . ': git environment is not set, using random Change-Id';
683 2         1007 return random_change_id();
684             }
685              
686             # First preference: change id is the last SHA used by this bot.
687 7         51 my $author = "$ENV{GIT_AUTHOR_NAME} <$ENV{GIT_AUTHOR_EMAIL}>";
688 7         116 my $change_id = _safeqx( @GIT, qw(rev-list -n1 --fixed-strings),
689             "--author=$author", 'HEAD' );
690 7 50       65 if ( my $error = $? ) {
691 0         0 carp __PACKAGE__ . qq{: no previous commits from "$author" were found};
692             }
693             else {
694 7         27 chomp $change_id;
695             }
696              
697             # Second preference: for a stable but random change-id, use hash of the
698             # bot name
699 7 100       75 if ( !$change_id ) {
700 3         115 my $tempfile = File::Temp->new(
701             'perl-Gerrit-Client-hash.XXXXXX',
702             TMPDIR => 1,
703             CLEANUP => 1
704             );
705 3         2725 $tempfile->printflush($author);
706 3         562 $change_id = _safeqx( @GIT, 'hash-object', "$tempfile" );
707 3 50       54 if ( my $error = $? ) {
708 0         0 carp __PACKAGE__ . qq{: git hash-object failed};
709             }
710             else {
711 3         34 chomp $change_id;
712             }
713             }
714              
715             # Check if we seem to have this change id already.
716             # This can happen if an author other than ourself has already used the
717             # change id.
718 7 50       1044 if ($change_id) {
719 7         109 my $found = _safeqx( @GIT, 'log', '-n1000', "--grep=I$change_id", 'HEAD' );
720 7 50 33     119 if ( !$? && $found ) {
721 0         0 carp __PACKAGE__ . qq{: desired Change-Id $change_id is already used};
722 0         0 undef $change_id;
723             }
724             }
725              
726 7 50       29 if ($change_id) {
727 7         104 return "I$change_id";
728             }
729              
730 0         0 carp __PACKAGE__ . q{: falling back to random Change-Id};
731              
732 0         0 return random_change_id();
733             }
734              
735             =item B<< git_environment(name => $name, email => $email,
736             author_only => [0|1] ) >>
737              
738             Returns a copy of %ENV modified suitably for the creation of git
739             commits by a script/bot.
740              
741             Options:
742              
743             =over
744              
745             =item B
746              
747             The human-readable name used for git commits. Mandatory.
748              
749             =item B
750              
751             The email address used for git commits. Mandatory.
752              
753             =item B
754              
755             If 1, the environment is only modified for the git I, and not
756             the git I. Depending on the gerrit setup, this may be
757             required to avoid complaints about missing "Forge Identity"
758             permissions.
759              
760             Defaults to 0.
761              
762             =back
763              
764             When generating commits for review in gerrit, this method may be used
765             in conjunction with L to ensure this bot has only one
766             outstanding change for review at any time, as in the following
767             example:
768              
769             local %ENV = git_environment(
770             name => 'Indent Bot',
771             email => 'indent-bot@example.com',
772             );
773              
774             # fix up indenting in all the .cpp files
775             (system('indent *.cpp') == 0) || die 'indent failed';
776              
777             # then commit and push them; commits are authored and committed by
778             # 'Indent Bot '. usage of next_change_id()
779             # ensures that this bot has a maximum of one outstanding change for
780             # review
781             my $message = "Fixed indentation\n\nChange-Id: ".next_change_id();
782             (system('git add -u *.cpp') == 0)
783             || die 'git add failed';
784             (system('git', 'commit', '-m', $message) == 0)
785             || die 'git commit failed';
786             (system('git push gerrit HEAD:refs/for/master') == 0)
787             || die 'git push failed';
788              
789             =cut
790              
791             sub git_environment {
792 2     2 1 1067 my (%options) = validate(
793             @_,
794             { name => 1,
795             email => 1,
796             author_only => 0,
797             }
798             );
799              
800 2         33 my %env = %ENV;
801              
802 2         8 $env{GIT_AUTHOR_NAME} = $options{name};
803 2         4 $env{GIT_AUTHOR_EMAIL} = $options{email};
804              
805 2 50       8 unless ( $options{author_only} ) {
806 2         5 $env{GIT_COMMITTER_NAME} = $options{name};
807 2         4 $env{GIT_COMMITTER_EMAIL} = $options{email};
808             }
809              
810 2         43 return %env;
811             }
812              
813             my @GERRIT_LABELS = qw(
814             code_review
815             sanity_review
816             verified
817             );
818              
819             # options to Gerrit::review which map directly to options to
820             # "ssh gerrit review ..."
821             my %GERRIT_REVIEW_OPTIONS = (
822             abandon => { type => BOOLEAN, default => 0 },
823             message => { type => SCALAR, default => undef },
824             project => { type => SCALAR, default => undef },
825             restore => { type => BOOLEAN, default => 0 },
826             stage => { type => BOOLEAN, default => 0 },
827             submit => { type => BOOLEAN, default => 0 },
828             ( map { $_ => { regex => qr{^[-+]?\d+$}, default => undef } }
829             @GERRIT_LABELS
830             )
831             );
832              
833             =item B<< review $revision, ssh_url => $ssh_url, http_url => $http_url ... >>
834              
835             Post a gerrit review, either by the `gerrit review' command using
836             ssh, or via REST.
837              
838             $revision is mandatory, and should be a git commit in full 40-digit form.
839             (Actually, a few other forms of $revision are accepted, but they are
840             deprecated - unabbreviated revisions are the only input which work for
841             both SSH and REST.)
842              
843             $ssh_url should be a URL with ssh schema referring to a
844             valid Gerrit installation (e.g. "ssh://user@gerrit.example.com:29418/").
845             The URL may optionally contain the relevant gerrit project.
846              
847             $http_url should be an HTTP or HTTPS URL pointing at the base of a Gerrit
848             installation (e.g. "https://gerrit.example.com/").
849              
850             At least one of $ssh_url or $http_url must be provided.
851              
852             =over
853              
854             B
855              
856             In Gerrit 2.6, it became possible to post reviews onto patch sets using
857             a new REST API. In earlier versions of gerrit, only the `gerrit review'
858             command may be used.
859              
860             Reviewing by REST has a significant advantage over `gerrit review':
861             comments may be posted directly onto the relevant lines of a patch.
862             `gerrit review' in contrast only supports setting a top-level message.
863              
864             Gerrit::Client supports posting review comments both by REST and
865             `gerrit review'. Most scripts will most likely prefer to use REST due
866             to the improved review granularity.
867              
868             To review by REST:
869              
870             =over
871              
872             =item *
873              
874             Provide the C argument, and most likely C and C.
875              
876             =item *
877              
878             Provide the C and C arguments.
879              
880             =item *
881              
882             Provide the C argument as a hash with the correct structure.
883             (See documentation below.)
884              
885             =back
886              
887             To review by SSH:
888              
889             =over
890              
891             =item *
892              
893             Provide the C argument.
894              
895             =item *
896              
897             Provide the C argument, and one or more of the score-related
898             arguments.
899              
900             =back
901              
902             To review by REST where supported, with fallback to SSH if REST
903             is unavailable (e.g on Gerrit < 2.6), provide all of the arguments
904             listed above. C will only be used if reviewing by REST was
905             not available.
906              
907             =back
908              
909             Other arguments to this method include:
910              
911             =over
912              
913             =item B<< http_auth_cb => $sub->($response_headers, $request_headers) >>
914              
915             A callback invoked when HTTP authentication to Gerrit is required.
916             The callback receives HTTP headers from the 401 Unauthorized response
917             in $response_headers, and should add corresponding request headers
918             (such as Authorization) into $request_headers.
919              
920             $response_headers also includes the usual AnyEvent::HTTP psuedo-headers,
921             and one additional psuedo-header, Method, which is the HTTP method being
922             used (i.e. "POST").
923              
924             The callback must return true if it was able to provide the necessary
925             authentication headers. It should return false if authentication failed.
926              
927             See also C, C and C,
928             to use the Digest-based authentication used by default in Gerrit.
929              
930             =item B
931              
932             =item B
933              
934             Passing these values is a shortcut for:
935              
936             http_auth_cb => Gerrit::Client::http_digest_auth($username, $password)
937              
938             ... which uses Gerrit's default Digest-based authentication.
939              
940             =item B<< reviewInput => $hashref >>
941              
942             A ReviewInput object as used by Gerrit's REST API.
943              
944             This is a simple data structure of the following form:
945              
946             {
947             message => "Some nits need to be fixed.",
948             labels => {
949             'Code-Review' => -1
950             },
951             comments => {
952             'gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java' => [
953             {
954             line => 23,
955             message => '[nit] trailing whitespace'
956             },
957             {
958             line => 49,
959             message => "[nit] s/conrtol/control"
960             }
961             ]
962             }
963             }
964              
965             This value is only used if reviewing via REST.
966              
967             =item B<< message => $string >>
968              
969             Top-level review message.
970              
971             This value is only used if reviewing via SSH, or if REST reviewing was
972             attempted but failed.
973              
974             =item B<< project => $string >>
975              
976             =item B<< branch => $string >>
977              
978             Project and branch values for the patch set to be reviewed.
979              
980             If reviewing via SSH, these are only necessary if the patch set can't
981             be unambiguously located by its git revision alone. If reviewing via
982             REST, these are always necessary.
983              
984             In any case, it's recommended to always provide these.
985              
986             =item B<< on_success => $cb->() >>
987              
988             =item B<< on_error => $cb->( $error ) >>
989              
990             Callbacks invoked when the operation succeeds or fails.
991              
992             =item B<< abandon => 1|0 >>
993              
994             =item B<< restore => 1|0 >>
995              
996             =item B<< stage => 1|0 >>
997              
998             =item B<< submit => 1|0 >>
999              
1000             =item B<< code_review => $number >>
1001              
1002             =item B<< sanity_review => $number >>
1003              
1004             =item B<< verified => $number >>
1005              
1006             Most of these options are passed to the `gerrit review' command. For
1007             information on their usage, please see the output of `gerrit review
1008             --help' on your gerrit installation, or see L
1009             documentation|http://gerrit.googlecode.com/svn/documentation/2.2.1/cmd-review.html>.
1010              
1011             Note that certain options can be disabled on a per-site basis.
1012             `gerrit review --help' will show only those options which are enabled
1013             on the given site.
1014              
1015             =back
1016              
1017             =cut
1018              
1019             sub review {
1020 0     0 1   my $commit_or_change = shift;
1021             my (%options) = validate(
1022             @_,
1023             { url => 0,
1024             ssh_url => 0,
1025             http_url => 0,
1026             http_auth_cb => 0,
1027             http_username => 0,
1028             http_password => 0,
1029             change => 0,
1030             branch => 0,
1031             reviewInput => { type => HASHREF, default => undef },
1032             on_success => { type => CODEREF, default => undef },
1033             on_error => {
1034             type => CODEREF,
1035             default => sub {
1036 0     0     warn __PACKAGE__ . "::review: error: ", @_;
1037             }
1038             },
1039 0           %GERRIT_REVIEW_OPTIONS,
1040             }
1041             );
1042              
1043 0   0       $options{ssh_url} ||= $options{url};
1044              
1045 0 0 0       if (!$options{ssh_url} && !$options{http_url}) {
1046 0           croak 'called without SSH or HTTP URL';
1047             }
1048              
1049 0 0 0       if (!$options{http_auth_cb} && $options{http_username} && $options{http_password}) {
      0        
1050 0           $options{http_auth_cb} = Gerrit::Client::http_digest_auth($options{http_username}, $options{http_password});
1051             }
1052              
1053 0 0         if ($options{ssh_url}) {
1054 0           $options{parsed_url} = _gerrit_parse_url( $options{ssh_url} );
1055             # project can be filled in by explicit 'project' argument, or from
1056             # URL, or left blank
1057 0   0       $options{project} ||= $options{parsed_url}{project};
1058 0 0         if ( !$options{project} ) {
1059 0           delete $options{project};
1060             }
1061             }
1062              
1063 0 0         if ($options{reviewInput}) {
1064 0           return _review_by_rest_then_ssh($commit_or_change, \%options)
1065             }
1066              
1067 0           return _review_by_ssh($commit_or_change, \%options);
1068             }
1069              
1070             sub _review_by_rest_then_ssh {
1071 0     0     my ($commit, $options) = @_;
1072              
1073 0           foreach my $opt (qw(project branch http_url change)) {
1074 0 0         if (!$options->{$opt}) {
1075 0           croak "Attempted to do review by REST with missing $opt argument";
1076             }
1077             }
1078              
1079 0 0         unless ($commit =~ /^[a-fA-F0-9]+$/) {
1080 0           croak "Invalid revision: $commit. For REST review, must specify a valid git revision";
1081             }
1082              
1083 0           my $data = $options->{reviewInput};
1084              
1085 0           my $jsondata = encode_json($data);
1086              
1087 0           my $url = join('/',
1088             $options->{http_url},
1089             'a',
1090             'changes',
1091             uri_escape(join('~', $options->{project}, $options->{branch}, $options->{change})),
1092             'revisions',
1093             $commit,
1094             'review',
1095             );
1096              
1097 0           _debug_print "Posting to $url ...\n";
1098              
1099 0           my $tried_auth = 0;
1100              
1101 0           my %post_args = (
1102             headers => { 'Content-Type' => 'application/json;charset=UTF-8' },
1103             recurse => 5,
1104             );
1105              
1106 0           my $handle_response;
1107             my $do_http_call;
1108              
1109             my $handle_no_rest = sub {
1110 0     0     _debug_print "Post to $url returned 404. Fall through to ssh...";
1111 0           return _review_by_ssh($commit, $options);
1112 0           };
1113              
1114             my $handle_ok = sub {
1115 0     0     _debug_print "Post to $url returned OK. Fall through to ssh...";
1116 0 0 0       if ($data->{message} || $data->{comments}) {
1117 0           delete $options->{message};
1118             }
1119 0 0         if ($data->{labels}) {
1120 0           foreach my $lbl (@GERRIT_LABELS) {
1121 0           delete $options->{$lbl};
1122             }
1123             }
1124              
1125 0           return _review_by_ssh($commit, $options);
1126 0           };
1127              
1128             my $handle_err = sub {
1129 0     0     my ($body, $hdr) = @_;
1130 0           my $errstr = "POST $url: $hdr->{Status} $hdr->{Reason}\n$body";
1131 0 0         if ($options->{on_error} ) {
1132 0           $options->{on_error}->($errstr);
1133             }
1134 0           _debug_print "POST to $url returned errors: " . Dumper($hdr);
1135 0           };
1136              
1137             my $handle_auth = sub {
1138 0     0     my ($body, $hdr) = @_;
1139              
1140 0           $tried_auth = 1;
1141              
1142 0           $hdr->{Method} = 'POST';
1143              
1144 0 0         if ($options->{http_auth_cb}->($hdr, $post_args{headers})) {
1145 0           _debug_print "Repeating POST to $url after auth";
1146 0           $do_http_call->();
1147             } else {
1148 0           warn __PACKAGE__ . ': HTTP authentication callback failed.';
1149 0           $handle_err->($body, $hdr);
1150             }
1151 0           };
1152              
1153 0 0         if (!$options->{http_auth_cb}) {
1154 0           $handle_auth = undef;
1155 0           _debug_print "No HTTP authentication callback.";
1156             }
1157              
1158             $handle_response = sub {
1159 0     0     my ($body, $hdr) = @_;
1160              
1161 0 0         if ($hdr->{Status} =~ /^2/) {
1162 0           $handle_ok->($body, $hdr);
1163 0           return;
1164             }
1165              
1166 0 0         if ($hdr->{Status} eq '404') {
1167 0           $handle_no_rest->($body, $hdr);
1168 0           return;
1169             }
1170              
1171 0 0 0       if ($hdr->{Status} eq '401' && !$tried_auth && $handle_auth) {
      0        
1172 0           $handle_auth->($body, $hdr);
1173 0           return;
1174             }
1175            
1176             # failed!
1177 0           $handle_err->($body, $hdr);
1178 0           };
1179              
1180 0     0     $do_http_call = sub { http_post($url, $jsondata, %post_args, $handle_response) };
  0            
1181 0           $do_http_call->();
1182            
1183 0           return;
1184             }
1185              
1186             sub _review_by_ssh {
1187 0     0     my ($commit_or_change, $options) = @_;
1188              
1189 0           my $parsed_url = $options->{parsed_url};
1190 0           my @cmd = ( @{ $parsed_url->{cmd} }, 'review', $commit_or_change, );
  0            
1191              
1192 0           while ( my ( $key, $spec ) = each %GERRIT_REVIEW_OPTIONS ) {
1193 0           my $value = $options->{$key};
1194              
1195             # code_review -> --code-review
1196 0           my $cmd_key = $key;
1197 0           $cmd_key =~ s{_}{-}g;
1198 0           $cmd_key = "--$cmd_key";
1199              
1200 0 0 0       if ( $spec->{type} && $spec->{type} eq BOOLEAN ) {
    0          
1201 0 0         if ($value) {
1202 0           push @cmd, $cmd_key;
1203             }
1204             }
1205             elsif ( defined($value) ) {
1206 0           push @cmd, $cmd_key, quote($value);
1207             }
1208             }
1209              
1210 0           my $cv = AnyEvent::Util::run_cmd( \@cmd );
1211              
1212 0           my $cmdstr;
1213             {
1214 0           local $LIST_SEPARATOR = '] [';
  0            
1215 0           $cmdstr = "[@cmd]";
1216             }
1217              
1218             $cv->cb(
1219             sub {
1220 0     0     my $status = shift->recv();
1221 0 0 0       if ( $status && $options->{on_error} ) {
1222 0           $options->{on_error}->("$cmdstr exited with status $status");
1223             }
1224 0 0 0       if ( !$status && $options->{on_success} ) {
1225 0           $options->{on_success}->();
1226             }
1227              
1228             # make sure we stay alive until this callback is executed
1229 0           undef $cv;
1230             }
1231 0           );
1232              
1233 0           return;
1234             }
1235              
1236             # options to Gerrit::Client::query which map directly to options to
1237             # "ssh gerrit query ..."
1238             my %GERRIT_QUERY_OPTIONS = (
1239             ( map { $_ => { type => BOOLEAN, default => 0 } }
1240             qw(
1241             all_approvals
1242             comments
1243             commit_message
1244             current_patch_set
1245             dependencies
1246             files
1247             patch_sets
1248             submit_records
1249             )
1250             )
1251             );
1252              
1253             =item B<< query $query, ssh_url => $gerrit_url, ... >>
1254              
1255             Wrapper for the `gerrit query' command; send a query to gerrit
1256             and invoke a callback with the results.
1257              
1258             $query is the Gerrit query string, whose format is described in L
1259             Gerrit
1260             documentation|https://gerrit.googlecode.com/svn/documentation/2.2.1/user-search.html>.
1261             "status:open age:1w" is an example of a simple Gerrit query.
1262              
1263             $gerrit_url is the URL with ssh schema of the Gerrit site to be queried
1264             (e.g. "ssh://user@gerrit.example.com:29418/").
1265             If the URL contains a path (project) component, it is ignored.
1266              
1267             All other arguments are optional, and include:
1268              
1269             =over
1270              
1271             =item B<< on_success => $cb->( @results ) >>
1272              
1273             Callback invoked when the query completes.
1274              
1275             Each element of @results is a hashref representing a Gerrit change,
1276             parsed from the JSON output of `gerrit query'. The format of Gerrit
1277             change objects is described in L
1278             https://gerrit.googlecode.com/svn/documentation/2.2.1/json.html>.
1279              
1280             =item B<< on_error => $cb->( $error ) >>
1281              
1282             Callback invoked when the query command fails.
1283             $error is a human-readable string describing the error.
1284              
1285             =item B<< all_approvals => 0|1 >>
1286              
1287             =item B<< comments => 0|1 >>
1288              
1289             =item B<< commit_message => 0|1 >>
1290              
1291             =item B<< current_patch_set => 0|1 >>
1292              
1293             =item B<< dependencies => 0|1 >>
1294              
1295             =item B<< files => 0|1 >>
1296              
1297             =item B<< patch_sets => 0|1 >>
1298              
1299             =item B<< submit_records => 0|1 >>
1300              
1301             These options are passed to the `gerrit query' command and may be used
1302             to increase the level of information returned by the query.
1303             For information on their usage, please see the output of `gerrit query
1304             --help' on your gerrit installation, or see L
1305             documentation|http://gerrit.googlecode.com/svn/documentation/2.2.1/cmd-query.html>.
1306              
1307             =back
1308              
1309             =cut
1310              
1311             sub query {
1312 0     0 1   my $query = shift;
1313             my (%options) = validate(
1314             @_,
1315             { url => 0,
1316             ssh_url => 0,
1317             on_success => { type => CODEREF, default => undef },
1318             on_error => {
1319             type => CODEREF,
1320             default => sub {
1321 0     0     warn __PACKAGE__ . "::query: error: ", @_;
1322             }
1323             },
1324 0           %GERRIT_QUERY_OPTIONS,
1325             }
1326             );
1327              
1328 0   0       $options{ssh_url} ||= $options{url};
1329              
1330 0           my $parsed_url = _gerrit_parse_url( $options{ssh_url} );
1331 0           my @cmd = ( @{ $parsed_url->{cmd} }, 'query', '--format', 'json' );
  0            
1332              
1333 0           while ( my ( $key, $spec ) = each %GERRIT_QUERY_OPTIONS ) {
1334 0           my $value = $options{$key};
1335 0 0         next unless $value;
1336              
1337             # some_option -> --some-option
1338 0           my $cmd_key = $key;
1339 0           $cmd_key =~ s{_}{-}g;
1340 0           $cmd_key = "--$cmd_key";
1341              
1342 0           push @cmd, $cmd_key;
1343             }
1344              
1345 0           push @cmd, quote($query);
1346              
1347 0           my $output;
1348 0           my $cv = AnyEvent::Util::run_cmd( \@cmd, '>' => \$output );
1349              
1350 0           my $cmdstr;
1351             {
1352 0           local $LIST_SEPARATOR = '] [';
  0            
1353 0           $cmdstr = "[@cmd]";
1354             }
1355              
1356             $cv->cb(
1357             sub {
1358             # make sure we stay alive until this callback is executed
1359 0     0     undef $cv;
1360              
1361 0           my $status = shift->recv();
1362 0 0 0       if ( $status && $options{on_error} ) {
1363 0           $options{on_error}->("$cmdstr exited with status $status");
1364 0           return;
1365             }
1366              
1367 0 0         return unless $options{on_success};
1368              
1369 0           my @results;
1370 0           foreach my $line ( split /\n/, $output ) {
1371 0           my $data = eval { decode_json($line) };
  0            
1372 0 0         if ($EVAL_ERROR) {
1373 0           $options{on_error}->("error parsing result `$line': $EVAL_ERROR");
1374 0           return;
1375             }
1376 0 0 0       next if ( $data->{type} && $data->{type} eq 'stats' );
1377 0           push @results, $data;
1378             }
1379              
1380 0           $options{on_success}->(@results);
1381 0           return;
1382             }
1383 0           );
1384              
1385 0           return;
1386             }
1387              
1388             =item B<< quote $string >>
1389              
1390             Returns a copy of the input string with special characters escaped, suitable
1391             for usage with Gerrit CLI commands.
1392              
1393             Gerrit commands run via ssh typically need extra quoting because the ssh layer
1394             already evaluates the command string prior to passing it to Gerrit.
1395             This function understands how to quote arguments for this case.
1396              
1397             B do not use this function for passing arguments to other Gerrit::Client
1398             functions; those perform appropriate quoting internally.
1399              
1400             =cut
1401              
1402             sub quote {
1403 0     0 1   my ($string) = @_;
1404              
1405             # character set comes from gerrit source:
1406             # gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java
1407             # 'split' function
1408 0           $string =~ s{([\t "'\\])}{\\$1}g;
1409 0           return $string;
1410             }
1411              
1412             =item B<< http_digest_auth($username, $password) >>
1413              
1414             Returns a callback to be used with REST-related Gerrit::Client functions.
1415             The callback enables Digest-based HTTP authentication with the given
1416             credentials.
1417              
1418             Note that only the Digest scheme used by Gerrit (as of 2.8) is supported:
1419             algorithm = MD5, qop = auth.
1420              
1421             =cut
1422             sub http_digest_auth {
1423 0     0 1   my ($username, $password, %args) = @_;
1424              
1425             my $cnonce_cb = $args{cnonce_cb} || sub {
1426 0     0     sprintf("%08x", rand() * ( 2**32 ));
1427 0   0       };
1428              
1429 0           my %noncecount;
1430              
1431             return sub {
1432 0     0     my ($in_headers, $out_headers) = @_;
1433              
1434 0           my $authenticate = $in_headers->{'www-authenticate'};
1435 0 0 0       if (!$authenticate || !($authenticate =~ /^Digest /)) {
1436 0           warn __PACKAGE__ . ': server did not offer digest authentication';
1437 0           return;
1438             }
1439              
1440 0           $authenticate =~ s/^Digest //;
1441              
1442 0           my %attr;
1443 0           while ($authenticate =~ /([a-zA-Z0-9\-]+)="([^"]+)"(,\s*)?/g) {
1444 0           $attr{$1} = $2;
1445             }
1446              
1447 0 0         if ($attr{qop}) {
1448 0           $attr{qop} = [ split(/,/, $attr{qop}) ];
1449             }
1450              
1451 0   0       $attr{algorithm} ||= 'MD5';
1452 0   0       $attr{qop} ||= [];
1453              
1454 0           _debug_print "digest attrs with defaults filled: " . Dumper(\%attr);
1455              
1456 0 0         unless (grep {$_ eq 'auth'} @{$attr{qop}}) {
  0            
  0            
1457 0           warn __PACKAGE__ . ": server didn't offer qop=auth for digest authentication";
1458 0           return;
1459             }
1460              
1461 0 0         unless ($attr{algorithm} eq 'MD5') {
1462 0           warn __PACKAGE__ . ": server didn't offer algorithm=MD5 for digest authentication";
1463             }
1464              
1465 0           my $nonce = $attr{nonce};
1466 0           my $cnonce = $cnonce_cb->();
1467              
1468 0   0       $noncecount{$nonce} = ($noncecount{$nonce}||0) + 1;
1469 0           my $count = $noncecount{$nonce};
1470 0           my $count_hex = sprintf("%08x", $count);
1471              
1472 0           my $uri = URI->new($in_headers->{URL})->path;
1473 0           my $method = $in_headers->{Method};
1474 0           _debug_print "uri $uri method $method\n";
1475              
1476 0           my $ha1 = md5_hex($username, ':', $attr{realm}, ':', $password);
1477 0           my $ha2 = md5_hex($method, ':', $uri);
1478 0           my $response = md5_hex($ha1, ':', $nonce, ':', $count_hex, ':', $cnonce, ':', 'auth', ':', $ha2);
1479              
1480 0           my $authstr = qq(Digest username="$username", realm="$attr{realm}", nonce="$nonce", uri="$uri", )
1481             .qq(cnonce="$cnonce", nc=$count_hex, qop="auth", response="$response");
1482            
1483 0           _debug_print "digest auth: $authstr\n";
1484              
1485 0           $out_headers->{Authorization} = $authstr;
1486 0           return 1;
1487             }
1488 0           }
1489              
1490             =back
1491              
1492             =head1 VARIABLES
1493              
1494             =over
1495              
1496             =item B<@Gerrit::Client::SSH>
1497              
1498             The ssh command and initial arguments used when Gerrit::Client invokes
1499             ssh.
1500              
1501             # force IPv6 for this connection
1502             local @Gerrit::Client::SSH = ('ssh', '-oAddressFamily=inet6');
1503             my $stream = Gerrit::Client::stream_events ...
1504              
1505             The default value is C<('ssh')>.
1506              
1507             =item B<@Gerrit::Client::GIT>
1508              
1509             The git command and initial arguments used when Gerrit::Client invokes
1510             git.
1511              
1512             # use a local object cache to speed up initial clones
1513             local @Gerrit::Client::GIT = ('env', "GIT_ALTERNATE_OBJECT_DIRECTORIES=$ENV{HOME}/gitcache", 'git');
1514             my $guard = Gerrit::Client::for_each_patchset ...
1515              
1516             The default value is C<('git')>.
1517              
1518             =item B<$Gerrit::Client::MAX_CONNECTIONS>
1519              
1520             Maximum number of simultaneous git connections Gerrit::Client may make
1521             to a single Gerrit server. The amount of parallel git clones and
1522             fetches should be throttled, otherwise the Gerrit server may drop
1523             incoming connections.
1524              
1525             The default value is C<2>.
1526              
1527             =item B<$Gerrit::Client::MAX_FORKS>
1528              
1529             Maximum number of processes allowed to run simultaneously for handling
1530             of patchsets in for_each_patchset. This limit applies only to local
1531             work processes, not git clones or fetches from gerrit.
1532              
1533             Note that C<$AnyEvent::Util::MAX_FORKS> may also impact the maximum number
1534             of processes. C<$AnyEvent::Util::MAX_FORKS> should be set higher than or
1535             equal to C<$Gerrit::Client::MAX_FORKS>.
1536              
1537             The default value is C<4>.
1538              
1539             =item B<$Gerrit::Client::DEBUG>
1540              
1541             If set to a true value, various debugging messages will be printed to
1542             standard error. May be set by the GERRIT_CLIENT_DEBUG environment
1543             variable.
1544              
1545             =back
1546              
1547             =head1 AUTHOR
1548              
1549             Rohan McGovern,
1550              
1551             =head1 COMPATIBILITY
1552              
1553             Gerrit::Client has been known to work with Gerrit 2.2.2.1, Gerrit 2.6-rc1,
1554             and Gerrit 2.8.5 and hence could reasonably be expected to work with any
1555             gerrit version in that range.
1556              
1557             Please note that different Gerrit versions may represent objects in
1558             slightly incompatible ways (e.g. "CRVW" vs "Code-Review" strings in
1559             event objects). Gerrit::Client does not insulate the caller against
1560             these changes.
1561              
1562             =head1 BUGS
1563              
1564             Please use L
1565             to view or report bugs.
1566              
1567             When reporting a reproducible bug, please include the output of your
1568             program with the environment variable GERRIT_CLIENT_DEBUG set to 1.
1569              
1570             =head1 LICENSE AND COPYRIGHT
1571              
1572             Copyright (C) 2012-2014 Rohan McGovern
1573              
1574             Copyright (C) 2012 Digia Plc and/or its subsidiary(-ies)
1575              
1576             This program is free software; you can redistribute it and/or modify
1577             it under the terms of the GNU Lesser General Public License version
1578             2.1 as published by the Free Software Foundation.
1579              
1580             This program is distributed in the hope that it will be useful, but
1581             WITHOUT ANY WARRANTY; without even the implied warranty of
1582             MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
1583             Lesser General Public License for more details.
1584              
1585             You should have received a copy of the GNU Lesser General Public
1586             License along with this program; if not, write to the Free Software
1587             Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
1588             USA.
1589              
1590             =cut
1591              
1592             1;