Completed
Push — master ( b60952...ee4ec3 )
by Greg
02:01
created

HubphCommands::prCreate()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 36

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 36
rs 9.344
c 0
b 0
f 0
cc 3
nc 3
nop 1
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'])) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
85% of this comment could be valid code. Did you maybe forget this after debugging?

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.

Loading history...
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
        $remote = preg_replace('#^git@[^:]*:#', '', $remote);
186
        $remote = preg_replace('#^[^:]*://[^/]/#', '', $remote);
187
        $remote = preg_replace('#\.git$#', '', $remote);
188
189
        return $remote;
190
    }
191
192
    protected function getRemote($remote = 'origin', $cwd = '')
193
    {
194
        if (!empty($cwd)) {
195
            $cwd = "-C $cwd";
196
        }
197
        return exec("git {$cwd} config --get remote.{$remote}.url");
198
    }
199
200
    /**
201
     * @command pr:find
202
     * @param $projectWithOrg The project to work on, e.g. org/project
203
     * @option $q Query term
204
     * @filter-output
205
     * @field-labels
206
     *   url: Url
207
     *   id: ID
208
     *   node_id: Node ID
209
     *   html_url: HTML Url
210
     *   diff_url: Diff Url
211
     *   patch_url: Patch Url
212
     *   issue_url: Issue Url
213
     *   number: Number
214
     *   state: State
215
     *   locked: Locked
216
     *   title: Title
217
     *   user: User
218
     *   body: Boday
219
     *   created_at: Created
220
     *   updated_at: Updated
221
     *   closed_at: Closed
222
     *   merged_at: Merged
223
     *   merge_commit_sha: Merge Commit
224
     *   assignee: Assignee
225
     *   assignees: Assignees
226
     *   requested_reviewers: Requested Reviewers
227
     *   requested_teams: Requested Teams
228
     *   labels: Labels
229
     *   milestone: Milestone
230
     *   commits_url: Commit Url
231
     *   review_comments_url: Review Comments Url
232
     *   review_comment_url: Review Comment Url
233
     *   comments_url: Comments Url
234
     *   statuses_url: Statuses Url
235
     *   head: Head
236
     *   base: Base
237
     *   _links: Links
238
     * @default-fields number,user,title
239
     * @default-string-field number
240
     * @return Consolidation\OutputFormatters\StructuredData\RowsOfFields
241
    */
242
    public function prFind($projectWithOrg = '', $options = ['as' => 'default', 'format' => 'yaml', 'q' => ''])
243
    {
244
        $api = $this->api($options['as']);
245
        $projectWithOrg = $this->projectWithOrg($projectWithOrg);
246
        $q = $options['q'];
247
248
        if (!empty($q)) {
249
            $q = $q . ' ';
250
        }
251
        $q = $q . 'repo:' . $projectWithOrg;
252
        $searchResults = $api->gitHubAPI()->api('search')->issues($q);
253
        $pullRequests = $searchResults['items'];
254
255
        $pullRequests = $this->keyById($pullRequests, 'number');
256
        $result = new RowsOfFields($pullRequests);
257
        $this->alterPRTables($result);
258
259
        return $result;
260
    }
261
262
    /**
263
     * @command pr:show
264
     * @field-labels
265
     *   url: Url
266
     *   id: ID
267
     *   node_id: Node ID
268
     *   html_url: HTML Url
269
     *   diff_url: Diff Url
270
     *   patch_url: Patch Url
271
     *   issue_url: Issue Url
272
     *   number: Number
273
     *   state: State
274
     *   locked: Locked
275
     *   title: Title
276
     *   user: User
277
     *   body: Boday
278
     *   created_at: Created
279
     *   updated_at: Updated
280
     *   closed_at: Closed
281
     *   merged_at: Merged
282
     *   merge_commit_sha: Merge Commit
283
     *   assignee: Assignee
284
     *   assignees: Assignees
285
     *   requested_reviewers: Requested Reviewers
286
     *   requested_teams: Requested Teams
287
     *   labels: Labels
288
     *   milestone: Milestone
289
     *   commits_url: Commit Url
290
     *   review_comments_url: Review Comments Url
291
     *   review_comment_url: Review Comment Url
292
     *   comments_url: Comments Url
293
     *   statuses_url: Statuses Url
294
     *   head: Head
295
     *   base: Base
296
     *   _links: Links
297
     * @return Consolidation\OutputFormatters\StructuredData\PropertyList
298
     */
299
    public function prShow($projectWithOrg = '', $number = '', $options = ['as' => 'default', 'format' => 'table'])
