File Coverage

blib/lib/Git/Deploy.pm
Criterion Covered Total %
statement 45 619 7.2
branch 0 412 0.0
condition 0 201 0.0
subroutine 15 75 20.0
pod 0 53 0.0
total 60 1360 4.4


line stmt bran cond sub pod time code
1             package Git::Deploy;
2              
3 1     1   2146 use strict;
  1         3  
  1         5920  
4 1     1   7 use warnings;
  1         3  
  1         37  
5 1     1   6 use Exporter;
  1         2  
  1         59  
6              
7             # generic utilities we use
8 1     1   5 use POSIX qw(strftime);
  1         2  
  1         9  
9 1     1   76 use Carp qw(confess);
  1         2  
  1         48  
10 1     1   1323 use Sys::Hostname qw(hostname);
  1         2839  
  1         90  
11 1     1   10 use Fcntl qw(:DEFAULT :flock);
  1         2  
  1         833  
12 1     1   7 use Cwd qw(cwd abs_path);
  1         2  
  1         61  
13 1     1   7 use File::Spec::Functions qw(catdir);
  1         2  
  1         55  
14 1     1   7 use Git::Deploy::Timing qw(push_timings);
  1         3  
  1         57  
15 1     1   12 use Git::Deploy::Say;
  1         1  
  1         242  
16 1     1   1798 use Data::Dumper;
  1         10682  
  1         2422  
