From 170c8118fafe239aeee32a1adb661b3d9e3b4ef0 Mon Sep 17 00:00:00 2001 From: "Kyle J. McKay" Date: Sun, 7 Mar 2021 18:46:41 -0700 Subject: [PATCH] update-all-hooks: convert to show better progress and errors Use the new Progress class to show a better running progress indicator since this operation can take a very long time when there are many projects and it's been run nice'd. Take advantage of the conversion to also show better error messages on any failures. Functionality remains unchanged. Signed-off-by: Kyle J. McKay --- toolbox/update-all-hooks.pl | 326 +++++++++++++++++++++++++++++++++++++++++ toolbox/update-all-hooks.sh | 85 +---------- toolbox/update-all-projects.sh | 2 +- 3 files changed, 334 insertions(+), 79 deletions(-) create mode 100755 toolbox/update-all-hooks.pl rewrite toolbox/update-all-hooks.sh (99%) diff --git a/toolbox/update-all-hooks.pl b/toolbox/update-all-hooks.pl new file mode 100755 index 0000000..d90a597 --- /dev/null +++ b/toolbox/update-all-hooks.pl @@ -0,0 +1,326 @@ +#!/usr/bin/perl + +# update-all-hooks.pl - Update all out-of-date hooks and install missing + +use strict; +use warnings; +use vars qw($VERSION); +BEGIN {*VERSION = \'2.0'} +use File::Basename; +use File::Spec; +use Cwd qw(realpath); +use POSIX qw(); +use Getopt::Long; +use Pod::Usage; +BEGIN { + eval 'require Pod::Text::Termcap; 1;' and + @Pod::Usage::ISA = (qw( Pod::Text::Termcap )); + defined($ENV{PERLDOC}) && $ENV{PERLDOC} ne "" or + $ENV{PERLDOC} = "-oterm -oman"; +} +use lib "__BASEDIR__"; +use Girocco::Config; +use Girocco::Util; +use Girocco::CLIUtil; +use Girocco::Project; + +my $shbin; +BEGIN { + $shbin = $Girocco::Config::posix_sh_bin; + defined($shbin) && $shbin ne "" or $shbin = "/bin/sh"; +} + +exit(&main(@ARGV)||0); + +my ($dryrun, $force, $quiet); + +sub die_usage { + pod2usage(-exitval => 2); +} + +sub do_help { + pod2usage(-verbose => 2, -exitval => 0); +} + +sub do_version { + print basename($0), " version ", $VERSION, "\n"; + exit 0; +} + +my $owning_group_id; +my $progress; + +sub pmsg { $progress->emit(@_) } +sub pwarn { $progress->warn(@_) } + +sub undefval($$) { return defined($_[0]) ? $_[0] : $_[1] } + +sub main { + local *ARGV = \@_; + my ($help, $version); + + umask 002; + close(DATA) if fileno(DATA); + Getopt::Long::Configure('bundling'); + GetOptions( + 'help|h' => sub {do_help}, + 'version|V' => sub {do_version}, + 'dry-run|n' => \$dryrun, + 'quiet|q' => \$quiet, + # --force currently doesn't do anything, but must be accepted + # because update-all-projects.sh will pass it along to both + # update-all-config and update-all-hooks and it *does* do something + # for update-all-config. + 'force|f' => \$force, + ) or die_usage; + $dryrun and $quiet = 0; + + -f jailed_file("/etc/group") or + die "Girocco group file not found: " . jailed_file("/etc/group") . "\n"; + + if (($owning_group_id = scalar(getgrnam($Girocco::Config::owning_group))) !~ /^\d+$/) { + die "\$Girocco::Config::owning_group invalid ($Girocco::Config::owning_group), refusing to run\n"; + } + + my @allprojs = Girocco::Project::get_full_list; + my @projects = (); + + my $root = $Girocco::Config::reporoot; + $root && -d $root or die "\$Girocco::Config::reporoot is invalid\n"; + $root =~ s,/+$,,; + $root ne "" or $root = "/"; + $root = $1 if $root =~ m|^(/.+)$|; + my $globalhooks="$root/_global/hooks"; + if (@ARGV) { + my %projnames = map {($_ => 1)} @allprojs; + foreach (@ARGV) { + s,/+$,,; + $_ or $_ = "/"; + -d $_ and $_ = realpath($_); + $_ = $1 if $_ =~ m|^(.+)$|; + s,^\Q$root\E/,,; + s,\.git$,,; + if (!exists($projnames{$_})) { + warn "$_: unknown to Girocco (not in etc/group)\n" + unless $quiet; + next; + } + push(@projects, $_); + } + } else { + @projects = sort {lc($a) cmp lc($b) || $a cmp $b} @allprojs; + } + + nice_me(18); + my $bad = 0; + $progress = Girocco::CLIUtil::Progress->new(scalar(@projects), + "Updating hooks"); + foreach (@projects) { + my $projdir = "$root/$_.git"; + if (! -d $projdir) { + pwarn "$_: does not exist -- skipping\n" unless $quiet; + next; + } + if (!is_git_dir($projdir)) { + pwarn "$_: is not a .git directory -- skipping\n" unless $quiet; + next; + } + if (-e "$projdir/.nohooks") { + pwarn "$_: found .nohooks -- skipping\n" unless $quiet; + next; + } + my @updates = (); + my $qhkdir = 0; + foreach my $hook (qw(pre-auto-gc pre-receive post-commit post-receive update)) { + my $doln = 0; + my $fphook = "$projdir/hooks/$hook"; + if (-f $fphook) { + if (! -l $fphook || undefval(readlink($fphook),'') ne "$globalhooks/$hook") { + $doln=1; + push(@updates, $hook); + } + } elsif (! -e $fphook) { + if (!$dryrun && ! -d "$projdir/hooks") { + if (!do_mkdir($_, $projdir, "hooks", $owning_group_id)) { + pwarn "$_: missing hooks subdirectory could not be created\n" + unless $qhkdir || $quiet; + $qhkdir = 1; + $bad = 1; + } + } + $doln=1; + push(@updates, "+$hook"); + } + if (!$dryrun && $doln && !symlink_sfn("$globalhooks/$hook", $fphook)) { + pwarn "$_: failed creating hooks/$hook -> $globalhooks/$hook symlink ($!)\n" + unless $quiet; + $bad = 1; + } + } + foreach my $hook (qw(post-update)) { + if (-e "$projdir/hooks/$hook") { + if (!$dryrun && !unlink("$projdir/hooks/$hook")) { + pwarn "$_: failed removing hooks/$hook ($!)\n" unless $quiet; + $bad = 1; + } + push(@updates, "-$hook"); + } + } + # do hooks stuff here + if (-d "$projdir/mob/hooks") { + if (! -l "$projdir/mob/hooks" || undefval(readlink("$projdir/mob/hooks"),'') ne "../hooks") { + if (!$dryrun && !symlink_sfn("../hooks", "$projdir/mob/hooks")) { + pwarn "$_: failed creating mob/hooks -> ../hooks symlink ($!)\n" unless $quiet; + $bad = 1; + } + push(@updates, "mob/hooks@ -> ../hooks"); + } + } + @updates && $dryrun and push(@updates, "(dryrun)"); + @updates and pmsg("$_:", @updates) unless $quiet; + } continue {$progress->update} + $progress = undef; + + return $bad ? 1 : 0; +} + +# comination of symlink + rename to force replace a symlink atomically +# as symlink by itself will not replace a pre-existing destination +# and unlink + symlink is not atomic +# but, if the destination is a directory an rmdir will be attempted +# before the rename even though that won't really be atomic, it's okay though +# because the rmdir will fail if the directory is not empty and if it is +# empty there's no window to miss running a hook since the directory didn't +# contain any hook in the first place (it was empty or the rmdir would've failed) +sub symlink_sfn +{ + my ($oldfile, $newfile) = @_; + my $tmpnewfile = $newfile . "_$$"; + unlink($tmpnewfile); + if (!symlink($oldfile, $tmpnewfile)) { + # failed + local $!; + unlink($tmpnewfile); + return 0; + } + -d $newfile && rmdir $newfile; + if (!rename($tmpnewfile, $newfile)) { + # failed + local $!; + unlink($tmpnewfile); + return 0; + } + # success + return 1; +} + +sub do_mkdir +{ + my ($proj, $projdir, $subdir, $grpid) = @_; + my $result = ""; + my $fpsubdir = $projdir . '/' . $subdir; + if (!$dryrun) { + mkdir($fpsubdir) && -d "$fpsubdir" or $result = "FAILED"; + if ($grpid && $grpid != $owning_group_id) { + my @info = stat($fpsubdir); + if (@info < 6 || $info[2] eq "" || $info[4] eq "" || $info[5] eq "") { + $result = "FAILED"; + } elsif ($info[5] != $grpid) { + if (!chown($info[4], $grpid, $fpsubdir)) { + $result = "FAILED"; + pwarn "chgrp: ($proj) $subdir: $!\n" unless $quiet; + } elsif (!chmod($info[2] & 07777, $fpsubdir)) { + $result = "FAILED"; + pwarn "chmod: ($proj) $subdir: $!\n" unless $quiet; + } + } + } + } else { + $result = "(dryrun)"; + } + pmsg("$proj: $subdir/: created", $result) unless $quiet; + return $result ne "FAILED"; +} + +__END__ + +=head1 NAME + +update-all-hooks.pl - Update all projects' hooks + +=head1 SYNOPSIS + +update-all-hooks.pl [] []... + + Options: + -h | --help detailed instructions + -V | --version show version + -n | --dry-run show what would be done but don't do it + -q | --quiet suppress change messages + + if given, only operate on these projects + +=head1 OPTIONS + +=over 8 + +=item B<-h>, B<--help> + +Print the full description of update-all-hook.pl's options. + +=item B<-V>, B<--version> + +Print the version of update-all-hooks.pl. + +=item B<-n>, B<--dry-run> + +Do not actually make any changes, just show what would be done without +actually doing it. + +=item B<-q>, B<--quiet> + +Suppress the messages about what's actually being changed. This option +is ignored if B<--dry-run> is in effect. + +The warnings about missing and unknown-to-Girocco projects are also +suppressed by this option. + +=item B<> + +If no project names are specified then I projects are processed. + +If one or more project names are specified then only those projects are +processed. Specifying non-existent projects produces a warning for them, +but the rest of the projects specified will still be processed. + +Each B may be either a full absolute path starting with +$Girocco::Config::reporoot or just the project name part with or without +a trailing C<.git>. + +Any explicitly specified projects that do exist but are not known to +Girocco will be skipped (with a warning). + +=back + +=head1 DESCRIPTION + +Inspect the project hooks of Girocco projects (i.e. $GIT_DIR/hooks) and +look for anomalies and out-of-date or missing hooks. + +Only the hooks directory and its contents are checked, the config item +C (supported by Git versions 2.9.0 and later) is I +inspected. But C takes cares of that setting. + +If an explicity specified project is located under $Girocco::Config::reporoot +but is not actually known to Girocco (i.e. it's not in the etc/group file) +then it will be skipped. + +By default, any anomalies or out-of-date hooks will be corrected with a +message to that effect. However using B<--dry-run> will only show the +correction(s) which would be made without making them and B<--quiet> will make +the correction(s) without any messages. + +Any projects that have a C<$GIT_DIR/.nohooks> file are always skipped (with a +message unless B<--quiet> is used). + +=cut diff --git a/toolbox/update-all-hooks.sh b/toolbox/update-all-hooks.sh dissimilarity index 99% index 813b014..3cc42bd 100755 --- a/toolbox/update-all-hooks.sh +++ b/toolbox/update-all-hooks.sh @@ -1,78 +1,7 @@ -#!/bin/sh - -# Update all out-of-date hooks in all current projects and install missing ones - -# If one or more project names are given, just update those instead - -set -e - -. @basedir@/shlib.sh - -force= -dryrun= -[ "$1" != "--force" ] && [ "$1" != "-f" ] || { force=1; shift; } -[ "$1" != "--dry-run" ] && [ "$1" != "-n" ] || { dryrun=1; shift; } -case "$1" in -*) echo "Invalid options: $1" >&2; exit 1;; esac - -if is_root; then - printf '%s\n' "$(basename "$0"): refusing to run as root -- bad things would happen" - exit 1 -fi - -umask 002 - -base="${cfg_reporoot%/}" -globalhooks="$base/_global/hooks" -hookbin="$cfg_basedir/hooks" -cmd='cut -d : -f 1 <"$cfg_chroot/etc/group" | grep -v ^_repo' -[ $# -eq 0 ] || cmd='printf "%s\n" "$@"' -eval "$cmd" | -( - while read -r proj; do - proj="${proj#$base/}" - proj="${proj%.git}" - projdir="$base/$proj.git" - [ -d "$projdir" ] || { echo "$proj: does not exist -- skipping"; continue; } - ! [ -e "$projdir/.nohooks" ] || { echo "$proj: .nohooks found -- skipping"; continue; } - updates= - for hook in pre-auto-gc pre-receive post-commit post-receive update; do - doln= - if [ -f "$projdir/hooks/$hook" ]; then - if - ! [ -L "$projdir/hooks/$hook" ] || - [ "$(readlink "$projdir/hooks/$hook")" != "$globalhooks/$hook" ] - then - doln=1 - updates="$updates $hook" - fi - elif ! [ -e "$projdir/hooks/$hook" ]; then - [ -n "$dryrun" ] || [ -d "$projdir/hooks" ] || mkdir "$projdir/hooks" - doln=1 - updates="$updates +$hook" - fi - if [ -z "$dryrun" ] && [ -n "$doln" ]; then - ln -sfn "$globalhooks/$hook" "$projdir/hooks/$hook" - fi - done - for hook in post-update; do - if [ -e "$projdir/hooks/$hook" ]; then - [ -n "$dryrun" ] || rm -f "$projdir/hooks/$hook" - updates="$updates -$hook" - fi - done - if - [ -d "$projdir/mob/hooks" ] && - { - ! [ -L "$projdir/mob/hooks" ] || - [ "$(readlink "$projdir/mob/hooks")" != "../hooks" ] - } - then - if [ -z "$dryrun" ]; then - [ -L "$projdir/mob/hooks" ] || rm -rf "$projdir/mob/hooks" - ln -sfn ../hooks "$projdir/mob/hooks" - fi - updates="$updates mob/hooks@ -> ../hooks" - fi - [ -z "$updates" ] || echo "$proj:$updates" ${dryrun:+(dryrun)} - done -) +#!/bin/sh +# +# update-all-hooks - Update all out-of-date hooks +# The old update-all-hooks.sh has been replaced by update-all-hooks.pl; +# it now simply runs that. + +exec @basedir@/toolbox/update-all-hooks.pl "$@" diff --git a/toolbox/update-all-projects.sh b/toolbox/update-all-projects.sh index 89eefd4..a0df417 100755 --- a/toolbox/update-all-projects.sh +++ b/toolbox/update-all-projects.sh @@ -36,6 +36,6 @@ for r in $required; do done [ -z "$bad" ] || exit 1 echo "Running update-all-hooks..." -"$mydir/update-all-hooks.sh" ${force:+--force} ${dryrun:+--dry-run} "$@" +"$mydir/update-all-hooks.pl" ${force:+--force} ${dryrun:+--dry-run} "$@" echo "Running update-all-config..." "$mydir/update-all-config.pl" ${force:+--force} ${dryrun:+--dry-run} "$@" -- 2.11.4.GIT