git-shell-verify: only check read-only mode for receive-pack
[girocco.git] / toolbox / update-all-hooks.pl
blobd90a59752ee59d811039340adaea4d212e4cccf7
1 #!/usr/bin/perl
3 # update-all-hooks.pl - Update all out-of-date hooks and install missing
5 use strict;
6 use warnings;
7 use vars qw($VERSION);
8 BEGIN {*VERSION = \'2.0'}
9 use File::Basename;
10 use File::Spec;
11 use Cwd qw(realpath);
12 use POSIX qw();
13 use Getopt::Long;
14 use Pod::Usage;
15 BEGIN {
16 eval 'require Pod::Text::Termcap; 1;' and
17 @Pod::Usage::ISA = (qw( Pod::Text::Termcap ));
18 defined($ENV{PERLDOC}) && $ENV{PERLDOC} ne "" or
19 $ENV{PERLDOC} = "-oterm -oman";
21 use lib "__BASEDIR__";
22 use Girocco::Config;
23 use Girocco::Util;
24 use Girocco::CLIUtil;
25 use Girocco::Project;
27 my $shbin;
28 BEGIN {
29 $shbin = $Girocco::Config::posix_sh_bin;
30 defined($shbin) && $shbin ne "" or $shbin = "/bin/sh";
33 exit(&main(@ARGV)||0);
35 my ($dryrun, $force, $quiet);
37 sub die_usage {
38 pod2usage(-exitval => 2);
41 sub do_help {
42 pod2usage(-verbose => 2, -exitval => 0);
45 sub do_version {
46 print basename($0), " version ", $VERSION, "\n";
47 exit 0;
50 my $owning_group_id;
51 my $progress;
53 sub pmsg { $progress->emit(@_) }
54 sub pwarn { $progress->warn(@_) }
56 sub undefval($$) { return defined($_[0]) ? $_[0] : $_[1] }
58 sub main {
59 local *ARGV = \@_;
60 my ($help, $version);
62 umask 002;
63 close(DATA) if fileno(DATA);
64 Getopt::Long::Configure('bundling');
65 GetOptions(
66 'help|h' => sub {do_help},
67 'version|V' => sub {do_version},
68 'dry-run|n' => \$dryrun,
69 'quiet|q' => \$quiet,
70 # --force currently doesn't do anything, but must be accepted
71 # because update-all-projects.sh will pass it along to both
72 # update-all-config and update-all-hooks and it *does* do something
73 # for update-all-config.
74 'force|f' => \$force,
75 ) or die_usage;
76 $dryrun and $quiet = 0;
78 -f jailed_file("/etc/group") or
79 die "Girocco group file not found: " . jailed_file("/etc/group") . "\n";
81 if (($owning_group_id = scalar(getgrnam($Girocco::Config::owning_group))) !~ /^\d+$/) {
82 die "\$Girocco::Config::owning_group invalid ($Girocco::Config::owning_group), refusing to run\n";
85 my @allprojs = Girocco::Project::get_full_list;
86 my @projects = ();
88 my $root = $Girocco::Config::reporoot;
89 $root && -d $root or die "\$Girocco::Config::reporoot is invalid\n";
90 $root =~ s,/+$,,;
91 $root ne "" or $root = "/";
92 $root = $1 if $root =~ m|^(/.+)$|;
93 my $globalhooks="$root/_global/hooks";
94 if (@ARGV) {
95 my %projnames = map {($_ => 1)} @allprojs;
96 foreach (@ARGV) {
97 s,/+$,,;
98 $_ or $_ = "/";
99 -d $_ and $_ = realpath($_);
100 $_ = $1 if $_ =~ m|^(.+)$|;
101 s,^\Q$root\E/,,;
102 s,\.git$,,;
103 if (!exists($projnames{$_})) {
104 warn "$_: unknown to Girocco (not in etc/group)\n"
105 unless $quiet;
106 next;
108 push(@projects, $_);
110 } else {
111 @projects = sort {lc($a) cmp lc($b) || $a cmp $b} @allprojs;
114 nice_me(18);
115 my $bad = 0;
116 $progress = Girocco::CLIUtil::Progress->new(scalar(@projects),
117 "Updating hooks");
118 foreach (@projects) {
119 my $projdir = "$root/$_.git";
120 if (! -d $projdir) {
121 pwarn "$_: does not exist -- skipping\n" unless $quiet;
122 next;
124 if (!is_git_dir($projdir)) {
125 pwarn "$_: is not a .git directory -- skipping\n" unless $quiet;
126 next;
128 if (-e "$projdir/.nohooks") {
129 pwarn "$_: found .nohooks -- skipping\n" unless $quiet;
130 next;
132 my @updates = ();
133 my $qhkdir = 0;
134 foreach my $hook (qw(pre-auto-gc pre-receive post-commit post-receive update)) {
135 my $doln = 0;
136 my $fphook = "$projdir/hooks/$hook";
137 if (-f $fphook) {
138 if (! -l $fphook || undefval(readlink($fphook),'') ne "$globalhooks/$hook") {
139 $doln=1;
140 push(@updates, $hook);
142 } elsif (! -e $fphook) {
143 if (!$dryrun && ! -d "$projdir/hooks") {
144 if (!do_mkdir($_, $projdir, "hooks", $owning_group_id)) {
145 pwarn "$_: missing hooks subdirectory could not be created\n"
146 unless $qhkdir || $quiet;
147 $qhkdir = 1;
148 $bad = 1;
151 $doln=1;
152 push(@updates, "+$hook");
154 if (!$dryrun && $doln && !symlink_sfn("$globalhooks/$hook", $fphook)) {
155 pwarn "$_: failed creating hooks/$hook -> $globalhooks/$hook symlink ($!)\n"
156 unless $quiet;
157 $bad = 1;
160 foreach my $hook (qw(post-update)) {
161 if (-e "$projdir/hooks/$hook") {
162 if (!$dryrun && !unlink("$projdir/hooks/$hook")) {
163 pwarn "$_: failed removing hooks/$hook ($!)\n" unless $quiet;
164 $bad = 1;
166 push(@updates, "-$hook");
169 # do hooks stuff here
170 if (-d "$projdir/mob/hooks") {
171 if (! -l "$projdir/mob/hooks" || undefval(readlink("$projdir/mob/hooks"),'') ne "../hooks") {
172 if (!$dryrun && !symlink_sfn("../hooks", "$projdir/mob/hooks")) {
173 pwarn "$_: failed creating mob/hooks -> ../hooks symlink ($!)\n" unless $quiet;
174 $bad = 1;
176 push(@updates, "mob/hooks@ -> ../hooks");
179 @updates && $dryrun and push(@updates, "(dryrun)");
180 @updates and pmsg("$_:", @updates) unless $quiet;
181 } continue {$progress->update}
182 $progress = undef;
184 return $bad ? 1 : 0;
187 # comination of symlink + rename to force replace a symlink atomically
188 # as symlink by itself will not replace a pre-existing destination
189 # and unlink + symlink is not atomic
190 # but, if the destination is a directory an rmdir will be attempted
191 # before the rename even though that won't really be atomic, it's okay though
192 # because the rmdir will fail if the directory is not empty and if it is
193 # empty there's no window to miss running a hook since the directory didn't
194 # contain any hook in the first place (it was empty or the rmdir would've failed)
195 sub symlink_sfn
197 my ($oldfile, $newfile) = @_;
198 my $tmpnewfile = $newfile . "_$$";
199 unlink($tmpnewfile);
200 if (!symlink($oldfile, $tmpnewfile)) {
201 # failed
202 local $!;
203 unlink($tmpnewfile);
204 return 0;
206 -d $newfile && rmdir $newfile;
207 if (!rename($tmpnewfile, $newfile)) {
208 # failed
209 local $!;
210 unlink($tmpnewfile);
211 return 0;
213 # success
214 return 1;
217 sub do_mkdir
219 my ($proj, $projdir, $subdir, $grpid) = @_;
220 my $result = "";
221 my $fpsubdir = $projdir . '/' . $subdir;
222 if (!$dryrun) {
223 mkdir($fpsubdir) && -d "$fpsubdir" or $result = "FAILED";
224 if ($grpid && $grpid != $owning_group_id) {
225 my @info = stat($fpsubdir);
226 if (@info < 6 || $info[2] eq "" || $info[4] eq "" || $info[5] eq "") {
227 $result = "FAILED";
228 } elsif ($info[5] != $grpid) {
229 if (!chown($info[4], $grpid, $fpsubdir)) {
230 $result = "FAILED";
231 pwarn "chgrp: ($proj) $subdir: $!\n" unless $quiet;
232 } elsif (!chmod($info[2] & 07777, $fpsubdir)) {
233 $result = "FAILED";
234 pwarn "chmod: ($proj) $subdir: $!\n" unless $quiet;
238 } else {
239 $result = "(dryrun)";
241 pmsg("$proj: $subdir/: created", $result) unless $quiet;
242 return $result ne "FAILED";
245 __END__
247 =head1 NAME
249 update-all-hooks.pl - Update all projects' hooks
251 =head1 SYNOPSIS
253 update-all-hooks.pl [<options>] [<projname>]...
255 Options:
256 -h | --help detailed instructions
257 -V | --version show version
258 -n | --dry-run show what would be done but don't do it
259 -q | --quiet suppress change messages
261 <projname> if given, only operate on these projects
263 =head1 OPTIONS
265 =over 8
267 =item B<-h>, B<--help>
269 Print the full description of update-all-hook.pl's options.
271 =item B<-V>, B<--version>
273 Print the version of update-all-hooks.pl.
275 =item B<-n>, B<--dry-run>
277 Do not actually make any changes, just show what would be done without
278 actually doing it.
280 =item B<-q>, B<--quiet>
282 Suppress the messages about what's actually being changed. This option
283 is ignored if B<--dry-run> is in effect.
285 The warnings about missing and unknown-to-Girocco projects are also
286 suppressed by this option.
288 =item B<<projname>>
290 If no project names are specified then I<all> projects are processed.
292 If one or more project names are specified then only those projects are
293 processed. Specifying non-existent projects produces a warning for them,
294 but the rest of the projects specified will still be processed.
296 Each B<projname> may be either a full absolute path starting with
297 $Girocco::Config::reporoot or just the project name part with or without
298 a trailing C<.git>.
300 Any explicitly specified projects that do exist but are not known to
301 Girocco will be skipped (with a warning).
303 =back
305 =head1 DESCRIPTION
307 Inspect the project hooks of Girocco projects (i.e. $GIT_DIR/hooks) and
308 look for anomalies and out-of-date or missing hooks.
310 Only the hooks directory and its contents are checked, the config item
311 C<core.hooksPath> (supported by Git versions 2.9.0 and later) is I<NOT>
312 inspected. But C<update-all-config.pl> takes cares of that setting.
314 If an explicity specified project is located under $Girocco::Config::reporoot
315 but is not actually known to Girocco (i.e. it's not in the etc/group file)
316 then it will be skipped.
318 By default, any anomalies or out-of-date hooks will be corrected with a
319 message to that effect. However using B<--dry-run> will only show the
320 correction(s) which would be made without making them and B<--quiet> will make
321 the correction(s) without any messages.
323 Any projects that have a C<$GIT_DIR/.nohooks> file are always skipped (with a
324 message unless B<--quiet> is used).
326 =cut