1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* This file is part of the Jira-CLI library. |
4
|
|
|
* For the full copyright and license information, please view |
5
|
|
|
* the LICENSE file that was distributed with this source code. |
6
|
|
|
* |
7
|
|
|
* @copyright Alexander Obuhovich <[email protected]> |
8
|
|
|
* @link https://github.com/console-helpers/jira-cli |
9
|
|
|
*/ |
10
|
|
|
|
11
|
|
|
namespace ConsoleHelpers\JiraCLI\Issue; |
12
|
|
|
|
13
|
|
|
|
14
|
|
|
use chobie\Jira\Issue; |
15
|
|
|
use chobie\Jira\Issues\Walker; |
16
|
|
|
use ConsoleHelpers\JiraCLI\JiraApi; |
17
|
|
|
|
18
|
|
|
class IssueCloner |
19
|
|
|
{ |
20
|
|
|
|
21
|
|
|
const LINK_DIRECTION_INWARD = 1; |
22
|
|
|
|
23
|
|
|
const LINK_DIRECTION_OUTWARD = 2; |
24
|
|
|
|
25
|
|
|
/** |
26
|
|
|
* Jira REST client. |
27
|
|
|
* |
28
|
|
|
* @var JiraApi |
29
|
|
|
*/ |
30
|
|
|
protected $jiraApi; |
31
|
|
|
|
32
|
|
|
/** |
33
|
|
|
* Specifies custom fields to copy during backporting. |
34
|
|
|
* |
35
|
|
|
* @var array |
36
|
|
|
*/ |
37
|
|
|
private $_copyCustomFields = array( |
38
|
|
|
'Change Log Group', 'Change Log Message', |
39
|
|
|
); |
40
|
|
|
|
41
|
|
|
/** |
42
|
|
|
* Custom fields map. |
43
|
|
|
* |
44
|
|
|
* @var array |
45
|
|
|
*/ |
46
|
|
|
private $_customFieldsMap = array(); |
47
|
|
|
|
48
|
|
|
/** |
49
|
|
|
* Fields to query during issue search. |
50
|
|
|
* |
51
|
|
|
* @var array |
52
|
|
|
*/ |
53
|
|
|
protected $queryFields = array('summary', 'issuelinks'); |
54
|
|
|
|
55
|
|
|
/** |
56
|
|
|
* Project of an issue. |
57
|
|
|
* |
58
|
|
|
* @var array |
59
|
|
|
*/ |
60
|
|
|
private $_issueProjects = array(); |
61
|
|
|
|
62
|
|
|
/** |
63
|
|
|
* IssueCloner constructor. |
64
|
|
|
* |
65
|
|
|
* @param JiraApi $jira_api Jira REST client. |
66
|
|
|
*/ |
67
|
2 |
|
public function __construct(JiraApi $jira_api) |
68
|
|
|
{ |
69
|
2 |
|
$this->jiraApi = $jira_api; |
70
|
|
|
|
71
|
2 |
|
$this->jiraApi->setOptions(0); // Don't expand fields. |
72
|
|
|
} |
73
|
|
|
|
74
|
|
|
/** |
75
|
|
|
* Returns issues. |
76
|
|
|
* |
77
|
|
|
* @param string $jql JQL. |
78
|
|
|
* @param string $link_name Link name. |
79
|
|
|
* @param integer $link_direction Link direction. |
80
|
|
|
* @param array $link_project_keys Link project keys. |
81
|
|
|
* |
82
|
|
|
* @return array |
83
|
|
|
*/ |
84
|
|
|
public function getIssues($jql, $link_name, $link_direction, array $link_project_keys) |
85
|
|
|
{ |
86
|
|
|
$this->_buildCustomFieldsMap(); |
87
|
|
|
|
88
|
|
|
$walker = new Walker($this->jiraApi); |
89
|
|
|
$walker->push($jql, implode(',', $this->_getQueryFields())); |
90
|
|
|
|
91
|
|
|
$cache = array(); |
92
|
|
|
|
93
|
|
|
foreach ( $walker as $issue ) { |
94
|
|
|
foreach ( $link_project_keys as $link_project_key ) { |
95
|
|
|
$linked_issue = $this->_getLinkedIssue($issue, $link_name, $link_direction); |
96
|
|
|
|
97
|
|
|
if ( $linked_issue !== null ) { |
98
|
|
|
$this->_addToProjectDetectionQueue($linked_issue); |
99
|
|
|
} |
100
|
|
|
|
101
|
|
|
$cache[] = array($issue, $linked_issue, $link_project_key); |
102
|
|
|
} |
103
|
|
|
} |
104
|
|
|
|
105
|
|
|
$this->_processProjectDetectionQueue(); |
106
|
|
|
|
107
|
|
|
$ret = array(); |
108
|
|
|
|
109
|
|
|
foreach ( $cache as $cached_data ) { |
110
|
|
|
list($issue, $linked_issue, $link_project_key) = $cached_data; |
111
|
|
|
|
112
|
|
|
if ( $linked_issue === null ) { |
113
|
|
|
$ret[] = $cached_data; |
114
|
|
|
continue; |
115
|
|
|
} |
116
|
|
|
|
117
|
|
|
if ( $this->isLinkAccepted($issue, $linked_issue) |
118
|
|
|
&& $this->_getIssueProject($linked_issue) === $link_project_key |
119
|
|
|
&& !$this->isAlreadyProcessed($issue, $linked_issue) |
120
|
|
|
) { |
121
|
|
|
$ret[] = $cached_data; |
122
|
|
|
} |
123
|
|
|
} |
124
|
|
|
|
125
|
|
|
return $ret; |
126
|
|
|
} |
127
|
|
|
|
128
|
|
|
/** |
129
|
|
|
* Adds an issue to the project detection queue. |
130
|
|
|
* |
131
|
|
|
* @param Issue $issue Issue. |
132
|
|
|
* |
133
|
|
|
* @return void |
134
|
|
|
*/ |
135
|
|
|
private function _addToProjectDetectionQueue(Issue $issue) |
136
|
|
|
{ |
137
|
|
|
$this->_issueProjects[$issue->getKey()] = null; |
138
|
|
|
} |
139
|
|
|
|
140
|
|
|
/** |
141
|
|
|
* Detects projects for queued issues. |
142
|
|
|
* |
143
|
|
|
* @return void |
144
|
|
|
*/ |
145
|
|
|
private function _processProjectDetectionQueue() |
146
|
|
|
{ |
147
|
|
|
$issues_without_projects = array_keys(array_filter($this->_issueProjects, function ($project_key) { |
148
|
|
|
return $project_key === null; |
149
|
|
|
})); |
150
|
|
|
|
151
|
|
|
if ( !$issues_without_projects ) { |
|
|
|
|
152
|
|
|
return; |
153
|
|
|
} |
154
|
|
|
|
155
|
|
|
$walker = new Walker($this->jiraApi); |
156
|
|
|
$walker->push( |
157
|
|
|
'key IN (' . implode(',', $issues_without_projects) . ')', |
158
|
|
|
'project' |
159
|
|
|
); |
160
|
|
|
|
161
|
|
|
foreach ( $walker as $linked_issue ) { |
162
|
|
|
$linked_issue_project_data = $linked_issue->get('project'); |
163
|
|
|
$this->_issueProjects[$linked_issue->getKey()] = $linked_issue_project_data['key']; |
164
|
|
|
} |
165
|
|
|
} |
166
|
|
|
|
167
|
|
|
/** |
168
|
|
|
* Returns an issue project. |
169
|
|
|
* |
170
|
|
|
* @param Issue $issue Issue. |
171
|
|
|
* |
172
|
|
|
* @return string |
173
|
|
|
* @throws \RuntimeException When issue's project isn't available. |
174
|
|
|
*/ |
175
|
|
|
private function _getIssueProject(Issue $issue) |
176
|
|
|
{ |
177
|
|
|
$issue_key = $issue->getKey(); |
178
|
|
|
|
179
|
|
|
if ( array_key_exists($issue_key, $this->_issueProjects) ) { |
180
|
|
|
return $this->_issueProjects[$issue_key]; |
181
|
|
|
} |
182
|
|
|
|
183
|
|
|
throw new \RuntimeException('The issue "' . $issue_key . '" project wasn\'t queried.'); |
184
|
|
|
} |
185
|
|
|
|
186
|
|
|
/** |
187
|
|
|
* Builds custom field map. |
188
|
|
|
* |
189
|
|
|
* @return void |
190
|
|
|
*/ |
191
|
|
|
private function _buildCustomFieldsMap() |
192
|
|
|
{ |
193
|
|
|
foreach ( $this->jiraApi->getFields() as $field_key => $field_data ) { |
194
|
|
|
if ( substr($field_key, 0, 12) === 'customfield_' ) { |
195
|
|
|
$this->_customFieldsMap[$field_data['name']] = $field_key; |
196
|
|
|
} |
197
|
|
|
} |
198
|
|
|
} |
199
|
|
|
|
200
|
|
|
/** |
201
|
|
|
* Returns query fields. |
202
|
|
|
* |
203
|
|
|
* @return array |
204
|
|
|
*/ |
205
|
|
|
private function _getQueryFields() |
206
|
|
|
{ |
207
|
|
|
$ret = $this->queryFields; |
208
|
|
|
|
209
|
|
|
foreach ( $this->_copyCustomFields as $custom_field ) { |
210
|
|
|
if ( isset($this->_customFieldsMap[$custom_field]) ) { |
211
|
|
|
$ret[] = $this->_customFieldsMap[$custom_field]; |
212
|
|
|
} |
213
|
|
|
} |
214
|
|
|
|
215
|
|
|
return $ret; |
216
|
|
|
} |
217
|
|
|
|
218
|
|
|
/** |
219
|
|
|
* Returns issue, which backports given issue (project not matched yet). |
220
|
|
|
* |
221
|
|
|
* @param Issue $issue Issue. |
222
|
|
|
* @param string $link_name Link name. |
223
|
|
|
* @param integer $link_direction Link direction. |
224
|
|
|
* |
225
|
|
|
* @return Issue|null |
226
|
|
|
* @throws \InvalidArgumentException When link direction isn't valid. |
227
|
|
|
*/ |
228
|
|
|
private function _getLinkedIssue(Issue $issue, $link_name, $link_direction) |
229
|
|
|
{ |
230
|
|
|
foreach ( $issue->get('issuelinks') as $issue_link ) { |
231
|
|
|
if ( $issue_link['type']['name'] !== $link_name ) { |
232
|
|
|
continue; |
233
|
|
|
} |
234
|
|
|
|
235
|
|
|
if ( $link_direction === self::LINK_DIRECTION_INWARD ) { |
236
|
|
|
$check_key = 'inwardIssue'; |
237
|
|
|
} |
238
|
|
|
elseif ( $link_direction === self::LINK_DIRECTION_OUTWARD ) { |
239
|
|
|
$check_key = 'outwardIssue'; |
240
|
|
|
} |
241
|
|
|
else { |
242
|
|
|
throw new \InvalidArgumentException('The "' . $link_direction . '" link direction isn\'t valid.'); |
243
|
|
|
} |
244
|
|
|
|
245
|
|
|
if ( array_key_exists($check_key, $issue_link) ) { |
246
|
|
|
return new Issue($issue_link[$check_key]); |
247
|
|
|
} |
248
|
|
|
} |
249
|
|
|
|
250
|
|
|
return null; |
251
|
|
|
} |
252
|
|
|
|
253
|
|
|
/** |
254
|
|
|
* Creates backports issues. |
255
|
|
|
* |
256
|
|
|
* @param Issue $issue Issue. |
257
|
|
|
* @param string $project_key Project key. |
258
|
|
|
* @param string $link_name Link name. |
259
|
|
|
* @param integer $link_direction Link direction. |
260
|
|
|
* @param array $component_ids Component IDs. |
261
|
|
|
* |
262
|
|
|
* @return string |
263
|
|
|
* @throws \RuntimeException When failed to create an issue. |
264
|
|
|
* @throws \InvalidArgumentException When link direction isn't valid. |
265
|
|
|
*/ |
266
|
|
|
public function createLinkedIssue(Issue $issue, $project_key, $link_name, $link_direction, array $component_ids) |
267
|
|
|
{ |
268
|
|
|
$create_fields = array( |
269
|
|
|
'description' => 'See ' . $issue->getKey() . '.', |
270
|
|
|
'components' => array(), |
271
|
|
|
); |
272
|
|
|
|
273
|
|
|
foreach ( $this->_copyCustomFields as $custom_field ) { |
274
|
|
|
if ( isset($this->_customFieldsMap[$custom_field]) ) { |
275
|
|
|
$custom_field_id = $this->_customFieldsMap[$custom_field]; |
276
|
|
|
$create_fields[$custom_field_id] = $this->getIssueCustomField($issue, $custom_field_id); |
277
|
|
|
} |
278
|
|
|
} |
279
|
|
|
|
280
|
|
|
foreach ( $component_ids as $component_id ) { |
281
|
|
|
$create_fields['components'][] = array('id' => (string)$component_id); |
282
|
|
|
} |
283
|
|
|
|
284
|
|
|
$create_issue_result = $this->jiraApi->createIssue( |
285
|
|
|
$project_key, |
286
|
|
|
$issue->get('summary'), |
|
|
|
|
287
|
|
|
$this->getChangelogEntryIssueTypeId(), |
288
|
|
|
$create_fields |
289
|
|
|
); |
290
|
|
|
|
291
|
|
|
$raw_create_issue_result = $create_issue_result->getResult(); |
292
|
|
|
|
293
|
|
|
if ( array_key_exists('errors', $raw_create_issue_result) ) { |
294
|
|
|
throw new \RuntimeException(sprintf( |
295
|
|
|
'Failed to create linked issue for "%s" issue. Errors: ' . PHP_EOL . '%s', |
296
|
|
|
$issue->getKey(), |
297
|
|
|
print_r($raw_create_issue_result['errors'], true) |
|
|
|
|
298
|
|
|
)); |
299
|
|
|
} |
300
|
|
|
|
301
|
|
|
if ( $link_direction === self::LINK_DIRECTION_INWARD ) { |
302
|
|
|
$issue_link_result = $this->jiraApi->api( |
|
|
|
|
303
|
|
|
JiraApi::REQUEST_POST, |
304
|
|
|
'/rest/api/2/issueLink', |
305
|
|
|
array( |
306
|
|
|
'type' => array('name' => $link_name), |
307
|
|
|
'inwardIssue' => array('key' => $raw_create_issue_result['key']), |
308
|
|
|
'outwardIssue' => array('key' => $issue->getKey()), |
309
|
|
|
) |
310
|
|
|
); |
311
|
|
|
} |
312
|
|
|
elseif ( $link_direction === self::LINK_DIRECTION_OUTWARD ) { |
313
|
|
|
$issue_link_result = $this->jiraApi->api( |
314
|
|
|
JiraApi::REQUEST_POST, |
315
|
|
|
'/rest/api/2/issueLink', |
316
|
|
|
array( |
317
|
|
|
'type' => array('name' => $link_name), |
318
|
|
|
'inwardIssue' => array('key' => $issue->getKey()), |
319
|
|
|
'outwardIssue' => array('key' => $raw_create_issue_result['key']), |
320
|
|
|
) |
321
|
|
|
); |
322
|
|
|
} |
323
|
|
|
else { |
324
|
|
|
throw new \InvalidArgumentException('The "' . $link_direction . '" link direction isn\'t valid.'); |
325
|
|
|
} |
326
|
|
|
|
327
|
|
|
return $raw_create_issue_result['key']; |
328
|
|
|
} |
329
|
|
|
|
330
|
|
|
/** |
331
|
|
|
* Determines if link was already processed. |
332
|
|
|
* |
333
|
|
|
* @param Issue $issue Issue. |
334
|
|
|
* @param Issue $linked_issue Linked issue. |
335
|
|
|
* |
336
|
|
|
* @return boolean |
337
|
|
|
*/ |
338
|
|
|
protected function isAlreadyProcessed(Issue $issue, Issue $linked_issue) |
|
|
|
|
339
|
|
|
{ |
340
|
|
|
return false; |
341
|
|
|
} |
342
|
|
|
|
343
|
|
|
/** |
344
|
|
|
* Determines if link is accepted. |
345
|
|
|
* |
346
|
|
|
* @param Issue $issue Issue. |
347
|
|
|
* @param Issue $linked_issue Linked issue. |
348
|
|
|
* |
349
|
|
|
* @return boolean |
350
|
|
|
*/ |
351
|
|
|
protected function isLinkAccepted(Issue $issue, Issue $linked_issue) |
|
|
|
|
352
|
|
|
{ |
353
|
|
|
return true; |
354
|
|
|
} |
355
|
|
|
|
356
|
|
|
/** |
357
|
|
|
* Returns ID of "Changelog Entry" issue type. |
358
|
|
|
* |
359
|
|
|
* @return integer |
360
|
|
|
* @throws \LogicException When "Changelog Entry" issue type wasn't found. |
361
|
|
|
*/ |
362
|
|
|
protected function getChangelogEntryIssueTypeId() |
363
|
|
|
{ |
364
|
|
|
static $issue_type_id; |
365
|
|
|
|
366
|
|
|
if ( !isset($issue_type_id) ) { |
367
|
|
|
foreach ( $this->jiraApi->getIssueTypes() as $issue_type ) { |
368
|
|
|
if ( $issue_type->getName() === 'Changelog Entry' ) { |
369
|
|
|
$issue_type_id = $issue_type->getId(); |
370
|
|
|
break; |
371
|
|
|
} |
372
|
|
|
} |
373
|
|
|
|
374
|
|
|
if ( !isset($issue_type_id) ) { |
375
|
|
|
throw new \LogicException('The "Changelog Entry" issue type not found.'); |
376
|
|
|
} |
377
|
|
|
} |
378
|
|
|
|
379
|
|
|
return $issue_type_id; |
380
|
|
|
} |
381
|
|
|
|
382
|
|
|
/** |
383
|
|
|
* Returns custom field value. |
384
|
|
|
* |
385
|
|
|
* @param Issue $issue Issue. |
386
|
|
|
* @param string $custom_field_id Custom field ID. |
387
|
|
|
* |
388
|
|
|
* @return mixed |
389
|
|
|
*/ |
390
|
|
|
protected function getIssueCustomField(Issue $issue, $custom_field_id) |
391
|
|
|
{ |
392
|
|
|
$custom_field_data = $issue->get($custom_field_id); |
393
|
|
|
|
394
|
|
|
if ( is_array($custom_field_data) ) { |
|
|
|
|
395
|
|
|
return array('value' => $custom_field_data['value']); |
396
|
|
|
} |
397
|
|
|
|
398
|
|
|
return $custom_field_data; |
399
|
|
|
} |
400
|
|
|
|
401
|
|
|
/** |
402
|
|
|
* Returns issue status name. |
403
|
|
|
* |
404
|
|
|
* @param Issue $issue Issue. |
405
|
|
|
* |
406
|
|
|
* @return string |
407
|
|
|
*/ |
408
|
|
|
public function getIssueStatusName(Issue $issue) |
409
|
|
|
{ |
410
|
|
|
$status = $issue->get('status'); |
411
|
|
|
|
412
|
|
|
return $status['name']; |
413
|
|
|
} |
414
|
|
|
|
415
|
|
|
} |
416
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.