Completed
Pull Request — 2.0 (#21)
by David
07:39
created

BuildService::findMergeRequestByCommitSha()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 33
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 33
rs 8.439
c 0
b 0
f 0
cc 5
eloc 16
nc 5
nop 2
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, [
0 ignored issues
show
Unused Code Comprehensibility introduced by
58% 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...
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) {
0 ignored issues
show
Bug introduced by
The expression $mergeRequests of type array|string is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
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);
0 ignored issues
show
Unused Code Comprehensibility introduced by
62% 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...
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);
0 ignored issues
show
Documentation introduced by
$stream is of type resource, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
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