Skip to content

Commit ba31403

Browse files
committed
プロジェクトを別プロジェクトにコピーできるように
1 parent 8e4eb29 commit ba31403

File tree

10 files changed

+551
-0
lines changed

10 files changed

+551
-0
lines changed

src/helpers/array_utils.php

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
namespace Takuya\Utils;
4+
5+
if ( !function_exists( __NAMESPACE__.'\array_map_with_key' ) ) {
6+
function array_map_with_key (array $array, callable $callable): array {
7+
return array_combine(array_keys($array),array_values(array_map($callable,array_keys($array),array_values($array))));
8+
}
9+
}
10+
if ( !function_exists( __NAMESPACE__.'\array_map_on_keys' ) ) {
11+
function array_map_on_keys (array $keys,array $array, callable $callable): array {
12+
return array_combine(array_keys($array),array_map(fn($k,$v)=> in_array($k,$keys)? $callable($v):$v,array_keys($array),array_values($array)));
13+
}
14+
}
15+
if ( !function_exists( __NAMESPACE__.'\array_select' ) ) {
16+
function array_select (array $keys_to_select,array $array): array {
17+
return array_filter( $array, fn( $k ) => in_array( $k, $keys_to_select ), ARRAY_FILTER_USE_KEY );
18+
}
19+
}
20+
if ( !function_exists( __NAMESPACE__.'\array_subtract' ) ) {
21+
function array_subtract (array $minuend,array $subtrahend): array {
22+
return array_values(array_udiff( $minuend, $subtrahend, function( $x, $y ) {
23+
return ( $x == $y )?0:-1;
24+
} ));
25+
}
26+
}
27+
if ( !function_exists( __NAMESPACE__.'\array_column_select' ) ) {
28+
function array_column_select (array $column_names, array $array_of_array): array {
29+
return array_map(fn($e)=>array_select( $column_names,$e), $array_of_array );
30+
}
31+
}
32+
if ( !function_exists( __NAMESPACE__.'\array_each_with_key' ) ) {// foreach 同等
33+
function array_each_with_key (array $array, callable $function): void {
34+
foreach ( $array as $key => $value ) {
35+
$function($key,$value);
36+
}
37+
}
38+
}
39+
40+
41+
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
namespace Takuya\BacklogApiClient\Backup\Copy;
4+
5+
use Takuya\BacklogApiClient\BacklogAPIClient;
6+
use Takuya\BacklogApiClient\Backup\Copy\Traits\CopyProject;
7+
use Takuya\BacklogApiClient\Backup\Copy\Traits\CopyIssue;
8+
9+
class BacklogCopy {
10+
11+
use CopyProject;
12+
use CopyIssue;
13+
14+
protected array $id_mapping;
15+
protected BacklogAPIClient $src_cli;
16+
protected BacklogAPIClient $dst_cli;
17+
18+
public function __construct ( BacklogAPIClient $api_client, $dst_space=null, ) {
19+
$this->src_cli = $api_client;
20+
$this->dst_cli = $this->dst_space ?? $api_client;
21+
}
22+
23+
24+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
3+
namespace Takuya\BacklogApiClient\Backup\Copy\Traits;
4+
5+
trait CopyComment {
6+
7+
public function copyCommentList ( $src_issue, $dst_issue ) {
8+
$comments = $this->src_cli->getCommentList( $src_issue->id );
9+
10+
// TODO 編集を再現するためには、ChageLogを考慮する必要がある。めんどくさすぎる。
11+
$ids = [];
12+
foreach ( array_filter( $comments, fn( $c ) => $c->content ) as $comment ) {
13+
$ids[] = $this->copyCommentContent( $comment, $dst_issue->id, !empty( $comment->stars ) );
14+
}
15+
return $ids;
16+
}
17+
18+
public function copyCommentContent ( $src_comment, $dst_issue_id, $stared = false, $append_user = true ) {
19+
if ( $append_user ) {
20+
$src_comment = $this->addUserInfoIntoCommentHead( $src_comment );
21+
}
22+
23+
24+
$ret = $this->dst_cli->addComment( $dst_issue_id, ['content' => $src_comment->content] );
25+
if ( $stared ) {//☆がついてたら星を入れる。正確にやるには全ユーザーのアカウントに切り替える必要がある。
26+
$params = ['commentId' => $ret->id];
27+
$this->dst_cli->addStar( $params );
28+
}
29+
return $ret;
30+
}
31+
32+
public function addUserInfoIntoCommentHead ( $comment ) {
33+
$creator = $comment->createdUser->name;
34+
$created = substr( $comment->created, 0, 10 ).' '.substr( $comment->created, 11, 5 );
35+
// 失われる情報を本文に残す
36+
$header = sprintf( <<<EOS
37+
%s / %s
38+
----
39+
40+
EOS, $creator, $created, );
41+
42+
$comment->content = $header.$comment->content;
43+
return $comment;
44+
}
45+
46+
}
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
<?php
2+
3+
namespace Takuya\BacklogApiClient\Backup\Copy\Traits;
4+
5+
use function Takuya\Utils\array_each_with_key;
6+
7+
trait CopyIssue {
8+
9+
use CopyComment;
10+
11+
public function copyIssueList($src_project_id,$dst_project_id){
12+
foreach ( $this->src_cli->issue_ids($src_project_id) as $issue_id ) {
13+
$this->copyIssue($issue_id,$dst_project_id);
14+
}
15+
}
16+
public function copyIssue ( $src_issue_id, $dst_project_id ) {
17+
// ただし、コメント・課題の作成者はAPIの制限で変更が不可能。
18+
$src_issue = $this->src_cli->getIssue( $src_issue_id );
19+
// ユーザー・種別・マイルストーンを一致させる。
20+
$file_ids = $this->getIdMapping( 'sharedFiles' );
21+
$user_ids = $this->getIdMapping( 'userIds' );
22+
$type_ids = $this->getIdMapping( 'typeIds' );
23+
$version_ids = $this->getIdMapping( 'versionIds' );
24+
25+
26+
// 課題をコピー
27+
$data = $this->formatIssue( $src_issue );
28+
$data = $this->remapiId( $data, 'issueTypeId', $type_ids );
29+
$data = $this->remapiId( $data, 'versionsId', $version_ids );
30+
$data['projectId'] = $dst_project_id;
31+
$dst_issue = $this->dst_cli->addIssue( $data );
32+
// 共有ファイルをリンクし直し
33+
$this->copyLinkSharedFiles( $src_issue, $dst_issue, $file_ids );
34+
// 添付ファイルをコピー
35+
$this->copyIssueAttachments( $src_issue, $dst_issue );
36+
// 課題の状態を更新して合わせる。
37+
$dst_issue = $this->updateIssueAttributes( $src_issue, $dst_issue );
38+
// コメントをコピーする
39+
$this->copyCommentList( $src_issue, $dst_issue );
40+
// スターをコピー
41+
42+
43+
return $dst_issue;
44+
}
45+
46+
public function getIdMapping ( $name ) {
47+
// TODO 変数依存を切る。メソッドを作ってメソッド内部で、API取得して比較する。
48+
return $this->id_mapping[$name];
49+
}
50+
51+
protected function formatIssue ( object $issue_api_result, $add_user_name = true,$assignee ='' ) {
52+
if ( $add_user_name ) {
53+
$issue_api_result = $this->addUserInfoIntoBody( $issue_api_result );
54+
}
55+
56+
$issue = (array)$issue_api_result;
57+
$keys = [
58+
//'id',
59+
//"issueKey",
60+
//"keyId",
61+
"summary",
62+
//"parentIssueId",//todo
63+
"description",
64+
"startDate",
65+
"dueDate",
66+
"estimatedHours",
67+
"actualHours",
68+
"issueType",
69+
"category",
70+
"versions",
71+
"priority",
72+
"assignee",// ユーザIDが不一致になる可能性がある。
73+
//"resolution", //
74+
//"status", //
75+
//"milestone",//
76+
//"createdUser",
77+
//"created",
78+
//"updatedUser",
79+
//"updated",
80+
// TODO parentIssueId をどうするか。
81+
// TODO attachment, customField
82+
];
83+
$map_entry = [
84+
"assignee" => fn( $e ) => $e['id'] ?? $e,
85+
'category' => fn( $x ) => array_map( fn( $e ) => $e['id'], $x ),
86+
'issueType' => fn( $e ) => $e['id'] ?? $e,
87+
'versions' => fn( $x ) => array_map( fn( $e ) => $e['id'], $x ),
88+
'priority' => fn( $e ) => $e['id'] ?? $e,
89+
'startDate' => fn( $e ) => substr( $e, 0, 10 ),
90+
'dueDate' => fn( $e ) => substr( $e, 0, 10 ),
91+
];
92+
$map_key = [
93+
"assignee" => "assigneeId",
94+
'category' => 'categoryId',
95+
'issueType' => 'issueTypeId',
96+
'versions' => 'versionsId',
97+
'priority' => 'priorityId',
98+
];
99+
$issue = json_decode( json_encode( $issue ), JSON_OBJECT_AS_ARRAY );
100+
$issue = array_filter( $issue, fn( $k ) => in_array( $k, $keys ), ARRAY_FILTER_USE_KEY );
101+
array_each_with_key( $map_entry, function( $k, $f ) use ( &$issue ) { $issue[$k] = $f( $issue[$k] ); } );
102+
array_each_with_key( $map_key, function( $old, $new ) use ( &$issue ) {
103+
$issue[$new] = $issue[$old];
104+
unset( $issue[$old] );
105+
} );
106+
if(!empty($assignee)){
107+
$issue->assignee = $assignee;
108+
}
109+
return $issue;
110+
}
111+
112+
protected function addUserInfoIntoBody ( object $issue_api_result ) {
113+
$issue = $issue_api_result;
114+
$creator = $issue->createdUser->name;
115+
$created = substr( $issue->created, 0, 10 ).' '.substr( $issue->created, 11, 5 );
116+
$updator = $issue->updatedUser->name;
117+
$updated = substr( $issue->updated, 0, 10 ).' '.substr( $issue->created, 11, 5 );
118+
// 失われる情報を本文に残す
119+
$footer = sprintf( <<<EOS
120+
121+
122+
123+
----
124+
作成 ( %s | %s )
125+
更新 ( %s | %s )
126+
127+
EOS, $creator, $created, $updator, $updated );
128+
129+
$issue->description = $issue->description.$footer;
130+
return $issue;
131+
}
132+
133+
protected function remapiId ( $data, $key, $mapping ) {
134+
if ( !is_array( $data[$key] ) ) {
135+
$data[$key] = $mapping[$data[$key]];
136+
}
137+
if ( is_array( $data[$key] ) ) {
138+
foreach ( $mapping as $old_id => $new_id ) {
139+
foreach ( $data[$key] as $idx => $value ) {
140+
if ( $value == $old_id ) {
141+
$data[$key][$idx] = $new_id;
142+
}
143+
}
144+
}
145+
}
146+
return $data;
147+
}
148+
149+
protected function copyLinkSharedFiles ( $src_issue, $dst_issue, $file_ids ) {
150+
if ( empty( $src_issue->sharedFiles ) ) {
151+
return;
152+
}
153+
$ids = array_map( fn( $e ) => $e->id, $src_issue->sharedFiles );
154+
foreach ( $ids as $old_id ) {
155+
$new_id = $file_ids[$old_id];
156+
$this->dst_cli->linkSharedFilesToIssue( $dst_issue->id, ['fileId' => [$new_id]] );
157+
}
158+
}
159+
160+
protected function copyIssueAttachments ( $src_issue, $dst_issue ) {
161+
if ( empty( $src_issue->attachments ) ) {
162+
return [];
163+
}
164+
$mapping = [];
165+
foreach ( $src_issue->attachments as $src_attachment ) {
166+
$part = [
167+
'name' => "file",
168+
'contents' => $this->src_cli->getIssueAttachment( $src_issue->id, $src_attachment->id ),
169+
"filename" => $src_attachment->name,
170+
];
171+
$param = ['multipart' => [$part]];
172+
$result = $this->dst_cli->postAttachmentFile( $param );
173+
$mapping[$src_attachment->id] = $result->id;
174+
}
175+
$params = ['attachmentId' => array_values( $mapping )];
176+
$this->dst_cli->updateIssue( $dst_issue->id, $params );
177+
return $mapping;
178+
}
179+
180+
protected function updateIssueAttributes ( object $src_issue, object $dst_issue ) {
181+
$params = [];
182+
if ( $src_issue->status->id != $dst_issue->status->id ) {
183+
$params['statusId'] = $src_issue->status->id;
184+
}
185+
if ( $src_issue->resolution?->id != $dst_issue->resolution?->id ) {
186+
$params['resolution'] = $src_issue->resolution->id;
187+
}
188+
189+
return !empty( $params ) ? $this->dst_cli->updateIssue( $dst_issue->id, $params ) : $dst_issue;
190+
}
191+
192+
193+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
namespace Takuya\BacklogApiClient\Backup\Copy\Traits;
4+
5+
use function Takuya\Utils\array_select;
6+
7+
trait CopyIssueType {
8+
9+
10+
public function copyIssueType ( $src_project_id, $dst_project_id ) {
11+
$keys = ["name", "color", "templateSummary", "templateDescription"];
12+
$getIssueTypeByUniq = function( $project_id, $api ) use ( $keys ) {
13+
$types = $api->getIssueTypeList( $project_id );
14+
$a = array_map( fn( $e ) => (array)$e, $types );
15+
// 名前・配色・テンプレートでユニークキーを取る。
16+
$a = array_combine( array_map( fn( $e ) => md5( join( '', array_select( $keys, $e ) ) ), $a ), $a );
17+
return $a;
18+
};
19+
//
20+
$src_types = $getIssueTypeByUniq( $src_project_id, $this->src_cli );
21+
$dst_types = $getIssueTypeByUniq( $dst_project_id, $this->dst_cli );
22+
// ユニークキーを比較し、元プロジェクトにないものを追加する。
23+
$diff_types = array_diff_key( $src_types, $dst_types );
24+
25+
foreach ( $diff_types as $uniq => $item ) {
26+
$dst_types[$uniq] = (array)$this->dst_cli->addIssueType( $dst_project_id, array_select( $keys, $item ) );
27+
}
28+
$src_dst_id_mapping = [];//
29+
foreach ( array_keys( $src_types ) as $uniq ) {
30+
$x = $src_types[$uniq]['id'];
31+
$y = $dst_types[$uniq]['id'];
32+
$src_dst_id_mapping[$x] = $y;
33+
}
34+
// 旧=>新 のIssueTypeIDマッピングを返す.
35+
return $src_dst_id_mapping;
36+
}
37+
38+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
namespace Takuya\BacklogApiClient\Backup\Copy\Traits;
4+
5+
use function Takuya\Utils\array_select;
6+
7+
trait CopyMilestone {
8+
9+
public function copyProjectMileStone ( $src_project_id, $dst_project_id ) {
10+
$keys = ["name", "description", "startDate", "releaseDueDate"];
11+
$getIssueTypeByUniq = function( $project_id, $api) use ( $keys ) {
12+
$ret = $api->getVersionMilestoneList( $project_id );
13+
$a = array_map( fn( $e ) => (array)$e, $ret );
14+
$a = array_combine( array_map( fn( $e ) => md5( join( '', array_select( $keys, $e ) ) ), $a ), $a );
15+
return $a;
16+
};
17+
$src = $getIssueTypeByUniq( $src_project_id, $this->src_cli );
18+
$dst = $getIssueTypeByUniq( $dst_project_id, $this->dst_cli );
19+
$diff = array_diff_key( $src, $dst );
20+
foreach ( $diff as $uniq => $item ) {
21+
$dst[$uniq] = (array)$this->dst_cli->addVersionMilestone( $dst_project_id, array_select( $keys, $item ) );
22+
}
23+
foreach ( array_keys( $src ) as $uniq ) {
24+
$x = $src[$uniq]['id'];
25+
$y = $dst[$uniq]['id'];
26+
$src_dst_id_mapping[$x] = $y;
27+
}
28+
// 旧=>新IDマッピングを返す.
29+
return $src_dst_id_mapping;
30+
}
31+
}

0 commit comments

Comments
 (0)