Completed
Push — 14.2 ( 4f180f...4cf4e3 )
by Ralf
24:18
created

checkout-build-archives.php ➔ parse_current_changelog()   C

Complexity

Conditions 8
Paths 14

Size

Total Lines 28
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 17
nc 14
nop 1
dl 0
loc 28
rs 5.3846
c 0
b 0
f 0
1
#!/usr/bin/php -qC
2
<?php
3
/**
4
 * EGroupware - checkout, build and release archives, build Debian and rpm packages
5
 *
6
 * @link http://www.egroupware.org
7
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
8
 * @author [email protected]
9
 * @copyright (c) 2009-16 by Ralf Becker <[email protected]>
10
 * @version $Id$
11
 */
12
13
if (php_sapi_name() !== 'cli')	// security precaution: forbit calling setup-cli as web-page
14
{
15
	die('<h1>checkout-build-archives.php must NOT be called as web-page --> exiting !!!</h1>');
16
}
17
date_default_timezone_set('Europe/Berlin');	// to get ride of 5.3 warnings
18
19
$verbose = 0;
20
$config = array(
21
	'packagename' => 'egroupware-epl',
22
	'version' => '14.3',        // '14.3'
23
	'packaging' => date('Ymd'), // '20160520'
24
	'branch'  => '14.2',        // checked out branch
25
	'tag' => '$version.$packaging',	// name of tag
26
	'checkoutdir' => realpath(__DIR__.'/../..'),
27
	'egw_buildroot' => '/tmp/build_root/epl_14.2_buildroot',
28
	'sourcedir' => '/home/download/stylite-epl/egroupware-epl-14.2',
29
	/* svn-config currently not used, as we use .mrconfig to define modules and urls
30
	'svntag' => 'tags/$version.$packaging',
31
	'svnbase' => 'svn+ssh://[email protected]/egroupware',
32
	'stylitebase' => 'svn+ssh://[email protected]/stylite',
33
	'svnbranch' => 'branches/14.2',         //'trunk', // 'branches/1.6' or 'tags/1.6.001'
34
	'svnalias' => 'aliases/default-ssh',    // default alias
35
	'extra' => array('$stylitebase/$svnbranch/stylite', '$stylitebase/$svnbranch/esyncpro', '$stylitebase/trunk/archive'),//, '$stylitebase/$svnbranch/groups'), //,'svn+ssh://[email protected]/stylite/trunk/eventmgr'),
36
	*/
37
	'extra' => array('stylite', 'esyncpro', 'archive'),	// create an extra archive for given apps
38
		// these apps are placed in egroupware-epl-contrib archive
39
		//'contrib' => array('phpgwapi', 'etemplate', 'jdots', 'phpbrain', 'wiki', 'sambaadmin', 'sitemgr', 'phpfreechat')),
40
	'aliasdir' => 'egroupware',             // directory created by the alias
41
	'types' => array('tar.bz2','tar.gz','zip','all.tar.bz2'),
42
	// add given extra-apps or (uncompressed!) archives to above all.tar.bz2 archive
43
	'all-add' => array(/*'contrib',*/ '/home/stylite/epl-trunk/phpfreechat_data_public.tar'),
44
	// diverse binaries we need
45
	'svn' => trim(`which svn`),
46
	'tar' => trim(`which tar`),
47
	'mv' => trim(`which mv`),
48
	'rm' => trim(`which rm`),
49
	'zip' => trim(`which zip`),
50
	'bzip2' => trim(`which bzip2`),
51
	'clamscan' => trim(`which clamscan`),
52
	'freshclam' => trim(`which freshclam`),
53
	'git' => trim(`which git`),
54
	'mr'  => trim(`which mr`),
55
	'gpg' => trim(`which gpg`),
56
	'editor' => trim(`which vi`),
57
	'rsync' => trim(`which rsync`).' --progress -e ssh --exclude "*-stylite-*" --exclude "*-esyncpro-*"',
58
	'composer' => ($composer=trim(`which composer.phar`)) ? $composer.' install --ignore-platform-reqs' : '',
59
	'after-checkout' => 'rm -rf */source */templates/*/source',
60
	'packager' => '[email protected]',
61
	'obs' => '/home/stylite/obs/stylite-epl',
62
	'obs_package_alias' => '',	// name used in obs package, if different from packagename
63
	'changelog' => false,   // eg. '* 1. Zeile\n* 2. Zeile' for debian.changes
64
	'changelog_packager' => 'Ralf Becker <[email protected]>',
65
	'editchangelog' => '* ',
66
	//'sfuser' => 'ralfbecker',
67
	//'release' => '$sfuser,[email protected]:/home/frs/project/e/eg/egroupware/eGroupware-$version/eGroupware-$version.$packaging/',
68
	'copychangelog' => '$sourcedir/README', //'$sfuser,[email protected]:/home/frs/project/e/eg/egroupware/README',
69
	'skip' => array(),
70
	'run' => array('checkout','editchangelog','tag','copy','virusscan','create','sign','obs','copychangelog'),
71
	'patchCmd' => '# run cmd after copy eg. "cd $egw_buildroot; patch -p1 /path/to/patch"',
72
	'github_user' => 'ralfbecker',	// Github user for following token
73
	'github_token' => '',	// Github repo personal access token from above user
74
);
75
76
// process config from command line
77
$argv = $_SERVER['argv'];
78
$prog = array_shift($argv);
79
80
while(($arg = array_shift($argv)))
81
{
82
	if ($arg == '-v' || $arg == '--verbose')
83
	{
84
		++$verbose;
85
	}
86
	elseif($arg == '-h' || $arg == '--help')
87
	{
88
		if (in_array('editchangelog', $config['skip']) || !in_array('editchangelog', $config['run']))
89
		{
90
			$config['changelog'] = parse_current_changelog(true);
91
		}
92
		usage();
93
	}
94
	elseif(substr($arg,0,2) == '--' && isset($config[$name=substr($arg,2)]))
95
	{
96
		$value = array_shift($argv);
97
		switch($name)
98
		{
99
			case 'extra':	// stored as array and allow to add/delete items with +/- prefix
100
			case 'types':
101
			case 'skip':
102
			case 'run':
103
			case 'types':
104
			case 'add-all':
105
			case 'modules':
106
				$op = '=';
107
				if (in_array($value[0], array('+', '-')))
108
				{
109
					$op = $value[0];
110
					$value = substr($value, 1);
111
				}
112
				if (in_array($value[0], array('[', '{')) && ($json = json_decode($value, true)))
113
				{
114
					$value = $json;
115
				}
116
				else
117
				{
118
					$value = array_unique(preg_split('/[ ,]+/', $value));
119
				}
120
				switch($op)
121
				{
122
					case '+':
123
						$config[$name] = array_unique(array_merge($config[$name], $value));
124
						break;
125
					case '-':
126
						$config[$name] = array_diff($config[$name], $value);
127
						break;
128
					default:
129
						$config[$name] = $value;
130
				}
131
				break;
132
133
			case 'svntag':
134
			case 'tag':
135
			case 'release':
136
			case 'copychangelog':
137
				$config[$name] = $value;
138
				if ($value) array_unshift($config['run'],$name);
139
				break;
140
141
			case 'editchangelog':
142
				$config[$name] = $value ? $value : true;
143
				if (!in_array('editchangelog',$config['run']))
144
				{
145
					array_unshift($config['run'],'editchangelog');
146
				}
147
				break;
148
149
			case 'obs':
150
				if (!is_dir($value))
151
				{
152
					usage("Path '$value' not found!");
153
				}
154
				if (!in_array('obs',$config['run'])) $config['run'][] = 'obs';
155
				// fall through
156
			default:
157
				$config[$name] = $value;
158
				break;
159
		}
160
	}
161
	else
162
	{
163
		usage("Unknown argument '$arg'!");
164
	}
165
}
166
if ($verbose > 1)
167
{
168
	echo "Using following config:\n".json_encode($config, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES)."\n\n";
169
}
170
$svn = $config['svn'];
171
172
$run = array_diff($config['run'],$config['skip']);
173
174
// if we dont edit the changelog, set packaging from changelog
175
if (!in_array('editchangelog', $run))
176
{
177
	parse_current_changelog(true);
178
}
179
foreach($run as $func)
180
{
181
	chdir(dirname(__FILE__));	// make relative filenames work, if other command changes dir
182
	call_user_func('do_'.$func);
183
}
184
185
/**
186
 * Read changelog for given branch from (last) tag or given revision from svn
187
 *
188
 * @param string $_path relativ path to repo starting with $config['aliasdir']
189
 * @param string $log_pattern =null	a preg regular expression or start of line a log message must match, to be returned
190
 * 	if regular perl regular expression given only first expression in brackets \\1 is used,
191
 * 	for a start of line match, only the first line is used, otherwise whole message is used
192
 * @param string& $last_tag =null from which tag on to query logs
193
 * @param string $prefix ='* ' prefix, which if not presend should be added to all log messages
194
 * @return string with changelog
195
 */
