1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Hubph\Cli; |
4
|
|
|
|
5
|
|
|
use Consolidation\AnnotatedCommand\CommandData; |
6
|
|
|
use Consolidation\Filter\FilterOutputData; |
7
|
|
|
use Consolidation\Filter\LogicalOpFactory; |
8
|
|
|
use Consolidation\OutputFormatters\Options\FormatterOptions; |
9
|
|
|
use Consolidation\OutputFormatters\StructuredData\RowsOfFields; |
10
|
|
|
use Consolidation\OutputFormatters\StructuredData\PropertyList; |
11
|
|
|
use Psr\Log\LoggerAwareInterface; |
12
|
|
|
use Psr\Log\LoggerAwareTrait; |
13
|
|
|
use Robo\Common\ConfigAwareTrait; |
14
|
|
|
use Robo\Contract\ConfigAwareInterface; |
15
|
|
|
use Consolidation\AnnotatedCommand\CommandError; |
16
|
|
|
use Hubph\HubphAPI; |
17
|
|
|
use Hubph\VersionIdentifiers; |
18
|
|
|
use Hubph\PullRequests; |
19
|
|
|
|
20
|
|
|
class HubphCommands extends \Robo\Tasks implements ConfigAwareInterface, LoggerAwareInterface |
21
|
|
|
{ |
22
|
|
|
use ConfigAwareTrait; |
23
|
|
|
use LoggerAwareTrait; |
24
|
|
|
|
25
|
|
|
/** |
26
|
|
|
* Report who we have authenticated as |
27
|
|
|
* |
28
|
|
|
* @command whoami |
29
|
|
|
*/ |
30
|
|
|
public function whoami($options = ['as' => 'default']) |
31
|
|
|
{ |
32
|
|
|
$api = $this->api($options['as']); |
33
|
|
|
$authenticated = $api->whoami(); |
34
|
|
|
$authenticatedUser = $authenticated['login']; |
35
|
|
|
|
36
|
|
|
$this->say("Authenticated as $authenticatedUser."); |
37
|
|
|
} |
38
|
|
|
|
39
|
|
|
/** |
40
|
|
|
* @command pr:close |
41
|
|
|
*/ |
42
|
|
|
public function prClose($projectWithOrg = '', $number = '', $options = ['as' => 'default']) |
43
|
|
|
{ |
44
|
|
|
if (empty($number) && preg_match('#^[0-9]*$#', $projectWithOrg)) { |
45
|
|
|
$number = $projectWithOrg; |
46
|
|
|
$projectWithOrg = ''; |
47
|
|
|
} |
48
|
|
|
$projectWithOrg = $this->projectWithOrg($projectWithOrg); |
49
|
|
|
list($org, $project) = explode('/', $projectWithOrg, 2); |
50
|
|
|
|
51
|
|
|
$api = $this->api($options['as']); |
52
|
|
|
$api->prClose($org, $project, $number); |
53
|
|
|
} |
54
|
|
|
|
55
|
|
|
/* |
56
|
|
|
* hubph pr:check --vid=php-7.0./31 --vid=php-7.1./20 |
57
|
|
|
* |
58
|
|
|
* status 0 and csv with PR numbers to close |
59
|
|
|
* |
60
|
|
|
* - or - |
61
|
|
|
* |
62
|
|
|
* status 1 if all vid/vvals exist and nothing more needs to be done |
63
|
|
|
*/ |
64
|
|
|
|
65
|
|
|
/** |
66
|
|
|
* @command pr:check |
67
|
|
|
*/ |
68
|
|
|
public function prCheck( |
69
|
|
|
$options = [ |
70
|
|
|
'message|m' => '', |
71
|
|
|
'file|F' => '', |
72
|
|
|
'base' => '', |
73
|
|
|
'head' => '', |
74
|
|
|
'as' => 'default', |
75
|
|
|
'format' => 'yaml', |
76
|
|
|
'idempotent' => false |
77
|
|
|
] |
78
|
|
|
) { |
79
|
|
|
$projectWithOrg = $this->projectWithOrg(); |
80
|
|
|
|
81
|
|
|
// Get the commit message from --message or --file |
82
|
|
|
$message = $this->getMessage($options); |
83
|
|
|
|
84
|
|
|
// Determine all of the vid/vval pairs if idempotent |
85
|
|
|
$vids = $this->getVids($options, $message); |
86
|
|
|
|
87
|
|
|
$api = $this->api($options['as']); |
88
|
|
|
list($status, $result) = $api->prCheck($projectWithOrg, $vids); |
89
|
|
|
|
90
|
|
|
if ($status) { |
91
|
|
|
return new CommandError($result, $status); |
92
|
|
|
} |
93
|
|
|
if (is_string($result)) { |
94
|
|
|
$this->logger->notice("No open pull requests that need to be closed."); |
95
|
|
|
return; |
96
|
|
|
} |
97
|
|
|
return implode(',', (array)$result); |
98
|
|
|
} |
99
|
|
|
|
100
|
|
|
/** |
101
|
|
|
* @command pr:create |
102
|
|
|
* @aliases pull-request |
103
|
|
|
*/ |
104
|
|
|
public function prCreate( |
105
|
|
|
$options = [ |
106
|
|
|
'message|m' => '', |
107
|
|
|
'file|F' => '', |
108
|
|
|
'base' => '', |
109
|
|
|
'head' => '', |
110
|
|
|
'as' => 'default', |
111
|
|
|
'format' => 'yaml', |
112
|
|
|
'idempotent' => false |
113
|
|
|
] |
114
|
|
|
) { |
115
|
|
|
$projectWithOrg = $this->projectWithOrg(); |
116
|
|
|
|
117
|
|
|
// Get the commit message from --message or --file |
118
|
|
|
$message = $this->getMessage($options); |
119
|
|
|
|
120
|
|
|
// Determine all of the vid/vval pairs if idempotent |
121
|
|
|
$vids = $this->getVids($options, $message); |
122
|
|
|
|
123
|
|
|
$api = $this->api($options['as']); |
124
|
|
|
list($status, $result) = $api->prCheck($projectWithOrg, $vids); |
125
|
|
|
|
126
|
|
|
if ($status) { |
127
|
|
|
return new CommandError($result, $status); |
128
|
|
|
} |
129
|
|
|
|
130
|
|
|
// Go ahead and create the requested PR |
131
|
|
|
|
132
|
|
|
// If $result is an array, it will contain |
133
|
|
|
// all of the pull request numbers to close. |
134
|
|
|
// TODO: We should make a wrapper object for $result |
135
|
|
|
if (is_array($result)) { |
136
|
|
|
list($org, $project) = explode('/', $projectWithOrg, 2); |
137
|
|
|
$api->prClose($org, $project, $result); |
138
|
|
|
} |
139
|
|
|
} |
140
|
|
|
|
141
|
|
|
protected function getMessage($options) |
142
|
|
|
{ |
143
|
|
|
if (!empty($options['message'])) { |
144
|
|
|
return $options['message']; |
145
|
|
|
} |
146
|
|
|
if (!empty($options['file'])) { |
147
|
|
|
return file_get_contents($options['file']); |
148
|
|
|
} |
149
|
|
|
return ''; |
150
|
|
|
} |
151
|
|
|
|
152
|
|
|
protected function getVids($options, $message) |
153
|
|
|
{ |
154
|
|
|
$vids = new VersionIdentifiers(); |
155
|
|
|
|
156
|
|
|
//if (empty($options['idempotent'])) { |
|
|
|
|
157
|
|
|
// return $vids; |
158
|
|
|
//} |
159
|
|
|
|
160
|
|
|
// Allow the caller to define more specific vid / vval patterns |
161
|
|
|
if (!empty($options['vid'])) { |
162
|
|
|
$vids->setVidPattern($options['vid']); |
163
|
|
|
} |
164
|
|
|
if (!empty($options['vval'])) { |
165
|
|
|
$vids->setVvalPattern($options['vval']); |
166
|
|
|
} |
167
|
|
|
|
168
|
|
|
$vids->addVidsFromMessage($message); |
169
|
|
|
return $vids; |
170
|
|
|
} |
171
|
|
|
|
172
|
|
|
protected function projectWithOrg($projectWithOrg = '') |
173
|
|
|
{ |
174
|
|
|
if (!empty($projectWithOrg)) { |
175
|
|
|
return $projectWithOrg; |
176
|
|
|
} |
177
|
|
|
|
178
|
|
|
return $this->getProjectWithOrgFromRemote(); |
179
|
|
|
} |
180
|
|
|
|
181
|
|
|
protected function getProjectWithOrgFromRemote($remote = 'origin', $cwd = '') |
182
|
|
|
{ |
183
|
|
|
$remote = $this->getRemote($remote, $cwd); |
184
|
|
|
|
185
|
|
|
return $this->getProjectWithOrfFromUrl($remote); |
186
|
|
|
} |
187
|
|
|
|
188
|
|
|
protected function getProjectWithOrfFromUrl($remote) |
189
|
|
|
{ |
190
|
|
|
$remote = preg_replace('#^git@[^:]*:#', '', $remote); |
191
|
|
|
$remote = preg_replace('#^[^:]*://[^/]/#', '', $remote); |
192
|
|
|
$remote = preg_replace('#\.git$#', '', $remote); |
193
|
|
|
|
194
|
|
|
return $remote; |
195
|
|
|
} |
196
|
|
|
|
197
|
|
|
protected function getRemote($remote = 'origin', $cwd = '') |
198
|
|
|
{ |
199
|
|
|
if (!empty($cwd)) { |
200
|
|
|
$cwd = "-C $cwd"; |
201
|
|
|
} |
202
|
|
|
return exec("git {$cwd} config --get remote.{$remote}.url"); |
203
|
|
|
} |
204
|
|
|
|
205
|
|
|
/** |
206
|
|
|
* @command pr:find |
207
|
|
|
* @param $projectWithOrg The project to work on, e.g. org/project |
208
|
|
|
* @option $q Query term |
209
|
|
|
* @filter-output |
210
|
|
|
* @field-labels |
211
|
|
|
* url: Url |
212
|
|
|
* id: ID |
213
|
|
|
* node_id: Node ID |
214
|
|
|
* html_url: HTML Url |
215
|
|
|
* diff_url: Diff Url |
216
|
|
|
* patch_url: Patch Url |
217
|
|
|
* issue_url: Issue Url |
218
|
|
|
* number: Number |
219
|
|
|
* state: State |
220
|
|
|
* locked: Locked |
221
|
|
|
* title: Title |
222
|
|
|
* user: User |
223
|
|
|
* body: Boday |
224
|
|
|
* created_at: Created |
225
|
|
|
* updated_at: Updated |
226
|
|
|
* closed_at: Closed |
227
|
|
|
* merged_at: Merged |
228
|
|
|
* merge_commit_sha: Merge Commit |
229
|
|
|
* assignee: Assignee |
230
|
|
|
* assignees: Assignees |
231
|
|
|
* requested_reviewers: Requested Reviewers |
232
|
|
|
* requested_teams: Requested Teams |
233
|
|
|
* labels: Labels |
234
|
|
|
* milestone: Milestone |
235
|
|
|
* commits_url: Commit Url |
236
|
|
|
* review_comments_url: Review Comments Url |
237
|
|
|
* review_comment_url: Review Comment Url |
238
|
|
|
* comments_url: Comments Url |
239
|
|
|
* statuses_url: Statuses Url |
240
|
|
|
* head: Head |
241
|
|
|
* base: Base |
242
|
|
|
* _links: Links |
243
|
|
|
* @default-fields number,user,title |
244
|
|
|
* @default-string-field number |
245
|
|
|
* @return Consolidation\OutputFormatters\StructuredData\RowsOfFields |
246
|
|
|
*/ |
247
|
|
|
public function prFind($projectWithOrg = '', $options = ['as' => 'default', 'format' => 'yaml', 'q' => '']) |
248
|
|
|
{ |
249
|
|
|
$api = $this->api($options['as']); |
250
|
|
|
$projectWithOrg = $this->projectWithOrg($projectWithOrg); |
251
|
|
|
$q = $options['q']; |
252
|
|
|
|
253
|
|
|
if (!empty($q)) { |
254
|
|
|
$q = $q . ' '; |
255
|
|
|
} |
256
|
|
|
$q = $q . 'repo:' . $projectWithOrg; |
257
|
|
|
$searchResults = $api->gitHubAPI()->api('search')->issues($q); |
258
|
|
|
$pullRequests = $searchResults['items']; |
259
|
|
|
|
260
|
|
|
$pullRequests = $this->keyById($pullRequests, 'number'); |
261
|
|
|
$result = new RowsOfFields($pullRequests); |
262
|
|
|
$this->alterPRTables($result); |
263
|
|
|
|
264
|
|
|
return $result; |
265
|
|
|
} |
266
|
|
|
|
267
|
|
|
/** |
268
|
|
|
* @command pr:show |
269
|
|
|
* @field-labels |
270
|
|
|
* url: Url |
271
|
|
|
* id: ID |
272
|
|
|
* node_id: Node ID |
273
|
|
|
* html_url: HTML Url |
274
|
|
|
* diff_url: Diff Url |
275
|
|
|
* patch_url: Patch Url |
276
|
|
|
* issue_url: Issue Url |
277
|
|
|
* number: Number |
278
|
|
|
* state: State |
279
|
|
|
* locked: Locked |
280
|
|
|
* title: Title |
281
|
|
|
* user: User |
282
|
|
|
* body: Boday |
283
|
|
|
* created_at: Created |
284
|
|
|
* updated_at: Updated |
285
|
|
|
* closed_at: Closed |
286
|
|
|
* merged_at: Merged |
287
|
|
|
* merge_commit_sha: Merge Commit |
288
|
|
|
* assignee: Assignee |
289
|
|
|
* assignees: Assignees |
290
|
|
|
* requested_reviewers: Requested Reviewers |
291
|
|
|
* requested_teams: Requested Teams |
292
|
|
|
* labels: Labels |
293
|
|
|
* milestone: Milestone |
294
|
|
|
* commits_url: Commit Url |
295
|
|
|
* review_comments_url: Review Comments Url |
296
|
|
|
* review_comment_url: Review Comment Url |
297
|
|
|
* comments_url: Comments Url |
298
|
|
|
* statuses_url: Statuses Url |
299
|
|
|
* head: Head |
300
|
|
|
* base: Base |
301
|
|
|
* _links: Links |
302
|
|
|
* @return Consolidation\OutputFormatters\StructuredData\PropertyList |
303
|
|
|
*/ |
304
|
|
|
public function prShow($projectWithOrg = '', $number = '', $options = ['as' => 'default', 'format' => 'table']) |
305
|
|
|
{ |
306
|
|
|
if (empty($number) && preg_match('#^[0-9]*$#', $projectWithOrg)) { |
307
|
|
|
$number = $projectWithOrg; |
308
|
|
|
$projectWithOrg = ''; |
309
|
|
|
} |
310
|
|
|
$api = $this->api($options['as']); |
311
|
|
|
$projectWithOrg = $this->projectWithOrg($projectWithOrg); |
312
|
|
|
|
313
|
|
|
list($org, $project) = explode('/', $projectWithOrg, 2); |
314
|
|
|
|
315
|
|
|
$pullRequests = $api->gitHubAPI()->api('pull_request')->show($org, $project, $number); |
316
|
|
|
|
317
|
|
|
$result = new PropertyList($pullRequests); |
318
|
|
|
$this->alterPRTables($result); |
319
|
|
|
|
320
|
|
|
return $result; |
321
|
|
|
} |
322
|
|
|
|
323
|
|
|
/** |
324
|
|
|
* @command pr:list |
325
|
|
|
* @param $projectWithOrg The project to work on, e.g. org/project |
326
|
|
|
* @filter-output |
327
|
|
|
* @field-labels |
328
|
|
|
* url: Url |
329
|
|
|
* id: ID |
330
|
|
|
* node_id: Node ID |
331
|
|
|
* html_url: HTML Url |
332
|
|
|
* diff_url: Diff Url |
333
|
|
|
* patch_url: Patch Url |
334
|
|
|
* issue_url: Issue Url |
335
|
|
|
* number: Number |
336
|
|
|
* state: State |
337
|
|
|
* locked: Locked |
338
|
|
|
* title: Title |
339
|
|
|
* user: User |
340
|
|
|
* body: Boday |
341
|
|
|
* created_at: Created |
342
|
|
|
* updated_at: Updated |
343
|
|
|
* closed_at: Closed |
344
|
|
|
* merged_at: Merged |
345
|
|
|
* merge_commit_sha: Merge Commit |
346
|
|
|
* assignee: Assignee |
347
|
|
|
* assignees: Assignees |
348
|
|
|
* requested_reviewers: Requested Reviewers |
349
|
|
|
* requested_teams: Requested Teams |
350
|
|
|
* labels: Labels |
351
|
|
|
* milestone: Milestone |
352
|
|
|
* commits_url: Commit Url |
353
|
|
|
* review_comments_url: Review Comments Url |
354
|
|
|
* review_comment_url: Review Comment Url |
355
|
|
|
* comments_url: Comments Url |
356
|
|
|
* statuses_url: Statuses Url |
357
|
|
|
* head: Head |
358
|
|
|
* base: Base |
359
|
|
|
* _links: Links |
360
|
|
|
* @default-fields number,user,title |
361
|
|
|
* @default-string-field number |
362
|
|
|
* @return Consolidation\OutputFormatters\StructuredData\RowsOfFields |
363
|
|
|
*/ |
364
|
|
|
public function prList($projectWithOrg = '', $options = ['state' => 'open', 'as' => 'default', 'format' => 'table']) |
365
|
|
|
{ |
366
|
|
|
$api = $this->api($options['as']); |
367
|
|
|
$projectWithOrg = $this->projectWithOrg($projectWithOrg); |
368
|
|
|
|
369
|
|
|
list($org, $project) = explode('/', $projectWithOrg, 2); |
370
|
|
|
|
371
|
|
|
$pullRequests = $api->gitHubAPI()->api('pull_request')->all($org, $project, ['state' => $options['state']]); |
372
|
|
|
|
373
|
|
|
$pullRequests = $this->keyById($pullRequests, 'number'); |
374
|
|
|
|
375
|
|
|
$result = new RowsOfFields($pullRequests); |
376
|
|
|
$this->alterPRTables($result); |
377
|
|
|
|
378
|
|
|
return $result; |
379
|
|
|
} |
380
|
|
|
|
381
|
|
|
protected function alterPRTables($data) |
382
|
|
|
{ |
383
|
|
|
$data->addRendererFunction( |
384
|
|
|
function ($key, $cellData, FormatterOptions $options, $rowData) { |
|
|
|
|
385
|
|
|
if (is_array($cellData)) { |
386
|
|
|
if (empty($cellData)) { |
387
|
|
|
return ''; |
388
|
|
|
} |
389
|
|
|
foreach (['login', 'label'] as $k) { |
390
|
|
|
if (isset($cellData[$k])) { |
391
|
|
|
return $cellData[$k]; |
392
|
|
|
} |
393
|
|
|
} |
394
|
|
|
// TODO: simplify |
395
|
|
|
// assignees |
396
|
|
|
// requested_reviewers |
397
|
|
|
// requested_teams |
398
|
|
|
// labels |
399
|
|
|
// _links |
400
|
|
|
return json_encode($cellData, true); |
401
|
|
|
} |
402
|
|
|
if (!is_string($cellData)) { |
403
|
|
|
return var_export($cellData, true); |
404
|
|
|
} |
405
|
|
|
return $cellData; |
406
|
|
|
} |
407
|
|
|
); |
408
|
|
|
} |
409
|
|
|
|
410
|
|
|
protected function keyById($data, $field) |
411
|
|
|
{ |
412
|
|
|
return |
413
|
|
|
array_column( |
414
|
|
|
array_map( |
415
|
|
|
function ($k) use ($data, $field) { |
416
|
|
|
return [$data[$k][$field], $data[$k]]; |
417
|
|
|
}, |
418
|
|
|
array_keys($data) |
419
|
|
|
), |
420
|
|
|
1, |
421
|
|
|
0 |
422
|
|
|
); |
423
|
|
|
} |
424
|
|
|
|
425
|
|
|
/** |
426
|
|
|
* @hook alter @filter-output |
427
|
|
|
* @option $filter Filter output based on provided expression |
428
|
|
|
* @default $filter '' |
429
|
|
|
*/ |
430
|
|
|
public function filterOutput($result, CommandData $commandData) |
431
|
|
|
{ |
432
|
|
|
$expr = $commandData->input()->getOption('filter'); |
433
|
|
|
if (!empty($expr)) { |
434
|
|
|
$factory = LogicalOpFactory::get(); |
435
|
|
|
$op = $factory->evaluate($expr); |
436
|
|
|
$filter = new FilterOutputData(); |
437
|
|
|
$result = $filter->filter($result, $op); |
438
|
|
|
} |
439
|
|
|
|
440
|
|
|
return $result; |
441
|
|
|
} |
442
|
|
|
|
443
|
|
|
protected function api($as = 'default') |
444
|
|
|
{ |
445
|
|
|
$api = new HubphAPI($this->getConfig()); |
446
|
|
|
$api->setAs($as); |
447
|
|
|
|
448
|
|
|
return $api; |
449
|
|
|
} |
450
|
|
|
} |
451
|
|
|
|
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.
The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.
This check looks for comments that seem to be mostly valid code and reports them.