Skip to content

Commit de141c3

Browse files
authored
Merge pull request #31 from davidrjonas/multiScmSupport-pluggable
Multi scm support - pluggable
2 parents d16f1ce + b04b593 commit de141c3

File tree

3 files changed

+235
-41
lines changed

3 files changed

+235
-41
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ Or from vim, to insert the output into the commit message, type `:r!composer-loc
4747
- `--no-links`: Don't include Compare links in plain text or any links in markdown
4848
- `--only-prod`: Only include changes from `packages`
4949
- `--only-dev`: Only include changes from `packages-dev`
50+
- `--vcs`: Force vcs (git, svn, ...). Default auto-detect from path
5051

5152
^ File includes anything available as a [protocol stream wrapper](http://php.net/manual/en/wrappers.php) such as URLs.
5253

@@ -204,4 +205,5 @@ Thanks to everyone who has shared ideas and code! In particular,
204205
- https://github.com/ihor-sviziev
205206
- https://github.com/wiese
206207
- https://github.com/jibran
208+
- https://github.com/soleuu
207209

composer-lock-diff

Lines changed: 194 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44
$opts = parseOpts();
55

66
$changes = array();
7+
$data_from = load($opts['from'], $opts['path'], $opts['vcs'], '');
8+
$data_to = load($opts['to'], $opts['path'], $opts['vcs'], 'composer.lock');
79

810
if (! $opts['only-dev']) {
9-
$changes['changes'] = diff('packages', $opts['from'], $opts['to'], $opts['path']);
11+
$changes['changes'] = diff('packages', $data_from, $data_to);
1012
}
1113

1214
if (! $opts['only-prod']) {
13-
$changes['changes-dev'] = diff('packages-dev', $opts['from'], $opts['to'], $opts['path']);
15+
$changes['changes-dev'] = diff('packages-dev', $data_from, $data_to);
1416
}
1517

1618
if ($opts['json']) {
@@ -31,28 +33,24 @@ if ($opts['md']) {
3133
));
3234
}
3335

34-
$table_titles = [
36+
$table_titles = array(
3537
'changes' => 'Production Changes',
3638
'changes-dev' => 'Dev Changes',
37-
];
39+
);
3840

3941
foreach($changes as $k => $diff) {
4042
print tableize($table_titles[$k], $diff, $table_opts);
4143
}
4244

43-
function diff($key, $from, $to, $base_path) {
45+
function diff($key, $data_from, $data_to) {
4446

4547
$pkgs = array();
4648

47-
$data = load($from, $base_path);
48-
49-
foreach($data->$key as $pkg) {
49+
foreach($data_from->$key as $pkg) {
5050
$pkgs[$pkg->name] = array(version($pkg), 'REMOVED', '');
5151
}
5252

53-
$data = load($to, $base_path);
54-
55-
foreach($data->$key as $pkg) {
53+
foreach($data_to->$key as $pkg) {
5654
if (! array_key_exists($pkg->name, $pkgs)) {
5755
$pkgs[$pkg->name] = array('NEW', version($pkg), '');
5856
continue;
@@ -150,46 +148,72 @@ function urlFormatterMd($url, $text) {
150148
return sprintf('[%s](%s)', $text, $url);
151149
}
152150

153-
function load($fileish, $base_path = '') {
154-
$orig = $fileish;
155-
156-
if (empty($base_path)) {
157-
$base_path = '.' . DIRECTORY_SEPARATOR;
158-
} else {
159-
$base_path = rtrim($base_path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
151+
// $fileish is what the user actually requested.
152+
// $default_fileish is what it should be if $fileish is empty
153+
function load($fileish, $base_path, $force_vcs, $default_fileish) {
154+
$loaders = ($force_vcs || (empty($fileish) && empty($default_fileish))) ? array() : array('loadFile');
155+
156+
$vcses = $force_vcs ? array($force_vcs) : getVcses();
157+
$errors = array();
158+
159+
foreach($vcses as $vcs) {
160+
$detector = 'vcsDetect' . ucfirst($vcs);
161+
if($vcs != $force_vcs && function_exists($detector)) {
162+
list($available, $err) = call_user_func($detector, $fileish, $base_path, $default_fileish);
163+
if ($err) {
164+
$errors[] = $err;
165+
continue;
166+
}
167+
if (!$available) continue;
168+
}
169+
$loaders[] = 'vcsLoad' . ucfirst($vcs);
160170
}
161171

162-
if (empty($fileish)) {
163-
$fileish = $base_path . 'composer.lock';
172+
if (empty($loaders)) {
173+
error_log(implode("\n", $errors));
174+
if ($force_vcs) {
175+
error_log("Requested vcs '$force_vcs' not installed or otherwise unavailable");
176+
} else {
177+
error_log("No loaders were found; perhaps your vcs cli tools are not installed, not in PATH, or otherwise unavailable");
178+
}
179+
exit(1);
164180
}
165181

166-
if (isUrl($fileish)) {
167-
if (! in_array(parse_url($fileish, PHP_URL_SCHEME), stream_get_wrappers())) {
168-
error_log("Error: no stream wrapper to open '$fileish'");
169-
exit(1);
182+
$errors = array();
183+
foreach($loaders as $loader) {
184+
list($result, $err) = call_user_func_array($loader, array($fileish, $base_path, $default_fileish));
185+
if (empty($err)) {
186+
return $result;
170187
}
171-
172-
return mustDecodeJson(file_get_contents($fileish), $fileish);
188+
$errors[] = "Failed to find '$fileish' with '$loader'; $err";
173189
}
174190

175-
if (file_exists($fileish)) {
176-
return mustDecodeJson(file_get_contents($fileish), $fileish);
191+
foreach($errors as $e) {
192+
error_log($e);
177193
}
178194

179-
if (strpos($orig, ':') === false) {
180-
$fileish .= ':' . $base_path . 'composer.lock';
181-
}
195+
exit(1);
196+
}
182197

183-
$lines = array();
198+
function loadFile($fileish, $base_path, $default_fileish) {
199+
if (empty($fileish)) {
200+
$fileish = $default_fileish;
201+
if (!empty($base_path)) {
202+
$fileish = joinPath($base_path, $fileish);
203+
}
204+
}
184205

185-
exec('git show '. escapeshellarg($fileish), $lines, $exit);
206+
// Does it look like a url that we can handle with stream wrappers?
207+
if (isUrl($fileish) && in_array(parse_url($fileish, PHP_URL_SCHEME), stream_get_wrappers())) {
208+
return array(mustDecodeJson(file_get_contents($fileish), $fileish), false);
209+
}
186210

187-
if ($exit !== 0) {
188-
error_log("Error: cannot open $orig or find it in git as $fileish");
189-
exit(1);
211+
// Is it a file in the local filesystem?
212+
if (file_exists($fileish)) {
213+
return array(mustDecodeJson(file_get_contents($fileish), $fileish), false);
190214
}
191215

192-
return mustDecodeJson(implode("\n", $lines), $fileish);
216+
return array(false, "Candidate '$fileish' does not look loadable from the fs or php stream wrappers");
193217
}
194218

195219
function isUrl($string) {
@@ -278,8 +302,126 @@ function formatCompareDrupal($url, $from, $to) {
278302
return sprintf('%s/compare/8.x-%s...8.x-%s', $url, substr(urlencode($from), 0, -2), substr(urlencode($to), 0, -2));
279303
}
280304

305+
//
306+
// ## VCSes ####################
307+
//
308+
309+
function getVcses() {
310+
return array('git', 'svn');
311+
}
312+
313+
function vcsDetectGit($_fileish) {
314+
// Is there a git executable?
315+
exec('sh -c "git --version" > /dev/null 2>&1', $_out, $exit);
316+
if ($exit !== 0) return array(false, "'git --version' exited with non-zero code '$exit'");
317+
318+
// Does this look like a git repo?
319+
$path = findUp('.', '.git');
320+
return array(!! $path, ($path) ? false : "Could not find .git in current directory or parents");
321+
}
322+
323+
function vcsLoadGit($fileish, $base_path, $_default_fileish) {
324+
// We don't care about $default_fileish here - we are expected to load from
325+
// git and we must make a filename to do that.
326+
if (empty($fileish)) {
327+
$fileish = 'HEAD';
328+
}
329+
330+
if (strpos($fileish, ':') === false) {
331+
$fileish .= ':' . $base_path . 'composer.lock';
332+
}
333+
334+
$lines = array();
335+
exec('git show ' . escapeshellarg($fileish), $lines, $exit);
336+
337+
if ($exit !== 0) {
338+
return array('', "'git show $fileish' exited with non-zero code '$exit'");
339+
}
340+
341+
return array(mustDecodeJson(implode("\n", $lines), $fileish), false);
342+
}
343+
344+
function vcsDetectSvn($fileish, $base_path, $default_fileish) {
345+
// Is there a git executable?
346+
exec('sh -c "svn --version" > /dev/null 2>&1', $_out, $exit);
347+
if ($exit !== 0) return array(false, "'svn --version' exited with non-zero code '$exit'");
348+
349+
if (strpos('svn://', $fileish) === 0) {
350+
return array(true, false);
351+
}
352+
353+
// Does this look like a svn repo?
354+
$path = findUp('.', '.svn');
355+
return array(!! $path, ($path) ? false : "Could not find .svn in current directory or parents");
356+
}
357+
358+
function vcsLoadSvn($fileish, $base_path, $_default_fileish) {
359+
// We don't care about $default_fileish here - we are expected to load from
360+
// svn and we must make a filename to do that.
361+
if (empty($fileish)) {
362+
$fileish = 'BASE';
363+
}
364+
365+
// If $fileish starts with a url scheme that 'svn cat' can handle or ^, or
366+
// if it contains a @, assume it is already a proper svn identifier.
367+
// - file:// http:// https:// svn:// svn+ssh:// => absolute url of
368+
// repository (file/http/https may have been handled with stream wrappers
369+
// if '--vcs svn' wasn't specified)
370+
// - ^ => relative url from current workspace repository
371+
// - @ => repository url with revision
372+
if (preg_match('#^\^|^(file|http|https|svn|svn\+ssh)://|@#i', $fileish) === 0) {
373+
$fileish = $base_path . 'composer.lock@'.$fileish;
374+
}
375+
376+
exec('svn cat ' . escapeshellarg($fileish), $lines, $exit);
377+
378+
if ($exit !== 0) {
379+
return array('', "'svn cat $fileish' exited with non-zero code '$exit'");
380+
}
381+
382+
return array(mustDecodeJson(implode("\n", $lines), $fileish), false);
383+
}
384+
385+
function findUp($path, $filename, $tries = 10) {
386+
if (empty($path)) {
387+
$path = '.';
388+
}
389+
390+
// > Trailing delimiters, such as \ and /, are also removed
391+
// > returns false on failure, e.g. if the file does not exist.
392+
$path = realpath($path);
393+
if ($path === false) return false;
394+
395+
do {
396+
$candidate = joinPath($path, $filename);
397+
398+
if (file_exists($candidate)) {
399+
return $candidate;
400+
}
401+
402+
$path = dirnameSafe($path);
403+
} while ($path !== false && --$tries > 0);
404+
405+
return false;
406+
}
407+
408+
function dirnameSafe($path) {
409+
$parent = dirname($path);
410+
return ($parent != $path && !empty($parent)) ? $parent : false;
411+
}
412+
413+
function joinPath(/* path parts */) {
414+
return implode(DIRECTORY_SEPARATOR, array_map(function($part) {
415+
return trim($part, DIRECTORY_SEPARATOR);
416+
}, func_get_args()));
417+
}
418+
419+
function ensureTrailingPathSep($path) {
420+
return trim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
421+
}
422+
281423
function parseOpts() {
282-
$given = getopt('hp:', array('path:', 'from:', 'to:', 'md', 'json', 'pretty', 'no-links', 'only-prod', 'only-dev', 'help'));
424+
$given = getopt('hp:', array('path:', 'from:', 'to:', 'md', 'json', 'pretty', 'no-links', 'only-prod', 'only-dev', 'help', 'vcs:'));
283425

284426
foreach(array('help' => 'h', 'path' => 'p') as $long => $short) {
285427
if (array_key_exists($short, $given)) {
@@ -292,38 +434,49 @@ function parseOpts() {
292434
usage();
293435
}
294436

437+
$vcs = array_key_exists('vcs', $given) ? $given['vcs'] : '';
438+
if ($vcs && !function_exists('vcsLoad' . ucfirst($vcs))) {
439+
error_log("Unsupported vcs '$vcs'\n");
440+
usage();
441+
}
442+
295443
return array(
296-
'path' => array_key_exists('path', $given) ? $given['path'] : '',
297-
'from' => array_key_exists('from', $given) ? $given['from'] : 'HEAD',
444+
'path' => array_key_exists('path', $given) ? ensureTrailingPathSep($given['path']) : '',
445+
'from' => array_key_exists('from', $given) ? $given['from'] : '',
298446
'to' => array_key_exists('to', $given) ? $given['to'] : '',
299447
'md' => array_key_exists('md', $given),
300448
'json' => array_key_exists('json', $given),
301449
'pretty' => version_compare(PHP_VERSION, '5.4.0', '>=') && array_key_exists('pretty', $given),
302450
'no-links' => array_key_exists('no-links', $given),
303451
'only-prod' => array_key_exists('only-prod', $given),
304452
'only-dev' => array_key_exists('only-dev', $given),
453+
'vcs' => $vcs,
305454
);
306455
}
307456

308457
function usage() {
458+
$vcses = implode(', ', getVcses());
309459
print <<<EOF
310460
Usage: composer-lock-diff [options]
311461
312462
Options:
313463
-h --help Print this message
314464
--path, -p Base to with which to prefix paths. Default "./"
315465
E.g. `-p app` would look for HEAD:app/composer.lock and app/composer.lock
316-
--from The file, git ref, or git ref with filename to compare from (HEAD:composer.lock)
466+
--from The file, git ref, or git ref with filename to compare from
467+
(git: HEAD:composer.lock, svn: composer.lock@BASE)
317468
--to The file, git ref, or git ref with filename to compare to (composer.lock)
318469
--json Format output as JSON
319470
--pretty Pretty print JSON output (PHP >= 5.4.0)
320471
--md Use markdown instead of plain text
321472
--no-links Don't include Compare links in plain text or any links in markdown
322473
--only-prod Only include changes from `packages`
323474
--only-dev Only include changes from `packages-dev`
475+
--vcs Force vcs ($vcses). Default: attempt to auto-detect
324476
325477
EOF;
326478

327479
exit(0);
328480
}
481+
# vim: ff=unix ts=4 ss=4 sr et
329482

test-svn.sh

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#!/bin/bash
2+
3+
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
4+
5+
svn --help > /dev/null || { echo "Fail: could not find 'svn' executable"; exit 1; }
6+
svnadmin --help > /dev/null || { echo "Fail: could not find 'svnadmin' executable"; exit 1; }
7+
8+
trap cleanup INT ERR
9+
10+
function cleanup() {
11+
cd "$DIR/test-data"
12+
rm -rf proj proj-working svnrepo
13+
}
14+
15+
set -eEx
16+
17+
cd test-data
18+
19+
mkdir -p proj/trunk
20+
21+
cp composer.from.json proj/trunk/composer.json
22+
cp composer.from.lock proj/trunk/composer.lock
23+
24+
svnadmin create svnrepo
25+
svn import ./proj file://$PWD/svnrepo -m "Initial commit"
26+
27+
svn checkout file://$PWD/svnrepo proj-working
28+
cp composer.to.json proj-working/trunk/composer.json
29+
cp composer.to.lock proj-working/trunk/composer.lock
30+
31+
cd proj-working/trunk
32+
../../../composer-lock-diff
33+
34+
cd ..
35+
../../composer-lock-diff -p trunk
36+
37+
cd ..
38+
rm -rf proj proj-working svnrepo
39+

0 commit comments

Comments
 (0)