196
function get_changelog_from_git($_path, $log_pattern=null, &$last_tag=null, $prefix='* ')
197
{
198
	//echo __FUNCTION__."('$branch_url','$log_pattern','$revision','$prefix')\n";
199
	global $config;
200
201
	$path = str_replace($config['aliasdir'], $config['checkoutdir'], $_path);
202
	if (!file_exists($path) || !is_dir($path) || !file_exists($path.'/.git'))
203
	{
204
		throw new Exception("$path is not a git repository!");
205
	}
206
	if (empty($last_tag))
207
	{
208
		$last_tag = get_last_git_tag();
209
	}
210
211
	$cmd = $config['git'].' log '.escapeshellarg($last_tag.'..HEAD');
212
	if (getcwd() != $path) $cmd = 'cd '.$path.'; '.$cmd;
213
	$output = null;
214
	run_cmd($cmd, $output);
215
216
	$changelog = '';
217
	foreach($output as $line)
0 ignored issues
show
Bug introduced by
The expression $output of type null|array 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...
218
	{
219
		if (substr($line, 0, 4) == "    " && ($msg = _match_log_pattern(substr($line, 4), $log_pattern, $prefix)))
220
		{
221
			$changelog .= $msg."\n";
222
		}
223
	}
224
	return $changelog;
225
}
226
227
/**
228
 * Get module path (starting with $config['aliasdir']) per repo from .mrconfig for svn and git
229
 *
230
 * @return array with $repro_url => $path => $url, eg. array(
231
 *		"[email protected]:EGroupware/egroupware.git" => array(
232
 *			"egroupware" => "[email protected]:EGroupware/egroupware.git"),
233
 *		"[email protected]:EGroupware/tracker.git" => array(
234
 *			"egroupware/tracker" => "[email protected]:EGroupware/tracker.git"),
235
 *		"svn+ssh://[email protected]/stylite" => array(
236
 *			"egroupware/stylite] => svn+ssh://[email protected]/stylite/branches/14.2/stylite",
237
 *			"egroupware/esyncpro] => svn+ssh://[email protected]/stylite/branches/14.2/esyncpro",
238
 */
239
function get_modules_per_repo()
240
{
241
	global $config, $verbose;
242
243
	if ($verbose) echo "Get modules from .mrconfig in checkoutdir $config[checkoutdir]\n";
244
245
	if (!is_dir($config['checkoutdir']))
246
	{
247
		throw new Exception("checkout directory '{$config['checkoutdir']} does NOT exists or is NO directory!");
248
	}
249
	if (!($mrconfig = file_get_contents($path=$config['checkoutdir'].'/.mrconfig')))
250
	{
251
		throw new Exception("$path not found!");
252
	}
253
	$module = $baseurl = null;
254
	$modules = array();
255
	foreach(explode("\n", $mrconfig) as $line)
256
	{
257
		$matches = null;
258
		if (isset($baseurl))
259
		{
260
			$line = str_replace("\$(git config --get remote.origin.url|sed 's|/egroupware.git||')", $baseurl, $line);
261
		}
262
		if ($line && $line[0] == '[' && preg_match('/^\[([^]]*)\]/', $line, $matches))
263
		{
264
			if (in_array($matches[1], array('DEFAULT', 'api/js/ckeditor', 'api/src/Accounts/Ads', 'phpgwapi/js/ckeditor', 'phpgwapi/inc/adldap')))
265
			{
266
				$module = null;
267
				continue;
268
			}
269
			$module = (string)$matches[1];
270
		}
271
		elseif (isset($module) && preg_match('/^checkout\s*=\s*(git\s+clone\s+(-b\s+[0-9.]+\s+)?((git|http)[^ ]+)|svn\s+checkout\s+((svn|http)[^ ]+))/', $line, $matches))
272
		{
273
			$repo = $url = substr($matches[1], 0, 3) == 'svn' ? $matches[5] : $matches[3];
274
			if (substr($matches[1], 0, 3) == 'svn') $repo = preg_replace('#/(trunk|branches)/.*$#', '', $repo);
275
			$modules[$repo][$config['aliasdir'].($module ? '/'.$module : '')] = $url;
276
			if ($module === '' && !isset($baseurl)) $baseurl = str_replace('/egroupware.git', '', $url);
277
		}
278
	}
279
	if ($verbose) print_r($modules);
280
	return $modules;
281
}
282
283
/**
284
 * Get commit of last git tag matching a given pattern
285
 *
286
 * @return string name of last tag matching $config['version'].'.*'
287
 */
288
function get_last_git_tag()
289
{
290
	global $config;
291
292
	if (!is_dir($config['checkoutdir']))
293
	{
294
		throw new Exception("checkout directory '{$config['checkoutdir']} does NOT exists or is NO directory!");
295
	}
296
	chdir($config['checkoutdir']);
297
298
	$cmd = $config['git'].' tag -l '.escapeshellarg($config['version'].'.*');
299
	$output = null;
300
	run_cmd($cmd, $output);
301
302
	return trim(array_pop($output));
303
}
304
305
/**
306
 * Checkout or update EGroupware
307
 *
308
 * Ensures an existing checkout is from the correct branch! Otherwise it get's deleted
309
 */
