From 45c9d7be37851cb7124b3bec14b0242c4c9f2d26 Mon Sep 17 00:00:00 2001 From: soleuu Date: Mon, 10 May 2021 15:16:08 +0200 Subject: [PATCH 1/9] multi SCM support (adding SVN) --- composer-lock-diff | 75 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 58 insertions(+), 17 deletions(-) diff --git a/composer-lock-diff b/composer-lock-diff index 1624db9..665aed9 100755 --- a/composer-lock-diff +++ b/composer-lock-diff @@ -6,11 +6,11 @@ $opts = parseOpts(); $changes = array(); if (! $opts['only-dev']) { - $changes['changes'] = diff('packages', $opts['from'], $opts['to'], $opts['path']); + $changes['changes'] = diff('packages', $opts['from'], $opts['to'], $opts['path'], $opts['scm-type']); } if (! $opts['only-prod']) { - $changes['changes-dev'] = diff('packages-dev', $opts['from'], $opts['to'], $opts['path']); + $changes['changes-dev'] = diff('packages-dev', $opts['from'], $opts['to'], $opts['path'], $opts['scm-type']); } if ($opts['json']) { @@ -40,17 +40,17 @@ foreach($changes as $k => $diff) { print tableize($table_titles[$k], $diff, $table_opts); } -function diff($key, $from, $to, $base_path) { +function diff($key, $from, $to, $base_path, $scm) { $pkgs = array(); - $data = load($from, $base_path); + $data = load($from, $base_path, $scm); foreach($data->$key as $pkg) { $pkgs[$pkg->name] = array(version($pkg), 'REMOVED', ''); } - $data = load($to, $base_path); + $data = load($to, $base_path, $scm); foreach($data->$key as $pkg) { if (! array_key_exists($pkg->name, $pkgs)) { @@ -150,7 +150,7 @@ function urlFormatterMd($url, $text) { return sprintf('[%s](%s)', $text, $url); } -function load($fileish, $base_path = '') { +function load($fileish, $base_path = '', $scm) { $orig = $fileish; if (empty($base_path)) { @@ -176,20 +176,38 @@ function load($fileish, $base_path = '') { return mustDecodeJson(file_get_contents($fileish), $fileish); } - if (strpos($orig, ':') === false) { - $fileish .= ':' . $base_path . 'composer.lock'; - } + //try to find scm revision + if (strtolower($scm) == 'git'){ + if (strpos($orig, ':') === false) { + $fileish .= ':' . $base_path . 'composer.lock'; + } - $lines = array(); + $lines = array(); + exec('git show '. escapeshellarg($fileish), $lines, $exit); - exec('git show '. escapeshellarg($fileish), $lines, $exit); + if ($exit !== 0) { + error_log("Error: cannot open $orig or find it in git as $fileish"); + exit(1); + } - if ($exit !== 0) { - error_log("Error: cannot open $orig or find it in git as $fileish"); - exit(1); + return mustDecodeJson(implode("\n", $lines), $fileish); + } + + elseif (strtolower($scm) == 'svn'){ + if (strpos($orig, '@') === false) { + $fileish = $base_path . 'composer.lock@'.$fileish; + } + exec('svn cat '. escapeshellarg($fileish), $lines, $exit); + + if ($exit !== 0) { + error_log("Error: cannot open $orig or find it in svn as $fileish"); + exit(1); + } + return mustDecodeJson(implode("\n", $lines), $fileish); } - return mustDecodeJson(implode("\n", $lines), $fileish); + error_log("Error: unhandled SCM $scm"); + exit(1); } function isUrl($string) { @@ -279,7 +297,7 @@ function formatCompareDrupal($url, $from, $to) { } 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', 'scm:')); foreach(array('help' => 'h', 'path' => 'p') as $long => $short) { if (array_key_exists($short, $given)) { @@ -292,8 +310,10 @@ function parseOpts() { usage(); } + $path = array_key_exists('path', $given) ? $given['path'] : ''; + return array( - 'path' => array_key_exists('path', $given) ? $given['path'] : '', + 'path' => $path, 'from' => array_key_exists('from', $given) ? $given['from'] : 'HEAD', 'to' => array_key_exists('to', $given) ? $given['to'] : '', 'md' => array_key_exists('md', $given), @@ -302,9 +322,30 @@ function parseOpts() { 'no-links' => array_key_exists('no-links', $given), 'only-prod' => array_key_exists('only-prod', $given), 'only-dev' => array_key_exists('only-dev', $given), + 'scm-type' => array_key_exists('scm', $given) ? $given['scm'] : scmDetect($path), ); } +function scmDetect($base_path){ + if (empty($base_path)) { + $base_path = '.' . DIRECTORY_SEPARATOR; + } else { + $base_path = rtrim($base_path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; + } + + $base_path = realpath($base_path); + while(!empty($base_path) && $base_path !== '/' ){ + if(is_dir($base_path.'/.svn')){ + return 'svn'; + }elseif(is_dir($base_path.'/.git')){ + return 'git'; + } + $base_path = dirname($base_path); + } + + return 'unknown'; +} + function usage() { print << Date: Wed, 12 May 2021 09:19:55 +0200 Subject: [PATCH 2/9] SCM renamed to VCS --- README.md | 2 ++ composer-lock-diff | 27 ++++++++++++++------------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 9c8458c..a119134 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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 diff --git a/composer-lock-diff b/composer-lock-diff index 665aed9..73bb2aa 100755 --- a/composer-lock-diff +++ b/composer-lock-diff @@ -6,11 +6,11 @@ $opts = parseOpts(); $changes = array(); if (! $opts['only-dev']) { - $changes['changes'] = diff('packages', $opts['from'], $opts['to'], $opts['path'], $opts['scm-type']); + $changes['changes'] = diff('packages', $opts['from'], $opts['to'], $opts['path'], $opts['vcs']); } if (! $opts['only-prod']) { - $changes['changes-dev'] = diff('packages-dev', $opts['from'], $opts['to'], $opts['path'], $opts['scm-type']); + $changes['changes-dev'] = diff('packages-dev', $opts['from'], $opts['to'], $opts['path'], $opts['vcs']); } if ($opts['json']) { @@ -40,17 +40,17 @@ foreach($changes as $k => $diff) { print tableize($table_titles[$k], $diff, $table_opts); } -function diff($key, $from, $to, $base_path, $scm) { +function diff($key, $from, $to, $base_path, $vcs) { $pkgs = array(); - $data = load($from, $base_path, $scm); + $data = load($from, $base_path, $vcs); foreach($data->$key as $pkg) { $pkgs[$pkg->name] = array(version($pkg), 'REMOVED', ''); } - $data = load($to, $base_path, $scm); + $data = load($to, $base_path, $vcs); foreach($data->$key as $pkg) { if (! array_key_exists($pkg->name, $pkgs)) { @@ -150,7 +150,7 @@ function urlFormatterMd($url, $text) { return sprintf('[%s](%s)', $text, $url); } -function load($fileish, $base_path = '', $scm) { +function load($fileish, $base_path = '', $vcs) { $orig = $fileish; if (empty($base_path)) { @@ -176,8 +176,8 @@ function load($fileish, $base_path = '', $scm) { return mustDecodeJson(file_get_contents($fileish), $fileish); } - //try to find scm revision - if (strtolower($scm) == 'git'){ + //try to find vcs revision + if (strtolower($vcs) == 'git'){ if (strpos($orig, ':') === false) { $fileish .= ':' . $base_path . 'composer.lock'; } @@ -193,7 +193,7 @@ function load($fileish, $base_path = '', $scm) { return mustDecodeJson(implode("\n", $lines), $fileish); } - elseif (strtolower($scm) == 'svn'){ + elseif (strtolower($vcs) == 'svn'){ if (strpos($orig, '@') === false) { $fileish = $base_path . 'composer.lock@'.$fileish; } @@ -206,7 +206,7 @@ function load($fileish, $base_path = '', $scm) { return mustDecodeJson(implode("\n", $lines), $fileish); } - error_log("Error: unhandled SCM $scm"); + error_log("Error: unhandled VCS $vcs"); exit(1); } @@ -297,7 +297,7 @@ function formatCompareDrupal($url, $from, $to) { } function parseOpts() { - $given = getopt('hp:', array('path:', 'from:', 'to:', 'md', 'json', 'pretty', 'no-links', 'only-prod', 'only-dev', 'help', 'scm:')); + $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)) { @@ -322,11 +322,11 @@ function parseOpts() { 'no-links' => array_key_exists('no-links', $given), 'only-prod' => array_key_exists('only-prod', $given), 'only-dev' => array_key_exists('only-dev', $given), - 'scm-type' => array_key_exists('scm', $given) ? $given['scm'] : scmDetect($path), + 'vcs' => array_key_exists('vcs', $given) ? $given['vcs'] : vcsDetect($path), ); } -function scmDetect($base_path){ +function vcsDetect($base_path){ if (empty($base_path)) { $base_path = '.' . DIRECTORY_SEPARATOR; } else { @@ -362,6 +362,7 @@ Options: --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 EOF; From 1e12f5d492a4b9fa21c66861f1f61de5a1b7e08b Mon Sep 17 00:00:00 2001 From: soleuu Date: Mon, 24 May 2021 20:12:32 +0200 Subject: [PATCH 3/9] svn - changing default --from value + fix 'Unable to find the wrapper "svn"' --- composer-lock-diff | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/composer-lock-diff b/composer-lock-diff index 73bb2aa..d896a62 100755 --- a/composer-lock-diff +++ b/composer-lock-diff @@ -163,7 +163,7 @@ function load($fileish, $base_path = '', $vcs) { $fileish = $base_path . 'composer.lock'; } - if (isUrl($fileish)) { + if (strpos('svn://', $fileish) !== 0 && 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); @@ -172,12 +172,12 @@ function load($fileish, $base_path = '', $vcs) { return mustDecodeJson(file_get_contents($fileish), $fileish); } - if (file_exists($fileish)) { + if (strpos('svn://', $fileish) !== 0 && file_exists($fileish)) { return mustDecodeJson(file_get_contents($fileish), $fileish); } //try to find vcs revision - if (strtolower($vcs) == 'git'){ + if ($vcs == 'git'){ if (strpos($orig, ':') === false) { $fileish .= ':' . $base_path . 'composer.lock'; } @@ -193,8 +193,12 @@ function load($fileish, $base_path = '', $vcs) { return mustDecodeJson(implode("\n", $lines), $fileish); } - elseif (strtolower($vcs) == 'svn'){ - if (strpos($orig, '@') === false) { + elseif ($vcs == 'svn'){ + # explaination: + # http:// https:// svn:// => absolute url of repository (http/https already handled before) + # ^ => relative url from current workspace repository + # @ => repository url with version + if (preg_match('#^\^|^svn://|@#', $orig) === 0) { $fileish = $base_path . 'composer.lock@'.$fileish; } exec('svn cat '. escapeshellarg($fileish), $lines, $exit); @@ -311,10 +315,12 @@ function parseOpts() { } $path = array_key_exists('path', $given) ? $given['path'] : ''; + $vcs = array_key_exists('vcs', $given) ? strtolower($given['vcs']) : vcsDetect($path); + $defaultFrom = $vcs == 'svn' ? "BASE" : "HEAD"; return array( 'path' => $path, - 'from' => array_key_exists('from', $given) ? $given['from'] : 'HEAD', + 'from' => array_key_exists('from', $given) ? $given['from'] : $defaultFrom, 'to' => array_key_exists('to', $given) ? $given['to'] : '', 'md' => array_key_exists('md', $given), 'json' => array_key_exists('json', $given), @@ -322,7 +328,7 @@ function parseOpts() { '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' => array_key_exists('vcs', $given) ? $given['vcs'] : vcsDetect($path), + 'vcs' => $vcs, ); } @@ -354,7 +360,8 @@ 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) @@ -368,4 +375,5 @@ EOF; exit(0); } +# vim: ff=unix ts=4 ss=4 sr et From 9eb5b9a473450cf22e3f61dd972e9e09359451dd Mon Sep 17 00:00:00 2001 From: David Jonas Date: Fri, 4 Jun 2021 00:01:54 -0700 Subject: [PATCH 4/9] Make vcsDetect() a little safer --- composer-lock-diff | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/composer-lock-diff b/composer-lock-diff index d896a62..73ee8b5 100755 --- a/composer-lock-diff +++ b/composer-lock-diff @@ -332,22 +332,37 @@ function parseOpts() { ); } -function vcsDetect($base_path){ +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 vcsDetect($base_path) { if (empty($base_path)) { - $base_path = '.' . DIRECTORY_SEPARATOR; - } else { - $base_path = rtrim($base_path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; + $base_path = '.'; } + // > Trailing delimiters, such as \ and /, are also removed + // > returns false on failure, e.g. if the file does not exist. $base_path = realpath($base_path); - while(!empty($base_path) && $base_path !== '/' ){ - if(is_dir($base_path.'/.svn')){ - return 'svn'; - }elseif(is_dir($base_path.'/.git')){ + if ($base_path === false) return 'unknown'; + + $tries = 10; + do { + if(is_dir(joinPath($base_path, '.git'))) { return 'git'; + } elseif(is_dir(joinPath($base_path, '.svn'))) { + return 'svn'; } - $base_path = dirname($base_path); - } + + $base_path = dirnameSafe($base_path); + } while($base_path !== false && --$tries > 0); return 'unknown'; } From b505628824b53ce2af588aa01d67da22f35b0345 Mon Sep 17 00:00:00 2001 From: David Jonas Date: Fri, 11 Jun 2021 18:43:09 -0700 Subject: [PATCH 5/9] Adds git and svn vcs* functions --- composer-lock-diff | 114 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/composer-lock-diff b/composer-lock-diff index 73ee8b5..13d81e4 100755 --- a/composer-lock-diff +++ b/composer-lock-diff @@ -300,6 +300,120 @@ 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('git --version', null, $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('svn --version', null, $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 parseOpts() { $given = getopt('hp:', array('path:', 'from:', 'to:', 'md', 'json', 'pretty', 'no-links', 'only-prod', 'only-dev', 'help', 'vcs:')); From 87b2d6bc6a06f0a434aa8380628895e2b056b6f1 Mon Sep 17 00:00:00 2001 From: David Jonas Date: Fri, 11 Jun 2021 18:44:09 -0700 Subject: [PATCH 6/9] Use new vcs* functions in load(), usage(), update cli args, pass loaded data to diff() --- composer-lock-diff | 172 +++++++++++++++++++-------------------------- 1 file changed, 71 insertions(+), 101 deletions(-) diff --git a/composer-lock-diff b/composer-lock-diff index 13d81e4..db1075e 100755 --- a/composer-lock-diff +++ b/composer-lock-diff @@ -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'], $opts['vcs']); + $changes['changes'] = diff('packages', $data_from, $data_to); } if (! $opts['only-prod']) { - $changes['changes-dev'] = diff('packages-dev', $opts['from'], $opts['to'], $opts['path'], $opts['vcs']); + $changes['changes-dev'] = diff('packages-dev', $data_from, $data_to); } if ($opts['json']) { @@ -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, $vcs) { +function diff($key, $data_from, $data_to) { $pkgs = array(); - $data = load($from, $base_path, $vcs); - - foreach($data->$key as $pkg) { + foreach($data_from->$key as $pkg) { $pkgs[$pkg->name] = array(version($pkg), 'REMOVED', ''); } - $data = load($to, $base_path, $vcs); - - 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; @@ -150,68 +148,72 @@ function urlFormatterMd($url, $text) { return sprintf('[%s](%s)', $text, $url); } -function load($fileish, $base_path = '', $vcs) { - $orig = $fileish; +// $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'); - if (empty($base_path)) { - $base_path = '.' . DIRECTORY_SEPARATOR; - } else { - $base_path = rtrim($base_path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; - } + $vcses = $force_vcs ? array($force_vcs) : getVcses(); + $errors = array(); - if (empty($fileish)) { - $fileish = $base_path . 'composer.lock'; + 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 (strpos('svn://', $fileish) !== 0 && 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); + 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"); } - - return mustDecodeJson(file_get_contents($fileish), $fileish); + exit(1); } - if (strpos('svn://', $fileish) !== 0 && file_exists($fileish)) { - return mustDecodeJson(file_get_contents($fileish), $fileish); + $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; + } + $errors[] = "Failed to find '$fileish' with '$loader'; $err"; } - //try to find vcs revision - if ($vcs == 'git'){ - if (strpos($orig, ':') === false) { - $fileish .= ':' . $base_path . 'composer.lock'; - } + foreach($errors as $e) { + error_log($e); + } - $lines = array(); - exec('git show '. escapeshellarg($fileish), $lines, $exit); + exit(1); +} - if ($exit !== 0) { - error_log("Error: cannot open $orig or find it in git as $fileish"); - exit(1); +function loadFile($fileish, $base_path, $default_fileish) { + if (empty($fileish)) { + $fileish = $default_fileish; + if (!empty($base_path)) { + $fileish = joinPath($base_path, $fileish); } - - return mustDecodeJson(implode("\n", $lines), $fileish); } - elseif ($vcs == 'svn'){ - # explaination: - # http:// https:// svn:// => absolute url of repository (http/https already handled before) - # ^ => relative url from current workspace repository - # @ => repository url with version - if (preg_match('#^\^|^svn://|@#', $orig) === 0) { - $fileish = $base_path . 'composer.lock@'.$fileish; - } - exec('svn cat '. 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 svn as $fileish"); - exit(1); - } - return mustDecodeJson(implode("\n", $lines), $fileish); + // Is it a file in the local filesystem? + if (file_exists($fileish)) { + return array(mustDecodeJson(file_get_contents($fileish), $fileish), false); } - error_log("Error: unhandled VCS $vcs"); - exit(1); + return array(false, "Candidate '$fileish' does not look loadable from the fs or php stream wrappers"); } function isUrl($string) { @@ -310,7 +312,7 @@ function getVcses() { function vcsDetectGit($_fileish) { // Is there a git executable? - exec('git --version', null, $exit); + 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? @@ -341,7 +343,7 @@ function vcsLoadGit($fileish, $base_path, $_default_fileish) { function vcsDetectSvn($fileish, $base_path, $default_fileish) { // Is there a git executable? - exec('svn --version', null, $exit); + 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) { @@ -428,13 +430,15 @@ function parseOpts() { usage(); } - $path = array_key_exists('path', $given) ? $given['path'] : ''; - $vcs = array_key_exists('vcs', $given) ? strtolower($given['vcs']) : vcsDetect($path); - $defaultFrom = $vcs == 'svn' ? "BASE" : "HEAD"; + $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' => $path, - 'from' => array_key_exists('from', $given) ? $given['from'] : $defaultFrom, + 'path' => array_key_exists('path', $given) ? $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), @@ -446,42 +450,8 @@ function parseOpts() { ); } -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 vcsDetect($base_path) { - if (empty($base_path)) { - $base_path = '.'; - } - - // > Trailing delimiters, such as \ and /, are also removed - // > returns false on failure, e.g. if the file does not exist. - $base_path = realpath($base_path); - if ($base_path === false) return 'unknown'; - - $tries = 10; - do { - if(is_dir(joinPath($base_path, '.git'))) { - return 'git'; - } elseif(is_dir(joinPath($base_path, '.svn'))) { - return 'svn'; - } - - $base_path = dirnameSafe($base_path); - } while($base_path !== false && --$tries > 0); - - return 'unknown'; -} - function usage() { + $vcses = implode(', ', getVcses()); print <<= 5.4.0) @@ -498,7 +468,7 @@ Options: --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 + --vcs Force vcs ($vcses). Default: attempt to auto-detect EOF; From 2017f16c4ca5e2266521d3287c43331bee881234 Mon Sep 17 00:00:00 2001 From: David Jonas Date: Thu, 26 Aug 2021 19:42:16 -0700 Subject: [PATCH 7/9] Adds a simple svn test script --- test-svn.sh | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100755 test-svn.sh diff --git a/test-svn.sh b/test-svn.sh new file mode 100755 index 0000000..4a88ae0 --- /dev/null +++ b/test-svn.sh @@ -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 + +function cleanup() { + cd "$DIR/test-data" + rm -rf proj proj-working svnrepo +} + +set -ex + +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 + From 69e05519f158ec2f3e2ebf8aea06a32aedb062d2 Mon Sep 17 00:00:00 2001 From: David Jonas Date: Thu, 26 Aug 2021 19:53:50 -0700 Subject: [PATCH 8/9] Improve error handling in test-svn.sh --- test-svn.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test-svn.sh b/test-svn.sh index 4a88ae0..63694fa 100755 --- a/test-svn.sh +++ b/test-svn.sh @@ -5,14 +5,14 @@ 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 +trap cleanup INT ERR function cleanup() { cd "$DIR/test-data" rm -rf proj proj-working svnrepo } -set -ex +set -eEx cd test-data @@ -32,7 +32,7 @@ cd proj-working/trunk ../../../composer-lock-diff cd .. -../../composer-lock-diff -p trunk/ +../../composer-lock-diff -p trunk cd .. rm -rf proj proj-working svnrepo From b04b5932e1ee269458359cc8033df6b386f5f894 Mon Sep 17 00:00:00 2001 From: David Jonas Date: Thu, 26 Aug 2021 19:54:59 -0700 Subject: [PATCH 9/9] Ensure trailing separator on --path --- composer-lock-diff | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/composer-lock-diff b/composer-lock-diff index db1075e..21dd9b1 100755 --- a/composer-lock-diff +++ b/composer-lock-diff @@ -416,6 +416,10 @@ function joinPath(/* path parts */) { }, 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', 'vcs:')); @@ -437,7 +441,7 @@ function parseOpts() { } return array( - 'path' => array_key_exists('path', $given) ? $given['path'] : '', + '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),