17              
18             our $VERSION = '6.01'; # VERSION: generated by DZP::OurPkgVersion
19              
20             our @ISA= qw(Exporter);
21              
22             our @EXPORT= qw(
23             $DEBUG
24             $SKIP_HOOKS
25             $VERBOSE
26              
27             check_if_working_dir_is_clean
28             check_for_unpushed_commits
29             check_rollouts_blocked
30             clear_ref_info
31             fetch
32             fetch_tag_info
33             fetch_tags
34             filter_names_by_date
35             filter_names_matching_head
36             find_refs_matching_head
37             find_tags_matching_head
38             get_branches
39             get_commit_for_name
40             get_config
41             get_config_int
42             get_config_path
43             get_config_bool
44             get_current_branch
45             get_deploy_file_name
46             get_hook_dir
47             get_ref_info
48             get_sha1_for_name
49             get_sorted_list_of_tags
50             git_cmd
51             git_errorcode
52             git_result
53             is_name_annotated_tag
54             make_dated_tag
55             make_tag
56             parse_rollout_status
57             print_refs
58             pull
59             push_all
60             push_remote
61             push_tag
62             push_tags
63             read_deploy_file
64             read_rollout_status
65             remote
66             store_tag_info
67             unlink_rollout_status_file
68             what_branches_can_reach_head
69             write_deploy_file
70             write_rollout_status
71             execute_deploy_hooks
72             execute_log_hooks
73             process_deploy_hooks
74             execute_hook
75             get_hook
76             get_sync_hook
77              
78             _slurp
79             init_gitdir
80             log_directory
81             reset_to_name
82              
83             _expand_template_variables
84             );
85              
86             our $DEBUG = $ENV{GIT_DEPLOY_DEBUG} || 0;
87             our $SKIP_HOOKS;
88             our $VERBOSE;
89              
90              
91             my $gitdir;
92             sub init_gitdir {
93 0 0   0 0   return $gitdir if $gitdir;
94             # test that we actually are in a git repository before we do anything non argument processing related
95 0           $gitdir= git_result( 'git rev-parse --git-dir', 128 );
96 0 0 0       _die "current working directory is not part of a git repository\n"
97             if !$gitdir
98             or $gitdir =~ /Not a git repository/;
99              
100             # XXX: Assume the root of the workdir is the parent of the gitdir
101             # change directory to the root dir of the tree, so that we have a normalized
102             # perspective of the repo (so .deploy and similar things end up in the expected
103             # place regardless of where the tool was run from).
104 0 0         chdir "$gitdir/.."
105             or _die "Failed to chdir to root of git working tree:'$gitdir/..': $!";
106 0           return $gitdir;
107             }
108              
109              
110             # execute a command and capture and return both its output result and its error code
111             sub git_cmd {
112 0     0 0   my $cmd= shift;
113              
114             # Hack because we don't want to log in the _die, _info and _warn
115             # calls below because that'll call Git::Deploy::log_directory(),
116             # which will call Git::Deploy::_get_config() which will call us
117             # again.
118 0 0         local $Git::Deploy::Say::SKIP_LOGING_DUE_TO_DEEP_RECURSION_WITH_GIT_DEPLOY_DEBUG = 1 if $DEBUG;
119              
120 0           $cmd .= " 2>&1";
121 0           my $res= `$cmd`;
122 0           my $error_code= $?;
123 0 0         if ( $error_code == -1 ) {
    0          
124 0           _die "failed to execute '$cmd': $!\n";
125             }
126             elsif ( $error_code & 127 ) {
127 0 0         _die sprintf "'$cmd' died with signal %d, %s coredump\n%s", ( $error_code & 127 ),
128             ( $error_code & 128 ) ? 'with' : 'without', $res;
129             }
130 0 0         if ($DEBUG) {
131 0           _info $cmd;
132 0 0         _warn "got error code: $error_code"
133             if $error_code;
134 0           _info "result: $res";
135             }
136 0 0         chomp($res) if defined $res;
137 0           return ( $res, $error_code >> 8 );
138             }
139              
140             #execute a command and return what it output
141             sub git_result {
142 0     0 0   my ( $cmd, @accept )= @_;
143 0           my ( $res, $error_code )= git_cmd($cmd);
144 0 0 0       if ( $error_code and !grep { $error_code == $_ } @accept ) {
  0            
145 0           _die sprintf "'$cmd' resulted in an unexpected exit code: %d\n%s", $error_code, $res;
146             }
147 0           return $res;
148             }
149              
150             BEGIN {
151 1     1   3 my $config_prefix= "deploy";
152 1         2 my %config;
153              
154             my $config_file;
155 0         0 my $repo_name;
156 0         0 my $repo_name_detection;
157             my %repo_name_detection_values = (
158             'dot-git-parent-dir' => sub {
159 0         0 my $cwd = cwd();
160 0         0 $cwd =~ s[.*/][];
161 0         0 return $cwd;
162             }
163 1         2417 );
164              
165             # _get_config($opts,$setting) # setting is mandatory!
166             # _get_config($opts,$setting,$default); # setting will default to $default
167             # $setting may either be a *fully* qualified setting name like "user.name" otherwise
168             # if $setting does not contain a period it will become "$config_prefix.$setting"
169             # if $setting _starts_ with a period it will become "$config_prefix.$setting" as well.
170             # $opts is any additional arguments to feed to git-config
171             # Note that if the setting "$config_prefix.config-file" is set then we will always
172             # check it first when looking up values that start with $config_prefix (others we wont bother).
173              
174             sub _get_config {
175 0 0   0     if (!defined $config_file) {
176             # on first run we check to see if there is a deploy.config-file specified
177 0           $config_file= ""; # prevent infinite loops
178 0           $config_file= _get_config("--path","$config_prefix.config-file",""); # and now we read this from the normal configs
179             }
180 0 0 0       if (defined $config_file
      0        
181             and $config_file ne ''
182             and !defined $repo_name_detection) {
183 0           $repo_name_detection = '';
184 0           $repo_name_detection = _get_config("--path","$config_prefix.repo-name-detection","");
185 0 0         if ($repo_name_detection) {
186 0 0         _die "The detection method <$repo_name_detection> is invalid"
187             unless my $detect = $repo_name_detection_values{$repo_name_detection};
188              
189 0           $repo_name = $detect->();
190             }
191             }
192 0           my $opts= shift;
193 0           my $setting= shift;
194 0           my $has_default= @_;
195 0           my $default= shift;
196 0 0         if ( $setting =~ m/^\./ ) {
    0          
197 0           $setting= $config_prefix . $setting;
198             } elsif ( $setting !~ m/\./ ) {
199 0           $setting= "$config_prefix.$setting";
200             }
201 0 0         unless ( exists $config{$setting}{$opts} ) {
202             # If we have a $config_file specified and we are looking for a $config_prefix
203             # config item we will want to look first in the config file, and only then look
204             # in the normal git config files if there is nothing specified in the $config_file.
205              
206 0           my @setting_internal_name = $setting;
207 0 0 0       if ($setting=~/^\Q$config_prefix\E\./ and $repo_name) {
208 0           my $repo_name_setting = $setting;
209 0           $repo_name_setting =~ s/^\Q$config_prefix\E\./${config_prefix}.repository $repo_name./;
210 0           unshift @setting_internal_name => $repo_name_setting;
211             }
212             SETTING_NAME:
213 0           for my $setting_internal_name (@setting_internal_name) {
214 0           my $last = $setting_internal_name eq $setting_internal_name[-1];
215             CONF_SOURCE:
216 0 0 0       foreach my $source (
217             ($config_file && $setting=~/^\Q$config_prefix\E\./)
218             ? ("--file $config_file","")
219             : ("")
220             ) {
221 0           my $cmd= "git config $source --get $opts '$setting_internal_name'";
222 0           my ($res,$error_code)= git_cmd($cmd);
223 0 0         if ($error_code == 1) {
    0          
    0          
    0          
    0          
224 0 0         if ($source=~/--file/) { # missing from our config file, but the rest?
    0          
225 0           next CONF_SOURCE;
226             } elsif ($has_default) {
227 0           $res= $default;
228             } else {
229 0 0         _die "Missing mandatory config setting $setting (internal name $setting_internal_name)" if $last;
230             }
231             } elsif ($error_code == 2) {
232 0           _die "Bad config, multiple entries from $cmd: $res";
233             } elsif ($error_code == 255) {
234 0           _die "Bad config value, maybe change '_' to '-'?";
235             } elsif ($error_code) {
236 0           _die "Got unexpected error code $error_code from $cmd: $res";
237             } elsif ( $res =~ m/\A\s*`\s*(.*)\s*`\s*\z/ ) {
238 0           my $opt_cmd= $1;
239 0           ($res, $error_code)= git_cmd($opt_cmd);
240 0 0         if ( $error_code ) {
241 0           die "config option $setting_internal_name = $res returned a non-zero exit code: $!\n";
242             }
243             }
244 0           $config{$setting}{$opts}= $res;
245              
246 0 0 0       last SETTING_NAME if !$error_code and $res;
247 0           last CONF_SOURCE;
248             }
249             }
250             }
251 0           return $config{$setting}{$opts};
252             }
253              
254 0     0 0   sub get_config { return _get_config("",@_) }
255 0     0 0   sub get_config_path { return _get_config("--path",@_) }
256 0     0 0   sub get_config_int { return _get_config("--int",@_) }
257 0     0 0   sub get_config_bool { return 'true' eq _get_config("--bool",@_) }
258              
259             }
260              
261              
262              
263             #execute a command and return its error code
264             sub git_errcode {
265 0     0 0   my ( $cmd, )= @_;
266 0           my ( $res, $error_code )= git_cmd($cmd);
267 0           return $error_code;
268             }
269              
270              
271             { # lexical scope for the definition of locally static variables. Not just static in the sense
272             # of C static vars, but also static in the sense the var is not modifiable once defined.
273             my @gfer_names= (
274             '%(*author)', '%(*authordate:iso)', '%(*authoremail)', '%(*authorname)',
275             '%(*body)', '%(*committer)', '%(*committerdate:iso)', '%(*committeremail)',
276             '%(*committername)', '%(*contents)', '%(*objectname)', '%(*parent)',
277             '%(*subject)', '%(*tree)', '%(author)', '%(authordate:iso)',
278             '%(authoremail)', '%(authorname)', '%(body)', '%(committer)',
279             '%(committerdate:iso)', '%(committeremail)', '%(committername)', '%(contents)',
280             '%(objectname)', '%(parent)', '%(refname)', '%(subject)',
281             '%(tag)', '%(tree)',
282             );
283             my %gfer_fields= map { $gfer_names[$_] => $_ } 0 .. $#gfer_names;
284             my $gfer_format= join( "%01%01%01", @gfer_names ) . "%00%00%00";
285              
286             my $ref_info;
287             my $ref_info_loaded;
288              
289             sub clear_ref_info {
290 0     0 0   _info "Clearing ref info\n";
291 0           undef $ref_info;
292             }
293              
294             sub get_ref_info {
295              
296             #my $repo= shift;
297 0 0   0 0   return $ref_info if $ref_info_loaded;
298 0           undef $ref_info;
299 0 0         _info "reading tag and branch info - this might take a second or two.\n"
300             if $DEBUG;
301              
302 0           push_timings("gdt_internal__get_ref_info__git_for_each_ref__start");
303 0           my $start_time= time;
304 0           my $generated_code= `git for-each-ref --format '$gfer_format'`;
305 0           push_timings("gdt_internal__get_ref_info__git_for_each_ref__end");
306              
307 0           my $elapsed= time - $start_time;
308 0 0         _info "git for-each-ref took $elapsed seconds\n" if $DEBUG;
309              
310             #print "git for-each-ref --perl --format '$gfer_format'\n";
311 0 0         if ( !$generated_code ) {
312 0           _die "No refs were returned from git for-each-ref (which shouldn't be possible)\n";
313             }
314              
315 0 0         _info "processing result\n" if $DEBUG;
316 0           $start_time= time;
317 0           push_timings("gdt_internal__get_ref_info__process_ref_info__start");
318              
319 0           my %ref;
320             my %commit;
321              
322             # seems gfer adds a newline each record
323 0           foreach my $chunk ( split /\x00\x00\x00\n?/, $generated_code ) {
324 0           my %info;
325 0           @info{@gfer_names}= split /\x01\x01\x01/, $chunk;
326              
327 0           local $_= $info{'%(refname)'};
328 0           ( my $typename= $_ ) =~ s!^refs/!!;
329 0 0 0       my %ref_data= (
    0          
    0          
    0          
    0          
    0          
330             commit => $info{'%(*objectname)'} || $info{'%(objectname)'},
331             refname => $info{'%(refname)'},
332             typename => $typename, (
333             s!^refs/(heads)/!!
334             ? (
335             refsdir => $1,
336             category => "branch",
337             type => "local",
338             barename => $_
339             )
340             : s!^refs/(remotes)/!! ? (
341             refsdir => $1,
342             category => "branch",
343             type => "remote",
344             barename => $_
345             )
346             : s!^refs/(tags)/!! ? (
347             refsdir => $1,
348             category => "tag",
349             $info{'%(tag)'}
350             ? ( type => "object", barename => $info{'%(tag)'} )
351             : ( type => "symbolic", barename => $_ ) )
352             : s!^refs/(stash)!! ? (
353             refsdir => $1,
354             category => "stash",
355             type => "stash",
356             barename => $_
357             )
358             : s!^refs/(bisect)!! ? (
359             refsdir => $1,
360             category => "bisect",
361             type => "bisect",
362             barename => $_
363             )
364             : _die "Cant parse type from refname: ",
365             Dumper( \%info ) ) );
366 0           my $commitname;
367 0 0 0       if ( $ref_data{category} eq "tag" and $ref_data{type} eq "object" ) {
368 0           $ref_data{sha1}= $info{'%(objectname)'};
369 0           $ref_data{message}= {
370             body => $info{'%(body)'},
371             subject => $info{'%(subject)'},
372             contents => $info{'%(contents)'} };
373 0           $commitname= $info{'%(*objectname)'};
374 0   0       $commit{$commitname} ||= {
375             sha1 => $info{'%(*objectname)'},
376             author => {
377             author => $info{'%(*author)'},
378             date => $info{'%(*authordate:iso)'},
379             email => $info{'%(*authoremail)'},
380             name => $info{'%(*authorname)'}
381             },
382             committer => {
383             committer => $info{'%(*committer)'},
384             date => $info{'%(*committerdate:iso)'},
385             email => $info{'%(*committeremail)'},
386             name => $info{'%(*committername)'}
387             },
388             parent => [ split /\s+/, $info{'%(*parent)'} ],
389             tree => $info{'%(*tree)'},
390             message => {
391             body => $info{'%(*body)'},
392             subject => $info{'%(*subject)'},
393             contents => $info{'%(*contents)'}
394             },
395             };
396             }
397             else {
398 0           $commitname= $info{'%(objectname)'};
399 0   0       $commit{$commitname} ||= {
400             sha1 => $info{'%(objectname)'},
401             author => {
402             author => $info{'%(author)'},
403             date => $info{'%(authordate:iso)'},
404             email => $info{'%(authoremail)'},
405             name => $info{'%(authorname)'}
406             },
407             committer => {
408             committer => $info{'%(committer)'},
409             date => $info{'%(committerdate:iso)'},
410             email => $info{'%(committeremail)'},
411             name => $info{'%(committername)'}
412             },
413             parent => [ split /\s+/, $info{'%(parent)'} ],
414             tree => $info{'%(tree)'},
415             message => {
416             body => $info{'%(body)'},
417             subject => $info{'%(subject)'},
418             contents => $info{'%(contents)'}
419             },
420             };
421             }
422 0           $ref{all}{$typename}= \%ref_data;
423 0           $ref{ $ref_data{category} }{ $ref_data{type} }{ $ref_data{barename} }= \%ref_data;
424 0           push @{ $commit{$commitname}{refs} }, $typename;
  0            
425             }
426 0           push_timings("gdt_internal__get_ref_info__process_ref_info__end");
427              
428 0           $elapsed= time - $start_time;
429 0 0         _info "processing ref data took $elapsed seconds\n", "returning from ref_info\n"
430             if $DEBUG;
431 0           $ref_info_loaded= 1;
432 0           return $ref_info= { refs => \%ref, commit => \%commit };
433             }
434              
435              
436              
437             sub _get_name_data {
438 0     0     my ($name)= @_;
439 0 0         return if $name eq 'HEAD';
440 0           my $ri= get_ref_info();
441 0           my $all= $ri->{refs}{all};
442             return
443 0   0       $all->{$name}
444             || $all->{"tags/$name"}
445             || $all->{"heads/$name"}
446             || $all->{"remotes/$name"};
447             }
448             }
449              
450             # $commit_sha1= get_commit_for_name($name)
451             # $sha1= get_sha1_for_name($name)
452             #
453             # These two routines are very similar, and in most cases return the exact same result.
454             # They differ for tags however. A lightweight tag will return the same commit id for both.
455             # An annotated tag will return the tag's id for get_sha1_for_name() and will return the
456             # commit id it points at from get_commit_for_name(). This is one way to distinguish the
457             # two types of tags (of course there are other ways).
458             #
459             #
460              
461             BEGIN {
462 1     1   3 my %name2commit;
463              
464             sub get_commit_for_name {
465 0     0 0   my ($name)= @_;
466 0 0         return '' if !$name;
467 0 0 0       $name ne 'HEAD'
468             and exists $name2commit{$name}
469             and return $name2commit{$name};
470              
471 0 0         if ( my $name_data= _get_name_data($name) ) {
472 0           return $name2commit{$name}= $name_data->{commit};
473             }
474             else {
475 0 0 0       _info "$name not in cache!" if $DEBUG and $name ne 'HEAD';
476 0           my $cmd= qq(git log -1 --pretty="format:%H" $name);
477 0           my $sha1= `$cmd 2>/dev/null`;
478 0   0       $sha1 ||= '';
479 0           chomp($sha1);
480 0 0         $name2commit{$name}= $sha1 if $sha1;
481 0           return $sha1;
482             }
483              
484             }
485              
486 1         8447 my %name2sha1;
487              
488             sub get_sha1_for_name {
489 0     0 0   my ($name)= @_;
490 0 0         return '' if !$name;
491 0 0 0       $name ne 'HEAD'
492             and exists $name2sha1{$name}
493             and return $name2sha1{$name};
494 0 0         if ( my $name_data= _get_name_data($name) ) {
495 0           return $name2commit{$name}= $name_data->{sha1};
496             }
497             else {
498 0           my $sha1= `git rev-parse $name 2>/dev/null`;
499 0   0       $sha1 ||= '';
500 0           chomp($sha1);
501 0 0         $name2sha1{$name}= $sha1 if $sha1;
502 0           return $sha1;
503             }
504             }
505             }
506              
507              
508             # check if a name is an annotated tag.
509             sub is_name_annotated_tag {
510 0     0 0   my ($name)= @_;
511 0           my $name_data= _get_name_data($name);
512             return
513 0 0 0       unless $name_data->{category} eq 'tag'
514             and $name_data->{type} eq 'object';
515 0           return ( $name_data->{commit}, $name_data->{sha1} );
516             }
517              
518             my %type;
519              
520              
521             # returns the tags sorted by their date stamp, with undated tags last alphabetically
522             # the idea is we want a list where we find a match for head ASAP
523             sub get_sorted_list_of_tags {
524 0     0 0   my $ref_info= get_ref_info();
525 0           my $all_refs= $ref_info->{refs}{all};
526 0           my @tags= map { s!^tags/!!; $_ }
  0            
  0            
527 0           grep { $all_refs->{$_}{category} eq 'tag' } keys %$all_refs;
528              
529             # ST: parse out datestamps first so we can use them as a key to sort by
530 0 0         @tags= map { $_->[0] }
  0            
531 0           sort { $b->[1] cmp $a->[1] || $a->[0] cmp $b->[0] }
532             map {
533 0           $type{$_}= 'tag';
534 0 0 0       [ $_, m/\D(20\d{6})[_-]?(\d+)?/ ? $1 . ( $2 || '' ) : '' ]
535             } @tags;
536              
537 0           return @tags;
538             }
539              
540              
541             # list filter to remove names that contain a date tag which is older than a specific date.
542             #
543             # my @filtered=filter_names_by_date($date,@list);
544              
545             sub filter_names_by_date {
546 0     0 0   my $ignore_older_than= shift;
547 0 0 0       return grep {
548 0           m/\D(20\d{6})[_-]?(\d+)?/ # does it have a date?
549             ? ( $1 . ( $2 || '' ) ge $ignore_older_than ) # yes - compare
550             : 1; # no - keep
551             } @_;
552             }
553              
554             # get a list of branches includes remote tracking branches as well as local.
555              
556             sub get_branches {
557 0           return map {
558 0     0 0   chomp;
559 0           s/^\s*(?:\*\s*)?//;
560 0 0         if ( $_ ne '(no branch)' ) {
561 0           $type{$_}= "branch";
562 0           $_;
563             }
564             else {
565 0           ();
566             }
567             } `git branch -a`;
568             }
569              
570             # find the current branch
571             # returns an empty list/undef if no branch found
572             # returns the empty string if the current branch is reported as '(no branch)'
573             sub get_current_branch {
574 0     0 0   for (`git branch`) {
575 0           chomp;
576 0 0         if ( $_ =~ s/^\s*\*\s*// ) {
577 0 0         return $_ ne '(no branch)' ? $_ : '';
578             }
579             }
580 0           return undef;
581             }
582              
583              
584              
585             sub what_branches_can_reach_head {
586 0     0 0   my $head= get_commit_for_name("HEAD");
587 0           my %special= (
588             'trunk' => 1,
589             'master' => 2,
590             'origin/trunk' => 3,
591             'origin/master' => 4,
592             );
593 0 0 0       my @branch=
      0        
594 0           sort { ( $special{$a} || 100 ) <=> ( $special{$b} || 100 ) || $a cmp $b }
595 0           grep { $_ ne "(no branch)" } map {
596 0           chomp;
597 0           s/^\s*(?:\*\s*)?//;
598 0           $_;
599             } `git branch -a --contains HEAD`;
600 0 0         return wantarray ? @branch : $branch[0];
601             }
602              
603              
604              
605             # filter through a list of items finding either the first or all
606             # items, (as controlled via $find_all).
607             #
608             # my @match_head= filter_names_matching_head($find_all, @names);
609              
610             sub filter_names_matching_head {
611 0     0 0   my $find_all= shift;
612 0 0 0       $find_all= "" if $find_all and $find_all eq 'first';
613              
614             # get the currently checked out commit sha1
615 0           my $head_sha1= get_commit_for_name('HEAD');
616              
617             # now loop through the tags to find a match
618 0           my @matched_names;
619 0           foreach my $name (@_) {
620 0           my $sha1= get_commit_for_name($name);
621              
622             # check if the sha1 is the same as HEAD
623 0 0         next unless $sha1 eq $head_sha1;
624              
625             # either return a singleton,
626 0 0         return $name unless $find_all;
627              
628             # or gether the results in a list for later return
629 0           push @matched_names, $name;
630              
631             }
632              
633 0           return @matched_names;
634             }
635              
636              
637             # find tags that match head,
638             #
639             # my $tag= find_tags_matching_head();
640             # my @tags= find_tags_matching_head('list');
641              
642             sub find_tags_matching_head {
643 0     0 0   my ($list)= @_;
644              
645             # report on existing tags
646 0           return filter_names_matching_head( $list, get_sorted_list_of_tags() );
647             }
648              
649             # find refs that match head,
650             #
651             # my $ref= find_refs_matching_head();
652             # my @refs= find_refs_matching_head('list');
653             #
654             # note this prefers tags over branches in the scalar form.
655              
656             sub find_refs_matching_head {
657 0     0 0   my ($list)= @_;
658              
659             # report on existing tags
660 0           return filter_names_matching_head( $list, get_sorted_list_of_tags(), get_branches, );
661             }
662              
663              
664              
665             # verify that the working directory is clean. If it is not clean returns the status output.
666             # if it is clean returns nothing.
667             sub check_if_working_dir_is_clean {
668 0     0 0   push_timings("gdt_internal__git_status__start");
669 0           my $status= `git status`;
670 0           push_timings("gdt_internal__git_status__end");
671 0 0         return if $status =~ /\(working directory clean\)/;
672 0           return $status;
673             }
674              
675             # make_tag($name,@message);
676             #
677             # @message will be in place modified such that %TAG is replaced by the
678             # new tagname.
679             #
680             # returns the new tagname.
681             #
682             sub make_tag {
683 0     0 0   my $tag_name= shift;
684              
685             #my @message= @_; # except that we actually modify @_ in place
686              
687 0 0         _die "\$tag_name not optional in 'make_tag'\n"
688             if !$tag_name;
689 0 0         _die "\$message not optional in 'make_tag'\n"
690             if !@_;
691              
692             # It is possible that start and rollout tags collide,
693             # at least while testing the script. So we play some suffix
694             # games to make them unique. It's unlikely to ever happen in
695             # practice as there is always a non trivial amount of time between
696             # the two steps.
697 0 0         if ( get_commit_for_name($tag_name) ) {
698 0           my $suffix= "A";
699 0           while ( get_commit_for_name( $tag_name . "_" . $suffix ) ) {
700 0           $suffix++;
701             }
702 0           $tag_name .= "_$suffix";
703             }
704              
705             # the space after the -m is *required* on cyan
706 0           my $message_opt= join " ", map { s/%TAG/$tag_name/g; "-m '$_'" } @_;
  0            
  0            
707              
708 0           my $cmd= "git tag $message_opt $tag_name";
709 0           my $error= `$cmd 2>&1`;
710 0 0         _die "failed to create tag $tag_name\n$error"
711             if $error;
712 0 0         _info "created tag '$tag_name'\n" if $VERBOSE;
713 0           clear_ref_info(); # spoil the tag info cache
714 0           return $tag_name;
715             }
716              
717              
718             # make_dated_tag($prefix,$date_fmt,@message);
719             #
720             # @message will be in place modified such that %TAG is replaced by the
721             # new tagname.
722             #
723             # returns the new tagname.
724             #
725             sub make_dated_tag {
726 0     0 0   my $prefix= shift;
727 0           my $date_fmt= shift;
728              
729             #my @message= @_; # except that we actually modify @_ in place
730 0           my $date= strftime $date_fmt, localtime;
731 0           my $tag_name= "$prefix-$date";
732 0           return make_tag( $tag_name, @_ );
733             }
734              
735             # preform an action against a remote site.
736             sub remote {
737 0     0 0   my ( $action, $remote_site, $remote_branch )= @_;
738 0           push_timings("gdt_internal__remote__action_${action}__start");
739 0 0         if ( !$remote_site ) {
740 0           _info "Note: not performing $action, as it is disabled\n";
741             }
742 0 0 0       return if !$remote_site or $remote_site eq 'none';
743              
744             #$remote_branch ||= get_current_branch()
745             # or _die "Not on a branch currently!"
746             # if !$remote_branch and defined $remote_branch;
747 0   0       $remote_branch ||= '';
748 0           my $cmd= "git $action $remote_site $remote_branch";
749 0 0         _info "$cmd", $action =~ /pull/ ? "" : "\n(not updating working directory)\n", "\n"
    0          
750             if $VERBOSE;
751 0           my ( $res, $error )= git_cmd($cmd);
752 0 0         my $name= "$remote_site" . ( $remote_branch ? ":$remote_branch" : "" );
753              
754             # if there is nothing new to fetch then we get error code 1, which does not
755             # really mean an error, so we will just pretend it is not.
756 0 0 0       if ( $action =~ /fetch/ and $error == 1 ) {
757 0 0         _info "got exit code 1 - nothing to fetch\n" if $VERBOSE;
758 0           $error= 0;
759             }
760              
761 0 0         _die "failed to git $action from '$name' errorcode: $error\n$cmd\n$res\n"
762             if $error;
763 0 0 0       _info "$res", "\n" if $VERBOSE and $res;
764 0           push_timings("gdt_internal__remote__action_${action}__end");
765             }
766              
767             # fetch tags from a remote site
768             sub fetch_tags {
769 0     0 0   my ( $remote_site )= @_;
770 0           remote( "fetch --tags", $remote_site, undef );
771             }
772              
773              
774             # push tags to a remote site
775             sub push_tags {
776 0     0 0   my ( $remote_site )= @_;
777 0           remote( "push --tags", $remote_site, undef );
778             }
779              
780             sub push_tag {
781 0     0 0   my ( $remote_site, $tag )= @_;
782 0           remote( "push", $remote_site, $tag );
783             }
784              
785             # push tags and all references to a remote site.
786             sub push_all {
787 0     0 0   my ( $remote_site )= @_;
788 0           remote( "push --tags --all", $remote_site, undef );
789             }
790              
791             # fetch a branch from a remote site.
792             sub fetch {
793 0     0 0   my ( $remote_site, $remote_branch )= @_;
794 0           remote( "fetch", $remote_site, $remote_branch );
795             }
796              
797             # pull a branch from a remote site.
798             sub pull {
799 0     0 0   my ( $remote_site, $remote_branch )= @_;
800 0           remote( "pull", $remote_site, $remote_branch );
801             }
802              
803             # push a branch to a remote site.
804             sub push_remote {
805 0     0 0   my ( $remote_site, $remote_branch )= @_;
806 0           remote( "push", $remote_site, $remote_branch );
807             }
808              
809              
810             # take a list of references and print them out in a formatted way.
811             # Currently the list is
812             #
813             # SHA1 *TYPE: NAME #NAME
814             # where the * may be a star or space and indicates that the ref points at HEAD,
815             # and the #NAME is optional, and points at the most recent tag with the same SHA1
816              
817             sub print_refs {
818 0     0 0   my $opts= shift;
819 0           my $array= shift;
820 0 0         my $head= get_commit_for_name('HEAD')
821             or _die "panic: no sha1 for HEAD?! wtf!";
822 0 0         if ( !$opts->{list} ) {
823 0 0         return if !@$array;
824 0           print shift @$array;
825 0 0         print "\n" if -t STDOUT;
826 0           return;
827             }
828 0           my %seen_sha1;
829 0           my $start= time;
830 0           foreach my $name ( reverse @$array ) {
831 0 0         if ( !ref $name ) {
832 0           my $sha1= get_commit_for_name($name);
833 0 0 0       push @{$seen_sha1{$sha1}}, $name
  0   0        
834             if !$seen_sha1{$sha1} or (!$opts->{prefix} or $name=~/^$opts->{prefix}/);
835             }
836             }
837 0           my $elapsed= time - $start;
838 0 0         _info "First loop took $elapsed seconds\n" if $DEBUG;
839 0           my $count= 0;
840 0           my $filtered= 0;
841 0           $start= time;
842              
843 0 0 0       _info "Filtering list by m/^$opts->{prefix}-[0-9]+/"
    0          
844             . (
845             $opts->{prefix} eq '.'
846             ? "\n"
847             : " (use `git-deploy show .` to see all).\n"
848             ) if $opts->{prefix} and !$opts->{tag_only};
849 0 0         _info "SHA1........ tag: PREFIX-YYYYMMDD-HHMM == Original rollout of same sha1\n"
850             if !$opts->{tag_only};
851 0 0         _info "Tags against active commit are marked with a '"
852             . color(COLOR_WARN) . "*" . color('reset') . color(COLOR_INFO)
853             . "' and are "
854             . color(COLOR_WARN) . "highlighted" . color('reset') . color(COLOR_INFO)
855             . " differently\n"
856             if !$opts->{tag_only};
857              
858 0           my @printed;
859              
860 0           my $last_sha1= "";
861 0           foreach my $name_idx (0..$#$array) {
862 0           my $name= $array->[$name_idx];
863 0 0         next if ref $name;
864 0 0 0       ++$filtered and next
      0        
865             if $opts->{prefix} and $name !~ m/^$opts->{prefix}-[0-9]+/;
866 0 0 0       last if $opts->{count} and $opts->{count} < ++$count;
867              
868 0           my $next_name= $array->[ $name_idx + 1 ];
869 0 0         my $next_sha1= $next_name ? get_commit_for_name($next_name) : "";
870 0           my $sha1= get_commit_for_name($name);
871              
872 0 0         if ( $opts->{tag_only} ) {
873 0 0 0       _print $name, ( $opts->{action} && $opts->{action} eq 'showtag' ) ? "" : "\n";
874 0           push @printed, $name;
875             }
876             else {
877 0 0         if ( $opts->{for_interactive} ) {
878             # next if $sha1 eq $head;
879 0           push @printed, $name;
880             }
881 0           my $tags_for_commit= $seen_sha1{$sha1};
882 0           pop @$tags_for_commit;
883              
884 0 0         _printf "%s%s%s %1s%s: %-25s%s%s%s\n",
    0          
    0          
    0          
    0          
885             @printed ? sprintf( "%4d.\t", 0 + @printed ) : "",
886             color( $sha1 eq $head ? COLOR_WARN : COLOR_SAY ),
887             $opts->{long_digest} ? $sha1 : substr( $sha1, 0, 12 ) . "..",
888             $sha1 eq $head ? "*" : " ",
889             $type{$name},
890             $name,
891             @$tags_for_commit ? " ==\t" . join("\t",reverse @$tags_for_commit) : '',
892             #$last_sha1 eq $next_sha1 ? " ***PROBABLY BAD***" : # XXX this doesnt work so leave it disabled for now
893             "",
894             color('reset'),
895             ;
896 0           $last_sha1= $sha1;
897             }
898             }
899 0 0 0       if ( @$array and @$array > $count ) {
900 0 0         my $filtered_str= $filtered ? " ($filtered filtered)" : "";
901 0 0 0       my $showing_str=
902             ( $opts->{count} && $opts->{count} < ( @$array - $filtered ) )
903             ? "Showing first $opts->{count}, "
904             : "";
905 0 0         _info "$showing_str", @$array - $count, " of ", 0 + @$array,
906             " not shown$filtered_str. Use --count=N or different filter to show more (N=0 shows all)\n"
907             if !$opts->{tag_only};
908             }
909 0           $elapsed= time - $start;
910 0 0         _info "Second loop took $elapsed seconds\n" if $DEBUG;
911 0 0 0       _warn "No tags match HEAD\n" if !@$array and !$opts->{tag_only};
912 0           return @printed;
913             }
914              
915              
916              
917              
918             sub get_deploy_file_name {
919 0     0 0   my ($file)= @_;
920 0   0       $file ||= get_config("deploy-file",".deploy");
921 0           return $file;
922             }
923              
924              
925             # Write a deploy file about what has been deployed.
926             # This should be available to be parsed by the code being deployed to know where it came from
927             #
928             sub write_deploy_file {
929 0     0 0   my ( $tag, $message, $file )= @_;
930              
931 0           $file= get_deploy_file_name($file);
932              
933 0 0         my $sha1= get_commit_for_name($tag)
934             or _die "panic: no sha1 for tag '$tag'!";
935 0 0         open my $out, ">", $file
936             or _die "Failed to open deploy file '$file' for write: $!";
937              
938 0 0 0       my $text= join "",
939             "commit: $sha1\n",
940             "tag: $tag\n",
941             "deploy-date: " . strftime( "%Y-%m-%d %H:%M:%S", localtime ) . "\n",
942             "deployed-from: " . hostname() . "\n",
943             "deployed-by: " . $ENV{USER} . "\n",
944             ( $message && @$message ) ? join( "\n", "", @$message, "", "" ) : "\n",
945             ;
946              
947 0 0         print $out $text
948             or _die "panic: failed to write to deploy file handle for '$file': $!";
949 0 0         close $out
950             or _die "panic: failed to close deploy file handle for '$file': $!";
951 0 0         _info "wrote deploy file '$file'\n" if $VERBOSE;
952 0           $text;
953             }
954              
955             # read the deploy file
956             # Unless $skip_check is true we will verify that the .deploy file corresponds to HEAD
957             # If things are good we return the files contents as a string.
958             # If there are any problems we return the empty string (not undef!)
959              
960              
961              
962             sub read_deploy_file {
963 0     0 0   my ( $file, $skip_check )= @_;
964 0           $file= get_deploy_file_name($file);
965 0 0 0       return "" unless $file and -e $file;
966              
967 0           my $deploy_file_text= _slurp($file);
968 0   0       $deploy_file_text ||= "";
969              
970 0 0         my $sha1= $deploy_file_text =~ /^commit: ([a-f0-9]{40})\n/ ? $1 : undef;
971 0 0 0       return ""
      0        
972             if !$skip_check
973             and ( !defined $sha1 or $sha1 ne get_commit_for_name('HEAD') );
974 0           return $deploy_file_text;
975             }
976              
977             sub _slurp {
978 0     0     my ($file_like,$no_die)= @_;
979 0           my $fh;
980 0 0         if ( !ref $file_like ) {
981 0 0         if (!open $fh, "<", $file_like) {
982 0 0         if ($no_die) {
983 0           return "";
984             } else {
985 0           _die "Failed to read '$file_like': $!";
986             }
987             }
988             }
989             else {
990 0           $fh= $file_like;
991             }
992 0 0         if (wantarray) {
993 0           my @lines= <$fh>;
994 0           return @lines;
995             }
996             else {
997 0           local $/;
998 0           return <$fh>;
999             }
1000             }
1001              
1002             sub _expand_template_variables {
1003 0     0     my ($message_ref, $variables) = @_;
1004              
1005 0           while (my ($tag, $string) = each %$variables) {
1006 0           $$message_ref =~ s/\{$tag\}/$string/g;
1007             }
1008 0           return;
1009             }
1010              
1011             BEGIN {
1012              
1013             # This block contains the logic necessary to manage an advisory locking scheme,
1014             # enforce a particular sequence of steps, as well as cross process storage of
1015             # necessary reference data like the rollout status.
1016             # One thing to keep in mind is that the tool is going to invoked multiple times
1017             # with differing steps in between.
1018              
1019             # The basic idea is we maintain a "lock" directory, and within it a file whose
1020             # presence tells others that they cannot do a rollout, and whose contents can
1021             # be used to ensure a specific order of actions is followed, and which can be
1022             # used as an advisory to others about the status, who is performing it and etc.
1023 1     1   3 my $lockdirname= "git-deploy";
1024 1         9 my $lockfilename= "lock-state";
1025              
1026             # additonally we maintain a file per rollout and start tag
1027             # these files only existing during a rollout and are erased afterwards
1028 1         2 my @tag_file_names= qw(rollout start);
1029              
1030             # utility sub, returns the lock_directory and the lockfilename for other subs
1031             # with some standard checking.
1032 1         3982 my $lock_dir;
1033             sub _rollout_lock_dir_and_file {
1034 0 0   0     if (!$lock_dir) {
1035 0           my $lock_dir_root= get_config("lock-dir-root","");
1036 0 0         if (!$lock_dir_root) {
1037 0 0         _die "panic: directory '$gitdir' must exist for a rollout lock step to occur"
1038             if !-d $gitdir;
1039 0           $lock_dir_root= $gitdir;
1040             }
1041 0           $lock_dir= "$lock_dir_root/$lockdirname";
1042             }
1043 0           return ( $lock_dir, "$lock_dir/$lockfilename" );
1044             }
1045              
1046             # write the details of a tag into a file so it can be accessed by a later
1047             # step of the process
1048             sub store_tag_info {
1049 0     0 0   my ( $type, $tag )= @_;
1050              
1051 0           _die "Bad type '$type'"
1052 0 0         unless grep { $type eq $_ } @tag_file_names;
1053              
1054 0           my ($lock_dir)= _rollout_lock_dir_and_file();
1055 0 0         open my $out_fh, ">", "$lock_dir/$type"
1056             or _die "Failed to open '$lock_dir/$type' for writing: $!";
1057 0 0         my $sha1= get_commit_for_name($tag)
1058             or _die "Invalid tag!";
1059 0           print $out_fh "$sha1 $tag";
1060 0           close $out_fh;
1061             }
1062              
1063              
1064             # fetch the details about a tag from the file
1065             sub fetch_tag_info {
1066 0     0 0   my ( $type )= @_;
1067              
1068 0           _die "Bad type '$type'"
1069 0 0         unless grep { $type eq $_ } @tag_file_names;
1070              
1071 0           my ($lock_dir)= _rollout_lock_dir_and_file();
1072 0           my $tag_info= _slurp("$lock_dir/$type","no-die");
1073 0           my ( $sha1, $tag )= split /\s+/, $tag_info;
1074              
1075             # validate tag is matches the sha1 as a crude sanity check
1076 0 0 0       return $tag if $tag and $sha1 and $sha1 eq get_commit_for_name($tag);
      0        
1077 0           return "";
1078             }
1079              
1080              
1081              
1082              
1083             # read the rollout status file takes the gitdir as an argument
1084             sub read_rollout_status {
1085 0     0 0   my ( $lock_dir, $lock_file )= _rollout_lock_dir_and_file();
1086 0 0         return "" if !-d $lock_dir;
1087 0 0         return "" if !-e $lock_file;
1088 0 0         unless (wantarray) {
1089 0           my $content= _slurp($lock_file);
1090 0           return $content;
1091             }
1092             else {
1093 0           my @content= _slurp($lock_file);
1094 0           return @content;
1095             }
1096             }
1097              
1098             # read the rollout status file and parses it into hashes.
1099             # in list context returns a list of hashes, in scalar context
1100             # returns an AoH.
1101             sub parse_rollout_status {
1102 0           my @lines= map {
1103 0     0 0   chomp;
1104 0           my %hash;
1105 0           @hash{qw(action time branch sha1 uid username)}= split /\t/, $_;
1106 0 0         $hash{branch}= "" if $hash{branch} eq '(no branch)';
1107 0           $hash{action} =~ s/:\z//;
1108 0           \%hash
1109             } read_rollout_status(@_);
1110 0 0         return wantarray ? @lines : \@lines;
1111             }
1112              
1113              
1114             sub check_rollouts_blocked {
1115 0     0 0   my ($force,$no_die)= @_;
1116 0           my $msg= "";
1117 0   0       $msg ||= get_config('block-reason','');
1118 0 0         if ($msg) {
    0          
1119 0           $msg= "Rollouts locally blocked: $msg\nUse `git config --unset deploy.block-reason` to unblock.";
1120             } elsif (my $sysadmin_lock= get_config_path('block-file','')) {
1121 0 0 0       if ($sysadmin_lock and -e $sysadmin_lock and !$force) {
      0        
1122 0           $msg= _slurp($sysadmin_lock);
1123              
1124 0           $msg= "Rollout blockfile '$sysadmin_lock' exists, cannot rollout!\n"
1125             . $msg;
1126             }
1127             }
1128 0 0         if ($msg) {
1129 0 0         if ($no_die) {
1130 0           return $msg;
1131             } else {
1132 0           _die $msg;
1133             }
1134             }
1135             }
1136             # write_rollout_status($dir,$status,$force,$other_checks)
1137             #
1138             # $dir is the directory to write the file to, a string.
1139             # $status is the type of action we are performing, 'start','sync','finish','abort'
1140             # $force is a flag that overrides the security checks
1141             # $other_checks is a code ref of other checks that should be performed prior to creating
1142             # the file, it should die if the step should not proceed.
1143             #
1144             # returns nothing, dies if the status file cannot be created or updated properly or if any
1145             # of the necessary preconditions are not satisfied.
1146             #
1147             # Note this is called before we create a tag.
1148             # so we do not know the tagname that will be used for the step at the time
1149             # we write the data out, and thus cant include it in the file.
1150             #
1151             sub write_rollout_status {
1152 0     0 0   my $status= shift;
1153 0           my $force= shift;
1154 0           my $other_checks= shift;
1155              
1156 0           my ( $lock_dir, $lock_file )= _rollout_lock_dir_and_file();
1157              
1158 0           my ( $opened_ok, $out_fh, @file );
1159              
1160             my $somethings_wrong=
1161             $force
1162 0     0     ? sub { 0 }
1163             : sub {
1164 0   0 0     my $first_line= shift || "It looks like somethings wrong:";
1165 0           my $last_line= shift;
1166 0           $first_line =~ s/\n+\z//;
1167              
1168             #$first_line .= ":" if $fl !~ /:\z/;
1169              
1170 0 0         _die join "\n", $first_line, @file ? "Log:\n\t" . join( "\t", @file ) : (), $last_line ? $last_line : (),
    0          
1171             "";
1172 0 0         };
1173              
1174 0 0         if ( $status eq 'start' ) {
    0          
    0          
1175 0           check_rollouts_blocked($force);
1176             mkdir $lock_dir
1177 0 0         or do {
1178 0           my $message= "You may not start a new rollout as it looks like one is already in progress!\n"
1179             . "Failed to create lock dir '$lock_dir' because '$!'\n";
1180 0           @file= _slurp($lock_file);
1181 0 0         $somethings_wrong->($message) if @file;
1182             };
1183             $opened_ok= sysopen( $out_fh, $lock_file, O_WRONLY | O_EXCL | O_CREAT )
1184 0 0         or do {
1185 0           my $message= "Can't start a new rollout, one is already in progress\n"
1186             . "Failed to create lock file '$lock_file' because '$!'\n";
1187 0           @file= _slurp($lock_file);
1188 0           $somethings_wrong->($message);
1189             };
1190             }
1191             elsif ( !-d $lock_dir ) {
1192 0           _die "It looks like you havent started yet!\n";
1193             }
1194             elsif ( $opened_ok= sysopen( $out_fh, $lock_file, O_RDWR ) ) {
1195 0           @file= _slurp($out_fh);
1196 0 0         if ( @file == 3 ) {
1197 0           $somethings_wrong->(
1198             "It looks like someone is just finishing a rollout",
1199             "Wait a minute or two and retry."
1200             );
1201             }
1202 0 0         if ( !@file ) {
1203 0           _die "It looks like you havent started yet!\n";
1204             }
1205 0 0 0       if ( !$file[0] or $file[0] !~ /^start:/ or @file > 2 ) {
      0        
1206 0           $somethings_wrong->();
1207             }
1208 0 0 0       if ( $status eq 'sync' and @file != 1 ) {
1209 0           $somethings_wrong->("It looks like maybe you already synced");
1210             }
1211 0 0         if ( $status eq 'finish' ) {
1212 0 0 0       if ( @file == 1 ) {
    0          
1213 0           $somethings_wrong->("It looks like maybe you havent synced yet");
1214             }
1215             elsif ( @file == 2 and $file[1] !~ /^(sync|release|manual-sync):/ ) {
1216 0           $somethings_wrong->("Can't $status in the current state:");
1217             }
1218             }
1219 0 0         if ( $status eq 'finnish' ) {
1220 0           $somethings_wrong->("git-deploy ole saatavilla suomeksi! (maybe you meant 'finish' instead?)");
1221             }
1222 0 0         if ( $status eq 'abort' ) {
1223 0 0 0       if ( @file == 2 and $file[1] !~ /^(sync|release|manual-sync):/ ) {
1224 0           $somethings_wrong->("Can't $status in the current state:");
1225             }
1226             }
1227 0 0         if ( $file[0] !~ /\t\Q$ENV{USER}\E$/ ) {
1228 0           $somethings_wrong->("Someone else is doing a rollout. You cannot proceed.");
1229             }
1230             }
1231 0 0         if ( !$opened_ok ) {
1232 0           _die "Failed to open lockfile '$lock_file': $!\n"
1233             . "There is a good chance this means someone is already rolling out.";
1234             }
1235 0 0         flock( $out_fh, LOCK_EX | LOCK_NB )
1236             or _die "Failed to lock file:$!\nSomebody already rolling out?\n";
1237 0           $other_checks->();
1238 0   0       my $status_line= join(
1239             "\t",
1240             "$status:", # must be first
1241             strftime( "%Y-%m-%d %H:%M:%S", localtime() ),
1242             get_current_branch() || '(no branch)',
1243             get_commit_for_name('HEAD'),
1244             $<,
1245             $ENV{USER} # must be last
1246             ).
1247             "\n";
1248 0           _log($status_line);
1249 0 0         print $out_fh $status_line
1250             or _die "panic: failed to print to deployment status lock file: $!";
1251 0 0         close $out_fh
1252             or _die "panic: failed to close deployment status lock file: $!";
1253              
1254             }
1255              
1256              
1257             sub unlink_rollout_status_file {
1258 0     0 0   my ( $lock_dir, $lock_file )= _rollout_lock_dir_and_file();
1259              
1260 0           for my $type (@tag_file_names) {
1261 0 0         if ( -e "$lock_dir/$type" ) {
1262 0 0         unlink "$lock_dir/$type"
1263             or _die "Failed to delete '$lock_dir/$type':$!";
1264             }
1265             }
1266 0 0         unlink $lock_file
1267             or _die "Failed to delete '$lock_file':$!";
1268 0 0         if ( -e "$lock_file~" ) {
1269 0 0         unlink $lock_file
1270             or _die "Failed to delete '$lock_file~':$!";
1271             }
1272 0 0         rmdir $lock_dir
1273             or _die "Failed to rmdir '$lock_dir':$!";
1274 0 0         _info "Removed rollout status locks\n" if $VERBOSE > 1;
1275             }
1276             }
1277              
1278              
1279             sub check_for_unpushed_commits {
1280 0     0 0   my ( $remote_site, $remote_branch, $force )= @_;
1281 0           push_timings("gdt_internal__check_for_unpushed_commits__start");
1282 0   0       $remote_branch ||= get_current_branch();
1283              
1284             #print "git cherry $remote_site/$remote_branch\n";# if $DEBUG;
1285 0           my @cherry= grep { /[0-9a-f]/ } `git cherry $remote_site/$remote_branch`;
  0            
1286 0 0         if (@cherry) {
1287 0           _warn "It looks like there are unpushed commits.\n",
1288             "Most likely this is harmless and you should just\n",
1289             "\tgit push\n",
1290             "and then continue with the deployment but you should review the following...\n";
1291 0           foreach my $cherry (@cherry) {
1292 0           chomp $cherry;
1293 0           my ( $type, $sha1 )= split /\s/, $cherry;
1294 0 0         if ( $type eq '-' ) {
1295 0           _warn "This commit appears to already be applied upstream:\n";
1296             }
1297             else {
1298 0           _warn "Unpushed commit:\n";
1299             }
1300 0           _print `git log -1 $sha1`;
1301             }
1302             }
1303 0           push_timings("gdt_internal__check_for_unpushed_commits__end");
1304 0 0 0       _die "Will not proceed.\n" if @cherry and !$force;
1305 0           return 0;
1306             }
1307              
1308              
1309             sub reset_to_name {
1310 0     0 0   my ( $action, $name, $prefix )= @_;
1311 0           my ($rbinfo)= parse_rollout_status();
1312 0           push_timings("gdt_internal__reset_to_name__start");
1313 0           my @cmd;
1314 0           my $cur_branch= get_current_branch();
1315 0 0         if ( $rbinfo->{branch} ne $cur_branch ) {
1316 0           _say "Will switch branch back to '$rbinfo->{branch}' from the current branch '$cur_branch'\n";
1317 0           push @cmd, [ "git reset --hard", qr/^HEAD is now at /m ];
1318 0           push @cmd, [ "git checkout $rbinfo->{branch}", qr/^Switched to branch /m ];
1319             }
1320 0           push @cmd, [ "git reset --hard $name", qr/^HEAD is now at /m ];
1321 0           push @cmd, [ "git checkout -f", '' ]; # we do this to guarantee that we execute git-hooks
1322 0           foreach my $tuple (@cmd) {
1323 0           my ( $cmd, $expect )= @$tuple;
1324 0 0 0       _info "$cmd\n" if $VERBOSE and $VERBOSE > 1;
1325 0           my $result= `$cmd 2>&1`;
1326 0 0 0       _die "command '$cmd' failed to produce expected output: $result"
1327             if $expect and $result !~ m/$expect/;
1328 0 0 0       _info "$result\n" if $expect and $VERBOSE and $VERBOSE > 1;
      0        
1329             }
1330 0           _say "Rolled back to '$name' succesfully\n";
1331              
1332             execute_deploy_hooks(
1333             action => $action,
1334             phase => $_,
1335             prefix => $prefix,
1336              
1337             # We don't want the abort to fail just because the
1338             # webserver didn't restart or something. This will warn if
1339             # the hooks fail, but will continue.
1340             ignore_exit_code => 1,
1341 0           ) for qw(post-tree-update post-reset);
1342              
1343 0           push_timings("gdt_internal__reset_to_name__end");
1344 0           return;
1345             }
1346              
1347              
1348             {
1349             my $root;
1350              
1351             sub get_hook_dir {
1352 0     0 0   my ( $prefix )= @_;
1353 0 0         return $root if defined $root;
1354              
1355 0           $root = get_config_path('hook-dir',undef);
1356              
1357 0 0         if ($SKIP_HOOKS) {
1358 0           $root= "";
1359 0           _warn "ALL HOOKS HAVE BEEN DISABLED.\n";
1360             }
1361 0 0 0       if ( not $root or not -e $root ) {
1362 0           $root= "";
1363 0 0 0       _info "Note: no deploy directory found. Directory '$root' does not exist\n"
1364             if $VERBOSE and $VERBOSE > 1;
1365 0           return;
1366             }
1367             else {
1368 0 0 0       _info "Note: Checking for hooks in '$root'\n"
1369             if $VERBOSE and $VERBOSE > 1;
1370             }
1371 0           return $root;
1372             }
1373             }
1374              
1375              
1376             sub get_hook {
1377 0     0 0   my ( $hook_name, $prefix )= @_;
1378 0 0         my $root= get_hook_dir( $prefix )
1379             or return;
1380 0           my $file= "$root/$hook_name/$prefix.$hook_name";
1381 0 0         return unless -e $file;
1382 0 0         if ( -x $file ) {
1383 0           return $file;
1384             }
1385             else {
1386 0           _warn "Found a $hook_name hook for '$prefix': '$file' however it is not executable! Ignoring!\n";
1387             }
1388 0           return;
1389             }
1390              
1391 0     0 0   sub get_sync_hook { return get_hook( "sync", @_ ) }
1392              
1393              
1394             sub execute_hook {
1395 0     0 0   my ($cmd, $ignore_exit_code)= @_;
1396              
1397 0           my ($file)= $cmd =~ m/([^\/]+)$/;
1398              
1399 0           push_timings("gdt_internal__execute_hook__${file}__start");
1400 0           system("$cmd 2>&1");
1401 0 0         if ( $? == -1 ) {
    0          
    0          
1402 0           my $msg = "failed to execute '$cmd': $!\n";
1403 0 0         $ignore_exit_code ? _warn $msg : _die $msg;
1404             }
1405             elsif ( $? & 127 ) {
1406 0 0         my $msg = sprintf "'$cmd' _died with signal %d, %s coredump\n", ( $? & 127 ), ( $? & 128 ) ? 'with' : 'without';
1407 0 0         $ignore_exit_code ? _warn $msg : _die $msg;
1408             }
1409             elsif ( $? >> 8 ) {
1410 0           my $msg = sprintf "error: '$cmd' exited with value %d\n", $? >> 8;
1411 0 0         $ignore_exit_code ? _warn $msg : _die $msg;
1412             }
1413 0           push_timings("gdt_internal__execute_hook__${file}__end");
1414 0           return 1;
1415             }
1416              
1417             sub process_deploy_hooks {
1418 0     0 0   my ( $hook_dir, $appname, $phase, $ignore_exit_code )= @_;
1419 0 0         _info "Checking for '$phase' hooks for '$appname' ",
    0          
1420             $appname eq 'common' ? '(generic hooks)' : '(appliction specific)', "\n"
1421             if $VERBOSE > 1;
1422              
1423 0           my $appdir= "$hook_dir/apps/$appname";
1424 0   0       my @checks= sort grep { !/\.bak\z/ and !/~\z/ } glob "$appdir/$phase.*";
  0            
1425 0 0         if ( !@checks ) {
1426 0 0         _info "No '$phase' hooks found '$appdir' ", -e $appdir ? "is empty." : "does not exist.", "\n" if $DEBUG;
    0          
1427 0           return;
1428             }
1429             else {
1430 0 0         _info "Found ", 0 + @checks, " '$phase' hooks to execute in '$appdir'\n" if $DEBUG;
1431             }
1432              
1433 0           push_timings("gdt_internal__process_deploy_hooks__phase_${phase}__start");
1434 0           foreach my $spec (@checks) {
1435 0           my $cmd= "";
1436 0 0         unless ( -x $spec ) {
1437 0           _warn "Deploy hook '$spec' is not executable! IGNORING!\n";
1438 0           next;
1439             }
1440 0           $cmd= $spec;
1441 0           _info "Executing $phase hook: $cmd";
1442 0           execute_hook($cmd, $ignore_exit_code);
1443             }
1444 0           push_timings("gdt_internal__process_deploy_hooks__phase_${phase}__end");
1445 0 0         _info "All '$phase' checks for '$appname' were successful\n" if $DEBUG;
1446             }
1447              
1448             sub execute_deploy_hooks {
1449 0     0 0   my (%args) = @_;
1450              
1451 0   0       my $action = $args{action} || _die "Missing action argument";
1452 0   0       my $phase = $args{phase} || _die "Missing phase argument";
1453 0   0       my $prefix = $args{prefix} || _die "Missing prefix argument";
1454 0   0       my $ignore_exit_code = $args{ignore_exit_code} || 0;
1455              
1456 0 0         my $root= get_hook_dir( $prefix )
1457             or return;
1458              
1459 0           local $ENV{GIT_DEPLOY_ACTION} = $action;
1460 0           local $ENV{GIT_DEPLOY_PHASE} = $phase;
1461 0           local $ENV{GIT_DEPLOY_PREFIX} = $prefix;
1462              
1463             # the tag information, if provided
1464 0 0         local $ENV{GIT_DEPLOY_START_TAG} = $args{start_tag} if defined $args{start_tag};
1465 0 0         local $ENV{GIT_DEPLOY_ROLLOUT_TAG} = $args{rollout_tag} if defined $args{rollout_tag};
1466              
1467             # the common 'app' is executed for everyone
1468 0           local $ENV{GIT_DEPLOY_HOOK_PREFIX} = 'common';
1469 0           process_deploy_hooks( $root, "common", $phase, $ignore_exit_code );
1470              
1471             # and then the 'app' specific stuff as determined by $prefix
1472 0           local $ENV{GIT_DEPLOY_HOOK_PREFIX} = $prefix;
1473 0           process_deploy_hooks( $root, $prefix, $phase, $ignore_exit_code );
1474             }
1475              
1476             sub execute_log_hooks {
1477 0     0 0   my (%args) = @_;
1478              
1479 0   0       my $level = $args{log_level} || _die "Missing log_level argument";
1480 0   0       my $message = $args{log_message} || _die "Missing log_message argument";
1481 0   0       my $announce = $args{log_announce} || 0;
1482 0 0         my $ignore_exit_code = exists $args{ignore_exit_code} ? $args{ignore_exit_code} : 1;
1483              
1484 0           local $ENV{GIT_DEPLOY_LOG_LEVEL} = $level;
1485 0           local $ENV{GIT_DEPLOY_LOG_MESSAGE} = $message;
1486 0           local $ENV{GIT_DEPLOY_LOG_ANNOUNCE} = $announce;
1487              
1488 0           execute_deploy_hooks(
1489             phase => "log",
1490             ignore_exit_code => $ignore_exit_code,
1491             %args,
1492             );
1493             }
1494              
1495             sub log_directory {
1496 0     0 0   my $log_directory = get_config_path("log-directory", '/tmp');
1497 0 0         return unless $log_directory;
1498 0           return $log_directory;
1499             }
1500              
1501             1;