update-all-config: convert from .sh to .pl
[girocco.git] / toolbox / update-all-config.pl
blob1895114935515b3d2ff0a5eda372c602d83b4fbb
1 #!/usr/bin/perl
3 # update-all-config.pl - Update all out-of-date config
5 use strict;
6 use warnings;
7 use vars qw($VERSION);
8 BEGIN {*VERSION = \'2.0'}
9 use File::Basename;
10 use Cwd qw(realpath);
11 use Getopt::Long;
12 use Pod::Usage;
13 use lib "__BASEDIR__";
14 use Girocco::Config;
15 use Girocco::Util;
16 use Girocco::CLIUtil;
17 use Girocco::Project;
19 my $shbin;
20 BEGIN {
21 $shbin = $Girocco::Config::posix_sh_bin;
22 defined($shbin) && $shbin ne "" or $shbin = "/bin/sh";
25 exit(&main(@ARGV)||0);
27 my ($dryrun, $force, $quiet);
29 sub die_usage {
30 pod2usage(-exitval => 2);
33 sub do_help {
34 pod2usage(-verbose => 2, -exitval => 0);
37 sub do_version {
38 print basename($0), " version ", $VERSION, "\n";
39 exit 0;
42 my ($dmode, $dperm, $drwxmode, $fmode, $fmodeoct, $fperm, $wall);
43 BEGIN {
44 $dmode=02775;
45 $dperm='drwxrwsr-x';
46 $drwxmode='ug+rwx,o+rx';
47 $fmode=0664;
48 $fmodeoct='0664';
49 $fperm='-rw-rw-r--';
50 $wall=0;
53 my $owning_group_id;
55 sub main {
56 local *ARGV = \@_;
57 my ($help, $version);
59 umask 002;
60 close(DATA) if fileno(DATA);
61 Getopt::Long::Configure('bundling');
62 GetOptions(
63 'help|h' => sub {do_help},
64 'version|V' => sub {do_version},
65 'dry-run|n' => \$dryrun,
66 'quiet|q' => \$quiet,
67 'force|f' => \$force,
68 ) or die_usage;
69 $dryrun and $quiet = 0;
71 -f jailed_file("/etc/group") or
72 die "Girocco group file not found: " . jailed_file("/etc/group") . "\n";
74 if (!defined($Girocco::Config::owning_group) || $Girocco::Config::owning_group eq "") {
75 die "\$Girocco::Config::owning_group unset, refusing to run without --force\n" unless $force;
76 $dmode=02777;
77 $dperm='drwxrwsrwx';
78 $drwxmode='a+rwx';
79 $fmode=0666;
80 $fmodeoct='0666';
81 $fperm='-rw-rw-rw-';
82 $wall=1;
83 warn "Mode 666 in effect\n" unless $quiet;
84 } elsif (($owning_group_id = scalar(getgrnam($Girocco::Config::owning_group))) !~ /^\d+$/) {
85 die "\$Girocco::Config::owning_group invalid ($Girocco::Config::owning_group), refusing to run\n";
88 my @allprojs = Girocco::Project::get_full_list;
89 my @projects = ();
91 my $root = $Girocco::Config::reporoot;
92 $root or die "\$Girocco::Config::reporoot is invalid\n";
93 $root =~ s,/+$,,;
94 $root ne "" or $root = "/";
95 $root = realpath($root);
96 if (@ARGV) {
97 my %projnames = map {($_ => 1)} @allprojs;
98 foreach (@ARGV) {
99 s,/+$,,;
100 $_ or $_ = "/";
101 -d $_ and $_ = realpath($_);
102 s,^\Q$root\E/,,;
103 s,\.git$,,;
104 if (!exists($projnames{$_})) {
105 warn "$_: unknown to Girocco (not in etc/group)\n"
106 unless $quiet;
107 next;
109 push(@projects, $_);
111 } else {
112 @projects = sort {lc($a) cmp lc($b)} @allprojs;
115 my $bad = 0;
116 foreach (@projects) {
117 my $projdir = "$root/$_.git";
118 if (! -d "$projdir") {
119 warn "$_: does not exist -- skipping\n" unless $quiet;
120 next;
122 if (!is_git_dir($projdir)) {
123 warn "$_: is not a .git directory -- skipping\n" unless $quiet;
124 next;
126 if (-e "$projdir/.noconfig") {
127 warn "$_: found .noconfig -- skipping\n" unless $quiet;
128 next;
130 process_one_project($_, $projdir) or $bad = 1;
133 return $bad ? 1 : 0;
136 my (@mkdirs, @mkfiles);
137 my (@fixdpermsdirs, @fixdpermsrwx, @fixfpermsfiles, @fixfpermsdirs);
138 BEGIN {
139 @mkdirs = qw(refs info hooks ctags htmlcache bundles reflogs objects objects/info);
140 @mkfiles = qw(config info/lastactivity);
141 @fixdpermsdirs = qw(. refs info ctags htmlcache bundles reflogs objects objects/info);
142 @fixdpermsrwx = qw(refs objects);
143 @fixfpermsfiles = qw(HEAD config description packed-refs README.html info/lastactivity
144 info/alternates info/http-alternates info/packs);
145 @fixfpermsdirs = qw(ctags);
148 my (@boolvars, @falsevars, @false0vars, @truevars);
149 BEGIN {
150 @boolvars = qw(gitweb.statusupdates);
151 @falsevars = qw(core.ignorecase receive.denynonfastforwards);
152 @false0vars = qw(gc.auto receive.autogc);
153 @truevars = qw(receive.updateserverinfo repack.writebitmaps transfer.fsckobjects);
156 my $hdr;
158 sub defval($$) {
159 return defined($_[0]) ? $_[0] : $_[1];
162 sub process_one_project
164 my ($proj, $projdir) = @_;
165 my $bad = 0;
166 my $reallybad = 0;
167 $hdr = 0;
168 do {
169 if (! -d "$projdir/$_") {
170 if (-e "$projdir/$_") {
171 warn "$proj: bypassing project, exists but not directory: $_\n" unless $quiet;
172 $reallybad = $bad = 1;
173 last;
174 } else {
175 do_mkdir($proj, $projdir, $_) or $bad = 1, last;
178 } foreach (@mkdirs);
179 return 0 if $reallybad;
181 $ENV{PROJDIR} = $projdir;
182 -d "$projdir/$_" && check_dperm($proj, $projdir, $_) or $bad = 1 foreach (@fixdpermsdirs);
183 my @dirs = split(/\n+/, qx(cd "\$PROJDIR" &&
184 exec find @fixdpermsrwx -xdev -type d \\( ! -path "objects/??" -o -prune \\) ! -perm -$drwxmode -print 2>/dev/null
185 )||"");
186 change_dpermrwx($proj, $projdir, $_) or $bad = 1 foreach (@dirs);
187 @dirs = split(/\n+/, qx(cd "\$PROJDIR" &&
188 exec find . -xdev -type d \\( ! -path "./objects/??" -o -prune \\) ! -perm -a+rx -print
189 )||"");
190 change_dpermrx($proj, $projdir, $_) or $bad = 1 foreach (@dirs);
192 do {
193 if (-e "$projdir/$_") {
194 if (! -f "$projdir/$_") {
195 warn "$proj: bypassing project, exists but not file: $_\n" unless $quiet;
196 $reallybad = $bad = 1;
197 last;
199 } else {
200 my $result = "(dryrun)";
201 if (!$dryrun) {
202 $result = "";
203 my $tf;
204 open($tf, '>', "$projdir/$_") && close ($tf) or $result = "FAILED", $bad = 1;
206 pmsg($proj, "$_: created", $result) unless $quiet;
208 } foreach(@mkfiles);
209 return 0 if $reallybad;
211 $dryrun || check_fperm config or $bad = 1;
212 my $config = read_config_file_hash("$projdir/config", !$quiet);
213 if (!defined($config)) {
214 warn "$proj: could not read config file -- skipping\n" unless $quiet;
215 return 0;
218 my $do_config = sub {
219 my ($item, $val) = @_;
220 my $oldval = defval($config->{$item},"");
221 my $result = "(dryrun)";
222 if (!$dryrun) {
223 $result = "";
224 system($Girocco::Config::git_bin, "config", "--file", "$projdir/config", "--replace-all", $item, $val) == 0 or
225 $result = "FAILED", $bad = 1;
227 if (!exists($config->{$item})) {
228 pmsg($proj, "config $item: created \"$val\"", $result) unless $quiet;
229 } else {
230 pmsg($proj, "config $item: \"$oldval\" -> \"$val\"", $result) unless $quiet;
233 my $do_config_unset = sub {
234 my ($item, $msg) = @_;
235 defined($msg) or $msg = "";
236 $msg eq "" or $msg = " " . $msg;
237 my $oldval = defval($config->{$item},"");
238 my $result = "(dryrun)";
239 if (!$dryrun) {
240 $result = "";
241 system($Girocco::Config::git_bin, "config", "--file", "$projdir/config", "--unset-all", $item) == 0 or
242 $result = "FAILED", $bad = 1;
244 pmsg($proj, "config $item: removed$msg \"$oldval\"", $result) unless $quiet;
247 my $cmplvl = defval($config->{'core.compression'},"");
248 if ($cmplvl !~ /^-?\d+$/ || $cmplvl < -1 || $cmplvl > 9 || "" . (0 + $cmplvl) ne "" . $cmplvl) {
249 pmsg($proj, "WARNING: replacing invalid core.compression value: \"$cmplvl\"") unless $cmplvl eq "" || $quiet;
250 $cmplvl = "";
251 } elsif ($cmplvl != 5) {
252 pmsg($proj, "WARNING: suboptimal core.compression value left unchanged: \"$cmplvl\"") unless $quiet;
254 $cmplvl ne "" or &$do_config('core.compression', 5);
255 my $grpshr = defval($config->{'core.sharedrepository'},"");
256 if ($grpshr eq "" || (valid_bool($grpshr) && !git_bool($grpshr))) {
257 &$do_config('core.sharedrepository', 1);
258 } elsif (!(valid_bool($grpshr) && git_bool($grpshr))) {
259 pmsg($proj, "WARNING: odd core.sharedrepository value left unchanged: \"$grpshr\"");
261 if (git_bool($config->{'core.bare'})) {
262 my $setlaru = 1;
263 my $laru = $config->{'core.logallrefupdates'};
264 if (defined($laru)) {
265 if (valid_bool($laru)) {
266 $setlaru = 0;
267 if (git_bool($laru)) {
268 pmsg($proj, "WARNING: core.logallrefupdates is true (left unchanged)") unless $quiet;
270 } else {
271 pmsg($proj, "WARNING: replacing non-boolean core.logallrefupdates value") unless $quiet;
274 !$setlaru or &$do_config('core.logallrefupdates', 'false');
275 } else {
276 pmsg($proj, "WARNING: core.bare is not true (left unchanged)") unless $quiet;
278 defval($config->{'transfer.unpacklimit'},"") eq "1" or &$do_config('transfer.unpacklimit', 1);
279 lc(defval($config->{'receive.denydeletecurrent'},"")) eq "warn" or &$do_config('receive.denydeletecurrent', 'warn');
280 do {
281 !exists($config->{$_}) || valid_bool(defval($config->{$_},"")) or &$do_config_unset($_, "(not a boolean)");
282 } foreach (@boolvars);
283 do {
284 (valid_bool(defval($config->{$_},"")) && !git_bool($config->{$_})) or &$do_config($_, "false");
285 } foreach (@falsevars);
286 do {
287 (valid_bool(defval($config->{$_},"")) && !git_bool($config->{$_})) or &$do_config($_, 0);
288 } foreach (@false0vars);
289 do {
290 (valid_bool(defval($config->{$_},"")) && git_bool($config->{$_})) or &$do_config($_, "true");
291 } foreach (@truevars);
293 if (defined($Girocco::Config::owning_group) && $Girocco::Config::owning_group ne "") {
294 my @items = split(/\n+/, qx(cd "\$PROJDIR" &&
295 exec find . -xdev \\( -type d -o -type f \\) ! -group $Girocco::Config::owning_group -print
296 )||"");
297 change_group($proj, $projdir, $_) or $bad = 1 foreach (@items);
299 foreach (@fixfpermsfiles) {
300 if (-e "$projdir/$_") {
301 if (! -f "$projdir/$_") {
302 warn "$proj: bypassing project, exists but not file: $_\n" unless $quiet;
303 $reallybad = $bad = 1;
304 last;
306 check_fperm($proj, $projdir, $_) or $bad = 1;
309 return 0 if $reallybad;
311 my @files = split(/\n+/, qx(cd "\$PROJDIR" &&
312 exec find @fixfpermsdirs -xdev -type f ! -perm $fmodeoct -print 2>/dev/null
313 )||"");
314 check_fperm($proj, $projdir, $_) or $bad = 1 foreach (@files);
315 @files = split(/\n+/, qx(cd "\$PROJDIR" &&
316 exec find . -xdev -type f ! -perm -a+r -print
317 )||"");
318 check_fpermr($proj, $projdir, $_) or $bad = 1 foreach (@files);
319 @files = split(/\n+/, qx(cd "\$PROJDIR" &&
320 exec find . -xdev -type d \\( -path ./hooks -o -path ./mob/hooks \\) -prune -o -type f -perm +a+x -print
321 )||"");
322 check_fpermnox($proj, $projdir, $_) or $bad = 1 foreach (@files);
324 my $bu = defval($config->{'gitweb.baseurl'},"");
325 if (-e "$projdir/.nofetch") {
326 $bu eq "" or pmsg($proj, "WARNING: .nofetch exists but gitweb.baseurl is not empty ($bu)") unless $quiet;
327 } else {
328 $bu ne "" or pmsg($proj, "WARNING: gitweb.baseurl is empty and .nofetch does not exist") unless $quiet;
331 return !$bad;
334 sub do_mkdir
336 my ($proj, $projdir, $subdir) = @_;
337 my $result = "";
338 if (!$dryrun) {
339 mkdir("$projdir/$subdir") && -d "$projdir/$subdir" or $result = "FAILED";
340 } else {
341 $result = "(dryrun)";
343 pmsg($proj, "$subdir/: created", $result);
344 return $result ne "FAILED";
347 sub check_dperm {
348 my ($proj, $projdir, $subdir) = @_;
349 my $oldmode = (stat("$projdir/$subdir"))[2];
350 if (!defined($oldmode) || $oldmode eq "") {
351 warn "chmod: $projdir/$subdir: No such file or directory\n" unless $quiet;
352 return 0;
354 my $newmode = ($oldmode & ~07777) | $dmode;
355 $newmode == $oldmode and return 1;
356 my $result = "";
357 if (!$dryrun) {
358 if (!chmod($newmode & 07777, "$projdir/$subdir")) {
359 $result = "FAILED";
360 warn "chmod: $projdir/$subdir: $!\n" unless $quiet;
362 } else {
363 $result = "(dryrun)";
365 pmsg($proj, "$subdir/:", get_mode_perm($oldmode), '->', get_mode_perm($newmode), $result);
366 return $result ne "FAILED";
369 sub change_dpermrwx {
370 my ($proj, $projdir, $subdir) = @_;
371 my $oldmode = (stat("$projdir/$subdir"))[2];
372 if (!defined($oldmode) || $oldmode eq "") {
373 warn "chmod: $projdir/$subdir: No such file or directory\n" unless $quiet;
374 return 0;
376 my $newmode = $oldmode | ($wall ? 0777 : 0775);
377 $newmode == $oldmode and return 1;
378 my $result = "";
379 if (!$dryrun) {
380 if (!chmod($newmode & 07777, "$projdir/$subdir")) {
381 $result = "FAILED";
382 warn "chmod: $projdir/$subdir: $!\n" unless $quiet;
384 } else {
385 $result = "(dryrun)";
387 pmsg($proj, "$subdir/:", get_mode_perm($oldmode), '->', get_mode_perm($newmode), $result);
388 return $result ne "FAILED";
391 sub change_dpermrx {
392 my ($proj, $projdir, $subdir) = @_;
393 my $oldmode = (stat("$projdir/$subdir"))[2];
394 if (!defined($oldmode) || $oldmode eq "") {
395 warn "chmod: $projdir/$subdir: No such file or directory\n" unless $quiet;
396 return 0;
398 my $newmode = $oldmode | 0555;
399 $newmode == $oldmode and return 1;
400 my $result = "";
401 if (!$dryrun) {
402 if (!chmod($newmode & 07777, "$projdir/$subdir")) {
403 $result = "FAILED";
404 warn "chmod: $projdir/$subdir: $!\n" unless $quiet;
406 } else {
407 $result = "(dryrun)";
409 pmsg($proj, "$subdir/:", get_mode_perm($oldmode), '->', get_mode_perm($newmode), $result);
410 return $result ne "FAILED";
413 sub check_fperm {
414 my ($proj, $projdir, $file) = @_;
415 my $oldmode = (stat("$projdir/$file"))[2];
416 if (!defined($oldmode) || $oldmode eq "") {
417 warn "chmod: $projdir/$file: No such file or directory\n" unless $quiet;
418 return 0;
420 my $newmode = ($oldmode & ~07777) | $fmode;
421 $newmode == $oldmode and return 1;
422 my $result = "";
423 if (!$dryrun) {
424 if (!chmod($newmode & 07777, "$projdir/$file")) {
425 $result = "FAILED";
426 warn "chmod: $projdir/$file: $!\n" unless $quiet;
428 } else {
429 $result = "(dryrun)";
431 pmsg($proj, "$file:", get_mode_perm($oldmode), '->', get_mode_perm($newmode), $result);
432 return $result ne "FAILED";
435 sub check_fpermr {
436 my ($proj, $projdir, $file) = @_;
437 my $oldmode = (stat("$projdir/$file"))[2];
438 if (!defined($oldmode) || $oldmode eq "") {
439 warn "chmod: $projdir/$file: No such file or directory\n" unless $quiet;
440 return 0;
442 my $newmode = $oldmode | 0444;
443 $newmode == $oldmode and return 1;
444 my $result = "";
445 if (!$dryrun) {
446 if (!chmod($newmode & 07777, "$projdir/$file")) {
447 $result = "FAILED";
448 warn "chmod: $projdir/$file: $!\n" unless $quiet;
450 } else {
451 $result = "(dryrun)";
453 pmsg($proj, "$file:", get_mode_perm($oldmode), '->', get_mode_perm($newmode), $result);
454 return $result ne "FAILED";
457 sub check_fpermnox {
458 my ($proj, $projdir, $file) = @_;
459 my $oldmode = (stat("$projdir/$file"))[2];
460 if (!defined($oldmode) || $oldmode eq "") {
461 warn "chmod: $projdir/$file: No such file or directory\n" unless $quiet;
462 return 0;
464 my $newmode = $oldmode & ~0111;
465 $newmode == $oldmode and return 1;
466 my $result = "";
467 if (!$dryrun) {
468 if (!chmod($newmode & 07777, "$projdir/$file")) {
469 $result = "FAILED";
470 warn "chmod: $projdir/$file: $!\n" unless $quiet;
472 } else {
473 $result = "(dryrun)";
475 pmsg($proj, "$file:", get_mode_perm($oldmode), '->', get_mode_perm($newmode), $result);
476 return $result ne "FAILED";
479 sub change_group {
480 my ($proj, $projdir, $item) = @_;
481 my @info = stat("$projdir/$item");
482 if (@info < 6 || $info[2] eq "" || $info[4] eq "" || $info[5] eq "") {
483 warn "chgrp: $projdir/$item: No such file or directory\n" unless $quiet;
484 return 0;
486 $info[5] == $owning_group_id and return 1;
487 my $result = "";
488 if (!$dryrun) {
489 if (!chown($info[4], $owning_group_id, "$projdir/$item")) {
490 $result = "FAILED";
491 warn "chgrp: $projdir/$item: $!\n" unless $quiet;
492 } elsif (!chmod($info[2] & 07777, "$projdir/$item")) {
493 $result = "FAILED";
494 warn "chmod: $projdir/$item: $!\n" unless $quiet;
496 } else {
497 $result = "(dryrun)";
499 my $isdir = ((($info[2] >> 12) & 017) == 004) ? '/' : '';
500 pmsg($proj, "$item$isdir: group", get_grp_nam($info[5]), '->', $Girocco::Config::owning_group, $result);
501 return $result ne "FAILED";
504 my $wrote; BEGIN {$wrote = ""}
505 sub pmsg {
506 my $proj = shift;
507 my $msg = join(" ", @_);
508 $msg =~ s/\s+$//;
509 my $prefix = "";
510 if (!$hdr) {
511 $prefix = $wrote . $proj . ":\n";
512 $hdr = 1;
514 print $prefix, " ", join(' ', @_), "\n";
515 $wrote = "\n";
518 my %ftypes;
519 BEGIN {%ftypes = (
520 000 => '?',
521 001 => 'p',
522 002 => 'c',
523 003 => '?',
524 004 => 'd',
525 005 => '?',
526 006 => 'b',
527 007 => '?',
528 010 => '-',
529 011 => '?',
530 012 => 'l',
531 013 => '?',
532 014 => 's',
533 015 => '?',
534 016 => 'w',
535 017 => '?'
537 my %fperms;
538 BEGIN {%fperms = (
539 0 => '---',
540 1 => '--x',
541 2 => '-w-',
542 3 => '-wx',
543 4 => 'r--',
544 5 => 'r-x',
545 6 => 'rw-',
546 7 => 'rwx'
549 sub get_mode_perm {
550 my $mode = $_[0];
551 my $str = $ftypes{($mode >> 12) & 017} .
552 $fperms{($mode >> 6) & 7} .
553 $fperms{($mode >> 3) & 7} .
554 $fperms{$mode & 7};
555 substr($str,3,1) = ($mode & 0100) ? 's' : 'S' if $mode & 04000;
556 substr($str,6,1) = ($mode & 0010) ? 's' : 'S' if $mode & 02000;
557 substr($str,9,1) = ($mode & 0001) ? 't' : 'T' if $mode & 01000;
558 return $str;
561 sub get_perm {
562 my $mode = (stat($_[0]))[2];
563 defined($mode) or return '??????????';
564 return get_mode_perm($mode);
567 sub get_grp_nam {
568 my $grpid = $_[0];
569 defined($grpid) or return '?';
570 my $grpnm = scalar(getgrgid($grpid));
571 return defined($grpnm) && $grpnm ne "" ? $grpnm : $grpid;
574 sub get_grp {
575 my $grp = (stat($_[0]))[5];
576 defined($grp) or return '?';
577 return get_grp_nam($grp);
580 __END__
582 =head1 NAME
584 update-all-config.pl - Update all projects' config settings
586 =head1 SYNOPSIS
588 update-all-config.pl [<options>] [<projname>]...
590 Options:
591 -h | --help detailed instructions
592 -V | --version show version
593 -n | --dry-run show what would be done but don't do it
594 -f | --force run without a Config.pm owning_group
595 -q | --quiet suppress change messages
597 <projname> if given, only operate on these projects
599 =head1 OPTIONS
601 =over 8
603 =item B<-h>, B<--help>
605 Print the full description of update-all-config.pl's options.
607 =item B<-V>, B<--version>
609 Print the version of update-all-config.pl.
611 =item B<-n>, B<--dry-run>
613 Do not actually make any changes, just show what would be done without
614 actually doing it.
616 =item B<-q>, B<--quiet>
618 Suppress the messages about what's actually being changed. This option
619 is ignored if B<--dry-run> is in effect.
621 The warnings about missing and unknown-to-Girocco projects are also
622 suppressed by this option.
624 =item B<-f>, B<--force>
626 Allow running without a $Girocco::Config::owning_group set. This is not
627 recommended as it results in world-writable items being used (instead of
628 just world-readable).
630 =item B<<projname>>
632 If no project names are specified then I<all> projects are processed.
634 If one or more project names are specified then only those projects are
635 processed. Specifying non-existent projects produces a warning for them,
636 but the rest of the projects specified will still be processed.
638 Each B<projname> may be either a full absolute path starting with
639 $Girocco::Config::reporoot or just the project name part with or without
640 a trailing C<.git>.
642 Any explicitly specified projects that do exist but are not known to
643 Girocco will be skipped (with a warning).
645 =back
647 =head1 DESCRIPTION
649 Inspect the C<config> files of Girocco projects (i.e. $GIT_DIR/config) and
650 look for anomalies and out-of-date settings.
652 Additionally check the existence and permissions on various files and
653 directories in the project.
655 If an explicity specified project is located under $Girocco::Config::reporoot
656 but is not actually known to Girocco (i.e. it's not in the etc/group file)
657 then it will be skipped.
659 By default, any anomalies or out-of-date settings will be corrected with a
660 message to that effect. However using B<--dry-run> will only show the
661 correction(s) which would be made without making them and B<--quiet> will make
662 the correction(s) without any messages.
664 Any projects that have a C<$GIT_DIR/.noconfig> file are always skipped (with a
665 message unless B<--quiet> is used).
667 =cut