about summary refs log tree commit homepage
path: root/lib/PublicInbox/MultiGit.pm
diff options
context:
space:
mode:
Diffstat (limited to 'lib/PublicInbox/MultiGit.pm')
-rw-r--r--lib/PublicInbox/MultiGit.pm145
1 files changed, 145 insertions, 0 deletions
diff --git a/lib/PublicInbox/MultiGit.pm b/lib/PublicInbox/MultiGit.pm
new file mode 100644
index 00000000..b7691806
--- /dev/null
+++ b/lib/PublicInbox/MultiGit.pm
@@ -0,0 +1,145 @@
+# Copyright (C) all contributors <meta@public-inbox.org>
+# License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
+
+# common git alternates + all.git||ALL.git management code
+package PublicInbox::MultiGit;
+use strict;
+use v5.10.1;
+use PublicInbox::Spawn qw(run_die run_qx);
+use PublicInbox::Import;
+use File::Temp 0.19;
+use List::Util qw(max);
+use PublicInbox::IO qw(read_all);
+use autodie qw(chmod close rename);
+
+sub new {
+        my ($cls, $topdir, $all, $epfx) = @_;
+        bless {
+                topdir => $topdir, # inboxdir || extindex.*.topdir
+                all => $all, # all.git or ALL.git
+                epfx => $epfx, # "git" (inbox) or "local" (lei/store)
+        }, $cls;
+}
+
+sub read_alternates {
+        my ($self, $moderef, $prune) = @_;
+        my $objpfx = "$self->{topdir}/$self->{all}/objects/";
+        my $f = "${objpfx}info/alternates";
+        my %alt; # line => score
+        my %seen; # $st_dev\0$st_ino => count
+        my $other = 0;
+        if (open(my $fh, '<', $f)) {
+                my $is_edir = defined($self->{epfx}) ?
+                        qr!\A\Q../../$self->{epfx}\E/([0-9]+)\.git/objects\z! :
+                        undef;
+                $$moderef = (stat($fh))[2] & 07777;
+                for my $rel (split(/^/m, read_all($fh, -s _))) {
+                        chomp(my $dir = $rel);
+                        my $score;
+                        if (defined($is_edir) && $dir =~ $is_edir) {
+                                $score = $1 + 0;
+                                substr($dir, 0, 0) = $objpfx;
+                        } else { # absolute paths, if any (extindex)
+                                $score = --$other;
+                        }
+                        if (my @st = stat($dir)) {
+                                next if $seen{"$st[0]\0$st[1]"}++;
+                                $alt{$rel} = $score;
+                        } else {
+                                warn "W: stat($dir) failed: $! ($f)";
+                                if ($prune) {
+                                        ++$$prune;
+                                } else {
+                                        $alt{$rel} = $score;
+                                }
+                        }
+                }
+        } elsif (!$!{ENOENT}) {
+                die "E: open($f): $!";
+        }
+        (\%alt, \%seen);
+}
+
+sub epoch_dir { "$_[0]->{topdir}/$_[0]->{epfx}" }
+
+sub write_alternates {
+        my ($self, $mode, $alt, @new) = @_;
+        my $all_dir = "$self->{topdir}/$self->{all}";
+        PublicInbox::Import::init_bare($all_dir);
+        my $out = join('', sort { $alt->{$b} <=> $alt->{$a} } keys %$alt);
+        my $info_dir = "$all_dir/objects/info";
+        my $fh = File::Temp->new(TEMPLATE => 'alt-XXXX', DIR => $info_dir);
+        print $fh $out, @new;
+        chmod($mode, $fh);
+        close $fh;
+        rename($fh->filename, "$info_dir/alternates");
+        $fh->unlink_on_destroy(0);
+}
+
+# returns true if new epochs exist
+sub merge_epochs {
+        my ($self, $alt, $seen) = @_;
+        my $epoch_dir = epoch_dir($self);
+        if (opendir my $dh, $epoch_dir) {
+                my $has_new;
+                for my $bn (grep(/\A[0-9]+\.git\z/, readdir($dh))) {
+                        my $rel = "../../$self->{epfx}/$bn/objects\n";
+                        next if exists($alt->{$rel});
+                        if (my @st = stat("$epoch_dir/$bn/objects")) {
+                                next if $seen->{"$st[0]\0$st[1]"}++;
+                                $alt->{$rel} = substr($bn, 0, -4) + 0;
+                                $has_new = 1;
+                        } else {
+                                warn "E: stat($epoch_dir/$bn/objects): $!";
+                        }
+                }
+                $has_new;
+        } else {
+                $!{ENOENT} ? undef : die "opendir($epoch_dir): $!";
+        }
+}
+
+sub fill_alternates {
+        my ($self) = @_;
+        my ($alt, $seen) = read_alternates($self, \(my $mode = 0644));
+        merge_epochs($self, $alt, $seen) and
+                write_alternates($self, $mode, $alt);
+}
+
+sub epoch_cfg_set {
+        my ($self, $epoch_nr) = @_;
+        my $f = epoch_dir($self)."/$epoch_nr.git/config";
+        my $v = "../../$self->{all}/config";
+        if (-r $f) {
+                chomp(my $x = run_qx([qw(git config -f), $f, 'include.path']));
+                return if $x eq $v;
+        }
+        run_die([qw(git config -f), $f, 'include.path', $v ]);
+}
+
+sub add_epoch {
+        my ($self, $epoch_nr) = @_;
+        my $git_dir = epoch_dir($self)."/$epoch_nr.git";
+        my $f = "$git_dir/config";
+        my $existing = -f $f;
+        PublicInbox::Import::init_bare($git_dir);
+        epoch_cfg_set($self, $epoch_nr) unless $existing;
+        fill_alternates($self);
+        $git_dir;
+}
+
+sub git_epochs  {
+        my ($self) = @_;
+        if (opendir(my $dh, epoch_dir($self))) {
+                my @epochs = map {
+                        substr($_, 0, -4) + 0; # drop ".git" suffix
+                } grep(/\A[0-9]+\.git\z/, readdir($dh));
+                wantarray ? sort { $b <=> $a } @epochs : (max(@epochs) // 0);
+        } elsif ($!{ENOENT}) {
+                wantarray ? () : 0;
+        } else {
+                die(epoch_dir($self).": $!");
+        }
+}
+
+1;