300
    {
301
        if (empty($number) && preg_match('#^[0-9]*$#', $projectWithOrg)) {
302
            $number = $projectWithOrg;
303
            $projectWithOrg = '';
304
        }
305
        $api = $this->api($options['as']);
306
        $projectWithOrg = $this->projectWithOrg($projectWithOrg);
307
308
        list($org, $project) = explode('/', $projectWithOrg, 2);
309
310
        $pullRequests = $api->gitHubAPI()->api('pull_request')->show($org, $project, $number);
311
312
        $result = new PropertyList($pullRequests);
313
        $this->alterPRTables($result);
314
315
        return $result;
316
    }
317
318
    /**
319
     * @command pr:list
320
     * @param $projectWithOrg The project to work on, e.g. org/project
321
     * @filter-output
322
     * @field-labels
323
     *   url: Url
324
     *   id: ID
325
     *   node_id: Node ID
326
     *   html_url: HTML Url
327
     *   diff_url: Diff Url
328
     *   patch_url: Patch Url
329
     *   issue_url: Issue Url
330
     *   number: Number
331
     *   state: State
332
     *   locked: Locked
333
     *   title: Title
334
     *   user: User
335
     *   body: Boday
336
     *   created_at: Created
337
     *   updated_at: Updated
338
     *   closed_at: Closed
339
     *   merged_at: Merged
340
     *   merge_commit_sha: Merge Commit
341
     *   assignee: Assignee
342
     *   assignees: Assignees
343
     *   requested_reviewers: Requested Reviewers
344
     *   requested_teams: Requested Teams
345
     *   labels: Labels
346
     *   milestone: Milestone
347
     *   commits_url: Commit Url
348
     *   review_comments_url: Review Comments Url
349
     *   review_comment_url: Review Comment Url
350
     *   comments_url: Comments Url
351
     *   statuses_url: Statuses Url
352
     *   head: Head
353
     *   base: Base
354
     *   _links: Links
355
     * @default-fields number,user,title
356
     * @default-string-field number
357
     * @return Consolidation\OutputFormatters\StructuredData\RowsOfFields
358
     */
359
    public function prList($projectWithOrg = '', $options = ['state' => 'open', 'as' => 'default', 'format' => 'table'])
360
    {
361
        $api = $this->api($options['as']);
362
        $projectWithOrg = $this->projectWithOrg($projectWithOrg);
363
364
        list($org, $project) = explode('/', $projectWithOrg, 2);
365
366
        $pullRequests = $api->gitHubAPI()->api('pull_request')->all($org, $project, ['state' => $options['state']]);
367
368
        $pullRequests = $this->keyById($pullRequests, 'number');
369
370
        $result = new RowsOfFields($pullRequests);
371
        $this->alterPRTables($result);
372
373
        return $result;
374
    }
375
376
    protected function alterPRTables($data)
377
    {
378
        $data->addRendererFunction(
379
            function ($key, $cellData, FormatterOptions $options, $rowData) {
0 ignored issues
show
Unused Code introduced by
The parameter $options is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $rowData is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
380
                if (is_array($cellData)) {
381
                    if (empty($cellData)) {
382
                        return '';
383
                    }
384
                    foreach (['login', 'label'] as $k) {
385
                        if (isset($cellData[$k])) {
386
                            return $cellData[$k];
387
                        }
388
                    }
389
                    // TODO: simplify
390
                    //   assignees
391
                    //   requested_reviewers
392
                    //   requested_teams
393
                    //   labels
394
                    //   _links
395
                    return json_encode($cellData, true);
396
                }
397
                if (!is_string($cellData)) {
398
                    return var_export($cellData, true);
399
                }
400
                return $cellData;
401
            }
402
        );
403
    }
404
405
    protected function keyById($data, $field)
406
    {
407
        return
408
            array_column(
409
                array_map(
410
                    function ($k) use($data, $field) {
411
                        return [$data[$k][$field], $data[$k]];
412
                    },
413
                    array_keys($data)
414
                ),
415
                1, 0
416
            );
417
    }
418
419
    /**
420
     * @hook alter @filter-output
421
     * @option $filter Filter output based on provided expression
422
     * @default $filter ''
423
     */
424
    public function filterOutput($result, CommandData $commandData)
425
    {
426
        $expr = $commandData->input()->getOption('filter');
427
        if (!empty($expr)) {
428
            $factory = LogicalOpFactory::get();
429
            $op = $factory->evaluate($expr);
430
            $filter = new FilterOutputData();
431
            $result = $filter->filter($result, $op);
432
        }
433
434
        return $result;
435
    }
436
437
    protected function api($as = 'default')
438
    {
439
        $api = new HubphAPI($this->getConfig());
440
        $api->setAs($as);
441
442
        return $api;
443
    }
444
}
445