Skip to content

Commit 6cbccf8

Browse files
committed
MC-37324: Add fallback to 'patch' command when 'git' command is not available
- Fix error message formatting
1 parent 0060375 commit 6cbccf8

File tree

7 files changed

+264
-12
lines changed

7 files changed

+264
-12
lines changed

config/services.xml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@
1919
<service id="Magento\CloudPatches\Patch\Status\StatusResolverException" autowire="false"/>
2020
<service id="Magento\CloudPatches\Patch\PatchIntegrityException" autowire="false"/>
2121
<service id="Magento\CloudPatches\Patch\Pool\PatchNotFoundException" autowire="false"/>
22-
<service id="Magento\CloudPatches\Patch\PatchCommandNotFound" autowire="false"/>
23-
<service id="Magento\CloudPatches\Patch\PatchCommandException" autowire="false"/>
2422
<service id="Magento\CloudPatches\Patch\ApplierException" autowire="false"/>
2523
<service id="Magento\CloudPatches\Shell\PackageNotFoundException" autowire="false"/>
2624
<service id="Magento\CloudPatches\Patch\Data\Patch" autowire="false"/>
@@ -29,6 +27,11 @@
2927
<service id="Magento\CloudPatches\Patch\Pool\RequiredPool" lazy="true"/>
3028
<service id="Magento\CloudPatches\Patch\Pool\LocalPool" lazy="true"/>
3129
<service id="Magento\CloudPatches\Patch\Status\StatusPool" autowire="false"/>
30+
<service id="Magento\CloudPatches\Patch\PatchCommandNotFound" autowire="false"/>
31+
<service id="Magento\CloudPatches\Patch\PatchCommandException" autowire="false"/>
32+
<service id="Magento\CloudPatches\Shell\Command\DriverException" autowire="false"/>
33+
<service id="Magento\CloudPatches\Shell\Command\GitDriverException" autowire="false"/>
34+
<service id="Magento\CloudPatches\Shell\Command\PatchDriverException" autowire="false"/>
3235
<service id="Magento\CloudPatches\Patch\PatchCommand" autowire="false"/>
3336
<service id="Magento\CloudPatches\Patch\PatchCommandInterface" alias="Magento\CloudPatches\Patch\PatchCommand"/>
3437
<service id="statusPool" class="Magento\CloudPatches\Patch\Status\StatusPool" lazy="true">

src/Shell/Command/DriverException.php

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\CloudPatches\Shell\Command;
9+
10+
use Magento\CloudPatches\Patch\PatchCommandException;
11+
use Throwable;
12+
13+
/**
14+
* Patch command driver exception
15+
*/
16+
class DriverException extends PatchCommandException
17+
{
18+
/**
19+
* @param string $message
20+
* @param int $code
21+
* @param Throwable|null $previous
22+
*/
23+
public function __construct(string $message, int $code = 0, Throwable $previous = null)
24+
{
25+
parent::__construct($this->formatMessage($message), $code, $previous);
26+
}
27+
28+
/**
29+
* Format error message
30+
*
31+
* @param string $message
32+
* @return string
33+
*/
34+
private function formatMessage(string $message): string
35+
{
36+
$result = $message;
37+
$errorMsg = null;
38+
$generalMsg = null;
39+
if (preg_match('#^.*?Error Output:(?<errors>.*?)$#is', $result, $matches)) {
40+
$errorMsg = PHP_EOL .'Error Output:' . $matches['errors'];
41+
$result = str_replace($errorMsg, '', $result);
42+
if (!trim(str_replace('=', '', $matches['errors']))) {
43+
$errorMsg = null;
44+
}
45+
}
46+
if (empty($errorMsg) && preg_match('#^.*?Output:(?<errors>.*?)$#is', $result, $matches)) {
47+
$generalMsg = PHP_EOL .'Output:' . $matches['errors'];
48+
if (!trim(str_replace('=', '', $matches['errors']))) {
49+
$generalMsg = null;
50+
}
51+
}
52+
53+
return $errorMsg ?? $generalMsg ?? $message;
54+
}
55+
}

src/Shell/Command/GitDriver.php

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77

88
namespace Magento\CloudPatches\Shell\Command;
99

10-
use Magento\CloudPatches\Patch\PatchCommandException;
1110
use Magento\CloudPatches\Shell\ProcessFactory;
1211
use Symfony\Component\Process\Exception\ProcessFailedException;
1312

@@ -39,7 +38,7 @@ public function apply(string $patch)
3938
$this->processFactory->create(['git', 'apply'], $patch)
4039
->mustRun();
4140
} catch (ProcessFailedException $exception) {
42-
throw new PatchCommandException('Failed to apply patch', $exception->getCode(), $exception);
41+
throw new GitDriverException($exception);
4342
}
4443
}
4544

