3 #==============================================================================
5 # File ID: f1ba77e4-444e-11e0-963c-00023faf1383
7 # Update many git local repositories at once
10 # ©opyleft 2011– Øyvind A. Holm <sunny@sunbase.org>
11 # License: GNU General Public License version 2 or later, see end of file for
13 #==============================================================================
17 use Cwd
qw{ abs_path getcwd
};
25 'aggressive-compress' => 0,
30 'delete-dangling' => 0,
39 'ga-update-desc' => 0,
52 our @opt_dirs_from = ();
53 our @opt_exec_after = ();
54 our @opt_exec_before = ();
57 $progname =~ s/^.*\/(.*?)$/$1/;
58 our $VERSION = '0.6.6';
60 my $archive_disk = "seagate-3tb"; # FIXME: Hardcoding
62 Getopt
::Long
::Configure
('bundling');
65 'aggressive-compress|C' => \
$Opt{'aggressive-compress'},
66 'all-options|A' => \
$Opt{'all-options'},
67 'allbr|a+' => \
$Opt{'allbr'},
68 'compress|c' => \
$Opt{'compress'},
69 'dangling|d' => \
$Opt{'dangling'},
70 'delete-dangling|D' => \
$Opt{'delete-dangling'},
71 'dirs-from=s' => \
@opt_dirs_from,
72 'dry-run|n' => \
$Opt{'dry-run'},
73 'exec-after|e=s' => \
@opt_exec_after,
74 'exec-before|E=s' => \
@opt_exec_before,
75 'fetch-prune|F' => \
$Opt{'fetch-prune'},
76 'fetch|f' => \
$Opt{'fetch'},
77 'ga-dropget|G' => \
$Opt{'ga-dropget'},
78 'ga-dropunused|u' => \
$Opt{'ga-dropunused'},
79 'ga-getnew|N' => \
$Opt{'ga-getnew'},
80 'ga-moveunused|U' => \
$Opt{'ga-moveunused'},
81 'ga-sync|g' => \
$Opt{'ga-sync'},
82 'ga-update-desc|S' => \
$Opt{'ga-update-desc'},
83 'help|h' => \
$Opt{'help'},
84 'lpar|l' => \
$Opt{'lpar'},
85 'pull|p' => \
$Opt{'pull'},
86 'push|P' => \
$Opt{'push'},
87 'quiet|q+' => \
$Opt{'quiet'},
88 'recursive|r' => \
$Opt{'recursive'},
89 'submodule|s' => \
$Opt{'submodule'},
90 'test|t' => \
$Opt{'test'},
91 'verbose|v+' => \
$Opt{'verbose'},
92 'version' => \
$Opt{'version'},
94 ) || die("$progname: Option error. Use -h for help.\n");
96 $Opt{'verbose'} -= $Opt{'quiet'};
97 $Opt{'help'} && usage
(0);
98 if ($Opt{'version'}) {
109 ($Opt{'ga-dropunused'} && $Opt{'ga-moveunused'}) &&
110 die("$progname: Can't use -u/--ga-dropunused and " .
111 "-U/--ga-moveunused at the same time\n");
113 if ($Opt{'all-options'}) {
115 $Opt{'dangling'} = 1;
116 $Opt{'fetch-prune'} = 1;
118 $Opt{'ga-update-desc'} = 1;
122 $Opt{'submodule'} = 1;
124 ($Opt{'ga-dropget'} || $Opt{'ga-dropunused'}
125 || $Opt{'ga-moveunused'}) &&
126 ($Opt{'ga-sync'} = 1);
129 my @err_compress = ();
130 my @err_dangling = ();
138 if ($Opt{'recursive'}) {
139 my $repos = `find . -type d -name .git -print0`;
140 $repos =~ s/\/\.git\000/\000/g
;
142 @Dirs = sort(grep { !$dupdir{$_}++ }
143 (@Dirs, split("\000", $repos)));
146 my $orig_dir = getcwd
();
147 my ($total_before, $total_after, $total_saved) = (0, 0, 0);
148 my ($totnum_before, $totnum_after) = (0, 0);
150 for my $dirfile (@opt_dirs_from) {
151 if ($dirfile eq '-') {
157 open(DirFP
, "<$dirfile") or
158 die("$progname: $dirfile: Cannot read " .
168 LOOP
: for my $f (@Dirs) {
170 -d
"$f/.git/." && ($object_dir = ".git/objects");
171 -d
"$f/objects/." && ($object_dir = "objects");
172 msg
(2, "object_dir = '$object_dir'");
173 if (length($object_dir)) {
175 warn("$progname: $f: Cannot chdir: $!\n");
178 $Opt{'verbose'} >= -1 &&
179 printf("================ %s "
180 . "================\n",
183 (chdir('..'), $object_dir = ".git/objects");
184 my $is_bare = (get_config
('core.bare') =~ /^true/)
187 msg
(2, "is_bare = '$is_bare'");
189 my $Lh = "[0-9a-fA-F]";
190 my $uuid_templ = "$Lh\{8}-$Lh\{4}-$Lh\{4}-" .
192 my $is_annex = (get_config
('annex.uuid') =~
196 msg
(2, "is_annex = '$is_annex'");
198 if (should
('exec-before')) {
199 for my $arg (@opt_exec_before) {
203 if (should
('lpar')) {
206 if (should
('test')) {
207 mysystem
("git", "fsck") && (
209 warn("$progname: $f: ERRORS FOUND! " .
210 "Skipping other actions " .
215 if (should
('fetch-prune')) {
216 mysystem
("git", "fetch", "--all", "--prune") &&
217 push(@err_fetch, $f);
218 } elsif (should
('fetch')) {
219 mysystem
("git", "fetch", "--all") &&
220 push(@err_fetch, $f);
222 if (should
('pull') && !$is_bare) {
223 mysystem
("git", "pull", "--ff-only") &&
225 if (-e
'.emptydirs') {
226 mysystem
('git', 'restore-dirs');
230 (should
('ga-sync') || should
('ga-getnew'))) {
231 should
('ga-sync') && mysystem
("ga", "sync") &&
232 push(@err_gasync, $f);
233 if (should
('ga-dropget')) {
234 mysystem
("ga", "drop", "--auto");
235 mysystem
("ga", "sync");
237 if (should
('ga-dropunused')) {
238 mysystem
("ga", "unused");
239 mysystem
("ga", "dropunused", "all");
240 mysystem
("ga", "sync");
242 if (should
('ga-moveunused')) {
243 if (open(my $fh, "git remote |")) {
245 while (my $curr_remote = <$fh>) {
246 if ($curr_remote =~ /^$archive_disk$/) {
247 mysystem
("ga", "unused");
248 mysystem
("ga", "move", "--unused",
249 "--to", $archive_disk);
250 mysystem
("ga", "sync");
256 warn("$progname: Cannot open " .
257 "'git remote' pipe: $!\n");
260 if (should
('ga-dropget') || should
('ga-getnew')) {
261 if (!$Opt{'ga-getnew'}) {
262 mysystem
("ga", "get",
264 mysystem
("ga", "sync");
266 mysystem
("ga-getnew");
270 if ($is_annex && should
('ga-update-desc')) {
271 mysystem
("ga", "update-desc");
273 if (should
('dangling')) {
274 mysystem
("git", "dangling") &&
275 push(@err_dangling, $f);
277 if (should
('allbr') &&
278 ($is_bare || $Opt{'allbr'} > 1)) {
279 mysystem
("git", "nobr");
280 mysystem
("git", "allbr", "-a");
281 mysystem
("git", "checkout", "-");
283 if (should
('push')) {
284 mysystem
("git", "pa") && push(@err_push, $f);
286 if (should
('submodule') && -e
".gitmodules") {
287 mysystem
("git", "submodule", "init");
288 mysystem
("git", "submodule", "update") &&
289 push(@err_updsub, $f);
291 if (should
('compress') ||
292 should
('aggressive-compress')) {
294 `(find $object_dir -type f -printf '%s+' ; echo 0) | bc`);
295 $total_before += $before;
296 chomp(my $numfiles_before =
297 `find $object_dir -type f | wc -l`);
298 $totnum_before += $numfiles_before;
299 $Opt{'dry-run'} || print("\n");
300 mysystem
("git", "count-objects", "-vH");
301 $Opt{'dry-run'} || print("\n");
302 if (should
('aggressive-compress')) {
303 mysystem
("git gc --aggressive") &&
304 push(@err_compress, $f);
306 mysystem
("git gc") &&
307 push(@err_compress, $f);
310 `(find $object_dir -type f -printf '%s+' ; echo 0) | bc`);
311 $total_after += $after;
312 my $saved = $before - $after;
313 chomp(my $numfiles_after =
314 `find $object_dir -type f | wc -l`);
315 $totnum_after += $numfiles_after;
317 printf("\nBefore: %s\n" .
319 "Saved : %s (%.4f%%)\n",
323 100.0 * $saved / $before);
324 printf("Number of files in %s: " .
325 "before: %u, after: %u, saved: %d\n",
329 $numfiles_before-$numfiles_after)
330 # Temporarily (?) disabled, it takes a heck of
331 # a long time and uses loads of CPU and memory.
332 # If the git-annex repo gets corrupted it's in
333 # most cases good enough to delete
334 # .git/annex/index anyway, and that can be done
336 # $is_annex && should('dangling') && mysystem("ga", "repair");
338 if (should
('delete-dangling') && !$is_bare) {
339 mysystem
("git", "dangling", "-D");
341 if (should
('lpar')) {
344 if (should
('exec-after')) {
345 for my $arg (@opt_exec_after) {
349 $Opt{'verbose'} >= -1 && print("\n");
351 chdir($orig_dir) || die(
352 "$progname: $orig_dir: Cannot return to " .
353 "original directory: $!\n");
355 scalar(@err_fetch) && print("$progname: Unable to fetch from: " .
356 join(" ", @err_fetch) . "\n");
357 scalar(@err_gasync) && print("$progname: Unable to run ga sync: " .
358 join(" ", @err_gasync) . "\n");
359 scalar(@err_dangling) &&
360 print("$progname: Unable to run git dangling: " .
361 join(" ", @err_dangling) . "\n");
362 scalar(@err_pull) && print("$progname: Unable to pull from: " .
363 join(" ", @err_pull) . "\n");
364 scalar(@err_push) && print("$progname: Unable to push from: " .
365 join(" ", @err_push) . "\n");
366 scalar(@err_updsub) &&
367 print("$progname: Unable to update submodules in: " .
368 join(" ", @err_updsub) . "\n");
369 scalar(@err_compress) && print("$progname: Unable to compress: " .
370 join(" ", @err_compress) . "\n");
371 scalar(@err_test) && print("$progname: Error in git fsck: " .
372 join(" ", @err_test) . "\n");
374 if ($Opt{'compress'} || $Opt{'aggressive-compress'}) {
375 my $total_saved = $total_before - $total_after;
376 printf("Before: %s\nAfter : %s\n",
377 commify
($total_before), commify
($total_after));
379 printf("Total : %s (%.4f%%)\n",
380 commify
($total_saved),
381 100.0 * $total_saved / $total_before);
382 printf("Number of object files: before: %u, after: %u, " .
383 "saved: %d\n", $totnum_before, $totnum_after,
384 $totnum_before-$totnum_after)
395 die("\n$progname: Child process interrupted, aborting.\n");
402 my $Str = reverse $_[0];
403 $Str =~ s/(\d\d\d)(?=\d)(?!\d*\,)/$1,/g;
404 return scalar reverse $Str;
412 chomp($retval = `git config --get "$name"`);
419 my $basename = basename
(abs_path
(getcwd
()));
420 my $retval = ($basename eq '.git') ?
1 : 0;
428 msg
(0, sprintf("%s '", $Opt{'dry-run'} ?
"Simulating" : "Executing") .
429 join(" ", @cmd) . "'...");
431 !$Opt{'dry-run'} && system(@cmd) && check_sig
($?
);
437 # Print program version {{{
438 print("$progname $VERSION\n");
446 get_config
("git-update-dirs.no-$name") eq 'true' && return 0;
448 if ($name =~ /^exec-(before|after)$/) {
449 scalar($1 eq "before" ?
@opt_exec_before : @opt_exec_after) &&
452 $Opt{$name} && ($retval = 1);
459 # Send the help message to stdout {{{
462 if ($Opt{'verbose'}) {
468 Execute a predefined or customised set of commands in multiple local Git
469 repositories in one operation.
471 Usage: $progname [options] [directories [...]]
473 Options, listed in the order they are executed in every Git repository:
475 -E X, --exec-before X
476 Execute command X in every repo before all other commands. This
477 option can be specified multiple times to run several commands.
478 To disable: git config git-update-dirs.no-exec-before true
480 Execute lpar before and after fetch/pull and push
481 To disable: git config git-update-dirs.no-lpar true
483 Test integrity of local repositories by running "git fsck".
484 To disable: git config git-update-dirs.no-test true
486 Fetch new commits from all remotes and prune deleted remote
488 To disable: git config git-update-dirs.no-fetch-prune true
490 Fetch new commits from all remotes.
491 To disable: git config git-update-dirs.no-fetch true
493 Also execute "git pull --ff-only".
494 To disable: git config git-update-dirs.no-pull true
496 If the repo is used by git-annex, run "ga sync".
497 To disable: git config git-update-dirs.no-ga-sync true
499 Drop annex files having more copies than necessary, get files with
500 fewer copies than necessary.
501 To disable: git config git-update-dirs.no-ga-dropget true
503 In a git-annex repo, run "ga unused" followed by "ga dropunused
504 all". Can't be used together with -U/--ga-moveunused.
505 To disable: git config git-update-dirs.no-ga-dropunused true
507 Move unused git-annex contents to the '$archive_disk' remote. Can't
508 be used together with -u/--ga-dropunused.
509 To disable: git config git-update-dirs.no-ga-moveunused true
511 Execute "ga-getnew", i.e. use "ga get --auto" to get all files from
512 one month back that don't have enough copies.
513 To disable: git config git-update-dirs.no-ga-getnew true
515 If the directory is controlled by git-annex, execute "ga
516 update-desc" to set the description to the output from ga-pwd(1).
517 To disable: git config git-update-dirs.no-ga-update-desc true
519 Execute "git dangling", i.e. turn all dangling commits into
521 To disable: git config git-update-dirs.no-dangling true
523 Execute "git nobr", "git allbr -a" and "git checkout -". If this
524 option is specified once, it's only executed in bare repos. To also
525 execute it in non-bare repos, it must be specified twice.
526 To disable: git config git-update-dirs.no-allbr true
528 Also execute "git pa".
529 To disable: git config git-update-dirs.no-push true
531 Update submodules if .gitmodules is found.
532 To disable: git config git-update-dirs.no-submodule true
534 Compress local repositories to save space.
535 To disable: git config git-update-dirs.no-compress true
536 -C, --aggressive-compress
537 Use --aggressive when compressing the repository.
538 To disable: git config git-update-dirs.no-aggressive-compress true
539 -D, --delete-dangling
540 Execute "git dangling -D" after execution to remove local commit-*
541 branches and tag-* tags. This option is ignored in bare repos.
542 To disable: git config git-update-dirs.no-delete-dangling true
544 Execute command X in every repo after all other commands. This
545 option can be specified multiple times to run several commands.
546 To disable: git config git-update-dirs.no-exec-after true
549 Run the program as if "-lFpgSdaPs" had been specified.
551 Read directory list from file X. If "-" is specified, read from
552 stdin. Can be specified multiple times to read from different files.
556 Simulate, don't actually execute any git commands.
558 Be more quiet. Can be repeated to increase silence.
560 Update all repositories recursively under the current directory.
562 Increase level of verbosity. Can be repeated.
564 Print version information.
566 To disable some of these commands in a specific repository, set the git
567 config variable git-update-dirs.no-OPTION to "true". For example, to
570 git config git-update-dirs.no-push true
572 Or disable aggressive compression:
574 git config git-update-dirs.no-aggressive-compress true
576 In this case aggressive compression will be disabled, and it will fall
577 back to regular compression.
579 Only the value "true" (with lower case letters) is recognised, any other
580 value will allow the command to run.
588 # Print a status message to stderr based on verbosity level {{{
589 my ($verbose_level, $Txt) = @_;
591 if ($Opt{'verbose'} >= $verbose_level) {
592 print(STDERR
"$progname: $Txt\n");
600 # This program is free software; you can redistribute it and/or modify it under
601 # the terms of the GNU General Public License as published by the Free Software
602 # Foundation; either version 2 of the License, or (at your option) any later
605 # This program is distributed in the hope that it will be useful, but WITHOUT
606 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
607 # FOR A PARTICULAR PURPOSE.
608 # See the GNU General Public License for more details.
610 # You should have received a copy of the GNU General Public License along with
612 # If not, see L<http://www.gnu.org/licenses/>.
614 # vim: set ts=8 sw=8 sts=8 noet fo+=w tw=79 fenc=UTF-8 :