|
1
|
|
|
<?php // phpcs:ignore WordPress.Files.FileName |
|
2
|
|
|
/** |
|
3
|
|
|
* Utilities for the changelogger tool. |
|
4
|
|
|
* |
|
5
|
|
|
* @package automattic/jetpack-changelogger |
|
6
|
|
|
*/ |
|
7
|
|
|
|
|
8
|
|
|
// phpcs:disable WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid, WordPress.NamingConventions.ValidVariableName |
|
9
|
|
|
|
|
10
|
|
|
namespace Automattic\Jetpack\Changelogger; |
|
11
|
|
|
|
|
12
|
|
|
use Automattic\Jetpack\Changelog\ChangeEntry; |
|
13
|
|
|
use Symfony\Component\Console\Helper\DebugFormatterHelper; |
|
14
|
|
|
use Symfony\Component\Console\Output\OutputInterface; |
|
15
|
|
|
use Symfony\Component\Process\Process; |
|
16
|
|
|
use function error_clear_last; // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.error_clear_lastFound |
|
17
|
|
|
use function Wikimedia\quietCall; |
|
18
|
|
|
|
|
19
|
|
|
/** |
|
20
|
|
|
* Utilities for the changelogger tool. |
|
21
|
|
|
*/ |
|
22
|
|
|
class Utils { |
|
23
|
|
|
|
|
24
|
|
|
/** |
|
25
|
|
|
* Calls `error_clear_last()` or emulates it. |
|
26
|
|
|
*/ |
|
27
|
|
|
public static function error_clear_last() { |
|
28
|
|
|
if ( is_callable( 'error_clear_last' ) ) { |
|
29
|
|
|
// phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.error_clear_lastFound |
|
30
|
|
|
error_clear_last(); |
|
31
|
|
|
} else { |
|
32
|
|
|
// @codeCoverageIgnoreStart |
|
33
|
|
|
quietCall( 'trigger_error', '', E_USER_NOTICE ); |
|
34
|
|
|
// @codeCoverageIgnoreEnd |
|
35
|
|
|
} |
|
36
|
|
|
} |
|
37
|
|
|
|
|
38
|
|
|
/** |
|
39
|
|
|
* Helper to run a process. |
|
40
|
|
|
* |
|
41
|
|
|
* @param string[] $command Command to execute. |
|
42
|
|
|
* @param OutputInterface $output OutputInterface to write debug output to. |
|
43
|
|
|
* @param DebugFormatterHelper $formatter Formatter to use to format debug output. |
|
44
|
|
|
* @param array $options An associative array with the following optional keys. Defaults are null unless otherwise specified. |
|
45
|
|
|
* - cwd: (string|null) The working directory or null to use the working dir of the current PHP process. |
|
46
|
|
|
* - env: (array|null) The environment variables or null to use the same environment as the current PHP process. |
|
47
|
|
|
* - input: (mixed|null) The input as stream resource, scalar or \Traversable, or null for no input. |
|
48
|
|
|
* - timeout: (float|null) The timeout in seconds or null to disable. Default 60. |
|
49
|
|
|
* - mustRun: (boolean) If set true, an exception will be thrown if the command fails. Default false. |
|
50
|
|
|
* @return Process The process, which has already been run. |
|
51
|
|
|
*/ |
|
52
|
|
|
public static function runCommand( array $command, OutputInterface $output, DebugFormatterHelper $formatter, array $options = array() ) { |
|
53
|
|
|
$options += array( |
|
54
|
|
|
'cwd' => null, |
|
55
|
|
|
'env' => null, |
|
56
|
|
|
'input' => null, |
|
57
|
|
|
'timeout' => 60, |
|
58
|
|
|
'mustRun' => false, |
|
59
|
|
|
); |
|
60
|
|
|
|
|
61
|
|
|
$process = new Process( $command, $options['cwd'], $options['env'], $options['input'], $options['timeout'] ); |
|
62
|
|
|
$output->writeln( |
|
63
|
|
|
$formatter->start( spl_object_hash( $process ), $process->getCommandLine() ), |
|
64
|
|
|
OutputInterface::VERBOSITY_DEBUG |
|
65
|
|
|
); |
|
66
|
|
|
$func = $options['mustRun'] ? 'mustRun' : 'run'; |
|
67
|
|
|
$process->$func( |
|
68
|
|
|
function ( $type, $buffer ) use ( $output, $formatter, $process ) { |
|
69
|
|
|
$output->writeln( |
|
70
|
|
|
$formatter->progress( spl_object_hash( $process ), $buffer, Process::ERR === $type ), |
|
71
|
|
|
OutputInterface::VERBOSITY_DEBUG |
|
72
|
|
|
); |
|
73
|
|
|
} |
|
74
|
|
|
); |
|
75
|
|
|
return $process; |
|
76
|
|
|
} |
|
77
|
|
|
|
|
78
|
|
|
/** |
|
79
|
|
|
* Load and parse a change file to an array. |
|
80
|
|
|
* |
|
81
|
|
|
* Header names are normalized. The entry is returned under the empty |
|
82
|
|
|
* string key. |
|
83
|
|
|
* |
|
84
|
|
|
* @param string $filename File to load. |
|
85
|
|
|
* @param mixed $diagnostics Output variable, set to an array with diagnostic data. |
|
86
|
|
|
* - warnings: An array of warning messages and applicable lines. |
|
87
|
|
|
* - lines: An array mapping headers to line numbers. |
|
88
|
|
|
* @return array |
|
89
|
|
|
* @throws \RuntimeException On error. |
|
90
|
|
|
*/ |
|
91
|
|
|
public static function loadChangeFile( $filename, &$diagnostics = null ) { |
|
92
|
|
|
$diagnostics = array( |
|
93
|
|
|
'warnings' => array(), |
|
94
|
|
|
'lines' => array(), |
|
95
|
|
|
); |
|
96
|
|
|
|
|
97
|
|
|
if ( ! file_exists( $filename ) ) { |
|
98
|
|
|
$ex = new \RuntimeException( 'File does not exist.' ); |
|
99
|
|
|
$ex->fileLine = null; |
|
|
|
|
|
|
100
|
|
|
throw $ex; |
|
101
|
|
|
} |
|
102
|
|
|
|
|
103
|
|
|
$fileinfo = new \SplFileInfo( $filename ); |
|
104
|
|
|
if ( $fileinfo->getType() !== 'file' ) { |
|
105
|
|
|
$ex = new \RuntimeException( "Expected a file, got {$fileinfo->getType()}." ); |
|
106
|
|
|
$ex->fileLine = null; |
|
107
|
|
|
throw $ex; |
|
108
|
|
|
} |
|
109
|
|
|
if ( ! $fileinfo->isReadable() ) { |
|
110
|
|
|
$ex = new \RuntimeException( 'File is not readable.' ); |
|
111
|
|
|
$ex->fileLine = null; |
|
112
|
|
|
throw $ex; |
|
113
|
|
|
} |
|
114
|
|
|
|
|
115
|
|
|
self::error_clear_last(); |
|
116
|
|
|
$contents = quietCall( 'file_get_contents', $filename ); |
|
117
|
|
|
// @codeCoverageIgnoreStart |
|
118
|
|
|
if ( false === $contents ) { |
|
119
|
|
|
$err = error_get_last(); |
|
120
|
|
|
$ex = new \RuntimeException( "Failed to read file: {$err['message']}" ); |
|
121
|
|
|
$ex->fileLine = null; |
|
122
|
|
|
throw $ex; |
|
123
|
|
|
} |
|
124
|
|
|
// @codeCoverageIgnoreEnd |
|
125
|
|
|
|
|
126
|
|
|
$ret = array(); |
|
127
|
|
|
$line = 1; |
|
128
|
|
|
while ( preg_match( '/^([A-Z][a-zA-Z0-9-]*):((?:.|\n[ \t])*)(?:\n|$)/', $contents, $m ) ) { |
|
129
|
|
|
if ( isset( $diagnostics['lines'][ $m[1] ] ) ) { |
|
130
|
|
|
$diagnostics['warnings'][] = array( |
|
131
|
|
|
"Duplicate header \"{$m[1]}\", previously seen on line {$diagnostics['lines'][ $m[1] ]}.", |
|
132
|
|
|
$line, |
|
133
|
|
|
); |
|
134
|
|
|
} else { |
|
135
|
|
|
$diagnostics['lines'][ $m[1] ] = $line; |
|
136
|
|
|
$ret[ $m[1] ] = trim( preg_replace( '/(\n[ \t]+)+/', ' ', $m[2] ) ); |
|
137
|
|
|
} |
|
138
|
|
|
$line += substr_count( $m[0], "\n" ); |
|
139
|
|
|
$contents = (string) substr( $contents, strlen( $m[0] ) ); |
|
140
|
|
|
} |
|
141
|
|
|
|
|
142
|
|
|
if ( '' !== $contents && "\n" !== $contents[0] ) { |
|
143
|
|
|
$ex = new \RuntimeException( 'Invalid header.' ); |
|
144
|
|
|
$ex->fileLine = $line; |
|
145
|
|
|
throw $ex; |
|
146
|
|
|
} |
|
147
|
|
|
$diagnostics['lines'][''] = $line + strspn( $contents, "\n" ); |
|
148
|
|
|
$ret[''] = trim( $contents ); |
|
149
|
|
|
|
|
150
|
|
|
return $ret; |
|
151
|
|
|
} |
|
152
|
|
|
|
|
153
|
|
|
/** |
|
154
|
|
|
* Load the changes files into an array of ChangeEntries. |
|
155
|
|
|
* |
|
156
|
|
|
* @param string $dir Changes directory. |
|
157
|
|
|
* @param array $subheadings Mapping from type codes to subheadings. |
|
158
|
|
|
* @param FormatterPlugin $formatter Formatter plugin to use. |
|
159
|
|
|
* @param OutputInterface $output OutputInterface to write diagnostics too. |
|
160
|
|
|
* @param mixed $files Output parameter, a list of files successfully processed is written to this variable. |
|
161
|
|
|
* @return ChangeEntry[] |
|
162
|
|
|
*/ |
|
163
|
|
|
public static function loadAllChanges( $dir, array $subheadings, FormatterPlugin $formatter, OutputInterface $output, &$files = null ) { |
|
164
|
|
|
$files = array(); |
|
165
|
|
|
$ret = array(); |
|
166
|
|
|
|
|
167
|
|
|
$allFiles = array(); |
|
168
|
|
View Code Duplication |
foreach ( new \DirectoryIterator( $dir ) as $file ) { |
|
169
|
|
|
$name = $file->getBasename(); |
|
170
|
|
|
if ( '.' !== $name[0] ) { |
|
171
|
|
|
$allFiles[ $name ] = $file->getPathname(); |
|
172
|
|
|
} |
|
173
|
|
|
} |
|
174
|
|
|
asort( $allFiles ); |
|
175
|
|
|
foreach ( $allFiles as $name => $path ) { |
|
176
|
|
|
$diagnostics = null; |
|
177
|
|
|
try { |
|
178
|
|
|
$data = self::loadChangeFile( $path, $diagnostics ); |
|
179
|
|
|
} catch ( \RuntimeException $ex ) { |
|
180
|
|
|
$output->writeln( "<error>$name: {$ex->getMessage()}</>" ); |
|
181
|
|
|
continue; |
|
182
|
|
|
} |
|
183
|
|
|
foreach ( $diagnostics['warnings'] as list( $msg, $line ) ) { |
|
184
|
|
|
$line = $line ? ":$line" : ''; |
|
185
|
|
|
$output->writeln( "<warning>$name$line: $msg</>" ); |
|
186
|
|
|
} |
|
187
|
|
|
try { |
|
188
|
|
|
$ret[] = $formatter->newChangeEntry( |
|
189
|
|
|
array( |
|
190
|
|
|
'significance' => isset( $data['Significance'] ) ? $data['Significance'] : null, |
|
191
|
|
|
'subheading' => isset( $data['Type'] ) ? ( isset( $subheadings[ $data['Type'] ] ) ? $subheadings[ $data['Type'] ] : ucfirst( $data['Type'] ) ) : null, |
|
192
|
|
|
'content' => $data[''], |
|
193
|
|
|
) |
|
194
|
|
|
); |
|
195
|
|
|
$files[] = $path; |
|
196
|
|
|
} catch ( \InvalidArgumentException $ex ) { |
|
197
|
|
|
$output->writeln( "<error>$name: {$ex->getMessage()}</>" ); |
|
198
|
|
|
} |
|
199
|
|
|
} |
|
200
|
|
|
|
|
201
|
|
|
return $ret; |
|
202
|
|
|
} |
|
203
|
|
|
|
|
204
|
|
|
} |
|
205
|
|
|
|
|
206
|
|
|
|
An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.
If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.