/
Pram.pm
289 lines (218 loc) · 7.77 KB
/
Pram.pm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
#!/usr/bin/env perl
package Gentoo::App::Pram;
our $VERSION = '0.200000';
use warnings;
use strict;
use Term::ANSIColor qw/colored/;
use File::Basename qw/basename/;
use File::Which qw/which/;
use Encode qw/decode/;
use File::Temp;
use HTTP::Tiny;
use constant E_ERROR => colored('ERROR', 'red');
use constant E_NO => colored('NO', 'red');
use constant E_YES => colored('YES', 'green');
use constant E_OK => colored('OK', 'green');
use constant E_MERGE => colored('MERGE', 'blue');
use constant CLOSES_GITHUB => qr#\ACloses: https?://github\.com#;
use Getopt::Long;
use Pod::Usage;
sub new {
my ($class, @args) = @_;
return bless { ref $args[0] ? %{ $args[0] } : @args }, $class;
}
sub new_with_opts {
my ($class) = @_;
my @opts = (
'repository|r=s',
'closes|c=s',
'editor|e=s',
'signoff|s',
'bug|b=s',
'help|h',
'man|m'
);
my %opts;
if (!GetOptions(\%opts, @opts)) {
print "\n";
pod2usage(-verbose => 1)
}
$opts{pr_number} = shift @ARGV;
return $class->new(\%opts);
}
sub run {
my ($self) = @_;
my $pr_number = $self->{pr_number};
my $closes = $self->{closes};
my $bug = $self->{bug};
$| = 1;
$self->{help} and pod2usage(-verbose => 1);
$self->{man} and pod2usage(-verbose => 2);
$bug and $closes and pod2usage(
-message => E_ERROR . qq#! --bug and --closes options are mutually exclusive!\n#,
-verbose => 1
);
run_checks($pr_number, "You must specify a Pull Request number!");
$bug and run_checks($bug, "You must specify a bug number when using --bug!");
$closes and run_checks($closes, "You must specify a bug number when using --closes!");
# Defaults to 'gentoo/gentoo' because we're worth it.
my $repo_name = $self->{repository} || 'gentoo/gentoo';
my $editor = $self->{editor} || $ENV{EDITOR} || 'less';
my $git_command = which('git') . ' am --keep-cr -S';
# Automatically pass the Sign-Off option to the git am command if the
# repository is Gentoo.
if ($repo_name =~ /gentoo\/gentoo/) {
$git_command = "$git_command -s";
}
# But don't add the option again if the -s option is passed to pram.
if ($self->{signoff}) {
if ($repo_name !~ /gentoo\/gentoo/) {
$git_command = "$git_command -s";
}
}
my $patch_url = "https://patch-diff.githubusercontent.com/raw/$repo_name/pull/$pr_number.patch";
$self->{pr_url} = "https://github.com/$repo_name/pull/$pr_number";
# Go!
$self->apply_patch(
$editor,
$git_command,
$self->modify_patch(
$self->fetch_patch($patch_url)
)
);
}
sub run_checks {
@_ == 2 || die qq#Usage: run_checks(obj, error_msg)#;
my ($obj, $error_msg) = @_;
$obj || pod2usage(
-message => E_ERROR . qq#! $error_msg\n#,
-verbose => 1
);
$obj =~ /^\d+$/ || pod2usage(
-message => E_ERROR . qq#! "$obj" is NOT a number!\n#,
-verbose => 1
);
}
sub my_sleep {
select(undef, undef, undef, 0.50);
}
sub fetch_patch {
@_ == 2 || die qq#Usage: fetch_patch(patch_url)\n#;
my ($self, $patch_url) = @_;
print "Fetching $patch_url ... ";
my $response = HTTP::Tiny->new->get($patch_url);
my $status = $response->{status};
$status != 200 and die "\n" . E_ERROR . qq#! Unreachable URL! Got HTTP status $status!\n#;
my $patch = $response->{content};
print E_OK . "!\n";
return decode('UTF-8', $patch);
}
sub add_header {
@_ == 3 || die qq#Usage: add_header(patch, header, msg)\n#;
my ($patch, $header, $msg) = @_;
print qq#$msg#;
my_sleep();
my $confirm = E_ERROR;
my $is_sub = $patch =~ s#---#$header#;
$is_sub and $confirm = E_OK;
print "$confirm!\n";
my_sleep();
return $patch;
}
sub modify_patch {
@_ == 2 || die qq#Usage: modify_patch(patch)\n#;
my ($self, $patch) = @_;
if (not $patch =~ CLOSES_GITHUB) {
my $pr_url = $self->{pr_url};
$patch = add_header(
$patch,
qq#Closes: $pr_url\n---#,
qq#Adding Github "Closes:" header ... #
);
}
if ($self->{bug}) {
my $bug = $self->{bug};
$patch = add_header(
$patch,
qq#Bug: https://bugs.gentoo.org/$bug\n---#,
qq#Adding Gentoo "Bug:" header with bug $bug ... #
);
}
if ($self->{closes}) {
my $closes = $self->{closes};
$patch = add_header(
$patch,
qq#Closes: https://bugs.gentoo.org/$closes\n---#,
qq#Adding Gentoo "Closes:" header with bug $closes ... #
);
}
return $patch;
}
sub apply_patch {
@_ == 4 || die qq#Usage: apply_patch(editor, git_command, patch)\n#;
my ($self, $editor, $git_command, $patch) = @_;
my $patch_location = File::Temp->new() . '.patch';
open my $fh, '>:encoding(UTF-8)', $patch_location || die E_ERROR . qq#! Can't write to $patch_location: $!!\n#;
print $fh $patch;
close $fh;
print "Opening $patch_location with $editor ... ";
my_sleep();
my $exit = system $editor => $patch_location;
$exit eq 0 || die E_ERROR . qq#! Could not open $patch_location: $!!\n#;
print E_OK . "!\n";
print E_MERGE . "? Do you want to apply this patch and merge this PR? [y/n] ";
chomp(my $answer = <STDIN>);
if ($answer =~ /^[Yy]$/) {
$git_command = "$git_command $patch_location";
print E_YES . "!\n";
print "Launching '$git_command' ... \n";
$exit = system join ' ', $git_command;
$exit eq 0 || die E_ERROR . qq#! Error when launching '$git_command': $!!\n#;
print E_OK . "!\n";
} else {
print E_NO . "!\nBailing out.\n";
}
print "Removing $patch_location ... ";
unlink $patch_location || die E_ERROR . qq#! Couldn't remove '$patch_location'!\n#;
print E_OK . "!\n";
}
1;
__END__
=head1 NAME
Gentoo::App::Pram - Library to fetch a GitHub Pull Request as an am-like patch.
=head1 DESCRIPTION
The purpose of this module is to fetch Pull Requests from GitHub's CDN as
am-like patches in order to facilitate the merging and closing of Pull
Requests. This module also takes care of adding "Closes:" and "Bug:" headers to
patches when necessary. See GLEP 0066.
=head1 FUNCTIONS
=over 4
=item * fetch_patch($patch_url)
Fetch patch from $patch_url. Return patch as a string.
=item * modify_patch($patch)
Modify the patch headers. This function only modifies the headers of the first
commit. Namely:
* Add a "Closes: https://github.com/XXX" header. Check first if it wasn't added
already by the contributor. This header is parsed by the Github bot upon merge.
The bot then automatically closes the pull request. See
https://help.github.com/articles/closing-issues-using-keywords for more info.
* Add a "Bug: https://bugs.gentoo.org/XXX" header when the `--bug XXX` option
is given. This header is parsed by the Gentoo Bugzilla bot upon merge. The bot
then writes a message in the bug report. See GLEP 0066 for more info.
* Add a "Closes: https://bugs.gentoo.org/XXX" header when the `--closes XXX`
option is given. This header is parsed by the Gentoo Bugzilla bot upon merge.
The bot then automatically closes the bug report. See GLEP 0066 for more info.
=item * apply_patch($editor, $git_command, $patch)
Apply $patch onto HEAD of the current git repository using $git_command. This
function also shows $patch in $editor for a final review.
=back
=head1 VERSION
version 0.200000
=head1 COPYRIGHT AND LICENSE
This software is copyright (c) 2016 by Patrice Clement.
This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.
=head1 AUTHOR
Patrice Clement <monsieurp@gentoo.org>
Kent Fredric <kentnl@gentoo.org>
=cut