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'), |
||||
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||||
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) |
||||
0 ignored issues
–
show
It seems like
print_r($raw_create_issue_result['errors'], true) can also be of type true ; however, parameter $values of sprintf() does only seem to accept double|integer|string , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
298 | )); |
||||
299 | } |
||||
300 | |||||
301 | if ( $link_direction === self::LINK_DIRECTION_INWARD ) { |
||||
302 | $issue_link_result = $this->jiraApi->api( |
||||
0 ignored issues
–
show
|
|||||
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) |
||||
0 ignored issues
–
show
The parameter
$linked_issue is not used and could be removed.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check looks for parameters that have been defined for a function or method, but which are not used in the method body. ![]() |
|||||
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) |
||||
0 ignored issues
–
show
The parameter
$linked_issue is not used and could be removed.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check looks for parameters that have been defined for a function or method, but which are not used in the method body. ![]() |
|||||
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) ) { |
||||
0 ignored issues
–
show
|
|||||
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 |