Completed
Push — add/changelog-tooling ( b2784f...e205c1 )
by
unknown
109:57 queued 100:19
created

Utils::runCommand()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 4
dl 0
loc 25
rs 9.52
c 0
b 0
f 0
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;
0 ignored issues
show
Bug introduced by
The property fileLine does not seem to exist in RuntimeException.

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.

Loading history...
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. An array is written to this parameter, with
161
	 *   keys being filenames in `$dir` and values being 0 for success, 1 for warnings, 2 for errors.
162
	 * @return ChangeEntry[] Keys are filenames in `$dir`.
163
	 */
164
	public static function loadAllChanges( $dir, array $subheadings, FormatterPlugin $formatter, OutputInterface $output, &$files = null ) {
165
		$files = array();
166
		$ret   = array();
167
168
		$allFiles = array();
169 View Code Duplication
		foreach ( new \DirectoryIterator( $dir ) as $file ) {
170
			$name = $file->getBasename();
171
			if ( '.' !== $name[0] ) {
172
				$allFiles[ $name ] = $file->getPathname();
173
			}
174
		}
175
		asort( $allFiles );
176
		foreach ( $allFiles as $name => $path ) {
177
			$diagnostics    = null;
178
			$files[ $name ] = 0;
179
			try {
180
				$data = self::loadChangeFile( $path, $diagnostics );
181
			} catch ( \RuntimeException $ex ) {
182
				$output->writeln( "<error>$name: {$ex->getMessage()}</>" );
183
				$files[ $name ] = 2;
184
				continue;
185
			}
186
			if ( $diagnostics['warnings'] ) {
187
				$files[ $name ] = 1;
188
				foreach ( $diagnostics['warnings'] as list( $msg, $line ) ) {
189
					$line = $line ? ":$line" : '';
190
					$output->writeln( "<warning>$name$line: $msg</>" );
191
				}
192
			}
193
			try {
194
				$ret[ $name ] = $formatter->newChangeEntry(
195
					array(
196
						'significance' => isset( $data['Significance'] ) ? $data['Significance'] : null,
197
						'subheading'   => isset( $data['Type'] ) ? ( isset( $subheadings[ $data['Type'] ] ) ? $subheadings[ $data['Type'] ] : ucfirst( $data['Type'] ) ) : null,
198
						'content'      => $data[''],
199
					)
200
				);
201
			} catch ( \InvalidArgumentException $ex ) {
202
				$output->writeln( "<error>$name: {$ex->getMessage()}</>" );
203
				$files[ $name ] = 2;
204
			}
205
		}
206
207
		return $ret;
208
	}
209
210
}
211
212