Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ Or from vim, to insert the output into the commit message, type `:r!composer-loc
- `--no-links`: Don't include Compare links in plain text or any links in markdown
- `--only-prod`: Only include changes from `packages`
- `--only-dev`: Only include changes from `packages-dev`
- `--vcs`: Force vcs (git, svn, ...). Default auto-detect from path

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

Expand Down Expand Up @@ -204,4 +205,5 @@ Thanks to everyone who has shared ideas and code! In particular,
- https://github.com/ihor-sviziev
- https://github.com/wiese
- https://github.com/jibran
- https://github.com/soleuu

235 changes: 194 additions & 41 deletions composer-lock-diff
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
$opts = parseOpts();

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

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

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

if ($opts['json']) {
Expand All @@ -31,28 +33,24 @@ if ($opts['md']) {
));
}

$table_titles = [
$table_titles = array(
'changes' => 'Production Changes',
'changes-dev' => 'Dev Changes',
];
);

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

function diff($key, $from, $to, $base_path) {
function diff($key, $data_from, $data_to) {

$pkgs = array();

$data = load($from, $base_path);

foreach($data->$key as $pkg) {
foreach($data_from->$key as $pkg) {
$pkgs[$pkg->name] = array(version($pkg), 'REMOVED', '');
}

$data = load($to, $base_path);

foreach($data->$key as $pkg) {
foreach($data_to->$key as $pkg) {
if (! array_key_exists($pkg->name, $pkgs)) {
$pkgs[$pkg->name] = array('NEW', version($pkg), '');
continue;
Expand Down Expand Up @@ -150,46 +148,72 @@ function urlFormatterMd($url, $text) {
return sprintf('[%s](%s)', $text, $url);
}

function load($fileish, $base_path = '') {
$orig = $fileish;

if (empty($base_path)) {
$base_path = '.' . DIRECTORY_SEPARATOR;
} else {
$base_path = rtrim($base_path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
// $fileish is what the user actually requested.
// $default_fileish is what it should be if $fileish is empty
function load($fileish, $base_path, $force_vcs, $default_fileish) {
$loaders = ($force_vcs || (empty($fileish) && empty($default_fileish))) ? array() : array('loadFile');

$vcses = $force_vcs ? array($force_vcs) : getVcses();
$errors = array();

foreach($vcses as $vcs) {
$detector = 'vcsDetect' . ucfirst($vcs);
if($vcs != $force_vcs && function_exists($detector)) {
list($available, $err) = call_user_func($detector, $fileish, $base_path, $default_fileish);
if ($err) {
$errors[] = $err;
continue;
}
if (!$available) continue;
}
$loaders[] = 'vcsLoad' . ucfirst($vcs);
}

if (empty($fileish)) {
$fileish = $base_path . 'composer.lock';
if (empty($loaders)) {
error_log(implode("\n", $errors));
if ($force_vcs) {
error_log("Requested vcs '$force_vcs' not installed or otherwise unavailable");
} else {
error_log("No loaders were found; perhaps your vcs cli tools are not installed, not in PATH, or otherwise unavailable");
}
exit(1);
}

if (isUrl($fileish)) {
if (! in_array(parse_url($fileish, PHP_URL_SCHEME), stream_get_wrappers())) {
error_log("Error: no stream wrapper to open '$fileish'");
exit(1);
$errors = array();
foreach($loaders as $loader) {
list($result, $err) = call_user_func_array($loader, array($fileish, $base_path, $default_fileish));
if (empty($err)) {
return $result;
}

return mustDecodeJson(file_get_contents($fileish), $fileish);
$errors[] = "Failed to find '$fileish' with '$loader'; $err";
}

if (file_exists($fileish)) {
return mustDecodeJson(file_get_contents($fileish), $fileish);
foreach($errors as $e) {
error_log($e);
}

if (strpos($orig, ':') === false) {
$fileish .= ':' . $base_path . 'composer.lock';
}
exit(1);
}

$lines = array();
function loadFile($fileish, $base_path, $default_fileish) {
if (empty($fileish)) {
$fileish = $default_fileish;
if (!empty($base_path)) {
$fileish = joinPath($base_path, $fileish);
}
}

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

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

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

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

//
// ## VCSes ####################
//

function getVcses() {
return array('git', 'svn');
}

function vcsDetectGit($_fileish) {
// Is there a git executable?
exec('sh -c "git --version" > /dev/null 2>&1', $_out, $exit);
if ($exit !== 0) return array(false, "'git --version' exited with non-zero code '$exit'");

// Does this look like a git repo?
$path = findUp('.', '.git');
return array(!! $path, ($path) ? false : "Could not find .git in current directory or parents");
}

function vcsLoadGit($fileish, $base_path, $_default_fileish) {
// We don't care about $default_fileish here - we are expected to load from
// git and we must make a filename to do that.
if (empty($fileish)) {
$fileish = 'HEAD';
}

if (strpos($fileish, ':') === false) {
$fileish .= ':' . $base_path . 'composer.lock';
}

$lines = array();
exec('git show ' . escapeshellarg($fileish), $lines, $exit);

if ($exit !== 0) {
return array('', "'git show $fileish' exited with non-zero code '$exit'");
}

return array(mustDecodeJson(implode("\n", $lines), $fileish), false);
}

function vcsDetectSvn($fileish, $base_path, $default_fileish) {
// Is there a git executable?
exec('sh -c "svn --version" > /dev/null 2>&1', $_out, $exit);
if ($exit !== 0) return array(false, "'svn --version' exited with non-zero code '$exit'");

if (strpos('svn://', $fileish) === 0) {
return array(true, false);
}

// Does this look like a svn repo?
$path = findUp('.', '.svn');
return array(!! $path, ($path) ? false : "Could not find .svn in current directory or parents");
}

function vcsLoadSvn($fileish, $base_path, $_default_fileish) {
// We don't care about $default_fileish here - we are expected to load from
// svn and we must make a filename to do that.
if (empty($fileish)) {
$fileish = 'BASE';
}

// If $fileish starts with a url scheme that 'svn cat' can handle or ^, or
// if it contains a @, assume it is already a proper svn identifier.
// - file:// http:// https:// svn:// svn+ssh:// => absolute url of
// repository (file/http/https may have been handled with stream wrappers
// if '--vcs svn' wasn't specified)
// - ^ => relative url from current workspace repository
// - @ => repository url with revision
if (preg_match('#^\^|^(file|http|https|svn|svn\+ssh)://|@#i', $fileish) === 0) {
$fileish = $base_path . 'composer.lock@'.$fileish;
}

exec('svn cat ' . escapeshellarg($fileish), $lines, $exit);

if ($exit !== 0) {
return array('', "'svn cat $fileish' exited with non-zero code '$exit'");
}

return array(mustDecodeJson(implode("\n", $lines), $fileish), false);
}

function findUp($path, $filename, $tries = 10) {
if (empty($path)) {
$path = '.';
}

// > Trailing delimiters, such as \ and /, are also removed
// > returns false on failure, e.g. if the file does not exist.
$path = realpath($path);
if ($path === false) return false;

do {
$candidate = joinPath($path, $filename);

if (file_exists($candidate)) {
return $candidate;
}

$path = dirnameSafe($path);
} while ($path !== false && --$tries > 0);

return false;
}

function dirnameSafe($path) {
$parent = dirname($path);
return ($parent != $path && !empty($parent)) ? $parent : false;
}

function joinPath(/* path parts */) {
return implode(DIRECTORY_SEPARATOR, array_map(function($part) {
return trim($part, DIRECTORY_SEPARATOR);
}, func_get_args()));
}

function ensureTrailingPathSep($path) {
return trim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
}

function parseOpts() {
$given = getopt('hp:', array('path:', 'from:', 'to:', 'md', 'json', 'pretty', 'no-links', 'only-prod', 'only-dev', 'help'));
$given = getopt('hp:', array('path:', 'from:', 'to:', 'md', 'json', 'pretty', 'no-links', 'only-prod', 'only-dev', 'help', 'vcs:'));

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

$vcs = array_key_exists('vcs', $given) ? $given['vcs'] : '';
if ($vcs && !function_exists('vcsLoad' . ucfirst($vcs))) {
error_log("Unsupported vcs '$vcs'\n");
usage();
}

return array(
'path' => array_key_exists('path', $given) ? $given['path'] : '',
'from' => array_key_exists('from', $given) ? $given['from'] : 'HEAD',
'path' => array_key_exists('path', $given) ? ensureTrailingPathSep($given['path']) : '',
'from' => array_key_exists('from', $given) ? $given['from'] : '',
'to' => array_key_exists('to', $given) ? $given['to'] : '',
'md' => array_key_exists('md', $given),
'json' => array_key_exists('json', $given),
'pretty' => version_compare(PHP_VERSION, '5.4.0', '>=') && array_key_exists('pretty', $given),
'no-links' => array_key_exists('no-links', $given),
'only-prod' => array_key_exists('only-prod', $given),
'only-dev' => array_key_exists('only-dev', $given),
'vcs' => $vcs,
);
}

function usage() {
$vcses = implode(', ', getVcses());
print <<<EOF
Usage: composer-lock-diff [options]

Options:
-h --help Print this message
--path, -p Base to with which to prefix paths. Default "./"
E.g. `-p app` would look for HEAD:app/composer.lock and app/composer.lock
--from The file, git ref, or git ref with filename to compare from (HEAD:composer.lock)
--from The file, git ref, or git ref with filename to compare from
(git: HEAD:composer.lock, svn: composer.lock@BASE)
--to The file, git ref, or git ref with filename to compare to (composer.lock)
--json Format output as JSON
--pretty Pretty print JSON output (PHP >= 5.4.0)
--md Use markdown instead of plain text
--no-links Don't include Compare links in plain text or any links in markdown
--only-prod Only include changes from `packages`
--only-dev Only include changes from `packages-dev`
--vcs Force vcs ($vcses). Default: attempt to auto-detect

EOF;

exit(0);
}
# vim: ff=unix ts=4 ss=4 sr et

39 changes: 39 additions & 0 deletions test-svn.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/bin/bash

DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"

svn --help > /dev/null || { echo "Fail: could not find 'svn' executable"; exit 1; }
svnadmin --help > /dev/null || { echo "Fail: could not find 'svnadmin' executable"; exit 1; }

trap cleanup INT ERR

function cleanup() {
cd "$DIR/test-data"
rm -rf proj proj-working svnrepo
}

set -eEx

cd test-data

mkdir -p proj/trunk

cp composer.from.json proj/trunk/composer.json
cp composer.from.lock proj/trunk/composer.lock

svnadmin create svnrepo
svn import ./proj file://$PWD/svnrepo -m "Initial commit"

svn checkout file://$PWD/svnrepo proj-working
cp composer.to.json proj-working/trunk/composer.json
cp composer.to.lock proj-working/trunk/composer.lock

cd proj-working/trunk
../../../composer-lock-diff

cd ..
../../composer-lock-diff -p trunk

cd ..
rm -rf proj proj-working svnrepo