@@ -52,7 +51,7 @@ public function revert(string $patch)
5251
$this->processFactory->create(['git', 'apply', '--reverse'], $patch)
5352
->mustRun();
5453
} catch (ProcessFailedException $exception) {
55-
throw new PatchCommandException('Failed to revert patch', $exception->getCode(), $exception);
54+
throw new GitDriverException($exception);
5655
}
5756
}
5857

@@ -65,7 +64,7 @@ public function applyCheck(string $patch)
6564
$this->processFactory->create(['git', 'apply', '--check'], $patch)
6665
->mustRun();
6766
} catch (ProcessFailedException $exception) {
68-
throw new PatchCommandException('Patch cannot be applied', $exception->getCode(), $exception);
67+
throw new GitDriverException($exception);
6968
}
7069
}
7170

@@ -78,7 +77,7 @@ public function revertCheck(string $patch)
7877
$this->processFactory->create(['git', 'apply', '--reverse', '--check'], $patch)
7978
->mustRun();
8079
} catch (ProcessFailedException $exception) {
81-
throw new PatchCommandException('Patch cannot be reverted', $exception->getCode(), $exception);
80+
throw new GitDriverException($exception);
8281
}
8382
}
8483

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\CloudPatches\Shell\Command;
9+
10+
use Symfony\Component\Process\Exception\ProcessFailedException;
11+
use Throwable;
12+
13+
/**
14+
* Git patch driver exception
15+
*/
16+
class GitDriverException extends DriverException
17+
{
18+
/**
19+
* @param Throwable $previous
20+
*/
21+
public function __construct(Throwable $previous)
22+
{
23+
$message = $previous->getMessage();
24+
if ($previous instanceof ProcessFailedException) {
25+
$message = $previous->getProcess()->getErrorOutput() ?: ($previous->getProcess()->getOutput() ?: $message);
26+
}
27+
parent::__construct($message, $previous->getCode(), $previous);
28+
}
29+
}

src/Shell/Command/PatchDriver.php

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77

88
namespace Magento\CloudPatches\Shell\Command;
99

10-
use Magento\CloudPatches\Patch\PatchCommandException;
1110
use Magento\CloudPatches\Shell\ProcessFactory;
1211
use Symfony\Component\Process\Exception\ProcessFailedException;
1312

@@ -40,7 +39,7 @@ public function apply(string $patch)
4039
$this->processFactory->create(['patch', '--silent', '-p1', '--no-backup-if-mismatch'], $patch)
4140
->mustRun();
4241
} catch (ProcessFailedException $exception) {
43-
throw new PatchCommandException('Failed to apply patch', $exception->getCode(), $exception);
42+
throw new PatchDriverException($exception);
4443
}
4544
}
4645

@@ -54,7 +53,7 @@ public function revert(string $patch)
5453
$this->processFactory->create(['patch', '--silent', '-p1', '--no-backup-if-mismatch', '--reverse'], $patch)
5554
->mustRun();
5655
} catch (ProcessFailedException $exception) {
57-
throw new PatchCommandException('Failed to revert patch', $exception->getCode(), $exception);
56+
throw new PatchDriverException($exception);
5857
}
5958
}
6059

@@ -67,7 +66,7 @@ public function applyCheck(string $patch)
6766
$this->processFactory->create(['patch', '--silent', '-p1', '--dry-run'], $patch)
6867
->mustRun();
6968
} catch (ProcessFailedException $exception) {
70-
throw new PatchCommandException('Patch cannot be applied', $exception->getCode(), $exception);
69+
throw new PatchDriverException($exception);
7170
}
7271
}
7372

