]> Sergey Matveev's repositories - public-inbox.git/blob - lib/PublicInbox/LeiP2q.pm
08ec81c5295ee3c43831dad38e3ef313c1e51dd6
[public-inbox.git] / lib / PublicInbox / LeiP2q.pm
1 # Copyright (C) 2021 all contributors <meta@public-inbox.org>
2 # License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
3
4 # front-end for the "lei patch-to-query" sub-command
5 package PublicInbox::LeiP2q;
6 use strict;
7 use v5.10.1;
8 use parent qw(PublicInbox::IPC);
9 use PublicInbox::Eml;
10 use PublicInbox::Smsg;
11 use PublicInbox::MsgIter qw(msg_part_text);
12 use PublicInbox::Git qw(git_unquote);
13 use PublicInbox::Spawn qw(popen_rd);
14 use URI::Escape qw(uri_escape_utf8);
15 my $FN = qr!((?:"?[^/\n]+/[^\r\n]+)|/dev/null)!;
16
17 sub xphrase ($) {
18         my ($s) = @_;
19         return () unless $s =~ /\S/;
20         # cf. xapian-core/queryparser/queryparser.lemony
21         # [\./:\\\@] - is_phrase_generator (implicit phrase search)
22         # FIXME not really sure about these..., we basically want to
23         # extract the longest phrase possible that Xapian can handle
24         map {
25                 s/\A\s*//;
26                 s/\s+\z//;
27                 m![^\./:\\\@\-\w]! ? qq("$_") : $_ ;
28         } ($s =~ m!(\w[\|=><,\./:\\\@\-\w\s]+)!g);
29 }
30
31 sub extract_terms { # eml->each_part callback
32         my ($p, $lei) = @_;
33         my $part = $p->[0]; # ignore $depth and @idx;
34         my $ct = $part->content_type || 'text/plain';
35         my ($s, undef) = msg_part_text($part, $ct);
36         defined $s or return;
37         my $in_diff;
38         # TODO: b: nq: q:
39         for (split(/\n/, $s)) {
40                 if ($in_diff && s/^ //) { # diff context
41                         push @{$lei->{qterms}->{dfctx}}, xphrase($_);
42                 } elsif (/^-- $/) { # email signature begins
43                         $in_diff = undef;
44                 } elsif (m!^diff --git $FN $FN!) {
45                         # wait until "---" and "+++" to capture filenames
46                         $in_diff = 1;
47                 } elsif (/^index ([a-f0-9]+)\.\.([a-f0-9]+)\b/) {
48                         my ($oa, $ob) = ($1, $2);
49                         push @{$lei->{qterms}->{dfpre}}, $oa;
50                         push @{$lei->{qterms}->{dfpost}}, $ob;
51                         # who uses dfblob?
52                 } elsif (m!^(?:---|\+{3}) ($FN)!) {
53                         next if $1 eq '/dev/null';
54                         my $fn = (split(m!/!, git_unquote($1.''), 2))[1];
55                         push @{$lei->{qterms}->{dfn}}, xphrase($fn);
56                 } elsif ($in_diff && s/^\+//) { # diff added
57                         push @{$lei->{qterms}->{dfb}}, xphrase($_);
58                 } elsif ($in_diff && s/^-//) { # diff removed
59                         push @{$lei->{qterms}->{dfa}}, xphrase($_);
60                 } elsif (/^@@ (?:\S+) (?:\S+) @@\s*$/) {
61                         # traditional diff w/o -p
62                 } elsif (/^@@ (?:\S+) (?:\S+) @@\s*(\S+.*)/) {
63                         push @{$lei->{qterms}->{dfhh}}, xphrase($1);
64                 } elsif (/^(?:dis)similarity index/ ||
65                                 /^(?:old|new) mode/ ||
66                                 /^(?:deleted|new) file mode/ ||
67                                 /^(?:copy|rename) (?:from|to) / ||
68                                 /^(?:dis)?similarity index / ||
69                                 /^\\ No newline at end of file/ ||
70                                 /^Binary files .* differ/) {
71                 } elsif ($_ eq '') {
72                         # possible to be in diff context, some mail may be
73                         # stripped by MUA or even GNU diff(1).  "git apply"
74                         # treats a bare "\n" as diff context, too
75                 } else {
76                         $in_diff = undef;
77                 }
78         }
79 }
80
81 my %pfx2smsg = (
82         t => [ qw(to) ],
83         c => [ qw(cc) ],
84         f => [ qw(from) ],
85         tc => [ qw(to cc) ],
86         tcf => [ qw(to cc from) ],
87         a => [ qw(to cc from) ],
88         s => [ qw(subject) ],
89         bs => [ qw(subject) ], # body handled elsewhere
90         d => [ qw(ds) ], # nonsense?
91         dt => [ qw(ds) ], # ditto...
92         rt => [ qw(ts) ], # ditto...
93 );
94
95 sub do_p2q { # via wq_do
96         my ($self) = @_;
97         my $lei = $self->{lei};
98         my $want = $lei->{opt}->{want} // [ qw(dfpost7) ];
99         my @want = split(/[, ]+/, "@$want");
100         for (@want) {
101                 /\A(?:(d|dt|rt):)?([0-9]+)(\.(?:day|weeks)s?)?\z/ or next;
102                 my ($pfx, $n, $unit) = ($1, $2, $3);
103                 $n *= 86400 * ($unit =~ /week/i ? 7 : 1);
104                 $_ = [ $pfx, $n ];
105         }
106         my $smsg = bless {}, 'PublicInbox::Smsg';
107         my $in = $self->{0};
108         my @cmd;
109         unless ($in) {
110                 my $input = $self->{input};
111                 my $devfd = $lei->path_to_fd($input) // return;
112                 if ($devfd >= 0) {
113                         $in = $lei->{$devfd};
114                 } elsif (-e $input) {
115                         open($in, '<', $input) or
116                                 return $lei->fail("open < $input: $!");
117                 } else {
118                         @cmd = (qw(git format-patch --stdout -1), $input);
119                         $in = popen_rd(\@cmd, undef, { 2 => $lei->{2} });
120                 }
121         };
122         my $str = do { local $/; <$in> };
123         @cmd && !close($in) and return $lei->fail("E: @cmd failed: $?");
124         my $eml = PublicInbox::Eml->new(\$str);
125         $lei->{diff_want} = +{ map { $_ => 1 } @want };
126         $smsg->populate($eml);
127         while (my ($pfx, $fields) = each %pfx2smsg) {
128                 next unless $lei->{diff_want}->{$pfx};
129                 for my $f (@$fields) {
130                         my $v = $smsg->{$f} // next;
131                         push @{$lei->{qterms}->{$pfx}}, xphrase($v);
132                 }
133         }
134         $eml->each_part(\&extract_terms, $lei, 1);
135         if ($lei->{opt}->{debug}) {
136                 my $json = ref(PublicInbox::Config->json)->new;
137                 $json->utf8->canonical->pretty;
138                 print { $lei->{2} } $json->encode($lei->{qterms});
139         }
140         my (@q, %seen);
141         for my $pfx (@want) {
142                 if (ref($pfx) eq 'ARRAY') {
143                         my ($p, $t_range) = @$pfx; # TODO
144
145                 } elsif ($pfx =~ m!\A(?:OR|XOR|AND|NOT)\z! ||
146                                 $pfx =~ m!\A(?:ADJ|NEAR)(?:/[0-9]+)?\z!) {
147                         push @q, $pfx;
148                 } else {
149                         my $plusminus = ($pfx =~ s/\A([\+\-])//) ? $1 : '';
150                         my $end = ($pfx =~ s/([0-9\*]+)\z//) ? $1 : '';
151                         my $x = delete($lei->{qterms}->{$pfx}) or next;
152                         my $star = $end =~ tr/*//d ? '*' : '';
153                         my $min_len = ($end || 0) + 0;
154
155                         # no wildcards for bool_pfx_external
156                         $star = '' if $pfx =~ /\A(dfpre|dfpost|mid)\z/;
157                         $pfx = "$plusminus$pfx:";
158                         if ($min_len) {
159                                 push @q, map {
160                                         my @t = ($pfx.$_.$star);
161                                         while (length > $min_len) {
162                                                 chop $_;
163                                                 push @t, 'OR', $pfx.$_.$star;
164                                         }
165                                         @t;
166                                 } @$x;
167                         } else {
168                                 push @q, map {
169                                         my $k = $pfx.$_.$star;
170                                         $seen{$k}++ ? () : $k
171                                 } @$x;
172                         }
173                 }
174         }
175         if ($lei->{opt}->{uri}) {
176                 @q = (join('+', map { uri_escape_utf8($_) } @q));
177         } else {
178                 @q = (join(' ', @q));
179         }
180         $lei->out(@q, "\n");
181 }
182
183 sub lei_p2q { # the "lei patch-to-query" entry point
184         my ($lei, $input) = @_;
185         my $self = bless {}, __PACKAGE__;
186         if ($lei->{opt}->{stdin}) {
187                 $self->{0} = delete $lei->{0}; # guard from _lei_atfork_child
188         } else {
189                 $self->{input} = $input;
190         }
191         my ($op_c, $ops) = $lei->workers_start($self, 1);
192         $lei->{wq1} = $self;
193         $self->wq_io_do('do_p2q', []);
194         $self->wq_close;
195         $lei->wait_wq_events($op_c, $ops);
196 }
197
198 sub ipc_atfork_child {
199         my ($self) = @_;
200         $self->{lei}->_lei_atfork_child;
201         $self->SUPER::ipc_atfork_child;
202 }
203
204 1;