3 # update-all-hooks.pl - Update all out-of-date hooks and install missing
8 BEGIN {*VERSION = \'2.0'}
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__";
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);
38 pod2usage
(-exitval
=> 2);
42 pod2usage
(-verbose
=> 2, -exitval
=> 0);
46 print basename
($0), " version ", $VERSION, "\n";
53 sub pmsg
{ $progress->emit(@_) }
54 sub pwarn
{ $progress->warn(@_) }
56 sub undefval
($$) { return defined($_[0]) ?
$_[0] : $_[1] }
63 close(DATA
) if fileno(DATA
);
64 Getopt
::Long
::Configure
('bundling');
66 'help|h' => sub {do_help
},
67 'version|V' => sub {do_version
},
68 'dry-run|n' => \
$dryrun,
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.
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
;
88 my $root = $Girocco::Config
::reporoot
;
89 $root && -d
$root or die "\$Girocco::Config::reporoot is invalid\n";
91 $root ne "" or $root = "/";
92 $root = $1 if $root =~ m
|^(/.+)$|;
93 my $globalhooks="$root/_global/hooks";
95 my %projnames = map {($_ => 1)} @allprojs;
99 -d
$_ and $_ = realpath
($_);
100 $_ = $1 if $_ =~ m
|^(.+)$|;
103 if (!exists($projnames{$_})) {
104 warn "$_: unknown to Girocco (not in etc/group)\n"
111 @projects = sort {lc($a) cmp lc($b) || $a cmp $b} @allprojs;
116 $progress = Girocco
::CLIUtil
::Progress
->new(scalar(@projects),
118 foreach (@projects) {
119 my $projdir = "$root/$_.git";
121 pwarn
"$_: does not exist -- skipping\n" unless $quiet;
124 if (!is_git_dir
($projdir)) {
125 pwarn
"$_: is not a .git directory -- skipping\n" unless $quiet;
128 if (-e
"$projdir/.nohooks") {
129 pwarn
"$_: found .nohooks -- skipping\n" unless $quiet;
134 foreach my $hook (qw(pre-auto-gc pre-receive post-commit post-receive update)) {
136 my $fphook = "$projdir/hooks/$hook";
138 if (! -l
$fphook || undefval
(readlink($fphook),'') ne "$globalhooks/$hook") {
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;
152 push(@updates, "+$hook");
154 if (!$dryrun && $doln && !symlink_sfn
("$globalhooks/$hook", $fphook)) {
155 pwarn
"$_: failed creating hooks/$hook -> $globalhooks/$hook symlink ($!)\n"
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;
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;
176 push(@updates, "mob/hooks@ -> ../hooks");
179 @updates && $dryrun and push(@updates, "(dryrun)");
180 @updates and pmsg
("$_:", @updates) unless $quiet;
181 } continue {$progress->update}
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)
197 my ($oldfile, $newfile) = @_;
198 my $tmpnewfile = $newfile . "_$$";
200 if (!symlink($oldfile, $tmpnewfile)) {
206 -d
$newfile && rmdir $newfile;
207 if (!rename($tmpnewfile, $newfile)) {
219 my ($proj, $projdir, $subdir, $grpid) = @_;
221 my $fpsubdir = $projdir . '/' . $subdir;
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 "") {
228 } elsif ($info[5] != $grpid) {
229 if (!chown($info[4], $grpid, $fpsubdir)) {
231 pwarn
"chgrp: ($proj) $subdir: $!\n" unless $quiet;
232 } elsif (!chmod($info[2] & 07777, $fpsubdir)) {
234 pwarn
"chmod: ($proj) $subdir: $!\n" unless $quiet;
239 $result = "(dryrun)";
241 pmsg
("$proj: $subdir/: created", $result) unless $quiet;
242 return $result ne "FAILED";
249 update-all-hooks.pl - Update all projects' hooks
253 update-all-hooks.pl [<options>] [<projname>]...
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
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
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.
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
300 Any explicitly specified projects that do exist but are not known to
301 Girocco will be skipped (with a warning).
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).