@@ -80,7 +79,7 @@ public function revertCheck(string $patch)
8079
$this->processFactory->create(['patch', '--silent', '-p1', '--reverse', '--dry-run'], $patch)
8180
->mustRun();
8281
} catch (ProcessFailedException $exception) {
83-
throw new PatchCommandException('Patch cannot be reverted', $exception->getCode(), $exception);
82+
throw new PatchDriverException($exception);
8483
}
8584
}
8685

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\CloudPatches\Shell\Command;
9+
10+
use Symfony\Component\Process\Exception\ProcessFailedException;
11+
use Throwable;
12+
13+
/**
14+
* Unix patch driver exception
15+
*/
16+
class PatchDriverException extends DriverException
17+
{
18+
/**
19+
* @param Throwable $previous
20+
*/
21+
public function __construct(Throwable $previous)
22+
{
23+
$message = $previous->getMessage();
24+
if ($previous instanceof ProcessFailedException) {
25+
$message = $previous->getProcess()->getErrorOutput() ?: ($previous->getProcess()->getOutput() ?: $message);
26+
}
27+
parent::__construct($message, $previous->getCode(), $previous);
28+
}
29+
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\CloudPatches\Test\Unit\Shell\Command;
9+
10+
use Magento\CloudPatches\Shell\Command\DriverException;
11+
use PHPUnit\Framework\TestCase;
12+
13+
class DriverExceptionTest extends TestCase
14+
{
15+
/**
16+
* Tests exception message formatting.
17+
*
18+
* @param string $errorOutput
19+
* @param string $expectedOutput
20+
* @dataProvider formatMessageDataProvider
21+
*/
22+
public function testFormatMessage(string $errorOutput, string $expectedOutput)
23+
{
24+
$exception = new DriverException($errorOutput);
25+
$this->assertEquals($expectedOutput, $exception->getMessage());
26+
}
27+
28+
/**
29+
* @return array
30+
*/
31+
public function formatMessageDataProvider(): array
32+
{
33+
return [
34+
[
35+
'error' => 'The command "\'patch\' \'--silent\' \'-p1\' \'--dry-run\'" failed.
36+
37+
Exit Code: 1(General error)
38+
39+
Working directory: /var/www/html
40+
41+
Output:
42+
================
43+
44+
45+
Error Output:
46+
================
47+
error: patch failed: path/to/path/file2.php b/path/to/path/file2.php:23
48+
error: path/to/path/file2.php b/path/to/path/file2.php: patch does not apply',
49+
50+
'expectedOutput' => '
51+
Error Output:
52+
================
53+
error: patch failed: path/to/path/file2.php b/path/to/path/file2.php:23
54+
error: path/to/path/file2.php b/path/to/path/file2.php: patch does not apply'
55+
],
56+
[
57+
'error' => 'The command "\'patch\' \'--silent\' \'-p1\' \'--dry-run\'" failed.
58+
59+
Exit Code: 1(General error)
60+
61+
Working directory: /var/www/html
62+
63+
Output:
64+
================
65+
Hmm... Looks like a unified diff to me...
66+
The text leading up to this was:
67+
--------------------------
68+
|diff --git a/path/to/path/file1.php b/path/to/path/file1.php
69+
|index 320e0adc29b..576281861d3 100644
70+
|--- a/path/to/path/file1.php
71+
|+++ b/path/to/path/file1.php
72+
--------------------------
73+
Patching file path/to/path/file1.php using Plan A...
74+
Hunk #1 succeeded at 30.
75+
Hunk #2 succeeded at 54.
76+
Hunk #3 succeeded at 76.
77+
Hunk #4 succeeded at 113.
78+
Hmm... The next patch looks like a unified diff to me...
79+
The text leading up to this was:
80+
--------------------------
81+
|diff --git a/path/to/path/file2.php b/path/to/path/file2.php
82+
|index 0ec65c88024..e550de9cb03 100644
83+
|--- a/path/to/path/file2.php
84+
|+++ b/path/to/path/file2.php
85+
--------------------------
86+
Patching file path/to/path/file2.php using Plan A...
87+
Hunk #1 succeeded at 71.
88+
Hunk #2 FAILED at 136.
89+
Hunk #3 succeeded at 154.
90+
1 out of 3 hunks FAILED -- saving rejects to file path/to/path/file2.php.rej
91+
done
92+
93+
94+
Error Output:
95+
================
96+
97+
',
98+
99+
'expectedOutput' => '
100+
Output:
101+
================
102+
Hmm... Looks like a unified diff to me...
103+
The text leading up to this was:
104+
--------------------------
105+
|diff --git a/path/to/path/file1.php b/path/to/path/file1.php
106+
|index 320e0adc29b..576281861d3 100644
107+
|--- a/path/to/path/file1.php
108+
|+++ b/path/to/path/file1.php
109+
--------------------------
110+
Patching file path/to/path/file1.php using Plan A...
111+
Hunk #1 succeeded at 30.
112+
Hunk #2 succeeded at 54.
113+
Hunk #3 succeeded at 76.
114+
Hunk #4 succeeded at 113.
115+
Hmm... The next patch looks like a unified diff to me...
116+
The text leading up to this was:
117+
--------------------------
118+
|diff --git a/path/to/path/file2.php b/path/to/path/file2.php
119+
|index 0ec65c88024..e550de9cb03 100644
120+
|--- a/path/to/path/file2.php
121+
|+++ b/path/to/path/file2.php
122+
--------------------------
123+
Patching file path/to/path/file2.php using Plan A...
124+
Hunk #1 succeeded at 71.
125+
Hunk #2 FAILED at 136.
126+
Hunk #3 succeeded at 154.
127+
1 out of 3 hunks FAILED -- saving rejects to file path/to/path/file2.php.rej
128+
done
129+
130+
'
131+
],
132+
[
133+
'error' => 'Some other output',
134+
'expectedOutput' => 'Some other output'
135+
],
136+
];
137+
}
138+
}

0 commit comments

Comments
 (0)