/usr/bin/gitkeeper is in mrb 0.3.
This file is owned by root:root, with mode 0o755.
The actual contents of the file can be viewed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 | #!/usr/bin/perl -w
#
# Mirror files between a git repository and their 'installed' location.
#
# This is my latest attempt at having something I don't hate, to manage and
# track the history of system configuration files. I don't want much, just
# something that is simple enough to be near to foolproof to remember how to
# use correctly, even when you haven't used it for months, yet complete and
# powerful enough to not handwave away the usual problems with git and rsync
# mirroring where ownership and/or file permission is complex and critical.
#
# It isn't strictly limited to mirroring "configuration files", this attempt
# should be generic enough to handle just about anything it makes sense to
# pull and push from git, but that was the initial motivation for having it.
#
# Copyright 2004 - 2016, Ron <ron@debian.org>
# This file is distributed under the terms of the GNU GPL version 2.
use strict;
use utf8;
use feature 'unicode_strings';
use Getopt::Long qw( :config gnu_getopt );
use Pod::Usage;
use File::Path qw( remove_tree );
use JSON::XS;
$ENV{PATH} = '/usr/bin:/bin';
my $conf_file = './gk.conf';
my $conf_set;
my $list_hosts;
my $create_conf;
my $working_dir;
my $destdir = '';
my $remote_user;
my $dry_run;
my $verbose = 0;
my $help;
GetOptions( 'config|c=s' => sub { (undef,$conf_file) = @_; $conf_set = 1; },
'list|l' => \$list_hosts,
'init' => \$create_conf,
'chdir|C=s' => \$working_dir,
'destdir=s' => \$destdir,
'user|u=s' => \$remote_user,
'dry-run|n' => \$dry_run,
'verbose|v+' => \$verbose,
'help|?' => \$help
) or pod2usage(-exitval => 2, -message => "Use -? for option details");
# Using perldoc requires perl-doc to be installed, if it isn't then it will just dump
# the entire source code out, along with a message telling you to install perl-doc.
pod2usage(-exitval => 0, -verbose => 2, -noperldoc => 1) if $help && $verbose;
pod2usage(-exitval => 0, -verbose => 1) if $help;
sub read_config($)
{ #{{{
my $file = shift;
die "$0: No '$file' configuration file found.\n" unless -e $file;
open(my $h, '<', $file) or die "$0: Failed to read '$file': $!\n";
my $data = do { local $/; <$h> };
close $h;
my $conf = eval { JSON::XS->new->utf8->relaxed->decode($data) };
die "$0: Failed to parse '$file': $@\n" if $@;
return $conf;
} #}}}
sub get_host_opts($$)
{ #{{{
my ($c, $h) = @_;
my %o;
$o{host} = $h;
$o{address} = $c->{hosts}{$h}{address};
$o{pull_pre_command} = $c->{hosts}{$h}{pull_pre_command} // $c->{pull_pre_command};
$o{pull_post_command} = $c->{hosts}{$h}{pull_post_command} // $c->{pull_post_command};
$o{push_pre_command} = $c->{hosts}{$h}{push_pre_command} // $c->{push_pre_command};
$o{push_post_command} = $c->{hosts}{$h}{push_post_command} // $c->{push_post_command};
$o{local_root} = $c->{hosts}{$h}{local_root} // $c->{local_root};
$o{remote_root} = $c->{hosts}{$h}{remote_root} // $c->{remote_root};
$o{remote_user} = $c->{hosts}{$h}{remote_user} // $c->{remote_user};
$o{rsync_opts} = $c->{hosts}{$h}{rsync_opts} // $c->{rsync_opts};
$o{rsync_pull_opts} = $c->{hosts}{$h}{rsync_pull_opts} // $c->{rsync_pull_opts};
$o{rsync_push_opts} = $c->{hosts}{$h}{rsync_push_opts} // $c->{rsync_push_opts};
$o{rsync_include} = $c->{hosts}{$h}{rsync_include} // $c->{rsync_include};
$o{rsync_exclude} = $c->{hosts}{$h}{rsync_exclude} // $c->{rsync_exclude};
$o{rsync_filter} = $c->{hosts}{$h}{rsync_filter} // $c->{rsync_filter};
$o{rsync_pull_filter} = $c->{hosts}{$h}{rsync_pull_filter} // $c->{rsync_pull_filter};
$o{rsync_push_filter} = $c->{hosts}{$h}{rsync_push_filter} // $c->{rsync_push_filter};
$o{chown} = $c->{hosts}{$h}{chown} // $c->{chown};
$o{chmod} = $c->{hosts}{$h}{chmod} // $c->{chmod};
die "$0: No address configured for host '$h'\n" unless $o{address};
die "$0: No sync sets configured for host '$h'\n" unless $c->{hosts}{$h}{sync} && @{$c->{hosts}{$h}{sync}};
return \%o;
} #}}}
sub get_sync_opts($$$)
{ #{{{
my ($h, $s, $n) = @_;
my %o = %{$h};
$o{local_root} = $s->{local_root} if defined $s->{local_root};
$o{remote_root} = $s->{remote_root} if defined $s->{remote_root};
$o{remote_user} = $s->{remote_user} if defined $s->{remote_user};
$o{remote_user} = $remote_user if $remote_user;
$o{rsync_opts} = $s->{rsync_opts} if defined $s->{rsync_opts};
$o{rsync_pull_opts} = $s->{rsync_pull_opts} if defined $s->{rsync_pull_opts};
$o{rsync_push_opts} = $s->{rsync_push_opts} if defined $s->{rsync_push_opts};
$o{rsync_include} = $s->{rsync_include} if defined $s->{rsync_include};
$o{rsync_exclude} = $s->{rsync_exclude} if defined $s->{rsync_exclude};
$o{rsync_filter} = $s->{rsync_filter} if defined $s->{rsync_filter};
$o{rsync_pull_filter} = $s->{rsync_pull_filter} if defined $s->{rsync_pull_filter};
$o{rsync_push_filter} = $s->{rsync_push_filter} if defined $s->{rsync_push_filter};
$o{chown} = $s->{chown} if defined $s->{chown};
$o{chmod} = $s->{chmod} if defined $s->{chmod};
$o{paths} = $s->{paths};
# local_root must not be empty, a local directory must be specified.
# remote_root may be empty, to use the home dir of the remote user,
# but it must be set to the empty string for that, to avoid accidents.
die "$0: No local_root configured for '$o{host}' sync set $n\n" unless $o{local_root};
die "$0: local_root is not a relative path ($o{host}: $o{local_root})\n" unless substr($o{local_root},0,1) ne '/';
die "$0: No remote_root configured for '$o{host}' sync set $n\n" unless defined $o{remote_root};
die "$0: No paths configured for '$o{host}' sync set $n\n" unless $o{paths} && @{$o{paths}};
for my $p (@{$o{paths}}) {
die "$0: sync path for '$o{host}' is not relative ($p)\n" unless substr($p,0,1) ne '/';
}
die "$0: local_root path '$o{local_root}' does not exist.\n" unless -e $o{local_root};
die "$0: local_root path '$o{local_root}' is not a directory.\n" unless -d $o{local_root};
$o{local_root} .= '/' unless substr($o{local_root},-1) eq '/';
$o{remote_root} .= '/' if $o{remote_root} && substr($o{remote_root},-1) ne '/';
$o{remote_root} = "$destdir/$o{remote_root}" if $destdir;
$o{remote_host} = $o{remote_user} ? "$o{remote_user}\@$o{address}" : $o{address};
return \%o;
} #}}}
sub maybe_exec($$$)
{ #{{{
my ($host, $label, $cmd) = @_;
return unless $cmd && @$cmd;
if ($dry_run) {
print " (not executing) $label: `" . join(' ', map { "'$_'" } @$cmd) . "`\n";
}
else {
print " $label: `" . join(' ', map { "'$_'" } @$cmd) . "`\n" if $verbose;
system( @$cmd ) == 0
or die "$0: $label for '$host' failed\n";
}
} #}}}
sub do_pull($$)
{ #{{{
my ($conf, $host) = @_;
my $host_opt = get_host_opts($conf, $host);
my $n = 0;
print "Pulling '$host' from $host_opt->{address} ...\n";
maybe_exec($host, 'pre-command', $host_opt->{pull_pre_command});
for my $s (@{$conf->{hosts}{$host}{sync}}) {
my $so = get_sync_opts($host_opt, $s, ++$n);
my @rsync_cmd = ( 'rsync', '--relative' );
push @rsync_cmd, @{$so->{rsync_opts}} if $so->{rsync_opts};
push @rsync_cmd, @{$so->{rsync_pull_opts}} if $so->{rsync_pull_opts};
push @rsync_cmd, map { '--include=' . $_ } @{$so->{rsync_include}} if $so->{rsync_include};
push @rsync_cmd, map { '--exclude=' . $_ } @{$so->{rsync_exclude}} if $so->{rsync_exclude};
push @rsync_cmd, map { '--filter=' . $_ } @{$so->{rsync_filter}} if $so->{rsync_filter};
push @rsync_cmd, map { '--filter=' . $_ } @{$so->{rsync_pull_filter}} if $so->{rsync_pull_filter};
push @rsync_cmd, '--dry-run' if $dry_run;
push @rsync_cmd, '--';
push @rsync_cmd, map { "$so->{remote_host}:$so->{remote_root}./$_" } @{$so->{paths}};
push @rsync_cmd, $so->{local_root};
print " " . join(' ', map { "'$_'" } @rsync_cmd) . "\n" if $verbose > 1;
system( @rsync_cmd ) == 0
or die "$0: rsync pull for '$host' failed\n";
}
maybe_exec($host, 'post-command', $host_opt->{pull_post_command});
print "Pulling '$host' done.\n\n";
} #}}}
sub do_push($$)
{ #{{{
my ($conf, $host) = @_;
my $host_opt = get_host_opts($conf, $host);
my $n = 0;
print "Pushing '$host' to $host_opt->{address} ...\n";
maybe_exec($host, 'pre-command', $host_opt->{push_pre_command});
for my $s (@{$conf->{hosts}{$host}{sync}}) {
my $so = get_sync_opts($host_opt, $s, ++$n);
my @rsync_cmd = ( 'rsync', '--relative' );
push @rsync_cmd, @{$so->{rsync_opts}} if $so->{rsync_opts};
push @rsync_cmd, @{$so->{rsync_push_opts}} if $so->{rsync_push_opts};
push @rsync_cmd, map { '--include=' . $_ } @{$so->{rsync_include}} if $so->{rsync_include};
push @rsync_cmd, map { '--exclude=' . $_ } @{$so->{rsync_exclude}} if $so->{rsync_exclude};
push @rsync_cmd, map { '--filter=' . $_ } @{$so->{rsync_filter}} if $so->{rsync_filter};
push @rsync_cmd, map { '--filter=' . $_ } @{$so->{rsync_push_filter}} if $so->{rsync_push_filter};
push @rsync_cmd, '-og', '--chown=' . $so->{chown} if $so->{chown};
push @rsync_cmd, '-p', '--chmod=' . $so->{chmod}, if $so->{chmod};
push @rsync_cmd, '--dry-run' if $dry_run;
push @rsync_cmd, '--';
# rsync expects local wildcards to be expanded "by the shell", but since
# we aren't passing through the shell here, we need to glob them ourself.
# The nested quoting avoids glob splitting the pattern on whitespace.
push @rsync_cmd, map { glob qq("$so->{local_root}./$_") } @{$so->{paths}};
push @rsync_cmd, "$so->{remote_host}:$so->{remote_root}";
print " " . join(' ', map { "'$_'" } @rsync_cmd) . "\n" if $verbose > 1;
system( @rsync_cmd ) == 0
or die "$0: rsync push for '$host' failed\n";
}
maybe_exec($host, 'post-command', $host_opt->{push_post_command});
print "Pushing '$host' done.\n\n";
} #}}}
sub git_check_ref_format($)
{ #{{{
# We could check this ourselves with a regex, but just punt to using the
# git check-ref-format test. The cost of shelling out to that here is
# minimal, and the rules have changed at least once before in the past,
# so just let the version of git we have to use decide.
#
# We place one extra restriction on what we will accept, disallowing a
# single quote anywhere in the refname, which lets us be lazy with what
# is needed to quote it for the export. We can fix that if needed, but
# there aren't many sane reasons to use them in a refname anyway.
my $ref = shift;
system( 'git', 'check-ref-format', '--allow-onelevel', $ref );
return 0 if $?;
return 0 if $ref =~ /'/;
return 1;
} #}}}
if ($working_dir)
{ #{{{
chdir($working_dir)
or die "$0: Failed to change to directory '$working_dir': $!\n";
} #}}}
if ($create_conf)
{ #{{{
die "$0: Configuration file '$conf_file' already exists.\n"
. " *** Refusing to overwrite it.\n\n" if -e $conf_file;
my $new_conf = <<EOF;
# gitkeeper configuration file.
#
# It is expected to contain a single JSON Object, which will be parsed by
# perl's JSON::XS in its relaxed mode (which allows trailing commas after
# the final element of an array or object, and '#'-comments anywhere that
# whitespace would be permitted).
{
# The default rsync options to use if not overridden for the host or paths.
# These options will be used for both push and pull operations.
# The --relative option is added automatically, but if you really don't
# want that for some reason you could pass --no-relative here.
#
# Note that if --protect-args is used, then brace expansion in the remote
# paths will not work.
"rsync_opts": [ "-rltS",
"-hivz",
"--protect-args",
"--prune-empty-dirs",
"--delete-excluded"
],
# Extra rsync options which will only be used for push or pull operations.
# These will be passed after the options above (and so they can selectively
# undo some of them too if desired).
#"rsync_push_opts": [],
#"rsync_pull_opts": [],
# Include and exclude filter rules used for both pull and push operations.
# The default set provided here ignores vim swap files.
"rsync_include": [ ".s[a-w][a-z]/", ".*.s[a-w][a-z]/" ],
"rsync_exclude": [ ".s[a-w][a-z]", ".*.s[a-w][a-z]" ],
# Additional filter rules for both pull and push operations, if you need
# something more fine grained than a simple include or exclude. You can
# use 'hide' and 'protect' rules here which only act on the sending or
# receiving side.
#"rsync_filter": [],
# Extra filter rules which will only be used for push or pull operations.
# The default set provided here means local vim swap files will not be
# deleted if pulling while files are open in the editor (which they would
# be if --delete-excluded and the default rsync_exclude from above are
# both active). We don't protect them when pushing, since it's probably
# ok to delete stale swap files there.
#"rsync_push_filter": [],
"rsync_pull_filter": [ "protect .s[a-w][a-z]", "protect .*.s[a-w][a-z]" ],
# If remote user is not set, then the ssh default for the remote system
# will be used (as configured by .ssh/config or similar).
"remote_user": "root",
# The file and directory ownership and permissions to set on the remote
# system for push operations. If the chown option is set, then the
# --owner and --group options will automatically be passed to rsync (else
# the --chown option would have no effect). Usually you will need to have
# super user privilege on the remote host to be able to use this option.
#
# If the chmod option is set then the --perms option will automatically be
# passed to rsync (else the --chmod option would have no effect).
"chown": "root:root",
"chmod": "D2755,F644",
# Optional shell commands to run (on the local system) before and/or after
# the synchronisation operation is performed. To run a command on the
# remote system, use ssh or similar to invoke it from the local system.
# The pre_command will be run for the host before any sync operations are
# performed, and the post_command will be run after all of them have been
# completed. If the pre_command fails, no sync will be done. If any of
# the sync operations fail, the post_command will not be run.
#"pull_pre_command": [],
#"pull_post_command": [],
#"push_pre_command": [],
#"push_post_command": [],
# Per-host configuration is specified in this object.
# You may define as many hosts in it as you please.
"hosts": {
# The host alias to pass for push or pull operations.
# "host1": {
# The domain name or IP address to use as the rsync remote.
# This field must be set for each host.
# "address": "host1.your.org",
# The local directory root to pull into and push from.
# This field may be overridden in the sync sets, but it must be set
# for all of them. It is a relative path (and so must not begin
# with a '/') to a directory under where gitkeeper was invoked.
#
# This directory will not be included in the --relative path that
# rsync clones to the remote host, but all directory structure that
# is under it will.
# "local_root": "adelaide-config",
# The sets of paths to sync for this host.
# You can define as many sets as you please here, if there are
# multiple directory trees to mirror, or if they require different
# option overrides (such as owner and permission) to be used.
# "sync": [
# Sync set 1.
# {
# The remote directory root to pull from and push into.
# This may either be an absolute path, or relative to the
# home directory for remote_user\@address.
#
# This directory will not be included in the --relative
# path that rsync clones, but all directory structure that
# is defined by the 'paths' under it will.
#
# If not set here, it will be inherited from the host
# options. It may be set to "" to use the home directory
# of remote_user\@address.
# "remote_root": "/some/directory",
# The actual files and/or directories to mirror.
# This option must be specified for each sync set.
# Shell wildcards may be used here, but brace expansion
# will not work if --protect-args is being used.
# "paths": [ "dir1",
# "dir2/file",
# "dir3/subdir/*",
# "file2",
# ],
# Other rsync options may be overridden here too if needed.
# "rsync_filter": [ "hide foo", # Don't copy foo.
# "protect foo", # Don't delete foo.
# ],
# },
# Sync set 2.
# {
# "remote_root": "/another/directory",
# "paths": [ "dir4" ],
# "chown": "user:group",
# "chmod": "D750,F640",
# },
# Sync set 3.
# ...
# ],
# },
# "host2": {
# ...
# },
},
}
# vi:sts=4:sw=4:et:foldmethod=marker
EOF
open(my $h, '>', $conf_file) or die "$0: Failed to create '$conf_file': $!\n";
print $h $new_conf;
close $h;
print "\n Created skeleton configuration in '$conf_file'.\n\n";
exit 0;
} #}}}
my $command = $ARGV[0];
if (($command // '') eq "export")
{ #{{{
my $ref = $ARGV[1] // '';
my $host = $ARGV[2] // '';
my $export_dir = './z-export';
pod2usage(-exitval => 1, -verbose => 0,
-message => "Error: export command requires a git ref.\n") unless $ref;
git_check_ref_format($ref)
or die "$0: Invalid git ref '$ref'\n";
# mkdir will fail if this is true, but explain why we bail out in this case.
die "$0: git export dir '$export_dir' already exists.\n"
. " *** Refusing to overwrite it.\n\n" if -e $export_dir;
mkdir( $export_dir, 0700 )
or die "$0: Failed to create git export dir '$export_dir': $!\n";
print " Exporting '$ref' to '$export_dir' ...\n" if $verbose;
if (system( "git archive --format=tar '$ref' | tar -C '$export_dir' -xf -" )) {
remove_tree( $export_dir );
die "$0: Failed to export git ref '$ref'\n";
}
my @gk_cmd = ( $0, '--chdir', $export_dir );
push @gk_cmd, '--config', $conf_file if $conf_set;
push @gk_cmd, '--list' if $list_hosts;
push @gk_cmd, '--destdir', $destdir if $destdir;
push @gk_cmd, '--user', $remote_user if $remote_user;
push @gk_cmd, '--dry-run' if $dry_run;
push @gk_cmd, '-' . ('v' x $verbose) if $verbose;
push @gk_cmd, 'push';
push @gk_cmd, $host if $host;
print " " . join(' ', map { "'$_'" } @gk_cmd) . "\n" if $verbose;
if (system( @gk_cmd )) {
remove_tree( $export_dir );
die "$0: Failed to push export of git ref '$ref'\n";
}
remove_tree( $export_dir );
exit 0;
} #}}}
if ($list_hosts)
{ #{{{
my $c = read_config($conf_file);
if ($c->{hosts} && %{$c->{hosts}}) {
print "\n Available hosts:\n";
for my $h (sort keys %{$c->{hosts}}) {
print " $h:\t" . ($c->{hosts}{$h}{address} // '(No address set)') . "\n";
}
print "\n";
}
else {
print "\n No hosts configured in '$conf_file'\n\n";
}
exit 0;
} #}}}
pod2usage(-exitval => 1, -verbose => 0,
-message => "Error: A command (push/pull/export) is required.\n") unless $command;
my $conf = read_config($conf_file);
my @hosts;
if ($ARGV[1]) {
die "$0: Error, no host '$ARGV[1]' defined in '$conf_file'.\n"
unless exists $conf->{hosts}{$ARGV[1]};
push @hosts, $ARGV[1];
}
else {
die "$0: Error, no hosts section defined in '$conf_file'.\n"
unless exists $conf->{hosts};
@hosts = sort keys %{$conf->{hosts}};
}
if ($command eq "pull") {
for my $h (@hosts) { do_pull($conf, $h); }
exit 0;
}
if ($command eq "push") {
for my $h (@hosts) { do_push($conf, $h); }
exit 0;
}
pod2usage(-exitval => 1, -verbose => 0, -message => "Error: Unknown command '$command'.\n");
__END__
=head1 NAME
gitkeeper - Mirror files between git and an installed location.
=head1 SYNOPSIS
B<gk> I<[options]> B<pull> I<[host]>
B<gk> I<[options]> B<push> I<[host]>
B<gk> I<[options]> B<export> I<git-ref> I<[host]>
=head1 DESCRIPTION
B<gitkeeper> is a remote administration aid. It enables configuration files to
be maintained locally, with a full history of changes, and synchronised on
demand with a remote target system. This allows files to still be altered
directly on the running system, if and/or when that is needed, with a simple
method to get that all back in sync with the archived copy again later.
It uses B<rsync>(1) and B<ssh>(1) for all operations on the remote hosts.
No special configuration beyond permission to use them is required. The use of
an B<ssh-agent> for managing remote logins may be an advantage though.
At its core, B<gitkeeper> is really just a tool for managing bidirectional
mirrors, of potentially sparse segments of the remote filesystem, so it's not
strictly limited to being used for configuration files, nor is it strictly
dependent upon B<git>(1) aside from the B<export> option, but those are the
primary uses that it was initially designed for.
=head1 COMMANDS
=over 8
=item B<pull>
Pull files from the remote system into the local mirror. This will update the
local directory content to match the live system, but it will not commit any
files to git or change the local index state in any way. If you wish to commit
changes imported in this way, you can just do that with normal git operations.
If the I<host> parameter is not explicitly specified, then all defined hosts
will be pulled.
=item B<push>
Push files from the local mirror to the remote system. This will push the state
of the current working directory, regardless of whether the repository tree is
currently clean or dirty.
If the I<host> parameter is not explicitly specified, then all defined hosts
will be pushed.
=item B<export>
Push files from a historical snapshot of the local mirror to the remote system.
This will do a B<git archive>(1) export of the given I<git-ref> to a temporary
directory, and then perform a B<push> operation on that tree. It is equivalent
to doing a B<git checkout> of the desired ref, and then doing a B<push> on that
tree state, except it will respect any B<gitattributes> you have set for what
will be exported, and it will not change the current working directory state.
If the I<host> parameter is not explicitly specified, then all hosts defined
in the exported configuration will be pushed.
=back
=head1 OPTIONS
=over 8
=item B<-c, --config> I<file>
The file describing what should be mirrored and how. If not specified then
F<gk.conf> will be looked for in the directory that B<gitkeeper> is invoked in.
Usually this should be a relative path under that location, but an absolute path
is permitted and may be useful in some circumstances.
If the B<export> command is used, then this file is not read until after the
export from git, and relative paths are resolved to files in the exported
directory. This is usually what you want since the configuration from the
exported snapshot will then be used. If you need to override that for some
reason then you can use an absolute path to an alternate configuration file.
=item B<-l, --list>
Show the list of host aliases defined in the configuration file. If this is
used with the B<export> command, then the configuration of the exported
snapshot will be shown.
=item B<--init>
Create a skeleton configuration file. This is a convenience to get an initial
configuration when bootstrapping a new mirror.
=item B<-C, --chdir> I<directory>
Change to the given directory before running B<gitkeeper>. Normally you would
just run it from the top level directory of the mirror, but this permits use
from elsewhere in a similar way to what C<make -C directory> allows.
=item B<--destdir> I<directory>
Prefix the remote paths to an alternate file system root. This always changes
only the remote path, regardless of whether a B<push> or B<pull> operation is
being performed. It acts like the C<DESTDIR> option for C<make install> and
allows mirroring files to and from an alternative filesystem location but with
the same subdirectory structure as what they would normally have.
You can use this to export files to a chroot, or to a temporary directory
somewhere, so that they can be examined without replacing the real files on
the remote system.
=item B<-u, --user>
Override the B<remote_user> option from the configuration file for access to
the remote host. There probably aren't many good reasons to ever use this
option, it's a pretty blunt hammer which will override it everywhere, but it
may be useful for exporting a mirror to some machine or location where it isn't
usually expected to go.
=item B<-n, --dry-run>
Don't actually copy any files, just show what would be done if this was a live
run. If this is used with the B<export> command, then the dry run will be
performed on the requested snapshot.
=item B<-v, --verbose>
Show more detail about what is being done. This option may be passed multiple
times to increase the level of verbosity even further.
If passed along with B<--help> then more verbose documentation will be shown.
=item B<-?, --help>
Show this help, again.
=back
=head1 CONFIGURATION
The B<gitkeeper> configuration file is expected to contain a single JSON Object,
which will be parsed by perl's JSON::XS in its relaxed mode (which allows
trailing commas after the final element of an array or object, and '#'-comments
anywhere that whitespace would be permitted).
=head2 Global options
There is only one required member of the top level object, though other options
may also be specified there to be inherited as defaults if not overridden for a
host or one of its sync sets.
=over 8
=item B<hosts>
The B<hosts> member defines a JSON object in which each member is a host name
alias that may be passed as the I<host> parameter to B<gitkeeper>. The alias
names are not used for any other purpose than as the I<host> identifier, and
may be any JSON string value. No other options may be included directly in
the B<hosts> section.
=back
{
"hosts": {
"host1": { ... },
"host2": { ... }
}
}
=head2 Per-host options
There are two required members which must be specified directly for each host
alias object. Other options may also be specified there which will override a
global default for that host and be inherited as defaults for its sync sets, if
not also overridden there.
=over 8
=item B<address>
The B<address> member is a JSON string value, that defines the hostname or IP
address used when connecting to the remote system. It must be a valid string
that can be passed as the host part of a remote B<rsync>(1) path. It should
not contain a I<user> part (that should instead be set with the B<remote_user>
option), but may contain a port specification.
=item B<sync>
The B<sync> member is a JSON array of objects. It must contain at least one
object, but there is no upper bound to the number which may be included. Each
of the objects in the B<sync> array define a mapping from a B<remote_root> to
a B<local_root>, the paths which will be mirrored under those roots, and the
B<rsync> options which will be applied when transferring them.
=back
"host1": {
"address": "myhost.mydomain.org",
"sync": [ { ... }, { ... } ]
}
=head2 Sync set options
Each object in the B<sync> array has one required member that must be specified
directly in it. Other options may also be specified there which will override
the global and host defaults, and some of those options must also be defined in
at least one of those places for each sync set.
=over 8
=item B<paths>
The B<paths> member is an array of JSON string values which specify the files
and/or directories under the B<remote_root> which will be mirrored with their
full directory structure. They may contain shell wildcards, but cannot contain
brace expansions if the B<rsync --protect-args> option is used.
They must all be relative paths (ie. they must not begin with a '/').
=back
"sync": [
{
"paths": [ "file1", "dir1", "dir2/subdir", "dir3/*.conf" ]
}
]
=head2 Required options
The following options must be defined for every sync set, though they may be
configured in either the top level object as global defaults, in the host alias
object for per-host defaults, or in the sync object itself.
=over 8
=item B<local_root>
A JSON string value that defines the local directory which remote B<paths> will
be mirrored under. This must be a relative path, which itself is rooted to the
directory under which B<gitkeeper> is invoked.
As a sanity check against accidents, this directory must already exist.
=item B<remote_root>
A JSON string value which defines the location on the remote system that the
specified B<paths> are relative to. This may be an absolute or relative path.
A relative path will be rooted to the home directory of the B<remote_user>.
A value of "" may be used to specify the home directory of the B<remote_user>.
=back
=head2 Additional options
The following options may be defined as global or per-host defaults, or set
explicitly in each sync set. It is not an error for them not to be set, and
a higher level default may be 'unset' by overriding it with an empty value.
=over 8
=item B<remote_user>
A JSON string which defines the username to use for access to the remote host.
If not set, then the ssh default for the remote system will be used (as
configured by .ssh/config or similar).
=item B<rsync_opts>
A JSON array of string values containing options to be passed to all invocations
of B<rsync>, for both B<push> and B<pull> operations. No word splitting or shell
quote stripping is done on the values used here, so each option must be its own
array element.
Note that the B<--relative> option is passed to B<rsync>(1) by default for all
invocations and does not need to be included in this set. If you really don't
want that option for some reason, and understand the consequences of not passing
it for this use, you can disable it with B<--no-relative>, but there's probably
no good reason to ever do that here.
"rsync_opts": [ "--prune-empty-dirs",
"--delete-excluded",
"--filter=protect .s[a-w][a-z]"
]
=item B<rsync_pull_opts>
Similar to B<rsync_opts> above, but options specified in this array are appended
to those only for B<pull> operations.
=item B<rsync_push_opts>
Similar to B<rsync_opts> above, but options specified in this array are appended
to those only for B<push> operations.
=item B<rsync_include>
A JSON array of string values which will be passed to B<rsync>(1) as B<--include>
options. This is a convenience which is eqivalent to adding those to B<rsync_opts>
ie. the following configurations would be identical in their operation if no other
ordering constraints for the filter rules applied.
"rsync_opts": [ "--include=.s[a-w][a-z]/" ]
"rsync_include": [ ".s[a-w][a-z]/" ]
=item B<rsync_exclude>
A JSON array of string values which will be passed to B<rsync>(1) as B<--exclude>
options. This is the same as B<rsync_include> above, except for excludes.
=item B<rsync_filter>
A JSON array of string values which will be passed to B<rsync>(1) as B<--filter>
options. This is similar to the include and exclude options above, except it
allows the full range of B<rsync> filter rules to be used.
=item B<rsync_pull_filter>
A JSON array of string values which will be passed to B<rsync>(1) as B<--filter>
options (in addition to the include, exclude, and filter options above) only for
B<pull> operations.
=item B<rsync_push_filter>
A JSON array of string values which will be passed to B<rsync>(1) as B<--filter>
options (in addition to the include, exclude, and filter options above) only for
B<push> operations.
=item B<chown>
A JSON string value which will be passed to B<rsync> as the B<--chown> option
for B<push> operations to set file and directory ownership on the remote host.
If this option is used, the B<--owner> and B<--group> options will automatically
added too, otherwise it would have no effect. You must have superuser privilege
on the remote host for this to work.
"chown": "root:bind"
=item B<chmod>
A JSON string value which will be passed to B<rsync> as the B<--chmod> option
for B<push> operations to set file and directory permissions on the remote host.
If this option is used, the B<--perms> option will automatically added too,
otherwise it would have no effect. Valid values here are anything that the
B<rsync> option would accept.
"chmod": "D2755,F664"
=back
=head2 Pre- and Post- command hooks
The following options may be used to execute arbitrary commands before and/or
after a B<pull> or B<push> operation. The commands are executed on the local
host, in the directory that B<gitkeeper> was invoked in, as the user which
B<gitkeeper> was invoked as. They can be used to perform operations on the
remote host by simply invoking B<ssh>(1) or similar themselves.
=over 8
=item B<pull_pre_command>
=item B<push_pre_command>
=item B<pull_post_command>
=item B<push_post_command>
A JSON array of string values containing the command to execute and the options
to pass to it. This will be passed as an array to the perl B<system()> command,
so if the array contains multiple elements, then no word splitting or other shell
interpretation will be performed. If it is a single string, then it will instead
be passed to the local shell, with all the caveats that accompany doing that.
If the pre-command fails, then no transfer will take place. If the transfer fails
for some reason then the post-command will not be executed.
That might change later if we let this get more complex and begin passing status
and other variables to the commands that are invoked, but at this stage, that
isn't really needed for any current use we have, so I'm not going to complicate
things now in anticipation of what later uses might require.
=back
=head1 FILES
=over 8
=item B<./gk.conf>
The default configuration file.
=back
=head1 SEE ALSO
B<git>(1),
B<rsync>(1),
B<ssh>(1),
B<ssh-agent>(1).
=head1 AUTHOR
B<gitkeeper> was written by Ron <ron@debian.org>.
=cut
# vi:sts=4:sw=4:et:foldmethod=marker
|