310
function do_checkout()
311
{
312
	global $config;
313
314
	echo "Starting checkout/update\n";
315
	if (!file_exists($config['checkoutdir']))
316
	{
317
		$cmd = $config['git'].' clone '.(!empty($config['branch']) ? ' -b '.$config['branch'] : '').
318
			' [email protected]:EGroupware/egroupware.git '.$config['checkoutdir'];
319
		run_cmd($cmd);
320
		run_cmd('mr up');	// need to run mr up twice for new checkout, because chained .mrconfig wont run first time (because not there yet!)
321
	}
322 View Code Duplication
	elseif (!is_dir($config['checkoutdir']) || !is_writable($config['checkoutdir']))
323
	{
324
		throw new Exception("svn checkout directory '{$config['checkoutdir']} exists and is NO directory or NOT writable!");
325
	}
326
	chdir($config['checkoutdir']);
327
328
	run_cmd('mr up');
329
}
330
331
/**
332
 * Create a tag using mr in svn or git for current checked out branch
333
 */
334
function do_tag()
335
{
336
	global $config;
337
338
	if (!is_dir($config['checkoutdir']))
339
	{
340
		throw new Exception("checkout directory '{$config['checkoutdir']} does NOT exists or is NO directory!");
341
	}
342
	chdir($config['checkoutdir']);
343
344
	$config['tag'] = config_translate('tag');	// allow to use config vars like $version in tag
345
346
	if (empty($config['tag'])) return;	// otherwise we copy everything in svn root!
347
348
	echo "Creating tag $config[tag]\n";
349
350
	$cmd = $config['mr'].' tag '.escapeshellarg($config['tag']).' '.escapeshellarg('Creating '.$config['tag']);
351
	run_cmd($cmd);
352
}
353
354
/**
355
 * Release sources by rsync'ing them to a distribution / download directory
356
 */
357
function do_release()
358
{
359
	global $config,$verbose;
360
361
	// push local changes to Github incl. tags
362
	if ($verbose) echo "Pushing changes and tags\n";
363
	chdir($config['checkoutdir']);
364
	run_cmd($config['mr']. ' up');		// in case someone else pushed something
365
	chdir($config['checkoutdir']);
366
	run_cmd($config['git'].' push');	// regular commits like changelog
367
	$tag = config_translate('tag');
368
	run_cmd($config['mr']. ' push origin '.$tag);	// pushing tags in all apps
369
370
	if (empty($config['github_user']) || empty($config['github_token']))
371
	{
372
		throw new Exception("No personal Github user or access token specified (--github_token)!");
373
	}
374
	if (empty($config['changelog']))
375
	{
376
		$config['changelog'] = parse_current_changelog();
377
	}
378
	$data = array(
379
		'tag_name' => $tag,
380
		'name' => $tag,
381
		'target_commitish' => $config['branch'],
382
		'body' => $config['changelog'],
383
	);
384
	$response = github_api("/repos/EGroupware/egroupware/releases", $data);
385
	$upload_url = preg_replace('/{\?[^}]+}$/', '', $response['upload_url']);	// remove {?name,label} template
386
387
	$archives = $config['sourcedir'].'/*egroupware-epl-'.$config['version'].'.'.$config['packaging'].'*';
388
389
	foreach(glob($archives) as $path)
390
	{
391
		$label = null;
392
		if (substr($path, -4) == '.zip')
393
		{
394
			$content_type = 'application/zip';
395
		}
396
		elseif(substr($path, -7) == '.tar.gz')
397
		{
398
			$content_type = 'application/x-gzip';
399
		}
400
		elseif(substr($path, -8) == '.tar.bz2')
401
		{
402
			$content_type = 'application/x-bzip2';
403
		}
404
		elseif(substr($path, -8) == '.txt.asc')
405
		{
406
			$content_type = 'text/plain';
407
			$label = 'Signed hashes of downloads';
408
		}
409
		else
410
		{
411
			continue;
412
		}
413
		$name = basename($path);
414
		github_api($upload_url, array(
415
			'name' => $name,
416
			'label' => isset($label) ? $label : $name,
417
		), 'FILE', $path, $content_type);
418
	}
419
420
	if (!empty($config['release']))
421
	{
422
		$target = config_translate('release');	// allow to use config vars like $svnbranch in module
423
		$cmd = $config['rsync'].' '.$archives.' '.$target;
424
		if ($verbose) echo $cmd."\n";
425
		passthru($cmd);
426
	}
427
}
428
429
/**
430
 * Sending a Github API request
431
 *
432
 * @param string $_url url of just path where to send request to (https://api.github.com is added automatic)
433
 * @param string|array $data payload, array get automatic added as get-parameter or json_encoded for POST
434
 * @param string $method ='POST'
435
 * @param string $upload =null path of file to upload, payload for request with $method='FILE'
436
 * @param string $content_type =null
437
 * @throws Exception
438
 * @return array with response
439
 */
