]> Sergey Matveev's repositories - public-inbox.git/blob - lib/PublicInbox/MultiGit.pm
imap+nntp: share COMPRESS implementation
[public-inbox.git] / lib / PublicInbox / MultiGit.pm
1 # Copyright (C) all contributors <meta@public-inbox.org>
2 # License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
3
4 # common git alternates + all.git||ALL.git management code
5 package PublicInbox::MultiGit;
6 use strict;
7 use v5.10.1;
8 use PublicInbox::Spawn qw(run_die);
9 use PublicInbox::Import;
10 use File::Temp 0.19;
11 use List::Util qw(max);
12
13 sub new {
14         my ($cls, $topdir, $all, $epfx) = @_;
15         bless {
16                 topdir => $topdir, # inboxdir || extindex.*.topdir
17                 all => $all, # all.git or ALL.git
18                 epfx => $epfx, # "git" (inbox) or "local" (lei/store)
19         }, $cls;
20 }
21
22 sub read_alternates {
23         my ($self, $moderef, $prune) = @_;
24         my $objpfx = "$self->{topdir}/$self->{all}/objects/";
25         my $f = "${objpfx}info/alternates";
26         my %alt; # line => score
27         my %seen; # $st_dev\0$st_ino => count
28         my $other = 0;
29         if (open(my $fh, '<', $f)) {
30                 my $is_edir = defined($self->{epfx}) ?
31                         qr!\A\Q../../$self->{epfx}\E/([0-9]+)\.git/objects\z! :
32                         undef;
33                 $$moderef = (stat($fh))[2] & 07777;
34                 for my $rel (split(/^/m, do { local $/; <$fh> })) {
35                         chomp(my $dir = $rel);
36                         my $score;
37                         if (defined($is_edir) && $dir =~ $is_edir) {
38                                 $score = $1 + 0;
39                                 substr($dir, 0, 0) = $objpfx;
40                         } else { # absolute paths, if any (extindex)
41                                 $score = --$other;
42                         }
43                         if (my @st = stat($dir)) {
44                                 next if $seen{"$st[0]\0$st[1]"}++;
45                                 $alt{$rel} = $score;
46                         } else {
47                                 warn "W: stat($dir) failed: $! ($f)";
48                                 if ($prune) {
49                                         ++$$prune;
50                                 } else {
51                                         $alt{$rel} = $score;
52                                 }
53                         }
54                 }
55         } elsif (!$!{ENOENT}) {
56                 die "E: open($f): $!";
57         }
58         (\%alt, \%seen);
59 }
60
61 sub epoch_dir { "$_[0]->{topdir}/$_[0]->{epfx}" }
62
63 sub write_alternates {
64         my ($self, $mode, $alt, @new) = @_;
65         my $all_dir = "$self->{topdir}/$self->{all}";
66         PublicInbox::Import::init_bare($all_dir);
67         my $out = join('', sort { $alt->{$b} <=> $alt->{$a} } keys %$alt);
68         my $info_dir = "$all_dir/objects/info";
69         my $fh = File::Temp->new(TEMPLATE => 'alt-XXXX', DIR => $info_dir);
70         my $f = $fh->filename;
71         print $fh $out, @new or die "print($f): $!";
72         chmod($mode, $fh) or die "fchmod($f): $!";
73         close $fh or die "close($f): $!";
74         my $fn = "$info_dir/alternates";
75         rename($f, $fn) or die "rename($f, $fn): $!";
76         $fh->unlink_on_destroy(0);
77 }
78
79 # returns true if new epochs exist
80 sub merge_epochs {
81         my ($self, $alt, $seen) = @_;
82         my $epoch_dir = epoch_dir($self);
83         if (opendir my $dh, $epoch_dir) {
84                 my $has_new;
85                 for my $bn (grep(/\A[0-9]+\.git\z/, readdir($dh))) {
86                         my $rel = "../../$self->{epfx}/$bn/objects\n";
87                         next if exists($alt->{$rel});
88                         if (my @st = stat("$epoch_dir/$bn/objects")) {
89                                 next if $seen->{"$st[0]\0$st[1]"}++;
90                                 $alt->{$rel} = substr($bn, 0, -4) + 0;
91                                 $has_new = 1;
92                         } else {
93                                 warn "E: stat($epoch_dir/$bn/objects): $!";
94                         }
95                 }
96                 $has_new;
97         } else {
98                 $!{ENOENT} ? undef : die "opendir($epoch_dir): $!";
99         }
100 }
101
102 sub fill_alternates {
103         my ($self) = @_;
104         my ($alt, $seen) = read_alternates($self, \(my $mode = 0644));
105         merge_epochs($self, $alt, $seen) and
106                 write_alternates($self, $mode, $alt);
107 }
108
109 sub epoch_cfg_set {
110         my ($self, $epoch_nr) = @_;
111         run_die([qw(git config -f), epoch_dir($self)."/$epoch_nr.git/config",
112                 'include.path', "../../$self->{all}/config" ]);
113 }
114
115 sub add_epoch {
116         my ($self, $epoch_nr) = @_;
117         my $git_dir = epoch_dir($self)."/$epoch_nr.git";
118         my $f = "$git_dir/config";
119         my $existing = -f $f;
120         PublicInbox::Import::init_bare($git_dir);
121         epoch_cfg_set($self, $epoch_nr) unless $existing;
122         fill_alternates($self);
123         $git_dir;
124 }
125
126 sub git_epochs  {
127         my ($self) = @_;
128         if (opendir(my $dh, epoch_dir($self))) {
129                 my @epochs = map {
130                         substr($_, 0, -4) + 0; # drop ".git" suffix
131                 } grep(/\A[0-9]+\.git\z/, readdir($dh));
132                 wantarray ? sort { $b <=> $a } @epochs : (max(@epochs) // 0);
133         } elsif ($!{ENOENT}) {
134                 wantarray ? () : 0;
135         } else {
136                 die(epoch_dir($self).": $!");
137         }
138 }
139
140 1;