File: gitkeeper

package info (click to toggle)
mrb 0.3
  • links: PTS
  • area: main
  • in suites: bullseye, buster, sid, stretch
  • size: 84 kB
  • ctags: 26
  • sloc: makefile: 295; perl: 286
file content (987 lines) | stat: -rwxr-xr-x 36,950 bytes parent folder | download
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