440
function github_api($_url, $data, $method='POST', $upload=null, $content_type=null)
441
{
442
	global $config, $verbose;
443
444
	$url = $_url[0] == '/' ? 'https://api.github.com'.$_url : $_url;
445
	$c = curl_init();
446
	curl_setopt($c, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
447
	curl_setopt($c, CURLOPT_USERPWD, $config['github_user'].':'.$config['github_token']);
448
	curl_setopt($c, CURLOPT_RETURNTRANSFER, true);
449
	curl_setopt($c, CURLOPT_USERAGENT, basename(__FILE__));
450
	curl_setopt($c, CURLOPT_TIMEOUT, 240);
451
	curl_setopt($c, CURLOPT_FOLLOWLOCATION, true);
452
453
	switch($method)
454
	{
455
		case 'POST':
456
			curl_setopt($c, CURLOPT_POST, true);
457
			if (is_array($data)) $data = json_encode($data, JSON_FORCE_OBJECT);
458
			curl_setopt($c, CURLOPT_POSTFIELDS, $data);
459
			break;
460
		case 'GET':
461
			if(count($data)) $url .= '?' . http_build_query($data);
462
			break;
463
		case 'FILE':
464
			curl_setopt($c, CURLOPT_HTTPHEADER, array("Content-type: $content_type"));
465
			curl_setopt($c, CURLOPT_POST, true);
466
			curl_setopt($c, CURLOPT_POSTFIELDS, file_get_contents($upload));
467
			if(count($data)) $url .= '?' . http_build_query($data);
468
			break;
469
		default:
470
			throw new Exception(__FUNCTION__.": Unknown/unimplemented method=$method!");
471
	}
472
	curl_setopt($c, CURLOPT_URL, $url);
473
474
	if (is_string($data)) $short_data = strlen($data) > 100 ? substr($data, 0, 100).' ...' : $data;
475
	if ($verbose) echo "Sending $method request to $url ".(isset($short_data)&&$method!='GET'?$short_data:'')."\n";
476
477
	if (($response = curl_exec($c)) === false)
478
	{
479
		// run failed request again to display response including headers
480
		curl_setopt($c, CURLOPT_HEADER, true);
481
		curl_setopt($c, CURLOPT_RETURNTRANSFER, false);
482
		curl_exec($c);
483
		throw new Exception("$method request to $url failed ".(isset($short_data)&&$method!='GET'?$short_data:''));
484
	}
485
486
	if ($verbose) echo (strlen($response) > 200 ? substr($response, 0, 200).' ...' : $response)."\n";
487
488
	curl_close($c);
489
490
	return json_decode($response, true);
491
}
492
493
/**
494
 * Fetch a config value allowing to use config vars like $svnbranch in it
495
 *
496
 * @param string $name
497
 * @param string $value =null value to use, default $config[$name]
498
 */
499
function config_translate($name, $value=null)
500
{
501
	global $config;
502
503
	if (!isset($value)) $value = $config[$name];
504
	if (is_string($value) && strpos($value, '$') !== false)
505
	{
506
		$translate = array();
507
		foreach($config as $n => $val)
508
		{
509
			if (is_string($val)) $translate['$'.$n] = $val;
510
		}
511
		$value = strtr($value, $translate);
512
	}
513
	return $value;
514
}
515
516
/**
517
 * Copy changelog by rsync'ing it to a distribution / download directory
518
 */
519
function do_copychangelog()
520
{
521
	global $config;
522
523
	$changelog = __DIR__.'/debian.changes';
524
	$cmd = $config['rsync'].' '.$changelog.' '.config_translate('copychangelog');
525
	passthru($cmd);
526
}
527
528
/**
529
 * Query changelog and let user edit it
530
 */
531
function do_editchangelog()
532
{
533
	global $config,$svn;
534
535
	echo "Querying changelog from Git/SVN\n";
536
	if (!isset($config['modules']))
537
	{
538
		$config['modules'] = get_modules_per_repo();
539
	}
540
	// query changelog per repo
541
	$changelog = '';
542
	$last_tag = null;
543
	foreach($config['modules'] as $branch_url => $modules)
544
	{
545
		$revision = null;
546
		if (substr($branch_url, -4) == '.git')
547
		{
548
			list($path) = each($modules);
549
			$changelog .= get_changelog_from_git($path, $config['editchangelog'], $last_tag);
550
		}
551
		else
552
		{
553
			$changelog .= get_changelog_from_svn($branch_url, $config['editchangelog'], $revision);
554
		}
555
	}
556
	$logfile = tempnam('/tmp','checkout-build-archives');
557
	file_put_contents($logfile,$changelog);
558
	$cmd = $config['editor'].' '.escapeshellarg($logfile);
559
	passthru($cmd);
560
	$config['changelog'] = file_get_contents($logfile);
561
	// remove trailing newlines
562
	while (substr($config['changelog'],-1) == "\n")
563
	{
564
		$config['changelog'] = substr($config['changelog'],0,-1);
565
	}
566
	// allow user to abort, by deleting the changelog
567
	if (strlen($config['changelog']) <= 2)
568
	{
569
		die("\nChangelog must not be empty --> aborting\n\n");
570
	}
571
	// commit changelog
572
	$changelog = $config['checkoutdir'].'/doc/rpm-build/debian.changes';
573
	if (!file_exists($changelog))
574
	{
575
		throw new Exception("Changelog '$changelog' not found!");
576
	}
577
	file_put_contents($changelog, update_changelog(file_get_contents($changelog)));
578
	if (file_exists($config['checkoutdir'].'/.git'))
579
	{
580
		$cmd = $config['git']." commit -m 'Changelog for $config[version].$config[packaging]' ".$changelog;
581
	}
582
	else
583
	{
584
		$cmd = $svn." commit -m 'Changelog for $config[version].$config[packaging]' ".$changelog;
585
	}
586
	run_cmd($cmd);
587
588
	// update obs changelogs (so all changlogs are updated in case of a later error and changelog step can be skiped)
589
	do_obs(true);	// true: only update debian.changes in obs checkouts
590
}
591
592
/**
593
 * Read changelog for given branch from (last) tag or given revision from svn
594
 *
595
 * @param string $branch_url ='svn+ssh://[email protected]/egroupware/branches/Stylite-EPL-10.1'
596
 * @param string $log_pattern =null	a preg regular expression or start of line a log message must match, to be returned
597
 * 	if regular perl regular expression given only first expression in brackets \\1 is used,
598
 * 	for a start of line match, only the first line is used, otherwise whole message is used
599
 * @param string& $revision =null from which to HEAD the log should be retrieved, default search revision of latest tag in ^/tags
600
 * @param string $prefix ='* ' prefix, which if not presend should be added to all log messages
601
 * @return string with changelog
602
 */
603
function get_changelog_from_svn($branch_url, $log_pattern=null, &$revision=null, $prefix='* ')
604
{
605
	//echo __FUNCTION__."('$branch_url','$log_pattern','$revision','$prefix')\n";
606
	global $config,$verbose,$svn;
607
608
	if (is_null($revision))
609
	{
610
		list($tags_url,$branch) = preg_split('#/(branches/|trunk)#',$branch_url);
611
		if (empty($branch)) $branch = $config['version'];
612
		$tags_url .= '/tags';
613
		$pattern='|/tags/('.preg_quote($config['version'], '|').'\.[0-9.]+)|';
614
		$matches = null;
615
		$revision = get_last_svn_tag($tags_url,$pattern,$matches);
616
		$tag = $matches[1];
617
	}
618
	elseif(!is_numeric($revision))
619
	{
620
		$revision = get_last_svn_tag($tags_url,$tag=$revision);
0 ignored issues
show
Bug introduced by
The variable $tags_url seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
621
	}
622
	$cmd = $svn.' log --xml -r '.escapeshellarg($revision.':HEAD').' '.escapeshellarg($branch_url);
623
	if (($v = $verbose))
624
	{
625
		echo "Querying SVN for log from r$revision".($tag ? " ($tag)" : '').":\n$cmd\n";
626
		$verbose = false;	// otherwise no $output!
627
	}
628
	$output = array();
629
	run_cmd($cmd,$output);
630
	$verbose = $v;
631
	array_shift($output);	// remove the command
632
633
	$xml = simplexml_load_string($output=implode("\n",$output));
634
	$message = '';
635
	foreach($xml as $log)
636
	{
637
		if (!($msg = _match_log_pattern($log->msg, $log_pattern, $prefix))) continue;	// no match --> ignore
638
639
		$message .= $msg."\n";
640
	}
641
	if ($verbose) echo $message;
642
643
	return $message;
644
}
645
646
/**
647
 * Return first row of matching log lines always prefixed with $prefix
648
 *
649
 * @param string $msg whole log message
650
 * @param string $log_pattern
651
 * @param string $prefix ='* '
652
 * @return string
653
 */
654
function _match_log_pattern($msg, $log_pattern, $prefix='* ')
655
{
656
	$pattern_len = strlen($log_pattern);
657
	$prefix_len = strlen($prefix);
658
659
	$matches = null;
660
	if ($log_pattern[0] == '/' && preg_match($log_pattern,$msg,$matches))
661
	{
662
		$msg = $matches[1];
663
	}
664
	elseif($log_pattern && $log_pattern[0] != '/' && substr($msg,0,$pattern_len) == $log_pattern)
665
	{
666
		list($msg) = explode("\n",$msg);
667
	}
668
	elseif($log_pattern)
669
	{
670
		return null;
671
	}
672
	if ($prefix_len && substr($msg,0,$prefix_len) != $prefix) $msg = $prefix.$msg;
673
674
	return $msg;
675
}
676
677
/**
678
 * Get revision of last svn tag matching a given pattern in the log message
679
 *
680
 * @param string $tags_url
681
 * @param string $pattern which has to be contained in the log message (NOT the tag itself)
682
 * 	or (perl) regular expression against which log message is matched
683
 * @param array &$matches=null on return matches of preg_match
684
 * @return int revision of last svn tag matching pattern
685
 */
686
function get_last_svn_tag($tags_url,$pattern,&$matches=null)
687
{
688
	global $verbose,$svn;
689
690
	$cmd = $svn.' log --xml --limit 40 -v '.escapeshellarg($tags_url);
691
	if (($v = $verbose))
692
	{
693
		echo "Querying SVN for last tags\n$cmd\n";
694
		$verbose = false;	// otherwise no $output!
695
	}
696
	$output = array();
697
	run_cmd($cmd,$output);
698
	$verbose = $v;
699
	array_shift($output);	// remove the command
700
701
	$xml = simplexml_load_string($output=implode("\n",$output));
702
	$is_regexp = $pattern[0] == substr($pattern, -1);
703
	foreach($xml as $log)
704
	{
705
		//print_r($log);
706
		if (!$is_regexp && strpos($log->paths->path, $pattern) !== false ||
707
			$is_regexp && preg_match($pattern, $log->paths->path, $matches))
708
		{
709
			if ($verbose) echo "Revision {$log['revision']} matches".($matches?': '.$matches[1] : '')."\n";
710
			return (int)$log['revision'];
711
		}
712
	}
713
	return null;
714
}
715
716
/**
717
 * Copy archive files to obs checkout and commit them
718
 *
719
 * @param boolean $only_update_changelog =false true update debian.changes, but nothing else, nor commit it
720
 */
721
function do_obs($only_update_changelog=false)
722
{
723
	global $config,$verbose;
724
725
	if (!is_dir($config['obs']))
726
	{
727
		usage("Path '$config[obs]' not found!");
728
	}
729
	if ($verbose) echo $only_update_changelog ? "Updating OBS changelogs\n" : "Updating OBS checkout\n";
730
	run_cmd('osc up '.$config['obs']);
731
732
	$n = 0;
733
	foreach(new RecursiveIteratorIterator(new RecursiveDirectoryIterator($config['obs'])) as $path)
734
	{
735
		if (basename(dirname($path)) == '.osc' ||
736
			!preg_match('/\/('.preg_quote($config['packagename']).
737
				($config['obs_package_alias'] ? '|'.preg_quote($config['obs_package_alias']) : '').
738
				')[a-z-]*-('.preg_quote($config['version']).'|14.2|trunk)/',$path))
739
		{
740
			continue;
741
		}
742
		$matches = null;
743
		if (preg_match('/\/('.preg_quote($config['packagename']).'[a-z-]*)-'.preg_quote($config['version']).'\.[0-9.]+[0-9](\.tar\.(gz|bz2))$/',$path,$matches) &&
744
			file_exists($new_name=$config['sourcedir'].'/'.$matches[1].'-'.$config['version'].'.'.$config['packaging'].$matches[2]))
745
		{
746
			if (basename($path) != basename($new_name))
747
			{
748
				unlink($path);
749
				if ($verbose) echo "rm $path\n";
750
			}
751
			copy($new_name,dirname($path).'/'.basename($new_name));
752
			if ($verbose) echo "cp $new_name ".dirname($path)."/\n";
753
			++$n;
754
		}
755
		// if we have no changelog (eg. because commands run separate), try parsing it from changelog file
756
		if (empty($config['changelog']))
757
		{
758
			$config['changelog'] = parse_current_changelog();
759
		}
760
		// updating dsc, spec and changelog files
761
		if (!$only_update_changelog && (substr($path,-4) == '.dsc' || substr($path,-5) == '.spec') ||
762
			!empty($config['changelog']) && basename($path) == 'debian.changes')
763
		{
764
			$content = $content_was = file_get_contents($path);
765
766
			if (substr($path,-4) == '.dsc' || substr($path,-5) == '.spec')
767
			{
768
				$content = preg_replace('/^Version: '.preg_quote($config['version']).'\.[0-9.]+[0-9]/m','Version: '.$config['version'].'.'.$config['packaging'],$content);
769
			}
770
			if (substr($path,-4) == '.dsc')
771
			{
772
				$content = preg_replace('/^(Debtransform-Tar: '.preg_quote($config['packagename']).'[a-z-]*)-'.
773
					preg_quote($config['version']).'\.[0-9.]+[0-9](\.tar\.(gz|bz2))$/m',
774
					'\\1-'.$config['version'].'.'.$config['packaging'].'\\2',$content);
775
			}
776
			if (basename($path) == 'debian.changes' && strpos($content,$config['version'].'.'.$config['packaging']) === false)
777
			{
778
				$content = update_changelog($content);
779
			}
780
			if (!empty($config['changelog']) && substr($path,-5) == '.spec' &&
781
				($pos_changelog = strpos($content,'%changelog')) !== false)
782
			{
783
				$pos_changelog += strlen("%changelog\n");
784
				$content = substr($content,0,$pos_changelog).' *'.date('D M d Y').' '.$config['changelog_packager']."\n".
785
					$config['changelog']."\n".substr($content,$pos_changelog);
786
			}
787
			if ($content != $content_was)
788
			{
789
				file_put_contents($path,$content);
790
				if ($verbose) echo "Updated $path\n";
791
				++$n;
792
			}
793
		}
794
	}
795
	if ($n && !$only_update_changelog)
796
	{
797
		echo "$n files updated in OBS checkout ($config[obs]), commiting them now...\n";
798
		//run_cmd('osc status '.$config['obs']);
799
		run_cmd('osc addremove '.$config['obs'].'/*');
800
		run_cmd('osc commit -m '.escapeshellarg('Version: '.$config['version'].'.'.$config['packaging'].":\n".$config['changelog']).' '.$config['obs']);
801
	}
802
}
803
804
/**
805
 * Parse current changelog from debian.changes file
806
 *
807
 * @param boolean $set_packaging =false true: set packaging from last changelog entry
808
 * @return string changelog entries without header and footer lines
809
 */
810
function parse_current_changelog($set_packaging=false)
811
{
812
	global $config;
813
814
	$changelog = file_get_contents($config['checkoutdir'].'/doc/rpm-build/debian.changes');
815
	$lines = explode("\n", $changelog, 100);
816
	$matches = null;
817
	foreach($lines as $n => $line)
818
	{
819
		if (preg_match($preg='/^'.preg_quote($config['packagename']).' \('.preg_quote($config['version']).'\.'.
820
			($set_packaging ? '([0-9]+)' : preg_quote($config['packaging'])).'/', $line, $matches))
821
		{
822
			if ($set_packaging)
823
			{
824
				$config['packaging'] = $matches[1];
825
			}
826
			$n += empty($lines[$n+1]) ? 2 : 1;	// overead empty line behind header
827
			$logentry = '';
828
			while($lines[$n])	// entry is terminated by empty line
829
			{
830
				$logentry .= (substr($lines[$n], 0, 2) == '  ' ? substr($lines[$n], 2) : $lines[$n])."\n";
831
				++$n;
832
			}
833
			return substr($logentry, 0, -1);	// remove training "\n"
834
		}
835
	}
836
	return null;	// paragraph for current version NOT found
837
}
838
839
/**
840
 * Update content of debian changelog file with new content from $config[changelog]
841
 *
842
 * @param string $content existing changelog content
843
 * @return string updated changelog content
844
 */
845
function update_changelog($content)
846
{
847
	global $config;
848
849
	list($header) = explode("\n", $content);
850
	$new_header = preg_replace('/\('.preg_quote($config['version']).'\.[0-9.]+[0-9](.*)\)/','('.$config['version'].'.'.$config['packaging'].'\\1)', $header);
851
	if (substr($config['changelog'],0,2) != '  ') $config['changelog'] = '  '.implode("\n  ",explode("\n",$config['changelog']));
852
	$content = $new_header."\n\n".$config['changelog'].
853
		"\n\n -- ".$config['changelog_packager'].'  '.date('r')."\n\n".$content;
854
855
	return $content;
856
}
857
858
/**
859
 * Sign sha1sum file
860
 */
861
function do_sign()
862
{
863
	global $config;
864
865 View Code Duplication
	if (substr($config['sourcedir'],0,2) == '~/')	// sha1_file cant deal with '~/rpm'
866
	{
867
		$config['sourcedir'] = getenv('HOME').substr($config['sourcedir'],1);
868
	}
869
	$sumsfile = $config['sourcedir'].'/sha1sum-'.$config['packagename'].'-'.$config['version'].'.'.$config['packaging'].'.txt';
870
871
	if (!file_exists($sumsfile))
872
	{
873
		echo "sha1sum file '$sumsfile' not found!\n";
874
		return;
875
	}
876
	// signing it
877
	if (empty($config['gpg']) || !file_exists($config['gpg']))
878
	{
879
		if (!empty($config['gpg'])) echo "{$config['gpg']} not found --> skipping signing sha1sum file!\n";
880
		return;
881
	}
882
	echo "Signing sha1sum file:\n";
883
	if (file_exists($sumsfile.'.asc')) unlink($sumsfile.'.asc');
884
	$cmd = $config['gpg'].' --local-user '.$config['packager'].' --clearsign '.$sumsfile;
885
	run_cmd($cmd);
886
	unlink($sumsfile);	// delete the unsigned file
887
}
888
889
/**
890
 * Create archives
891
 */
892
function do_create()
893
{
894
	global $config;
895
896
	if (!file_exists($config['sourcedir'])) mkdir($config['sourcedir'],0755,true);
897 View Code Duplication
	if (substr($config['sourcedir'],0,2) == '~/')	// sha1_file cant deal with '~/rpm'
898
	{
899
		$config['sourcedir'] = getenv('HOME').substr($config['sourcedir'],1);
900
	}
901
	$sumsfile = $config['sourcedir'].'/sha1sum-'.$config['packagename'].'-'.$config['version'].'.'.$config['packaging'].'.txt';
902
	$sums = '';
903
904
	chdir($config['egw_buildroot']);
905
906
	if($config['extra'])
907
	{
908
		$exclude = $exclude_all = array();
909
		foreach($config['extra'] as $name => $modules)
910
		{
911
			foreach((array)$modules as $module)
912
			{
913
				$exclude[] = basename($module);
914
				if (!empty($config['all-add']) && !in_array($module, $config['all-add']) && (is_int($name) || !in_array($name, $config['all-add'])))
915
				{
916
					$exclude_all[] = basename($module);
917
				}
918
			}
919
		}
920
		$exclude_extra = ' --exclude=egroupware/'.implode(' --exclude=egroupware/', $exclude);
921
		$exclude_all_extra =  $exclude_all ? ' --exclude=egroupware/'.implode(' --exclude=egroupware/', $exclude_all) : '';
922
	}
923
	foreach($config['types'] as $type)
924
	{
925
		echo "Creating $type archives\n";
926
		$tar_type = $type == 'tar.bz2' ? 'j' : 'z';
927
928
		$file = $config['sourcedir'].'/'.$config['packagename'].'-'.$config['version'].'.'.$config['packaging'].'.'.$type;
929
		switch($type)
930
		{
931
			case 'all.tar.bz2':	// single tar-ball for debian builds not easily supporting to use multiple
932
				$file = $config['sourcedir'].'/'.$config['packagename'].'-all-'.$config['version'].'.'.$config['packaging'].'.tar';
933
				$cmd = $config['tar'].' --owner=root --group=root -cf '.$file.$exclude_all_extra.' egroupware';
934
				if (!empty($config['all-add']))
935
				{
936
					foreach((array)$config['all-add'] as $add)
937
					{
938
						if (substr($add, -4) != '.tar') continue;	// probably a module
939
						if (!($tar = realpath($add))) throw new Exception("File '$add' not found!");
940
						$cmd .= '; '.$config['tar'].' --owner=root --group=root -Af '.$file.' '.$tar;
941
					}
942
				}
943
				if (file_exists($file.'.bz2')) $cmd .= '; rm -f '.$file.'.bz2';
944
				$cmd .= '; '.$config['bzip2'].' '.$file;
945
				// run cmd now and continue without adding all tar-ball to sums, as we dont want to publish it
946
				run_cmd($cmd);
947
				continue 2;
948
			case 'tar.bz2':
949 View Code Duplication
			case 'tar.gz':
950
				$cmd = $config['tar'].' --owner=root --group=root -c'.$tar_type.'f '.$file.$exclude_extra.' egroupware';
951
				break;
952
			case 'zip':
953
				$cmd = file_exists($file) ? $config['rm'].' -f '.$file.'; ' : '';
954
				$cmd .= $config['mv'].' egroupware/'.implode(' egroupware/', $exclude).' . ;';
955
				$cmd .= $config['zip'].' -q -r -9 '.$file.' egroupware ;';
956
				$cmd .= $config['mv'].' '.implode(' ', $exclude).' egroupware';
957
				break;
958
		}
959
		run_cmd($cmd);
960
		$sums .= sha1_file($file)."\t".basename($file)."\n";
961
962
		foreach($config['extra'] as $name => $modules)
963
		{
964
			if (is_numeric($name)) $name = $modules;
965
			$dirs = ' egroupware/'.implode(' egroupware/', (array)$modules);
966
			$file = $config['sourcedir'].'/'.$config['packagename'].'-'.$name.'-'.$config['version'].'.'.$config['packaging'].'.'.$type;
967
			switch($type)
968
			{
969
				case 'all.tar.bz2':
970
					break;	// nothing to do
971
				case 'tar.bz2':
972 View Code Duplication
				case 'tar.gz':
973
					$cmd = $config['tar'].' --owner=root --group=root -c'.$tar_type.'f '.$file.$dirs;
974
					break;
975
				case 'zip':
976
					$cmd = file_exists($file) ? $config['rm'].' -f '.$file.'; ' : '';
977
					$cmd .= $config['zip'].' -q -r -9 '.$file.$dirs;
978
					break;
979
			}
980
			run_cmd($cmd);
981
			$sums .= sha1_file($file)."\t".basename($file)."\n";
982
		}
983
	}
984
	// writing sha1sum file
985
	file_put_contents($sumsfile,$sums);
986
}
987
988
/**
989
 * Scan checkout for viruses, if clamscan is installed (not fatal if not!)
990
 */
991
function do_virusscan()
992
{
993
	global $config,$verbose;
994
995
	if (!file_exists($config['clamscan']) || !is_executable($config['clamscan']))
996
	{
997
		echo "Virusscanner '$config[clamscan]' not found --> skipping virus scan!\n";
998
		return;
999
	}
1000
	// try updating virus database
1001
	if (file_exists($config['freshclam']))
1002
	{
1003
		echo "Updating virus signatures\n";
1004
		$cmd = '/usr/bin/sudo '.$config['freshclam'];
1005
		if (!$verbose && function_exists('posix_getuid') && posix_getuid()) echo $cmd."\n";
1006
		$output = null;
1007
		run_cmd($cmd,$output,1);	// 1 = ignore already up to date database
1008
	}
1009
	echo "Starting virusscan\n";
1010
	$cmd = $config['clamscan'].' --quiet -r '.$config['egw_buildroot'];
1011
	run_cmd($cmd);
1012
	echo "Virusscan successful (no viruses found).\n";
1013
}
1014
1015
/**
1016
 * Copy non .svn/.git parts to egw_buildroot and fix permissions and ownership
1017
 *
1018
 * We need to stash local modifications (currently only in egroupware main module) to revert eg. .mrconfig modifications
1019
 */
1020
function do_copy()
1021
{
1022
	global $config;
1023
1024
	// copy everything, but .svn dirs from checkoutdir to egw_buildroot
1025
	echo "Copying non-svn/git dirs to buildroot\n";
1026
1027
	if (!file_exists($config['egw_buildroot']))
1028
	{
1029
		run_cmd("mkdir -p $config[egw_buildroot]");
1030
	}
1031
1032
	// we need to stash uncommited changes like .mrconfig, before copying
1033
	if (file_exists($config['checkoutdir'].'/.git')) run_cmd("cd $config[checkoutdir]; git stash");
1034
1035
	try {
1036
		$cmd = '/usr/bin/rsync -r --delete --delete-excluded --exclude .svn --exclude .git\* --exclude .mrconfig --exclude node_modules/ '.$config['checkoutdir'].'/ '.$config['egw_buildroot'].'/'.$config['aliasdir'].'/';
1037
		run_cmd($cmd);
1038
	}
1039
	catch (Exception $e) {
1040
		// catch failures to pop stash, before throwing exception
1041
	}
1042
	if (file_exists($config['checkoutdir'].'/.git')) run_cmd("git stash pop");
1043
	if (isset($e)) throw $e;
1044
1045
	if (($cmd = config_translate('patchCmd')) && $cmd[0] != '#')
1046
	{
1047
		echo "Running $cmd\n";
1048
		run_cmd($cmd);
1049
	}
1050
	// fix permissions
1051
	echo "Fixing permissions\n";
1052
	chdir($config['egw_buildroot'].'/'.$config['aliasdir']);
1053
	run_cmd('/bin/chmod -R a-x,u=rwX,g=rX,o=rX .');
1054
	run_cmd('/bin/chmod +x */*cli.php phpgwapi/cron/*.php doc/rpm-build/*.php');
1055
}
1056
1057
/**
1058
 * Checkout or update EGroupware
1059
 *
1060
 * Ensures an existing checkout is from the correct branch! Otherwise it get's deleted
1061
 */
1062
function do_svncheckout()
1063
{
1064
	global $config,$svn;
1065
1066
	echo "Starting svn checkout/update\n";
1067
	if (!file_exists($config['checkoutdir']))
1068
	{
1069
		mkdir($config['checkoutdir'],0755,true);
1070
	}
1071 View Code Duplication
	elseif (!is_dir($config['checkoutdir']) || !is_writable($config['checkoutdir']))
1072
	{
1073
		throw new Exception("svn checkout directory '{$config['checkoutdir']} exists and is NO directory or NOT writable!");
1074
	}
1075
	chdir($config['checkoutdir']);
1076
1077
	// do we use a just created tag --> list of taged modules
1078
	if ($config['svntag'])
1079
	{
1080
		if (!isset($config['modules']))
1081
		{
1082
			get_modules_per_repo();
1083
		}
1084
		$config['svntag'] = config_translate('svntag');	// in case svntag command did not run, translate tag name
1085
1086
		if (file_exists($config['aliasdir']))
1087
		{
1088
			system('/bin/rm -rf .svn '.$config['aliasdir']);	// --> remove the whole checkout, as we dont implement switching tags
1089
			clearstatcache();
1090
		}
1091
		foreach($config['modules'] as $repo => $modules)
1092
		{
1093
			$cmd = $svn.' co ';
1094
			foreach($modules as $path => $url)
1095
			{
1096
				if ($path == $config['aliasdir'])
1097
				{
1098
					$cmd = $svn.' co '.$repo.'/'.$config['svntag'].'/'.$path;
1099
					run_cmd($cmd);
1100
					chdir($path);
1101
					$cmd = $svn.' co ';
1102
					continue;
1103
				}
1104
				if(file_exists($config['aliasdir']))
1105
				{
1106
					die("'egroupware' applications must be first one in externals!\n");
1107
				}
1108
				$cmd .= ' '.$repo.'/'.$config['svntag'].'/'.basename($path);
1109
			}
1110
			run_cmd($cmd);
1111
		}
1112
	}
1113
	// regular branch update, without tag
1114
	else
1115
	{
1116
		$svnbranch = $config['svnbase'].'/'.$config['svnbranch'];
1117
		if (file_exists($config['aliasdir']))
1118
		{
1119
			// check if correct branch
1120
			$cmd = 'LANG=C '.$svn.' info';
1121
			$output = $ret = null;
1122
			exec($cmd,$output,$ret);
1123
			foreach($output as $line)
0 ignored issues
show
Bug introduced by
The expression $output of type null|array 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...
1124
			{
1125
				if ($ret || substr($line,0,5) == 'URL: ')
1 ignored issue
show
Bug Best Practice introduced by
The expression $ret of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
1126
				{
1127
					$url = substr($line,5);
1128
					if ($ret || substr($url,0,strlen($svnbranch)) != $svnbranch)	// wrong branch (or no svn dir)
1 ignored issue
show
Bug Best Practice introduced by
The expression $ret of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
1129
					{
1130
						echo "Checkout is of wrong branch --> deleting it\n";
1131
						system('/bin/rm -rf .svn '.$config['aliasdir']);	// --> remove the whole checkout
1132
						clearstatcache();
1133
					}
1134
					break;
1135
				}
1136
			}
1137
		}
1138
		$url = $svnbranch.'/'.$config['svnalias'];
1139
		$cmd = $svn.' co '.$url.' .';
1140
		run_cmd($cmd);
1141
1142
		chdir($config['aliasdir']);
1143
		foreach($config['extra'] as $module)
1144
		{
1145
			$module = config_translate(null, $module);	// allow to use config vars like $svnbranch in module
1146
			$url = strpos($module,'://') === false ? $svnbranch.'/' : '';
1147
			$url .= $module;
1148
			$cmd = $svn.' co '.$url;
1149
			run_cmd($cmd);
1150
		}
1151
	}
1152
	// do composer install to fetch dependencies
1153
	if ($config['composer'])
1154
	{
1155
		run_cmd($config['composer']);
1156
	}
1157
	// run after-checkout command(s), eg. to purge source directories
1158
	run_cmd($config['after-checkout']);
1159
}
1160
1161
/**
1162
 * Get module path per svn repo from our config
1163
 *
1164
 * @return array with $repro_url => $path => $url, eg. array(
1165
 *		"svn+ssh://[email protected]/egroupware" => array(
1166
 *			"egroupware" => "svn+ssh://[email protected]/egroupware/branches/14.2/egroupware",
1167
 *			"egroupware/addressbook" => "svn+ssh://[email protected]/egroupware/branches/14.2/addressbook",
1168
 */
1169
function get_modules_per_svn_repo()
1170
{
1171
	global $config,$svn,$verbose;
1172
1173
	// process alias/externals
1174
	$svnbranch = $config['svnbase'].'/'.$config['svnbranch'];
1175
	$url = $svnbranch.'/'.$config['svnalias'];
1176
	$cmd = $svn.' propget svn:externals --strict '.$url;
1177
	if ($verbose) echo $cmd."\n";
1178
	$output = $ret = null;
1179
	exec($cmd,$output,$ret);
1180
	$config['modules'] = array();
1181
	foreach($output as $line)
0 ignored issues
show
Bug introduced by
The expression $output of type null|array 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...
1182
	{
1183
		$line = trim($line);
1184
		if (empty($line) || $line[0] == '#') continue;
1185
		list($path,$url) = preg_split('/[ \t\r\n]+/',$line);
1186
		$matches = null;
1187
		if (!preg_match('/([a-z+]+:\/\/[a-z@.]+\/[a-z]+)\/(branches|tags|trunk)/',$url,$matches)) die("Invalid SVN URL: $url\n");
1188
		$repo = $matches[1];
1189
		if ($repo == 'http://svn.egroupware.org/egroupware') $repo = 'svn+ssh://[email protected]/egroupware';
1190
		$config['modules'][$repo][$path] = $url;
1191
	}
1192
	// process extra modules
1193
	foreach($config['extra'] as $module)
1194
	{
1195
		$module = config_translate(null, $module);	// allow to use config vars like $svnbranch in module
1196
		$url = strpos($module,'://') === false ? $svnbranch.'/' : '';
1197
		$url .= $module;
1198
		if (strpos($module,'://') !== false) $module = basename($module);
1199
		if (!preg_match('/([a-z+]+:\/\/[a-z@.]+\/[a-z]+)\/(branches|tags|trunk)/',$url,$matches)) die("Invalid SVN URL: $url\n");
1200
		$repo = $matches[1];
1201
		if ($repo == 'http://svn.egroupware.org/egroupware') $repo = 'svn+ssh://[email protected]/egroupware';
1202
		$config['modules'][$repo][$config['aliasdir'].'/'.$module] = $url;
1203
	}
1204
	if ($verbose) print_r($config['modules']);
1205
	return $config['modules'];
1206
}
1207
1208
/**
1209
 * Create svn tag or branch
1210
 */
1211
function do_svntag()
1212
{
1213
	global $config,$svn;
1214
1215
	if (empty($config['svntag'])) return;	// otherwise we copy everything in svn root!
1216
1217
	$config['svntag'] = config_translate('svntag');	// allow to use config vars like $version in tag
1218
1219
	echo "Creating SVN tag $config[svntag]\n";
1220
	if (!isset($config['modules']))
1221
	{
1222
		get_modules_per_repo();
1223
	}
1224
	// create tags (per repo)
1225
	foreach($config['modules'] as $repo => $modules)
0 ignored issues
show
Bug introduced by
The expression $config['modules'] of type null is not traversable.
Loading history...
1226
	{
1227
		$cmd = $svn.' cp --parents -m '.escapeshellarg('Creating '.$config['svntag']).' '.implode(' ',$modules).' '.$repo.'/'.$config['svntag'].'/';
1228
		run_cmd($cmd);
1229
	}
1230
}
1231
1232
/**
1233
 * Runs given shell command, exists with error-code after echoing the output of the failed command (if not already running verbose)
1234
 *
1235
 * @param string $cmd
1236
 * @param array& $output=null $output of command, only if !$verbose !!!
1237
 * @param int|array $no_bailout =null exit code(s) to NOT bail out
1238
 * @return int exit code of $cmd
1239
 */
1240
function run_cmd($cmd,array &$output=null,$no_bailout=null)
1241
{
1242
	global $verbose;
1243
1244
	if ($verbose && func_num_args() == 1)
1245
	{
1246
		echo $cmd."\n";
1247
		$ret = null;
1248
		system($cmd,$ret);
1249
	}
1250
	else
1251
	{
1252
		$output[] = $cmd;
1253
		exec($cmd,$output,$ret);
1254
		if ($verbose) echo implode("\n",$output)."\n";
1255
	}
1256
	if ($ret && !in_array($ret,(array)$no_bailout))
1 ignored issue
show
Bug Best Practice introduced by
The expression $ret of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
1257
	{
1258
		if (!$verbose) echo implode("\n",$output)."\n";
1259
		throw new Exception("Error during '$cmd' --> aborting",$ret);
1260
	}
1261
	return $ret;
1262
}
1263
1264
/**
1265
 * Format array or other types as (one-line) string, eg. for error_log statements
1266
 *
1267
 * @param mixed $var variable to dump
1268
 * @return string
1269
 */
1270 View Code Duplication
function array2string($var)
1 ignored issue
show
Duplication introduced by
This function seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1271
{
1272
	switch (($type = gettype($var)))
1273
	{
1274
		case 'boolean':
1275
			return $var ? 'TRUE' : 'FALSE';
1276
		case 'string':
1277
			return "'$var'";
1278
		case 'integer':
1279
		case 'double':
1280
		case 'resource':
1281
			return $var;
1282
		case 'NULL':
1283
			return 'NULL';
1284
		case 'object':
1285
		case 'array':
1286
			return str_replace(array("\n",'    '/*,'Array'*/),'',print_r($var,true));
1287
	}
1288
	return 'UNKNOWN TYPE!';
1289
}
1290
1291
/**
1292
 * Give usage information and an optional error-message, before stoping program execution with exit-code 90 or 0
1293
 *
1294
 * @param string $error =null optional error-message
1295
 */
1296
function usage($error=null)
0 ignored issues
show
Best Practice introduced by
The function usage() has been defined more than once; this definition is ignored, only the first definition in admin/admin-cli.php (L305-338) is considered.

This check looks for functions that have already been defined in other files.

Some Codebases, like WordPress, make a practice of defining functions multiple times. This may lead to problems with the detection of function parameters and types. If you really need to do this, you can mark the duplicate definition with the @ignore annotation.

/**
 * @ignore
 */
function getUser() {

}

function getUser($id, $realm) {

}

See also the PhpDoc documentation for @ignore.

Loading history...
1297
{
1298
	global $prog,$config,$verbose;
1299
1300
	echo "Usage: $prog [-h|--help] [-v|--verbose] [options, ...]\n\n";
1301
	echo "options and their defaults:\n";
1302
	if ($verbose)
1303
	{
1304
		if (!isset($config['modules'])) $config['modules'] = get_modules_per_repo();
1305
	}
1306
	else
1307
	{
1308
		unset($config['modules']);	// they give an error, because of nested array and are quite lengthy
1309
	}
1310
	foreach($config as $name => $default)
1311
	{
1312
		if (is_array($default)) $default = json_encode ($default, JSON_UNESCAPED_SLASHES);
1313
		echo '--'.str_pad($name,20).$default."\n";
1314
	}
1315
	if ($error)
1 ignored issue
show
Bug Best Practice introduced by
The expression $error of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1316
	{
1317
		echo "$error\n\n";
1318
		exit(90);
1319
	}
1320
	exit(0);
1321
}
1322