Completed
Push — add/relase-scripts-package ( 097cd4...aa7fa7 )
by Yaroslav
55:18 queued 47:13
created

Release::handle_composer_dependencies()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
nc 3
nop 1
dl 0
loc 23
rs 9.552
c 0
b 0
f 0
1
<?php
2
/**
3
 * Set of scripts for Jetpack.
4
 *
5
 * @package automattic/jetpack-scripts
6
 */
7
8
namespace Automattic\Jetpack\Scripts;
9
10
/**
11
 * Package release handler
12
 */
13
class Release {
14
	/**
15
	 * List of update statuses for dependencies
16
	 *
17
	 * @var Array
18
	 */
19
	private $update_statuses = array();
20
21
	/**
22
	 * Sets a logger
23
	 */
24
	public function __construct() {
25
		$this->logger = new Logger();
0 ignored issues
show
Bug introduced by
The property logger does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
26
	}
27
28
	/**
29
	 * Class entry point. Called externally via composer script
30
	 */
31
	public static function run() {
32
		$release = new Release();
33
		Logger::info( 'Doing stuff...' );
34
		$root = Dependency_Tree::generate();
35
		Logger::log( $root );
36
37
		$list = $release->get_package_dependencies_to_update( $root );
38
39
		if ( ! empty( $list ) ) {
40
			$release->handle_dependencies_updates( $list );
41
		}
42
43
		$release->handle_package_update( $root );
44
45
		Logger::info( 'Done with running!' );
46
	}
47
48
	/**
49
	 * Handles composer dependencies updates
50
	 *
51
	 * @param Array $package package as associative array.
52
	 */
53
	public function handle_composer_dependencies( $package ) {
54
		$name   = $package['name'];
55
		$answer = $this->handle_polar_question( "Do you want to update composer dependencies for $name [y/n]? " );
56
57
		if ( false === $answer ) {
58
			Logger::log( 'As you wish. skipping composer updates', 'dark_gray' );
59
			return false;
60
		}
61
62
		$dependencies = array_map(
63
			function ( $require ) {
64
				return $require['name'];
65
			},
66
			$package['requires']
67
		);
68
69
		$folder = explode( '/', $package['name'] )[1];
70
71
		foreach ( $dependencies as $dep ) {
72
			Cmd::run( "composer --working-dir=$folder require $dep 2>&1" );
73
		}
74
		return true;
75
	}
76
77
	/**
78
	 * Handles root package update
79
	 *
80
	 * @param String $package package name.
81
	 */
82
	public function handle_package_update( $package ) {
83
		$name = $package['name'];
84
		$sh   = new Git_Shell_Command( $name );
85
86
		$sh->clone_repository();
87
88
		$tag  = $sh->get_latest_tag();
89
		$diff = $sh->get_diff_between( $tag, 'master' );
90
91
		if ( $this->is_update_requested( $name, $tag, $diff ) ) {
92
			$this->do_package_update( $package );
93
		}
94
	}
95
96
	/**
97
	 * Loops through provided list of dependencies and tries to update them
98
	 *
99
	 * @param Array $list list of dependencies.
100
	 */
101
	public function handle_dependencies_updates( $list ) {
102
		Logger::info( 'Here is the list of dependencies with some unreleased changes:' );
103
		Logger::log( $list, 1 );
0 ignored issues
show
Documentation introduced by
$list is of type array, 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...
104
105
		foreach ( $list as $dep ) {
106
			if ( $this->is_update_requested( $dep[0], $dep[2], $dep[1] ) ) {
107
				$this->do_package_update( $dep[3] );
108
			}
109
		}
110
	}
111
112
	/**
113
	 * Run an actual package update
114
	 *
115
	 * @param String $package package name.
116
	 * @throws \Exception Invalid provided version.
117
	 */
118
	public function do_package_update( $package ) {
119
		$name = $package['name'];
120
		Logger::info( "Updating package: $name" );
121
122
		$prompt = $this->logger->get_colored_string(
123
			'Please provide a version number that should be used for new release in SemVer format (x.x.x): ',
124
			'green'
125
		);
126
127
		$version = readline( $prompt );
128
129
		if ( 3 !== count( explode( '.', $version ) ) ) {
130
			throw new \Exception( "$version is not in a SemVer format" );
131
		}
132
133
		$version = 'v' . $version;
134
		$branch  = "release-$version";
135
136
		$sh = new Git_Shell_Command( $name );
137
		$sh->clone_repository();
138
		$sh->checkout_new_branch( $branch );
139
140
		$is_updated = $this->handle_composer_dependencies( $package );
0 ignored issues
show
Documentation introduced by
$package is of type string, but the function expects a array.

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...
141
142
		// If composer deps were updated, we need to commit changed composer.json.
143
		if ( $is_updated ) {
144
			$git_status = $sh->status();
145
			if ( 0 !== strlen( $git_status ) ) {
146
				Logger::info( 'Git directory is not clean' );
147
				Logger::info( $git_status );
148
149
				$answer = $this->handle_polar_question( 'Do you want to commit these changes [y/n]? ' );
150
151
				if ( true === $answer ) {
152
					$sh->commit();
153
				}
154
			}
155
		}
156
157
		$sh->tag_new_version( $version );
158
		$sh->push_to_remote( $branch );
159
160
		return true;
161
	}
162
163
	/**
164
	 * Checks if user requested an update
165
	 *
166
	 * @param String $name repo name.
167
	 * @param String $tag latest released version.
168
	 * @param String $diff short diff of unreleased changes.
169
	 */
170
	public function is_update_requested( $name, $tag, $diff ) {
171
		Logger::log( "Package name: $name", 'blue' );
172
		Logger::log( "Latest stable version: $tag", 'blue' );
173
		Logger::log( "Unreleased changes: $diff", 'blue' );
174
175
		return $this->handle_polar_question( 'Do you want to update this package [y/n]? ' );
176
	}
177
178
	/**
179
	 * Prompts a polar (Y/N) question to a user, and returns a bool
180
	 *
181
	 * @param String $prompt question to ask.
182
	 * @throws \Exception Invalid response.
183
	 */
184
	public function handle_polar_question( $prompt ) {
185
		$in = readline( $this->logger->get_colored_string( $prompt, 'green' ) );
186
187
		if ( 'y' === $in || 'Y' === $in ) {
188
			return true;
189
		} elseif ( 'n' === $in || 'N' === $in ) {
190
			return false;
191
		} else {
192
			throw new \Exception( 'Invalid response. Expected Y/y/N/n' );
193
		}
194
	}
195
196
197
	/**
198
	 * Checks wether a package have some unreleased changes
199
	 *
200
	 * @param String $name repository name.
201
	 */
202
	public function is_update_possible( $name ) {
203
		/**
204
		 * Inside $name repository:
205
		 * - get latest tag: `git describe --abbrev=0`
206
		 * - compare tag against master: `git diff $tag master --shortstat`
207
		 * - if output is not empty - there is a difference
208
		 */
209
210
		if ( array_key_exists( $name, $this->update_statuses ) ) {
211
			return $this->update_statuses[ $name ];
212
		}
213
214
		$sh = new Git_Shell_Command( $name );
215
		$sh->clone_repository();
216
		$tag = $sh->get_latest_tag();
217
		$out = $sh->get_diff_between( $tag, 'master' );
218
219
		$result = array(
220
			'status' => true,
221
			'diff'   => $out,
222
			'tag'    => $tag,
223
		);
224
		if ( 0 === strlen( $out ) ) {
225
			$result['status'] = false;
226
		}
227
228
		$this->update_statuses[ $name ] = $result;
229
		return $result;
230
	}
231
232
	/**
233
	 * Walks through the dependency tree and builds a list of deps that needs to be updated
234
	 * This list is build in order from branch up to root, meaning it is the order that could be used for updating the whole tree in single run
235
	 *
236
	 * @param Array $object tree node.
237
	 */
238
	public function get_package_dependencies_to_update( $object ) {
239
		$object_requires = $object['requires'];
240
		$deps_to_update  = array();
241
242
		foreach ( $object_requires as $dependency ) {
243
			// Check if we need to update this package.
244
			$update = $this->is_update_possible( $dependency['name'] );
245
			if ( $update['status'] ) {
246
				array_unshift( $deps_to_update, array( $dependency['name'], $update['diff'], $update['tag'], $dependency ) );
247
				if ( array_key_exists( 'requires', $dependency ) ) {
248
					// $dependency have dependencies, lets recursively go through them too.
249
					$deps = $this->get_package_dependencies_to_update( $dependency );
250
					array_merge( $deps, $deps_to_update );
251
				}
252
			}
253
		}
254
255
		return $deps_to_update;
256
	}
257
}
258