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.5';
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)) {
174 $Opt{'verbose'} >= -1 &&
175 print("================ $f " .
176 "================\n");
178 warn("$progname: $f: Cannot chdir: $!\n");
182 (chdir('..'), $object_dir = ".git/objects");
183 my $is_bare = (get_config
('core.bare') =~ /^true/)
186 msg
(2, "is_bare = '$is_bare'");
188 my $Lh = "[0-9a-fA-F]";
189 my $uuid_templ = "$Lh\{8}-$Lh\{4}-$Lh\{4}-" .
191 my $is_annex = (get_config
('annex.uuid') =~
195 msg
(2, "is_annex = '$is_annex'");
197 if (should
('exec-before')) {
198 for my $arg (@opt_exec_before) {
202 if (should
('lpar')) {
205 if (should
('test')) {
206 mysystem
("git", "fsck") && (
208 warn("$progname: $f: ERRORS FOUND! " .
209 "Skipping other actions " .
214 if (should
('fetch-prune')) {
215 mysystem
("git", "fetch", "--all", "--prune") &&
216 push(@err_fetch, $f);
217 } elsif (should
('fetch')) {
218 mysystem
("git", "fetch", "--all") &&
219 push(@err_fetch, $f);
221 if (should
('pull') && !$is_bare) {
222 mysystem
("git", "pull", "--ff-only") &&
224 if (-e
'.emptydirs') {
225 mysystem
('git', 'restore-dirs');
229 (should
('ga-sync') || should
('ga-getnew'))) {
230 should
('ga-sync') && mysystem
("ga", "sync") &&
231 push(@err_gasync, $f);
232 if (should
('ga-dropget')) {
233 mysystem
("ga", "drop", "--auto");
234 mysystem
("ga", "sync");
236 if (should
('ga-dropunused')) {
237 mysystem
("ga", "unused");
238 mysystem
("ga", "dropunused", "all");
239 mysystem
("ga", "sync");
241 if (should
('ga-moveunused')) {
242 if (open(my $fh, "git remote |")) {
244 while (my $curr_remote = <$fh>) {
245 if ($curr_remote =~ /^$archive_disk$/) {
246 mysystem
("ga", "unused");
247 mysystem
("ga", "move", "--unused",
248 "--to", $archive_disk);
249 mysystem
("ga", "sync");
255 warn("$progname: Cannot open " .
256 "'git remote' pipe: $!\n");
259 if (should
('ga-dropget') || should
('ga-getnew')) {
260 if (!$Opt{'ga-getnew'}) {
261 mysystem
("ga", "get",
263 mysystem
("ga", "sync");
265 mysystem
("ga-getnew");
269 if ($is_annex && should
('ga-update-desc')) {
270 mysystem
("ga", "update-desc");
272 if (should
('dangling')) {
273 mysystem
("git", "dangling") &&
274 push(@err_dangling, $f);
276 if (should
('allbr') &&
277 ($is_bare || $Opt{'allbr'} > 1)) {
278 mysystem
("git", "nobr");
279 mysystem
("git", "allbr", "-a");
280 mysystem
("git", "checkout", "-");
282 if (should
('push')) {
283 mysystem
("git", "pa") && push(@err_push, $f);
285 if (should
('submodule') && -e
".gitmodules") {
286 mysystem
("git", "submodule", "init");
287 mysystem
("git", "submodule", "update") &&
288 push(@err_updsub, $f);
290 if (should
('compress') ||
291 should
('aggressive-compress')) {
293 `(find $object_dir -type f -printf '%s+' ; echo 0) | bc`);
294 $total_before += $before;
295 chomp(my $numfiles_before =
296 `find $object_dir -type f | wc -l`);
297 $totnum_before += $numfiles_before;
298 $Opt{'dry-run'} || print("\n");
299 mysystem
("git", "count-objects", "-vH");
300 $Opt{'dry-run'} || print("\n");
301 if (should
('aggressive-compress')) {
302 mysystem
("git gc --aggressive") &&
303 push(@err_compress, $f);
305 mysystem
("git gc") &&
306 push(@err_compress, $f);
309 `(find $object_dir -type f -printf '%s+' ; echo 0) | bc`);
310 $total_after += $after;
311 my $saved = $before - $after;
312 chomp(my $numfiles_after =
313 `find $object_dir -type f | wc -l`);
314 $totnum_after += $numfiles_after;
316 printf("\nBefore: %s\n" .
318 "Saved : %s (%.4f%%)\n",
322 100.0 * $saved / $before);
323 printf("Number of files in %s: " .
324 "before: %u, after: %u, saved: %d\n",
328 $numfiles_before-$numfiles_after)
329 # Temporarily (?) disabled, it takes a heck of
330 # a long time and uses loads of CPU and memory.
331 # If the git-annex repo gets corrupted it's in
332 # most cases good enough to delete
333 # .git/annex/index anyway, and that can be done
335 # $is_annex && should('dangling') && mysystem("ga", "repair");
337 if (should
('delete-dangling') && !$is_bare) {
338 mysystem
("git", "dangling", "-D");
340 if (should
('lpar')) {
343 if (should
('exec-after')) {
344 for my $arg (@opt_exec_after) {
348 $Opt{'verbose'} >= -1 && print("\n");
350 chdir($orig_dir) || die(
351 "$progname: $orig_dir: Cannot return to " .
352 "original directory: $!\n");
354 scalar(@err_fetch) && print("$progname: Unable to fetch from: " .
355 join(" ", @err_fetch) . "\n");
356 scalar(@err_gasync) && print("$progname: Unable to run ga sync: " .
357 join(" ", @err_gasync) . "\n");
358 scalar(@err_dangling) &&
359 print("$progname: Unable to run git dangling: " .
360 join(" ", @err_dangling) . "\n");
361 scalar(@err_pull) && print("$progname: Unable to pull from: " .
362 join(" ", @err_pull) . "\n");
363 scalar(@err_push) && print("$progname: Unable to push from: " .
364 join(" ", @err_push) . "\n");
365 scalar(@err_updsub) &&
366 print("$progname: Unable to update submodules in: " .
367 join(" ", @err_updsub) . "\n");
368 scalar(@err_compress) && print("$progname: Unable to compress: " .
369 join(" ", @err_compress) . "\n");
370 scalar(@err_test) && print("$progname: Error in git fsck: " .
371 join(" ", @err_test) . "\n");
373 if ($Opt{'compress'} || $Opt{'aggressive-compress'}) {
374 my $total_saved = $total_before - $total_after;
375 printf("Before: %s\nAfter : %s\n",
376 commify
($total_before), commify
($total_after));
378 printf("Total : %s (%.4f%%)\n",
379 commify
($total_saved),
380 100.0 * $total_saved / $total_before);
381 printf("Number of object files: before: %u, after: %u, " .
382 "saved: %d\n", $totnum_before, $totnum_after,
383 $totnum_before-$totnum_after)
394 die("\n$progname: Child process interrupted, aborting.\n");
401 my $Str = reverse $_[0];
402 $Str =~ s/(\d\d\d)(?=\d)(?!\d*\,)/$1,/g;
403 return scalar reverse $Str;
411 chomp($retval = `git config --get "$name"`);
418 my $basename = basename
(abs_path
(getcwd
()));
419 my $retval = ($basename eq '.git') ?
1 : 0;
427 msg
(0, sprintf("%s '", $Opt{'dry-run'} ?
"Simulating" : "Executing") .
428 join(" ", @cmd) . "'...");
430 !$Opt{'dry-run'} && system(@cmd) && check_sig
($?
);
436 # Print program version {{{
437 print("$progname $VERSION\n");
445 get_config
("git-update-dirs.no-$name") eq 'true' && return 0;
447 if ($name =~ /^exec-(before|after)$/) {
448 scalar($1 eq "before" ?
@opt_exec_before : @opt_exec_after) &&
451 $Opt{$name} && ($retval = 1);
458 # Send the help message to stdout {{{
461 if ($Opt{'verbose'}) {
467 Execute a predefined or customised set of commands in multiple local Git
468 repositories in one operation.
470 Usage: $progname [options] [directories [...]]
472 Options, listed in the order they are executed in every Git repository:
474 -E X, --exec-before X
475 Execute command X in every repo before all other commands. This
476 option can be specified multiple times to run several commands.
477 To disable: git config git-update-dirs.no-exec-before true
479 Execute lpar before and after fetch/pull and push
480 To disable: git config git-update-dirs.no-lpar true
482 Test integrity of local repositories by running "git fsck".
483 To disable: git config git-update-dirs.no-test true
485 Fetch new commits from all remotes and prune deleted remote
487 To disable: git config git-update-dirs.no-fetch-prune true
489 Fetch new commits from all remotes.
490 To disable: git config git-update-dirs.no-fetch true
492 Also execute "git pull --ff-only".
493 To disable: git config git-update-dirs.no-pull true
495 If the repo is used by git-annex, run "ga sync".
496 To disable: git config git-update-dirs.no-ga-sync true
498 Drop annex files having more copies than necessary, get files with
499 fewer copies than necessary.
500 To disable: git config git-update-dirs.no-ga-dropget true
502 In a git-annex repo, run "ga unused" followed by "ga dropunused
503 all". Can't be used together with -U/--ga-moveunused.
504 To disable: git config git-update-dirs.no-ga-dropunused true
506 Move unused git-annex contents to the '$archive_disk' remote. Can't
507 be used together with -u/--ga-dropunused.
508 To disable: git config git-update-dirs.no-ga-moveunused true
510 Execute "ga-getnew", i.e. use "ga get --auto" to get all files from
511 one month back that don't have enough copies.
512 To disable: git config git-update-dirs.no-ga-getnew true
514 If the directory is controlled by git-annex, execute "ga
515 update-desc" to set the description to the output from ga-pwd(1).
516 To disable: git config git-update-dirs.no-ga-update-desc true
518 Execute "git dangling", i.e. turn all dangling commits into
520 To disable: git config git-update-dirs.no-dangling true
522 Execute "git nobr", "git allbr -a" and "git checkout -". If this
523 option is specified once, it's only executed in bare repos. To also
524 execute it in non-bare repos, it must be specified twice.
525 To disable: git config git-update-dirs.no-allbr true
527 Also execute "git pa".
528 To disable: git config git-update-dirs.no-push true
530 Update submodules if .gitmodules is found.
531 To disable: git config git-update-dirs.no-submodule true
533 Compress local repositories to save space.
534 To disable: git config git-update-dirs.no-compress true
535 -C, --aggressive-compress
536 Use --aggressive when compressing the repository.
537 To disable: git config git-update-dirs.no-aggressive-compress true
538 -D, --delete-dangling
539 Execute "git dangling -D" after execution to remove local commit-*
540 branches and tag-* tags. This option is ignored in bare repos.
541 To disable: git config git-update-dirs.no-delete-dangling true
543 Execute command X in every repo after all other commands. This
544 option can be specified multiple times to run several commands.
545 To disable: git config git-update-dirs.no-exec-after true
548 Run the program as if "-lFpgSdaPs" had been specified.
550 Read directory list from file X. If "-" is specified, read from
551 stdin. Can be specified multiple times to read from different files.
555 Simulate, don't actually execute any git commands.
557 Be more quiet. Can be repeated to increase silence.
559 Update all repositories recursively under the current directory.
561 Increase level of verbosity. Can be repeated.
563 Print version information.
565 To disable some of these commands in a specific repository, set the git
566 config variable git-update-dirs.no-OPTION to "true". For example, to
569 git config git-update-dirs.no-push true
571 Or disable aggressive compression:
573 git config git-update-dirs.no-aggressive-compress true
575 In this case aggressive compression will be disabled, and it will fall
576 back to regular compression.
578 Only the value "true" (with lower case letters) is recognised, any other
579 value will allow the command to run.
587 # Print a status message to stderr based on verbosity level {{{
588 my ($verbose_level, $Txt) = @_;
590 if ($Opt{'verbose'} >= $verbose_level) {
591 print(STDERR
"$progname: $Txt\n");
599 # This program is free software; you can redistribute it and/or modify it under
600 # the terms of the GNU General Public License as published by the Free Software
601 # Foundation; either version 2 of the License, or (at your option) any later
604 # This program is distributed in the hope that it will be useful, but WITHOUT
605 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
606 # FOR A PARTICULAR PURPOSE.
607 # See the GNU General Public License for more details.
609 # You should have received a copy of the GNU General Public License along with
611 # If not, see L<http://www.gnu.org/licenses/>.
613 # vim: set ts=8 sw=8 sts=8 noet fo+=w tw=79 fenc=UTF-8 :