]> Sergey Matveev's repositories - public-inbox.git/blob - lib/PublicInbox/Fetch.pm
new public-inbox-{clone,fetch} commands
[public-inbox.git] / lib / PublicInbox / Fetch.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 # Wrapper to "git fetch" remote public-inboxes
4 package PublicInbox::Fetch;
5 use strict;
6 use v5.10.1;
7 use parent qw(PublicInbox::IPC);
8 use URI ();
9 use PublicInbox::Spawn qw(popen_rd);
10 use PublicInbox::Admin;
11 use PublicInbox::LEI;
12 use PublicInbox::LeiCurl;
13 use PublicInbox::LeiMirror;
14 use IO::Uncompress::Gunzip qw(gunzip $GunzipError);
15 use File::Temp ();
16
17 sub new { bless {}, __PACKAGE__ }
18
19 sub fetch_cmd ($$) {
20         my ($lei, $opt) = @_;
21         my @cmd = qw(git);
22         $opt->{$_} = $lei->{$_} for (0..2);
23         # we support "-c $key=$val" for arbitrary git config options
24         # e.g.: git -c http.proxy=socks5h://127.0.0.1:9050
25         push(@cmd, '-c', $_) for @{$lei->{opt}->{c} // []};
26         push @cmd, 'fetch';
27         push @cmd, '-q' if $lei->{opt}->{quiet};
28         push @cmd, '-v' if $lei->{opt}->{verbose};
29         @cmd;
30 }
31
32 sub remote_url ($$) {
33         my ($lei, $dir) = @_; # TODO: support non-"origin"?
34         my $cmd = [ qw(git config remote.origin.url) ];
35         my $fh = popen_rd($cmd, undef, { -C => $dir, 2 => $lei->{2} });
36         my $url = <$fh>;
37         close $fh or return;
38         chomp $url;
39         $url;
40 }
41
42 sub do_fetch {
43         my ($cls, $lei, $cd) = @_;
44         my $ibx_ver;
45         my $curl = PublicInbox::LeiCurl->new($lei) or return;
46         my $dir = PublicInbox::Admin::resolve_inboxdir($cd, \$ibx_ver);
47         if ($ibx_ver == 1) {
48                 my $url = remote_url($lei, $dir) //
49                         die "E: $dir missing remote.origin.url\n";
50                 my $uri = URI->new($url);
51                 my $torsocks = $curl->torsocks($lei, $uri);
52                 my $opt = { -C => $dir };
53                 my $cmd = [ @$torsocks, fetch_cmd($lei, $opt) ];
54                 my $cerr = PublicInbox::LeiMirror::run_reap($lei, $cmd, $opt);
55                 $lei->child_error($cerr, "@$cmd failed") if $cerr;
56                 return;
57         }
58         # v2:
59         opendir my $dh, "$dir/git" or die "opendir $dir/git: $!";
60         my @epochs = sort { $b <=> $a } map { substr($_, 0, -4) + 0 }
61                                 grep(/\A[0-9]+\.git\z/, readdir($dh));
62         my ($git_url, $epoch);
63         for my $nr (@epochs) { # try newest epoch, first
64                 my $edir = "$dir/git/$nr.git";
65                 if (defined(my $url = remote_url($lei, $edir))) {
66                         $git_url = $url;
67                         $epoch = $nr;
68                         last;
69                 } else {
70                         warn "W: $edir missing remote.origin.url\n";
71                 }
72         }
73         $git_url or die "Unable to determine git URL\n";
74         my $inbox_url = $git_url;
75         $inbox_url =~ s!/git/$epoch(?:\.git)?/?\z!! or
76                 $inbox_url =~ s!/$epoch(?:\.git)?/?\z!! or die <<EOM;
77 Unable to infer inbox URL from <$git_url>
78 EOM
79         $lei->qerr("# inbox URL: $inbox_url/");
80         my $muri = URI->new("$inbox_url/manifest.js.gz");
81         my $ft = File::Temp->new(TEMPLATE => 'manifest-XXXX',
82                                 UNLINK => 1, DIR => $dir);
83         my $fn = $ft->filename;
84         my @opt = (qw(-R -o), $fn);
85         my $mf = "$dir/manifest.js.gz";
86         my $m0; # current manifest.js.gz contents
87         if (open my $fh, '<', $mf) {
88                 $m0 = eval {
89                         PublicInbox::LeiMirror::decode_manifest($fh, $mf, $mf)
90                 };
91                 $lei->err($@) if $@;
92                 push @opt, '-z', $mf if defined($m0);
93         }
94         my $curl_cmd = $curl->for_uri($lei, $muri, @opt);
95         my $opt = {};
96         $opt->{$_} = $lei->{$_} for (0..2);
97         my $cerr = PublicInbox::LeiMirror::run_reap($lei, $curl_cmd, $opt);
98         return $lei->child_error($cerr, "@$curl_cmd failed") if $cerr;
99         return if !-s $ft; # 304 Not Modified via curl -z
100
101         my $m1 = PublicInbox::LeiMirror::decode_manifest($ft, $fn, $muri);
102         my $mdiff = { %$m1 };
103
104         # filter out unchanged entries
105         while (my ($k, $v0) = each %{$m0 // {}}) {
106                 my $cur = $m1->{$k} // next;
107                 my $f0 = $v0->{fingerprint} // next;
108                 my $f1 = $cur->{fingerprint} // next;
109                 my $t0 = $v0->{modified} // next;
110                 my $t1 = $cur->{modified} // next;
111                 delete($mdiff->{$k}) if $f0 eq $f1 && $t0 == $t1;
112         }
113         my $ibx_uri = URI->new("$inbox_url/");
114         my ($path_pfx, $v1_bare, @v2_epochs) =
115                 PublicInbox::LeiMirror::deduce_epochs($mdiff, $ibx_uri->path);
116         defined($v1_bare) and die <<EOM;
117 E: got v1 `$v1_bare' when expecting v2 epoch(s) in <$muri>, WTF?
118 EOM
119         my @epoch_nr = sort { $a <=> $b }
120                 map { my ($nr) = (m!/([0-9]+)\.git\z!g) } @v2_epochs;
121
122         # n.b. this expects all epochs are from the same host
123         my $torsocks = $curl->torsocks($lei, $muri);
124         for my $nr (@epoch_nr) {
125                 my $dir = "$dir/git/$nr.git";
126                 my $cmd;
127                 my $opt = {};
128                 if (-d $dir) {
129                         $opt->{-C} = $dir;
130                         $cmd = [ @$torsocks, fetch_cmd($lei, $opt) ];
131                 } else {
132                         my $e_uri = $ibx_uri->clone;
133                         $e_uri->path($ibx_uri->path."git/$nr.git");
134                         $cmd = [ @$torsocks,
135                                 PublicInbox::LeiMirror::clone_cmd($lei, $opt),
136                                 $$e_uri, $dir ];
137                 }
138                 my $cerr = PublicInbox::LeiMirror::run_reap($lei, $cmd, $opt);
139                 return $lei->child_error($cerr, "@$cmd failed") if $cerr;
140         }
141         rename($fn, $mf) or die "E: rename($fn, $mf): $!\n";
142         $ft->unlink_on_destroy(0);
143 }
144
145 1;