# Copyright (C) all contributors # License: AGPL-3.0+ # common git alternates + all.git||ALL.git management code package PublicInbox::MultiGit; use strict; use v5.10.1; use PublicInbox::Spawn qw(run_die); use PublicInbox::Import; use File::Temp 0.19; use List::Util qw(max); 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, do { local $/; <$fh> })) { 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); my $f = $fh->filename; print $fh $out, @new or die "print($f): $!"; chmod($mode, $fh) or die "fchmod($f): $!"; close $fh or die "close($f): $!"; my $fn = "$info_dir/alternates"; rename($f, $fn) or die "rename($f, $fn): $!"; $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) = @_; run_die([qw(git config -f), epoch_dir($self)."/$epoch_nr.git/config", 'include.path', "../../$self->{all}/config" ]); } 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;