1
|
|
|
<?php |
2
|
|
|
namespace TheCodingMachine\WashingMachine\Gitlab; |
3
|
|
|
use Gitlab\Client; |
4
|
|
|
use Gitlab\ResultPager; |
5
|
|
|
use GuzzleHttp\Psr7\Stream; |
6
|
|
|
use GuzzleHttp\Psr7\StreamWrapper; |
7
|
|
|
use Psr\Log\LoggerInterface; |
8
|
|
|
use Symfony\Component\Filesystem\Filesystem; |
9
|
|
|
|
10
|
|
|
/** |
11
|
|
|
* Class to access different data in Gitlab from the build reference |
12
|
|
|
*/ |
13
|
|
|
class BuildService |
14
|
|
|
{ |
15
|
|
|
/** |
16
|
|
|
* @var Client |
17
|
|
|
*/ |
18
|
|
|
private $client; |
19
|
|
|
/** |
20
|
|
|
* @var LoggerInterface |
21
|
|
|
*/ |
22
|
|
|
private $logger; |
23
|
|
|
|
24
|
|
|
public function __construct(Client $client, LoggerInterface $logger) |
25
|
|
|
{ |
26
|
|
|
$this->client = $client; |
27
|
|
|
$this->logger = $logger; |
28
|
|
|
} |
29
|
|
|
|
30
|
|
|
/** |
31
|
|
|
* @param string $projectName |
32
|
|
|
* @param string $commitSha |
33
|
|
|
* @return array The merge request object |
34
|
|
|
* @throws MergeRequestNotFoundException |
35
|
|
|
*/ |
36
|
|
|
public function findMergeRequestByCommitSha(string $projectName, string $commitSha) : array |
37
|
|
|
{ |
38
|
|
|
// Find in the merge requests (since our build was triggered recently, it should definitely be there) |
39
|
|
|
/*$mergeRequests = $this->client->merge_requests->all($projectName, [ |
|
|
|
|
40
|
|
|
'order_by' => 'updated_at', |
41
|
|
|
'sort' => 'desc' |
42
|
|
|
]);*/ |
43
|
|
|
|
44
|
|
|
$pager = new ResultPager($this->client); |
45
|
|
|
$mergeRequests = $pager->fetch($this->client->api('merge_requests'), 'all', [ |
46
|
|
|
$projectName, [ |
47
|
|
|
'order_by' => 'updated_at', |
48
|
|
|
'sort' => 'desc' |
49
|
|
|
] |
50
|
|
|
]); |
51
|
|
|
do { |
52
|
|
|
$this->logger->debug('Called API, got '.count($mergeRequests).' merge requests'); |
53
|
|
|
foreach ($mergeRequests as $mergeRequest) { |
|
|
|
|
54
|
|
|
// Let's only return this PR if the returned commit is the FIRST one (otherwise, the commit ID is on an outdated version of the PR) |
55
|
|
|
|
56
|
|
|
if ($mergeRequest['sha'] === $commitSha) { |
57
|
|
|
return $mergeRequest; |
58
|
|
|
} |
59
|
|
|
} |
60
|
|
|
|
61
|
|
|
if (!$pager->hasNext()) { |
62
|
|
|
break; |
63
|
|
|
} |
64
|
|
|
$mergeRequests = $pager->fetchNext(); |
65
|
|
|
} while (true); |
66
|
|
|
|
67
|
|
|
throw new MergeRequestNotFoundException('Could not find a PR whose last commit/buildRef ID is '.$commitSha); |
68
|
|
|
} |
69
|
|
|
|
70
|
|
|
public function getLatestCommitIdFromBranch(string $projectName, string $branchName) : string |
71
|
|
|
{ |
72
|
|
|
$branch = $this->client->repositories->branch($projectName, $branchName); |
73
|
|
|
return $branch['commit']['id']; |
74
|
|
|
} |
75
|
|
|
|
76
|
|
|
private $pipelines = []; |
77
|
|
|
|
78
|
|
|
private function getPipelines(string $projectName) : array |
79
|
|
|
{ |
80
|
|
|
if (!isset($this->pipelines[$projectName])) { |
81
|
|
|
$pager = new ResultPager($this->client); |
82
|
|
|
$this->pipelines[$projectName] = $pager->fetchAll($this->client->api('projects'), 'pipelines', |
83
|
|
|
[ $projectName ] |
84
|
|
|
); |
85
|
|
|
} |
86
|
|
|
return $this->pipelines[$projectName]; |
87
|
|
|
} |
88
|
|
|
|
89
|
|
|
public function findPipelineByCommit(string $projectName, string $commitId) : ?array |
90
|
|
|
{ |
91
|
|
|
$pipelines = $this->getPipelines($projectName); |
92
|
|
|
$this->logger->debug('Analysing '.count($pipelines).' pipelines to find pipeline for commit '.$commitId); |
93
|
|
|
|
94
|
|
|
foreach ($pipelines as $pipeline) { |
95
|
|
|
if ($pipeline['sha'] === $commitId) { |
96
|
|
|
return $pipeline; |
97
|
|
|
} |
98
|
|
|
} |
99
|
|
|
|
100
|
|
|
return null; |
101
|
|
|
} |
102
|
|
|
|
103
|
|
|
/** |
104
|
|
|
* Recursive function that attempts to find a build in the previous commits. |
105
|
|
|
* |
106
|
|
|
* @param string $projectName |
107
|
|
|
* @param string $commitId |
108
|
|
|
* @param string|null $excludePipelineId A pipeline ID we want to exclude (we don't want to get the current pipeline ID). |
109
|
|
|
* @param int $numIter |
110
|
|
|
* @return array |
111
|
|
|
* @throws BuildNotFoundException |
112
|
|
|
*/ |
113
|
|
|
public function getLatestPipelineFromCommitId(string $projectName, string $commitId, string $excludePipelineId = null, int $numIter = 0) : array |
114
|
|
|
{ |
115
|
|
|
$this->logger->debug('Looking for latest pipeline for commit '.$commitId); |
116
|
|
|
$pipeline = $this->findPipelineByCommit($projectName, $commitId); |
117
|
|
|
|
118
|
|
|
if ($pipeline !== null && $pipeline['id'] !== $excludePipelineId) { |
119
|
|
|
if ($pipeline['id'] !== $excludePipelineId) { |
120
|
|
|
$this->logger->debug('Found pipeline '.$pipeline['id'].' for commit '.$commitId); |
121
|
|
|
return $pipeline; |
122
|
|
|
} else { |
123
|
|
|
$this->logger->debug('Ignoring pipeline '.$excludePipelineId.' for commit '.$commitId); |
124
|
|
|
} |
125
|
|
|
} |
126
|
|
|
|
127
|
|
|
$numIter++; |
128
|
|
|
// Let's find a build in the last 10 commits. |
129
|
|
|
if ($numIter > 10) { |
130
|
|
|
$this->logger->debug('Could not find a build for commit '.$projectName.':'.$commitId.', after iterating on 10 parent commits.'); |
131
|
|
|
throw new BuildNotFoundException('Could not find a build for commit '.$projectName.':'.$commitId); |
132
|
|
|
} |
133
|
|
|
$this->logger->debug('Could not find a build for commit '.$projectName.':'.$commitId.'. Looking for a build in parent commit.'); |
134
|
|
|
|
135
|
|
|
// Let's get the commit info |
136
|
|
|
$commit = $this->client->repositories->commit($projectName, $commitId); |
137
|
|
|
$parentIds = $commit['parent_ids']; |
138
|
|
|
|
139
|
|
|
if (count($parentIds) !== 1) { |
140
|
|
|
$this->logger->debug('Cannot look into parent commit because it is a merge from 2 branches.'); |
141
|
|
|
throw new BuildNotFoundException('Could not find a build for commit '.$projectName.':'.$commitId); |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
// Not found? Let's recurse. |
145
|
|
|
return $this->getLatestPipelineFromCommitId($projectName, $parentIds[0], $excludePipelineId, $numIter); |
146
|
|
|
} |
147
|
|
|
|
148
|
|
|
/** |
149
|
|
|
* @param string $projectName |
150
|
|
|
* @param string $branchName |
151
|
|
|
* @param string $excludePipelineId A pipeline ID we want to exclude (we don't want to get the current pipeline ID). |
152
|
|
|
* @return array |
153
|
|
|
* @throws BuildNotFoundException |
154
|
|
|
*/ |
155
|
|
|
public function getLatestPipelineFromBranch(string $projectName, string $branchName, string $excludePipelineId) : array |
156
|
|
|
{ |
157
|
|
|
$commitId = $this->getLatestCommitIdFromBranch($projectName, $branchName); |
158
|
|
|
|
159
|
|
|
try { |
160
|
|
|
return $this->getLatestPipelineFromCommitId($projectName, $commitId, $excludePipelineId); |
161
|
|
|
} catch (BuildNotFoundException $e) { |
162
|
|
|
throw new BuildNotFoundException('Could not find a build for branch '.$projectName.':'.$branchName, 0, $e); |
163
|
|
|
} |
164
|
|
|
} |
165
|
|
|
|
166
|
|
|
/** |
167
|
|
|
* @param string $projectName |
168
|
|
|
* @param string $pipelineId |
169
|
|
|
* @param string $buildName |
170
|
|
|
* @param string $jobStage |
171
|
|
|
* @param string $file |
172
|
|
|
* @throws BuildNotFoundException |
173
|
|
|
*/ |
174
|
|
|
public function dumpArtifact(string $projectName, string $pipelineId, string $buildName, string $jobStage, string $file) |
175
|
|
|
{ |
176
|
|
|
// Call seems broken |
177
|
|
|
//$artifactContent = $this->client->jobs->artifactsByRefName($projectName, $buildRef, $jobName); |
|
|
|
|
178
|
|
|
|
179
|
|
|
$jobs = $this->client->jobs->pipelineJobs($projectName, $pipelineId); |
180
|
|
|
$job = null; |
181
|
|
|
foreach ($jobs as $jobItem) { |
182
|
|
|
if ($jobItem['name'] === $buildName && $jobItem['stage'] === $jobStage && (in_array($jobItem['status'], ['failed', 'success']))) { |
183
|
|
|
$job = $jobItem; |
184
|
|
|
break; |
185
|
|
|
} |
186
|
|
|
} |
187
|
|
|
|
188
|
|
|
if ($job === null) { |
189
|
|
|
throw new BuildNotFoundException('Could not find finished job with build name "'.$buildName.'" and stage "'.$jobStage.'" in pipeline "'.$pipelineId.'"'); |
190
|
|
|
} |
191
|
|
|
|
192
|
|
|
$artifactContent = $this->client->jobs->artifacts($projectName, $job['id']); |
193
|
|
|
|
194
|
|
|
$stream = StreamWrapper::getResource($artifactContent); |
195
|
|
|
|
196
|
|
|
$filesystem = new Filesystem(); |
197
|
|
|
$filesystem->dumpFile($file, $stream); |
|
|
|
|
198
|
|
|
} |
199
|
|
|
|
200
|
|
|
/** |
201
|
|
|
* @param string $projectName |
202
|
|
|
* @param string $branchName |
203
|
|
|
* @param string $buildName |
204
|
|
|
* @param string $jobStage |
205
|
|
|
* @param string $file |
206
|
|
|
* @param string $excludePipelineId A pipeline ID we want to exclude (we don't want to get the current pipeline ID). |
207
|
|
|
* @throws BuildNotFoundException |
208
|
|
|
*/ |
209
|
|
|
public function dumpArtifactFromBranch(string $projectName, string $branchName, string $buildName, string $jobStage, string $file, string $excludePipelineId) |
210
|
|
|
{ |
211
|
|
|
$pipeline = $this->getLatestPipelineFromBranch($projectName, $branchName, $excludePipelineId); |
212
|
|
|
$this->dumpArtifact($projectName, $pipeline['id'], $buildName, $jobStage, $file); |
213
|
|
|
} |
214
|
|
|
} |
215
|
|
|
|
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.